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
|
||||
// select legacy-browser fallbacks, which can replace their modern cascade.
|
||||
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-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 " +
|
||||
@@ -7735,7 +7735,7 @@ const _CSS_SUPPORTED_DECLARATIONS = new Set((
|
||||
"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 " +
|
||||
"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 " +
|
||||
"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 " +
|
||||
@@ -7839,14 +7839,18 @@ function _cssTopLevelComma(text) {
|
||||
function _cssSupportsDeclaration(name, value) {
|
||||
name = name.trim().toLowerCase();
|
||||
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 (!_CSS_SUPPORTED_DECLARATIONS.has(name)) return false;
|
||||
const lower = value.toLowerCase();
|
||||
if (["initial", "inherit", "unset", "revert", "revert-layer"].includes(lower)) return true;
|
||||
if (name === "display") {
|
||||
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") {
|
||||
return ["static", "relative", "absolute", "fixed", "sticky"].includes(lower);
|
||||
@@ -7860,10 +7864,190 @@ function _cssSupportsDeclaration(name, value) {
|
||||
if (name === "scrollbar-gutter") {
|
||||
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)) {
|
||||
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
|
||||
@@ -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();
|
||||
if (!condition) return false;
|
||||
if (!condition || !_cssBalancedSupportsSyntax(condition)) return null;
|
||||
const grouped = _cssEnclosingGroup(condition);
|
||||
if (grouped !== null) return _cssSupportsCondition(grouped);
|
||||
if (/^not\s/i.test(condition)) return !_cssSupportsCondition(condition.slice(3).trim());
|
||||
if (grouped !== null) return _cssSupportsConditionResult(grouped);
|
||||
if (/^not\s/i.test(condition)) {
|
||||
const result = _cssSupportsConditionResult(condition.slice(3).trim());
|
||||
return result === null ? null : !result;
|
||||
}
|
||||
const orParts = _cssSplitTopLevel(condition, "or");
|
||||
if (orParts) return orParts.some(_cssSupportsCondition);
|
||||
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(")")) {
|
||||
return _cssSupportsSelector(condition.slice(condition.indexOf("(") + 1, -1));
|
||||
}
|
||||
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));
|
||||
}
|
||||
|
||||
function _cssSupportsCondition(condition) {
|
||||
return _cssSupportsConditionResult(condition) === true;
|
||||
}
|
||||
|
||||
globalThis.CSS = {
|
||||
supports(prop, value){
|
||||
try {
|
||||
|
||||
@@ -2457,6 +2457,7 @@ pub fn build_extension() -> Extension {
|
||||
ops.push(op_image_metadata());
|
||||
ops.push(op_layout_geometry());
|
||||
ops.push(op_computed_style());
|
||||
ops.push(op_css_supports());
|
||||
ops.push(op_layout_metrics());
|
||||
ops.push(op_scroll_offset());
|
||||
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()
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// render builds; default scraping builds retain their deliberately unbounded
|
||||
/// 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(red, light-dark(white, black)))"),
|
||||
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();
|
||||
assert_eq!(
|
||||
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]")
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user