Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

goxcel 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

Processing Sequence

The processing follows this sequence:

  1. Parse: Read GXL template and parse into AST
  2. Load: Read JSON/YAML data file
  3. Render: Transform template + data into workbook
    • BookUsecase orchestrates sheet rendering
    • SheetUsecase processes nodes (Grid, For, Anchor)
    • CellUsecase expands expressions and infers types
  4. Generate: Create OOXML structure with styles
  5. 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

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:

  1. Universal Templates: A single template syntax generates Excel, PDF, Word, and HTML
  2. Visual Editors: WYSIWYG editors that generate GXL templates

Core Values

  1. Simplicity First: Readable code, minimal API, convention over configuration
  2. Developer Experience: Clear errors, comprehensive docs, easy onboarding
  3. Reliability: Backward compatibility, comprehensive tests, stable releases
  4. Performance: Optimize hot paths, streaming for large data, predictable behavior
  5. Openness: Open source, transparent decisions, MIT license
  6. 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:

  1. Check Go is installed: go version
  2. Verify GOPATH: go env GOPATH
  3. Check binary location: ls $GOPATH/bin/goxcel
  4. Ensure PATH includes Go bin directory

Build Errors

If build fails:

  1. Update Go: go version should be 1.21+
  2. 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

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

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):

  1. Current loop variable (.sub)
  2. Parent loop variable (.item)
  3. Root data (.rootValue)

Best Practices

Template Design

  1. Keep grids simple: One table per Grid tag
  2. Use anchors sparingly: Sequential flow is easier to maintain
  3. Name meaningfully: Clear sheet and workbook names
  4. Comment complex sections: Use XML comments <!-- -->

Data Structure

  1. Match template paths: Ensure JSON structure matches template expressions
  2. Use arrays for loops: Structure data to match For loops
  3. Consistent types: Use same type for similar values
  4. Avoid deep nesting: Keep data structure reasonably flat

Type Management

  1. Use type hints for IDs: Force strings for numeric IDs like 001
  2. Explicit numbers: Use :number for calculations
  3. Date formats: Use ISO 8601 format: YYYY-MM-DD
  4. Boolean clarity: Use true/false not "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

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:

  1. What You See Is What You Get: The template structure should closely match the Excel output
  2. Separation of Concerns: Data and presentation should be cleanly separated
  3. Progressive Enhancement: Simple cases should be simple, complex cases should be possible
  4. Human-First: Optimize for human readability over parser efficiency
  5. 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:

  1. Overview - High-level introduction and concepts
  2. File Format - File structure, encoding, and metadata
  3. Core Tags - Book, Sheet, Grid, Anchor, Merge
  4. Control Structures - For loops, If/Else conditionals
  5. Expressions - Value interpolation and expression language
  6. Components - Images, Shapes, Charts, Pivot Tables
  7. Styling - Style system and formatting
  8. Data Context - How data flows through templates
  9. Validation Rules - Constraints and error conditions
  10. Rendering Semantics - How templates are processed
  11. 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

VersionDateChanges
0.12024-11-03Initial specification
0.1.12024-11-04Cell 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

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

  • 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:

  1. Optional header comments
  2. One <Book> root element
  3. One or more <Sheet> elements
  4. 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>
  • 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 }}

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

  • Use descriptive names: invoice-template.gxl, sales-report.gxl
  • Use kebab-case: monthly-report.gxl (not MonthlyReport.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 checker
  • gxl-format: Auto-formatter
  • Editor plugins: VS Code, Sublime, etc.

Best Practices

1. Use Version Control

  • Store .gxl files 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>

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 title
  • author: Document author
  • created: Creation date
  • modified: Last modification date

Rules

  1. Must be root element: Every GXL file must have exactly one <Book> element
  2. Must contain sheets: At least one <Sheet> element is required
  3. 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

  1. Unique names: No two sheets can have the same name
  2. At least one sheet: A workbook must contain at least one sheet
  3. Order matters: Sheets appear in the order defined
  4. 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 (use Sales & 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., #FF0000 or FF0000)
  • fill_color / color: Background fill color in RGB hex; # optional
  • border / border_style: Border style for the grid's cells. Supported: thin, medium, thick, dashed, dotted, double
  • border_color: Border color in RGB hex; # optional
  • border_sides: Comma-separated sides to apply (default all). 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:

ABCDE
1Header 1Header 2Side Note
2Data 1Data 2
3Row 3More 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 A1 by 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="">
ScopeAffects all subsequent contentOnly affects that grid
Cursor MovementMoves cursor permanentlyDoesn't affect cursor
Use CaseChange layout flowPlace 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:

  1. Position content at specific locations
  2. Create multiple independent sections
  3. 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

  1. Use sparingly: Let content flow naturally when possible
  2. Prefer Grid ref: For independent content, use <Grid ref=""> instead of <Anchor> to avoid affecting layout flow
  3. Document reasons: Add comments explaining why specific positioning is needed
  4. Avoid overlaps: Ensure anchored content doesn't overlap
  5. 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

  1. Use for headers: Merge cells for titles and section headers
  2. Preserve alignment: Consider how merged cells affect layout
  3. Document merges: Add comments for complex merge patterns
  4. 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

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

  1. Parser evaluates <path> to get an array from data context
  2. 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
  3. 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)


Implementation Status

FeatureStatusVersion
<For> loops✅ Implementedv1.0
Loop variables (loop.index, loop.number)✅ Implementedv1.0
Nested loops✅ Implementedv1.0
Array iteration✅ Implementedv1.0
Map/object iteration✅ Implementedv1.0
loop.startRow, loop.endRow⏳ Plannedv1.1
<If> / <Else>⏳ Plannedv1.1
<Switch> / <Case>💭 Considerationv2.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

  1. Inside Grid cells
  2. In tag attributes (planned)
  3. 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
  • true or false (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 ResultExcel Cell TypeExample
NumberNumber123, 45.67, -10
StringText"Hello", "ABC123"
BooleanBooleantrue, false
Date (ISO 8601)Date"2024-11-03"
Null/UndefinedEmptynull

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

OperatorDescriptionExample
+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:

  1. Parentheses ()
  2. Exponentiation ^
  3. Multiplication *, Division /, Modulo %
  4. Addition +, Subtraction -

Comparison Operators (Planned)

Status: Planned for v1.1 (for use with <If>)

Operators

OperatorDescriptionExample
==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

OperatorDescriptionExample
&&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

FunctionDescriptionExample
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

FunctionDescriptionExample
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

FunctionDescriptionExample
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

FunctionDescriptionExample
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

  1. Parse template: Extract expressions
  2. Evaluate expressions: Resolve against data context
  3. Type conversion: Convert to appropriate Excel types
  4. 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


Implementation Status

FeatureStatusVersion
Basic interpolation {{ var }}✅ Implementedv1.0
Dot notation {{ obj.prop }}✅ Implementedv1.0
Array access {{ arr[0] }}✅ Implementedv1.0
Attribute interpolation✅ Implementedv1.0
Formula interpolation✅ Implementedv1.0
Arithmetic operators⏳ Plannedv1.2
Comparison operators⏳ Plannedv1.1
Logical operators⏳ Plannedv1.1
Built-in functions⏳ Plannedv1.2
Ternary operator⏳ Plannedv1.2
Null coalescing⏳ Plannedv1.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

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

  1. Use relative paths for portability
  2. Specify dimensions to control layout
  3. Optimize images before embedding (reduce file size)
  4. 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 - Rectangle
    • rounded - Rounded rectangle
    • ellipse - Circle/ellipse
    • arrow - Arrow
    • line - Straight line
    • star - Star shape
    • triangle - Triangle
    • diamond - 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 chart
    • bar - Horizontal bar chart
    • line - Line chart
    • pie - Pie chart
    • scatter - Scatter plot
    • area - Area chart
    • doughnut - Doughnut chart
    • radar - Radar chart
    • combo - 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

Componentv1.0 (Placeholder)v1.2 (Rendering)v2.0 (Advanced)
Image-
Shape-
Chart-
Pivot Table-
Button--💭
Slider--💭

Legend:

  • ✅ Implemented
  • ⏳ Planned
  • 💭 Under consideration
  • - Not planned

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

Featurev1.0v1.1v1.2
Markdown Bold/Italic
Type Hints
Auto Type Inference
Style Tag🔄
Named Styles🔄
Conditional Formatting

Legend: ✅ Implemented | 🔄 Planned | ❌ Not Planned


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

  1. Prepare data: Create JSON or YAML data structure
  2. Pass to renderer: Provide data when rendering template
  3. Access in template: Use expressions {{ }} to access data
  4. 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 TypeExcel TypeExample
StringText"Hello World"
NumberNumber123, 45.67
BooleanBooleantrue, false
NullEmptynull
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

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"}
        }
      }
    }
  }
}

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:

  • A1
  • Z100
  • AA1
  • XFD1048576

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:C10
  • B2:D5
  • A1: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.

TagRequired 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

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'

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

  1. Template Parsing: Parse .gxl text into an internal structure
  2. Data Binding: Merge JSON context into expressions
  3. Loop Expansion: Generate repeated content from arrays
  4. Grid Placement: Convert pipe-delimited grids into cells
  5. Component Insertion: Add images, charts, etc.
  6. Excel Generation: Write final .xlsx file

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 fills A1:B2
  • After grid, cursor moves to A3 (next row after grid)

Cursor Movement Rules

ElementCursor 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:

  1. First grid placed at A1:B1, cursor moves to A2
  2. Second grid placed at A2:B3, cursor moves to A4

Result:

AB
1Header 1Header 2
2Data 1Data 2
3Data 3Data 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:

  1. First grid at A1, cursor moves to A2
  2. Anchor places grid at D1 (cursor stays at A2)
  3. Third grid at A2, cursor moves to A3

Result:

ABCD
1TitleSide Note
2Next 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:

  1. Enter loop with users array (2 items)
  2. First iteration: name = "Alice", age = 30
    • Grid placed at A1:B1
    • Cursor moves to A2
  3. Second iteration: name = "Bob", age = 25
    • Grid placed at A2:B2
    • Cursor moves to A3

Result:

AB
1Alice30
2Bob25

Loop Variables

Inside loops, special variables are available:

VariableDescriptionExample
{{_index}}Zero-based index0, 1, 2, ...
{{_number}}One-based number1, 2, 3, ...
{{_startRow}}First row of iteration1, 2, 3, ...
{{_endRow}}Last row of iteration1, 2, 3, ...

Example:

<For src="items">
  <Grid>
  | {{_number}} | {{name}} |
  </Grid>
</For>

Data:

{
  "items": [
    {"name": "Apple"},
    {"name": "Banana"}
  ]
}

Result:

AB
11Apple
22Banana

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:

  1. First iteration:
    • First grid at A1 (title "Intro")
    • Second grid at A2 (content "Welcome")
    • Cursor at A3
  2. Second iteration:
    • First grid at A3 (title "Body")
    • Second grid at A4 (content "Main content")
    • Cursor at A5

Result:

A
1Intro
2Welcome
3Body
4Main 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:

  1. 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
  2. Outer loop iteration 2 (Vegetables):
    • Grid at A4: "Vegetables"
    • Inner loop iteration 1: Grid at A5: "- Carrot"
    • Cursor at A6

Result:

A
1Fruits
2- Apple
3- Banana
4Vegetables
5- Carrot

Expression Evaluation

Expressions like {{name}} are evaluated during the render phase.

Evaluation Process

  1. Parse expression: Extract variable path (name, user.name, items[0].price)
  2. Lookup in context: Traverse JSON data to find value
  3. Type coercion: Convert to string for cell output
  4. 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's name ("Alice")
  • Outer loop's name is shadowed

To access outer scope explicitly (future enhancement):

| - {{name}} (Dept: {{..name}}) |

Component Rendering

Components like <Image>, <Chart> are rendered after grids.

Rendering Order

  1. Grids and loops: Populate all cells
  2. Components: Insert images, charts at specified positions
  3. 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:

  1. First grid at A1, cursor moves to A2
  2. Image inserted at E1 (no cursor movement)
  3. Second grid at A2, cursor moves to A3

Result:

AB...E
1Sales Report[Logo]
2Q1Q2

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}} when user is not an object

Behavior:

  • v1.0: Insert empty string or error marker
  • Future: Configurable (strict mode vs. lenient mode)

Best Practices

  1. Validate data structure before rendering
  2. Provide default values in data prep
  3. Test templates with sample data
  4. 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:

  1. Parse phase: Build AST
  2. 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
    • Final grid at A6:D6 (total)
  3. Write phase: Generate .xlsx

Result:

ABCD
1Invoice #INV-001
2Date: 2024-01-15
3ItemQuantityPriceTotal
4Widget105.00=B4*C4
5Gadget512.50=B5*C5
6Total:=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)

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

ABCD
1Invoice #INV-2024-001
2Date: 2024-01-15
3Customer: Acme Corp
4
5ItemQuantityPriceTotal
6Consulting10$150.00=B6*C6 → 1500
7Development40$200.00=B7*C7 → 8000
8Grand 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

AB
1Sales Report
2Period: Q1 2024
3
4RegionTotal Sales
5North$150000
6South$200000

Sheet: Details

ABC
1Region: North
2ProductUnitsRevenue
3Widget A1000$50000
4Widget B2000$100000
5
6Region: South
7ProductUnitsRevenue
8Widget A1500$75000
9Widget C2500$125000
10

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

ABCDE
1Employee Directory[Logo]
2As of 2024-01-20
3
4IDNameDepartmentEmail
5E001Alice JohnsonEngineeringalice@example.com
6E002Bob SmithSalesbob@example.com
7E003Carol WilliamsMarketingcarol@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

ABC
1Product Catalog
2
3Electronics
4SKUProductPrice
5E001Laptop$999.99
6E002Mouse$29.99
7
8Books
9SKUProductPrice
10B001Programming Book$49.99
11B002Novel$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

ABCDE
1#ProductQuantityUnit PriceValue
21Widget A100$10.00=C2*D2 → 1000
32Widget B50$25.00=C3*D3 → 1250
43Widget C200$5.00=C4*D4 → 1000
5Total:=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

ABCD
1Annual Report 2024 (merged across A1:D1)
2
3QuarterRevenueExpensesProfit
4Q1$100000$70000=B4-C4 → 30000
5Q2$120000$80000=B5-C5 → 40000
6Q3$110000$75000=B6-C6 → 35000
7Q4$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

AB
1TechCorp Inc.
22024 Annual Report
3
4Total Revenue:$500000
5Total Expenses:$350000
6Net Profit:$150000

Sheet: Revenue

AB
1MonthAmount
2Jan$40000
3Feb$45000
4Mar$50000

Sheet: Expenses

AB
1CategoryAmount
2Salaries$200000
3Marketing$80000
4Operations$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

ABCD
Data TypeAuto-detectedWith Type HintDescription
StringHello WorldTextDefault text
Number1500.5 (number)42 (number)Numeric values
Float0.15 (number)1500.5 (number)Decimal numbers
BooleanTRUE (boolean)FALSE (boolean)TRUE/FALSE
Formula(calculated)(calculated)Excel formulas
Date2025-11-03T15:30:002025-11-03Date values
Mixed ContentPrice: 1500.5 yenTotal: 42 itemsAlways string
Force String Type00123 (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)
    }
}

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:

  1. Validate XML syntax
  2. Check all tags are properly closed
  3. Ensure no special characters in attribute values
  4. 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:

  1. Use | as cell delimiter
  2. Each row must start and end with |
  3. 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:

  1. Use valid Excel cell references (A1, B2, etc.)
  2. Column letters must be uppercase
  3. 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:

  1. Verify JSON/YAML syntax
  2. Check data path matches template expression
  3. 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:

  1. Use type hints: {{ .value:number }}
  2. Ensure numeric values in JSON are not quoted
  3. Check date format is ISO 8601

Example:

// Bad
{
  "price": "100"
}

// Good
{
  "price": 100
}

Loop Not Iterating

Symptom: <For> loop produces no output

Solutions:

  1. Verify data path points to an array
  2. Check loop syntax: each="item in items"
  3. 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:

  1. Check for write permissions
  2. Ensure output path exists
  3. Verify sufficient disk space
  4. Close Excel if file is already open

Missing Styles

Symptom: Bold/italic formatting not applied

Solutions:

  1. Use markdown syntax correctly: **bold**, _italic_
  2. Ensure no extra spaces around markers
  3. 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:

  1. Use valid range: A1:B2
  2. Ensure cells exist before merging
  3. Check for overlapping merge ranges

Performance Issues

Slow Generation

Symptom: Template takes long time to generate

Solutions:

  1. Reduce loop iterations if possible
  2. Simplify complex expressions
  3. Use batch operations where applicable
  4. Check system resources (CPU, memory)

Memory Errors

Symptom: Out of memory errors

Solutions:

  1. Process large datasets in chunks
  2. Reduce template complexity
  3. Increase available memory
  4. Optimize data structure

CLI Issues

Command Not Found

Symptom: goxcel: command not found

Solutions:

  1. Ensure goxcel is installed: go install github.com/ryo-arima/goxcel/cmd/goxcel@latest
  2. Check $GOPATH/bin is in $PATH
  3. Verify installation: which goxcel

Invalid Arguments

Symptom: CLI command fails

Solutions:

  1. Check required flags: --template, --data, --output
  2. Verify file paths are correct
  3. Use absolute paths if relative paths fail
  4. 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 parsing
  • U-R1/R2: Rendering
  • R-W1/W2: Writing

Common Error Codes

  • RP2: Failed to read GXL file
  • UR2: Failed to render template
  • RW2: Failed to write XLSX file
  • FSR2: Failed to read data file

Getting Help

If issues persist:

  1. Check GitHub issues: https://github.com/ryo-arima/goxcel/issues
  2. Review specification docs
  3. Create a minimal reproduction case
  4. 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