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