Files
Reactive-Resume/packages/pdf/src/semantic/resolve.ts
T
2026-07-30 12:39:15 +02:00

146 lines
4.9 KiB
TypeScript

import type { ResolveStylesheetResult, SemanticCssDiagnostic, SemanticNode } from "@reactive-resume/resume/stylesheet";
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 { EMPTY_SEMANTIC_CSS_SOURCE } from "@reactive-resume/schema/resume/stylesheet";
import { shouldShowResumeHeader } from "../templates/shared/cover-letter";
import { getTemplatePageSize } from "../templates/shared/page-size";
import { adaptResolvedPdfNode } from "./adapter";
import { buildPdfBaseStyles } from "./base-styles";
import { createBindingInventory } from "./binding-inventory";
import { semanticNodeKeys } from "./node-keys";
import { getTemplateSemanticBindingRegistry } from "./template-manifest";
import { buildSemanticTree } from "./tree";
export type ResolveResumePresentationInput = {
data: ResumeData;
template: Template;
applied?: StylesheetSource;
mode: StylesheetMode;
};
export type ResolvedResumeRuntime = {
presentation: ResolvedResumePresentation;
sourceTree: SemanticNode;
renderTree: SemanticNode;
diagnostics: readonly SemanticCssDiagnostic[];
};
const EMPTY_PRESENTATION = Object.freeze({}) satisfies ResolvedResumePresentation;
const mergeAuthoredPageTrees = (data: ResumeData, template: Template): SemanticNode => {
const pageTrees = data.metadata.layout.pages.map((page, index) =>
buildSemanticTree({
data,
template,
page,
pageNumber: index + 1,
showHeader: shouldShowResumeHeader(data, index),
}),
);
const root = pageTrees[0];
return {
key: semanticNodeKeys.resume(),
kind: "resume",
attributes: { template },
roles: [],
children: pageTrees.flatMap((tree) => tree.children),
...(root?.id === undefined ? {} : { id: root.id }),
};
};
const authoredPageDimensions = (data: ResumeData) => {
const size = getTemplatePageSize(data.metadata.page.format);
const dimensions =
size === "LETTER"
? { width: 612, height: 792 }
: typeof size === "object"
? { width: size.width, height: 841.89 }
: { width: 595.28, height: 841.89 };
return data.metadata.layout.pages.map((_page, index) => ({
pageKey: semanticNodeKeys.page(index + 1),
...dimensions,
}));
};
const toPresentation = (
resolved: ResolveStylesheetResult["nodes"],
base: ReturnType<typeof buildPdfBaseStyles>,
): ResolvedResumePresentation => {
return Object.freeze(
Object.fromEntries(
Object.entries(resolved).map(([nodeKey, node]) => [nodeKey, adaptResolvedPdfNode(node, base[nodeKey])]),
),
) as ResolvedResumePresentation;
};
export function resolveStylesheetMode(data: ResumeData): StylesheetMode {
return data.metadata.stylesheet?.mode ?? "legacy";
}
export function resolveResumeRuntime({
data,
template,
applied,
mode,
}: ResolveResumePresentationInput): ResolvedResumeRuntime {
const sourceTree = mergeAuthoredPageTrees(data, template);
if (mode !== "semantic") {
return { presentation: EMPTY_PRESENTATION, sourceTree, renderTree: sourceTree, diagnostics: [] };
}
const source = applied ??
data.metadata.stylesheet?.applied ?? {
languageVersion: 1,
text: EMPTY_SEMANTIC_CSS_SOURCE,
};
const compiled = compileStylesheet(source);
if (!compiled.program) {
return { presentation: EMPTY_PRESENTATION, sourceTree, renderTree: sourceTree, diagnostics: compiled.diagnostics };
}
const baseStyles = buildPdfBaseStyles({ data, template, tree: sourceTree });
const inventory = createBindingInventory(sourceTree, getTemplateSemanticBindingRegistry(template));
const aliases: Record<string, string[]> = {};
for (const [aliasKey, binding] of Object.entries(inventory.bindings)) {
if (binding.type !== "alias") continue;
aliases[binding.canonicalNodeKey] = [...(aliases[binding.canonicalNodeKey] ?? []), aliasKey];
}
const resolved = resolveStylesheet(compiled.program, sourceTree, {
baseStyles,
baseSettings: {
picture: data.picture,
template,
design: data.metadata.design,
typography: data.metadata.typography,
page: data.metadata.page,
layout: { sidebarWidth: data.metadata.layout.sidebarWidth },
},
pages: authoredPageDimensions(data),
aliases,
});
if (resolved.diagnostics.some(({ severity }) => severity === "error")) {
return {
presentation: EMPTY_PRESENTATION,
sourceTree,
renderTree: sourceTree,
diagnostics: [...compiled.diagnostics, ...resolved.diagnostics],
};
}
return {
presentation: toPresentation(resolved.nodes, baseStyles),
sourceTree,
renderTree: resolved.renderTree,
diagnostics: [...compiled.diagnostics, ...resolved.diagnostics],
};
}
export function resolveResumePresentation(input: ResolveResumePresentationInput): ResolvedResumePresentation {
return resolveResumeRuntime(input).presentation;
}