-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add named iteration variable syntax for nested loops #63
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Add support for explicit iteration variable names in foreach loops:
{{#foreach item in Items}}...{{/foreach}}
This enables access to parent loop variables in nested loops:
{{#foreach category in Categories}}
{{#foreach product in category.Products}}
{{category.Name}}: {{product.Name}}
{{/foreach}}
{{/foreach}}
- Update LoopDetector regex to parse "item in Collection" syntax
- Add IterationVariableName property to LoopBlock and LoopContext
- Update LoopContext.TryResolveVariable to handle named variables
- Update TemplateValidator to validate named iteration variables
- Add 8 integration tests for named iteration variables
- Update documentation (loops.md, CLAUDE.md, README.md)
Both implicit ({{Name}}) and explicit ({{item.Name}}) syntax work
when using named iteration variables, maintaining backward compatibility.
Closes #58
- Validate that iteration variable names don't start with '@' (reserved for loop metadata) - Validate that 'in' keyword cannot be used as iteration variable name - Update regex to capture '@' prefix for proper validation - Fix locale-dependent test assertions using regex pattern - Add tests for reserved variable name validation
Add XML documentation to LoopContext.TryResolveVariable explaining the resolution order (local scope wins over parent scope). Also add integration test verifying that same-named variables in nested loops shadow correctly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds support for named iteration variable syntax in foreach loops, enabling explicit access to parent loop variables in nested loops. This solves a long-standing issue where shadowed property names in nested loops couldn't be accessed.
Key Changes:
- Introduces new syntax
{{#foreach item in Collection}}alongside the existing implicit{{#foreach Collection}}syntax - Enables parent loop variable access in nested loops (e.g.,
{{category.Name}}from within innerproductloop) - Maintains full backward compatibility - existing templates continue to work unchanged
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
docs/for-template-authors/loops.md |
Comprehensive documentation of named iteration variable syntax with examples and best practices |
TriasDev.Templify/Loops/LoopDetector.cs |
Updated regex pattern to parse "item in Collection" syntax and added validation for reserved keywords |
TriasDev.Templify/Loops/LoopBlock.cs |
Added IterationVariableName property to store the optional named variable |
TriasDev.Templify/Loops/LoopContext.cs |
Enhanced variable resolution to handle named variable references and property access |
TriasDev.Templify/Visitors/LoopVisitor.cs |
Updated to pass iteration variable name when creating loop contexts |
TriasDev.Templify/Loops/LoopProcessor.cs |
Updated constructor signature for backward compatibility (sets null for unnamed variables) |
TriasDev.Templify/Core/TemplateValidator.cs |
Extended validation logic to support named iteration variable resolution |
TriasDev.Templify.Tests/Integration/NamedIterationVariableIntegrationTests.cs |
Added 8 comprehensive integration tests covering all new functionality including error cases |
TriasDev.Templify.Tests/Visitors/*Tests.cs |
Updated existing unit tests to pass null for new parameter (backward compatibility) |
TriasDev.Templify.Tests/LoopContext*Tests.cs |
Updated reflection-based tests to handle new constructor parameters |
README.md |
Updated feature list to show both loop syntax options |
CLAUDE.md |
Added documentation of named iteration variable syntax with nested loop examples |
Comments suppressed due to low confidence (1)
TriasDev.Templify/Loops/LoopProcessor.cs:415
- The named iteration variable feature was not implemented in TextTemplateProcessor, only in the Word document processing path (DocumentTemplateProcessor with LoopVisitor). The regex patterns in LoopProcessor.TryDetectLoop (line 383) and FindMatchingEndInElements (line 432) still use the old pattern that doesn't support "item in Collection" syntax.
If this limitation is intentional, it should be documented. If it's an oversight, the LoopProcessor regex patterns should be updated to match the LoopDetector patterns and support named iteration variables in text templates as well.
// Check for {{#foreach CollectionName}}
System.Text.RegularExpressions.Match match = System.Text.RegularExpressions.Regex.Match(
startText,
@"\{\{#foreach\s+(\w+)\}\}",
System.Text.RegularExpressions.RegexOptions.IgnoreCase);
if (!match.Success)
{
return null;
}
string collectionName = match.Groups[1].Value;
// Find matching end marker
int endIndex = FindMatchingEndInElements(elements, startIndex);
if (endIndex == -1)
{
return null;
}
// Get content elements
List<OpenXmlElement> contentElements = new List<OpenXmlElement>();
for (int j = startIndex + 1; j < endIndex; j++)
{
contentElements.Add(elements[j]);
}
return new LoopBlock(
collectionName,
iterationVariableName: null,
contentElements,
elements[startIndex],
elements[endIndex],
isTableRowLoop: false,
emptyBlock: null);
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| { | ||
| private static readonly Regex _foreachStartPattern = new Regex( | ||
| @"\{\{#foreach\s+([\w.]+)\}\}", | ||
| @"\{\{#foreach\s+(?:(@?\w+)\s+in\s+)?([\w.]+)\}\}", |
Copilot
AI
Dec 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The regex pattern captures the '@' prefix in the iteration variable name, but the validation logic checks if the variable name starts with '@'. This creates an inconsistency: the regex allows '@' at the start of the variable name (via @?\w+), but the validation immediately rejects it.
Consider removing the '@?' from the regex pattern since '@' is never valid as a prefix for iteration variable names. The pattern should be \{\{#foreach\s+(?:(\w+)\s+in\s+)?([\w.]+)\}\} instead.
| @"\{\{#foreach\s+(?:(@?\w+)\s+in\s+)?([\w.]+)\}\}", | |
| @"\{\{#foreach\s+(?:(\w+)\s+in\s+)?([\w.]+)\}\}", |
| } | ||
|
|
||
| // Property access via iteration variable (e.g., {{item.Name}}) | ||
| if (variableName.StartsWith(IterationVariableName + ".", StringComparison.Ordinal)) |
Copilot
AI
Dec 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The string concatenation IterationVariableName + "." is performed on every variable resolution when an iteration variable name is present. For templates with many variable references in loops, this could create unnecessary string allocations. Consider caching the concatenated prefix (e.g., storing IterationVariableNamePrefix as IterationVariableName + "." in the constructor) to avoid repeated allocations.
| } | ||
|
|
||
| // Property access via iteration variable (e.g., {{item.Name}}) | ||
| if (placeholder.StartsWith(iterationVariableName + ".", StringComparison.Ordinal)) |
Copilot
AI
Dec 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to the issue in LoopContext.cs, the string concatenation iterationVariableName + "." is performed repeatedly during validation. Consider caching this value to avoid repeated allocations during validation of templates with many variable references.
| }; | ||
|
|
||
| DocumentTemplateProcessor processor = new DocumentTemplateProcessor(); | ||
| MemoryStream outputStream = new MemoryStream(); |
Copilot
AI
Dec 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Disposable 'MemoryStream' is created but not disposed.
| }; | ||
|
|
||
| DocumentTemplateProcessor processor = new DocumentTemplateProcessor(); | ||
| MemoryStream outputStream = new MemoryStream(); |
Copilot
AI
Dec 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Disposable 'MemoryStream' is created but not disposed.
- Add _iterationVariablePrefix field in LoopContext to avoid repeated string concatenation during variable resolution - Cache prefix as local variable in TemplateValidator.CanResolveInScope - Add comment explaining why @? is kept in regex (for better error messages) Addresses review comments from Copilot.
Summary
Adds support for named iteration variable syntax in foreach loops, enabling explicit access to parent loop variables in nested loops.
New syntax:
Nested loop parent access:
Changes
LoopDetectorregex to parseitem in CollectionsyntaxIterationVariableNameproperty toLoopBlockandLoopContextLoopContext.TryResolveVariableto handle named variablesTemplateValidatorto validate named iteration variablesBackward Compatibility
Both implicit (
{{Name}}) and explicit ({{item.Name}}) syntax work when using named iteration variables - existing templates continue to work unchanged.Test plan
Closes #58