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::char::{ToLowercase, ToUppercase};
7
8use icu_segmenter::WordSegmenter;
9use layout_api::wrapper_traits::{SharedSelection, ThreadSafeLayoutNode};
10use style::computed_values::white_space_collapse::T as WhiteSpaceCollapse;
11use style::values::specified::text::TextTransformCase;
12use unicode_bidi::Level;
13
14use super::text_run::TextRun;
15use super::{
16    InlineBox, InlineBoxIdentifier, InlineBoxes, InlineFormattingContext, InlineItem,
17    SharedInlineStyles,
18};
19use crate::cell::ArcRefCell;
20use crate::context::LayoutContext;
21use crate::dom::LayoutBox;
22use crate::dom_traversal::NodeAndStyleInfo;
23use crate::flow::float::FloatBox;
24use crate::flow::inline::AnonymousBlockBox;
25use crate::flow::{BlockContainer, BlockLevelBox, PseudoElement};
26use crate::formatting_contexts::IndependentFormattingContext;
27use crate::layout_box_base::LayoutBoxBase;
28use crate::positioned::AbsolutelyPositionedBox;
29use crate::style_ext::ComputedValuesExt;
30
31#[derive(Default)]
32pub(crate) struct InlineFormattingContextBuilder {
33    /// A stack of [`SharedInlineStyles`] including one for the root, one for each inline box on the
34    /// inline box stack, and importantly, one for every `display: contents` element that we are
35    /// currently processing. Normally `display: contents` elements don't affect the structure of
36    /// the [`InlineFormattingContext`], but the styles they provide do style their children.
37    pub shared_inline_styles_stack: Vec<SharedInlineStyles>,
38
39    /// The collection of text strings that make up this [`InlineFormattingContext`] under
40    /// construction.
41    pub text_segments: Vec<String>,
42
43    /// The current offset in the final text string of this [`InlineFormattingContext`],
44    /// used to properly set the text range of new [`InlineItem::TextRun`]s.
45    current_text_offset: usize,
46
47    /// The current character offset in the final text string of this [`InlineFormattingContext`],
48    /// used to properly set the text range of new [`InlineItem::TextRun`]s. Note that this is
49    /// different from the UTF-8 code point offset.
50    current_character_offset: usize,
51
52    /// If the [`InlineFormattingContext`] that we are building has a selection shared with its
53    /// originating node in the DOM, this will not be `None`.
54    pub shared_selection: Option<SharedSelection>,
55
56    /// Whether the last processed node ended with whitespace. This is used to
57    /// implement rule 4 of <https://www.w3.org/TR/css-text-3/#collapse>:
58    ///
59    /// > Any collapsible space immediately following another collapsible space—even one
60    /// > outside the boundary of the inline containing that space, provided both spaces are
61    /// > within the same inline formatting context—is collapsed to have zero advance width.
62    /// > (It is invisible, but retains its soft wrap opportunity, if any.)
63    last_inline_box_ended_with_collapsible_white_space: bool,
64
65    /// Whether or not the current state of the inline formatting context is on a word boundary
66    /// for the purposes of `text-transform: capitalize`.
67    on_word_boundary: bool,
68
69    /// Whether or not this inline formatting context will contain floats.
70    pub contains_floats: bool,
71
72    /// The current list of [`InlineItem`]s in this [`InlineFormattingContext`] under
73    /// construction. This is stored in a flat list to make it easy to access the last
74    /// item.
75    pub inline_items: Vec<InlineItem>,
76
77    /// The current [`InlineBox`] tree of this [`InlineFormattingContext`] under construction.
78    pub inline_boxes: InlineBoxes,
79
80    /// The ongoing stack of inline boxes stack of the builder.
81    ///
82    /// Contains all the currently ongoing inline boxes we entered so far.
83    /// The traversal is at all times as deep in the tree as this stack is,
84    /// which is why the code doesn't need to keep track of the actual
85    /// container root (see `handle_inline_level_element`).
86    ///
87    /// When an inline box ends, it's removed from this stack.
88    inline_box_stack: Vec<InlineBoxIdentifier>,
89
90    /// Whether this [`InlineFormattingContextBuilder`] is empty for the purposes of ignoring
91    /// during box tree construction. An IFC is empty if it only contains TextRuns with
92    /// completely collapsible whitespace. When that happens it can be ignored completely.
93    pub is_empty: bool,
94}
95
96impl InlineFormattingContextBuilder {
97    pub(crate) fn new(info: &NodeAndStyleInfo) -> Self {
98        Self {
99            // For the purposes of `text-transform: capitalize` the start of the IFC is a word boundary.
100            on_word_boundary: true,
101            is_empty: true,
102            shared_inline_styles_stack: vec![info.into()],
103            shared_selection: info.node.selection(),
104            ..Default::default()
105        }
106    }
107
108    pub(crate) fn currently_processing_inline_box(&self) -> bool {
109        !self.inline_box_stack.is_empty()
110    }
111
112    fn push_control_character_string(&mut self, string_to_push: &str) {
113        self.text_segments.push(string_to_push.to_owned());
114        self.current_text_offset += string_to_push.len();
115        self.current_character_offset += string_to_push.chars().count();
116    }
117
118    fn shared_inline_styles(&self) -> SharedInlineStyles {
119        self.shared_inline_styles_stack
120            .last()
121            .expect("Should always have at least one SharedInlineStyles")
122            .clone()
123    }
124
125    pub(crate) fn push_atomic(
126        &mut self,
127        independent_formatting_context_creator: impl FnOnce()
128            -> ArcRefCell<IndependentFormattingContext>,
129        old_layout_box: Option<LayoutBox>,
130    ) -> InlineItem {
131        // If there is an existing undamaged layout box that's compatible, use that.
132        let independent_formatting_context = old_layout_box
133            .and_then(|layout_box| match layout_box {
134                LayoutBox::InlineLevel(InlineItem::Atomic(atomic, ..)) => Some(atomic.clone()),
135                _ => None,
136            })
137            .unwrap_or_else(independent_formatting_context_creator);
138
139        let inline_level_box = InlineItem::Atomic(
140            independent_formatting_context,
141            self.current_text_offset,
142            Level::ltr(), /* This will be assigned later if necessary. */
143        );
144        self.inline_items.push(inline_level_box.clone());
145        self.is_empty = false;
146
147        // Push an object replacement character for this atomic, which will ensure that the line breaker
148        // inserts a line breaking opportunity here.
149        self.push_control_character_string("\u{fffc}");
150
151        self.last_inline_box_ended_with_collapsible_white_space = false;
152        self.on_word_boundary = true;
153
154        inline_level_box
155    }
156
157    pub(crate) fn push_absolutely_positioned_box(
158        &mut self,
159        absolutely_positioned_box_creator: impl FnOnce() -> ArcRefCell<AbsolutelyPositionedBox>,
160        old_layout_box: Option<LayoutBox>,
161    ) -> InlineItem {
162        let absolutely_positioned_box = old_layout_box
163            .and_then(|layout_box| match layout_box {
164                LayoutBox::InlineLevel(InlineItem::OutOfFlowAbsolutelyPositionedBox(
165                    positioned_box,
166                    ..,
167                )) => Some(positioned_box.clone()),
168                _ => None,
169            })
170            .unwrap_or_else(absolutely_positioned_box_creator);
171
172        // We cannot just reuse the old inline item, because the `current_text_offset` may have changed.
173        let inline_level_box = InlineItem::OutOfFlowAbsolutelyPositionedBox(
174            absolutely_positioned_box,
175            self.current_text_offset,
176        );
177
178        self.inline_items.push(inline_level_box.clone());
179        self.is_empty = false;
180        inline_level_box
181    }
182
183    pub(crate) fn push_float_box(
184        &mut self,
185        float_box_creator: impl FnOnce() -> ArcRefCell<FloatBox>,
186        old_layout_box: Option<LayoutBox>,
187    ) -> InlineItem {
188        let inline_level_box = old_layout_box
189            .and_then(|layout_box| match layout_box {
190                LayoutBox::InlineLevel(inline_item) => Some(inline_item),
191                _ => None,
192            })
193            .unwrap_or_else(|| InlineItem::OutOfFlowFloatBox(float_box_creator()));
194
195        debug_assert!(
196            matches!(inline_level_box, InlineItem::OutOfFlowFloatBox(..),),
197            "Created float box with incompatible `old_layout_box`"
198        );
199
200        self.inline_items.push(inline_level_box.clone());
201        self.is_empty = false;
202        self.contains_floats = true;
203        inline_level_box
204    }
205
206    pub(crate) fn push_block_level_box(
207        &mut self,
208        block_level_box: ArcRefCell<BlockLevelBox>,
209        block_builder_info: &NodeAndStyleInfo,
210        layout_context: &LayoutContext,
211    ) {
212        assert!(self.currently_processing_inline_box());
213        self.contains_floats = self.contains_floats || block_level_box.borrow().contains_floats();
214
215        if let Some(InlineItem::AnonymousBlock(anonymous_block)) = self.inline_items.last() {
216            if let BlockContainer::BlockLevelBoxes(ref mut block_level_boxes) =
217                anonymous_block.borrow_mut().contents
218            {
219                block_level_boxes.push(block_level_box);
220                return;
221            }
222        }
223        let info = &block_builder_info
224            .with_pseudo_element(layout_context, PseudoElement::ServoAnonymousBox)
225            .expect("Should never fail to create anonymous box");
226        self.inline_items
227            .push(InlineItem::AnonymousBlock(ArcRefCell::new(
228                AnonymousBlockBox {
229                    base: LayoutBoxBase::new(info.into(), info.style.clone()),
230                    contents: BlockContainer::BlockLevelBoxes(vec![block_level_box]),
231                },
232            )));
233    }
234
235    pub(crate) fn start_inline_box(
236        &mut self,
237        inline_box_creator: impl FnOnce() -> ArcRefCell<InlineBox>,
238        old_layout_box: Option<LayoutBox>,
239    ) {
240        // If there is an existing undamaged layout box that's compatible, use the `InlineBox` within it.
241        let inline_box = old_layout_box
242            .and_then(|layout_box| match layout_box {
243                LayoutBox::InlineLevel(InlineItem::StartInlineBox(inline_box)) => Some(inline_box),
244                _ => None,
245            })
246            .unwrap_or_else(inline_box_creator);
247
248        let borrowed_inline_box = inline_box.borrow();
249        self.push_control_character_string(borrowed_inline_box.base.style.bidi_control_chars().0);
250
251        self.shared_inline_styles_stack
252            .push(borrowed_inline_box.shared_inline_styles.clone());
253        std::mem::drop(borrowed_inline_box);
254
255        let identifier = self.inline_boxes.start_inline_box(inline_box.clone());
256        self.inline_items
257            .push(InlineItem::StartInlineBox(inline_box));
258        self.inline_box_stack.push(identifier);
259        self.is_empty = false;
260    }
261
262    /// End the ongoing inline box in this [`InlineFormattingContextBuilder`], returning
263    /// shared references to all of the box tree items that were created for it. More than
264    /// a single box tree items may be produced for a single inline box when that inline
265    /// box is split around a block-level element.
266    pub(crate) fn end_inline_box(&mut self) {
267        self.shared_inline_styles_stack.pop();
268        self.inline_items.push(InlineItem::EndInlineBox);
269        let identifier = self
270            .inline_box_stack
271            .pop()
272            .expect("Ended non-existent inline box");
273        self.inline_boxes.end_inline_box(identifier);
274        let inline_level_box = self.inline_boxes.get(&identifier);
275        let bidi_control_chars = inline_level_box.borrow().base.style.bidi_control_chars();
276        self.push_control_character_string(bidi_control_chars.1);
277    }
278
279    pub(crate) fn push_text<'dom>(&mut self, text: Cow<'dom, str>, info: &NodeAndStyleInfo<'dom>) {
280        let white_space_collapse = info.style.clone_white_space_collapse();
281        let collapsed = WhitespaceCollapse::new(
282            text.chars(),
283            white_space_collapse,
284            self.last_inline_box_ended_with_collapsible_white_space,
285        );
286
287        // TODO: Not all text transforms are about case, this logic should stop ignoring
288        // TextTransform::FULL_WIDTH and TextTransform::FULL_SIZE_KANA.
289        let text_transform = info.style.clone_text_transform().case();
290        let capitalized_text: String;
291        let char_iterator: Box<dyn Iterator<Item = char>> = match text_transform {
292            TextTransformCase::None => Box::new(collapsed),
293            TextTransformCase::Capitalize => {
294                // `TextTransformation` doesn't support capitalization, so we must capitalize the whole
295                // string at once and make a copy. Here `on_word_boundary` indicates whether or not the
296                // inline formatting context as a whole is on a word boundary. This is different from
297                // `last_inline_box_ended_with_collapsible_white_space` because the word boundaries are
298                // between atomic inlines and at the start of the IFC, and because preserved spaces
299                // are a word boundary.
300                let collapsed_string: String = collapsed.collect();
301                capitalized_text = capitalize_string(&collapsed_string, self.on_word_boundary);
302                Box::new(capitalized_text.chars())
303            },
304            _ => {
305                // If `text-transform` is active, wrap the `WhitespaceCollapse` iterator in
306                // a `TextTransformation` iterator.
307                Box::new(TextTransformation::new(collapsed, text_transform))
308            },
309        };
310
311        let white_space_collapse = info.style.clone_white_space_collapse();
312        let mut character_count = 0;
313        let new_text: String = char_iterator
314            .inspect(|&character| {
315                character_count += 1;
316
317                self.is_empty = self.is_empty &&
318                    match white_space_collapse {
319                        WhiteSpaceCollapse::Collapse => character.is_ascii_whitespace(),
320                        WhiteSpaceCollapse::PreserveBreaks => {
321                            character.is_ascii_whitespace() && character != '\n'
322                        },
323                        WhiteSpaceCollapse::Preserve | WhiteSpaceCollapse::BreakSpaces => false,
324                    };
325            })
326            .collect();
327
328        if new_text.is_empty() {
329            return;
330        }
331
332        if let Some(last_character) = new_text.chars().next_back() {
333            self.on_word_boundary = last_character.is_whitespace();
334            self.last_inline_box_ended_with_collapsible_white_space =
335                self.on_word_boundary && white_space_collapse != WhiteSpaceCollapse::Preserve;
336        }
337
338        let new_range = self.current_text_offset..self.current_text_offset + new_text.len();
339        self.current_text_offset = new_range.end;
340
341        let new_character_range =
342            self.current_character_offset..self.current_character_offset + character_count;
343        self.current_character_offset = new_character_range.end;
344
345        self.text_segments.push(new_text);
346
347        let current_inline_styles = self.shared_inline_styles();
348
349        if let Some(InlineItem::TextRun(text_run)) = self.inline_items.last() {
350            if text_run
351                .borrow()
352                .inline_styles
353                .ptr_eq(&current_inline_styles)
354            {
355                text_run.borrow_mut().text_range.end = new_range.end;
356                text_run.borrow_mut().character_range.end = new_character_range.end;
357                return;
358            }
359        }
360
361        self.inline_items
362            .push(InlineItem::TextRun(ArcRefCell::new(TextRun::new(
363                info.into(),
364                current_inline_styles,
365                new_range,
366                new_character_range,
367            ))));
368    }
369
370    pub(crate) fn enter_display_contents(&mut self, shared_inline_styles: SharedInlineStyles) {
371        self.shared_inline_styles_stack.push(shared_inline_styles);
372    }
373
374    pub(crate) fn leave_display_contents(&mut self) {
375        self.shared_inline_styles_stack.pop();
376    }
377
378    /// Finish the current inline formatting context, returning [`None`] if the context was empty.
379    pub(crate) fn finish(
380        self,
381        layout_context: &LayoutContext,
382        has_first_formatted_line: bool,
383        is_single_line_text_input: bool,
384        default_bidi_level: Level,
385    ) -> Option<InlineFormattingContext> {
386        if self.is_empty {
387            return None;
388        }
389
390        assert!(self.inline_box_stack.is_empty());
391        Some(InlineFormattingContext::new_with_builder(
392            self,
393            layout_context,
394            has_first_formatted_line,
395            is_single_line_text_input,
396            default_bidi_level,
397        ))
398    }
399}
400
401fn preserve_segment_break() -> bool {
402    true
403}
404
405pub struct WhitespaceCollapse<InputIterator> {
406    char_iterator: InputIterator,
407    white_space_collapse: WhiteSpaceCollapse,
408
409    /// Whether or not we should collapse white space completely at the start of the string.
410    /// This is true when the last character handled in our owning [`super::InlineFormattingContext`]
411    /// was collapsible white space.
412    remove_collapsible_white_space_at_start: bool,
413
414    /// Whether or not the last character produced was newline. There is special behavior
415    /// we do after each newline.
416    following_newline: bool,
417
418    /// Whether or not we have seen any non-white space characters, indicating that we are not
419    /// in a collapsible white space section at the beginning of the string.
420    have_seen_non_white_space_characters: bool,
421
422    /// Whether the last character that we processed was a non-newline white space character. When
423    /// collapsing white space we need to wait until the next non-white space character or the end
424    /// of the string to push a single white space.
425    inside_white_space: bool,
426
427    /// When we enter a collapsible white space region, we may need to wait to produce a single
428    /// white space character as soon as we encounter a non-white space character. When that
429    /// happens we queue up the non-white space character for the next iterator call.
430    character_pending_to_return: Option<char>,
431}
432
433impl<InputIterator> WhitespaceCollapse<InputIterator> {
434    pub fn new(
435        char_iterator: InputIterator,
436        white_space_collapse: WhiteSpaceCollapse,
437        trim_beginning_white_space: bool,
438    ) -> Self {
439        Self {
440            char_iterator,
441            white_space_collapse,
442            remove_collapsible_white_space_at_start: trim_beginning_white_space,
443            inside_white_space: false,
444            following_newline: false,
445            have_seen_non_white_space_characters: false,
446            character_pending_to_return: None,
447        }
448    }
449
450    fn is_leading_trimmed_white_space(&self) -> bool {
451        !self.have_seen_non_white_space_characters && self.remove_collapsible_white_space_at_start
452    }
453
454    /// Whether or not we need to produce a space character if the next character is not a newline
455    /// and not white space. This happens when we are exiting a section of white space and we
456    /// waited to produce a single space character for the entire section of white space (but
457    /// not following or preceding a newline).
458    fn need_to_produce_space_character_after_white_space(&self) -> bool {
459        self.inside_white_space && !self.following_newline && !self.is_leading_trimmed_white_space()
460    }
461}
462
463impl<InputIterator> Iterator for WhitespaceCollapse<InputIterator>
464where
465    InputIterator: Iterator<Item = char>,
466{
467    type Item = char;
468
469    fn next(&mut self) -> Option<Self::Item> {
470        // Point 4.1.1 first bullet:
471        // > If white-space is set to normal, nowrap, or pre-line, whitespace
472        // > characters are considered collapsible
473        // If whitespace is not considered collapsible, it is preserved entirely, which
474        // means that we can simply return the input string exactly.
475        if self.white_space_collapse == WhiteSpaceCollapse::Preserve ||
476            self.white_space_collapse == WhiteSpaceCollapse::BreakSpaces
477        {
478            // From <https://drafts.csswg.org/css-text-3/#white-space-processing>:
479            // > Carriage returns (U+000D) are treated identically to spaces (U+0020) in all respects.
480            //
481            // In the non-preserved case these are converted to space below.
482            return match self.char_iterator.next() {
483                Some('\r') => Some(' '),
484                next => next,
485            };
486        }
487
488        if let Some(character) = self.character_pending_to_return.take() {
489            self.inside_white_space = false;
490            self.have_seen_non_white_space_characters = true;
491            self.following_newline = false;
492            return Some(character);
493        }
494
495        while let Some(character) = self.char_iterator.next() {
496            // Don't push non-newline whitespace immediately. Instead wait to push it until we
497            // know that it isn't followed by a newline. See `push_pending_whitespace_if_needed`
498            // above.
499            if character.is_ascii_whitespace() && character != '\n' {
500                self.inside_white_space = true;
501                continue;
502            }
503
504            // Point 4.1.1:
505            // > 2. Collapsible segment breaks are transformed for rendering according to the
506            // >    segment break transformation rules.
507            if character == '\n' {
508                // From <https://drafts.csswg.org/css-text-3/#line-break-transform>
509                // (4.1.3 -- the segment break transformation rules):
510                //
511                // > When white-space is pre, pre-wrap, or pre-line, segment breaks are not
512                // > collapsible and are instead transformed into a preserved line feed"
513                if self.white_space_collapse != WhiteSpaceCollapse::Collapse {
514                    self.inside_white_space = false;
515                    self.following_newline = true;
516                    return Some(character);
517
518                // Point 4.1.3:
519                // > 1. First, any collapsible segment break immediately following another
520                // >    collapsible segment break is removed.
521                // > 2. Then any remaining segment break is either transformed into a space (U+0020)
522                // >    or removed depending on the context before and after the break.
523                } else if !self.following_newline &&
524                    preserve_segment_break() &&
525                    !self.is_leading_trimmed_white_space()
526                {
527                    self.inside_white_space = false;
528                    self.following_newline = true;
529                    return Some(' ');
530                } else {
531                    self.following_newline = true;
532                    continue;
533                }
534            }
535
536            // Point 4.1.1:
537            // > 2. Any sequence of collapsible spaces and tabs immediately preceding or
538            // >    following a segment break is removed.
539            // > 3. Every collapsible tab is converted to a collapsible space (U+0020).
540            // > 4. Any collapsible space immediately following another collapsible space—even
541            // >    one outside the boundary of the inline containing that space, provided both
542            // >    spaces are within the same inline formatting context—is collapsed to have zero
543            // >    advance width.
544            if self.need_to_produce_space_character_after_white_space() {
545                self.inside_white_space = false;
546                self.character_pending_to_return = Some(character);
547                return Some(' ');
548            }
549
550            self.inside_white_space = false;
551            self.have_seen_non_white_space_characters = true;
552            self.following_newline = false;
553            return Some(character);
554        }
555
556        if self.need_to_produce_space_character_after_white_space() {
557            self.inside_white_space = false;
558            return Some(' ');
559        }
560
561        None
562    }
563
564    fn size_hint(&self) -> (usize, Option<usize>) {
565        self.char_iterator.size_hint()
566    }
567
568    fn count(self) -> usize
569    where
570        Self: Sized,
571    {
572        self.char_iterator.count()
573    }
574}
575
576enum PendingCaseConversionResult {
577    Uppercase(ToUppercase),
578    Lowercase(ToLowercase),
579}
580
581impl PendingCaseConversionResult {
582    fn next(&mut self) -> Option<char> {
583        match self {
584            PendingCaseConversionResult::Uppercase(to_uppercase) => to_uppercase.next(),
585            PendingCaseConversionResult::Lowercase(to_lowercase) => to_lowercase.next(),
586        }
587    }
588}
589
590/// This is an iterator that consumes a char iterator and produces character transformed
591/// by the given CSS `text-transform` value. It currently does not support
592/// `text-transform: capitalize` because Unicode segmentation libraries do not support
593/// streaming input one character at a time.
594pub struct TextTransformation<InputIterator> {
595    /// The input character iterator.
596    char_iterator: InputIterator,
597    /// The `text-transform` value to use.
598    text_transform: TextTransformCase,
599    /// If an uppercasing or lowercasing produces more than one character, this
600    /// caches them so that they can be returned in subsequent iterator calls.
601    pending_case_conversion_result: Option<PendingCaseConversionResult>,
602}
603
604impl<InputIterator> TextTransformation<InputIterator> {
605    pub fn new(char_iterator: InputIterator, text_transform: TextTransformCase) -> Self {
606        Self {
607            char_iterator,
608            text_transform,
609            pending_case_conversion_result: None,
610        }
611    }
612}
613
614impl<InputIterator> Iterator for TextTransformation<InputIterator>
615where
616    InputIterator: Iterator<Item = char>,
617{
618    type Item = char;
619
620    fn next(&mut self) -> Option<Self::Item> {
621        if let Some(character) = self
622            .pending_case_conversion_result
623            .as_mut()
624            .and_then(|result| result.next())
625        {
626            return Some(character);
627        }
628        self.pending_case_conversion_result = None;
629
630        for character in self.char_iterator.by_ref() {
631            match self.text_transform {
632                TextTransformCase::None => return Some(character),
633                TextTransformCase::Uppercase => {
634                    let mut pending_result =
635                        PendingCaseConversionResult::Uppercase(character.to_uppercase());
636                    if let Some(character) = pending_result.next() {
637                        self.pending_case_conversion_result = Some(pending_result);
638                        return Some(character);
639                    }
640                },
641                TextTransformCase::Lowercase => {
642                    let mut pending_result =
643                        PendingCaseConversionResult::Lowercase(character.to_lowercase());
644                    if let Some(character) = pending_result.next() {
645                        self.pending_case_conversion_result = Some(pending_result);
646                        return Some(character);
647                    }
648                },
649                // `text-transform: capitalize` currently cannot work on a per-character basis,
650                // so must be handled outside of this iterator.
651                TextTransformCase::Capitalize => return Some(character),
652            }
653        }
654        None
655    }
656}
657
658/// Given a string and whether the start of the string represents a word boundary, create a copy of
659/// the string with letters after word boundaries capitalized.
660pub(crate) fn capitalize_string(string: &str, allow_word_at_start: bool) -> String {
661    let mut output_string = String::new();
662    output_string.reserve(string.len());
663
664    let word_segmenter = WordSegmenter::new_auto();
665    let mut bounds = word_segmenter.segment_str(string).peekable();
666    let mut byte_index = 0;
667    for character in string.chars() {
668        let current_byte_index = byte_index;
669        byte_index += character.len_utf8();
670
671        if let Some(next_index) = bounds.peek() {
672            if *next_index == current_byte_index {
673                bounds.next();
674
675                if current_byte_index != 0 || allow_word_at_start {
676                    output_string.extend(character.to_uppercase());
677                    continue;
678                }
679            }
680        }
681
682        output_string.push(character);
683    }
684
685    output_string
686}