-
Notifications
You must be signed in to change notification settings - Fork 1
feat: add support for headers and footers #81
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4601462
09e1437
0a7a393
d5a086e
d828cd8
b661053
7735a0c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -536,11 +536,160 @@ public DocumentBuilder SetSubject(string subject) | |
| return this; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Adds a default header with the specified text. | ||
| /// </summary> | ||
| public DocumentBuilder AddHeader(string text) => AddHeader(text, HeaderFooterValues.Default); | ||
|
|
||
| /// <summary> | ||
| /// Adds a header with the specified text. | ||
| /// </summary> | ||
| public DocumentBuilder AddHeader(string text, HeaderFooterValues type) | ||
| { | ||
| return AddHeaderPart(type, null, text); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Adds a header with multiple paragraphs. | ||
| /// </summary> | ||
| public DocumentBuilder AddHeaderWithParagraphs(HeaderFooterValues type, params string[] texts) | ||
| { | ||
| return AddHeaderPart(type, null, texts); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Adds a default footer with the specified text. | ||
| /// </summary> | ||
| public DocumentBuilder AddFooter(string text) => AddFooter(text, HeaderFooterValues.Default); | ||
|
|
||
| /// <summary> | ||
| /// Adds a footer with the specified text. | ||
| /// </summary> | ||
| public DocumentBuilder AddFooter(string text, HeaderFooterValues type) | ||
| { | ||
| return AddFooterPart(type, null, text); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Adds a footer with multiple paragraphs. | ||
| /// </summary> | ||
| public DocumentBuilder AddFooterWithParagraphs(HeaderFooterValues type, params string[] texts) | ||
| { | ||
| return AddFooterPart(type, null, texts); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Adds a default header with the specified text and formatting. | ||
| /// </summary> | ||
| public DocumentBuilder AddHeader(string text, RunProperties formatting) => AddHeader(text, formatting, HeaderFooterValues.Default); | ||
|
|
||
| /// <summary> | ||
| /// Adds a header with the specified text and formatting. | ||
| /// </summary> | ||
| public DocumentBuilder AddHeader(string text, RunProperties formatting, HeaderFooterValues type) | ||
| { | ||
| return AddHeaderPart(type, formatting, text); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Adds a default footer with the specified text and formatting. | ||
| /// </summary> | ||
| public DocumentBuilder AddFooter(string text, RunProperties formatting) => AddFooter(text, formatting, HeaderFooterValues.Default); | ||
|
|
||
| /// <summary> | ||
| /// Adds a footer with the specified text and formatting. | ||
| /// </summary> | ||
| public DocumentBuilder AddFooter(string text, RunProperties formatting, HeaderFooterValues type) | ||
| { | ||
| return AddFooterPart(type, formatting, text); | ||
| } | ||
|
|
||
| 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); | ||
| SectionProperties sectionProps = EnsureSectionProperties(); | ||
|
|
||
| // Remove existing header reference of the same type to avoid duplicates | ||
| HeaderReference? existing = sectionProps.Elements<HeaderReference>() | ||
| .FirstOrDefault(r => r.Type?.Value == type || (type == HeaderFooterValues.Default && r.Type == null)); | ||
| existing?.Remove(); | ||
|
|
||
| sectionProps.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); | ||
| SectionProperties sectionProps = EnsureSectionProperties(); | ||
|
|
||
| // Remove existing footer reference of the same type to avoid duplicates | ||
| FooterReference? existingFooter = sectionProps.Elements<FooterReference>() | ||
| .FirstOrDefault(r => r.Type?.Value == type || (type == HeaderFooterValues.Default && r.Type == null)); | ||
| existingFooter?.Remove(); | ||
|
|
||
| sectionProps.Append(new FooterReference { Type = type, Id = partId }); | ||
| return this; | ||
|
Comment on lines
+607
to
+644
|
||
| } | ||
|
|
||
| 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; | ||
| } | ||
|
Comment on lines
+607
to
+678
|
||
|
|
||
| /// <summary> | ||
| /// Returns the document as a MemoryStream for processing. | ||
| /// </summary> | ||
| public MemoryStream ToStream() | ||
| { | ||
| // Ensure SectionProperties is the last child of Body (OpenXML schema requirement) | ||
| SectionProperties? sectPr = _body.Elements<SectionProperties>().FirstOrDefault(); | ||
| if (sectPr != null) | ||
| { | ||
| sectPr.Remove(); | ||
| _body.Append(sectPr); | ||
| } | ||
|
|
||
| // Save and close the document | ||
| _document.MainDocumentPart!.Document.Save(); | ||
| _document.Dispose(); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -569,6 +569,115 @@ public bool HasUpdateFieldsOnOpen() | |
| /// </summary> | ||
| public string? GetDocumentLastModifiedBy() => _document.PackageProperties.LastModifiedBy; | ||
|
|
||
| /// <summary> | ||
| /// Gets the text content of the default header. | ||
| /// </summary> | ||
| public string GetHeaderText() => GetHeaderText(HeaderFooterValues.Default); | ||
|
|
||
| /// <summary> | ||
| /// Gets the text content of the default footer. | ||
| /// </summary> | ||
| public string GetFooterText() => GetFooterText(HeaderFooterValues.Default); | ||
|
|
||
| /// <summary> | ||
| /// Gets the RunProperties from the first run in the default header. | ||
| /// </summary> | ||
| public RunProperties? GetHeaderRunProperties() => GetHeaderRunProperties(HeaderFooterValues.Default); | ||
|
|
||
| /// <summary> | ||
| /// Gets the RunProperties from the first run in the default footer. | ||
| /// </summary> | ||
| public RunProperties? GetFooterRunProperties() => GetFooterRunProperties(HeaderFooterValues.Default); | ||
|
|
||
| /// <summary> | ||
| /// Gets the text content of a header by type. | ||
| /// </summary> | ||
| public string GetHeaderText(HeaderFooterValues type) | ||
| { | ||
| HeaderPart headerPart = GetHeaderPart(type); | ||
| return headerPart.Header?.InnerText ?? string.Empty; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Gets the text content of a footer by type. | ||
| /// </summary> | ||
| public string GetFooterText(HeaderFooterValues type) | ||
| { | ||
| FooterPart footerPart = GetFooterPart(type); | ||
| return footerPart.Footer?.InnerText ?? string.Empty; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Gets all paragraph texts from a header by type. | ||
| /// </summary> | ||
| public List<string> GetHeaderParagraphTexts(HeaderFooterValues type) | ||
| { | ||
| HeaderPart headerPart = GetHeaderPart(type); | ||
| return headerPart.Header?.Elements<Paragraph>() | ||
| .Select(p => p.InnerText) | ||
| .ToList() ?? new List<string>(); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Gets all paragraph texts from a footer by type. | ||
| /// </summary> | ||
| public List<string> GetFooterParagraphTexts(HeaderFooterValues type) | ||
| { | ||
| FooterPart footerPart = GetFooterPart(type); | ||
| return footerPart.Footer?.Elements<Paragraph>() | ||
| .Select(p => p.InnerText) | ||
| .ToList() ?? new List<string>(); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Gets the RunProperties from the first run in the header. | ||
| /// </summary> | ||
| public RunProperties? GetHeaderRunProperties(HeaderFooterValues type) | ||
| { | ||
| HeaderPart headerPart = GetHeaderPart(type); | ||
| return headerPart.Header?.Descendants<Run>().FirstOrDefault()?.RunProperties; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Gets the RunProperties from the first run in the footer. | ||
| /// </summary> | ||
| public RunProperties? GetFooterRunProperties(HeaderFooterValues type) | ||
| { | ||
| FooterPart footerPart = GetFooterPart(type); | ||
| return footerPart.Footer?.Descendants<Run>().FirstOrDefault()?.RunProperties; | ||
| } | ||
|
|
||
| private HeaderPart GetHeaderPart(HeaderFooterValues type) | ||
| { | ||
| return (HeaderPart)GetHeaderFooterPart<HeaderReference>(type, "Header"); | ||
| } | ||
|
|
||
| private FooterPart GetFooterPart(HeaderFooterValues type) | ||
| { | ||
| return (FooterPart)GetHeaderFooterPart<FooterReference>(type, "Footer"); | ||
| } | ||
|
|
||
| private OpenXmlPart GetHeaderFooterPart<TReference>(HeaderFooterValues type, string partName) | ||
| where TReference : HeaderFooterReferenceType | ||
| { | ||
| MainDocumentPart mainPart = _document.MainDocumentPart | ||
| ?? throw new InvalidOperationException("MainDocumentPart not found"); | ||
|
|
||
| SectionProperties sectionProps = (mainPart.Document?.Body) | ||
| ?.Elements<SectionProperties>().FirstOrDefault() | ||
| ?? throw new InvalidOperationException("SectionProperties not found"); | ||
|
|
||
| TReference? reference = sectionProps.Elements<TReference>() | ||
| .FirstOrDefault(r => r.Type?.Value == type || (type == HeaderFooterValues.Default && r.Type == null)); | ||
|
|
||
| if (reference?.Id?.Value == null) | ||
| { | ||
| throw new InvalidOperationException($"{partName} reference of type {type} not found"); | ||
| } | ||
|
Comment on lines
+670
to
+676
|
||
|
|
||
| return mainPart.GetPartById(reference.Id.Value); | ||
| } | ||
|
|
||
| public void Dispose() | ||
| { | ||
| _document?.Dispose(); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AddHeaderPartintends to avoid duplicate header references, but it only removes the first match and doesn’t account for existing default header refs whereTypeis omitted (Word treats missingTypeas Default). This can leave multiple default header references inSectionProperties, 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.