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 traverse_eager_pseudo_element(PseudoElement::Before, parent_element_info, context, handler);
130
131 if parent_element_info.node.is_text_input() {
136 let node_text_content = parent_element_info.node.node_text_content();
137 if node_text_content.is_empty() {
138 handler.handle_text(parent_element_info, "\u{200B}".into());
139 } else {
140 handler.handle_text(parent_element_info, node_text_content);
141 }
142 } else {
143 for child in parent_element_info.node.children() {
144 if child.is_text_node() {
145 let info = NodeAndStyleInfo::new(
146 child,
147 child.style(&context.style_context),
148 child.take_restyle_damage(),
149 );
150 handler.handle_text(&info, child.node_text_content());
151 } else if child.is_element() {
152 traverse_element(child, context, handler);
153 }
154 }
155 }
156
157 traverse_eager_pseudo_element(PseudoElement::After, parent_element_info, context, handler);
158}
159
160fn traverse_element<'dom>(
161 element: ServoThreadSafeLayoutNode<'dom>,
162 context: &LayoutContext,
163 handler: &mut impl TraversalHandler<'dom>,
164) {
165 let damage = element.take_restyle_damage();
166 if damage.has_box_damage() {
167 element.unset_all_pseudo_boxes();
168 }
169
170 let style = element.style(&context.style_context);
171 let info = NodeAndStyleInfo::new(element, style, damage);
172
173 match Display::from(info.style.get_box().display) {
174 Display::None => element.unset_all_boxes(),
175 Display::Contents => {
176 if ReplacedContents::for_element(element, context).is_some() {
177 element.unset_all_boxes()
180 } else {
181 let shared_inline_styles: SharedInlineStyles = (&info).into();
182 element
183 .box_slot()
184 .set(LayoutBox::DisplayContents(shared_inline_styles.clone()));
185
186 handler.enter_display_contents(shared_inline_styles);
187 traverse_children_of(&info, context, handler);
188 handler.leave_display_contents();
189 }
190 },
191 Display::GeneratingBox(display) => {
192 let contents = Contents::for_element(element, context);
193 let display = display.used_value_for_contents(&contents);
194 let box_slot = element.box_slot();
195 handler.handle_element(&info, display, contents, box_slot);
196 },
197 }
198}
199
200fn traverse_eager_pseudo_element<'dom>(
201 pseudo_element_type: PseudoElement,
202 node_info: &NodeAndStyleInfo<'dom>,
203 context: &LayoutContext,
204 handler: &mut impl TraversalHandler<'dom>,
205) {
206 assert!(pseudo_element_type.is_eager());
207
208 let Some(pseudo_element_info) = node_info.with_pseudo_element(context, pseudo_element_type)
211 else {
212 return;
213 };
214 if pseudo_element_info.style.ineffective_content_property() {
215 return;
216 }
217
218 match Display::from(pseudo_element_info.style.get_box().display) {
219 Display::None => {},
220 Display::Contents => {
221 let items = generate_pseudo_element_content(&pseudo_element_info, context);
222 let box_slot = pseudo_element_info.node.box_slot();
223 let shared_inline_styles: SharedInlineStyles = (&pseudo_element_info).into();
224 box_slot.set(LayoutBox::DisplayContents(shared_inline_styles.clone()));
225
226 handler.enter_display_contents(shared_inline_styles);
227 traverse_pseudo_element_contents(&pseudo_element_info, context, handler, items);
228 handler.leave_display_contents();
229 },
230 Display::GeneratingBox(display) => {
231 let items = generate_pseudo_element_content(&pseudo_element_info, context);
232 let box_slot = pseudo_element_info.node.box_slot();
233 let contents = Contents::for_pseudo_element(items);
234 handler.handle_element(&pseudo_element_info, display, contents, box_slot);
235 },
236 }
237}
238
239fn traverse_pseudo_element_contents<'dom>(
240 info: &NodeAndStyleInfo<'dom>,
241 context: &LayoutContext,
242 handler: &mut impl TraversalHandler<'dom>,
243 items: Vec<PseudoElementContentItem>,
244) {
245 let mut anonymous_info = None;
246 for item in items {
247 match item {
248 PseudoElementContentItem::Text(text) => handler.handle_text(info, text.into()),
249 PseudoElementContentItem::Replaced(contents) => {
250 let anonymous_info = anonymous_info.get_or_insert_with(|| {
251 info.with_pseudo_element(context, PseudoElement::ServoAnonymousBox)
252 .unwrap_or_else(|| info.clone())
253 });
254 let display_inline = DisplayGeneratingBox::OutsideInside {
255 outside: DisplayOutside::Inline,
256 inside: DisplayInside::Flow {
257 is_list_item: false,
258 },
259 };
260 debug_assert!(
262 Display::from(anonymous_info.style.get_box().display) ==
263 Display::GeneratingBox(display_inline)
264 );
265 handler.handle_element(
266 anonymous_info,
267 display_inline,
268 Contents::Replaced(contents),
269 anonymous_info.node.box_slot(),
270 )
271 },
272 }
273 }
274}
275
276impl Contents {
277 pub fn is_replaced(&self) -> bool {
279 matches!(self, Contents::Replaced(_))
280 }
281
282 pub(crate) fn for_element(
283 node: ServoThreadSafeLayoutNode<'_>,
284 context: &LayoutContext,
285 ) -> Self {
286 if let Some(replaced) = ReplacedContents::for_element(node, context) {
287 return Self::Replaced(replaced);
288 }
289 let is_widget = matches!(
290 node.type_id(),
291 Some(LayoutNodeType::Element(
292 LayoutElementType::HTMLInputElement |
293 LayoutElementType::HTMLSelectElement |
294 LayoutElementType::HTMLTextAreaElement
295 ))
296 );
297 if is_widget {
298 Self::Widget(NonReplacedContents::OfElement)
299 } else {
300 Self::NonReplaced(NonReplacedContents::OfElement)
301 }
302 }
303
304 pub(crate) fn for_pseudo_element(contents: Vec<PseudoElementContentItem>) -> Self {
305 Self::NonReplaced(NonReplacedContents::OfPseudoElement(contents))
306 }
307
308 pub(crate) fn non_replaced_contents(self) -> Option<NonReplacedContents> {
309 match self {
310 Self::NonReplaced(contents) | Self::Widget(contents) => Some(contents),
311 Self::Replaced(_) => None,
312 }
313 }
314}
315
316impl NonReplacedContents {
317 pub(crate) fn traverse<'dom>(
318 self,
319 context: &LayoutContext,
320 info: &NodeAndStyleInfo<'dom>,
321 handler: &mut impl TraversalHandler<'dom>,
322 ) {
323 match self {
324 NonReplacedContents::OfElement => traverse_children_of(info, context, handler),
325 NonReplacedContents::OfPseudoElement(items) => {
326 traverse_pseudo_element_contents(info, context, handler, items)
327 },
328 }
329 }
330}
331
332fn get_quote_from_pair<I, S>(item: &ContentItem<I>, opening: &S, closing: &S) -> String
333where
334 S: ToString + ?Sized,
335{
336 match item {
337 ContentItem::OpenQuote => opening.to_string(),
338 ContentItem::CloseQuote => closing.to_string(),
339 _ => unreachable!("Got an unexpected ContentItem type when processing quotes."),
340 }
341}
342
343fn generate_pseudo_element_content(
345 pseudo_element_info: &NodeAndStyleInfo,
346 context: &LayoutContext,
347) -> Vec<PseudoElementContentItem> {
348 match &pseudo_element_info.style.get_counters().content {
349 Content::Items(items) => {
350 let mut vec = vec![];
351 for item in items.items.iter() {
352 match item {
353 ContentItem::String(s) => {
354 vec.push(PseudoElementContentItem::Text(s.to_string()));
355 },
356 ContentItem::Attr(attr) => {
357 let element = pseudo_element_info
358 .node
359 .as_element()
360 .expect("Expected an element");
361
362 let attr_name = match element.is_html_element_in_html_document() {
377 true => &*attr.attribute.to_ascii_lowercase(),
378 false => &*attr.attribute,
379 };
380
381 let attr_val =
382 element.get_attr(&attr.namespace_url, &LocalName::from(attr_name));
383 vec.push(PseudoElementContentItem::Text(
384 attr_val.map_or("".to_string(), |s| s.to_string()),
385 ));
386 },
387 ContentItem::Image(image) => {
388 if let Some(replaced_content) =
389 ReplacedContents::from_image(pseudo_element_info.node, context, image)
390 {
391 vec.push(PseudoElementContentItem::Replaced(replaced_content));
392 }
393 },
394 ContentItem::OpenQuote | ContentItem::CloseQuote => {
395 let maybe_quote = match &pseudo_element_info.style.get_list().quotes {
397 Quotes::QuoteList(quote_list) => {
398 quote_list.0.first().map(|quote_pair| {
399 get_quote_from_pair(
400 item,
401 &*quote_pair.opening,
402 &*quote_pair.closing,
403 )
404 })
405 },
406 Quotes::Auto => {
407 let lang = &pseudo_element_info.style.get_font()._x_lang;
408 let quotes = quotes_for_lang(lang.0.as_ref(), 0);
409 Some(get_quote_from_pair(item, "es.opening, "es.closing))
410 },
411 };
412 if let Some(quote) = maybe_quote {
413 vec.push(PseudoElementContentItem::Text(quote));
414 }
415 },
416 ContentItem::Counter(_, _) |
417 ContentItem::Counters(_, _, _) |
418 ContentItem::NoOpenQuote |
419 ContentItem::NoCloseQuote => {
420 },
422 }
423 }
424 vec
425 },
426 Content::Normal | Content::None => unreachable!(),
427 }
428}