diff --git a/packages/docx/src/builder.ts b/packages/docx/src/builder.ts index d3822bcef..583c3a53c 100644 --- a/packages/docx/src/builder.ts +++ b/packages/docx/src/builder.ts @@ -122,27 +122,13 @@ function blendWithWhite(hex: string, opacity: number): string { // --- Section rendering dispatch --- -const BUILT_IN_SECTIONS = new Set([ - "profiles", - "experience", - "education", - "projects", - "skills", - "languages", - "interests", - "awards", - "certifications", - "publications", - "volunteer", - "references", -]); - function renderSection(sectionId: string, data: ResumeData, colorHex: string): Paragraph[] { if (sectionId === "summary") { return renderSummary(data.summary, colorHex); } - if (BUILT_IN_SECTIONS.has(sectionId)) { + // ponytail: data.sections keys are the source of truth; no need to maintain a parallel Set + if (sectionId in data.sections) { const section = data.sections[sectionId as SectionType]; if (section) { return renderBuiltInSection(sectionId as SectionType, section, colorHex); @@ -405,17 +391,19 @@ export function buildDocument(data: ResumeData): Document { const documentChildren: (Paragraph | Table)[] = []; + // ponytail: hoisted once; sidebar call spreads only the two differing color keys + const mainConfig = { + headingFont, + headingSizeHalfPt: headingSize, + bodyFont, + bodySizeHalfPt: bodySize, + textColorHex, + primaryColorHex: colorHex, + }; + // Header placement depends on template if (templateConfig.headerPosition === "full-width") { - // Configure colors for main content - setRenderConfig({ - headingFont, - headingSizeHalfPt: headingSize, - bodyFont, - bodySizeHalfPt: bodySize, - textColorHex, - primaryColorHex: colorHex, - }); + setRenderConfig(mainConfig); documentChildren.push(...buildHeader(data, colorHex, textColorHex)); } @@ -424,27 +412,13 @@ export function buildDocument(data: ResumeData): Document { const isFullWidth = layoutPage.fullWidth || layoutPage.sidebar.length === 0; if (isFullWidth) { - setRenderConfig({ - headingFont, - headingSizeHalfPt: headingSize, - bodyFont, - bodySizeHalfPt: bodySize, - textColorHex, - primaryColorHex: colorHex, - }); + setRenderConfig(mainConfig); for (const sectionId of [...layoutPage.main, ...layoutPage.sidebar]) { documentChildren.push(...renderSection(sectionId, data, colorHex)); } } else { // Render main sections with normal colors - setRenderConfig({ - headingFont, - headingSizeHalfPt: headingSize, - bodyFont, - bodySizeHalfPt: bodySize, - textColorHex, - primaryColorHex: colorHex, - }); + setRenderConfig(mainConfig); const mainParagraphs: Paragraph[] = []; if (templateConfig.headerPosition === "main-only") { @@ -455,14 +429,7 @@ export function buildDocument(data: ResumeData): Document { } // Render sidebar sections with potentially inverted colors - setRenderConfig({ - headingFont, - headingSizeHalfPt: headingSize, - bodyFont, - bodySizeHalfPt: bodySize, - textColorHex: sidebarTextColorHex, - primaryColorHex: sidebarHeadingColorHex, - }); + setRenderConfig({ ...mainConfig, textColorHex: sidebarTextColorHex, primaryColorHex: sidebarHeadingColorHex }); const sidebarParagraphs: Paragraph[] = []; if (templateConfig.headerPosition === "sidebar-only") { diff --git a/packages/docx/src/html-to-docx.ts b/packages/docx/src/html-to-docx.ts index 8ba4e9953..d26cdf6c8 100644 --- a/packages/docx/src/html-to-docx.ts +++ b/packages/docx/src/html-to-docx.ts @@ -22,7 +22,6 @@ interface InlineStyle { } type InlineChild = TextRun | ExternalHyperlink; -type NumberingOptions = { reference: string; level: number; instance?: number }; /** Module-level link color, set per htmlToParagraphs invocation. */ let currentLinkColor = "0563C1"; @@ -123,18 +122,7 @@ function collectInlineChildren(node: Node, style: InlineStyle): InlineChild[] { return children; } -function getNumberingOptions(reference: string, level: number, instance?: number): NumberingOptions { - return instance === undefined ? { reference, level } : { reference, level, instance }; -} - -function processBlockElement( - el: HTMLElement, - style: InlineStyle, - paragraphs: Paragraph[], - listLevel?: number, - numberingRef?: string, - listIndex?: number, -): void { +function processBlockElement(el: HTMLElement, style: InlineStyle, paragraphs: Paragraph[], listLevel?: number): void { const tag = el.tagName; const mergedStyle = mergeStyle(style, tag, el); @@ -149,20 +137,13 @@ function processBlockElement( if (tag === "P" || tag === "DIV") { const inlineChildren = collectInlineChildren(el, mergedStyle); if (inlineChildren.length > 0) { - paragraphs.push( - new Paragraph({ - children: inlineChildren, - ...(listLevel != null && numberingRef - ? { numbering: getNumberingOptions(numberingRef, listLevel, listIndex) } - : {}), - }), - ); + paragraphs.push(new Paragraph({ children: inlineChildren })); } return; } if (tag === "UL" || tag === "OL") { - const isOrdered = tag === "OL"; + // ponytail: ordered-list numbering (numberingRef path) was never reachable; always uses bullet const level = listLevel != null ? listLevel + 1 : 0; for (const li of el.children) { @@ -177,30 +158,16 @@ function processBlockElement( if (liChild.nodeType === Node.TEXT_NODE) { const text = (liChild.textContent ?? "").trim(); if (text) { - paragraphs.push( - new Paragraph({ - children: [new TextRun({ text, ...mergedStyle })], - ...(isOrdered && numberingRef - ? { numbering: getNumberingOptions(numberingRef, level, listIndex) } - : { bullet: { level } }), - }), - ); + paragraphs.push(new Paragraph({ children: [new TextRun({ text, ...mergedStyle })], bullet: { level } })); } } else if (liChild.nodeType === Node.ELEMENT_NODE) { - processBlockElement(liChild as HTMLElement, mergedStyle, paragraphs, level, numberingRef, listIndex); + processBlockElement(liChild as HTMLElement, mergedStyle, paragraphs, level); } } } else { const inlineChildren = collectInlineChildren(li, mergedStyle); if (inlineChildren.length > 0) { - paragraphs.push( - new Paragraph({ - children: inlineChildren, - ...(isOrdered && numberingRef - ? { numbering: getNumberingOptions(numberingRef, level, listIndex) } - : { bullet: { level } }), - }), - ); + paragraphs.push(new Paragraph({ children: inlineChildren, bullet: { level } })); } } } diff --git a/packages/docx/src/section-renderers.ts b/packages/docx/src/section-renderers.ts index 8d7f6535f..a2acc62cc 100644 --- a/packages/docx/src/section-renderers.ts +++ b/packages/docx/src/section-renderers.ts @@ -43,6 +43,15 @@ function getHtmlStyle(): HtmlStyleConfig { }; } +/** Base TextRun properties (font, size, color) shared across all section renderers. */ +function getBaseRun() { + return { + ...(bodyFont ? { font: bodyFont } : {}), + ...(bodySize ? { size: bodySize } : {}), + ...(textColor ? { color: textColor } : {}), + }; +} + /** * Creates a section heading paragraph with primary color and bottom border. * Uses the heading typography set via `setRenderConfig`. @@ -70,11 +79,7 @@ function sectionHeading(title: string, colorHex: string): Paragraph { } function titleAndSubtitle(primary: string, secondary: string, rightText?: string): Paragraph { - const baseRun = { - ...(bodyFont ? { font: bodyFont } : {}), - ...(bodySize ? { size: bodySize } : {}), - ...(textColor ? { color: textColor } : {}), - }; + const baseRun = getBaseRun(); const children: (TextRun | ExternalHyperlink)[] = [new TextRun({ text: primary, bold: true, ...baseRun })]; if (secondary) { @@ -151,12 +156,7 @@ function renderExperience(section: Sections["experience"], colorHex: string): Pa if (section.hidden || items.length === 0) return []; const paragraphs: Paragraph[] = [sectionHeading(section.title, colorHex)]; - - const baseRun = { - ...(bodyFont ? { font: bodyFont } : {}), - ...(bodySize ? { size: bodySize } : {}), - ...(textColor ? { color: textColor } : {}), - }; + const baseRun = getBaseRun(); for (const item of items) { if (item.roles && item.roles.length > 0) { @@ -202,11 +202,7 @@ function renderEducation(section: Sections["education"], colorHex: string): Para if (section.hidden || items.length === 0) return []; const paragraphs: Paragraph[] = [sectionHeading(section.title, colorHex)]; - const baseRun = { - ...(bodyFont ? { font: bodyFont } : {}), - ...(bodySize ? { size: bodySize } : {}), - ...(textColor ? { color: textColor } : {}), - }; + const baseRun = getBaseRun(); for (const item of items) { const degreeArea = [item.degree, item.area].filter(Boolean).join(", "); @@ -259,11 +255,7 @@ function renderSkills(section: Sections["skills"], colorHex: string): Paragraph[ if (section.hidden || items.length === 0) return []; const paragraphs: Paragraph[] = [sectionHeading(section.title, colorHex)]; - const baseRun = { - ...(bodyFont ? { font: bodyFont } : {}), - ...(bodySize ? { size: bodySize } : {}), - ...(textColor ? { color: textColor } : {}), - }; + const baseRun = getBaseRun(); for (const item of items) { const children: TextRun[] = [new TextRun({ text: item.name, bold: true, ...baseRun })]; @@ -287,11 +279,7 @@ function renderLanguages(section: Sections["languages"], colorHex: string): Para if (section.hidden || items.length === 0) return []; const paragraphs: Paragraph[] = [sectionHeading(section.title, colorHex)]; - const baseRun = { - ...(bodyFont ? { font: bodyFont } : {}), - ...(bodySize ? { size: bodySize } : {}), - ...(textColor ? { color: textColor } : {}), - }; + const baseRun = getBaseRun(); for (const item of items) { const children: TextRun[] = [new TextRun({ text: item.language, bold: true, ...baseRun })]; @@ -311,11 +299,7 @@ function renderInterests(section: Sections["interests"], colorHex: string): Para if (section.hidden || items.length === 0) return []; const paragraphs: Paragraph[] = [sectionHeading(section.title, colorHex)]; - const baseRun = { - ...(bodyFont ? { font: bodyFont } : {}), - ...(bodySize ? { size: bodySize } : {}), - ...(textColor ? { color: textColor } : {}), - }; + const baseRun = getBaseRun(); for (const item of items) { const children: TextRun[] = [new TextRun({ text: item.name, bold: true, ...baseRun })]; @@ -418,11 +402,7 @@ function renderReferences(section: Sections["references"], colorHex: string): Pa if (section.hidden || items.length === 0) return []; const paragraphs: Paragraph[] = [sectionHeading(section.title, colorHex)]; - const baseRun = { - ...(bodyFont ? { font: bodyFont } : {}), - ...(bodySize ? { size: bodySize } : {}), - ...(textColor ? { color: textColor } : {}), - }; + const baseRun = getBaseRun(); for (const item of items) { const children: TextRun[] = [new TextRun({ text: item.name, bold: true, ...baseRun })]; @@ -457,11 +437,7 @@ function renderProfiles(section: Sections["profiles"], colorHex: string): Paragr if (section.hidden || items.length === 0) return []; const paragraphs: Paragraph[] = [sectionHeading(section.title, colorHex)]; - const baseRun = { - ...(bodyFont ? { font: bodyFont } : {}), - ...(bodySize ? { size: bodySize } : {}), - ...(textColor ? { color: textColor } : {}), - }; + const baseRun = getBaseRun(); for (const item of items) { const children: (TextRun | ExternalHyperlink)[] = [new TextRun({ text: item.network, bold: true, ...baseRun })];