Skip to content

Commit 1edeb93

Browse files
authored
Merge pull request #14 from Amberg/workitems/SubTemplates
Subtemplates and Conditional table rows
2 parents d47ee70 + d768861 commit 1edeb93

23 files changed

+1065
-360
lines changed

Directory.Build.props

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
<WarningsNotAsErrors>CS1030</WarningsNotAsErrors>
44
</PropertyGroup>
55

6+
<PropertyGroup>
7+
<TargetFrameworks>net6.0;net7.0;net8.0</TargetFrameworks>
8+
</PropertyGroup>
9+
610
<!--Code Analysis-->
711
<PropertyGroup>
812
<AnalysisLevelDocumentation>latest-minimum</AnalysisLevelDocumentation>
@@ -28,7 +32,7 @@
2832
<RepositoryType>git</RepositoryType>
2933
<Authors>Amberg</Authors>
3034
<Company>Orphis AG</Company>
31-
<PackageDescription>Bind data models to you word templates</PackageDescription>
35+
<PackageDescription>Easily bind data models to word templates</PackageDescription>
3236
<RepositoryUrl>https://github.com/Amberg/DocxTemplater</RepositoryUrl>
3337
<RepositoryType>git</RepositoryType>
3438
<PackageLicenseExpression>MIT</PackageLicenseExpression>

DocxTemplater.Images/DocxTemplater.Images.csproj

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
3-
<TargetFrameworks>net6.0;net7.0;net8.0</TargetFrameworks>
43
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
54
</PropertyGroup>
65
<ItemGroup>

DocxTemplater.Images/ImageFormatter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public bool CanHandle(Type type, string prefix)
2828

2929
public void ApplyFormat(FormatterContext context, Text target)
3030
{
31-
// TODO: handle oter ppi values than default 96
31+
// TODO: handle other ppi values than default 96
3232
// see https://learn.microsoft.com/en-us/dotnet/api/documentformat.openxml.wordprocessing.pixelsperinch?view=openxml-2.8.1#remarks
3333
if (context.Value is not byte[] imageBytes)
3434
{

DocxTemplater.Test/DocxTemplateTest.cs

Lines changed: 182 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Dynamic;
77
using System.Globalization;
88
using Bold = DocumentFormat.OpenXml.Wordprocessing.Bold;
9+
using Break = DocumentFormat.OpenXml.Drawing.Break;
910
using Paragraph = DocumentFormat.OpenXml.Wordprocessing.Paragraph;
1011
using Run = DocumentFormat.OpenXml.Wordprocessing.Run;
1112
using RunProperties = DocumentFormat.OpenXml.Wordprocessing.RunProperties;
@@ -597,6 +598,67 @@ public void BindCollection()
597598
"I'm only here if NumericValue is greater than 0 - INNERVALUE2B will be replaced X"));
598599
}
599600

601+
602+
603+
[Test]
604+
public void SupTemplateTest()
605+
{
606+
var template = @"<w:p xmlns:w=""http://schemas.openxmlformats.org/wordprocessingml/2006/main"">
607+
<w:pPr>
608+
<w:pBdr>
609+
<w:bottom w:val=""double"" w:sz=""6"" w:space=""1"" w:color=""auto""/>
610+
</w:pBdr>
611+
</w:pPr>
612+
<w:r>
613+
<w:t>Test {{ds.Name}} {{ds.Number}}</w:t>
614+
</w:r>
615+
</w:p>";
616+
617+
using var memStream = new MemoryStream();
618+
using var wpDocument = WordprocessingDocument.Create(memStream, WordprocessingDocumentType.Document);
619+
620+
MainDocumentPart mainPart = wpDocument.AddMainDocumentPart();
621+
mainPart.Document = new Document(new Body(
622+
new Paragraph(
623+
new Run(new Text("Start of Document")),
624+
new Break(),
625+
new Run(new Text("{{#ds.Items}}"))
626+
),
627+
new Paragraph(
628+
new Run(new Text("{{.Name}}")),
629+
new Run(new Text("{{.}:T('ds.Template')}"))
630+
),
631+
new Paragraph(
632+
new Run(new Text("{{/ds.Items}}"))
633+
)
634+
));
635+
wpDocument.Save();
636+
memStream.Position = 0;
637+
var docTemplate = new DocxTemplate(memStream);
638+
docTemplate.BindModel("ds",
639+
new
640+
{
641+
Template = template,
642+
Items = new[]
643+
{
644+
new {Name = "Item1 ", Number = 55 },
645+
new {Name = "Item2 ", Number = 96 }
646+
}
647+
});
648+
var result = docTemplate.Process();
649+
//docTemplate.Validate();
650+
Assert.That(result, Is.Not.Null);
651+
result.Position = 0;
652+
653+
var document = WordprocessingDocument.Open(result, false);
654+
var body = document.MainDocumentPart.Document.Body;
655+
//check values have been replaced
656+
Assert.That(body.InnerText, Is.EqualTo("Start of DocumentItem1 Test Item1 55Item2 Test Item2 96"));
657+
658+
659+
}
660+
661+
600662
[Test]
601663
public void BindCollectionToTable()
602664
{
@@ -657,7 +719,7 @@ public void BindCollectionToTable()
657719
docTemplate.Validate();
658720
Assert.That(result, Is.Not.Null);
659721
result.Position = 0;
660-
// result.SaveAsFileAndOpenInWord();
722+
result.SaveAsFileAndOpenInWord();
661723
var document = WordprocessingDocument.Open(result, false);
662724
var body = document.MainDocumentPart.Document.Body;
663725
var table = body.Descendants<Table>().First();
@@ -757,6 +819,125 @@ public void ProcessBillTemplate2()
757819
result.SaveAsFileAndOpenInWord();
758820
}
759821

822+
private enum RowType
823+
{
824+
Normal = 1,
825+
Underscore = 2,
826+
Red = 3,
827+
FromTemplate = 4,
828+
}
829+
830+
[Test]
831+
public void ConditionalTableRowsExtended()
832+
{
833+
// TODO: allow usage of enums in template
834+
using var fileStream = File.OpenRead("Resources/ConditionalTableRows.docx");
835+
var docTemplate = new DocxTemplate(fileStream);
836+
var model = new
837+
{
838+
RowTemplate = File.ReadAllBytes("Resources/RowTemplate.docx"),
839+
Positions = new[]
840+
{
841+
new { Type = (int)RowType.Normal, Description = "Description1", Tax = 20.5, Count = 55, Price = 55.20, TotalPrice = 20.9 },
842+
new { Type = (int)RowType.Underscore, Description = "Underscore 2", Tax = 20.5, Count = 55, Price = 55.20, TotalPrice = 20.9 },
843+
new { Type = (int)RowType.Normal, Description = "Description3", Tax = 200.5, Count = 550, Price = 550.20, TotalPrice = 200.9 },
844+
new { Type = (int)RowType.Red, Description = "Description4", Tax = 200.5, Count = 550, Price = 550.20, TotalPrice = 200.9 },
845+
new { Type = (int)RowType.Normal, Description = "Description5", Tax = 200.5, Count = 550, Price = 550.20, TotalPrice = 200.9 },
846+
new { Type = (int)RowType.FromTemplate, Description = "Description 6", Tax = 200.5, Count = 550, Price = 550.20, TotalPrice = 200.9 },
847+
new { Type = (int)RowType.FromTemplate, Description = "Description 7", Tax = 200.5, Count = 550, Price = 550.20, TotalPrice = 200.9 },
848+
}
849+
};
850+
docTemplate.BindModel("ds", model);
851+
docTemplate.BindModel("RowType", Enum.GetValues<RowType>().ToDictionary(x => x.ToString(), x => (int)x));
852+
var result = docTemplate.Process();
853+
docTemplate.Validate();
854+
result.Position = 0;
855+
result.SaveAsFileAndOpenInWord();
856+
}
857+
858+
[Test]
859+
public void ConditionalTableRows()
860+
{
861+
var model = new
862+
{
863+
Positions = new[]
864+
{
865+
new { Type = 1, Index = 1, Description = "Description", Tax = 20.5, Count = 55, Price = 55.20, TotalPrice = 20.9 },
866+
new { Type = 2, Index = 2, Description = "Description1", Tax = 20.5, Count = 55, Price = 55.20, TotalPrice = 20.9 },
867+
new { Type = 1, Index = 3, Description = "Description2", Tax = 200.5, Count = 550, Price = 550.20, TotalPrice = 200.9 },
868+
}
869+
};
870+
871+
var xml = @"<w:tbl xmlns:w=""http://schemas.openxmlformats.org/wordprocessingml/2006/main"">
872+
<w:tblPr>
873+
<w:tblW w:w=""5000"" w:type=""pct""/>
874+
<w:tblBorders>
875+
<w:top w:val=""single"" w:sz=""4"" w:space=""0"" w:color=""auto""/>
876+
<w:left w:val=""single"" w:sz=""4"" w:space=""0"" w:color=""auto""/>
877+
<w:bottom w:val=""single"" w:sz=""4"" w:space=""0"" w:color=""auto""/>
878+
<w:right w:val=""single"" w:sz=""4"" w:space=""0"" w:color=""auto""/>
879+
</w:tblBorders>
880+
</w:tblPr>
881+
<w:tblGrid>
882+
<w:gridCol w:w=""10296""/>
883+
</w:tblGrid>
884+
<w:tr>
885+
<w:tc>
886+
<w:p><w:r><w:t>Header Col 1</w:t></w:r></w:p>
887+
</w:tc>
888+
<w:tc>
889+
<w:p><w:r><w:t>Header Col 2</w:t></w:r></w:p>
890+
</w:tc>
891+
</w:tr>
892+
<w:tr>
893+
<w:tc>
894+
<w:tcPr>
895+
<w:tcW w:w=""0"" w:type=""auto""/>
896+
<w:tcBorders>
897+
<w:top w:val=""single"" w:color=""auto"" w:sz=""4"" w:space=""0"" />
898+
</w:tcBorders>
899+
</w:tcPr>
900+
<w:p><w:r><w:t>{{#Positions}}{?{.Type == 1}}</w:t><w:t>{{.Index}}</w:t></w:r></w:p>
901+
</w:tc>
902+
<w:tc>
903+
<w:tcPr>
904+
<w:tcW w:w=""0"" w:type=""auto""/>
905+
</w:tcPr>
906+
<w:p><w:r><w:t>{{.Description}}{{/}}</w:t></w:r></w:p>
907+
</w:tc>
908+
</w:tr>
909+
<w:tr>
910+
<w:tc>
911+
<w:tcPr>
912+
<w:tcW w:w=""0"" w:type=""auto""/>
913+
</w:tcPr>
914+
<w:p><w:r><w:t>{?{.Type == 2}}{{.Tax}} Other Row</w:t></w:r></w:p>
915+
</w:tc>
916+
<w:tc>
917+
<w:tcPr>
918+
<w:tcW w:w=""0"" w:type=""auto""/>
919+
</w:tcPr>
920+
<w:p><w:r><w:t>{{.Count}} Other Row Col 2{{/}}{{/Positions}}</w:t></w:r></w:p>
921+
</w:tc>
922+
</w:tr>
923+
</w:tbl>";
924+
925+
using var memStream = new MemoryStream();
926+
using var wpDocument = WordprocessingDocument.Create(memStream, WordprocessingDocumentType.Document);
927+
MainDocumentPart mainPart = wpDocument.AddMainDocumentPart();
928+
mainPart.Document = new Document(new Body(new Table(xml)));
929+
wpDocument.Save();
930+
memStream.Position = 0;
931+
var docTemplate = new DocxTemplate(memStream);
932+
933+
934+
docTemplate.BindModel("ds", model);
935+
var result = docTemplate.Process();
936+
docTemplate.Validate();
937+
result.Position = 0;
938+
result.SaveAsFileAndOpenInWord();
939+
}
940+
760941
private static DriveStudentOverviewReportingModel CrateBillTemplate2Model()
761942
{
762943
DriveStudentOverviewReportingModel model = new()

DocxTemplater.Test/DocxTemplater.Test.csproj

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
3-
<TargetFramework>net8.0</TargetFramework>
43
<ImplicitUsings>enable</ImplicitUsings>
54
<Nullable>disable</Nullable>
65
<IsPackable>false</IsPackable>
@@ -11,7 +10,7 @@
1110
</PropertyGroup>
1211
<ItemGroup>
1312
<PackageReference Include="AutoBogus" Version="2.13.1" />
14-
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
13+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
1514
<PackageReference Include="NUnit" Version="4.1.0" />
1615
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
1716
<PackageReference Include="NUnit.Analyzers" Version="4.2.0">
@@ -37,6 +36,9 @@
3736
<None Update="Resources\ComplexTemplate.docx">
3837
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
3938
</None>
39+
<None Update="Resources\ConditionalTableRows.docx">
40+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
41+
</None>
4042
<None Update="Resources\DynamicTable.docx">
4143
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
4244
</None>
@@ -55,6 +57,9 @@
5557
<None Update="Resources\NestedCollectionsInTable.docx">
5658
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
5759
</None>
60+
<None Update="Resources\RowTemplate.docx">
61+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
62+
</None>
5863
<None Update="Resources\testImage.jpg">
5964
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
6065
</None>

DocxTemplater.Test/PatternMatcherTest.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ static IEnumerable PatternMatcherArgumentParsingTest_Cases()
8787
yield return new TestCaseData("{{Foo}:format('param')}").Returns(new[] { "param" });
8888
yield return new TestCaseData("{{Foo}:format(a,b)}").Returns(new[] { "a", "b" });
8989
yield return new TestCaseData("{{Foo}:format(a,b,c)}").Returns(new[] { "a", "b", "c" });
90+
yield return new TestCaseData("{{Foo}:format(a.b, c.d)}").Returns(new[] { "a.b", "c.d" });
91+
yield return new TestCaseData("{{Foo}:format('a.b', c.d)}").Returns(new[] { "a.b", "c.d" });
9092
yield return new TestCaseData("{{Foo}:format(a,'a b',c)}").Returns(new[] { "a", "a b", "c" });
9193
yield return new TestCaseData("{{Foo}:format(a,b,'YYYY_MMM/DD FF',d)}").Returns(new[] { "a", "b", "YYYY_MMM/DD FF", "d" });
9294
yield return new TestCaseData("{{Foo}:format(a,'John Doe','YYYY_MMM/DD FF',d)}").Returns(new[] { "a", "John Doe", "YYYY_MMM/DD FF", "d" });
Binary file not shown.
22.5 KB
Binary file not shown.

DocxTemplater.sln

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
88
.editorconfig = .editorconfig
99
Directory.Build.props = Directory.Build.props
1010
GitVersion.yml = GitVersion.yml
11-
Todo.txt = Todo.txt
1211
EndProjectSection
1312
EndProject
1413
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DocxTemplater", "DocxTemplater\DocxTemplater.csproj", "{3903B788-BBF1-49CB-8E0F-67A25B25815B}"
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using DocumentFormat.OpenXml;
2+
using DocumentFormat.OpenXml.Wordprocessing;
3+
using DocxTemplater.Formatter;
4+
using System.Collections.Generic;
5+
6+
namespace DocxTemplater.Blocks
7+
{
8+
internal class CollectionSeparatorBlock : ContentBlock
9+
{
10+
public CollectionSeparatorBlock(VariableReplacer variableReplacer, PatternType patternType, Text startTextNode,
11+
PatternMatch startMatch)
12+
: base(variableReplacer, patternType, startTextNode, startMatch)
13+
{
14+
}
15+
16+
public override void Expand(ModelLookup models, OpenXmlElement parentNode)
17+
{
18+
int count = (int)models.GetValue($"{ParentBlock.StartMatch.Variable}._Idx");
19+
int length = (int)models.GetValue($"{ParentBlock.StartMatch.Variable}._Length");
20+
// last element is rendered first - get length and count ot to not render the last separator
21+
if (length - count == 0)
22+
{
23+
return;
24+
}
25+
base.Expand(models, parentNode);
26+
}
27+
28+
protected override void InsertContent(OpenXmlElement parentNode, IEnumerable<OpenXmlElement> paragraphs)
29+
{
30+
var element = m_insertionPoint.GetElement(parentNode) ?? throw new OpenXmlTemplateException($"Insertion point {m_insertionPoint.Id} not found");
31+
element.InsertBeforeSelf(paragraphs);
32+
}
33+
}
34+
}

0 commit comments

Comments
 (0)