Skip to main content

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