Skip to main content

layout/flow/inline/
construct.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::cell::LazyCell;
7use std::char::{ToLowercase, ToUppercase};
8use std::ops::{ControlFlow, Range};
9
10use icu_properties::BidiClass;
11use icu_segmenter::WordSegmenter;
12use layout_api::{LayoutNode, SharedSelection};
13use servo_base::text::Utf32CodeUnits;
14use style::computed_values::_webkit_text_security::T as WebKitTextSecurity;
15use style::computed_values::direction::T as Direction;
16use style::computed_values::white_space_collapse::T as WhiteSpaceCollapse;
17use style::dom::NodeInfo;
18use style::selector_parser::PseudoElement;
19use style::values::specified::text::TextTransformCase;
20use unicode_bidi::Level;
21use unicode_categories::UnicodeCategories;
22
23use super::text_run::TextRun;
24use super::{
25    InlineBox, InlineBoxIdentifier, InlineBoxes, InlineFormattingContext, InlineItem,
26    SharedInlineStyles,
27};
28use crate::cell::ArcRefCell;
29use crate::context::LayoutContext;
30use crate::dom::{LayoutBox, NodeExt};
31use crate::dom_traversal::{BoxTreeString, NodeAndStyleInfo};
32use crate::flow::BlockLevelBox;
33use crate::flow::float::FloatBox;
34use crate::formatting_contexts::IndependentFormattingContext;
35use crate::positioned::AbsolutelyPositionedBox;
36use crate::style_ext::ComputedValuesExt;
37
38#[derive(Default)]
39pub(crate) struct InlineFormattingContextBuilder {
40    /// A stack of [`SharedInlineStyles`] including one for the root, one for each inline box on the
41    /// inline box stack, and importantly, one for every `display: contents` element that we are
42    /// currently processing. Normally `display: contents` elements don't affect the structure of
43    /// the [`InlineFormattingContext`], but the styles they provide do style their children.
44    pub shared_inline_styles_stack: Vec<SharedInlineStyles>,
45
46    /// The collection of text strings that make up this [`InlineFormattingContext`] under
47    /// construction.
48    pub text_segments: Vec<String>,
49
50    /// The current offset in the final text string of this [`InlineFormattingContext`],
51    /// used to properly set the text range of new [`InlineItem::TextRun`]s.
52    current_text_offset: usize,
53
54    /// The current character offset in the final text string of this [`InlineFormattingContext`],
55    /// used to properly set the text range of new [`InlineItem::TextRun`]s. Note that this is
56    /// different from the UTF-8 code point offset.
57    current_character_offset: usize,
58
59    /// If the [`InlineFormattingContext`] that we are building has a selection shared with its
60    /// originating node in the DOM, this will not be `None`.
61    pub shared_selection: Option<SharedSelection>,
62
63    /// Whether the last processed node ended with whitespace. This is used to
64    /// implement rule 4 of <https://www.w3.org/TR/css-text-3/#collapse>:
65    ///
66    /// > Any collapsible space immediately following another collapsible space—even one
67    /// > outside the boundary of the inline containing that space, provided both spaces are
68    /// > within the same inline formatting context—is collapsed to have zero advance width.
69    /// > (It is invisible, but retains its soft wrap opportunity, if any.)
70    last_inline_box_ended_with_collapsible_white_space: bool,
71
72    /// Whether or not the current state of the inline formatting context is on a word boundary
73    /// for the purposes of `text-transform: capitalize`.
74    on_word_boundary: bool,
75
76    /// Whether or not this inline formatting context will contain floats.
77    pub contains_floats: bool,
78
79    /// The current list of [`InlineItem`]s in this [`InlineFormattingContext`] under
80    /// construction. This is stored in a flat list to make it easy to access the last
81    /// item.
82    pub inline_items: Vec<InlineItem>,
83
84    /// The current [`InlineBox`] tree of this [`InlineFormattingContext`] under construction.
85    pub inline_boxes: InlineBoxes,
86
87    /// The ongoing stack of inline boxes stack of the builder.
88    ///
89    /// Contains all the currently ongoing inline boxes we entered so far.
90    /// The traversal is at all times as deep in the tree as this stack is,
91    /// which is why the code doesn't need to keep track of the actual
92    /// container root (see `handle_inline_level_element`).
93    ///
94    /// When an inline box ends, it's removed from this stack.
95    inline_box_stack: Vec<InlineBoxIdentifier>,
96
97    /// Whether this [`InlineFormattingContextBuilder`] is empty for the purposes of ignoring
98    /// during box tree construction. An IFC is empty if it only contains TextRuns with
99    /// completely collapsible whitespace. When that happens it can be ignored completely.
100    pub is_empty: bool,
101
102    /// Whether or not the `::first-letter` pseudo-element of this inline formatting context
103    /// has been processed yet.
104    has_processed_first_letter: bool,
105
106    /// Whether or not the inline formatting context under construction has any kind of
107    /// right-to-left content such as a character with an RTL character class or a `dir`
108    /// attribute specifying right-to-left content.
109    pub(crate) has_right_to_left_content: bool,
110}
111
112impl InlineFormattingContextBuilder {
113    /// <https://drafts.csswg.org/css-text/#white-space>:
114    /// > Except where specified otherwise, white space processing in CSS affects only the document
115    /// > white space characters: spaces (U+0020), tabs (U+0009), and segment breaks.
116    ///
117    /// From <https://github.com/w3c/csswg-drafts/issues/5147#issuecomment-637816669>:
118    /// > HTML clearly treats CR, LF, and CRLF as segment breaks.
119    ///
120    /// Other browsers also consider the form feed character (0x0c) to be document white space, it
121    /// seems.
122    ///
123    /// Taken all together, this is equivalent to the WhatWG Infra Standard's definition of ASCII
124    /// white space.
125    pub(crate) fn is_document_white_space(character: char) -> bool {
126        character.is_ascii_whitespace()
127    }
128
129    pub(crate) fn new(info: &NodeAndStyleInfo, context: &LayoutContext) -> Self {
130        let has_right_to_left_content = info.style.get_inherited_box().direction == Direction::Rtl;
131        Self {
132            // For the purposes of `text-transform: capitalize` the start of the IFC is a word boundary.
133            on_word_boundary: true,
134            is_empty: true,
135            shared_inline_styles_stack: vec![SharedInlineStyles::from_info_and_context(
136                info, context,
137            )],
138            shared_selection: info.node.selection(),
139            has_right_to_left_content,
140            ..Default::default()
141        }
142    }
143
144    pub(crate) fn currently_processing_inline_box(&self) -> bool {
145        !self.inline_box_stack.is_empty()
146    }
147
148    fn push_control_character_string(&mut self, string_to_push: &str) {
149        self.text_segments.push(string_to_push.to_owned());
150        self.current_text_offset += string_to_push.len();
151        self.current_character_offset += string_to_push.chars().count();
152    }
153
154    fn shared_inline_styles(&self) -> SharedInlineStyles {
155        self.shared_inline_styles_stack
156            .last()
157            .expect("Should always have at least one SharedInlineStyles")
158            .clone()
159    }
160
161    pub(crate) fn push_atomic(
162        &mut self,
163        independent_formatting_context_creator: impl FnOnce()
164            -> ArcRefCell<IndependentFormattingContext>,
165        old_layout_box: Option<LayoutBox>,
166    ) -> InlineItem {
167        // If there is an existing undamaged layout box that's compatible, use that.
168        let independent_formatting_context = old_layout_box
169            .and_then(|layout_box| match layout_box {
170                LayoutBox::InlineLevel(InlineItem::Atomic(atomic, ..)) => Some(atomic),
171                _ => None,
172            })
173            .unwrap_or_else(independent_formatting_context_creator);
174
175        let inline_level_box = InlineItem::Atomic(
176            independent_formatting_context,
177            self.current_text_offset,
178            Level::ltr(), /* This will be assigned later if necessary. */
179        );
180        self.inline_items.push(inline_level_box.clone());
181        self.is_empty = false;
182
183        // Push an object replacement character for this atomic, which will ensure that the line breaker
184        // inserts a line breaking opportunity here.
185        self.push_control_character_string("\u{fffc}");
186
187        self.last_inline_box_ended_with_collapsible_white_space = false;
188        self.on_word_boundary = true;
189
190        // Atomics such as images should prevent any following text as being interpreted as the first letter.
191        self.has_processed_first_letter = true;
192
193        inline_level_box
194    }
195
196    pub(crate) fn push_absolutely_positioned_box(
197        &mut self,
198        absolutely_positioned_box_creator: impl FnOnce() -> ArcRefCell<AbsolutelyPositionedBox>,
199        old_layout_box: Option<LayoutBox>,
200    ) -> InlineItem {
201        let absolutely_positioned_box = old_layout_box
202            .and_then(|layout_box| match layout_box {
203                LayoutBox::InlineLevel(InlineItem::OutOfFlowAbsolutelyPositionedBox(
204                    positioned_box,
205                    ..,
206                )) => Some(positioned_box),
207                _ => None,
208            })
209            .unwrap_or_else(absolutely_positioned_box_creator);
210
211        // We cannot just reuse the old inline item, because the `current_text_offset` may have changed.
212        let inline_level_box = InlineItem::OutOfFlowAbsolutelyPositionedBox(
213            absolutely_positioned_box,
214            self.current_text_offset,
215        );
216
217        self.inline_items.push(inline_level_box.clone());
218        self.is_empty = false;
219        inline_level_box
220    }
221
222    pub(crate) fn push_float_box(
223        &mut self,
224        float_box_creator: impl FnOnce() -> ArcRefCell<FloatBox>,
225        old_layout_box: Option<LayoutBox>,
226    ) -> InlineItem {
227        let inline_level_box = old_layout_box
228            .and_then(|layout_box| match layout_box {
229                LayoutBox::InlineLevel(inline_item) => Some(inline_item),
230                _ => None,
231            })
232            .unwrap_or_else(|| InlineItem::OutOfFlowFloatBox(float_box_creator()));
233
234        debug_assert!(
235            matches!(inline_level_box, InlineItem::OutOfFlowFloatBox(..),),
236            "Created float box with incompatible `old_layout_box`"
237        );
238
239        self.inline_items.push(inline_level_box.clone());
240        self.is_empty = false;
241        self.contains_floats = true;
242        inline_level_box
243    }
244
245    pub(crate) fn push_block_level_box(&mut self, block_level: ArcRefCell<BlockLevelBox>) {
246        assert!(self.currently_processing_inline_box());
247        self.contains_floats = self.contains_floats || block_level.borrow().contains_floats();
248        self.inline_items.push(InlineItem::BlockLevel(block_level));
249    }
250
251    pub(crate) fn start_inline_box(
252        &mut self,
253        inline_box_creator: impl FnOnce() -> ArcRefCell<InlineBox>,
254        old_layout_box: Option<LayoutBox>,
255    ) -> InlineItem {
256        // If there is an existing undamaged layout box that's compatible, use the `InlineBox` within it.
257        let inline_box = old_layout_box
258            .and_then(|layout_box| match layout_box {
259                LayoutBox::InlineLevel(InlineItem::StartInlineBox(inline_box)) => Some(inline_box),
260                _ => None,
261            })
262            .unwrap_or_else(inline_box_creator);
263
264        let borrowed_inline_box = inline_box.borrow();
265
266        let style = &borrowed_inline_box.base.style;
267        self.push_control_character_string(style.bidi_control_chars().0);
268        self.has_right_to_left_content =
269            self.has_right_to_left_content || style.get_inherited_box().direction == Direction::Rtl;
270
271        self.shared_inline_styles_stack
272            .push(borrowed_inline_box.shared_inline_styles.clone());
273        std::mem::drop(borrowed_inline_box);
274
275        let identifier = self.inline_boxes.start_inline_box(inline_box.clone());
276        let inline_item = InlineItem::StartInlineBox(inline_box);
277        self.inline_items.push(inline_item.clone());
278        self.inline_box_stack.push(identifier);
279        self.is_empty = false;
280        inline_item
281    }
282
283    /// End the ongoing inline box in this [`InlineFormattingContextBuilder`], returning
284    /// shared references to all of the box tree items that were created for it. More than
285    /// a single box tree items may be produced for a single inline box when that inline
286    /// box is split around a block-level element.
287    pub(crate) fn end_inline_box(&mut self) {
288        let identifier = self
289            .inline_box_stack
290            .pop()
291            .expect("Ended non-existent inline box");
292        let inline_level_box = self.inline_boxes.get(&identifier);
293
294        self.shared_inline_styles_stack.pop();
295        self.inline_items
296            .push(InlineItem::EndInlineBox(inline_level_box.clone()));
297        self.inline_boxes.end_inline_box(identifier);
298        let bidi_control_chars = inline_level_box.borrow().base.style.bidi_control_chars();
299        self.push_control_character_string(bidi_control_chars.1);
300    }
301
302    /// This is like [`Self::push_text`], except that it might possibly add an anonymous box if
303    ///
304    ///  - This inline formatting context has a `::first-letter` style.
305    ///  - No anonymous box for `::first-letter` has been added yet.
306    ///  - First letter content is detected in this text.
307    ///
308    /// Note that this should only be used when processing text in block containers.
309    pub(crate) fn push_text_with_possible_first_letter<'dom>(
310        &mut self,
311        text: BoxTreeString<'dom>,
312        info: &NodeAndStyleInfo<'dom>,
313        container_info: &NodeAndStyleInfo<'dom>,
314        layout_context: &LayoutContext,
315    ) -> bool {
316        let document_selection = info.node.document_selection_in_text_node();
317        if self.has_processed_first_letter || !container_info.pseudo_element_chain().is_empty() {
318            self.push_text(text, info, document_selection);
319            return false;
320        }
321
322        let Some(first_letter_info) =
323            container_info.with_pseudo_element(layout_context, PseudoElement::FirstLetter)
324        else {
325            self.push_text(text, info, document_selection);
326            return false;
327        };
328
329        let first_letter_range = first_letter_range(&text[..]);
330        if first_letter_range.is_empty() {
331            return false;
332        }
333
334        let intersect_ranges = |a: Range<Utf32CodeUnits>, b: Range<Utf32CodeUnits>| {
335            let start = a.start.max(b.start);
336            let end = b.end.min(b.end);
337            if start < end { Some(start..end) } else { None }
338        };
339
340        // Push any leading white space first.
341        let first_letter_range_u32 = LazyCell::new(|| {
342            Utf32CodeUnits::length_of(&text[..first_letter_range.start])..
343                Utf32CodeUnits::length_of(&text[..first_letter_range.end])
344        });
345        if first_letter_range.start != 0 {
346            let leading_whitespace_range = 0..first_letter_range.start;
347            let leading_whitespace_selection_range =
348                document_selection.clone().and_then(|document_selection| {
349                    let leading_whitespace_range_u32 =
350                        Utf32CodeUnits::zero()..first_letter_range_u32.start;
351                    intersect_ranges(document_selection, leading_whitespace_range_u32)
352                });
353
354            self.push_text(
355                Cow::Borrowed(&text[leading_whitespace_range]).into(),
356                info,
357                leading_whitespace_selection_range,
358            );
359        }
360
361        // Push the first-letter text into an anonymous box with the `::first-letter` style.
362        let box_slot = first_letter_info.node.box_slot();
363        let inline_item = self.start_inline_box(
364            || ArcRefCell::new(InlineBox::new(&first_letter_info, layout_context)),
365            None,
366        );
367        box_slot.set(LayoutBox::InlineLevel(inline_item));
368
369        let first_letter_text = Cow::Borrowed(&text[first_letter_range.clone()]);
370        let first_letter_selection_range =
371            document_selection.clone().and_then(|document_selection| {
372                intersect_ranges(document_selection, (*first_letter_range_u32).clone()).map(
373                    |range| {
374                        range.start - first_letter_range_u32.start..
375                            range.end - first_letter_range_u32.start
376                    },
377                )
378            });
379        self.push_text(
380            first_letter_text.into(),
381            &first_letter_info,
382            first_letter_selection_range,
383        );
384        self.end_inline_box();
385        self.has_processed_first_letter = true;
386
387        // Now push the non-first-letter text.
388        let remaining_selection_range = document_selection.and_then(|document_selection| {
389            let remaining_text_range_u32 = first_letter_range_u32.end..document_selection.end;
390            intersect_ranges(document_selection, remaining_text_range_u32).map(|range| {
391                range.start - first_letter_range_u32.end..range.end - first_letter_range_u32.end
392            })
393        });
394        self.push_text(
395            Cow::Borrowed(&text[first_letter_range.end..]).into(),
396            info,
397            remaining_selection_range,
398        );
399
400        true
401    }
402
403    pub(crate) fn push_text<'dom>(
404        &mut self,
405        text: BoxTreeString<'dom>,
406        info: &NodeAndStyleInfo<'dom>,
407        document_selection: Option<Range<Utf32CodeUnits>>,
408    ) {
409        let white_space_collapse = info.style.clone_white_space_collapse();
410        let collapsed = WhitespaceCollapse::new(
411            text.chars(),
412            white_space_collapse,
413            self.last_inline_box_ended_with_collapsible_white_space,
414        );
415
416        // TODO: Not all text transforms are about case, this logic should stop ignoring
417        // TextTransform::FULL_WIDTH and TextTransform::FULL_SIZE_KANA.
418        let text_transform = info.style.clone_text_transform().case();
419        let capitalized_text: String;
420        let char_iterator: Box<dyn Iterator<Item = char>> = match text_transform {
421            TextTransformCase::None => Box::new(collapsed),
422            TextTransformCase::Capitalize => {
423                // `TextTransformation` doesn't support capitalization, so we must capitalize the whole
424                // string at once and make a copy. Here `on_word_boundary` indicates whether or not the
425                // inline formatting context as a whole is on a word boundary. This is different from
426                // `last_inline_box_ended_with_collapsible_white_space` because the word boundaries are
427                // between atomic inlines and at the start of the IFC, and because preserved spaces
428                // are a word boundary.
429                let collapsed_string: String = collapsed.collect();
430                capitalized_text = capitalize_string(&collapsed_string, self.on_word_boundary);
431                Box::new(capitalized_text.chars())
432            },
433            _ => {
434                // If `text-transform` is active, wrap the `WhitespaceCollapse` iterator in
435                // a `TextTransformation` iterator.
436                Box::new(TextTransformation::new(collapsed, text_transform))
437            },
438        };
439
440        let char_iterator = if info.style.clone__webkit_text_security() != WebKitTextSecurity::None
441        {
442            Box::new(TextSecurityTransform::new(
443                char_iterator,
444                info.style.clone__webkit_text_security(),
445            ))
446        } else {
447            char_iterator
448        };
449
450        let bidi_class_map = icu_properties::maps::bidi_class();
451        let white_space_collapse = info.style.clone_white_space_collapse();
452        let mut character_count = 0;
453        let new_text: String = char_iterator
454            .inspect(|&character| {
455                character_count += 1;
456
457                // If this character has a strong right-to-left class the new inline formatting context will
458                // need to be BiDi-aware. This match is derived from the list of strong right-to-left classes
459                // at https://www.unicode.org/reports/tr44/#Bidi_Class_Values.
460                self.has_right_to_left_content = self.has_right_to_left_content ||
461                    matches!(
462                        bidi_class_map.get(character),
463                        BidiClass::RightToLeft |
464                            BidiClass::ArabicLetter |
465                            BidiClass::RightToLeftEmbedding |
466                            BidiClass::RightToLeftIsolate |
467                            BidiClass::RightToLeftOverride
468                    );
469
470                self.is_empty = self.is_empty &&
471                    match white_space_collapse {
472                        WhiteSpaceCollapse::Collapse => Self::is_document_white_space(character),
473                        WhiteSpaceCollapse::PreserveBreaks => {
474                            Self::is_document_white_space(character) && character != '\n'
475                        },
476                        WhiteSpaceCollapse::Preserve | WhiteSpaceCollapse::BreakSpaces => false,
477                    };
478            })
479            .collect();
480
481        if new_text.is_empty() {
482            return;
483        }
484
485        if let Some(last_character) = new_text.chars().next_back() {
486            self.on_word_boundary = last_character.is_whitespace();
487            self.last_inline_box_ended_with_collapsible_white_space =
488                self.on_word_boundary && white_space_collapse != WhiteSpaceCollapse::Preserve;
489        }
490
491        let new_utf8_range = self.current_text_offset..self.current_text_offset + new_text.len();
492        self.current_text_offset = new_utf8_range.end;
493
494        let new_character_range =
495            self.current_character_offset..self.current_character_offset + character_count;
496        self.current_character_offset = new_character_range.end;
497
498        self.text_segments.push(new_text);
499
500        if self
501            .try_to_push_text_range_to_previous_text_run(
502                info,
503                &document_selection,
504                &new_utf8_range,
505                &new_character_range,
506            )
507            .is_break()
508        {
509            return;
510        }
511
512        let current_inline_styles = self.shared_inline_styles();
513        let box_slot = info.node.is_text_node().then(|| info.node.box_slot());
514        let text_run = ArcRefCell::new(TextRun::new(
515            info.into(),
516            current_inline_styles,
517            new_utf8_range,
518            new_character_range,
519            document_selection.unwrap_or_default(),
520            box_slot
521                .as_ref()
522                .and_then(|box_slot| box_slot.take_layout_box_as_text_run()),
523        ));
524        self.inline_items
525            .push(InlineItem::TextRun(text_run.clone()));
526
527        if let Some(box_slot) = box_slot {
528            box_slot.set(LayoutBox::Text(text_run));
529        }
530    }
531
532    fn try_to_push_text_range_to_previous_text_run(
533        &mut self,
534        info: &NodeAndStyleInfo,
535        new_text_selection: &Option<Range<Utf32CodeUnits>>,
536        new_range: &Range<usize>,
537        new_character_range: &Range<usize>,
538    ) -> ControlFlow<()> {
539        // First check to see if the last item was actually a text run.
540        let Some(InlineItem::TextRun(text_run_arc)) = self.inline_items.last() else {
541            return ControlFlow::Continue(());
542        };
543
544        // Currently to merge two text runs the styles need to be the same.
545        if !text_run_arc
546            .borrow()
547            .inline_styles
548            .ptr_eq(&self.shared_inline_styles())
549        {
550            return ControlFlow::Continue(());
551        }
552
553        let mut text_run = text_run_arc.borrow_mut();
554        if let Some(next_text_selection) = new_text_selection {
555            let existing_characters = text_run.character_range.end - text_run.character_range.start;
556            if !text_run.document_selection.is_empty() {
557                // If both the new and old text had selections, they are only compatible
558                // if the old selection extends to the end of the old run run.
559                if text_run.document_selection.end.0 == existing_characters {
560                    text_run.document_selection.end += next_text_selection.end;
561                } else {
562                    return ControlFlow::Continue(());
563                }
564            } else {
565                // If only the new part of the text run has a selection, we can use it directly.
566                text_run.document_selection = Utf32CodeUnits(existing_characters) +
567                    next_text_selection.start..
568                    Utf32CodeUnits(existing_characters) + next_text_selection.end;
569            }
570        }
571
572        text_run.text_range.end = new_range.end;
573        text_run.character_range.end = new_character_range.end;
574
575        // If this text node does not have a `TextRun` in the box slot, this means that
576        // it is either new or dirty, which means that the entire `TextRun` just extended
577        // is dirty as well. In this case, never reuse existing shaping results. Clear
578        // all old items to ensure this.
579        let box_slot = info.node.box_slot();
580        let old_text_run = box_slot.take_layout_box_as_text_run();
581        if old_text_run.is_none() {
582            text_run.items.clear();
583        }
584
585        box_slot.set(LayoutBox::Text(text_run_arc.clone()));
586        ControlFlow::Break(())
587    }
588
589    pub(crate) fn enter_display_contents(&mut self, shared_inline_styles: SharedInlineStyles) {
590        self.shared_inline_styles_stack.push(shared_inline_styles);
591    }
592
593    pub(crate) fn leave_display_contents(&mut self) {
594        self.shared_inline_styles_stack.pop();
595    }
596
597    /// Finish the current inline formatting context, returning [`None`] if the context was empty.
598    pub(crate) fn finish(
599        self,
600        layout_context: &LayoutContext,
601        has_first_formatted_line: bool,
602        is_single_line_text_input: bool,
603        default_bidi_level: Level,
604    ) -> Option<InlineFormattingContext> {
605        if self.is_empty {
606            return None;
607        }
608
609        assert!(self.inline_box_stack.is_empty());
610        Some(InlineFormattingContext::new_with_builder(
611            self,
612            layout_context,
613            has_first_formatted_line,
614            is_single_line_text_input,
615            default_bidi_level,
616        ))
617    }
618}
619
620fn preserve_segment_break() -> bool {
621    true
622}
623
624pub struct WhitespaceCollapse<InputIterator> {
625    char_iterator: InputIterator,
626    white_space_collapse: WhiteSpaceCollapse,
627
628    /// Whether or not we should collapse white space completely at the start of the string.
629    /// This is true when the last character handled in our owning [`super::InlineFormattingContext`]
630    /// was collapsible white space.
631    remove_collapsible_white_space_at_start: bool,
632
633    /// Whether or not the last character produced was newline. There is special behavior
634    /// we do after each newline.
635    following_newline: bool,
636
637    /// Whether or not we have seen any non-white space characters, indicating that we are not
638    /// in a collapsible white space section at the beginning of the string.
639    have_seen_non_white_space_characters: bool,
640
641    /// Whether the last character that we processed was a non-newline white space character. When
642    /// collapsing white space we need to wait until the next non-white space character or the end
643    /// of the string to push a single white space.
644    inside_white_space: bool,
645
646    /// When we enter a collapsible white space region, we may need to wait to produce a single
647    /// white space character as soon as we encounter a non-white space character. When that
648    /// happens we queue up the non-white space character for the next iterator call.
649    character_pending_to_return: Option<char>,
650}
651
652impl<InputIterator> WhitespaceCollapse<InputIterator> {
653    pub fn new(
654        char_iterator: InputIterator,
655        white_space_collapse: WhiteSpaceCollapse,
656        trim_beginning_white_space: bool,
657    ) -> Self {
658        Self {
659            char_iterator,
660            white_space_collapse,
661            remove_collapsible_white_space_at_start: trim_beginning_white_space,
662            inside_white_space: false,
663            following_newline: false,
664            have_seen_non_white_space_characters: false,
665            character_pending_to_return: None,
666        }
667    }
668
669    fn is_leading_trimmed_white_space(&self) -> bool {
670        !self.have_seen_non_white_space_characters && self.remove_collapsible_white_space_at_start
671    }
672
673    /// Whether or not we need to produce a space character if the next character is not a newline
674    /// and not white space. This happens when we are exiting a section of white space and we
675    /// waited to produce a single space character for the entire section of white space (but
676    /// not following or preceding a newline).
677    fn need_to_produce_space_character_after_white_space(&self) -> bool {
678        self.inside_white_space && !self.following_newline && !self.is_leading_trimmed_white_space()
679    }
680}
681
682impl<InputIterator> Iterator for WhitespaceCollapse<InputIterator>
683where
684    InputIterator: Iterator<Item = char>,
685{
686    type Item = char;
687
688    fn next(&mut self) -> Option<Self::Item> {
689        // Point 4.1.1 first bullet:
690        // > If white-space is set to normal, nowrap, or pre-line, whitespace
691        // > characters are considered collapsible
692        // If whitespace is not considered collapsible, it is preserved entirely, which
693        // means that we can simply return the input string exactly.
694        if self.white_space_collapse == WhiteSpaceCollapse::Preserve ||
695            self.white_space_collapse == WhiteSpaceCollapse::BreakSpaces
696        {
697            // From <https://drafts.csswg.org/css-text-3/#white-space-processing>:
698            // > Carriage returns (U+000D) are treated identically to spaces (U+0020) in all respects.
699            //
700            // In the non-preserved case these are converted to space below.
701            return match self.char_iterator.next() {
702                Some('\r') => Some(' '),
703                next => next,
704            };
705        }
706
707        if let Some(character) = self.character_pending_to_return.take() {
708            self.inside_white_space = false;
709            self.have_seen_non_white_space_characters = true;
710            self.following_newline = false;
711            return Some(character);
712        }
713
714        while let Some(character) = self.char_iterator.next() {
715            // Don't push non-newline whitespace immediately. Instead wait to push it until we
716            // know that it isn't followed by a newline. See `push_pending_whitespace_if_needed`
717            // above.
718            if InlineFormattingContextBuilder::is_document_white_space(character) &&
719                character != '\n'
720            {
721                self.inside_white_space = true;
722                continue;
723            }
724
725            // Point 4.1.1:
726            // > 2. Collapsible segment breaks are transformed for rendering according to the
727            // >    segment break transformation rules.
728            if character == '\n' {
729                // From <https://drafts.csswg.org/css-text-3/#line-break-transform>
730                // (4.1.3 -- the segment break transformation rules):
731                //
732                // > When white-space is pre, pre-wrap, or pre-line, segment breaks are not
733                // > collapsible and are instead transformed into a preserved line feed"
734                if self.white_space_collapse != WhiteSpaceCollapse::Collapse {
735                    self.inside_white_space = false;
736                    self.following_newline = true;
737                    return Some(character);
738
739                // Point 4.1.3:
740                // > 1. First, any collapsible segment break immediately following another
741                // >    collapsible segment break is removed.
742                // > 2. Then any remaining segment break is either transformed into a space (U+0020)
743                // >    or removed depending on the context before and after the break.
744                } else if !self.following_newline &&
745                    preserve_segment_break() &&
746                    !self.is_leading_trimmed_white_space()
747                {
748                    self.inside_white_space = false;
749                    self.following_newline = true;
750                    return Some(' ');
751                } else {
752                    self.following_newline = true;
753                    continue;
754                }
755            }
756
757            // Point 4.1.1:
758            // > 2. Any sequence of collapsible spaces and tabs immediately preceding or
759            // >    following a segment break is removed.
760            // > 3. Every collapsible tab is converted to a collapsible space (U+0020).
761            // > 4. Any collapsible space immediately following another collapsible space—even
762            // >    one outside the boundary of the inline containing that space, provided both
763            // >    spaces are within the same inline formatting context—is collapsed to have zero
764            // >    advance width.
765            if self.need_to_produce_space_character_after_white_space() {
766                self.inside_white_space = false;
767                self.character_pending_to_return = Some(character);
768                return Some(' ');
769            }
770
771            self.inside_white_space = false;
772            self.have_seen_non_white_space_characters = true;
773            self.following_newline = false;
774            return Some(character);
775        }
776
777        if self.need_to_produce_space_character_after_white_space() {
778            self.inside_white_space = false;
779            return Some(' ');
780        }
781
782        None
783    }
784
785    fn size_hint(&self) -> (usize, Option<usize>) {
786        self.char_iterator.size_hint()
787    }
788
789    fn count(self) -> usize
790    where
791        Self: Sized,
792    {
793        self.char_iterator.count()
794    }
795}
796
797enum PendingCaseConversionResult {
798    Uppercase(ToUppercase),
799    Lowercase(ToLowercase),
800}
801
802impl PendingCaseConversionResult {
803    fn next(&mut self) -> Option<char> {
804        match self {
805            PendingCaseConversionResult::Uppercase(to_uppercase) => to_uppercase.next(),
806            PendingCaseConversionResult::Lowercase(to_lowercase) => to_lowercase.next(),
807        }
808    }
809}
810
811/// This is an iterator that consumes a char iterator and produces character transformed
812/// by the given CSS `text-transform` value. It currently does not support
813/// `text-transform: capitalize` because Unicode segmentation libraries do not support
814/// streaming input one character at a time.
815pub struct TextTransformation<InputIterator> {
816    /// The input character iterator.
817    char_iterator: InputIterator,
818    /// The `text-transform` value to use.
819    text_transform: TextTransformCase,
820    /// If an uppercasing or lowercasing produces more than one character, this
821    /// caches them so that they can be returned in subsequent iterator calls.
822    pending_case_conversion_result: Option<PendingCaseConversionResult>,
823}
824
825impl<InputIterator> TextTransformation<InputIterator> {
826    pub fn new(char_iterator: InputIterator, text_transform: TextTransformCase) -> Self {
827        Self {
828            char_iterator,
829            text_transform,
830            pending_case_conversion_result: None,
831        }
832    }
833}
834
835impl<InputIterator> Iterator for TextTransformation<InputIterator>
836where
837    InputIterator: Iterator<Item = char>,
838{
839    type Item = char;
840
841    fn next(&mut self) -> Option<Self::Item> {
842        if let Some(character) = self
843            .pending_case_conversion_result
844            .as_mut()
845            .and_then(|result| result.next())
846        {
847            return Some(character);
848        }
849        self.pending_case_conversion_result = None;
850
851        for character in self.char_iterator.by_ref() {
852            match self.text_transform {
853                TextTransformCase::None => return Some(character),
854                TextTransformCase::Uppercase => {
855                    let mut pending_result =
856                        PendingCaseConversionResult::Uppercase(character.to_uppercase());
857                    if let Some(character) = pending_result.next() {
858                        self.pending_case_conversion_result = Some(pending_result);
859                        return Some(character);
860                    }
861                },
862                TextTransformCase::Lowercase => {
863                    let mut pending_result =
864                        PendingCaseConversionResult::Lowercase(character.to_lowercase());
865                    if let Some(character) = pending_result.next() {
866                        self.pending_case_conversion_result = Some(pending_result);
867                        return Some(character);
868                    }
869                },
870                // `text-transform: capitalize` currently cannot work on a per-character basis,
871                // so must be handled outside of this iterator.
872                TextTransformCase::Capitalize => return Some(character),
873            }
874        }
875        None
876    }
877}
878
879pub struct TextSecurityTransform<InputIterator> {
880    /// The input character iterator.
881    char_iterator: InputIterator,
882    /// The `-webkit-text-security` value to use.
883    text_security: WebKitTextSecurity,
884}
885
886impl<InputIterator> TextSecurityTransform<InputIterator> {
887    pub fn new(char_iterator: InputIterator, text_security: WebKitTextSecurity) -> Self {
888        Self {
889            char_iterator,
890            text_security,
891        }
892    }
893}
894
895impl<InputIterator> Iterator for TextSecurityTransform<InputIterator>
896where
897    InputIterator: Iterator<Item = char>,
898{
899    type Item = char;
900
901    fn next(&mut self) -> Option<Self::Item> {
902        // The behavior of `-webkit-text-security` isn't specified, so we have some
903        // flexibility in the implementation. We just need to maintain a rough
904        // compatability with other browsers.
905        Some(match self.char_iterator.next()? {
906            // This is not ideal, but zero width space is used for some special reasons in
907            // `<input>` fields, so these remain untransformed, otherwise they would show up
908            // in empty text fields.
909            '\u{200B}' => '\u{200B}',
910            // Newlines are preserved, so that `<br>` keeps working as expected.
911            '\n' => '\n',
912            character => match self.text_security {
913                WebKitTextSecurity::None => character,
914                WebKitTextSecurity::Circle => '○',
915                WebKitTextSecurity::Disc => '●',
916                WebKitTextSecurity::Square => '■',
917            },
918        })
919    }
920}
921
922/// Given a string and whether the start of the string represents a word boundary, create a copy of
923/// the string with letters after word boundaries capitalized.
924pub(crate) fn capitalize_string(string: &str, allow_word_at_start: bool) -> String {
925    let mut output_string = String::new();
926    output_string.reserve(string.len());
927
928    let word_segmenter = WordSegmenter::new_auto();
929    let mut bounds = word_segmenter.segment_str(string).peekable();
930    let mut byte_index = 0;
931    for character in string.chars() {
932        let current_byte_index = byte_index;
933        byte_index += character.len_utf8();
934
935        if let Some(next_index) = bounds.peek() &&
936            *next_index == current_byte_index
937        {
938            bounds.next();
939
940            if current_byte_index != 0 || allow_word_at_start {
941                output_string.extend(character.to_uppercase());
942                continue;
943            }
944        }
945
946        output_string.push(character);
947    }
948
949    output_string
950}
951
952/// Computes the range of the first letter.
953///
954/// The range includes any preceding punctuation and white space, and any trailing punctuation. Any
955/// non-punctuation following the letter/number/symbol of first-letter ends the range. Intervening
956/// spaces within trailing punctuation are not supported yet.
957///
958/// If the resulting range is empty, no compatible first-letter text was found.
959///
960/// <https://drafts.csswg.org/css-pseudo/#first-letter-pattern>
961fn first_letter_range(text: &str) -> Range<usize> {
962    enum State {
963        /// All characters that precede the `PrecedingWhitespaceAndPunctuation` state.
964        Start,
965        /// All preceding punctuation and intervening whitepace that precedes the `Lns` state.
966        PrecedingPunctuation,
967        /// Unicode general category L: letter, N: number and S: symbol
968        Lns,
969        /// All punctuation (but no whitespace or other characters), that
970        /// come after the `Lns` state.
971        TrailingPunctuation,
972    }
973
974    let mut start = 0;
975    let mut state = State::Start;
976    for (index, character) in text.char_indices() {
977        match &mut state {
978            State::Start => {
979                if character.is_letter() || character.is_number() || character.is_symbol() {
980                    start = index;
981                    state = State::Lns;
982                } else if character.is_punctuation() {
983                    start = index;
984                    state = State::PrecedingPunctuation
985                }
986            },
987            State::PrecedingPunctuation => {
988                if character.is_letter() || character.is_number() || character.is_symbol() {
989                    state = State::Lns;
990                } else if !character.is_separator_space() && !character.is_punctuation() {
991                    return 0..0;
992                }
993            },
994            State::Lns => {
995                // TODO: Implement support for intervening spaces
996                // <https://drafts.csswg.org/css-pseudo/#first-letter-pattern>
997                if character.is_punctuation() &&
998                    !character.is_punctuation_open() &&
999                    !character.is_punctuation_dash()
1000                {
1001                    state = State::TrailingPunctuation;
1002                } else {
1003                    return start..index;
1004                }
1005            },
1006            State::TrailingPunctuation => {
1007                // TODO: Implement support for intervening spaces
1008                // <https://drafts.csswg.org/css-pseudo/#first-letter-pattern>
1009                if character.is_punctuation() &&
1010                    !character.is_punctuation_open() &&
1011                    !character.is_punctuation_dash()
1012                {
1013                    continue;
1014                } else {
1015                    return start..index;
1016                }
1017            },
1018        }
1019    }
1020
1021    match state {
1022        State::Start | State::PrecedingPunctuation => 0..0,
1023        State::Lns | State::TrailingPunctuation => start..text.len(),
1024    }
1025}
1026
1027#[cfg(test)]
1028mod tests {
1029    use super::*;
1030
1031    fn assert_first_letter_eq(text: &str, expected: &str) {
1032        let range = first_letter_range(text);
1033        assert_eq!(&text[range], expected);
1034    }
1035
1036    #[test]
1037    fn test_first_letter_range() {
1038        // All spaces
1039        assert_first_letter_eq("", "");
1040        assert_first_letter_eq("  ", "");
1041
1042        // Spaces and punctuation only
1043        assert_first_letter_eq("(", "");
1044        assert_first_letter_eq(" (", "");
1045        assert_first_letter_eq("( ", "");
1046        assert_first_letter_eq("()", "");
1047
1048        // Invalid chars
1049        assert_first_letter_eq("\u{0903}", "");
1050
1051        // First letter only
1052        assert_first_letter_eq("A", "A");
1053        assert_first_letter_eq(" A", "A");
1054        assert_first_letter_eq("A ", "A");
1055        assert_first_letter_eq(" A ", "A");
1056
1057        // Word
1058        assert_first_letter_eq("App", "A");
1059        assert_first_letter_eq(" App", "A");
1060        assert_first_letter_eq("App ", "A");
1061
1062        // Preceding punctuation(s), intervening spaces and first letter
1063        assert_first_letter_eq(r#""A"#, r#""A"#);
1064        assert_first_letter_eq(r#" "A"#, r#""A"#);
1065        assert_first_letter_eq(r#""A "#, r#""A"#);
1066        assert_first_letter_eq(r#"" A"#, r#"" A"#);
1067        assert_first_letter_eq(r#" "A "#, r#""A"#);
1068        assert_first_letter_eq(r#"("A"#, r#"("A"#);
1069        assert_first_letter_eq(r#" ("A"#, r#"("A"#);
1070        assert_first_letter_eq(r#"( "A"#, r#"( "A"#);
1071        assert_first_letter_eq(r#"[ ( "A"#, r#"[ ( "A"#);
1072
1073        // First letter and succeeding punctuation(s)
1074        // TODO: modify test cases when intervening spaces in succeeding puntuations is supported
1075        assert_first_letter_eq(r#"A""#, r#"A""#);
1076        assert_first_letter_eq(r#"A" "#, r#"A""#);
1077        assert_first_letter_eq(r#"A)]"#, r#"A)]"#);
1078        assert_first_letter_eq(r#"A" )]"#, r#"A""#);
1079        assert_first_letter_eq(r#"A)] >"#, r#"A)]"#);
1080
1081        // All
1082        assert_first_letter_eq(r#" ("A" )]"#, r#"("A""#);
1083        assert_first_letter_eq(r#" ("A")] >"#, r#"("A")]"#);
1084
1085        // Non ASCII chars
1086        assert_first_letter_eq("一", "一");
1087        assert_first_letter_eq(" 一 ", "一");
1088        assert_first_letter_eq("一二三", "一");
1089        assert_first_letter_eq(" 一二三 ", "一");
1090        assert_first_letter_eq("(一二三)", "(一");
1091        assert_first_letter_eq(" (一二三) ", "(一");
1092        assert_first_letter_eq("((一", "((一");
1093        assert_first_letter_eq(" ( (一", "( (一");
1094        assert_first_letter_eq("一)", "一)");
1095        assert_first_letter_eq("一))", "一))");
1096        assert_first_letter_eq("一) )", "一)");
1097    }
1098}