mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-08-23 23:02:17 +10:00
feat: add semantic CSS stylesheets (#3274)
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor Agent
parent
4ac19f81b3
commit
d2ffbf9618
@@ -0,0 +1,237 @@
|
||||
import type { StylesheetPreflightRunner } from "@reactive-resume/pdf/server";
|
||||
import type { CompileStylesheetResult } from "@reactive-resume/resume/stylesheet";
|
||||
import type { ResumeData } from "@reactive-resume/schema/resume/data";
|
||||
import type { SemanticStylesheet, StylesheetSource } from "@reactive-resume/schema/resume/stylesheet";
|
||||
import type { StylesheetSnapshot } from "./stylesheet-service";
|
||||
import { ORPCError } from "@orpc/client";
|
||||
import { compileStylesheet } from "@reactive-resume/resume/stylesheet";
|
||||
import { EMPTY_SEMANTIC_CSS_SOURCE } from "@reactive-resume/schema/resume/stylesheet";
|
||||
import { templateSchema } from "@reactive-resume/schema/templates";
|
||||
import { recordSemanticCssEvent } from "./stylesheet-observability";
|
||||
|
||||
type PrepareImportedResumeDataInput = {
|
||||
data: ResumeData;
|
||||
resumeId: string;
|
||||
revision: number;
|
||||
runner?: StylesheetPreflightRunner | undefined;
|
||||
};
|
||||
|
||||
type ValidateHistoricalStylesheetInput = {
|
||||
data: ResumeData;
|
||||
resumeId: string;
|
||||
revision: number;
|
||||
stylesheet: SemanticStylesheet;
|
||||
runner?: StylesheetPreflightRunner | undefined;
|
||||
};
|
||||
|
||||
const byteCount = (source: StylesheetSource): number => new TextEncoder().encode(source.text).byteLength;
|
||||
|
||||
const observeCompile = (
|
||||
input: Pick<PrepareImportedResumeDataInput, "data" | "resumeId" | "revision">,
|
||||
source: StylesheetSource,
|
||||
): CompileStylesheetResult => {
|
||||
const startedAt = performance.now();
|
||||
try {
|
||||
const result = compileStylesheet(source);
|
||||
recordSemanticCssEvent({
|
||||
name: "semantic_css.compile",
|
||||
resumeId: input.resumeId,
|
||||
durationMs: performance.now() - startedAt,
|
||||
languageVersion: source.languageVersion,
|
||||
sourceBytes: byteCount(source),
|
||||
template: input.data.metadata.template,
|
||||
diagnosticCodes: result.diagnostics.map(({ code }) => code),
|
||||
pageCount: null,
|
||||
revision: input.revision,
|
||||
success: result.program !== null,
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
recordSemanticCssEvent({
|
||||
name: "semantic_css.compile",
|
||||
resumeId: input.resumeId,
|
||||
durationMs: performance.now() - startedAt,
|
||||
languageVersion: source.languageVersion,
|
||||
sourceBytes: byteCount(source),
|
||||
template: input.data.metadata.template,
|
||||
diagnosticCodes: [],
|
||||
pageCount: null,
|
||||
revision: input.revision,
|
||||
success: false,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const unavailableError = () =>
|
||||
new ORPCError("SEMANTIC_STYLESHEET_UNAVAILABLE", {
|
||||
status: 503,
|
||||
message: "Semantic stylesheet PDF preflight is unavailable.",
|
||||
});
|
||||
|
||||
const validationError = (message: string) =>
|
||||
new ORPCError("STYLESHEET_VALIDATION_FAILED", {
|
||||
status: 400,
|
||||
message,
|
||||
});
|
||||
|
||||
const runPreflight = async (
|
||||
input: Pick<PrepareImportedResumeDataInput, "data" | "resumeId" | "revision" | "runner">,
|
||||
stylesheet: StylesheetSource,
|
||||
) => {
|
||||
const startedAt = performance.now();
|
||||
try {
|
||||
if (!input.runner) throw unavailableError();
|
||||
const result = await input.runner.run({
|
||||
data: input.data,
|
||||
template: input.data.metadata.template,
|
||||
stylesheet,
|
||||
});
|
||||
recordSemanticCssEvent({
|
||||
name: "semantic_css.preflight",
|
||||
resumeId: input.resumeId,
|
||||
durationMs: performance.now() - startedAt,
|
||||
languageVersion: stylesheet.languageVersion,
|
||||
sourceBytes: byteCount(stylesheet),
|
||||
template: input.data.metadata.template,
|
||||
diagnosticCodes: result.diagnostics.map(({ code }) => code),
|
||||
pageCount: result.ok ? result.pageCount : null,
|
||||
revision: input.revision,
|
||||
success: result.ok,
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
recordSemanticCssEvent({
|
||||
name: "semantic_css.preflight",
|
||||
resumeId: input.resumeId,
|
||||
durationMs: performance.now() - startedAt,
|
||||
languageVersion: stylesheet.languageVersion,
|
||||
sourceBytes: byteCount(stylesheet),
|
||||
template: input.data.metadata.template,
|
||||
diagnosticCodes: [],
|
||||
pageCount: null,
|
||||
revision: input.revision,
|
||||
success: false,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const validApplied = async (input: PrepareImportedResumeDataInput, stylesheet: StylesheetSource): Promise<boolean> => {
|
||||
const compiled = observeCompile(input, stylesheet);
|
||||
return compiled.program !== null && (await runPreflight(input, stylesheet)).ok;
|
||||
};
|
||||
|
||||
export async function prepareImportedResumeData(input: PrepareImportedResumeDataInput): Promise<ResumeData> {
|
||||
const stylesheet = input.data.metadata.stylesheet;
|
||||
if (stylesheet?.mode !== "semantic") return input.data;
|
||||
|
||||
let applied = stylesheet.source;
|
||||
if (!(await validApplied(input, stylesheet.source))) {
|
||||
applied = stylesheet.applied;
|
||||
if (!(await validApplied(input, stylesheet.applied))) {
|
||||
applied = { languageVersion: 1, text: EMPTY_SEMANTIC_CSS_SOURCE };
|
||||
if (!(await validApplied(input, applied))) {
|
||||
throw validationError("The empty stylesheet failed PDF preflight.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...input.data,
|
||||
metadata: {
|
||||
...input.data.metadata,
|
||||
stylesheet: { ...stylesheet, applied },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function validateHistoricalStylesheet(
|
||||
input: ValidateHistoricalStylesheetInput,
|
||||
): Promise<SemanticStylesheet> {
|
||||
const compiled = observeCompile(input, input.stylesheet.applied);
|
||||
if (!compiled.program) throw validationError("The historical applied stylesheet is invalid.");
|
||||
if (!(await runPreflight(input, input.stylesheet.applied)).ok) {
|
||||
throw validationError("The historical applied stylesheet failed PDF preflight.");
|
||||
}
|
||||
return input.stylesheet;
|
||||
}
|
||||
|
||||
export async function convertLegacyStylesheet(snapshot: StylesheetSnapshot): Promise<StylesheetSource> {
|
||||
const startedAt = performance.now();
|
||||
try {
|
||||
const { convertLegacyStyleRules } = await import("@reactive-resume/pdf/semantic");
|
||||
const source = convertLegacyStyleRules(snapshot.data).source;
|
||||
recordSemanticCssEvent({
|
||||
name: "semantic_css.convert_legacy",
|
||||
resumeId: snapshot.id,
|
||||
durationMs: performance.now() - startedAt,
|
||||
languageVersion: source.languageVersion,
|
||||
sourceBytes: byteCount(source),
|
||||
template: snapshot.data.metadata.template,
|
||||
diagnosticCodes: [],
|
||||
pageCount: null,
|
||||
revision: snapshot.stylesheetRevision,
|
||||
success: true,
|
||||
});
|
||||
return source;
|
||||
} catch (error) {
|
||||
recordSemanticCssEvent({
|
||||
name: "semantic_css.convert_legacy",
|
||||
resumeId: snapshot.id,
|
||||
durationMs: performance.now() - startedAt,
|
||||
languageVersion: 1,
|
||||
sourceBytes: 0,
|
||||
template: snapshot.data.metadata.template,
|
||||
diagnosticCodes: [],
|
||||
pageCount: null,
|
||||
revision: snapshot.stylesheetRevision,
|
||||
success: false,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkLegacyStylesheetParity(input: {
|
||||
data: ResumeData;
|
||||
stylesheet: StylesheetSource;
|
||||
resumeId: string;
|
||||
revision: number;
|
||||
}): Promise<{ mismatches: readonly string[] }> {
|
||||
const startedAt = performance.now();
|
||||
try {
|
||||
const { compareLegacySemanticPresentation } = await import("@reactive-resume/pdf/semantic");
|
||||
const result = await compareLegacySemanticPresentation({
|
||||
data: input.data,
|
||||
convertedSource: input.stylesheet,
|
||||
templates: templateSchema.options,
|
||||
});
|
||||
recordSemanticCssEvent({
|
||||
name: "semantic_css.parity_check",
|
||||
resumeId: input.resumeId,
|
||||
durationMs: performance.now() - startedAt,
|
||||
languageVersion: input.stylesheet.languageVersion,
|
||||
sourceBytes: byteCount(input.stylesheet),
|
||||
template: input.data.metadata.template,
|
||||
diagnosticCodes: [],
|
||||
pageCount: null,
|
||||
revision: input.revision,
|
||||
success: result.mismatches.length === 0,
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
recordSemanticCssEvent({
|
||||
name: "semantic_css.parity_check",
|
||||
resumeId: input.resumeId,
|
||||
durationMs: performance.now() - startedAt,
|
||||
languageVersion: input.stylesheet.languageVersion,
|
||||
sourceBytes: byteCount(input.stylesheet),
|
||||
template: input.data.metadata.template,
|
||||
diagnosticCodes: [],
|
||||
pageCount: null,
|
||||
revision: input.revision,
|
||||
success: false,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user