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);
+ }
}