Skip to main content

layout/flow/inline/
text_transform.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
5//! # Logic for text transform in inline formatting contexts
6//!
7//! Inline formatting contexts do a variety of text transformations on their text content
8//! including white space collapsing, application of the `text-transform` CSS property,
9//! and application of the `-webkit-text-security` property. This module contains code to
10//! handle this as well as code to map from offsets in the original DOM node to the final
11//! IFC text and vice-versa.
12
13use arrayvec::ArrayVec;
14use icu_segmenter::WordSegmenter;
15use malloc_size_of_derive::MallocSizeOf;
16use servo_base::text::Utf32CodeUnits;
17use style::computed_values::_webkit_text_security::T as WebKitTextSecurity;
18use style::computed_values::white_space_collapse::T as WhiteSpaceCollapse;
19use style::properties::ComputedValues;
20use style::values::specified::text::{TextTransform, TextTransformCase};
21
22use crate::flow::inline::construct::InlineFormattingContextBuilder;
23
24/// <https://github.com/rust-lang/rust/blob/1.97.1/library/core/src/char/mod.rs#L523>
25///
26/// This is the maximum amount of characters that can be produced from case mapping,
27/// and by consequence the maximum amount of characters that can be produced during
28/// inline formatting context text transformation.
29const MAX_CASE_MAPPING_LENGTH: usize = 3;
30
31/// A single iteration in a pipeline of character iterators, that handle things like
32/// whitespace collapse and `text-transform` processing for text in an
33/// [`InlineFormattingContext`]. Each iteration can consume multiple characters and
34/// produce zero or more characters (up to 3). Consumption of characters greater than the
35/// characters produced by [`CharacterTransformIteration`] indicate that those characters
36/// have been collapsed.
37#[derive(Clone)]
38pub struct CharacterTransformIteration {
39    /// The number of characters consumed during this iteration of character transformation.
40    consumed: Utf32CodeUnits,
41    /// The characters that were produced during this iteration.
42    characters: ArrayVec<char, MAX_CASE_MAPPING_LENGTH>,
43}
44
45impl CharacterTransformIteration {
46    fn case_mapped(iterator: impl ExactSizeIterator<Item = char>) -> Self {
47        debug_assert!(iterator.len() <= MAX_CASE_MAPPING_LENGTH);
48        Self {
49            consumed: Utf32CodeUnits(1),
50            characters: iterator.collect(),
51        }
52    }
53
54    fn one_to_one(character: char) -> Self {
55        Self {
56            consumed: Utf32CodeUnits(1),
57            characters: std::iter::once(character).collect(),
58        }
59    }
60
61    fn collapse(amount_collapsed: usize, character: Option<char>) -> Self {
62        Self {
63            consumed: Utf32CodeUnits(amount_collapsed),
64            characters: character.into_iter().collect(),
65        }
66    }
67
68    fn is_one_to_one(&self) -> bool {
69        self.characters.len() == 1 && self.consumed.0 == 1
70    }
71
72    pub fn characters(&self) -> &[char] {
73        &self.characters
74    }
75}
76
77pub struct WhitespaceCollapse<InputIterator> {
78    input_iterator: InputIterator,
79    white_space_collapse: WhiteSpaceCollapse,
80
81    /// Whether or not we are in the process of collapse leading white space. This is true
82    /// when the last character handled in our owning [`super::InlineFormattingContext`]
83    /// was collapsible white space and we have not seen any non-whitespace characters
84    /// during processing of this iterator's input.
85    trimming_leading_white_space: bool,
86
87    /// Whether or not the last character produced was newline. There is special behavior
88    /// we do after each newline.
89    following_newline: bool,
90
91    /// When whitespace collapses before a non-whitespace character, the iterator returns
92    /// the collapsed whitespace and in the next iteration the non-whitespace character
93    /// must be returned. This value caches it until the next iteration.
94    character_pending_to_return: Option<char>,
95}
96
97impl<InputIterator: Iterator<Item = char>> WhitespaceCollapse<InputIterator> {
98    pub fn new(
99        input_iterator: InputIterator,
100        white_space_collapse: WhiteSpaceCollapse,
101        should_trim_leading_white_space: bool,
102    ) -> Self {
103        Self {
104            input_iterator,
105            white_space_collapse,
106            following_newline: false,
107            trimming_leading_white_space: should_trim_leading_white_space,
108            character_pending_to_return: None,
109        }
110    }
111
112    /// In some cases, white space is replaced by a single character (when not
113    /// following a newline and when leading whitespace is not being trimmed). In all
114    /// other cases, the white space is simply removed. This method handles that.
115    fn iteration_for_collapsed_whitespace(
116        &self,
117        collapsed_whitespace: usize,
118    ) -> CharacterTransformIteration {
119        if !self.following_newline && !self.trimming_leading_white_space {
120            CharacterTransformIteration::collapse(collapsed_whitespace, Some(' '))
121        } else {
122            CharacterTransformIteration::collapse(collapsed_whitespace, None)
123        }
124    }
125
126    fn iteration_for_collected_white_space(
127        &self,
128        collected_whitespace: usize,
129    ) -> Option<CharacterTransformIteration> {
130        (collected_whitespace != 0)
131            .then(|| self.iteration_for_collapsed_whitespace(collected_whitespace))
132    }
133}
134
135impl<InputIterator: Iterator<Item = char>> Iterator for WhitespaceCollapse<InputIterator> {
136    type Item = CharacterTransformIteration;
137
138    fn next(&mut self) -> Option<Self::Item> {
139        // Point 4.1.1 first bullet:
140        // > If white-space is set to normal, nowrap, or pre-line, whitespace
141        // > characters are considered collapsible
142        // If whitespace is not considered collapsible, it is preserved entirely, which
143        // means that we can simply return the input string exactly.
144        if self.white_space_collapse == WhiteSpaceCollapse::Preserve ||
145            self.white_space_collapse == WhiteSpaceCollapse::BreakSpaces
146        {
147            // From <https://drafts.csswg.org/css-text-3/#white-space-processing>:
148            // > Carriage returns (U+000D) are treated identically to spaces (U+0020) in all respects.
149            //
150            // In the non-preserved case these are converted to space below.
151            return match self.input_iterator.next() {
152                Some('\r') => Some(CharacterTransformIteration::one_to_one(' ')),
153                next => next.map(CharacterTransformIteration::one_to_one),
154            };
155        }
156
157        if let Some(character) = self.character_pending_to_return.take() {
158            // Once we produce a non-whitespace character, we are no longer trimming leading whitespace.
159            self.trimming_leading_white_space = false;
160            self.following_newline = false;
161            return Some(CharacterTransformIteration::one_to_one(character));
162        }
163
164        // When we enter a collapsible white space region, we may need to wait to produce
165        // a single white space character as soon as we encounter a non-white space
166        // character. When that happens we queue up the non-white space character for the
167        // next iterator call.
168        let mut collected_whitespace = 0;
169
170        while let Some(character) = self.input_iterator.next() {
171            // Don't push non-newline whitespace immediately. Instead wait to push it until we
172            // know that it isn't followed by a newline. See `push_pending_whitespace_if_needed`
173            // above.
174            if InlineFormattingContextBuilder::is_document_white_space(character) &&
175                character != '\n'
176            {
177                collected_whitespace += 1;
178                continue;
179            }
180
181            // Point 4.1.1:
182            // > 2. Collapsible segment breaks are transformed for rendering according to the
183            // >    segment break transformation rules.
184            if character == '\n' {
185                // From <https://drafts.csswg.org/css-text-3/#line-break-transform>
186                // (4.1.3 -- the segment break transformation rules):
187                //
188                // > When white-space is pre, pre-wrap, or pre-line, segment breaks are not
189                // > collapsible and are instead transformed into a preserved line feed"
190                //
191                // > 1. First, any collapsible segment break immediately following another
192                // >    collapsible segment break is removed.
193                // > 2. Then any remaining segment break is either transformed into a space (U+0020)
194                // >    or removed depending on the context before and after the break.
195                let iteration = if self.white_space_collapse != WhiteSpaceCollapse::Collapse {
196                    CharacterTransformIteration::collapse(collected_whitespace + 1, Some('\n'))
197                } else {
198                    self.iteration_for_collapsed_whitespace(collected_whitespace + 1)
199                };
200
201                self.following_newline = true;
202                return Some(iteration);
203            }
204
205            // Non-whitespace character
206
207            // Point 4.1.1:
208            // > 2. Any sequence of collapsible spaces and tabs immediately preceding or
209            // >    following a segment break is removed.
210            // > 3. Every collapsible tab is converted to a collapsible space (U+0020).
211            // > 4. Any collapsible space immediately following another collapsible space—even
212            // >    one outside the boundary of the inline containing that space, provided both
213            // >    spaces are within the same inline formatting context—is collapsed to have zero
214            // >    advance width.
215            if let Some(iteration) = self.iteration_for_collected_white_space(collected_whitespace)
216            {
217                self.character_pending_to_return = Some(character);
218                return Some(iteration);
219            }
220
221            // Once we produce a non-whitespace character, we are no longer trimming leading whitespace.
222            self.trimming_leading_white_space = false;
223            self.following_newline = false;
224            return Some(CharacterTransformIteration::one_to_one(character));
225        }
226
227        self.iteration_for_collected_white_space(collected_whitespace)
228    }
229}
230
231pub(crate) struct TextTransformationIterator<'a> {
232    case_map_iterator: Box<dyn Iterator<Item = CharacterTransformIteration> + 'a>,
233    full_width: bool,
234    full_size_kana: bool,
235}
236
237impl<'a> TextTransformationIterator<'a> {
238    pub(crate) fn new(
239        mut text: &'a str,
240        style: &ComputedValues,
241        trim_leading_white_space: bool,
242        on_word_boundary: bool,
243    ) -> Self {
244        let text_security = style.clone__webkit_text_security();
245
246        // <https://drafts.csswg.org/css-text-4/#text-transform-property>
247        let text_transform = style.clone_text_transform();
248
249        if text_transform.intersects(TextTransform::MATH_AUTO) {
250            // `math-auto` only does anything “on text nodes containing a single character” per
251            // https://w3c.github.io/mathml-core/#math-auto-transform
252            //
253            // TODO: should this be single character after whitespace collapsing?
254            // TODO: does `::first-letter` mess with this check?
255            let mut char_iter = text.chars();
256            if let Some(first_char) = char_iter.next() &&
257                let None = char_iter.next() &&
258                let Some(&mapping) = super::mathml_italics::ITALICS_MAPPINGS.get(&first_char)
259            {
260                text = mapping
261            }
262        }
263
264        let chars = text
265            .chars()
266            .map(move |character| map_character_for_webkit_text_security(text_security, character));
267        let white_space_collapse = style.clone_white_space_collapse();
268        let iterator =
269            WhitespaceCollapse::new(chars, white_space_collapse, trim_leading_white_space);
270
271        // https://drafts.csswg.org/css-text-4/#text-transform-order
272        // > When multiple transformations need to be applied,
273        // > they are applied in the following order:
274        // >
275        // > * `word-space-transform`
276        // > * `capitalize`, `uppercase`, and `lowercase`
277        // > * `full-width`
278        // > * `full-size-kana`
279        // >
280        // > Word space transformation and text transformation happen after
281        // > § 4.3.1 Phase I: Collapsing and Transformation but before
282        // > § 4.3.2 Phase II: Trimming and Positioning. This means for instance that full-width
283        // > only transforms spaces (U+0020) to U+3000 IDEOGRAPHIC SPACE within
284        // > preserved white space.
285
286        let case_map_iterator = match text_transform.case() {
287            TextTransformCase::None => {
288                Box::new(iterator) as Box<dyn Iterator<Item = CharacterTransformIteration>>
289            },
290            TextTransformCase::Lowercase => {
291                Box::new(simple_case_transform_iterator(iterator, |character| {
292                    CharacterTransformIteration::case_mapped(character.to_lowercase())
293                }))
294            },
295            TextTransformCase::Uppercase => {
296                Box::new(simple_case_transform_iterator(iterator, |character| {
297                    CharacterTransformIteration::case_mapped(character.to_uppercase())
298                }))
299            },
300            TextTransformCase::Capitalize => Box::new(capitalization_iterator(
301                iterator,
302                text.len(),
303                on_word_boundary,
304            )),
305        };
306
307        Self {
308            case_map_iterator,
309            full_width: text_transform.intersects(TextTransform::FULL_WIDTH),
310            full_size_kana: text_transform.intersects(TextTransform::FULL_SIZE_KANA),
311        }
312    }
313}
314
315impl Iterator for TextTransformationIterator<'_> {
316    type Item = CharacterTransformIteration;
317
318    fn next(&mut self) -> Option<Self::Item> {
319        // https://drafts.csswg.org/css-text-4/#text-transform-order
320        // > When multiple transformations need to be applied,
321        // > they are applied in the following order:
322        // >
323        // > * `word-space-transform`
324        // > * `capitalize`, `uppercase`, and `lowercase`
325        // > * `full-width`
326        // > * `full-size-kana`
327        let mut iteration = self.case_map_iterator.next()?;
328        map_characters_with_phf(
329            self.full_width,
330            &mut iteration.characters,
331            &super::full_width::FULL_WIDTH_MAPPINGS,
332        );
333        map_characters_with_phf(
334            self.full_size_kana,
335            &mut iteration.characters,
336            &super::small_kana::SMALL_KANA_MAPPINGS,
337        );
338        Some(iteration)
339    }
340}
341
342fn simple_case_transform_iterator(
343    input_iterator: impl Iterator<Item = CharacterTransformIteration>,
344    mapping: impl Fn(char) -> CharacterTransformIteration,
345) -> impl Iterator<Item = CharacterTransformIteration> {
346    input_iterator.map(move |iteration| {
347        if iteration.is_one_to_one() {
348            mapping(iteration.characters[0])
349        } else {
350            iteration
351        }
352    })
353}
354
355/// Given an input iterator, a size hint for the number items in the iterator,
356/// and a boolean determining whether the start of the input represents a word
357/// boundary, return an iterator that capitalizes one-to-one mapped characters
358/// from the input iterator.
359pub(crate) fn capitalization_iterator(
360    input_iterator: impl Iterator<Item = CharacterTransformIteration>,
361    size_hint: usize,
362    allow_word_at_start: bool,
363) -> impl Iterator<Item = CharacterTransformIteration> {
364    let mut iterations: Vec<_> = input_iterator.collect();
365    let mut string = String::with_capacity(size_hint);
366    for iteration in &iterations {
367        string.extend(iteration.characters());
368    }
369
370    let word_segmenter = WordSegmenter::new_auto();
371    let mut bounds = word_segmenter.segment_str(&string).peekable();
372
373    let mut current_byte_index = 0;
374    for iteration in iterations.iter_mut() {
375        let bytes_to_advance: usize = iteration
376            .characters()
377            .iter()
378            .map(|character| character.len_utf8())
379            .sum();
380        if bytes_to_advance == 0 {
381            continue;
382        }
383
384        let at_word_start = bounds.peek() == Some(&current_byte_index);
385        if at_word_start {
386            bounds.next();
387        }
388
389        // TODO: currently we titlecase the first `char` of each word,
390        // instead it should be the first typographic letter unit:
391        // https://drafts.csswg.org/css-text-4/#typographic-letter-unit
392        // WPT /css/css-text/text-transform/text-transform-capitalize-026.html
393        if iteration.is_one_to_one() &&
394            at_word_start &&
395            (current_byte_index != 0 || allow_word_at_start)
396        {
397            // TODO: Replace this with a call to `character.to_titlecase()` when available:
398            // See: https://github.com/rust-lang/rust/issues/153892
399            // See: https://doc.rust-lang.org/stable/std/primitive.char.html#difference-from-uppercase
400            *iteration =
401                CharacterTransformIteration::case_mapped(iteration.characters[0].to_uppercase());
402        }
403
404        current_byte_index += bytes_to_advance;
405    }
406
407    iterations.into_iter()
408}
409
410/// Map a character according to the rules of the `-webkit-text-security` CSS property.
411///
412/// Note: The behavior of `-webkit-text-security` isn't specified, so we have some
413/// flexibility in the implementation. We just need to maintain a rough compatibility with
414/// other browsers.
415fn map_character_for_webkit_text_security(mode: WebKitTextSecurity, character: char) -> char {
416    if let WebKitTextSecurity::None = mode {
417        return character;
418    }
419
420    // TODO: When MSRV is 1.95+ use std::hint::cold_path().
421    match character {
422        // This is not ideal, but zero width space is used for some special reasons in
423        // `<input>` fields, so these remain untransformed, otherwise they would show up
424        // in empty text fields.
425        '\u{200B}' => '\u{200B}',
426        // Newlines are preserved, so that `<br>` keeps working as expected.
427        '\n' => '\n',
428        _ => match mode {
429            WebKitTextSecurity::None => character, // unreachable
430            WebKitTextSecurity::Circle => '○',
431            WebKitTextSecurity::Disc => '●',
432            WebKitTextSecurity::Square => '■',
433        },
434    }
435}
436
437fn map_characters_with_phf(enabled: bool, characters: &mut [char], map: &phf::Map<char, char>) {
438    if enabled {
439        // TODO: When MSRV is 1.95+ use std::hint::cold_path().
440
441        for character in characters {
442            if let Some(mapping) = map.get(character) {
443                *character = *mapping
444            }
445        }
446    }
447}
448
449#[derive(MallocSizeOf, Clone, Copy)]
450struct OffsetMapKnownPosition {
451    original_offset: Utf32CodeUnits,
452    final_offset: Utf32CodeUnits,
453}
454
455#[derive(Default, MallocSizeOf)]
456pub struct OffsetMap {
457    /// Not including `IMPLICIT_KNOWN_POSITION_AT_START`
458    known_positions: Vec<OffsetMapKnownPosition>,
459    /// `Default` initializes to `false`
460    last_range_maps_one_to_one: bool,
461}
462
463impl std::fmt::Debug for OffsetMap {
464    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
465        f.debug_struct("OffsetMap")
466            .field("total_original_size", &self.total_original_size())
467            .field("total_final_size", &self.total_final_size())
468            .finish()
469    }
470}
471
472static IMPLICIT_KNOWN_POSITION_AT_START: OffsetMapKnownPosition = OffsetMapKnownPosition {
473    original_offset: Utf32CodeUnits(0),
474    final_offset: Utf32CodeUnits(0),
475};
476
477impl OffsetMap {
478    fn last_known_position(&self) -> &OffsetMapKnownPosition {
479        self.known_positions
480            .last()
481            .unwrap_or(&IMPLICIT_KNOWN_POSITION_AT_START)
482    }
483
484    pub fn total_original_size(&self) -> Utf32CodeUnits {
485        self.last_known_position().original_offset
486    }
487
488    pub fn total_final_size(&self) -> Utf32CodeUnits {
489        self.last_known_position().final_offset
490    }
491
492    pub fn push_range(
493        &mut self,
494        additional_original_length: Utf32CodeUnits,
495        additional_final_length: Utf32CodeUnits,
496    ) {
497        let this_range_maps_one_to_one = additional_original_length == additional_final_length;
498        if this_range_maps_one_to_one &&
499            self.last_range_maps_one_to_one &&
500            let Some(last) = self.known_positions.last_mut()
501        {
502            last.original_offset += additional_original_length;
503            last.final_offset += additional_final_length;
504        } else {
505            let last = self.last_known_position();
506            self.known_positions.push(OffsetMapKnownPosition {
507                original_offset: last.original_offset + additional_original_length,
508                final_offset: last.final_offset + additional_final_length,
509            });
510        }
511        self.last_range_maps_one_to_one = this_range_maps_one_to_one;
512    }
513
514    pub(crate) fn push_iteration(&mut self, iteration: &CharacterTransformIteration) {
515        self.push_range(
516            iteration.consumed,
517            Utf32CodeUnits(iteration.characters.len()),
518        );
519    }
520
521    pub fn map(&self, target_original_offset: Utf32CodeUnits) -> Utf32CodeUnits {
522        self.map_common(
523            target_original_offset,
524            |position| position.original_offset,
525            |position| position.final_offset,
526        )
527    }
528
529    pub fn reverse_map(&self, target_final_offset: Utf32CodeUnits) -> Utf32CodeUnits {
530        self.map_common(
531            target_final_offset,
532            |position| position.final_offset,
533            |position| position.original_offset,
534        )
535    }
536
537    fn map_common(
538        &self,
539        target_offset: Utf32CodeUnits,
540        get_input_offset: impl Copy + Fn(&OffsetMapKnownPosition) -> Utf32CodeUnits,
541        get_output_offset: impl Fn(&OffsetMapKnownPosition) -> Utf32CodeUnits,
542    ) -> Utf32CodeUnits {
543        if target_offset.0 == 0 {
544            // Implict known position
545            return Utf32CodeUnits(0);
546        }
547        match self
548            .known_positions
549            .binary_search_by_key(&target_offset, get_input_offset)
550        {
551            Ok(index) => {
552                // Exact known position
553                get_output_offset(&self.known_positions[index])
554            },
555            Err(index) => {
556                // `index` is where inserting a new position would keep the `Vec` sorted
557                if let Some(position_after) = self.known_positions.get(index) {
558                    let position_before = if index > 0 {
559                        &self.known_positions[index - 1]
560                    } else {
561                        &IMPLICIT_KNOWN_POSITION_AT_START
562                    };
563                    debug_assert!(target_offset > get_input_offset(position_before));
564                    debug_assert!(target_offset < get_input_offset(position_after));
565                    let offset_within_range = target_offset - get_input_offset(position_before);
566                    let candidate = get_output_offset(position_before) + offset_within_range;
567                    // If the output range is shorter, to go beyond it
568                    let upper_bound = get_output_offset(position_after);
569                    upper_bound.min(candidate)
570                } else {
571                    // `target_offset` at or past the end of the text covered by this map
572                    get_output_offset(self.last_known_position())
573                }
574            },
575        }
576    }
577}
578
579#[test]
580fn test_offsetmap_basic_expansion() {
581    let original_string = "aßΰb";
582    let final_string = "ASS\u{3a5}\u{308}\u{301}B";
583    assert_eq!(original_string.to_uppercase(), final_string);
584
585    let mut offset_map = OffsetMap::default();
586    offset_map.push_iteration(&CharacterTransformIteration::case_mapped(
587        'a'.to_uppercase(),
588    ));
589    offset_map.push_iteration(&CharacterTransformIteration::case_mapped(
590        'ß'.to_uppercase(),
591    ));
592    offset_map.push_iteration(&CharacterTransformIteration::case_mapped(
593        'ΰ'.to_uppercase(),
594    ));
595    offset_map.push_iteration(&CharacterTransformIteration::case_mapped(
596        'b'.to_uppercase(),
597    ));
598
599    assert_eq!(offset_map.map(Utf32CodeUnits(0)).0, 0);
600    assert_eq!(offset_map.map(Utf32CodeUnits(1)).0, 1);
601    assert_eq!(offset_map.map(Utf32CodeUnits(2)).0, 3);
602    assert_eq!(offset_map.map(Utf32CodeUnits(3)).0, 6);
603    assert_eq!(offset_map.map(Utf32CodeUnits(4)).0, 7);
604
605    // Beyond the last index should always map to the index after the last character
606    // (for handling selections).
607    assert_eq!(offset_map.map(Utf32CodeUnits(5)).0, 7);
608    assert_eq!(offset_map.map(Utf32CodeUnits(100)).0, 7);
609
610    let map_substring = |offset: usize, length: usize| {
611        let start = offset_map
612            .map(Utf32CodeUnits(offset))
613            .to_utf8_code_units_in(final_string);
614        let end = offset_map
615            .map(Utf32CodeUnits(offset + length))
616            .to_utf8_code_units_in(final_string);
617        &final_string[start.0..end.0]
618    };
619    assert_eq!(map_substring(0, 1), "A");
620    assert_eq!(map_substring(0, 2), "ASS");
621    assert_eq!(map_substring(0, 3), "ASS\u{3a5}\u{308}\u{301}");
622    assert_eq!(map_substring(0, 4), "ASS\u{3a5}\u{308}\u{301}B");
623    assert_eq!(map_substring(1, 1), "SS");
624}
625
626#[test]
627fn test_offsetmap_basic_collapse() {
628    let _original_string = "  aaa  b \nc";
629    let final_string = "aaa b\nc";
630
631    let mut offset_map = OffsetMap::default();
632    offset_map.push_iteration(&CharacterTransformIteration::collapse(2, None));
633    offset_map.push_iteration(&CharacterTransformIteration::one_to_one('a'));
634    offset_map.push_iteration(&CharacterTransformIteration::one_to_one('a'));
635    offset_map.push_iteration(&CharacterTransformIteration::one_to_one('a'));
636    assert_eq!(
637        offset_map.known_positions.len(),
638        2,
639        "Consecutive one-to-one mappings are merged"
640    );
641
642    offset_map.push_iteration(&CharacterTransformIteration::collapse(2, Some(' ')));
643    offset_map.push_iteration(&CharacterTransformIteration::one_to_one('b'));
644    offset_map.push_iteration(&CharacterTransformIteration::collapse(2, Some('\n')));
645    offset_map.push_iteration(&CharacterTransformIteration::one_to_one('c'));
646
647    assert_eq!(offset_map.map(Utf32CodeUnits(0)).0, 0);
648    assert_eq!(offset_map.map(Utf32CodeUnits(1)).0, 0);
649    assert_eq!(offset_map.map(Utf32CodeUnits(2)).0, 0);
650    assert_eq!(offset_map.map(Utf32CodeUnits(3)).0, 1);
651    assert_eq!(offset_map.map(Utf32CodeUnits(4)).0, 2);
652    assert_eq!(offset_map.map(Utf32CodeUnits(5)).0, 3);
653    // Mapping from the middle of the collapsed sequence should map to after the replacement.
654    assert_eq!(offset_map.map(Utf32CodeUnits(6)).0, 4);
655    assert_eq!(offset_map.map(Utf32CodeUnits(7)).0, 4);
656    assert_eq!(offset_map.map(Utf32CodeUnits(8)).0, 5);
657    // Mapping from the middle of the collapsed sequence should map to after the replacement.
658    assert_eq!(offset_map.map(Utf32CodeUnits(9)).0, 6);
659    assert_eq!(offset_map.map(Utf32CodeUnits(10)).0, 6);
660    assert_eq!(offset_map.map(Utf32CodeUnits(11)).0, 7);
661
662    // Beyond the last index should always map to the index after the last character
663    // (for handling selections).
664    assert_eq!(offset_map.map(Utf32CodeUnits(12)).0, 7);
665    assert_eq!(offset_map.map(Utf32CodeUnits(100)).0, 7);
666
667    let map_substring = |offset: usize, length: usize| {
668        let start = offset_map.map(Utf32CodeUnits(offset)).0;
669        let end = offset_map.map(Utf32CodeUnits(offset + length)).0;
670        &final_string[start..end]
671    };
672    assert_eq!(map_substring(0, 1), "");
673    assert_eq!(map_substring(0, 3), "a");
674    assert_eq!(map_substring(0, 5), "aaa");
675    assert_eq!(map_substring(0, 6), "aaa ");
676    assert_eq!(map_substring(0, 7), "aaa ");
677    assert_eq!(map_substring(0, 8), "aaa b");
678    assert_eq!(map_substring(0, 11), "aaa b\nc");
679}