mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-10-03 18:23:47 +10:00
fix(stylesheet): improve unsupported-gradient diagnostics (#3468)
* test(stylesheet): strengthen basics diagnostics * fix(css): tighten gradient diagnostics
This commit is contained in:
@@ -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", {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user