feat: add semantic CSS stylesheets (#3274)

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
Amruth Pillai
2026-07-30 12:39:15 +02:00
committed by GitHub
co-authored by Cursor Agent
parent 4ac19f81b3
commit d2ffbf9618
320 changed files with 78393 additions and 2915 deletions
+186
View File
@@ -1,13 +1,18 @@
import type { CustomSectionType } from "./data";
import { describe, expect, it } from "vitest";
import {
baseSectionSchema,
basicsSchema,
customFieldSchema,
customSectionItemDefinitionByType,
customSectionSchema,
experienceItemSchema,
layoutSchema,
pageSchema,
parseResumeData,
pictureSchema,
resumeDataSchema,
sectionTypeSchema,
skillItemSchema,
styleRuleSchema,
styleRulesSchema,
@@ -16,6 +21,86 @@ import {
} from "./data";
import { defaultResumeData } from "./default";
const representativeCustomSectionItemByType = {
summary: { id: "summary-item", hidden: false, content: "<p>Summary</p>" },
profiles: { id: "profile-item", hidden: false, icon: "", network: "GitHub", username: "ada" },
experience: {
id: "experience-item",
hidden: false,
company: "Analytical Engines",
position: "Programmer",
location: "London",
period: "18421843",
description: "<p>Wrote the first algorithm.</p>",
},
education: {
id: "education-item",
hidden: false,
school: "University of London",
degree: "Mathematics",
area: "Mathematics",
grade: "",
location: "London",
period: "1830",
description: "",
},
projects: { id: "project-item", hidden: false, name: "Bernoulli Notes", period: "1843", description: "" },
skills: { id: "skill-item", hidden: false, icon: "", name: "Mathematics", proficiency: "Expert" },
languages: { id: "language-item", hidden: false, language: "English", fluency: "Native" },
interests: { id: "interest-item", hidden: false, icon: "", name: "Poetry" },
awards: { id: "award-item", hidden: false, title: "Medal", awarder: "Society", date: "1843", description: "" },
certifications: {
id: "certification-item",
hidden: false,
title: "Certificate",
issuer: "Society",
date: "1843",
description: "",
},
publications: {
id: "publication-item",
hidden: false,
title: "Notes",
publisher: "Scientific Memoirs",
date: "1843",
description: "",
},
volunteer: {
id: "volunteer-item",
hidden: false,
organization: "Society",
location: "London",
period: "1843",
description: "",
},
references: {
id: "reference-item",
hidden: false,
name: "Charles Babbage",
position: "Inventor",
phone: "",
description: "",
},
"cover-letter": {
id: "cover-letter-item",
hidden: false,
recipient: "<p>Charles Babbage</p>",
content: "<p>Dear Charles,</p>",
},
} as const satisfies Record<CustomSectionType, Record<string, unknown>>;
const customSectionFixture = (type: CustomSectionType, item: Record<string, unknown>) => ({
id: `custom-${type}`,
type,
title: "Custom section",
icon: "",
columns: 1,
hidden: false,
keepTogether: false,
startOnNewPage: false,
items: [item],
});
describe("resumeDataSchema", () => {
it("validates the default resume", () => {
expect(resumeDataSchema.safeParse(defaultResumeData).success).toBe(true);
@@ -29,6 +114,107 @@ describe("resumeDataSchema", () => {
const partial = { ...defaultResumeData, basics: undefined };
expect(resumeDataSchema.safeParse(partial).success).toBe(false);
});
it("preserves cover-letter fields when parsing the overlapping content shape", () => {
const result = customSectionSchema.parse({
id: "cover-letter",
type: "cover-letter",
title: "Cover Letter",
icon: "",
columns: 1,
hidden: false,
keepTogether: false,
startOnNewPage: false,
items: [{ id: "item", hidden: false, recipient: "Ada Lovelace", content: "<p>Hello</p>" }],
});
expect(result.items[0]).toMatchObject({ recipient: "Ada Lovelace", content: "<p>Hello</p>" });
});
it("accepts overlapping item fields when the selected renderer requirements are satisfied", () => {
const section = customSectionFixture("experience", {
...representativeCustomSectionItemByType.experience,
content: "<p>Also valid summary content</p>",
});
expect(customSectionSchema.parse(section).items[0]).toMatchObject({
content: "<p>Also valid summary content</p>",
roles: [],
website: { url: "", label: "", inlineLink: false },
});
});
it("rejects a renderer-unsafe custom section before PDF or DOCX dispatch", () => {
const rendererUnsafeSection = customSectionFixture("experience", representativeCustomSectionItemByType.summary);
expect(customSectionSchema.safeParse(rendererUnsafeSection).success).toBe(false);
expect(
resumeDataSchema.safeParse({
...defaultResumeData,
customSections: [rendererUnsafeSection],
}).success,
).toBe(false);
});
it("returns renderer-safe normalized data without losing compatible overlapping fields", () => {
const data = {
...structuredClone(defaultResumeData),
customSections: [
customSectionFixture("experience", {
...representativeCustomSectionItemByType.experience,
content: "<p>Preserve this overlapping field</p>",
}),
],
};
const parsed = parseResumeData(data);
expect(parsed.customSections[0]?.items[0]).toMatchObject({
content: "<p>Preserve this overlapping field</p>",
roles: [],
website: { url: "", label: "", inlineLink: false },
});
});
});
describe("customSectionItemDefinitionByType", () => {
it("maps every custom section type to its named item schema", () => {
expect(
Object.fromEntries(
sectionTypeSchema.options.map((type) => [type, customSectionItemDefinitionByType[type].schemaName]),
),
).toEqual({
summary: "summaryItemSchema",
profiles: "profileItemSchema",
experience: "experienceItemSchema",
education: "educationItemSchema",
projects: "projectItemSchema",
skills: "skillItemSchema",
languages: "languageItemSchema",
interests: "interestItemSchema",
awards: "awardItemSchema",
certifications: "certificationItemSchema",
publications: "publicationItemSchema",
volunteer: "volunteerItemSchema",
references: "referenceItemSchema",
"cover-letter": "coverLetterItemSchema",
});
});
it.each(sectionTypeSchema.options)("accepts the representative %s item shape", (type) => {
const section = customSectionFixture(type, representativeCustomSectionItemByType[type]);
expect(customSectionSchema.safeParse(section).success).toBe(true);
});
it.each(sectionTypeSchema.options)("rejects an item shape that does not match %s", (type) => {
const mismatchedItem =
type === "summary"
? representativeCustomSectionItemByType.experience
: representativeCustomSectionItemByType.summary;
const section = customSectionFixture(type, mismatchedItem);
expect(customSectionSchema.safeParse(section).success).toBe(false);
});
});
describe("websiteSchema", () => {
+44 -30
View File
@@ -1,5 +1,6 @@
import z from "zod";
import { templateSchema } from "../templates";
import { semanticStylesheetSchema } from "./stylesheet";
const iconSchema = z
.string()
@@ -358,38 +359,47 @@ export const sectionTypeSchema = z.enum([
export type CustomSectionType = z.infer<typeof sectionTypeSchema>;
const customSectionItemSchema = z.union([
// coverLetterItemSchema must come before summaryItemSchema because both have 'content',
// but coverLetterItemSchema also requires 'recipient'. If summaryItemSchema is first,
// cover letter items will match it and lose the 'recipient' field.
coverLetterItemSchema,
summaryItemSchema,
profileItemSchema,
experienceItemSchema,
educationItemSchema,
projectItemSchema,
skillItemSchema,
languageItemSchema,
interestItemSchema,
awardItemSchema,
certificationItemSchema,
publicationItemSchema,
volunteerItemSchema,
referenceItemSchema,
// Correlation protects renderer requirements; it does not make otherwise-overlapping item shapes exclusive.
// Keep cover-letter before summary so the overlapping content shapes retain their established precedence.
export const customSectionItemDefinitionByType = {
"cover-letter": { schemaName: "coverLetterItemSchema", schema: coverLetterItemSchema.catchall(z.any()) },
summary: { schemaName: "summaryItemSchema", schema: summaryItemSchema.catchall(z.any()) },
profiles: { schemaName: "profileItemSchema", schema: profileItemSchema.catchall(z.any()) },
experience: { schemaName: "experienceItemSchema", schema: experienceItemSchema.catchall(z.any()) },
education: { schemaName: "educationItemSchema", schema: educationItemSchema.catchall(z.any()) },
projects: { schemaName: "projectItemSchema", schema: projectItemSchema.catchall(z.any()) },
skills: { schemaName: "skillItemSchema", schema: skillItemSchema.catchall(z.any()) },
languages: { schemaName: "languageItemSchema", schema: languageItemSchema.catchall(z.any()) },
interests: { schemaName: "interestItemSchema", schema: interestItemSchema.catchall(z.any()) },
awards: { schemaName: "awardItemSchema", schema: awardItemSchema.catchall(z.any()) },
certifications: { schemaName: "certificationItemSchema", schema: certificationItemSchema.catchall(z.any()) },
publications: { schemaName: "publicationItemSchema", schema: publicationItemSchema.catchall(z.any()) },
volunteer: { schemaName: "volunteerItemSchema", schema: volunteerItemSchema.catchall(z.any()) },
references: { schemaName: "referenceItemSchema", schema: referenceItemSchema.catchall(z.any()) },
} as const satisfies Record<CustomSectionType, { schemaName: string; schema: z.ZodType }>;
export type CustomSectionItem = z.infer<(typeof customSectionItemDefinitionByType)[CustomSectionType]["schema"]>;
const customSectionSchemaOptions = Object.entries(customSectionItemDefinitionByType).map(([type, { schema }]) =>
baseSectionSchema.extend({
id: z.string().describe("The unique identifier for the custom section. Usually generated as a UUID."),
type: z
.literal(type as CustomSectionType)
.describe("The type of items this custom section contains. Determines which item schema and form fields to use."),
items: z
.array(schema)
.describe("The items to display in the custom section. Items follow the schema of the section type."),
}),
);
const [firstCustomSectionSchema, ...remainingCustomSectionSchemas] = customSectionSchemaOptions;
if (!firstCustomSectionSchema) throw new Error("At least one custom section schema is required.");
export const customSectionSchema = z.discriminatedUnion("type", [
firstCustomSectionSchema,
...remainingCustomSectionSchemas,
]);
export type CustomSectionItem = z.infer<typeof customSectionItemSchema>;
export const customSectionSchema = baseSectionSchema.extend({
id: z.string().describe("The unique identifier for the custom section. Usually generated as a UUID."),
type: sectionTypeSchema.describe(
"The type of items this custom section contains. Determines which item schema and form fields to use.",
),
items: z
.array(customSectionItemSchema)
.describe("The items to display in the custom section. Items follow the schema of the section type."),
});
export type CustomSection = z.infer<typeof customSectionSchema>;
const customSectionsSchema = z.array(customSectionSchema);
@@ -638,6 +648,7 @@ export const metadataSchema = z.object({
styleRules: styleRulesSchema.describe(
"Structured style rules that target semantic resume sections and slots for React PDF rendering.",
),
stylesheet: semanticStylesheetSchema.optional(),
});
export const resumeDataSchema = z.looseObject({
@@ -656,6 +667,9 @@ export const resumeDataSchema = z.looseObject({
});
export type ResumeData = z.infer<typeof resumeDataSchema>;
export const parseResumeData = (data: unknown): ResumeData => resumeDataSchema.parse(data);
export type LayoutPage = z.infer<typeof pageLayoutSchema>;
export type Typography = z.infer<typeof typographySchema>;
export type Design = z.infer<typeof designSchema>;
@@ -0,0 +1,72 @@
import { describe, expect, it } from "vitest";
import z from "zod";
import { sectionTypeSchema } from "./data";
import { defaultResumeData } from "./default";
import { createCustomSectionItemJsonSchemas, createResumeDataJsonSchema } from "./json-schema";
describe("createResumeDataJsonSchema", () => {
it("describes accepted ResumeData input even when the Zod schema contains transforms", () => {
const schema = createResumeDataJsonSchema();
expect(schema).toMatchObject({
$schema: "https://json-schema.org/draft/2020-12/schema",
type: "object",
required: ["picture", "basics", "summary", "sections", "customSections", "metadata"],
properties: {
picture: { type: "object" },
basics: { type: "object" },
sections: { type: "object" },
metadata: { type: "object" },
},
});
});
it("enforces the custom-section type and item correlation", () => {
const generatedSchema = z.fromJSONSchema(createResumeDataJsonSchema());
const valid = {
...defaultResumeData,
customSections: [
{
id: "custom-summary",
type: "summary",
title: "Summary",
icon: "",
columns: 1,
hidden: false,
keepTogether: false,
startOnNewPage: false,
items: [{ id: "summary-item", hidden: false, content: "<p>Summary</p>" }],
},
],
};
const mismatched = {
...valid,
customSections: valid.customSections.map((section) => ({ ...section, type: "experience" })),
};
expect(generatedSchema.safeParse(valid).success).toBe(true);
expect(generatedSchema.safeParse(mismatched).success).toBe(false);
});
});
describe("createCustomSectionItemJsonSchemas", () => {
it("emits every type-keyed item schema with representative required shapes", () => {
const schemas = createCustomSectionItemJsonSchemas();
expect(Object.keys(schemas)).toEqual(sectionTypeSchema.options);
expect(schemas.summary).toMatchObject({
schemaName: "summaryItemSchema",
schema: { required: ["id", "hidden", "content"] },
});
expect(schemas.experience).toMatchObject({
schemaName: "experienceItemSchema",
schema: {
required: ["id", "hidden", "company", "position", "location", "period", "description"],
},
});
expect(schemas["cover-letter"]).toMatchObject({
schemaName: "coverLetterItemSchema",
schema: { required: ["id", "hidden", "recipient", "content"] },
});
});
});
+28
View File
@@ -0,0 +1,28 @@
import type { CustomSectionType } from "./data";
import z from "zod";
import { customSectionItemDefinitionByType, resumeDataSchema, sectionTypeSchema } from "./data";
const toInputJsonSchema = (schema: z.ZodType) =>
z.toJSONSchema(schema, {
io: "input",
unrepresentable: "any",
});
export function createResumeDataJsonSchema() {
return toInputJsonSchema(resumeDataSchema);
}
export function createCustomSectionItemJsonSchemas() {
return Object.fromEntries(
sectionTypeSchema.options.map((type) => {
const { schemaName, schema } = customSectionItemDefinitionByType[type];
return [type, { schemaName, schema: toInputJsonSchema(schema) }];
}),
) as Record<
CustomSectionType,
{
schemaName: (typeof customSectionItemDefinitionByType)[CustomSectionType]["schemaName"];
schema: ReturnType<typeof toInputJsonSchema>;
}
>;
}
@@ -0,0 +1,46 @@
import { describe, expect, it } from "vitest";
import { resumeDataSchema } from "./data";
import { defaultResumeData } from "./default";
import { semanticStylesheetSchema, stylesheetSourceSchema } from "./stylesheet";
describe("semanticStylesheetSchema", () => {
it("preserves separate editable and applied sources", () => {
const result = semanticStylesheetSchema.parse({
mode: "semantic",
source: { languageVersion: 1, text: "@version 1;\nsection {" },
applied: { languageVersion: 1, text: "@version 1;\nsection { color: red; }\n" },
});
expect(result.source.text).toContain("section {");
expect(result.applied.text).toContain("color: red");
});
it("keeps resumes without a stylesheet valid for legacy rendering", () => {
expect(resumeDataSchema.parse(defaultResumeData).metadata.stylesheet).toBeUndefined();
});
it("rejects non-positive language versions", () => {
expect(
semanticStylesheetSchema.safeParse({
mode: "semantic",
source: { languageVersion: 0, text: "" },
applied: { languageVersion: 1, text: "" },
}).success,
).toBe(false);
});
it("rejects unknown stylesheet fields", () => {
expect(
semanticStylesheetSchema.safeParse({
mode: "semantic",
source: { languageVersion: 1, text: "" },
applied: { languageVersion: 1, text: "" },
unknown: true,
}).success,
).toBe(false);
});
it("rejects unknown stylesheet source fields", () => {
expect(stylesheetSourceSchema.safeParse({ languageVersion: 1, text: "", unknown: true }).success).toBe(false);
});
});
+18
View File
@@ -0,0 +1,18 @@
import { z } from "zod";
export const EMPTY_SEMANTIC_CSS_SOURCE = "@version 1;\n";
export const stylesheetSourceSchema = z.strictObject({
languageVersion: z.number().int().positive(),
text: z.string(),
});
export const semanticStylesheetSchema = z.strictObject({
mode: z.enum(["legacy", "semantic"]),
source: stylesheetSourceSchema,
applied: stylesheetSourceSchema,
});
export type StylesheetSource = z.infer<typeof stylesheetSourceSchema>;
export type SemanticStylesheet = z.infer<typeof semanticStylesheetSchema>;
export type StylesheetMode = SemanticStylesheet["mode"];