xlsx-handlebars
v0.2.2
Published
A Rust library for processing XLSX files with Handlebars templates, supporting WASM, Node.js, Deno, and browsers
Maintainers
Readme
xlsx-handlebars
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-handlebarsnpm
npm install xlsx-handlebarsDeno
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:
mergeCellproduces 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:
hyperlinkproduces 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 menuveryHidden- 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 versionRun 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 testUtility 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")); // 53Excel 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
Support this project with a donation via Alipay:

