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);