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 Widget(NonReplacedContents),
84 Replaced(ReplacedContents),
87}
88
89#[derive(Debug)]
90#[allow(clippy::enum_variant_names)]
91pub(super) enum NonReplacedContents {
92 OfElement,
94 OfPseudoElement(Vec<PseudoElementContentItem>),
97}
98
99#[derive(Debug)]
100pub(super) enum PseudoElementContentItem {
101 Text(String),
102 Replaced(ReplacedContents),
103}
104
105pub(super) trait TraversalHandler<'dom> {
106 fn handle_text(&mut self, info: &NodeAndStyleInfo<'dom>, text: Cow<'dom, str>);
107
108 fn handle_element(
110 &mut self,
111 info: &NodeAndStyleInfo<'dom>,
112 display: DisplayGeneratingBox,
113 contents: Contents,
114 box_slot: BoxSlot<'dom>,
115 );
116
117 fn enter_display_contents(&mut self, _: SharedInlineStyles) {}
119
120 fn leave_display_contents(&mut self) {}
122}
123
124fn traverse_children_of<'dom>(
125 parent_element_info: &NodeAndStyleInfo<'dom>,
126 context: &LayoutContext,
127 handler: &mut impl TraversalHandler<'dom>,
128) {
129 parent_element_info
130 .node
131 .set_uses_content_attribute_with_attr(false);
132
133 let is_element = parent_element_info.pseudo_element_chain().is_empty();
134 if is_element {
135 traverse_eager_pseudo_element(PseudoElement::Before, parent_element_info, context, handler);
136 }
137
138 if parent_element_info.node.is_text_input() {
143 let node_text_content = parent_element_info.node.node_text_content();
144 if node_text_content.is_empty() {
145 handler.handle_text(parent_element_info, "\u{200B}".into());
146 } else {
147 handler.handle_text(parent_element_info, node_text_content);
148 }
149 } else {
150 for child in parent_element_info.node.children() {
151 if child.is_text_node() {
152 let info = NodeAndStyleInfo::new(
153 child,
154 child.style(&context.style_context),
155 child.take_restyle_damage(),
156 );
157 handler.handle_text(&info, child.node_text_content());
158 } else if child.is_element() {
159 traverse_element(child, context, handler);
160 }
161 }
162 }
163
164 if is_element {
165 traverse_eager_pseudo_element(PseudoElement::After, parent_element_info, context, handler);
166 }
167}
168
169fn traverse_element<'dom>(
170 element: ServoThreadSafeLayoutNode<'dom>,
171 context: &LayoutContext,
172 handler: &mut impl TraversalHandler<'dom>,
173) {
174 let damage = element.take_restyle_damage();
175 if damage.has_box_damage() {
176 element.unset_all_pseudo_boxes();
177 }
178
179 let style = element.style(&context.style_context);
180 let info = NodeAndStyleInfo::new(element, style, damage);
181
182 match Display::from(info.style.get_box().display) {
183 Display::None => element.unset_all_boxes(),
184 Display::Contents => {
185 if ReplacedContents::for_element(element, context).is_some() {
186 element.unset_all_boxes()
189 } else {
190 let shared_inline_styles: SharedInlineStyles = (&info).into();
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);
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 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: SharedInlineStyles = (&pseudo_element_info).into();
233 box_slot.set(LayoutBox::DisplayContents(shared_inline_styles.clone()));
234
235 handler.enter_display_contents(shared_inline_styles);
236 traverse_pseudo_element_contents(&pseudo_element_info, context, handler, items);
237 handler.leave_display_contents();
238 },
239 Display::GeneratingBox(display) => {
240 let items = generate_pseudo_element_content(&pseudo_element_info, context);
241 let box_slot = pseudo_element_info.node.box_slot();
242 let contents = Contents::for_pseudo_element(items);
243 handler.handle_element(&pseudo_element_info, display, contents, box_slot);
244 },
245 }
246}
247
248fn traverse_pseudo_element_contents<'dom>(
249 info: &NodeAndStyleInfo<'dom>,
250 context: &LayoutContext,
251 handler: &mut impl TraversalHandler<'dom>,
252 items: Vec<PseudoElementContentItem>,
253) {
254 let mut anonymous_info = None;
255 for item in items {
256 match item {
257 PseudoElementContentItem::Text(text) => handler.handle_text(info, text.into()),
258 PseudoElementContentItem::Replaced(contents) => {
259 let anonymous_info = anonymous_info.get_or_insert_with(|| {
260 info.with_pseudo_element(context, PseudoElement::ServoAnonymousBox)
261 .unwrap_or_else(|| info.clone())
262 });
263 let display_inline = DisplayGeneratingBox::OutsideInside {
264 outside: DisplayOutside::Inline,
265 inside: DisplayInside::Flow {
266 is_list_item: false,
267 },
268 };
269 debug_assert!(
271 Display::from(anonymous_info.style.get_box().display) ==
272 Display::GeneratingBox(display_inline)
273 );
274 handler.handle_element(
275 anonymous_info,
276 display_inline,
277 Contents::Replaced(contents),
278 anonymous_info.node.box_slot(),
279 )
280 },
281 }
282 }
283}
284
285impl Contents {
286 pub fn is_replaced(&self) -> bool {
288 matches!(self, Contents::Replaced(_))
289 }
290
291 pub(crate) fn for_element(
292 node: ServoThreadSafeLayoutNode<'_>,
293 context: &LayoutContext,
294 ) -> Self {
295 if let Some(replaced) = ReplacedContents::for_element(node, context) {
296 return Self::Replaced(replaced);
297 }
298 let is_widget = matches!(
299 node.type_id(),
300 Some(LayoutNodeType::Element(
301 LayoutElementType::HTMLInputElement |
302 LayoutElementType::HTMLSelectElement |
303 LayoutElementType::HTMLTextAreaElement
304 ))
305 );
306 if is_widget {
307 Self::Widget(NonReplacedContents::OfElement)
308 } else {
309 Self::NonReplaced(NonReplacedContents::OfElement)
310 }
311 }
312
313 pub(crate) fn for_pseudo_element(contents: Vec<PseudoElementContentItem>) -> Self {
314 Self::NonReplaced(NonReplacedContents::OfPseudoElement(contents))
315 }
316
317 pub(crate) fn non_replaced_contents(self) -> Option<NonReplacedContents> {
318 match self {
319 Self::NonReplaced(contents) | Self::Widget(contents) => Some(contents),
320 Self::Replaced(_) => None,
321 }
322 }
323}
324
325impl NonReplacedContents {
326 pub(crate) fn traverse<'dom>(
327 self,
328 context: &LayoutContext,
329 info: &NodeAndStyleInfo<'dom>,
330 handler: &mut impl TraversalHandler<'dom>,
331 ) {
332 match self {
333 NonReplacedContents::OfElement => traverse_children_of(info, context, handler),
334 NonReplacedContents::OfPseudoElement(items) => {
335 traverse_pseudo_element_contents(info, context, handler, items)
336 },
337 }
338 }
339}
340
341fn get_quote_from_pair<I, S>(item: &ContentItem<I>, opening: &S, closing: &S) -> String
342where
343 S: ToString + ?Sized,
344{
345 match item {
346 ContentItem::OpenQuote => opening.to_string(),
347 ContentItem::CloseQuote => closing.to_string(),
348 _ => unreachable!("Got an unexpected ContentItem type when processing quotes."),
349 }
350}
351
352fn generate_pseudo_element_content(
354 pseudo_element_info: &NodeAndStyleInfo,
355 context: &LayoutContext,
356) -> Vec<PseudoElementContentItem> {
357 match &pseudo_element_info.style.get_counters().content {
358 Content::Items(items) => {
359 let mut vec = vec![];
360 for item in items.items.iter() {
361 match item {
362 ContentItem::String(s) => {
363 vec.push(PseudoElementContentItem::Text(s.to_string()));
364 },
365 ContentItem::Attr(attr) => {
366 let element = pseudo_element_info
367 .node
368 .as_element()
369 .expect("Expected an element");
370
371 let attr_name = match element.is_html_element_in_html_document() {
386 true => &*attr.attribute.to_ascii_lowercase(),
387 false => &*attr.attribute,
388 };
389
390 pseudo_element_info
391 .node
392 .set_uses_content_attribute_with_attr(true);
393 let attr_val =
394 element.get_attr(&attr.namespace_url, &LocalName::from(attr_name));
395 vec.push(PseudoElementContentItem::Text(
396 attr_val.map_or("".to_string(), |s| s.to_string()),
397 ));
398 },
399 ContentItem::Image(image) => {
400 if let Some(replaced_content) =
401 ReplacedContents::from_image(pseudo_element_info.node, context, image)
402 {
403 vec.push(PseudoElementContentItem::Replaced(replaced_content));
404 }
405 },
406 ContentItem::OpenQuote | ContentItem::CloseQuote => {
407 let maybe_quote = match &pseudo_element_info.style.get_list().quotes {
409 Quotes::QuoteList(quote_list) => {
410 quote_list.0.first().map(|quote_pair| {
411 get_quote_from_pair(
412 item,
413 &*quote_pair.opening,
414 &*quote_pair.closing,
415 )
416 })
417 },
418 Quotes::Auto => {
419 let lang = &pseudo_element_info.style.get_font()._x_lang;
420 let quotes = quotes_for_lang(lang.0.as_ref(), 0);
421 Some(get_quote_from_pair(item, "es.opening, "es.closing))
422 },
423 };
424 if let Some(quote) = maybe_quote {
425 vec.push(PseudoElementContentItem::Text(quote));
426 }
427 },
428 ContentItem::Counter(_, _) |
429 ContentItem::Counters(_, _, _) |
430 ContentItem::NoOpenQuote |
431 ContentItem::NoCloseQuote => {
432 },
434 }
435 }
436 vec
437 },
438 Content::Normal | Content::None => unreachable!(),
439 }
440}