xlsx-handlebars

Crates.ioxlsx-handlebars
lib.rsxlsx-handlebars
version0.2.2
created_at2025-10-01 17:33:00.800271+00
updated_at2025-11-05 10:01:36.494916+00
descriptionA Rust library for processing XLSX files with Handlebars templates, supporting WASM, Node.js, Deno, and browsers
homepagehttps://github.com/sail-sail/xlsx-handlebars
repositoryhttps://github.com/sail-sail/xlsx-handlebars
max_upload_size
id1863071
size197,190
黄智勇 (sail-sail)

documentation

https://docs.rs/xlsx-handlebars

README

xlsx-handlebars

Crates.io Documentation License

English | 中文文档 | Demo

A Rust library for processing XLSX files with Handlebars templates, supporting multiple platforms:

  • 🦀 Rust Native
  • 🌐 WebAssembly (WASM)
  • 📦 npm Package
  • 🟢 Node.js
  • 🦕 Deno
  • 🌍 Browser
  • 📋 JSR (JavaScript Registry)

Features

  • High Performance: Renders 100,000 rows in just 2.12 seconds (~47,000 rows/sec) - 14-28x faster than Python, 7-14x faster than JavaScript
  • Smart Merge: Automatically handles Handlebars syntax split by XML tags
  • XLSX Validation: Built-in file format validation to ensure valid input files
  • Handlebars Support: Full template engine with variables, conditions, loops, and Helper functions
  • Cross-Platform: Rust native + WASM support for multiple runtimes
  • TypeScript: Complete type definitions and IntelliSense
  • Zero Dependencies: WASM binary with no external dependencies

Installation

Rust

cargo add xlsx-handlebars

npm

npm install xlsx-handlebars

Deno

import init, { render_template } from "jsr:@sail/xlsx-handlebars";

Usage Examples

Rust

use xlsx_handlebars::render_template;
use serde_json::json;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Read XLSX template file
    let template_bytes = std::fs::read("template.xlsx")?;
    
    // Prepare data
    let data = json!({
        "name": "John Doe",
        "company": "ABC Tech Inc.",
        "position": "Software Engineer",
        "projects": [
            {"name": "Project A", "status": "Completed"},
            {"name": "Project B", "status": "In Progress"}
        ],
        "has_bonus": true,
        "bonus_amount": 5000
    });
    
    // Render template
    let result = render_template(template_bytes, &data)?;
    
    // Save result
    std::fs::write("output.xlsx", result)?;
    
    Ok(())
}

JavaScript/TypeScript (Node.js)

import init, { render_template } from 'xlsx-handlebars';
import fs from 'fs';

async function processTemplate() {
    // Initialize WASM module
    await init();
    
    // Read template file
    const templateBytes = fs.readFileSync('template.xlsx');
    
    // Prepare data
    const data = {
        name: "Jane Smith",
        company: "XYZ Technology Ltd.",
        position: "Senior Developer",
        projects: [
            { name: "E-commerce Platform", status: "Completed" },
            { name: "Mobile App", status: "In Development" }
        ],
        has_bonus: true,
        bonus_amount: 8000
    };
    
    // Render template
    const result = render_template(templateBytes, JSON.stringify(data));
    
    // Save result
    fs.writeFileSync('output.xlsx', new Uint8Array(result));
}

processTemplate().catch(console.error);

Deno

import init, { render_template } from "https://deno.land/x/xlsx_handlebars/mod.ts";

async function processTemplate() {
    // Initialize WASM module
    await init();
    
    // Read template file
    const templateBytes = await Deno.readFile("template.xlsx");
    
    // Prepare data
    const data = {
        name: "Alice Johnson",
        department: "R&D",
        projects: [
            { name: "AI Customer Service", status: "Live" },
            { name: "Data Visualization Platform", status: "In Development" }
        ]
    };
    
    // Render template
    const result = render_template(templateBytes, JSON.stringify(data));
    
    // Save result
    await Deno.writeFile("output.xlsx", new Uint8Array(result));
}

if (import.meta.main) {
    await processTemplate();
}

Browser

<!DOCTYPE html>
<html>
<head>
    <title>XLSX Handlebars Example</title>
</head>
<body>
    <input type="file" id="fileInput" accept=".xlsx">
    <button onclick="processFile()">Process Template</button>
    
    <script type="module">
        import init, { render_template } from './pkg/xlsx_handlebars.js';
        
        // Initialize WASM
        await init();
        
        window.processFile = async function() {
            const fileInput = document.getElementById('fileInput');
            const file = fileInput.files[0];
            
            if (!file) return;
            
            const arrayBuffer = await file.arrayBuffer();
            const templateBytes = new Uint8Array(arrayBuffer);
            
            const data = {
                name: "John Doe",
                company: "Example Company"
            };
            
            try {
                const result = render_template(templateBytes, JSON.stringify(data));
                
                // Download result
                const blob = new Blob([new Uint8Array(result)], {
                    type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
                });
                const url = URL.createObjectURL(blob);
                const a = document.createElement('a');
                a.href = url;
                a.download = 'processed.xlsx';
                a.click();
            } catch (error) {
                console.error('Processing failed:', error);
            }
        };
    </script>
</body>
</html>

Template Syntax

Basic Variable Substitution

Employee Name: {{name}}
Company: {{company}}
Position: {{position}}

Conditional Rendering

{{#if has_bonus}}
Bonus: ${{bonus_amount}}
{{else}}
No Bonus
{{/if}}

{{#unless is_intern}}
Full-time Employee
{{/unless}}

Loop Rendering

Project Experience:
{{#each projects}}
- {{name}}: {{description}} ({{status}})
{{/each}}

Skills:
{{#each skills}}
{{@index}}. {{this}}
{{/each}}

Helper Functions

Built-in Helper functions:

<!-- Basic helpers -->
{{upper name}}           <!-- Convert to uppercase -->
{{lower company}}        <!-- Convert to lowercase -->
{{len projects}}         <!-- Array length -->
{{#if (eq status "completed")}}Completed{{/if}}    <!-- Equality comparison -->
{{#if (gt score 90)}}Excellent{{/if}}              <!-- Greater than comparison -->
{{#if (lt age 30)}}Young{{/if}}                    <!-- Less than comparison -->

<!-- String concatenation -->
{{concat "Hello" " " "World"}}                     <!-- String concatenation -->
{{concat "Total: " count}}                         <!-- Mix strings and variables -->

<!-- Excel-specific helpers -->
{{num employee.salary}}                            <!-- Mark cell as number type -->
{{formula "=SUM(A1:B1)"}}                         <!-- Static Excel formula -->
{{formula (concat "=SUM(" (_c) "1:" (_c) "10)")}} <!-- Dynamic formula with current column -->
{{mergeCell "C4:D5"}}                             <!-- Merge cells C4 to D5 -->
{{img logo.data 100 100}}                          <!-- Insert image with width and height -->

<!-- Column name conversion helpers -->
{{toColumnName "A" 5}}                             <!-- A + 5 offset = F -->
{{toColumnName (_c) 3}}                            <!-- Current column + 3 offset -->
{{toColumnIndex "AA"}}                             <!-- AA column index = 27 -->

Excel Formula Helpers

Static Formula:

<!-- In Excel cell -->
{{formula "=SUM(A1:B1)"}}
{{formula "=AVERAGE(C2:C10)"}}
{{formula "=IF(D1>100,\"High\",\"Low\")"}}

Dynamic Formula with concat:

<!-- Dynamic row reference -->
{{formula (concat "=A" (_r) "*B" (_r))}}

<!-- Dynamic column reference -->
{{formula (concat "=SUM(" (_c) "2:" (_c) "10)")}}

<!-- Complex dynamic formula -->
{{formula (concat "=IF(" (_cr) ">100,\"High\",\"Low\")")}}

Available position helpers:

  • (_c) - Current column letter (A, B, C, ...)
  • (_r) - Current row number (1, 2, 3, ...)
  • (_cr) - Current cell reference (A1, B2, C3, ...)

Column Name Conversion Helpers

toColumnName - Convert column name or index to a new column name with optional offset:

<!-- Basic usage: offset from specified column -->
{{toColumnName "A" 0}}     <!-- A (no offset) -->
{{toColumnName "A" 5}}     <!-- F (A + 5) -->
{{toColumnName "Z" 1}}     <!-- AA (Z + 1) -->

<!-- Use with current column -->
{{toColumnName (_c) 3}}    <!-- Current column + 3 offset -->

<!-- Application in dynamic formulas -->
{{formula (concat "=SUM(" (_c) "1:" (toColumnName (_c) 3) "1)")}}
<!-- Example: If current column is B, generates formula =SUM(B1:E1) -->

toColumnIndex - Convert column name to column index (1-based):

{{toColumnIndex "A"}}      <!-- 1 -->
{{toColumnIndex "Z"}}      <!-- 26 -->
{{toColumnIndex "AA"}}     <!-- 27 -->
{{toColumnIndex "AB"}}     <!-- 28 -->

Merge Cells Helper

mergeCell - Mark cell ranges that need to be merged:

<!-- Static cell merging -->
{{mergeCell "C4:D5"}}      <!-- Merge C4 to D5 region -->
{{mergeCell "F4:G4"}}      <!-- Merge F4 to G4 region -->

<!-- Dynamic cell merging: from current position -->
{{mergeCell (concat (_c) (_r) ":" (toColumnName (_c) 3) (_r))}}
<!-- Example: If current is B5, merges B5:E5 (4 columns to the right) -->

<!-- Dynamic cell merging: spanning rows and columns -->
{{mergeCell (concat (_c) (_r) ":" (toColumnName (_c) 2) (add (_r) 2))}}
<!-- Example: If current is C3, merges C3:E5 (3×3 region) -->

<!-- Dynamic merging in loops -->
{{#each sections}}
  {{mergeCell (concat "A" (add @index 2) ":D" (add @index 2))}}
  <!-- Merge columns A-D for each section row -->
{{/each}}

Notes:

  • mergeCell produces no output, only collects merge information
  • Merge range format must be StartCell:EndCell (e.g., "A1:B2")
  • Duplicate merge ranges are automatically deduplicated
  • Merge information is automatically added to the Excel file after rendering

Hyperlink Helper

hyperlink - Add hyperlinks to Excel cells:

<!-- Basic usage: link to another worksheet -->
{{hyperlink (_cr) "Sheet2!A1" "View Details"}}

<!-- Link to external URL (requires pre-configuration in template) -->
{{hyperlink (_cr) "https://example.com" "Visit Website"}}

<!-- Dynamic links -->
{{#each items}}
  {{hyperlink (_cr) (concat "Details!" name) name}}
{{/each}}

Parameters:

  • First parameter: Cell reference, typically use (_cr) for current cell
  • Second parameter: Link target (worksheet reference or URL)
  • Third parameter: Display text (optional)

Notes:

  • hyperlink produces no output, only collects hyperlink information
  • Hyperlinks are automatically added to the Excel file after rendering
  • Supports internal worksheet references (e.g., "Sheet2!A1")
  • External links require pre-configured relationships in the template Excel file

Number Type Helper

Use {{num value}} to ensure a cell is treated as a number in Excel:

<!-- Without num: treated as text -->
{{employee.salary}}

<!-- With num: treated as number -->
{{num employee.salary}}

This is especially useful when:

  • The value might be a string but should be treated as a number
  • You want to ensure proper number formatting in Excel
  • You need the value to work in formulas

Image Insertion Helper

img - Insert base64-encoded images into Excel:

<!-- Basic usage: insert image with original dimensions -->
{{img logo.data}}

<!-- Specify width and height (in pixels) -->
{{img photo.data 150 200}}

<!-- Use dimensions from data -->
{{img image.data image.width image.height}}

Features:

  • ✅ Supports common image formats: PNG, JPEG, WebP, BMP, TIFF, GIF
  • ✅ Auto-detects actual image dimensions
  • ✅ Optional width and height specification (in pixels)
  • ✅ Image positioned at current cell location
  • ✅ Images are not constrained by cell size, maintain aspect ratio
  • ✅ Supports multiple images in the same sheet
  • ✅ Supports images in multiple sheets
  • ✅ Uses UUID to avoid ID conflicts

Complete Example:

// Prepare image data in JavaScript
import fs from 'fs';

const imageBuffer = fs.readFileSync('logo.png');
const base64Image = imageBuffer.toString('base64');

const data = {
  company: {
    logo: base64Image,
    name: "Tech Company"
  },
  products: [
    {
      name: "Product A",
      photo: base64Image,
      width: 120,
      height: 120
    },
    {
      name: "Product B", 
      photo: base64Image,
      width: 100,
      height: 100
    }
  ]
};

// Use in template
<!-- Excel template example -->
Company Logo: {{img company.logo 100 50}}

Product List:
{{#each products}}
Product Name: {{name}}
Image: {{img photo width height}}
{{/each}}

Usage Tips:

  • If only width is specified, height scales proportionally
  • If only height is specified, width scales proportionally
  • If neither is specified, original image dimensions are used
  • Image will be placed at the cell location where {{img}} is called
  • base64 data should not include the data:image/png;base64, prefix, just the pure base64 string

Worksheet Management Helpers

deleteCurrentSheet - Delete the current worksheet being rendered:

<!-- Basic usage -->
{{deleteCurrentSheet}}

<!-- Conditional deletion -->
{{#if shouldDelete}}
  {{deleteCurrentSheet}}
{{/if}}

<!-- Delete inactive sheets -->
{{#unless isActive}}
  {{deleteCurrentSheet}}
{{/unless}}

Features:

  • ✅ Removes worksheet and its relationships from workbook
  • ✅ Cleans up related files (rels, content types)
  • ✅ Drawing files are preserved (safe approach)
  • ✅ Cannot delete the last worksheet (Excel requirement)
  • ✅ Delayed execution after all rendering completes

setCurrentSheetName - Rename the current worksheet:

<!-- Static name -->
{{setCurrentSheetName "Sales Report"}}

<!-- Dynamic name -->
{{setCurrentSheetName (concat department.name " - " year)}}

<!-- Loop-based naming -->
{{#each departments}}
  {{setCurrentSheetName (concat "Department " @index " - " name)}}
{{/each}}

Features:

  • ✅ Auto-filters invalid characters: \ / ? * [ ]
  • ✅ Auto-limits length to 31 characters
  • ✅ Auto-handles duplicate names with numeric suffixes
  • ✅ Supports dynamic name generation

hideCurrentSheet - Hide the current worksheet:

<!-- Normal hide (user can unhide via right-click) -->
{{hideCurrentSheet}}
{{hideCurrentSheet "hidden"}}

<!-- Very hidden (requires VBA to unhide) -->
{{hideCurrentSheet "veryHidden"}}

<!-- Conditional hiding -->
{{#unless (eq userRole "admin")}}
  {{hideCurrentSheet "veryHidden"}}
{{/unless}}

Hide Levels:

  • hidden - Normal hide, users can unhide via Excel's right-click menu
  • veryHidden - Super hide, requires VBA or property editor to unhide

Features:

  • ✅ Cannot hide all worksheets (Excel requires at least one visible)
  • ✅ Two hiding levels: normal and super hidden
  • ✅ Useful for permission control and sensitive data

Common Use Cases:

<!-- Multi-language reports: delete unused language sheets -->
{{#if (ne language "en-US")}}
  {{deleteCurrentSheet}}
{{/if}}

<!-- Dynamic department reports: rename sheets by department -->
{{setCurrentSheetName (concat department.name " Report")}}

<!-- Permission control: hide admin sheets from regular users -->
{{#unless (eq userRole "admin")}}
  {{hideCurrentSheet "veryHidden"}}
{{/unless}}

<!-- Conditional workflow: delete, rename, or hide based on status -->
{{#if (eq status "inactive")}}
  {{deleteCurrentSheet}}
{{else}}
  {{setCurrentSheetName (concat "Active - " name)}}
  {{#if isInternal}}
    {{hideCurrentSheet}}
  {{/if}}
{{/if}}

Complex Example

=== Employee Report ===

Basic Information:
Name: {{employee.name}}
Department: {{employee.department}}
Position: {{employee.position}}
Hire Date: {{employee.hire_date}}

{{#if employee.has_bonus}}
💰 Bonus: ${{employee.bonus_amount}}
{{/if}}

Project Experience (Total {{len projects}}):
{{#each projects}}
{{@index}}. {{name}}
   Description: {{description}}
   Status: {{status}}
   Team Size: {{team_size}} people
   
{{/each}}

Skills Assessment:
{{#each skills}}
- {{name}}: {{level}}/10 ({{years}} years of experience)
{{/each}}

To remove an entire row in a table, simply add to any cell:
{{removeRow}}


{{#if (gt performance.score 90)}}
🎉 Performance Rating: Excellent
{{else if (gt performance.score 80)}}
👍 Performance Rating: Good
{{else}}
📈 Performance Rating: Needs Improvement
{{/if}}

Build and Development

Build WASM Package

# Build all targets
npm run build

# Or build separately
npm run build:web    # Browser version
npm run build:npm    # Node.js version 
npm run build:jsr    # Deno version

Run Examples

# Rust example
cargo run --example rust_example

# Node.js example
node examples/node_example.js

# Deno example  
deno run --allow-read --allow-write examples/deno_example.ts

# Browser example
cd tests/npm_test
node serve.js
# Then open http://localhost:8080 in your browser
# Select examples/template.xlsx file to test

Utility Functions

xlsx-handlebars provides a set of utility functions to help you work more efficiently with Excel operations.

Excel Column Name Conversion

Convert between Excel column names and column indices.

use xlsx_handlebars::{to_column_name, to_column_index};

// Column name increment
assert_eq!(to_column_name("A", 0), "A");
assert_eq!(to_column_name("A", 1), "B");
assert_eq!(to_column_name("Z", 1), "AA");
assert_eq!(to_column_name("AA", 1), "AB");

// Column name to index (1-based)
assert_eq!(to_column_index("A"), 1);
assert_eq!(to_column_index("Z"), 26);
assert_eq!(to_column_index("AA"), 27);
assert_eq!(to_column_index("BA"), 53);

JavaScript/TypeScript Example:

import { wasm_to_column_name, wasm_to_column_index } from 'xlsx-handlebars';

// Column name increment
console.log(wasm_to_column_name("A", 1));  // "B"
console.log(wasm_to_column_name("Z", 1));  // "AA"

// Column name to index
console.log(wasm_to_column_index("AA"));   // 27
console.log(wasm_to_column_index("BA"));   // 53

Excel Date Conversion

Convert between Unix timestamps and Excel date serial numbers. Excel uses serial numbers starting from 1900-01-01 to represent dates.

use xlsx_handlebars::{timestamp_to_excel_date, excel_date_to_timestamp};

// Timestamp to Excel date
let timestamp = 1704067200000i64;  // 2024-01-01 00:00:00 UTC
let excel_date = timestamp_to_excel_date(timestamp);
println!("Excel date serial number: {}", excel_date);  // 45294.0

// Excel date to timestamp
if let Some(ts) = excel_date_to_timestamp(45294.0) {
    println!("Timestamp: {}", ts);  // 1704067200000
}

JavaScript/TypeScript Example:

import { 
    wasm_timestamp_to_excel_date, 
    wasm_excel_date_to_timestamp 
} from 'xlsx-handlebars';

// Date to Excel serial number
const date = new Date('2024-01-01T00:00:00Z');
const excelDate = wasm_timestamp_to_excel_date(date.getTime());
console.log('Excel date:', excelDate);  // 45294.0

// Excel serial number to date
const timestamp = wasm_excel_date_to_timestamp(45294.0);
if (timestamp !== null) {
    const convertedDate = new Date(timestamp);
    console.log('Date:', convertedDate.toISOString());
}

Common Use Cases:

// Dynamically generate cell references
let start_col = "B";
let num_cols = 5;
for i in 0..num_cols {
    let col_name = to_column_name(start_col, i);
    let col_index = to_column_index(&col_name);
    println!("Column {}: name={}, index={}", i, col_name, col_index);
}
// Include dates in template data
use serde_json::json;

let date_timestamp = 1704067200000i64;  // 2024-01-01
let excel_date = timestamp_to_excel_date(date_timestamp);

let data = json!({
    "report_date": excel_date,
    "employee": {
        "name": "John Doe",
        "hire_date": timestamp_to_excel_date(1609459200000i64)  // 2021-01-01
    }
});

These utility functions help you:

  • ✅ Dynamically generate cell references and formulas
  • ✅ Handle Excel date formats
  • ✅ Avoid loading heavy external libraries
  • ✅ Support both Rust and JavaScript/TypeScript

Technical Features

Performance and Compatibility

Blazing Fast Performance ⚡

xlsx-handlebars delivers industry-leading performance powered by Rust:

Data Size Processing Time Throughput
1,000 rows ~0.02s Real-time generation
10,000 rows ~0.21s Online exports
100,000 rows ~2.12s Batch processing
1,000,000 rows ~21s Big data reports

Performance Comparison (100,000 rows):

Technology Time Speed vs xlsx-handlebars
xlsx-handlebars (Rust) 2.12s 1x (baseline)
Python (openpyxl) 30-60s 14-28x slower
JavaScript (xlsx.js) 15-30s 7-14x slower
Java (Apache POI) 8-15s 3-7x slower
C# (EPPlus) 5-10s 2-4x slower

Why So Fast?

  • 🦀 Rust's Zero-Cost Abstractions: Compile-time optimizations with no runtime overhead
  • 🔄 Streaming Architecture: Process ZIP entries directly in memory without file I/O
  • Event-Driven XML Parsing: Uses quick-xml for efficient parsing without building full DOM trees
  • 🎯 Single-Pass Rendering: All template substitutions in one iteration

Compatibility

  • Zero-Copy: Efficient memory management between Rust and WASM
  • Streaming: Suitable for processing large XLSX files
  • Cross-Platform: Supports Windows, macOS, Linux, Web
  • Modern Browsers: Supports all modern browsers with WASM support

License

This project is licensed under the MIT License - see the LICENSE-MIT file for details.

Support


xlsx-handlebars - Making XLSX template processing simple and efficient

⭐ Star the project · 🐛 Report Issues · 💬 Join Discussions

Support this project with a donation via Alipay:

Support this project with a donation

Commit count: 0

cargo fmt