goxcel
Template-driven Excel generation library for Go
Generate Excel (.xlsx) files from human-readable grid templates. Combines Markdown-like syntax with programmatic Excel generation power.
Architecture Overview
System Architecture
The architecture follows Clean Architecture principles with clear separation of concerns:
- Input Layer: GXL templates and data files (JSON/YAML)
- Application Layer: CLI tool and Go library interface
- Config Layer: Dependency injection with BaseConfig and Logger
- Controller Layer: Command handlers and request routing
- Business Logic: Usecase layer (Book → Sheet → Cell)
- Data Access: Repository layer for parsing and writing
- Models: Strongly-typed data structures (GXL AST, XLSX models)
- Output Layer: Generated Excel files
Processing Flow
The processing follows this sequence:
- Parse: Read GXL template and parse into AST
- Load: Read JSON/YAML data file
- Render: Transform template + data into workbook
- BookUsecase orchestrates sheet rendering
- SheetUsecase processes nodes (Grid, For, Anchor)
- CellUsecase expands expressions and infers types
- Generate: Create OOXML structure with styles
- Write: Output .xlsx file
Why goxcel?
- Visual Templates: Grid-oriented templates that look like your Excel output
- Data-Driven: Separate data from layout using JSON contexts
- Pure Go: No external dependencies, no C libraries
- Type-Safe: Strong typing with compile-time safety
- Extensible: Formulas, merges, images, charts, and more
Features
✅ Grid templates with pipe-delimited syntax
✅ Value interpolation ({{ expr }})
✅ Control structures (For loops, conditionals)
✅ Excel formulas
✅ Cell merging
✅ Components (Images, Shapes, Charts)
✅ Structured logging
✅ CLI tool
Quick Example
Template (.gxl):
<Sheet name="Sales">
<Grid>| Product | Qty | Price |</Grid>
<For each="item in items">
<Grid>| {{ item.name }} | {{ item.qty }} | {{ item.price }} |</Grid>
</For>
</Sheet>
Data (JSON):
{"items": [{"name": "Apple", "qty": 10, "price": 100}]}
Generate:
goxcel generate --template report.gxl --data data.json --output report.xlsx
Next Steps
- New? Quick Start
- Vision? Mission & Strategy
- Details? Specification
Status & License
Active development. Core features stable. MIT License.
Vision & Strategy
Mission
Make Excel generation as simple as writing Markdown - developers should visualize output directly in templates without deep OOXML knowledge.
Problems Solved
Traditional approaches:
- Verbose cell-by-cell code
- Layout mixed with business logic
- Poor preview capability
- Non-technical users cannot contribute
Our solution:
- Visual grid templates
- Data/presentation separation
- Template-first approach
<Grid>
| Name | Quantity | Price |
</Grid>
<For each="item in items">
<Grid>
| {{ item.name }} | {{ item.qty }} | {{ item.price }} |
</Grid>
</For>
Benefits:
- ✅ Visual structure matches Excel output
- ✅ Data and layout are separated
- ✅ Templates can be versioned and reused
- ✅ Non-programmers can understand templates
- ✅ Easy to review and validate
Vision
Our Vision for the Future
To become the standard template language for structured document generation across all formats.
While goxcel starts with Excel, we envision a future where:
- Universal Templates: A single template syntax generates Excel, PDF, Word, and HTML
- Visual Editors: WYSIWYG editors that generate GXL templates
Core Values
- Simplicity First: Readable code, minimal API, convention over configuration
- Developer Experience: Clear errors, comprehensive docs, easy onboarding
- Reliability: Backward compatibility, comprehensive tests, stable releases
- Performance: Optimize hot paths, streaming for large data, predictable behavior
- Openness: Open source, transparent decisions, MIT license
- Pragmatism: Ship working software, validate with users, iterate
Roadmap
v1.0 (Current - Q4 2024) ✓
Core features: Parsing, Grid layout, Interpolation, For loops, Formulas, Merging, CLI
v1.1 (Q1 2025)
Conditionals, Anchor positioning, Basic styling, Cell formatting, Validation
v1.2 (Q2 2025)
Images, Charts, Shapes, Named ranges, Sheet protection, Hyperlinks
v2.0 (Q3 2025)
Pivot tables, Advanced charts, Conditional formatting, Streaming mode, Multi-sheet refs
v3.0+ (2026+)
Multi-format (PDF, HTML, Word), Visual editor, AI assistance, Enterprise features
Strategy
Phase 1 (Now): Excel excellence with clean architecture
Phase 2 (2025): Enhanced capabilities and ecosystem growth
Phase 3 (2026): Multi-format support and marketplace
Phase 4 (2027+): Intelligence layer and enterprise adoption
Commitment: SemVer, 2-year LTS, 6-month deprecation notice, migration guides
Installation
Prerequisites
- Go: Version 1.21 or higher
- Git: For cloning the repository
Install via go install
The easiest way to install goxcel:
go install github.com/ryo-arima/goxcel/cmd/goxcel@latest
This will install the goxcel binary to $GOPATH/bin (usually ~/go/bin).
Verify Installation
goxcel --version
Build from Source
Clone Repository
git clone https://github.com/ryo-arima/goxcel.git
cd goxcel
Build Binary
make build
The binary will be created at .bin/goxcel.
Install Locally
make install
Or copy manually:
cp .bin/goxcel /usr/local/bin/
# or
cp .bin/goxcel $GOPATH/bin/
Docker (Optional)
Build Docker image:
docker build -t goxcel .
Run with Docker:
docker run -v $(pwd):/workspace goxcel \
generate \
--template /workspace/template.gxl \
--data /workspace/data.json \
--output /workspace/output.xlsx
Verify Setup
Test with sample files:
# Navigate to repository
cd goxcel
# Generate sample
.bin/goxcel generate \
--template .etc/sample.gxl \
--data .etc/sample.json \
--output sample.xlsx
# Check output
ls -lh sample.xlsx
Environment Setup
Add to PATH
If goxcel is not found, add Go bin to PATH:
Linux/macOS (bash/zsh):
echo 'export PATH=$PATH:$HOME/go/bin' >> ~/.bashrc
# or for zsh
echo 'export PATH=$PATH:$HOME/go/bin' >> ~/.zshrc
source ~/.bashrc # or source ~/.zshrc
Windows (PowerShell):
$env:PATH += ";$env:USERPROFILE\go\bin"
Configure Logging (Optional)
Set environment variables for logging:
export GOXCEL_LOG_LEVEL=DEBUG
export GOXCEL_LOG_STRUCTURED=true
Troubleshooting
Command Not Found
If goxcel command is not found:
- Check Go is installed:
go version - Verify GOPATH:
go env GOPATH - Check binary location:
ls $GOPATH/bin/goxcel - Ensure PATH includes Go bin directory
Build Errors
If build fails:
- Update Go:
go versionshould be 1.21+ - Clean and rebuild:
go clean -cache make clean make build
Permission Denied
On Linux/macOS, make binary executable:
chmod +x .bin/goxcel
# or for installed binary
chmod +x $GOPATH/bin/goxcel
Next Steps
- Quick Start Guide - Create your first template
- Basic Concepts - Understand GXL fundamentals
- Specification - Detailed format reference
Quick Start
This guide walks you through creating your first Excel file with goxcel in 5 minutes.
Step 1: Create a Template
Create a file named hello.gxl:
<Book name="HelloWorkbook">
<Sheet name="Greeting">
<Grid>
| **Message** | **Value** |
| Hello | {{ .name }} |
| Generated | {{ .date }} |
</Grid>
</Sheet>
</Book>
What this does:
- Creates a workbook named "HelloWorkbook"
- Creates a sheet named "Greeting"
- Defines a 2-column table with headers in bold
- Uses
{{ .name }}and{{ .date }}to inject data
Step 2: Create Data File
Create data.json:
{
"name": "World",
"date": "2025-11-04"
}
Step 3: Generate Excel File
Run goxcel:
goxcel generate \
--template hello.gxl \
--data data.json \
--output hello.xlsx
Output:
[INFO] Starting generate command
[INFO] GXL file parsed successfully
[INFO] Template rendered successfully
[INFO] XLSX file written successfully
Step 4: Open the File
Open hello.xlsx in Excel or LibreOffice:
| Message | Value |
|-----------|------------|
| Hello | World |
| Generated | 2025-11-04 |
Headers will be bold.
Example 2: Using Loops
Create invoice.gxl:
<Book name="Invoice">
<Sheet name="Items">
<Grid>
| **Item** | **Quantity** | **Price** |
</Grid>
<For each="item in items">
<Grid>
| {{ .item.name }} | {{ .item.quantity:number }} | {{ .item.price:number }} |
</Grid>
</For>
<Grid>
| **Total** | | {{ .total:number }} |
</Grid>
</Sheet>
</Book>
Create invoice-data.json:
{
"items": [
{"name": "Widget A", "quantity": 5, "price": 10.00},
{"name": "Widget B", "quantity": 3, "price": 25.50},
{"name": "Widget C", "quantity": 2, "price": 15.75}
],
"total": 157.00
}
Generate:
goxcel generate \
--template invoice.gxl \
--data invoice-data.json \
--output invoice.xlsx
Result:
| Item | Quantity | Price |
|----------|----------|-------|
| Widget A | 5 | 10.00 |
| Widget B | 3 | 25.50 |
| Widget C | 2 | 15.75 |
| Total | | 157.00|
Example 3: Positioning with Anchors
Create positioned.gxl:
<Book name="Report">
<Sheet name="Dashboard">
<Anchor ref="A1" />
<Grid>
| **Title** |
| Sales Report |
</Grid>
<Anchor ref="A5" />
<Grid>
| **Region** | **Sales** |
| North | {{ .north:number }} |
| South | {{ .south:number }} |
</Grid>
<Anchor ref="E5" />
<Grid>
| **Summary** |
| Total: {{ .total:number }} |
</Grid>
</Sheet>
</Book>
Create report-data.json:
{
"north": 15000,
"south": 23000,
"total": 38000
}
This creates content at specific cell positions (A1, A5, E5).
Common Features
Cell Formatting
<Grid>
| **Bold Text** | _Italic Text_ |
</Grid>
Type Hints
<Grid>
| {{ .text:string }} |
| {{ .count:number }} |
| {{ .active:boolean }} |
| {{ .created:date }} |
</Grid>
Formulas
<Grid>
| Value 1 | Value 2 | Sum |
| 10 | 20 | =A2+B2 |
</Grid>
Cell Merging
<Grid>
| Title |
</Grid>
<Merge range="A1:C1" />
Dry Run Mode
Preview without creating a file:
goxcel generate \
--template hello.gxl \
--data data.json \
--dry-run
Output shows parsed structure and cell data.
Using YAML Data
goxcel also supports YAML:
# data.yaml
name: World
date: 2025-11-04
items:
- name: Item 1
value: 100
- name: Item 2
value: 200
goxcel generate \
--template template.gxl \
--data data.yaml \
--output output.xlsx
Next Steps
- Basic Concepts - Understand GXL fundamentals
- Core Tags - Complete tag reference
- Examples - More complex examples
- Troubleshooting - Common issues
Optional: Format Your Template
Use the built-in formatter to keep your .gxl templates readable and consistent:
goxcel format template.gxl # print to stdout
goxcel format -w template.gxl # overwrite in place
goxcel format -o formatted.gxl template.gxl
What it does:
- Pretty-prints tags with indentation
- Inlines empty tags as a single line:
<Merge range="A1:C1"> </Merge> - Removes double blank lines outside content
- Aligns
|columns inside<Grid>so tables are easy to read - Preserves comments and significant text
Quick Reference
Template Structure
<Book name="WorkbookName">
<Sheet name="SheetName">
<!-- Content here -->
</Sheet>
</Book>
Grid Syntax
<Grid>
| Header1 | Header2 |
| Value1 | Value2 |
</Grid>
Data Binding
{{ .path.to.value }}
{{ .value:type }}
Control Structures
<For each="item in items">
<!-- Repeated content -->
</For>
Positioning
<Anchor ref="A1" />
<Merge range="A1:B2" />
Basic Concepts
Understanding these core concepts will help you effectively use goxcel.
Templates and Data
Template (GXL File)
A template defines the structure of your Excel file. It's an XML file with .gxl extension that contains:
- Workbook and sheet definitions
- Table layouts (grids)
- Data binding expressions
- Control structures (loops)
- Positioning instructions
Example:
<Book name="Report">
<Sheet name="Data">
<Grid>
| Name | {{ .userName }} |
</Grid>
</Sheet>
</Book>
Data File
Data provides the values to inject into the template. Supported formats:
- JSON (
.json) - YAML (
.yaml,.yml)
Example:
{
"userName": "John Doe"
}
Generation Process
Template (.gxl) + Data (.json/.yaml) → goxcel → Excel File (.xlsx)
GXL Structure
Hierarchy
Book (Workbook)
└── Sheet (Worksheet)
├── Grid (Table)
├── For (Loop)
├── Anchor (Position)
└── Merge (Cell merge)
Book
The root element representing an Excel workbook:
<Book name="MyWorkbook">
<!-- Sheets go here -->
</Book>
Sheet
Represents a worksheet within the workbook:
<Sheet name="Sheet1">
<!-- Content goes here -->
</Sheet>
You can have multiple sheets:
<Book name="Report">
<Sheet name="Summary">
<!-- ... -->
</Sheet>
<Sheet name="Details">
<!-- ... -->
</Sheet>
</Book>
Grid
Defines a table using pipe-delimited syntax:
<Grid>
| Header1 | Header2 | Header3 |
| Value1 | Value2 | Value3 |
| Value4 | Value5 | Value6 |
</Grid>
Rules:
- Each row starts and ends with
| - Cells are separated by
| - Whitespace around values is trimmed
- Consistent column count per row recommended
Data Binding
Expressions
Use {{ }} to inject data:
<Grid>
| Name | {{ .name }} |
| Age | {{ .age }} |
</Grid>
With data:
{
"name": "Alice",
"age": 30
}
Nested Access
Use dot notation for nested data:
{{ .user.profile.email }}
With data:
{
"user": {
"profile": {
"email": "alice@example.com"
}
}
}
Array Access in Loops
<For each="item in items">
<Grid>
| {{ .item.name }} | {{ .item.value }} |
</Grid>
</For>
Type System
Automatic Type Inference
goxcel automatically detects types:
- Number:
123,45.67,-10 - Boolean:
true,false - Date:
2025-11-04,2025-11-04T10:30:00 - Formula:
=SUM(A1:A10) - String: Everything else
Type Hints
Force a specific type:
{{ .value:number }} <!-- Force as number -->
{{ .text:string }} <!-- Force as string -->
{{ .flag:boolean }} <!-- Force as boolean -->
{{ .created:date }} <!-- Force as date -->
Example:
<Grid>
| ID | {{ .id:string }} | <!-- "001" stays as text -->
| Amount | {{ .amount:number }} | <!-- Ensures numeric -->
</Grid>
Control Structures
For Loops
Iterate over arrays:
<For each="item in items">
<!-- This repeats for each item -->
<Grid>
| {{ .item.name }} |
</Grid>
</For>
Data:
{
"items": [
{"name": "Apple"},
{"name": "Banana"},
{"name": "Cherry"}
]
}
Output:
| Apple |
| Banana |
| Cherry |
Nested Loops
<For each="dept in departments">
<Grid>
| **{{ .dept.name }}** |
</Grid>
<For each="emp in dept.employees">
<Grid>
| {{ .emp.name }} | {{ .emp.role }} |
</Grid>
</For>
</For>
Positioning
Sequential (Default)
Content flows from current position:
<Grid>
| Row 1 |
</Grid>
<Grid>
| Row 2 | <!-- Appears below Row 1 -->
</Grid>
Absolute with Anchor
Set specific cell position:
<Anchor ref="A1" />
<Grid>
| Title |
</Grid>
<Anchor ref="A10" />
<Grid>
| Data starts here |
</Grid>
Grid with Ref
Position a grid at specific location:
<Grid ref="E5">
| Summary |
</Grid>
Styling
Markdown Syntax
Apply formatting within cells:
<Grid>
| **Bold text** |
| _Italic text_ |
| **_Bold and italic_** |
</Grid>
Supported:
**text**→ Bold_text_→ Italic
Formulas
Excel formulas work directly:
<Grid>
| 10 | 20 | =A1+B1 |
| =SUM(A1:A10) |
</Grid>
Cell Merging
Merge cells after defining them:
<Grid>
| Large Title |
</Grid>
<Merge range="A1:C1" />
Context Stack
When using loops, data context changes:
<!-- Root context -->
{{ .rootValue }}
<For each="item in items">
<!-- Item context (can still access root) -->
{{ .item.name }}
{{ .rootValue }} <!-- Still accessible -->
<For each="sub in item.subs">
<!-- Sub context (can access item and root) -->
{{ .sub.value }}
{{ .item.name }}
{{ .rootValue }}
</For>
</For>
Context Stack (innermost to outermost):
- Current loop variable (
.sub) - Parent loop variable (
.item) - Root data (
.rootValue)
Best Practices
Template Design
- Keep grids simple: One table per Grid tag
- Use anchors sparingly: Sequential flow is easier to maintain
- Name meaningfully: Clear sheet and workbook names
- Comment complex sections: Use XML comments
<!-- -->
Data Structure
- Match template paths: Ensure JSON structure matches template expressions
- Use arrays for loops: Structure data to match For loops
- Consistent types: Use same type for similar values
- Avoid deep nesting: Keep data structure reasonably flat
Type Management
- Use type hints for IDs: Force strings for numeric IDs like
001 - Explicit numbers: Use
:numberfor calculations - Date formats: Use ISO 8601 format:
YYYY-MM-DD - Boolean clarity: Use
true/falsenot"true"/"false"
Common Patterns
Header with Data Rows
<Grid>
| **Name** | **Email** | **Status** |
</Grid>
<For each="user in users">
<Grid>
| {{ .user.name }} | {{ .user.email }} | {{ .user.status }} |
</Grid>
</For>
Summary Section
<Anchor ref="A1" />
<Grid>
| **Report Summary** |
</Grid>
<Merge range="A1:C1" />
<Grid>
| Generated | {{ .date }} |
| Total Records | {{ .count:number }} |
</Grid>
Multi-Sheet Report
<Book name="MonthlyReport">
<Sheet name="Summary">
<Grid>
| **Total Sales** | {{ .total:number }} |
</Grid>
</Sheet>
<Sheet name="Details">
<For each="item in items">
<Grid>
| {{ .item.name }} | {{ .item.amount:number }} |
</Grid>
</For>
</Sheet>
</Book>
Next Steps
- Core Tags Reference - Complete tag documentation
- Control Structures - Loops and conditionals
- Expressions - Data binding details
- Examples - Real-world examples
GXL Format Specification
Version: 0.1.1
Status: Stable
Last Updated: 2024-11-04
Introduction
GXL (Grid eXcel Language) is a template format for describing Excel workbooks using a human-readable, Markdown-compatible syntax with custom XML-like tags. It enables developers to create Excel files by writing templates that visually represent the final spreadsheet output.
What is GXL?
GXL stands for Grid eXcel Language. It is:
- A template format: Not a programming language, but a declarative template system
- Human-readable: Designed to be easy to read and write by humans
- Grid-oriented: Uses pipe-delimited syntax to represent tabular data visually
- Markdown-compatible: Can coexist with regular Markdown documentation
- XML-inspired: Uses custom tags similar to HTML/XML for structure
Design Philosophy
GXL is built on these core principles:
- What You See Is What You Get: The template structure should closely match the Excel output
- Separation of Concerns: Data and presentation should be cleanly separated
- Progressive Enhancement: Simple cases should be simple, complex cases should be possible
- Human-First: Optimize for human readability over parser efficiency
- Extensibility: Design for future features without breaking existing templates
Who Should Use GXL?
Target Audience
- Backend Developers: Building reporting systems, data exports, invoice generators
- Data Engineers: Creating data pipelines with Excel outputs
- Full-Stack Developers: Adding Excel export features to web applications
- DevOps Engineers: Generating operational reports and dashboards
- Technical Writers: Creating documentation with embedded data tables
Use Cases
- Reports: Financial reports, sales reports, analytics dashboards
- Invoices: Customer invoices, purchase orders, receipts
- Data Exports: Database exports, API response dumps, log summaries
- Templates: Reusable document templates with variable data
- Bulk Operations: Mass generation of personalized documents
Specification Structure
This specification is organized into the following sections:
- Overview - High-level introduction and concepts
- File Format - File structure, encoding, and metadata
- Core Tags - Book, Sheet, Grid, Anchor, Merge
- Control Structures - For loops, If/Else conditionals
- Expressions - Value interpolation and expression language
- Components - Images, Shapes, Charts, Pivot Tables
- Styling - Style system and formatting
- Data Context - How data flows through templates
- Validation Rules - Constraints and error conditions
- Rendering Semantics - How templates are processed
- Examples - Complete working examples
Quick Example
Here's a minimal GXL template to get you started:
Template (invoice.gxl):
<Book>
<Sheet name="Invoice">
<Grid>
| Invoice #{{ invoiceNumber }} | Date: {{ date }} |
</Grid>
<Grid>
| Item | Quantity | Price | Total |
</Grid>
<For each="item in items">
<Grid>
| {{ item.name }} | {{ item.qty }} | ${{ item.price }} | ={{ item.qty }}*{{ item.price }} |
</Grid>
</For>
<Grid>
| | | Total: | =SUM(D3:D{{ items.length + 2 }}) |
</Grid>
</Sheet>
</Book>
Data (data.json):
{
"invoiceNumber": "INV-2024-001",
"date": "2024-11-03",
"items": [
{"name": "Widget A", "qty": 10, "price": 25.00},
{"name": "Widget B", "qty": 5, "price": 50.00}
]
}
Output: An Excel file with:
- Header row with invoice number and date
- Table with item details
- Automatic total calculation using Excel formula
Version History
| Version | Date | Changes |
|---|---|---|
| 0.1 | 2024-11-03 | Initial specification |
| 0.1.1 | 2024-11-04 | Cell type system, markdown styles |
Current Implementation Status (v1.0)
Fully Implemented ✅:
- Grid layout with pipe syntax
- Value interpolation (
{{ expr }}) - For loops with nested support
- Cell type inference and hints (
:int,:float,:bool,:date) - Markdown styles (
**bold**,_italic_) - Excel formulas
- Cell merging
- Anchor positioning
- Component placeholders (Image, Chart, Shape)
Planned (v1.1+) ⏳:
- If/Else conditionals
- Advanced styling (
<Style>tag) - Number formatting
- Data validation
- Sheet protection
Compatibility
Excel Version Support
- Excel 2007+ (.xlsx / OOXML format)
- LibreOffice Calc 6.0+
- Google Sheets (import)
Implementation Compatibility
- goxcel v1.0.x: Implements GXL 0.1.1
- Pure Go, no external dependencies
- Go 1.20+ required
Next Steps
- Core Tags - Fundamental tags reference
- Expressions - Value interpolation
- Control Structures - Loops and conditionals
- Examples - Complete working examples
File Format
This section describes the physical structure of GXL template files.
File Extension
Primary Extension
.gxl- GXL template file
Alternative Extensions (Future)
.gxl.md- GXL with Markdown emphasis.gxl.xml- GXL with XML emphasis
Character Encoding
Required Encoding
- UTF-8 without BOM (Byte Order Mark)
Why UTF-8?
- Universal character support (international characters, emoji, etc.)
- Backward compatible with ASCII
- Industry standard for text files
- Git-friendly (no encoding issues)
BOM Policy
- Do NOT use BOM (Byte Order Mark)
- BOM causes parsing issues in many tools
- UTF-8 BOM is unnecessary and discouraged
Example
# Correct UTF-8 file (no BOM)
<Book>
<Sheet name="日本語シート">
...
Line Endings
Recommended
- LF (Line Feed)
\n- Unix/Linux/macOS style
Also Supported
- CRLF (Carriage Return + Line Feed)
\r\n- Windows style
Rationale
- LF is the Git default
- LF works across all platforms
- Most modern editors handle both automatically
Git Configuration
Configure .gitattributes to normalize line endings:
*.gxl text eol=lf
File Structure
Overall Organization
A GXL file consists of:
- Optional header comments
- One
<Book>root element - One or more
<Sheet>elements - Content within sheets (Grid, components, control structures)
Visual Structure
┌─────────────────────────────────┐
│ <!-- Optional Comments --> │
├─────────────────────────────────┤
│ <Book> │
│ ├─ <Sheet name="Sheet1"> │
│ │ ├─ <Grid> ... </Grid> │
│ │ ├─ <For> ... </For> │
│ │ └─ <Merge ... /> │
│ │ │
│ └─ <Sheet name="Sheet2"> │
│ └─ <Grid> ... </Grid> │
│ </Book> │
└─────────────────────────────────┘
Comments
XML-Style Comments
<!-- This is a comment -->
<!--
Multi-line comment
can span multiple lines
-->
Comment Rules
- Comments are ignored during parsing
- Can appear anywhere outside tags
- Cannot appear inside tag attributes
- Cannot be nested
Use Cases
<!-- Invoice Template v2.1 -->
<!-- Author: John Doe -->
<!-- Last Modified: 2024-11-03 -->
<Book>
<!-- Sales Data Sheet -->
<Sheet name="Sales">
<!-- Header row with company logo -->
<Grid>
| Company Name | Date |
</Grid>
<!-- TODO: Add monthly breakdown -->
</Sheet>
</Book>
Whitespace Handling
General Rules
- Significant: Whitespace inside
<Grid>cells - Insignificant: Whitespace outside tags and around tag names
- Trimmed: Leading/trailing whitespace in cell content
Cell Content Trimming
<Grid>
| Hello | World |
</Grid>
Equivalent to:
<Grid>
| Hello | World |
</Grid>
Both produce cells with content "Hello" and "World" (no extra spaces).
Preserving Whitespace
To preserve leading/trailing spaces, use expressions:
<Grid>
| {{ " Hello " }} |
</Grid>
Indentation
Indentation is insignificant and used for readability:
<Book>
<Sheet name="Example">
<Grid>
| A | B |
</Grid>
</Sheet>
</Book>
Same as:
<Book>
<Sheet name="Example">
<Grid>
| A | B |
</Grid>
</Sheet>
</Book>
Automatic Formatting (CLI)
goxcel includes a template formatter available via the CLI:
goxcel format <file.gxl> # pretty-print to stdout
goxcel format -w <file.gxl> # overwrite in place
goxcel format -o out.gxl <file.gxl>
Rules applied by the formatter:
- Indentation: tags are indented with 2 spaces per nesting level
- Empty elements: when a tag has no text and no children, it is inlined on one line
- Example:
<Merge range="A1:C1"> </Merge>
- Example:
- Blank lines: consecutive blank lines outside content are collapsed (no double blank lines)
- Grid alignment: inside
<Grid>, pipe-delimited rows are aligned so that|characters line up by column - Preservation: non-whitespace character data and XML comments are preserved
Before and After:
Before
<Grid>
| A | B |C|
| 1| 22 |333|
</Grid>
<Merge range="A1:C1">
</Merge>
After
<Grid>
| A | B | C |
| 1 | 22 | 333 |
</Grid>
<Merge range="A1:C1"> </Merge>
Note: Grid alignment uses character count (runes) for width; full-width East Asian display widths are not accounted for.
Case Sensitivity
Tag Names
- Case-sensitive:
<Book>≠<book> - Convention: PascalCase (e.g.,
<Book>,<Sheet>,<Grid>)
Attribute Names
- Case-sensitive:
name="Sheet1"≠Name="Sheet1" - Convention: camelCase (e.g.,
name,dataRange,fillColor)
Attribute Values
- Case-sensitive: Depend on context
- Sheet names:
"Sales"≠"sales" - Cell references:
"A1"="a1"(normalized to uppercase) - Expressions:
{{ user.Name }}≠{{ user.name }}
- Sheet names:
File Size Limits
Practical Limits
- File size: No hard limit (limited by available memory)
- Sheets: Recommended max 100 sheets per workbook
- Rows per sheet: Excel limit is 1,048,576
- Columns per sheet: Excel limit is 16,384 (XFD)
Performance Considerations
- Large templates (>10MB) may be slow to parse
- Use streaming mode for large datasets (future feature)
- Consider splitting large workbooks into multiple files
MIME Type
Proposed MIME Type
text/x-gxl- GXL template file
HTTP Headers
Content-Type: text/x-gxl; charset=utf-8
Content-Disposition: attachment; filename="template.gxl"
File Upload Detection
Web servers and applications should recognize .gxl extension:
# Apache .htaccess
AddType text/x-gxl .gxl
Metadata (Future)
Embedded Metadata (Planned)
<Book
title="Sales Report"
author="John Doe"
version="1.0"
created="2024-11-03"
>
<Metadata>
<Property name="department" value="Finance" />
<Property name="confidential" value="true" />
</Metadata>
<Sheet name="Data">
...
</Sheet>
</Book>
Status: Planned for GXL v0.2
File Naming Conventions
Recommended Naming
- Use descriptive names:
invoice-template.gxl,sales-report.gxl - Use kebab-case:
monthly-report.gxl(notMonthlyReport.gxl) - Avoid spaces: Use hyphens or underscores
- Be specific: Include purpose in name
Version Suffixes
invoice-template-v1.gxl
invoice-template-v2.gxl
sales-report-2024.gxl
Environment Suffixes
report-dev.gxl
report-staging.gxl
report-prod.gxl
Validation
Well-Formedness
A valid GXL file must be:
- Valid UTF-8 encoding
- Properly nested XML-like tags
- One root
<Book>element - At least one
<Sheet>element
Validation Tools (Future)
gxl-lint: Syntax checkergxl-format: Auto-formatter- Editor plugins: VS Code, Sublime, etc.
Best Practices
1. Use Version Control
- Store
.gxlfiles in Git - Track changes with meaningful commit messages
- Use branches for template variations
2. Document Templates
- Add comments explaining complex logic
- Include authorship and version info
- Document expected data structure
3. Organize Large Templates
- Group related content with comments
- Use consistent indentation
- Split very large templates into multiple files (via includes, future feature)
4. Test Templates
- Test with sample data
- Validate generated Excel files
- Check for edge cases (empty arrays, null values)
Example Template Structure
<?xml version="1.0" encoding="UTF-8"?>
<!--
Invoice Template v1.0
Author: John Doe
Created: 2024-11-03
Required data structure:
{
"company": {"name": string, "address": string},
"invoice": {"number": string, "date": string},
"items": [{"name": string, "qty": number, "price": number}]
}
-->
<Book>
<!-- Invoice Sheet -->
<Sheet name="Invoice">
<!-- Company Header -->
<Grid>
| {{ company.name }} | |
| {{ company.address }} | |
</Grid>
<Merge range="A1:B1" />
<Merge range="A2:B2" />
<!-- Invoice Details -->
<Grid>
| Invoice: {{ invoice.number }} | Date: {{ invoice.date }} |
</Grid>
<!-- Item Table -->
<Grid>
| Item | Qty | Price | Total |
</Grid>
<For each="item in items">
<Grid>
| {{ item.name }} | {{ item.qty }} | {{ item.price }} | ={{ item.qty }}*{{ item.price }} |
</Grid>
</For>
<!-- Total Row -->
<Grid>
| | | Total: | =SUM(D6:D{{ 5 + items.length }}) |
</Grid>
</Sheet>
</Book>
Related Sections
- Core Tags - Learn about
<Book>,<Sheet>,<Grid> - Expressions - Value interpolation syntax
- Validation Rules - What makes a valid GXL file
Core Tags
Core tags are the fundamental building blocks of GXL templates. They define the structure of workbooks, sheets, and cell content.
Book
The root element that defines an Excel workbook.
Syntax
<Book>
<!-- sheets and content -->
</Book>
Attributes
Currently, <Book> has no required attributes. Future versions may support:
title: Workbook titleauthor: Document authorcreated: Creation datemodified: Last modification date
Rules
- Must be root element: Every GXL file must have exactly one
<Book>element - Must contain sheets: At least one
<Sheet>element is required - No content outside: No text or tags allowed outside
<Book>
Examples
Minimal workbook:
<Book>
<Sheet name="Sheet1">
<Grid>
| Hello | World |
</Grid>
</Sheet>
</Book>
Multiple sheets:
<Book>
<Sheet name="Sales">
<Grid>
| Product | Revenue |
</Grid>
</Sheet>
<Sheet name="Expenses">
<Grid>
| Category | Amount |
</Grid>
</Sheet>
</Book>
Future Features (Planned)
<Book
title="Annual Report 2024"
author="Finance Department"
created="2024-01-01"
>
<Metadata>
<Property name="department" value="Finance" />
<Property name="classification" value="Internal" />
</Metadata>
<Sheet name="Summary">
...
</Sheet>
</Book>
Sheet
Defines a worksheet within the workbook.
Syntax
<Sheet name="SheetName">
<!-- content -->
</Sheet>
Attributes
name (required)
- Type: String
- Description: The name of the worksheet as it appears in Excel
- Constraints:
- Must be unique within the workbook
- Maximum 31 characters (Excel limitation)
- Cannot contain:
\ / ? * [ ]or: - Cannot be empty
- Leading/trailing spaces are trimmed
col_width (optional)
- Type: Length
- Description: Default column width for the entire sheet
- Units:
ch(characters, default if no unit),cm,mm,in,pt,px - Examples:
"8.43","1cm","72px"
row_height (optional)
- Type: Length
- Description: Default row height for the entire sheet
- Units:
pt(points, default if no unit),cm,mm,in,px - Examples:
"15","1cm","20px"
Note: For backward compatibility, row_heigh is accepted as an alias of row_height.
Rules
- Unique names: No two sheets can have the same name
- At least one sheet: A workbook must contain at least one sheet
- Order matters: Sheets appear in the order defined
- Case-sensitive:
"Sales"and"sales"are different sheets
Examples
Basic sheet:
<Sheet name="Sales Data" col_width="1cm" row_height="1cm">
<Grid>
| Date | Amount |
| 2024-01-01 | 1000 |
</Grid>
</Sheet>
Multiple sheets with different purposes:
<Book>
<!-- Data sheet -->
<Sheet name="Raw Data">
<Grid>
| ID | Name | Value |
</Grid>
<For each="row in data">
<Grid>
| {{ row.id }} | {{ row.name }} | {{ row.value }} |
</Grid>
</For>
</Sheet>
<!-- Summary sheet -->
<Sheet name="Summary">
<Grid>
| Total Records | =COUNTA('Raw Data'!A:A)-1 |
| Total Value | =SUM('Raw Data'!C:C) |
</Grid>
</Sheet>
<!-- Charts sheet -->
<Sheet name="Visualizations">
<Chart
ref="A1"
type="column"
dataRange="'Raw Data'!A1:C100"
title="Data Overview"
/>
</Sheet>
</Book>
Naming Best Practices
Good names:
- Descriptive:
Sales 2024,Employee List,Profit & Loss - Concise: Keep under 20 characters when possible
- Clear purpose: Name indicates content
Avoid:
- Generic names:
Sheet1,Sheet2 - Special characters:
Sales/Expenses(useSales & Expenses) - Too long:
This is a very long sheet name that exceeds the limit
Sheet References in Formulas
When referencing cells from other sheets, use single quotes if sheet name contains spaces:
<Sheet name="Summary">
<Grid>
| Total from Sales | =SUM('Sales Data'!B:B) |
</Grid>
</Sheet>
Grid
Defines a grid of cells using pipe-delimited rows.
Syntax
Basic usage:
<Grid>
| Cell A1 | Cell B1 | Cell C1 |
| Cell A2 | Cell B2 | Cell C2 |
| Cell A3 | Cell B3 | Cell C3 |
</Grid>
With absolute positioning (v1.0+):
<Grid ref="D5">
| Cell D5 | Cell E5 |
| Cell D6 | Cell E6 |
</Grid>
With style attributes (v1.x+):
<Grid font="Arial" font_size="12" text_color="#333333" fill_color="#FFFFCC">
| Header 1 | Header 2 |
| Data 1 | Data 2 |
</Grid>
Attributes
ref (optional, v1.0+)
- Type: String (A1 notation)
- Description: Absolute starting position for the grid
- Default: Current cursor position
- Examples:
"A1","B5","D10" - Behavior: When specified, the grid is placed at the absolute position without affecting the cursor position
Style attributes (optional, v1.x+)
font/font_name: Font family for all cells in the grid (e.g.,Arial)font_size/text_size: Font size in points (integer)font_color/text_color: Font color in RGB hex;#optional (e.g.,#FF0000orFF0000)fill_color/color: Background fill color in RGB hex;#optionalborder/border_style: Border style for the grid's cells. Supported:thin,medium,thick,dashed,dotted,doubleborder_color: Border color in RGB hex;#optionalborder_sides: Comma-separated sides to apply (defaultall). Options:all,top,right,bottom,left
These defaults apply to every cell produced by the Grid unless overridden by per-cell formatting (e.g., markdown **bold**).
Examples (borders):
<Grid border="thin" border_color="#999999">
| A | B |
| 1 | 2 |
</Grid>
<Grid border="dashed" border_color="#FF0000" border_sides="top,bottom">
| Header 1 | Header 2 |
| Data 1 | Data 2 |
</Grid>
Example - Grid with ref:
<!-- Sequential grids -->
<Grid>
| Header 1 | Header 2 |
</Grid>
<Grid>
| Data 1 | Data 2 |
</Grid>
<!-- Absolute position at E1 (doesn't affect cursor) -->
<Grid ref="E1">
| Side Note |
</Grid>
<!-- Continues from A3 (after the first two grids) -->
<Grid>
| Row 3 | More Data |
</Grid>
Result:
| A | B | C | D | E | |
|---|---|---|---|---|---|
| 1 | Header 1 | Header 2 | Side Note | ||
| 2 | Data 1 | Data 2 | |||
| 3 | Row 3 | More Data |
Structure
- Rows: Each line within
<Grid>represents one row - Columns: Cells are delimited by
|(pipe character) - Optional pipes: Leading and trailing pipes are optional
- Whitespace: Trimmed around cell content
Cell Content Types
1. Literal Values
<Grid>
| Plain text | 123 | 45.67 | true |
</Grid>
Cell types are inferred:
- Numbers:
123,45.67,-10.5 - Strings:
Hello,Product Name - Booleans:
true,false - Dates:
2024-01-01(ISO format)
2. Formulas
Cells starting with = are Excel formulas:
<Grid>
| Product | Price | Quantity | Total |
| Widget | 10.50 | 5 | =B2*C2 |
| Gadget | 25.00 | 3 | =B3*C3 |
| | | Grand Total | =SUM(D2:D3) |
</Grid>
Supported formula features:
- Cell references:
A1,B2,$A$1 - Ranges:
A1:A10,B2:D5 - Functions:
SUM(),AVERAGE(),IF(), etc. - Operators:
+,-,*,/,^ - Sheet references:
'Sheet1'!A1
3. Interpolated Values
Use {{ }} for dynamic values:
<Grid>
| {{ user.name }} | {{ user.email }} | {{ user.age }} |
</Grid>
4. Mixed (Formulas with Interpolation)
Combine formulas and expressions:
<Grid>
| Total | =SUM(A1:A{{ rowCount }}) |
| Average | =AVERAGE(B1:B{{ rowCount }}) |
</Grid>
Empty Cells
Multiple consecutive pipes create empty cells:
<Grid>
| A | | C | <!-- B is empty -->
| | B | | <!-- A and C are empty -->
</Grid>
Positioning
Grid cells are placed relative to the current cursor position (unless ref is specified):
- Starts at
A1by default - Advances after each
<Grid>block - Can be reset with
<Anchor>
Sequential positioning:
<!-- Starts at A1 -->
<Grid>
| Row 1 |
</Grid>
<!-- Continues at A2 -->
<Grid>
| Row 2 |
</Grid>
With Anchor:
<!-- Reset to E1 -->
<Anchor ref="E1" />
<Grid>
| Over here |
</Grid>
With Grid ref attribute (v1.0+):
<!-- Normal sequential -->
<Grid>
| Row 1 |
</Grid>
<!-- Absolute position (cursor stays at A2) -->
<Grid ref="E1">
| Absolute |
</Grid>
<!-- Continues at A2 -->
<Grid>
| Row 2 |
</Grid>
Comparison: Anchor vs Grid ref
| Feature | <Anchor> | <Grid ref=""> |
|---|---|---|
| Scope | Affects all subsequent content | Only affects that grid |
| Cursor Movement | Moves cursor permanently | Doesn't affect cursor |
| Use Case | Change layout flow | Place independent content |
Multi-column Grids
<Grid>
| Name | Email | Phone | Address |
| John | j@example.com | 555-1234 | 123 Main St |
| Jane | jane@example.com | 555-5678 | 456 Oak Ave |
</Grid>
Column Alignment
Pipes don't need to align, but alignment improves readability:
<!-- Not aligned (valid but hard to read) -->
<Grid>
| Name | Email | Phone |
| John Doe | john@example.com | 555-1234 |
| Jane Smith | jane@example.com | 555-5678 |
</Grid>
<!-- Aligned (recommended) -->
<Grid>
| Name | Email | Phone |
| John Doe | john@example.com | 555-1234 |
| Jane Smith | jane@example.com | 555-5678 |
</Grid>
Anchor
Sets the absolute position for subsequent grid placement.
Syntax
<Anchor ref="CellReference" />
Attributes
ref (required)
- Type: String
- Description: Absolute cell reference in A1 notation
- Format:
[Column][Row](e.g.,A1,Z100,AA5) - Valid examples:
A1,B10,AA1,XFD1048576
Purpose
By default, content flows from top-left (A1) downward. Use <Anchor> to:
- Position content at specific locations
- Create multiple independent sections
- Layout complex reports
Cursor Behavior
- Initial position:
A1(if no anchor specified) - After Grid: Cursor advances downward by number of rows
- After Anchor: Cursor jumps to specified position
Examples
Position content at specific cell:
<Anchor ref="A1" />
<Grid>
| Header |
</Grid>
<Anchor ref="A10" />
<Grid>
| Footer |
</Grid>
Create side-by-side sections:
<!-- Left section -->
<Anchor ref="A1" />
<Grid>
| Section 1 |
| Data here |
</Grid>
<!-- Right section -->
<Anchor ref="E1" />
<Grid>
| Section 2 |
| Data here |
</Grid>
Complex layout:
<Sheet name="Dashboard">
<!-- Title at top -->
<Anchor ref="A1" />
<Grid>
| Sales Dashboard |
</Grid>
<Merge range="A1:F1" />
<!-- KPIs in row 3 -->
<Anchor ref="A3" />
<Grid>
| Revenue | $1,000,000 |
| Orders | 500 |
</Grid>
<Anchor ref="D3" />
<Grid>
| Customers | 250 |
| Conversion | 5% |
</Grid>
<!-- Chart at row 7 -->
<Anchor ref="A7" />
<Chart ref="A7" type="column" dataRange="A3:B4" />
</Sheet>
Best Practices
- Use sparingly: Let content flow naturally when possible
- Prefer Grid ref: For independent content, use
<Grid ref="">instead of<Anchor>to avoid affecting layout flow - Document reasons: Add comments explaining why specific positioning is needed
- Avoid overlaps: Ensure anchored content doesn't overlap
- Test thoroughly: Verify layout with different data sizes
When to use Anchor vs Grid ref:
- Use
<Anchor>: When you want to change the layout flow permanently (e.g., switching from top section to side panel) - Use
<Grid ref="">: When you want to place independent content (e.g., logo, side notes) without affecting the main flow
Merge
Merges a range of cells into a single cell.
Syntax
<Merge range="StartCell:EndCell" />
Attributes
range (required)
- Type: String
- Description: Cell range in A1 notation
- Format:
StartCell:EndCell(e.g.,A1:C1,B2:D5) - Examples:
A1:C1(horizontal),A1:A3(vertical),B2:D4(rectangular)
Behavior
- Content: Only the top-left cell's content is visible
- Other cells: Content in other cells of the range is discarded
- Formulas: Can reference merged cells normally
- Formatting: Merge applies to entire range
Examples
Horizontal merge (title spanning columns):
<Grid>
| Sales Report for Q4 2024 | | | |
| Region | Q1 | Q2 | Q3 | Q4 |
</Grid>
<Merge range="A1:E1" />
Vertical merge (row headers):
<Grid>
| Category | Product A | 100 |
| | Product B | 150 |
| | Product C | 200 |
</Grid>
<Merge range="A1:A3" />
Rectangular merge:
<Grid>
| Large Merged Area | | |
| | | |
| | | |
</Grid>
<Merge range="A1:C3" />
Multiple merges:
<Grid>
| Title | | | Date | |
| Section 1 | Data | Data | Section 2 | Data |
</Grid>
<Merge range="A1:C1" /> <!-- Title -->
<Merge range="D1:E1" /> <!-- Date -->
Complex Example: Invoice Header
<Grid>
| Company Name | | | Invoice #12345 |
| 123 Main Street | | | Date: 2024-11-03 |
| City, State 12345 | | | Due: 2024-12-03 |
</Grid>
<Merge range="A1:C1" /> <!-- Company name -->
<Merge range="A2:C2" /> <!-- Address line 1 -->
<Merge range="A3:C3" /> <!-- Address line 2 -->
Best Practices
- Use for headers: Merge cells for titles and section headers
- Preserve alignment: Consider how merged cells affect layout
- Document merges: Add comments for complex merge patterns
- Test formulas: Ensure formulas work with merged cells
Common Patterns
Report title:
<Grid>
| Annual Sales Report | | | |
</Grid>
<Merge range="A1:D1" />
Section headers:
<Grid>
| Q1 Results | | |
| Jan | Feb | Mar |
</Grid>
<Merge range="A1:C1" />
Grouped data:
<Grid>
| Department | | Employee | Hours |
| Engineering | | John | 40 |
| | | Jane | 38 |
| | | Bob | 42 |
</Grid>
<Merge range="A2:B4" /> <!-- Department spans 3 rows -->
Summary
Core tags provide the foundation for GXL templates:
<Book>: Root element containing all sheets<Sheet>: Individual worksheets with unique names<Grid>: Pipe-delimited cell content<Anchor>: Position content at specific cells<Merge>: Combine cells into single merged cell
Next Steps
- Control Structures - Learn about loops and conditionals
- Expressions - Dynamic value interpolation
- Examples - See complete templates using core tags
Control Structures
Status: Partially implemented (v1.0)
Control structures enable dynamic template behavior based on data.
For Loop (Implemented ✅)
Iterates over array data, expanding rows downward for each element.
Syntax
<For each="variableName in dataPath">
<!-- content repeated for each element -->
</For>
Attributes
each (required)
- Type: String
- Format:
<variable> in <path> - Description: Defines the loop variable and data source
Components:
<variable>: Local variable name for the current iteration<path>: Dot-notation path to array in data context
How It Works
- Parser evaluates
<path>to get an array from data context - For each element in array:
- Creates local scope with
<variable>bound to current element - Renders content within
<For>tags - Advances cursor downward by number of rows generated
- Creates local scope with
- After loop completes, cursor is positioned after all generated content
Basic Example
Template:
<Grid>
| Name | Email |
</Grid>
<For each="user in users">
<Grid>
| {{ user.name }} | {{ user.email }} |
</Grid>
</For>
Data:
{
"users": [
{"name": "Alice", "email": "alice@example.com"},
{"name": "Bob", "email": "bob@example.com"},
{"name": "Charlie", "email": "charlie@example.com"}
]
}
Output:
A1: Name | B1: Email
A2: Alice | B2: alice@example.com
A3: Bob | B3: bob@example.com
A4: Charlie | B4: charlie@example.com
Loop Variables
Built-in variables available within <For> loops.
loop.index
- Type: Integer
- Description: Zero-based iteration index
- Range: 0 to (array.length - 1)
loop.number
- Type: Integer
- Description: One-based iteration number
- Range: 1 to array.length
loop.startRow
- Type: Integer
- Description: Starting row number for current iteration (absolute)
- Use case: Building cell references in formulas
loop.endRow
- Type: Integer
- Description: Ending row number for current iteration (absolute)
- Use case: Creating dynamic ranges
Example with Loop Variables
<Grid>
| # | Item | Quantity | Price | Total |
</Grid>
<For each="item in items">
<Grid>
| {{ loop.number }} | {{ item.name }} | {{ item.qty }} | {{ item.price }} | =C{{ loop.number + 1 }}*D{{ loop.number + 1 }} |
</Grid>
</For>
<Grid>
| | | | Total: | =SUM(E2:E{{ items.length + 1 }}) |
</Grid>
Nested Loops
Loops can be nested to handle hierarchical data structures.
Basic Nested Loop
<Grid>
| Category | Product | Price |
</Grid>
<For each="category in categories">
<Grid>
| {{ category.name }} | | |
</Grid>
<For each="product in category.products">
<Grid>
| | {{ product.name }} | {{ product.price }} |
</Grid>
</For>
</For>
Data:
{
"categories": [
{
"name": "Electronics",
"products": [
{"name": "Laptop", "price": 1200},
{"name": "Mouse", "price": 25}
]
},
{
"name": "Books",
"products": [
{"name": "Novel", "price": 15},
{"name": "Textbook", "price": 80}
]
}
]
}
Nested Loop Variables
In nested loops, each level has its own loop variable:
<For each="category in categories">
<Grid>
| Category {{ loop.number }}: {{ category.name }} |
</Grid>
<For each="item in category.items">
<Grid>
| Item {{ loop.number }} in category {{ loop.parent.number }}: {{ item.name }} |
</Grid>
</For>
</For>
Note: loop.parent access is planned for future versions.
Advanced For Loop Patterns
With Formulas
<Grid>
| Product | Q1 | Q2 | Q3 | Q4 | Total |
</Grid>
<For each="product in products">
<Grid>
| {{ product.name }} | {{ product.q1 }} | {{ product.q2 }} | {{ product.q3 }} | {{ product.q4 }} | =SUM(B{{ loop.number + 1 }}:E{{ loop.number + 1 }}) |
</Grid>
</For>
<Grid>
| Total | =SUM(B2:B{{ products.length + 1 }}) | =SUM(C2:C{{ products.length + 1 }}) | =SUM(D2:D{{ products.length + 1 }}) | =SUM(E2:E{{ products.length + 1 }}) | =SUM(F2:F{{ products.length + 1 }}) |
</Grid>
With Conditional Content
<For each="order in orders">
<Grid>
| Order #{{ order.id }} | {{ order.date }} | ${{ order.total }} | {{ order.status }} |
</Grid>
<!-- Future: Will be replaced with <If> when implemented -->
<!-- For now, use empty expressions or conditional data preparation -->
</For>
With Multi-Row Content
<For each="invoice in invoices">
<!-- Invoice header -->
<Grid>
| Invoice #{{ invoice.number }} | | Date: {{ invoice.date }} |
</Grid>
<Merge range="A{{ loop.startRow }}:B{{ loop.startRow }}" />
<!-- Invoice items -->
<For each="item in invoice.items">
<Grid>
| {{ item.name }} | {{ item.qty }} | ${{ item.price }} |
</Grid>
</For>
<!-- Invoice total -->
<Grid>
| | Total: | ${{ invoice.total }} |
</Grid>
<!-- Spacer row -->
<Grid>
| | | |
</Grid>
</For>
If / Else (Conditional Rendering)
Conditionally render content based on boolean expressions.
Status: Planned for v1.1 (not yet implemented in goxcel v1.0)
Syntax
<If cond="expression">
<!-- rendered if expression is truthy -->
</If>
With Else:
<If cond="expression">
<!-- rendered if truthy -->
<Else>
<!-- rendered if falsy -->
</Else>
</If>
Attributes
cond (required)
- Type: String (expression)
- Description: Expression evaluated to boolean
Truthy values:
- Non-zero numbers:
1,-5,3.14 - Non-empty strings:
"hello","false" - Boolean true:
true - Non-empty arrays:
[1, 2] - Non-null objects:
{"key": "value"}
Falsy values:
- Zero:
0,0.0 - Empty string:
"" - Boolean false:
false - Null:
null - Undefined:
undefined - Empty array:
[]
Examples
Simple conditional:
<If cond="showHeader">
<Grid>
| Company Name | Report Date |
</Grid>
</If>
<Grid>
| Data | Data |
</Grid>
With Else:
<If cond="isPremium">
<Grid>
| Premium Customer | Discount: 20% |
</Grid>
<Else>
<Grid>
| Standard Customer | Discount: 5% |
</Grid>
</Else>
</If>
Comparison operators:
<If cond="total > 1000">
<Grid>
| Discount Applied | 10% |
</Grid>
</If>
<If cond="status == 'paid'">
<Grid>
| Payment Status | PAID |
</Grid>
<Else>
<Grid>
| Payment Status | PENDING |
</Grid>
</Else>
</If>
With nested paths:
<If cond="user.subscription.isPremium">
<Grid>
| Welcome, Premium Member! |
</Grid>
</If>
Combining with For Loops
<For each="item in items">
<If cond="item.inStock">
<Grid>
| {{ item.name }} | In Stock | ${{ item.price }} |
</Grid>
<Else>
<Grid>
| {{ item.name }} | Out of Stock | - |
</Grid>
</Else>
</If>
</For>
Nested Conditionals
<If cond="hasData">
<If cond="dataType == 'sales'">
<Grid>
| Sales Report |
</Grid>
<Else>
<Grid>
| Other Report |
</Grid>
</Else>
</If>
<Else>
<Grid>
| No Data Available |
</Grid>
</Else>
</If>
Switch / Case (Future)
Status: Under consideration for v2.0+
Multiple conditional branches based on a value:
<Switch value="status">
<Case match="pending">
<Grid>| Status: Pending |</Grid>
</Case>
<Case match="approved">
<Grid>| Status: Approved |</Grid>
</Case>
<Case match="rejected">
<Grid>| Status: Rejected |</Grid>
</Case>
<Default>
<Grid>| Status: Unknown |</Grid>
</Default>
</Switch>
While Loop (Future)
Status: Under consideration (low priority)
Conditional looping:
<While cond="index < maxRows">
<Grid>
| Row {{ index }} |
</Grid>
<!-- Note: Need mechanism to update 'index' -->
</While>
Challenges:
- Requires mutable state
- Risk of infinite loops
- Complex to implement safely
Alternative: Pre-process data to create finite arrays, then use <For>
Best Practices
1. Keep Loops Simple
Good:
<For each="item in items">
<Grid>
| {{ item.name }} | {{ item.value }} |
</Grid>
</For>
Avoid:
<For each="item in items">
<For each="sub in item.subs">
<For each="detail in sub.details">
<!-- Too deeply nested -->
</For>
</For>
</For>
2. Use Descriptive Variable Names
Good:
<For each="employee in employees">
<For each="product in products">
<For each="transaction in transactions">
Avoid:
<For each="i in items">
<For each="x in list">
<For each="e in data">
3. Pre-calculate Complex Logic
Instead of complex conditionals in template, prepare data:
Better approach:
{
"items": [
{"name": "A", "displayPrice": "$10.00", "showDiscount": true},
{"name": "B", "displayPrice": "$20.00", "showDiscount": false}
]
}
<For each="item in items">
<Grid>
| {{ item.name }} | {{ item.displayPrice }} |
</Grid>
</For>
4. Document Complex Loops
<!--
Generate invoice sections
Each invoice contains:
- Header row with invoice number
- Item rows (nested loop)
- Total row
- Blank separator
-->
<For each="invoice in invoices">
<!-- header -->
<Grid>| Invoice #{{ invoice.number }} |</Grid>
<!-- items -->
<For each="item in invoice.items">
<Grid>| {{ item.name }} | ${{ item.price }} |</Grid>
</For>
<!-- total -->
<Grid>| Total | ${{ invoice.total }} |</Grid>
<Grid>| | |</Grid>
</For>
5. Handle Empty Arrays
Prepare data to always have valid arrays:
{
"items": [] // Empty array instead of null/undefined
}
Or use conditionals (when available):
<If cond="items.length > 0">
<For each="item in items">
<Grid>| {{ item.name }} |</Grid>
</For>
<Else>
<Grid>| No items found |</Grid>
</Else>
</If>
Error Handling
Invalid Data Path
If dataPath doesn't exist or isn't an array:
- Behavior: Loop is skipped (zero iterations)
- Warning: Implementation should log warning
Null/Undefined Values
If data path resolves to null or undefined:
- Behavior: Treated as empty array (zero iterations)
- No error: Graceful degradation
Non-Array Values
If data path resolves to non-array value:
- Behavior: Implementation-dependent
- goxcel v1.0: Treats as single-element array
- Future: May throw error or skip
Performance Considerations
Large Datasets
For loops generate rows during rendering. Very large arrays can:
- Increase memory usage
- Slow rendering
- Create huge Excel files
Recommendations:
- Limit arrays to reasonable sizes (< 10,000 rows)
- Use pagination for large datasets
- Consider streaming mode (future feature)
Nested Loops
Each nesting level multiplies row count:
- 100 categories × 50 products = 5,000 rows
- 10 invoices × 20 items = 200 rows
Watch for:
- Cartesian products (unintended)
- Deep nesting (>3 levels)
Related Topics
- Expressions - Variable interpolation syntax
- Data Context - How data is structured and accessed
- Rendering Semantics - How loops affect rendering
Implementation Status
| Feature | Status | Version |
|---|---|---|
<For> loops | ✅ Implemented | v1.0 |
Loop variables (loop.index, loop.number) | ✅ Implemented | v1.0 |
| Nested loops | ✅ Implemented | v1.0 |
| Array iteration | ✅ Implemented | v1.0 |
| Map/object iteration | ✅ Implemented | v1.0 |
loop.startRow, loop.endRow | ⏳ Planned | v1.1 |
<If> / <Else> | ⏳ Planned | v1.1 |
<Switch> / <Case> | 💭 Consideration | v2.0+ |
Legend: ✅ Implemented | ⏳ Planned | 💭 Under consideration
| <While> | 💭 Under consideration | TBD |
Expressions
Expressions enable dynamic content in GXL templates through value interpolation and evaluation.
Value Interpolation
Syntax
{{ expression }}
{{ expression:type }}
Double curly braces {{ }} evaluate expressions and insert the result into the document.
Where Interpolation Works
- Inside Grid cells
- In tag attributes (planned)
- Within Excel formulas
Type Hints (v1.0)
Explicit type specification using colon syntax:
{{ .quantity:int }} # Integer → Excel number
{{ .price:float }} # Float → Excel number
{{ .amount:number }} # Generic number
{{ .active:bool }} # Boolean → TRUE/FALSE
{{ .date:date }} # ISO date → Excel date
{{ .name:string }} # Force string type
Auto-Inference (default):
Without type hints, goxcel automatically detects:
- Starts with
=→ Formula true/false(case-insensitive) → Boolean- Numeric pattern (int/float) → Number
- ISO 8601 date (YYYY-MM-DD) → Date
- Everything else → String
Literal Values:
{{ "literal text" }} # String literal
{{ 123 }} # Number literal
{{ true }} # Boolean literal
Basic Interpolation
Simple Variables
<Grid>
| {{ title }} | {{ date }} | {{ amount }} |
</Grid>
Data:
{
"title": "Sales Report",
"date": "2024-11-03",
"amount": 1500.00
}
Result:
A1: Sales Report | B1: 2024-11-03 | C1: 1500.00
Dot Notation (Object Access)
Access nested object properties using dot notation.
Syntax
{{ object.property }}
{{ object.nested.property }}
Examples
<Grid>
| {{ user.name }} | {{ user.email }} | {{ user.profile.age }} |
</Grid>
Data:
{
"user": {
"name": "Alice",
"email": "alice@example.com",
"profile": {
"age": 30,
"city": "New York"
}
}
}
Result:
A1: Alice | B1: alice@example.com | C1: 30
Array Access
Index Notation
{{ array[0] }}
{{ array[1].property }}
Examples
<Grid>
| {{ items[0].name }} | {{ items[0].price }} |
| {{ items[1].name }} | {{ items[1].price }} |
| {{ items[2].name }} | {{ items[2].price }} |
</Grid>
Data:
{
"items": [
{"name": "Apple", "price": 1.50},
{"name": "Banana", "price": 0.75},
{"name": "Cherry", "price": 3.00}
]
}
Cell Type Handling
GXL automatically detects and sets appropriate Excel cell types for proper data representation.
Automatic Type Inference
GXL automatically infers cell types based on value patterns:
<Grid>
| Type | Example | Result |
| Number | {{ 42 }} | Excel numeric cell |
| Float | {{ 3.14159 }} | Excel numeric cell |
| Boolean | {{ true }} | Excel boolean cell (TRUE) |
| Formula | =SUM(A1:A10) | Excel formula cell |
| Date | {{ "2025-11-03" }} | Excel date cell |
| String | {{ "Hello" }} | Excel text cell |
</Grid>
Inference Rules:
- Values starting with
=→ Formula type trueorfalse(case-insensitive) → Boolean type- Numeric patterns (
123,45.67,-10.5) → Number type - ISO date format (
YYYY-MM-DD) → Date type - Everything else → String type
Explicit Type Hints
Use type hints to explicitly control cell types:
<Grid>
| Description | Auto-detected | Type Hint |
| Integer | {{ .quantity }} | {{ .quantity:int }} |
| Float | {{ .price }} | {{ .price:float }} |
| Boolean | {{ .enabled }} | {{ .enabled:bool }} |
| Date | {{ .timestamp }} | {{ .timestamp:date }} |
| Force String | {{ .zipCode }} | {{ .zipCode:string }} |
</Grid>
Data:
{
"quantity": 42,
"price": 1500.50,
"enabled": false,
"timestamp": "2025-11-03T15:30:00",
"zipCode": "00123"
}
Why use type hints?
- Force numeric values to be treated as strings (e.g., zip codes, IDs)
- Ensure proper type when auto-detection might be ambiguous
- Control how data is stored in Excel for formulas and calculations
Literal Values
You can also use literal values with type hints:
<Grid>
| String Literal | {{ "Hello World" }} |
| Number Literal | {{ 42 }} |
| Boolean Literal | {{ true }} |
| With Type Hint | {{ "123":string }} |
</Grid>
Mixed Content
When multiple expressions appear in a single cell, the result is always a string:
<Grid>
| Description | Type |
| Price: {{ .price }} yen | String (mixed content) |
| Total: {{ .quantity }} items | String (mixed content) |
| {{ .amount }} | Number (single expression) |
</Grid>
Array Length
<Grid>
| Total Items | {{ items.length }} |
</Grid>
Attribute Interpolation
Expressions can be used in tag attributes.
Chart with Dynamic Range
<Chart
ref="A10"
type="column"
dataRange="A1:C{{ rowCount }}"
title="Sales for {{ year }}"
/>
Merge with Dynamic Range
<Merge range="A1:{{ lastColumn }}1" />
Anchor with Computed Position
<Anchor ref="{{ startColumn }}{{ startRow }}" />
Formula Interpolation
Use expressions within Excel formulas.
Dynamic Cell References
<Grid>
| Total | =SUM(B2:B{{ rowCount + 1 }}) |
</Grid>
Dynamic Ranges
<Grid>
| Average | =AVERAGE(A{{ startRow }}:A{{ endRow }}) |
| Maximum | =MAX(A{{ startRow }}:A{{ endRow }}) |
| Minimum | =MIN(A{{ startRow }}:A{{ endRow }}) |
</Grid>
With Loop Variables
<For each="item in items">
<Grid>
| {{ item.name }} | {{ item.qty }} | {{ item.price }} | =B{{ loop.number + 1 }}*C{{ loop.number + 1 }} |
</Grid>
</For>
Type Coercion
Expression results are automatically converted to appropriate types.
Type Inference
| Expression Result | Excel Cell Type | Example |
|---|---|---|
| Number | Number | 123, 45.67, -10 |
| String | Text | "Hello", "ABC123" |
| Boolean | Boolean | true, false |
| Date (ISO 8601) | Date | "2024-11-03" |
| Null/Undefined | Empty | null |
Examples
<Grid>
| {{ 100 }} | {{ "Text" }} | {{ true }} | {{ "2024-11-03" }} |
</Grid>
Result:
- A1: Number 100
- B1: Text "Text"
- C1: Boolean TRUE
- D1: Date (formatted based on Excel settings)
String Concatenation
Using Plus Operator (Planned)
<Grid>
| {{ firstName + " " + lastName }} |
</Grid>
Template Literals (Future)
<Grid>
| {{ `Full name: ${firstName} ${lastName}` }} |
</Grid>
Current Workaround
Pre-concatenate in data:
{
"fullName": "Alice Smith",
"firstName": "Alice",
"lastName": "Smith"
}
<Grid>
| {{ fullName }} |
</Grid>
Arithmetic Operators (Planned)
Status: Planned for v1.2
Operators
| Operator | Description | Example |
|---|---|---|
+ | Addition | {{ a + b }} |
- | Subtraction | {{ a - b }} |
* | Multiplication | {{ a * b }} |
/ | Division | {{ a / b }} |
% | Modulo | {{ a % b }} |
^ | Exponentiation | {{ a ^ b }} |
Examples
<Grid>
| Subtotal | {{ subtotal }} |
| Tax (10%) | {{ subtotal * 0.1 }} |
| Total | {{ subtotal + (subtotal * 0.1) }} |
</Grid>
Precedence
Standard mathematical precedence:
- Parentheses
() - Exponentiation
^ - Multiplication
*, Division/, Modulo% - Addition
+, Subtraction-
Comparison Operators (Planned)
Status: Planned for v1.1 (for use with <If>)
Operators
| Operator | Description | Example |
|---|---|---|
== | Equal | {{ a == b }} |
!= | Not equal | {{ a != b }} |
< | Less than | {{ a < b }} |
> | Greater than | {{ a > b }} |
<= | Less than or equal | {{ a <= b }} |
>= | Greater than or equal | {{ a >= b }} |
Examples with If (Future)
<If cond="price > 100">
<Grid>| Premium Product |</Grid>
</If>
<If cond="status == 'active'">
<Grid>| Active |</Grid>
<Else>
<Grid>| Inactive |</Grid>
</Else>
</If>
Logical Operators (Planned)
Status: Planned for v1.1
Operators
| Operator | Description | Example |
|---|---|---|
&& | Logical AND | {{ a && b }} |
|| | Logical OR | {{ a || b }} |
! | Logical NOT | {{ !a }} |
Examples
<If cond="isActive && isPremium">
<Grid>| Premium Active User |</Grid>
</If>
<If cond="outOfStock || discontinued">
<Grid>| Not Available |</Grid>
</If>
<If cond="!isDeleted">
<Grid>| {{ item.name }} |</Grid>
</If>
Built-in Functions (Planned)
Status: Planned for v1.2+
String Functions
| Function | Description | Example |
|---|---|---|
len(str) | String length | {{ len(name) }} |
upper(str) | Uppercase | {{ upper(name) }} |
lower(str) | Lowercase | {{ lower(name) }} |
trim(str) | Trim whitespace | {{ trim(input) }} |
substr(str, start, len) | Substring | {{ substr(text, 0, 10) }} |
Array Functions
| Function | Description | Example |
|---|---|---|
len(array) | Array length | {{ len(items) }} |
sum(array) | Sum of numbers | {{ sum(prices) }} |
avg(array) | Average | {{ avg(scores) }} |
min(array) | Minimum value | {{ min(values) }} |
max(array) | Maximum value | {{ max(values) }} |
Math Functions
| Function | Description | Example |
|---|---|---|
round(num) | Round to integer | {{ round(3.7) }} |
round(num, decimals) | Round to decimals | {{ round(3.14159, 2) }} |
floor(num) | Round down | {{ floor(3.7) }} |
ceil(num) | Round up | {{ ceil(3.2) }} |
abs(num) | Absolute value | {{ abs(-5) }} |
Date Functions
| Function | Description | Example |
|---|---|---|
now() | Current date/time | {{ now() }} |
today() | Current date | {{ today() }} |
year(date) | Extract year | {{ year(orderDate) }} |
month(date) | Extract month | {{ month(orderDate) }} |
day(date) | Extract day | {{ day(orderDate) }} |
Examples
<Grid>
| Product Name | {{ upper(product.name) }} |
| Total Items | {{ len(items) }} |
| Average Price | {{ round(avg(prices), 2) }} |
| Report Date | {{ today() }} |
</Grid>
Conditional Expressions (Ternary)
Status: Planned for v1.2
Syntax
{{ condition ? valueIfTrue : valueIfFalse }}
Examples
<Grid>
| Status | {{ isActive ? "Active" : "Inactive" }} |
| Price | {{ inStock ? price : "N/A" }} |
| Discount | {{ isPremium ? "20%" : "5%" }} |
</Grid>
Null Coalescing
Status: Planned for v1.2
Syntax
{{ value ?? defaultValue }}
Examples
<Grid>
| Name | {{ user.name ?? "Anonymous" }} |
| Email | {{ user.email ?? "No email provided" }} |
| Phone | {{ user.phone ?? "N/A" }} |
</Grid>
Escaping Special Characters
Escaping Braces
To include literal {{ or }} in output:
<Grid>
| Template syntax uses {{ "{{" }} and {{ "}}" }} |
</Grid>
Result:
A1: Template syntax uses {{ and }}
Escaping Pipes
To include literal | in grid cells:
<Grid>
| Column A {{ "|" }} Column B |
</Grid>
Or use expression:
<Grid>
| {{ "Value | with | pipes" }} |
</Grid>
Error Handling
Undefined Variables
If variable doesn't exist in data context:
- Behavior: Renders empty string
- Warning: Implementation should log warning
- No error: Graceful degradation
Invalid Paths
<Grid>
| {{ user.nonexistent.property }} |
</Grid>
If path is invalid:
- Behavior: Empty cell
- Warning: Logged if possible
Type Errors
<Grid>
| {{ "string" + 123 }} | <!-- Type mismatch -->
</Grid>
Behavior depends on implementation:
- May coerce to string:
"string123" - May return empty
- May throw error
Best Practices
1. Keep Expressions Simple
Good:
<Grid>
| {{ user.name }} | {{ user.email }} |
</Grid>
Avoid:
<Grid>
| {{ user.profile.personal.names.first + " " + user.profile.personal.names.last }} |
</Grid>
Better: Pre-compute in data:
{
"user": {
"fullName": "Alice Smith",
"name": "Alice",
"email": "alice@example.com"
}
}
2. Use Descriptive Data Keys
Good:
{
"invoiceNumber": "INV-2024-001",
"customerName": "Acme Corp",
"orderTotal": 5000.00
}
Avoid:
{
"n": "INV-2024-001",
"c": "Acme Corp",
"t": 5000.00
}
3. Handle Missing Data
Prepare data to avoid undefined values:
{
"user": {
"name": "Alice",
"email": "alice@example.com",
"phone": "" // Empty string instead of null/undefined
}
}
4. Pre-format Complex Values
Instead of:
<Grid>
| {{ price * 1.1 }} | <!-- Calculate in template -->
</Grid>
Do:
{
"price": 100,
"priceWithTax": 110
}
<Grid>
| {{ priceWithTax }} |
</Grid>
5. Document Expected Data Structure
<!--
Required data structure:
{
"company": {
"name": string,
"address": string
},
"invoice": {
"number": string,
"date": string (ISO 8601),
"items": [
{
"name": string,
"qty": number,
"price": number
}
],
"total": number
}
}
-->
<Book>
<Sheet name="Invoice">
<Grid>
| {{ company.name }} |
</Grid>
...
</Sheet>
</Book>
Expression Evaluation Order
- Parse template: Extract expressions
- Evaluate expressions: Resolve against data context
- Type conversion: Convert to appropriate Excel types
- Cell generation: Insert values into cells
Performance Considerations
Expression Complexity
Simple expressions are fast:
{{ user.name }} <!-- Fast: direct property access -->
Complex expressions may be slower:
{{ sum(filter(items, item => item.active).map(item => item.price)) }} <!-- Slower -->
Recommendation: Pre-compute complex values in data preparation step.
Large Arrays
Accessing arrays in expressions:
{{ items[999].name }} <!-- Fine for small arrays -->
For very large arrays (> 10,000 elements):
- Pre-filter data before passing to template
- Avoid iteration in expressions
Related Topics
- Data Context - How data is structured and accessed
- Control Structures - Using expressions in loops and conditionals
- Core Tags - Where expressions can be used
Implementation Status
| Feature | Status | Version |
|---|---|---|
Basic interpolation {{ var }} | ✅ Implemented | v1.0 |
Dot notation {{ obj.prop }} | ✅ Implemented | v1.0 |
Array access {{ arr[0] }} | ✅ Implemented | v1.0 |
| Attribute interpolation | ✅ Implemented | v1.0 |
| Formula interpolation | ✅ Implemented | v1.0 |
| Arithmetic operators | ⏳ Planned | v1.2 |
| Comparison operators | ⏳ Planned | v1.1 |
| Logical operators | ⏳ Planned | v1.1 |
| Built-in functions | ⏳ Planned | v1.2 |
| Ternary operator | ⏳ Planned | v1.2 |
| Null coalescing | ⏳ Planned | v1.2 |
Components
Components are special tags that insert rich content like images, shapes, charts, and pivot tables into worksheets.
Current Status: Component declarations are implemented in GXL v0.1 and goxcel v1.0 creates placeholders for these components. Full rendering is planned for future versions.
Image
Inserts an image at a specific cell location.
Syntax
<Image
ref="CellReference"
src="path/to/image.png"
width="120"
height="60"
/>
Attributes
ref (required)
- Type: String (cell reference)
- Description: Top-left anchor cell for the image
- Format: A1 notation (e.g.,
B3,AA10)
src (required)
- Type: String
- Description: Path to image file or resource identifier
- Formats:
- Relative path:
assets/logo.png - Absolute path:
/path/to/image.png - URL (future):
https://example.com/logo.png - Resource key (future):
@logo
- Relative path:
width (optional)
- Type: Integer
- Description: Image width in pixels
- Default: Original image width
height (optional)
- Type: Integer
- Description: Image height in pixels
- Default: Original image height
Supported Formats
v1.0 (Placeholder):
- Any format (not validated)
Planned (v1.2):
- PNG (
.png) - JPEG (
.jpg,.jpeg) - GIF (
.gif) - BMP (
.bmp) - SVG (
.svg) - via rasterization
Examples
Basic image:
<Image ref="A1" src="company-logo.png" />
With dimensions:
<Image
ref="B3"
src="assets/product-photo.jpg"
width="200"
height="150"
/>
Multiple images:
<Sheet name="Products">
<Grid>
| Product | Image | Description |
</Grid>
<For each="product in products">
<Grid>
| {{ product.name }} | | {{ product.description }} |
</Grid>
<Image
ref="B{{ loop.number + 1 }}"
src="{{ product.imagePath }}"
width="100"
height="100"
/>
</For>
</Sheet>
Behavior
v1.0: Creates a placeholder text cell with image path v1.2: Embeds actual image into workbook
Best Practices
- Use relative paths for portability
- Specify dimensions to control layout
- Optimize images before embedding (reduce file size)
- Test path resolution with different working directories
Shape
Inserts a shape (rectangle, arrow, etc.) with optional text.
Syntax
<Shape
ref="CellReference"
kind="rectangle"
text="Label"
width="150"
height="50"
/>
Attributes
ref (required)
- Type: String (cell reference)
- Description: Top-left anchor cell
kind (required)
- Type: String
- Description: Shape type
- Values:
rectangle- Rectanglerounded- Rounded rectangleellipse- Circle/ellipsearrow- Arrowline- Straight linestar- Star shapetriangle- Trianglediamond- Diamond
text (optional)
- Type: String
- Description: Text content inside shape
- Default: Empty
width (optional)
- Type: Integer
- Description: Shape width in pixels
- Default: 100
height (optional)
- Type: Integer
- Description: Shape height in pixels
- Default: 50
style (optional)
- Type: String
- Description: Named style preset
- Examples:
banner,callout,warning,success
Examples
Simple shape:
<Shape ref="D3" kind="rectangle" text="Important" />
Callout banner:
<Shape
ref="A1"
kind="rounded"
text="URGENT: Read This"
width="200"
height="60"
style="warning"
/>
Workflow arrows:
<Shape ref="B5" kind="rectangle" text="Step 1" width="120" height="40" />
<Shape ref="D5" kind="arrow" width="40" height="10" />
<Shape ref="F5" kind="rectangle" text="Step 2" width="120" height="40" />
<Shape ref="H5" kind="arrow" width="40" height="10" />
<Shape ref="J5" kind="rectangle" text="Step 3" width="120" height="40" />
Behavior
v1.0: Creates placeholder text cell v1.2: Renders actual shape with formatting
Chart
Creates a chart visualization from data ranges.
Syntax
<Chart
ref="CellReference"
type="column"
dataRange="A1:C10"
title="Chart Title"
width="500"
height="300"
/>
Attributes
ref (required)
- Type: String (cell reference)
- Description: Top-left anchor cell for chart
type (required)
- Type: String
- Description: Chart type
- Values:
column- Vertical bar chartbar- Horizontal bar chartline- Line chartpie- Pie chartscatter- Scatter plotarea- Area chartdoughnut- Doughnut chartradar- Radar chartcombo- Combination chart
dataRange (required)
- Type: String
- Description: Source data range in A1 notation
- Format:
StartCell:EndCell - Supports interpolation:
A1:C{{ rowCount }}
title (optional)
- Type: String
- Description: Chart title
- Default: No title
width (optional)
- Type: Integer
- Description: Chart width in pixels
- Default: 480
height (optional)
- Type: Integer
- Description: Chart height in pixels
- Default: 288
Advanced Attributes (Planned v1.2+)
<Chart
ref="A10"
type="column"
dataRange="A1:C10"
title="Sales by Region"
xAxisTitle="Region"
yAxisTitle="Revenue ($)"
legend="bottom"
colors="#4CAF50,#2196F3,#FF9800"
stacked="true"
/>
Examples
Basic column chart:
<Grid>
| Month | Revenue | Target |
| Jan | 10000 | 12000 |
| Feb | 15000 | 12000 |
| Mar | 13000 | 12000 |
</Grid>
<Chart
ref="E1"
type="column"
dataRange="A1:C4"
title="Monthly Performance"
/>
Dynamic data range:
<Grid>
| Category | Sales |
</Grid>
<For each="item in items">
<Grid>
| {{ item.category }} | {{ item.sales }} |
</Grid>
</For>
<Chart
ref="D1"
type="pie"
dataRange="A1:B{{ items.length + 1 }}"
title="Sales by Category"
width="400"
height="400"
/>
Multiple charts:
<Sheet name="Dashboard">
<!-- Data -->
<Grid>
| Product | Q1 | Q2 | Q3 | Q4 |
| Product A | 100 | 120 | 110 | 130 |
| Product B | 80 | 90 | 95 | 100 |
</Grid>
<!-- Chart 1: Column -->
<Chart
ref="A5"
type="column"
dataRange="A1:E3"
title="Quarterly Sales"
/>
<!-- Chart 2: Line -->
<Chart
ref="A20"
type="line"
dataRange="A1:E3"
title="Sales Trend"
/>
</Sheet>
Behavior
v1.0: Creates placeholder text cell with chart description v1.2: Generates actual Excel chart object
Pivot Table
Creates a pivot table from source data.
Syntax
<Pivot
ref="CellReference"
sourceRange="A1:D100"
rows="Category"
columns="Month"
values="SUM:Sales"
/>
Attributes
ref (required)
- Type: String (cell reference)
- Description: Top-left cell for pivot table
sourceRange (required)
- Type: String
- Description: Source data range in A1 notation
- Must include: Header row with field names
rows (optional)
- Type: String (comma-separated)
- Description: Fields to use as row labels
- Example:
"Category,Product"
columns (optional)
- Type: String (comma-separated)
- Description: Fields to use as column labels
- Example:
"Year,Month"
values (required)
- Type: String (comma-separated)
- Description: Aggregate functions and fields
- Format:
FUNCTION:FieldName - Functions:
SUM,COUNT,AVERAGE,MAX,MIN,PRODUCT,STDDEV,VAR - Examples:
"SUM:Sales","COUNT:Orders,SUM:Revenue"
filters (optional)
- Type: String (comma-separated)
- Description: Fields to use as filters
- Example:
"Region,Department"
Examples
Basic pivot table:
<Grid>
| Product | Category | Region | Sales |
| Widget A | Electronics | North | 1000 |
| Widget B | Electronics | South | 1500 |
| Gadget A | Toys | North | 800 |
| Gadget B | Toys | South | 1200 |
</Grid>
<Pivot
ref="F1"
sourceRange="A1:D5"
rows="Category"
columns="Region"
values="SUM:Sales"
/>
Multiple aggregations:
<Pivot
ref="A20"
sourceRange="A1:E1000"
rows="Product,Category"
columns="Year"
values="SUM:Revenue,COUNT:Orders,AVERAGE:Price"
filters="Region,SalesRep"
/>
Dynamic source range:
<For each="row in data">
<Grid>
| {{ row.product }} | {{ row.category }} | {{ row.sales }} |
</Grid>
</For>
<Pivot
ref="E1"
sourceRange="A1:C{{ data.length + 1 }}"
rows="Category"
values="SUM:Sales"
/>
Behavior
v1.0: Creates placeholder text cell v2.0: Generates actual Excel pivot table
Component Positioning
Absolute Positioning
Components use absolute cell references:
<Image ref="B2" src="logo.png" />
<Chart ref="F2" type="column" dataRange="A1:C10" />
Relative to Cursor
Components don't affect cursor position. They overlay cells without moving subsequent content:
<Grid>
| Header |
</Grid>
<!-- Image overlays B1:C3, but doesn't move cursor -->
<Image ref="B1" src="image.png" width="200" height="100" />
<!-- Grid continues at A2 -->
<Grid>
| Data |
</Grid>
Avoiding Overlaps
Plan component positions to avoid overlapping:
<!-- Data occupies A1:D10 -->
<Grid>
| A | B | C | D |
</Grid>
<For each="row in rows">
<Grid>
| {{ row.a }} | {{ row.b }} | {{ row.c }} | {{ row.d }} |
</Grid>
</For>
<!-- Place chart starting at F1 (clear of data) -->
<Chart ref="F1" type="column" dataRange="A1:D10" />
Future Enhancements
Conditional Components (v1.2+)
<If cond="includeChart">
<Chart ref="E1" type="column" dataRange="A1:C10" />
</If>
Component Loops (v1.2+)
<For each="dataset in datasets">
<Chart
ref="A{{ loop.index * 20 + 1 }}"
type="line"
dataRange="{{ dataset.range }}"
title="{{ dataset.title }}"
/>
</For>
Component Styling (v1.3+)
<Chart
ref="A1"
type="column"
dataRange="A1:C10"
colors="#FF5733,#33FF57,#3357FF"
borderColor="#000000"
borderWidth="2"
backgroundColor="#FFFFFF"
/>
Interactive Components (v2.0+)
<Button
ref="A1"
text="Click Me"
action="macro:refreshData"
/>
<Slider
ref="C1"
min="0"
max="100"
value="50"
linkedCell="D1"
/>
Best Practices
1. Plan Layout First
Sketch the desired layout before writing template:
+--------+--------+--------+
| Data | Data | Chart |
| | | |
+--------+--------+--------+
2. Use Descriptive Comments
<!-- Company logo in top-right -->
<Image ref="F1" src="logo.png" width="120" height="60" />
<!-- Sales chart below data table -->
<Chart ref="A15" type="column" dataRange="A1:C12" title="Monthly Sales" />
3. Test with Real Data
Verify component positions with actual data sizes:
- What if array has 100 items instead of 10?
- Does chart still fit on one page?
4. Optimize Resource Files
- Images: Use compressed formats (PNG, JPEG)
- File size: Keep images under 500KB when possible
- Dimensions: Resize images before embedding
5. Version Control Resources
Store images and other resources in version control alongside templates:
project/
├── templates/
│ └── report.gxl
└── assets/
├── logo.png
├── icon.png
└── chart-background.png
Implementation Status
| Component | v1.0 (Placeholder) | v1.2 (Rendering) | v2.0 (Advanced) |
|---|---|---|---|
| Image | ✅ | ⏳ | - |
| Shape | ✅ | ⏳ | - |
| Chart | ✅ | ⏳ | - |
| Pivot Table | ✅ | - | ⏳ |
| Button | - | - | 💭 |
| Slider | - | - | 💭 |
Legend:
- ✅ Implemented
- ⏳ Planned
- 💭 Under consideration
- - Not planned
Related Topics
- Core Tags - Positioning with Anchor
- Control Structures - Dynamic component placement
- Expressions - Dynamic component attributes
Styling
Status: Partially implemented (v1.0)
Styling system for cell formatting using markdown syntax and type hints.
Implemented Features (v1.0)
Markdown-Style Formatting
Inline text formatting using markdown syntax:
<Grid>
| **Bold Text** | _Italic Text_ | Normal Text |
</Grid>
Supported:
**text**: Bold_text_: Italic
Parsing: Automatic detection and style application during rendering.
Cell Type Hints
Explicit type specification for cells:
<Grid>
| {{ .quantity:int }} | {{ .price:float }} | {{ .active:bool }} |
</Grid>
Supported Types:
:int,:float,:number→ Number:bool,:boolean→ Boolean:date→ Date (ISO 8601):string→ String (explicit)
Auto-inference: Without type hints, goxcel automatically infers types from values.
Planned Features (v1.1+)
Style Tag
<Style selector="A1:C1" bold fillColor="#4CAF50" color="#FFFFFF" />
Attributes (Future)
Font: fontFamily, fontSize, bold, italic, underline
Color: color (text), fillColor (background)
Alignment: hAlign (left/center/right), vAlign (top/middle/bottom)
Borders: border, borderColor
Implementation Status
| Feature | v1.0 | v1.1 | v1.2 |
|---|---|---|---|
| Markdown Bold/Italic | ✅ | ✅ | ✅ |
| Type Hints | ✅ | ✅ | ✅ |
| Auto Type Inference | ✅ | ✅ | ✅ |
| Style Tag | ❌ | 🔄 | ✅ |
| Named Styles | ❌ | ❌ | 🔄 |
| Conditional Formatting | ❌ | ❌ | ❌ |
Legend: ✅ Implemented | 🔄 Planned | ❌ Not Planned
Related
Data Context
The data context is the JSON or YAML data structure passed to a GXL template during rendering. It provides the values for all expressions and control structures.
Overview
What is Data Context?
The data context is a structured data object (JSON or YAML) that contains:
- Variables: Simple values (strings, numbers, booleans)
- Objects: Nested structures with properties
- Arrays: Lists of items for iteration
How It Works
- Prepare data: Create JSON or YAML data structure
- Pass to renderer: Provide data when rendering template
- Access in template: Use expressions
{{ }}to access data - Render output: Template is filled with data values
Supported Formats
JSON (.json):
{
"title": "Sales Report",
"items": [
{"name": "Widget", "price": 10.00}
]
}
YAML (.yaml, .yml):
title: Sales Report
items:
- name: Widget
price: 10.00
Both formats are equivalent and can be used interchangeably.
Data Structure
Simple Values
{
"title": "Sales Report",
"date": "2024-11-03",
"amount": 1500.00,
"isPaid": true
}
Access in template:
<Grid>
| {{ title }} | {{ date }} | {{ amount }} | {{ isPaid }} |
</Grid>
Nested Objects
{
"company": {
"name": "Acme Corp",
"address": {
"street": "123 Main St",
"city": "New York",
"zip": "10001"
}
}
}
Access in template:
<Grid>
| {{ company.name }} |
| {{ company.address.street }} |
| {{ company.address.city }}, {{ company.address.zip }} |
</Grid>
Arrays
{
"items": [
{"name": "Widget A", "price": 10.00},
{"name": "Widget B", "price": 25.00},
{"name": "Widget C", "price": 15.00}
]
}
Access in template:
<Grid>
| Product | Price |
</Grid>
<For each="item in items">
<Grid>
| {{ item.name }} | ${{ item.price }} |
</Grid>
</For>
Data Types
Supported Types
| JSON Type | Excel Type | Example |
|---|---|---|
| String | Text | "Hello World" |
| Number | Number | 123, 45.67 |
| Boolean | Boolean | true, false |
| Null | Empty | null |
| Array | (Iterable) | [1, 2, 3] |
| Object | (Structure) | {"key": "value"} |
Type Conversion
Strings
{"text": "Hello", "code": "ABC123"}
→ Excel text cells
Numbers
{"integer": 123, "decimal": 45.67, "negative": -10.5}
→ Excel number cells
Booleans
{"isActive": true, "isDeleted": false}
→ Excel boolean cells (TRUE/FALSE)
Dates
{"date": "2024-11-03", "datetime": "2024-11-03T14:30:00Z"}
→ Excel date cells (formatted based on locale)
Null
{"emptyField": null}
→ Empty Excel cell
Common Patterns
Invoice Data
{
"invoice": {
"number": "INV-2024-001",
"date": "2024-11-03",
"dueDate": "2024-12-03"
},
"customer": {
"name": "Acme Corp",
"email": "billing@acme.com",
"address": "123 Main St, New York, NY 10001"
},
"items": [
{"description": "Consulting Services", "hours": 40, "rate": 150.00},
{"description": "Development Work", "hours": 80, "rate": 200.00}
],
"subtotal": 22000.00,
"tax": 2200.00,
"total": 24200.00
}
Report Data
{
"report": {
"title": "Monthly Sales Report",
"period": "November 2024",
"generatedAt": "2024-11-03T10:00:00Z"
},
"summary": {
"totalSales": 150000.00,
"totalOrders": 450,
"averageOrder": 333.33
},
"salesByRegion": [
{"region": "North", "sales": 50000.00, "orders": 150},
{"region": "South", "sales": 45000.00, "orders": 135},
{"region": "East", "sales": 30000.00, "orders": 90},
{"region": "West": 25000.00, "orders": 75}
]
}
Hierarchical Data
{
"departments": [
{
"name": "Engineering",
"employees": [
{"name": "Alice", "role": "Engineer", "salary": 100000},
{"name": "Bob", "role": "Senior Engineer", "salary": 120000}
]
},
{
"name": "Sales",
"employees": [
{"name": "Charlie", "role": "Sales Rep", "salary": 80000},
{"name": "Diana", "role": "Sales Manager", "salary": 110000}
]
}
]
}
Best Practices
1. Use Clear Key Names
Good:
{
"customerName": "Acme Corp",
"invoiceNumber": "INV-2024-001",
"orderTotal": 5000.00
}
Avoid:
{
"cn": "Acme Corp",
"inv": "INV-2024-001",
"tot": 5000.00
}
2. Pre-format Data
Instead of complex template logic, prepare data:
Better:
{
"price": 100.00,
"priceFormatted": "$100.00",
"priceWithTax": 110.00,
"discount": "20%"
}
3. Handle Missing Values
Provide default values instead of null/undefined:
Good:
{
"user": {
"name": "Alice",
"email": "alice@example.com",
"phone": "" // Empty string instead of null
}
}
4. Flatten When Possible
Deeply nested structures are hard to work with:
Instead of:
{
"data": {
"customer": {
"profile": {
"personal": {
"name": {
"first": "Alice",
"last": "Smith"
}
}
}
}
}
}
Use:
{
"customerFirstName": "Alice",
"customerLastName": "Smith",
"customerFullName": "Alice Smith"
}
5. Include Metadata
{
"_meta": {
"version": "1.0",
"generated": "2024-11-03T10:00:00Z",
"source": "api-v2"
},
"data": {
...
}
}
Data Preparation
From Database
// Example in Go
rows, _ := db.Query("SELECT name, price FROM products")
defer rows.Close()
products := []map[string]interface{}{}
for rows.Next() {
var name string
var price float64
rows.Scan(&name, &price)
products = append(products, map[string]interface{}{
"name": name,
"price": price,
})
}
data := map[string]interface{}{
"products": products,
"total": len(products),
}
From API
// Example in JavaScript
const response = await fetch('/api/sales');
const apiData = await response.json();
const templateData = {
report: {
title: "Sales Report",
date: new Date().toISOString().split('T')[0]
},
sales: apiData.results.map(item => ({
product: item.product_name,
quantity: item.qty,
revenue: item.total_amount
}))
};
From Files
# Example in Python
import json
import csv
# Load CSV
with open('data.csv') as f:
reader = csv.DictReader(f)
items = list(reader)
# Prepare context
data = {
"title": "Data Export",
"date": "2024-11-03",
"items": items,
"count": len(items)
}
# Save as JSON
with open('context.json', 'w') as f:
json.dump(data, f, indent=2)
Validation
Schema Validation (Recommended)
Use JSON Schema to validate data before rendering:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["invoice", "customer", "items"],
"properties": {
"invoice": {
"type": "object",
"required": ["number", "date"],
"properties": {
"number": {"type": "string"},
"date": {"type": "string", "format": "date"}
}
},
"items": {
"type": "array",
"items": {
"type": "object",
"required": ["description", "amount"],
"properties": {
"description": {"type": "string"},
"amount": {"type": "number"}
}
}
}
}
}
Related Topics
- Expressions - How to access data in templates
- Control Structures - Iterating over arrays
- Examples - Complete examples with data
Validation Rules
This section defines the constraints and error conditions for GXL templates.
Document Structure
Root Element
Rule: Every GXL file must have exactly one <Book> root element.
Valid:
<Book>
<Sheet name="Sheet1">
...
</Sheet>
</Book>
Invalid:
<!-- Missing <Book> -->
<Sheet name="Sheet1">
...
</Sheet>
<!-- Multiple <Book> elements -->
<Book>
<Sheet name="Sheet1">...</Sheet>
</Book>
<Book>
<Sheet name="Sheet2">...</Sheet>
</Book>
Required Sheets
Rule: At least one <Sheet> element is required within <Book>.
Valid:
<Book>
<Sheet name="Sheet1">
<Grid>| Data |</Grid>
</Sheet>
</Book>
Invalid:
<Book>
<!-- No sheets -->
</Book>
Sheet Names
Uniqueness
Rule: Sheet names must be unique within a workbook.
Valid:
<Book>
<Sheet name="Sales">...</Sheet>
<Sheet name="Expenses">...</Sheet>
<Sheet name="Summary">...</Sheet>
</Book>
Invalid:
<Book>
<Sheet name="Data">...</Sheet>
<Sheet name="Data">...</Sheet> <!-- Duplicate name -->
</Book>
Length
Rule: Sheet names must not exceed 31 characters (Excel limitation).
Valid:
<Sheet name="Sales Report 2024">...</Sheet> <!-- 18 characters -->
Invalid:
<Sheet name="This is a very long sheet name that exceeds the limit">...</Sheet> <!-- 58 characters -->
Special Characters
Rule: Sheet names cannot contain: \ / ? * [ ] :
Valid:
<Sheet name="Sales & Marketing">...</Sheet>
<Sheet name="Q1-Q2 Comparison">...</Sheet>
<Sheet name="Profit (Net)">...</Sheet>
Invalid:
<Sheet name="Sales/Marketing">...</Sheet> <!-- Contains / -->
<Sheet name="Q1:Q2">...</Sheet> <!-- Contains : -->
<Sheet name="Data[1]">...</Sheet> <!-- Contains [ ] -->
Empty Names
Rule: Sheet names cannot be empty.
Invalid:
<Sheet name="">...</Sheet>
<Sheet>...</Sheet> <!-- Missing name attribute -->
Cell References
A1 Notation
Rule: Cell references must use valid A1 notation.
Format: [Column][Row]
- Column: A-Z, AA-ZZ, AAA-XFD
- Row: 1-1048576
Valid:
A1Z100AA1XFD1048576
Invalid:
1A(row before column)A0(row must be >= 1)A1048577(exceeds row limit)XFE1(exceeds column limit)
Range Notation
Rule: Ranges must use format StartCell:EndCell.
Valid:
A1:C10B2:D5A1:XFD1048576
Invalid:
A1-C10(use:not-)C10:A1(start must come before end)A1:(incomplete range)
Tag Structure
Properly Nested
Rule: Tags must be properly nested (no overlapping).
Valid:
<Book>
<Sheet name="Sheet1">
<For each="item in items">
<Grid>
| {{ item.name }} |
</Grid>
</For>
</Sheet>
</Book>
Invalid:
<Book>
<Sheet name="Sheet1">
<For each="item in items">
<Grid>
| {{ item.name }} |
</Sheet>
</For> <!-- Overlapping with </Sheet> -->
</Book>
Closed Tags
Rule: All opening tags must have matching closing tags.
Valid:
<Book>
<Sheet name="Sheet1">
<Grid>| Data |</Grid>
</Sheet>
</Book>
Invalid:
<Book>
<Sheet name="Sheet1">
<Grid>| Data |</Grid>
<!-- Missing </Sheet> -->
</Book>
Self-Closing Tags
Rule: Self-closing tags must end with />.
Valid:
<Anchor ref="A1" />
<Merge range="A1:C1" />
<Image ref="B3" src="logo.png" />
Invalid:
<Anchor ref="A1"> <!-- Should be self-closing -->
<Merge range="A1:C1"> <!-- Should be self-closing -->
Attributes
Required Attributes
Rule: Required attributes must be present.
| Tag | Required Attributes |
|---|---|
<Sheet> | name |
<Anchor> | ref |
<Merge> | range |
<For> | each |
<If> | cond |
<Image> | ref, src |
<Shape> | ref, kind |
<Chart> | ref, type, dataRange |
<Pivot> | ref, sourceRange, values |
Invalid:
<Sheet>...</Sheet> <!-- Missing name -->
<Anchor /> <!-- Missing ref -->
<For>...</For> <!-- Missing each -->
Attribute Syntax
Rule: Attributes must use format name="value" with quotes.
Valid:
<Sheet name="Sales">
<Anchor ref="A1" />
Invalid:
<Sheet name=Sales> <!-- Missing quotes -->
<Sheet name='Sales'> <!-- Use double quotes -->
<Anchor ref=A1 /> <!-- Missing quotes -->
Unknown Attributes
Rule: Unknown attributes generate warnings (but don't fail).
<Sheet name="Sales" unknownAttr="value"> <!-- Warning: unknownAttr -->
...
</Sheet>
Expressions
Balanced Braces
Rule: Expression braces must be balanced.
Valid:
| {{ value }} |
| {{ user.name }} |
| {{ items[0].price }} |
Invalid:
| {{ value | <!-- Missing closing }} -->
| { value }} | <!-- Missing opening { -->
| {{ value } | <!-- Unbalanced -->
Valid Paths
Rule: Expression paths must use valid identifier syntax.
Valid:
{{ user }}{{ user.name }}{{ items[0] }}{{ data.nested.property }}
Invalid:
{{ user.123 }}(identifiers can't start with number){{ user..name }}(consecutive dots){{ user. }}(trailing dot)
Control Structures
For Loop Syntax
Rule: each attribute must follow format <var> in <path>.
Valid:
<For each="item in items">
<For each="user in users">
<For each="row in data.rows">
Invalid:
<For each="item"> <!-- Missing 'in' clause -->
<For each="in items"> <!-- Missing variable -->
<For each="item of items"> <!-- Use 'in' not 'of' -->
If Condition
Rule: cond attribute must contain a valid expression.
Valid:
<If cond="isActive">
<If cond="total > 1000">
<If cond="status == 'paid'">
Invalid:
<If cond=""> <!-- Empty condition -->
<If> <!-- Missing cond attribute -->
Grid Structure
Pipe Delimiters
Rule: Grid rows must use | to delimit cells.
Valid:
<Grid>
| A | B | C |
| 1 | 2 | 3 |
</Grid>
Acceptable (leading/trailing pipes optional):
<Grid>
A | B | C
1 | 2 | 3
</Grid>
Warning (inconsistent):
<Grid>
| A | B | C |
A | B | C <!-- Missing trailing pipe (acceptable but inconsistent) -->
</Grid>
Merge Ranges
Valid Ranges
Rule: Merge ranges must be rectangular and valid.
Valid:
<Merge range="A1:C1" /> <!-- Horizontal -->
<Merge range="A1:A3" /> <!-- Vertical -->
<Merge range="B2:D4" /> <!-- Rectangular -->
Invalid:
<Merge range="A1:A1" /> <!-- Single cell (no merge needed) -->
<Merge range="C1:A1" /> <!-- End before start -->
Component Positioning
No Overlaps (Recommended)
Rule: Components should not overlap data cells.
Warning (overlap):
<Grid>
| A | B | C | D |
| 1 | 2 | 3 | 4 |
</Grid>
<!-- Chart overlaps cells C1:D2 -->
<Chart ref="C1" type="column" dataRange="A1:B2" width="200" height="100" />
Better:
<Grid>
| A | B |
| 1 | 2 |
</Grid>
<!-- Chart in separate area -->
<Chart ref="D1" type="column" dataRange="A1:B2" width="200" height="100" />
Validation Levels
Error (Parsing Fails)
These violations prevent template parsing:
- Missing
<Book>root element - Unclosed tags
- Malformed attributes
- Invalid tag nesting
Warning (Logged but Continues)
These violations generate warnings:
- Unknown attributes
- Unknown tags (future extensibility)
- Empty arrays in
<For>loops - Undefined variables in expressions
Info (Best Practice)
These are recommendations:
- Component overlaps
- Very long sheet names (< 31 but > 20)
- Deeply nested loops (> 3 levels)
- Large data ranges (> 10,000 rows)
Validation Tools
Command-Line Validation (Planned)
goxcel validate template.gxl
goxcel validate --strict template.gxl
goxcel validate --schema schema.json template.gxl
Programmatic Validation (Planned)
import "github.com/ryo-arima/goxcel/pkg/validator"
result := validator.Validate("template.gxl")
if !result.IsValid {
for _, err := range result.Errors {
fmt.Println(err.Message)
}
}
Error Messages
Good Error Messages
Error messages should include:
- What: Description of the problem
- Where: File location (line/column if possible)
- Why: Explanation of the rule violated
- How: Suggestion for fixing
Example:
Error at line 15, column 10:
<Sheet name="Sales/Marketing">
^
Sheet name contains invalid character '/'.
Sheet names cannot contain: \ / ? * [ ] :
Suggestion: Use 'Sales & Marketing' or 'Sales-Marketing'
Related Topics
- File Format - File structure requirements
- Core Tags - Tag syntax and attributes
- Expressions - Expression syntax rules
Rendering Semantics
This document describes how GXL templates are processed and rendered into Excel workbooks.
Rendering Overview
Rendering is the process of transforming a .gxl template + JSON data into an Excel workbook.
Key Concepts
- Template Parsing: Parse
.gxltext into an internal structure - Data Binding: Merge JSON context into expressions
- Loop Expansion: Generate repeated content from arrays
- Grid Placement: Convert pipe-delimited grids into cells
- Component Insertion: Add images, charts, etc.
- Excel Generation: Write final
.xlsxfile
Execution Phases
1. Parse Phase
Input: .gxl text file
Output: Abstract Syntax Tree (AST)
Process:
- Read file line by line
- Identify tags:
<Book>,<Sheet>,<Grid>,<For>, etc. - Parse attributes:
name="Sheet1",src="data.items", etc. - Build hierarchical tree structure
- Validate syntax (tag nesting, attribute format)
Errors:
- Unclosed tags
- Invalid tag names
- Missing required attributes
2. Render Phase
Input: AST + JSON data context
Output: Expanded cell data + components
Process:
- Walk AST tree depth-first
- Evaluate expressions with data context
- Expand loops (generate rows)
- Place grids starting at current cursor position
- Record component placements
- Track cursor position after each element
Result: In-memory representation of all cells and objects with final positions.
3. Write Phase
Input: Expanded cell data + components
Output: .xlsx file
Process:
- Create Excel workbook object
- Create sheets with specified names
- Write cell values at computed positions
- Apply formatting (if styled)
- Insert components at recorded positions
- Save workbook to file
Output: Binary Excel file ready to open in Excel/LibreOffice.
Cursor Positioning
The cursor determines where the next content will be placed.
Initial Position
Each sheet starts with cursor at A1.
Grid Placement
Grids are placed starting at the current cursor position.
Example:
<Grid>
| Name | Age |
| Alice | 30 |
</Grid>
- If cursor is at
A1, grid fillsA1:B2 - After grid, cursor moves to
A3(next row after grid)
Cursor Movement Rules
| Element | Cursor Behavior |
|---|---|
<Grid> | Moves to first column of next row after grid |
<For> | Moves to next row after all loop iterations |
<Anchor> | Does not move cursor (absolute positioning) |
<Merge> | No movement (operates on existing cells) |
<Image>, <Chart> | No movement (absolute positioning) |
Example: Sequential Grids
Template:
<Sheet name="Report">
<Grid>
| Header 1 | Header 2 |
</Grid>
<Grid>
| Data 1 | Data 2 |
| Data 3 | Data 4 |
</Grid>
</Sheet>
Rendering:
- First grid placed at
A1:B1, cursor moves toA2 - Second grid placed at
A2:B3, cursor moves toA4
Result:
| A | B | |
|---|---|---|
| 1 | Header 1 | Header 2 |
| 2 | Data 1 | Data 2 |
| 3 | Data 3 | Data 4 |
Example: Anchor Positioning
Template:
<Sheet name="Report">
<Grid>
| Title |
</Grid>
<Anchor cell="D1">
<Grid>
| Side Note |
</Grid>
</Anchor>
<Grid>
| Next Row |
</Grid>
</Sheet>
Rendering:
- First grid at
A1, cursor moves toA2 - Anchor places grid at
D1(cursor stays atA2) - Third grid at
A2, cursor moves toA3
Result:
| A | B | C | D | |
|---|---|---|---|---|
| 1 | Title | Side Note | ||
| 2 | Next Row |
Loop Expansion
Loops generate multiple rows by iterating over arrays.
Basic Loop
Template:
<For src="users">
<Grid>
| {{name}} | {{age}} |
</Grid>
</For>
Data:
{
"users": [
{"name": "Alice", "age": 30},
{"name": "Bob", "age": 25}
]
}
Rendering:
- Enter loop with
usersarray (2 items) - First iteration:
name = "Alice",age = 30- Grid placed at
A1:B1 - Cursor moves to
A2
- Grid placed at
- Second iteration:
name = "Bob",age = 25- Grid placed at
A2:B2 - Cursor moves to
A3
- Grid placed at
Result:
| A | B | |
|---|---|---|
| 1 | Alice | 30 |
| 2 | Bob | 25 |
Loop Variables
Inside loops, special variables are available:
| Variable | Description | Example |
|---|---|---|
{{_index}} | Zero-based index | 0, 1, 2, ... |
{{_number}} | One-based number | 1, 2, 3, ... |
{{_startRow}} | First row of iteration | 1, 2, 3, ... |
{{_endRow}} | Last row of iteration | 1, 2, 3, ... |
Example:
<For src="items">
<Grid>
| {{_number}} | {{name}} |
</Grid>
</For>
Data:
{
"items": [
{"name": "Apple"},
{"name": "Banana"}
]
}
Result:
| A | B | |
|---|---|---|
| 1 | 1 | Apple |
| 2 | 2 | Banana |
Multi-Row Loop Bodies
Loops can have multiple grids per iteration.
Template:
<For src="sections">
<Grid>
| **{{title}}** |
</Grid>
<Grid>
| {{content}} |
</Grid>
</For>
Data:
{
"sections": [
{"title": "Intro", "content": "Welcome"},
{"title": "Body", "content": "Main content"}
]
}
Rendering:
- First iteration:
- First grid at
A1(title "Intro") - Second grid at
A2(content "Welcome") - Cursor at
A3
- First grid at
- Second iteration:
- First grid at
A3(title "Body") - Second grid at
A4(content "Main content") - Cursor at
A5
- First grid at
Result:
| A | |
|---|---|
| 1 | Intro |
| 2 | Welcome |
| 3 | Body |
| 4 | Main content |
Nested Loops
Loops can be nested to handle hierarchical data.
Template:
<For src="categories">
<Grid>
| **{{name}}** |
</Grid>
<For src="items">
<Grid>
| - {{title}} |
</Grid>
</For>
</For>
Data:
{
"categories": [
{
"name": "Fruits",
"items": [
{"title": "Apple"},
{"title": "Banana"}
]
},
{
"name": "Vegetables",
"items": [
{"title": "Carrot"}
]
}
]
}
Rendering:
- Outer loop iteration 1 (Fruits):
- Grid at
A1: "Fruits" - Inner loop iteration 1: Grid at
A2: "- Apple" - Inner loop iteration 2: Grid at
A3: "- Banana" - Cursor at
A4
- Grid at
- Outer loop iteration 2 (Vegetables):
- Grid at
A4: "Vegetables" - Inner loop iteration 1: Grid at
A5: "- Carrot" - Cursor at
A6
- Grid at
Result:
| A | |
|---|---|
| 1 | Fruits |
| 2 | - Apple |
| 3 | - Banana |
| 4 | Vegetables |
| 5 | - Carrot |
Expression Evaluation
Expressions like {{name}} are evaluated during the render phase.
Evaluation Process
- Parse expression: Extract variable path (
name,user.name,items[0].price) - Lookup in context: Traverse JSON data to find value
- Type coercion: Convert to string for cell output
- Error handling: If path not found, use empty string or error marker
Lookup Order (Nested Loops)
When loops are nested, variables are looked up from innermost to outermost scope.
Example:
<For src="departments">
<Grid>
| Department: {{name}} |
</Grid>
<For src="employees">
<Grid>
| - {{name}} (Dept: {{name}}) |
</Grid>
</For>
</For>
Data:
{
"departments": [
{
"name": "Engineering",
"employees": [
{"name": "Alice"}
]
}
]
}
Rendering:
In the inner loop:
- First
{{name}}resolves to employee'sname("Alice") - Outer loop's
nameis shadowed
To access outer scope explicitly (future enhancement):
| - {{name}} (Dept: {{..name}}) |
Component Rendering
Components like <Image>, <Chart> are rendered after grids.
Rendering Order
- Grids and loops: Populate all cells
- Components: Insert images, charts at specified positions
- Merges: Apply cell merges after grid placement
Component Positioning
Components use absolute positioning and do not affect cursor.
Example:
<Sheet name="Report">
<Grid>
| Sales Report |
</Grid>
<Image src="logo.png" cell="E1" width="100" height="50" />
<Grid>
| Q1 | Q2 |
</Grid>
</Sheet>
Rendering:
- First grid at
A1, cursor moves toA2 - Image inserted at
E1(no cursor movement) - Second grid at
A2, cursor moves toA3
Result:
| A | B | ... | E | |
|---|---|---|---|---|
| 1 | Sales Report | [Logo] | ||
| 2 | Q1 | Q2 |
Error Handling
Parse Errors
Causes:
- Syntax errors (unclosed tags)
- Invalid attributes
Behavior:
- Rendering stops
- Error message with line number
Runtime Errors
Causes:
- Undefined variable:
{{missing}} - Invalid data type:
{{user.name}}whenuseris not an object
Behavior:
- v1.0: Insert empty string or error marker
- Future: Configurable (strict mode vs. lenient mode)
Best Practices
- Validate data structure before rendering
- Provide default values in data prep
- Test templates with sample data
- Handle missing data gracefully
Rendering Example: Invoice
Template:
<Book>
<Sheet name="Invoice">
<Grid>
| Invoice #{{invoiceNumber}} |
| Date: {{date}} |
</Grid>
<Grid>
| Item | Quantity | Price | Total |
</Grid>
<For src="items">
<Grid>
| {{name}} | {{quantity}} | {{price}} | =B{{_startRow}}*C{{_startRow}} |
</Grid>
</For>
<Grid>
| | | **Total:** | =SUM(D4:D{{_endRow}}) |
</Grid>
</Sheet>
</Book>
Data:
{
"invoiceNumber": "INV-001",
"date": "2024-01-15",
"items": [
{"name": "Widget", "quantity": 10, "price": 5.00},
{"name": "Gadget", "quantity": 5, "price": 12.50}
]
}
Rendering Steps:
- Parse phase: Build AST
- Render phase:
- First grid at
A1:A2(header) - Second grid at
A3:D3(table header) - Loop starts at row 4:
- Iteration 1: Grid at
A4:D4(Widget),_startRow=4 - Iteration 2: Grid at
A5:D5(Gadget),_startRow=5
- Iteration 1: Grid at
- Final grid at
A6:D6(total)
- First grid at
- Write phase: Generate
.xlsx
Result:
| A | B | C | D | |
|---|---|---|---|---|
| 1 | Invoice #INV-001 | |||
| 2 | Date: 2024-01-15 | |||
| 3 | Item | Quantity | Price | Total |
| 4 | Widget | 10 | 5.00 | =B4*C4 |
| 5 | Gadget | 5 | 12.50 | =B5*C5 |
| 6 | Total: | =SUM(D4:D5) |
Performance Considerations
Large Data Sets
Challenge: Rendering 10,000+ rows can be slow
Optimization:
- Stream data instead of loading all into memory
- Use efficient Excel library (excelize)
- Limit formula recalculations
Complex Templates
Challenge: Deeply nested loops with many expressions
Optimization:
- Pre-process data to flatten structures
- Cache evaluated expressions
- Avoid redundant lookups
Future Enhancements
v1.1:
- Conditional rendering (
<If>) - Loop filtering (
<For src="items" filter="status=='active'">)
v1.2:
- Partial rendering (update only changed cells)
- Template caching (compile once, render many times)
v2.0:
- Incremental rendering (stream large datasets)
- Parallel rendering (multi-sheet concurrency)
Related Topics
- Core Tags - Tag syntax and behavior
- Control Structures - Loop details
- Expressions - Expression evaluation
- Examples - Complete rendering examples
Complete Examples
This document provides complete, working examples of GXL templates with sample data and expected output.
Example 1: Simple Invoice
Template (invoice.gxl)
<Book>
<Sheet name="Invoice">
<Grid>
| Invoice #{{invoiceNumber}} |
| Date: {{date}} |
| Customer: {{customer.name}} |
</Grid>
<Grid>
| | | |
</Grid>
<Grid>
| Item | Quantity | Price | Total |
</Grid>
<For src="items">
<Grid>
| {{name}} | {{quantity}} | ${{price}} | =B{{_startRow}}*C{{_startRow}} |
</Grid>
</For>
<Grid>
| | | **Grand Total:** | =SUM(D7:D{{_endRow}}) |
</Grid>
</Sheet>
</Book>
Data Context
{
"invoiceNumber": "INV-2024-001",
"date": "2024-01-15",
"customer": {
"name": "Acme Corp"
},
"items": [
{"name": "Consulting", "quantity": 10, "price": 150.00},
{"name": "Development", "quantity": 40, "price": 200.00}
]
}
Expected Output
Sheet: Invoice
| A | B | C | D | |
|---|---|---|---|---|
| 1 | Invoice #INV-2024-001 | |||
| 2 | Date: 2024-01-15 | |||
| 3 | Customer: Acme Corp | |||
| 4 | ||||
| 5 | Item | Quantity | Price | Total |
| 6 | Consulting | 10 | $150.00 | =B6*C6 → 1500 |
| 7 | Development | 40 | $200.00 | =B7*C7 → 8000 |
| 8 | Grand Total: | =SUM(D6:D7) → 9500 |
Example 2: Sales Report with Multiple Sheets
Template (sales-report.gxl)
<Book>
<Sheet name="Summary">
<Grid>
| **Sales Report** |
| Period: {{period}} |
</Grid>
<Grid>
| | |
</Grid>
<Grid>
| Region | Total Sales |
</Grid>
<For src="regions">
<Grid>
| {{name}} | ${{totalSales}} |
</Grid>
</For>
</Sheet>
<Sheet name="Details">
<For src="regions">
<Grid>
| **Region: {{name}}** |
</Grid>
<Grid>
| Product | Units | Revenue |
</Grid>
<For src="products">
<Grid>
| {{name}} | {{units}} | ${{revenue}} |
</Grid>
</For>
<Grid>
| | |
</Grid>
</For>
</Sheet>
</Book>
Data Context
{
"period": "Q1 2024",
"regions": [
{
"name": "North",
"totalSales": 150000,
"products": [
{"name": "Widget A", "units": 1000, "revenue": 50000},
{"name": "Widget B", "units": 2000, "revenue": 100000}
]
},
{
"name": "South",
"totalSales": 200000,
"products": [
{"name": "Widget A", "units": 1500, "revenue": 75000},
{"name": "Widget C", "units": 2500, "revenue": 125000}
]
}
]
}
Expected Output
Sheet: Summary
| A | B | |
|---|---|---|
| 1 | Sales Report | |
| 2 | Period: Q1 2024 | |
| 3 | ||
| 4 | Region | Total Sales |
| 5 | North | $150000 |
| 6 | South | $200000 |
Sheet: Details
| A | B | C | |
|---|---|---|---|
| 1 | Region: North | ||
| 2 | Product | Units | Revenue |
| 3 | Widget A | 1000 | $50000 |
| 4 | Widget B | 2000 | $100000 |
| 5 | |||
| 6 | Region: South | ||
| 7 | Product | Units | Revenue |
| 8 | Widget A | 1500 | $75000 |
| 9 | Widget C | 2500 | $125000 |
| 10 |
Example 3: Employee Directory with Anchored Logo
Template (directory.gxl)
<Book>
<Sheet name="Employees">
<Anchor cell="E1">
<Image src="company-logo.png" width="100" height="50" />
</Anchor>
<Grid>
| **Employee Directory** |
| As of {{date}} |
</Grid>
<Grid>
| | |
</Grid>
<Grid>
| ID | Name | Department | Email |
</Grid>
<For src="employees">
<Grid>
| {{id}} | {{firstName}} {{lastName}} | {{department}} | {{email}} |
</Grid>
</For>
</Sheet>
</Book>
Data Context
{
"date": "2024-01-20",
"employees": [
{
"id": "E001",
"firstName": "Alice",
"lastName": "Johnson",
"department": "Engineering",
"email": "alice@example.com"
},
{
"id": "E002",
"firstName": "Bob",
"lastName": "Smith",
"department": "Sales",
"email": "bob@example.com"
},
{
"id": "E003",
"firstName": "Carol",
"lastName": "Williams",
"department": "Marketing",
"email": "carol@example.com"
}
]
}
Expected Output
Sheet: Employees
| A | B | C | D | E | |
|---|---|---|---|---|---|
| 1 | Employee Directory | [Logo] | |||
| 2 | As of 2024-01-20 | ||||
| 3 | |||||
| 4 | ID | Name | Department | ||
| 5 | E001 | Alice Johnson | Engineering | alice@example.com | |
| 6 | E002 | Bob Smith | Sales | bob@example.com | |
| 7 | E003 | Carol Williams | Marketing | carol@example.com |
Example 4: Nested Categories
Template (catalog.gxl)
<Book>
<Sheet name="Catalog">
<Grid>
| **Product Catalog** |
</Grid>
<Grid>
| | |
</Grid>
<For src="categories">
<Grid>
| **{{name}}** |
</Grid>
<Grid>
| SKU | Product | Price |
</Grid>
<For src="products">
<Grid>
| {{sku}} | {{name}} | ${{price}} |
</Grid>
</For>
<Grid>
| | |
</Grid>
</For>
</Sheet>
</Book>
Data Context
{
"categories": [
{
"name": "Electronics",
"products": [
{"sku": "E001", "name": "Laptop", "price": 999.99},
{"sku": "E002", "name": "Mouse", "price": 29.99}
]
},
{
"name": "Books",
"products": [
{"sku": "B001", "name": "Programming Book", "price": 49.99},
{"sku": "B002", "name": "Novel", "price": 19.99}
]
}
]
}
Expected Output
Sheet: Catalog
| A | B | C | |
|---|---|---|---|
| 1 | Product Catalog | ||
| 2 | |||
| 3 | Electronics | ||
| 4 | SKU | Product | Price |
| 5 | E001 | Laptop | $999.99 |
| 6 | E002 | Mouse | $29.99 |
| 7 | |||
| 8 | Books | ||
| 9 | SKU | Product | Price |
| 10 | B001 | Programming Book | $49.99 |
| 11 | B002 | Novel | $19.99 |
| 12 |
Example 5: Loop Variables and Formulas
Template (inventory.gxl)
<Book>
<Sheet name="Inventory">
<Grid>
| # | Product | Quantity | Unit Price | Value |
</Grid>
<For src="items">
<Grid>
| {{_number}} | {{product}} | {{quantity}} | ${{unitPrice}} | =C{{_startRow}}*D{{_startRow}} |
</Grid>
</For>
<Grid>
| | | | **Total:** | =SUM(E2:E{{_endRow}}) |
</Grid>
</Sheet>
</Book>
Data Context
{
"items": [
{"product": "Widget A", "quantity": 100, "unitPrice": 10.00},
{"product": "Widget B", "quantity": 50, "unitPrice": 25.00},
{"product": "Widget C", "quantity": 200, "unitPrice": 5.00}
]
}
Expected Output
Sheet: Inventory
| A | B | C | D | E | |
|---|---|---|---|---|---|
| 1 | # | Product | Quantity | Unit Price | Value |
| 2 | 1 | Widget A | 100 | $10.00 | =C2*D2 → 1000 |
| 3 | 2 | Widget B | 50 | $25.00 | =C3*D3 → 1250 |
| 4 | 3 | Widget C | 200 | $5.00 | =C4*D4 → 1000 |
| 5 | Total: | =SUM(E2:E4) → 3250 |
Example 6: Cell Merging
Template (banner.gxl)
<Book>
<Sheet name="Report">
<Grid>
| Annual Report 2024 | | | |
</Grid>
<Merge range="A1:D1" />
<Grid>
| | | | |
</Grid>
<Grid>
| Quarter | Revenue | Expenses | Profit |
</Grid>
<For src="quarters">
<Grid>
| {{name}} | ${{revenue}} | ${{expenses}} | =B{{_startRow}}-C{{_startRow}} |
</Grid>
</For>
</Sheet>
</Book>
Data Context
{
"quarters": [
{"name": "Q1", "revenue": 100000, "expenses": 70000},
{"name": "Q2", "revenue": 120000, "expenses": 80000},
{"name": "Q3", "revenue": 110000, "expenses": 75000},
{"name": "Q4", "revenue": 130000, "expenses": 85000}
]
}
Expected Output
Sheet: Report
| A | B | C | D | |
|---|---|---|---|---|
| 1 | Annual Report 2024 (merged across A1:D1) | |||
| 2 | ||||
| 3 | Quarter | Revenue | Expenses | Profit |
| 4 | Q1 | $100000 | $70000 | =B4-C4 → 30000 |
| 5 | Q2 | $120000 | $80000 | =B5-C5 → 40000 |
| 6 | Q3 | $110000 | $75000 | =B6-C6 → 35000 |
| 7 | Q4 | $130000 | $85000 | =B7-C7 → 45000 |
Example 7: Multi-Sheet Workbook
Template (company-report.gxl)
<Book>
<Sheet name="Overview">
<Grid>
| **{{companyName}}** |
| {{year}} Annual Report |
</Grid>
<Grid>
| | |
</Grid>
<Grid>
| Total Revenue: | ${{totalRevenue}} |
| Total Expenses: | ${{totalExpenses}} |
| Net Profit: | ${{netProfit}} |
</Grid>
</Sheet>
<Sheet name="Revenue">
<Grid>
| Month | Amount |
</Grid>
<For src="revenueByMonth">
<Grid>
| {{month}} | ${{amount}} |
</Grid>
</For>
</Sheet>
<Sheet name="Expenses">
<Grid>
| Category | Amount |
</Grid>
<For src="expensesByCategory">
<Grid>
| {{category}} | ${{amount}} |
</Grid>
</For>
</Sheet>
</Book>
Data Context
{
"companyName": "TechCorp Inc.",
"year": 2024,
"totalRevenue": 500000,
"totalExpenses": 350000,
"netProfit": 150000,
"revenueByMonth": [
{"month": "Jan", "amount": 40000},
{"month": "Feb", "amount": 45000},
{"month": "Mar", "amount": 50000}
],
"expensesByCategory": [
{"category": "Salaries", "amount": 200000},
{"category": "Marketing", "amount": 80000},
{"category": "Operations", "amount": 70000}
]
}
Expected Output
Sheet: Overview
| A | B | |
|---|---|---|
| 1 | TechCorp Inc. | |
| 2 | 2024 Annual Report | |
| 3 | ||
| 4 | Total Revenue: | $500000 |
| 5 | Total Expenses: | $350000 |
| 6 | Net Profit: | $150000 |
Sheet: Revenue
| A | B | |
|---|---|---|
| 1 | Month | Amount |
| 2 | Jan | $40000 |
| 3 | Feb | $45000 |
| 4 | Mar | $50000 |
Sheet: Expenses
| A | B | |
|---|---|---|
| 1 | Category | Amount |
| 2 | Salaries | $200000 |
| 3 | Marketing | $80000 |
| 4 | Operations | $70000 |
Best Practices Demonstrated
1. Clear Structure
- Use blank rows (
<Grid>| | |</Grid>) for spacing - Separate sections visually
2. Nested Data
- Use nested loops for hierarchical data (Example 4)
- Access nested properties with dot notation
3. Loop Variables
- Use
{{_number}}for row numbering (Example 5) - Use
{{_startRow}}in formulas for dynamic references
4. Formulas
- Reference cells with row variables:
=B{{_startRow}} - Use SUM with
{{_endRow}}for dynamic ranges
5. Anchoring
- Position logos/images independently (Example 3)
- Keep content flow unaffected
6. Multi-Sheet Reports
- Organize related data across sheets (Examples 2, 7)
- Summary sheet + detail sheets
Common Patterns
Pattern 1: Header + Table
<Grid>
| **{{title}}** |
</Grid>
<Grid>
| | |
</Grid>
<Grid>
| Column1 | Column2 |
</Grid>
<For src="data">
<Grid>
| {{field1}} | {{field2}} |
</Grid>
</For>
Pattern 2: Grouped Data
<For src="groups">
<Grid>
| **{{groupName}}** |
</Grid>
<For src="items">
<Grid>
| - {{itemName}} |
</Grid>
</For>
<Grid>
| | |
</Grid>
</For>
Pattern 3: Summary + Detail
<!-- Summary Sheet -->
<Sheet name="Summary">
<Grid>
| Category | Total |
</Grid>
<For src="categories">
<Grid>
| {{name}} | {{total}} |
</Grid>
</For>
</Sheet>
<!-- Detail Sheet -->
<Sheet name="Details">
<For src="categories">
<Grid>
| **{{name}}** |
</Grid>
<For src="items">
<Grid>
| {{item}} | {{value}} |
</Grid>
</For>
</For>
</Sheet>
Example 9: Cell Types and Type Hints
Template (cell-types.gxl)
<Book>
<Sheet name="TypeDemo">
<Grid>
| Data Type | Auto-detected | With Type Hint | Description |
</Grid>
<Grid>
| String | Hello World | {{ "Text" }} | Default text |
| Number | {{ .price }} | {{ .quantity:int }} | Numeric values |
| Float | {{ .discount }} | {{ .price:float }} | Decimal numbers |
| Boolean | {{ .active }} | {{ .enabled:bool }} | TRUE/FALSE |
| Formula | =SUM(B2:B5) | =AVERAGE(C2:C5) | Excel formulas |
| Date | {{ .timestamp }} | {{ .created:date }} | Date values |
</Grid>
<Grid>
| | | | |
</Grid>
<Grid>
| **Mixed Content** | Price: {{ .price }} yen | Total: {{ .quantity }} items | Always string |
</Grid>
<Grid>
| | | | |
</Grid>
<Grid>
| **Force String Type** | {{ .zipCode:string }} | {{ .id:string }} | Preserve leading zeros |
</Grid>
</Sheet>
</Book>
Data Context
{
"price": 1500.50,
"quantity": 42,
"discount": 0.15,
"active": true,
"enabled": false,
"timestamp": "2025-11-03T15:30:00",
"created": "2025-11-03",
"zipCode": "00123",
"id": "00456"
}
Expected Output
Sheet: TypeDemo
| A | B | C | D |
|---|---|---|---|
| Data Type | Auto-detected | With Type Hint | Description |
| String | Hello World | Text | Default text |
| Number | 1500.5 (number) | 42 (number) | Numeric values |
| Float | 0.15 (number) | 1500.5 (number) | Decimal numbers |
| Boolean | TRUE (boolean) | FALSE (boolean) | TRUE/FALSE |
| Formula | (calculated) | (calculated) | Excel formulas |
| Date | 2025-11-03T15:30:00 | 2025-11-03 | Date values |
| Mixed Content | Price: 1500.5 yen | Total: 42 items | Always string |
| Force String Type | 00123 (text) | 00456 (text) | Preserve leading zeros |
Type Inference:
- Numbers are stored as Excel numeric cells (can be used in formulas)
- Formulas are evaluated by Excel
- Booleans become TRUE/FALSE
- Dates can be formatted with Excel date formats
- Type hints override automatic detection
Available Type Hints:
:int,:float,:number- Numeric types:bool,:boolean- Boolean types:date- Date types:string- Force text (preserves leading zeros)
Running Examples
Using goxcel CLI
# Render a template
goxcel generate -t invoice.gxl -d data.json -o output.xlsx
# With YAML data
goxcel generate -t invoice.gxl -d data.yaml -o output.xlsx
# Dry run (preview structure)
goxcel generate -t invoice.gxl -d data.json --dry-run
Using goxcel as Library (Go)
package main
import (
"github.com/ryo-arima/goxcel/pkg/config"
"github.com/ryo-arima/goxcel/pkg/controller"
)
func main() {
cfg := config.NewBaseConfig()
ctrl := controller.NewCommonController(cfg)
err := ctrl.Generate(
"invoice.gxl",
"data.json",
"output.xlsx",
false, // dry-run
)
if err != nil {
panic(err)
}
}
Related Topics
- Core Tags - Tag syntax reference
- Control Structures - Loop details
- Expressions - Data access and interpolation
- Rendering - How templates are processed
FAQ
General
What is goxcel?
goxcel is a template-based Excel file generator that uses GXL (Grid eXchange Language) format to define Excel workbook structures with data binding capabilities.
What file formats does goxcel support?
- Input: GXL (
.gxl) templates, JSON/YAML data files - Output: Excel (
.xlsx) files
Is goxcel production-ready?
goxcel is currently at version 0.1.1 (Stable) with v1.0 features implemented including grid layouts, loops, expressions, and markdown styling.
Templates
How do I create a GXL template?
GXL templates are XML files with a .gxl extension. Basic structure:
<Book name="MyWorkbook">
<Sheet name="Sheet1">
<Grid>
| Header1 | Header2 |
| {{ .value1 }} | {{ .value2 }} |
</Grid>
</Sheet>
</Book>
Can I use loops in templates?
Yes, use the <For> tag:
<For each="item in items">
<Grid>
| {{ .item.name }} | {{ .item.value }} |
</Grid>
</For>
How do I apply cell styling?
Use markdown syntax or type hints:
**bold text**for bold_italic text_for italic{{ .value:number }}for type hints
Data
What data formats are supported?
JSON and YAML files are supported for data input.
How do I access nested data?
Use dot notation: {{ .user.profile.name }}
Can I use expressions?
Yes, mustache-style expressions {{ .path }} with automatic type inference.
CLI
How do I generate an Excel file?
goxcel generate --template template.gxl --data data.json --output output.xlsx
Can I preview without creating a file?
Yes, use the --dry-run flag:
goxcel generate --template template.gxl --data data.json --dry-run
Troubleshooting
Template parsing fails
Check:
- XML syntax is valid
- All tags are properly closed
- Grid syntax uses
|delimiters
Data not appearing in output
Check:
- JSON/YAML data structure matches template paths
- Expression syntax is correct:
{{ .path }} - Data file is valid JSON/YAML
Excel file won't open
Ensure:
- Output path is writable
- No special characters in filename
- Sufficient disk space
Performance
How large can templates be?
goxcel can handle templates with thousands of rows. Performance depends on:
- Number of loops and iterations
- Complexity of expressions
- Available system memory
Can I generate multiple sheets?
Yes, define multiple <Sheet> tags in your template.
Development
Can I use goxcel as a Go library?
Yes:
import "github.com/ryo-arima/goxcel/pkg/controller"
conf := config.NewBaseConfigWithFile("template.gxl")
ctrl := controller.NewCommonController(conf)
err := ctrl.Generate("template.gxl", "data.json", "output.xlsx", false)
How do I contribute?
See the GitHub repository for contribution guidelines.
Troubleshooting
Template Errors
XML Parsing Failed
Symptom: failed to parse GXL XML error
Solutions:
- Validate XML syntax
- Check all tags are properly closed
- Ensure no special characters in attribute values
- Verify file encoding is UTF-8
Example:
<!-- Bad -->
<Sheet name="Sheet 1">
<!-- Good -->
<Sheet name="Sheet1">
</Sheet>
Invalid Grid Syntax
Symptom: Grid not rendering correctly
Solutions:
- Use
|as cell delimiter - Each row must start and end with
| - Consistent column count per row
Example:
<!-- Bad -->
<Grid>
Header1 | Header2
Value1 | Value2
</Grid>
<!-- Good -->
<Grid>
| Header1 | Header2 |
| Value1 | Value2 |
</Grid>
Anchor Reference Error
Symptom: invalid anchor ref error
Solutions:
- Use valid Excel cell references (A1, B2, etc.)
- Column letters must be uppercase
- Row numbers must be positive
Example:
<!-- Bad -->
<Anchor ref="a1" />
<!-- Good -->
<Anchor ref="A1" />
Data Errors
Data Not Displaying
Symptom: Template expressions show as empty or literal
Solutions:
- Verify JSON/YAML syntax
- Check data path matches template expression
- Ensure data file is loaded correctly
Example:
// data.json
{
"user": {
"name": "John"
}
}
<!-- Template -->
<Grid>
| Name |
| {{ .user.name }} |
</Grid>
Type Inference Issues
Symptom: Numbers displayed as text
Solutions:
- Use type hints:
{{ .value:number }} - Ensure numeric values in JSON are not quoted
- Check date format is ISO 8601
Example:
// Bad
{
"price": "100"
}
// Good
{
"price": 100
}
Loop Not Iterating
Symptom: <For> loop produces no output
Solutions:
- Verify data path points to an array
- Check loop syntax:
each="item in items" - Ensure array is not empty
Example:
{
"items": [
{"name": "Item 1"},
{"name": "Item 2"}
]
}
<For each="item in items">
<Grid>
| {{ .item.name }} |
</Grid>
</For>
Output Errors
Excel File Won't Open
Symptom: Generated .xlsx file is corrupted
Solutions:
- Check for write permissions
- Ensure output path exists
- Verify sufficient disk space
- Close Excel if file is already open
Missing Styles
Symptom: Bold/italic formatting not applied
Solutions:
- Use markdown syntax correctly:
**bold**,_italic_ - Ensure no extra spaces around markers
- Check GXL version supports styling
Example:
<!-- Bad -->
<Grid>
| ** bold ** |
</Grid>
<!-- Good -->
<Grid>
| **bold** |
</Grid>
Merged Cells Not Working
Symptom: <Merge> tag has no effect
Solutions:
- Use valid range:
A1:B2 - Ensure cells exist before merging
- Check for overlapping merge ranges
Performance Issues
Slow Generation
Symptom: Template takes long time to generate
Solutions:
- Reduce loop iterations if possible
- Simplify complex expressions
- Use batch operations where applicable
- Check system resources (CPU, memory)
Memory Errors
Symptom: Out of memory errors
Solutions:
- Process large datasets in chunks
- Reduce template complexity
- Increase available memory
- Optimize data structure
CLI Issues
Command Not Found
Symptom: goxcel: command not found
Solutions:
- Ensure goxcel is installed:
go install github.com/ryo-arima/goxcel/cmd/goxcel@latest - Check
$GOPATH/binis in$PATH - Verify installation:
which goxcel
Invalid Arguments
Symptom: CLI command fails
Solutions:
- Check required flags:
--template,--data,--output - Verify file paths are correct
- Use absolute paths if relative paths fail
- Check file permissions
Example:
# Full command with all flags
goxcel generate \
--template /path/to/template.gxl \
--data /path/to/data.json \
--output /path/to/output.xlsx
Logging and Debugging
Enable Debug Logging
Set log level in code:
logger := util.NewLogger(util.LoggerConfig{
Level: "DEBUG",
// ... other config
})
Check Log Messages
Look for message codes:
GXL-P1/P2: GXL parsingU-R1/R2: RenderingR-W1/W2: Writing
Common Error Codes
RP2: Failed to read GXL fileUR2: Failed to render templateRW2: Failed to write XLSX fileFSR2: Failed to read data file
Getting Help
If issues persist:
- Check GitHub issues: https://github.com/ryo-arima/goxcel/issues
- Review specification docs
- Create a minimal reproduction case
- Report bug with logs and sample files
Changelog
All notable changes to goxcel will be documented in this file.
[0.1.1] - 2025-11-04
Added
- Config-based dependency injection for usecase layer
- Logger integration throughout codebase with message codes
- Cell styling support: FontName, FontSize, FontColor, FillColor
- Sheet configuration: Row heights, column widths, freeze panes
- Style collector for dynamic OOXML style generation
- GXL extensions: SheetConfigTag, ColumnTag, RowHeightTag, StyleTag
Changed
- Refactored usecase layer to use config.BaseConfig
- Removed nil checks for logger (always instantiated)
- Updated documentation structure (removed guide and development sections)
- Consolidated non-specification documentation
Fixed
- XML structure conflict in XMLFillColor (separated XMLBgColor)
- Interface implementation: Added RenderSheet method to DefaultSheetUsecase
- Nil pointer dereference in logger usage
[0.1.0] - 2025-11-03
Added
- Core GXL template parsing
- Grid layout with pipe-delimited syntax
- Mustache-style expression interpolation
{{ .path }} - For loop iteration
<For each="item in items"> - Anchor positioning
<Anchor ref="A1" /> - Cell type inference (string, number, boolean, date, formula)
- Type hints
{{ .value:number }} - Markdown styling
**bold**,_italic_ - Cell merging
<Merge range="A1:B2" /> - Formula support
=SUM(A1:A10) - JSON and YAML data support
- CLI with generate command
- Dry-run mode for template validation
- Comprehensive logging system with message codes
Implemented
- Clean architecture (config, controller, usecase, repository, model layers)
- GXL XML parser
- XLSX file writer using OOXML format
- Context stack for nested data access
- Dynamic style management
- Complete test coverage for core functionality
[Planned for v1.1]
To Be Added
- If/Else conditional structures
- Enhanced style tag with direct attributes
- Number formatting patterns
- Date formatting
- Currency formatting
- Sheet-level configuration via GXL
- Column width and row height specification
- Enhanced color support (themes, indexed colors)
Under Consideration
- Image embedding
<Image> - Chart generation
<Chart> - Pivot tables
<Pivot> - Data validation
- Conditional formatting
- Named ranges
- Multiple data source support
- Template inheritance
- Macros/VBA support
Version History
- 0.1.1 (2025-11-04): Config refactoring, styling features, documentation updates
- 0.1.0 (2025-11-03): Initial stable release with v1.0 features
License
goxcel is released under the MIT License.
MIT License
Copyright (c) 2025 ryo-arima
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Third-Party Licenses
goxcel uses the following open-source libraries:
Go Standard Library
- License: BSD-style
- Copyright: The Go Authors
github.com/spf13/cobra
- License: Apache 2.0
- Used for: CLI framework
gopkg.in/yaml.v3
- License: MIT / Apache 2.0
- Used for: YAML parsing
Contributing
By contributing to goxcel, you agree that your contributions will be licensed under the MIT License.
Contact
For license inquiries or questions, please open an issue on GitHub: https://github.com/ryo-arima/goxcel