From 1718f4965909f25713e38543a5091c01fe3c673c Mon Sep 17 00:00:00 2001 From: Manuel Amstutz Date: Mon, 22 Jan 2024 10:05:34 +0100 Subject: [PATCH 01/14] Changed dynamic table placeholder for data row from 'Columns' to 'Rows' --- DocxTemplater.Test/DocxTemplateTest.cs | 1 - .../Resources/DynamicTable.docx | Bin 18262 -> 18317 bytes ...leWithComplexObjectsAsHeaderAndValues.docx | Bin 18514 -> 18535 bytes DocxTemplater/Blocks/DynamicTableBlock.cs | 4 ++-- 4 files changed, 2 insertions(+), 3 deletions(-) diff --git a/DocxTemplater.Test/DocxTemplateTest.cs b/DocxTemplater.Test/DocxTemplateTest.cs index a0b9c14..4192b9c 100644 --- a/DocxTemplater.Test/DocxTemplateTest.cs +++ b/DocxTemplater.Test/DocxTemplateTest.cs @@ -15,7 +15,6 @@ namespace DocxTemplater.Test { internal class DocxTemplateTest { - [Test] public void DynamicTable() { diff --git a/DocxTemplater.Test/Resources/DynamicTable.docx b/DocxTemplater.Test/Resources/DynamicTable.docx index 7a12eb923fd0c86f0dd8ead3ba95233635b72f0d..b1cc832a2d20a9692d576809555a0fb904aa0871 100644 GIT binary patch delta 6038 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 diff --git a/DocxTemplater.Test/Resources/DynamicTableWithComplexObjectsAsHeaderAndValues.docx b/DocxTemplater.Test/Resources/DynamicTableWithComplexObjectsAsHeaderAndValues.docx index c76758af056408da6e683c36f78620f678e573fc..0b26221407ce146ced860d240ae4de7ab6494f90 100644 GIT binary patch delta 5055 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 delta 5021 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 D_S{M& diff --git a/DocxTemplater/Blocks/DynamicTableBlock.cs b/DocxTemplater/Blocks/DynamicTableBlock.cs index ff5aa30..1aa5a51 100644 --- a/DocxTemplater/Blocks/DynamicTableBlock.cs +++ b/DocxTemplater/Blocks/DynamicTableBlock.cs @@ -23,7 +23,7 @@ public override void Expand(ModelDictionary models, OpenXmlElement parentNode) { var headersName = $"{m_tablenName}.{nameof(IDynamicTable.Headers)}"; - var columnsName = $"{m_tablenName}.Columns"; + var columnsName = $"{m_tablenName}.{nameof(IDynamicTable.Rows)}"; var table = m_content.OfType().FirstOrDefault(); var headerRow = table?.Elements().FirstOrDefault(row => row.Descendants().Any(d => d.HasMarker(PatternType.Variable) && d.Text.Contains($"{{{{{headersName}"))); @@ -33,7 +33,7 @@ public override void Expand(ModelDictionary models, OpenXmlElement parentNode) var dataCell = dataRow?.Elements().FirstOrDefault(row => row.Descendants().Any(d => d.HasMarker(PatternType.Variable) && d.Text.Contains($"{{{{{columnsName}"))); if (headerCell == null || dataCell == null) { - throw new OpenXmlTemplateException($"Dynamic table block must contain exactly one table with at least two rows and one column, but found"); + throw new OpenXmlTemplateException($"Dynamic table block must contain exactly one table with at least a header and a data row"); } // write headers From 901f4dc8f19fc91cd6931389bafa88522b791c32 Mon Sep 17 00:00:00 2001 From: Manuel Amstutz Date: Wed, 24 Jan 2024 18:30:23 +0100 Subject: [PATCH 02/14] Add html tag arround Html content if missing Word shows error message if tag is missing --- DocxTemplater.Test/DocxTemplateTest.cs | 34 ++++++++++++++++++++++++ DocxTemplater/Formatter/HtmlFormatter.cs | 11 +++++++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/DocxTemplater.Test/DocxTemplateTest.cs b/DocxTemplater.Test/DocxTemplateTest.cs index 4192b9c..c39c39e 100644 --- a/DocxTemplater.Test/DocxTemplateTest.cs +++ b/DocxTemplater.Test/DocxTemplateTest.cs @@ -121,6 +121,40 @@ public void MissingVariableThrows() Assert.Throws(() => docTemplate.Process()); } + [TestCase("

Test

", "

Test

")] + [TestCase("

Test

","

Test

")] + [TestCase("

Test

", "

Test

")] + [TestCase("Test", "Test")] + [TestCase("foo
Test", "foo
Test")] + public void HtmlIsAlwaysEnclosedWithHtmlTags(string html, string expexted) + { + using var memStream = new MemoryStream(); + using var wpDocument = WordprocessingDocument.Create(memStream, WordprocessingDocumentType.Document); + MainDocumentPart mainPart = wpDocument.AddMainDocumentPart(); + mainPart.Document = new Document(new Body(new Paragraph(new Run(new Text("Here comes HTML {{ds}:html}"))))); + wpDocument.Save(); + memStream.Position = 0; + var docTemplate = new DocxTemplate(memStream); + docTemplate.BindModel("ds", html); + + var result = docTemplate.Process(); + docTemplate.Validate(); + Assert.IsNotNull(result); + result.SaveAsFileAndOpenInWord(); + result.Position = 0; + var document = WordprocessingDocument.Open(result, false); + // check word contains altChunk + var body = document.MainDocumentPart.Document.Body; + var altChunk = body.Descendants().FirstOrDefault(); + Assert.IsNotNull(altChunk); + // extract html part + var htmlPart = document.MainDocumentPart.GetPartById(altChunk.Id); + var stream = htmlPart.GetStream(); + var content = new StreamReader(stream).ReadToEnd(); + Assert.That(content, Is.EqualTo(expexted)); + // check html part contains html; + } + [Test] public void MissingVariableWithSkipErrorHandling() { diff --git a/DocxTemplater/Formatter/HtmlFormatter.cs b/DocxTemplater/Formatter/HtmlFormatter.cs index 4171d9b..0eb4360 100644 --- a/DocxTemplater/Formatter/HtmlFormatter.cs +++ b/DocxTemplater/Formatter/HtmlFormatter.cs @@ -26,6 +26,15 @@ public void ApplyFormat(FormatterContext context, Text target) return; } + // fix html - ensure starts and ends with and + if (!html.StartsWith("", StringComparison.CurrentCultureIgnoreCase)) + { + html = "" + html; + } + if (!html.EndsWith("", StringComparison.CurrentCultureIgnoreCase)) + { + html = html + ""; + } var root = target.GetRoot(); string alternativeFormatImportPartId = null; if (root is OpenXmlPartRootElement openXmlPartRootElement && openXmlPartRootElement.OpenXmlPart != null) @@ -45,7 +54,7 @@ public void ApplyFormat(FormatterContext context, Text target) } if (alternativeFormatImportPartId == null) { - throw new OpenXmlTemplateException("Could not find a valid image part"); + throw new OpenXmlTemplateException("Could not find root to insert HTML"); } AltChunk altChunk = new() { From e1c3709fd082331eb819e84c738f093cf9e13805 Mon Sep 17 00:00:00 2001 From: Manuel Amstutz Date: Wed, 24 Jan 2024 18:30:48 +0100 Subject: [PATCH 03/14] Null check for stream in DocxTemplate ctor --- DocxTemplater/DocxTemplate.cs | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/DocxTemplater/DocxTemplate.cs b/DocxTemplater/DocxTemplate.cs index 32ad6f0..a7bbfca 100644 --- a/DocxTemplater/DocxTemplate.cs +++ b/DocxTemplater/DocxTemplate.cs @@ -26,6 +26,7 @@ public sealed class DocxTemplate : IDisposable public DocxTemplate(Stream docXStream, ProcessSettings settings = null) { + ArgumentNullException.ThrowIfNull(docXStream); Settings = settings ?? ProcessSettings.Default; m_stream = new MemoryStream(); docXStream.CopyTo(m_stream); @@ -112,7 +113,7 @@ public Stream Process() { ProcessNode(header.Header); } - ProcessNode(m_wpDocument.MainDocumentPart.Document.Body); + ProcessNode(m_wpDocument.MainDocumentPart.RootElement); foreach (var footer in m_wpDocument.MainDocumentPart.FooterParts) { ProcessNode(footer.Footer); @@ -122,35 +123,35 @@ public Stream Process() return m_stream; } - private void ProcessNode(OpenXmlCompositeElement content) + private void ProcessNode(OpenXmlPartRootElement rootElement) { #if DEBUG Console.WriteLine("----------- Original --------"); - Console.WriteLine(content.ToPrettyPrintXml()); + Console.WriteLine(rootElement.ToPrettyPrintXml()); #endif - PreProcess(content); + PreProcess(rootElement); - DocxTemplate.IsolateAndMergeTextTemplateMarkers(content); + DocxTemplate.IsolateAndMergeTextTemplateMarkers(rootElement); #if DEBUG Console.WriteLine("----------- Isolate Texts --------"); - Console.WriteLine(content.ToPrettyPrintXml()); + Console.WriteLine(rootElement.ToPrettyPrintXml()); #endif - var loops = ExpandLoops(content); + var loops = ExpandLoops(rootElement); #if DEBUG Console.WriteLine("----------- After Loops --------"); - Console.WriteLine(content.ToPrettyPrintXml()); + Console.WriteLine(rootElement.ToPrettyPrintXml()); #endif - m_variableReplacer.ReplaceVariables(content); + m_variableReplacer.ReplaceVariables(rootElement); foreach (var loop in loops) { - loop.Expand(m_models, content); + loop.Expand(m_models, rootElement); } - Cleanup(content); + Cleanup(rootElement); #if DEBUG Console.WriteLine("----------- Completed --------"); - Console.WriteLine(content.ToPrettyPrintXml()); + Console.WriteLine(rootElement.ToPrettyPrintXml()); #endif } @@ -205,7 +206,7 @@ private static void Cleanup(OpenXmlCompositeElement element) } } - private IReadOnlyCollection ExpandLoops(OpenXmlCompositeElement element) + private IReadOnlyCollection ExpandLoops(OpenXmlPartRootElement element) { // TODO: store metadata for tag in cache From 8ee47f74646cca305da0fdc9b6c12a551d71a7ae Mon Sep 17 00:00:00 2001 From: Manuel Amstutz Date: Wed, 24 Jan 2024 18:31:14 +0100 Subject: [PATCH 04/14] Fix: HTML and image formatter working now inside loop --- DocxTemplater.Test/DocxTemplateTest.cs | 25 ++++++++++++++++++++- DocxTemplater/Blocks/ConditionalBlock.cs | 6 +++-- DocxTemplater/Blocks/ContentBlock.cs | 16 +++---------- DocxTemplater/Blocks/DynamicTableBlock.cs | 6 +++-- DocxTemplater/Blocks/LoopBlock.cs | 5 +++-- DocxTemplater/Formatter/HtmlFormatter.cs | 2 +- DocxTemplater/Formatter/VariableReplacer.cs | 8 +++++++ 7 files changed, 47 insertions(+), 21 deletions(-) diff --git a/DocxTemplater.Test/DocxTemplateTest.cs b/DocxTemplater.Test/DocxTemplateTest.cs index c39c39e..7d42ddd 100644 --- a/DocxTemplater.Test/DocxTemplateTest.cs +++ b/DocxTemplater.Test/DocxTemplateTest.cs @@ -122,7 +122,7 @@ public void MissingVariableThrows() } [TestCase("

Test

", "

Test

")] - [TestCase("

Test

","

Test

")] + [TestCase("

Test

", "

Test

")] [TestCase("

Test

", "

Test

")] [TestCase("Test", "Test")] [TestCase("foo
Test", "foo
Test")] @@ -155,6 +155,29 @@ public void HtmlIsAlwaysEnclosedWithHtmlTags(string html, string expexted) // check html part contains html; } + [Test] + public void InsertHtmlInLoop() + { + using var memStream = new MemoryStream(); + using var wpDocument = WordprocessingDocument.Create(memStream, WordprocessingDocumentType.Document); + MainDocumentPart mainPart = wpDocument.AddMainDocumentPart(); + mainPart.Document = new Document(new Body(new Paragraph(new Run(new Text("{{#Items}}{{Items}:html}{{/Items}}"))))); + wpDocument.Save(); + memStream.Position = 0; + + var docTemplate = new DocxTemplate(memStream); + docTemplate.BindModel("Items", new[] { "

Test1

", "

Test2

" }); + var result = docTemplate.Process(); + docTemplate.Validate(); + Assert.IsNotNull(result); + result.SaveAsFileAndOpenInWord(); + // check document contains 2 altChunks + var document = WordprocessingDocument.Open(result, false); + var body = document.MainDocumentPart.Document.Body; + var altChunks = body.Descendants().ToList(); + Assert.That(altChunks.Count, Is.EqualTo(2)); + } + [Test] public void MissingVariableWithSkipErrorHandling() { diff --git a/DocxTemplater/Blocks/ConditionalBlock.cs b/DocxTemplater/Blocks/ConditionalBlock.cs index 4a58647..0350619 100644 --- a/DocxTemplater/Blocks/ConditionalBlock.cs +++ b/DocxTemplater/Blocks/ConditionalBlock.cs @@ -1,6 +1,7 @@ using DocumentFormat.OpenXml; using DocxTemplater.Formatter; using System.Collections.Generic; +using System.Linq; namespace DocxTemplater.Blocks { @@ -23,8 +24,9 @@ public override void Expand(ModelDictionary models, OpenXmlElement parentNode) var content = conditionResult ? m_content : m_elseContent; if (content != null) { - var paragraphs = CreateBlockContentForCurrentVariableStack(content); - InsertContent(parentNode, paragraphs); + var cloned = content.Select(x => x.CloneNode(true)).ToList(); + InsertContent(parentNode, cloned); + m_variableReplacer.ReplaceVariables(cloned); ExpandChildBlocks(models, parentNode); } var element = m_leadingPart.GetElement(parentNode); diff --git a/DocxTemplater/Blocks/ContentBlock.cs b/DocxTemplater/Blocks/ContentBlock.cs index 8528a42..02fb5a7 100644 --- a/DocxTemplater/Blocks/ContentBlock.cs +++ b/DocxTemplater/Blocks/ContentBlock.cs @@ -25,8 +25,9 @@ public ContentBlock(VariableReplacer variableReplacer) public virtual void Expand(ModelDictionary models, OpenXmlElement parentNode) { - var paragraphs = CreateBlockContentForCurrentVariableStack(m_content); - InsertContent(parentNode, paragraphs); + var cloned = m_content.Select(x => x.CloneNode(true)).ToList(); + InsertContent(parentNode, cloned); + m_variableReplacer.ReplaceVariables(cloned); ExpandChildBlocks(models, parentNode); } @@ -38,17 +39,6 @@ protected void ExpandChildBlocks(ModelDictionary models, OpenXmlElement parentNo } } - protected IEnumerable CreateBlockContentForCurrentVariableStack(IReadOnlyCollection content) - { - var paragraphs = content.Select(x => - { - var cloned = x.CloneNode(true); - m_variableReplacer.ReplaceVariables(cloned); - return cloned; - }); - return paragraphs; - } - protected void InsertContent(OpenXmlElement parentNode, IEnumerable paragraphs) { var element = m_leadingPart.GetElement(parentNode); diff --git a/DocxTemplater/Blocks/DynamicTableBlock.cs b/DocxTemplater/Blocks/DynamicTableBlock.cs index 1aa5a51..0817c3a 100644 --- a/DocxTemplater/Blocks/DynamicTableBlock.cs +++ b/DocxTemplater/Blocks/DynamicTableBlock.cs @@ -41,8 +41,9 @@ public override void Expand(ModelDictionary models, OpenXmlElement parentNode) { models.RemoveLoopVariable(headersName); models.AddLoopVariable(headersName, header); - var clonedCell = CreateBlockContentForCurrentVariableStack(new List { headerCell }); + var clonedCell = headerCell.CloneNode(true); headerCell.InsertAfterSelf(clonedCell); + m_variableReplacer.ReplaceVariables(clonedCell); ExpandChildBlocks(models, parentNode); } models.RemoveLoopVariable(headersName); @@ -63,8 +64,9 @@ public override void Expand(ModelDictionary models, OpenXmlElement parentNode) { models.RemoveLoopVariable(columnsName); models.AddLoopVariable(columnsName, column); - var clonedCell = CreateBlockContentForCurrentVariableStack(new List { dataCell }).Single(); + var clonedCell = dataCell.CloneNode(true); insertion.InsertAfterSelf(clonedCell); + m_variableReplacer.ReplaceVariables(clonedCell); ExpandChildBlocks(models, parentNode); } insertion.Remove(); diff --git a/DocxTemplater/Blocks/LoopBlock.cs b/DocxTemplater/Blocks/LoopBlock.cs index be950fd..5859e08 100644 --- a/DocxTemplater/Blocks/LoopBlock.cs +++ b/DocxTemplater/Blocks/LoopBlock.cs @@ -28,8 +28,9 @@ public override void Expand(ModelDictionary models, OpenXmlElement parentNode) models.RemoveLoopVariable(m_collectionName); models.AddLoopVariable(m_collectionName, item); - var paragraphs = CreateBlockContentForCurrentVariableStack(m_content); - InsertContent(parentNode, paragraphs); + var cloned = m_content.Select(x => x.CloneNode(true)).ToList(); + InsertContent(parentNode, cloned); + m_variableReplacer.ReplaceVariables(cloned); ExpandChildBlocks(models, parentNode); } models.RemoveLoopVariable(m_collectionName); diff --git a/DocxTemplater/Formatter/HtmlFormatter.cs b/DocxTemplater/Formatter/HtmlFormatter.cs index 0eb4360..8817fd1 100644 --- a/DocxTemplater/Formatter/HtmlFormatter.cs +++ b/DocxTemplater/Formatter/HtmlFormatter.cs @@ -33,7 +33,7 @@ public void ApplyFormat(FormatterContext context, Text target) } if (!html.EndsWith("", StringComparison.CurrentCultureIgnoreCase)) { - html = html + ""; + html += ""; } var root = target.GetRoot(); string alternativeFormatImportPartId = null; diff --git a/DocxTemplater/Formatter/VariableReplacer.cs b/DocxTemplater/Formatter/VariableReplacer.cs index 901417e..38325ac 100644 --- a/DocxTemplater/Formatter/VariableReplacer.cs +++ b/DocxTemplater/Formatter/VariableReplacer.cs @@ -59,6 +59,14 @@ public void ApplyFormatter(PatternMatch patternMatch, object value, Text target) } + public void ReplaceVariables(IReadOnlyCollection content) + { + foreach (var element in content) + { + ReplaceVariables(element); + } + } + public void ReplaceVariables(OpenXmlElement cloned) { var variables = cloned.GetElementsWithMarker(PatternType.Variable).OfType().ToList(); From c21a6b09de121ee05217466d8962772036255319 Mon Sep 17 00:00:00 2001 From: Manuel Amstutz Date: Fri, 26 Jan 2024 08:22:56 +0100 Subject: [PATCH 05/14] Rename ModelDictionary to ModelLookup --- DocxTemplater.Test/ScriptCompilerTest.cs | 4 +-- DocxTemplater/Blocks/ConditionalBlock.cs | 2 +- DocxTemplater/Blocks/ContentBlock.cs | 4 +-- DocxTemplater/Blocks/DynamicTableBlock.cs | 2 +- DocxTemplater/Formatter/VariableReplacer.cs | 4 +-- .../{ModelDictionary.cs => ModelLookup.cs} | 36 ++++++++++++++++--- 6 files changed, 40 insertions(+), 12 deletions(-) rename DocxTemplater/{ModelDictionary.cs => ModelLookup.cs} (80%) diff --git a/DocxTemplater.Test/ScriptCompilerTest.cs b/DocxTemplater.Test/ScriptCompilerTest.cs index bc1c7c8..073608a 100644 --- a/DocxTemplater.Test/ScriptCompilerTest.cs +++ b/DocxTemplater.Test/ScriptCompilerTest.cs @@ -3,12 +3,12 @@ internal class ScriptCompilerTest { private ScriptCompiler m_scriptCompiler; - private ModelDictionary m_modelDictionary; + private ModelLookup m_modelDictionary; [SetUp] public void Setup() { - m_modelDictionary = new ModelDictionary(); + m_modelDictionary = new ModelLookup(); m_scriptCompiler = new ScriptCompiler(m_modelDictionary); } diff --git a/DocxTemplater/Blocks/ConditionalBlock.cs b/DocxTemplater/Blocks/ConditionalBlock.cs index 0350619..a96c1e2 100644 --- a/DocxTemplater/Blocks/ConditionalBlock.cs +++ b/DocxTemplater/Blocks/ConditionalBlock.cs @@ -18,7 +18,7 @@ public ConditionalBlock(string condition, VariableReplacer variableReplacer, Scr m_scriptCompiler = scriptCompiler; } - public override void Expand(ModelDictionary models, OpenXmlElement parentNode) + public override void Expand(ModelLookup models, OpenXmlElement parentNode) { var conditionResult = m_scriptCompiler.CompileScript(m_condition)(); var content = conditionResult ? m_content : m_elseContent; diff --git a/DocxTemplater/Blocks/ContentBlock.cs b/DocxTemplater/Blocks/ContentBlock.cs index 02fb5a7..b984634 100644 --- a/DocxTemplater/Blocks/ContentBlock.cs +++ b/DocxTemplater/Blocks/ContentBlock.cs @@ -23,7 +23,7 @@ public ContentBlock(VariableReplacer variableReplacer) public IReadOnlyCollection ChildBlocks => m_childBlocks; - public virtual void Expand(ModelDictionary models, OpenXmlElement parentNode) + public virtual void Expand(ModelLookup models, OpenXmlElement parentNode) { var cloned = m_content.Select(x => x.CloneNode(true)).ToList(); InsertContent(parentNode, cloned); @@ -31,7 +31,7 @@ public virtual void Expand(ModelDictionary models, OpenXmlElement parentNode) ExpandChildBlocks(models, parentNode); } - protected void ExpandChildBlocks(ModelDictionary models, OpenXmlElement parentNode) + protected void ExpandChildBlocks(ModelLookup models, OpenXmlElement parentNode) { foreach (var child in m_childBlocks) { diff --git a/DocxTemplater/Blocks/DynamicTableBlock.cs b/DocxTemplater/Blocks/DynamicTableBlock.cs index 0817c3a..dde2e3c 100644 --- a/DocxTemplater/Blocks/DynamicTableBlock.cs +++ b/DocxTemplater/Blocks/DynamicTableBlock.cs @@ -16,7 +16,7 @@ public DynamicTableBlock(string tablenName, VariableReplacer variableReplacer) m_tablenName = tablenName; } - public override void Expand(ModelDictionary models, OpenXmlElement parentNode) + public override void Expand(ModelLookup models, OpenXmlElement parentNode) { var model = models.GetValue(m_tablenName); if (model is IDynamicTable dynamicTable) diff --git a/DocxTemplater/Formatter/VariableReplacer.cs b/DocxTemplater/Formatter/VariableReplacer.cs index 38325ac..18edf00 100644 --- a/DocxTemplater/Formatter/VariableReplacer.cs +++ b/DocxTemplater/Formatter/VariableReplacer.cs @@ -8,11 +8,11 @@ namespace DocxTemplater.Formatter { internal class VariableReplacer { - private readonly ModelDictionary m_models; + private readonly ModelLookup m_models; private readonly ProcessSettings m_processSettings; private readonly List m_formatters; - public VariableReplacer(ModelDictionary models, ProcessSettings processSettings) + public VariableReplacer(ModelLookup models, ProcessSettings processSettings) { m_models = models; m_processSettings = processSettings; diff --git a/DocxTemplater/ModelDictionary.cs b/DocxTemplater/ModelLookup.cs similarity index 80% rename from DocxTemplater/ModelDictionary.cs rename to DocxTemplater/ModelLookup.cs index 5f236e8..7356fca 100644 --- a/DocxTemplater/ModelDictionary.cs +++ b/DocxTemplater/ModelLookup.cs @@ -1,23 +1,27 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Reflection; namespace DocxTemplater { - internal class ModelDictionary + internal class ModelLookup { private readonly Dictionary m_models; private readonly Dictionary m_loopVariables; + private readonly Stack m_loopVariablesStack; + private readonly Lazy m_defaultModelPrefix; private string m_rootModelPrefix; - public ModelDictionary() + public ModelLookup() { m_models = new Dictionary(); m_loopVariables = new Dictionary(); + m_loopVariablesStack = new Stack(); m_defaultModelPrefix = new Lazy(() => m_rootModelPrefix = m_models.Keys.FirstOrDefault()); } @@ -31,6 +35,7 @@ public void Add(string prefix, object model) public void AddLoopVariable(string name, object value) { name = AddPathPrefixInSingleModelMode(name); + m_loopVariablesStack.Push(value); m_loopVariables.Add(name, value); } @@ -43,7 +48,11 @@ public bool IsLoopVariable(string name) public void RemoveLoopVariable(string name) { name = AddPathPrefixInSingleModelMode(name); - m_loopVariables.Remove(name); + if (m_loopVariables.Remove(name)) + { + m_loopVariablesStack.Pop(); + } + Debug.Assert(m_loopVariables.Count == m_loopVariablesStack.Count); } private string AddPathPrefixInSingleModelMode(string name) @@ -64,6 +73,26 @@ private string AddPathPrefixInSingleModelMode(string name) public object GetValue(string variableName) { + object nextModel = null; + m_models.TryGetValue(variableName, out nextModel); + return nextModel; + } + + + public object GetValueOld(string variableName) + { + //remove and count leading dots + var leadingDotCount = variableName.TakeWhile(x => x == '.').Count(); + object model = null; + if (leadingDotCount > 0) + { + if(m_loopVariablesStack.Count < leadingDotCount) + { + throw new OpenXmlTemplateException($"Property not found:{variableName}"); + } + variableName = variableName[leadingDotCount..]; + model = m_loopVariablesStack.ElementAt(leadingDotCount - 1); + } var parts = variableName.Split('.'); var path = parts[0]; @@ -73,7 +102,6 @@ public object GetValue(string variableName) startIndex = -1; path = m_defaultModelPrefix.Value; } - object model = null; for (int i = startIndex; i < parts.Length; i++) { if (!m_loopVariables.TryGetValue(path, out var nextModel) && !m_models.TryGetValue(path, out nextModel)) From a9bf22345f83ba9d1383bc0c08b98b8d88547d58 Mon Sep 17 00:00:00 2001 From: Manuel Amstutz Date: Fri, 26 Jan 2024 13:03:29 +0100 Subject: [PATCH 06/14] Implicit Iterator --- DocxTemplater.Test/DocxTemplateTest.cs | 27 +++ DocxTemplater.Test/ModelLookupTest.cs | 67 +++++++ DocxTemplater.Test/ScriptCompilerTest.cs | 3 +- DocxTemplater/Blocks/DynamicTableBlock.cs | 10 +- DocxTemplater/Blocks/LoopBlock.cs | 8 +- DocxTemplater/DocxTemplate.cs | 4 +- DocxTemplater/ModelLookup.cs | 209 ++++++++++------------ DocxTemplater/ScriptCompiler.cs | 13 +- DocxTemplater/VariableScope.cs | 9 + 9 files changed, 220 insertions(+), 130 deletions(-) create mode 100644 DocxTemplater.Test/ModelLookupTest.cs create mode 100644 DocxTemplater/VariableScope.cs diff --git a/DocxTemplater.Test/DocxTemplateTest.cs b/DocxTemplater.Test/DocxTemplateTest.cs index 7d42ddd..882d192 100644 --- a/DocxTemplater.Test/DocxTemplateTest.cs +++ b/DocxTemplater.Test/DocxTemplateTest.cs @@ -121,6 +121,33 @@ public void MissingVariableThrows() Assert.Throws(() => docTemplate.Process()); } + [Test] + public void ImplicitIterator() + { + using var memStream = new MemoryStream(); + using var wpDocument = WordprocessingDocument.Create(memStream, WordprocessingDocumentType.Document); + MainDocumentPart mainPart = wpDocument.AddMainDocumentPart(); + mainPart.Document = new Document(new Body(new Paragraph(new Run(new Text("{{#ds}} {{.OuterVal}} {{#.Inner}} {{.InnerVal}} {{..OuterVal}} {{/.Inner}} {{/ds}}"))))); + wpDocument.Save(); + memStream.Position = 0; + + var docTemplate = new DocxTemplate(memStream); + var model = new[] + { + new { OuterVal = "OuterValue0", Inner = new[] { new { InnerVal = "InnerValue00" } } }, + new { OuterVal = "OuterValue1", Inner = new[] { new { InnerVal = "InnerValue10" } , new { InnerVal = "InnerValue11" } } }, + new { OuterVal = "OuterValue2", Inner = new[] { new { InnerVal = "InnerValue20" } , new { InnerVal = "InnerValue21" } } } + }; + docTemplate.BindModel("ds", model); + var result = docTemplate.Process(); + docTemplate.Validate(); + Assert.IsNotNull(result); + // check result text + var document = WordprocessingDocument.Open(result, false); + var body = document.MainDocumentPart.Document.Body; + Assert.That(body.InnerText, Is.EqualTo(" OuterValue0 InnerValue00 OuterValue0 OuterValue1 InnerValue10 OuterValue1 InnerValue11 OuterValue1 OuterValue2 InnerValue20 OuterValue2 InnerValue21 OuterValue2 ")); + } + [TestCase("

Test

", "

Test

")] [TestCase("

Test

", "

Test

")] [TestCase("

Test

", "

Test

")] diff --git a/DocxTemplater.Test/ModelLookupTest.cs b/DocxTemplater.Test/ModelLookupTest.cs new file mode 100644 index 0000000..cbb428f --- /dev/null +++ b/DocxTemplater.Test/ModelLookupTest.cs @@ -0,0 +1,67 @@ +namespace DocxTemplater.Test +{ + internal class ModelLookupTest + { + [Test] + public void LookupFirstModelDoesNotNeedAPrefix() + { + var modelLookup = new ModelLookup(); + modelLookup.Add("y", new {a = 6}); + modelLookup.Add("x", new {a = 55}); + modelLookup.Add("x.b", 42); + Assert.That(modelLookup.GetValue("x.a"), Is.EqualTo(55)); + Assert.That(modelLookup.GetValue("x.b"), Is.EqualTo(42)); + Assert.That(modelLookup.GetValue("y.a"), Is.EqualTo(6)); + Assert.That(modelLookup.GetValue("a"), Is.EqualTo(6)); // y is added first.. y does not need a prefix + } + + [Test] + public void LookupFirstModelWithNestedPath() + { + var modelLookup = new ModelLookup(); + modelLookup.Add("y.a.b", new {c = 6}); + Assert.That(modelLookup.GetValue("y.a.b.c"), Is.EqualTo(6)); + Assert.That(modelLookup.GetValue("a.b.c"), Is.EqualTo(6)); + + modelLookup.Add("x.aa.bb", new {c = 55}); + Assert.That(modelLookup.GetValue("x.aa.bb.c"), Is.EqualTo(55)); + Assert.Throws(() => modelLookup.GetValue("aa.bb.c")); + } + + [Test] + public void ScopeVariablesAndImplicitAccessWithDot() + { + var modelLookup = new ModelLookup(); + modelLookup.Add("y", new { a = 6 }); + Assert.That(modelLookup.GetValue("y.a"), Is.EqualTo(6)); + + using (var scope = modelLookup.OpenScope()) + { + scope.AddVariable("y", new {a = 55}); + Assert.That(modelLookup.GetValue("y.a"), Is.EqualTo(55)); + } + Assert.That(modelLookup.GetValue("y.a"), Is.EqualTo(6)); + + using (var outher = modelLookup.OpenScope()) + { + outher.AddVariable("y", new {a = 66}); + Assert.That(modelLookup.GetValue("y.a"), Is.EqualTo(66)); + using (var inner = modelLookup.OpenScope()) + { + inner.AddVariable("y", new {a = 77}); + Assert.That(modelLookup.GetValue("y.a"), Is.EqualTo(77)); + Assert.That(modelLookup.GetValue(".a"), Is.EqualTo(77)); + Assert.That(modelLookup.GetValue("..a"), Is.EqualTo(66)); + Assert.That(modelLookup.GetValue("...a"), Is.EqualTo(6)); + } + } + + // Add variable with leading dots to scope --> dots are removed + using (var scope = modelLookup.OpenScope()) + { + scope.AddVariable("...y", new {a = 55}); + Assert.That(modelLookup.GetValue("y.a"), Is.EqualTo(55)); + } + } + } +} diff --git a/DocxTemplater.Test/ScriptCompilerTest.cs b/DocxTemplater.Test/ScriptCompilerTest.cs index 073608a..fc039a5 100644 --- a/DocxTemplater.Test/ScriptCompilerTest.cs +++ b/DocxTemplater.Test/ScriptCompilerTest.cs @@ -31,7 +31,8 @@ public void WithMemberAccess() new { b = 6 } } }); - m_modelDictionary.AddLoopVariable("y.items", new + var blockScope = m_modelDictionary.OpenScope(); + blockScope.AddVariable("y.items", new { b = 5 }); diff --git a/DocxTemplater/Blocks/DynamicTableBlock.cs b/DocxTemplater/Blocks/DynamicTableBlock.cs index dde2e3c..bf7bcc9 100644 --- a/DocxTemplater/Blocks/DynamicTableBlock.cs +++ b/DocxTemplater/Blocks/DynamicTableBlock.cs @@ -39,14 +39,13 @@ public override void Expand(ModelLookup models, OpenXmlElement parentNode) // write headers foreach (var header in dynamicTable.Headers.Reverse()) { - models.RemoveLoopVariable(headersName); - models.AddLoopVariable(headersName, header); + using var headerScope = models.OpenScope(); + headerScope.AddVariable(headersName, header); var clonedCell = headerCell.CloneNode(true); headerCell.InsertAfterSelf(clonedCell); m_variableReplacer.ReplaceVariables(clonedCell); ExpandChildBlocks(models, parentNode); } - models.RemoveLoopVariable(headersName); // remove header cell headerCell.Remove(); @@ -62,15 +61,14 @@ public override void Expand(ModelLookup models, OpenXmlElement parentNode) var insertion = cellInsertionPoint.GetElement(clonedRow); foreach (var column in row.Reverse()) { - models.RemoveLoopVariable(columnsName); - models.AddLoopVariable(columnsName, column); + using var columnScope = models.OpenScope(); + columnScope.AddVariable(columnsName, column); var clonedCell = dataCell.CloneNode(true); insertion.InsertAfterSelf(clonedCell); m_variableReplacer.ReplaceVariables(clonedCell); ExpandChildBlocks(models, parentNode); } insertion.Remove(); - models.RemoveLoopVariable(columnsName); } dataRow.Remove(); dataCell.Remove(); diff --git a/DocxTemplater/Blocks/LoopBlock.cs b/DocxTemplater/Blocks/LoopBlock.cs index 5859e08..8ce3973 100644 --- a/DocxTemplater/Blocks/LoopBlock.cs +++ b/DocxTemplater/Blocks/LoopBlock.cs @@ -16,7 +16,7 @@ public LoopBlock(string collectionName, VariableReplacer variableReplacer) m_collectionName = collectionName; } - public override void Expand(ModelDictionary models, OpenXmlElement parentNode) + public override void Expand(ModelLookup models, OpenXmlElement parentNode) { var model = models.GetValue(m_collectionName); if (model is IEnumerable enumerable) @@ -25,15 +25,13 @@ public override void Expand(ModelDictionary models, OpenXmlElement parentNode) foreach (var item in enumerable.Reverse()) { count++; - models.RemoveLoopVariable(m_collectionName); - models.AddLoopVariable(m_collectionName, item); - + using var loopScope = models.OpenScope(); + loopScope.AddVariable(m_collectionName, item); var cloned = m_content.Select(x => x.CloneNode(true)).ToList(); InsertContent(parentNode, cloned); m_variableReplacer.ReplaceVariables(cloned); ExpandChildBlocks(models, parentNode); } - models.RemoveLoopVariable(m_collectionName); } else { diff --git a/DocxTemplater/DocxTemplate.cs b/DocxTemplater/DocxTemplate.cs index a7bbfca..3c2f04d 100644 --- a/DocxTemplater/DocxTemplate.cs +++ b/DocxTemplater/DocxTemplate.cs @@ -16,7 +16,7 @@ public sealed class DocxTemplate : IDisposable { private readonly Stream m_stream; private readonly WordprocessingDocument m_wpDocument; - private readonly ModelDictionary m_models; + private readonly ModelLookup m_models; private static readonly FileFormatVersions TargetMinimumVersion = FileFormatVersions.Office2010; @@ -42,7 +42,7 @@ public DocxTemplate(Stream docXStream, ProcessSettings settings = null) }; m_wpDocument = WordprocessingDocument.Open(m_stream, true, openSettings); - m_models = new ModelDictionary(); + m_models = new ModelLookup(); m_scriptCompiler = new ScriptCompiler(m_models); m_variableReplacer = new VariableReplacer(m_models, Settings); Processed = false; diff --git a/DocxTemplater/ModelLookup.cs b/DocxTemplater/ModelLookup.cs index 7356fca..ce3a476 100644 --- a/DocxTemplater/ModelLookup.cs +++ b/DocxTemplater/ModelLookup.cs @@ -9,160 +9,149 @@ namespace DocxTemplater { internal class ModelLookup { - private readonly Dictionary m_models; - private readonly Dictionary m_loopVariables; - private readonly Stack m_loopVariablesStack; - - private readonly Lazy m_defaultModelPrefix; - - private string m_rootModelPrefix; + private readonly Dictionary m_rootScope; + private readonly Stack> m_blockScopes; public ModelLookup() { - m_models = new Dictionary(); - m_loopVariables = new Dictionary(); - m_loopVariablesStack = new Stack(); - m_defaultModelPrefix = new Lazy(() => m_rootModelPrefix = m_models.Keys.FirstOrDefault()); + m_rootScope = new Dictionary(); + m_blockScopes = new Stack>(); + m_blockScopes.Push(m_rootScope); } - public IReadOnlyDictionary Models => m_models; + public IReadOnlyDictionary Models => m_rootScope; public void Add(string prefix, object model) { - m_models.Add(prefix, model); + m_rootScope.Add(prefix, model); } - public void AddLoopVariable(string name, object value) + public IVariableScope OpenScope() { - name = AddPathPrefixInSingleModelMode(name); - m_loopVariablesStack.Push(value); - m_loopVariables.Add(name, value); + return new VariableScope(m_blockScopes); } public bool IsLoopVariable(string name) { - name = AddPathPrefixInSingleModelMode(name); - return m_loopVariables.ContainsKey(name); - } - - public void RemoveLoopVariable(string name) - { - name = AddPathPrefixInSingleModelMode(name); - if (m_loopVariables.Remove(name)) - { - m_loopVariablesStack.Pop(); - } - Debug.Assert(m_loopVariables.Count == m_loopVariablesStack.Count); + return m_blockScopes.Peek().ContainsKey(name) && m_blockScopes.Count > 1; } - private string AddPathPrefixInSingleModelMode(string name) + public object GetValue(string variableName) { - var dotIndex = name.IndexOf('.'); - if (dotIndex == -1 || !m_models.ContainsKey(name[..dotIndex])) + var leadingDotsCount = variableName.TakeWhile(x => x == '.').Count(); + variableName = variableName[leadingDotsCount..]; + int partIndex = 0; + var parts = variableName.Split('.'); + object model = null; + string modelRootPath = variableName; + if (leadingDotsCount == 0) { - if (m_defaultModelPrefix.Value != null && - !name.Equals(m_rootModelPrefix, StringComparison.CurrentCultureIgnoreCase) && - !name.StartsWith(m_defaultModelPrefix.Value + ".", StringComparison.CurrentCultureIgnoreCase)) + model = SearchLongestPathInLookup(parts, out modelRootPath, out partIndex, 0); + if (model == null && m_rootScope.Count > 0) { - name = $"{m_rootModelPrefix}.{name}"; + var firstModelEntry = m_rootScope.First(); + // a.b.c.d and b.c.d.e ==> a.b.c.d.e + parts = firstModelEntry.Key.Split('.').Concat(variableName.Split('.')).Distinct().ToArray(); + model = SearchLongestPathInLookup(parts, out modelRootPath, out partIndex, 0); } - } - - return name; - } - - public object GetValue(string variableName) - { - object nextModel = null; - m_models.TryGetValue(variableName, out nextModel); - return nextModel; - } - - public object GetValueOld(string variableName) - { - //remove and count leading dots - var leadingDotCount = variableName.TakeWhile(x => x == '.').Count(); - object model = null; - if (leadingDotCount > 0) - { - if(m_loopVariablesStack.Count < leadingDotCount) + if (model == null) { - throw new OpenXmlTemplateException($"Property not found:{variableName}"); + throw new OpenXmlTemplateException($"Model {variableName} not found"); } - variableName = variableName[leadingDotCount..]; - model = m_loopVariablesStack.ElementAt(leadingDotCount - 1); } - var parts = variableName.Split('.'); - var path = parts[0]; - - int startIndex = 0; - if (!m_models.ContainsKey(path) && m_models.Count > 0) + else { - startIndex = -1; - path = m_defaultModelPrefix.Value; + modelRootPath = "parent scope"; + model = m_blockScopes.ElementAt(leadingDotsCount - 1).Values.FirstOrDefault(); } - for (int i = startIndex; i < parts.Length; i++) + if (model == null) { - if (!m_loopVariables.TryGetValue(path, out var nextModel) && !m_models.TryGetValue(path, out nextModel)) + throw new OpenXmlTemplateException($"Model {variableName} not found"); + } + + for (int i = partIndex; i < parts.Length; i++) + { + var propertyName = parts[i]; + if (model is ITemplateModel templateModel) + { + if (!templateModel.TryGetPropertyValue(propertyName, out model)) + { + throw new OpenXmlTemplateException($"Property {propertyName} not found in {modelRootPath}"); + } + } + else if (model is IDictionary dict) { - if (model == null) + if (!dict.TryGetValue(parts[i], out model)) { - throw new OpenXmlTemplateException($"Model {path} not found"); + throw new OpenXmlTemplateException($"Property {propertyName} not found in {modelRootPath}"); } - if (model is ITemplateModel templateModel) + } + else + { + var property = model.GetType().GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.GetProperty | BindingFlags.Instance); + if (property != null) { - if (templateModel.TryGetPropertyValue(parts[i], out var value)) - { - model = value; - } - else - { - throw new OpenXmlTemplateException($"Property {parts[i]} not found in {path}"); - } + model = property.GetValue(model); } - else if (model is IDictionary dict) + else if (model is ICollection) { - if (dict.TryGetValue(parts[i], out var value)) - { - model = value; - } - else - { - throw new OpenXmlTemplateException($"Property {parts[i]} not found in {path}"); - } + throw new OpenXmlTemplateException($"Property {propertyName} on collection {modelRootPath} not found - is collection start missing? '#{variableName}'"); } else { - var property = model.GetType().GetProperty(parts[i], BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.GetProperty | BindingFlags.Instance); - if (property != null) - { - model = property.GetValue(model); - } - else if (model is ICollection) - { - throw new OpenXmlTemplateException($"Property {parts[i]} on collection {path} not found - is collection start missing? '#{variableName}'"); - } - else - { - throw new OpenXmlTemplateException($"Property {parts[i]} not found in {parts[Math.Max(i - 1, 0)]}"); - } + throw new OpenXmlTemplateException($"Property {propertyName} not found in {modelRootPath}"); } } - else + } + return model; + } + + private object SearchLongestPathInLookup(string[] parts, out string modelRootPath, out int partIndex, int startScopeIndex) + { + modelRootPath = null; + partIndex = parts.Length; + foreach (Dictionary scope in m_blockScopes.Skip(startScopeIndex)) + { + partIndex = parts.Length; + // search the longest path in the lookup + for (; partIndex > 0; partIndex--) { - model = nextModel; - if (path == variableName) + modelRootPath = string.Join('.', parts[..partIndex]); + if (scope.TryGetValue(modelRootPath, out var model)) { - break; + return model; } } - if (i + 1 < parts.Length) - { - path = $"{path}.{parts[i + 1]}"; - } } - return model; + return null; + } + + + internal class VariableScope : IVariableScope + { + private readonly Dictionary m_scope; + private readonly Stack> m_scopeStack; + + public VariableScope(Stack> scopeStack) + { + m_scopeStack = scopeStack; + m_scope = new Dictionary(); + scopeStack.Push(m_scope); + } + + public void AddVariable(string name, object value) + { + // remove leading dots + name = name.TrimStart('.'); + Debug.Assert(m_scopeStack.Count > 1, "Added Block variable in root scope"); + m_scope.Add(name, value); + } + + public void Dispose() + { + m_scopeStack.Pop(); + } } } } diff --git a/DocxTemplater/ScriptCompiler.cs b/DocxTemplater/ScriptCompiler.cs index 8a0a5b1..7d4d293 100644 --- a/DocxTemplater/ScriptCompiler.cs +++ b/DocxTemplater/ScriptCompiler.cs @@ -1,4 +1,5 @@ -using DynamicExpresso; +using DocumentFormat.OpenXml.Vml; +using DynamicExpresso; using System; using System.Dynamic; @@ -6,9 +7,9 @@ namespace DocxTemplater { internal class ScriptCompiler { - private readonly ModelDictionary m_modelDictionary; + private readonly ModelLookup m_modelDictionary; - public ScriptCompiler(ModelDictionary modelDictionary) + public ScriptCompiler(ModelLookup modelDictionary) { this.m_modelDictionary = modelDictionary; } @@ -34,10 +35,10 @@ public Func CompileScript(string scriptAsString) private class ModelVariable : DynamicObject { - private readonly ModelDictionary m_modelDictionary; + private readonly ModelLookup m_modelDictionary; private readonly string m_rootName; - public ModelVariable(ModelDictionary modelDictionary, string rootName) + public ModelVariable(ModelLookup modelDictionary, string rootName) { m_modelDictionary = modelDictionary; m_rootName = rootName; @@ -49,7 +50,7 @@ public override bool TryGetMember(GetMemberBinder binder, out object result) { var name = m_rootName + "." + binder.Name; result = m_modelDictionary.GetValue(name); - if (m_modelDictionary.IsLoopVariable(name)) + if (result != null && !result.GetType().IsPrimitive) { result = new ModelVariable(m_modelDictionary, name); } diff --git a/DocxTemplater/VariableScope.cs b/DocxTemplater/VariableScope.cs new file mode 100644 index 0000000..f7cbe70 --- /dev/null +++ b/DocxTemplater/VariableScope.cs @@ -0,0 +1,9 @@ +using System; + +namespace DocxTemplater +{ + internal interface IVariableScope : IDisposable + { + void AddVariable(string name, object value); + } +} From cb04c0d1bec5f4e83cc2fe938979aff32510d157 Mon Sep 17 00:00:00 2001 From: Manuel Amstutz Date: Fri, 26 Jan 2024 14:33:19 +0100 Subject: [PATCH 07/14] Format code --- DocxTemplater.Test/ModelLookupTest.cs | 26 ++++++++++++++------------ DocxTemplater/ModelLookup.cs | 16 +++++----------- DocxTemplater/ScriptCompiler.cs | 3 +-- 3 files changed, 20 insertions(+), 25 deletions(-) diff --git a/DocxTemplater.Test/ModelLookupTest.cs b/DocxTemplater.Test/ModelLookupTest.cs index cbb428f..3939eba 100644 --- a/DocxTemplater.Test/ModelLookupTest.cs +++ b/DocxTemplater.Test/ModelLookupTest.cs @@ -6,8 +6,8 @@ internal class ModelLookupTest public void LookupFirstModelDoesNotNeedAPrefix() { var modelLookup = new ModelLookup(); - modelLookup.Add("y", new {a = 6}); - modelLookup.Add("x", new {a = 55}); + modelLookup.Add("y", new { a = 6 }); + modelLookup.Add("x", new { a = 55 }); modelLookup.Add("x.b", 42); Assert.That(modelLookup.GetValue("x.a"), Is.EqualTo(55)); Assert.That(modelLookup.GetValue("x.b"), Is.EqualTo(42)); @@ -19,11 +19,11 @@ public void LookupFirstModelDoesNotNeedAPrefix() public void LookupFirstModelWithNestedPath() { var modelLookup = new ModelLookup(); - modelLookup.Add("y.a.b", new {c = 6}); + modelLookup.Add("y.a.b", new { c = 6 }); Assert.That(modelLookup.GetValue("y.a.b.c"), Is.EqualTo(6)); Assert.That(modelLookup.GetValue("a.b.c"), Is.EqualTo(6)); - modelLookup.Add("x.aa.bb", new {c = 55}); + modelLookup.Add("x.aa.bb", new { c = 55 }); Assert.That(modelLookup.GetValue("x.aa.bb.c"), Is.EqualTo(55)); Assert.Throws(() => modelLookup.GetValue("aa.bb.c")); } @@ -37,29 +37,31 @@ public void ScopeVariablesAndImplicitAccessWithDot() using (var scope = modelLookup.OpenScope()) { - scope.AddVariable("y", new {a = 55}); + scope.AddVariable("y", new { a = 55 }); Assert.That(modelLookup.GetValue("y.a"), Is.EqualTo(55)); } Assert.That(modelLookup.GetValue("y.a"), Is.EqualTo(6)); using (var outher = modelLookup.OpenScope()) { - outher.AddVariable("y", new {a = 66}); - Assert.That(modelLookup.GetValue("y.a"), Is.EqualTo(66)); - using (var inner = modelLookup.OpenScope()) - { - inner.AddVariable("y", new {a = 77}); + outher.AddVariable("y", new { a = 66 }); + Assert.That(modelLookup.GetValue("y.a"), Is.EqualTo(66)); +#pragma warning disable IDE0063 // Use simple 'using' statement + using (var inner = modelLookup.OpenScope()) + { + inner.AddVariable("y", new { a = 77 }); Assert.That(modelLookup.GetValue("y.a"), Is.EqualTo(77)); Assert.That(modelLookup.GetValue(".a"), Is.EqualTo(77)); Assert.That(modelLookup.GetValue("..a"), Is.EqualTo(66)); Assert.That(modelLookup.GetValue("...a"), Is.EqualTo(6)); - } + } +#pragma warning restore IDE0063 // Use simple 'using' statement } // Add variable with leading dots to scope --> dots are removed using (var scope = modelLookup.OpenScope()) { - scope.AddVariable("...y", new {a = 55}); + scope.AddVariable("...y", new { a = 55 }); Assert.That(modelLookup.GetValue("y.a"), Is.EqualTo(55)); } } diff --git a/DocxTemplater/ModelLookup.cs b/DocxTemplater/ModelLookup.cs index ce3a476..3cef1e8 100644 --- a/DocxTemplater/ModelLookup.cs +++ b/DocxTemplater/ModelLookup.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections; +using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -14,9 +13,9 @@ internal class ModelLookup public ModelLookup() { - m_rootScope = new Dictionary(); - m_blockScopes = new Stack>(); - m_blockScopes.Push(m_rootScope); + m_rootScope = new Dictionary(); + m_blockScopes = new Stack>(); + m_blockScopes.Push(m_rootScope); } public IReadOnlyDictionary Models => m_rootScope; @@ -28,12 +27,7 @@ public void Add(string prefix, object model) public IVariableScope OpenScope() { - return new VariableScope(m_blockScopes); - } - - public bool IsLoopVariable(string name) - { - return m_blockScopes.Peek().ContainsKey(name) && m_blockScopes.Count > 1; + return new VariableScope(m_blockScopes); } public object GetValue(string variableName) diff --git a/DocxTemplater/ScriptCompiler.cs b/DocxTemplater/ScriptCompiler.cs index 7d4d293..b989088 100644 --- a/DocxTemplater/ScriptCompiler.cs +++ b/DocxTemplater/ScriptCompiler.cs @@ -1,5 +1,4 @@ -using DocumentFormat.OpenXml.Vml; -using DynamicExpresso; +using DynamicExpresso; using System; using System.Dynamic; From d8801724e47d7796e1cf2b565a6408f3c23376e3 Mon Sep 17 00:00:00 2001 From: Manuel Amstutz Date: Fri, 26 Jan 2024 16:29:23 +0100 Subject: [PATCH 08/14] Conditions have to be written as {?{condition}} --- DocxTemplater.Test/DocxTemplateTest.cs | 17 ++++++++++------- DocxTemplater.Test/PatternMatcherTest.cs | 13 +++++++++---- DocxTemplater/ModelLookup.cs | 5 +++++ DocxTemplater/PatterMatcher.cs | 15 +++++++-------- 4 files changed, 31 insertions(+), 19 deletions(-) diff --git a/DocxTemplater.Test/DocxTemplateTest.cs b/DocxTemplater.Test/DocxTemplateTest.cs index 882d192..b5feb38 100644 --- a/DocxTemplater.Test/DocxTemplateTest.cs +++ b/DocxTemplater.Test/DocxTemplateTest.cs @@ -233,7 +233,7 @@ public void LoopStartAndEndTagsAreRemoved() mainPart.Document = new Document(new Body( new Paragraph(new Run(new Text("Text123"))), new Paragraph(new Run(new Text("{{#ds.Items}}"))), - new Paragraph(new Run(new Text("{{Items.Name}} {{Items.Price < 6}} less than 6 {{else}} more than 6{{/}}"))), + new Paragraph(new Run(new Text("{{Items.Name}} {?{Items.Price < 6}} less than 6 {{else}} more than 6{{/}}"))), new Paragraph(new Run(new Text("{{/ds.Items}}"))), new Paragraph(new Run(new Text("Text456"))) )); @@ -260,16 +260,19 @@ public void ConditionsWithAndWithoutPrefix() using var wpDocument = WordprocessingDocument.Create(memStream, WordprocessingDocumentType.Document); MainDocumentPart mainPart = wpDocument.AddMainDocumentPart(); mainPart.Document = new Document(new Body( - new Paragraph(new Run(new Text("{{ Test > 5 }}Test1{{ else }}else1{{ / }}"))), - new Paragraph(new Run(new Text("{{ds.Test > 5}}Test2{{else}}else2{{/}}"))), - new Paragraph(new Run(new Text("{{ds2.Test > 5}}Test3{{else}}else3{{/}}"))) - + new Paragraph(new Run(new Text("{?{ Test > 5 }}Test1{{ else }}else1{{ / }}"))), + new Paragraph(new Run(new Text("{?{ ds.Test > 5}}Test2{{else}}else2{{/}}"))), + new Paragraph(new Run(new Text("{?{ ds2.Test > 5}}Test3{{else}}else3{{/}}"))), + new Paragraph(new Run(new Text("{?{ds3.MyBool}}Test4{{e}}else4{{/}}"))), + new Paragraph(new Run(new Text("{?{!ds4.MyBool}}Test5{{e}}else4{{/}}"))) )); wpDocument.Save(); memStream.Position = 0; var docTemplate = new DocxTemplate(memStream); docTemplate.BindModel("ds", new { Test = 6 }); docTemplate.BindModel("ds2", new { Test = 6 }); + docTemplate.BindModel("ds3", new { MyBool = true }); + docTemplate.BindModel("ds4", new { MyBool = false }); var result = docTemplate.Process(); docTemplate.Validate(); Assert.IsNotNull(result); @@ -279,7 +282,7 @@ public void ConditionsWithAndWithoutPrefix() // check result text var document = WordprocessingDocument.Open(result, false); var body = document.MainDocumentPart.Document.Body; - Assert.That(body.InnerText, Is.EqualTo("Test1Test2Test3")); + Assert.That(body.InnerText, Is.EqualTo("Test1Test2Test3Test4Test5")); } [Test] @@ -414,7 +417,7 @@ public void BindCollection() new Run(new Text("{{Items.Value}}")), // --> same as ds.Items.Value new Run(new Text("{{ds.Items.InnerCollection.Name}}")), new Run(new Text("{{Items.InnerCollection.InnerValue}}")), // --> same as ds.Items.InnerCollection.InnerValue - new Run(new Text("{{ds.Items.InnerCollection.NumericValue > 0 }}I'm only here if NumericValue is greater than 0 - {{ds.Items.InnerCollection.InnerValue}:toupper()}{{else}}I'm here if if this is not the case{{/}}")), + new Run(new Text("{?{.NumericValue > 0 }}I'm only here if NumericValue is greater than 0 - {{ds.Items.InnerCollection.InnerValue}:toupper()}{{e}}I'm here if if this is not the case{{/}}")), new Run(new Text("{{/ds.Items.InnerCollection}}")), new Run(new Text("{{/Items}}")), // --> same as ds.Items.InnerCollection new Run(new Text("will be replaced {{company.Name}}")) diff --git a/DocxTemplater.Test/PatternMatcherTest.cs b/DocxTemplater.Test/PatternMatcherTest.cs index ab998a4..93e1ba4 100644 --- a/DocxTemplater.Test/PatternMatcherTest.cs +++ b/DocxTemplater.Test/PatternMatcherTest.cs @@ -33,16 +33,21 @@ static IEnumerable TestPatternMatch_Cases() yield return new TestCaseData("{{/ds.items_foo}}").Returns(new[] { PatternType.CollectionEnd }).SetName("LoopEnd Underscore dots"); yield return new TestCaseData("{{/Items.InnerCollection}}").Returns(new[] { PatternType.CollectionEnd }); yield return new TestCaseData("{{#items.InnerCollection}}").Returns(new[] { PatternType.CollectionStart }); - yield return new TestCaseData("{{a.foo > 5}}").Returns(new[] { PatternType.Condition }); - yield return new TestCaseData("{{ a > 5 }}").Returns(new[] { PatternType.Condition }); - yield return new TestCaseData("{ { a > 5 } }").Returns(new[] { PatternType.Condition }); - yield return new TestCaseData("{{ a / 20 >= 12 }}").Returns(new[] { PatternType.Condition }); + yield return new TestCaseData("{?{ a.foo > 5}}").Returns(new[] { PatternType.Condition }); + yield return new TestCaseData("{?{ a > 5 }}").Returns(new[] { PatternType.Condition }); + yield return new TestCaseData("{? { a > 5 } }").Returns(new[] { PatternType.Condition }); + yield return new TestCaseData("{?{MyBool}}").Returns(new[] { PatternType.Condition }); + yield return new TestCaseData("{?{!MyBool}}").Returns(new[] { PatternType.Condition }); + yield return new TestCaseData("{ ? { MyBool}}").Returns(new[] { PatternType.Condition }); + yield return new TestCaseData("{?{ a / 20 >= 12 }}").Returns(new[] { PatternType.Condition }); yield return new TestCaseData("{{var}:F(d)}").Returns(new[] { PatternType.Variable }); yield return new TestCaseData("{{ds.foo.var}:F(d)}").Returns(new[] { PatternType.Variable }).SetName("Variable with dot"); yield return new TestCaseData("{{ds.foo_blubb.var}:F(d)}").Returns(new[] { PatternType.Variable }).SetName("Variable with underscore"); yield return new TestCaseData("{{var}:toupper}").Returns(new[] { PatternType.Variable }); yield return new TestCaseData("{{else}}").Returns(new[] { PatternType.ConditionElse }); yield return new TestCaseData("{{ else }}").Returns(new[] { PatternType.ConditionElse }); + yield return new TestCaseData("{{ e }}").Returns(new[] { PatternType.ConditionElse }); + yield return new TestCaseData("{{e}}").Returns(new[] { PatternType.ConditionElse }); yield return new TestCaseData("{{var}:format(a,b)}").Returns(new[] { PatternType.Variable }) .SetName("Multiple Arguments"); yield return new TestCaseData("{{/}}").Returns(new[] { PatternType.ConditionEnd }); diff --git a/DocxTemplater/ModelLookup.cs b/DocxTemplater/ModelLookup.cs index 3cef1e8..7c3b3b9 100644 --- a/DocxTemplater/ModelLookup.cs +++ b/DocxTemplater/ModelLookup.cs @@ -147,5 +147,10 @@ public void Dispose() m_scopeStack.Pop(); } } + + public object GetScopeParentLevel(int parentLevel) + { + return m_blockScopes.ElementAt(parentLevel).Values.FirstOrDefault(); + } } } diff --git a/DocxTemplater/PatterMatcher.cs b/DocxTemplater/PatterMatcher.cs index 02d7003..26040e7 100644 --- a/DocxTemplater/PatterMatcher.cs +++ b/DocxTemplater/PatterMatcher.cs @@ -20,16 +20,15 @@ internal static class PatternMatcher {{images}:foo(arg1,arg2)} -- variable with formatter and arguments */ - private static readonly Regex PatternRegex = new(@"\{\s*\{\s* + private static readonly Regex PatternRegex = new(@"\{\s*(?\?\s*)?\{\s* # a leading ? indicates a condition (?: - (?else) | - (?: - (?[\/\#])? #prefix + (?(?:else)|(?:e)) | + (?(condMarker) # if condition marker is set, we expect a condition + (?[a-zA-Z0-9+\-*\/><=\s\.\!]+)? #condition name (without brackets) + | (?: - (?[a-zA-Z0-9\._]+) #variable name - | #or - (?[a-zA-Z0-9+\-*\/><=\s\.]{2,}) #condition - )? + (?[\/\#])?(?[a-zA-Z0-9\._]+)? #variable name + ) ) ) \s*\} From 488cd26d9effc247521d360f7cbc7aade19adca7 Mon Sep 17 00:00:00 2001 From: Manuel Amstutz Date: Fri, 26 Jan 2024 16:29:50 +0100 Subject: [PATCH 09/14] Implicit iterator support for script compiler --- DocxTemplater/ScriptCompiler.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/DocxTemplater/ScriptCompiler.cs b/DocxTemplater/ScriptCompiler.cs index b989088..9701d19 100644 --- a/DocxTemplater/ScriptCompiler.cs +++ b/DocxTemplater/ScriptCompiler.cs @@ -1,12 +1,14 @@ using DynamicExpresso; using System; using System.Dynamic; +using System.Text.RegularExpressions; namespace DocxTemplater { internal class ScriptCompiler { private readonly ModelLookup m_modelDictionary; + private static readonly Regex RegexWordStartingWithDot = new(@"^(\.+)([a-zA-z0-9_]+)", RegexOptions.Compiled); public ScriptCompiler(ModelLookup modelDictionary) { @@ -15,7 +17,9 @@ public ScriptCompiler(ModelLookup modelDictionary) public Func CompileScript(string scriptAsString) { + // replace replace leading dots (implicit scope) with variables var interpreter = new Interpreter(); + scriptAsString = RegexWordStartingWithDot.Replace(scriptAsString, (m) => OnVariableReplace(m, interpreter)); var identifiers = interpreter.DetectIdentifiers(scriptAsString); foreach (var identifier in identifiers.UnknownIdentifiers) { @@ -32,6 +36,15 @@ public Func CompileScript(string scriptAsString) return interpreter.ParseAsDelegate>(scriptAsString); } + private string OnVariableReplace(Match match, Interpreter interpreter) + { + var dotCount = match.Groups[1].Length; + var scope = m_modelDictionary.GetScopeParentLevel(dotCount - 1); + var varName = $"__s{dotCount}_"; // choose a variable name that is unlikely to be used by the user + interpreter.SetVariable(varName, scope); + return $"{varName}.{match.Groups[2].Value}"; + } + private class ModelVariable : DynamicObject { private readonly ModelLookup m_modelDictionary; From 9349024688d438c2b1b3bcec46371db66f41b7a2 Mon Sep 17 00:00:00 2001 From: Manuel Amstutz Date: Fri, 26 Jan 2024 17:09:10 +0100 Subject: [PATCH 10/14] Collection Separator --- DocxTemplater.Test/DocxTemplateTest.cs | 21 ++++++++++++++ DocxTemplater.Test/PatternMatcherTest.cs | 6 ++-- DocxTemplater/Blocks/LoopBlock.cs | 28 ++++++++++++++++-- DocxTemplater/DocxTemplate.cs | 36 +++++++++++++++--------- DocxTemplater/ModelLookup.cs | 4 +++ DocxTemplater/PatterMatcher.cs | 19 +++++++++---- DocxTemplater/PatternType.cs | 1 + 7 files changed, 91 insertions(+), 24 deletions(-) diff --git a/DocxTemplater.Test/DocxTemplateTest.cs b/DocxTemplater.Test/DocxTemplateTest.cs index b5feb38..26a3f7b 100644 --- a/DocxTemplater.Test/DocxTemplateTest.cs +++ b/DocxTemplater.Test/DocxTemplateTest.cs @@ -253,6 +253,27 @@ public void LoopStartAndEndTagsAreRemoved() Assert.That(body.Descendants().Count(), Is.EqualTo(4)); } + + [Test] + public void CollectionSeparatorTest() + { + using var memStream = new MemoryStream(); + using var wpDocument = WordprocessingDocument.Create(memStream, WordprocessingDocumentType.Document); + MainDocumentPart mainPart = wpDocument.AddMainDocumentPart(); + mainPart.Document = new Document(new Body(new Paragraph(new Run(new Text("{{#ds}}{{.}}{{:s:}},{{/ds}}"))))); + wpDocument.Save(); + memStream.Position = 0; + var docTemplate = new DocxTemplate(memStream); + docTemplate.BindModel("ds", new[] { "Item1", "Item2", "Item3" }); + var result = docTemplate.Process(); + docTemplate.Validate(); + Assert.IsNotNull(result); + // check result text + var document = WordprocessingDocument.Open(result, false); + var body = document.MainDocumentPart.Document.Body; + Assert.That(body.InnerText, Is.EqualTo("Item1,Item2,Item3")); + } + [Test] public void ConditionsWithAndWithoutPrefix() { diff --git a/DocxTemplater.Test/PatternMatcherTest.cs b/DocxTemplater.Test/PatternMatcherTest.cs index 93e1ba4..978e9ba 100644 --- a/DocxTemplater.Test/PatternMatcherTest.cs +++ b/DocxTemplater.Test/PatternMatcherTest.cs @@ -46,8 +46,10 @@ static IEnumerable TestPatternMatch_Cases() yield return new TestCaseData("{{var}:toupper}").Returns(new[] { PatternType.Variable }); yield return new TestCaseData("{{else}}").Returns(new[] { PatternType.ConditionElse }); yield return new TestCaseData("{{ else }}").Returns(new[] { PatternType.ConditionElse }); - yield return new TestCaseData("{{ e }}").Returns(new[] { PatternType.ConditionElse }); - yield return new TestCaseData("{{e}}").Returns(new[] { PatternType.ConditionElse }); + yield return new TestCaseData("{{ : }}").Returns(new[] { PatternType.ConditionElse }); + yield return new TestCaseData("{{:}}").Returns(new[] { PatternType.ConditionElse }); + yield return new TestCaseData("{{:s:}}").Returns(new[] { PatternType.CollectionSeparator }); + yield return new TestCaseData("{{: s :}}").Returns(new[] { PatternType.CollectionSeparator }); yield return new TestCaseData("{{var}:format(a,b)}").Returns(new[] { PatternType.Variable }) .SetName("Multiple Arguments"); yield return new TestCaseData("{{/}}").Returns(new[] { PatternType.ConditionEnd }); diff --git a/DocxTemplater/Blocks/LoopBlock.cs b/DocxTemplater/Blocks/LoopBlock.cs index 8ce3973..e7a19a9 100644 --- a/DocxTemplater/Blocks/LoopBlock.cs +++ b/DocxTemplater/Blocks/LoopBlock.cs @@ -9,6 +9,7 @@ namespace DocxTemplater.Blocks internal class LoopBlock : ContentBlock { private readonly string m_collectionName; + private IReadOnlyCollection m_separatorBlock; public LoopBlock(string collectionName, VariableReplacer variableReplacer) : base(variableReplacer) @@ -21,16 +22,24 @@ public override void Expand(ModelLookup models, OpenXmlElement parentNode) var model = models.GetValue(m_collectionName); if (model is IEnumerable enumerable) { - int count = 0; - foreach (var item in enumerable.Reverse()) + var items = enumerable.Reverse().ToList(); + int counter = 0; + foreach (var item in items) { - count++; + counter++; using var loopScope = models.OpenScope(); loopScope.AddVariable(m_collectionName, item); var cloned = m_content.Select(x => x.CloneNode(true)).ToList(); InsertContent(parentNode, cloned); m_variableReplacer.ReplaceVariables(cloned); ExpandChildBlocks(models, parentNode); + if (counter < items.Count && m_separatorBlock != null) + { + var clonedSeparator = m_separatorBlock.Select(x => x.CloneNode(true)).ToList(); + InsertContent(parentNode, clonedSeparator); + m_variableReplacer.ReplaceVariables(clonedSeparator); + ExpandChildBlocks(models, parentNode); + } } } else @@ -39,6 +48,19 @@ public override void Expand(ModelLookup models, OpenXmlElement parentNode) } } + public override void SetContent(OpenXmlElement leadingPart, IReadOnlyCollection blockContent) + { + if (m_leadingPart == null) + { + base.SetContent(leadingPart, blockContent); + } + else + { + m_separatorBlock = blockContent; + leadingPart.RemoveWithEmptyParent(); + } + } + public override string ToString() { return $"LoopBlock: {m_collectionName}"; diff --git a/DocxTemplater/DocxTemplate.cs b/DocxTemplater/DocxTemplate.cs index 3c2f04d..7ab3147 100644 --- a/DocxTemplater/DocxTemplate.cs +++ b/DocxTemplater/DocxTemplate.cs @@ -228,6 +228,28 @@ private IReadOnlyCollection ExpandLoops(OpenXmlPartRootElement ele blockStack.Push((new LoopBlock(match.Variable, m_variableReplacer), match, text)); } } + else if (value == PatternType.CollectionSeparator) + { + var (block, patternMatch, matchedTextNode) = blockStack.Pop(); + if (block is not LoopBlock) + { + throw new OpenXmlTemplateException($"Separator in '{block}' is invalid"); + } + var loopContent = ExtractBlockContent(matchedTextNode, text, out var leadingPart); + block.SetContent(leadingPart, loopContent); + blockStack.Push((block, patternMatch, text)); // push same block again on Stack but with other text element + } + else if (value == PatternType.CollectionEnd) + { + var (block, patternMatch, matchedTextNode) = blockStack.Pop(); + if (patternMatch.Type != PatternType.CollectionStart) + { + throw new OpenXmlTemplateException($"'{block}' is not closed"); + } + var loopContent = ExtractBlockContent(matchedTextNode, text, out var leadingPart); + block.SetContent(leadingPart, loopContent); + blockStack.Peek().Block.AddInnerBlock(block); + } else if (value == PatternType.Condition) { var match = PatternMatcher.FindSyntaxPatterns(text.Text).Single(); @@ -238,7 +260,7 @@ private IReadOnlyCollection ExpandLoops(OpenXmlPartRootElement ele var (block, patternMatch, matchedTextNode) = blockStack.Pop(); if (block is not ConditionalBlock) { - throw new OpenXmlTemplateException($"'{block}' is not closed"); + throw new OpenXmlTemplateException($"else block in '{block}' is invalid"); } var loopContent = ExtractBlockContent(matchedTextNode, text, out var leadingPart); block.SetContent(leadingPart, loopContent); @@ -255,18 +277,6 @@ private IReadOnlyCollection ExpandLoops(OpenXmlPartRootElement ele block.SetContent(leadingPart, loopContent); blockStack.Peek().Block.AddInnerBlock(block); } - else if (value == PatternType.CollectionEnd) - { - var (block, patternMatch, matchedTextNode) = blockStack.Pop(); - if (patternMatch.Type != PatternType.CollectionStart) - { - throw new OpenXmlTemplateException($"'{block}' is not closed"); - } - var loopContent = ExtractBlockContent(matchedTextNode, text, out var leadingPart); - block.SetContent(leadingPart, loopContent); - blockStack.Peek().Block.AddInnerBlock(block); - } - } var (contentBlock, _, _) = blockStack.Pop(); return contentBlock.ChildBlocks; diff --git a/DocxTemplater/ModelLookup.cs b/DocxTemplater/ModelLookup.cs index 7c3b3b9..78942aa 100644 --- a/DocxTemplater/ModelLookup.cs +++ b/DocxTemplater/ModelLookup.cs @@ -58,6 +58,10 @@ public object GetValue(string variableName) { modelRootPath = "parent scope"; model = m_blockScopes.ElementAt(leadingDotsCount - 1).Values.FirstOrDefault(); + if(parts.Length == 1 && string.IsNullOrWhiteSpace(parts[0])) + { + return model; + } } if (model == null) { diff --git a/DocxTemplater/PatterMatcher.cs b/DocxTemplater/PatterMatcher.cs index 26040e7..d0930e3 100644 --- a/DocxTemplater/PatterMatcher.cs +++ b/DocxTemplater/PatterMatcher.cs @@ -22,7 +22,8 @@ internal static class PatternMatcher private static readonly Regex PatternRegex = new(@"\{\s*(?\?\s*)?\{\s* # a leading ? indicates a condition (?: - (?(?:else)|(?:e)) | + (?:\s*s\s*:) | + (?(?:else)|:) | (?(condMarker) # if condition marker is set, we expect a condition (?[a-zA-Z0-9+\-*\/><=\s\.\!]+)? #condition name (without brackets) | @@ -50,13 +51,15 @@ public static IEnumerable FindSyntaxPatterns(string text) var result = new List(matches.Count); foreach (Match match in matches) { - if (match.Groups["else"].Success) + if (match.Groups["separator"].Success) + { + result.Add(new PatternMatch(match, PatternType.CollectionSeparator, null, null, null, null, null, match.Index, match.Length)); + } + else if (match.Groups["else"].Success) { result.Add(new PatternMatch(match, PatternType.ConditionElse, null, null, null, null, null, match.Index, match.Length)); } - else - - if (match.Groups["condition"].Success) + else if (match.Groups["condition"].Success) { result.Add(new PatternMatch(match, PatternType.Condition, match.Groups["condition"].Value, null, null, null, null, match.Index, match.Length)); } @@ -75,12 +78,16 @@ public static IEnumerable FindSyntaxPatterns(string text) result.Add(new PatternMatch(match, PatternType.CollectionEnd, null, match.Groups["prefix"].Value, match.Groups["varname"].Value, match.Groups["formatter"].Value, match.Groups["arg"].Value.Split(','), match.Index, match.Length)); } } - else + else if(match.Groups["varname"].Success) { var argGroup = match.Groups["arg"]; var arguments = argGroup.Success ? argGroup.Captures.Select(x => x.Value).ToArray() : Array.Empty(); result.Add(new PatternMatch(match, PatternType.Variable, null, null, match.Groups["varname"].Value, match.Groups["formatter"].Value, arguments, match.Index, match.Length)); } + else + { + throw new OpenXmlTemplateException($"Invalid syntax '{match.Value}'"); + } } return result; } diff --git a/DocxTemplater/PatternType.cs b/DocxTemplater/PatternType.cs index 86e1d09..afbf10a 100644 --- a/DocxTemplater/PatternType.cs +++ b/DocxTemplater/PatternType.cs @@ -6,6 +6,7 @@ internal enum PatternType Condition, ConditionEnd, CollectionStart, + CollectionSeparator, CollectionEnd, Variable, ConditionElse From d783a4d2a395765a8bb8d8f8851ca0d07d0b9d1e Mon Sep 17 00:00:00 2001 From: Manuel Amstutz Date: Fri, 26 Jan 2024 17:14:39 +0100 Subject: [PATCH 11/14] Formatting --- DocxTemplater/ModelLookup.cs | 2 +- DocxTemplater/PatterMatcher.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/DocxTemplater/ModelLookup.cs b/DocxTemplater/ModelLookup.cs index 78942aa..caf8eff 100644 --- a/DocxTemplater/ModelLookup.cs +++ b/DocxTemplater/ModelLookup.cs @@ -58,7 +58,7 @@ public object GetValue(string variableName) { modelRootPath = "parent scope"; model = m_blockScopes.ElementAt(leadingDotsCount - 1).Values.FirstOrDefault(); - if(parts.Length == 1 && string.IsNullOrWhiteSpace(parts[0])) + if (parts.Length == 1 && string.IsNullOrWhiteSpace(parts[0])) { return model; } diff --git a/DocxTemplater/PatterMatcher.cs b/DocxTemplater/PatterMatcher.cs index d0930e3..13f523c 100644 --- a/DocxTemplater/PatterMatcher.cs +++ b/DocxTemplater/PatterMatcher.cs @@ -78,7 +78,7 @@ public static IEnumerable FindSyntaxPatterns(string text) result.Add(new PatternMatch(match, PatternType.CollectionEnd, null, match.Groups["prefix"].Value, match.Groups["varname"].Value, match.Groups["formatter"].Value, match.Groups["arg"].Value.Split(','), match.Index, match.Length)); } } - else if(match.Groups["varname"].Success) + else if (match.Groups["varname"].Success) { var argGroup = match.Groups["arg"]; var arguments = argGroup.Success ? argGroup.Captures.Select(x => x.Value).ToArray() : Array.Empty(); From a0e3b83b23695cdbfac027521e947c932e016871 Mon Sep 17 00:00:00 2001 From: Manuel Amstutz Date: Fri, 26 Jan 2024 17:19:43 +0100 Subject: [PATCH 12/14] Update Readme --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 72c3202..c8915d1 100644 --- a/README.md +++ b/README.md @@ -62,8 +62,11 @@ The syntax is case insensitive | Syntax | Desciption | |----------------|--------------------------| | {{SomeVar}} | Simple Variable replacement -| {{someVar > 5}}...{{else}}...{{/}} | Conditional blocks -| {{/Items}}...{\{Items.Name}} ... {{/Items}} | Text block bound to collection items +| {?{someVar > 5}}...{{:}}...{{/}} | Conditional blocks +| {{/Items}}...{{Items.Name}} ... {{/Items}} | Text block bound to collection of complex items +| {{/Items}}...{{.Name}} ... {{/Items}} | Dot notation - implicit iterator +| {{/Items}}...{{.}:toUpper} ... {{/Items}} | A list of string all upper case +| {{/Items}}{{.}}{{:s:}},{{/Items}} | A list of string comma separated | {{SomeString:ToUpper()}} | Variable with formatter to upper | {{SomeDate:Format("MM/dd/yyyy")}} | Date variable with formatting | {{SomeDate:F("MM/dd/yyyy")}} | Date variable with formatting - short syntax From 22d882d3550de8c53f0349caf6a4dcbf512a365d Mon Sep 17 00:00:00 2001 From: Manuel Amstutz Date: Fri, 26 Jan 2024 17:25:23 +0100 Subject: [PATCH 13/14] Update Readme --- README.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c8915d1..d4b7b47 100644 --- a/README.md +++ b/README.md @@ -110,15 +110,24 @@ will render a table row for each item in the collection | John | Developer| | Alice | CEO| +#### Separator + +If you want to render a separator between the items in the collection, you can use the separator syntax: +``` +{{#Items}} This text {{.Name}} is rendered for each element in the items collection {{:s:}} This is rendered between each elment {{/items}} +``` + + + ### Conditional Blocks Show or hide a given section depending on a condition: -**{{\}}** .. content .. **{{/}}** +**{?{\}}** .. content .. **{{/}}** All document content between the start and end tag is rendered only if the condition is met ``` -{{Item.Value >= 0}}Only visible if value is >= 0 {{/}} -{{else}}Otherwise this text is shown{{/}} +{?{Item.Value >= 0}}Only visible if value is >= 0 +{{:}}Otherwise this text is shown{{/}} ``` ## Formatters From 8974247aeb5aa503f8e5b4baf64eda64df6baf66 Mon Sep 17 00:00:00 2001 From: Manuel Amstutz Date: Fri, 26 Jan 2024 17:29:57 +0100 Subject: [PATCH 14/14] Adapted tests to new syntax --- DocxTemplater.Test/DocxTemplateTest.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/DocxTemplater.Test/DocxTemplateTest.cs b/DocxTemplater.Test/DocxTemplateTest.cs index 26a3f7b..6923e99 100644 --- a/DocxTemplater.Test/DocxTemplateTest.cs +++ b/DocxTemplater.Test/DocxTemplateTest.cs @@ -284,8 +284,8 @@ public void ConditionsWithAndWithoutPrefix() new Paragraph(new Run(new Text("{?{ Test > 5 }}Test1{{ else }}else1{{ / }}"))), new Paragraph(new Run(new Text("{?{ ds.Test > 5}}Test2{{else}}else2{{/}}"))), new Paragraph(new Run(new Text("{?{ ds2.Test > 5}}Test3{{else}}else3{{/}}"))), - new Paragraph(new Run(new Text("{?{ds3.MyBool}}Test4{{e}}else4{{/}}"))), - new Paragraph(new Run(new Text("{?{!ds4.MyBool}}Test5{{e}}else4{{/}}"))) + new Paragraph(new Run(new Text("{?{ds3.MyBool}}Test4{{:}}else4{{/}}"))), + new Paragraph(new Run(new Text("{?{!ds4.MyBool}}Test5{{:}}else4{{/}}"))) )); wpDocument.Save(); memStream.Position = 0; @@ -438,7 +438,7 @@ public void BindCollection() new Run(new Text("{{Items.Value}}")), // --> same as ds.Items.Value new Run(new Text("{{ds.Items.InnerCollection.Name}}")), new Run(new Text("{{Items.InnerCollection.InnerValue}}")), // --> same as ds.Items.InnerCollection.InnerValue - new Run(new Text("{?{.NumericValue > 0 }}I'm only here if NumericValue is greater than 0 - {{ds.Items.InnerCollection.InnerValue}:toupper()}{{e}}I'm here if if this is not the case{{/}}")), + new Run(new Text("{?{.NumericValue > 0 }}I'm only here if NumericValue is greater than 0 - {{ds.Items.InnerCollection.InnerValue}:toupper()}{{:}}I'm here if if this is not the case{{/}}")), new Run(new Text("{{/ds.Items.InnerCollection}}")), new Run(new Text("{{/Items}}")), // --> same as ds.Items.InnerCollection new Run(new Text("will be replaced {{company.Name}}"))