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