Skip to content

feat: add support for headers and footers#81

Merged
vaceslav merged 7 commits intomainfrom
feature/15-header-footer-support
Mar 5, 2026
Merged

feat: add support for headers and footers#81
vaceslav merged 7 commits intomainfrom
feature/15-header-footer-support

Conversation

@vaceslav
Copy link
Contributor

@vaceslav vaceslav commented Mar 5, 2026

Summary

  • Extends DocumentWalker, DocumentTemplateProcessor, and TemplateValidator to process placeholders, conditionals, and loops in document headers and footers using the existing visitor pipeline
  • Updates HasFields() to detect field codes (TOC, PAGE, etc.) in header/footer parts
  • Adds AddHeader/AddFooter to test DocumentBuilder and GetHeaderText/GetFooterText to DocumentVerifier
  • Adds 11 integration tests covering placeholder replacement, conditionals, loops, multiple header/footer types, formatting preservation, and validation

Closes #15

Test plan

  • dotnet build templify.sln compiles without errors
  • dotnet test — all 983 tests pass (11 new + 972 existing)
  • dotnet format --verify-no-changes --no-restore — formatting check passes

vaceslav added 4 commits March 5, 2026 14:18
Process placeholders, conditionals, and loops in document headers and
footers using the same visitor pipeline as the body. Also extends
template validation and field detection to cover header/footer parts.
Update README, Examples, ARCHITECTURE, REFACTORING, TODO, CLAUDE.md,
CHANGELOG, mkdocs, and developer docs to reflect the new header/footer
processing feature. Add new docs page for template authors and additional
integration tests for nested properties, formatting preservation, and
markdown in headers/footers.
- Extract GetHeaderPart/GetFooterPart helpers in DocumentVerifier
- Use lazy IEnumerable in HasFields to short-circuit on first match
- Extract shared GetHeaderFooterElements iterator in TemplateValidator
…tion

- DocumentBuilder: extract AddHeaderPart/AddFooterPart/AppendParagraphs helpers
- DocumentVerifier: extract generic GetHeaderFooterPart<TReference> helper
- TemplateValidator: delegate FindAllPlaceholders to FindAllPlaceholdersInElements
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds first-class processing and validation of template constructs inside Word header/footer parts, extending the existing visitor pipeline beyond the document body (closes #15).

Changes:

  • Extend DocumentWalker + DocumentTemplateProcessor to traverse and process header/footer parts with the same visitors (placeholders/conditionals/loops).
  • Extend TemplateValidator and field detection to account for headers/footers.
  • Add integration tests and supporting test helpers, plus documentation updates describing header/footer support.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
mkdocs.yml Adds new docs page to site navigation.
docs/for-template-authors/headers-footers.md New guide for header/footer template behavior and examples.
docs/for-developers/quick-start.md Documents that ProcessTemplate processes headers/footers automatically.
TriasDev.Templify/Visitors/DocumentWalker.cs Adds WalkHeadersAndFooters traversal using existing WalkElements pipeline.
TriasDev.Templify/Core/DocumentTemplateProcessor.cs Processes headers/footers during template processing; expands HasFields() to include header/footer parts.
TriasDev.Templify/Core/TemplateValidator.cs Validates header/footer parts and checks missing variables for them.
TriasDev.Templify.Tests/Integration/HeaderFooterTests.cs Adds integration coverage for header/footer placeholders, conditionals, loops, formatting, and multiple types.
TriasDev.Templify.Tests/Helpers/DocumentVerifier.cs Adds header/footer text + formatting extraction helpers for tests.
TriasDev.Templify.Tests/Helpers/DocumentBuilder.cs Adds helpers to create headers/footers in generated test documents.
TriasDev.Templify/TODO.md Marks header/footer support as completed.
TriasDev.Templify/REFACTORING.md Updates refactoring notes to reflect implemented header/footer support.
TriasDev.Templify/README.md Updates feature list and supported locations to include headers/footers.
TriasDev.Templify/Examples.md Adds a header/footer examples section and updates troubleshooting text.
TriasDev.Templify/ARCHITECTURE.md Updates architecture notes to reflect header/footer traversal support.
CLAUDE.md Updates developer notes to include header/footer traversal step.
CHANGELOG.md Adds unreleased entry describing header/footer support.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +607 to +664
private DocumentBuilder AddHeaderPart(HeaderFooterValues type, RunProperties? formatting, params string[] texts)
{
MainDocumentPart mainPart = _document.MainDocumentPart!;
HeaderPart headerPart = mainPart.AddNewPart<HeaderPart>();
headerPart.Header = new Header();
AppendParagraphs(headerPart.Header, formatting, texts);
headerPart.Header.Save();

string partId = mainPart.GetIdOfPart(headerPart);
EnsureSectionProperties().Append(new HeaderReference { Type = type, Id = partId });
return this;
}

private DocumentBuilder AddFooterPart(HeaderFooterValues type, RunProperties? formatting, params string[] texts)
{
MainDocumentPart mainPart = _document.MainDocumentPart!;
FooterPart footerPart = mainPart.AddNewPart<FooterPart>();
footerPart.Footer = new Footer();
AppendParagraphs(footerPart.Footer, formatting, texts);
footerPart.Footer.Save();

string partId = mainPart.GetIdOfPart(footerPart);
EnsureSectionProperties().Append(new FooterReference { Type = type, Id = partId });
return this;
}

private static void AppendParagraphs(OpenXmlCompositeElement container, RunProperties? formatting, string[] texts)
{
foreach (string text in texts)
{
Paragraph paragraph = new Paragraph();
Run run = new Run();
Text textElement = new Text(text) { Space = SpaceProcessingModeValues.Preserve };
run.Append(textElement);
if (formatting != null)
{
run.RunProperties = (RunProperties)formatting.CloneNode(true);
}

paragraph.Append(run);
container.Append(paragraph);
}
}

/// <summary>
/// Ensures the body has SectionProperties and returns it.
/// </summary>
private SectionProperties EnsureSectionProperties()
{
SectionProperties? sectionProps = _body.Elements<SectionProperties>().FirstOrDefault();
if (sectionProps == null)
{
sectionProps = new SectionProperties();
_body.Append(sectionProps);
}

return sectionProps;
}
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

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

AddHeaderPart/AddFooterPart append SectionProperties to the body when it doesn’t exist. Since AddParagraph (and other content builders) always use _body.Append(...), any content added after calling AddHeader/AddFooter will be appended after SectionProperties, producing an invalid WordprocessingML body order (sectPr must be last). Consider inserting new paragraphs/tables before an existing SectionProperties, or moving SectionProperties to the end before saving.

Copilot uses AI. Check for mistakes.
Comment on lines +607 to +630
private DocumentBuilder AddHeaderPart(HeaderFooterValues type, RunProperties? formatting, params string[] texts)
{
MainDocumentPart mainPart = _document.MainDocumentPart!;
HeaderPart headerPart = mainPart.AddNewPart<HeaderPart>();
headerPart.Header = new Header();
AppendParagraphs(headerPart.Header, formatting, texts);
headerPart.Header.Save();

string partId = mainPart.GetIdOfPart(headerPart);
EnsureSectionProperties().Append(new HeaderReference { Type = type, Id = partId });
return this;
}

private DocumentBuilder AddFooterPart(HeaderFooterValues type, RunProperties? formatting, params string[] texts)
{
MainDocumentPart mainPart = _document.MainDocumentPart!;
FooterPart footerPart = mainPart.AddNewPart<FooterPart>();
footerPart.Footer = new Footer();
AppendParagraphs(footerPart.Footer, formatting, texts);
footerPart.Footer.Save();

string partId = mainPart.GetIdOfPart(footerPart);
EnsureSectionProperties().Append(new FooterReference { Type = type, Id = partId });
return this;
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

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

AddHeaderPart/AddFooterPart always Append a new HeaderReference/FooterReference for the given type. If a caller adds the same header/footer type twice, the document ends up with multiple references of the same type, and DocumentVerifier currently reads the first one. Consider removing/replacing any existing reference of the same Type before appending the new one.

Copilot uses AI. Check for mistakes.
Comment on lines +670 to +676
TReference? reference = sectionProps.Elements<TReference>()
.FirstOrDefault(r => r.Type?.Value == type);

if (reference?.Id?.Value == null)
{
throw new InvalidOperationException($"{partName} reference of type {type} not found");
}
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

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

GetHeaderFooterPart only matches references where r.Type?.Value == type. In real Word docs, the default header/footer reference often omits the w:type attribute (null implies Default). As written, GetHeaderText()/GetFooterText() can throw for valid documents. Consider treating Type == null as HeaderFooterValues.Default when type is Default (and/or falling back to a reference with null type).

Copilot uses AI. Check for mistakes.
Comment on lines +611 to +621
private static void ValidateHeadersAndFooters(
WordprocessingDocument document,
HashSet<string> allPlaceholders,
List<ValidationError> errors)
{
foreach (List<OpenXmlElement> elements in GetHeaderFooterElements(document))
{
_ = ValidateConditionals(elements, allPlaceholders, errors);
ValidateLoops(elements, allPlaceholders, errors);
FindAllPlaceholdersInElements(elements, allPlaceholders);
}
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

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

ValidateHeadersAndFooters validates conditionals/loops and collects placeholders, but it doesn’t validate table-row loops (LoopDetector.DetectTableRowLoops) inside header/footer tables. Since DocumentWalker supports table row loops when walking headers/footers, templates with {{#foreach}} row markers in header/footer tables could pass validation incorrectly. Consider extending header/footer validation to also run table-row loop validation for any Table elements in these parts.

Copilot uses AI. Check for mistakes.
Comment on lines +9 to +14
| Type | Description |
|------|-------------|
| **Default** | Standard header/footer for most pages |
| **First Page** | Header/footer shown only on the first page |
| **Even Page** | Header/footer for even-numbered pages |

Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

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

The Markdown table under "Supported Header/Footer Types" uses a double leading pipe (|| ...) on each row, which renders as an extra empty column in most Markdown engines. Use single leading/trailing pipes (| Type | Description |) for a valid 2-column table.

Copilot uses AI. Check for mistakes.
vaceslav added 2 commits March 5, 2026 15:08
- Move SectionProperties to end of Body in DocumentBuilder.ToStream()
- Remove duplicate header/footer references of same type before adding
- Handle null Type as Default in DocumentVerifier reference lookup
- Add table-row loop validation for headers/footers in TemplateValidator
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +638 to +644
// Remove existing footer reference of the same type to avoid duplicates
FooterReference? existingFooter = sectionProps.Elements<FooterReference>()
.FirstOrDefault(r => r.Type?.Value == type);
existingFooter?.Remove();

sectionProps.Append(new FooterReference { Type = type, Id = partId });
return this;
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

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

Same issue for footers: AddFooterPart only removes the first matching FooterReference and won’t remove an existing default footer ref with Type omitted. This can result in duplicate footer references for the default case. Remove all matches and include the (type == Default && r.Type == null) case.

Copilot uses AI. Check for mistakes.
Comment on lines +618 to +624
// Remove existing header reference of the same type to avoid duplicates
HeaderReference? existing = sectionProps.Elements<HeaderReference>()
.FirstOrDefault(r => r.Type?.Value == type);
existing?.Remove();

sectionProps.Append(new HeaderReference { Type = type, Id = partId });
return this;
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

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

AddHeaderPart intends to avoid duplicate header references, but it only removes the first match and doesn’t account for existing default header refs where Type is omitted (Word treats missing Type as Default). This can leave multiple default header references in SectionProperties, which can confuse Word and tests that expect a single effective header. Consider removing all matching references, and treat (type == Default && r.Type == null) as a match as well.

Copilot uses AI. Check for mistakes.
AddHeaderPart/AddFooterPart now treat a missing Type attribute as
Default when checking for duplicate references, matching the behavior
already present in DocumentVerifier.GetHeaderFooterPart.
@vaceslav vaceslav merged commit 5b37abe into main Mar 5, 2026
14 checks passed
@vaceslav vaceslav deleted the feature/15-header-footer-support branch March 5, 2026 15:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add support for headers and footers

2 participants