Skip to content
Merged
119 changes: 119 additions & 0 deletions TriasDev.Templify.Tests/Helpers/DocumentBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,125 @@ public static RunProperties CreateFormatting(
return properties;
}

/// <summary>
/// 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.
/// </summary>
/// <param name="tocEntries">List of entries to show in the TOC, each with (text, pageNumber)</param>
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;
}

/// <summary>
/// Adds a heading paragraph with the specified style level (1-3).
/// These are typically what TOC entries reference.
/// </summary>
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;
}

/// <summary>
/// Adds a page break to simulate multi-page documents.
/// </summary>
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;
}

/// <summary>
/// Returns the document as a MemoryStream for processing.
/// </summary>
Expand Down
86 changes: 86 additions & 0 deletions TriasDev.Templify.Tests/Helpers/DocumentVerifier.cs
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,92 @@ public bool IsListItem(int paragraphIndex)
return numProps != null && numProps.NumberingId != null;
}

/// <summary>
/// Checks if the document contains a TOC field.
/// </summary>
public bool HasTableOfContents()
{
return _body.Descendants<FieldCode>()
.Any(fc => fc.Text?.Contains("TOC", StringComparison.OrdinalIgnoreCase) == true);
}

/// <summary>
/// Gets all TOC entry texts from the document.
/// Note: This extracts the cached/displayed TOC entries, not the actual headings.
/// </summary>
public List<string> GetTableOfContentsEntries()
{
List<string> entries = new List<string>();
bool inTocField = false;
bool afterSeparator = false;

foreach (Paragraph para in _body.Descendants<Paragraph>())
{
foreach (Run run in para.Descendants<Run>())
{
FieldChar? fieldChar = run.GetFirstChild<FieldChar>();
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;
}
}
}
Comment on lines +472 to +492

Copilot AI Jan 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This foreach loop immediately maps its iteration variable to another variable - consider mapping the sequence explicitly using '.Select(...)'.

Copilot uses AI. Check for mistakes.

// 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;
}

/// <summary>
/// Gets all page numbers displayed in TOC entries.
/// </summary>
public List<int> 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();
}

/// <summary>
/// Checks if the document has the UpdateFieldsOnOpen setting enabled.
/// When enabled, Word will prompt to update fields (including TOC) when the document is opened.
/// </summary>
public bool HasUpdateFieldsOnOpen()
{
DocumentSettingsPart? settingsPart = _document.MainDocumentPart?.DocumentSettingsPart;
if (settingsPart?.Settings == null)
{
return false;
}

UpdateFieldsOnOpen? updateFields = settingsPart.Settings.GetFirstChild<UpdateFieldsOnOpen>();
return updateFields?.Val?.Value == true;
}

public void Dispose()
{
_document?.Dispose();
Expand Down
Loading