diff --git a/moli-renderer-v8/src/script_vm/tests/dom_xhr/computed_style/advanced_style_values.rs b/moli-renderer-v8/src/script_vm/tests/dom_xhr/computed_style/advanced_style_values.rs index 43e4beb6db..84d2bbe2b2 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_xhr/computed_style/advanced_style_values.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_xhr/computed_style/advanced_style_values.rs @@ -674,6 +674,51 @@ fn computed_style_resolves_valid_typed_css_math_and_rejects_invalid_unit_algebra ); } +#[test] +fn object_dimension_attributes_participate_in_the_css_cascade() { + let mut vm = new_parsed_test_vm( + "https://object-dimension-presentation-hints.test/", + r#" + + + "#, + ); + + let result = vm + .eval( + r#" +(() => { + const target = document.getElementById('target'); + const authorRule = document.getElementById('author-rule'); + const size = element => { + const computed = getComputedStyle(element); + return [computed.width, computed.height]; + }; + const values = [...size(target)]; + + target.setAttribute('width', '12.5px'); + target.setAttribute('height', '25.%ignored'); + values.push(...size(target)); + + target.style.width = '9px'; + target.removeAttribute('width'); + values.push(...size(target)); + target.style.removeProperty('width'); + target.removeAttribute('height'); + values.push(...size(target)); + values.push(...size(authorRule)); + return JSON.stringify(values); +})() +"#, + ) + .expect("object dimension presentation hints should evaluate"); + + assert_eq!( + result, + r#"["100px","50px","12.5px","25%","9px","25%","auto","auto","70px","80px"]"# + ); +} + #[test] fn svg_generic_presentation_attributes_apply_to_every_svg_element() { let mut vm = new_parsed_test_vm( diff --git a/moli-selector/src/stylo/presentation.rs b/moli-selector/src/stylo/presentation.rs index f8e9e2d3b1..7b2794021c 100644 --- a/moli-selector/src/stylo/presentation.rs +++ b/moli-selector/src/stylo/presentation.rs @@ -163,6 +163,9 @@ impl QueryElement<'_> { ); } } else if element.namespace() == HTML_NAMESPACE { + if element.local_name() == "object" { + append_html_object_dimension_declarations(element, &mut block); + } append_html_table_presentation_declarations(*self, &mut block); } @@ -658,6 +661,75 @@ fn append_html_table_cell_padding_declarations( } } +fn append_html_object_dimension_declarations( + element: &Element, + block: &mut PropertyDeclarationBlock, +) { + for (attribute, is_width) in [("width", true), ("height", false)] { + let Some(value) = element.attribute(attribute) else { + continue; + }; + let Some(dimension) = parse_html_dimension_attribute(value) else { + continue; + }; + use style::values::generics::length::Size; + let size = match dimension { + HtmlDimension::Pixels(value) => LengthPercentage::Length(NoCalcLength::from_px(value)), + HtmlDimension::Percentage(value) => { + LengthPercentage::Percentage(NoCalcPercentage::new(value / 100.0)) + } + }; + let size = Size::LengthPercentage(NonNegative(size)); + let declaration = if is_width { + PropertyDeclaration::Width(size) + } else { + PropertyDeclaration::Height(size) + }; + block.push(declaration, Importance::Normal); + } +} + +#[derive(Clone, Copy, Debug, PartialEq)] +enum HtmlDimension { + Pixels(f32), + Percentage(f32), +} + +/// Parses an HTML dimension value as a non-negative CSS pixel length or +/// percentage. HTML's legacy algorithm deliberately accepts trailing junk, +/// but requires the value itself to start with an ASCII digit after leading +/// ASCII whitespace. +fn parse_html_dimension_attribute(value: &str) -> Option { + let value = value.trim_start_matches(['\t', '\n', '\u{000C}', '\r', ' ']); + let bytes = value.as_bytes(); + if !bytes.first().is_some_and(u8::is_ascii_digit) { + return None; + } + + let integer_end = bytes + .iter() + .take_while(|byte| byte.is_ascii_digit()) + .count(); + let mut number_end = integer_end; + if bytes.get(integer_end) == Some(&b'.') { + number_end += 1; + number_end += bytes[number_end..] + .iter() + .take_while(|byte| byte.is_ascii_digit()) + .count(); + } + + let number = value[..number_end] + .parse::() + .ok() + .filter(|number| number.is_finite())?; + if bytes.get(number_end) == Some(&b'%') { + Some(HtmlDimension::Percentage(number)) + } else { + Some(HtmlDimension::Pixels(number)) + } +} + /// Mirrors Blink's legacy `cellpadding` state: an absent or exactly empty /// attribute keeps the historical 1px default; non-empty values use loose /// signed-integer parsing and are clamped to `uint16_t`. @@ -880,4 +952,48 @@ mod tests { assert_eq!(parse_html_legacy_color("transparent"), None); assert_eq!(parse_html_legacy_color(""), None); } + + #[test] + fn html_dimension_attribute_uses_legacy_dimension_parsing() { + assert_eq!( + parse_html_dimension_attribute(" 100"), + Some(HtmlDimension::Pixels(100.0)) + ); + assert_eq!( + parse_html_dimension_attribute("12.5px"), + Some(HtmlDimension::Pixels(12.5)) + ); + assert_eq!( + parse_html_dimension_attribute("25.5%ignored"), + Some(HtmlDimension::Percentage(25.5)) + ); + assert_eq!( + parse_html_dimension_attribute("1.%"), + Some(HtmlDimension::Percentage(1.0)) + ); + assert_eq!( + parse_html_dimension_attribute("1."), + Some(HtmlDimension::Pixels(1.0)) + ); + assert_eq!( + parse_html_dimension_attribute("1.%ignored"), + Some(HtmlDimension::Percentage(1.0)) + ); + assert_eq!( + parse_html_dimension_attribute("1.x%"), + Some(HtmlDimension::Pixels(1.0)) + ); + assert_eq!( + parse_html_dimension_attribute("1 %"), + Some(HtmlDimension::Pixels(1.0)) + ); + assert_eq!(parse_html_dimension_attribute("\u{a0}10"), None); + assert_eq!( + parse_html_dimension_attribute("0%"), + Some(HtmlDimension::Percentage(0.0)) + ); + assert_eq!(parse_html_dimension_attribute("+10"), None); + assert_eq!(parse_html_dimension_attribute(".5"), None); + assert_eq!(parse_html_dimension_attribute("auto"), None); + } }