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            let shared_inline_styles = SharedInlineStyles::from_info_and_context(&info, context);
185            element
186                .box_slot()
187                .set(LayoutBox::DisplayContents(shared_inline_styles.clone()));
188
189            handler.enter_display_contents(shared_inline_styles);
190            traverse_children_of(&info, context, handler);
191            handler.leave_display_contents();
192        },
193        Display::GeneratingBox(display) => {
194            let contents = Contents::for_element(element, context);
195            let display = display.used_value_for_contents(&contents, &info);
196            let box_slot = element.box_slot();
197            handler.handle_element(&info, display, contents, box_slot);
198        },
199    }
200}
201
202fn traverse_eager_pseudo_element<'dom>(
203    pseudo_element_type: PseudoElement,
204    node_info: &NodeAndStyleInfo<'dom>,
205    context: &LayoutContext,
206    handler: &mut impl TraversalHandler<'dom>,
207) {
208    assert!(pseudo_element_type.is_eager());
209
210    // If this node doesn't have this eager pseudo-element, exit early. This depends on
211    // the style applied to the element.
212    let Some(pseudo_element_info) = node_info.with_pseudo_element(context, pseudo_element_type)
213    else {
214        return;
215    };
216    if pseudo_element_info.style.ineffective_content_property() {
217        return;
218    }
219
220    match Display::from(pseudo_element_info.style.get_box().display) {
221        Display::None => {},
222        Display::Contents => {
223            let items = generate_pseudo_element_content(&pseudo_element_info, context);
224            let box_slot = pseudo_element_info.node.box_slot();
225            let shared_inline_styles =
226                SharedInlineStyles::from_info_and_context(&pseudo_element_info, context);
227            box_slot.set(LayoutBox::DisplayContents(shared_inline_styles.clone()));
228
229            handler.enter_display_contents(shared_inline_styles);
230            traverse_pseudo_element_contents(&pseudo_element_info, context, handler, items);
231            handler.leave_display_contents();
232        },
233        Display::GeneratingBox(display) => {
234            let items = generate_pseudo_element_content(&pseudo_element_info, context);
235            let box_slot = pseudo_element_info.node.box_slot();
236            let contents = Contents::for_pseudo_element(items);
237            handler.handle_element(&pseudo_element_info, display, contents, box_slot);
238        },
239    }
240}
241
242fn traverse_pseudo_element_contents<'dom>(
243    info: &NodeAndStyleInfo<'dom>,
244    context: &LayoutContext,
245    handler: &mut impl TraversalHandler<'dom>,
246    items: Vec<PseudoElementContentItem>,
247) {
248    let mut anonymous_info = None;
249    for item in items {
250        match item {
251            PseudoElementContentItem::Text(text) => handler.handle_text(info, text.into()),
252            PseudoElementContentItem::Replaced(contents) => {
253                let anonymous_info = anonymous_info.get_or_insert_with(|| {
254                    info.with_pseudo_element(context, PseudoElement::ServoAnonymousBox)
255                        .unwrap_or_else(|| info.clone())
256                });
257                let display_inline = DisplayGeneratingBox::OutsideInside {
258                    outside: DisplayOutside::Inline,
259                    inside: DisplayInside::Flow {
260                        is_list_item: false,
261                    },
262                };
263                // `display` is not inherited, so we get the initial value
264                debug_assert!(
265                    Display::from(anonymous_info.style.get_box().display) ==
266                        Display::GeneratingBox(display_inline)
267                );
268                handler.handle_element(
269                    anonymous_info,
270                    display_inline,
271                    Contents::Replaced(contents),
272                    anonymous_info.node.box_slot(),
273                )
274            },
275        }
276    }
277}
278
279impl Contents {
280    /// Returns true iff the `try_from` impl below would return `Err(_)`
281    pub fn is_replaced(&self) -> bool {
282        matches!(self, Contents::Replaced(_))
283    }
284
285    pub(crate) fn for_element(node: ServoLayoutNode<'_>, context: &LayoutContext) -> Self {
286        let is_widget = matches!(
287            node.type_id(),
288            Some(LayoutNodeType::Element(
289                LayoutElementType::HTMLButtonElement |
290                    LayoutElementType::HTMLInputElement |
291                    LayoutElementType::HTMLMeterElement |
292                    LayoutElementType::HTMLProgressElement |
293                    LayoutElementType::HTMLSelectElement |
294                    LayoutElementType::HTMLTextAreaElement
295            ))
296        );
297        if is_widget {
298            Self::Widget(NonReplacedContents::OfElement)
299        } else if let Some(replaced) = ReplacedContents::for_element(node, context) {
300            Self::Replaced(replaced)
301        } else {
302            Self::NonReplaced(NonReplacedContents::OfElement)
303        }
304    }
305
306    pub(crate) fn for_pseudo_element(contents: Vec<PseudoElementContentItem>) -> Self {
307        Self::NonReplaced(NonReplacedContents::OfPseudoElement(contents))
308    }
309
310    pub(crate) fn non_replaced_contents(self) -> Option<NonReplacedContents> {
311        match self {
312            Self::NonReplaced(contents) | Self::Widget(contents) => Some(contents),
313            Self::Replaced(_) => None,
314        }
315    }
316}
317
318impl NonReplacedContents {
319    pub(crate) fn traverse<'dom>(
320        self,
321        context: &LayoutContext,
322        info: &NodeAndStyleInfo<'dom>,
323        handler: &mut impl TraversalHandler<'dom>,
324    ) {
325        match self {
326            NonReplacedContents::OfElement => traverse_children_of(info, context, handler),
327            NonReplacedContents::OfPseudoElement(items) => {
328                traverse_pseudo_element_contents(info, context, handler, items)
329            },
330        }
331    }
332}
333
334fn get_quote_from_pair<I, S>(item: &ContentItem<I>, opening: &S, closing: &S) -> String
335where
336    S: ToString + ?Sized,
337{
338    match item {
339        ContentItem::OpenQuote => opening.to_string(),
340        ContentItem::CloseQuote => closing.to_string(),
341        _ => unreachable!("Got an unexpected ContentItem type when processing quotes."),
342    }
343}
344
345/// <https://www.w3.org/TR/CSS2/generate.html#propdef-content>
346pub(crate) fn generate_pseudo_element_content(
347    pseudo_element_info: &NodeAndStyleInfo,
348    context: &LayoutContext,
349) -> Vec<PseudoElementContentItem> {
350    match &pseudo_element_info.style.get_counters().content {
351        Content::Items(items) => {
352            items
353                .items
354                .iter()
355                .filter_map(|item| {
356                    match item {
357                        ContentItem::String(s) => {
358                            Some(PseudoElementContentItem::Text(s.to_string()))
359                        },
360                        ContentItem::Image(image) => {
361                            ReplacedContents::from_image(pseudo_element_info.node, context, image)
362                                .map(PseudoElementContentItem::Replaced)
363                        },
364                        ContentItem::OpenQuote | ContentItem::CloseQuote => {
365                            // TODO(xiaochengh): calculate quote depth
366                            let maybe_quote = match &pseudo_element_info.style.get_list().quotes {
367                                Quotes::QuoteList(quote_list) => {
368                                    quote_list.0.first().map(|quote_pair| {
369                                        get_quote_from_pair(
370                                            item,
371                                            &*quote_pair.opening,
372                                            &*quote_pair.closing,
373                                        )
374                                    })
375                                },
376                                Quotes::Auto => {
377                                    let lang = &pseudo_element_info.style.get_font()._x_lang;
378                                    let quotes = quotes_for_lang(lang.0.as_ref(), 0);
379                                    Some(get_quote_from_pair(
380                                        item,
381                                        &quotes.opening,
382                                        &quotes.closing,
383                                    ))
384                                },
385                            };
386                            maybe_quote.map(PseudoElementContentItem::Text)
387                        },
388                        ContentItem::Counter(_, style) | ContentItem::Counters(_, _, style) => {
389                            // TODO: Add support for counters, this assumes a value of 0.
390                            Some(PseudoElementContentItem::Text(
391                                generate_counter_representation(style).to_string(),
392                            ))
393                        },
394                        ContentItem::NoOpenQuote | ContentItem::NoCloseQuote => None,
395                    }
396                })
397                .collect()
398        },
399        Content::Normal | Content::None => unreachable!(),
400    }
401}