diff --git a/packages/docx/src/builder.ts b/packages/docx/src/builder.ts index 738dd32a5..5d7169508 100644 --- a/packages/docx/src/builder.ts +++ b/packages/docx/src/builder.ts @@ -58,33 +58,13 @@ const NO_BORDERS = { right: { style: BorderStyle.NONE, size: 0 }, } as const; -// --- Template layout config --- - -interface TemplateConfig { - /** Which side the sidebar appears on */ - sidebarSide: "left" | "right" | "none"; - /** Sidebar background: "solid" = full primary color, "tint" = 20% opacity, "none" = no background */ - sidebarBackground: "solid" | "tint" | "none"; - /** Where the header is rendered */ - headerPosition: "full-width" | "main-only" | "sidebar-only"; -} - -export const TEMPLATE_CONFIGS: Record = { - azurill: { sidebarSide: "left", sidebarBackground: "none", headerPosition: "full-width" }, - bronzor: { sidebarSide: "right", sidebarBackground: "none", headerPosition: "full-width" }, - chikorita: { sidebarSide: "right", sidebarBackground: "solid", headerPosition: "main-only" }, - ditgar: { sidebarSide: "left", sidebarBackground: "tint", headerPosition: "sidebar-only" }, - ditto: { sidebarSide: "left", sidebarBackground: "none", headerPosition: "full-width" }, - gengar: { sidebarSide: "left", sidebarBackground: "tint", headerPosition: "sidebar-only" }, - glalie: { sidebarSide: "left", sidebarBackground: "tint", headerPosition: "sidebar-only" }, - kakuna: { sidebarSide: "right", sidebarBackground: "none", headerPosition: "full-width" }, - lapras: { sidebarSide: "right", sidebarBackground: "none", headerPosition: "full-width" }, - leafish: { sidebarSide: "right", sidebarBackground: "none", headerPosition: "full-width" }, - meowth: { sidebarSide: "left", sidebarBackground: "none", headerPosition: "full-width" }, - onyx: { sidebarSide: "right", sidebarBackground: "none", headerPosition: "full-width" }, - pikachu: { sidebarSide: "left", sidebarBackground: "none", headerPosition: "main-only" }, - rhyhorn: { sidebarSide: "right", sidebarBackground: "none", headerPosition: "full-width" }, - scizor: { sidebarSide: "left", sidebarBackground: "none", headerPosition: "full-width" }, +// Sidebar and header placement come from `templateLayouts`, as in the PDF; only the sidebar's background is DOCX's +// own: "solid" fills it with the primary colour (text inverts), "tint" with 20% of it. +const SIDEBAR_BACKGROUND: Partial> = { + chikorita: "solid", + ditgar: "tint", + gengar: "tint", + glalie: "tint", }; type PagePlan = { kind: "single"; sections: string[] } | { kind: "split" }; @@ -296,7 +276,7 @@ function buildTwoColumnTable( sidebarParagraphs: Paragraph[], sidebarWidthPct: number, gapXTwips: number, - sidebarSide: "left" | "right" | "none", + sidebarSide: "left" | "right", sidebarShadingHex?: string, ): Table { const mainWidthPct = 100 - sidebarWidthPct; @@ -311,11 +291,8 @@ function buildTwoColumnTable( const margins: { right?: number; left?: number } = {}; - if (sidebarSide === "left") { - margins.right = gapXTwips; - } else if (sidebarSide === "right") { - margins.left = gapXTwips; - } + if (sidebarSide === "left") margins.right = gapXTwips; + else margins.left = gapXTwips; const sidebarCell = new TableCell({ width: { size: sidebarWidthPct, type: WidthType.PERCENTAGE }, @@ -376,20 +353,13 @@ export function buildDocument(data: ResumeData, resolveTitle?: SectionTitleResol const sidebarWidth = data.metadata.layout.sidebarWidth; - // Template-aware layout config - const templateConfig = TEMPLATE_CONFIGS[data.metadata.template]; - - // Compute sidebar background shading hex - let sidebarShadingHex: string | undefined; - if (templateConfig.sidebarBackground === "solid") { - sidebarShadingHex = colorHex; - } else if (templateConfig.sidebarBackground === "tint") { - sidebarShadingHex = blendWithWhite(colorHex, 0.2); - } - - // Determine sidebar text colors — inverted when sidebar has a solid background - const sidebarTextColorHex = templateConfig.sidebarBackground === "solid" ? bgColorHex : textColorHex; - const sidebarHeadingColorHex = templateConfig.sidebarBackground === "solid" ? bgColorHex : colorHex; + const layout = templateLayouts[data.metadata.template]; + const background = SIDEBAR_BACKGROUND[data.metadata.template]; + const sidebarShadingHex = + background === "solid" ? colorHex : background === "tint" ? blendWithWhite(colorHex, 0.2) : undefined; + // Text on a solid sidebar inverts. + const sidebarTextColorHex = background === "solid" ? bgColorHex : textColorHex; + const sidebarHeadingColorHex = background === "solid" ? bgColorHex : colorHex; // Configure heading typography for section renderers const headingFont = data.metadata.typography.heading.fontFamily || "Calibri"; @@ -409,7 +379,7 @@ export function buildDocument(data: ResumeData, resolveTitle?: SectionTitleResol const showHeader = shouldShowResumeHeader(data); // Header placement depends on template - if (templateConfig.headerPosition === "full-width" && showHeader) { + if (layout.headerPlacement === "full-width" && showHeader) { setRenderConfig(mainConfig); documentChildren.push(...buildHeader(data, colorHex, textColorHex)); } @@ -428,7 +398,7 @@ export function buildDocument(data: ResumeData, resolveTitle?: SectionTitleResol setRenderConfig(mainConfig); const mainParagraphs: Paragraph[] = []; - if (templateConfig.headerPosition === "main-only" && showHeader) { + if (layout.headerPlacement === "main-only" && showHeader) { mainParagraphs.push(...buildHeader(data, colorHex, textColorHex)); } for (const sectionId of layoutPage.main) { @@ -439,7 +409,7 @@ export function buildDocument(data: ResumeData, resolveTitle?: SectionTitleResol setRenderConfig({ ...mainConfig, textColorHex: sidebarTextColorHex, primaryColorHex: sidebarHeadingColorHex }); const sidebarParagraphs: Paragraph[] = []; - if (templateConfig.headerPosition === "sidebar-only" && showHeader) { + if (layout.headerPlacement === "sidebar-only" && showHeader) { sidebarParagraphs.push(...buildHeader(data, sidebarHeadingColorHex, sidebarTextColorHex)); } for (const sectionId of layoutPage.sidebar) { @@ -454,7 +424,7 @@ export function buildDocument(data: ResumeData, resolveTitle?: SectionTitleResol sidebarWidth, gapXTwips, // The side chosen in Design, else the template's own. - data.metadata.layout.sidebarSide ?? templateConfig.sidebarSide, + data.metadata.layout.sidebarSide ?? layout.sidebarSide ?? "left", sidebarShadingHex, ), ); diff --git a/packages/docx/src/cover-letter-layout.test.ts b/packages/docx/src/cover-letter-layout.test.ts index 41171aacf..17fd2f1ad 100644 --- a/packages/docx/src/cover-letter-layout.test.ts +++ b/packages/docx/src/cover-letter-layout.test.ts @@ -7,8 +7,8 @@ const builderSource = readFileSync(fileURLToPath(new URL("./builder.ts", import. describe("cover letter DOCX layout", () => { it("gates resume headers for cover-letter-only documents", () => { expect(builderSource).toContain("shouldShowResumeHeader(data)"); - expect(builderSource).toContain('templateConfig.headerPosition === "full-width" && showHeader'); - expect(builderSource).toContain('templateConfig.headerPosition === "main-only" && showHeader'); - expect(builderSource).toContain('templateConfig.headerPosition === "sidebar-only" && showHeader'); + expect(builderSource).toContain('layout.headerPlacement === "full-width" && showHeader'); + expect(builderSource).toContain('layout.headerPlacement === "main-only" && showHeader'); + expect(builderSource).toContain('layout.headerPlacement === "sidebar-only" && showHeader'); }); }); diff --git a/packages/docx/src/template-layouts.test.ts b/packages/docx/src/template-layouts.test.ts index 998532b0a..a2cb766f6 100644 --- a/packages/docx/src/template-layouts.test.ts +++ b/packages/docx/src/template-layouts.test.ts @@ -1,19 +1,5 @@ import { describe, expect, it } from "vitest"; -import { templateLayouts } from "@reactive-resume/schema/templates"; -import { planPageColumns, TEMPLATE_CONFIGS } from "./builder"; - -describe("DOCX template configs", () => { - it("put the sidebar and header where the PDF does for two-column templates", () => { - for (const [template, layout] of Object.entries(templateLayouts)) { - if (layout.columns !== 2) continue; - const config = TEMPLATE_CONFIGS[template as keyof typeof TEMPLATE_CONFIGS]; - expect({ sidebarSide: config.sidebarSide, headerPosition: config.headerPosition }, template).toEqual({ - sidebarSide: layout.sidebarSide, - headerPosition: layout.headerPlacement, - }); - } - }); -}); +import { planPageColumns } from "./builder"; describe("planPageColumns", () => { const page = { fullWidth: false, main: ["experience"], sidebar: ["skills"] }; diff --git a/packages/import/src/date.test.ts b/packages/import/src/date.test.ts index 00ebb9129..9d9b5373a 100644 --- a/packages/import/src/date.test.ts +++ b/packages/import/src/date.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { formatDate, formatPeriod, formatSingleDate, toRangeDates, toSingleDates, toYearMonth } from "./date"; +import { formatDate, toRangeDates, toSingleDates, toYearMonth } from "./date"; describe("formatDate", () => { it("formats YYYY-MM as 'Month Year'", () => { @@ -10,10 +10,6 @@ describe("formatDate", () => { expect(formatDate("2024-03-15")).toBe("March 2024"); }); - it("formats YYYY-MM-DD with includeDay=true as 'Month DD, Year'", () => { - expect(formatDate("2024-03-15", true)).toBe("March 15, 2024"); - }); - it("returns YYYY unchanged when only year provided", () => { expect(formatDate("2024")).toBe("2024"); }); @@ -26,68 +22,12 @@ describe("formatDate", () => { expect(formatDate("2024-12")).toBe("December 2024"); }); - it("includes day even with single-digit days", () => { - expect(formatDate("2024-03-05", true)).toBe("March 05, 2024"); - }); - it("returns 'undefined' month name when out-of-range month is supplied", () => { // Defensive: behavior documents what happens with bad input expect(formatDate("2024-13")).toBe("undefined 2024"); }); }); -describe("formatPeriod", () => { - it("returns empty string when both dates missing", () => { - expect(formatPeriod()).toBe(""); - }); - - it("returns end date alone when start is missing", () => { - expect(formatPeriod(undefined, "2024-05")).toBe("2024-05"); - }); - - it("returns start - Present when end is missing", () => { - expect(formatPeriod("2024-01")).toBe("January 2024 - Present"); - }); - - it("formats both ends when both provided", () => { - expect(formatPeriod("2020-06", "2024-03")).toBe("June 2020 - March 2024"); - }); - - it("treats empty string start same as undefined", () => { - expect(formatPeriod("", "2024-05")).toBe("2024-05"); - }); - - it("treats empty string end same as undefined", () => { - expect(formatPeriod("2024-01", "")).toBe("January 2024 - Present"); - }); - - it("returns empty when start empty and end empty", () => { - expect(formatPeriod("", "")).toBe(""); - }); -}); - -describe("formatSingleDate", () => { - it("returns empty string when date is undefined", () => { - expect(formatSingleDate()).toBe(""); - }); - - it("returns empty string when date is empty string", () => { - expect(formatSingleDate("")).toBe(""); - }); - - it("formats full date with day included", () => { - expect(formatSingleDate("2024-03-15")).toBe("March 15, 2024"); - }); - - it("formats year-month even with includeDay flag implied", () => { - expect(formatSingleDate("2024-03")).toBe("March 2024"); - }); - - it("returns plain year when only year present", () => { - expect(formatSingleDate("2024")).toBe("2024"); - }); -}); - describe("structured dates", () => { it("keeps the year and month of ISO dates", () => { expect(toYearMonth("2024-01-15")).toBe("2024-01"); diff --git a/packages/import/src/date.ts b/packages/import/src/date.ts index 6f6d0d53e..47116e401 100644 --- a/packages/import/src/date.ts +++ b/packages/import/src/date.ts @@ -9,49 +9,19 @@ function getMonthName(month: string | undefined): string { return fmt.format(new Date(2000, index, 1)); } -/** - * Formats a partial ISO 8601 date string (YYYY, YYYY-MM, or YYYY-MM-DD) - * into a human-readable format like "January 2024" or "January 15, 2024". - */ -export function formatDate(date: string, includeDay = false): string { +/** Formats a partial ISO 8601 date (YYYY, YYYY-MM or YYYY-MM-DD) as "January 2024", or the year alone. */ +export function formatDate(date: string): string { const parts = date.split("-"); if (parts.length >= 2) { const [year, month] = parts; - const monthName = getMonthName(month); - - if (parts.length === 3 && includeDay) { - return `${monthName} ${parts[2]}, ${year}`; - } - - return `${monthName} ${year}`; + return `${getMonthName(month)} ${year}`; } // YYYY only return date; } -/** - * Formats a date range from start and end dates. - * Returns "Start - End", "Start - Present" if no end, or just the end date if no start. - */ -export function formatPeriod(startDate?: string, endDate?: string): string { - if (!startDate && !endDate) return ""; - if (!startDate) return endDate || ""; - if (!endDate) return `${formatDate(startDate)} - Present`; - - return `${formatDate(startDate)} - ${formatDate(endDate)}`; -} - -/** - * Formats a single date with day included (e.g., "January 15, 2024"). - * Falls back to month-year or year-only for partial dates. - */ -export function formatSingleDate(date?: string): string { - if (!date) return ""; - return formatDate(date, true); -} - /** The year and month of an ISO 8601 date ("2024-01-15" → "2024-01"); null when it isn't one. */ export function toYearMonth(date?: string): YearMonth | null { const match = /^(\d{4})(?:-(0[1-9]|1[0-2]))?/.exec(date?.trim() ?? ""); @@ -59,7 +29,7 @@ export function toYearMonth(date?: string): YearMonth | null { return match[2] ? `${match[1]}-${match[2]}` : match[1]; } -/** Structured dates for a range of ISO dates; a start with no end is ongoing, as `formatPeriod` prints it. */ +/** Structured dates for a range of ISO dates; a start with no end is ongoing. */ export function toRangeDates(startDate?: string, endDate?: string): ResumeDates { const start = toYearMonth(startDate); const end = toYearMonth(endDate); diff --git a/packages/import/src/json-resume.test.ts b/packages/import/src/json-resume.test.ts index 8d694a40c..091cd5339 100644 --- a/packages/import/src/json-resume.test.ts +++ b/packages/import/src/json-resume.test.ts @@ -91,7 +91,7 @@ describe("parseJSONResume", () => { expect(item.company).toBe("Acme Corp"); expect(item.position).toBe("Senior Engineer"); expect(item.location).toBe("Berlin"); - expect(item.period.length).toBeGreaterThan(0); + expect(item.dates?.start).toBeTruthy(); expect(item.description).toContain("Shipped X"); }); diff --git a/packages/import/src/json-resume.tsx b/packages/import/src/json-resume.tsx index a4a233cb0..3adcd45b3 100644 --- a/packages/import/src/json-resume.tsx +++ b/packages/import/src/json-resume.tsx @@ -1,10 +1,10 @@ import type { ResumeData } from "@reactive-resume/schema/resume/data"; -import { ZodError, z } from "zod"; +import { z } from "zod"; import { getNetworkIcon } from "@reactive-resume/resume/icons"; import { resumeDataSchema } from "@reactive-resume/schema/resume/data"; import { defaultResumeData } from "@reactive-resume/schema/resume/default"; import { generateId } from "@reactive-resume/utils/string"; -import { formatPeriod, formatSingleDate, toRangeDates, toSingleDates } from "./date"; +import { toRangeDates, toSingleDates } from "./date"; import { rethrowAsImportError } from "./error"; import { arrayToHtmlList, toHtmlDescription } from "./html"; import { parseLevel } from "./level"; @@ -220,7 +220,7 @@ function convertJSONResume(jsonResume: JSONResume): ResumeData { company: work.name || "", position: work.position || "", location: work.location || "", - period: formatPeriod(work.startDate, work.endDate), + period: "", dates: toRangeDates(work.startDate, work.endDate), website: createItemWebsite(work.url), roles: [], @@ -243,7 +243,7 @@ function convertJSONResume(jsonResume: JSONResume): ResumeData { area: edu.area || "", grade: edu.score || "", location: "", - period: formatPeriod(edu.startDate, edu.endDate), + period: "", dates: toRangeDates(edu.startDate, edu.endDate), website: createItemWebsite(edu.url), description: edu.courses && edu.courses.length > 0 ? arrayToHtmlList(edu.courses) : "", @@ -261,7 +261,7 @@ function convertJSONResume(jsonResume: JSONResume): ResumeData { id: generateId(), hidden: false, name: project.name || "", - period: formatPeriod(project.startDate, project.endDate), + period: "", dates: toRangeDates(project.startDate, project.endDate), website: createItemWebsite(project.url), description: toHtmlDescription(project.description, project.highlights), @@ -332,7 +332,7 @@ function convertJSONResume(jsonResume: JSONResume): ResumeData { hidden: false, title: award.title || "", awarder: award.awarder || "", - date: formatSingleDate(award.date), + date: "", dates: toSingleDates(award.date), website: createItemWebsite(), description: award.summary ? `

${award.summary}

` : "", @@ -351,7 +351,7 @@ function convertJSONResume(jsonResume: JSONResume): ResumeData { hidden: false, title: cert.name || "", issuer: cert.issuer || "", - date: formatSingleDate(cert.date), + date: "", dates: toSingleDates(cert.date), website: createItemWebsite(cert.url), description: "", @@ -370,7 +370,7 @@ function convertJSONResume(jsonResume: JSONResume): ResumeData { hidden: false, title: pub.name || "", publisher: pub.publisher || "", - date: formatSingleDate(pub.releaseDate), + date: "", dates: toSingleDates(pub.releaseDate), website: createItemWebsite(pub.url), description: pub.summary ? `

${pub.summary}

` : "", @@ -389,7 +389,7 @@ function convertJSONResume(jsonResume: JSONResume): ResumeData { hidden: false, organization: vol.organization || "", location: "", - period: formatPeriod(vol.startDate, vol.endDate), + period: "", dates: toRangeDates(vol.startDate, vol.endDate), website: createItemWebsite(vol.url), description: toHtmlDescription(vol.summary, vol.highlights), @@ -441,7 +441,6 @@ export function parseJSONResume(json: string): ResumeData { const jsonResume = jsonResumeSchema.parse(JSON.parse(json)); return convertJSONResume(jsonResume); } catch (error) { - if (error instanceof ZodError) rethrowAsImportError(error); - throw error; + rethrowAsImportError(error); } } diff --git a/packages/import/src/reactive-resume-json.tsx b/packages/import/src/reactive-resume-json.tsx index 1a6ac3523..a97e08447 100644 --- a/packages/import/src/reactive-resume-json.tsx +++ b/packages/import/src/reactive-resume-json.tsx @@ -1,5 +1,4 @@ import type { ResumeData } from "@reactive-resume/schema/resume/data"; -import { ZodError } from "zod"; import { resumeDataSchema, sectionTypeSchema } from "@reactive-resume/schema/resume/data"; import { rethrowAsImportError } from "./error"; @@ -69,7 +68,6 @@ export function parseReactiveResumeJSON(json: string): ResumeData { const parsed = resumeDataSchema.parse(JSON.parse(json)); return resumeDataSchema.parse(normalizeBuiltInSectionsInLayout(parsed)); } catch (error) { - if (error instanceof ZodError) rethrowAsImportError(error); - throw error; + rethrowAsImportError(error); } } diff --git a/packages/pdf/src/document.tsx b/packages/pdf/src/document.tsx index 132bfb462..d1add08ca 100644 --- a/packages/pdf/src/document.tsx +++ b/packages/pdf/src/document.tsx @@ -44,7 +44,6 @@ export const ResumeDocument = ({ semanticRuntime, }: ResumeDocumentProps) => { const TemplatePageComponent = getTemplatePage(template); - const creationDate = useMemo(() => new Date(), []); const hasCjkContent = useMemo(() => resumeContentContainsCJK(data), [data]); const scripts = useMemo(() => resumeContentScripts(data), [data]); const typography = useMemo( @@ -85,9 +84,6 @@ export const ResumeDocument = ({ renderOptions={renderOptions} > 0) sources.add(url); + if (typeof src === "string" && src.length > 0) sources.add(src); } node.children.forEach(visit); }; diff --git a/packages/pdf/src/forme/primitives.tsx b/packages/pdf/src/forme/primitives.tsx index fb9d54251..835ef1175 100644 --- a/packages/pdf/src/forme/primitives.tsx +++ b/packages/pdf/src/forme/primitives.tsx @@ -21,9 +21,6 @@ type FlowProps = { fixed?: boolean | undefined; /** react-pdf only; Forme has no equivalent, so it's accepted and ignored. */ minPresenceAhead?: number | undefined; - debug?: boolean | undefined; - id?: string | undefined; - bookmark?: string | undefined; children?: ReactNode; } & DataAttributes; @@ -37,11 +34,9 @@ export type TextProps = FlowProps & { export type LinkProps = TextProps & { src: string }; export type ImageProps = { - src: string | { uri: string } | undefined; + src: string | undefined; style?: StyleProp | undefined; fixed?: boolean | undefined; - debug?: boolean | undefined; - cache?: boolean | undefined; } & DataAttributes; export type SvgProps = { @@ -68,12 +63,9 @@ export type DocumentProps = { author?: string | undefined; subject?: string | undefined; creator?: string | undefined; - producer?: string | undefined; language?: string | undefined; /** "auto" hyphenates words by the document language; "manual" breaks only at soft hyphens. */ hyphenation?: "auto" | "manual" | undefined; - creationDate?: Date | undefined; - pageMode?: string | undefined; children?: ReactNode; }; diff --git a/packages/pdf/src/forme/testing.ts b/packages/pdf/src/forme/testing.ts index 52305846c..e178c3de2 100644 --- a/packages/pdf/src/forme/testing.ts +++ b/packages/pdf/src/forme/testing.ts @@ -51,8 +51,5 @@ export function pdf(element: ReactElement) { const [root] = renderHostTree(element); return { container: { document: root ? toRenderedNode(root) : null }, - toBuffer: () => renderToBuffer(element), - toBlob: async () => - new Blob([(await renderToBuffer(element)) as Uint8Array], { type: "application/pdf" }), }; } diff --git a/packages/pdf/src/forme/to-forme.tsx b/packages/pdf/src/forme/to-forme.tsx index fcf0b7fc6..52b743852 100644 --- a/packages/pdf/src/forme/to-forme.tsx +++ b/packages/pdf/src/forme/to-forme.tsx @@ -180,11 +180,7 @@ const flowStyle = (props: Record, style: FormeStyle): FormeStyl return next; }; -const imageSource = (src: unknown): string | undefined => { - if (typeof src === "string") return src; - if (src && typeof src === "object" && "uri" in src && typeof src.uri === "string") return src.uri; - return undefined; -}; +const imageSource = (src: unknown): string | undefined => (typeof src === "string" ? src : undefined); const number = (value: unknown, context: Context): number | undefined => { const points = toPoints(value, context); @@ -418,7 +414,6 @@ function convertNode(node: HostNode, parentContext: Context, key: number): React { key, style: bordered.style, - ...(typeof props.bookmark === "string" ? { bookmark: props.bookmark } : {}), }, ...bordered.children, ); diff --git a/packages/resume/package.json b/packages/resume/package.json index 42c600e24..a482e2a36 100644 --- a/packages/resume/package.json +++ b/packages/resume/package.json @@ -12,7 +12,6 @@ "./markdown": "./src/markdown.ts", "./patch": "./src/patch.ts", "./proposals": "./src/proposals.ts", - "./section-availability": "./src/section-availability.ts", "./section-sort": "./src/section-sort.ts", "./social-meta": "./src/social-meta.ts", "./stylesheet": "./src/stylesheet/index.ts", diff --git a/packages/resume/src/section-availability.test.ts b/packages/resume/src/section-availability.test.ts deleted file mode 100644 index ce4729545..000000000 --- a/packages/resume/src/section-availability.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { sampleResumeData } from "@reactive-resume/schema/resume/sample"; -import { getSectionAvailability } from "./section-availability"; - -describe("section availability", () => { - it("includes every printable section and records every authored location", () => { - const data = structuredClone(sampleResumeData); - const customSection = data.customSections[0]; - const laterPage = data.metadata.layout.pages[2]; - if (!customSection || !laterPage) throw new Error("Sample resume lacks expected section fixtures."); - const customSectionId = customSection.id; - laterPage.sidebar.push(customSectionId, customSectionId, "unknown-section"); - - const availability = getSectionAvailability(data); - const sectionIds = availability.map((entry) => entry.sectionId); - - expect(sectionIds).toEqual([ - "summary", - ...Object.keys(data.sections), - ...data.customSections.map((section) => section.id), - ]); - expect(sectionIds).not.toContain("picture"); - expect(sectionIds).not.toContain("basics"); - expect(sectionIds).not.toContain("custom"); - expect(sectionIds).not.toContain("unknown-section"); - expect(availability.find((entry) => entry.sectionId === customSectionId)?.locations).toEqual([ - { pageIndex: 1, columnId: "main" }, - { pageIndex: 2, columnId: "sidebar" }, - { pageIndex: 2, columnId: "sidebar" }, - ]); - }); - - it("derives hidden and placement state independently without mutation", () => { - const data = structuredClone(sampleResumeData); - data.sections.experience.hidden = true; - data.sections.awards.items = []; - for (const page of data.metadata.layout.pages) { - page.main = page.main.filter((id) => id !== "experience"); - page.sidebar = page.sidebar.filter((id) => id !== "experience"); - page.main = page.main.filter((id) => id !== "projects"); - page.sidebar = page.sidebar.filter((id) => id !== "projects"); - } - data.summary.hidden = true; - const before = structuredClone(data); - - const availability = getSectionAvailability(data); - - expect(availability.find((entry) => entry.sectionId === "experience")).toEqual({ - sectionId: "experience", - hidden: true, - locations: [], - }); - expect(availability.find((entry) => entry.sectionId === "summary")).toEqual({ - sectionId: "summary", - hidden: true, - locations: [{ pageIndex: 0, columnId: "main" }], - }); - expect(availability.find((entry) => entry.sectionId === "projects")).toEqual({ - sectionId: "projects", - hidden: false, - locations: [], - }); - expect(availability.find((entry) => entry.sectionId === "awards")).toEqual({ - sectionId: "awards", - hidden: false, - locations: [{ pageIndex: 1, columnId: "main" }], - }); - expect(data).toEqual(before); - }); -}); diff --git a/packages/resume/src/section-availability.ts b/packages/resume/src/section-availability.ts deleted file mode 100644 index d76efacb2..000000000 --- a/packages/resume/src/section-availability.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { ResumeData } from "@reactive-resume/schema/resume/data"; - -export type SectionLocation = { - pageIndex: number; - columnId: "main" | "sidebar"; -}; - -export type SectionAvailability = { - sectionId: string; - hidden: boolean; - locations: SectionLocation[]; -}; - -function getSectionLocations(data: ResumeData, sectionId: string): SectionLocation[] { - const locations: SectionLocation[] = []; - - for (const [pageIndex, page] of data.metadata.layout.pages.entries()) { - for (const columnId of ["main", "sidebar"] as const) { - for (const id of page[columnId]) { - if (id === sectionId) locations.push({ pageIndex, columnId }); - } - } - } - - return locations; -} - -export function getSectionAvailability(data: ResumeData): SectionAvailability[] { - const sections = [ - { sectionId: "summary", hidden: data.summary.hidden }, - ...Object.entries(data.sections).map(([sectionId, section]) => ({ sectionId, hidden: section.hidden })), - ...data.customSections.map((section) => ({ sectionId: section.id, hidden: section.hidden })), - ]; - - return sections.map((section) => ({ - ...section, - locations: getSectionLocations(data, section.sectionId), - })); -}