1use 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#[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
39pub(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 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 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 #[inline]
119 pub(crate) fn simple_lower(&self, c: char) -> char {
120 self.simple_helper(c, MappingKind::Lower)
121 }
122
123 #[inline]
125 pub(crate) fn simple_upper(&self, c: char) -> char {
126 self.simple_helper(c, MappingKind::Upper)
127 }
128
129 #[inline]
131 pub(crate) fn simple_title(&self, c: char) -> char {
132 self.simple_helper(c, MappingKind::Title)
133 }
134
135 #[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 char::from_u32(folded as u32).unwrap_or(c)
144 } else {
145 c
146 }
147 } else {
148 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 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 #[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 pub(crate) fn is_cased(&self, c: char) -> bool {
192 self.lookup_data(c).case_type().is_some()
193 }
194
195 #[inline(always)]
196 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 debug_assert!(kind != MappingKind::Title || IS_TITLE_CONTEXT);
209 debug_assert!(
211 !IS_TITLE_CONTEXT || kind == MappingKind::Title || kind == MappingKind::Lower
212 );
213
214 if IS_TITLE_CONTEXT && locale == CaseMapLocale::Dutch && kind == MappingKind::Lower {
218 if (c == 'j' || c == 'J') && context.is_dutch_ij_pair_at_beginning(self) {
222 return sink.write_char('J');
223 }
224 }
225
226 if !IS_TITLE_CONTEXT && locale == CaseMapLocale::Greek && kind == MappingKind::Upper {
232 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 match data {
244 Some(GreekPrecomposedLetterData::Vowel(vowel, mut precomposed_diacritics)) => {
245 let mut diacritics = context.add_greek_diacritics(precomposed_diacritics);
248 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 match vowel {
273 GreekVowel::Η => {
274 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 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 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 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 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 ('\u{49}', true) => '\u{131}', ('\u{130}', true) => '\u{69}', ('\u{49}', false) => '\u{69}', (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 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 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 return Some(FullMappingResult::CodePoint('i'));
443 } else if c == '\u{307}' && context.preceded_by_capital_i::<IS_TITLE_CONTEXT>(self) {
444 return Some(FullMappingResult::Remove);
453 } else if c == 'I' && !context.followed_by_dot_above(self) {
454 return Some(FullMappingResult::CodePoint('\u{131}'));
457 }
458 }
459
460 if c == '\u{130}' {
461 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 return Some(FullMappingResult::CodePoint('\u{3c2}'));
471 }
472
473 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 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 return Some(FullMappingResult::Remove);
494 }
495 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 ('\u{49}', true) => Some(FullMappingResult::CodePoint('\u{131}')),
517 ('\u{130}', true) => Some(FullMappingResult::CodePoint('\u{69}')),
518
519 ('\u{49}', false) => Some(FullMappingResult::CodePoint('\u{69}')),
521 ('\u{130}', false) => Some(FullMappingResult::String(Self::I_DOT)),
522 (_, _) => None,
523 }
524 }
525 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 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 pub(crate) fn add_case_closure_to<S: ClosureSink>(&self, c: char, set: &mut S) {
556 match c {
562 '\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 '\u{130}' => {
574 set.add_string(Self::I_DOT);
575 return;
576 }
577
578 '\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 let codepoint = c as i32 + delta;
593 let mapped = char::from_u32(codepoint as u32).unwrap_or(c);
595 set.add_char(mapped);
596 }
597 }
598 return;
599 }
600
601 let idx = data.exception_index();
603 let exception = self.exceptions.get(idx);
604
605 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 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 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#[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 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(); 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 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 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 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 match mapping.dot_type(c) {
854 DotType::NoDot | DotType::SoftDotted => break,
856 _ => return false,
858 }
859 }
860
861 !(j_has_acute ^ i_has_acute)
863 }
864}