refactor(stylesheet): move Semantic CSS to the browser (#3329)

This commit is contained in:
Amruth Pillai
2026-08-16 16:50:27 +02:00
committed by GitHub
parent f848e57436
commit 9509b5bc2e
203 changed files with 11838 additions and 14842 deletions
@@ -54,7 +54,6 @@ export const buildAllTemplatesFixture = (template: Template) => {
data.metadata.stylesheet = {
mode: "semantic",
source: comprehensiveStylesheet,
applied: comprehensiveStylesheet,
};
return data;
};
@@ -15,7 +15,7 @@ describe("Semantic CSS all-template presentation", () => {
const runtime = resolveResumeRuntime({
data,
template,
applied: comprehensiveStylesheet,
source: comprehensiveStylesheet,
mode: "semantic",
});
@@ -34,7 +34,7 @@ describe("Semantic CSS all-template presentation", () => {
const { sourceTree } = resolveResumeRuntime({
data,
template,
applied: comprehensiveStylesheet,
source: comprehensiveStylesheet,
mode: "semantic",
});
const templateParts = new Set<string>();
@@ -54,7 +54,7 @@ const buildFixture = (template: Template, rule = ""): ResumeData => {
: [{ fullWidth: true, main: ["skills"], sidebar: [] }];
const stylesheet = { languageVersion: 1, text: `@version 1; ${rule}` };
data.metadata.stylesheet = { mode: "semantic", source: stylesheet, applied: stylesheet };
data.metadata.stylesheet = { mode: "semantic", source: stylesheet };
return data;
};
@@ -88,7 +88,7 @@ const finalOnyxCompanyStyle = async (keyword?: "inherit" | "initial" | "revert"
keyword ? `section[type="experience"] field[name="company"] { font-weight: ${keyword}; }` : ""
}`;
const stylesheet = { languageVersion: 1, text };
data.metadata.stylesheet = { mode: "semantic", source: stylesheet, applied: stylesheet };
data.metadata.stylesheet = { mode: "semantic", source: stylesheet };
const element = createElement(ResumeDocument, { data, template: "onyx" }) as unknown as Parameters<typeof pdf>[0];
const instance = pdf(element);
await expect.poll(() => instance.container.document).not.toBeNull();
@@ -97,7 +97,7 @@ const buildFixture = (
data.metadata.page.hideIcons = false;
data.metadata.layout.pages = [{ fullWidth: true, main: [section], sidebar: [] }];
const stylesheet = { languageVersion: 1, text: `@version 1; ${text}` };
data.metadata.stylesheet = { mode: "semantic", source: stylesheet, applied: stylesheet };
data.metadata.stylesheet = { mode: "semantic", source: stylesheet };
return data;
};
@@ -71,7 +71,7 @@ const fixture = (mode: "legacy" | "semantic", section: "experience" | "education
data.metadata.layout.pages = [{ fullWidth: true, main: [section], sidebar: [] }];
if (mode === "semantic") {
const stylesheet = { languageVersion: 1, text: `@version 1; ${rule}` };
data.metadata.stylesheet = { mode, source: stylesheet, applied: stylesheet };
data.metadata.stylesheet = { mode, source: stylesheet };
}
return data;
};
-35
View File
@@ -1,44 +1,9 @@
import type { ResumeData } from "@reactive-resume/schema/resume/data";
import type { StylesheetMode, StylesheetSource } from "@reactive-resume/schema/resume/stylesheet";
import type { Template } from "@reactive-resume/schema/templates";
import type { ResolvedResumeRuntime } from "./resolve";
import { resolveResumeRuntime, resolveStylesheetMode } from "./resolve";
export type InspectResumePdfOptions = {
data: ResumeData;
template?: Template | undefined;
applied?: StylesheetSource | undefined;
mode?: StylesheetMode | undefined;
};
export type ResumePdfRenderResult<T> =
| { ok: true; value: T; diagnostics: ResolvedResumeRuntime["diagnostics"] }
| { ok: false; diagnostics: ResolvedResumeRuntime["diagnostics"] };
export const inspectResumePdf = ({
data,
template = data.metadata.template,
applied,
mode = resolveStylesheetMode(data),
}: InspectResumePdfOptions): ResolvedResumeRuntime =>
resolveResumeRuntime({
data,
template,
mode,
...(applied ? { applied } : {}),
});
export const hasSemanticErrors = ({ diagnostics }: Pick<ResolvedResumeRuntime, "diagnostics">): boolean =>
diagnostics.some(({ severity }) => severity === "error");
export type {
ResolvedResumeRuntime,
ResolveResumePresentationInput,
} from "./resolve";
export * from "./legacy-converter";
export * from "./legacy-parity";
export * from "./preflight-core";
export * from "./public-projection";
export {
resolveResumePresentation,
resolveResumeRuntime,
@@ -8,7 +8,7 @@ import { ResumeDocument } from "../document";
import { semanticNodeKeys } from "./node-keys";
import { resolveResumePresentation, resolveStylesheetMode } from "./resolve";
const applied = (text: string) => ({ languageVersion: 1, text });
const source = (text: string) => ({ languageVersion: 1, text });
type HostNode = {
type: string;
@@ -90,7 +90,7 @@ const resolveIssueFixture = (text: string) => {
return resolveResumePresentation({
data,
template: "onyx",
applied: applied(text),
source: source(text),
mode: "semantic",
});
};
@@ -138,7 +138,7 @@ describe("semantic issue fixtures", () => {
resolveResumePresentation({
data,
template: "onyx",
applied: applied(`@version 1;${text}`),
source: source(`@version 1;${text}`),
mode: "semantic",
});
@@ -157,11 +157,11 @@ describe("semantic issue fixtures", () => {
header { background-color: #1e293b; }
name { color: white; }
`);
const invalid = compileStylesheet(applied("@version 1; header { background-image: linear-gradient(red, blue); }"));
const invalid = compileStylesheet(source("@version 1; header { background-image: linear-gradient(red, blue); }"));
expect(valid[headerKey]?.style?.backgroundColor).toBe("#1e293b");
expect(valid[semanticNodeKeys.headerPart(headerKey, "name")]?.style?.color).toBe("white");
expect(invalid.program).toBeNull();
expect(invalid.program).not.toBeNull();
});
it("unbolds only skill names and leaves experience titles unchanged (#2223)", () => {
@@ -180,8 +180,7 @@ describe("semantic issue fixtures", () => {
const semanticData = buildIssueFixture();
semanticData.metadata.stylesheet = {
mode: "semantic",
source: applied("@version 1;"),
applied: applied("@version 1;"),
source: source("@version 1;"),
};
const legacyData = buildIssueFixture();
@@ -191,7 +190,7 @@ describe("semantic issue fixtures", () => {
resolveResumePresentation({
data: legacyData,
template: "onyx",
applied: applied("@version 1; name { color: red; }"),
source: source("@version 1; name { color: red; }"),
mode: "legacy",
}),
).toEqual({});
@@ -199,7 +198,7 @@ describe("semantic issue fixtures", () => {
it("applies issue-regression styles to the final existing PDF primitives", async () => {
const data = buildIssueFixture();
const stylesheet = applied(`
const stylesheet = source(`
@version 1;
header { background-color: #1e293b; }
name { color: white; }
@@ -208,7 +207,7 @@ describe("semantic issue fixtures", () => {
section[type="skills"] field[name="name"] { font-weight: 400; }
level icon[role~="active"] { opacity: 0.2; }
`);
data.metadata.stylesheet = { mode: "semantic", source: stylesheet, applied: stylesheet };
data.metadata.stylesheet = { mode: "semantic", source: stylesheet };
const element = createElement(ResumeDocument, { data, template: "onyx" }) as unknown as Parameters<typeof pdf>[0];
const instance = pdf(element);
await vi.waitFor(() => expect(instance.container.document).not.toBeNull());
@@ -226,11 +225,11 @@ describe("semantic issue fixtures", () => {
it("unbolds only the final skill-name primitive and preserves the experience title weight (#2223)", async () => {
const data = buildIssueFixture();
const stylesheet = applied(`
const stylesheet = source(`
@version 1;
section[type="skills"] field[name="name"] { font-weight: 400; }
`);
data.metadata.stylesheet = { mode: "semantic", source: stylesheet, applied: stylesheet };
data.metadata.stylesheet = { mode: "semantic", source: stylesheet };
const element = createElement(ResumeDocument, { data, template: "onyx" }) as unknown as Parameters<typeof pdf>[0];
const instance = pdf(element);
await vi.waitFor(() => expect(instance.container.document).not.toBeNull());
@@ -253,12 +252,12 @@ describe("semantic issue fixtures", () => {
keywords: [],
}));
data.metadata.layout.pages = [{ fullWidth: true, main: ["skills"], sidebar: [] }];
const stylesheet = applied(`
const stylesheet = source(`
@version 1;
section[type="skills"] item:nth-child(2) { display: none; }
section[type="skills"] item:last-child { order: -1; }
`);
data.metadata.stylesheet = { mode: "semantic", source: stylesheet, applied: stylesheet };
data.metadata.stylesheet = { mode: "semantic", source: stylesheet };
const element = createElement(ResumeDocument, { data, template: "onyx" }) as unknown as Parameters<typeof pdf>[0];
const instance = pdf(element);
await vi.waitFor(() => expect(instance.container.document).not.toBeNull());
@@ -286,7 +286,6 @@ export async function compareLegacySemanticPresentation(
stylesheet: {
mode: "semantic",
source: input.convertedSource,
applied: input.convertedSource,
},
},
};
@@ -162,7 +162,7 @@ const semanticData = (data: ResumeData): ResumeData => {
const conversion = convertLegacyStyleRules(data);
const semantic = structuredClone(data);
semantic.metadata.styleRules = [...conversion.sanitizedRules];
semantic.metadata.stylesheet = { mode: "semantic", source: conversion.source, applied: conversion.source };
semantic.metadata.stylesheet = { mode: "semantic", source: conversion.source };
return semantic;
};
@@ -28,7 +28,7 @@ const buildFixture = (
if (mode !== "missing") {
const stylesheet = { languageVersion: 1, text };
data.metadata.stylesheet = { mode, source: stylesheet, applied: stylesheet };
data.metadata.stylesheet = { mode, source: stylesheet };
}
return data;
@@ -53,7 +53,6 @@ const buildFixture = (mode: "legacy" | "semantic"): ResumeData => {
data.metadata.stylesheet = {
mode,
source: semanticSource(),
applied: semanticSource(),
};
}
return data;
@@ -57,7 +57,7 @@ const buildFixture = (value: string): ResumeData => {
languageVersion: 1,
text: `@version 1; section[type="summary"] { break-before: ${value}; break-inside: ${value}; }`,
};
data.metadata.stylesheet = { mode: "semantic", source, applied: source };
data.metadata.stylesheet = { mode: "semantic", source };
return data;
};
@@ -58,7 +58,7 @@ const parsePdf = (data: Uint8Array): Promise<ParsedPdf> => getDocument({ data })
const overflowingFixture = (pageSize: "A4" | "LETTER"): ResumeData => {
const data = structuredClone(defaultResumeData);
const applied = {
const source = {
languageVersion: 1,
text: `
@version 1;
@@ -75,7 +75,7 @@ const overflowingFixture = (pageSize: "A4" | "LETTER"): ResumeData => {
(_value, index) => `<p>Overflow line ${index + 1} with enough text to occupy the authored page.</p>`,
).join("");
data.metadata.layout.pages = [{ fullWidth: true, main: ["summary"], sidebar: [] }];
data.metadata.stylesheet = { mode: "semantic", source: applied, applied };
data.metadata.stylesheet = { mode: "semantic", source };
return data;
};
@@ -96,14 +96,14 @@ const readPhysicalPages = async (document: ParsedPdf) => {
describe("semantic pagination bindings", () => {
it("passes resolved authored-page size to the existing Page primitive", async () => {
const data = structuredClone(defaultResumeData);
const applied = {
const source = {
languageVersion: 1,
text: '@version 1;\npage[page-number="1"] { size: LETTER; }',
};
data.picture.hidden = true;
data.basics.name = "Ada Lovelace";
data.metadata.layout.pages = [{ fullWidth: true, main: [], sidebar: [] }];
data.metadata.stylesheet = { mode: "semantic", source: applied, applied };
data.metadata.stylesheet = { mode: "semantic", source };
const page = findFirst(await renderHostTree(data), "PAGE");
@@ -1,204 +0,0 @@
import type { ResumeData } from "@reactive-resume/schema/resume/data";
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { defaultResumeData } from "@reactive-resume/schema/resume/default";
let renderPreflightPdf: typeof import("./preflight-core").renderPreflightPdf;
const rendererMock = vi.hoisted(() => ({
pdf: vi.fn(() => ({
toBlob: vi.fn(async () => new Blob(["%PDF-1.7"], { type: "application/pdf" })),
})),
}));
vi.mock("#react-pdf-renderer", async (importOriginal) => ({
...(await importOriginal<typeof import("@react-pdf/renderer")>()),
pdf: rendererMock.pdf,
}));
beforeAll(async () => {
vi.resetModules();
({ renderPreflightPdf } = await import("./preflight-core"));
});
afterAll(() => {
vi.doUnmock("#react-pdf-renderer");
vi.resetModules();
});
const validStylesheet = {
languageVersion: 1,
text: "@version 1;",
} as const;
const pageLimits = {
maxPageWidthPt: 2_000,
maxPageHeightPt: 20_000,
maxPageAreaPt2: 20_000_000,
} as const;
const createRendererUnsafeResumeData = (): ResumeData => {
const data = structuredClone(defaultResumeData);
data.customSections = [
{
id: "custom-experience",
type: "experience",
title: "Experience",
icon: "",
columns: 1,
hidden: false,
keepTogether: false,
startOnNewPage: false,
items: [{ id: "summary-shaped-item", hidden: false, content: "<p>Missing company</p>" }],
} as never,
];
return data;
};
const createLegacyRendererSafeResumeData = (): ResumeData =>
({
...structuredClone(defaultResumeData),
customSections: [
{
id: "custom-experience",
type: "experience",
title: "Experience",
icon: "",
columns: 1,
hidden: false,
keepTogether: false,
startOnNewPage: false,
items: [
{
id: "experience-item",
hidden: false,
company: "Analytical Engines",
position: "Programmer",
location: "London",
period: "18421843",
description: "<p>Wrote the first algorithm.</p>",
content: "<p>Compatible overlap</p>",
},
],
},
],
}) as unknown as ResumeData;
describe("renderPreflightPdf", () => {
beforeEach(() => {
rendererMock.pdf.mockReset();
rendererMock.pdf.mockImplementation(() => ({
toBlob: vi.fn(async () => new Blob(["%PDF-1.7"], { type: "application/pdf" })),
}));
});
it("renders a valid semantic candidate to transferable PDF bytes", async () => {
const result = await renderPreflightPdf(
{
data: createLegacyRendererSafeResumeData(),
template: defaultResumeData.metadata.template,
stylesheet: validStylesheet,
},
pageLimits,
);
expect(result).toMatchObject({ ok: true, diagnostics: [] });
expect(result.ok && new TextDecoder().decode(result.bytes)).toBe("%PDF-1.7");
expect(rendererMock.pdf).toHaveBeenCalledWith(
expect.objectContaining({
props: expect.objectContaining({
data: expect.objectContaining({
customSections: [
expect.objectContaining({
items: [
expect.objectContaining({
content: "<p>Compatible overlap</p>",
roles: [],
website: { url: "", label: "", inlineLink: false },
}),
],
}),
],
}),
}),
}),
);
});
it("returns compiler diagnostics without starting the renderer", async () => {
const result = await renderPreflightPdf(
{
data: defaultResumeData,
template: defaultResumeData.metadata.template,
stylesheet: { languageVersion: 1, text: "@version 1; page { color: ; }" },
},
pageLimits,
);
expect(result).toMatchObject({
ok: false,
code: "STYLESHEET_PREFLIGHT_INVALID",
diagnostics: [expect.objectContaining({ severity: "error" })],
});
expect(rendererMock.pdf).not.toHaveBeenCalled();
});
it("rejects renderer-unsafe data before semantic inspection or React PDF dispatch", async () => {
const result = renderPreflightPdf(
{
data: createRendererUnsafeResumeData(),
template: defaultResumeData.metadata.template,
stylesheet: validStylesheet,
},
pageLimits,
);
await expect(result).rejects.toMatchObject({
name: "ZodError",
issues: expect.arrayContaining([expect.objectContaining({ path: ["customSections", 0, "items", 0, "company"] })]),
});
expect(rendererMock.pdf).not.toHaveBeenCalled();
});
it("returns a stable public failure when React PDF throws", async () => {
rendererMock.pdf.mockReturnValueOnce({
toBlob: vi.fn(() => Promise.reject(new Error("sensitive renderer details"))),
});
const result = await renderPreflightPdf(
{
data: defaultResumeData,
template: defaultResumeData.metadata.template,
stylesheet: validStylesheet,
},
pageLimits,
);
expect(result).toMatchObject({
ok: false,
code: "STYLESHEET_PREFLIGHT_RENDER_FAILED",
message: "PDF rendering failed.",
diagnostics: [],
});
});
it.each([
["width", "2001pt 1000pt"],
["height", "1000pt 20001pt"],
["area", "1500pt 15000pt"],
])("rejects authored page %s limits before starting the renderer", async (_limit, size) => {
const result = await renderPreflightPdf(
{
data: defaultResumeData,
template: defaultResumeData.metadata.template,
stylesheet: { languageVersion: 1, text: `@version 1; page { size: ${size}; }` },
},
pageLimits,
);
expect(result).toMatchObject({
ok: false,
code: "STYLESHEET_PREFLIGHT_PAGE_SIZE_LIMIT",
});
expect(rendererMock.pdf).not.toHaveBeenCalled();
});
});
@@ -1,138 +0,0 @@
import type { SemanticCssDiagnostic } from "@reactive-resume/resume/stylesheet";
import type { ResumeData } from "@reactive-resume/schema/resume/data";
import type { StylesheetSource } from "@reactive-resume/schema/resume/stylesheet";
import type { Template } from "@reactive-resume/schema/templates";
import type { PdfPreflightFailureCode } from "./preflight-reference";
import { createElement } from "react";
import { parseResumeData } from "@reactive-resume/schema/resume/data";
import { pdf } from "#react-pdf-renderer";
import { ResumeDocument } from "../document";
import { getTemplatePageSize } from "../templates/shared/page-size";
import { semanticNodeKeys } from "./node-keys";
import { resolveResumeRuntime } from "./resolve";
export type { PdfPreflightFailureCode } from "./preflight-reference";
export type StylesheetPreflightInput = {
data: ResumeData;
template: Template;
stylesheet: StylesheetSource;
};
export type PdfPreflightPageLimits = {
maxPageWidthPt: number;
maxPageHeightPt: number;
maxPageAreaPt2: number;
};
export type PdfPreflightFailure = {
ok: false;
code: PdfPreflightFailureCode;
message: string;
diagnostics: readonly SemanticCssDiagnostic[];
};
export type PdfPreflightResult =
| {
ok: true;
pageCount: number;
byteCount: number;
diagnostics: readonly SemanticCssDiagnostic[];
}
| PdfPreflightFailure;
export type RenderPreflightPdfResult =
| {
ok: true;
bytes: Uint8Array;
diagnostics: readonly SemanticCssDiagnostic[];
}
| PdfPreflightFailure;
export type StylesheetPreflightRunner = {
run(input: StylesheetPreflightInput): Promise<PdfPreflightResult>;
};
export type BrowserPdfPreflightResult =
| (Extract<PdfPreflightResult, { ok: true }> & { pdf: ArrayBuffer })
| PdfPreflightFailure;
const pageDimensions = (size: "A4" | "LETTER" | { width: number; height?: number }) => {
if (size === "LETTER") return { width: 612, height: 792 };
if (size === "A4") return { width: 595.28, height: 841.89 };
return { width: size.width, height: size.height ?? 841.89 };
};
const pageSizeFailure = (
data: ResumeData,
presentation: ReturnType<typeof resolveResumeRuntime>["presentation"],
limits: PdfPreflightPageLimits,
): PdfPreflightFailure | undefined => {
const fallbackSize = getTemplatePageSize(data.metadata.page.format);
for (const index of data.metadata.layout.pages.keys()) {
const size = presentation[semanticNodeKeys.page(index + 1)]?.size ?? fallbackSize;
const { width, height } = pageDimensions(size);
if (width > limits.maxPageWidthPt || height > limits.maxPageHeightPt || width * height > limits.maxPageAreaPt2) {
return {
ok: false,
code: "STYLESHEET_PREFLIGHT_PAGE_SIZE_LIMIT",
message: "The authored page size exceeds the PDF preflight limit.",
diagnostics: [],
};
}
}
};
export async function renderPreflightPdf(
input: StylesheetPreflightInput,
pageLimits: PdfPreflightPageLimits,
): Promise<RenderPreflightPdfResult> {
const parsedData = parseResumeData(input.data);
const data = {
...parsedData,
metadata: {
...parsedData.metadata,
stylesheet: {
mode: "semantic" as const,
source: input.stylesheet,
applied: input.stylesheet,
},
},
};
const inspection = resolveResumeRuntime({
data,
template: input.template,
applied: input.stylesheet,
mode: "semantic",
});
if (inspection.diagnostics.some(({ severity }) => severity === "error")) {
return {
ok: false,
code: "STYLESHEET_PREFLIGHT_INVALID",
message: "The stylesheet cannot be rendered.",
diagnostics: inspection.diagnostics,
};
}
const pageFailure = pageSizeFailure(data, inspection.presentation, pageLimits);
if (pageFailure) return { ...pageFailure, diagnostics: inspection.diagnostics };
try {
const document = createElement(ResumeDocument, { data, template: input.template }) as Parameters<typeof pdf>[0];
const blob = await pdf(document).toBlob();
return {
ok: true,
bytes: new Uint8Array(await blob.arrayBuffer()),
diagnostics: inspection.diagnostics,
};
} catch {
return {
ok: false,
code: "STYLESHEET_PREFLIGHT_RENDER_FAILED",
message: "PDF rendering failed.",
diagnostics: inspection.diagnostics,
};
}
}
@@ -1,55 +0,0 @@
export const PDF_PREFLIGHT_DIAGNOSTIC_CATALOG = {
STYLESHEET_PREFLIGHT_INVALID: {
meaning: "The stylesheet has compiler or semantic errors.",
action: "Fix the accompanying Semantic CSS diagnostics.",
},
STYLESHEET_PREFLIGHT_PAGE_SIZE_LIMIT: {
meaning: "An authored page exceeds the PDF dimension or area budget.",
action: "Use a smaller page size.",
},
STYLESHEET_PREFLIGHT_BYTE_LIMIT: {
meaning: "The rendered PDF exceeds the byte budget.",
action: "Reduce pages, images, or styled content.",
},
STYLESHEET_PREFLIGHT_PAGE_LIMIT: {
meaning: "The rendered PDF exceeds the page-count budget.",
action: "Reduce content or pagination.",
},
STYLESHEET_PREFLIGHT_TIMEOUT: {
meaning: "PDF preflight exceeded its deadline.",
action: "Reduce stylesheet or document complexity and retry.",
},
STYLESHEET_PREFLIGHT_MEMORY_LIMIT: {
meaning: "PDF preflight exceeded its memory budget.",
action: "Reduce document, image, or layout complexity.",
},
STYLESHEET_PREFLIGHT_RENDER_FAILED: {
meaning: "The PDF renderer could not render the candidate stylesheet.",
action: "Simplify the candidate and inspect accompanying diagnostics.",
},
STYLESHEET_PREFLIGHT_PARSE_FAILED: {
meaning: "The rendered PDF could not be inspected.",
action: "Retry after simplifying the candidate.",
},
STYLESHEET_PREFLIGHT_WORKER_FAILED: {
meaning: "The isolated PDF preflight worker failed or its queue was full.",
action: "Retry; simplify the candidate if the failure repeats.",
},
} as const;
export type PdfPreflightFailureCode = keyof typeof PDF_PREFLIGHT_DIAGNOSTIC_CATALOG;
export const STYLESHEET_PREFLIGHT_LIMITS = Object.freeze({
// Render deadline (after the worker is warm). A rich resume on a throttled/shared
// vCPU renders in ~5-18s, so 5s spuriously failed real resumes; the worker is now
// warmed+reused so this ceiling only bounds a genuinely stuck render.
timeoutMs: 30_000,
maxPages: 20,
maxBytes: 10_000_000,
maxPageWidthPt: 2_000,
maxPageHeightPt: 20_000,
maxPageAreaPt2: 20_000_000,
maxOldGenerationMb: 256,
maxConcurrentWorkers: 1,
maxQueuedRequests: 32,
});
@@ -1,135 +0,0 @@
import type { PublicStyleProjection } from "./public-projection";
import { describe, expect, it } from "vitest";
import { defaultResumeData } from "@reactive-resume/schema/resume/default";
import {
createPublicStyleProjection,
PUBLIC_STYLE_PROJECTION_FORMAT_VERSION,
SEMANTIC_TREE_VERSION,
validatePublicStyleProjection,
} from "./public-projection";
const buildData = () => {
const data = structuredClone(defaultResumeData);
const applied = {
languageVersion: 1,
text: "@version 1;\nname { color: #123456; }\n",
};
data.basics.name = "Ada Lovelace";
data.metadata.layout.pages = [{ fullWidth: true, main: [], sidebar: [] }];
data.metadata.stylesheet = { mode: "semantic", source: applied, applied };
return data;
};
describe("public semantic style projection", () => {
it("contains only resolved, JSON-safe presentation keyed by stable node key", async () => {
const projection = await createPublicStyleProjection({ data: buildData() });
const serialized = JSON.stringify(projection);
expect(projection).toMatchObject({
formatVersion: PUBLIC_STYLE_PROJECTION_FORMAT_VERSION,
languageVersion: 1,
semanticTreeVersion: SEMANTIC_TREE_VERSION,
registryFingerprint: expect.stringMatching(/^[a-f0-9]{64}$/),
adapterFingerprint: expect.stringMatching(/^[a-f0-9]{64}$/),
renderDataHash: expect.stringMatching(/^[a-f0-9]{64}$/),
});
expect(projection.nodes["page-1/region-header/header/name"]).toEqual({
style: { color: "#123456" },
});
expect(serialized).not.toContain("@version");
expect(serialized).not.toMatch(/source|comment|diagnostic|selector|variable|range/i);
expect(serialized).not.toContain("undefined");
});
it("carries final sibling visibility and order without stylesheet source", async () => {
const data = buildData();
data.basics.email = "ada@example.com";
data.basics.phone = "+44 123";
data.basics.location = "London";
const applied = {
languageVersion: 1,
text: `@version 1;
contact-item[name="location"] { display: none; }
contact-item[name="phone"] { order: -1; }
`,
};
data.metadata.stylesheet = { mode: "semantic", source: applied, applied };
const projection = await createPublicStyleProjection({ data });
expect(projection.nodes["page-1/region-header/header/contact-list/contact-location"]).toMatchObject({
hidden: true,
});
expect(projection.nodes["page-1/region-header/header/contact-list/contact-phone"]).toMatchObject({ order: 0 });
expect(projection.nodes["page-1/region-header/header/contact-list/contact-email"]).toMatchObject({ order: 1 });
});
it("rejects changed nodes and every version or fingerprint mismatch", async () => {
const data = buildData();
const valid = await createPublicStyleProjection({ data });
const cases = [
{ ...valid, formatVersion: 2 },
{ ...valid, languageVersion: 2 },
{ ...valid, semanticTreeVersion: 2 },
{ ...valid, registryFingerprint: "0".repeat(64) },
{ ...valid, adapterFingerprint: "0".repeat(64) },
{ ...valid, renderDataHash: "0".repeat(64) },
{
...valid,
nodes: {
...valid.nodes,
"page-1/region-header/header/name": { style: { color: "#ff0000" } },
},
},
];
for (const projection of cases) {
await expect(validatePublicStyleProjection(data, projection as unknown as PublicStyleProjection)).resolves.toBe(
false,
);
}
});
it("rejects projections hashed for different public render data", async () => {
const data = buildData();
const projection = await createPublicStyleProjection({ data });
const changed = structuredClone(data);
changed.basics.name = "Grace Hopper";
await expect(validatePublicStyleProjection(changed, projection)).resolves.toBe(false);
});
it("changes the projection hash when only the applied presentation changes", async () => {
const red = buildData();
const blue = buildData();
const blueApplied = {
languageVersion: 1,
text: "@version 1;\nname { color: #654321; }\n",
};
blue.metadata.stylesheet = { mode: "semantic", source: blueApplied, applied: blueApplied };
const redProjection = await createPublicStyleProjection({ data: red });
const blueProjection = await createPublicStyleProjection({ data: blue });
expect(redProjection.nodes["page-1/region-header/header/name"]).not.toEqual(
blueProjection.nodes["page-1/region-header/header/name"],
);
expect(redProjection.renderDataHash).not.toBe(blueProjection.renderDataHash);
});
it("rejects extra or non-JSON node fields instead of exposing compiler internals", async () => {
const data = buildData();
const projection = await createPublicStyleProjection({ data });
const node = projection.nodes["page-1/region-header/header/name"];
await expect(
validatePublicStyleProjection(data, {
...projection,
nodes: {
...projection.nodes,
"page-1/region-header/header/name": { ...node, diagnostics: [{ message: "private" }] },
},
} as unknown as PublicStyleProjection),
).resolves.toBe(false);
});
});
@@ -1,369 +0,0 @@
import type { SemanticNode } from "@reactive-resume/resume/stylesheet";
import type { ResumeData } from "@reactive-resume/schema/resume/data";
import type { ResolvedPdfNodePresentation } from "./adapter";
import type { ResolvedResumeRuntime } from "./resolve";
import {
computeRenderDataHash,
PROPERTY_REGISTRY_V1,
projectPublicRenderData,
SEMANTIC_REGISTRY_V1,
SUPPORTED_SEMANTIC_CSS_VERSIONS,
TEMPLATE_PART_CHILD_KINDS_V1,
} from "@reactive-resume/resume/stylesheet";
import { EMPTY_SEMANTIC_CSS_SOURCE } from "@reactive-resume/schema/resume/stylesheet";
import { resolveResumeRuntime, resolveStylesheetMode } from "./resolve";
import { getTemplateSemanticRegistryFingerprintInput } from "./template-manifest";
export const PUBLIC_STYLE_PROJECTION_FORMAT_VERSION = 1;
export const SEMANTIC_TREE_VERSION = 1;
const PDF_ADAPTER_VERSION = 1;
const REACT_PDF_RENDERER_VERSION = "4.5";
type PublicPdfStyleValue = string | number | null;
type PublicPdfPageSize = "A4" | "LETTER" | { width: number; height?: number };
export type PublicPdfNodePresentation = {
style?: Readonly<Record<string, PublicPdfStyleValue>>;
size?: PublicPdfPageSize;
break?: boolean;
wrap?: boolean;
fixed?: boolean;
minPresenceAhead?: number;
orphans?: number;
widows?: number;
hidden?: boolean;
order?: number;
};
export type PublicStyleProjection = {
formatVersion: typeof PUBLIC_STYLE_PROJECTION_FORMAT_VERSION;
languageVersion: number;
semanticTreeVersion: typeof SEMANTIC_TREE_VERSION;
registryFingerprint: string;
adapterFingerprint: string;
renderDataHash: string;
nodes: Readonly<Record<string, PublicPdfNodePresentation>>;
};
type ProjectionFingerprints = Pick<
PublicStyleProjection,
"formatVersion" | "languageVersion" | "semanticTreeVersion" | "registryFingerprint" | "adapterFingerprint"
>;
const isPlainObject = (value: unknown): value is Record<string, unknown> => {
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
const prototype = Object.getPrototypeOf(value);
return prototype === Object.prototype || prototype === null;
};
const hasExactKeys = (value: Record<string, unknown>, allowed: readonly string[]): boolean =>
Object.keys(value).every((key) => allowed.includes(key)) && Object.getOwnPropertySymbols(value).length === 0;
const finiteNumber = (value: unknown): value is number => typeof value === "number" && Number.isFinite(value);
const isPageSize = (value: unknown): value is PublicPdfPageSize => {
if (value === "A4" || value === "LETTER") return true;
if (!isPlainObject(value) || !hasExactKeys(value, ["width", "height"]) || !finiteNumber(value.width)) return false;
return value.height === undefined || finiteNumber(value.height);
};
const isPublicNode = (value: unknown): value is PublicPdfNodePresentation => {
if (
!isPlainObject(value) ||
!hasExactKeys(value, [
"style",
"size",
"break",
"wrap",
"fixed",
"minPresenceAhead",
"orphans",
"widows",
"hidden",
"order",
])
) {
return false;
}
if (value.style !== undefined) {
if (!isPlainObject(value.style)) return false;
for (const styleValue of Object.values(value.style)) {
if (styleValue !== null && typeof styleValue !== "string" && !finiteNumber(styleValue)) {
return false;
}
}
}
if (value.size !== undefined && !isPageSize(value.size)) return false;
for (const key of ["break", "wrap", "fixed"] as const) {
if (value[key] !== undefined && typeof value[key] !== "boolean") return false;
}
for (const key of ["minPresenceAhead", "orphans", "widows"] as const) {
if (value[key] !== undefined && !finiteNumber(value[key])) return false;
}
if (value.hidden !== undefined && typeof value.hidden !== "boolean") return false;
if (value.order !== undefined && (!Number.isInteger(value.order) || (value.order as number) < 0)) return false;
return true;
};
const isProjectionShape = (value: unknown): value is PublicStyleProjection => {
if (
!isPlainObject(value) ||
!hasExactKeys(value, [
"formatVersion",
"languageVersion",
"semanticTreeVersion",
"registryFingerprint",
"adapterFingerprint",
"renderDataHash",
"nodes",
]) ||
!Number.isInteger(value.formatVersion) ||
!Number.isInteger(value.languageVersion) ||
!Number.isInteger(value.semanticTreeVersion) ||
typeof value.registryFingerprint !== "string" ||
typeof value.adapterFingerprint !== "string" ||
typeof value.renderDataHash !== "string" ||
!isPlainObject(value.nodes)
) {
return false;
}
return Object.values(value.nodes).every(isPublicNode);
};
type PublicNodeStructure = Pick<PublicPdfNodePresentation, "hidden" | "order">;
const toPublicNode = (
presentation: ResolvedPdfNodePresentation,
structure: PublicNodeStructure,
): PublicPdfNodePresentation => ({
...(presentation.style
? {
style: Object.freeze(
Object.fromEntries(
Object.entries(presentation.style).map(([property, value]) => [
property,
value === undefined ? null : (value as string | number),
]),
),
),
}
: {}),
...(presentation.size === undefined ? {} : { size: presentation.size }),
...(presentation.break === undefined ? {} : { break: presentation.break }),
...(presentation.wrap === undefined ? {} : { wrap: presentation.wrap }),
...(presentation.fixed === undefined ? {} : { fixed: presentation.fixed }),
...(presentation.minPresenceAhead === undefined ? {} : { minPresenceAhead: presentation.minPresenceAhead }),
...(presentation.orphans === undefined ? {} : { orphans: presentation.orphans }),
...(presentation.widows === undefined ? {} : { widows: presentation.widows }),
...(structure.hidden === undefined ? {} : { hidden: structure.hidden }),
...(structure.order === undefined ? {} : { order: structure.order }),
});
const indexChildren = (tree: SemanticNode) => {
const children = new Map<string, readonly string[]>();
const visit = (node: SemanticNode) => {
children.set(
node.key,
node.children.map(({ key }) => key),
);
for (const child of node.children) visit(child);
};
visit(tree);
return children;
};
const projectNodeStructure = (
sourceTree: SemanticNode,
renderTree: SemanticNode,
): Readonly<Record<string, PublicNodeStructure>> => {
const renderedChildren = indexChildren(renderTree);
const structure: Record<string, PublicNodeStructure> = {};
const visit = (node: SemanticNode) => {
const rendered = renderedChildren.get(node.key) ?? [];
const order = new Map(rendered.map((key, index) => [key, index]));
for (const [sourceIndex, child] of node.children.entries()) {
const renderedIndex = order.get(child.key);
structure[child.key] =
renderedIndex === undefined ? { hidden: true } : renderedIndex === sourceIndex ? {} : { order: renderedIndex };
visit(child);
}
};
visit(sourceTree);
return structure;
};
const fingerprints = Promise.all([
computeRenderDataHash({
domainVersion: 1,
data: {
semanticTreeVersion: SEMANTIC_TREE_VERSION,
semanticRegistry: SEMANTIC_REGISTRY_V1,
templateParts: TEMPLATE_PART_CHILD_KINDS_V1,
templateManifests: getTemplateSemanticRegistryFingerprintInput(),
},
}),
computeRenderDataHash({
domainVersion: 1,
data: {
adapterVersion: PDF_ADAPTER_VERSION,
reactPdfRendererVersion: REACT_PDF_RENDERER_VERSION,
propertyRegistry: PROPERTY_REGISTRY_V1,
},
}),
]);
const getFingerprints = async () => {
const [registryFingerprint, adapterFingerprint] = await fingerprints;
return { registryFingerprint, adapterFingerprint };
};
export const getPublicStyleProjectionFingerprints = getFingerprints;
const dataForPublicProjection = (data: ResumeData, languageVersion: number): ResumeData => {
if (data.metadata.stylesheet?.mode === "semantic") return data;
const source = { languageVersion, text: EMPTY_SEMANTIC_CSS_SOURCE };
return {
...data,
metadata: {
...data.metadata,
stylesheet: { mode: "semantic", source, applied: source },
},
};
};
const projectionFingerprints = async (data: ResumeData, languageVersion?: number): Promise<ProjectionFingerprints> => ({
formatVersion: PUBLIC_STYLE_PROJECTION_FORMAT_VERSION,
languageVersion:
languageVersion ??
(data.metadata.stylesheet?.mode === "semantic" ? data.metadata.stylesheet.applied.languageVersion : 1),
semanticTreeVersion: SEMANTIC_TREE_VERSION,
...(await getFingerprints()),
});
const hashProjection = (
data: ResumeData,
nodes: PublicStyleProjection["nodes"],
projection: ProjectionFingerprints,
): Promise<string> =>
computeRenderDataHash({
domainVersion: 1,
data: projectPublicRenderData(data),
resolvedNodes: nodes,
projectionFingerprints: projection,
});
export async function createPublicStyleProjection(input: { data: ResumeData }): Promise<PublicStyleProjection> {
const runtime = resolveResumeRuntime({
data: input.data,
template: input.data.metadata.template,
mode: resolveStylesheetMode(input.data),
});
if (runtime.diagnostics.some(({ severity }) => severity === "error")) {
throw new Error("Applied semantic stylesheet cannot be projected");
}
const structure = projectNodeStructure(runtime.sourceTree, runtime.renderTree);
const nodes = Object.freeze(
Object.fromEntries(
Object.entries(runtime.presentation).map(([nodeKey, presentation]) => [
nodeKey,
toPublicNode(presentation, structure[nodeKey] ?? {}),
]),
),
);
const projection = await projectionFingerprints(input.data);
return Object.freeze({
...projection,
renderDataHash: await hashProjection(input.data, nodes, projection),
nodes,
});
}
export async function validatePublicStyleProjection(
data: ResumeData,
projection: PublicStyleProjection,
): Promise<boolean> {
if (!isProjectionShape(projection)) return false;
if (!SUPPORTED_SEMANTIC_CSS_VERSIONS.includes(projection.languageVersion as 1)) return false;
if (
data.metadata.stylesheet?.mode === "semantic" &&
projection.languageVersion !== data.metadata.stylesheet.applied.languageVersion
) {
return false;
}
const projectionData = dataForPublicProjection(data, projection.languageVersion);
const expected = await projectionFingerprints(projectionData, projection.languageVersion);
if (
projection.formatVersion !== expected.formatVersion ||
projection.languageVersion !== expected.languageVersion ||
projection.semanticTreeVersion !== expected.semanticTreeVersion ||
projection.registryFingerprint !== expected.registryFingerprint ||
projection.adapterFingerprint !== expected.adapterFingerprint
) {
return false;
}
try {
return projection.renderDataHash === (await hashProjection(projectionData, projection.nodes, expected));
} catch {
return false;
}
}
const toResolvedPresentation = (
nodes: PublicStyleProjection["nodes"],
): Readonly<Record<string, ResolvedPdfNodePresentation>> =>
Object.freeze(
Object.fromEntries(
Object.entries(nodes).map(([nodeKey, { hidden: _hidden, order: _order, style, ...presentation }]) => [
nodeKey,
{
...presentation,
...(style
? {
style: Object.fromEntries(
Object.entries(style).map(([property, value]) => [property, value === null ? undefined : value]),
) as NonNullable<ResolvedPdfNodePresentation["style"]>,
}
: {}),
},
]),
),
);
const applyProjectedStructure = (node: SemanticNode, nodes: PublicStyleProjection["nodes"]): SemanticNode => ({
...node,
attributes: { ...node.attributes },
roles: [...node.roles],
children: node.children
.map((child, sourceIndex) => ({ child, sourceIndex, structure: nodes[child.key] }))
.filter(({ structure }) => !structure?.hidden)
.sort(
(left, right) =>
(left.structure?.order ?? left.sourceIndex) - (right.structure?.order ?? right.sourceIndex) ||
left.sourceIndex - right.sourceIndex,
)
.map(({ child }) => applyProjectedStructure(child, nodes)),
});
export async function resolvePublicStyleProjectionRuntime(
data: ResumeData,
projection: PublicStyleProjection,
): Promise<ResolvedResumeRuntime> {
if (!(await validatePublicStyleProjection(data, projection))) {
throw new Error("Public style projection does not match the resume render data");
}
const projectionData = dataForPublicProjection(data, projection.languageVersion);
const base = resolveResumeRuntime({
data: projectionData,
template: projectionData.metadata.template,
mode: "legacy",
});
return {
presentation: toResolvedPresentation(projection.nodes),
sourceTree: base.sourceTree,
renderTree: applyProjectedStructure(base.sourceTree, projection.nodes),
diagnostics: [],
};
}
+42 -6
View File
@@ -1,16 +1,52 @@
import { describe, expect, it } from "vitest";
import { inspectResumePdf } from "@reactive-resume/pdf/semantic";
import { resolveResumeRuntime } from "@reactive-resume/pdf/semantic";
import { sampleResumeData } from "@reactive-resume/schema/resume/sample";
describe("@reactive-resume/pdf/semantic", () => {
it("publicly exposes invalid applied-source diagnostics", () => {
it("falls back to base presentation and preserves fatal source diagnostics", () => {
const data = structuredClone(sampleResumeData);
const invalid = { languageVersion: 1, text: "@version 1; section { color: ; }" };
data.metadata.stylesheet = { mode: "semantic", source: invalid, applied: invalid };
data.metadata.stylesheet = { mode: "semantic", source: { languageVersion: 2, text: "@version 2;" } };
const inspection = inspectResumePdf({ data });
const inspection = resolveResumeRuntime({ data, template: data.metadata.template, mode: "semantic" });
expect(inspection.diagnostics).toContainEqual(expect.objectContaining({ severity: "error" }));
expect(inspection.diagnostics).toContainEqual(
expect.objectContaining({ code: "UNSUPPORTED_VERSION", severity: "error" }),
);
expect(inspection.presentation).toEqual({});
});
it("falls back to base presentation when a selector list exceeds the resource limit", () => {
const data = structuredClone(sampleResumeData);
const selectors = new Array(65).fill("section").join(",");
data.metadata.stylesheet = {
mode: "semantic",
source: { languageVersion: 1, text: `@version 1;\n${selectors} { color: red; }` },
};
const inspection = resolveResumeRuntime({ data, template: data.metadata.template, mode: "semantic" });
expect(inspection.diagnostics).toContainEqual(
expect.objectContaining({ code: "RESOURCE_LIMIT", severity: "error" }),
);
expect(inspection.presentation).toEqual({});
expect(inspection.renderTree).toEqual(inspection.sourceTree);
});
it("keeps valid PDF presentation when a neighboring value is recoverable", () => {
const data = structuredClone(sampleResumeData);
data.metadata.stylesheet = {
mode: "semantic",
source: {
languageVersion: 1,
text: "@version 1; name { color: #123456; opacity: var(--missing); }",
},
};
const inspection = resolveResumeRuntime({ data, template: data.metadata.template, mode: "semantic" });
expect(inspection.presentation["page-1/region-header/header/name"]?.style?.color).toBe("#123456");
expect(inspection.diagnostics).toContainEqual(
expect.objectContaining({ code: "UNRESOLVED_VARIABLE", severity: "error" }),
);
});
});
+7 -7
View File
@@ -3,7 +3,7 @@ import type { ResumeData } from "@reactive-resume/schema/resume/data";
import type { StylesheetMode, StylesheetSource } from "@reactive-resume/schema/resume/stylesheet";
import type { Template } from "@reactive-resume/schema/templates";
import type { ResolvedResumePresentation } from "./context";
import { compileStylesheet, resolveStylesheet } from "@reactive-resume/resume/stylesheet";
import { compileStylesheet, isFatalStylesheetDiagnostic, resolveStylesheet } from "@reactive-resume/resume/stylesheet";
import { EMPTY_SEMANTIC_CSS_SOURCE } from "@reactive-resume/schema/resume/stylesheet";
import { shouldShowResumeHeader } from "../templates/shared/cover-letter";
import { getTemplatePageSize } from "../templates/shared/page-size";
@@ -17,7 +17,7 @@ import { buildSemanticTree } from "./tree";
export type ResolveResumePresentationInput = {
data: ResumeData;
template: Template;
applied?: StylesheetSource;
source?: StylesheetSource;
mode: StylesheetMode;
};
@@ -85,7 +85,7 @@ export function resolveStylesheetMode(data: ResumeData): StylesheetMode {
export function resolveResumeRuntime({
data,
template,
applied,
source,
mode,
}: ResolveResumePresentationInput): ResolvedResumeRuntime {
const sourceTree = mergeAuthoredPageTrees(data, template);
@@ -93,12 +93,12 @@ export function resolveResumeRuntime({
return { presentation: EMPTY_PRESENTATION, sourceTree, renderTree: sourceTree, diagnostics: [] };
}
const source = applied ??
data.metadata.stylesheet?.applied ?? {
const stylesheetSource = source ??
data.metadata.stylesheet?.source ?? {
languageVersion: 1,
text: EMPTY_SEMANTIC_CSS_SOURCE,
};
const compiled = compileStylesheet(source);
const compiled = compileStylesheet(stylesheetSource);
if (!compiled.program) {
return { presentation: EMPTY_PRESENTATION, sourceTree, renderTree: sourceTree, diagnostics: compiled.diagnostics };
}
@@ -123,7 +123,7 @@ export function resolveResumeRuntime({
pages: authoredPageDimensions(data),
aliases,
});
if (resolved.diagnostics.some(({ severity }) => severity === "error")) {
if (resolved.diagnostics.some(isFatalStylesheetDiagnostic)) {
return {
presentation: EMPTY_PRESENTATION,
sourceTree,
@@ -47,7 +47,7 @@ const findSemanticNode = (
const buildFixture = (): ResumeData => {
const data = structuredClone(defaultResumeData);
const applied = {
const source = {
languageVersion: 1,
text: `
@version 1;
@@ -60,7 +60,7 @@ const buildFixture = (): ResumeData => {
data.basics.name = "Ada Lovelace";
data.summary.content = "<p>First <strong>bold</strong></p><ul><li>Item</li></ul>";
data.metadata.layout.pages = [{ fullWidth: true, main: ["summary"], sidebar: [] }];
data.metadata.stylesheet = { mode: "semantic", source: applied, applied };
data.metadata.stylesheet = { mode: "semantic", source };
return data;
};
@@ -39,7 +39,7 @@ const findFirst = (node: HostNode, predicate: (candidate: HostNode) => boolean):
const buildFixture = (): ResumeData => {
const data = structuredClone(defaultResumeData);
const applied = {
const source = {
languageVersion: 1,
text: `
@version 1;
@@ -50,15 +50,15 @@ const buildFixture = (): ResumeData => {
data.picture.hidden = true;
data.basics.name = "Ada Lovelace";
data.metadata.layout.pages = [{ fullWidth: true, main: [], sidebar: [] }];
data.metadata.stylesheet = { mode: "semantic", source: applied, applied };
data.metadata.stylesheet = { mode: "semantic", source };
return data;
};
const buildNodeBudgetFixture = (mode: "legacy" | "semantic"): ResumeData => {
const data = structuredClone(defaultResumeData);
const applied = { languageVersion: 1, text: "@version 1;\n" };
const source = { languageVersion: 1, text: "@version 1;\n" };
data.metadata.layout.pages = [{ fullWidth: true, main: ["skills"], sidebar: [] }];
data.metadata.stylesheet = { mode, source: applied, applied };
data.metadata.stylesheet = { mode, source };
data.sections.skills.items = Array.from({ length: 2_000 }, (_, index) => ({
id: `skill-${index}`,
hidden: false,
@@ -72,6 +72,13 @@ const buildNodeBudgetFixture = (mode: "legacy" | "semantic"): ResumeData => {
return data;
};
const buildFatalSourceFixture = (): ResumeData => {
const data = structuredClone(defaultResumeData);
data.metadata.layout.pages = [{ fullWidth: true, main: [], sidebar: [] }];
data.metadata.stylesheet = { mode: "semantic", source: { languageVersion: 2, text: "@version 2;" } };
return data;
};
const renderFinalProps = async (element: unknown) => {
const renderer = await vi.importActual<typeof import("@react-pdf/renderer")>("@react-pdf/renderer");
const instance = renderer.pdf(element as Parameters<typeof renderer.pdf>[0]);
@@ -100,28 +107,15 @@ describe("browser/server semantic runtime identity", () => {
expect(browserProps.fixed).toMatchObject({ type: "VIEW", fixed: true });
}, 30_000);
it("rejects a valid stylesheet when later content exceeds the Semantic CSS node budget", async () => {
const data = buildNodeBudgetFixture("semantic");
it("renders browser and server PDFs with base styles when the stylesheet is fatal", async () => {
const data = buildFatalSourceFixture();
const results = await Promise.allSettled([
createResumePdfBlob({ data, template: "onyx" }),
createResumePdfFile({ data, filename: "resume.pdf", template: "onyx" }),
]);
const blob = await createResumePdfBlob({ data, template: "onyx" });
const file = await createResumePdfFile({ data, filename: "resume.pdf", template: "onyx" });
expect(results).toEqual([
expect.objectContaining({
status: "rejected",
reason: expect.objectContaining({
cause: expect.arrayContaining([expect.objectContaining({ code: "RESOURCE_LIMIT", severity: "error" })]),
}),
}),
expect.objectContaining({
status: "rejected",
reason: expect.objectContaining({
cause: expect.arrayContaining([expect.objectContaining({ code: "RESOURCE_LIMIT", severity: "error" })]),
}),
}),
]);
expect(blob.type).toBe("application/pdf");
expect(file.type).toBe("application/pdf");
expect(await renderFinalProps(captured.browser)).toEqual(await renderFinalProps(captured.server));
}, 15_000);
it("keeps legacy PDF rendering unaffected by the semantic node budget", async () => {
@@ -49,7 +49,7 @@ const semanticFixture = (rule: string): ResumeData => {
customFields: [],
};
const stylesheet = { languageVersion: 1, text: `@version 1; ${rule}` };
data.metadata.stylesheet = { mode: "semantic", source: stylesheet, applied: stylesheet };
data.metadata.stylesheet = { mode: "semantic", source: stylesheet };
return data;
};
@@ -10,7 +10,6 @@ import { createBindingInventory } from "./binding-inventory";
import {
getTemplateSemanticBindingRegistry,
getTemplateSemanticManifest,
getTemplateSemanticRegistryFingerprintInput,
validateTemplateSemanticManifest,
} from "./template-manifest";
import { buildSemanticTree } from "./tree";
@@ -423,8 +422,6 @@ const EXPECTED_LAYOUT = {
} as const satisfies Readonly<Record<Template, Omit<TemplateSemanticManifest, "template" | "parts">>>;
const flattenTree = (node: SemanticNode): SemanticNode[] => [node, ...node.children.flatMap(flattenTree)];
const flattenValues = (value: unknown): unknown[] =>
typeof value === "object" && value !== null ? [value, ...Object.values(value).flatMap(flattenValues)] : [value];
const findNodes = (node: SemanticNode, predicate: (candidate: SemanticNode) => boolean): SemanticNode[] =>
flattenTree(node).filter(predicate);
const findPart = (node: SemanticNode, name: string): SemanticNode | undefined =>
@@ -488,7 +485,8 @@ describe("template semantic manifests", () => {
});
it("registers child kinds for every primitive template part", () => {
for (const manifest of Object.values(getTemplateSemanticRegistryFingerprintInput())) {
for (const template of templateSchema.options) {
const manifest = getTemplateSemanticManifest(template);
for (const part of manifest.parts) {
if (part.binding.type === "alias") continue;
expect(TEMPLATE_PART_CHILD_KINDS_V1, `${manifest.template}:${part.name}`).toHaveProperty(part.name);
@@ -1315,22 +1313,6 @@ describe("template semantic manifests", () => {
},
);
it("publishes stable, deterministic, deeply frozen fingerprint input without functions", () => {
const first = getTemplateSemanticRegistryFingerprintInput();
const second = getTemplateSemanticRegistryFingerprintInput();
const serialized = JSON.stringify(first);
expect(second).toBe(first);
expect(JSON.stringify(second)).toBe(serialized);
expect(Object.isFrozen(first)).toBe(true);
expect(Object.isFrozen(first.azurill.parts)).toBe(true);
expect(flattenValues(first).every((value) => typeof value !== "function")).toBe(true);
expect(() => {
(first.azurill.parts as unknown as object[]).pop();
}).toThrow();
expect(JSON.stringify(getTemplateSemanticRegistryFingerprintInput())).toBe(serialized);
});
it.each(templateSchema.options)(
"%s binds every manifest node to existing chrome without synthetic wrappers",
(template) => {
@@ -334,10 +334,6 @@ export function getTemplateSemanticManifest(template: Template): TemplateSemanti
return TEMPLATE_SEMANTIC_MANIFESTS[template];
}
export function getTemplateSemanticRegistryFingerprintInput(): Readonly<Record<Template, TemplateSemanticManifest>> {
return TEMPLATE_SEMANTIC_MANIFESTS;
}
export function getTemplateSemanticBindingRegistry(template: Template): SemanticBindingRegistry {
const manifest = getTemplateSemanticManifest(template);
const canonicalBindings = Object.fromEntries(
-1
View File
@@ -998,6 +998,5 @@ export { semanticNodeKeys } from "./node-keys";
export {
getTemplateSemanticBindingRegistry,
getTemplateSemanticManifest,
getTemplateSemanticRegistryFingerprintInput,
validateTemplateSemanticManifest,
} from "./template-manifest";