Skip to content

Commit

Permalink
Merge pull request #8 from Amberg/workitems/v2.0.0
Browse files Browse the repository at this point in the history
Workitems/v2.0.0
  • Loading branch information
Amberg authored Jan 26, 2024
2 parents 70f2733 + 8974247 commit 5640a5e
Show file tree
Hide file tree
Showing 20 changed files with 534 additions and 248 deletions.
123 changes: 115 additions & 8 deletions DocxTemplater.Test/DocxTemplateTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ namespace DocxTemplater.Test
{
internal class DocxTemplateTest
{

[Test]
public void DynamicTable()
{
Expand Down Expand Up @@ -122,6 +121,90 @@ public void MissingVariableThrows()
Assert.Throws<OpenXmlTemplateException>(() => 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("<html><body><h1>Test</h1></body></html>", "<html><body><h1>Test</h1></body></html>")]
[TestCase("<body><h1>Test</h1></body>", "<html><body><h1>Test</h1></body></html>")]
[TestCase("<h1>Test</h1>", "<html><h1>Test</h1></html>")]
[TestCase("Test", "<html>Test</html>")]
[TestCase("foo<br>Test", "<html>foo<br>Test</html>")]
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<AltChunk>().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 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[] { "<h1>Test1</h1>", "<h1>Test2</h1>" });
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<AltChunk>().ToList();
Assert.That(altChunks.Count, Is.EqualTo(2));
}

[Test]
public void MissingVariableWithSkipErrorHandling()
{
Expand Down Expand Up @@ -150,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")))
));
Expand All @@ -170,23 +253,47 @@ public void LoopStartAndEndTagsAreRemoved()
Assert.That(body.Descendants<Paragraph>().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()
{
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("{{ 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{{:}}else4{{/}}"))),
new Paragraph(new Run(new Text("{?{!ds4.MyBool}}Test5{{:}}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);
Expand All @@ -196,7 +303,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]
Expand Down Expand Up @@ -331,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("{{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()}{{:}}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}}"))
Expand Down
69 changes: 69 additions & 0 deletions DocxTemplater.Test/ModelLookupTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
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<OpenXmlTemplateException>(() => 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));
#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 });
Assert.That(modelLookup.GetValue("y.a"), Is.EqualTo(55));
}
}
}
}
15 changes: 11 additions & 4 deletions DocxTemplater.Test/PatternMatcherTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,23 @@ 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("{{ : }}").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 });
Expand Down
Binary file modified DocxTemplater.Test/Resources/DynamicTable.docx
Binary file not shown.
Binary file not shown.
7 changes: 4 additions & 3 deletions DocxTemplater.Test/ScriptCompilerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand All @@ -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
});
Expand Down
8 changes: 5 additions & 3 deletions DocxTemplater/Blocks/ConditionalBlock.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using DocumentFormat.OpenXml;
using DocxTemplater.Formatter;
using System.Collections.Generic;
using System.Linq;

namespace DocxTemplater.Blocks
{
Expand All @@ -17,14 +18,15 @@ 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;
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);
Expand Down
20 changes: 5 additions & 15 deletions DocxTemplater/Blocks/ContentBlock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,32 +23,22 @@ public ContentBlock(VariableReplacer variableReplacer)

public IReadOnlyCollection<ContentBlock> ChildBlocks => m_childBlocks;

public virtual void Expand(ModelDictionary models, OpenXmlElement parentNode)
public virtual void Expand(ModelLookup 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);
}

protected void ExpandChildBlocks(ModelDictionary models, OpenXmlElement parentNode)
protected void ExpandChildBlocks(ModelLookup models, OpenXmlElement parentNode)
{
foreach (var child in m_childBlocks)
{
child.Expand(models, parentNode);
}
}

protected IEnumerable<OpenXmlElement> CreateBlockContentForCurrentVariableStack(IReadOnlyCollection<OpenXmlElement> 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<OpenXmlElement> paragraphs)
{
var element = m_leadingPart.GetElement(parentNode);
Expand Down
Loading

0 comments on commit 5640a5e

Please sign in to comment.