@fieldsync/template-engine
v1.1.0
Published
Document generation engine — DOCX, XLSX, PPTX, PDF with a pipe-chain token grammar
Readme
@fieldsync/reporting
Document generation for Node.js. Pass a template file and a JSON data object, get back a formatted report in DOCX, PDF, XLSX, or PPTX. Document structure is entirely defined by your templates — no schema required.
The rendering engine handles filtering, sorting, aggregation, formatting, and conditional logic directly from template tokens. You bring raw data and a template; no application-level preprocessing is required.
How it works
- Create a template — a
.docx,.xlsx, or.pptxfile with placeholder tokens. - Prepare your data — a plain JSON object whose keys match your tokens.
- Render — via the CLI, the
render()API, or a mapper script.
The output format is inferred from the template extension. A .docx template produces both DOCX and PDF by default.
Requirements
- Node.js 18+
- Yarn 4
Installation
yarn add @fieldsync/reportingToken syntax
See SYNTAX.md for the full token and pipe reference — scalar substitution, date formatting, pipe chains, loops, conditionals, images, and format-specific behaviour.
Quick reference
| Token | Description |
|---|---|
| {field} | Scalar substitution |
| {field\|fallback} | Fallback when null/empty |
| {field:date} | Date formatting |
| {field\|pipe1\|pipe2:arg} | Pipe chain |
| {#array}…{/array} | Loop |
| {#field}…{/field} | Conditional (truthy) |
| {^field}…{/field} | Conditional (falsy) |
| {%field} | Image (size hints {%field:WxH} / {%field:W}) |
| {%field:native\|portrait\|landscape\|original} | Image orientation (default native) |
All pipe verbs: where sort limit slice map count sum avg first last join eq ne gt gte lt lte contains and or not upper lower trim truncate append fixed round commas percent
Image orientation: Word ignores EXIF orientation and draws stored pixels, so phone photos land sideways. The engine bakes the rotation in — default native (show as shot). Override the default for all images with the imageOrientation render option, or per-image with a {%photo:portrait} hint. See SYNTAX.md for the full reference.
CLI
yarn render --template <path> --data <json-path> [options]Options
| Flag | Description |
|---|---|
| --template <path> | Template file (.docx, .xlsx, .pptx) |
| --data <path> | JSON file with raw template data |
| --out <dir> | Output directory (default: ./output) |
| --name <stem> | Output filename stem (default: data file stem) |
| --docx | Produce DOCX output |
| --pdf | Produce PDF output |
| --xlsx | Produce XLSX output |
| --pptx | Produce PPTX output |
When no format flag is given, the default is inferred from the template extension: .docx → DOCX + PDF, .xlsx → XLSX, .pptx → PPTX.
Examples
# DOCX + PDF (default for .docx template)
yarn render --template templates/report.docx --data data/site.json
# PDF only
yarn render --template templates/report.docx --data data/site.json --pdf
# XLSX
yarn render --template templates/report.xlsx --data data/site.json
# PPTX
yarn render --template templates/report.pptx --data data/site.json
# Custom output directory and filename
yarn render \
--template templates/report.docx \
--data data/site.json \
--out reports/ \
--name water-tower-Q2-2026Programmatic API
render — unified entry point
Pass raw data directly — no preprocessing required.
import { render } from "@fieldsync/reporting";
const written = await render({
template: "templates/inspection-report.docx",
data: rawPayload, // plain JSON, no preprocessing
outDir: "reports/",
outName: "water-tower-Q2-2026",
formats: ["docx", "pdf"], // optional — inferred from template if omitted
});
// written: string[] of output pathsLow-level renderers
import {
renderTemplate, // DOCX + PDF
renderTemplateXlsx, // XLSX
renderTemplatePptx, // PPTX
} from "@fieldsync/reporting";
await renderTemplate("templates/report.docx", data, "output/report.docx");
await renderTemplate("templates/report.docx", data, "output/report.pdf", { format: "pdf" });
await renderTemplateXlsx("templates/report.xlsx", data, "output/report.xlsx");
await renderTemplatePptx("templates/report.pptx", data, "output/report.pptx");Token engine API
The pipe evaluation engine is exported for use in tests, custom tooling, or preprocessing:
import {
parseToken, // parse a token string into key + pipe list + fallback
applyPipes, // apply a pipe chain to a value
evaluateSection, // resolve a section tag → loop items or boolean
substituteText, // substitute all {tokens} in a string
parseSectionOpen, // parse a {#key|pipes} tag
parseSectionClose, // parse a {/key} tag
resolveKey, // resolve a dot-notation key from a context object
injectLoopStatusShallow, // inject _index/_count/_first/_last into an array
formatDateVal, // format a timestamp using a date variant string
makePipedGetter, // factory for docxtemplater-compatible getters
PIPE_VERBS, // Set<string> of all recognized pipe verb names
} from "@fieldsync/reporting";Examples
// Resolve a value by dot-notation
resolveKey("report.status", data); // "IN REVIEW"
// Parse a token string
parseToken("findings|where:severity=High|count|0");
// { key: "findings", pipes: [where…, count], fallback: "0", dateVariant: null }
// Apply a pipe chain manually (pass the data context for and/or pipes)
applyPipes(data.findings, [
{ verb: "where", args: ["severity=High"] },
{ verb: "count", args: [] },
], data);
// → 2
// Check a cross-field condition
applyPipes(data.isApproved, [
{ verb: "and", args: ["license!=null"] },
], data);
// → true or false
// Evaluate a section tag against a data context
evaluateSection("findings", [{ verb: "where", args: ["requiresRepair=true"] }], false, data);
// → { type: "loop", items: [ ...items with _index/_count/_first/_last injected ] }
// Substitute all tokens in a text string
substituteText("{site.name|upper} — Status: {report.status|lower}", data);
// → "WATER TOWER A — Status: approved"Data-layer utilities
Filtering, sorting, aggregation, date formatting, and null fallbacks are all handled by the pipe grammar directly in the template. These four helpers cover the remaining cases that require JavaScript — structural safety at the data boundary and conditions too complex for a pipe chain.
import { ensureArray, normalizeLoops, filterWhere, addFlags } from "@fieldsync/template-engine";ensureArray(val)
Guard a single loop target that may arrive as null, undefined, or a single object instead of an array. The engine requires arrays for loop tags and will throw on anything else.
data.findings = ensureArray(raw.findings); // null → [], single obj → [obj]normalizeLoops(data, keys)
Shorthand for calling ensureArray on multiple keys at once.
const safe = normalizeLoops(raw, ["findings", "equipment", "photos"]);filterWhere(arr, predicate)
Split one source array into multiple named subsets for independent loops. A single where pipe filters in-place; filterWhere lets you produce two separately-named arrays from the same source.
data.openFindings = filterWhere(findings, f => f.resolvedDate == null);
data.closedFindings = filterWhere(findings, f => f.resolvedDate != null);
// {#openFindings}…{/openFindings} and {#closedFindings} are now independent loopsaddFlags(arr, flags)
Pre-compute boolean fields on array items for conditions that go beyond what and/or pipes can express — multi-field cross-array checks, regex, date math, etc.
data.findings = addFlags(rawFindings, {
isEscalated: f => f.severity === "High" && f.daysOpen > 30 && !f.assignee,
});
// {#isEscalated}…{/isEscalated} works in your templateFull example — raw data, all logic in the template
import { render } from "@fieldsync/reporting";
import { readFileSync } from "fs";
// Raw data — no preprocessing
const data = JSON.parse(readFileSync("data/inspection.json", "utf8"));
// Template tokens do all the work:
// {site.name|upper} → uppercase site name
// {findings|count} → total count
// {findings|where:severity=High|count} → high severity count
// {findings|where:resolvedDate=null|count|0} → open count (fallback 0)
// {findings|avg:elevation|fixed:1|append: ft} → "298.5 ft"
// {equipment|map:tenant|join} → comma-separated tenant list
// {#findings|sort:severity,elevation:desc|limit:10} → sorted + sliced loop
// {#isApproved|and:license!=null} → cross-field conditional
// {#report.status|eq:APPROVED|or:report.status=SUBMITTED}
// {resolvedDate:date|Open} → date or "Open" fallback
// {description|truncate:200} → truncate long text
// {complianceRate|percent:1} → "87.3%"
const paths = await render({
template: "templates/inspection-report.docx",
data,
outDir: "output/",
outName: "tower-A-Q2-2026",
});
console.log("Written:", paths);Feature showcase
The repository includes templates and payloads that exercise every supported feature across all four formats:
# Build templates
yarn template:showcase # templates/feature-showcase.docx
yarn template:showcase-xlsx # templates/feature-showcase.xlsx
yarn template:showcase-pptx # templates/feature-showcase.pptx
# Render with the default payload
yarn render:showcase # output/ ← DOCX + PDF
yarn render:showcase-xlsx # output/ ← XLSX
yarn render:showcase-pptx # output/ ← PPTX
# Pipe unit tests (all 95 checks)
yarn test:pipes
# Render all five data variants and validate output (59 checks)
yarn test:variants
# Run both
yarn testTest variants in data/variants/ exercise all conditional branches:
| Variant | What it tests |
|---|---|
| 01-approved.json | isApproved=true; resolvedFindings loop runs; all nulls filled (no fallbacks fire); all equipment have attachments |
| 02-pending-empty.json | Inverted {^report.isInReview} PENDING banner; empty arrays (loop bodies run 0 times); null fallbacks fire |
| 03-high-risk.json | All 5 findings High severity + requiresRepair=true; {#requiresRepair} fires 5×; {^requiresRepair} never fires |
| 04-monitor-only.json | {^requiresRepair} fires 4×; {#requiresRepair} never fires; requiresRepair=true count = 0 |
| 05-mixed-equipment.json | All equipment attachment states: 4-attachment inner loop, 1-attachment loop, empty {^attachments} |
See data/feature-showcase.json, scripts/test-pipes.ts, and scripts/test-variants.ts.
Custom mappers
When your source data needs structural transformation (e.g., splitting one raw array into two differently-named arrays for separate template loops), write a mapper script that shapes the data and calls render().
import { render } from "@fieldsync/reporting";
import { readFileSync } from "fs";
const raw = JSON.parse(readFileSync("data/payload.json", "utf8"));
// Shape the data however you need before passing to render()
const data = {
...raw,
openFindings: raw.findings.filter((f: any) => !f.resolvedDate),
closedFindings: raw.findings.filter((f: any) => f.resolvedDate),
};
await render({ template: "templates/report.docx", data, outDir: "output/" });Template generation scripts
The scripts/ directory contains one-off scripts that generate the template files under templates/. Run these to regenerate a template from scratch.
yarn template # regenerate all showcase templates
yarn template:showcase # templates/feature-showcase.docx
yarn template:showcase-xlsx # templates/feature-showcase.xlsx
yarn template:showcase-pptx # templates/feature-showcase.pptxProject structure
fieldsync-template-engine/
├── data/
│ ├── feature-showcase.json # Default showcase payload
│ ├── sample-data.json # Minimal sample payload
│ └── variants/ # Test data variants (one per conditional branch)
│ ├── 01-approved.json
│ ├── 02-pending-empty.json
│ ├── 03-high-risk.json
│ ├── 04-monitor-only.json
│ └── 05-mixed-equipment.json
├── fluent-templates/ # Feature gap analysis docs
├── output/ # Rendered files — gitignored
├── scripts/
│ ├── create-template-feature-showcase.ts
│ ├── create-template-showcase-xlsx.ts
│ ├── create-template-showcase-pptx.ts
│ ├── render-feature-showcase.ts
│ ├── render-showcase-xlsx.ts
│ ├── render-showcase-pptx.ts
│ ├── run-sample-report.ts
│ ├── test-pipes.ts # Unit tests for all pipe verbs (95 checks)
│ └── test-variants.ts # Render all variants, validate XLSX output (59 checks)
├── src/
│ ├── cli/render.ts # CLI entry point
│ ├── renderers/
│ │ ├── render-template.ts # DOCX + PDF (docxtemplater + pdfkit)
│ │ ├── render-template-xlsx.ts # XLSX (exceljs)
│ │ └── render-template-pptx.ts # PPTX (pizzip + XML)
│ ├── token-engine.ts # Pipe grammar, shared by all renderers
│ ├── template-utils.ts # Optional data-layer utilities
│ ├── render.ts # Unified render() dispatcher
│ └── index.ts # Public exports
├── templates/ # Template files — gitignored
└── web/ # Local browser UI (see Web Studio section)Development
yarn typecheck # type-check without emitting
yarn lint # lint
yarn lint:fix # lint and auto-fix
yarn template:showcase # (re)build feature-showcase templates
yarn render:showcase # render DOCX/PDF showcase
yarn render:showcase-xlsx # render XLSX showcase
yarn render:showcase-pptx # render PPTX showcase
yarn test:pipes # 95 pipe unit tests
yarn test:variants # render all 5 variants × 4 formats, validate output (59 checks)
yarn test # run both test suitesWeb Studio
The web/ directory contains a local browser UI for uploading templates and data payloads, triggering renders, and downloading the resulting files — no CLI required.
Starting the studio
cd web
yarn devThis starts two processes concurrently:
| Process | URL | Description |
|---------|-----|-------------|
| API server | http://localhost:3001 | Express — handles file uploads, render jobs, and output downloads |
| Client | http://localhost:5173 | Vite + React UI |
Open http://localhost:5173 in your browser.
What you can do
- Upload a template (.docx, .xlsx, or .pptx) or select one already in
templates/ - Upload a data payload (.json) or select one already in
data/ - Choose output formats (DOCX, PDF, XLSX, PPTX — availability depends on the template type)
- Click Render — output files appear in the Output panel and are immediately downloadable
- Regenerate templates — the ⟳ Regenerate button in the Templates panel runs
yarn templateto rebuild all showcase templates from their scripts
API endpoints (port 3001)
| Method | Path | Description |
|--------|------|-------------|
| GET | /api/templates | List template files |
| POST | /api/templates/upload | Upload a template (multipart/form-data, field file) |
| DELETE | /api/templates/:name | Delete a template |
| POST | /api/templates/generate | Run yarn template to regenerate showcase templates |
| GET | /api/data | List data payload files |
| POST | /api/data/upload | Upload a data file (multipart/form-data, field file) |
| DELETE | /api/data/:name | Delete a data file |
| POST | /api/render | Render a template — body: { template, dataFile, formats[] } |
| GET | /api/output | List rendered output files |
| DELETE | /api/output/:name | Delete an output file |
| GET | /output/:name | Download a rendered file |
