refactor(docx): getBaseRun helper, hoist mainConfig, derive sections, remove dead OL code

- Add getBaseRun() next to getHtmlStyle() and replace 8 inline baseRun spreads (~27 lines).
- Hoist one mainConfig object in buildDocument; sidebar call spreads only the two
  differing color keys (~17 lines).
- Remove BUILT_IN_SECTIONS Set; derive membership via `sectionId in data.sections` (~14 lines).
- Delete unreachable ordered-list numbering machinery in html-to-docx.ts: numberingRef is
  always undefined through all call paths, so isOrdered && numberingRef is always false (~15 lines).

Claude-Session: https://claude.ai/code/session_012Bnvt1MghwHj4qQRxuQUGa
This commit is contained in:
Amruth Pillai
2026-07-04 19:27:01 +02:00
parent 439ae114f9
commit dfe75390cd
3 changed files with 39 additions and 129 deletions
+16 -49
View File
@@ -122,27 +122,13 @@ function blendWithWhite(hex: string, opacity: number): string {
// --- Section rendering dispatch ---
const BUILT_IN_SECTIONS = new Set<string>([
"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") {
+6 -39
View File
@@ -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 } }));
}
}
}
+17 -41
View File
@@ -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 })];