Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,5 @@ TestResults/
.idea/
_ReSharper.Caches/

TestResult/
TestResult/
ExampleOutput*
78 changes: 78 additions & 0 deletions DOCUMENT_ASSEMBLER.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# OpenXmlPowerTools

OpenXmlPowerTools provides guidance and example code for programming with Open XML Documents (DOCX, XLSX, and PPTX). It is based on, and extends the functionality of the Open XML SDK.

## Document Assembler

The `DocumentAssembler` module is a powerful tool for generating documents from templates and data. It allows you to create `.docx` files with dynamic content, such as tables, conditional sections, and repeating elements, based on data from an XML file.

### Key Features

* **Template-Based Document Generation**: Create documents from Word templates (`.docx`) and populate them with data from XML files.
* **Content Replacement**: Use simple placeholders in your template to insert data from your XML file.
* **Dynamic Tables**: Automatically generate tables in your document based on data from your XML file.
* **Conditional Content**: Include or exclude parts of your document based on conditions in your data.
* **Repeating Content**: Repeat sections of your document for each item in a collection in your data.
* **Error Handling**: The `DocumentAssembler` will report errors in the generated document if it encounters any issues with your template or data.

### How it Works

The `DocumentAssembler` works by processing a Word document that contains special markup in content controls or in paragraphs. This markup defines how the document should be assembled based on the provided XML data.

The process is as follows:

1. **Create a Template**: Start with a regular Word document (`.docx`).
2. **Add Placeholders**: Use content controls or special syntax in paragraphs to define placeholders for your data.
3. **Provide Data**: Create an XML file that contains the data you want to insert into the document.
4. **Assemble the Document**: Use the `DocumentAssembler.AssembleDocument` method to merge the template and data, producing a new Word document.

### Template Syntax

The template syntax uses XML elements within content controls or as text in the format `<#ElementName ... #>`.

#### Content Replacement

To replace a placeholder with a value from your XML data, you can use the `Content` element. The `Select` attribute contains an XPath expression to select the data from the XML file.

**Example:**

`<#Content Select="Customer/Name" #>`

#### Tables

To generate a table, you use the `Table` element. The `Select` attribute specifies the collection of data to iterate over. The table in the template must have a prototype row, which will be repeated for each item in the data.

**Example:**

`<#Table Select="Customers/Customer" #>`

#### Conditional Content

You can conditionally include content using the `Conditional` element. The `Select` attribute specifies the data to test, and the `Match` or `NotMatch` attribute specifies the value to compare against.

**Example:**

`<#Conditional Select="Customer/Country" Match="USA" #>
... content to include if the customer is from the USA ...
<#EndConditional #>`

#### Repeating Content

To repeat a section of the document, you can use the `Repeat` element. The `Select` attribute specifies the collection of data to iterate over.

**Example:**

`<#Repeat Select="Customers/Customer" #>
... content to repeat for each customer ...
<#EndRepeat #>`

### Getting Started

To use the `DocumentAssembler`, you will need to:

1. Add a reference to the `OpenXmlPowerTools` library in your project.
2. Create a Word template with the appropriate placeholders.
3. Create an XML data file.
4. Call the `DocumentAssembler.AssembleDocument` method to generate your document.

For more detailed examples and documentation, please refer to the `DocumentAssembler`, `DocumentAssembler01`, `DocumentAssembler02`, and `DocumentAssembler03` projects in the `OpenXmlPowerToolsExamples` directory.
288 changes: 287 additions & 1 deletion OpenXmlPowerTools.Tests/DocumentAssemblerTests.cs

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions OpenXmlPowerTools.sln
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MarkupSimplifierApp", "OpenXmlPowerToolsExamples\MarkupSimplifierApp\MarkupSimplifierApp.csproj", "{6731E031-9C81-48FB-97A7-0E945993BCE2}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "OpenXmlPowerToolsExamples", "OpenXmlPowerToolsExamples", "{AA7E2DBC-70B3-4F8A-AC47-4416CDA9F3DA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DocumentAssembler04", "OpenXmlPowerToolsExamples\DocumentAssembler04\DocumentAssembler04.csproj", "{94E64B7D-BB4A-4478-B3BF-69F83C2FD379}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -208,6 +212,10 @@ Global
{6731E031-9C81-48FB-97A7-0E945993BCE2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6731E031-9C81-48FB-97A7-0E945993BCE2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6731E031-9C81-48FB-97A7-0E945993BCE2}.Release|Any CPU.Build.0 = Release|Any CPU
{94E64B7D-BB4A-4478-B3BF-69F83C2FD379}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{94E64B7D-BB4A-4478-B3BF-69F83C2FD379}.Debug|Any CPU.Build.0 = Debug|Any CPU
{94E64B7D-BB4A-4478-B3BF-69F83C2FD379}.Release|Any CPU.ActiveCfg = Release|Any CPU
{94E64B7D-BB4A-4478-B3BF-69F83C2FD379}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -242,6 +250,7 @@ Global
{D4078011-2611-46A7-8A30-55E4AB8FA786} = {A83D6B58-6D38-46AF-8C20-5CFC170A1063}
{DCE8EC51-1E58-49A0-82CF-5BE269FA0A9D} = {A83D6B58-6D38-46AF-8C20-5CFC170A1063}
{6731E031-9C81-48FB-97A7-0E945993BCE2} = {A83D6B58-6D38-46AF-8C20-5CFC170A1063}
{94E64B7D-BB4A-4478-B3BF-69F83C2FD379} = {AA7E2DBC-70B3-4F8A-AC47-4416CDA9F3DA}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E623EFF5-2CA4-4FA0-B3AB-53F921DA212E}
Expand Down
100 changes: 91 additions & 9 deletions OpenXmlPowerTools/DocumentAssembler/DocumentAssembler.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;
using System;
using System.Collections;
using System.Collections.Generic;
Expand Down Expand Up @@ -66,7 +67,7 @@ private static void ProcessTemplatePart(XElement data, TemplateError te, OpenXml
ProcessOrphanEndRepeatEndConditional(xDocRoot, te);

// do the actual content replacement
xDocRoot = ContentReplacementTransform(xDocRoot, data, te) as XElement;
xDocRoot = ContentReplacementTransform(xDocRoot, data, te, part) as XElement;

xDoc.Elements().First().ReplaceWith(xDocRoot);
part.PutXDocument();
Expand Down Expand Up @@ -508,6 +509,25 @@ private class RunReplacementInfo
</xs:schema>",
}
},
{
PA.Image,
new PASchemaSet() {
XsdMarkup =
@"<xs:schema attributeFormDefault='unqualified' elementFormDefault='qualified' xmlns:xs='http://www.w3.org/2001/XMLSchema'>
<xs:element name='Image'>
<xs:complexType>
<xs:attribute name='Select' type='xs:string' use='required' />
<xs:attribute name='Optional' type='xs:boolean' use='optional' />
<xs:attribute name='Align' type='xs:string' use='optional' />
<xs:attribute name='Width' type='xs:string' use='optional' />
<xs:attribute name='Height' type='xs:string' use='optional' />
<xs:attribute name='MaxWidth' type='xs:string' use='optional' />
<xs:attribute name='MaxHeight' type='xs:string' use='optional' />
</xs:complexType>
</xs:element>
</xs:schema>",
}
},
{
PA.Table,
new PASchemaSet() {
Expand Down Expand Up @@ -601,7 +621,7 @@ private class RunReplacementInfo

private static Dictionary<XName, PASchemaSet> s_PASchemaSets;

private static object? ContentReplacementTransform(XNode node, XElement data, TemplateError templateError)
private static object? ContentReplacementTransform(XNode node, XElement data, TemplateError templateError, OpenXmlPart owningPart)
{
if (node is XElement element)
{
Expand All @@ -612,7 +632,7 @@ private class RunReplacementInfo

var xPath = (string)element.Attribute(PA.Select);
var optionalString = (string)element.Attribute(PA.Optional);
var optional = (optionalString != null && optionalString.ToLower() == "true");
var optional = bool.TryParse(optionalString, out var optionalValue) && optionalValue;

string newValue;
try
Expand Down Expand Up @@ -649,11 +669,73 @@ private class RunReplacementInfo
return list;
}
}
if (element.Name == PA.Image)
{
var xPath = (string)element.Attribute(PA.Select);
var optionalString = (string)element.Attribute(PA.Optional);
var optional = bool.TryParse(optionalString, out var optionalValue) && optionalValue;
var alignString = (string)element.Attribute(PA.Align);
var widthAttr = (string)element.Attribute(PA.Width);
var heightAttr = (string)element.Attribute(PA.Height);
var maxWidthAttr = (string)element.Attribute(PA.MaxWidth);
var maxHeightAttr = (string)element.Attribute(PA.MaxHeight);

string base64Content;
try
{
base64Content = EvaluateXPathToString(data, xPath, optional);
}
catch (XPathException e)
{
return CreateContextErrorMessage(element, "XPathException: " + e.Message, templateError);
}

if (string.IsNullOrEmpty(base64Content))
{
return null;
}

byte[] imageBytes;
try
{
imageBytes = Convert.FromBase64String(base64Content);
}
catch (FormatException e)
{
return CreateContextErrorMessage(element, "Image: " + e.Message, templateError);
}

if (!ImageHelper.TryGetJustification(alignString, out var justification, out var justificationError))
{
return CreateContextErrorMessage(element, justificationError, templateError);
}

if (owningPart == null)
{
throw new OpenXmlPowerToolsException("Image: owning part is not available.");
}

if (!ImageHelper.TryCalculateImageDimensions(imageBytes, widthAttr, heightAttr, maxWidthAttr, maxHeightAttr, out var widthEmu, out var heightEmu, out var sizeError))
{
return CreateContextErrorMessage(element, sizeError, templateError);
}

var imagePart = ImageHelper.AddImagePart(owningPart);
using (var stream = new MemoryStream(imageBytes))
{
imagePart.FeedData(stream);
}

var relationshipId = owningPart.GetIdOfPart(imagePart);
var docPrId = ImageHelper.GetNextDocPrId(owningPart);
var imageElement = ImageHelper.CreateImageElement(relationshipId, docPrId, widthEmu, heightEmu, justification);
return imageElement;
}
if (element.Name == PA.Repeat)
{
var selector = (string)element.Attribute(PA.Select);
var optionalString = (string)element.Attribute(PA.Optional);
var optional = (optionalString != null && optionalString.ToLower() == "true");
var optional = bool.TryParse(optionalString, out var optionalValue) && optionalValue;

IEnumerable<XElement> repeatingData;
try
Expand All @@ -676,7 +758,7 @@ private class RunReplacementInfo
{
var content = element
.Elements()
.Select(e => ContentReplacementTransform(e, d, templateError))
.Select(e => ContentReplacementTransform(e, d, templateError, owningPart))
.ToList();
return content;
})
Expand Down Expand Up @@ -706,7 +788,7 @@ private class RunReplacementInfo
.Skip(2)
.ToList();
var footerRows = footerRowsBeforeTransform
.Select(x => ContentReplacementTransform(x, data, templateError))
.Select(x => ContentReplacementTransform(x, data, templateError, owningPart))
.ToList();
if (protoRow == null)
{
Expand Down Expand Up @@ -784,14 +866,14 @@ private class RunReplacementInfo

if ((match != null && testValue == match) || (notMatch != null && testValue != notMatch))
{
var content = element.Elements().Select(e => ContentReplacementTransform(e, data, templateError));
var content = element.Elements().Select(e => ContentReplacementTransform(e, data, templateError, owningPart));
return content;
}
return null;
}
return new XElement(element.Name,
element.Attributes(),
element.Nodes().Select(n => ContentReplacementTransform(n, data, templateError)));
element.Nodes().Select(n => ContentReplacementTransform(n, data, templateError, owningPart)));
}
return node;
}
Expand Down Expand Up @@ -884,4 +966,4 @@ private static string EvaluateXPathToString(XElement element, string xPath, bool
return xPathSelectResult.ToString();
}
}
}
}
Loading
Loading