Skip to content

Template variable list#122

Merged
Amberg merged 1 commit into
mainfrom
workitems/TemplateVariableList
Jun 3, 2026
Merged

Template variable list#122
Amberg merged 1 commit into
mainfrom
workitems/TemplateVariableList

Conversation

@Amberg

@Amberg Amberg commented Jun 3, 2026

Copy link
Copy Markdown
Owner

Template Schema Inspection — GetTemplateSchema()

Adds a static analysis API that inspects a template without rendering it and returns the structural schema of the variables, collections and nested objects the template references. Callers can validate their model against the template's expectations up front, or generate a skeleton model to fill in.

Public API

using var template = DocxTemplate.Open("template.docx");

// Template: "Hello {{customer.Name}}" and "{{#items}}{{items.Price}}{{/items}}"
var schema = template.GetTemplateSchema();

schema.Roots["customer"].Properties["Name"].Kind;          // Scalar
schema.Roots["items"].Kind;                                // Collection
schema.Roots["items"].ItemSchema.Properties["Price"].Kind; // Scalar
Type Description
DocxTemplate.GetTemplateSchema()TemplateSchema Returns the template's schema.
TemplateSchema Roots = top-level models (case-insensitive), matching the BindModel prefixes.
TemplateSchemaNode Name, Kind, Properties (objects), ItemSchema (collections).
TemplateNodeKind Scalar | Object | Collection.

What is covered

The schema is collected from the block tree and covers:

  • Variables & expressions ({{x}}, {{(a + b)}}) — expression operands parsed via SchemaExpressionParser.
  • Loops / collections ({{#items}}…{{/items}}) including nested item schemas and references to outer models.
  • Conditions (if/else), switch/case/default — reported as the union of all branches, so a caller must be prepared to bind anything reachable at runtime.
  • Range loops, inline keywords, ignore blocks.

Each block type contributes through a new CollectSchema(SchemaBuilder) method (base in ContentBlock, overrides in LoopBlock, ConditionalBlock, SwitchBlock/CaseBlock, RangeLoopBlock, IgnoreBlock, InlineKeyWordBlock).

Schema and rendering on the same instance

GetTemplateSchema() builds the same block tree the renderer uses (which mutates the XML). To keep Process() working afterwards, the pipeline was split into two phases:

  • BuildBlockTree — pre-process + marker isolation + loop expansion → model-independent.
  • RenderNode — variable replacement + extensions + loop.Expand + cleanup → model-dependent.
  • ProcessNode = BuildBlockTree + RenderNode (unchanged behaviour for existing callers).

GetTemplateSchema() caches the built block tree per node (header/body/footer) and the resulting TemplateSchema; Process() then renders from the cache instead of rebuilding the already-extracted tree. The previous "destructive — do not call Process afterwards, reopen the stream" contract is removed.

Guards / idempotency:

  • A second GetTemplateSchema() call returns the cached result.
  • Calling GetTemplateSchema() after Process() throws a clear exception.
  • Process() without a prior schema call, or called twice, behaves as before.

Known limitations (documented on TemplateSchema)

  • Sub-templates (:tmpl): the referenced sub-template is a runtime template string, invisible to static analysis.
  • Dynamic tables (:dyntable): only the collection itself is reported, not its runtime-defined rows/columns.
  • String-key indexing (props["key"]): the key is not statically known and is not reflected in the schema.

Tests

  • New: TemplateSchemaTest, SchemaBuilderTest, SchemaExpressionParserTest.
  • ComplexTemplateTest.ProcessComplexTemplate extended to call GetTemplateSchema() then Process() on the same instance.
  • All schema tests (41/41) and ProcessComplexTemplate (3/3) pass on net8.0 / net9.0 / net10.0.

Docs

README gains a Template Schema Inspection section and a feature bullet.

@Amberg Amberg force-pushed the workitems/TemplateVariableList branch from 1672f43 to 152cabd Compare June 3, 2026 13:17
Statically analyze a template without rendering it and return the
structural schema of the variables, collections and nested objects it
references. Useful to validate a caller's model against the template's
expectations, or to generate a skeleton model to fill in.

- New public API: DocxTemplate.GetTemplateSchema() -> TemplateSchema,
  with TemplateSchemaNode / TemplateNodeKind (Scalar/Object/Collection).
- The schema is the union over all branches (if/else, switch/case, every
  loop body); each block type contributes via CollectSchema.
- Split ProcessNode into BuildBlockTree (model-independent) + RenderNode
  so GetTemplateSchema can build the block tree once, cache it, and let a
  subsequent Process() render from the cache on the same instance. The
  former "destructive, do not call Process afterwards" contract is gone.
- README documents the feature and its known limitations.
@Amberg Amberg force-pushed the workitems/TemplateVariableList branch from 152cabd to 3aad106 Compare June 3, 2026 13:19
@Amberg Amberg merged commit 7dd9787 into main Jun 3, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant