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