From 37eba4a31a06b549d885868f93bee1e31c005ea2 Mon Sep 17 00:00:00 2001 From: Manuel Amstutz Date: Tue, 5 Nov 2024 14:41:51 +0100 Subject: [PATCH 1/5] Fix crash with bookmarks in different roots Bookmarks are now removed during processing --- ...arkInDifferentTableRowDoesNotcauseCrash.cs | 55 +++++++++++++++++++ DocxTemplater/TemplateProcessor.cs | 16 ++++-- 2 files changed, 65 insertions(+), 6 deletions(-) create mode 100644 DocxTemplater.Test/BookmarkInDifferentTableRowDoesNotcauseCrash.cs diff --git a/DocxTemplater.Test/BookmarkInDifferentTableRowDoesNotcauseCrash.cs b/DocxTemplater.Test/BookmarkInDifferentTableRowDoesNotcauseCrash.cs new file mode 100644 index 0000000..70f52bb --- /dev/null +++ b/DocxTemplater.Test/BookmarkInDifferentTableRowDoesNotcauseCrash.cs @@ -0,0 +1,55 @@ +using DocumentFormat.OpenXml; +using DocumentFormat.OpenXml.Packaging; +using DocumentFormat.OpenXml.Wordprocessing; + +namespace DocxTemplater.Test +{ + internal class BookmarkInDifferentTableRowDoesNotcauseCrash + { + [Test] + public void Test() + { + string content = @" + + This is sentence one. + + + + This is sentence two. {{.}} + + + + + This + + + + is sentence three.{{.}} + + "; + + using var memStream = new MemoryStream(); + using var wpDocument = WordprocessingDocument.Create(memStream, WordprocessingDocumentType.Document); + var mainPart = wpDocument.AddMainDocumentPart(); + mainPart.Document = new Document + { + Body = new Body + { + InnerXml = content + } + }; + wpDocument.Save(); + memStream.Position = 0; + var docTemplate = new DocxTemplate(memStream); + docTemplate.BindModel("ds", "hi there"); + var result = docTemplate.Process(); + docTemplate.Validate(); + + // get body + var document = WordprocessingDocument.Open(result, false); + var body = document.MainDocumentPart.Document.Body; + Assert.That(body.Descendants().Count(), Is.EqualTo(0)); + Assert.That(body.Descendants().Count(), Is.EqualTo(0)); + } + } +} diff --git a/DocxTemplater/TemplateProcessor.cs b/DocxTemplater/TemplateProcessor.cs index 65f74de..2a47ccd 100644 --- a/DocxTemplater/TemplateProcessor.cs +++ b/DocxTemplater/TemplateProcessor.cs @@ -106,16 +106,20 @@ private static void Cleanup(OpenXmlCompositeElement element, bool removeEmptyEle } } - // make all Bookmark ids unique - uint id = 0; - foreach (var bookmarkStart in element.Descendants()) + // remove all bookmarks -> not useful for generated documents and complex to handle + // because of special cases in tables see + // https://learn.microsoft.com/en-us/dotnet/api/documentformat.openxml.wordprocessing.bookmarkstart?view=openxml-3.0.1#remarks + foreach (var bookmark in element.Descendants().ToList()) { - bookmarkStart.Id = $"{id++}"; - bookmarkStart.NextSibling().Id = bookmarkStart.Id; + bookmark.RemoveWithEmptyParent(); + } + foreach (var bookmark in element.Descendants().ToList()) + { + bookmark.RemoveWithEmptyParent(); } // make dock properties ids unique - id = 1; + uint id = 1; var dockProperties = element.Descendants().ToList(); var existingIds = new HashSet(dockProperties.Select(x => x.Id.Value).ToList()); foreach (var docPropertiesWithSameId in dockProperties.GroupBy(x => x.Id).Where(x => x.Count() > 1)) From d870437b65f96349a4cb749eb0993cfca1b8a14b Mon Sep 17 00:00:00 2001 From: Manuel Amstutz Date: Tue, 5 Nov 2024 14:42:25 +0100 Subject: [PATCH 2/5] Define dependencies with version range instead of a fixed version --- DocxTemplater.Images/DocxTemplater.Images.csproj | 2 +- DocxTemplater.Markdown/DocxTemplater.Markdown.csproj | 2 +- DocxTemplater/DocxTemplater.csproj | 4 ++-- DocxTemplater/OpenXmlHelper.cs | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/DocxTemplater.Images/DocxTemplater.Images.csproj b/DocxTemplater.Images/DocxTemplater.Images.csproj index f5a7671..feba5c1 100644 --- a/DocxTemplater.Images/DocxTemplater.Images.csproj +++ b/DocxTemplater.Images/DocxTemplater.Images.csproj @@ -7,7 +7,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/DocxTemplater.Markdown/DocxTemplater.Markdown.csproj b/DocxTemplater.Markdown/DocxTemplater.Markdown.csproj index 4f8a152..6b1b09e 100644 --- a/DocxTemplater.Markdown/DocxTemplater.Markdown.csproj +++ b/DocxTemplater.Markdown/DocxTemplater.Markdown.csproj @@ -7,7 +7,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/DocxTemplater/DocxTemplater.csproj b/DocxTemplater/DocxTemplater.csproj index 5b99be4..568ed64 100644 --- a/DocxTemplater/DocxTemplater.csproj +++ b/DocxTemplater/DocxTemplater.csproj @@ -4,8 +4,8 @@ True - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/DocxTemplater/OpenXmlHelper.cs b/DocxTemplater/OpenXmlHelper.cs index 843a116..3ecdc7f 100644 --- a/DocxTemplater/OpenXmlHelper.cs +++ b/DocxTemplater/OpenXmlHelper.cs @@ -65,7 +65,7 @@ public static OpenXmlElement ElementBeforeInDocument(this OpenXmlEleme return null; } - public static OpenXmlElement ElementAfterInDocument(this OpenXmlElement element) + public static TElement ElementAfterInDocument(this OpenXmlElement element) where TElement : OpenXmlElement { var parent = element.Parent; From df21a5d822f2be36821e6b36a4fe736a959f44e5 Mon Sep 17 00:00:00 2001 From: Manuel Amstutz Date: Wed, 6 Nov 2024 06:21:18 +0100 Subject: [PATCH 3/5] Document scaling arguments for image formatter in readme --- DocxTemplater/OpenXmlHelper.cs | 6 + README.md | 231 +++++++++++++++++---------------- 2 files changed, 123 insertions(+), 114 deletions(-) diff --git a/DocxTemplater/OpenXmlHelper.cs b/DocxTemplater/OpenXmlHelper.cs index 3ecdc7f..2dc98d3 100644 --- a/DocxTemplater/OpenXmlHelper.cs +++ b/DocxTemplater/OpenXmlHelper.cs @@ -509,6 +509,11 @@ public static int CmToEmu(int centimeter) return centimeter * 360000; } + public static int MmToEmu(int millimeter) + { + return millimeter * 36000; + } + public static int PixelsToEmu(int pixels) { return pixels * 9525; @@ -525,6 +530,7 @@ public static int LengthToEmu(int value, string unit) return unit switch { "cm" => CmToEmu(value), + "mm" => MmToEmu(value), "px" => PixelsToEmu(value), "in" => InchToEmu(value), _ => throw new ArgumentException("Unsupported unit: " + unit) diff --git a/README.md b/README.md index 2f9665e..38357ca 100644 --- a/README.md +++ b/README.md @@ -1,133 +1,132 @@ +To update the README, you can edit the file directly in the repository. Here is a revised version of the README content you can use: + +--- + # DocxTemplater -_DocxTemplater is a library to generate docx documents from a docx template. -The template can be **bound to multiple datasources** and be edited by non-programmers. -It supports placeholder **replacement** and **loops** and **images**_ +_DocxTemplater is a library to generate docx documents from a docx template. The template can be **bound to multiple datasources** and be edited by non-programmers. It supports placeholder **replacement**, **loops**, and **images**._ [![CI-Build](https://github.com/Amberg/DocxTemplater/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/Amberg/DocxTemplater/actions/workflows/ci.yml) -**Features:** -* Variable Replacement -* Collections - Bind to collections -* Conditional Blocks -* Dynamic Tables - Columns are defined by the datasource -* Markdown Support - Converts Markdown to OpenXML -* HTML Snippets - Replace placeholder with HTML Content -* Images - Replace placeholder with Image data - +## Features +- Variable Replacement +- Collections - Bind to collections +- Conditional Blocks +- Dynamic Tables - Columns are defined by the datasource +- Markdown Support - Converts Markdown to OpenXML +- HTML Snippets - Replace placeholder with HTML Content +- Images - Replace placeholder with Image data ## Quickstart -Create a docx template with placeholder syntax - +Create a docx template with placeholder syntax: ``` This Text: {{ds.Title}} - will be replaced ``` -Open the template, add a model and store the result to a file. -```c# - var template = DocxTemplate.Open("template.docx"); - // to open the file from a stream use the constructor directly - // var template = new DocxTemplate(stream); - template.BindModel("ds", new { Title = "Some Text"}); - template.Save("generated.docx"); + +Open the template, add a model, and store the result to a file: +```csharp +var template = DocxTemplate.Open("template.docx"); +// To open the file from a stream use the constructor directly +// var template = new DocxTemplate(stream); +template.BindModel("ds", new { Title = "Some Text" }); +template.Save("generated.docx"); ``` -The generated word document then contains +The generated word document will contain: ``` This Text: Some Text - will be replaced ``` -### Install DocxTemplater via NuGet -If you want to include DocxTemplater in your project, you can [install it directly from NuGet](https://www.nuget.org/packages/DocxTemplater) +### Install DocxTemplater via NuGet -To install DocxTemplater, run the following command in the Package Manager Console +To include DocxTemplater in your project, you can [install it directly from NuGet](https://www.nuget.org/packages/DocxTemplater). +Run the following command in the Package Manager Console: ``` PM> Install-Package DocxTemplater ``` -or for Image support + +For Image support: ``` PM> Install-Package DocxTemplater.Images ``` ## Placeholder Syntax -A placeholder can consist of three parts: {{**property**}:**formatter**(**arguments**)} - -- **property**: the path to the property in the datasource objects. -- **formatter**: formatter applied to convert the model value to openxml _(ae. toupper, tolower img format etc)_ -- **arguments**: formatter arguments - some formatter have arguments - -The syntax is case insensitive - -**Quick Reference:** (Expamples) - -| Syntax | Desciption | -|----------------|--------------------------| -| {{SomeVar}} | Simple Variable replacement -| {?{someVar > 5}}...{{:}}...{{/}} | Conditional blocks -| {{#Items}}...{{Items.Name}} ... {{/Items}} | Text block bound to collection of complex items -| {{#Items}}...{{.Name}} ... {{/Items}} | Same as above with dot notation - implicit iterator - renders property 'Name' of each item in the collection -| {{#Items}}...{{.}:toUpper} ... {{/Items}} | A list of string all upper case - dot notation -| {{#Items}}{{.}}{{:s:}},{{/Items}} | A list of strings comma separated - dot notation -| {{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 -| {{SomeBytes}:img()} | Image Formatter for image data -| {{SomeHtmlString}:html()} | Inserts html string into word document +A placeholder can consist of three parts: `{{**property**:**formatter**(**arguments**)}}` + +- **property**: The path to the property in the datasource objects. +- **formatter**: Formatter applied to convert the model value to OpenXML (e.g., `toupper`, `tolower`, `img` format). +- **arguments**: Formatter arguments - some formatters have arguments. + +The syntax is case insensitive. + +### Quick Reference Examples + +| Syntax | Description | +|-------------------------------------|----------------------------------------------------------------| +| `{{SomeVar}}` | Simple Variable replacement | +| `{?{someVar > 5}}...{{:}}...{{/}}` | Conditional blocks | +| `{{#Items}}...{{Items.Name}} ... {{/Items}}` | Text block bound to collection of complex items | +| `{{#Items}}...{{.Name}} ... {{/Items}}` | Same as above with dot notation - implicit iterator | +| `{{#Items}}...{{.}:toUpper} ... {{/Items}}` | A list of string all upper case - dot notation | +| `{{#Items}}{{.}}{{:s:}},{{/Items}}` | A list of strings comma separated - dot notation | +| `{{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 | +| `{{SomeBytes}:img()}` | Image Formatter for image data | +| `{{SomeHtmlString}:html()}` | Inserts HTML string into the word document | + ### Collections -To repeat document content for each item in a collection the loop syntax can be used: -**{{#_\_}}** .. content .. **{{__}}** -All document content between the start and end tag is rendered for each element in the collection. +To repeat document content for each item in a collection, use the loop syntax: +`**{{#_\_}}** ... content ... **{{/_\_}}**` +All document content between the start and end tag is rendered for each element in the collection: ``` -{{#Items}} This text {{Items.Name}} is rendered for each element in the items collection {{/items}} +{{#Items}} This text {{Items.Name}} is rendered for each element in the items collection {{/Items}} ``` -This can be used, for example, to bind a collection to a table. In this case, the start and end tag has to be placed in the row of the table - -| Name | Position | -|----------------|----------| -| **{{#Items}}** {{Items.Name}} | {{Items.Position}} **{{/Items}}**| +This can be used, for example, to bind a collection to a table. In this case, the start and end tag have to be placed in the row of the table: +| Name | Position | +|--------------|-----------| +| **{{#Items}}** {{Items.Name}} | {{Items.Position}} **{{/Items}}** | This template bound to a model: -```c# - var template = DocxTemplate.Open("template.docx"); - var model = new - { - Items = new[] - { - new { Name = "John", Position = "Developer" }, - new { Name = "Alice", Position = "CEO" } - } - }; - template.BindModel("ds", model); - template.Save("generated.docx"); -``` - -will render a table row for each item in the collection - -| Name | Position | -|----------------|----------| -| John | Developer| -| Alice | CEO| +```csharp +var template = DocxTemplate.Open("template.docx"); +var model = new +{ + Items = new[] + { + new { Name = "John", Position = "Developer" }, + new { Name = "Alice", Position = "CEO" } + } +}; +template.BindModel("ds", model); +template.Save("generated.docx"); +``` + +Will render a table row for each item in the collection: +| Name | Position | +|-------|-----------| +| John | Developer | +| Alice | CEO | #### Separator -If you want to render a separator between the items in the collection, you can use the separator syntax: +To render a separator between the items in the collection, 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}} +{{#Items}} This text {{.Name}} is rendered for each element in the items collection {{:s:}} This is rendered between each element {{/Items}} ``` - - ### Conditional Blocks Show or hide a given section depending on a condition: -**{?{\}}** .. content .. **{{/}}** -All document content between the start and end tag is rendered only if the condition is met +`**{?{\}}** ... 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 {{:}}Otherwise this text is shown{{/}} @@ -135,61 +134,66 @@ All document content between the start and end tag is rendered only if the condi ## Formatters -If no formatter is specified, the model value is converted into a text with "ToString". +If no formatter is specified, the model value is converted into a text with `ToString`. -This is not sufficient for all data types. That is why there are formatters that convert text or binary data into the desired representation +This is not sufficient for all data types. That is why there are formatters that convert text or binary data into the desired representation. -The formatter name is always case insensitive +The formatter name is always case insensitive. ### String Formatters -- ToUpper, ToLower +- `ToUpper` +- `ToLower` ### FormatPatterns -Any type that implements ```IFormattable``` can be formatted with the standard format strings for this type +Any type that implements `IFormattable` can be formatted with the standard format strings for this type. -**See:** -[Standard date and time format strings](https://learn.microsoft.com/en-us/dotnet/standard/base-types/standard-date-and-time-format-strings) -[Standard numeric format strings](https://learn.microsoft.com/en-us/dotnet/standard/base-types/standard-numeric-format-strings) -.. and many more +See: +- [Standard date and time format strings](https://learn.microsoft.com/en-us/dotnet/standard/base-types/standard-date-and-time-format-strings) +- [Standard numeric format strings](https://learn.microsoft.com/en-us/dotnet/standard/base-types/standard-numeric-format-strings) -**Examples:** +Examples: +``` {{SomeDate}:format(d)} ----> "6/15/2009" (en-US) {{SomeDouble}:format(f2)} ----> "1234.42" (en-US) +``` ### Image Formatter ---- - -**_NOTE:_** for the Image formatter the nuget package *DocxTemplater.Images* is required - ---- +**_NOTE:_** For the Image formatter, the NuGet package `DocxTemplater.Images` is required. -Because the image formatter is not standard, it must be added -```c# +Because the image formatter is not standard, it must be added: +```csharp var docTemplate = new DocxTemplate(fileStream); docTemplate.RegisterFormatter(new ImageFormatter()); ``` -The image formatter replaces a placeholder with an image stored in a byte array +The image formatter replaces a placeholder with an image stored in a byte array. The placeholder can be placed in a TextBox so that the end user can easily adjust the image size in the template. The size of the image is then adapted to the size of the TextBox. -The stretching behavior can be configured +The stretching behavior can be configured: +| Arg | Example | Description | +|------------|---------------------------------|--------------------------------------------------| +| `KEEPRATIO`| `{{imgData}:img(keepratio)}` | Scales the image to fit the container - keeps aspect ratio | +| `STRETCHW` | `{imgData}:img(STRETCHW)}` | Scales the image to fit the width of the container | +| `STRETCHH` | `{imgData}:img(STRETCHH)}` | Scales the image to fit the height of the container | + +If the image is used without any container the image scaling can be with 'w' or 'h' argument. Use 'r' to rotate the image. +Is only one of the arguments 'w' or 'h' used the image is scaled to the given width or height and the aspect ratio is kept. +The size of the image can be specified in different units (cm,mm,in,px) | Arg | Example | Description |----------------|----------|--- -| KEEPRATIO| {{imgData}:img(keepratio)} | Scales the image to fit the container - keeps aspect ratio -| STRETCHW | {imgData}:img(STRETCHW)}| Scales the image to fit the width of the container -| STRETCHH | {imgData}:img(STRETCHH)}| Scales the image to fit the height of the container +| w | `{{imgData}:img(w:100mm)}` | Scales the image to a width of 100 millimeters +| h | `{{imgData}:img(h:100in)}` | Scales the image to a height of 100 inches +| r | `{{imgData}:img(r:90)}` | Rotates the image by 90 degrees ### Error Handling -If a placeholder is not found in the model an exception is thrown. -This can be configured with the ```ProcessSettings``` - -```c# +If a placeholder is not found in the model, an exception is thrown. This can be configured with the `ProcessSettings`: +```csharp var docTemplate = new DocxTemplate(memStream); docTemplate.Settings.BindingErrorHandling = BindingErrorHandling.SkipBindingAndRemoveContent; var result = docTemplate.Process(); @@ -197,12 +201,11 @@ var result = docTemplate.Process(); ### Culture -The culture used to format the model values can be configured with the ```ProcessSettings``` - -```c# +The culture used to format the model values can be configured with the `ProcessSettings`: +```csharp var docTemplate = new DocxTemplate(memStream, new ProcessSettings() { Culture = new CultureInfo("en-us") }); var result = docTemplate.Process(); -``` +``` \ No newline at end of file From 5e201e5154f41dcf2634c82125cc965cbbf601e3 Mon Sep 17 00:00:00 2001 From: Manuel Amstutz Date: Wed, 6 Nov 2024 07:07:14 +0100 Subject: [PATCH 4/5] More README.md --- README.md | 116 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 82 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 38357ca..7e729fc 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,3 @@ -To update the README, you can edit the file directly in the repository. Here is a revised version of the README content you can use: - ---- - # DocxTemplater _DocxTemplater is a library to generate docx documents from a docx template. The template can be **bound to multiple datasources** and be edited by non-programmers. It supports placeholder **replacement**, **loops**, and **images**._ @@ -47,14 +43,18 @@ Run the following command in the Package Manager Console: PM> Install-Package DocxTemplater ``` -For Image support: -``` -PM> Install-Package DocxTemplater.Images -``` +#### Additional Extension Packages + +Enhance DocxTemplater with these optional extension packages: + +| Package | Description +|--------------|----------------------------------- +| [DocxTemplater.Images ](https://www.nuget.org/packages/DocxTemplater.Images) |Enables embedding images in generated Word documents| +| [DocxTemplater.Markdown ](https://www.nuget.org/packages/DocxTemplater.Markdown) | Allows use of Markdown syntax for generating parts of Word documents| ## Placeholder Syntax -A placeholder can consist of three parts: `{{**property**:**formatter**(**arguments**)}}` +A placeholder can consist of three parts: `{{property:formatter(arguments)}}` - **property**: The path to the property in the datasource objects. - **formatter**: Formatter applied to convert the model value to OpenXML (e.g., `toupper`, `tolower`, `img` format). @@ -77,11 +77,11 @@ The syntax is case insensitive. | `{{SomeDate}:F('MM/dd/yyyy')}` | Date variable with formatting - short syntax | | `{{SomeBytes}:img()}` | Image Formatter for image data | | `{{SomeHtmlString}:html()}` | Inserts HTML string into the word document | - +--- ### Collections To repeat document content for each item in a collection, use the loop syntax: -`**{{#_\_}}** ... content ... **{{/_\_}}**` +**{{#\}}** ... content ... **{{<\/collection>}}** All document content between the start and end tag is rendered for each element in the collection: ``` @@ -113,25 +113,25 @@ Will render a table row for each item in the collection: |-------|-----------| | John | Developer | | Alice | CEO | - -#### Separator +--- +### Separator To render a separator between the items in the collection, use the separator syntax: ``` {{#Items}} This text {{.Name}} is rendered for each element in the items collection {{:s:}} This is rendered between each element {{/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 {{:}}Otherwise this text is shown{{/}} ``` - +--- ## Formatters If no formatter is specified, the model value is converted into a text with `ToString`. @@ -158,7 +158,7 @@ Examples: {{SomeDate}:format(d)} ----> "6/15/2009" (en-US) {{SomeDouble}:format(f2)} ----> "1234.42" (en-US) ``` - +--- ### Image Formatter **_NOTE:_** For the Image formatter, the NuGet package `DocxTemplater.Images` is required. @@ -169,27 +169,75 @@ var docTemplate = new DocxTemplate(fileStream); docTemplate.RegisterFormatter(new ImageFormatter()); ``` -The image formatter replaces a placeholder with an image stored in a byte array. +The Image Formatter replaces a placeholder with an image stored as a byte array. + +The placeholder can be positioned in a `TextBox`, allowing end-users to adjust the image size easily within the template. The image will then automatically resize to match the dimensions of the `TextBox`. -The placeholder can be placed in a TextBox so that the end user can easily adjust the image size in the template. The size of the image is then adapted to the size of the TextBox. +#### Stretching Behavior -The stretching behavior can be configured: -| Arg | Example | Description | -|------------|---------------------------------|--------------------------------------------------| -| `KEEPRATIO`| `{{imgData}:img(keepratio)}` | Scales the image to fit the container - keeps aspect ratio | -| `STRETCHW` | `{imgData}:img(STRETCHW)}` | Scales the image to fit the width of the container | -| `STRETCHH` | `{imgData}:img(STRETCHH)}` | Scales the image to fit the height of the container | +You can configure the image's stretching behavior as follows: -If the image is used without any container the image scaling can be with 'w' or 'h' argument. Use 'r' to rotate the image. -Is only one of the arguments 'w' or 'h' used the image is scaled to the given width or height and the aspect ratio is kept. -The size of the image can be specified in different units (cm,mm,in,px) +| Argument | Example | Description | +|--------------|-----------------------------------|-----------------------------------------------------------| +| `KEEPRATIO` | `{{imgData}:img(keepratio)}` | Scales the image to fit the container while preserving the aspect ratio | +| `STRETCHW` | `{{imgData}:img(STRETCHW)}` | Scales the image to fit the container’s width | +| `STRETCHH` | `{{imgData}:img(STRETCHH)}` | Scales the image to fit the container’s height | -| Arg | Example | Description -|----------------|----------|--- -| w | `{{imgData}:img(w:100mm)}` | Scales the image to a width of 100 millimeters -| h | `{{imgData}:img(h:100in)}` | Scales the image to a height of 100 inches -| r | `{{imgData}:img(r:90)}` | Rotates the image by 90 degrees +If the image is not placed in a container, scaling can be applied using the `w` (width) or `h` (height) arguments. The `r` (rotate) argument can be used to rotate the image. + +- When only `w` or `h` is specified, the image scales to the specified width or height, maintaining its aspect ratio. +- The size of the image can be specified in various units: cm, mm, in, px. + + +| Argument | Example | Description | +|----------|------------------------------------|--------------------------------------------------------------------------------------| +| `w` | `{{imgData}:img(w:100mm)}` | Scales the image to a width of 100 mm, preserving aspect ratio | +| `h` | `{{imgData}:img(h:100in)}` | Scales the image to a height of 100 inches, preserving aspect ratio | +| `r` | `{{imgData}:img(r:90)}` | Rotates the image by 90 degrees | +| `w,h` | `{{imgData}:img(w:50px,h:20px)}` | Stretches the image to 50 x 20 pixels without preserving the aspect ratio | + +--- +### Markdown Formatter +The Markdown Formatter in DocxTemplater allows you to convert Markdown text into OpenXML elements, which can be included in your Word documents. This feature supports placeholder replacement within Markdown text and can handle various Markdown elements including tables, lists, and more. + +**_NOTE:_** For the Markdown formatter, the NuGet package `DocxTemplater.Markdown` is required. + +Because the markdown formatter is not standard, it must be added: +```csharp +var docTemplate = new DocxTemplate(fileStream); +docTemplate.RegisterFormatter(new MarkDownFormatter()); +``` + + +#### Usage + +To use the Markdown formatter, you need to specify the `md` prefix and pass in the Markdown text as a string. Here is an example: + +```csharp +// Initialize the template +var template = DocxTemplate.Open("template.docx"); +var markdown = """ + | Header 1 | Header 2 | + |----------|----------| + | Row 1 Col 1 | Row 1 Col 2 | + | Row 2 Col 1 | Row 2 Col 2 | + """; +// Bind model with Markdown content +template.BindModel("ds", new { MarkdownContent = markdown }); + +// Save the generated document +template.Save("generated.docx"); +``` + +In your template, you would have a placeholder like this: + +``` +{{ds.MarkdownContent:MD}} +``` + + +--- ### Error Handling If a placeholder is not found in the model, an exception is thrown. This can be configured with the `ProcessSettings`: @@ -198,7 +246,7 @@ var docTemplate = new DocxTemplate(memStream); docTemplate.Settings.BindingErrorHandling = BindingErrorHandling.SkipBindingAndRemoveContent; var result = docTemplate.Process(); ``` - +--- ### Culture The culture used to format the model values can be configured with the `ProcessSettings`: From aa0dfa15c5ef008641afa7d4ad650341ef2fbb6b Mon Sep 17 00:00:00 2001 From: Manuel Amstutz Date: Thu, 7 Nov 2024 16:39:34 +0100 Subject: [PATCH 5/5] Document and fix the Index variable in loops --- DocxTemplater.Test/DocxTemplater.Test.csproj | 3 +- DocxTemplater.Test/PatternMatcherTest.cs | 3 + .../SpecialCollectionVariableTest.cs | 63 +++++++++++++++++++ DocxTemplater/PatterMatcher.cs | 2 +- README.md | 6 ++ 5 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 DocxTemplater.Test/SpecialCollectionVariableTest.cs diff --git a/DocxTemplater.Test/DocxTemplater.Test.csproj b/DocxTemplater.Test/DocxTemplater.Test.csproj index 1f24d35..a9cff9e 100644 --- a/DocxTemplater.Test/DocxTemplater.Test.csproj +++ b/DocxTemplater.Test/DocxTemplater.Test.csproj @@ -1,6 +1,7 @@  - enable + 12.0 + enable disable false true diff --git a/DocxTemplater.Test/PatternMatcherTest.cs b/DocxTemplater.Test/PatternMatcherTest.cs index f7e269d..673d9e4 100644 --- a/DocxTemplater.Test/PatternMatcherTest.cs +++ b/DocxTemplater.Test/PatternMatcherTest.cs @@ -57,6 +57,9 @@ static IEnumerable TestPatternMatch_Cases() 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("{?{ds.QrBills.idx == 2}}").Returns(new[] { PatternType.Condition }); + yield return new TestCaseData("{?{ds.QrBills._Idx == 2}}").Returns(new[] { PatternType.Condition }).SetName("underscore in variable name"); + yield return new TestCaseData("{?{ds.QrBills._Idx % 2 == 0}}").Returns(new[] { PatternType.Condition }).SetName("modulo in condition"); 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}}") diff --git a/DocxTemplater.Test/SpecialCollectionVariableTest.cs b/DocxTemplater.Test/SpecialCollectionVariableTest.cs new file mode 100644 index 0000000..fb0d271 --- /dev/null +++ b/DocxTemplater.Test/SpecialCollectionVariableTest.cs @@ -0,0 +1,63 @@ +using DocumentFormat.OpenXml.Packaging; +using DocumentFormat.OpenXml.Wordprocessing; +using DocumentFormat.OpenXml; + +namespace DocxTemplater.Test +{ + internal class SpecialCollectionVariableTest + { + [Test] + public void TestIndexVariableInLoop() + { + var model = new[] { "Item1", "Item2", "Item3", "Item4" }; + var template = "Items:{{#Items}}{{Items._Idx}}{{.}} {{/Items}}"; + + 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(template))))); + wpDocument.Save(); + memStream.Position = 0; + var docTemplate = new DocxTemplate(memStream); + docTemplate.BindModel("Items", model); + var result = docTemplate.Process(); + result.Position = 0; + // compare body + result.Position = 0; + var document = WordprocessingDocument.Open(result, false); + var body = document.MainDocumentPart.Document.Body; + Assert.That(body.InnerXml, Is.EqualTo("" + + "" + + "Items:1Item1" + + " 2Item2" + + " 3Item3" + + " 4Item4" + + " ")); + } + + [Test] + public void TestConditionWithIndexVariableInLoop() + { + var model = new[] { "Item1", "Item2", "Item3", "Item4" }; + var template = "Items:{{#Items}}{?{Items._Idx % 2 == 0}}{{.}}{{/}}{{/Items}}"; + + 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(template))))); + wpDocument.Save(); + memStream.Position = 0; + var docTemplate = new DocxTemplate(memStream); + docTemplate.BindModel("Items", model); + var result = docTemplate.Process(); + result.Position = 0; + // compare body + result.Position = 0; + var document = WordprocessingDocument.Open(result, false); + var body = document.MainDocumentPart.Document.Body; + Assert.That(body.InnerXml, Is.EqualTo("Items:" + + "Item2" + + "Item4")); + } + } +} diff --git a/DocxTemplater/PatterMatcher.cs b/DocxTemplater/PatterMatcher.cs index ba38c21..735a881 100644 --- a/DocxTemplater/PatterMatcher.cs +++ b/DocxTemplater/PatterMatcher.cs @@ -25,7 +25,7 @@ internal static class PatternMatcher (?:\s*s\s*:) | (?(?:else)|:) | (?(condMarker) # if condition marker is set, we expect a condition - (?[a-zA-Z0-9+\-*\/><=\s\.\!&\|]+)? #condition name (without brackets) + (?[a-zA-Z0-9+\-*\/><=\s\.\!&\|_%]+)? #condition expression (without brackets) | (?: (?[\/\#])?(?[a-zA-Z0-9\._]+)? #variable name diff --git a/README.md b/README.md index 7e729fc..b4a19e7 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,7 @@ The syntax is case insensitive. | `{{SomeDate}:F('MM/dd/yyyy')}` | Date variable with formatting - short syntax | | `{{SomeBytes}:img()}` | Image Formatter for image data | | `{{SomeHtmlString}:html()}` | Inserts HTML string into the word document | +| `{{#Items}}{?{Items._Idx % 2 == 0}}{{.}}{{/}}{{/Items}}` | Renders every second item in a list | --- ### Collections @@ -113,6 +114,11 @@ Will render a table row for each item in the collection: |-------|-----------| | John | Developer | | Alice | CEO | + +#### Accessing the Index of the Current Item + +To access the index of the current item, use the special variable `Items._Idx` In this example, the collection is called "Items". + --- ### Separator