mirror of
https://github.com/lexmount/moli.git
synced 2026-09-28 16:01:39 +00:00
Split style value conversions by role
This commit is contained in:
File diff suppressed because it is too large
Load Diff
+2341
File diff suppressed because it is too large
Load Diff
+1935
File diff suppressed because it is too large
Load Diff
+551
@@ -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);
|
||||
}
|
||||
}
|
||||
+527
@@ -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,
|
||||
}
|
||||
}
|
||||
+1941
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user