layout/
dom_traversal.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::borrow::Cow;
6
7use layout_api::{
8    LayoutElement, LayoutElementType, LayoutNode, LayoutNodeType, PseudoElementChain,
9};
10use script::layout_dom::ServoLayoutNode;
11use servo_arc::Arc as ServoArc;
12use style::dom::NodeInfo;
13use style::properties::ComputedValues;
14use style::selector_parser::PseudoElement;
15use style::values::generics::counters::{Content, ContentItem};
16use style::values::specified::Quotes;
17use web_atoms::LocalName;
18
19use crate::context::LayoutContext;
20use crate::dom::{BoxSlot, LayoutBox, NodeExt};
21use crate::flow::inline::SharedInlineStyles;
22use crate::lists::generate_counter_representation;
23use crate::quotes::quotes_for_lang;
24use crate::replaced::ReplacedContents;
25use crate::style_ext::{Display, DisplayGeneratingBox, DisplayInside, DisplayOutside};
26
27/// A data structure used to pass and store related layout information together to
28/// avoid having to repeat the same arguments in argument lists.
29#[derive(Clone)]
30pub(crate) struct NodeAndStyleInfo<'dom> {
31    pub node: ServoLayoutNode<'dom>,
32    pub style: ServoArc<ComputedValues>,
33}
34
35impl<'dom> NodeAndStyleInfo<'dom> {
36    pub(crate) fn new(node: ServoLayoutNode<'dom>, style: ServoArc<ComputedValues>) -> Self {
37        Self { node, style }
38    }
39
40    pub(crate) fn pseudo_element_chain(&self) -> PseudoElementChain {
41        self.node.pseudo_element_chain()
42    }
43
44    pub(crate) fn with_pseudo_element(
45        &self,
46        context: &LayoutContext,
47        pseudo_element_type: PseudoElement,
48    ) -> Option<Self> {
49        let element = self.node.as_element()?.with_pseudo(pseudo_element_type)?;
50        let style = element.style(&context.style_context);
51        Some(NodeAndStyleInfo {
52            node: element.as_node(),
53            style,
54        })
55    }
56}
57
58#[derive(Debug)]
59pub(super) enum Contents {
60    /// Any kind of content that is not replaced nor a widget, including the contents of pseudo-elements.
61    NonReplaced(NonReplacedContents),
62    /// A widget with native appearance. This has several behavior in common with replaced elements,
63    /// but isn't fully replaced (see discussion in <https://github.com/w3c/csswg-drafts/issues/12876>).
64    /// Examples: `<input>`, `<textarea>`, `<select>`...
65    /// <https://drafts.csswg.org/css-ui/#widget>
66    Widget(NonReplacedContents),
67    /// Example: an `<img src=…>` element.
68    /// <https://drafts.csswg.org/css2/conform.html#replaced-element>
69    Replaced(ReplacedContents),
70}
71
72#[derive(Debug)]
73pub(super) enum NonReplacedContents {
74    /// Refers to a DOM subtree, plus `::before` and `::after` pseudo-elements.
75    OfElement,
76    /// Content of a `::before` or `::after` pseudo-element that is being generated.
77    /// <https://drafts.csswg.org/css2/generate.html#content>
78    OfPseudoElement(Vec<PseudoElementContentItem>),
79}
80
81#[derive(Debug)]
82pub(super) enum PseudoElementContentItem {
83    Text(String),
84    Replaced(ReplacedContents),
85}
86
87pub(super) trait TraversalHandler<'dom> {
88    fn handle_text(&mut self, info: &NodeAndStyleInfo<'dom>, text: Cow<'dom, str>);
89
90    /// Or pseudo-element
91    fn handle_element(
92        &mut self,
93        info: &NodeAndStyleInfo<'dom>,
94        display: DisplayGeneratingBox,
95        contents: Contents,
96        box_slot: BoxSlot<'dom>,
97    );
98
99    /// Notify the handler that we are about to recurse into a `display: contents` element.
100    fn enter_display_contents(&mut self, _: SharedInlineStyles) {}
101
102    /// Notify the handler that we have finished a `display: contents` element.
103    fn leave_display_contents(&mut self) {}
104}
105
106fn traverse_children_of<'dom>(
107    parent_element_info: &NodeAndStyleInfo<'dom>,
108    context: &LayoutContext,
109    handler: &mut impl TraversalHandler<'dom>,
110) {
111    parent_element_info
112        .node
113        .set_uses_content_attribute_with_attr(false);
114
115    let is_element = parent_element_info.pseudo_element_chain().is_empty();
116    if is_element {
117        traverse_eager_pseudo_element(PseudoElement::Before, parent_element_info, context, handler);
118    }
119
120    for child in parent_element_info.node.flat_tree_children() {
121        if child.is_text_node() {
122            let info = NodeAndStyleInfo::new(child, child.style(&context.style_context));
123            handler.handle_text(&info, child.text_content());
124        } else if child.is_element() {
125            traverse_element(child, context, handler);
126        }
127    }
128
129    if is_element {
130        traverse_eager_pseudo_element(PseudoElement::After, parent_element_info, context, handler);
131    }
132}
133
134fn traverse_element<'dom>(
135    element: ServoLayoutNode<'dom>,
136    context: &LayoutContext,
137    handler: &mut impl TraversalHandler<'dom>,
138) {
139    let style = element.style(&context.style_context);
140    let info = NodeAndStyleInfo::new(element, style);
141
142    match Display::from(info.style.get_box().display) {
143        Display::None => {},
144        Display::Contents => {
145            if ReplacedContents::for_element(element, context).is_some() {
146                // `display: content` on a replaced element computes to `display: none`
147                // <https://drafts.csswg.org/css-display-3/#valdef-display-contents>
148                element.unset_all_boxes()
149            } else {
150                let shared_inline_styles =
151                    SharedInlineStyles::from_info_and_context(&info, context);
152                element
153                    .box_slot()
154                    .set(LayoutBox::DisplayContents(shared_inline_styles.clone()));
155
156                handler.enter_display_contents(shared_inline_styles);
157                traverse_children_of(&info, context, handler);
158                handler.leave_display_contents();
159            }
160        },
161        Display::GeneratingBox(display) => {
162            let contents = Contents::for_element(element, context);
163            let display = display.used_value_for_contents(&contents);
164            let box_slot = element.box_slot();
165            handler.handle_element(&info, display, contents, box_slot);
166        },
167    }
168}
169
170fn traverse_eager_pseudo_element<'dom>(
171    pseudo_element_type: PseudoElement,
172    node_info: &NodeAndStyleInfo<'dom>,
173    context: &LayoutContext,
174    handler: &mut impl TraversalHandler<'dom>,
175) {
176    assert!(pseudo_element_type.is_eager());
177
178    // If this node doesn't have this eager pseudo-element, exit early. This depends on
179    // the style applied to the element.
180    let Some(pseudo_element_info) = node_info.with_pseudo_element(context, pseudo_element_type)
181    else {
182        return;
183    };
184    if pseudo_element_info.style.ineffective_content_property() {
185        return;
186    }
187
188    match Display::from(pseudo_element_info.style.get_box().display) {
189        Display::None => {},
190        Display::Contents => {
191            let items = generate_pseudo_element_content(&pseudo_element_info, context);
192            let box_slot = pseudo_element_info.node.box_slot();
193            let shared_inline_styles =
194                SharedInlineStyles::from_info_and_context(&pseudo_element_info, context);
195            box_slot.set(LayoutBox::DisplayContents(shared_inline_styles.clone()));
196
197            handler.enter_display_contents(shared_inline_styles);
198            traverse_pseudo_element_contents(&pseudo_element_info, context, handler, items);
199            handler.leave_display_contents();
200        },
201        Display::GeneratingBox(display) => {
202            let items = generate_pseudo_element_content(&pseudo_element_info, context);
203            let box_slot = pseudo_element_info.node.box_slot();
204            let contents = Contents::for_pseudo_element(items);
205            handler.handle_element(&pseudo_element_info, display, contents, box_slot);
206        },
207    }
208}
209
210fn traverse_pseudo_element_contents<'dom>(
211    info: &NodeAndStyleInfo<'dom>,
212    context: &LayoutContext,
213    handler: &mut impl TraversalHandler<'dom>,
214    items: Vec<PseudoElementContentItem>,
215) {
216    let mut anonymous_info = None;
217    for item in items {
218        match item {
219            PseudoElementContentItem::Text(text) => handler.handle_text(info, text.into()),
220            PseudoElementContentItem::Replaced(contents) => {
221                let anonymous_info = anonymous_info.get_or_insert_with(|| {
222                    info.with_pseudo_element(context, PseudoElement::ServoAnonymousBox)
223                        .unwrap_or_else(|| info.clone())
224                });
225                let display_inline = DisplayGeneratingBox::OutsideInside {
226                    outside: DisplayOutside::Inline,
227                    inside: DisplayInside::Flow {
228                        is_list_item: false,
229                    },
230                };
231                // `display` is not inherited, so we get the initial value
232                debug_assert!(
233                    Display::from(anonymous_info.style.get_box().display) ==
234                        Display::GeneratingBox(display_inline)
235                );
236                handler.handle_element(
237                    anonymous_info,
238                    display_inline,
239                    Contents::Replaced(contents),
240                    anonymous_info.node.box_slot(),
241                )
242            },
243        }
244    }
245}
246
247impl Contents {
248    /// Returns true iff the `try_from` impl below would return `Err(_)`
249    pub fn is_replaced(&self) -> bool {
250        matches!(self, Contents::Replaced(_))
251    }
252
253    pub(crate) fn for_element(node: ServoLayoutNode<'_>, context: &LayoutContext) -> Self {
254        if let Some(replaced) = ReplacedContents::for_element(node, context) {
255            return Self::Replaced(replaced);
256        }
257        let is_widget = matches!(
258            node.type_id(),
259            Some(LayoutNodeType::Element(
260                LayoutElementType::HTMLInputElement |
261                    LayoutElementType::HTMLSelectElement |
262                    LayoutElementType::HTMLTextAreaElement
263            ))
264        );
265        if is_widget {
266            Self::Widget(NonReplacedContents::OfElement)
267        } else {
268            Self::NonReplaced(NonReplacedContents::OfElement)
269        }
270    }
271
272    pub(crate) fn for_pseudo_element(contents: Vec<PseudoElementContentItem>) -> Self {
273        Self::NonReplaced(NonReplacedContents::OfPseudoElement(contents))
274    }
275
276    pub(crate) fn non_replaced_contents(self) -> Option<NonReplacedContents> {
277        match self {
278            Self::NonReplaced(contents) | Self::Widget(contents) => Some(contents),
279            Self::Replaced(_) => None,
280        }
281    }
282}
283
284impl NonReplacedContents {
285    pub(crate) fn traverse<'dom>(
286        self,
287        context: &LayoutContext,
288        info: &NodeAndStyleInfo<'dom>,
289        handler: &mut impl TraversalHandler<'dom>,
290    ) {
291        match self {
292            NonReplacedContents::OfElement => traverse_children_of(info, context, handler),
293            NonReplacedContents::OfPseudoElement(items) => {
294                traverse_pseudo_element_contents(info, context, handler, items)
295            },
296        }
297    }
298}
299
300fn get_quote_from_pair<I, S>(item: &ContentItem<I>, opening: &S, closing: &S) -> String
301where
302    S: ToString + ?Sized,
303{
304    match item {
305        ContentItem::OpenQuote => opening.to_string(),
306        ContentItem::CloseQuote => closing.to_string(),
307        _ => unreachable!("Got an unexpected ContentItem type when processing quotes."),
308    }
309}
310
311/// <https://www.w3.org/TR/CSS2/generate.html#propdef-content>
312pub(crate) fn generate_pseudo_element_content(
313    pseudo_element_info: &NodeAndStyleInfo,
314    context: &LayoutContext,
315) -> Vec<PseudoElementContentItem> {
316    match &pseudo_element_info.style.get_counters().content {
317        Content::Items(items) => {
318            let mut vec = vec![];
319            for item in items.items.iter() {
320                match item {
321                    ContentItem::String(s) => {
322                        vec.push(PseudoElementContentItem::Text(s.to_string()));
323                    },
324                    ContentItem::Attr(attr) => {
325                        let element = pseudo_element_info
326                            .node
327                            .as_element()
328                            .expect("Expected an element");
329
330                        // From
331                        // <https://html.spec.whatwg.org/multipage/#case-sensitivity-of-the-css-%27attr%28%29%27-function>
332                        //
333                        // > CSS Values and Units leaves the case-sensitivity of attribute names for
334                        // > the purpose of the `attr()` function to be defined by the host language.
335                        // > [[CSSVALUES]].
336                        // >
337                        // > When comparing the attribute name part of a CSS `attr()`function to the
338                        // > names of namespace-less attributes on HTML elements in HTML documents,
339                        // > the name part of the CSS `attr()` function must first be converted to
340                        // > ASCII lowercase. The same function when compared to other attributes must
341                        // > be compared according to its original case. In both cases, to match the
342                        // > values must be identical to each other (and therefore the comparison is
343                        // > case sensitive).
344                        let attr_name = match element.is_html_element_in_html_document() {
345                            true => &*attr.attribute.to_ascii_lowercase(),
346                            false => &*attr.attribute,
347                        };
348
349                        pseudo_element_info
350                            .node
351                            .set_uses_content_attribute_with_attr(true);
352                        let attr_val =
353                            element.attribute(&attr.namespace_url, &LocalName::from(attr_name));
354                        vec.push(PseudoElementContentItem::Text(
355                            attr_val.map_or("".to_string(), |s| s.to_string()),
356                        ));
357                    },
358                    ContentItem::Image(image) => {
359                        if let Some(replaced_content) =
360                            ReplacedContents::from_image(pseudo_element_info.node, context, image)
361                        {
362                            vec.push(PseudoElementContentItem::Replaced(replaced_content));
363                        }
364                    },
365                    ContentItem::OpenQuote | ContentItem::CloseQuote => {
366                        // TODO(xiaochengh): calculate quote depth
367                        let maybe_quote = match &pseudo_element_info.style.get_list().quotes {
368                            Quotes::QuoteList(quote_list) => {
369                                quote_list.0.first().map(|quote_pair| {
370                                    get_quote_from_pair(
371                                        item,
372                                        &*quote_pair.opening,
373                                        &*quote_pair.closing,
374                                    )
375                                })
376                            },
377                            Quotes::Auto => {
378                                let lang = &pseudo_element_info.style.get_font()._x_lang;
379                                let quotes = quotes_for_lang(lang.0.as_ref(), 0);
380                                Some(get_quote_from_pair(item, &quotes.opening, &quotes.closing))
381                            },
382                        };
383                        if let Some(quote) = maybe_quote {
384                            vec.push(PseudoElementContentItem::Text(quote));
385                        }
386                    },
387                    ContentItem::Counter(_, style) | ContentItem::Counters(_, _, style) => {
388                        // TODO: Add support for counters, this assumes a value of 0.
389                        vec.push(PseudoElementContentItem::Text(
390                            generate_counter_representation(style).to_string(),
391                        ));
392                    },
393                    ContentItem::NoOpenQuote | ContentItem::NoCloseQuote => {},
394                }
395            }
396            vec
397        },
398        Content::Normal | Content::None => unreachable!(),
399    }
400}