Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions apps/builder/app/shared/html.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,27 @@ test("generate style attribute as local styles", () => {
);
});

test("generate nested css math functions as unparsed local styles", () => {
const value =
"clamp(1rem, calc(1rem + (2rem - 1rem) * ((100vw - 20rem) / (80rem - 20rem))), 2rem)";
const fragment = generateFragmentFromHtml(`
<div style="font-size:${value}">One clamp div</div>
`);

expect(fragment.styles).toEqual(
expect.arrayContaining([
expect.objectContaining({
property: "fontSize",
value: {
type: "unparsed",
value:
"clamp(1rem,calc(1rem + (2rem - 1rem)*((100vw - 20rem)/(80rem - 20rem))),2rem)",
},
}),
])
);
});

test("script as html embed", () => {
expect(generateFragmentFromHtml(`<script>a;</script>`)).toEqual(
renderTemplate(
Expand Down
33 changes: 33 additions & 0 deletions packages/css-data/src/parse-css-value.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,44 @@ describe("Parse CSS value", () => {
});
});

test("Nested CSS math values", () => {
const value =
"clamp(1rem, calc(1rem + (2rem - 1rem) * ((100vw - 20rem) / (80rem - 20rem))), 2rem)";

expect(parseCssValue("font-size", value)).toEqual({
type: "unparsed",
value,
});
});

test("CSS numeric functions", () => {
expect(parseCssValue("width", "round(10px, 1px)")).toEqual({
type: "unparsed",
value: "round(10px, 1px)",
});
expect(parseCssValue("width", "abs(-10px)")).toEqual({
type: "unparsed",
value: "abs(-10px)",
});
expect(parseCssValue("width", "calc(pi * 1px)")).toEqual({
type: "unparsed",
value: "calc(pi * 1px)",
});
});

test("Invalid function values", () => {
expect(parseCssValue("width", "blur(4)")).toEqual({
type: "invalid",
value: "blur(4)",
});
expect(parseCssValue("font-size", "clamp(foo)")).toEqual({
type: "invalid",
value: "clamp(foo)",
});
expect(parseCssValue("color", "abs(-10px)")).toEqual({
type: "invalid",
value: "abs(-10px)",
});
});
});

Expand Down
101 changes: 101 additions & 0 deletions packages/css-data/src/parse-css-value.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
type CssNode,
definitionSyntax,
type FunctionNode,
generate,
lexer,
Expand Down Expand Up @@ -54,6 +55,91 @@ const splitRepeated = (nodes: CssNode[]) => {
return lists;
};

const cssNumericFunctionNames = new Set([
"calc",
"min",
"max",
"clamp",
"round",
"mod",
"rem",
"sin",
"cos",
"tan",
"asin",
"acos",
"atan",
"atan2",
"pow",
"sqrt",
"hypot",
"log",
"exp",
"abs",
"sign",
]);

const cssMathConstants = new Set(["e", "pi", "infinity", "-infinity", "nan"]);

const cssNumericTypeNames = new Set([
"length",
"length-percentage",
"percentage",
"number",
"integer",
"angle",
"time",
"frequency",
"resolution",
"flex",
"alpha-value",
]);

const canFallbackToCssMath = (ast: CssNode, syntax: string | undefined) => {
if (syntax === undefined) {
return false;
}

let hasCssNumericType = false;
try {
definitionSyntax.walk(definitionSyntax.parse(syntax), (node) => {
if (
node.type === "Type" &&
"name" in node &&
cssNumericTypeNames.has(node.name)
) {
hasCssNumericType = true;
}
});
} catch {
return false;
}
if (hasCssNumericType === false) {
return false;
}

let hasCssNumericFunction = false;
let hasUnknownIdentifier = false;
walk(ast, (node) => {
if (node.type === "Function" && cssNumericFunctionNames.has(node.name)) {
hasCssNumericFunction = true;
}
if (
node.type === "Identifier" &&
cssMathConstants.has(node.name.toLowerCase()) === false
) {
hasUnknownIdentifier = true;
}
});
return hasCssNumericFunction && hasUnknownIdentifier === false;
};

const getSyntaxMatchErrorSyntax = (error: Error | null | undefined) => {
if (error != null && "syntax" in error && typeof error.syntax === "string") {
return error.syntax;
}
};

// Because csstree parser has bugs we use CSSStyleValue to validate css properties if available
// and fall back to csstree.
export const isValidDeclaration = (
Expand Down Expand Up @@ -109,6 +195,10 @@ export const isValidDeclaration = (
// @todo remove after csstree fixes
// - https://github.com/csstree/csstree/issues/246
// - https://github.com/csstree/csstree/issues/164
if (typeof CSS !== "undefined" && CSS.supports(property, value)) {
return true;
}

if (typeof CSSStyleValue !== "undefined") {
try {
CSSStyleValue.parse(property, value);
Expand Down Expand Up @@ -143,6 +233,17 @@ export const isValidDeclaration = (
return true;
}

// css-tree does not fully validate modern CSS math with nested calc()
// operators, for example `font-size: clamp(... calc(... / ...) ...)`.
// Browser-valid values should be preserved as unparsed instead of stored as
// invalid values, which are intended for transient editor state.
if (
matchResult.matched == null &&
canFallbackToCssMath(ast, getSyntaxMatchErrorSyntax(matchResult.error))
) {
return true;
}

return matchResult.matched != null;
};

Expand Down
Loading