diff --git a/TriasDev.Templify.Tests/Helpers/DocumentBuilder.cs b/TriasDev.Templify.Tests/Helpers/DocumentBuilder.cs index 5d92bb6..11118b3 100644 --- a/TriasDev.Templify.Tests/Helpers/DocumentBuilder.cs +++ b/TriasDev.Templify.Tests/Helpers/DocumentBuilder.cs @@ -509,6 +509,33 @@ public DocumentBuilder AddPageBreak() return this; } + /// + /// Sets a document metadata property on the template. + /// + public DocumentBuilder SetAuthor(string author) + { + _document.PackageProperties.Creator = author; + return this; + } + + /// + /// Sets the document title metadata property. + /// + public DocumentBuilder SetTitle(string title) + { + _document.PackageProperties.Title = title; + return this; + } + + /// + /// Sets the document subject metadata property. + /// + public DocumentBuilder SetSubject(string subject) + { + _document.PackageProperties.Subject = subject; + return this; + } + /// /// Returns the document as a MemoryStream for processing. /// diff --git a/TriasDev.Templify.Tests/Helpers/DocumentVerifier.cs b/TriasDev.Templify.Tests/Helpers/DocumentVerifier.cs index 7c818f9..a27b893 100644 --- a/TriasDev.Templify.Tests/Helpers/DocumentVerifier.cs +++ b/TriasDev.Templify.Tests/Helpers/DocumentVerifier.cs @@ -534,6 +534,41 @@ public bool HasUpdateFieldsOnOpen() return updateFields?.Val?.Value == true; } + /// + /// Gets the document Author (Creator) property. + /// + public string? GetDocumentAuthor() => _document.PackageProperties.Creator; + + /// + /// Gets the document Title property. + /// + public string? GetDocumentTitle() => _document.PackageProperties.Title; + + /// + /// Gets the document Subject property. + /// + public string? GetDocumentSubject() => _document.PackageProperties.Subject; + + /// + /// Gets the document Description (Comments) property. + /// + public string? GetDocumentDescription() => _document.PackageProperties.Description; + + /// + /// Gets the document Keywords property. + /// + public string? GetDocumentKeywords() => _document.PackageProperties.Keywords; + + /// + /// Gets the document Category property. + /// + public string? GetDocumentCategory() => _document.PackageProperties.Category; + + /// + /// Gets the document LastModifiedBy property. + /// + public string? GetDocumentLastModifiedBy() => _document.PackageProperties.LastModifiedBy; + public void Dispose() { _document?.Dispose(); diff --git a/TriasDev.Templify.Tests/Integration/DocumentPropertiesTests.cs b/TriasDev.Templify.Tests/Integration/DocumentPropertiesTests.cs new file mode 100644 index 0000000..d42e607 --- /dev/null +++ b/TriasDev.Templify.Tests/Integration/DocumentPropertiesTests.cs @@ -0,0 +1,258 @@ +// Copyright (c) 2025 TriasDev GmbH & Co. KG +// Licensed under the MIT License. See LICENSE file in the project root for full license information. + +using TriasDev.Templify.Core; +using TriasDev.Templify.Tests.Helpers; + +namespace TriasDev.Templify.Tests.Integration; + +/// +/// Integration tests for document metadata properties (Author, Title, etc.). +/// +public sealed class DocumentPropertiesTests +{ + [Fact] + public void ProcessTemplate_SetAuthorOnly_ChangesAuthorPreservesOthers() + { + // Arrange + DocumentBuilder builder = new DocumentBuilder(); + builder.AddParagraph("Hello {{Name}}!"); + builder.SetAuthor("Original Author"); + builder.SetTitle("Original Title"); + + MemoryStream templateStream = builder.ToStream(); + + Dictionary data = new Dictionary + { + ["Name"] = "World" + }; + + PlaceholderReplacementOptions options = new PlaceholderReplacementOptions + { + DocumentProperties = new DocumentProperties + { + Author = "New Author" + } + }; + + DocumentTemplateProcessor processor = new DocumentTemplateProcessor(options); + MemoryStream outputStream = new MemoryStream(); + + // Act + ProcessingResult result = processor.ProcessTemplate(templateStream, outputStream, data); + + // Assert + Assert.True(result.IsSuccess); + + using DocumentVerifier verifier = new DocumentVerifier(outputStream); + Assert.Equal("New Author", verifier.GetDocumentAuthor()); + Assert.Equal("Original Title", verifier.GetDocumentTitle()); + } + + [Fact] + public void ProcessTemplate_SetMultipleProperties_AllSetCorrectly() + { + // Arrange + DocumentBuilder builder = new DocumentBuilder(); + builder.AddParagraph("Content"); + + MemoryStream templateStream = builder.ToStream(); + + Dictionary data = new Dictionary(); + + PlaceholderReplacementOptions options = new PlaceholderReplacementOptions + { + DocumentProperties = new DocumentProperties + { + Author = "Test Author", + Title = "Test Title", + Subject = "Test Subject", + Description = "Test Description", + Keywords = "test, keywords", + Category = "Test Category", + LastModifiedBy = "Test User" + } + }; + + DocumentTemplateProcessor processor = new DocumentTemplateProcessor(options); + MemoryStream outputStream = new MemoryStream(); + + // Act + ProcessingResult result = processor.ProcessTemplate(templateStream, outputStream, data); + + // Assert + Assert.True(result.IsSuccess); + + using DocumentVerifier verifier = new DocumentVerifier(outputStream); + Assert.Equal("Test Author", verifier.GetDocumentAuthor()); + Assert.Equal("Test Title", verifier.GetDocumentTitle()); + Assert.Equal("Test Subject", verifier.GetDocumentSubject()); + Assert.Equal("Test Description", verifier.GetDocumentDescription()); + Assert.Equal("test, keywords", verifier.GetDocumentKeywords()); + Assert.Equal("Test Category", verifier.GetDocumentCategory()); + Assert.Equal("Test User", verifier.GetDocumentLastModifiedBy()); + } + + [Fact] + public void ProcessTemplate_NoDocumentProperties_PreservesOriginalAuthor() + { + // Arrange + DocumentBuilder builder = new DocumentBuilder(); + builder.AddParagraph("Content"); + builder.SetAuthor("Template Author"); + + MemoryStream templateStream = builder.ToStream(); + + Dictionary data = new Dictionary(); + + DocumentTemplateProcessor processor = new DocumentTemplateProcessor(); + MemoryStream outputStream = new MemoryStream(); + + // Act + ProcessingResult result = processor.ProcessTemplate(templateStream, outputStream, data); + + // Assert + Assert.True(result.IsSuccess); + + using DocumentVerifier verifier = new DocumentVerifier(outputStream); + Assert.Equal("Template Author", verifier.GetDocumentAuthor()); + } + + [Fact] + public void ProcessTemplate_DocumentPropertiesWithAllNulls_PreservesOriginalValues() + { + // Arrange + DocumentBuilder builder = new DocumentBuilder(); + builder.AddParagraph("Content"); + builder.SetAuthor("Template Author"); + builder.SetTitle("Template Title"); + builder.SetSubject("Template Subject"); + + MemoryStream templateStream = builder.ToStream(); + + Dictionary data = new Dictionary(); + + PlaceholderReplacementOptions options = new PlaceholderReplacementOptions + { + DocumentProperties = new DocumentProperties() + }; + + DocumentTemplateProcessor processor = new DocumentTemplateProcessor(options); + MemoryStream outputStream = new MemoryStream(); + + // Act + ProcessingResult result = processor.ProcessTemplate(templateStream, outputStream, data); + + // Assert + Assert.True(result.IsSuccess); + + using DocumentVerifier verifier = new DocumentVerifier(outputStream); + Assert.Equal("Template Author", verifier.GetDocumentAuthor()); + Assert.Equal("Template Title", verifier.GetDocumentTitle()); + Assert.Equal("Template Subject", verifier.GetDocumentSubject()); + } + + [Fact] + public void ProcessTemplate_EmptyStringAuthor_SetsToEmpty() + { + // Arrange + DocumentBuilder builder = new DocumentBuilder(); + builder.AddParagraph("Content"); + builder.SetAuthor("Template Author"); + + MemoryStream templateStream = builder.ToStream(); + + Dictionary data = new Dictionary(); + + PlaceholderReplacementOptions options = new PlaceholderReplacementOptions + { + DocumentProperties = new DocumentProperties + { + Author = "" + } + }; + + DocumentTemplateProcessor processor = new DocumentTemplateProcessor(options); + MemoryStream outputStream = new MemoryStream(); + + // Act + ProcessingResult result = processor.ProcessTemplate(templateStream, outputStream, data); + + // Assert + Assert.True(result.IsSuccess); + + using DocumentVerifier verifier = new DocumentVerifier(outputStream); + Assert.Equal("", verifier.GetDocumentAuthor()); + } + + [Fact] + public void ProcessTemplate_DocumentPropertiesWithPlaceholderReplacement_BothWork() + { + // Arrange + DocumentBuilder builder = new DocumentBuilder(); + builder.AddParagraph("Hello {{Name}}!"); + + MemoryStream templateStream = builder.ToStream(); + + Dictionary data = new Dictionary + { + ["Name"] = "World" + }; + + PlaceholderReplacementOptions options = new PlaceholderReplacementOptions + { + DocumentProperties = new DocumentProperties + { + Author = "Generated By Templify", + Title = "Generated Document" + } + }; + + DocumentTemplateProcessor processor = new DocumentTemplateProcessor(options); + MemoryStream outputStream = new MemoryStream(); + + // Act + ProcessingResult result = processor.ProcessTemplate(templateStream, outputStream, data); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal(1, result.ReplacementCount); + + using DocumentVerifier verifier = new DocumentVerifier(outputStream); + Assert.Equal("Hello World!", verifier.GetParagraphText(0)); + Assert.Equal("Generated By Templify", verifier.GetDocumentAuthor()); + Assert.Equal("Generated Document", verifier.GetDocumentTitle()); + } + + [Fact] + public void ProcessTemplate_SetAuthorOnTemplateWithNoOriginalValue_SetsAuthor() + { + // Arrange — template with no pre-existing Author/Creator + DocumentBuilder builder = new DocumentBuilder(); + builder.AddParagraph("Content"); + + MemoryStream templateStream = builder.ToStream(); + + Dictionary data = new Dictionary(); + + PlaceholderReplacementOptions options = new PlaceholderReplacementOptions + { + DocumentProperties = new DocumentProperties + { + Author = "Brand New Author" + } + }; + + DocumentTemplateProcessor processor = new DocumentTemplateProcessor(options); + MemoryStream outputStream = new MemoryStream(); + + // Act + ProcessingResult result = processor.ProcessTemplate(templateStream, outputStream, data); + + // Assert + Assert.True(result.IsSuccess); + + using DocumentVerifier verifier = new DocumentVerifier(outputStream); + Assert.Equal("Brand New Author", verifier.GetDocumentAuthor()); + } +} diff --git a/TriasDev.Templify/Core/DocumentProperties.cs b/TriasDev.Templify/Core/DocumentProperties.cs new file mode 100644 index 0000000..d37a247 --- /dev/null +++ b/TriasDev.Templify/Core/DocumentProperties.cs @@ -0,0 +1,48 @@ +// Copyright (c) 2025 TriasDev GmbH & Co. KG +// Licensed under the MIT License. See LICENSE file in the project root for full license information. + +namespace TriasDev.Templify.Core; + +/// +/// Specifies document metadata properties to set on the output document. +/// Properties left as null preserve the original template value. +/// +public sealed class DocumentProperties +{ + /// + /// Gets or initializes the document author. + /// Maps to the OPC Creator property (shown as "Author" in Word). + /// + public string? Author { get; init; } + + /// + /// Gets or initializes the document title. + /// + public string? Title { get; init; } + + /// + /// Gets or initializes the document subject. + /// + public string? Subject { get; init; } + + /// + /// Gets or initializes the document description. + /// Maps to "Comments" in the Word document properties dialog. + /// + public string? Description { get; init; } + + /// + /// Gets or initializes the document keywords. + /// + public string? Keywords { get; init; } + + /// + /// Gets or initializes the document category. + /// + public string? Category { get; init; } + + /// + /// Gets or initializes the last modified by value. + /// + public string? LastModifiedBy { get; init; } +} diff --git a/TriasDev.Templify/Core/DocumentTemplateProcessor.cs b/TriasDev.Templify/Core/DocumentTemplateProcessor.cs index 8157ca3..293ba59 100644 --- a/TriasDev.Templify/Core/DocumentTemplateProcessor.cs +++ b/TriasDev.Templify/Core/DocumentTemplateProcessor.cs @@ -136,6 +136,12 @@ public ProcessingResult ProcessTemplate( ApplyUpdateFieldsOnOpen(document); } + // Apply document properties if configured + if (_options.DocumentProperties != null) + { + ApplyDocumentProperties(document, _options.DocumentProperties); + } + // Save changes document.MainDocumentPart.Document.Save(); } @@ -357,4 +363,50 @@ private static void ApplyUpdateFieldsOnOpen(WordprocessingDocument document) settingsPart.Settings.Save(); } + + /// + /// Applies configured document metadata properties to the output document. + /// Only non-null property values are applied; null values preserve the original template value. + /// + /// The Word document to update. + /// The document properties to apply. + private static void ApplyDocumentProperties(WordprocessingDocument document, DocumentProperties props) + { + var packageProps = document.PackageProperties; + + if (props.Author != null) + { + packageProps.Creator = props.Author; + } + + if (props.Title != null) + { + packageProps.Title = props.Title; + } + + if (props.Subject != null) + { + packageProps.Subject = props.Subject; + } + + if (props.Description != null) + { + packageProps.Description = props.Description; + } + + if (props.Keywords != null) + { + packageProps.Keywords = props.Keywords; + } + + if (props.Category != null) + { + packageProps.Category = props.Category; + } + + if (props.LastModifiedBy != null) + { + packageProps.LastModifiedBy = props.LastModifiedBy; + } + } } diff --git a/TriasDev.Templify/Core/PlaceholderReplacementOptions.cs b/TriasDev.Templify/Core/PlaceholderReplacementOptions.cs index 3757508..022e796 100644 --- a/TriasDev.Templify/Core/PlaceholderReplacementOptions.cs +++ b/TriasDev.Templify/Core/PlaceholderReplacementOptions.cs @@ -107,6 +107,14 @@ public sealed class PlaceholderReplacementOptions /// public UpdateFieldsOnOpenMode UpdateFieldsOnOpen { get; init; } = UpdateFieldsOnOpenMode.Never; + /// + /// Gets or initializes the document metadata properties to set on the output document. + /// When null (default), the original template properties are preserved unchanged. + /// Only non-null property values within are applied; + /// properties left as null preserve the original template value. + /// + public DocumentProperties? DocumentProperties { get; init; } + /// /// Creates a new instance of with default settings. /// diff --git a/TriasDev.Templify/Examples.md b/TriasDev.Templify/Examples.md index da688d4..376c916 100644 --- a/TriasDev.Templify/Examples.md +++ b/TriasDev.Templify/Examples.md @@ -19,6 +19,7 @@ This document provides practical examples for common use cases. 13. [Processing Multiple Templates](#processing-multiple-templates) 14. [Web Application Integration](#web-application-integration) 15. [Report Generation](#report-generation) +16. [Document Properties](#document-properties) --- @@ -1736,6 +1737,64 @@ public class SalesData --- +## Document Properties + +### Setting Document Metadata + +Set metadata properties on the output document to brand generated documents or track their origin. + +```csharp +using TriasDev.Templify; + +var data = new Dictionary +{ + ["InvoiceNumber"] = "INV-2025-001", + ["CustomerName"] = "Acme Corporation", + ["Total"] = 4153.10m +}; + +var options = new PlaceholderReplacementOptions +{ + DocumentProperties = new DocumentProperties + { + Author = "Billing System v2.0", + Title = "Invoice INV-2025-001", + Category = "Invoices" + } +}; + +var processor = new DocumentTemplateProcessor(options); + +using var templateStream = File.OpenRead("invoice-template.docx"); +using var outputStream = File.Create("invoice-output.docx"); + +var result = processor.ProcessTemplate(templateStream, outputStream, data); +// Output document has Author="Billing System v2.0", Title="Invoice INV-2025-001", Category="Invoices" +// All other metadata properties (Subject, Keywords, etc.) are preserved from the template +``` + +### Combining with Other Options + +```csharp +using System.Globalization; + +var invoiceNumber = "INV-2025-042"; + +var options = new PlaceholderReplacementOptions +{ + Culture = new CultureInfo("de-DE"), + UpdateFieldsOnOpen = UpdateFieldsOnOpenMode.Auto, + DocumentProperties = new DocumentProperties + { + Author = "Rechnungssystem", + Title = $"Rechnung {invoiceNumber}", + LastModifiedBy = "Templify" + } +}; +``` + +--- + ## Best Practices ### 1. Reuse Processor Instances diff --git a/TriasDev.Templify/README.md b/TriasDev.Templify/README.md index fbc7554..4b6d406 100644 --- a/TriasDev.Templify/README.md +++ b/TriasDev.Templify/README.md @@ -796,6 +796,56 @@ var processor = new DocumentTemplateProcessor(options); Page numbers are calculated by Word's layout engine at render time. The OpenXML SDK (and Templify) can only store the document structure, not the rendered output. Only Word can calculate actual page numbers based on fonts, margins, page breaks, etc. +### Document Properties + +Set metadata properties (Author, Title, Subject, etc.) on the output document. This is useful for branding generated documents or tracking how they were created. + +```csharp +var options = new PlaceholderReplacementOptions +{ + DocumentProperties = new DocumentProperties + { + Author = "Generated by Templify", + Title = "Invoice INV-2025-001", + Subject = "Monthly Invoice", + Description = "Auto-generated invoice for November 2025", + Keywords = "invoice, templify, auto-generated", + Category = "Finance", + LastModifiedBy = "Templify Engine" + } +}; + +var processor = new DocumentTemplateProcessor(options); +``` + +**Behavior:** +- When `DocumentProperties` is `null` (default), all original template metadata is preserved unchanged. +- When `DocumentProperties` is set, only non-null property values are applied. Properties left as `null` preserve the original template value. +- Setting a property to an empty string (`""`) explicitly clears it. + +| Property | Word Property Dialog Field | OPC Property | +|----------|---------------------------|--------------| +| `Author` | Author | `Creator` | +| `Title` | Title | `Title` | +| `Subject` | Subject | `Subject` | +| `Description` | Comments | `Description` | +| `Keywords` | Keywords | `Keywords` | +| `Category` | Category | `Category` | +| `LastModifiedBy` | Last Modified By | `LastModifiedBy` | + +**Example: Set only Author, preserve everything else:** + +```csharp +var options = new PlaceholderReplacementOptions +{ + DocumentProperties = new DocumentProperties + { + Author = "My Application" + // Title, Subject, etc. remain as they were in the template + } +}; +``` + ### Culture and Formatting Templify allows you to control how numbers, dates, and other culture-sensitive values are formatted in your documents. This is particularly important for international documents or when you need consistent formatting across different systems. @@ -1137,6 +1187,7 @@ Configuration options for template processing. - `BooleanFormatterRegistry`: Custom formatters for boolean display - `EnableNewlineSupport`: Convert \n to line breaks in Word (default: `true`) - `TextReplacements`: Dictionary of text replacements (e.g., HTML entities) +- `DocumentProperties`: Metadata properties to set on the output document (default: `null` — preserves original) ### ProcessingResult diff --git a/docs/for-developers/quick-start.md b/docs/for-developers/quick-start.md index 5b37be3..fa037d6 100644 --- a/docs/for-developers/quick-start.md +++ b/docs/for-developers/quick-start.md @@ -56,6 +56,7 @@ var processor = new DocumentTemplateProcessor(options); | `EnableNewlineSupport` | `bool` | `true` | Convert `\n` to Word line breaks | | `UpdateFieldsOnOpen` | enum | `Never` | When to prompt Word to update fields | | `TextReplacements` | dictionary | `null` | Text replacement lookup table | +| `DocumentProperties` | `DocumentProperties?` | `null` | Metadata properties to set on output document | ## Update Fields on Open (TOC Support) @@ -107,6 +108,35 @@ var options = new PlaceholderReplacementOptions > **Note:** When enabled, Word displays a prompt asking the user to confirm field updates. This is a security measure built into Word. +## Document Properties + +Set metadata on the output document (Author, Title, Subject, etc.). Properties left as `null` preserve the original template value. + +```csharp +var options = new PlaceholderReplacementOptions +{ + DocumentProperties = new DocumentProperties + { + Author = "My Application", + Title = "Generated Report" + } +}; +``` + +### Available Properties + +| Property | Maps to in Word | +|----------|----------------| +| `Author` | Author (Creator) | +| `Title` | Title | +| `Subject` | Subject | +| `Description` | Comments | +| `Keywords` | Keywords | +| `Category` | Category | +| `LastModifiedBy` | Last Modified By | + +> **Note:** When `DocumentProperties` is `null` (default), all original template metadata is preserved. When set, only non-null properties are applied. + ## Need Help? - 🐛 [Report Issues](https://github.com/triasdev/templify/issues)