1use std::borrow::Cow;
6
7use fonts::ByteIndex;
8use html5ever::LocalName;
9use layout_api::wrapper_traits::{
10 PseudoElementChain, ThreadSafeLayoutElement, ThreadSafeLayoutNode,
11};
12use layout_api::{LayoutDamage, LayoutElementType, LayoutNodeType};
13use range::Range;
14use script::layout_dom::ServoThreadSafeLayoutNode;
15use selectors::Element as SelectorsElement;
16use servo_arc::Arc as ServoArc;
17use style::dom::NodeInfo;
18use style::properties::ComputedValues;
19use style::selector_parser::PseudoElement;
20use style::values::generics::counters::{Content, ContentItem};
21use style::values::specified::Quotes;
22
23use crate::context::LayoutContext;
24use crate::dom::{BoxSlot, LayoutBox, NodeExt};
25use crate::flow::inline::SharedInlineStyles;
26use crate::quotes::quotes_for_lang;
27use crate::replaced::ReplacedContents;
28use crate::style_ext::{Display, DisplayGeneratingBox, DisplayInside, DisplayOutside};
29
30#[derive(Clone)]
33pub(crate) struct NodeAndStyleInfo<'dom> {
34 pub node: ServoThreadSafeLayoutNode<'dom>,
35 pub style: ServoArc<ComputedValues>,
36 pub damage: LayoutDamage,
37}
38
39impl<'dom> NodeAndStyleInfo<'dom> {
40 pub(crate) fn new(
41 node: ServoThreadSafeLayoutNode<'dom>,
42 style: ServoArc<ComputedValues>,
43 damage: LayoutDamage,
44 ) -> Self {
45 Self {
46 node,
47 style,
48 damage,
49 }
50 }
51
52 pub(crate) fn pseudo_element_chain(&self) -> PseudoElementChain {
53 self.node.pseudo_element_chain()
54 }
55
56 pub(crate) fn with_pseudo_element(
57 &self,
58 context: &LayoutContext,
59 pseudo_element_type: PseudoElement,
60 ) -> Option<Self> {
61 let element = self.node.as_element()?.with_pseudo(pseudo_element_type)?;
62 let style = element.style(&context.style_context);
63 Some(NodeAndStyleInfo {
64 node: element.as_node(),
65 style,
66 damage: self.damage,
67 })
68 }
69
70 pub(crate) fn get_selection_range(&self) -> Option<Range<ByteIndex>> {
71 self.node.selection()
72 }
73}
74
75#[derive(Debug)]
76pub(super) enum Contents {
77 NonReplaced(NonReplacedContents),
79 Replaced(ReplacedContents),
82}
83
84#[derive(Debug)]
85#[allow(clippy::enum_variant_names)]
86pub(super) enum NonReplacedContents {
87 OfElement,
89 OfPseudoElement(Vec<PseudoElementContentItem>),
92 OfTextControl,
94}
95
96#[derive(Debug)]
97pub(super) enum PseudoElementContentItem {
98 Text(String),
99 Replaced(ReplacedContents),
100}
101
102pub(super) trait TraversalHandler<'dom> {
103 fn handle_text(&mut self, info: &NodeAndStyleInfo<'dom>, text: Cow<'dom, str>);
104
105 fn handle_element(
107 &mut self,
108 info: &NodeAndStyleInfo<'dom>,
109 display: DisplayGeneratingBox,
110 contents: Contents,
111 box_slot: BoxSlot<'dom>,
112 );
113
114 fn enter_display_contents(&mut self, _: SharedInlineStyles) {}
116
117 fn leave_display_contents(&mut self) {}
119}
120
121fn traverse_children_of<'dom>(
122 parent_element_info: &NodeAndStyleInfo<'dom>,
123 context: &LayoutContext,
124 handler: &mut impl TraversalHandler<'dom>,
125) {
126 traverse_eager_pseudo_element(PseudoElement::Before, parent_element_info, context, handler);
127
128 if parent_element_info.node.is_text_input() {
133 let node_text_content = parent_element_info.node.node_text_content();
134 if node_text_content.is_empty() {
135 handler.handle_text(parent_element_info, "\u{200B}".into());
136 } else {
137 handler.handle_text(parent_element_info, node_text_content);
138 }
139 } else {
140 for child in parent_element_info.node.children() {
141 if child.is_text_node() {
142 let info = NodeAndStyleInfo::new(
143 child,
144 child.style(&context.style_context),
145 child.take_restyle_damage(),
146 );
147 handler.handle_text(&info, child.node_text_content());
148 } else if child.is_element() {
149 traverse_element(child, context, handler);
150 }
151 }
152 }
153
154 traverse_eager_pseudo_element(PseudoElement::After, parent_element_info, context, handler);
155}
156
157fn traverse_element<'dom>(
158 element: ServoThreadSafeLayoutNode<'dom>,
159 context: &LayoutContext,
160 handler: &mut impl TraversalHandler<'dom>,
161) {
162 element.unset_all_pseudo_boxes();
163
164 let replaced = ReplacedContents::for_element(element, context);
165 let style = element.style(&context.style_context);
166 let damage = element.take_restyle_damage();
167 let info = NodeAndStyleInfo::new(element, style, damage);
168
169 match Display::from(info.style.get_box().display) {
170 Display::None => element.unset_all_boxes(),
171 Display::Contents => {
172 if replaced.is_some() {
173 element.unset_all_boxes()
176 } else {
177 let shared_inline_styles: SharedInlineStyles = (&info).into();
178 element
179 .box_slot()
180 .set(LayoutBox::DisplayContents(shared_inline_styles.clone()));
181
182 handler.enter_display_contents(shared_inline_styles);
183 traverse_children_of(&info, context, handler);
184 handler.leave_display_contents();
185 }
186 },
187 Display::GeneratingBox(display) => {
188 let contents = if let Some(replaced) = replaced {
189 Contents::Replaced(replaced)
190 } else if matches!(
191 element.type_id(),
192 Some(LayoutNodeType::Element(
193 LayoutElementType::HTMLInputElement | LayoutElementType::HTMLTextAreaElement
194 ))
195 ) {
196 NonReplacedContents::OfTextControl.into()
197 } else {
198 NonReplacedContents::OfElement.into()
199 };
200 let display = display.used_value_for_contents(&contents);
201 let box_slot = element.box_slot();
202 handler.handle_element(&info, display, contents, box_slot);
203 },
204 }
205}
206
207fn traverse_eager_pseudo_element<'dom>(
208 pseudo_element_type: PseudoElement,
209 node_info: &NodeAndStyleInfo<'dom>,
210 context: &LayoutContext,
211 handler: &mut impl TraversalHandler<'dom>,
212) {
213 assert!(pseudo_element_type.is_eager());
214
215 let Some(pseudo_element_info) = node_info.with_pseudo_element(context, pseudo_element_type)
218 else {
219 return;
220 };
221 if pseudo_element_info.style.ineffective_content_property() {
222 return;
223 }
224
225 match Display::from(pseudo_element_info.style.get_box().display) {
226 Display::None => {},
227 Display::Contents => {
228 let items = generate_pseudo_element_content(&pseudo_element_info, context);
229 let box_slot = pseudo_element_info.node.box_slot();
230 let shared_inline_styles: SharedInlineStyles = (&pseudo_element_info).into();
231 box_slot.set(LayoutBox::DisplayContents(shared_inline_styles.clone()));
232
233 handler.enter_display_contents(shared_inline_styles);
234 traverse_pseudo_element_contents(&pseudo_element_info, context, handler, items);
235 handler.leave_display_contents();
236 },
237 Display::GeneratingBox(display) => {
238 let items = generate_pseudo_element_content(&pseudo_element_info, context);
239 let box_slot = pseudo_element_info.node.box_slot();
240 let contents = NonReplacedContents::OfPseudoElement(items).into();
241 handler.handle_element(&pseudo_element_info, display, contents, box_slot);
242 },
243 }
244}
245
246fn traverse_pseudo_element_contents<'dom>(
247 info: &NodeAndStyleInfo<'dom>,
248 context: &LayoutContext,
249 handler: &mut impl TraversalHandler<'dom>,
250 items: Vec<PseudoElementContentItem>,
251) {
252 let mut anonymous_info = None;
253 for item in items {
254 match item {
255 PseudoElementContentItem::Text(text) => handler.handle_text(info, text.into()),
256 PseudoElementContentItem::Replaced(contents) => {
257 let anonymous_info = anonymous_info.get_or_insert_with(|| {
258 info.with_pseudo_element(context, PseudoElement::ServoAnonymousBox)
259 .unwrap_or_else(|| info.clone())
260 });
261 let display_inline = DisplayGeneratingBox::OutsideInside {
262 outside: DisplayOutside::Inline,
263 inside: DisplayInside::Flow {
264 is_list_item: false,
265 },
266 };
267 debug_assert!(
269 Display::from(anonymous_info.style.get_box().display) ==
270 Display::GeneratingBox(display_inline)
271 );
272 handler.handle_element(
273 anonymous_info,
274 display_inline,
275 Contents::Replaced(contents),
276 anonymous_info.node.box_slot(),
277 )
278 },
279 }
280 }
281}
282
283impl Contents {
284 pub fn is_replaced(&self) -> bool {
286 matches!(self, Contents::Replaced(_))
287 }
288}
289
290impl From<NonReplacedContents> for Contents {
291 fn from(non_replaced_contents: NonReplacedContents) -> Self {
292 Contents::NonReplaced(non_replaced_contents)
293 }
294}
295
296impl NonReplacedContents {
297 pub(crate) fn traverse<'dom>(
298 self,
299 context: &LayoutContext,
300 info: &NodeAndStyleInfo<'dom>,
301 handler: &mut impl TraversalHandler<'dom>,
302 ) {
303 match self {
304 NonReplacedContents::OfElement | NonReplacedContents::OfTextControl => {
305 traverse_children_of(info, context, handler)
306 },
307 NonReplacedContents::OfPseudoElement(items) => {
308 traverse_pseudo_element_contents(info, context, handler, items)
309 },
310 }
311 }
312}
313
314fn get_quote_from_pair<I, S>(item: &ContentItem<I>, opening: &S, closing: &S) -> String
315where
316 S: ToString + ?Sized,
317{
318 match item {
319 ContentItem::OpenQuote => opening.to_string(),
320 ContentItem::CloseQuote => closing.to_string(),
321 _ => unreachable!("Got an unexpected ContentItem type when processing quotes."),
322 }
323}
324
325fn generate_pseudo_element_content(
327 pseudo_element_info: &NodeAndStyleInfo,
328 context: &LayoutContext,
329) -> Vec<PseudoElementContentItem> {
330 match &pseudo_element_info.style.get_counters().content {
331 Content::Items(items) => {
332 let mut vec = vec![];
333 for item in items.items.iter() {
334 match item {
335 ContentItem::String(s) => {
336 vec.push(PseudoElementContentItem::Text(s.to_string()));
337 },
338 ContentItem::Attr(attr) => {
339 let element = pseudo_element_info
340 .node
341 .as_element()
342 .expect("Expected an element");
343
344 let attr_name = match element.is_html_element_in_html_document() {
359 true => &*attr.attribute.to_ascii_lowercase(),
360 false => &*attr.attribute,
361 };
362
363 let attr_val =
364 element.get_attr(&attr.namespace_url, &LocalName::from(attr_name));
365 vec.push(PseudoElementContentItem::Text(
366 attr_val.map_or("".to_string(), |s| s.to_string()),
367 ));
368 },
369 ContentItem::Image(image) => {
370 if let Some(replaced_content) =
371 ReplacedContents::from_image(pseudo_element_info.node, context, image)
372 {
373 vec.push(PseudoElementContentItem::Replaced(replaced_content));
374 }
375 },
376 ContentItem::OpenQuote | ContentItem::CloseQuote => {
377 let maybe_quote = match &pseudo_element_info.style.get_list().quotes {
379 Quotes::QuoteList(quote_list) => {
380 quote_list.0.first().map(|quote_pair| {
381 get_quote_from_pair(
382 item,
383 &*quote_pair.opening,
384 &*quote_pair.closing,
385 )
386 })
387 },
388 Quotes::Auto => {
389 let lang = &pseudo_element_info.style.get_font()._x_lang;
390 let quotes = quotes_for_lang(lang.0.as_ref(), 0);
391 Some(get_quote_from_pair(item, "es.opening, "es.closing))
392 },
393 };
394 if let Some(quote) = maybe_quote {
395 vec.push(PseudoElementContentItem::Text(quote));
396 }
397 },
398 ContentItem::Counter(_, _) |
399 ContentItem::Counters(_, _, _) |
400 ContentItem::NoOpenQuote |
401 ContentItem::NoCloseQuote => {
402 },
404 }
405 }
406 vec
407 },
408 Content::Normal | Content::None => unreachable!(),
409 }
410}