diff --git a/TriasDev.Templify.Tests/Helpers/DocumentBuilder.cs b/TriasDev.Templify.Tests/Helpers/DocumentBuilder.cs
index a79cc5c..5d92bb6 100644
--- a/TriasDev.Templify.Tests/Helpers/DocumentBuilder.cs
+++ b/TriasDev.Templify.Tests/Helpers/DocumentBuilder.cs
@@ -390,6 +390,125 @@ public static RunProperties CreateFormatting(
return properties;
}
+ ///
+ /// Adds a simple Table of Contents field to the document.
+ /// This creates a TOC field that Word can update but Templify cannot.
+ /// The TOC will contain entries pointing to page numbers that become stale after processing.
+ ///
+ /// List of entries to show in the TOC, each with (text, pageNumber)
+ public DocumentBuilder AddTableOfContents(params (string text, int pageNumber)[] tocEntries)
+ {
+ // Add TOC title
+ Paragraph titlePara = new Paragraph();
+ Run titleRun = new Run();
+ titleRun.RunProperties = new RunProperties(new Bold());
+ titleRun.Append(new Text("Table of Contents"));
+ titlePara.Append(titleRun);
+ _body.Append(titlePara);
+
+ // Add TOC field begin
+ Paragraph tocPara = new Paragraph();
+
+ // Field begin character
+ Run beginRun = new Run();
+ beginRun.Append(new FieldChar { FieldCharType = FieldCharValues.Begin });
+ tocPara.Append(beginRun);
+
+ // Field instruction (TOC command)
+ Run instrRun = new Run();
+ instrRun.Append(new FieldCode(" TOC \\o \"1-3\" \\h \\z \\u ") { Space = SpaceProcessingModeValues.Preserve });
+ tocPara.Append(instrRun);
+
+ // Field separator
+ Run sepRun = new Run();
+ sepRun.Append(new FieldChar { FieldCharType = FieldCharValues.Separate });
+ tocPara.Append(sepRun);
+
+ _body.Append(tocPara);
+
+ // Add TOC entries (these are the cached/displayed values)
+ foreach ((string text, int pageNumber) in tocEntries)
+ {
+ Paragraph entryPara = new Paragraph();
+
+ // Hyperlink-like entry text
+ Run textRun = new Run();
+ textRun.Append(new Text(text) { Space = SpaceProcessingModeValues.Preserve });
+ entryPara.Append(textRun);
+
+ // Tab character to separate text from page number
+ Run tabRun = new Run();
+ tabRun.Append(new TabChar());
+ entryPara.Append(tabRun);
+
+ // Page number (this is the cached value that becomes stale)
+ Run pageRun = new Run();
+ pageRun.Append(new Text(pageNumber.ToString()));
+ entryPara.Append(pageRun);
+
+ _body.Append(entryPara);
+ }
+
+ // Field end character
+ Paragraph endPara = new Paragraph();
+ Run endRun = new Run();
+ endRun.Append(new FieldChar { FieldCharType = FieldCharValues.End });
+ endPara.Append(endRun);
+ _body.Append(endPara);
+
+ return this;
+ }
+
+ ///
+ /// Adds a heading paragraph with the specified style level (1-3).
+ /// These are typically what TOC entries reference.
+ ///
+ public DocumentBuilder AddHeading(string text, int level = 1)
+ {
+ Paragraph paragraph = new Paragraph();
+
+ // Add paragraph properties with heading style
+ ParagraphProperties paraProps = new ParagraphProperties();
+ paraProps.Append(new ParagraphStyleId { Val = $"Heading{level}" });
+ paragraph.Append(paraProps);
+
+ Run run = new Run();
+ RunProperties runProps = new RunProperties(new Bold());
+ if (level == 1)
+ {
+ runProps.Append(new FontSize { Val = "32" }); // 16pt
+ }
+ else if (level == 2)
+ {
+ runProps.Append(new FontSize { Val = "28" }); // 14pt
+ }
+ else
+ {
+ runProps.Append(new FontSize { Val = "24" }); // 12pt
+ }
+ run.RunProperties = runProps;
+ run.Append(new Text(text) { Space = SpaceProcessingModeValues.Preserve });
+
+ paragraph.Append(run);
+ _body.Append(paragraph);
+
+ return this;
+ }
+
+ ///
+ /// Adds a page break to simulate multi-page documents.
+ ///
+ public DocumentBuilder AddPageBreak()
+ {
+ Paragraph paragraph = new Paragraph();
+ Run run = new Run();
+ run.Append(new Break { Type = BreakValues.Page });
+ paragraph.Append(run);
+ _body.Append(paragraph);
+
+ 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 1d365f0..7c818f9 100644
--- a/TriasDev.Templify.Tests/Helpers/DocumentVerifier.cs
+++ b/TriasDev.Templify.Tests/Helpers/DocumentVerifier.cs
@@ -448,6 +448,92 @@ public bool IsListItem(int paragraphIndex)
return numProps != null && numProps.NumberingId != null;
}
+ ///
+ /// Checks if the document contains a TOC field.
+ ///
+ public bool HasTableOfContents()
+ {
+ return _body.Descendants()
+ .Any(fc => fc.Text?.Contains("TOC", StringComparison.OrdinalIgnoreCase) == true);
+ }
+
+ ///
+ /// Gets all TOC entry texts from the document.
+ /// Note: This extracts the cached/displayed TOC entries, not the actual headings.
+ ///
+ public List GetTableOfContentsEntries()
+ {
+ List entries = new List();
+ bool inTocField = false;
+ bool afterSeparator = false;
+
+ foreach (Paragraph para in _body.Descendants())
+ {
+ foreach (Run run in para.Descendants())
+ {
+ FieldChar? fieldChar = run.GetFirstChild();
+ if (fieldChar != null)
+ {
+ if (fieldChar.FieldCharType?.Value == FieldCharValues.Begin)
+ {
+ inTocField = true;
+ afterSeparator = false;
+ }
+ else if (fieldChar.FieldCharType?.Value == FieldCharValues.Separate)
+ {
+ afterSeparator = true;
+ }
+ else if (fieldChar.FieldCharType?.Value == FieldCharValues.End)
+ {
+ inTocField = false;
+ afterSeparator = false;
+ }
+ }
+ }
+
+ // Collect text from paragraphs after the separator (the TOC entries)
+ if (inTocField && afterSeparator)
+ {
+ string text = para.InnerText.Trim();
+ if (!string.IsNullOrEmpty(text))
+ {
+ entries.Add(text);
+ }
+ }
+ }
+
+ return entries;
+ }
+
+ ///
+ /// Gets all page numbers displayed in TOC entries.
+ ///
+ public List GetTableOfContentsPageNumbers()
+ {
+ // TOC entries typically end with a page number
+ return GetTableOfContentsEntries()
+ .Select(entry => entry.Split('\t', ' '))
+ .Where(parts => parts.Length > 0 && int.TryParse(parts[^1], out _))
+ .Select(parts => int.Parse(parts[^1]))
+ .ToList();
+ }
+
+ ///
+ /// Checks if the document has the UpdateFieldsOnOpen setting enabled.
+ /// When enabled, Word will prompt to update fields (including TOC) when the document is opened.
+ ///
+ public bool HasUpdateFieldsOnOpen()
+ {
+ DocumentSettingsPart? settingsPart = _document.MainDocumentPart?.DocumentSettingsPart;
+ if (settingsPart?.Settings == null)
+ {
+ return false;
+ }
+
+ UpdateFieldsOnOpen? updateFields = settingsPart.Settings.GetFirstChild();
+ return updateFields?.Val?.Value == true;
+ }
+
public void Dispose()
{
_document?.Dispose();
diff --git a/TriasDev.Templify.Tests/Integration/TableOfContentsTests.cs b/TriasDev.Templify.Tests/Integration/TableOfContentsTests.cs
new file mode 100644
index 0000000..fc00e6f
--- /dev/null
+++ b/TriasDev.Templify.Tests/Integration/TableOfContentsTests.cs
@@ -0,0 +1,564 @@
+// Copyright (c) 2025 TriasDev GmbH & Co. KG
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+
+using System.Globalization;
+using TriasDev.Templify.Core;
+using TriasDev.Templify.Tests.Helpers;
+
+namespace TriasDev.Templify.Tests.Integration;
+
+///
+/// Tests documenting the behavior of Table of Contents (TOC) in processed documents.
+///
+/// IMPORTANT: Templify cannot update TOC page numbers. This is a fundamental limitation
+/// because page numbers are calculated by Word's layout engine at render time.
+///
+/// When a template with TOC is processed and content is removed (e.g., conditionals
+/// evaluate to false, loops produce fewer items), the TOC will display stale page numbers.
+/// Users must manually update the TOC in Word after processing.
+///
+public sealed class TableOfContentsTests
+{
+ ///
+ /// This test documents that TOC page numbers become STALE after template processing
+ /// when content is removed via conditionals. This is EXPECTED BEHAVIOR.
+ ///
+ /// Scenario:
+ /// - Template has TOC showing: Chapter 1 (page 1), Chapter 2 (page 3), Chapter 3 (page 5)
+ /// - A conditional removes Chapter 2 entirely
+ /// - After processing, the TOC STILL shows pages 1, 3, 5 (unchanged/stale)
+ /// - The actual content has shifted, but TOC page numbers are NOT updated
+ ///
+ /// This is a fundamental limitation: page numbers are calculated by Word's layout
+ /// engine at render time. Templify cannot know what page content will appear on.
+ ///
+ /// Users must manually update the TOC in Word after processing (Ctrl+A, F9).
+ ///
+ [Fact]
+ public void ProcessTemplate_WithTOC_PageNumbersRemainStaleAfterContentRemoval()
+ {
+ // Arrange: Create a document with TOC and conditional content
+ DocumentBuilder builder = new DocumentBuilder();
+
+ // Add TOC with original page numbers
+ builder.AddTableOfContents(
+ ("Chapter 1: Introduction", 1),
+ ("Chapter 2: Optional Section", 3),
+ ("Chapter 3: Conclusion", 5)
+ );
+
+ // Chapter 1 - always present
+ builder.AddHeading("Chapter 1: Introduction");
+ builder.AddParagraph("This is the introduction content.");
+ builder.AddPageBreak();
+
+ // Chapter 2 - conditional, will be removed
+ builder.AddParagraph("{{#if IncludeOptionalSection}}");
+ builder.AddHeading("Chapter 2: Optional Section");
+ builder.AddParagraph("This optional section has lots of content...");
+ builder.AddParagraph("More content that takes up space...");
+ builder.AddPageBreak();
+ builder.AddParagraph("{{/if}}");
+
+ // Chapter 3 - always present
+ builder.AddHeading("Chapter 3: Conclusion");
+ builder.AddParagraph("This is the conclusion.");
+
+ MemoryStream templateStream = builder.ToStream();
+
+ // Data: IncludeOptionalSection is false, so Chapter 2 will be removed
+ Dictionary data = new Dictionary
+ {
+ ["IncludeOptionalSection"] = false
+ };
+
+ PlaceholderReplacementOptions options = new PlaceholderReplacementOptions
+ {
+ Culture = CultureInfo.InvariantCulture
+ };
+
+ 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);
+
+ // Verify TOC still exists
+ Assert.True(verifier.HasTableOfContents());
+
+ // Get TOC entries after processing
+ List tocEntries = verifier.GetTableOfContentsEntries();
+
+ // This documents the LIMITATION: TOC entries are NOT updated
+ // The TOC still contains entries for Chapter 2, even though it was removed
+ // The cached page numbers in the TOC entries remain unchanged
+ Assert.True(tocEntries.Count >= 3, $"Expected at least 3 TOC entries, found {tocEntries.Count}: [{string.Join(", ", tocEntries)}]");
+
+ // Verify original TOC entries still exist with their original content
+ Assert.Contains(tocEntries, e => e.Contains("Chapter 1") && e.Contains("1"));
+ Assert.Contains(tocEntries, e => e.Contains("Chapter 2") && e.Contains("3"));
+ Assert.Contains(tocEntries, e => e.Contains("Chapter 3") && e.Contains("5"));
+ }
+
+ ///
+ /// Verifies that TOC structure is preserved after template processing.
+ /// The TOC field itself should not be corrupted.
+ ///
+ [Fact]
+ public void ProcessTemplate_WithTOC_PreservesTocStructure()
+ {
+ // Arrange
+ DocumentBuilder builder = new DocumentBuilder();
+
+ builder.AddTableOfContents(
+ ("Section A", 1),
+ ("Section B", 2)
+ );
+
+ builder.AddHeading("Section A");
+ builder.AddParagraph("Content for {{SectionName}}");
+ builder.AddHeading("Section B");
+ builder.AddParagraph("More content");
+
+ MemoryStream templateStream = builder.ToStream();
+
+ Dictionary data = new Dictionary
+ {
+ ["SectionName"] = "the first section"
+ };
+
+ PlaceholderReplacementOptions options = new PlaceholderReplacementOptions
+ {
+ Culture = CultureInfo.InvariantCulture
+ };
+
+ 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);
+
+ // TOC should still be present
+ Assert.True(verifier.HasTableOfContents());
+
+ // TOC entries should be preserved (check that expected entries exist)
+ List entries = verifier.GetTableOfContentsEntries();
+ Assert.True(entries.Count >= 2, $"Expected at least 2 TOC entries, found {entries.Count}");
+ Assert.Contains(entries, e => e.Contains("Section A"));
+ Assert.Contains(entries, e => e.Contains("Section B"));
+ }
+
+ ///
+ /// USER ISSUE: When a template with TOC is processed, the TOC becomes stale.
+ ///
+ /// The user expects that when opening the document in Word, the TOC will be
+ /// automatically updated to reflect the correct page numbers.
+ ///
+ /// SOLUTION: Set UpdateFieldsOnOpen = true so Word prompts to update fields on open.
+ ///
+ [Fact]
+ public void ProcessTemplate_WithTOC_ShouldSetUpdateFieldsOnOpen_SoWordRefreshesToc()
+ {
+ // Arrange: Create a document with TOC and conditional content (user's scenario)
+ DocumentBuilder builder = new DocumentBuilder();
+
+ // TOC with page numbers that will become stale after processing
+ builder.AddTableOfContents(
+ ("Chapter 1: Introduction", 1),
+ ("Chapter 2: Optional Section", 3),
+ ("Chapter 3: Conclusion", 5)
+ );
+
+ // Chapter 1 - always present
+ builder.AddHeading("Chapter 1: Introduction");
+ builder.AddParagraph("Introduction content.");
+ builder.AddPageBreak();
+
+ // Chapter 2 - conditional, will be removed (causes page number shift)
+ builder.AddParagraph("{{#if IncludeOptionalSection}}");
+ builder.AddHeading("Chapter 2: Optional Section");
+ builder.AddParagraph("Optional content that takes space.");
+ builder.AddPageBreak();
+ builder.AddParagraph("{{/if}}");
+
+ // Chapter 3 - always present (now on earlier page after Chapter 2 removed)
+ builder.AddHeading("Chapter 3: Conclusion");
+ builder.AddParagraph("Conclusion content.");
+
+ MemoryStream templateStream = builder.ToStream();
+
+ Dictionary data = new Dictionary
+ {
+ ["IncludeOptionalSection"] = false // Chapter 2 will be removed
+ };
+
+ // FIX: Enable UpdateFieldsOnOpen so Word refreshes TOC on open
+ PlaceholderReplacementOptions options = new PlaceholderReplacementOptions
+ {
+ Culture = CultureInfo.InvariantCulture,
+ UpdateFieldsOnOpen = UpdateFieldsOnOpenMode.Always // THE FIX: Word will prompt to update TOC
+ };
+
+ 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);
+
+ // The document should have UpdateFieldsOnOpen enabled
+ // so Word will prompt to refresh the TOC when opened
+ Assert.True(verifier.HasUpdateFieldsOnOpen(),
+ "Document should have UpdateFieldsOnOpen=true so Word refreshes TOC on open");
+ }
+
+ ///
+ /// Documents that placeholder replacement works correctly in content
+ /// that follows a TOC, without corrupting the TOC.
+ ///
+ [Fact]
+ public void ProcessTemplate_PlaceholdersAfterTOC_ReplacesCorrectly()
+ {
+ // Arrange
+ DocumentBuilder builder = new DocumentBuilder();
+
+ builder.AddTableOfContents(
+ ("Overview", 1)
+ );
+
+ builder.AddHeading("Overview");
+ builder.AddParagraph("Document created for {{CustomerName}} on {{Date}}.");
+
+ MemoryStream templateStream = builder.ToStream();
+
+ Dictionary data = new Dictionary
+ {
+ ["CustomerName"] = "Acme Corp",
+ ["Date"] = "2025-01-06"
+ };
+
+ PlaceholderReplacementOptions options = new PlaceholderReplacementOptions
+ {
+ Culture = CultureInfo.InvariantCulture
+ };
+
+ DocumentTemplateProcessor processor = new DocumentTemplateProcessor(options);
+ MemoryStream outputStream = new MemoryStream();
+
+ // Act
+ ProcessingResult result = processor.ProcessTemplate(templateStream, outputStream, data);
+
+ // Assert
+ Assert.True(result.IsSuccess);
+ Assert.Equal(2, result.ReplacementCount);
+
+ using DocumentVerifier verifier = new DocumentVerifier(outputStream);
+
+ // TOC preserved
+ Assert.True(verifier.HasTableOfContents());
+
+ // Placeholders replaced
+ List paragraphs = verifier.GetAllParagraphTexts();
+ Assert.Contains(paragraphs, p => p.Contains("Acme Corp") && p.Contains("2025-01-06"));
+ }
+
+ ///
+ /// Verifies that the UpdateFieldsOnOpen option sets the appropriate document setting.
+ /// When enabled, Word will prompt the user to update all fields (including TOC)
+ /// when the document is first opened.
+ ///
+ [Fact]
+ public void ProcessTemplate_WithUpdateFieldsOnOpen_SetsDocumentSetting()
+ {
+ // Arrange
+ DocumentBuilder builder = new DocumentBuilder();
+
+ builder.AddTableOfContents(
+ ("Chapter 1", 1),
+ ("Chapter 2", 5)
+ );
+
+ builder.AddHeading("Chapter 1");
+ builder.AddParagraph("Content for {{Title}}");
+
+ MemoryStream templateStream = builder.ToStream();
+
+ Dictionary data = new Dictionary
+ {
+ ["Title"] = "Test Document"
+ };
+
+ PlaceholderReplacementOptions options = new PlaceholderReplacementOptions
+ {
+ Culture = CultureInfo.InvariantCulture,
+ UpdateFieldsOnOpen = UpdateFieldsOnOpenMode.Always // Enable auto-update
+ };
+
+ 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);
+
+ // The document should have UpdateFieldsOnOpen setting enabled
+ Assert.True(verifier.HasUpdateFieldsOnOpen(),
+ "Expected UpdateFieldsOnOpen to be set in document settings");
+ }
+
+ ///
+ /// Verifies that UpdateFieldsOnOpen is NOT set by default.
+ /// This ensures backward compatibility.
+ ///
+ [Fact]
+ public void ProcessTemplate_WithoutUpdateFieldsOnOpen_DoesNotSetDocumentSetting()
+ {
+ // Arrange
+ DocumentBuilder builder = new DocumentBuilder();
+
+ builder.AddTableOfContents(("Section", 1));
+ builder.AddParagraph("Content");
+
+ MemoryStream templateStream = builder.ToStream();
+
+ Dictionary data = new Dictionary();
+
+ // Default options (UpdateFieldsOnOpen = Never)
+ 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);
+
+ // UpdateFieldsOnOpen should NOT be set
+ Assert.False(verifier.HasUpdateFieldsOnOpen(),
+ "Expected UpdateFieldsOnOpen to NOT be set by default");
+ }
+
+ ///
+ /// SOLUTION TEST: With UpdateFieldsOnOpen enabled, Word will automatically prompt
+ /// to update the TOC when the document is opened, fixing stale page numbers.
+ ///
+ /// This test verifies the recommended solution for users with TOC documents.
+ ///
+ [Fact]
+ public void ProcessTemplate_WithTOC_AndUpdateFieldsOnOpen_SolutionForStalePageNumbers()
+ {
+ // Arrange: Create a document with TOC and conditional content
+ DocumentBuilder builder = new DocumentBuilder();
+
+ builder.AddTableOfContents(
+ ("Chapter 1: Introduction", 1),
+ ("Chapter 2: Optional Section", 3),
+ ("Chapter 3: Conclusion", 5)
+ );
+
+ builder.AddHeading("Chapter 1: Introduction");
+ builder.AddParagraph("Introduction content.");
+
+ builder.AddParagraph("{{#if IncludeOptionalSection}}");
+ builder.AddHeading("Chapter 2: Optional Section");
+ builder.AddParagraph("Optional content.");
+ builder.AddParagraph("{{/if}}");
+
+ builder.AddHeading("Chapter 3: Conclusion");
+ builder.AddParagraph("Conclusion content.");
+
+ MemoryStream templateStream = builder.ToStream();
+
+ Dictionary data = new Dictionary
+ {
+ ["IncludeOptionalSection"] = false // Chapter 2 will be removed
+ };
+
+ // THE SOLUTION: Enable UpdateFieldsOnOpen
+ PlaceholderReplacementOptions options = new PlaceholderReplacementOptions
+ {
+ UpdateFieldsOnOpen = UpdateFieldsOnOpenMode.Always
+ };
+
+ 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);
+
+ // TOC still exists
+ Assert.True(verifier.HasTableOfContents());
+
+ // UpdateFieldsOnOpen is set - Word will prompt to update TOC on open
+ Assert.True(verifier.HasUpdateFieldsOnOpen(),
+ "UpdateFieldsOnOpen should be enabled so Word updates TOC on open");
+
+ // Note: The TOC entries still contain stale data in the file,
+ // but Word will refresh them when the user opens the document
+ // and confirms the update prompt.
+ }
+
+ ///
+ /// Tests that Auto mode sets UpdateFieldsOnOpen when document contains a TOC.
+ ///
+ [Fact]
+ public void ProcessTemplate_WithAutoMode_AndTOC_SetsUpdateFieldsOnOpen()
+ {
+ // Arrange: Document with TOC
+ DocumentBuilder builder = new DocumentBuilder();
+
+ builder.AddTableOfContents(
+ ("Chapter 1", 1),
+ ("Chapter 2", 2)
+ );
+
+ builder.AddHeading("Chapter 1");
+ builder.AddParagraph("Content for {{Title}}");
+
+ MemoryStream templateStream = builder.ToStream();
+
+ Dictionary data = new Dictionary
+ {
+ ["Title"] = "Test"
+ };
+
+ // Auto mode - should detect TOC and set the flag
+ PlaceholderReplacementOptions options = new PlaceholderReplacementOptions
+ {
+ UpdateFieldsOnOpen = UpdateFieldsOnOpenMode.Auto
+ };
+
+ 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);
+
+ // Auto mode should have detected the TOC and set UpdateFieldsOnOpen
+ Assert.True(verifier.HasUpdateFieldsOnOpen(),
+ "Auto mode should set UpdateFieldsOnOpen when document contains TOC");
+ }
+
+ ///
+ /// Tests that Auto mode does NOT set UpdateFieldsOnOpen when document has no fields.
+ ///
+ [Fact]
+ public void ProcessTemplate_WithAutoMode_WithoutFields_DoesNotSetUpdateFieldsOnOpen()
+ {
+ // Arrange: Document without any fields (no TOC, no PAGE, etc.)
+ DocumentBuilder builder = new DocumentBuilder();
+
+ builder.AddParagraph("Hello {{Name}}!");
+ builder.AddParagraph("This document has no fields.");
+
+ MemoryStream templateStream = builder.ToStream();
+
+ Dictionary data = new Dictionary
+ {
+ ["Name"] = "World"
+ };
+
+ // Auto mode - should NOT set flag because there are no fields
+ PlaceholderReplacementOptions options = new PlaceholderReplacementOptions
+ {
+ UpdateFieldsOnOpen = UpdateFieldsOnOpenMode.Auto
+ };
+
+ 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);
+
+ // Auto mode should NOT have set UpdateFieldsOnOpen (no fields detected)
+ Assert.False(verifier.HasUpdateFieldsOnOpen(),
+ "Auto mode should NOT set UpdateFieldsOnOpen when document has no fields");
+ }
+
+ ///
+ /// Documents the recommended usage: Auto mode for applications processing various templates.
+ /// Users who upload templates with TOC will get the update prompt; others won't.
+ ///
+ [Fact]
+ public void ProcessTemplate_AutoMode_RecommendedForMixedTemplates()
+ {
+ // This test documents the recommended approach for applications
+ // that process templates uploaded by users (like the user's scenario).
+
+ // Template WITH TOC
+ DocumentBuilder builderWithToc = new DocumentBuilder();
+ builderWithToc.AddTableOfContents(("Section", 1));
+ builderWithToc.AddParagraph("Content");
+ MemoryStream templateWithToc = builderWithToc.ToStream();
+
+ // Template WITHOUT TOC
+ DocumentBuilder builderWithoutToc = new DocumentBuilder();
+ builderWithoutToc.AddParagraph("Simple {{Placeholder}}");
+ MemoryStream templateWithoutToc = builderWithoutToc.ToStream();
+
+ // Same options for both - Auto mode handles detection
+ PlaceholderReplacementOptions options = new PlaceholderReplacementOptions
+ {
+ UpdateFieldsOnOpen = UpdateFieldsOnOpenMode.Auto
+ };
+
+ Dictionary data = new Dictionary
+ {
+ ["Placeholder"] = "content"
+ };
+
+ DocumentTemplateProcessor processor = new DocumentTemplateProcessor(options);
+
+ // Process template WITH TOC
+ MemoryStream outputWithToc = new MemoryStream();
+ processor.ProcessTemplate(templateWithToc, outputWithToc, data);
+
+ // Process template WITHOUT TOC
+ MemoryStream outputWithoutToc = new MemoryStream();
+ processor.ProcessTemplate(templateWithoutToc, outputWithoutToc, data);
+
+ // Assert: Only the TOC document should have UpdateFieldsOnOpen set
+ using DocumentVerifier verifierWithToc = new DocumentVerifier(outputWithToc);
+ using DocumentVerifier verifierWithoutToc = new DocumentVerifier(outputWithoutToc);
+
+ Assert.True(verifierWithToc.HasUpdateFieldsOnOpen(),
+ "Document WITH TOC should have UpdateFieldsOnOpen");
+ Assert.False(verifierWithoutToc.HasUpdateFieldsOnOpen(),
+ "Document WITHOUT TOC should NOT have UpdateFieldsOnOpen");
+ }
+}
diff --git a/TriasDev.Templify/Core/DocumentTemplateProcessor.cs b/TriasDev.Templify/Core/DocumentTemplateProcessor.cs
index fee70e2..067f096 100644
--- a/TriasDev.Templify/Core/DocumentTemplateProcessor.cs
+++ b/TriasDev.Templify/Core/DocumentTemplateProcessor.cs
@@ -3,6 +3,7 @@
using System.Text.Json;
using DocumentFormat.OpenXml.Packaging;
+using DocumentFormat.OpenXml.Wordprocessing;
using TriasDev.Templify.Placeholders;
using TriasDev.Templify.Utilities;
using TriasDev.Templify.Visitors;
@@ -14,6 +15,24 @@ namespace TriasDev.Templify.Core;
///
public sealed class DocumentTemplateProcessor
{
+ ///
+ /// Field types that may need updating when document content changes.
+ /// Used by Auto mode to detect if UpdateFieldsOnOpen should be set.
+ ///
+ private static readonly string[] _dynamicFieldTypes = new[]
+ {
+ "TOC", // Table of Contents
+ "PAGE", // Current page number
+ "NUMPAGES", // Total page count
+ "PAGEREF", // Page references (cross-references to bookmarks)
+ "DATE", // Current date
+ "TIME", // Current time
+ "FILENAME", // Document filename
+ "REF", // Cross-references
+ "NOTEREF", // Footnote/endnote references
+ "SECTIONPAGES" // Pages in current section
+ };
+
private readonly PlaceholderReplacementOptions _options;
///
@@ -103,6 +122,19 @@ public ProcessingResult ProcessTemplate(
// Walk the document with the composite visitor
walker.Walk(document, composite, globalContext);
+ // Apply UpdateFieldsOnOpen setting based on mode
+ bool shouldUpdateFields = _options.UpdateFieldsOnOpen switch
+ {
+ UpdateFieldsOnOpenMode.Always => true,
+ UpdateFieldsOnOpenMode.Auto => HasFields(document),
+ _ => false
+ };
+
+ if (shouldUpdateFields)
+ {
+ ApplyUpdateFieldsOnOpen(document);
+ }
+
// Save changes
document.MainDocumentPart.Document.Save();
}
@@ -239,4 +271,88 @@ private static void CopyStream(Stream source, Stream destination)
source.CopyTo(destination);
}
+
+ ///
+ /// Checks if the document contains any fields that would benefit from updating on open.
+ ///
+ /// The Word document to check.
+ /// True if the document contains fields like TOC, PAGE, NUMPAGES, etc.
+ private static bool HasFields(WordprocessingDocument document)
+ {
+ if (document.MainDocumentPart?.Document?.Body == null)
+ {
+ return false;
+ }
+
+ return document.MainDocumentPart.Document.Body
+ .Descendants()
+ .Any(fc =>
+ {
+ if (string.IsNullOrWhiteSpace(fc.Text))
+ {
+ return false;
+ }
+
+ // Field codes typically start with the field type, followed by
+ // spaces, switches (starting with '\'), and parameters.
+ // Example: " TOC \o \"1-3\" \h \z \u "
+ string text = fc.Text.TrimStart();
+
+ // Get the first token (up to whitespace or a backslash for switches)
+ int endIndex = text.IndexOfAny(new[] { ' ', '\t', '\r', '\n', '\\' });
+ string fieldTypeInDoc = endIndex >= 0 ? text[..endIndex] : text;
+
+ foreach (string field in _dynamicFieldTypes)
+ {
+ if (fieldTypeInDoc.Equals(field, StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ });
+ }
+
+ ///
+ /// Configures the document to update all fields (including TOC) when opened in Word.
+ ///
+ /// The Word document to configure.
+ private static void ApplyUpdateFieldsOnOpen(WordprocessingDocument document)
+ {
+ if (document.MainDocumentPart == null)
+ {
+ return;
+ }
+
+ // Get or create the document settings part
+ DocumentSettingsPart? settingsPart = document.MainDocumentPart.DocumentSettingsPart;
+ if (settingsPart == null)
+ {
+ settingsPart = document.MainDocumentPart.AddNewPart();
+ settingsPart.Settings = new Settings();
+ }
+
+ if (settingsPart.Settings == null)
+ {
+ settingsPart.Settings = new Settings();
+ }
+
+ Settings settings = settingsPart.Settings;
+
+ // Check if UpdateFieldsOnOpen already exists
+ UpdateFieldsOnOpen? existingElement = settings.GetFirstChild();
+ if (existingElement != null)
+ {
+ // Update the existing element
+ existingElement.Val = true;
+ }
+ else
+ {
+ // Add new UpdateFieldsOnOpen element
+ settings.PrependChild(new UpdateFieldsOnOpen { Val = true });
+ }
+
+ settingsPart.Settings.Save();
+ }
}
diff --git a/TriasDev.Templify/Core/PlaceholderReplacementOptions.cs b/TriasDev.Templify/Core/PlaceholderReplacementOptions.cs
index 3b4e34b..3757508 100644
--- a/TriasDev.Templify/Core/PlaceholderReplacementOptions.cs
+++ b/TriasDev.Templify/Core/PlaceholderReplacementOptions.cs
@@ -82,6 +82,31 @@ public sealed class PlaceholderReplacementOptions
///
public IReadOnlyDictionary? TextReplacements { get; init; }
+ ///
+ /// Gets or initializes when Word should update all fields (including Table of Contents)
+ /// when the document is first opened after processing.
+ /// Default is .
+ ///
+ ///
+ ///
+ /// When template processing removes or adds content (via conditionals or loops), fields like
+ /// Table of Contents (TOC) contain stale page numbers.
+ ///
+ ///
+ /// Available modes:
+ ///
+ ///
+ /// - - Never prompt to update fields (default)
+ /// - - Always prompt to update fields
+ /// - - Only prompt if document contains fields (TOC, PAGE, etc.). Recommended for applications processing various templates.
+ ///
+ ///
+ /// Note: When enabled, Word will display a prompt asking the user to confirm
+ /// field updates. This is a security measure built into Word.
+ ///
+ ///
+ public UpdateFieldsOnOpenMode UpdateFieldsOnOpen { get; init; } = UpdateFieldsOnOpenMode.Never;
+
///
/// Creates a new instance of with default settings.
///
diff --git a/TriasDev.Templify/Core/UpdateFieldsOnOpenMode.cs b/TriasDev.Templify/Core/UpdateFieldsOnOpenMode.cs
new file mode 100644
index 0000000..b7d1fcd
--- /dev/null
+++ b/TriasDev.Templify/Core/UpdateFieldsOnOpenMode.cs
@@ -0,0 +1,29 @@
+// 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 when Word should update all fields (including Table of Contents)
+/// when the document is first opened after processing.
+///
+public enum UpdateFieldsOnOpenMode
+{
+ ///
+ /// Never set the UpdateFieldsOnOpen flag. This is the default for backward compatibility.
+ ///
+ Never,
+
+ ///
+ /// Always set the UpdateFieldsOnOpen flag, regardless of document content.
+ /// Word will prompt the user to update fields every time the document is opened.
+ ///
+ Always,
+
+ ///
+ /// Automatically detect if the document contains fields (TOC, PAGE, NUMPAGES, etc.)
+ /// and only set the UpdateFieldsOnOpen flag if fields are present.
+ /// This is the recommended setting for applications that process various templates.
+ ///
+ Auto
+}
diff --git a/TriasDev.Templify/README.md b/TriasDev.Templify/README.md
index 5770181..fbc7554 100644
--- a/TriasDev.Templify/README.md
+++ b/TriasDev.Templify/README.md
@@ -755,6 +755,47 @@ var options = new PlaceholderReplacementOptions
var processor = new DocumentTemplateProcessor(options);
```
+### Update Fields on Open (TOC Support)
+
+When your templates contain dynamic fields like Table of Contents (TOC), page numbers, or dates, these fields may become stale after processing if content is added or removed (e.g., via conditionals or loops). Templify can instruct Word to prompt users to update these fields when the document is opened.
+
+```csharp
+// Option 1: Auto-detect fields (recommended for applications processing various templates)
+var options = new PlaceholderReplacementOptions
+{
+ UpdateFieldsOnOpen = UpdateFieldsOnOpenMode.Auto
+};
+// Only prompts if document contains TOC, PAGE, NUMPAGES, PAGEREF, DATE, TIME, or FILENAME fields
+
+// Option 2: Always prompt to update fields
+var options = new PlaceholderReplacementOptions
+{
+ UpdateFieldsOnOpen = UpdateFieldsOnOpenMode.Always
+};
+// Every processed document will prompt to update fields when opened
+
+// Option 3: Never prompt (default)
+var options = new PlaceholderReplacementOptions
+{
+ UpdateFieldsOnOpen = UpdateFieldsOnOpenMode.Never
+};
+// No prompt, fields remain as-is (may show stale page numbers)
+
+var processor = new DocumentTemplateProcessor(options);
+```
+
+**When to use each mode:**
+
+| Mode | Use Case |
+|------|----------|
+| `Auto` | Applications where users upload various templates (some with TOC, some without). Only documents with fields will prompt. |
+| `Always` | When you know all templates have TOC or other fields that need updating. |
+| `Never` | When you don't want any prompts, or templates don't have dynamic fields. |
+
+**Why can't Templify update TOC page numbers directly?**
+
+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.
+
### 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.
@@ -1091,6 +1132,11 @@ Configuration options for template processing.
**Properties:**
- `MissingVariableBehavior`: How to handle missing variables (default: `LeaveUnchanged`)
+- `Culture`: Culture for formatting numbers and dates (default: `CurrentCulture`)
+- `UpdateFieldsOnOpen`: When to prompt Word to update fields like TOC (default: `Never`)
+- `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)
### ProcessingResult
diff --git a/docs/for-developers/quick-start.md b/docs/for-developers/quick-start.md
index d362e69..5b37be3 100644
--- a/docs/for-developers/quick-start.md
+++ b/docs/for-developers/quick-start.md
@@ -31,6 +31,82 @@ if (result.Success)
}
```
+## Configuration Options
+
+Customize template processing with `PlaceholderReplacementOptions`:
+
+```csharp
+var options = new PlaceholderReplacementOptions
+{
+ MissingVariableBehavior = MissingVariableBehavior.ReplaceWithEmpty,
+ Culture = CultureInfo.GetCultureInfo("de-DE"),
+ EnableNewlineSupport = true,
+ UpdateFieldsOnOpen = UpdateFieldsOnOpenMode.Auto
+};
+
+var processor = new DocumentTemplateProcessor(options);
+```
+
+### Available Options
+
+| Option | Type | Default | Description |
+|--------|------|---------|-------------|
+| `MissingVariableBehavior` | enum | `LeaveUnchanged` | How to handle missing variables |
+| `Culture` | `CultureInfo` | `CurrentCulture` | Culture for formatting numbers and dates |
+| `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 |
+
+## Update Fields on Open (TOC Support)
+
+When templates contain Table of Contents (TOC) or other dynamic fields, and content changes during processing (via conditionals or loops), page numbers become stale.
+
+**Why this happens:** The OpenXML SDK cannot calculate page numbers—only Word's layout engine can determine actual pagination.
+
+### UpdateFieldsOnOpenMode Options
+
+| Mode | Description |
+|------|-------------|
+| `Never` | Never prompt to update fields (default) |
+| `Always` | Always prompt to update fields |
+| `Auto` | **Recommended** - Only prompt if document contains fields (TOC, PAGE, etc.) |
+
+### Usage Examples
+
+**For applications processing user-uploaded templates (recommended):**
+
+```csharp
+// Auto-detect: only prompts if document has TOC, PAGE, etc.
+var options = new PlaceholderReplacementOptions
+{
+ UpdateFieldsOnOpen = UpdateFieldsOnOpenMode.Auto
+};
+```
+
+**For templates known to have TOC:**
+
+```csharp
+var options = new PlaceholderReplacementOptions
+{
+ UpdateFieldsOnOpen = UpdateFieldsOnOpenMode.Always
+};
+```
+
+### Fields Detected in Auto Mode
+
+- `TOC` - Table of Contents
+- `PAGE` - Current page number
+- `NUMPAGES` - Total page count
+- `PAGEREF` - Page references
+- `DATE` - Current date
+- `TIME` - Current time
+- `FILENAME` - Document filename
+- `REF` - Cross-references
+- `NOTEREF` - Footnote/endnote references
+- `SECTIONPAGES` - Pages in current section
+
+> **Note:** When enabled, Word displays a prompt asking the user to confirm field updates. This is a security measure built into Word.
+
## Need Help?
- 🐛 [Report Issues](https://github.com/triasdev/templify/issues)