# xlsx-handlebars
[](https://crates.io/crates/xlsx-handlebars)
[](https://docs.rs/xlsx-handlebars)
[](https://github.com/sail-sail/xlsx-handlebars#license)
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
```bash
cargo add xlsx-handlebars
```
### npm
```bash
npm install xlsx-handlebars
```
### Deno
```typescript
import init, { render_template } from "jsr:@sail/xlsx-handlebars";
```
## Usage Examples
### Rust
```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)
```javascript
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
```typescript
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
```html
<!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
```handlebars
Employee Name: {{name}}
Company: {{company}}
Position: {{position}}
```
### Conditional Rendering
```handlebars
{{#if has_bonus}}
Bonus: ${{bonus_amount}}
{{else}}
No Bonus
{{/if}}
{{#unless is_intern}}
Full-time Employee
{{/unless}}
```
### Loop Rendering
```handlebars
Project Experience:
{{#each projects}}
- {{name}}: {{description}} ({{status}})
{{/each}}
Skills:
{{#each skills}}
{{@index}}. {{this}}
{{/each}}
```
### Helper Functions
Built-in Helper functions:
```handlebars
{{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 -->
{{concat "Hello" " " "World"}} <!-- String concatenation -->
{{concat "Total: " count}} <!-- Mix strings and variables -->
{{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 -->
{{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**:
```handlebars
{{formula "=SUM(A1:B1)"}}
{{formula "=AVERAGE(C2:C10)"}}
{{formula "=IF(D1>100,\"High\",\"Low\")"}}
```
**Dynamic Formula with `concat`**:
```handlebars
{{formula (concat "=A" (_r) "*B" (_r))}}
{{formula (concat "=SUM(" (_c) "2:" (_c) "10)")}}
{{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:
```handlebars
{{toColumnName "A" 0}} <!-- A (no offset) -->
{{toColumnName "A" 5}} <!-- F (A + 5) -->
{{toColumnName "Z" 1}} <!-- AA (Z + 1) -->
{{toColumnName (_c) 3}} <!-- Current column + 3 offset -->
{{formula (concat "=SUM(" (_c) "1:" (toColumnName (_c) 3) "1)")}}
```
**`toColumnIndex`** - Convert column name to column index (1-based):
```handlebars
{{toColumnIndex "A"}} <!-- 1 -->
{{toColumnIndex "Z"}} <!-- 26 -->
{{toColumnIndex "AA"}} <!-- 27 -->
{{toColumnIndex "AB"}} <!-- 28 -->
```
#### Merge Cells Helper
**`mergeCell`** - Mark cell ranges that need to be merged:
```handlebars
{{mergeCell "C4:D5"}} <!-- Merge C4 to D5 region -->
{{mergeCell "F4:G4"}} <!-- Merge F4 to G4 region -->
{{mergeCell (concat (_c) (_r) ":" (toColumnName (_c) 3) (_r))}}
{{mergeCell (concat (_c) (_r) ":" (toColumnName (_c) 2) (add (_r) 2))}}
{{#each sections}}
{{mergeCell (concat "A" (add @index 2) ":D" (add @index 2))}}
{{/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:
```handlebars
{{hyperlink (_cr) "Sheet2!A1" "View Details"}}
{{hyperlink (_cr) "https://example.com" "Visit Website"}}
{{#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:
```handlebars
{{employee.salary}}
{{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:
```handlebars
{{img logo.data}}
{{img photo.data 150 200}}
{{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**:
```javascript
// 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
```
```handlebars
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:
```handlebars
{{deleteCurrentSheet}}
{{#if shouldDelete}}
{{deleteCurrentSheet}}
{{/if}}
{{#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:
```handlebars
{{setCurrentSheetName "Sales Report"}}
{{setCurrentSheetName (concat department.name " - " year)}}
{{#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:
```handlebars
{{hideCurrentSheet}}
{{hideCurrentSheet "hidden"}}
{{hideCurrentSheet "veryHidden"}}
{{#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**:
```handlebars
{{#if (ne language "en-US")}}
{{deleteCurrentSheet}}
{{/if}}
{{setCurrentSheetName (concat department.name " Report")}}
{{#unless (eq userRole "admin")}}
{{hideCurrentSheet "veryHidden"}}
{{/unless}}
{{#if (eq status "inactive")}}
{{deleteCurrentSheet}}
{{else}}
{{setCurrentSheetName (concat "Active - " name)}}
{{#if isInternal}}
{{hideCurrentSheet}}
{{/if}}
{{/if}}
```
### Complex Example
```handlebars
=== 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
```bash
# 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
```bash
# 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.
```rust
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**:
```javascript
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.
```rust
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**:
```javascript
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**:
```rust
// 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);
}
```
```rust
// 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:
| 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):
| **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](LICENSE-MIT) file for details.
## Support
- π [Documentation](https://docs.rs/xlsx-handlebars)
- π [Issues](https://github.com/sail-sail/xlsx-handlebars/issues)
- π¬ [Discussions](https://github.com/sail-sail/xlsx-handlebars/discussions)
---
<div align="center">
<p>
<strong>xlsx-handlebars</strong> - Making XLSX template processing simple and efficient
</p>
<p>
<a href="https://github.com/sail-sail/xlsx-handlebars">β Star the project</a>
Β·
<a href="https://github.com/sail-sail/xlsx-handlebars/issues">π Report Issues</a>
Β·
<a href="https://github.com/sail-sail/xlsx-handlebars/discussions">π¬ Join Discussions</a>
</p>
</div>
## Support this project with a donation via Alipay:
