fix(stylesheet): improve unsupported-gradient diagnostics (#3468)

* test(stylesheet): strengthen basics diagnostics

* fix(css): tighten gradient diagnostics
This commit is contained in:
Amruth Pillai
2026-09-05 19:29:21 -07:00
committed by GitHub
parent 0fbeeeb4c4
commit 38832014b9
6 changed files with 148 additions and 4 deletions
@@ -208,6 +208,19 @@ describe("Semantic CSS editor extensions", () => {
expect(selected).toHaveBeenCalledWith(tokens[0], expect.any(DOMRect));
});
it("keeps unsupported-gradient diagnostics visible in the editor", () => {
const source = "@version 1;\nheader { background-image: linear-gradient(red, blue); }\n";
const compiled = compileStylesheet({ languageVersion: 1, text: source });
expect(mapCompilerDiagnostics(source.length, compiled.diagnostics)).toContainEqual(
expect.objectContaining({
severity: "error",
message: "Gradients are not supported by Semantic CSS. Use background-color or another supported property.",
source: "UNSUPPORTED_PROPERTY",
}),
);
});
it("preserves exact clipboard text and emits one change for an IME composition", async () => {
const writeText = vi.fn().mockResolvedValue(undefined);
Object.defineProperty(navigator, "clipboard", { configurable: true, value: { writeText } });
@@ -59,6 +59,23 @@ describe("stylesheet worker", () => {
);
});
it("explains that unsupported gradients need a supported background replacement", () => {
handleMessage?.(
new MessageEvent("message", {
data: request("@version 1;\nheader { background-image: linear-gradient(red, blue); }\n"),
}),
);
const response = postMessage.mock.calls[0]?.[0] as CompileWorkerResponse | undefined;
expect(response?.diagnostics).toContainEqual(
expect.objectContaining({
code: "UNSUPPORTED_PROPERTY",
severity: "error",
message: "Gradients are not supported by Semantic CSS. Use background-color or another supported property.",
}),
);
});
it("returns overlapping semantic diagnostics once", () => {
handleMessage?.(
new MessageEvent("message", {
+14
View File
@@ -192,6 +192,20 @@ The most useful declarations usually fall into a few groups:
Use `display: none` only to hide an existing semantic node. Semantic CSS cannot add, remove, duplicate, or re-parent resume
data.
Semantic CSS keeps background styling PDF-safe. Use a flat color for headers and regions:
```css
@version 1;
header {
background-color: #1e293b;
}
```
Gradient declarations such as `background-image: linear-gradient(...)` remain unsupported. The editor reports that limitation
and suggests `background-color` or another supported property; the unsupported declaration is omitted while neighboring valid
declarations remain available for preview and export.
### Style rich-text lists
`list-item` is the outer row that holds a marker and its content. Use it for row layout and spacing. Use `list-marker`
@@ -42,6 +42,11 @@ const mergedStyle = (node: HostNode | undefined): Record<string, unknown> =>
const containsStyle = (node: HostNode, property: string, value: unknown): boolean =>
mergedStyle(node)[property] === value || (node.children ?? []).some((child) => containsStyle(child, property, value));
const nodesWithStyle = (node: HostNode, property: string, value: unknown): HostNode[] => [
...(mergedStyle(node)[property] === value ? [node] : []),
...(node.children ?? []).flatMap((child) => nodesWithStyle(child, property, value)),
];
const textRuns = (node: HostNode): string[] => [
...(node.type === "TEXT" ? [nodeText(node)] : []),
...(node.children ?? []).flatMap((child) => textRuns(child)),
@@ -53,7 +58,11 @@ const buildIssueFixture = (): ResumeData => {
data.basics = {
...data.basics,
name: "Ada Lovelace",
headline: "Computing pioneer",
email: "ada@example.com",
phone: "+44 123",
location: "London",
customFields: [{ id: "custom-1", icon: "globe", text: "Ada Labs", link: "" }],
};
data.sections.experience.items = [
{
@@ -200,8 +209,12 @@ describe("semantic issue fixtures", () => {
const data = buildIssueFixture();
const stylesheet = source(`
@version 1;
header { background-color: #1e293b; }
header { background-color: #1e293b; padding: 10pt; }
name { color: white; }
headline { font-size: 14pt; }
contact-list { gap: 8pt; }
contact-item { padding: 1pt; }
icon { font-size: 16pt; }
link { text-decoration: none; }
section[type="experience"] field[name="company"] { font-weight: 400; }
section[type="skills"] field[name="name"] { font-weight: 400; }
@@ -214,12 +227,18 @@ describe("semantic issue fixtures", () => {
const document = instance.container.document as HostNode;
expect(mergedStyle(findText(document, "Ada Lovelace"))).toMatchObject({ color: "white" });
expect(mergedStyle(findText(document, "Computing pioneer"))).toMatchObject({ fontSize: 14 });
expect(mergedStyle(findText(document, "Analytical Engines"))).toMatchObject({ fontWeight: "400" });
expect(mergedStyle(findText(document, "TypeScript"))).toMatchObject({ fontWeight: "400" });
expect(mergedStyle(findPrimitive(document, "LINK", "ada@example.com")).paddingTop).toBe(1);
expect(mergedStyle(findPrimitive(document, "LINK", "+44 123")).paddingTop).toBe(1);
expect(nodesWithStyle(document, "fontSize", 16).some(({ type }) => type === "SVG")).toBe(true);
expect(mergedStyle(findPrimitive(document, "LINK", "ada@example.com"))).toMatchObject({
textDecoration: "none",
});
expect(containsStyle(document, "backgroundColor", "#1e293b")).toBe(true);
expect(containsStyle(document, "paddingTop", 10)).toBe(true);
expect(containsStyle(document, "rowGap", 8)).toBe(true);
expect(containsStyle(document, "opacity", 0.2)).toBe(true);
});
@@ -70,6 +70,64 @@ describe("Semantic CSS value compilation", () => {
);
});
it("explains that gradient backgrounds are unsupported and suggests a safe replacement", () => {
const result = compileStylesheet({
languageVersion: 1,
text: "@version 1; header { background-image: linear-gradient(red, blue); color: white; }",
});
expect(result.program).not.toBeNull();
expect(result.program?.rules[0]?.declarations).toContainEqual(
expect.objectContaining({ property: "color", value: "white" }),
);
expect(result.diagnostics).toContainEqual(
expect.objectContaining({
code: "UNSUPPORTED_PROPERTY",
severity: "error",
message: "Gradients are not supported by Semantic CSS. Use background-color or another supported property.",
}),
);
});
it.each([
"linear-gradient(red, blue)",
"radial-gradient(red, blue)",
"conic-gradient(red, blue)",
"repeating-linear-gradient(red, blue)",
"repeating-radial-gradient(red, blue)",
"repeating-conic-gradient(red, blue)",
])("recognizes the standard gradient function %s", (value) => {
const result = compileStylesheet({
languageVersion: 1,
text: `@version 1; header { background-image: ${value}; }`,
});
expect(result.diagnostics).toContainEqual(
expect.objectContaining({
code: "UNSUPPORTED_PROPERTY",
message: "Gradients are not supported by Semantic CSS. Use background-color or another supported property.",
}),
);
});
it.each([
["custom function names", "background-image: not-linear-gradient(red, blue)", "background-image"],
["quoted function-like text", 'content: "linear-gradient(red, blue)"', "content"],
] as const)("does not treat %s as a standard gradient function", (_case, declaration, property) => {
const result = compileStylesheet({
languageVersion: 1,
text: `@version 1; header { ${declaration}; }`,
});
expect(result.diagnostics).toContainEqual(
expect.objectContaining({
code: "UNSUPPORTED_PROPERTY",
severity: "error",
message: `The ${property} property is not supported by Semantic CSS.`,
}),
);
});
it("omits an invalid value without dropping valid declarations in the rule", () => {
const result = compileStylesheet({
languageVersion: 1,
+26 -3
View File
@@ -149,6 +149,29 @@ function diagnostic(
diagnostics.push(createDiagnostic(code, severity, message, range(node?.loc)));
}
const gradientFunctionNames = new Set([
"linear-gradient",
"radial-gradient",
"conic-gradient",
"repeating-linear-gradient",
"repeating-radial-gradient",
"repeating-conic-gradient",
]);
function containsGradientFunction(node: AstNode | string | null | undefined): boolean {
if (!node || typeof node === "string") return false;
if (node.type === "Function" && node.name && gradientFunctionNames.has(identifier(node.name).toLowerCase()))
return true;
return children(node).some(containsGradientFunction);
}
function unsupportedPropertyMessage(property: string, value: AstNode | string | null | undefined): string {
if (containsGradientFunction(value)) {
return "Gradients are not supported by Semantic CSS. Use background-color or another supported property.";
}
return `The ${property} property is not supported by Semantic CSS.`;
}
function splitValue(value: string): string[] {
const parts: string[] = [];
let start = 0;
@@ -560,6 +583,8 @@ export function compileProgram(stylesheet: ParsedStylesheet, languageVersion: nu
const decodedProperty = identifier(declaration.property);
const property = decodedProperty.startsWith("--") ? decodedProperty : decodedProperty.toLowerCase();
const lowerProperty = property.toLowerCase();
const value =
typeof declaration.value === "string" ? declaration.value : csstree.generate(declaration.value as CssNode);
if (lowerProperty.startsWith("--resume-")) {
diagnostic(
diagnostics,
@@ -573,7 +598,7 @@ export function compileProgram(stylesheet: ParsedStylesheet, languageVersion: nu
diagnostic(
diagnostics,
property === "src" ? "FORBIDDEN_CSS_VALUE" : "UNSUPPORTED_PROPERTY",
`The ${property} property is not supported by Semantic CSS.`,
unsupportedPropertyMessage(property, declaration.value),
declaration,
);
continue;
@@ -583,8 +608,6 @@ export function compileProgram(stylesheet: ParsedStylesheet, languageVersion: nu
continue;
}
const value =
typeof declaration.value === "string" ? declaration.value : csstree.generate(declaration.value as CssNode);
const trimmedValue = value.trim();
const expanded = /var\s*\(/i.test(decodeCssEscapes(trimmedValue))
? ([[property, trimmedValue]] as const)