diff --git a/TriasDev.Templify.Tests/Odt/OdtValidationTests.cs b/TriasDev.Templify.Tests/Odt/OdtValidationTests.cs
index a364f8f..f1a750a 100644
--- a/TriasDev.Templify.Tests/Odt/OdtValidationTests.cs
+++ b/TriasDev.Templify.Tests/Odt/OdtValidationTests.cs
@@ -103,6 +103,45 @@ public void EmptyLoopCollection_AddsWarning_AndMissingCollectionIsError()
Assert.Equal(new[] { "Absent" }, result.MissingVariables);
}
+ [Fact]
+ public void TableRowAndListItemBlocks_AreValid()
+ {
+ // The marker rows and items hold the markers in their own paragraphs; those paragraphs are not markers of
+ // a loop or conditional inside the cell or item.
+ OdtDocumentBuilder template = new OdtDocumentBuilder()
+ .AddTable(new[] { "{{#foreach Rows}}", "" }, new[] { "{{Cell}}", "{{@index}}" }, new[] { "{{/foreach}}", "" })
+ .AddTable(new[] { "{{#if Flag}}" }, new[] { "x" }, new[] { "{{#else}}" }, new[] { "y" }, new[] { "{{/if}}" })
+ .AddXml("{{#foreach item in Items}}"
+ + "{{item}}"
+ + "{{/foreach}}")
+ .AddXml("{{#if Flag}}"
+ + "a"
+ + "{{/if}}");
+
+ ValidationResult result = Validate(template);
+ ValidationResult withData = Validate(template, new Dictionary
+ {
+ ["Rows"] = new List> { new() { ["Cell"] = "c" } },
+ ["Items"] = new List { "a" },
+ ["Flag"] = true,
+ });
+
+ Assert.True(result.IsValid, string.Join("; ", result.Errors.Select(e => e.Message)));
+ Assert.True(withData.IsValid, string.Join("; ", withData.Errors.Select(e => e.Message)));
+ Assert.Empty(withData.MissingVariables);
+ }
+
+ [Fact]
+ public void TableRowLoop_UnmatchedInsideCell_IsStillReported()
+ {
+ OdtDocumentBuilder template = new OdtDocumentBuilder()
+ .AddTable(new[] { "{{#foreach Rows}}" }, new[] { "{{#if A}}" }, new[] { "{{/foreach}}" });
+
+ ValidationResult result = Validate(template);
+
+ Assert.Contains(result.Errors, e => e.Message.Contains("'{{#if A}}'", StringComparison.Ordinal));
+ }
+
[Fact]
public void ReadOnlyDictionaryOverload_Works()
{
diff --git a/TriasDev.Templify/OpenDocument/OdtTemplateValidator.cs b/TriasDev.Templify/OpenDocument/OdtTemplateValidator.cs
index 9128314..a035ac3 100644
--- a/TriasDev.Templify/OpenDocument/OdtTemplateValidator.cs
+++ b/TriasDev.Templify/OpenDocument/OdtTemplateValidator.cs
@@ -267,22 +267,29 @@ public void WalkBlocks(IReadOnlyList blocks)
}
IReadOnlyList loops;
+ IReadOnlyList conditionals;
if (blocks.All(OdtMarkerText.IsRow))
{
loops = Detect(() => OdtLoopDetector.DetectTableRowLoops(blocks));
- Detect(() => OdtConditionalDetector.DetectTableRowConditionals(blocks));
+ conditionals = Detect(() => OdtConditionalDetector.DetectTableRowConditionals(blocks));
}
else if (blocks.All(IsListItem))
{
loops = Detect(() => OdtLoopDetector.DetectListItemLoops(blocks));
- Detect(() => OdtConditionalDetector.DetectListItemConditionals(blocks));
+ conditionals = Detect(() => OdtConditionalDetector.DetectListItemConditionals(blocks));
}
else
{
loops = Detect(() => OdtLoopDetector.DetectLoops(blocks));
- Detect(() => OdtConditionalDetector.DetectConditionals(blocks));
+ conditionals = Detect(() => OdtConditionalDetector.DetectConditionals(blocks));
}
+ // Marker blocks are removed by processing. A marker row or list item holds its marker in a paragraph of
+ // its own, which must not be detected again as an unmatched marker inside the cell or item.
+ HashSet markers = new HashSet(
+ loops.SelectMany(l => new[] { l.StartMarker, l.EndMarker })
+ .Concat(conditionals.SelectMany(c => c.Branches.Select(b => b.Marker).Append(c.EndMarker))));
+
foreach (OdtLoopBlock loop in loops)
{
_allPlaceholders.Add(loop.CollectionName);
@@ -293,6 +300,11 @@ public void WalkBlocks(IReadOnlyList blocks)
foreach (XElement block in blocks)
{
+ if (markers.Contains(block))
+ {
+ continue;
+ }
+
foreach (List sequence in GetChildSequences(block))
{
WalkBlocks(sequence);