Skip to content

Add template syntax validation and parse blocks from a syntax tree - #155

Merged
Amberg merged 3 commits into
mainfrom
workitems/TemplateSyntaxValidation
Oct 5, 2026
Merged

Amberg merged 3 commits into
mainfrom
workitems/TemplateSyntaxValidation

Conversation

@Amberg

@Amberg Amberg commented Oct 5, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • DocxTemplate.ValidateTemplateSyntax() checks a template without a model and returns all syntax errors at once (TemplateSyntaxError with Severity, Part, Tag, Message, Context).
  • Single parser for validation and rendering: the new TemplateSyntaxParser produces a syntax tree with the same shape as the ContentBlock tree; BuildBlockTree creates the blocks from it. The stack-based matching in ExpandLoops (StartBlock / CloseBlock / AutoCloseCaseOrDefaultIfNeeded) is gone.
  • PatternMatcher gets an overload with an error callback so invalid patterns can be collected instead of throwing on the first one.
  • Everything between {{:ignore}} and {{/:ignore}} is treated as opaque text: tags inside are neither validated nor marked (previously they were still turned into blocks and their markers removed by Cleanup). An ignore block is only closed by {{/:ignore}}.

Detected

Severity Issue
Error unclosed blocks, closing tag without opener
Error {{:}} outside a condition / twice, {{:s:}} outside a collection loop, {{#case}} without {{#switch}}, {{#}} / {{#switch}} / {{#case}} without name or expression, unknown inline keyword
Warning mismatching closing tag ({{#Items}}…{{/Orders}}, {?{…}}…{{/Items}}) – rendering closes the current block anyway; the implicit model prefix may be omitted ({{#ds.Items}}…{{/Items}})
Warning malformed tags left as text ({{Name}, {{first name}}, stray }})
Warning unbalanced parentheses / unterminated strings in conditions, expressions, switch selectors and case values; {{#s}} / {{#c}} without argument

Behaviour changes in Process()

Process() now throws an OpenXmlTemplateException listing all errors of severity Error before touching the document. Warnings never block rendering.

  • {{:}} inside a loop, {{:s:}} in a range loop and {{#case}} outside a switch were silently wrong or failed at render time; they are parse errors now.
  • A {{/}} inside {{:ignore}}…{{/:ignore}} no longer closes the ignore block (it is plain text there).
  • Templates containing literal {{/}} or a mismatching closing tag name render as before.

The validator uncovered a typo in ComplexTemplate.docx ({{/.HardwarRevisions}}), fixed in the XML.

Test plan

  • New TemplateSyntaxValidationTest (valid templates, every error class and its severity, header/footer parts, tags split across runs, Process() throws with all errors / renders despite warnings, ignore blocks render verbatim, validation after GetTemplateSchema() and after a failed Process(), all resource templates validate cleanly)
  • Full solution test run on net8.0 / net9.0 / net10.0 after rebasing onto main

🤖 Generated with Claude Code

Amberg and others added 3 commits October 5, 2026 08:28
Add DocxTemplate.ValidateTemplateSyntax(), which checks a template
without a model and reports all syntax errors at once: unclosed blocks,
closing tags without an opener or with a mismatching name, else /
separator / case tags outside their block, unknown inline keywords,
malformed tags such as {{Name} and unbalanced parentheses or
unterminated strings in expressions.

The new TemplateSyntaxParser produces a syntax tree with the same shape
as the ContentBlock tree, and BuildBlockTree now creates the blocks from
it instead of running its own stack-based matching. This keeps a single
parser for validation and rendering. Process() throws with all errors of
severity Error; warnings (malformed tags, suspicious expressions) are only
reported by ValidateTemplateSyntax().

PatternMatcher gets an overload with an error callback so invalid
patterns can be collected instead of throwing on the first one. Closing
tags may omit the implicit model prefix ({{#ds.Items}}...{{/Items}}).

Fix the loop end tag typo {{/.HardwarRevisions}} in ComplexTemplate.docx
that the validator uncovered.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- Treat everything between {{:ignore}} and {{/:ignore}} as opaque text:
  tags inside are neither validated nor marked, so documentation of the
  syntax or TOC fields can no longer abort rendering. An ignore block is
  only closed by {{/:ignore}}.
- Downgrade mismatching closing tags ({{#Items}}...{{/Orders}}) to a
  warning - rendering closes the current block regardless of the name,
  as it always did.
- Report {{#}} without a collection name, run the expression check on
  switch selectors and case values, and warn about {{#s}} / {{#c}}
  without an argument (parsed as a loop over 's' / 'c').
- Refuse ValidateTemplateSyntax after a failed Process, like Process
  itself does.
- Capture the part text in BuildBlockTree instead of a separate
  pre-pass in GetTemplateSchema, which built a second CharacterMap for
  every part.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Shorten the "Advanced Model Binding" heading so it gets a readable
anchor and list syntax validation under features.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@Amberg
Amberg merged commit 6b649a5 into main Oct 5, 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