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