Split style value conversions by role

This commit is contained in:
ldm0
2026-09-28 13:39:59 +08:00
parent 76956aa40f
commit 2f3a7e4bfc
6 changed files with 7326 additions and 7253 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,551 @@
use super::*;
pub(in crate::native_bridge::element::styles) fn style_property_priority(
runtime: &JsContextHost,
handle: DomHandle,
property: &str,
) -> String {
let Some(property) = canonical_specified_cssom_query_property_name(property) else {
return String::new();
};
if let Some(state) = runtime.element_inline_style_declaration_state(handle)
&& let Some(priority) = inline_state_property_priority_with_pdb(state, &property)
{
return if priority {
"important".to_owned()
} else {
String::new()
};
}
let entries = style_entries(runtime, handle);
if let Some(priority) = style_entries_property_priority_with_pdb(&entries, &property) {
return if priority {
"important".to_owned()
} else {
String::new()
};
}
if let Some(entry) = inline_style_entry(runtime, handle, &property) {
return if entry.priority {
"important".to_owned()
} else {
String::new()
};
}
if let Some(longhands) = shorthand_longhands(&property) {
let mut priority = None;
for longhand in longhands {
let Some(entry) = inline_style_entry(runtime, handle, longhand) else {
return String::new();
};
if priority.is_some_and(|current| current != entry.priority) {
return String::new();
}
priority = Some(entry.priority);
}
if priority == Some(true) {
return "important".to_owned();
}
}
String::new()
}
pub(super) fn canonical_specified_cssom_query_property_name(property: &str) -> Option<String> {
if property.starts_with("--") && !moli_css_parse::is_cssom_custom_property_name(property) {
return None;
}
let property = canonical_style_property_name(property);
if !property.starts_with("--") && !known_style_property(&property) {
return None;
}
Some(if property == "-webkit-transform" {
"transform".to_owned()
} else {
property
})
}
pub(super) fn canonical_computed_cssom_query_property_name(property: &str) -> Option<String> {
if property.starts_with("--") {
return moli_css_parse::is_cssom_custom_property_name(property)
.then(|| property.to_owned());
}
let property = canonical_style_property_name(property);
let property = if property == "-webkit-transform" {
"transform".to_owned()
} else {
property
};
computed_property_is_queryable(&property).then_some(property)
}
pub(in crate::native_bridge::element::styles) fn style_css_text_for_computed(
runtime: &JsContextHost,
handle: DomHandle,
) -> String {
let _ = (runtime, handle);
String::new()
}
pub(super) fn specified_color_value_is_valid(value: &str) -> bool {
let value = value.trim();
if moli_css_parse::css_value_may_contain_env_function(value) {
return moli_css_parse::css_declaration_value_has_valid_env_functions(value);
}
matches!(
value.to_ascii_lowercase().as_str(),
"inherit" | "initial" | "unset" | "revert" | "revert-layer" | "revert-rule"
) || specified_color_component_value_is_valid(value)
}
fn specified_color_component_value_is_valid(value: &str) -> bool {
let value = value.trim();
value.eq_ignore_ascii_case("transparent")
|| value.eq_ignore_ascii_case("currentcolor")
|| ident_is_system_color(value)
|| css_named_color_rgb(value).is_some()
|| css_hex_color_rgb(value).is_some()
|| ((value.starts_with("rgb(") || value.starts_with("rgba(")) && value.ends_with(')'))
}
pub(in crate::native_bridge::element::styles) fn style_property_names_with_context(
runtime: &JsContextHost,
handle: DomHandle,
mode: StyleMode,
context: StyleComputationContext,
) -> Vec<String> {
if mode == StyleMode::Computed {
return super::super::super::computed_names::computed_property_names(
runtime, handle, context,
);
}
if let Some(state) = runtime.element_inline_style_declaration_state(handle) {
return state.property_names();
}
style_entries(runtime, handle)
.into_iter()
.map(|entry| entry.name)
.collect()
}
pub(in crate::native_bridge::element::styles) fn style_property_count_with_context(
runtime: &JsContextHost,
handle: DomHandle,
mode: StyleMode,
context: StyleComputationContext,
) -> usize {
if mode == StyleMode::Computed {
return super::super::super::computed_names::computed_property_count(
runtime, handle, context,
);
}
style_property_names_with_context(runtime, handle, mode, context).len()
}
pub(in crate::native_bridge::element::styles) fn style_property_name_at_with_context(
runtime: &JsContextHost,
handle: DomHandle,
mode: StyleMode,
context: StyleComputationContext,
index: usize,
) -> Option<String> {
if mode == StyleMode::Computed {
return super::super::super::computed_names::computed_property_name_at(
runtime, handle, context, index,
);
}
style_property_names_with_context(runtime, handle, mode, context)
.get(index)
.cloned()
}
pub(in crate::native_bridge::element::styles) fn style_property_index_exists_with_context(
runtime: &JsContextHost,
handle: DomHandle,
mode: StyleMode,
context: StyleComputationContext,
index: usize,
) -> bool {
if mode == StyleMode::Computed {
return super::super::super::computed_names::computed_property_name_at(
runtime, handle, context, index,
)
.is_some();
}
index < style_property_count_with_context(runtime, handle, mode, context)
}
pub(in crate::native_bridge::element::styles) fn computed_style_applies(
runtime: &JsContextHost,
handle: DomHandle,
) -> bool {
runtime
.dom_host()
.containing_shadow_root(handle)
.is_none_or(|shadow_root| !runtime.shadow_root_is_disconnected_for_style(shadow_root))
}
#[cfg(test)]
mod tests {
use super::super::super::style_world::connected_shadow_roots_for_test;
use super::{
KEYFRAME_NESTING_DEPTH_LIMIT, animation_shorthand_names, box_shorthand_component,
collect_custom_functions_from_css, compress_box_shorthand_value,
custom_function_container_rule_texts, format_css_number, format_css_px,
keyframe_has_supported_animation_values, keyframe_property_values,
normalize_computed_color_functions, normalize_css_integer_token, normalize_style_value,
simple_var_function_parts,
};
use crate::dom::native::{DomHost, NativeDom};
use crate::native_bridge::DomHandle;
use std::collections::HashMap;
fn test_host() -> DomHost {
DomHost::from_dom(NativeDom::new_html(
url::Url::parse("https://example.test/").expect("valid test url"),
))
}
fn connect_for_test(host: &mut DomHost, parent: DomHandle, child: DomHandle) {
assert!(host.append_child_without_mutation_effects(parent, child));
}
fn nested_keyframes(depth: usize) -> String {
let mut css = "@keyframes anim { from { left: 0px; } to { left: 10px; } }".to_owned();
for _ in 0..depth {
css = format!("@media all {{ {css} }}");
}
css
}
#[test]
fn css_number_serialization_discards_bounded_f32_integer_noise() {
assert_eq!(format_css_number(120.000005), "120");
assert_eq!(format_css_number(-120.000005), "-120");
assert_eq!(format_css_number(120.00005), "120.00005");
}
#[test]
fn css_pixel_serialization_matches_blink_six_significant_digits() {
assert_eq!(format_css_px(33.328_125), "33.3281px");
assert_eq!(format_css_px(0.015_625), "0.015625px");
assert_eq!(format_css_px(999_999.0), "999999px");
assert_eq!(format_css_px(1_000_000.0), "1e+06px");
}
#[test]
fn connected_shadow_roots_for_document_excludes_child_document_roots() {
let mut host = test_host();
let document = host.document_handle();
let active_host = host.create_element("section");
connect_for_test(&mut host, document, active_host);
let active_root = host
.attach_shadow_root(active_host, "open")
.expect("active document host should accept shadow root");
let child_document = host.create_detached_html_document();
let child_host = host.create_parser_element_without_attributes_for_document(
child_document,
"article".to_owned(),
"http://www.w3.org/1999/xhtml".to_owned(),
None,
);
connect_for_test(&mut host, child_document, child_host);
let child_root = host
.attach_shadow_root(child_host, "open")
.expect("child document host should accept shadow root");
host.mark_subtree_connected_preserving_owner_document(child_document);
assert_eq!(
connected_shadow_roots_for_test(&host, document),
vec![active_root]
);
assert_eq!(
connected_shadow_roots_for_test(&host, child_document),
vec![child_root]
);
}
#[test]
fn box_shorthand_component_keeps_function_whitespace_internal() {
assert_eq!(
box_shorthand_component("calc(10px + 5px) auto", 0).as_deref(),
Some("calc(10px + 5px)")
);
assert_eq!(
box_shorthand_component("calc(10px + 5px) auto", 1).as_deref(),
Some("auto")
);
assert_eq!(
box_shorthand_component("calc(50% + 2px) 4px", 0).as_deref(),
Some("calc(50% + 2px)")
);
assert_eq!(
box_shorthand_component("4px calc(50% + 2px)", 1).as_deref(),
Some("calc(50% + 2px)")
);
}
#[test]
fn font_shorthand_slash_normalization_stays_renderer_local() {
assert_eq!(
normalize_style_value("font", "10px/1 Ahem"),
"10px / 1 Ahem"
);
assert_eq!(
normalize_style_value("font", "var(--font/size) / var(--line/height) Ahem"),
"var(--font/size) / var(--line/height) Ahem"
);
}
#[test]
fn content_value_normalization_stays_renderer_local() {
assert_eq!(normalize_style_value("content", "'string'"), r#""string""#);
assert_eq!(
normalize_style_value("content", "url(http://localhost/)"),
r#"url("http://localhost/")"#
);
assert_eq!(
normalize_style_value("content", "counter(par-num, decimal)"),
"counter(par-num)"
);
assert_eq!(
normalize_style_value("content", "attr( |bar )"),
"attr( |bar )"
);
}
#[test]
fn font_family_value_normalization_stays_renderer_local() {
assert_eq!(
normalize_style_value("font-family", "'Lucida Grande'"),
"Lucida Grande"
);
assert_eq!(normalize_style_value("font-family", "'34J'"), r#""34J""#);
assert_eq!(
normalize_style_value("font-family", "'serif'"),
r#""serif""#
);
assert_eq!(normalize_style_value("font-family", "'A B'"), r#""A B""#);
}
#[test]
fn css_integer_token_normalization_stays_renderer_local() {
assert_eq!(
normalize_css_integer_token("1111111111111111111111111").as_deref(),
Some("1111111111111111111111111")
);
assert_eq!(normalize_css_integer_token("+0012").as_deref(), Some("12"));
assert_eq!(normalize_css_integer_token("-000").as_deref(), Some("0"));
assert_eq!(normalize_css_integer_token("1.0"), None);
assert_eq!(normalize_css_integer_token("1px"), None);
}
#[test]
fn simple_var_function_projection_stays_renderer_local() {
assert_eq!(
simple_var_function_parts("var(--x)")
.filter(|parts| parts.fallback.is_none())
.map(|parts| parts.name)
.as_deref(),
Some("--x")
);
assert_eq!(
simple_var_function_parts("var(--x, red)")
.and_then(|parts| parts.fallback)
.as_deref(),
Some("red")
);
assert_eq!(
simple_var_function_parts(" var( --x ) ")
.filter(|parts| parts.fallback.is_none())
.map(|parts| parts.name)
.as_deref(),
Some("--x")
);
assert!(
simple_var_function_parts("var(--x, 1)").is_some_and(|parts| parts.fallback.is_some())
);
assert_eq!(simple_var_function_parts("var(x)"), None);
assert_eq!(simple_var_function_parts("calc(var(--x))"), None);
assert_eq!(normalize_style_value("color", "var(--x)"), "var(--x)");
assert_eq!(normalize_style_value("z-index", "var(--z)"), "var(--z)");
}
#[test]
fn animation_shorthand_name_projection_stays_renderer_local() {
assert_eq!(
animation_shorthand_names("1s linear infinite alternate anim"),
vec!["anim"]
);
assert_eq!(
animation_shorthand_names("spin 1s ease, 200ms fade-in forwards"),
vec!["spin", "fade-in"]
);
assert!(animation_shorthand_names("none").is_empty());
}
#[test]
fn anchor_size_normalization_stays_renderer_local() {
assert_eq!(
normalize_style_value(
"width",
"anchor-size(width, anchor-size(--foo height, 10px))"
),
"anchor-size(width, anchor-size(--foo height, 10px))"
);
assert_eq!(
normalize_style_value("width", "anchor-size(width --target)"),
"anchor-size(--target width)"
);
}
#[test]
fn custom_css_function_parser_extracts_container_results() {
let mut functions = HashMap::new();
collect_custom_functions_from_css(
r#"
@function --b() {
@container --cont (width = 5px) { result: 5px; }
@container --cont (width = 10px) { result: 10px; }
}
"#,
&mut functions,
);
let function = functions.get("--b").expect("custom function");
assert_eq!(function.container_results.len(), 2);
assert_eq!(function.container_results[0].container_name, "--cont");
assert_eq!(function.container_results[0].width_px, 5.0);
assert_eq!(function.container_results[0].result, "5px");
}
#[test]
fn custom_css_function_container_projection_uses_rule_local_css_text() {
let rules = custom_function_container_rule_texts(
r#"
@container --cont (width = 5px) { result: 5px; }
@container --next (width = 10px) { result: 10px; }
"#,
);
assert_eq!(rules.len(), 2);
assert_eq!(
rules[0].css_text,
"@container --cont (width = 5px) {result: 5px;}"
);
assert!(
!rules[0].css_text.contains("--next"),
"custom-function container projection must not capture trailing rules"
);
}
#[test]
fn compress_box_shorthand_value_keeps_function_components_intact() {
assert_eq!(
compress_box_shorthand_value("calc(5px + 5px) 1px calc(5px + 5px) 1px"),
"calc(5px + 5px) 1px"
);
assert_eq!(
compress_box_shorthand_value(
"calc(5px + 5px) calc(5px + 5px) calc(5px + 5px) calc(5px + 5px)"
),
"calc(5px + 5px)"
);
}
#[test]
fn normalize_computed_color_functions_replaces_only_full_system_color_tokens() {
assert_eq!(
normalize_computed_color_functions("1px 1px MenuText", None),
"rgb(0, 0, 0) 1px 1px"
);
assert_eq!(
normalize_computed_color_functions("1px 1px menutext", None),
"rgb(0, 0, 0) 1px 1px"
);
assert_eq!(
normalize_computed_color_functions("1px 1px NotMenuText", None),
"1px 1px NotMenuText"
);
assert_eq!(
normalize_computed_color_functions("1px 1px menutext, 2px 2px linktext", None),
"rgb(0, 0, 0) 1px 1px, rgb(0, 0, 238) 2px 2px"
);
assert_eq!(
normalize_computed_color_functions(
"1px 1px color-mix(in srgb, rgb(0,0,0), white)",
None
),
"1px 1px color-mix(in srgb, rgb(0,0,0), white)"
);
assert_eq!(
normalize_computed_color_functions(
"1px 1px currentcolor, 2px 2px LinkText",
Some("rgb(10, 20, 30)")
),
"rgb(10, 20, 30) 1px 1px, rgb(0, 0, 238) 2px 2px"
);
}
#[test]
fn keyframe_supported_animation_scan_has_depth_limit() {
let names = vec!["anim".to_owned()];
assert!(keyframe_has_supported_animation_values(
&nested_keyframes(KEYFRAME_NESTING_DEPTH_LIMIT),
&names
));
assert!(!keyframe_has_supported_animation_values(
&nested_keyframes(KEYFRAME_NESTING_DEPTH_LIMIT + 1),
&names
));
}
#[test]
fn keyframe_animation_scan_uses_stylo_nested_rule_snapshots() {
let names = vec!["move".to_owned()];
let css = r#"
@media all {
@supports (display: block) {
@keyframes move {
from { left: 0px; }
to { left: 20px; }
}
}
}
"#;
assert!(keyframe_has_supported_animation_values(css, &names));
assert_eq!(
keyframe_property_values(css, &names, "left"),
Some(("0px".to_owned(), "20px".to_owned()))
);
}
#[test]
fn keyframe_animation_scan_uses_pdb_declaration_values() {
let names = vec!["fade".to_owned()];
let css = r#"
@keyframes fade {
from {
background-color: rgb(0 128 0 / 50%);
width: calc(7px * up);
}
to {
background-color: rgb(0 128 0 / 50%);
width: calc(10px + 1vmin + 10%);
}
}
"#;
assert!(keyframe_has_supported_animation_values(css, &names));
assert_eq!(
keyframe_property_values(css, &names, "background-color"),
Some((
"rgba(0, 128, 0, 0.5)".to_owned(),
"rgba(0, 128, 0, 0.5)".to_owned()
))
);
assert_eq!(keyframe_property_values(css, &names, "width"), None);
}
}
@@ -0,0 +1,527 @@
use super::*;
pub(super) fn resolve_computed_custom_function_calls(
runtime: &JsContextHost,
handle: DomHandle,
property: &str,
value: &str,
inputs: &FullStyleWorldSnapshot,
context: StyleComputationContext,
) -> String {
if !property.starts_with("--") || !value.contains("--") || !value.contains("()") {
return value.to_owned();
}
let calls = dashed_no_arg_function_calls(value);
if calls.is_empty() {
return value.to_owned();
}
let scope = computed_custom_property_source_scope(
runtime,
handle,
property,
value,
inputs,
context.viewport_width(),
)
.or_else(|| runtime.dom_host().containing_shadow_root(handle));
let Some(scope) = scope else {
return value.to_owned();
};
let functions = visible_custom_functions_for_scope(runtime, inputs, scope, context.viewport());
if functions.is_empty() {
return value.to_owned();
}
let mut resolved = value.to_owned();
let mut changed = false;
for call in calls {
let Some(function) = functions.get(call.as_str()) else {
continue;
};
let Some(result) = evaluate_custom_function(runtime, handle, function, context) else {
continue;
};
resolved = resolved.replace(&format!("{call}()"), &result);
changed = true;
}
if changed { resolved } else { value.to_owned() }
}
pub(super) fn dashed_no_arg_function_calls(value: &str) -> Vec<String> {
let mut calls = Vec::new();
let bytes = value.as_bytes();
let mut index = 0;
while index + 4 <= bytes.len() {
if bytes[index] != b'-' || bytes.get(index + 1) != Some(&b'-') {
index += 1;
continue;
}
let start = index;
index += 2;
while index < bytes.len() && css_identifier_byte(bytes[index]) {
index += 1;
}
if index == start + 2 || bytes.get(index) != Some(&b'(') {
continue;
}
let mut close = index + 1;
while close < bytes.len() && bytes[close].is_ascii_whitespace() {
close += 1;
}
if bytes.get(close) != Some(&b')') {
continue;
}
let name = &value[start..index];
if !calls.iter().any(|call| call == name) {
calls.push(name.to_owned());
}
index = close + 1;
}
calls
}
fn css_identifier_byte(byte: u8) -> bool {
byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_')
}
fn computed_custom_property_source_scope(
runtime: &JsContextHost,
handle: DomHandle,
property: &str,
value: &str,
inputs: &FullStyleWorldSnapshot,
viewport_width: Option<f64>,
) -> Option<DomHandle> {
for (root, sources) in inputs.shadow_stylesheet_sources.iter().rev() {
if stylesheet_sources_compute_custom_property_value(
runtime,
handle,
property,
value,
inputs,
Some((*root, sources)),
viewport_width,
) {
return Some(*root);
}
}
if stylesheet_sources_compute_custom_property_value(
runtime,
handle,
property,
value,
inputs,
None,
viewport_width,
) {
return runtime.dom_host().owner_document_handle(handle);
}
None
}
fn stylesheet_sources_compute_custom_property_value(
runtime: &JsContextHost,
handle: DomHandle,
property: &str,
value: &str,
inputs: &FullStyleWorldSnapshot,
scoped_sources: Option<(DomHandle, &[StyloStylesheetSource])>,
viewport_width: Option<f64>,
) -> bool {
let mut scoped_inputs = FullStyleWorldSnapshot {
document_stylesheet_sources: Vec::new(),
shadow_stylesheet_sources: Vec::new(),
script_custom_property_registrations: inputs.script_custom_property_registrations.clone(),
environment: inputs.environment,
quirks_mode: inputs.quirks_mode,
};
if let Some((root, sources)) = scoped_sources {
scoped_inputs
.shadow_stylesheet_sources
.push((root, sources.to_vec()));
} else {
scoped_inputs.document_stylesheet_sources = inputs.document_stylesheet_sources.clone();
}
let viewport = StyleViewport {
width: viewport_width.or_else(|| runtime.style_viewport().width),
..runtime.style_viewport()
};
let Some(read_document) = runtime.dom_host().owner_document_handle(handle) else {
return false;
};
runtime
.computed_style_property_value_from_ephemeral_stylo(
handle,
property,
&scoped_inputs,
read_document,
viewport,
)
.is_some_and(|candidate| candidate.trim() == value.trim())
}
#[derive(Clone, Debug)]
pub(super) struct CustomCssFunction {
pub(super) result: Option<String>,
pub(super) container_results: Vec<CustomCssFunctionContainerResult>,
}
#[derive(Clone, Debug)]
pub(super) struct CustomCssFunctionContainerResult {
pub(super) container_name: String,
pub(super) width_px: f64,
pub(super) result: String,
}
struct CustomFunctionRuleText {
pub(super) name: String,
pub(super) block: String,
}
pub(super) struct CustomFunctionContainerRuleText {
pub(super) css_text: String,
pub(super) block: String,
}
fn visible_custom_functions_for_scope(
runtime: &JsContextHost,
inputs: &FullStyleWorldSnapshot,
scope: DomHandle,
viewport: StyleViewport,
) -> HashMap<String, CustomCssFunction> {
let mut functions = HashMap::new();
for source in inputs
.document_stylesheet_sources
.iter()
.filter(|source| source.media_matches(runtime.emulated_media(), viewport))
{
collect_custom_functions_from_stylesheet_source(runtime, source, &mut functions);
}
if runtime
.dom_host()
.node(scope)
.is_some_and(Node::is_document)
{
return functions;
}
let shadow_chain = shadow_root_ancestor_chain(runtime, scope);
for (root, sources) in &inputs.shadow_stylesheet_sources {
if shadow_chain.contains(root) {
for source in sources
.iter()
.filter(|source| source.media_matches(runtime.emulated_media(), viewport))
{
collect_custom_functions_from_stylesheet_source(runtime, source, &mut functions);
}
}
}
functions
}
fn collect_custom_functions_from_stylesheet_source(
runtime: &JsContextHost,
source: &StyloStylesheetSource,
functions: &mut HashMap<String, CustomCssFunction>,
) {
if let Some(owner) = source.owner_style_sheet_owner()
&& let Some(processing_source) = runtime.owner_style_sheet_processing_source(owner)
{
// `@function` is a renderer compatibility extension that Stylo does not
// retain in its parsed rule tree. Inline owners therefore project this
// extension from their immutable processing input while cascade and
// CSSOM continue to share the live Stylo stylesheet.
collect_custom_functions_from_css(processing_source.css_text(), functions);
return;
}
collect_custom_functions_from_css(&source.serialized_css_text(), functions);
}
fn shadow_root_ancestor_chain(runtime: &JsContextHost, scope: DomHandle) -> Vec<DomHandle> {
let mut chain = Vec::new();
let mut current = Some(scope);
while let Some(root) = current {
if !runtime.dom_host().is_shadow_root(root) {
break;
}
chain.push(root);
current = runtime
.dom_host()
.shadow_root_host(root)
.and_then(|host| runtime.dom_host().containing_shadow_root(host));
}
chain.reverse();
chain
}
pub(super) fn collect_custom_functions_from_css(
css_text: &str,
functions: &mut HashMap<String, CustomCssFunction>,
) {
for rule in custom_function_rule_texts(css_text) {
if let Some(function) = parse_custom_css_function(&rule.block) {
functions.insert(rule.name, function);
}
}
}
fn custom_function_name(prelude: &str) -> Option<String> {
let trimmed = prelude.trim();
let open = trimmed.find('(')?;
let name = trimmed[..open].trim();
let after_open = &trimmed[open + 1..];
let close = after_open.find(')')?;
if !after_open[..close].trim().is_empty() {
return None;
}
if !name.starts_with("--") || name.len() == 2 {
return None;
}
Some(name.to_owned())
}
fn parse_custom_css_function(block: &str) -> Option<CustomCssFunction> {
let declarations = moli_css_parse::parse_declaration_list(
block,
moli_css_parse::DeclarationParseOptions {
canonicalize_property_name: false,
unescape_value_semicolons: true,
preserve_empty_values: false,
},
);
let result = declarations
.into_iter()
.rev()
.find(|declaration| declaration.name.eq_ignore_ascii_case("result"))
.map(|declaration| declaration.value);
let mut container_results = Vec::new();
for rule in custom_function_container_rule_texts(block) {
let Some(container) = parse_custom_function_container_rule(&rule) else {
continue;
};
container_results.push(container);
}
if result.is_none() && container_results.is_empty() {
return None;
}
Some(CustomCssFunction {
result,
container_results,
})
}
fn parse_custom_function_container_rule(
rule: &CustomFunctionContainerRuleText,
) -> Option<CustomCssFunctionContainerResult> {
let view = moli_css_parse::parse_condition_rule_view_with_stylo(&rule.css_text)?;
if view.rule_type != CssRuleType::Container {
return None;
}
let width_px = container_query_width_equality_px(view.container_query.as_deref()?)?;
let declarations = moli_css_parse::parse_declaration_list(
&rule.block,
moli_css_parse::DeclarationParseOptions {
canonicalize_property_name: false,
unescape_value_semicolons: true,
preserve_empty_values: false,
},
);
let result = declarations
.into_iter()
.rev()
.find(|declaration| declaration.name.eq_ignore_ascii_case("result"))?
.value;
Some(CustomCssFunctionContainerResult {
container_name: view.container_name.unwrap_or_default(),
width_px,
result,
})
}
fn custom_function_rule_texts(css_text: &str) -> Vec<CustomFunctionRuleText> {
custom_css_projection_at_rules(css_text)
.into_iter()
.filter_map(|rule| {
if !rule.name.eq_ignore_ascii_case("function") {
return None;
}
Some(CustomFunctionRuleText {
name: custom_function_name(&rule.prelude)?,
block: rule.block?,
})
})
.collect()
}
pub(super) fn custom_function_container_rule_texts(
block: &str,
) -> Vec<CustomFunctionContainerRuleText> {
custom_css_projection_at_rules(block)
.into_iter()
.filter_map(|rule| {
if !rule.name.eq_ignore_ascii_case("container") {
return None;
}
Some(CustomFunctionContainerRuleText {
css_text: rule.css_text,
block: rule.block?,
})
})
.collect()
}
fn container_query_width_equality_px(query: &str) -> Option<f64> {
let query = query.trim();
let inner = query.strip_prefix('(')?.strip_suffix(')')?.trim();
let (feature, value) = inner.split_once('=')?;
if feature.trim() != "width" {
return None;
}
moli_css_parse::parse_px_length(value, moli_css_parse::UnitlessLength::ZeroOnly)
}
fn evaluate_custom_function(
runtime: &JsContextHost,
handle: DomHandle,
function: &CustomCssFunction,
context: StyleComputationContext,
) -> Option<String> {
for container in &function.container_results {
if named_container_width(runtime, handle, &container.container_name, context)
.is_some_and(|width| css_px_values_equal(width, container.width_px))
{
return Some(container.result.clone());
}
}
function.result.clone()
}
fn named_container_width(
runtime: &JsContextHost,
handle: DomHandle,
container_name: &str,
context: StyleComputationContext,
) -> Option<f64> {
let mut current = flat_tree_element_parent(runtime, handle);
let mut visited = HashSet::new();
while let Some(candidate) = current {
if !visited.insert(candidate) {
return None;
}
if element_is_named_size_container(runtime, candidate, container_name, context) {
return inline_width_px(runtime, candidate);
}
current = flat_tree_element_parent(runtime, candidate);
}
None
}
pub(super) fn flat_tree_element_parent(
runtime: &JsContextHost,
handle: DomHandle,
) -> Option<DomHandle> {
if let Some(slot) = runtime.dom_host().assigned_slot_for_node(handle) {
return runtime
.dom_host()
.node(slot)
.is_some_and(Node::is_element)
.then_some(slot);
}
if runtime.dom_host().is_shadow_root(handle) {
return runtime.dom_host().shadow_root_host(handle);
}
let parent = runtime
.dom_host()
.node(handle)
.and_then(Node::parent_node)?;
if runtime.dom_host().is_shadow_root(parent) {
return runtime.dom_host().shadow_root_host(parent);
}
runtime
.dom_host()
.node(parent)
.is_some_and(Node::is_element)
.then_some(parent)
}
fn element_is_named_size_container(
runtime: &JsContextHost,
handle: DomHandle,
container_name: &str,
context: StyleComputationContext,
) -> bool {
let name = style_property_value_with_context(
runtime,
handle,
StyleMode::Computed,
"container-name",
context,
);
if !container_name_list_contains(&name, container_name) {
return false;
}
let ty = style_property_value_with_context(
runtime,
handle,
StyleMode::Computed,
"container-type",
context,
);
container_type_is_size_container(&ty)
}
fn container_name_list_contains(value: &str, container_name: &str) -> bool {
value.split_whitespace().any(|name| name == container_name)
}
pub(super) fn container_type_is_size_container(value: &str) -> bool {
value
.split_whitespace()
.any(|ty| matches!(ty, "size" | "inline-size"))
}
fn inline_width_px(runtime: &JsContextHost, handle: DomHandle) -> Option<f64> {
let read = ComputedStyleRead::new(runtime, handle);
inline_width_px_with_resolution(runtime, handle, read.resolution_context())
}
pub(super) fn inline_width_px_with_resolution(
runtime: &JsContextHost,
handle: DomHandle,
resolution: StyleResolutionContext<'_>,
) -> Option<f64> {
let inline = style_property_value(runtime, handle, StyleMode::Inline, "width");
let computed = if inline.is_empty() {
resolution.computed_property(runtime, handle, "width")
} else {
inline
};
moli_css_parse::parse_px_length(&computed, moli_css_parse::UnitlessLength::ZeroOnly)
}
fn css_px_values_equal(left: f64, right: f64) -> bool {
(left - right).abs() < 0.001
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) enum HiddenAttributeState {
Missing,
Hidden,
UntilFound,
}
pub(super) fn element_hidden_attribute_state(
runtime: &JsContextHost,
handle: DomHandle,
) -> HiddenAttributeState {
let value = runtime
.dom_host()
.node(handle)
.and_then(Node::as_element)
.and_then(|element| element.attribute("hidden"));
match value {
None => HiddenAttributeState::Missing,
Some(value) if value.eq_ignore_ascii_case("until-found") => {
HiddenAttributeState::UntilFound
}
Some(_) => HiddenAttributeState::Hidden,
}
}