Unify CSS feature query truth
This commit is contained in:
1 parent
33d94eaf85
commit
d41a4b88e6
5 files changed
+749
-29
No files matched your search
Vendored
+233
-12
@@ -7725,7 +7725,7 @@ globalThis.scrollX = 0; globalThis.scrollY = 0;
|
|||||||
// is not harmless: Tailwind and other framework sheets use negative probes to
|
// is not harmless: Tailwind and other framework sheets use negative probes to
|
||||||
// select legacy-browser fallbacks, which can replace their modern cascade.
|
// select legacy-browser fallbacks, which can replace their modern cascade.
|
||||||
const _CSS_SUPPORTED_DECLARATIONS = new Set((
|
const _CSS_SUPPORTED_DECLARATIONS = new Set((
|
||||||
"display width height min-width min-height max-width max-height box-sizing aspect-ratio " +
|
"display width height min-width min-height max-width max-height box-sizing aspect-ratio content " +
|
||||||
"margin margin-top margin-right margin-bottom margin-left margin-inline margin-inline-start " +
|
"margin margin-top margin-right margin-bottom margin-left margin-inline margin-inline-start " +
|
||||||
"margin-inline-end margin-block margin-block-start margin-block-end padding padding-top " +
|
"margin-inline-end margin-block margin-block-start margin-block-end padding padding-top " +
|
||||||
"padding-right padding-bottom padding-left padding-inline padding-inline-start padding-inline-end " +
|
"padding-right padding-bottom padding-left padding-inline padding-inline-start padding-inline-end " +
|
||||||
@@ -7735,7 +7735,7 @@ const _CSS_SUPPORTED_DECLARATIONS = new Set((
|
|||||||
"background-position background-clip -webkit-background-clip mask-image -webkit-mask-image " +
|
"background-position background-clip -webkit-background-clip mask-image -webkit-mask-image " +
|
||||||
"mask-size -webkit-mask-size mask-repeat -webkit-mask-repeat color -webkit-text-fill-color fill " +
|
"mask-size -webkit-mask-size mask-repeat -webkit-mask-repeat color -webkit-text-fill-color fill " +
|
||||||
"stroke stroke-width border-color font-size font font-weight font-family font-style text-align " +
|
"stroke stroke-width border-color font-size font font-weight font-family font-style text-align " +
|
||||||
"text-transform text-decoration text-decoration-line line-height align-items justify-items " +
|
"text-transform text-decoration text-decoration-line line-height white-space overflow-wrap word-wrap word-break text-wrap text-wrap-style align-items justify-items " +
|
||||||
"place-items align-self justify-self place-self align-content justify-content place-content " +
|
"place-items align-self justify-self place-self align-content justify-content place-content " +
|
||||||
"flex-direction flex-wrap flex-grow flex-shrink flex-basis flex order position float object-fit " +
|
"flex-direction flex-wrap flex-grow flex-shrink flex-basis flex order position float object-fit " +
|
||||||
"top right bottom left inset overflow overflow-x overflow-y scrollbar-gutter visibility opacity animation " +
|
"top right bottom left inset overflow overflow-x overflow-y scrollbar-gutter visibility opacity animation " +
|
||||||
@@ -7839,14 +7839,18 @@ function _cssTopLevelComma(text) {
|
|||||||
function _cssSupportsDeclaration(name, value) {
|
function _cssSupportsDeclaration(name, value) {
|
||||||
name = name.trim().toLowerCase();
|
name = name.trim().toLowerCase();
|
||||||
value = value.trim();
|
value = value.trim();
|
||||||
if (!value) return false;
|
if (typeof Deno.core.ops.op_css_supports === "function") {
|
||||||
|
try { return !!Deno.core.ops.op_css_supports(name, value); }
|
||||||
|
catch (_) { return false; }
|
||||||
|
}
|
||||||
|
if (!value || _cssHasInvalidSupportsValueSyntax(value)) return false;
|
||||||
if (name.startsWith("--")) return name.length > 2;
|
if (name.startsWith("--")) return name.length > 2;
|
||||||
if (!_CSS_SUPPORTED_DECLARATIONS.has(name)) return false;
|
if (!_CSS_SUPPORTED_DECLARATIONS.has(name)) return false;
|
||||||
const lower = value.toLowerCase();
|
const lower = value.toLowerCase();
|
||||||
if (["initial", "inherit", "unset", "revert", "revert-layer"].includes(lower)) return true;
|
if (["initial", "inherit", "unset", "revert", "revert-layer"].includes(lower)) return true;
|
||||||
if (name === "display") {
|
if (name === "display") {
|
||||||
return ["none", "flex", "inline-flex", "inline", "inline-block", "grid",
|
return ["none", "flex", "inline-flex", "inline", "inline-block", "grid",
|
||||||
"inline-grid", "block", "flow-root", "contents"].includes(lower);
|
"inline-grid", "block", "flow-root", "table", "inline-table", "contents"].includes(lower);
|
||||||
}
|
}
|
||||||
if (name === "position") {
|
if (name === "position") {
|
||||||
return ["static", "relative", "absolute", "fixed", "sticky"].includes(lower);
|
return ["static", "relative", "absolute", "fixed", "sticky"].includes(lower);
|
||||||
@@ -7860,10 +7864,190 @@ function _cssSupportsDeclaration(name, value) {
|
|||||||
if (name === "scrollbar-gutter") {
|
if (name === "scrollbar-gutter") {
|
||||||
return lower === "auto" || lower === "stable" || lower === "stable both-edges";
|
return lower === "auto" || lower === "stable" || lower === "stable both-edges";
|
||||||
}
|
}
|
||||||
|
if (name === "white-space") {
|
||||||
|
return ["normal", "nowrap", "pre", "pre-wrap", "pre-line", "break-spaces"].includes(lower);
|
||||||
|
}
|
||||||
|
if (name === "overflow-wrap" || name === "word-wrap") {
|
||||||
|
return ["normal", "break-word", "anywhere"].includes(lower);
|
||||||
|
}
|
||||||
|
if (name === "word-break") {
|
||||||
|
return ["normal", "break-all", "keep-all", "break-word"].includes(lower);
|
||||||
|
}
|
||||||
|
if (name === "text-wrap") {
|
||||||
|
return ["auto", "wrap", "balance", "wrap balance", "balance wrap"].includes(lower);
|
||||||
|
}
|
||||||
|
if (name === "text-wrap-style") return lower === "auto" || lower === "balance";
|
||||||
|
if (["filter", "backdrop-filter", "-webkit-backdrop-filter", "perspective"].includes(name)) {
|
||||||
|
return lower === "none";
|
||||||
|
}
|
||||||
|
if (name === "contain") return lower === "none";
|
||||||
|
if (name === "content-visibility") return lower === "visible";
|
||||||
|
if (name === "content") return _cssSupportsContent(value);
|
||||||
|
if (["width", "height", "min-width", "min-height", "max-width", "max-height", "flex-basis"].includes(name)) {
|
||||||
|
return _cssSupportsDimension(value, true) || (name === "width" && lower === "fit-content");
|
||||||
|
}
|
||||||
|
if (/^(?:margin(?:-(?:top|right|bottom|left|inline|inline-start|inline-end|block|block-start|block-end))?|padding(?:-(?:top|right|bottom|left|inline|inline-start|inline-end|block|block-start|block-end))?|inset(?:-(?:inline|inline-start|inline-end|block|block-start|block-end))?|top|right|bottom|left)$/.test(name)) {
|
||||||
|
const allowAuto = name.startsWith("margin") || name === "top" || name === "right" || name === "bottom" || name === "left" || name.startsWith("inset");
|
||||||
|
const parts = _cssSplitWhitespace(value);
|
||||||
|
const max = /^(?:margin|padding|inset)$/.test(name) ? 4 : (/(?:inline|block)$/.test(name) ? 2 : 1);
|
||||||
|
return parts.length > 0 && parts.length <= max && parts.every((part) => _cssSupportsDimension(part, allowAuto));
|
||||||
|
}
|
||||||
|
if (["align-items", "justify-items", "align-self", "justify-self"].includes(name)) {
|
||||||
|
return _cssSupportsSelfAlignment(lower);
|
||||||
|
}
|
||||||
|
if (name === "align-content" || name === "justify-content") {
|
||||||
|
return _cssSupportsContentAlignment(lower) || (name === "justify-content" && ["left", "right"].includes(lower));
|
||||||
|
}
|
||||||
|
if (name === "flex-direction") return ["row", "row-reverse", "column", "column-reverse"].includes(lower);
|
||||||
|
if (name === "flex-wrap") return ["nowrap", "wrap", "wrap-reverse"].includes(lower);
|
||||||
|
if (name === "flex-grow" || name === "flex-shrink") {
|
||||||
|
return /^(?:\d+(?:\.\d*)?|\.\d+)$/.test(lower);
|
||||||
|
}
|
||||||
|
if (name === "order") return /^[-+]?\d+$/.test(lower);
|
||||||
|
if (name === "opacity") return /^[-+]?(?:\d+(?:\.\d*)?|\.\d+)$/.test(lower);
|
||||||
|
if (name === "z-index") return lower === "auto" || /^[-+]?\d+$/.test(lower);
|
||||||
if (["color", "-webkit-text-fill-color", "background-color", "border-color"].includes(name)) {
|
if (["color", "-webkit-text-fill-color", "background-color", "border-color"].includes(name)) {
|
||||||
return _cssSupportsColor(value);
|
return _cssSupportsColor(value);
|
||||||
}
|
}
|
||||||
return true;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _cssHasInvalidSupportsValueSyntax(value) {
|
||||||
|
let depth = 0, quote = "";
|
||||||
|
for (let i = 0; i < value.length; i++) {
|
||||||
|
const character = value[i];
|
||||||
|
if (quote) {
|
||||||
|
if (character === "\\") i++;
|
||||||
|
else if (character === quote) quote = "";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (character === "\\") { i++; continue; }
|
||||||
|
if (character === "'" || character === '"') quote = character;
|
||||||
|
else if (character === "(" || character === "[") depth++;
|
||||||
|
else if (character === ")" || character === "]") {
|
||||||
|
if (--depth < 0) return true;
|
||||||
|
} else if (depth === 0 && /[;{}]/.test(character)) return true;
|
||||||
|
else if (depth === 0 && character === "!" && /^\s*important\b/i.test(value.slice(i + 1))) return true;
|
||||||
|
}
|
||||||
|
return depth !== 0 || !!quote;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _cssSplitWhitespace(value) {
|
||||||
|
const values = [], split = _cssSplitTopLevel(value, " ");
|
||||||
|
if (split) return split;
|
||||||
|
let depth = 0, quote = "", start = -1;
|
||||||
|
for (let i = 0; i <= value.length; i++) {
|
||||||
|
const character = value[i] || " ";
|
||||||
|
if (quote) {
|
||||||
|
if (character === "\\") i++;
|
||||||
|
else if (character === quote) quote = "";
|
||||||
|
} else if (character === "'" || character === '"') quote = character;
|
||||||
|
else if (character === "(") depth++;
|
||||||
|
else if (character === ")") depth--;
|
||||||
|
if (/\s/.test(character) && depth === 0 && !quote) {
|
||||||
|
if (start >= 0) values.push(value.slice(start, i));
|
||||||
|
start = -1;
|
||||||
|
} else if (start < 0) start = i;
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _cssSupportsDimension(value, allowAuto) {
|
||||||
|
const lower = value.trim().toLowerCase();
|
||||||
|
if (allowAuto && lower === "auto") return true;
|
||||||
|
if (/^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:px|pt|em|ex|rem|vw|vh|dvw|dvh|svw|svh|lvw|lvh|vmin|vmax|%)$/i.test(lower)) return true;
|
||||||
|
if (/^[-+]?0(?:\.0*)?$/.test(lower)) return true;
|
||||||
|
return /^(?:calc|min|max|clamp|var)\(.+\)$/i.test(lower);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _cssSupportsSelfAlignment(value) {
|
||||||
|
return /^(?:auto|normal|stretch|baseline|first baseline|center|(?:safe |unsafe )?(?:start|end|self-start|self-end|flex-start|flex-end))$/.test(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _cssSupportsContentAlignment(value) {
|
||||||
|
return /^(?:normal|stretch|baseline|first baseline|space-between|space-around|space-evenly|(?:safe |unsafe )?(?:start|end|flex-start|flex-end|center))$/.test(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _cssSupportsContent(value) {
|
||||||
|
const lower = value.trim().toLowerCase();
|
||||||
|
if (lower === "none" || lower === "normal" || _cssSupportsSingleUrl(value)) return true;
|
||||||
|
let rest = value.trim(), found = false;
|
||||||
|
while (rest) {
|
||||||
|
rest = rest.trimStart();
|
||||||
|
if (rest[0] === "'" || rest[0] === '"') {
|
||||||
|
const quote = rest[0];
|
||||||
|
let end = 1;
|
||||||
|
for (; end < rest.length; end++) {
|
||||||
|
if (rest[end] === "\\") end++;
|
||||||
|
else if (rest[end] === quote) break;
|
||||||
|
}
|
||||||
|
if (end >= rest.length) return false;
|
||||||
|
rest = rest.slice(end + 1);
|
||||||
|
found = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const keyword = /^(?:open-quote|close-quote|no-open-quote|no-close-quote)\b/i.exec(rest);
|
||||||
|
if (keyword) {
|
||||||
|
rest = rest.slice(keyword[0].length);
|
||||||
|
found = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const fn = /^(attr|counter|counters)\(/i.exec(rest);
|
||||||
|
if (!fn) return false;
|
||||||
|
let depth = 0, quote = "", end = -1;
|
||||||
|
for (let i = fn[1].length; i < rest.length; i++) {
|
||||||
|
const character = rest[i];
|
||||||
|
if (quote) {
|
||||||
|
if (character === "\\") i++;
|
||||||
|
else if (character === quote) quote = "";
|
||||||
|
} else if (character === "'" || character === '"') quote = character;
|
||||||
|
else if (character === "(") depth++;
|
||||||
|
else if (character === ")" && --depth === 0) { end = i; break; }
|
||||||
|
}
|
||||||
|
const argumentsText = rest.slice(fn[0].length, end).trim();
|
||||||
|
if (end < 0 || !_cssSupportsContentFunction(fn[1].toLowerCase(), argumentsText)) return false;
|
||||||
|
rest = rest.slice(end + 1);
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _cssSupportsSingleUrl(value) {
|
||||||
|
value = value.trim();
|
||||||
|
if (!/^url\(/i.test(value) || !value.endsWith(")")) return false;
|
||||||
|
let depth = 0, quote = "";
|
||||||
|
for (let i = 0; i < value.length; i++) {
|
||||||
|
const character = value[i];
|
||||||
|
if (quote) {
|
||||||
|
if (character === "\\") i++;
|
||||||
|
else if (character === quote) quote = "";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (character === "\\") { i++; continue; }
|
||||||
|
if (character === "'" || character === '"') quote = character;
|
||||||
|
else if (character === "(") depth++;
|
||||||
|
else if (character === ")" && --depth === 0) {
|
||||||
|
return i === value.length - 1 && value.slice(4, i).trim().length > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _cssSupportsContentFunction(name, argumentsText) {
|
||||||
|
const argumentsList = _cssSplitTopLevel(argumentsText, ",") || [argumentsText];
|
||||||
|
const ident = (value) => /^[a-z0-9_\\-]+$/i.test(value.trim());
|
||||||
|
const counterStyle = (value) => /^(?:decimal|decimal-leading-zero|lower-alpha|lower-latin|upper-alpha|upper-latin|lower-roman|upper-roman)$/i.test(value.trim());
|
||||||
|
if (name === "attr") return argumentsList.length === 1 && ident(argumentsList[0].trim().split(/\s+/)[0]);
|
||||||
|
if (name === "counter") {
|
||||||
|
return argumentsList.length >= 1 && argumentsList.length <= 2 && ident(argumentsList[0]) &&
|
||||||
|
(argumentsList.length === 1 || counterStyle(argumentsList[1]));
|
||||||
|
}
|
||||||
|
if (name === "counters") {
|
||||||
|
return argumentsList.length >= 2 && argumentsList.length <= 3 && ident(argumentsList[0]) &&
|
||||||
|
/^(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')$/.test(argumentsList[1].trim()) &&
|
||||||
|
(argumentsList.length === 2 || counterStyle(argumentsList[2]));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the contents only when one pair of parentheses encloses the complete
|
// Return the contents only when one pair of parentheses encloses the complete
|
||||||
@@ -8001,24 +8185,61 @@ function _cssSupportsSelector(selector) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function _cssSupportsCondition(condition) {
|
function _cssBalancedSupportsSyntax(condition) {
|
||||||
|
const stack = [];
|
||||||
|
let quote = "";
|
||||||
|
for (let i = 0; i < condition.length; i++) {
|
||||||
|
const character = condition[i];
|
||||||
|
if (quote) {
|
||||||
|
if (character === "\\") i++;
|
||||||
|
else if (character === quote) quote = "";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (character === "\\") { i++; continue; }
|
||||||
|
if (character === "'" || character === '"') quote = character;
|
||||||
|
else if (character === "(") stack.push(")");
|
||||||
|
else if (character === "[") stack.push("]");
|
||||||
|
else if ((character === ")" || character === "]") && stack.pop() !== character) return false;
|
||||||
|
}
|
||||||
|
return !quote && stack.length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// `null` means invalid syntax, which is distinct from a valid false leaf.
|
||||||
|
// In particular `not <invalid>` must remain false rather than flipping true.
|
||||||
|
function _cssSupportsConditionResult(condition) {
|
||||||
condition = condition.trim();
|
condition = condition.trim();
|
||||||
if (!condition) return false;
|
if (!condition || !_cssBalancedSupportsSyntax(condition)) return null;
|
||||||
const grouped = _cssEnclosingGroup(condition);
|
const grouped = _cssEnclosingGroup(condition);
|
||||||
if (grouped !== null) return _cssSupportsCondition(grouped);
|
if (grouped !== null) return _cssSupportsConditionResult(grouped);
|
||||||
if (/^not\s/i.test(condition)) return !_cssSupportsCondition(condition.slice(3).trim());
|
if (/^not\s/i.test(condition)) {
|
||||||
|
const result = _cssSupportsConditionResult(condition.slice(3).trim());
|
||||||
|
return result === null ? null : !result;
|
||||||
|
}
|
||||||
const orParts = _cssSplitTopLevel(condition, "or");
|
const orParts = _cssSplitTopLevel(condition, "or");
|
||||||
if (orParts) return orParts.some(_cssSupportsCondition);
|
|
||||||
const andParts = _cssSplitTopLevel(condition, "and");
|
const andParts = _cssSplitTopLevel(condition, "and");
|
||||||
if (andParts) return andParts.every(_cssSupportsCondition);
|
if (orParts && andParts) return null;
|
||||||
|
if (orParts) {
|
||||||
|
const results = orParts.map(_cssSupportsConditionResult);
|
||||||
|
return results.includes(null) ? null : results.some(Boolean);
|
||||||
|
}
|
||||||
|
if (andParts) {
|
||||||
|
const results = andParts.map(_cssSupportsConditionResult);
|
||||||
|
return results.includes(null) ? null : results.every(Boolean);
|
||||||
|
}
|
||||||
if (/^selector\(/i.test(condition) && condition.endsWith(")")) {
|
if (/^selector\(/i.test(condition) && condition.endsWith(")")) {
|
||||||
return _cssSupportsSelector(condition.slice(condition.indexOf("(") + 1, -1));
|
return _cssSupportsSelector(condition.slice(condition.indexOf("(") + 1, -1));
|
||||||
}
|
}
|
||||||
const colon = condition.indexOf(":");
|
const colon = condition.indexOf(":");
|
||||||
if (colon < 0) return false;
|
if (colon < 0) {
|
||||||
|
return /^[a-z_-][a-z0-9_-]*\([\s\S]*\)$/i.test(condition) ? false : null;
|
||||||
|
}
|
||||||
return _cssSupportsDeclaration(condition.slice(0, colon), condition.slice(colon + 1));
|
return _cssSupportsDeclaration(condition.slice(0, colon), condition.slice(colon + 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _cssSupportsCondition(condition) {
|
||||||
|
return _cssSupportsConditionResult(condition) === true;
|
||||||
|
}
|
||||||
|
|
||||||
globalThis.CSS = {
|
globalThis.CSS = {
|
||||||
supports(prop, value){
|
supports(prop, value){
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -2457,6 +2457,7 @@ pub fn build_extension() -> Extension {
|
|||||||
ops.push(op_image_metadata());
|
ops.push(op_image_metadata());
|
||||||
ops.push(op_layout_geometry());
|
ops.push(op_layout_geometry());
|
||||||
ops.push(op_computed_style());
|
ops.push(op_computed_style());
|
||||||
|
ops.push(op_css_supports());
|
||||||
ops.push(op_layout_metrics());
|
ops.push(op_layout_metrics());
|
||||||
ops.push(op_scroll_offset());
|
ops.push(op_scroll_offset());
|
||||||
ops.push(op_scroll_to());
|
ops.push(op_scroll_to());
|
||||||
@@ -2703,6 +2704,15 @@ fn op_computed_style(state: &OpState, #[string] nid_str: String) -> String {
|
|||||||
serde_json::Value::Object(object).to_string()
|
serde_json::Value::Object(object).to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Use the renderer's declaration parser as the single feature-query source
|
||||||
|
/// of truth. Keeping this bridge synchronous and state-free makes the common
|
||||||
|
/// two-argument `CSS.supports()` overload a single native call.
|
||||||
|
#[cfg(feature = "render")]
|
||||||
|
#[op2(fast)]
|
||||||
|
fn op_css_supports(#[string] name: &str, #[string] value: &str) -> bool {
|
||||||
|
obscura_render::style::supports_declaration(name, value)
|
||||||
|
}
|
||||||
|
|
||||||
/// Root scrolling overflow in CSS pixels. The JS CSSOM probes this op only in
|
/// Root scrolling overflow in CSS pixels. The JS CSSOM probes this op only in
|
||||||
/// render builds; default scraping builds retain their deliberately unbounded
|
/// render builds; default scraping builds retain their deliberately unbounded
|
||||||
/// synthetic scrolling behavior.
|
/// synthetic scrolling behavior.
|
||||||
|
|||||||
@@ -3086,13 +3086,19 @@ mod tests {
|
|||||||
CSS.supports("color", "light-dark(rgb(1, 2, 3), color-mix(in srgb, white 50%, black))"),
|
CSS.supports("color", "light-dark(rgb(1, 2, 3), color-mix(in srgb, white 50%, black))"),
|
||||||
CSS.supports("(color:light-dark(red, light-dark(white, black)))"),
|
CSS.supports("(color:light-dark(red, light-dark(white, black)))"),
|
||||||
CSS.supports("color", "light-dark(red)"),
|
CSS.supports("color", "light-dark(red)"),
|
||||||
CSS.supports("color", "light-dark(red, rgb(1, 2, 3)")
|
CSS.supports("color", "light-dark(red, rgb(1, 2, 3)"),
|
||||||
|
CSS.supports("border", "2px dashed red"),
|
||||||
|
CSS.supports("border-width", "10%"),
|
||||||
|
CSS.supports("word-break", "break-all"),
|
||||||
|
CSS.supports("filter", "blur(2px)"),
|
||||||
|
CSS.supports("content", "attr(data-label)"),
|
||||||
|
CSS.supports("display", "grid;")
|
||||||
])"#,
|
])"#,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result,
|
result,
|
||||||
serde_json::json!("[false,false,false,true,false,true,true,true,false,false,false,true,false,true,true,false,false]")
|
serde_json::json!("[false,false,false,false,false,true,true,true,false,false,false,true,false,true,true,false,false,true,false,true,false,true,false]")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2517,13 +2517,13 @@ pub(crate) fn supports_condition_applies(condition: &str) -> bool {
|
|||||||
.or_else(|| condition.strip_prefix("supports"))
|
.or_else(|| condition.strip_prefix("supports"))
|
||||||
.unwrap_or(condition)
|
.unwrap_or(condition)
|
||||||
.trim();
|
.trim();
|
||||||
eval_supports_condition(condition)
|
eval_supports_condition(condition).unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn eval_supports_condition(condition: &str) -> bool {
|
fn eval_supports_condition(condition: &str) -> Option<bool> {
|
||||||
let condition = condition.trim();
|
let condition = condition.trim();
|
||||||
if condition.is_empty() {
|
if condition.is_empty() || !balanced_supports_syntax(condition) {
|
||||||
return false;
|
return None;
|
||||||
}
|
}
|
||||||
if let Some(inner) = enclosing_parenthesized(condition) {
|
if let Some(inner) = enclosing_parenthesized(condition) {
|
||||||
return eval_supports_condition(inner);
|
return eval_supports_condition(inner);
|
||||||
@@ -2536,23 +2536,76 @@ fn eval_supports_condition(condition: &str) -> bool {
|
|||||||
.get(3)
|
.get(3)
|
||||||
.is_some_and(u8::is_ascii_whitespace)
|
.is_some_and(u8::is_ascii_whitespace)
|
||||||
{
|
{
|
||||||
return !eval_supports_condition(condition[3..].trim());
|
return eval_supports_condition(condition[3..].trim()).map(|result| !result);
|
||||||
}
|
}
|
||||||
if let Some(parts) = split_supports_operator(condition, "or") {
|
let or_parts = split_supports_operator(condition, "or");
|
||||||
return parts.into_iter().any(eval_supports_condition);
|
let and_parts = split_supports_operator(condition, "and");
|
||||||
|
// CSS Conditional Rules does not permit `and` and `or` at the same
|
||||||
|
// nesting level. Treat the entire condition as invalid rather than
|
||||||
|
// inventing JavaScript-like precedence.
|
||||||
|
if or_parts.is_some() && and_parts.is_some() {
|
||||||
|
return None;
|
||||||
}
|
}
|
||||||
if let Some(parts) = split_supports_operator(condition, "and") {
|
if let Some(parts) = or_parts {
|
||||||
return parts.into_iter().all(eval_supports_condition);
|
let results = parts
|
||||||
|
.into_iter()
|
||||||
|
.map(eval_supports_condition)
|
||||||
|
.collect::<Option<Vec<_>>>()?;
|
||||||
|
return Some(results.into_iter().any(|result| result));
|
||||||
|
}
|
||||||
|
if let Some(parts) = and_parts {
|
||||||
|
let results = parts
|
||||||
|
.into_iter()
|
||||||
|
.map(eval_supports_condition)
|
||||||
|
.collect::<Option<Vec<_>>>()?;
|
||||||
|
return Some(results.into_iter().all(|result| result));
|
||||||
}
|
}
|
||||||
let lower = condition.to_ascii_lowercase();
|
let lower = condition.to_ascii_lowercase();
|
||||||
if lower.starts_with("selector(") && condition.ends_with(')') {
|
if lower.starts_with("selector(") && condition.ends_with(')') {
|
||||||
let inner = &condition["selector(".len()..condition.len() - 1];
|
let inner = &condition["selector(".len()..condition.len() - 1];
|
||||||
return obscura_dom::selector::parse_selector(inner.trim()).is_ok();
|
return Some(obscura_dom::selector::parse_selector(inner.trim()).is_ok());
|
||||||
}
|
}
|
||||||
let Some((name, value)) = condition.split_once(':') else {
|
let Some((name, value)) = condition.split_once(':') else {
|
||||||
return false;
|
// A syntactically balanced unknown functional condition is a valid
|
||||||
|
// general-enclosed leaf whose result is false. Everything else is a
|
||||||
|
// malformed condition, important for `not`: invalid syntax must not
|
||||||
|
// become true merely because it was negated.
|
||||||
|
return condition
|
||||||
|
.find('(')
|
||||||
|
.filter(|index| *index > 0 && condition.ends_with(')'))
|
||||||
|
.map(|_| false);
|
||||||
};
|
};
|
||||||
crate::style::supports_declaration(name, value)
|
Some(crate::style::supports_declaration(name, value))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn balanced_supports_syntax(condition: &str) -> bool {
|
||||||
|
let mut stack = Vec::new();
|
||||||
|
let mut quote = None;
|
||||||
|
let mut escaped = false;
|
||||||
|
for character in condition.chars() {
|
||||||
|
if escaped {
|
||||||
|
escaped = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if character == '\\' {
|
||||||
|
escaped = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(active) = quote {
|
||||||
|
if character == active {
|
||||||
|
quote = None;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
match character {
|
||||||
|
'\'' | '"' => quote = Some(character),
|
||||||
|
'(' => stack.push(')'),
|
||||||
|
'[' => stack.push(']'),
|
||||||
|
')' | ']' if stack.pop() != Some(character) => return false,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
!escaped && quote.is_none() && stack.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the contents when one outer parenthesis pair encloses the complete
|
/// Return the contents when one outer parenthesis pair encloses the complete
|
||||||
@@ -4671,6 +4724,21 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn supports_conditions_reject_invalid_boolean_grammar() {
|
||||||
|
assert!(supports_condition_applies("(display:grid) and (word-break:break-all)"));
|
||||||
|
assert!(supports_condition_applies("(display:grid) or (unknown:value)"));
|
||||||
|
assert!(!supports_condition_applies(
|
||||||
|
"(display:grid) and (word-break:break-all) or (display:flex)"
|
||||||
|
));
|
||||||
|
assert!(!supports_condition_applies(
|
||||||
|
"not ((display:grid) and (word-break:break-all) or (display:flex))"
|
||||||
|
));
|
||||||
|
assert!(!supports_condition_applies("not ()"));
|
||||||
|
assert!(!supports_condition_applies("(display:grid;)"));
|
||||||
|
assert!(!supports_condition_applies("not (display:grid"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn supports_polygon_clip_path_activates_only_painted_shape_subset() {
|
fn supports_polygon_clip_path_activates_only_painted_shape_subset() {
|
||||||
let css = "@supports (clip-path:polygon(0 0,100% 0,50% 100%)){\
|
let css = "@supports (clip-path:polygon(0 0,100% 0,50% 100%)){\
|
||||||
|
|||||||
@@ -1625,10 +1625,13 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
|
|||||||
/// modern framework sheets use negative feature probes to isolate legacy
|
/// modern framework sheets use negative feature probes to isolate legacy
|
||||||
/// browser fallbacks, and activating those fallbacks corrupts the modern
|
/// browser fallbacks, and activating those fallbacks corrupts the modern
|
||||||
/// cascade.
|
/// cascade.
|
||||||
pub(crate) fn supports_declaration(name: &str, value: &str) -> bool {
|
/// Return whether the renderer accepts and faithfully implements a CSS
|
||||||
|
/// declaration. This is also exposed to the JavaScript runtime so
|
||||||
|
/// `CSS.supports(property, value)` and stylesheet `@supports` cannot drift.
|
||||||
|
pub fn supports_declaration(name: &str, value: &str) -> bool {
|
||||||
let name = name.trim().to_ascii_lowercase();
|
let name = name.trim().to_ascii_lowercase();
|
||||||
let value = value.trim();
|
let value = value.trim();
|
||||||
if value.is_empty() {
|
if value.is_empty() || has_invalid_supports_value_syntax(value) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if name.starts_with("--") {
|
if name.starts_with("--") {
|
||||||
@@ -1715,6 +1718,7 @@ pub(crate) fn supports_declaration(name: &str, value: &str) -> bool {
|
|||||||
| "mask-repeat"
|
| "mask-repeat"
|
||||||
| "-webkit-mask-repeat"
|
| "-webkit-mask-repeat"
|
||||||
| "color"
|
| "color"
|
||||||
|
| "content"
|
||||||
| "-webkit-text-fill-color"
|
| "-webkit-text-fill-color"
|
||||||
| "fill"
|
| "fill"
|
||||||
| "stroke"
|
| "stroke"
|
||||||
@@ -1860,6 +1864,8 @@ pub(crate) fn supports_declaration(name: &str, value: &str) -> bool {
|
|||||||
| "inline-grid"
|
| "inline-grid"
|
||||||
| "block"
|
| "block"
|
||||||
| "flow-root"
|
| "flow-root"
|
||||||
|
| "table"
|
||||||
|
| "inline-table"
|
||||||
| "contents"
|
| "contents"
|
||||||
),
|
),
|
||||||
"position" => matches!(
|
"position" => matches!(
|
||||||
@@ -1908,6 +1914,16 @@ pub(crate) fn supports_declaration(name: &str, value: &str) -> bool {
|
|||||||
"clip-path" | "-webkit-clip-path" => {
|
"clip-path" | "-webkit-clip-path" => {
|
||||||
value.eq_ignore_ascii_case("none") || parse_clip_path_polygon(value).is_some()
|
value.eq_ignore_ascii_case("none") || parse_clip_path_polygon(value).is_some()
|
||||||
}
|
}
|
||||||
|
// These properties currently participate only in containing-block
|
||||||
|
// bookkeeping. Advertising an effect that is not painted is worse
|
||||||
|
// than a conservative false result because feature queries commonly
|
||||||
|
// use them to select the only visible implementation of an effect.
|
||||||
|
"filter" | "backdrop-filter" | "-webkit-backdrop-filter" | "perspective" => {
|
||||||
|
value.eq_ignore_ascii_case("none")
|
||||||
|
}
|
||||||
|
"contain" => value.eq_ignore_ascii_case("none"),
|
||||||
|
"content-visibility" => value.eq_ignore_ascii_case("visible"),
|
||||||
|
"content" => supports_content_value(value),
|
||||||
"animation-fill-mode" => parse_animation_fill_mode(value).is_some(),
|
"animation-fill-mode" => parse_animation_fill_mode(value).is_some(),
|
||||||
"animation-play-state" => parse_animation_play_state(value).is_some(),
|
"animation-play-state" => parse_animation_play_state(value).is_some(),
|
||||||
"float" => matches!(
|
"float" => matches!(
|
||||||
@@ -1997,10 +2013,380 @@ pub(crate) fn supports_declaration(name: &str, value: &str) -> bool {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
_ => true,
|
_ => supports_conservative_known_value(&name, value),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// CSS.supports() parses a declaration value, not an entire declaration list.
|
||||||
|
/// A top-level semicolon, brace, or `!important` therefore makes the overload
|
||||||
|
/// invalid. Delimiters inside strings or functions remain ordinary tokens.
|
||||||
|
fn has_invalid_supports_value_syntax(value: &str) -> bool {
|
||||||
|
let bytes = value.as_bytes();
|
||||||
|
let mut depth = 0i32;
|
||||||
|
let mut quote = None;
|
||||||
|
let mut index = 0usize;
|
||||||
|
while index < bytes.len() {
|
||||||
|
let byte = bytes[index];
|
||||||
|
if let Some(active) = quote {
|
||||||
|
if byte == b'\\' {
|
||||||
|
index += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if byte == active {
|
||||||
|
quote = None;
|
||||||
|
}
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
match byte {
|
||||||
|
b'\\' => index += 1,
|
||||||
|
b'\'' | b'"' => quote = Some(byte),
|
||||||
|
b'(' | b'[' => depth += 1,
|
||||||
|
b')' | b']' => {
|
||||||
|
depth -= 1;
|
||||||
|
if depth < 0 {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b';' | b'{' | b'}' if depth == 0 => return true,
|
||||||
|
b'!' if depth == 0
|
||||||
|
&& value[index + 1..]
|
||||||
|
.trim_start()
|
||||||
|
.get(..9)
|
||||||
|
.is_some_and(|word| word.eq_ignore_ascii_case("important")) =>
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
depth != 0 || quote.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn supports_content_value(value: &str) -> bool {
|
||||||
|
let lower = value.trim().to_ascii_lowercase();
|
||||||
|
if matches!(lower.as_str(), "none" | "normal") || supports_single_url(value) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let mut rest = value.trim();
|
||||||
|
let mut found = false;
|
||||||
|
while !rest.is_empty() {
|
||||||
|
rest = rest.trim_start();
|
||||||
|
let Some(first) = rest.chars().next() else { break };
|
||||||
|
if matches!(first, '\'' | '"') {
|
||||||
|
let mut escaped = false;
|
||||||
|
let mut end = None;
|
||||||
|
for (offset, character) in rest[first.len_utf8()..].char_indices() {
|
||||||
|
if escaped {
|
||||||
|
escaped = false;
|
||||||
|
} else if character == '\\' {
|
||||||
|
escaped = true;
|
||||||
|
} else if character == first {
|
||||||
|
end = Some(first.len_utf8() + offset + first.len_utf8());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let Some(end) = end else { return false };
|
||||||
|
rest = &rest[end..];
|
||||||
|
found = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let name_end = rest
|
||||||
|
.char_indices()
|
||||||
|
.find_map(|(index, ch)| (!ch.is_alphanumeric() && !matches!(ch, '-' | '_' | '\\')).then_some(index))
|
||||||
|
.unwrap_or(rest.len());
|
||||||
|
let name = rest[..name_end].to_ascii_lowercase();
|
||||||
|
let tail = rest[name_end..].trim_start();
|
||||||
|
if matches!(
|
||||||
|
name.as_str(),
|
||||||
|
"open-quote" | "close-quote" | "no-open-quote" | "no-close-quote"
|
||||||
|
) {
|
||||||
|
rest = tail;
|
||||||
|
found = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if !matches!(name.as_str(), "attr" | "counter" | "counters") || !tail.starts_with('(') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let mut depth = 0i32;
|
||||||
|
let mut quote = None;
|
||||||
|
let mut escaped = false;
|
||||||
|
let mut end = None;
|
||||||
|
for (offset, character) in tail.char_indices() {
|
||||||
|
if escaped {
|
||||||
|
escaped = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if character == '\\' {
|
||||||
|
escaped = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(active) = quote {
|
||||||
|
if character == active {
|
||||||
|
quote = None;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
match character {
|
||||||
|
'\'' | '"' => quote = Some(character),
|
||||||
|
'(' => depth += 1,
|
||||||
|
')' => {
|
||||||
|
depth -= 1;
|
||||||
|
if depth == 0 {
|
||||||
|
end = Some(offset + 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let Some(end) = end else { return false };
|
||||||
|
let arguments = tail[1..end - 1].trim();
|
||||||
|
if !supports_content_function(&name, arguments) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
rest = &tail[end..];
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
found
|
||||||
|
}
|
||||||
|
|
||||||
|
fn supports_single_url(value: &str) -> bool {
|
||||||
|
let value = value.trim();
|
||||||
|
if !value
|
||||||
|
.get(..4)
|
||||||
|
.is_some_and(|prefix| prefix.eq_ignore_ascii_case("url("))
|
||||||
|
|| !value.ends_with(')')
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let mut depth = 0i32;
|
||||||
|
let mut quote = None;
|
||||||
|
let mut escaped = false;
|
||||||
|
for (index, character) in value.char_indices() {
|
||||||
|
if escaped {
|
||||||
|
escaped = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if character == '\\' {
|
||||||
|
escaped = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(active) = quote {
|
||||||
|
if character == active {
|
||||||
|
quote = None;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
match character {
|
||||||
|
'\'' | '"' => quote = Some(character),
|
||||||
|
'(' => depth += 1,
|
||||||
|
')' => {
|
||||||
|
depth -= 1;
|
||||||
|
if depth == 0 {
|
||||||
|
return index + character.len_utf8() == value.len()
|
||||||
|
&& parse_url(value).is_some();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn supports_content_function(name: &str, arguments: &str) -> bool {
|
||||||
|
let arguments = split_top_level(arguments, ',');
|
||||||
|
let ident = |value: &str| {
|
||||||
|
let value = value.trim();
|
||||||
|
!value.is_empty()
|
||||||
|
&& value
|
||||||
|
.chars()
|
||||||
|
.all(|character| character.is_alphanumeric() || matches!(character, '-' | '_' | '\\'))
|
||||||
|
};
|
||||||
|
let counter_style = |value: &str| {
|
||||||
|
matches!(
|
||||||
|
value.trim().to_ascii_lowercase().as_str(),
|
||||||
|
"decimal"
|
||||||
|
| "decimal-leading-zero"
|
||||||
|
| "lower-alpha"
|
||||||
|
| "lower-latin"
|
||||||
|
| "upper-alpha"
|
||||||
|
| "upper-latin"
|
||||||
|
| "lower-roman"
|
||||||
|
| "upper-roman"
|
||||||
|
)
|
||||||
|
};
|
||||||
|
match name {
|
||||||
|
"attr" => arguments.len() == 1
|
||||||
|
&& arguments[0]
|
||||||
|
.split_whitespace()
|
||||||
|
.next()
|
||||||
|
.is_some_and(ident),
|
||||||
|
"counter" => (1..=2).contains(&arguments.len())
|
||||||
|
&& ident(arguments[0])
|
||||||
|
&& arguments.get(1).is_none_or(|value| counter_style(value)),
|
||||||
|
"counters" => (2..=3).contains(&arguments.len())
|
||||||
|
&& ident(arguments[0])
|
||||||
|
&& {
|
||||||
|
let separator = arguments[1].trim();
|
||||||
|
separator.len() >= 2
|
||||||
|
&& matches!(separator.as_bytes()[0], b'\'' | b'"')
|
||||||
|
&& separator.as_bytes().last() == separator.as_bytes().first()
|
||||||
|
}
|
||||||
|
&& arguments.get(2).is_none_or(|value| counter_style(value)),
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn supports_conservative_known_value(name: &str, value: &str) -> bool {
|
||||||
|
let lower = value.trim().to_ascii_lowercase();
|
||||||
|
let finite_number = |input: &str| {
|
||||||
|
input
|
||||||
|
.trim()
|
||||||
|
.parse::<f32>()
|
||||||
|
.ok()
|
||||||
|
.is_some_and(|number| number.is_finite())
|
||||||
|
};
|
||||||
|
let dimension = |input: &str, auto: bool| {
|
||||||
|
let input = input.trim();
|
||||||
|
(auto && input.eq_ignore_ascii_case("auto"))
|
||||||
|
|| (!matches!(dimension_value(input), crate::Dimension::Auto)
|
||||||
|
&& resolve_contextual_length(input, 16.0, 16.0, 1.0, 1.0, 100.0).is_some())
|
||||||
|
|| (input.contains('(')
|
||||||
|
&& resolve_contextual_length(input, 16.0, 16.0, 1.0, 1.0, 100.0).is_some())
|
||||||
|
};
|
||||||
|
let dimensions = |input: &str, auto: bool, max: usize| {
|
||||||
|
let tokens = split_ws_paren(input);
|
||||||
|
!tokens.is_empty() && tokens.len() <= max && tokens.iter().all(|token| dimension(token, auto))
|
||||||
|
};
|
||||||
|
match name {
|
||||||
|
"width" => lower == "fit-content" || dimension(value, true),
|
||||||
|
"height" | "min-width" | "min-height" | "max-width" | "max-height" | "flex-basis" => {
|
||||||
|
dimension(value, true)
|
||||||
|
}
|
||||||
|
"margin" | "margin-inline" | "margin-block" => dimensions(value, true, 4),
|
||||||
|
"margin-top" | "margin-right" | "margin-bottom" | "margin-left"
|
||||||
|
| "margin-inline-start" | "margin-inline-end" | "margin-block-start"
|
||||||
|
| "margin-block-end" => dimension(value, true),
|
||||||
|
"padding" | "padding-inline" | "padding-block" | "inset" => dimensions(value, false, 4),
|
||||||
|
"padding-top" | "padding-right" | "padding-bottom" | "padding-left"
|
||||||
|
| "padding-inline-start" | "padding-inline-end" | "padding-block-start"
|
||||||
|
| "padding-block-end" => dimension(value, false),
|
||||||
|
"top" | "right" | "bottom" | "left" | "inset-inline" | "inset-block" => {
|
||||||
|
dimensions(value, true, 2)
|
||||||
|
}
|
||||||
|
"inset-inline-start" | "inset-inline-end" | "inset-block-start" | "inset-block-end" => {
|
||||||
|
dimension(value, true)
|
||||||
|
}
|
||||||
|
"aspect-ratio" => lower == "auto" || parse_aspect_ratio(value).is_some(),
|
||||||
|
"align-items" | "justify-items" | "align-self" | "justify-self" => {
|
||||||
|
self_alignment_value(value).is_some()
|
||||||
|
}
|
||||||
|
"place-items" | "place-self" => self_alignment_pair(value).is_some(),
|
||||||
|
"align-content" => content_alignment_value(value).is_some(),
|
||||||
|
"justify-content" => matches!(lower.as_str(), "left" | "right") || content_alignment_value(value).is_some(),
|
||||||
|
"place-content" => content_alignment_pair(value).is_some(),
|
||||||
|
"flex-direction" => matches!(lower.as_str(), "row" | "row-reverse" | "column" | "column-reverse"),
|
||||||
|
"flex-wrap" => matches!(lower.as_str(), "nowrap" | "wrap" | "wrap-reverse"),
|
||||||
|
"flex-grow" | "flex-shrink" => finite_number(value) && value.trim().parse::<f32>().is_ok_and(|number| number >= 0.0),
|
||||||
|
"flex" => supports_flex_value(value),
|
||||||
|
"order" => value.trim().parse::<i32>().is_ok(),
|
||||||
|
"opacity" => finite_number(value),
|
||||||
|
"z-index" => lower == "auto" || value.trim().parse::<i32>().is_ok(),
|
||||||
|
"clear" => matches!(lower.as_str(), "none" | "left" | "right" | "both" | "inline-start" | "inline-end"),
|
||||||
|
"vertical-align" => matches!(lower.as_str(), "top" | "baseline" | "text-top" | "middle" | "bottom" | "text-bottom"),
|
||||||
|
"border-collapse" => matches!(lower.as_str(), "collapse" | "separate"),
|
||||||
|
"border-spacing" => dimensions(value, false, 2),
|
||||||
|
"column-count" | "-webkit-column-count" => lower == "auto" || parse_column_count(value).is_some(),
|
||||||
|
"columns" | "-webkit-columns" => split_ws_paren(value).into_iter().any(|token| parse_column_count(token).is_some()),
|
||||||
|
"break-inside" => matches!(lower.as_str(), "auto" | "avoid" | "avoid-column"),
|
||||||
|
"-webkit-column-break-inside" => matches!(lower.as_str(), "auto" | "avoid"),
|
||||||
|
"grid-auto-flow" => {
|
||||||
|
let tokens = lower.split_whitespace().collect::<Vec<_>>();
|
||||||
|
!tokens.is_empty() && tokens.len() <= 2 && tokens.iter().all(|token| matches!(*token, "row" | "column" | "dense"))
|
||||||
|
}
|
||||||
|
"grid-template-columns" | "grid-template-rows" => {
|
||||||
|
let (tracks, _) = parse_track_list_named(value);
|
||||||
|
!tracks.is_empty() || lower.starts_with("subgrid")
|
||||||
|
}
|
||||||
|
"grid-template-areas" => !parse_grid_areas(value).is_empty() || lower == "none",
|
||||||
|
"grid-column" | "grid-row" => split_top_level(value, '/').into_iter().all(|part| parse_grid_line_kind(part.trim()).is_some()),
|
||||||
|
"grid-column-start" | "grid-column-end" | "grid-row-start" | "grid-row-end" => parse_grid_line_kind(value).is_some(),
|
||||||
|
"transform-origin" => parse_transform_origin(value).is_some(),
|
||||||
|
"translate" => lower == "none" || dimensions(value, false, 3),
|
||||||
|
"scale" => lower == "none" || {
|
||||||
|
let values = value.split_whitespace().collect::<Vec<_>>();
|
||||||
|
!values.is_empty() && values.len() <= 2 && values.iter().all(|value| scale_number(value).is_some())
|
||||||
|
},
|
||||||
|
"transform" => supports_transform_value(value),
|
||||||
|
"background-color" | "color" | "-webkit-text-fill-color" => parse_color(value).is_some(),
|
||||||
|
"background-image" | "mask-image" | "-webkit-mask-image" => {
|
||||||
|
lower == "none" || parse_url(value).is_some() || !parse_background_gradient_layers(value, false).is_empty()
|
||||||
|
}
|
||||||
|
"background-repeat" | "mask-repeat" | "-webkit-mask-repeat" => parse_image_repeat(value).is_some(),
|
||||||
|
"background-size" | "mask-size" | "-webkit-mask-size" => {
|
||||||
|
matches!(lower.as_str(), "auto" | "cover" | "contain") || parse_background_size(value).is_some()
|
||||||
|
}
|
||||||
|
"background-clip" | "-webkit-background-clip" => matches!(lower.as_str(), "border-box" | "padding-box" | "content-box" | "text"),
|
||||||
|
"font-size" => is_font_size_token(value),
|
||||||
|
"font-weight" => specified_font_weight(value).is_some(),
|
||||||
|
"font-family" => !value.trim().is_empty(),
|
||||||
|
"font-style" => lower == "normal" || lower == "italic" || lower.starts_with("oblique"),
|
||||||
|
"text-align" => matches!(lower.as_str(), "left" | "right" | "start" | "end" | "center" | "justify"),
|
||||||
|
"text-transform" => matches!(lower.as_str(), "none" | "uppercase" | "lowercase" | "capitalize"),
|
||||||
|
"text-decoration" | "text-decoration-line" => lower.split_whitespace().all(|token| matches!(token, "none" | "underline")),
|
||||||
|
"line-height" => lower == "normal" || finite_number(value) || dimension(value, false),
|
||||||
|
"gap" | "grid-gap" => dimensions(value, false, 2),
|
||||||
|
"row-gap" | "grid-row-gap" | "column-gap" | "grid-column-gap" | "-webkit-column-gap" => lower == "normal" || dimension(value, false),
|
||||||
|
"counter-reset" | "counter-increment" | "counter-set" => parse_counter_directives(value, 0).is_some(),
|
||||||
|
"list-style-type" => list_style_keyword(value.trim()).is_some(),
|
||||||
|
"list-style" => value.split_whitespace().any(|token| list_style_keyword(token).is_some()),
|
||||||
|
"animation" | "animation-name" => !value.trim().is_empty(),
|
||||||
|
"background" | "font" | "grid-template" | "grid" | "box-shadow" | "-webkit-box-shadow" | "fill" | "stroke" | "stroke-width" | "letter-spacing" | "will-change" => false,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn supports_flex_value(value: &str) -> bool {
|
||||||
|
let lower = value.trim().to_ascii_lowercase();
|
||||||
|
if matches!(lower.as_str(), "none" | "auto" | "initial") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let tokens = split_ws_paren(value);
|
||||||
|
if tokens.is_empty() || tokens.len() > 3 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let mut numbers = 0usize;
|
||||||
|
for token in tokens {
|
||||||
|
if token.parse::<f32>().ok().is_some_and(|number| number.is_finite() && number >= 0.0) {
|
||||||
|
numbers += 1;
|
||||||
|
} else if matches!(dimension_value(token), crate::Dimension::Auto) && !token.eq_ignore_ascii_case("auto") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
numbers <= 2
|
||||||
|
}
|
||||||
|
|
||||||
|
fn supports_transform_value(value: &str) -> bool {
|
||||||
|
if value.trim().eq_ignore_ascii_case("none") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let functions = transform_functions(value);
|
||||||
|
!functions.is_empty() && functions.into_iter().all(|(name, arguments)| {
|
||||||
|
let values = arguments.split(',').map(str::trim).filter(|value| !value.is_empty()).collect::<Vec<_>>();
|
||||||
|
match name.to_ascii_lowercase().as_str() {
|
||||||
|
"translate" => (1..=2).contains(&values.len()) && values.iter().all(|value| !matches!(dimension_value(value), crate::Dimension::Auto)),
|
||||||
|
"translate3d" => values.len() == 3 && values.iter().all(|value| !matches!(dimension_value(value), crate::Dimension::Auto)),
|
||||||
|
"translatex" | "translatey" => values.len() == 1 && !matches!(dimension_value(values[0]), crate::Dimension::Auto),
|
||||||
|
"scale" => (1..=2).contains(&values.len()) && values.iter().all(|value| scale_number(value).is_some()),
|
||||||
|
"scalex" | "scaley" => values.len() == 1 && scale_number(values[0]).is_some(),
|
||||||
|
"rotate" | "rotatex" | "rotatey" | "rotatez" => values.len() == 1 && angle_degrees(values[0]).is_some(),
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn apply_animation_shorthand(style: &mut LayoutStyle, value: &str) {
|
fn apply_animation_shorthand(style: &mut LayoutStyle, value: &str) {
|
||||||
let first = split_top_level(value, ',').into_iter().next().unwrap_or("").trim();
|
let first = split_top_level(value, ',').into_iter().next().unwrap_or("").trim();
|
||||||
if first.is_empty() {
|
if first.is_empty() {
|
||||||
@@ -6370,6 +6756,35 @@ fn split_ws_paren(s: &str) -> Vec<&str> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn supports_reports_only_implemented_value_subsets() {
|
||||||
|
for value in ["normal", "break-all", "keep-all", "break-word"] {
|
||||||
|
assert!(supports_declaration("word-break", value), "{value}");
|
||||||
|
}
|
||||||
|
for property in ["filter", "backdrop-filter", "-webkit-backdrop-filter"] {
|
||||||
|
assert!(supports_declaration(property, "none"), "{property}");
|
||||||
|
assert!(!supports_declaration(property, "blur(2px)"), "{property}");
|
||||||
|
}
|
||||||
|
assert!(!supports_declaration("perspective", "800px"));
|
||||||
|
assert!(!supports_declaration("contain", "paint"));
|
||||||
|
assert!(!supports_declaration("content-visibility", "auto"));
|
||||||
|
for content in [
|
||||||
|
"none",
|
||||||
|
"\"new\"",
|
||||||
|
"attr(data-label)",
|
||||||
|
"counter(item) '. '",
|
||||||
|
"url(icon.svg)",
|
||||||
|
] {
|
||||||
|
assert!(supports_declaration("content", content), "{content}");
|
||||||
|
}
|
||||||
|
assert!(!supports_declaration("content", "unknown-function(x)"));
|
||||||
|
assert!(!supports_declaration("content", "counter(item, banana)"));
|
||||||
|
assert!(!supports_declaration("content", "url(icon.svg) garbage()"));
|
||||||
|
assert!(!supports_declaration("display", "grid;"));
|
||||||
|
assert!(!supports_declaration("color", "red !important"));
|
||||||
|
assert!(!supports_declaration("display", "banana"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn clip_path_polygon_supports_only_geometry_it_can_resolve() {
|
fn clip_path_polygon_supports_only_geometry_it_can_resolve() {
|
||||||
assert!(supports_declaration(
|
assert!(supports_declaration(
|
||||||
|
|||||||
Reference in new issue
Block a user