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