Skip to content

Commit

Permalink
Merge pull request #6 from Amberg/workitems/v1.1.0
Browse files Browse the repository at this point in the history
Div Bug fixes / Improvements and Error Handling Configuration
  • Loading branch information
Amberg authored Jan 21, 2024
2 parents c5e738a + 5aba8c5 commit 70f2733
Show file tree
Hide file tree
Showing 21 changed files with 579 additions and 60 deletions.
252 changes: 248 additions & 4 deletions DocxTemplater.Test/DocxTemplateTest.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
using System.Collections;
using System.Globalization;
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;
using DocxTemplater.Images;
using System.Collections;
using System.Dynamic;
using System.Globalization;
using Bold = DocumentFormat.OpenXml.Wordprocessing.Bold;
using Paragraph = DocumentFormat.OpenXml.Wordprocessing.Paragraph;
using Run = DocumentFormat.OpenXml.Wordprocessing.Run;
Expand All @@ -14,6 +15,231 @@ namespace DocxTemplater.Test
{
internal class DocxTemplateTest
{

[Test]
public void DynamicTable()
{
using var fileStream = File.OpenRead("Resources/DynamicTable.docx");
var docTemplate = new DocxTemplate(fileStream);
var tableModel = new DynamicTable();
tableModel.AddRow(new Dictionary<object, object>() { { "Header1", "Value1" }, { "Header2", "Value2" }, { "Header3", "Value3" } });
tableModel.AddRow(new Dictionary<object, object>() { { "Header1", "Value4" }, { "Header2", "Value5" }, { "Header3", "Value6" } });
tableModel.AddRow(new Dictionary<object, object>() { { "Header1", "Value7" }, { "Header2", "Value8" }, { "Header3", "Value9" } });

docTemplate.BindModel("ds", tableModel);

var result = docTemplate.Process();
docTemplate.Validate();
result.Position = 0;
result.SaveAsFileAndOpenInWord();
result.Position = 0;
var document = WordprocessingDocument.Open(result, false);
var body = document.MainDocumentPart.Document.Body;
var table = body.Descendants<Table>().First();
var rows = table.Descendants<TableRow>().ToList();
Assert.That(rows.Count, Is.EqualTo(5));
Assert.That(rows[0].InnerText, Is.EqualTo("Header1Header2Header3"));
Assert.That(rows[2].InnerText, Is.EqualTo("Value1Value2Value3"));
Assert.That(rows[3].InnerText, Is.EqualTo("Value4Value5Value6"));
Assert.That(rows[4].InnerText, Is.EqualTo("Value7Value8Value9"));
}

/// <summary>
/// Dynamic tables are only required if the number of columns is not known at design time.
/// otherwise a simple table bound to a collection of objects is sufficient.
/// </summary>
[Test]
public void DynamicTableWithComplexObjectsAsHeaderAndValues()
{
using var fileStream = File.OpenRead("Resources/DynamicTableWithComplexObjectsAsHeaderAndValues.docx");
var docTemplate = new DocxTemplate(fileStream);
docTemplate.Settings.Culture = new CultureInfo("en-US");
var tableModel = new DynamicTable();
tableModel.AddRow(new Dictionary<object, object>()
{
{
new {HeaderTitle = "Header1"}, new { TheDouble = 20.0, TheDate = new DateTime(2007, 11, 12) }
},
{
new {HeaderTitle = "Header2"}, new { TheDouble = 30.0, TheDate = new DateTime(2007, 9, 12) }
},
{
new {HeaderTitle = "Header3"}, new { TheDouble = 40.0, TheDate = new DateTime(2001, 11, 14) }
}
});
tableModel.AddRow(new Dictionary<object, object>()
{
{
new {HeaderTitle = "Header1"}, new { TheDouble = 50.0, TheDate = new DateTime(2007, 11, 12) }
},
{
new {HeaderTitle = "Header2"}, new { TheDouble = 60.0, TheDate = new DateTime(2007, 9, 12) }
},
{
new {HeaderTitle = "Header3"}, new { TheDouble = 70.0, TheDate = new DateTime(2002, 11, 9) }
}
});
tableModel.AddRow(new Dictionary<object, object>()
{
{
new {HeaderTitle = "Header1"}, new { TheDouble = 80.0, TheDate = new DateTime(2007, 11, 12) }
},
{
new {HeaderTitle = "Header2"}, new { TheDouble = 90.0, TheDate = new DateTime(2007, 9, 12) }
},
{
new {HeaderTitle = "Header3"}, new { TheDouble = 100.0, TheDate = new DateTime(2003, 11, 12) }
}
});

docTemplate.BindModel("ds", tableModel);
var result = docTemplate.Process();
docTemplate.Validate();
result.Position = 0;
result.SaveAsFileAndOpenInWord();
result.Position = 0;
var document = WordprocessingDocument.Open(result, false);
var body = document.MainDocumentPart.Document.Body;
var table = body.Descendants<Table>().First();
var rows = table.Descendants<TableRow>().ToList();
Assert.That(rows.Count, Is.EqualTo(5));
Assert.That(rows[0].InnerText, Is.EqualTo("HEADER1HEADER2HEADER3"));
Assert.That(rows[2].InnerText, Is.EqualTo("20.00 11/12/200730.00 9/12/200740.00 11/14/2001"));
Assert.That(rows[3].InnerText, Is.EqualTo("50.00 11/12/200760.00 9/12/200770.00 11/9/2002"));
Assert.That(rows[4].InnerText, Is.EqualTo("80.00 11/12/200790.00 9/12/2007100.00 11/12/2003"));
}

[Test]
public void MissingVariableThrows()
{
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("{{missing}}")))));
wpDocument.Save();
memStream.Position = 0;
var docTemplate = new DocxTemplate(memStream);
Assert.Throws<OpenXmlTemplateException>(() => docTemplate.Process());
}

[Test]
public void MissingVariableWithSkipErrorHandling()
{
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("Text1{{missing}}Text2{{missing2}:toupper}{{missingImg}:img()}")))));
wpDocument.Save();
memStream.Position = 0;
var docTemplate = new DocxTemplate(memStream);
docTemplate.Settings.BindingErrorHandling = BindingErrorHandling.SkipBindingAndRemoveContent;
var result = docTemplate.Process();

var document = WordprocessingDocument.Open(result, false);
var body = document.MainDocumentPart.Document.Body;
//check values have been replaced
Assert.That(body.InnerText, Is.EqualTo("Text1Text2"));
}

[Test]
public void LoopStartAndEndTagsAreRemoved()
{
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("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("{{/ds.Items}}"))),
new Paragraph(new Run(new Text("Text456")))
));
wpDocument.Save();
memStream.Position = 0;
var docTemplate = new DocxTemplate(memStream);
docTemplate.BindModel("ds", new { Items = new[] { new { Name = "Item1", Price = 5 }, new { Name = "Item2", Price = 7 } } });
var result = docTemplate.Process();
docTemplate.Validate();
Assert.IsNotNull(result);
result.Position = 0;
result.SaveAsFileAndOpenInWord();
result.Position = 0;
// there should only be 4 paragraphs after processing
var document = WordprocessingDocument.Open(result, false);
var body = document.MainDocumentPart.Document.Body;
Assert.That(body.Descendants<Paragraph>().Count(), Is.EqualTo(4));
}

[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{{/}}")))

));
wpDocument.Save();
memStream.Position = 0;
var docTemplate = new DocxTemplate(memStream);
docTemplate.BindModel("ds", new { Test = 6 });
docTemplate.BindModel("ds2", new { Test = 6 });
var result = docTemplate.Process();
docTemplate.Validate();
Assert.IsNotNull(result);
result.Position = 0;
result.SaveAsFileAndOpenInWord();
result.Position = 0;
// check result text
var document = WordprocessingDocument.Open(result, false);
var body = document.MainDocumentPart.Document.Body;
Assert.That(body.InnerText, Is.EqualTo("Test1Test2Test3"));
}

[Test]
public void BindToMultipleModels()
{
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("{{obj.var1}}")),
new Run(new Text("{{obj.var2}}")),
new Run(new Text("{{dynObj.var3}}")),
new Run(new Text("{{dict.var4}}")),
new Run(new Text("{{interface.var5}}"))
)));
wpDocument.Save();
memStream.Position = 0;
var docTemplate = new DocxTemplate(memStream);

docTemplate.BindModel("obj", new { var1 = "var1", var2 = "var2" });
dynamic dynObj = new ExpandoObject();
dynObj.var3 = "var3";
docTemplate.BindModel("dynObj", dynObj);

var dict = new Dictionary<string, object>();
dict.Add("var4", "var4");
docTemplate.BindModel("dict", dict);

var dummyModel = new DummyModel();
dummyModel.Add("var5", "var5");
docTemplate.BindModel("interface", dummyModel);

var result = docTemplate.Process();
docTemplate.Validate();
Assert.IsNotNull(result);
result.Position = 0;

var document = WordprocessingDocument.Open(result, false);
var body = document.MainDocumentPart.Document.Body;
Assert.That(body.InnerText, Is.EqualTo("var1var2var3var4var5"));
}

[Test]
public void ReplaceTextBoldIsPreserved()
{
Expand Down Expand Up @@ -416,7 +642,25 @@ public decimal TotalPrice
set;
}
}
}

private class DummyModel : ITemplateModel
{
private readonly Dictionary<string, object> m_dict;

public DummyModel()
{
m_dict = new Dictionary<string, object>();
}

public void Add(string key, object value)
{
m_dict.Add(key, value);
}

public bool TryGetPropertyValue(string propertyName, out object value)
{
return m_dict.TryGetValue(propertyName, out value);
}
}
}
}
6 changes: 6 additions & 0 deletions DocxTemplater.Test/DocxTemplater.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@
<None Update="Resources\BillTemplate2.docx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Resources\DynamicTable.docx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Resources\DynamicTableWithComplexObjectsAsHeaderAndValues.docx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Resources\ImageFormatterTest.docx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
Expand Down
30 changes: 22 additions & 8 deletions DocxTemplater.Test/PatternMatcherTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,30 +25,35 @@ static IEnumerable TestPatternMatch_Cases()
yield return new TestCaseData("{{Foo}:blupp()}").Returns(new[] { PatternType.Variable });


yield return new TestCaseData("{{/Items}}").Returns(new[] { PatternType.LoopEnd });
yield return new TestCaseData("{{#items}}").Returns(new[] { PatternType.LoopStart });
yield return new TestCaseData("{{#ds.items_foo}}").Returns(new[] { PatternType.LoopStart }).SetName("LoopStart Underscore dots");
yield return new TestCaseData("{{/ds.items_foo}}").Returns(new[] { PatternType.LoopEnd }).SetName("LoopEnd Underscore dots");
yield return new TestCaseData("{{/Items.InnerCollection}}").Returns(new[] { PatternType.LoopEnd });
yield return new TestCaseData("{{#items.InnerCollection}}").Returns(new[] { PatternType.LoopStart });
yield return new TestCaseData("{{/Items}}").Returns(new[] { PatternType.CollectionEnd });
yield return new TestCaseData("{{ /Items }}").Returns(new[] { PatternType.CollectionEnd });
yield return new TestCaseData("{{#items}}").Returns(new[] { PatternType.CollectionStart });
yield return new TestCaseData("{{ #items }}").Returns(new[] { PatternType.CollectionStart });
yield return new TestCaseData("{{#ds.items_foo}}").Returns(new[] { PatternType.CollectionStart }).SetName("LoopStart Underscore dots");
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("{{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("{{var}:format(a,b)}").Returns(new[] { PatternType.Variable })
.SetName("Multiple Arguments");
yield return new TestCaseData("{{/}}").Returns(new[] { PatternType.ConditionEnd });
yield return new TestCaseData("{ { / } }").Returns(new[] { PatternType.ConditionEnd });
yield return new TestCaseData(
"NumericValue is greater than 0 - {{ds.Items.InnerCollection.InnerValue}:toupper()}{{else}}" +
"I'm here if if this is not the case{{/}}{{/ds.Items.InnerCollection}}{{/Items}}")
.Returns(new[]
{
PatternType.Variable, PatternType.ConditionElse, PatternType.ConditionEnd, PatternType.LoopEnd,
PatternType.LoopEnd
PatternType.Variable, PatternType.ConditionElse, PatternType.ConditionEnd, PatternType.CollectionEnd,
PatternType.CollectionEnd
})
.SetName("Complex Match 1");
}
Expand All @@ -75,6 +80,15 @@ static IEnumerable PatternMatcherArgumentParsingTest_Cases()
yield return new TestCaseData("{{Foo}:format(a,b,'YYYY_MMM/DD FF',d)}").Returns(new[] { "a", "b", "YYYY_MMM/DD FF", "d" });
yield return new TestCaseData("{{Foo}:format(a,'John Doe','YYYY_MMM/DD FF',d)}").Returns(new[] { "a", "John Doe", "YYYY_MMM/DD FF", "d" });
}

[TestCase("Some TExt {{ Variable }} Some other text", ExpectedResult = "Variable")]
[TestCase("Some TExt {{Variable }} Some other text", ExpectedResult = "Variable")]
[TestCase("Some TExt {{ Variable }} Some other text", ExpectedResult = "Variable")]
public string AllowWhiteSpaceForVariables(string syntax)
{
var match = PatternMatcher.FindSyntaxPatterns(syntax).First();
return match.Variable;
}
}
}

Binary file added DocxTemplater.Test/Resources/DynamicTable.docx
Binary file not shown.
Binary file not shown.
16 changes: 11 additions & 5 deletions DocxTemplater.Test/TestHelper.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
#if DEBUG
using System.Diagnostics;
#endif
using System.Diagnostics;


namespace DocxTemplater.Test
{
internal static class TestHelper
{
public static void SaveAsFileAndOpenInWord(this Stream stream, string extension = "docx")
{
#if DEBUG
#if RELEASE
return;
#pragma warning disable CS0162
#endif
if (Environment.GetEnvironmentVariable("DOCX_TEMPLATER_VISUAL_TESTING") == null)
{
return;
}
stream.Position = 0;
var fileName = Path.ChangeExtension(Path.GetTempFileName(), extension);
using (var fileStream = File.OpenWrite(fileName))
Expand All @@ -22,7 +28,7 @@ public static void SaveAsFileAndOpenInWord(this Stream stream, string extension
UseShellExecute = true
};
using var proc = Process.Start(psi);
#endif
#pragma warning restore CS0162
}
}
}
8 changes: 8 additions & 0 deletions DocxTemplater/BindingErrorHandling.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace DocxTemplater
{
public enum BindingErrorHandling
{
ThrowException,
SkipBindingAndRemoveContent
}
}
Loading

0 comments on commit 70f2733

Please sign in to comment.