Skip to main content

blitz_dom/
resolved_style.rs

1//! Computation of *resolved* CSS property values, as exposed to JavaScript by
2//! `getComputedStyle()`.
3//!
4//! For most properties the resolved value is the stylo computed value. For
5//! layout-dependent properties (`width`/`height`, margins, padding, insets,
6//! `transform-origin`, grid track sizes) it is the *used* value, computed from
7//! the most recent layout.
8
9use cssparser::Parser;
10use selectors::matching::QuirksMode;
11use style::computed_values::box_sizing::T as BoxSizing;
12use style::computed_values::position::T as Position;
13use style::parser::ParserContext;
14use style::properties::declaration_block::{Importance, parse_style_attribute};
15use style::properties::{
16    ComputedValues, NonCustomPropertyId, PropertyDeclaration, PropertyDeclarationBlock,
17    PropertyDeclarationId, PropertyId, ShorthandId, SourcePropertyDeclaration,
18    parse_one_declaration_into,
19};
20use style::servo_arc::Arc as ServoArc;
21use style::stylesheets::supports_rule::parse_condition_or_declaration;
22use style::stylesheets::{CssRuleType, Origin, OriginSet, UrlExtraData};
23use style::stylist::RegisterCustomPropertyResult;
24use style::values::computed::length::CSSPixelLength;
25use style::values::computed::{GridTemplateAreas, GridTemplateComponent, LengthPercentage};
26use style::values::generics::position::{Inset as GenericInset, PreferredRatio};
27use style::values::resolved;
28use style::values::specified::box_::{DisplayInside, DisplayOutside};
29use style_traits::{CssStringWriter, ParsingMode, ToCss};
30
31use blitz_traits::node_id::NodeId;
32use url::Url;
33
34use crate::BaseDocument;
35use crate::layout::replaced::is_replaced_element;
36use crate::local_name;
37
38/// Serialize a used length (in CSS pixels) the way stylo serializes computed lengths
39fn format_px(px: f32) -> String {
40    CSSPixelLength::new(px).to_css_string()
41}
42
43/// Resolve a computed inset value to CSS pixels against the given percentage
44/// basis. Returns `None` for `auto` (and unsupported anchor functions).
45fn resolve_inset<P>(val: &GenericInset<P, LengthPercentage>, basis: f32) -> Option<f32> {
46    match val {
47        GenericInset::LengthPercentage(lp) => Some(lp.resolve(CSSPixelLength::new(basis)).px()),
48        _ => None,
49    }
50}
51
52/// Serialize a transform matrix component rounded to 6 decimal places (as
53/// browsers do when serializing resolved transform matrices)
54fn format_matrix_component(v: f64) -> String {
55    let rounded = (v * 1e6).round() / 1e6;
56    // Avoid "-0"
57    let rounded = if rounded == 0.0 { 0.0 } else { rounded };
58    format!("{rounded}")
59}
60
61/// Serialize a used transform matrix the way `getComputedStyle()` does
62/// (`matrix(...)` for 2D transforms, `matrix3d(...)` otherwise).
63fn format_matrix(m: &euclid::default::Transform3D<f64>, is_3d: bool) -> String {
64    let c = format_matrix_component;
65    if is_3d {
66        format!(
67            "matrix3d({}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {})",
68            c(m.m11),
69            c(m.m12),
70            c(m.m13),
71            c(m.m14),
72            c(m.m21),
73            c(m.m22),
74            c(m.m23),
75            c(m.m24),
76            c(m.m31),
77            c(m.m32),
78            c(m.m33),
79            c(m.m34),
80            c(m.m41),
81            c(m.m42),
82            c(m.m43),
83            c(m.m44)
84        )
85    } else {
86        format!(
87            "matrix({}, {}, {}, {}, {}, {})",
88            c(m.m11),
89            c(m.m12),
90            c(m.m21),
91            c(m.m22),
92            c(m.m41),
93            c(m.m42)
94        )
95    }
96}
97
98/// Check whether `name` names a CSS property supported by the style engine.
99/// Custom properties (`--*`) are not considered "supported" here, matching the
100/// behaviour of the `in` operator on `CSSStyleDeclaration` objects in browsers.
101pub fn css_property_is_supported(name: &str) -> bool {
102    matches!(
103        PropertyId::parse_enabled_for_all_content(name),
104        Ok(property_id) if !matches!(property_id, PropertyId::Custom(_))
105    )
106}
107
108/// Parse a CSS `transform` list into a 4x4 matrix, as required by the
109/// `DOMMatrix(DOMString)` constructor and `DOMMatrix.setMatrixValue()`
110/// (<https://drafts.fxtf.org/geometry/#parse-a-string-into-an-abstract-matrix>).
111///
112/// Returns the 16 matrix components in column-major order (`m11, m12, ...,
113/// m44`) and whether the list contained only 2D transform functions.
114/// Returns `None` if the string fails to parse as a transform list or uses
115/// relative lengths (which cannot be resolved without a context).
116pub fn parse_transform_matrix(value: &str) -> Option<([f64; 16], bool)> {
117    use style::properties::longhands::transform;
118    // Transform lists cannot contain URLs, so any base URL will do
119    let url_data = UrlExtraData(ServoArc::new(Url::parse("about:blank").unwrap()));
120    let context = ParserContext::new(
121        Origin::Author,
122        &url_data,
123        Some(CssRuleType::Style),
124        ParsingMode::DEFAULT,
125        QuirksMode::NoQuirks,
126        Default::default(),
127        None,
128        None,
129        Default::default(),
130    );
131    let mut parser = Parser::new(value);
132    let transform = parser
133        .parse_entirely(|t| transform::parse(&context, t))
134        .ok()?;
135    let (m, is_3d) = transform.to_transform_3d_matrix_f64(None).ok()?;
136    Some((
137        [
138            m.m11, m.m12, m.m13, m.m14, m.m21, m.m22, m.m23, m.m24, m.m31, m.m32, m.m33, m.m34,
139            m.m41, m.m42, m.m43, m.m44,
140        ],
141        !is_3d,
142    ))
143}
144
145/// The names of the properties exposed by the `CSSStyleDeclaration` returned
146/// from `getComputedStyle()` (its indexed properties): every enabled longhand,
147/// sorted alphabetically.
148pub fn resolved_style_property_names() -> &'static [&'static str] {
149    static NAMES: std::sync::OnceLock<Vec<&'static str>> = std::sync::OnceLock::new();
150    NAMES.get_or_init(|| {
151        let mut names: Vec<&'static str> = NonCustomPropertyId::iter()
152            .filter_map(|id| id.as_longhand())
153            .map(|longhand| longhand.name())
154            .filter(|name| PropertyId::parse_enabled_for_all_content(name).is_ok())
155            .collect();
156        names.sort_unstable();
157        names
158    })
159}
160
161impl BaseDocument {
162    /// Check whether `value` is a valid value for the CSS property `property`.
163    /// Used by CSSOM APIs (`element.style.setProperty` and friends), which must
164    /// ignore invalid declarations.
165    pub fn css_declaration_is_valid(&self, property: &str, value: &str) -> bool {
166        let Ok(property_id) = PropertyId::parse_enabled_for_all_content(property) else {
167            return false;
168        };
169        let mut declarations = SourcePropertyDeclaration::default();
170        parse_one_declaration_into(
171            &mut declarations,
172            property_id,
173            value,
174            Origin::Author,
175            &self.url.url_extra_data(),
176            None,
177            ParsingMode::DEFAULT,
178            QuirksMode::NoQuirks,
179            CssRuleType::Style,
180        )
181        .is_ok()
182    }
183
184    /// Parse a `style` attribute string into a [`PropertyDeclarationBlock`].
185    /// Invalid declarations are dropped (per CSS error recovery).
186    fn parse_style_attr_block(&self, style_attr: &str) -> PropertyDeclarationBlock {
187        parse_style_attribute(
188            style_attr,
189            &self.url.url_extra_data(),
190            None,
191            QuirksMode::NoQuirks,
192            CssRuleType::Style,
193        )
194    }
195
196    /// The canonical CSSOM serialization of a `style` attribute string
197    /// (`CSSStyleDeclaration.cssText` getter).
198    pub fn style_attr_serialize(&self, style_attr: &str) -> String {
199        let block = self.parse_style_attr_block(style_attr);
200        let mut css = CssStringWriter::new();
201        let _ = block.to_css(&mut css);
202        css
203    }
204
205    /// The canonical serialization of `property`'s value in a `style` attribute
206    /// string (`CSSStyleDeclaration.getPropertyValue()`). Handles shorthands.
207    /// Returns an empty string if the property is not set or not recognised.
208    pub fn style_attr_get_property(&self, style_attr: &str, property: &str) -> String {
209        let Ok(property_id) = PropertyId::parse_enabled_for_all_content(property) else {
210            return String::new();
211        };
212        let block = self.parse_style_attr_block(style_attr);
213        let mut css = CssStringWriter::new();
214        let _ = block.property_value_to_css(&property_id, &mut css);
215        css
216    }
217
218    /// Set `property` to `value` in a `style` attribute string
219    /// (`CSSStyleDeclaration.setProperty()`), expanding shorthands.
220    ///
221    /// Returns the new (canonically serialized) style attribute, or `None` if
222    /// the declaration was invalid (in which case the style is unchanged, per
223    /// CSSOM). An empty `value` removes the property.
224    pub fn style_attr_set_property(
225        &self,
226        style_attr: &str,
227        property: &str,
228        value: &str,
229        important: bool,
230    ) -> Option<String> {
231        let property_id = PropertyId::parse_enabled_for_all_content(property).ok()?;
232        let mut block = self.parse_style_attr_block(style_attr);
233
234        if value.trim().is_empty() {
235            if let Some(first_declaration) = block.first_declaration_to_remove(&property_id) {
236                block.remove_property(&property_id, first_declaration);
237            }
238        } else {
239            let mut source = SourcePropertyDeclaration::default();
240            parse_one_declaration_into(
241                &mut source,
242                property_id,
243                value,
244                Origin::Author,
245                &self.url.url_extra_data(),
246                None,
247                ParsingMode::DEFAULT,
248                QuirksMode::NoQuirks,
249                CssRuleType::Style,
250            )
251            .ok()?;
252            let importance = if important {
253                Importance::Important
254            } else {
255                Importance::Normal
256            };
257            block.extend(source.drain(), importance);
258        }
259
260        let mut css = CssStringWriter::new();
261        let _ = block.to_css(&mut css);
262        Some(css)
263    }
264
265    /// Remove `property` from a `style` attribute string
266    /// (`CSSStyleDeclaration.removeProperty()`), removing all longhands if it
267    /// is a shorthand.
268    ///
269    /// Returns the new (canonically serialized) style attribute and the removed
270    /// property's previous serialized value.
271    pub fn style_attr_remove_property(
272        &self,
273        style_attr: &str,
274        property: &str,
275    ) -> Option<(String, String)> {
276        let property_id = PropertyId::parse_enabled_for_all_content(property).ok()?;
277        let mut block = self.parse_style_attr_block(style_attr);
278
279        let mut removed_value = CssStringWriter::new();
280        let _ = block.property_value_to_css(&property_id, &mut removed_value);
281        if let Some(first_declaration) = block.first_declaration_to_remove(&property_id) {
282            block.remove_property(&property_id, first_declaration);
283        }
284
285        let mut css = CssStringWriter::new();
286        let _ = block.to_css(&mut css);
287        Some((css, removed_value))
288    }
289
290    /// Evaluate a `@supports` condition (e.g. `(display: grid)`,
291    /// `selector(:hover)`) or a bare declaration (e.g. `display: grid`), as
292    /// used by the CSSOM `CSS.supports(conditionText)` API. Returns `false`
293    /// for unparseable conditions.
294    pub fn css_supports_condition(&self, condition: &str) -> bool {
295        let mut parser = Parser::new(condition);
296        let Ok(condition) = parser.parse_entirely(parse_condition_or_declaration) else {
297            return false;
298        };
299
300        let url_data = self.url.url_extra_data();
301        let context = ParserContext::new(
302            Origin::Author,
303            &url_data,
304            Some(CssRuleType::Style),
305            ParsingMode::DEFAULT,
306            QuirksMode::NoQuirks,
307            Default::default(),
308            None,
309            None,
310            Default::default(),
311        );
312        condition.eval(&context)
313    }
314
315    /// Register a custom property via script (`CSS.registerProperty()`).
316    /// On success all styles are invalidated so that declarations of the
317    /// property are re-parsed against the new registration.
318    pub fn register_custom_property(
319        &mut self,
320        name: &str,
321        syntax: &str,
322        inherits: bool,
323        initial_value: Option<&str>,
324    ) -> RegisterCustomPropertyResult {
325        let url_data = self.url.url_extra_data();
326        let result =
327            self.stylist
328                .register_custom_property(&url_data, name, syntax, inherits, initial_value);
329        if matches!(result, RegisterCustomPropertyResult::SuccessfullyRegistered) {
330            self.stylist
331                .force_stylesheet_origins_dirty(OriginSet::all());
332        }
333        result
334    }
335
336    /// Compute the resolved value of a CSS property for the given node, as exposed
337    /// by `getComputedStyle()`. Returns an empty string for unknown properties and
338    /// for nodes without styles.
339    ///
340    /// Layout-dependent properties (`width`/`height`, margins, padding, insets,
341    /// `transform-origin`, `grid-template-rows`/`columns`) resolve to *used* values, so [`resolve`](Self::resolve) should be called
342    /// before this method to ensure layout is up to date.
343    pub fn resolved_style_value(&self, node_id: NodeId, property_name: &str) -> String {
344        let Some(node) = self.get_node(node_id) else {
345            return String::new();
346        };
347        // The computed style of an element that is not in the document is empty
348        if !node.flags.is_in_document() {
349            return String::new();
350        }
351        // Elements inside a `display: none` subtree are skipped by the style
352        // traversal, so their style has to be computed on demand.
353        let undisplayed_styles;
354        let stored_styles = node.primary_styles();
355        let styles: &ComputedValues = match &stored_styles {
356            Some(styles) => styles,
357            None => match self.resolve_undisplayed_style(node_id) {
358                Some(styles) => {
359                    undisplayed_styles = styles;
360                    &undisplayed_styles
361                }
362                None => return String::new(),
363            },
364        };
365
366        let display = styles.clone_display();
367        // Non-atomic inline elements are laid out as style spans within their
368        // inline root's text layout rather than as boxes of their own, so their
369        // layout-dependent properties resolve to computed (not used) values.
370        let is_non_atomic_inline = display.outside() == DisplayOutside::Inline
371            && display.inside() == DisplayInside::Flow
372            && !node.flags.is_inline_root()
373            && node.element_data().is_none_or(|data| {
374                let tag = &data.name.local;
375                !(is_replaced_element(tag)
376                    || *tag == local_name!("input")
377                    || *tag == local_name!("textarea")
378                    || *tag == local_name!("button"))
379            });
380        let generates_box =
381            node.flags.is_in_document() && !display.is_none() && stored_styles.is_some();
382        let has_layout_box = generates_box && !is_non_atomic_inline;
383
384        let Ok(property_id) = PropertyId::parse_enabled_for_all_content(property_name) else {
385            return String::new();
386        };
387        // Logical longhands resolve as the physical longhand they map to
388        let property_name = match property_id.as_shorthand() {
389            Err(PropertyDeclarationId::Longhand(longhand)) => {
390                longhand.to_physical(styles.writing_mode).name()
391            }
392            _ => property_name,
393        };
394
395        // Used lengths are serialized unzoomed, like computed lengths
396        let zoom = styles.effective_zoom.value();
397        let format_used_px = |px: f32| format_px(px / zoom);
398
399        // Layout-dependent "used value" special cases
400        match property_name {
401            // Shorthands of longhands that resolve to used values
402            "margin" | "margin-block" | "margin-inline" | "padding" | "padding-block"
403            | "padding-inline" | "inset" | "inset-block" | "inset-inline"
404                if has_layout_box =>
405            {
406                if let Ok(shorthand) = property_id.as_shorthand() {
407                    let sides: Vec<String> = shorthand
408                        .longhands()
409                        .map(|longhand| self.resolved_style_value(node_id, longhand.name()))
410                        .collect();
411                    return serialize_sides(&sides);
412                }
413            }
414            "grid-template-columns" | "grid-template-rows"
415                if display.inside() == DisplayInside::Grid =>
416            {
417                if let Some(info) =
418                    node.element_data()
419                        .and_then(|data| match &data.detailed_layout_info {
420                            taffy::DetailedLayoutInfo::Grid(info) => Some(info),
421                            _ => None,
422                        })
423                {
424                    return if property_name == "grid-template-columns" {
425                        info.grid_template_columns(zoom)
426                    } else {
427                        info.grid_template_rows(zoom)
428                    };
429                }
430            }
431            // Browsers serialize the `grid-template` shorthand from the computed
432            // track lists, except that a `none` track list on a grid container
433            // is replaced by the used (implicit) track sizes.
434            "grid-template" if display.inside() == DisplayInside::Grid => {
435                let pos_styles = styles.get_position();
436                let rows_are_none =
437                    matches!(pos_styles.grid_template_rows, GridTemplateComponent::None);
438                let columns_are_none = matches!(
439                    pos_styles.grid_template_columns,
440                    GridTemplateComponent::None
441                );
442                let info = node
443                    .element_data()
444                    .and_then(|data| match &data.detailed_layout_info {
445                        taffy::DetailedLayoutInfo::Grid(info) => Some(info),
446                        _ => None,
447                    });
448                if let Some(info) = info
449                    && (rows_are_none || columns_are_none)
450                    && matches!(pos_styles.grid_template_areas, GridTemplateAreas::None)
451                {
452                    let rows = if rows_are_none {
453                        info.grid_template_rows(zoom)
454                    } else {
455                        pos_styles.grid_template_rows.to_css_string()
456                    };
457                    let columns = if columns_are_none {
458                        info.grid_template_columns(zoom)
459                    } else {
460                        pos_styles.grid_template_columns.to_css_string()
461                    };
462                    return format!("{rows} / {columns}");
463                }
464            }
465            "width" | "height" if has_layout_box => {
466                // Used value: the layout size interpreted according to `box-sizing`
467                // (border-box size for `border-box`, content-box size for `content-box`)
468                let layout = node.final_layout();
469                let border_box = styles.get_position().box_sizing == BoxSizing::BorderBox;
470                let size = if property_name == "width" {
471                    if border_box {
472                        layout.size.width
473                    } else {
474                        layout.size.width
475                            - layout.border.left
476                            - layout.border.right
477                            - layout.padding.left
478                            - layout.padding.right
479                    }
480                } else if border_box {
481                    layout.size.height
482                } else {
483                    layout.size.height
484                        - layout.border.top
485                        - layout.border.bottom
486                        - layout.padding.top
487                        - layout.padding.bottom
488                };
489                return format_used_px(size.max(0.0));
490            }
491            "margin-top" | "margin-right" | "margin-bottom" | "margin-left" if has_layout_box => {
492                // Used value: the margin resolved by layout (percentages and
493                // `auto` margins resolved to lengths)
494                let layout = node.final_layout();
495                let margin = match property_name {
496                    "margin-top" => layout.margin.top,
497                    "margin-right" => layout.margin.right,
498                    "margin-bottom" => layout.margin.bottom,
499                    "margin-left" => layout.margin.left,
500                    _ => unreachable!(),
501                };
502                return format_used_px(margin);
503            }
504            "padding-top" | "padding-right" | "padding-bottom" | "padding-left"
505                if has_layout_box =>
506            {
507                // Used value: the padding resolved by layout (percentages
508                // resolved to lengths)
509                let layout = self.physical_unrounded_geometry(node_id).1;
510                let padding = match property_name {
511                    "padding-top" => layout.padding.top,
512                    "padding-right" => layout.padding.right,
513                    "padding-bottom" => layout.padding.bottom,
514                    "padding-left" => layout.padding.left,
515                    _ => unreachable!(),
516                };
517                return format_used_px(padding);
518            }
519            "transform-origin" | "perspective-origin" if has_layout_box => {
520                // Used value: percentages resolved against the border box
521                let layout = self.physical_unrounded_geometry(node_id).1;
522                let resolve = |value: &LengthPercentage, basis: f32| {
523                    format_used_px(value.resolve(CSSPixelLength::new(basis)).px())
524                };
525                let box_styles = styles.get_box();
526                return if property_name == "transform-origin" {
527                    let origin = &box_styles.transform_origin;
528                    let x = resolve(&origin.horizontal, layout.size.width);
529                    let y = resolve(&origin.vertical, layout.size.height);
530                    if origin.depth.px() == 0.0 {
531                        format!("{x} {y}")
532                    } else {
533                        format!("{x} {y} {}", format_used_px(origin.depth.px()))
534                    }
535                } else {
536                    let origin = &box_styles.perspective_origin;
537                    let x = resolve(&origin.horizontal, layout.size.width);
538                    let y = resolve(&origin.vertical, layout.size.height);
539                    format!("{x} {y}")
540                };
541            }
542            "top" | "right" | "bottom" | "left" if has_layout_box => {
543                let position = styles.clone_position();
544                let parent_layout = node
545                    .containing_block()
546                    .and_then(|id| self.get_node(id))
547                    .map(|parent| *parent.final_layout());
548
549                match position {
550                    // Used value: the relative offset. An `auto` side resolves
551                    // to the negation of the opposite side. When both sides
552                    // are set the axis is overconstrained and each side
553                    // resolves to its computed value instead.
554                    Position::Relative => {
555                        let (cb_width, cb_height) = parent_layout
556                            .map(|pl| {
557                                (
558                                    pl.size.width
559                                        - pl.border.left
560                                        - pl.border.right
561                                        - pl.padding.left
562                                        - pl.padding.right,
563                                    pl.size.height
564                                        - pl.border.top
565                                        - pl.border.bottom
566                                        - pl.padding.top
567                                        - pl.padding.bottom,
568                                )
569                            })
570                            .unwrap_or((0.0, 0.0));
571                        let pos_styles = styles.get_position();
572                        let is_vertical = matches!(property_name, "top" | "bottom");
573                        let basis = if is_vertical { cb_height } else { cb_width };
574                        let (start, end) = if is_vertical {
575                            (
576                                resolve_inset(&pos_styles.top, basis),
577                                resolve_inset(&pos_styles.bottom, basis),
578                            )
579                        } else {
580                            (
581                                resolve_inset(&pos_styles.left, basis),
582                                resolve_inset(&pos_styles.right, basis),
583                            )
584                        };
585                        let is_start = matches!(property_name, "top" | "left");
586                        let used = match (start, end) {
587                            (Some(start), Some(end)) => {
588                                if is_start {
589                                    start
590                                } else {
591                                    end
592                                }
593                            }
594                            (Some(start), None) => {
595                                if is_start {
596                                    start
597                                } else {
598                                    -start
599                                }
600                            }
601                            (None, Some(end)) => {
602                                if is_start {
603                                    -end
604                                } else {
605                                    end
606                                }
607                            }
608                            (None, None) => 0.0,
609                        };
610                        return format_used_px(used);
611                    }
612                    // A specified (non-`auto`) inset resolves as-is (with
613                    // percentages resolved against the containing block), even
614                    // when overconstrained. An `auto` inset resolves to the
615                    // used distance between the box's margin edge and the
616                    // corresponding edge of the containing block's padding box.
617                    Position::Absolute | Position::Fixed => {
618                        if let Some(pl) = parent_layout {
619                            let cb_width = pl.size.width - pl.border.left - pl.border.right;
620                            let cb_height = pl.size.height - pl.border.top - pl.border.bottom;
621
622                            let pos_styles = styles.get_position();
623                            let (inset, basis) = match property_name {
624                                "top" => (&pos_styles.top, cb_height),
625                                "bottom" => (&pos_styles.bottom, cb_height),
626                                "left" => (&pos_styles.left, cb_width),
627                                "right" => (&pos_styles.right, cb_width),
628                                _ => unreachable!(),
629                            };
630                            if let Some(value) = resolve_inset(inset, basis) {
631                                return format_used_px(value);
632                            }
633
634                            let layout = node.final_layout();
635                            let margin_box_top =
636                                layout.location.y - layout.margin.top - pl.border.top;
637                            let margin_box_left =
638                                layout.location.x - layout.margin.left - pl.border.left;
639                            let margin_box_width =
640                                layout.margin.left + layout.size.width + layout.margin.right;
641                            let margin_box_height =
642                                layout.margin.top + layout.size.height + layout.margin.bottom;
643                            let used = match property_name {
644                                "top" => margin_box_top,
645                                "bottom" => cb_height - margin_box_top - margin_box_height,
646                                "left" => margin_box_left,
647                                "right" => cb_width - margin_box_left - margin_box_width,
648                                _ => unreachable!(),
649                            };
650                            return format_used_px(used);
651                        }
652                    }
653                    // `static` and `sticky` boxes resolve to the computed value
654                    _ => {}
655                }
656            }
657            // `min-width: auto` / `min-height: auto` resolve to `auto` only for
658            // boxes with a preferred aspect ratio and for flex and grid items;
659            // otherwise (including when no box is generated) they resolve to `0px`
660            "min-width" | "min-height" => {
661                let pos_styles = styles.get_position();
662                let is_auto = if property_name == "min-width" {
663                    pos_styles.min_width.is_auto()
664                } else {
665                    pos_styles.min_height.is_auto()
666                };
667                let has_aspect_ratio =
668                    !matches!(pos_styles.aspect_ratio.ratio, PreferredRatio::None);
669                let preserves_auto =
670                    generates_box && (has_aspect_ratio || self.is_flex_or_grid_item(node_id));
671                if is_auto && !preserves_auto {
672                    return format_px(0.0);
673                }
674            }
675            "transform" if has_layout_box => {
676                let transform = &styles.get_box().transform;
677                if !transform.0.is_empty() {
678                    // Used value: the transform list resolved to a matrix, with
679                    // percentages resolved against the border box
680                    let layout = node.final_layout();
681                    let reference_box = euclid::Rect::new(
682                        euclid::Point2D::new(CSSPixelLength::new(0.0), CSSPixelLength::new(0.0)),
683                        euclid::Size2D::new(
684                            CSSPixelLength::new(layout.size.width),
685                            CSSPixelLength::new(layout.size.height),
686                        ),
687                    );
688                    if let Ok((matrix, is_3d)) =
689                        transform.to_transform_3d_matrix_f64(Some(&reference_box))
690                    {
691                        return format_matrix(&matrix, is_3d);
692                    }
693                }
694            }
695            _ => {}
696        }
697
698        // General case: serialize the stylo computed value
699        let css = match property_id.as_shorthand() {
700            // Serialize shorthands from the resolved values of their longhands
701            Ok(shorthand) => serialize_resolved_shorthand(styles, shorthand),
702            Err(declaration_id) => styles.computed_value_to_string(declaration_id),
703        };
704        canonicalize_computed_alignment(property_name, css)
705    }
706
707    /// Whether the node's box is a flex or grid item, i.e. its nearest ancestor
708    /// that generates a box (skipping `display: contents` ancestors) is a flex
709    /// or grid container.
710    fn is_flex_or_grid_item(&self, node_id: NodeId) -> bool {
711        let mut parent_id = self.get_node(node_id).and_then(|node| node.parent);
712        while let Some(parent) = parent_id.and_then(|id| self.get_node(id)) {
713            let Some(parent_styles) = parent.primary_styles() else {
714                return false;
715            };
716            let display = parent_styles.clone_display();
717            if display.is_contents() {
718                parent_id = parent.parent;
719                continue;
720            }
721            return matches!(display.inside(), DisplayInside::Flex | DisplayInside::Grid);
722        }
723        false
724    }
725}
726
727/// Serialize the values of a two-sided (start, end) or four-sided (top, right,
728/// bottom, left) shorthand, omitting sides that repeat the opposite side.
729fn serialize_sides(sides: &[String]) -> String {
730    let len = match sides {
731        [start, end] if start == end => 1,
732        [top, right, bottom, left] if right == left => {
733            if top != bottom {
734                3
735            } else if top != right {
736                2
737            } else {
738                1
739            }
740        }
741        _ => sides.len(),
742    };
743    sides[..len].join(" ")
744}
745
746/// Serialize the resolved value of a shorthand property by resolving each of
747/// its longhands and re-serializing them as the shorthand (as
748/// `getComputedStyle()` does for shorthands). Returns an empty string if the
749/// longhand values cannot be represented as the shorthand.
750fn serialize_resolved_shorthand(styles: &ComputedValues, shorthand: ShorthandId) -> String {
751    // `all` cannot be serialized from longhands
752    if shorthand == ShorthandId::All {
753        return String::new();
754    }
755
756    let declarations: Vec<PropertyDeclaration> = shorthand
757        .longhands()
758        .map(|longhand| {
759            let mut context = resolved::Context {
760                style: styles,
761                for_property: PropertyId::NonCustom(longhand.into()),
762                current_longhand: Some(longhand),
763            };
764            styles.computed_or_resolved_declaration(longhand, Some(&mut context))
765        })
766        .collect();
767    let declaration_refs: Vec<&PropertyDeclaration> = declarations.iter().collect();
768
769    let mut css = CssStringWriter::new();
770    let _ = shorthand.longhands_to_css(&declaration_refs, &mut css);
771    css
772}
773
774/// Canonicalize the computed value of an alignment property the way css-align-3
775/// now requires: `flex-start`/`flex-end` compute to `flow-start`/`flow-end`
776/// (e.g. `align-self: flex-start` resolves to `flow-start`, `safe flex-end` to
777/// `safe flow-end`). Stylo does not yet do this, so patch the serialized value
778/// here until it does (see https://github.com/servo/stylo).
779fn canonicalize_computed_alignment(property_name: &str, value: String) -> String {
780    const ALIGNMENT_PROPERTIES: [&str; 9] = [
781        "align-content",
782        "align-items",
783        "align-self",
784        "justify-content",
785        "justify-items",
786        "justify-self",
787        "place-content",
788        "place-items",
789        "place-self",
790    ];
791    if !ALIGNMENT_PROPERTIES.contains(&property_name) || !value.contains("flex-") {
792        return value;
793    }
794    value
795        .replace("flex-start", "flow-start")
796        .replace("flex-end", "flow-end")
797}