mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-10-03 10:13:47 +10:00
refactor: drop dead render props, duplicate template tables and unused import helpers
This commit is contained in:
@@ -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<Template, TemplateConfig> = {
|
||||
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<Record<Template, "solid" | "tint">> = {
|
||||
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,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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"] };
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
|
||||
@@ -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 ? `<p>${award.summary}</p>` : "",
|
||||
@@ -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 ? `<p>${pub.summary}</p>` : "",
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
<Document
|
||||
pageMode="useNone"
|
||||
creationDate={creationDate}
|
||||
producer="Reactive Resume"
|
||||
title={resumeData.basics.name}
|
||||
author={resumeData.basics.name}
|
||||
creator={resumeData.basics.name}
|
||||
|
||||
@@ -59,8 +59,7 @@ export function imageSources(tree: HostNode[]): string[] {
|
||||
if ("text" in node) return;
|
||||
if (node.type === HOST.image) {
|
||||
const { src } = node.props;
|
||||
const url = typeof src === "string" ? src : (src as { uri?: unknown } | undefined)?.uri;
|
||||
if (typeof url === "string" && url.length > 0) sources.add(url);
|
||||
if (typeof src === "string" && src.length > 0) sources.add(src);
|
||||
}
|
||||
node.children.forEach(visit);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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<ArrayBuffer>], { type: "application/pdf" }),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -180,11 +180,7 @@ const flowStyle = (props: Record<string, unknown>, 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,
|
||||
);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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),
|
||||
}));
|
||||
}
|
||||
Reference in New Issue
Block a user