mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-08-25 15:52:21 +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,190 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { sectionTypeSchema } from "@reactive-resume/schema/resume/data";
|
||||
import { templateSchema } from "@reactive-resume/schema/templates";
|
||||
import * as semanticRegistry from "./semantic";
|
||||
import { canContainNode, SEMANTIC_NODE_KINDS, SEMANTIC_REGISTRY_V1, TEMPLATE_PART_CHILD_KINDS_V1 } from "./semantic";
|
||||
|
||||
const inlineParents = [
|
||||
"contact-item",
|
||||
"field",
|
||||
"link",
|
||||
"rich-heading",
|
||||
"blockquote",
|
||||
"paragraph",
|
||||
"list-item-content",
|
||||
"strong",
|
||||
"emphasis",
|
||||
"underline",
|
||||
"strike",
|
||||
"code",
|
||||
"text-span",
|
||||
"mark",
|
||||
];
|
||||
|
||||
const expectedParents = {
|
||||
resume: [],
|
||||
page: ["resume"],
|
||||
region: ["page"],
|
||||
header: ["region"],
|
||||
picture: ["header"],
|
||||
name: ["header"],
|
||||
headline: ["header"],
|
||||
"contact-list": ["header"],
|
||||
"contact-item": ["contact-list"],
|
||||
section: ["region"],
|
||||
"section-heading": ["section"],
|
||||
"section-items": ["section"],
|
||||
item: ["section-items", "item"],
|
||||
"item-header": ["item"],
|
||||
"combined-text": ["item-header", "template-part"],
|
||||
field: ["contact-item", "item", "item-header", "combined-text"],
|
||||
link: ["item", "item-header", "rich-text", "template-part", ...inlineParents],
|
||||
icon: ["contact-item", "section-heading", "item", "item-header", "level"],
|
||||
level: ["item", "item-header"],
|
||||
"rich-text": ["item", "field"],
|
||||
"rich-heading": ["rich-text"],
|
||||
blockquote: ["rich-text", "list-item-content"],
|
||||
paragraph: ["rich-text", "list-item-content"],
|
||||
list: ["rich-text", "list-item-content"],
|
||||
"list-item": ["list"],
|
||||
"list-item-content": ["list-item"],
|
||||
"list-marker": ["list-item"],
|
||||
strong: inlineParents,
|
||||
emphasis: inlineParents,
|
||||
underline: inlineParents,
|
||||
strike: inlineParents,
|
||||
code: inlineParents,
|
||||
"text-span": inlineParents,
|
||||
mark: inlineParents,
|
||||
"hard-break": inlineParents,
|
||||
"horizontal-rule": ["rich-text", "list-item-content"],
|
||||
"template-part": [
|
||||
"page",
|
||||
"region",
|
||||
"header",
|
||||
"section",
|
||||
"section-heading",
|
||||
"section-items",
|
||||
"item",
|
||||
"item-header",
|
||||
"contact-list",
|
||||
"contact-item",
|
||||
"template-part",
|
||||
],
|
||||
};
|
||||
|
||||
const expectedTemplatePartChildren = {
|
||||
"timeline-line": [],
|
||||
"timeline-marker": ["template-part"],
|
||||
"timeline-dot": [],
|
||||
"timeline-content": ["item-header", "field", "link", "item", "level"],
|
||||
"featured-summary": ["section"],
|
||||
"sidebar-background": [],
|
||||
"header-band": ["template-part", "name", "headline"],
|
||||
"picture-anchor": ["picture"],
|
||||
"contact-offset": [],
|
||||
"header-intro": ["template-part"],
|
||||
"header-body": ["picture", "name", "headline", "section"],
|
||||
"header-contact-band": ["contact-list"],
|
||||
"inline-item-header-leading": ["field", "combined-text"],
|
||||
"inline-item-header-middle": ["field", "link"],
|
||||
"inline-item-header-trailing": ["field"],
|
||||
"education-grade-row": ["combined-text"],
|
||||
"header-divider": ["name", "headline"],
|
||||
"contact-item-content": ["link", "icon", "field"],
|
||||
"header-name-rule": [],
|
||||
"contact-row-primary": ["contact-item"],
|
||||
"contact-row-secondary": ["contact-item"],
|
||||
} as const;
|
||||
|
||||
describe("semantic registry", () => {
|
||||
it("registers every stable semantic node and parentage contract", () => {
|
||||
expect(Object.keys(SEMANTIC_REGISTRY_V1)).toEqual(Object.keys(expectedParents));
|
||||
|
||||
for (const [kind, parents] of Object.entries(expectedParents)) {
|
||||
expect(SEMANTIC_REGISTRY_V1[kind as keyof typeof SEMANTIC_REGISTRY_V1].parents).toEqual(parents);
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps list rows separate from their inner content", () => {
|
||||
expect(canContainNode("resume", "page")).toBe(true);
|
||||
expect(canContainNode("section", "item")).toBe(false);
|
||||
expect(canContainNode("section-items", "item")).toBe(true);
|
||||
expect(canContainNode("item", "item")).toBe(true);
|
||||
expect(canContainNode("list-item", "list-item-content")).toBe(true);
|
||||
expect(canContainNode("list-item-content", "list-marker")).toBe(false);
|
||||
});
|
||||
|
||||
it("permits only the registered level decoration ancestry and state roles", () => {
|
||||
expect(canContainNode("level", "icon")).toBe(true);
|
||||
expect(canContainNode("level", "field")).toBe(false);
|
||||
expect(SEMANTIC_REGISTRY_V1.icon.attributes).toEqual(["type"]);
|
||||
expect(SEMANTIC_REGISTRY_V1.icon.roles).toEqual(["decoration", "active", "inactive"]);
|
||||
expect(SEMANTIC_REGISTRY_V1.icon.roles).not.toContain("primary-text");
|
||||
});
|
||||
|
||||
it("registers list content direction without broadening unrelated rich-text attributes", () => {
|
||||
expect(SEMANTIC_REGISTRY_V1["list-item-content"].attributes).toEqual(["direction"]);
|
||||
expect(SEMANTIC_REGISTRY_V1["list-item"].attributes).toEqual([]);
|
||||
});
|
||||
|
||||
it("publishes finite semantic attribute domains", () => {
|
||||
expect(SEMANTIC_REGISTRY_V1.resume.attributeValues?.template).toEqual(templateSchema.options);
|
||||
expect(SEMANTIC_REGISTRY_V1.section.attributeValues).toMatchObject({
|
||||
type: sectionTypeSchema.options,
|
||||
placement: ["main", "sidebar"],
|
||||
origin: ["main", "sidebar"],
|
||||
});
|
||||
});
|
||||
|
||||
it("permits truthful contact and nested template parts without broadening unrelated parents", () => {
|
||||
expect(canContainNode("contact-item", "template-part")).toBe(true);
|
||||
expect(canContainNode("template-part", "template-part")).toBe(false);
|
||||
expect(canContainNode("template-part", "template-part", "timeline-marker")).toBe(true);
|
||||
expect(canContainNode("contact-list", "template-part")).toBe(true);
|
||||
expect(canContainNode("rich-text", "template-part")).toBe(false);
|
||||
});
|
||||
|
||||
it("registers contact-list template parts and their child contracts", () => {
|
||||
expect(SEMANTIC_REGISTRY_V1["template-part"].parents).toContain("contact-list");
|
||||
expect(TEMPLATE_PART_CHILD_KINDS_V1["contact-row-primary"]).toEqual(["contact-item"]);
|
||||
expect(TEMPLATE_PART_CHILD_KINDS_V1["contact-row-secondary"]).toEqual(["contact-item"]);
|
||||
});
|
||||
|
||||
it("freezes the exact child-kind allowlist for every existing primitive template part", () => {
|
||||
const registry = semanticRegistry as unknown as {
|
||||
TEMPLATE_PART_CHILD_KINDS_V1?: Readonly<Record<string, readonly string[]>>;
|
||||
};
|
||||
|
||||
expect(registry.TEMPLATE_PART_CHILD_KINDS_V1).toEqual(expectedTemplatePartChildren);
|
||||
expect(Object.isFrozen(registry.TEMPLATE_PART_CHILD_KINDS_V1)).toBe(true);
|
||||
for (const children of Object.values(registry.TEMPLATE_PART_CHILD_KINDS_V1 ?? {})) {
|
||||
expect(Object.isFrozen(children)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("allows only each named template part's exact child kinds", () => {
|
||||
for (const [part, allowed] of Object.entries(expectedTemplatePartChildren)) {
|
||||
for (const child of SEMANTIC_NODE_KINDS) {
|
||||
expect(canContainNode("template-part", child, part)).toBe((allowed as readonly string[]).includes(child));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects omitted and unknown template-part names without changing ordinary two-argument containment", () => {
|
||||
expect(canContainNode("template-part", "picture")).toBe(false);
|
||||
expect(canContainNode("template-part", "picture", "unknown-part")).toBe(false);
|
||||
expect(canContainNode("header", "picture")).toBe(true);
|
||||
expect(canContainNode("header", "field")).toBe(false);
|
||||
});
|
||||
|
||||
it("allows alias tokens only on canonical owner kinds that use them", () => {
|
||||
expect(SEMANTIC_REGISTRY_V1.region.attributes).toContain("part");
|
||||
expect(SEMANTIC_REGISTRY_V1.section.attributes).toContain("part");
|
||||
expect(SEMANTIC_REGISTRY_V1["item-header"].attributes).toContain("part");
|
||||
expect(SEMANTIC_REGISTRY_V1["contact-item"].attributes).toContain("part");
|
||||
expect(SEMANTIC_REGISTRY_V1.header.attributes).not.toContain("part");
|
||||
expect(SEMANTIC_REGISTRY_V1.item.attributes).not.toContain("part");
|
||||
expect(SEMANTIC_REGISTRY_V1.field.attributes).not.toContain("part");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user