Skip to main content

icu_casemap/
internals.rs

1// This file is part of ICU4X. For terms of use, please see the file
2// called LICENSE at the top level of the ICU4X source tree
3// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).
4
5//! This module contains most of the actual algorithms for case mapping.
6//!
7//! Primarily, it implements methods on `CaseMap`, which contains the data model.
8
9use crate::greek_to_me::{
10    self, GreekCombiningCharacterSequenceDiacritics, GreekDiacritics, GreekPrecomposedLetterData,
11    GreekVowel,
12};
13use crate::provider::data::{DotType, MappingKind};
14use crate::provider::exception_helpers::ExceptionSlot;
15use crate::provider::{CaseMap, CaseMapUnfold};
16use crate::set::ClosureSink;
17use crate::titlecase::TrailingCase;
18use core::fmt;
19use icu_locale_core::LanguageIdentifier;
20use writeable::Writeable;
21
22const ACUTE: char = '\u{301}';
23
24// Used to control the behavior of CaseMapper::fold.
25// Currently only used to decide whether to use Turkic (T) mappings for dotted/dotless i.
26#[derive(Copy, Clone, Default)]
27pub(crate) struct FoldOptions {
28    exclude_special_i: bool,
29}
30
31impl FoldOptions {
32    pub fn with_turkic_mappings() -> Self {
33        Self {
34            exclude_special_i: true,
35        }
36    }
37}
38
39/// Helper type that wraps a writeable in a prefix string
40pub(crate) struct StringAndWriteable<'a, W> {
41    pub string: &'a str,
42    pub writeable: W,
43}
44
45impl<Wr: Writeable> Writeable for StringAndWriteable<'_, Wr> {
46    fn write_to<W: fmt::Write + ?Sized>(&self, sink: &mut W) -> fmt::Result {
47        sink.write_str(self.string)?;
48        self.writeable.write_to(sink)
49    }
50    fn writeable_length_hint(&self) -> writeable::LengthHint {
51        writeable::LengthHint::exact(self.string.len()) + self.writeable.writeable_length_hint()
52    }
53}
54
55pub(crate) struct FullCaseWriteable<'a, 'data, const IS_TITLE_CONTEXT: bool> {
56    data: &'data CaseMap<'data>,
57    src: &'a str,
58    locale: CaseMapLocale,
59    mapping: MappingKind,
60    titlecase_tail_casing: TrailingCase,
61}
62
63impl<'a, const IS_TITLE_CONTEXT: bool> Writeable for FullCaseWriteable<'a, '_, IS_TITLE_CONTEXT> {
64    fn write_to<W: fmt::Write + ?Sized>(&self, sink: &mut W) -> fmt::Result {
65        let src = self.src;
66        let mut mapping = self.mapping;
67        let mut iter = src.char_indices();
68        for (i, c) in &mut iter {
69            let context = ContextIterator::new(&src[..i], &src[i..]);
70            self.data
71                .full_helper::<IS_TITLE_CONTEXT, W>(c, context, self.locale, mapping, sink)?;
72            if IS_TITLE_CONTEXT {
73                if self.titlecase_tail_casing == TrailingCase::Lower {
74                    mapping = MappingKind::Lower;
75                } else {
76                    break;
77                }
78            }
79        }
80        // Write the rest of the string
81        if IS_TITLE_CONTEXT && self.titlecase_tail_casing == TrailingCase::Unchanged {
82            sink.write_str(iter.as_str())?;
83        }
84        Ok(())
85    }
86    fn writeable_length_hint(&self) -> writeable::LengthHint {
87        writeable::LengthHint::at_least(self.src.len())
88    }
89    fn write_to_string(&self) -> alloc::borrow::Cow<'a, str> {
90        writeable::to_string_or_borrow(self, self.src.as_bytes())
91    }
92}
93
94impl<'data> CaseMap<'data> {
95    fn simple_helper(&self, c: char, kind: MappingKind) -> char {
96        let data = self.lookup_data(c);
97        if !data.has_exception() {
98            if data.is_relevant_to(kind) {
99                let folded = c as i32 + data.delta() as i32;
100                // GIGO: delta should be valid
101                char::from_u32(folded as u32).unwrap_or(c)
102            } else {
103                c
104            }
105        } else {
106            let idx = data.exception_index();
107            let exception = self.exceptions.get(idx);
108            if data.is_relevant_to(kind) {
109                if let Some(simple) = exception.get_simple_case_slot_for(c) {
110                    return simple;
111                }
112            }
113            exception.slot_char_for_kind(kind).unwrap_or(c)
114        }
115    }
116
117    // Returns the lowercase mapping of the given `char`.
118    #[inline]
119    pub(crate) fn simple_lower(&self, c: char) -> char {
120        self.simple_helper(c, MappingKind::Lower)
121    }
122
123    // Returns the uppercase mapping of the given `char`.
124    #[inline]
125    pub(crate) fn simple_upper(&self, c: char) -> char {
126        self.simple_helper(c, MappingKind::Upper)
127    }
128
129    // Returns the titlecase mapping of the given `char`.
130    #[inline]
131    pub(crate) fn simple_title(&self, c: char) -> char {
132        self.simple_helper(c, MappingKind::Title)
133    }
134
135    // Return the simple case folding mapping of the given char.
136    #[inline]
137    pub(crate) fn simple_fold(&self, c: char, options: FoldOptions) -> char {
138        let data = self.lookup_data(c);
139        if !data.has_exception() {
140            if data.is_upper_or_title() {
141                let folded = c as i32 + data.delta() as i32;
142                // GIGO: delta should be valid
143                char::from_u32(folded as u32).unwrap_or(c)
144            } else {
145                c
146            }
147        } else {
148            // TODO: if we move conditional fold and no_simple_case_folding into
149            // simple_helper, this function can just call simple_helper.
150            let idx = data.exception_index();
151            let exception = self.exceptions.get(idx);
152            if exception.bits.has_conditional_fold() {
153                self.simple_fold_special_case(c, options)
154            } else if exception.bits.no_simple_case_folding() {
155                c
156            } else if data.is_upper_or_title() && exception.has_slot(ExceptionSlot::Delta) {
157                // unwrap_or case should never happen but best to avoid panics
158                exception.get_simple_case_slot_for(c).unwrap_or('\0')
159            } else if let Some(slot_char) = exception.slot_char_for_kind(MappingKind::Fold) {
160                slot_char
161            } else {
162                c
163            }
164        }
165    }
166
167    fn dot_type(&self, c: char) -> DotType {
168        let data = self.lookup_data(c);
169        if !data.has_exception() {
170            data.dot_type()
171        } else {
172            let idx = data.exception_index();
173            self.exceptions.get(idx).bits.dot_type()
174        }
175    }
176
177    // Returns true if this code point is is case-sensitive.
178    // This is not currently exposed.
179    #[allow(dead_code)]
180    fn is_case_sensitive(&self, c: char) -> bool {
181        let data = self.lookup_data(c);
182        if !data.has_exception() {
183            data.is_sensitive()
184        } else {
185            let idx = data.exception_index();
186            self.exceptions.get(idx).bits.is_sensitive()
187        }
188    }
189
190    /// Returns whether the character is cased
191    pub(crate) fn is_cased(&self, c: char) -> bool {
192        self.lookup_data(c).case_type().is_some()
193    }
194
195    #[inline(always)]
196    // IS_TITLE_CONTEXT must be true if kind is MappingKind::Title
197    // The kind may be a different kind with IS_TITLE_CONTEXT still true because
198    // titlecasing a segment involves switching to lowercase later
199    fn full_helper<const IS_TITLE_CONTEXT: bool, W: fmt::Write + ?Sized>(
200        &self,
201        c: char,
202        context: ContextIterator,
203        locale: CaseMapLocale,
204        kind: MappingKind,
205        sink: &mut W,
206    ) -> fmt::Result {
207        // If using a title mapping IS_TITLE_CONTEXT must be true
208        debug_assert!(kind != MappingKind::Title || IS_TITLE_CONTEXT);
209        // In a title context, kind MUST be Title or Lower
210        debug_assert!(
211            !IS_TITLE_CONTEXT || kind == MappingKind::Title || kind == MappingKind::Lower
212        );
213
214        // ICU4C's non-standard extension for Dutch IJ titlecasing
215        // handled here instead of in full_lower_special_case because J does not have conditional
216        // special casemapping.
217        if IS_TITLE_CONTEXT && locale == CaseMapLocale::Dutch && kind == MappingKind::Lower {
218            // When titlecasing, a J found immediately after an I at the beginning of the segment
219            // should also uppercase. They are both allowed to have an acute accent but it must
220            // be present on both letters or neither. They may not have any other combining marks.
221            if (c == 'j' || c == 'J') && context.is_dutch_ij_pair_at_beginning(self) {
222                return sink.write_char('J');
223            }
224        }
225
226        // ICU4C's non-standard extension for Greek uppercasing:
227        // https://icu.unicode.org/design/case/greek-upper.
228        // Effectively removes Greek accents from Greek vowels during uppercasing,
229        // whilst attempting to preserve additional marks like the dialytika (diæresis)
230        // and ypogegrammeni (combining small iota).
231        if !IS_TITLE_CONTEXT && locale == CaseMapLocale::Greek && kind == MappingKind::Upper {
232            // Remove all combining diacritics on a Greek letter.
233            // Ypogegrammeni is not an accent mark and is handled by regular casemapping (it turns into
234            // a capital iota).
235            // The dialytika is removed here, but it may be added again when the base letter is being processed.
236            if greek_to_me::is_greek_diacritic_except_ypogegrammeni(c)
237                && context.preceded_by_greek_letter()
238            {
239                return Ok(());
240            }
241            let data = greek_to_me::get_data(c);
242            // Check if the character is a Greek vowel
243            match data {
244                Some(GreekPrecomposedLetterData::Vowel(vowel, mut precomposed_diacritics)) => {
245                    // Get the diacritics on the character itself, and add any further combining diacritics
246                    // from the context.
247                    let mut diacritics = context.add_greek_diacritics(precomposed_diacritics);
248                    // If the previous vowel had an accent (which would be removed) but no dialytika,
249                    // and this is an iota or upsilon, add a dialytika since it is necessary to disambiguate
250                    // the now-unaccented adjacent vowels from a digraph/diphthong.
251                    // Use a precomposed dialytika if the accent was precomposed, and a combining dialytika
252                    // if the accent was combining, so as to map NFD to NFD and NFC to NFC.
253                    if !diacritics.dialytika && (vowel == GreekVowel::Ι || vowel == GreekVowel::Υ)
254                    {
255                        if let Some(preceding_vowel) = context.preceding_greek_vowel_diacritics() {
256                            if !preceding_vowel.combining.dialytika
257                                && !preceding_vowel.precomposed.dialytika
258                            {
259                                if preceding_vowel.combining.accented {
260                                    diacritics.dialytika = true;
261                                } else {
262                                    precomposed_diacritics.dialytika =
263                                        preceding_vowel.precomposed.accented;
264                                }
265                            }
266                        }
267                    }
268                    // Write the base of the uppercased combining character sequence.
269                    // In most branches this is [`upper_base`], i.e., the uppercase letter with all accents removed.
270                    // In some branches the base has a precomposed diacritic.
271                    // In the case of the Greek disjunctive "or", a combining tonos may also be written.
272                    match vowel {
273                        GreekVowel::Η => {
274                            // The letter η (eta) is allowed to retain a tonos when it is form a single-letter word to distinguish
275                            // the feminine definite article ἡ (monotonic η) from the disjunctive "or" ἤ (monotonic ή).
276                            //
277                            // A lone η with an accent other than the oxia/tonos is not expected,
278                            // so there is no need to special-case the oxia/tonos.
279                            // The ancient ᾖ (exist.PRS.SUBJ.3s) has a iota subscript as well as the circumflex,
280                            // so it would not be given an oxia/tonos under this rule, and the subjunctive is formed with a particle
281                            // (e.g. να είναι) since Byzantine times anyway.
282                            if diacritics.accented
283                                && !context.followed_by_cased_letter(self)
284                                && !context.preceded_by_cased_letter(self)
285                                && !diacritics.ypogegrammeni
286                            {
287                                if precomposed_diacritics.accented {
288                                    sink.write_char('Ή')?;
289                                } else {
290                                    sink.write_char('Η')?;
291                                    sink.write_char(greek_to_me::TONOS)?;
292                                }
293                            } else {
294                                sink.write_char('Η')?;
295                            }
296                        }
297                        GreekVowel::Ι => sink.write_char(if precomposed_diacritics.dialytika {
298                            diacritics.dialytika = false;
299                            'Ϊ'
300                        } else {
301                            vowel.into()
302                        })?,
303                        GreekVowel::Υ => sink.write_char(if precomposed_diacritics.dialytika {
304                            diacritics.dialytika = false;
305                            'Ϋ'
306                        } else {
307                            vowel.into()
308                        })?,
309                        _ => sink.write_char(vowel.into())?,
310                    };
311                    if diacritics.dialytika {
312                        sink.write_char(greek_to_me::DIALYTIKA)?;
313                    }
314                    if precomposed_diacritics.ypogegrammeni {
315                        sink.write_char('Ι')?;
316                    }
317
318                    return Ok(());
319                }
320                // Rho might have breathing marks, we handle it specially
321                // to remove them
322                Some(GreekPrecomposedLetterData::Consonant(true)) => {
323                    sink.write_char(greek_to_me::CAPITAL_RHO)?;
324                    return Ok(());
325                }
326                _ => (),
327            }
328        }
329
330        let data = self.lookup_data(c);
331        if !data.has_exception() {
332            if data.is_relevant_to(kind) {
333                let mapped = c as i32 + data.delta() as i32;
334                // GIGO: delta should be valid
335                let mapped = char::from_u32(mapped as u32).unwrap_or(c);
336                sink.write_char(mapped)
337            } else {
338                sink.write_char(c)
339            }
340        } else {
341            let idx = data.exception_index();
342            let exception = self.exceptions.get(idx);
343            if exception.bits.has_conditional_special() {
344                if let Some(special) = match kind {
345                    MappingKind::Lower => {
346                        self.full_lower_special_case::<IS_TITLE_CONTEXT>(c, context, locale)
347                    }
348                    MappingKind::Fold => self.full_fold_special_case(c, context, locale),
349                    MappingKind::Upper | MappingKind::Title => self
350                        .full_upper_or_title_special_case::<IS_TITLE_CONTEXT>(c, context, locale),
351                } {
352                    return special.write_to(sink);
353                }
354            }
355            if let Some(mapped_string) = exception.get_fullmappings_slot_for_kind(kind) {
356                if !mapped_string.is_empty() {
357                    return sink.write_str(mapped_string);
358                }
359            }
360
361            if kind == MappingKind::Fold && exception.bits.no_simple_case_folding() {
362                return sink.write_char(c);
363            }
364
365            if data.is_relevant_to(kind) {
366                if let Some(simple) = exception.get_simple_case_slot_for(c) {
367                    return sink.write_char(simple);
368                }
369            }
370
371            if let Some(slot_char) = exception.slot_char_for_kind(kind) {
372                sink.write_char(slot_char)
373            } else {
374                sink.write_char(c)
375            }
376        }
377    }
378
379    // These constants are used for hardcoded locale-specific foldings.
380    const I_DOT: &'static str = "\u{69}\u{307}";
381    const J_DOT: &'static str = "\u{6a}\u{307}";
382    const I_OGONEK_DOT: &'static str = "\u{12f}\u{307}";
383    const I_DOT_GRAVE: &'static str = "\u{69}\u{307}\u{300}";
384    const I_DOT_ACUTE: &'static str = "\u{69}\u{307}\u{301}";
385    const I_DOT_TILDE: &'static str = "\u{69}\u{307}\u{303}";
386
387    // Special case folding mappings, hardcoded.
388    // This handles the special Turkic mappings for uppercase I and dotted uppercase I
389    // For non-Turkic languages, this mapping is normally not used.
390    // For Turkic languages (tr, az), this mapping can be used instead of the normal mapping for these characters.
391    fn simple_fold_special_case(&self, c: char, options: FoldOptions) -> char {
392        debug_assert!(c == '\u{49}' || c == '\u{130}');
393        let is_turkic = options.exclude_special_i;
394        match (c, is_turkic) {
395            // Turkic mappings
396            ('\u{49}', true) => '\u{131}', // 0049; T; 0131; # LATIN CAPITAL LETTER I
397            ('\u{130}', true) => '\u{69}', /* 0130; T; 0069; # LATIN CAPITAL LETTER I WITH DOT ABOVE */
398
399            // Default mappings
400            ('\u{49}', false) => '\u{69}', // 0049; C; 0069; # LATIN CAPITAL LETTER I
401
402            // There is no simple case folding for U+130.
403            (c, _) => c,
404        }
405    }
406
407    fn full_lower_special_case<const IS_TITLE_CONTEXT: bool>(
408        &self,
409        c: char,
410        context: ContextIterator,
411        locale: CaseMapLocale,
412    ) -> Option<FullMappingResult<'_>> {
413        if locale == CaseMapLocale::Lithuanian {
414            // Lithuanian retains the dot in a lowercase i when followed by accents.
415            // Introduce an explicit dot above when lowercasing capital I's and J's
416            // whenever there are more accents above (of the accents used in
417            // Lithuanian: grave, acute, and tilde above).
418
419            // Check for accents above I, J, and I-with-ogonek.
420            if c == 'I' && context.followed_by_more_above(self) {
421                return Some(FullMappingResult::String(Self::I_DOT));
422            } else if c == 'J' && context.followed_by_more_above(self) {
423                return Some(FullMappingResult::String(Self::J_DOT));
424            } else if c == '\u{12e}' && context.followed_by_more_above(self) {
425                return Some(FullMappingResult::String(Self::I_OGONEK_DOT));
426            }
427
428            // These characters are precomposed with accents above, so we don't
429            // have to look at the context.
430            if c == '\u{cc}' {
431                return Some(FullMappingResult::String(Self::I_DOT_GRAVE));
432            } else if c == '\u{cd}' {
433                return Some(FullMappingResult::String(Self::I_DOT_ACUTE));
434            } else if c == '\u{128}' {
435                return Some(FullMappingResult::String(Self::I_DOT_TILDE));
436            }
437        }
438
439        if locale == CaseMapLocale::Turkish {
440            if c == '\u{130}' {
441                // I and i-dotless; I-dot and i are case pairs in Turkish and Azeri
442                return Some(FullMappingResult::CodePoint('i'));
443            } else if c == '\u{307}' && context.preceded_by_capital_i::<IS_TITLE_CONTEXT>(self) {
444                // When lowercasing, remove dot_above in the sequence I + dot_above,
445                // which will turn into i. This matches the behaviour of the
446                // canonically equivalent I-dot_above.
447                //
448                // In a titlecase context, we do not want to apply this behavior to cases where the I
449                // was at the beginning of the string, as that I and its marks should be handled by the
450                // uppercasing rules (which ignore it, see below)
451
452                return Some(FullMappingResult::Remove);
453            } else if c == 'I' && !context.followed_by_dot_above(self) {
454                // When lowercasing, unless an I is before a dot_above, it turns
455                // into a dotless i.
456                return Some(FullMappingResult::CodePoint('\u{131}'));
457            }
458        }
459
460        if c == '\u{130}' {
461            // Preserve canonical equivalence for I with dot. Turkic is handled above.
462            return Some(FullMappingResult::String(Self::I_DOT));
463        }
464
465        if c == '\u{3a3}'
466            && context.preceded_by_cased_letter(self)
467            && !context.followed_by_cased_letter(self)
468        {
469            // Greek capital sigman maps depending on surrounding cased letters.
470            return Some(FullMappingResult::CodePoint('\u{3c2}'));
471        }
472
473        // No relevant special case mapping. Use a normal mapping.
474        None
475    }
476
477    fn full_upper_or_title_special_case<const IS_TITLE_CONTEXT: bool>(
478        &self,
479        c: char,
480        context: ContextIterator,
481        locale: CaseMapLocale,
482    ) -> Option<FullMappingResult<'_>> {
483        if locale == CaseMapLocale::Turkish && c == 'i' {
484            // In Turkic languages, i turns into a dotted capital I.
485            return Some(FullMappingResult::CodePoint('\u{130}'));
486        }
487        if locale == CaseMapLocale::Lithuanian
488            && c == '\u{307}'
489            && context.preceded_by_soft_dotted(self)
490        {
491            // Lithuanian retains the dot in a lowercase i when followed by accents.
492            // Remove dot_above after i with upper or titlecase.
493            return Some(FullMappingResult::Remove);
494        }
495        // ICU4C's non-standard extension for Armenian ligature ech-yiwn.
496        if c == '\u{587}' {
497            return match (locale, IS_TITLE_CONTEXT) {
498                (CaseMapLocale::Armenian, false) => Some(FullMappingResult::String("ԵՎ")),
499                (CaseMapLocale::Armenian, true) => Some(FullMappingResult::String("Եվ")),
500                (_, false) => Some(FullMappingResult::String("ԵՒ")),
501                (_, true) => Some(FullMappingResult::String("Եւ")),
502            };
503        }
504        None
505    }
506
507    fn full_fold_special_case(
508        &self,
509        c: char,
510        _context: ContextIterator,
511        locale: CaseMapLocale,
512    ) -> Option<FullMappingResult<'_>> {
513        let is_turkic = locale == CaseMapLocale::Turkish;
514        match (c, is_turkic) {
515            // Turkic mappings
516            ('\u{49}', true) => Some(FullMappingResult::CodePoint('\u{131}')),
517            ('\u{130}', true) => Some(FullMappingResult::CodePoint('\u{69}')),
518
519            // Default mappings
520            ('\u{49}', false) => Some(FullMappingResult::CodePoint('\u{69}')),
521            ('\u{130}', false) => Some(FullMappingResult::String(Self::I_DOT)),
522            (_, _) => None,
523        }
524    }
525    /// IS_TITLE_CONTEXT is true iff the mapping is MappingKind::Title, primarily exists
526    /// to avoid perf impacts on other more common modes of operation
527    ///
528    /// titlecase_tail_casing is only read in IS_TITLE_CONTEXT
529    pub(crate) fn full_helper_writeable<'a: 'data, const IS_TITLE_CONTEXT: bool>(
530        &'data self,
531        src: &'a str,
532        locale: CaseMapLocale,
533        mapping: MappingKind,
534        titlecase_tail_casing: TrailingCase,
535    ) -> FullCaseWriteable<'a, 'data, IS_TITLE_CONTEXT> {
536        // Ensure that they are either both true or both false
537        debug_assert!(IS_TITLE_CONTEXT == (mapping == MappingKind::Title));
538
539        FullCaseWriteable::<IS_TITLE_CONTEXT> {
540            data: self,
541            src,
542            locale,
543            mapping,
544            titlecase_tail_casing,
545        }
546    }
547
548    /// Adds all simple case mappings and the full case folding for `c` to `set`.
549    /// Also adds special case closure mappings.
550    /// The character itself is not added.
551    /// For example, the mappings
552    /// - for s include long s
553    /// - for sharp s include ss
554    /// - for k include the Kelvin sign
555    pub(crate) fn add_case_closure_to<S: ClosureSink>(&self, c: char, set: &mut S) {
556        // Hardcode the case closure of i and its relatives and ignore the
557        // data file data for these characters.
558        // The Turkic dotless i and dotted I with their case mapping conditions
559        // and case folding option make the related characters behave specially.
560        // This code matches their closure behavior to their case folding behavior.
561        match c {
562            // Regular i and I are in one equivalence class.
563            '\u{49}' => {
564                set.add_char('\u{69}');
565                return;
566            }
567            '\u{69}' => {
568                set.add_char('\u{49}');
569                return;
570            }
571
572            // Dotted I is in a class with <0069 0307> (for canonical equivalence with <0049 0307>)
573            '\u{130}' => {
574                set.add_string(Self::I_DOT);
575                return;
576            }
577
578            // Dotless i is in a class by itself
579            '\u{131}' => {
580                return;
581            }
582
583            _ => {}
584        }
585
586        let data = self.lookup_data(c);
587        if !data.has_exception() {
588            if data.case_type().is_some() {
589                let delta = data.delta() as i32;
590                if delta != 0 {
591                    // Add the one simple case mapping, no matter what type it is.
592                    let codepoint = c as i32 + delta;
593                    // GIGO: delta should be valid
594                    let mapped = char::from_u32(codepoint as u32).unwrap_or(c);
595                    set.add_char(mapped);
596                }
597            }
598            return;
599        }
600
601        // c has exceptions, so there may be multiple simple and/or full case mappings.
602        let idx = data.exception_index();
603        let exception = self.exceptions.get(idx);
604
605        // Add all simple case mappings.
606        for slot in [
607            ExceptionSlot::Lower,
608            ExceptionSlot::Fold,
609            ExceptionSlot::Upper,
610            ExceptionSlot::Title,
611        ] {
612            if let Some(simple) = exception.get_char_slot(slot) {
613                set.add_char(simple);
614            }
615        }
616        if let Some(simple) = exception.get_simple_case_slot_for(c) {
617            set.add_char(simple);
618        }
619
620        exception.add_full_and_closure_mappings(set);
621    }
622
623    /// Maps the string to single code points and adds the associated case closure
624    /// mappings.
625    ///
626    /// (see docs on CaseMapper::add_string_case_closure_to)
627    pub(crate) fn add_string_case_closure_to<S: ClosureSink>(
628        &self,
629        s: &str,
630        set: &mut S,
631        unfold_data: &CaseMapUnfold,
632    ) -> bool {
633        if s.chars().count() <= 1 {
634            // The string is too short to find any match.
635            return false;
636        }
637        match unfold_data.get(s) {
638            Some(closure_string) => {
639                for c in closure_string.chars() {
640                    set.add_char(c);
641                    self.add_case_closure_to(c, set);
642                }
643                true
644            }
645            None => false,
646        }
647    }
648}
649
650// An internal representation of locale. Non-Root values of this
651// enumeration imply that hard-coded special cases exist for this
652// language.
653#[derive(Copy, Clone, Eq, PartialEq, Debug)]
654pub enum CaseMapLocale {
655    Root,
656    Turkish,
657    Lithuanian,
658    Greek,
659    Dutch,
660    Armenian,
661}
662
663impl CaseMapLocale {
664    pub const fn from_langid(langid: &LanguageIdentifier) -> Self {
665        use icu_locale_core::subtags::{language, Language};
666        const TR: Language = language!("tr");
667        const AZ: Language = language!("az");
668        const LT: Language = language!("lt");
669        const EL: Language = language!("el");
670        const NL: Language = language!("nl");
671        const HY: Language = language!("hy");
672        match langid.language {
673            TR | AZ => Self::Turkish,
674            LT => Self::Lithuanian,
675            EL => Self::Greek,
676            NL => Self::Dutch,
677            HY => Self::Armenian,
678            _ => Self::Root,
679        }
680    }
681}
682
683pub enum FullMappingResult<'a> {
684    Remove,
685    CodePoint(char),
686    String(&'a str),
687}
688
689impl FullMappingResult<'_> {
690    #[allow(dead_code)]
691    fn add_to_set<S: ClosureSink>(&self, set: &mut S) {
692        match *self {
693            FullMappingResult::CodePoint(c) => set.add_char(c),
694            FullMappingResult::String(s) => set.add_string(s),
695            FullMappingResult::Remove => {}
696        }
697    }
698}
699
700impl Writeable for FullMappingResult<'_> {
701    fn write_to<W: fmt::Write + ?Sized>(&self, sink: &mut W) -> fmt::Result {
702        match *self {
703            FullMappingResult::CodePoint(c) => sink.write_char(c),
704            FullMappingResult::String(s) => sink.write_str(s),
705            FullMappingResult::Remove => Ok(()),
706        }
707    }
708}
709
710pub(crate) struct ContextIterator<'a> {
711    before: &'a str,
712    after: &'a str,
713}
714
715impl<'a> ContextIterator<'a> {
716    // Returns a context iterator with the characters before
717    // and after the character at a given index, given the preceding
718    // string and the succeeding string including the character itself
719    pub fn new(before: &'a str, char_and_after: &'a str) -> Self {
720        let mut char_and_after = char_and_after.chars();
721        char_and_after.next(); // skip the character itself
722        let after = char_and_after.as_str();
723        Self { before, after }
724    }
725
726    fn add_greek_diacritics(&self, mut diacritics: GreekDiacritics) -> GreekDiacritics {
727        diacritics.consume_greek_diacritics(self.after);
728        diacritics
729    }
730
731    fn preceded_by_greek_letter(&self) -> bool {
732        greek_to_me::preceded_by_greek_letter(self.before)
733    }
734
735    fn preceding_greek_vowel_diacritics(
736        &self,
737    ) -> Option<GreekCombiningCharacterSequenceDiacritics> {
738        greek_to_me::preceding_greek_vowel_diacritics(self.before)
739    }
740
741    fn preceded_by_soft_dotted(&self, mapping: &CaseMap) -> bool {
742        for c in self.before.chars().rev() {
743            match mapping.dot_type(c) {
744                DotType::SoftDotted => return true,
745                DotType::OtherAccent => continue,
746                _ => return false,
747            }
748        }
749        false
750    }
751    /// Checks if the preceding character is a capital I, allowing for non-Above combining characters in between.
752    ///
753    /// If I_MUST_NOT_START_STRING is true, additionally will require that the capital I does not start the string
754    fn preceded_by_capital_i<const I_MUST_NOT_START_STRING: bool>(
755        &self,
756        mapping: &CaseMap,
757    ) -> bool {
758        let mut iter = self.before.chars().rev();
759        while let Some(c) = iter.next() {
760            if c == 'I' {
761                if I_MUST_NOT_START_STRING {
762                    return iter.next().is_some();
763                } else {
764                    return true;
765                }
766            }
767            if mapping.dot_type(c) != DotType::OtherAccent {
768                break;
769            }
770        }
771        false
772    }
773    fn preceded_by_cased_letter(&self, mapping: &CaseMap) -> bool {
774        for c in self.before.chars().rev() {
775            let data = mapping.lookup_data(c);
776            if !data.is_ignorable() {
777                return data.case_type().is_some();
778            }
779        }
780        false
781    }
782    fn followed_by_cased_letter(&self, mapping: &CaseMap) -> bool {
783        for c in self.after.chars() {
784            let data = mapping.lookup_data(c);
785            if !data.is_ignorable() {
786                return data.case_type().is_some();
787            }
788        }
789        false
790    }
791    fn followed_by_more_above(&self, mapping: &CaseMap) -> bool {
792        for c in self.after.chars() {
793            match mapping.dot_type(c) {
794                DotType::Above => return true,
795                DotType::OtherAccent => continue,
796                _ => return false,
797            }
798        }
799        false
800    }
801    fn followed_by_dot_above(&self, mapping: &CaseMap) -> bool {
802        for c in self.after.chars() {
803            if c == '\u{307}' {
804                return true;
805            }
806            if mapping.dot_type(c) != DotType::OtherAccent {
807                return false;
808            }
809        }
810        false
811    }
812
813    /// Checks the preceding and surrounding context of a j or J
814    /// and returns true if it is preceded by an i or I at the start of the string.
815    /// If one has an acute accent,
816    /// both must have the accent for this to return true. No other accents are handled.
817    fn is_dutch_ij_pair_at_beginning(&self, mapping: &CaseMap) -> bool {
818        let mut before = self.before.chars().rev();
819        let mut i_has_acute = false;
820        loop {
821            match before.next() {
822                Some('i') | Some('I') => break,
823                Some('í') | Some('Í') => {
824                    i_has_acute = true;
825                    break;
826                }
827                Some(ACUTE) => i_has_acute = true,
828                _ => return false,
829            }
830        }
831
832        if before.next().is_some() {
833            // not at the beginning of a string, doesn't matter
834            return false;
835        }
836        let mut j_has_acute = false;
837        for c in self.after.chars() {
838            if c == ACUTE {
839                j_has_acute = true;
840                continue;
841            }
842            // We are supposed to check that `j` has no other combining marks aside
843            // from potentially an acute accent. Once we hit the first non-combining mark
844            // we are done.
845            //
846            // ICU4C checks for `gc=Mn` to determine if something is a combining mark,
847            // however this requires extra data (and is the *only* point in the casemapping algorithm
848            // where there is a direct dependency on properties data not mediated by the casemapping data trie).
849            //
850            // Instead, we can check for ccc via dot_type, the same way the rest of the algorithm does.
851            //
852            // See https://unicode-org.atlassian.net/browse/ICU-22429
853            match mapping.dot_type(c) {
854                // Not a combining character; ccc = 0
855                DotType::NoDot | DotType::SoftDotted => break,
856                // found combining character, bail
857                _ => return false,
858            }
859        }
860
861        // either both should have an acute accent, or none. this is an XNOR operation
862        !(j_has_acute ^ i_has_acute)
863    }
864}