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 ).
45// https://github.com/unicode-org/icu4x/blob/main/documents/process/boilerplate.md#library-annotations
6#![cfg_attr(not(any(test, doc)), no_std)]
7#![cfg_attr(
8 not(test),
9 deny(
10 clippy::indexing_slicing,
11 clippy::unwrap_used,
12 clippy::expect_used,
13 clippy::panic,
14 )
15)]
16#![warn(missing_docs)]
1718//! Normalizing text into Unicode Normalization Forms.
19//!
20//! This module is published as its own crate ([`icu_normalizer`](https://docs.rs/icu_normalizer/latest/icu_normalizer/))
21//! and as part of the [`icu`](https://docs.rs/icu/latest/icu/) crate. See the latter for more details on the ICU4X project.
22//!
23//! # Functionality
24//!
25//! The top level of the crate provides normalization of input into the four normalization forms defined in [UAX #15: Unicode
26//! Normalization Forms](https://www.unicode.org/reports/tr15/): NFC, NFD, NFKC, and NFKD.
27//!
28//! Three kinds of contiguous inputs are supported: known-well-formed UTF-8 (`&str`), potentially-not-well-formed UTF-8,
29//! and potentially-not-well-formed UTF-16. Additionally, an iterator over `char` can be wrapped in a normalizing iterator.
30//!
31//! The `uts46` module provides the combination of mapping and normalization operations for [UTS #46: Unicode IDNA
32//! Compatibility Processing](https://www.unicode.org/reports/tr46/). This functionality is not meant to be used by
33//! applications directly. Instead, it is meant as a building block for a full implementation of UTS #46, such as the
34//! [`idna`](https://docs.rs/idna/latest/idna/) crate.
35//!
36//! The `properties` module provides the non-recursive canonical decomposition operation on a per `char` basis and
37//! the canonical compositon operation given two `char`s. It also provides access to the Canonical Combining Class
38//! property. These operations are primarily meant for [HarfBuzz](https://harfbuzz.github.io/), the types
39//! [`CanonicalComposition`](properties::CanonicalComposition), [`CanonicalDecomposition`](properties::CanonicalDecomposition),
40//! and [`CanonicalCombiningClassMap`](properties::CanonicalCombiningClassMap) implement the [`harfbuzz_traits`] if
41//! the `harfbuzz_traits` Cargo feature is enabled.
42//!
43//! Notably, this normalizer does _not_ provide the normalization “quick check” that can result in “maybe” in
44//! addition to “yes” and “no”. The normalization checks provided by this crate always give a definitive
45//! non-“maybe” answer.
46//!
47//! # Examples
48//!
49//! ```
50//! let nfc = icu_normalizer::ComposingNormalizerBorrowed::new_nfc();
51//! assert_eq!(nfc.normalize("a\u{0308}"), "ä");
52//! assert!(nfc.is_normalized("ä"));
53//!
54//! let nfd = icu_normalizer::DecomposingNormalizerBorrowed::new_nfd();
55//! assert_eq!(nfd.normalize("ä"), "a\u{0308}");
56//! assert!(!nfd.is_normalized("ä"));
57//! ```
5859extern crate alloc;
6061// TODO: The plan is to replace
62// `#[cfg(not(icu4x_unstable_fast_trie_only))]`
63// with
64// `#[cfg(feature = "serde")]`
65// and
66// `#[cfg(icu4x_unstable_fast_trie_only)]`
67// with
68// `#[cfg(not(feature = "serde"))]`
69//
70// Before doing so:
71// * The type of the UTS 46 trie needs to be
72// disentangled from the type of the NFD/NFKD tries.
73// This will involve a more generic iterator hidden
74// inside the public iterator types.
75// * datagen needs to emit fast-mode tries for the
76// NFD and NFKD tries.
77// * The markers and possibly the data struct type
78// for NFD and NFKD need to be revised per policy.
7980#[cfg(not(icu4x_unstable_fast_trie_only))]
81type Trie<'trie> = CodePointTrie<'trie, u32>;
8283#[cfg(icu4x_unstable_fast_trie_only)]
84type Trie<'trie> = FastCodePointTrie<'trie, u32>;
8586#[cfg(feature = "harfbuzz_traits")]
87mod harfbuzz;
88pub mod properties;
89pub mod provider;
90pub mod uts46;
9192use crate::provider::CanonicalCompositions;
93use crate::provider::DecompositionData;
94use crate::provider::NormalizerNfdDataV1;
95use crate::provider::NormalizerNfkdDataV1;
96use crate::provider::NormalizerUts46DataV1;
97use alloc::borrow::Cow;
98use alloc::string::String;
99use icu_collections::char16trie::Char16Trie;
100use icu_collections::char16trie::Char16TrieIterator;
101use icu_collections::char16trie::TrieResult;
102#[cfg(not(icu4x_unstable_fast_trie_only))]
103use icu_collections::codepointtrie::CodePointTrie;
104#[cfg(icu4x_unstable_fast_trie_only)]
105use icu_collections::codepointtrie::FastCodePointTrie;
106#[cfg(icu4x_unstable_fast_trie_only)]
107use icu_collections::codepointtrie::TypedCodePointTrie;
108#[cfg(feature = "icu_properties")]
109use icu_properties::props::CanonicalCombiningClass;
110use icu_provider::prelude::*;
111use provider::DecompositionTables;
112use provider::NormalizerNfcV1;
113use provider::NormalizerNfdTablesV1;
114use provider::NormalizerNfkdTablesV1;
115use smallvec::SmallVec;
116#[cfg(feature = "utf8_iter")]
117use utf8_iter::Utf8CharsEx;
118#[cfg(feature = "utf16_iter")]
119use utf16_iter::Utf16CharsEx;
120use zerovec::{ZeroSlice, zeroslice};
121122// The optimizations in the area where `likely` is used
123// are extremely brittle. `likely` is useful in the typed-trie
124// case on the UTF-16 fast path, but in order not to disturb
125// the untyped-trie case on the UTF-16 fast path, make the
126// annotations no-ops in the untyped-trie case.
127128// `cold_path` and `likely` come from
129// https://github.com/rust-lang/hashbrown/commit/64bd7db1d1b148594edfde112cdb6d6260e2cfc3 .
130// See https://github.com/rust-lang/hashbrown/commit/64bd7db1d1b148594edfde112cdb6d6260e2cfc3#commitcomment-164768806
131// for permission to relicense under Unicode-3.0.
132133#[cfg(all(icu4x_unstable_fast_trie_only, feature = "utf16_iter"))]
134#[inline(always)]
135#[cold]
136fn cold_path() {}
137138#[cfg(all(icu4x_unstable_fast_trie_only, feature = "utf16_iter"))]
139#[inline(always)]
140pub(crate) fn likely(b: bool) -> bool {
141if b {
142true
143} else {
144 cold_path();
145false
146}
147}
148149// End import from https://github.com/rust-lang/hashbrown/commit/64bd7db1d1b148594edfde112cdb6d6260e2cfc3 .
150151/// No-op for typed trie case.
152#[cfg(all(not(icu4x_unstable_fast_trie_only), feature = "utf16_iter"))]
153#[inline(always)]
154fn likely(b: bool) -> bool {
155 b
156}
157158// This type exists as a shim for `icu_properties` `CanonicalCombiningClass` when the crate is disabled
159// It should not be exposed to users.
160#[cfg(not(feature = "icu_properties"))]
161#[derive(#[automatically_derived]
impl ::core::marker::Copy for CanonicalCombiningClass { }Copy, #[automatically_derived]
impl ::core::clone::Clone for CanonicalCombiningClass {
#[inline]
fn clone(&self) -> CanonicalCombiningClass {
let _: ::core::clone::AssertParamIsClone<u8>;
*self
}
}Clone, #[automatically_derived]
impl ::core::cmp::Eq for CanonicalCombiningClass {
#[inline]
#[doc(hidden)]
#[coverage(off)]
fn assert_fields_are_eq(&self) {
let _: ::core::cmp::AssertParamIsEq<u8>;
}
}Eq, #[automatically_derived]
impl ::core::cmp::PartialEq for CanonicalCombiningClass {
#[inline]
fn eq(&self, other: &CanonicalCombiningClass) -> bool {
self.0 == other.0
}
}PartialEq, #[automatically_derived]
impl ::core::cmp::PartialOrd for CanonicalCombiningClass {
#[inline]
fn partial_cmp(&self, other: &CanonicalCombiningClass)
-> ::core::option::Option<::core::cmp::Ordering> {
::core::option::Option::Some(::core::cmp::Ord::cmp(self, other))
}
}PartialOrd, #[automatically_derived]
impl ::core::cmp::Ord for CanonicalCombiningClass {
#[inline]
fn cmp(&self, other: &CanonicalCombiningClass) -> ::core::cmp::Ordering {
::core::cmp::Ord::cmp(&self.0, &other.0)
}
}Ord)]
162struct CanonicalCombiningClass(u8);
163164#[cfg(not(feature = "icu_properties"))]
165#[allow(non_upper_case_globals)]
166impl CanonicalCombiningClass {
167// See https://www.unicode.org/reports/tr44/#Canonical_Combining_Class_Values
168const NotReordered: Self = Self(0);
169const Above: Self = Self(230);
170const KanaVoicing: Self = Self(8);
171}
172173/// Treatment of the ignorable marker (0xFFFFFFFF) in data.
174#[derive(#[automatically_derived]
impl ::core::fmt::Debug for IgnorableBehavior {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::write_str(f,
match self {
IgnorableBehavior::Unsupported => "Unsupported",
IgnorableBehavior::Ignored => "Ignored",
IgnorableBehavior::ReplacementCharacter =>
"ReplacementCharacter",
})
}
}Debug, #[automatically_derived]
impl ::core::cmp::PartialEq for IgnorableBehavior {
#[inline]
fn eq(&self, other: &IgnorableBehavior) -> bool {
let __self_discr = ::core::intrinsics::discriminant_value(self);
let __arg1_discr = ::core::intrinsics::discriminant_value(other);
__self_discr == __arg1_discr
}
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for IgnorableBehavior {
#[inline]
#[doc(hidden)]
#[coverage(off)]
fn assert_fields_are_eq(&self) {}
}Eq)]
175enum IgnorableBehavior {
176/// 0xFFFFFFFF in data is not supported.
177Unsupported,
178/// Ignorables are ignored.
179Ignored,
180/// Ignorables are treated as singleton decompositions
181 /// to the REPLACEMENT CHARACTER.
182ReplacementCharacter,
183}
184185/// Marker for UTS 46 ignorables.
186///
187/// See trie-value-format.md
188const IGNORABLE_MARKER: u32 = 0xFFFFFFFF;
189190/// Marker that the decomposition does not round trip via NFC.
191///
192/// See trie-value-format.md
193const NON_ROUND_TRIP_MARKER: u32 = 1 << 30;
194195/// Marker that the first character of the decomposition
196/// can combine backwards.
197///
198/// See trie-value-format.md
199const BACKWARD_COMBINING_MARKER: u32 = 1 << 31;
200201/// Mask for the bits have to be zero for this to be a BMP
202/// singleton decomposition, or value baked into the surrogate
203/// range.
204///
205/// See trie-value-format.md
206const HIGH_ZEROS_MASK: u32 = 0x3FFF0000;
207208/// Mask for the bits have to be zero for this to be a complex
209/// decomposition.
210///
211/// See trie-value-format.md
212const LOW_ZEROS_MASK: u32 = 0xFFE0;
213214/// Checks if a trie value carries a (non-zero) canonical
215/// combining class.
216///
217/// See trie-value-format.md
218#[inline]
219fn trie_value_has_ccc(trie_value: u32) -> bool {
220 (trie_value & 0x3FFFFE00) == 0xD800
221}
222223/// Checks if the trie signifies a special non-starter decomposition.
224///
225/// See trie-value-format.md
226fn trie_value_indicates_special_non_starter_decomposition(trie_value: u32) -> bool {
227 (trie_value & 0x3FFFFF00) == 0xD900
228}
229230/// Checks if a trie value signifies a character whose decomposition
231/// starts with a non-starter.
232///
233/// See trie-value-format.md
234fn decomposition_starts_with_non_starter(trie_value: u32) -> bool {
235trie_value_has_ccc(trie_value)
236}
237238/// Extracts a canonical combining class (possibly zero) from a trie value.
239///
240/// See trie-value-format.md
241fn ccc_from_trie_value(trie_value: u32) -> CanonicalCombiningClass {
242if trie_value_has_ccc(trie_value) {
243CanonicalCombiningClass(trie_valueas u8)
244 } else {
245CanonicalCombiningClass::NotReordered246 }
247}
248249/// The tail (everything after the first character) of the NFKD form U+FDFA
250/// as 16-bit units.
251static FDFA_NFKD: [u16; 17] = [
2520x644, 0x649, 0x20, 0x627, 0x644, 0x644, 0x647, 0x20, 0x639, 0x644, 0x64A, 0x647, 0x20, 0x648,
2530x633, 0x644, 0x645,
254];
255256/// Marker value for U+FDFA in NFKD. (Unified with Hangul syllable marker,
257/// but they differ by `NON_ROUND_TRIP_MARKER`.)
258///
259/// See trie-value-format.md
260const FDFA_MARKER: u16 = 1;
261262// These constants originate from page 143 of Unicode 14.0
263/// Syllable base
264const HANGUL_S_BASE: u32 = 0xAC00;
265/// Lead jamo base
266const HANGUL_L_BASE: u32 = 0x1100;
267/// Vowel jamo base
268const HANGUL_V_BASE: u32 = 0x1161;
269/// Trail jamo base (deliberately off by one to account for the absence of a trail)
270const HANGUL_T_BASE: u32 = 0x11A7;
271/// Lead jamo count
272const HANGUL_L_COUNT: u32 = 19;
273/// Vowel jamo count
274const HANGUL_V_COUNT: u32 = 21;
275/// Trail jamo count (deliberately off by one to account for the absence of a trail)
276const HANGUL_T_COUNT: u32 = 28;
277/// Vowel jamo count times trail jamo count
278const HANGUL_N_COUNT: u32 = 588;
279/// Syllable count
280const HANGUL_S_COUNT: u32 = 11172;
281282/// One past the conjoining jamo block
283const HANGUL_JAMO_LIMIT: u32 = 0x1200;
284285/// If `opt` is `Some`, unwrap it. If `None`, panic if debug assertions
286/// are enabled and return `default` if debug assertions are not enabled.
287///
288/// Use this only if the only reason why `opt` could be `None` is bogus
289/// data from the provider.
290#[inline(always)]
291fn unwrap_or_gigo<T>(opt: Option<T>, default: T) -> T {
292if let Some(val) = opt {
293val294 } else {
295// GIGO case
296if true {
if !false { ::core::panicking::panic("assertion failed: false") };
};debug_assert!(false);
297default298 }
299}
300301/// Convert a `u32` _obtained from data provider data_ to `char`.
302#[inline(always)]
303fn char_from_u32(u: u32) -> char {
304unwrap_or_gigo(char::from_u32(u), char::REPLACEMENT_CHARACTER)
305}
306307/// Convert a `u16` _obtained from data provider data_ to `char`.
308#[inline(always)]
309fn char_from_u16(u: u16) -> char {
310char_from_u32(u32::from(u))
311}
312313const EMPTY_U16: &ZeroSlice<u16> = ::zerovec::ZeroSlice::new_empty()zeroslice![];
314315const EMPTY_CHAR: &ZeroSlice<char> = ::zerovec::ZeroSlice::new_empty()zeroslice![];
316317#[inline(always)]
318fn in_inclusive_range(c: char, start: char, end: char) -> bool {
319u32::from(c).wrapping_sub(u32::from(start)) <= (u32::from(end) - u32::from(start))
320}
321322#[inline(always)]
323#[cfg(feature = "utf16_iter")]
324fn in_inclusive_range16(u: u16, start: u16, end: u16) -> bool {
325 u.wrapping_sub(start) <= (end - start)
326}
327328/// Performs canonical composition (including Hangul) on a pair of
329/// characters or returns `None` if these characters don't compose.
330/// Composition exclusions are taken into account.
331#[inline]
332fn compose(iter: Char16TrieIterator, starter: char, second: char) -> Option<char> {
333let v = u32::from(second).wrapping_sub(HANGUL_V_BASE);
334if v >= HANGUL_JAMO_LIMIT - HANGUL_V_BASE {
335return compose_non_hangul(iter, starter, second);
336 }
337if v < HANGUL_V_COUNT {
338let l = u32::from(starter).wrapping_sub(HANGUL_L_BASE);
339if l < HANGUL_L_COUNT {
340let lv = l * HANGUL_N_COUNT + v * HANGUL_T_COUNT;
341// Safe, because the inputs are known to be in range.
342return Some(unsafe { char::from_u32_unchecked(HANGUL_S_BASE + lv) });
343 }
344return None;
345 }
346if in_inclusive_range(second, '\u{11A8}', '\u{11C2}') {
347let lv = u32::from(starter).wrapping_sub(HANGUL_S_BASE);
348if lv < HANGUL_S_COUNT && lv % HANGUL_T_COUNT == 0 {
349let lvt = lv + (u32::from(second) - HANGUL_T_BASE);
350// Safe, because the inputs are known to be in range.
351return Some(unsafe { char::from_u32_unchecked(HANGUL_S_BASE + lvt) });
352 }
353 }
354None355}
356357/// Performs (non-Hangul) canonical composition on a pair of characters
358/// or returns `None` if these characters don't compose. Composition
359/// exclusions are taken into account.
360fn compose_non_hangul(mut iter: Char16TrieIterator, starter: char, second: char) -> Option<char> {
361// To make the trie smaller, the pairs are stored second character first.
362 // Given how this method is used in ways where it's known that `second`
363 // is or isn't a starter. We could potentially split the trie into two
364 // tries depending on whether `second` is a starter.
365match iter.next(second) {
366 TrieResult::NoMatch => None,
367 TrieResult::NoValue => match iter.next(starter) {
368 TrieResult::NoMatch => None,
369 TrieResult::FinalValue(i) => {
370if let Some(c) = char::from_u32(ias u32) {
371Some(c)
372 } else {
373// GIGO case
374if true {
if !false { ::core::panicking::panic("assertion failed: false") };
};debug_assert!(false);
375None376 }
377 }
378 TrieResult::NoValue | TrieResult::Intermediate(_) => {
379// GIGO case
380if true {
if !false { ::core::panicking::panic("assertion failed: false") };
};debug_assert!(false);
381None382 }
383 },
384 TrieResult::FinalValue(_) | TrieResult::Intermediate(_) => {
385// GIGO case
386if true {
if !false { ::core::panicking::panic("assertion failed: false") };
};debug_assert!(false);
387None388 }
389 }
390}
391392/// See trie-value-format.md
393#[inline(always)]
394fn starter_and_decomposes_to_self_impl(trie_val: u32) -> bool {
395// The REPLACEMENT CHARACTER has `NON_ROUND_TRIP_MARKER` set,
396 // and this function needs to ignore that.
397(trie_val & !(BACKWARD_COMBINING_MARKER | NON_ROUND_TRIP_MARKER)) == 0
398}
399400/// See trie-value-format.md
401#[inline(always)]
402fn potential_passthrough_and_cannot_combine_backwards_impl(trie_val: u32) -> bool {
403 (trie_val & (NON_ROUND_TRIP_MARKER | BACKWARD_COMBINING_MARKER)) == 0
404}
405406/// Struct for holding together a character and the value
407/// looked up for it from the NFD trie in a more explicit
408/// way than an anonymous pair.
409/// Also holds a flag about the supplementary-trie provenance.
410#[derive(#[automatically_derived]
impl ::core::fmt::Debug for CharacterAndTrieValue {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field2_finish(f,
"CharacterAndTrieValue", "character", &self.character, "trie_val",
&&self.trie_val)
}
}Debug, #[automatically_derived]
impl ::core::cmp::PartialEq for CharacterAndTrieValue {
#[inline]
fn eq(&self, other: &CharacterAndTrieValue) -> bool {
self.character == other.character && self.trie_val == other.trie_val
}
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for CharacterAndTrieValue {
#[inline]
#[doc(hidden)]
#[coverage(off)]
fn assert_fields_are_eq(&self) {
let _: ::core::cmp::AssertParamIsEq<char>;
let _: ::core::cmp::AssertParamIsEq<u32>;
}
}Eq)]
411struct CharacterAndTrieValue {
412 character: char,
413/// See trie-value-format.md
414trie_val: u32,
415}
416417impl CharacterAndTrieValue {
418#[inline(always)]
419pub fn new(c: char, trie_value: u32) -> Self {
420CharacterAndTrieValue {
421 character: c,
422 trie_val: trie_value,
423 }
424 }
425426#[inline(always)]
427pub fn starter_and_decomposes_to_self(&self) -> bool {
428starter_and_decomposes_to_self_impl(self.trie_val)
429 }
430431/// See trie-value-format.md
432#[inline(always)]
433 #[cfg(feature = "utf8_iter")]
434pub fn starter_and_decomposes_to_self_except_replacement(&self) -> bool {
435// This intentionally leaves `NON_ROUND_TRIP_MARKER` in the value
436 // to be compared with zero. U+FFFD has that flag set despite really
437 // being being round-tripping in order to make UTF-8 errors
438 // ineligible for passthrough.
439(self.trie_val & !BACKWARD_COMBINING_MARKER) == 0
440}
441442/// See trie-value-format.md
443#[inline(always)]
444pub fn can_combine_backwards(&self) -> bool {
445 (self.trie_val & BACKWARD_COMBINING_MARKER) != 0
446}
447/// See trie-value-format.md
448#[inline(always)]
449pub fn potential_passthrough(&self) -> bool {
450 (self.trie_val & NON_ROUND_TRIP_MARKER) == 0
451}
452/// See trie-value-format.md
453#[inline(always)]
454pub fn potential_passthrough_and_cannot_combine_backwards(&self) -> bool {
455potential_passthrough_and_cannot_combine_backwards_impl(self.trie_val)
456 }
457}
458459/// Pack a `char` and a `CanonicalCombiningClass` in
460/// 32 bits (the former in the lower 24 bits and the
461/// latter in the high 8 bits). The latter can be
462/// initialized to 0xFF upon creation, in which case
463/// it can be actually set later by calling
464/// `set_ccc_from_trie_if_not_already_set`. This is
465/// a micro optimization to avoid the Canonical
466/// Combining Class trie lookup when there is only
467/// one combining character in a sequence. This type
468/// is intentionally non-`Copy` to get compiler help
469/// in making sure that the class is set on the
470/// instance on which it is intended to be set
471/// and not on a temporary copy.
472///
473/// Note that 0xFF is won't be assigned to an actual
474/// canonical combining class per definition D104
475/// in The Unicode Standard.
476//
477// NOTE: The Pernosco debugger has special knowledge
478// of this struct. Please do not change the bit layout
479// or the crate-module-qualified name of this struct
480// without coordination.
481#[derive(#[automatically_derived]
impl ::core::fmt::Debug for CharacterAndClass {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_tuple_field1_finish(f,
"CharacterAndClass", &&self.0)
}
}Debug)]
482struct CharacterAndClass(u32);
483484impl CharacterAndClass {
485pub fn new(c: char, ccc: CanonicalCombiningClass) -> Self {
486CharacterAndClass(u32::from(c) | (u32::from(ccc.0) << 24))
487 }
488pub fn new_with_placeholder(c: char) -> Self {
489CharacterAndClass(u32::from(c) | ((0xFF) << 24))
490 }
491pub fn new_with_trie_value(c_tv: CharacterAndTrieValue) -> Self {
492Self::new(c_tv.character, ccc_from_trie_value(c_tv.trie_val))
493 }
494pub fn new_starter(c: char) -> Self {
495CharacterAndClass(u32::from(c))
496 }
497/// This method must exist for Pernosco to apply its special rendering.
498 /// Also, this must not be dead code!
499pub fn character(&self) -> char {
500// Safe, because the low 24 bits came from a `char`
501 // originally.
502unsafe { char::from_u32_unchecked(self.0 & 0xFFFFFF) }
503 }
504/// This method must exist for Pernosco to apply its special rendering.
505pub fn ccc(&self) -> CanonicalCombiningClass {
506CanonicalCombiningClass((self.0 >> 24) as u8)
507 }
508509pub fn character_and_ccc(&self) -> (char, CanonicalCombiningClass) {
510 (self.character(), self.ccc())
511 }
512pub fn set_ccc_from_trie_if_not_already_set(&mut self, trie: &Trie) {
513if self.0 >> 24 != 0xFF {
514return;
515 }
516let scalar = self.0 & 0xFFFFFF;
517self.0 = ((ccc_from_trie_value(trie.get32_u32(scalar)).0 as u32) << 24) | scalar;
518 }
519}
520521// This function exists as a borrow check helper.
522#[inline(always)]
523fn sort_slice_by_ccc(slice: &mut [CharacterAndClass], trie: &Trie) {
524// We don't look up the canonical combining class for starters
525 // of for single combining characters between starters. When
526 // there's more than one combining character between starters,
527 // we look up the canonical combining class for each character
528 // exactly once.
529if slice.len() < 2 {
530return;
531 }
532slice533 .iter_mut()
534 .for_each(|cc| cc.set_ccc_from_trie_if_not_already_set(trie));
535slice.sort_by_key(|cc| cc.ccc());
536}
537538/// An iterator adaptor that turns an `Iterator` over `char` into
539/// a lazily-decomposed `char` sequence.
540#[derive(#[automatically_derived]
impl<'data, I: ::core::fmt::Debug> ::core::fmt::Debug for
Decomposition<'data, I> where I: Iterator<Item = char> {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
let names: &'static _ =
&["delegate", "buffer", "buffer_pos", "pending", "trie",
"scalars16", "scalars24", "supplementary_scalars16",
"supplementary_scalars24",
"decomposition_passthrough_bound", "ignorable_behavior"];
let values: &[&dyn ::core::fmt::Debug] =
&[&self.delegate, &self.buffer, &self.buffer_pos, &self.pending,
&self.trie, &self.scalars16, &self.scalars24,
&self.supplementary_scalars16,
&self.supplementary_scalars24,
&self.decomposition_passthrough_bound,
&&self.ignorable_behavior];
::core::fmt::Formatter::debug_struct_fields_finish(f, "Decomposition",
names, values)
}
}Debug)]
541pub struct Decomposition<'data, I>
542where
543I: Iterator<Item = char>,
544{
545 delegate: I,
546 buffer: SmallVec<[CharacterAndClass; 17]>, // Enough to hold NFKD for U+FDFA
547/// The index of the next item to be read from `buffer`.
548 /// The purpose if this index is to avoid having to move
549 /// the rest upon every read.
550buffer_pos: usize,
551// At the start of `next()` if not `None`, this is a pending unnormalized
552 // starter. When `Decomposition` appears alone, this is never a non-starter.
553 // However, when `Decomposition` appears inside a `Composition`, this
554 // may become a non-starter before `decomposing_next()` is called.
555pending: Option<CharacterAndTrieValue>, // None at end of stream
556 // See trie-value-format.md
557trie: &'data Trie<'data>,
558 scalars16: &'data ZeroSlice<u16>,
559 scalars24: &'data ZeroSlice<char>,
560 supplementary_scalars16: &'data ZeroSlice<u16>,
561 supplementary_scalars24: &'data ZeroSlice<char>,
562/// The lowest character for which either of the following does
563 /// not hold:
564 /// 1. Decomposes to self.
565 /// 2. Decomposition starts with a non-starter
566decomposition_passthrough_bound: u32, // never above 0xC0
567ignorable_behavior: IgnorableBehavior, // Arguably should be a type parameter
568}
569570impl<'data, I> Decomposition<'data, I>
571where
572I: Iterator<Item = char>,
573{
574/// Constructs a decomposing iterator adapter from a delegate
575 /// iterator and references to the necessary data, without
576 /// supplementary data.
577 ///
578 /// Use `DecomposingNormalizer::normalize_iter()` instead unless
579 /// there's a good reason to use this constructor directly.
580 ///
581 /// Public but hidden in order to be able to use this from the
582 /// collator.
583#[doc(hidden)] // used in collator
584pub fn new(
585 delegate: I,
586 decompositions: &'data DecompositionData,
587 tables: &'data DecompositionTables,
588 ) -> Self {
589Self::new_with_supplements(
590delegate,
591decompositions,
592tables,
593None,
5940xC0,
595 IgnorableBehavior::Unsupported,
596 )
597 }
598599/// Constructs a decomposing iterator adapter from a delegate
600 /// iterator and references to the necessary data, including
601 /// supplementary data.
602 ///
603 /// Use `DecomposingNormalizer::normalize_iter()` instead unless
604 /// there's a good reason to use this constructor directly.
605fn new_with_supplements(
606 delegate: I,
607 decompositions: &'data DecompositionData,
608 tables: &'data DecompositionTables,
609 supplementary_tables: Option<&'data DecompositionTables>,
610 decomposition_passthrough_bound: u8,
611 ignorable_behavior: IgnorableBehavior,
612 ) -> Self {
613let mut ret = Decomposition::<I> {
614delegate,
615 buffer: SmallVec::new(), // Normalized
616buffer_pos: 0,
617// Initialize with a placeholder starter in case
618 // the real stream starts with a non-starter.
619pending: Some(CharacterAndTrieValue::new('\u{FFFF}', 0)),
620#[allow(clippy::useless_conversion, clippy::expect_used)] // Expectation always succeeds when untyped tries are in use
621trie: <&Trie>::try_from(&decompositions.trie).expect("Unexpected trie type in data"),
622 scalars16: &tables.scalars16,
623 scalars24: &tables.scalars24,
624 supplementary_scalars16: if let Some(supplementary) = supplementary_tables {
625&supplementary.scalars16
626 } else {
627EMPTY_U16628 },
629 supplementary_scalars24: if let Some(supplementary) = supplementary_tables {
630&supplementary.scalars24
631 } else {
632EMPTY_CHAR633 },
634 decomposition_passthrough_bound: u32::from(decomposition_passthrough_bound),
635ignorable_behavior,
636 };
637let _ = ret.next(); // Remove the U+FFFF placeholder
638ret639 }
640641fn push_decomposition16(
642&mut self,
643 offset: usize,
644 len: usize,
645 only_non_starters_in_trail: bool,
646 slice16: &ZeroSlice<u16>,
647 ) -> (char, usize) {
648let (starter, tail) = slice16649 .get_subslice(offset..offset + len)
650 .and_then(|slice| slice.split_first())
651 .map_or_else(
652 || {
653// GIGO case
654if true {
if !false { ::core::panicking::panic("assertion failed: false") };
};debug_assert!(false);
655 (char::REPLACEMENT_CHARACTER, EMPTY_U16)
656 },
657 |(first, trail)| (char_from_u16(first), trail),
658 );
659if only_non_starters_in_trail {
660// All the rest are combining
661self.buffer.extend(
662tail.iter()
663 .map(|u| CharacterAndClass::new_with_placeholder(char_from_u16(u))),
664 );
665 (starter, 0)
666 } else {
667let mut i = 0;
668let mut combining_start = 0;
669for u in tail.iter() {
670let ch = char_from_u16(u);
671let trie_value = self.trie.get(ch);
672self.buffer.push(CharacterAndClass::new_with_trie_value(
673 CharacterAndTrieValue::new(ch, trie_value),
674 ));
675 i += 1;
676// Half-width kana and iota subscript don't occur in the tails
677 // of these multicharacter decompositions.
678if !decomposition_starts_with_non_starter(trie_value) {
679 combining_start = i;
680 }
681 }
682 (starter, combining_start)
683 }
684 }
685686fn push_decomposition32(
687&mut self,
688 offset: usize,
689 len: usize,
690 only_non_starters_in_trail: bool,
691 slice32: &ZeroSlice<char>,
692 ) -> (char, usize) {
693let (starter, tail) = slice32694 .get_subslice(offset..offset + len)
695 .and_then(|slice| slice.split_first())
696 .unwrap_or_else(|| {
697// GIGO case
698if true {
if !false { ::core::panicking::panic("assertion failed: false") };
};debug_assert!(false);
699 (char::REPLACEMENT_CHARACTER, EMPTY_CHAR)
700 });
701if only_non_starters_in_trail {
702// All the rest are combining
703self.buffer
704 .extend(tail.iter().map(CharacterAndClass::new_with_placeholder));
705 (starter, 0)
706 } else {
707let mut i = 0;
708let mut combining_start = 0;
709for ch in tail.iter() {
710let trie_value = self.trie.get(ch);
711self.buffer.push(CharacterAndClass::new_with_trie_value(
712 CharacterAndTrieValue::new(ch, trie_value),
713 ));
714 i += 1;
715// Half-width kana and iota subscript don't occur in the tails
716 // of these multicharacter decompositions.
717if !decomposition_starts_with_non_starter(trie_value) {
718 combining_start = i;
719 }
720 }
721 (starter, combining_start)
722 }
723 }
724725#[inline(always)]
726fn attach_trie_value(&self, c: char) -> CharacterAndTrieValue {
727CharacterAndTrieValue::new(c, self.trie.get(c))
728 }
729730fn delegate_next_no_pending(&mut self) -> Option<CharacterAndTrieValue> {
731if true {
if !self.pending.is_none() {
::core::panicking::panic("assertion failed: self.pending.is_none()")
};
};debug_assert!(self.pending.is_none());
732loop {
733let c = self.delegate.next()?;
734735// TODO(#2384): Measure if this check is actually an optimization.
736if u32::from(c) < self.decomposition_passthrough_bound {
737return Some(CharacterAndTrieValue::new(c, 0));
738 }
739740let trie_val = self.trie.get(c);
741// TODO: Can we do something better about the cost of this branch in the
742 // non-UTS 46 case?
743if trie_val == IGNORABLE_MARKER {
744match self.ignorable_behavior {
745 IgnorableBehavior::Unsupported => {
746if true {
if !false { ::core::panicking::panic("assertion failed: false") };
};debug_assert!(false);
747 }
748 IgnorableBehavior::ReplacementCharacter => {
749return Some(CharacterAndTrieValue::new(
750c,
751u32::from(char::REPLACEMENT_CHARACTER) | NON_ROUND_TRIP_MARKER,
752 ));
753 }
754 IgnorableBehavior::Ignored => {
755// Else ignore this character by reading the next one from the delegate.
756continue;
757 }
758 }
759 }
760return Some(CharacterAndTrieValue::new(c, trie_val));
761 }
762 }
763764fn delegate_next(&mut self) -> Option<CharacterAndTrieValue> {
765if let Some(pending) = self.pending.take() {
766// Only happens as part of `Composition` and as part of
767 // the contiguous-buffer methods of `DecomposingNormalizer`.
768 // I.e. does not happen as part of standalone iterator
769 // usage of `Decomposition`.
770Some(pending)
771 } else {
772self.delegate_next_no_pending()
773 }
774 }
775776fn decomposing_next(&mut self, c_and_trie_val: CharacterAndTrieValue) -> char {
777let (starter, combining_start) = {
778let c = c_and_trie_val.character;
779// See trie-value-format.md
780let decomposition = c_and_trie_val.trie_val;
781// The REPLACEMENT CHARACTER has `NON_ROUND_TRIP_MARKER` set,
782 // and that flag needs to be ignored here.
783if (decomposition & !(BACKWARD_COMBINING_MARKER | NON_ROUND_TRIP_MARKER)) == 0 {
784// The character is its own decomposition
785(c, 0)
786 } else {
787let high_zeros = (decomposition & HIGH_ZEROS_MASK) == 0;
788let low_zeros = (decomposition & LOW_ZEROS_MASK) == 0;
789if !high_zeros && !low_zeros {
790// Decomposition into two BMP characters: starter and non-starter
791let starter = char_from_u32(decomposition & 0x7FFF);
792let combining = char_from_u32((decomposition >> 15) & 0x7FFF);
793self.buffer
794 .push(CharacterAndClass::new_with_placeholder(combining));
795 (starter, 0)
796 } else if high_zeros {
797// Do the check by looking at `c` instead of looking at a marker
798 // in `singleton` below, because if we looked at the trie value,
799 // we'd still have to check that `c` is in the Hangul syllable
800 // range in order for the subsequent interpretations as `char`
801 // to be safe.
802 // Alternatively, `FDFA_MARKER` and the Hangul marker could
803 // be unified. That would add a branch for Hangul and remove
804 // a branch from singleton decompositions. It seems more
805 // important to favor Hangul syllables than singleton
806 // decompositions.
807 // Note that it would be valid to hoist this Hangul check
808 // one or even two steps earlier in this check hierarchy.
809 // Right now, it's assumed the kind of decompositions into
810 // BMP starter and non-starter, which occur in many languages,
811 // should be checked before Hangul syllables, which are about
812 // one language specifically. Hopefully, we get some
813 // instruction-level parallelism out of the disjointness of
814 // operations on `c` and `decomposition`.
815let hangul_offset = u32::from(c).wrapping_sub(HANGUL_S_BASE); // SIndex in the spec
816if hangul_offset < HANGUL_S_COUNT {
817if true {
{
match (&decomposition, &1) {
(left_val, right_val) => {
if !(*left_val == *right_val) {
let kind = ::core::panicking::AssertKind::Eq;
::core::panicking::assert_failed(kind, &*left_val,
&*right_val, ::core::option::Option::None);
}
}
}
};
};debug_assert_eq!(decomposition, 1);
818// Hangul syllable
819 // The math here comes from page 144 of Unicode 14.0
820let l = hangul_offset / HANGUL_N_COUNT;
821let v = (hangul_offset % HANGUL_N_COUNT) / HANGUL_T_COUNT;
822let t = hangul_offset % HANGUL_T_COUNT;
823824// The unsafe blocks here are OK, because the values stay
825 // within the Hangul jamo block and, therefore, the scalar
826 // value range by construction.
827self.buffer.push(CharacterAndClass::new_starter(unsafe {
828char::from_u32_unchecked(HANGUL_V_BASE + v)
829 }));
830let first = unsafe { char::from_u32_unchecked(HANGUL_L_BASE + l) };
831if t != 0 {
832self.buffer.push(CharacterAndClass::new_starter(unsafe {
833char::from_u32_unchecked(HANGUL_T_BASE + t)
834 }));
835 (first, 2)
836 } else {
837 (first, 1)
838 }
839 } else {
840let singleton = decompositionas u16;
841if singleton != FDFA_MARKER {
842// Decomposition into one BMP character
843let starter = char_from_u16(singleton);
844 (starter, 0)
845 } else {
846// Special case for the NFKD form of U+FDFA.
847self.buffer.extend(FDFA_NFKD.map(|u| {
848// SAFETY: `FDFA_NFKD` is known not to contain
849 // surrogates.
850CharacterAndClass::new_starter(unsafe {
851char::from_u32_unchecked(u32::from(u))
852 })
853 }));
854 ('\u{0635}', 17)
855 }
856 }
857 } else {
858if true {
if !low_zeros { ::core::panicking::panic("assertion failed: low_zeros") };
};debug_assert!(low_zeros);
859// Only 12 of 14 bits used as of Unicode 16.
860let offset = (((decomposition & !(0b11 << 30)) >> 16) as usize) - 1;
861// Only 3 of 4 bits used as of Unicode 16.
862let len_bits = decomposition & 0b1111;
863let only_non_starters_in_trail = (decomposition & 0b10000) != 0;
864if offset < self.scalars16.len() {
865self.push_decomposition16(
866offset,
867 (len_bits + 2) as usize,
868only_non_starters_in_trail,
869self.scalars16,
870 )
871 } else if offset < self.scalars16.len() + self.scalars24.len() {
872self.push_decomposition32(
873offset - self.scalars16.len(),
874 (len_bits + 1) as usize,
875only_non_starters_in_trail,
876self.scalars24,
877 )
878 } else if offset879 < self.scalars16.len()
880 + self.scalars24.len()
881 + self.supplementary_scalars16.len()
882 {
883self.push_decomposition16(
884offset - (self.scalars16.len() + self.scalars24.len()),
885 (len_bits + 2) as usize,
886only_non_starters_in_trail,
887self.supplementary_scalars16,
888 )
889 } else {
890self.push_decomposition32(
891offset892 - (self.scalars16.len()
893 + self.scalars24.len()
894 + self.supplementary_scalars16.len()),
895 (len_bits + 1) as usize,
896only_non_starters_in_trail,
897self.supplementary_scalars24,
898 )
899 }
900 }
901 }
902 };
903// Either we're inside `Composition` or `self.pending.is_none()`.
904905self.gather_and_sort_combining(combining_start);
906starter907 }
908909fn gather_and_sort_combining(&mut self, combining_start: usize) {
910// Not a `for` loop to avoid holding a mutable reference to `self` across
911 // the loop body.
912while let Some(ch_and_trie_val) = self.delegate_next() {
913if !trie_value_has_ccc(ch_and_trie_val.trie_val) {
914self.pending = Some(ch_and_trie_val);
915break;
916 } else if !trie_value_indicates_special_non_starter_decomposition(
917 ch_and_trie_val.trie_val,
918 ) {
919self.buffer
920 .push(CharacterAndClass::new_with_trie_value(ch_and_trie_val));
921 } else {
922// The Tibetan special cases are starters that decompose into non-starters.
923let mapped = match ch_and_trie_val.character {
924'\u{0340}' => {
925// COMBINING GRAVE TONE MARK
926CharacterAndClass::new('\u{0300}', CanonicalCombiningClass::Above)
927 }
928'\u{0341}' => {
929// COMBINING ACUTE TONE MARK
930CharacterAndClass::new('\u{0301}', CanonicalCombiningClass::Above)
931 }
932'\u{0343}' => {
933// COMBINING GREEK KORONIS
934CharacterAndClass::new('\u{0313}', CanonicalCombiningClass::Above)
935 }
936'\u{0344}' => {
937// COMBINING GREEK DIALYTIKA TONOS
938self.buffer.push(CharacterAndClass::new(
939'\u{0308}',
940 CanonicalCombiningClass::Above,
941 ));
942 CharacterAndClass::new('\u{0301}', CanonicalCombiningClass::Above)
943 }
944'\u{0F73}' => {
945// TIBETAN VOWEL SIGN II
946self.buffer.push(CharacterAndClass::new(
947'\u{0F71}',
948 CanonicalCombiningClass(129),
949 ));
950 CharacterAndClass::new('\u{0F72}', CanonicalCombiningClass(130))
951 }
952'\u{0F75}' => {
953// TIBETAN VOWEL SIGN UU
954self.buffer.push(CharacterAndClass::new(
955'\u{0F71}',
956 CanonicalCombiningClass(129),
957 ));
958 CharacterAndClass::new('\u{0F74}', CanonicalCombiningClass(132))
959 }
960'\u{0F81}' => {
961// TIBETAN VOWEL SIGN REVERSED II
962self.buffer.push(CharacterAndClass::new(
963'\u{0F71}',
964 CanonicalCombiningClass(129),
965 ));
966 CharacterAndClass::new('\u{0F80}', CanonicalCombiningClass(130))
967 }
968'\u{FF9E}' => {
969// HALFWIDTH KATAKANA VOICED SOUND MARK
970CharacterAndClass::new('\u{3099}', CanonicalCombiningClass::KanaVoicing)
971 }
972'\u{FF9F}' => {
973// HALFWIDTH KATAKANA VOICED SOUND MARK
974CharacterAndClass::new('\u{309A}', CanonicalCombiningClass::KanaVoicing)
975 }
976_ => {
977// GIGO case
978if true {
if !false { ::core::panicking::panic("assertion failed: false") };
};debug_assert!(false);
979 CharacterAndClass::new_with_placeholder(char::REPLACEMENT_CHARACTER)
980 }
981 };
982self.buffer.push(mapped);
983 }
984 }
985// Slicing succeeds by construction; we've always ensured that `combining_start`
986 // is in permissible range.
987#[expect(clippy::indexing_slicing)]
988sort_slice_by_ccc(&mut self.buffer[combining_start..], self.trie);
989 }
990}
991992impl<I> Iteratorfor Decomposition<'_, I>
993where
994I: Iterator<Item = char>,
995{
996type Item = char;
997998fn next(&mut self) -> Option<char> {
999if let Some(ret) = self.buffer.get(self.buffer_pos).map(|c| c.character()) {
1000self.buffer_pos += 1;
1001if self.buffer_pos == self.buffer.len() {
1002self.buffer.clear();
1003self.buffer_pos = 0;
1004 }
1005return Some(ret);
1006 }
1007if true {
{
match (&self.buffer_pos, &0) {
(left_val, right_val) => {
if !(*left_val == *right_val) {
let kind = ::core::panicking::AssertKind::Eq;
::core::panicking::assert_failed(kind, &*left_val,
&*right_val, ::core::option::Option::None);
}
}
}
};
};debug_assert_eq!(self.buffer_pos, 0);
1008let c_and_trie_val = self.pending.take()?;
1009Some(self.decomposing_next(c_and_trie_val))
1010 }
1011}
10121013/// An iterator adaptor that turns an `Iterator` over `char` into
1014/// a lazily-decomposed and then canonically composed `char` sequence.
1015#[derive(#[automatically_derived]
impl<'data, I: ::core::fmt::Debug> ::core::fmt::Debug for
Composition<'data, I> where I: Iterator<Item = char> {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field4_finish(f, "Composition",
"decomposition", &self.decomposition, "canonical_compositions",
&self.canonical_compositions, "unprocessed_starter",
&self.unprocessed_starter, "composition_passthrough_bound",
&&self.composition_passthrough_bound)
}
}Debug)]
1016pub struct Composition<'data, I>
1017where
1018I: Iterator<Item = char>,
1019{
1020/// The decomposing part of the normalizer than operates before
1021 /// the canonical composition is performed on its output.
1022decomposition: Decomposition<'data, I>,
1023/// Non-Hangul canonical composition data.
1024canonical_compositions: Char16Trie<'data>,
1025/// To make `next()` yield in cases where there's a non-composing
1026 /// starter in the decomposition buffer, we put it here to let it
1027 /// wait for the next `next()` call (or a jump forward within the
1028 /// `next()` call).
1029unprocessed_starter: Option<char>,
1030/// The lowest character for which any one of the following does
1031 /// not hold:
1032 /// 1. Roundtrips via decomposition and recomposition.
1033 /// 2. Decomposition starts with a non-starter
1034 /// 3. Is not a backward-combining starter
1035composition_passthrough_bound: u32,
1036}
10371038impl<'data, I> Composition<'data, I>
1039where
1040I: Iterator<Item = char>,
1041{
1042fn new(
1043 decomposition: Decomposition<'data, I>,
1044 canonical_compositions: Char16Trie<'data>,
1045 composition_passthrough_bound: u16,
1046 ) -> Self {
1047Self {
1048decomposition,
1049canonical_compositions,
1050 unprocessed_starter: None,
1051 composition_passthrough_bound: u32::from(composition_passthrough_bound),
1052 }
1053 }
10541055/// Performs canonical composition (including Hangul) on a pair of
1056 /// characters or returns `None` if these characters don't compose.
1057 /// Composition exclusions are taken into account.
1058#[inline(always)]
1059pub fn compose(&self, starter: char, second: char) -> Option<char> {
1060compose(self.canonical_compositions.iter(), starter, second)
1061 }
10621063/// Performs (non-Hangul) canonical composition on a pair of characters
1064 /// or returns `None` if these characters don't compose. Composition
1065 /// exclusions are taken into account.
1066#[inline(always)]
1067fn compose_non_hangul(&self, starter: char, second: char) -> Option<char> {
1068compose_non_hangul(self.canonical_compositions.iter(), starter, second)
1069 }
1070}
10711072impl<I> Iteratorfor Composition<'_, I>
1073where
1074I: Iterator<Item = char>,
1075{
1076type Item = char;
10771078#[inline]
1079fn next(&mut self) -> Option<char> {
1080let mut undecomposed_starter = CharacterAndTrieValue::new('\u{0}', 0); // The compiler can't figure out that this gets overwritten before use.
1081if self.unprocessed_starter.is_none() {
1082// The loop is only broken out of as goto forward
1083#[expect(clippy::never_loop)]
1084loop {
1085if let Some((character, ccc)) = self1086 .decomposition
1087 .buffer
1088 .get(self.decomposition.buffer_pos)
1089 .map(|c| c.character_and_ccc())
1090 {
1091self.decomposition.buffer_pos += 1;
1092if self.decomposition.buffer_pos == self.decomposition.buffer.len() {
1093self.decomposition.buffer.clear();
1094self.decomposition.buffer_pos = 0;
1095 }
1096if ccc == CanonicalCombiningClass::NotReordered {
1097// Previous decomposition contains a starter. This must
1098 // now become the `unprocessed_starter` for it to have
1099 // a chance to compose with the upcoming characters.
1100 //
1101 // E.g. parenthesized Hangul in NFKC comes through here,
1102 // but suitable composition exclusion could exercise this
1103 // in NFC.
1104self.unprocessed_starter = Some(character);
1105break; // We already have a starter, so skip taking one from `pending`.
1106}
1107return Some(character);
1108 }
1109if true {
{
match (&self.decomposition.buffer_pos, &0) {
(left_val, right_val) => {
if !(*left_val == *right_val) {
let kind = ::core::panicking::AssertKind::Eq;
::core::panicking::assert_failed(kind, &*left_val,
&*right_val, ::core::option::Option::None);
}
}
}
};
};debug_assert_eq!(self.decomposition.buffer_pos, 0);
1110undecomposed_starter = self.decomposition.pending.take()?;
1111if u32::from(undecomposed_starter.character) < self.composition_passthrough_bound
1112 || undecomposed_starter.potential_passthrough()
1113 {
1114// TODO(#2385): In the NFC case (moot for NFKC and UTS46), if the upcoming
1115 // character is not below `decomposition_passthrough_bound` but is
1116 // below `composition_passthrough_bound`, we read from the trie
1117 // unnecessarily.
1118if let Some(upcoming) = self.decomposition.delegate_next_no_pending() {
1119let cannot_combine_backwards = u32::from(upcoming.character)
1120 < self.composition_passthrough_bound
1121 || !upcoming.can_combine_backwards();
1122self.decomposition.pending = Some(upcoming);
1123if cannot_combine_backwards {
1124// Fast-track succeeded!
1125return Some(undecomposed_starter.character);
1126 }
1127 } else {
1128// End of stream
1129return Some(undecomposed_starter.character);
1130 }
1131 }
1132break; // Not actually looping
1133}
1134 }
1135let mut starter = '\u{0}'; // The compiler can't figure out this gets overwritten before use.
11361137 // The point of having this boolean is to have only one call site to
1138 // `self.decomposition.decomposing_next`, which is hopefully beneficial for
1139 // code size under inlining.
1140let mut attempt_composition = false;
1141loop {
1142if let Some(unprocessed) = self.unprocessed_starter.take() {
1143if true {
{
match (&undecomposed_starter, &CharacterAndTrieValue::new('\u{0}', 0))
{
(left_val, right_val) => {
if !(*left_val == *right_val) {
let kind = ::core::panicking::AssertKind::Eq;
::core::panicking::assert_failed(kind, &*left_val,
&*right_val, ::core::option::Option::None);
}
}
}
};
};debug_assert_eq!(undecomposed_starter, CharacterAndTrieValue::new('\u{0}', 0));
1144if true {
{
match (&starter, &'\u{0}') {
(left_val, right_val) => {
if !(*left_val == *right_val) {
let kind = ::core::panicking::AssertKind::Eq;
::core::panicking::assert_failed(kind, &*left_val,
&*right_val, ::core::option::Option::None);
}
}
}
};
};debug_assert_eq!(starter, '\u{0}');
1145starter = unprocessed;
1146 } else {
1147if true {
{
match (&self.decomposition.buffer_pos, &0) {
(left_val, right_val) => {
if !(*left_val == *right_val) {
let kind = ::core::panicking::AssertKind::Eq;
::core::panicking::assert_failed(kind, &*left_val,
&*right_val, ::core::option::Option::None);
}
}
}
};
};debug_assert_eq!(self.decomposition.buffer_pos, 0);
1148let next_starter = self.decomposition.decomposing_next(undecomposed_starter);
1149if !attempt_composition {
1150starter = next_starter;
1151 } else if let Some(composed) = self.compose(starter, next_starter) {
1152starter = composed;
1153 } else {
1154// This is our yield point. We'll pick this up above in the
1155 // next call to `next()`.
1156self.unprocessed_starter = Some(next_starter);
1157return Some(starter);
1158 }
1159 }
1160// We first loop by index to avoid moving the contents of `buffer`, but
1161 // if there's a discontiguous match, we'll start modifying `buffer` instead.
1162loop {
1163let (character, ccc) = if let Some((character, ccc)) = self1164 .decomposition
1165 .buffer
1166 .get(self.decomposition.buffer_pos)
1167 .map(|c| c.character_and_ccc())
1168 {
1169 (character, ccc)
1170 } else {
1171self.decomposition.buffer.clear();
1172self.decomposition.buffer_pos = 0;
1173break;
1174 };
1175if let Some(composed) = self.compose(starter, character) {
1176starter = composed;
1177self.decomposition.buffer_pos += 1;
1178continue;
1179 }
1180let mut most_recent_skipped_ccc = ccc;
1181 {
1182let _ = self1183 .decomposition
1184 .buffer
1185 .drain(0..self.decomposition.buffer_pos);
1186 }
1187self.decomposition.buffer_pos = 0;
1188if most_recent_skipped_ccc == CanonicalCombiningClass::NotReordered {
1189// We failed to compose a starter. Discontiguous match not allowed.
1190 // We leave the starter in `buffer` for `next()` to find.
1191return Some(starter);
1192 }
1193let mut i = 1; // We have skipped one non-starter.
1194while let Some((character, ccc)) = self
1195.decomposition
1196 .buffer
1197 .get(i)
1198 .map(|c| c.character_and_ccc())
1199 {
1200if ccc == CanonicalCombiningClass::NotReordered {
1201// Discontiguous match not allowed.
1202return Some(starter);
1203 }
1204if true {
if !(ccc >= most_recent_skipped_ccc) {
::core::panicking::panic("assertion failed: ccc >= most_recent_skipped_ccc")
};
};debug_assert!(ccc >= most_recent_skipped_ccc);
1205if ccc != most_recent_skipped_ccc {
1206// Using the non-Hangul version as a micro-optimization, since
1207 // we already rejected the case where `second` is a starter
1208 // above, and conjoining jamo are starters.
1209if let Some(composed) = self.compose_non_hangul(starter, character) {
1210self.decomposition.buffer.remove(i);
1211 starter = composed;
1212continue;
1213 }
1214 }
1215 most_recent_skipped_ccc = ccc;
1216 i += 1;
1217 }
1218break;
1219 }
12201221if true {
{
match (&self.decomposition.buffer_pos, &0) {
(left_val, right_val) => {
if !(*left_val == *right_val) {
let kind = ::core::panicking::AssertKind::Eq;
::core::panicking::assert_failed(kind, &*left_val,
&*right_val, ::core::option::Option::None);
}
}
}
};
};debug_assert_eq!(self.decomposition.buffer_pos, 0);
12221223if !self.decomposition.buffer.is_empty() {
1224return Some(starter);
1225 }
1226// Now we need to check if composition with an upcoming starter is possible.
1227if let Some(pending) = self.decomposition.pending.take() {
1228// We know that `pending_starter` decomposes to start with a starter.
1229 // Otherwise, it would have been moved to `self.decomposition.buffer`
1230 // by `self.decomposing_next()`. We do this set lookup here in order
1231 // to get an opportunity to go back to the fast track.
1232 // Note that this check has to happen _after_ checking that `pending`
1233 // holds a character, because this flag isn't defined to be meaningful
1234 // when `pending` isn't holding a character.
1235if u32::from(pending.character) < self.composition_passthrough_bound
1236 || !pending.can_combine_backwards()
1237 {
1238// Won't combine backwards anyway.
1239self.decomposition.pending = Some(pending);
1240return Some(starter);
1241 }
1242// Consume what we peeked.
1243undecomposed_starter = pending;
1244// The following line is OK, because we're about to loop back
1245 // to `self.decomposition.decomposing_next(c);`, which will
1246 // restore the between-`next()`-calls invariant of `pending`
1247 // before this function returns.
1248attempt_composition = true;
1249continue;
1250 }
1251// End of input
1252return Some(starter);
1253 }
1254 }
1255}
12561257macro_rules! composing_normalize_to {
1258 ($(#[$meta:meta])*,
1259$normalize_to:ident,
1260$write:path,
1261$slice:ty,
1262$prolog:block,
1263$always_valid_utf:literal,
1264$as_slice:ident,
1265$fast:block,
1266$text:ident,
1267$sink:ident,
1268$composition:ident,
1269$composition_passthrough_bound:ident,
1270$undecomposed_starter:ident,
1271$pending_slice:ident,
1272$len_utf:ident,
1273 ) => {
1274 $(#[$meta])*
1275pub fn $normalize_to<W: $write + ?Sized>(
1276&self,
1277$text: $slice,
1278$sink: &mut W,
1279 ) -> core::fmt::Result {
1280$prolog
1281let mut $composition = self.normalize_iter($text.chars());
1282debug_assert_eq!($composition.decomposition.ignorable_behavior, IgnorableBehavior::Unsupported);
1283for cc in $composition.decomposition.buffer.drain(..) {
1284$sink.write_char(cc.character())?;
1285 }
12861287// Try to get the compiler to hoist the bound to a register.
1288let $composition_passthrough_bound = $composition.composition_passthrough_bound;
1289'outer: loop {
1290debug_assert_eq!($composition.decomposition.buffer_pos, 0);
1291let mut $undecomposed_starter =
1292if let Some(pending) = $composition.decomposition.pending.take() {
1293 pending
1294 } else {
1295return Ok(());
1296 };
1297if u32::from($undecomposed_starter.character) < $composition_passthrough_bound ||
1298$undecomposed_starter.potential_passthrough()
1299 {
1300// We don't know if a `REPLACEMENT_CHARACTER` occurred in the slice or
1301 // was returned in response to an error by the iterator. Assume the
1302 // latter for correctness even though it pessimizes the former.
1303if $always_valid_utf || $undecomposed_starter.character != char::REPLACEMENT_CHARACTER {
1304let $pending_slice = &$text[$text.len() - $composition.decomposition.delegate.$as_slice().len() - $undecomposed_starter.character.$len_utf()..];
1305// The `$fast` block must either:
1306 // 1. Return due to reaching EOF
1307 // 2. Leave a starter with its trie value in `$undecomposed_starter`
1308 // and, if there is still more input, leave the next character
1309 // and its trie value in `$composition.decomposition.pending`.
1310$fast
1311}
1312 }
1313// Fast track above, full algorithm below
1314let mut starter = $composition
1315.decomposition
1316 .decomposing_next($undecomposed_starter);
1317'bufferloop: loop {
1318// We first loop by index to avoid moving the contents of `buffer`, but
1319 // if there's a discontiguous match, we'll start modifying `buffer` instead.
1320loop {
1321let (character, ccc) = if let Some((character, ccc)) = $composition
1322.decomposition
1323 .buffer
1324 .get($composition.decomposition.buffer_pos)
1325 .map(|c| c.character_and_ccc())
1326 {
1327 (character, ccc)
1328 } else {
1329$composition.decomposition.buffer.clear();
1330$composition.decomposition.buffer_pos = 0;
1331break;
1332 };
1333if let Some(composed) = $composition.compose(starter, character) {
1334 starter = composed;
1335$composition.decomposition.buffer_pos += 1;
1336continue;
1337 }
1338let mut most_recent_skipped_ccc = ccc;
1339if most_recent_skipped_ccc == CanonicalCombiningClass::NotReordered {
1340// We failed to compose a starter. Discontiguous match not allowed.
1341 // Write the current `starter` we've been composing, make the unmatched
1342 // starter in the buffer the new `starter` (we know it's been decomposed)
1343 // and process the rest of the buffer with that as the starter.
1344$sink.write_char(starter)?;
1345 starter = character;
1346$composition.decomposition.buffer_pos += 1;
1347continue 'bufferloop;
1348 } else {
1349 {
1350let _ = $composition
1351.decomposition
1352 .buffer
1353 .drain(0..$composition.decomposition.buffer_pos);
1354 }
1355$composition.decomposition.buffer_pos = 0;
1356 }
1357let mut i = 1; // We have skipped one non-starter.
1358while let Some((character, ccc)) = $composition
1359.decomposition
1360 .buffer
1361 .get(i)
1362 .map(|c| c.character_and_ccc())
1363 {
1364if ccc == CanonicalCombiningClass::NotReordered {
1365// Discontiguous match not allowed.
1366$sink.write_char(starter)?;
1367for cc in $composition.decomposition.buffer.drain(..i) {
1368$sink.write_char(cc.character())?;
1369 }
1370 starter = character;
1371 {
1372let removed = $composition.decomposition.buffer.remove(0);
1373debug_assert_eq!(starter, removed.character());
1374 }
1375debug_assert_eq!($composition.decomposition.buffer_pos, 0);
1376continue 'bufferloop;
1377 }
1378debug_assert!(ccc >= most_recent_skipped_ccc);
1379if ccc != most_recent_skipped_ccc {
1380// Using the non-Hangul version as a micro-optimization, since
1381 // we already rejected the case where `second` is a starter
1382 // above, and conjoining jamo are starters.
1383if let Some(composed) =
1384$composition.compose_non_hangul(starter, character)
1385 {
1386$composition.decomposition.buffer.remove(i);
1387 starter = composed;
1388continue;
1389 }
1390 }
1391 most_recent_skipped_ccc = ccc;
1392 i += 1;
1393 }
1394break;
1395 }
1396debug_assert_eq!($composition.decomposition.buffer_pos, 0);
13971398if !$composition.decomposition.buffer.is_empty() {
1399$sink.write_char(starter)?;
1400for cc in $composition.decomposition.buffer.drain(..) {
1401$sink.write_char(cc.character())?;
1402 }
1403// We had non-empty buffer, so can't compose with upcoming.
1404continue 'outer;
1405 }
1406// Now we need to check if composition with an upcoming starter is possible.
1407if $composition.decomposition.pending.is_some() {
1408// We know that `pending_starter` decomposes to start with a starter.
1409 // Otherwise, it would have been moved to `composition.decomposition.buffer`
1410 // by `composition.decomposing_next()`. We do this set lookup here in order
1411 // to get an opportunity to go back to the fast track.
1412 // Note that this check has to happen _after_ checking that `pending`
1413 // holds a character, because this flag isn't defined to be meaningful
1414 // when `pending` isn't holding a character.
1415let pending = $composition.decomposition.pending.as_ref().unwrap();
1416if u32::from(pending.character) < $composition.composition_passthrough_bound
1417 || !pending.can_combine_backwards()
1418 {
1419// Won't combine backwards anyway.
1420$sink.write_char(starter)?;
1421continue 'outer;
1422 }
1423let pending_starter = $composition.decomposition.pending.take().unwrap();
1424let decomposed = $composition.decomposition.decomposing_next(pending_starter);
1425if let Some(composed) = $composition.compose(starter, decomposed) {
1426 starter = composed;
1427 } else {
1428$sink.write_char(starter)?;
1429 starter = decomposed;
1430 }
1431continue 'bufferloop;
1432 }
1433// End of input
1434$sink.write_char(starter)?;
1435return Ok(());
1436 } // 'bufferloop
1437}
1438 }
1439 };
1440}
14411442macro_rules! decomposing_normalize_to {
1443 ($(#[$meta:meta])*,
1444$normalize_to:ident,
1445$write:path,
1446$slice:ty,
1447$prolog:block,
1448$as_slice:ident,
1449$fast:block,
1450$text:ident,
1451$sink:ident,
1452$decomposition:ident,
1453$decomposition_passthrough_bound:ident,
1454$undecomposed_starter:ident,
1455$pending_slice:ident,
1456$outer:lifetime, // loop labels use lifetime tokens
1457) => {
1458 $(#[$meta])*
1459pub fn $normalize_to<W: $write + ?Sized>(
1460&self,
1461$text: $slice,
1462$sink: &mut W,
1463 ) -> core::fmt::Result {
1464$prolog
14651466let mut $decomposition = self.normalize_iter($text.chars());
1467debug_assert_eq!($decomposition.ignorable_behavior, IgnorableBehavior::Unsupported);
14681469// Try to get the compiler to hoist the bound to a register.
1470let $decomposition_passthrough_bound = $decomposition.decomposition_passthrough_bound;
1471$outer: loop {
1472for cc in $decomposition.buffer.drain(..) {
1473$sink.write_char(cc.character())?;
1474 }
1475debug_assert_eq!($decomposition.buffer_pos, 0);
1476let mut $undecomposed_starter = if let Some(pending) = $decomposition.pending.take() {
1477 pending
1478 } else {
1479return Ok(());
1480 };
1481if $undecomposed_starter.starter_and_decomposes_to_self() {
1482// Don't bother including `undecomposed_starter` in a contiguous buffer
1483 // write: Just write it right away:
1484$sink.write_char($undecomposed_starter.character)?;
14851486let $pending_slice = $decomposition.delegate.$as_slice();
1487$fast
1488}
1489let starter = $decomposition.decomposing_next($undecomposed_starter);
1490$sink.write_char(starter)?;
1491 }
1492 }
1493 };
1494}
14951496macro_rules! normalizer_methods {
1497 () => {
1498/// Normalize a string slice into a `Cow<'a, str>`.
1499pub fn normalize<'a>(&self, text: &'a str) -> Cow<'a, str> {
1500let (head, tail) = self.split_normalized(text);
1501if tail.is_empty() {
1502return Cow::Borrowed(head);
1503 }
1504let mut ret = String::new();
1505 ret.reserve(text.len());
1506 ret.push_str(head);
1507let _ = self.normalize_to(tail, &mut ret);
1508 Cow::Owned(ret)
1509 }
15101511/// Split a string slice into maximum normalized prefix and unnormalized suffix
1512 /// such that the concatenation of the prefix and the normalization of the suffix
1513 /// is the normalization of the whole input.
1514pub fn split_normalized<'a>(&self, text: &'a str) -> (&'a str, &'a str) {
1515let up_to = self.is_normalized_up_to(text);
1516 text.split_at_checked(up_to).unwrap_or_else(|| {
1517// Internal bug, not even GIGO, never supposed to happen
1518debug_assert!(false);
1519 ("", text)
1520 })
1521 }
15221523/// Return the index a string slice is normalized up to.
1524fn is_normalized_up_to(&self, text: &str) -> usize {
1525let mut sink = IsNormalizedSinkStr::new(text);
1526let _ = self.normalize_to(text, &mut sink);
1527 text.len() - sink.remaining_len()
1528 }
15291530/// Check whether a string slice is normalized.
1531pub fn is_normalized(&self, text: &str) -> bool {
1532self.is_normalized_up_to(text) == text.len()
1533 }
15341535/// Normalize a slice of potentially-invalid UTF-16 into a `Cow<'a, [u16]>`.
1536 ///
1537 /// Unpaired surrogates are mapped to the REPLACEMENT CHARACTER
1538 /// before normalizing.
1539 ///
1540 /// ✨ *Enabled with the `utf16_iter` Cargo feature.*
1541#[cfg(feature = "utf16_iter")]
1542pub fn normalize_utf16<'a>(&self, text: &'a [u16]) -> Cow<'a, [u16]> {
1543let (head, tail) = self.split_normalized_utf16(text);
1544if tail.is_empty() {
1545return Cow::Borrowed(head);
1546 }
1547let mut ret = alloc::vec::Vec::with_capacity(text.len());
1548 ret.extend_from_slice(head);
1549let _ = self.normalize_utf16_to(tail, &mut ret);
1550 Cow::Owned(ret)
1551 }
15521553/// Split a slice of potentially-invalid UTF-16 into maximum normalized (and valid)
1554 /// prefix and unnormalized suffix such that the concatenation of the prefix and the
1555 /// normalization of the suffix is the normalization of the whole input.
1556 ///
1557 /// ✨ *Enabled with the `utf16_iter` Cargo feature.*
1558#[cfg(feature = "utf16_iter")]
1559pub fn split_normalized_utf16<'a>(&self, text: &'a [u16]) -> (&'a [u16], &'a [u16]) {
1560let up_to = self.is_normalized_utf16_up_to(text);
1561 text.split_at_checked(up_to).unwrap_or_else(|| {
1562// Internal bug, not even GIGO, never supposed to happen
1563debug_assert!(false);
1564 (&[], text)
1565 })
1566 }
15671568/// Return the index a slice of potentially-invalid UTF-16 is normalized up to.
1569 ///
1570 /// ✨ *Enabled with the `utf16_iter` Cargo feature.*
1571#[cfg(feature = "utf16_iter")]
1572fn is_normalized_utf16_up_to(&self, text: &[u16]) -> usize {
1573let mut sink = IsNormalizedSinkUtf16::new(text);
1574let _ = self.normalize_utf16_to(text, &mut sink);
1575 text.len() - sink.remaining_len()
1576 }
15771578/// Checks whether a slice of potentially-invalid UTF-16 is normalized.
1579 ///
1580 /// Unpaired surrogates are treated as the REPLACEMENT CHARACTER.
1581 ///
1582 /// ✨ *Enabled with the `utf16_iter` Cargo feature.*
1583#[cfg(feature = "utf16_iter")]
1584pub fn is_normalized_utf16(&self, text: &[u16]) -> bool {
1585self.is_normalized_utf16_up_to(text) == text.len()
1586 }
15871588/// Normalize a slice of potentially-invalid UTF-8 into a `Cow<'a, str>`.
1589 ///
1590 /// Ill-formed byte sequences are mapped to the REPLACEMENT CHARACTER
1591 /// according to the WHATWG Encoding Standard.
1592 ///
1593 /// ✨ *Enabled with the `utf8_iter` Cargo feature.*
1594#[cfg(feature = "utf8_iter")]
1595pub fn normalize_utf8<'a>(&self, text: &'a [u8]) -> Cow<'a, str> {
1596let (head, tail) = self.split_normalized_utf8(text);
1597if tail.is_empty() {
1598return Cow::Borrowed(head);
1599 }
1600let mut ret = String::new();
1601 ret.reserve(text.len());
1602 ret.push_str(head);
1603let _ = self.normalize_utf8_to(tail, &mut ret);
1604 Cow::Owned(ret)
1605 }
16061607/// Split a slice of potentially-invalid UTF-8 into maximum normalized (and valid)
1608 /// prefix and unnormalized suffix such that the concatenation of the prefix and the
1609 /// normalization of the suffix is the normalization of the whole input.
1610 ///
1611 /// ✨ *Enabled with the `utf8_iter` Cargo feature.*
1612#[cfg(feature = "utf8_iter")]
1613pub fn split_normalized_utf8<'a>(&self, text: &'a [u8]) -> (&'a str, &'a [u8]) {
1614let up_to = self.is_normalized_utf8_up_to(text);
1615let (head, tail) = text.split_at_checked(up_to).unwrap_or_else(|| {
1616// Internal bug, not even GIGO, never supposed to happen
1617debug_assert!(false);
1618 (&[], text)
1619 });
1620// SAFETY: The normalization check also checks for
1621 // UTF-8 well-formedness.
1622(unsafe { str::from_utf8_unchecked(head) }, tail)
1623 }
16241625/// Return the index a slice of potentially-invalid UTF-8 is normalized up to
1626 ///
1627 /// ✨ *Enabled with the `utf8_iter` Cargo feature.*
1628#[cfg(feature = "utf8_iter")]
1629fn is_normalized_utf8_up_to(&self, text: &[u8]) -> usize {
1630let mut sink = IsNormalizedSinkUtf8::new(text);
1631let _ = self.normalize_utf8_to(text, &mut sink);
1632 text.len() - sink.remaining_len()
1633 }
16341635/// Check if a slice of potentially-invalid UTF-8 is normalized.
1636 ///
1637 /// Ill-formed byte sequences are mapped to the REPLACEMENT CHARACTER
1638 /// according to the WHATWG Encoding Standard before checking.
1639 ///
1640 /// ✨ *Enabled with the `utf8_iter` Cargo feature.*
1641#[cfg(feature = "utf8_iter")]
1642pub fn is_normalized_utf8(&self, text: &[u8]) -> bool {
1643self.is_normalized_utf8_up_to(text) == text.len()
1644 }
1645 };
1646}
16471648/// Borrowed version of a normalizer for performing decomposing normalization.
1649#[derive(#[automatically_derived]
impl<'a> ::core::fmt::Debug for DecomposingNormalizerBorrowed<'a> {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field5_finish(f,
"DecomposingNormalizerBorrowed", "decompositions",
&self.decompositions, "tables", &self.tables,
"supplementary_tables", &self.supplementary_tables,
"decomposition_passthrough_bound",
&self.decomposition_passthrough_bound,
"composition_passthrough_bound",
&&self.composition_passthrough_bound)
}
}Debug)]
1650pub struct DecomposingNormalizerBorrowed<'a> {
1651 decompositions: &'a DecompositionData<'a>,
1652 tables: &'a DecompositionTables<'a>,
1653 supplementary_tables: Option<&'a DecompositionTables<'a>>,
1654 decomposition_passthrough_bound: u8, // never above 0xC0
1655composition_passthrough_bound: u16, // never above 0x0300
1656}
16571658impl DecomposingNormalizerBorrowed<'static> {
1659/// Cheaply converts a [`DecomposingNormalizerBorrowed<'static>`] into a [`DecomposingNormalizer`].
1660 ///
1661 /// Note: Due to branching and indirection, using [`DecomposingNormalizer`] might inhibit some
1662 /// compile-time optimizations that are possible with [`DecomposingNormalizerBorrowed`].
1663pub const fn static_to_owned(self) -> DecomposingNormalizer {
1664DecomposingNormalizer {
1665 decompositions: DataPayload::from_static_ref(self.decompositions),
1666 tables: DataPayload::from_static_ref(self.tables),
1667 supplementary_tables: if let Some(s) = self.supplementary_tables {
1668// `map` not available in const context
1669Some(DataPayload::from_static_ref(s))
1670 } else {
1671None1672 },
1673 decomposition_passthrough_bound: self.decomposition_passthrough_bound,
1674 composition_passthrough_bound: self.composition_passthrough_bound,
1675 }
1676 }
16771678/// NFD constructor using compiled data.
1679 ///
1680 /// ✨ *Enabled with the `compiled_data` Cargo feature.*
1681 ///
1682 /// [📚 Help choosing a constructor](icu_provider::constructors)
1683#[cfg(feature = "compiled_data")]
1684pub const fn new_nfd() -> Self {
1685const _: () = if !(provider::Baked::SINGLETON_NORMALIZER_NFD_TABLES_V1.scalars16.const_len()
+
provider::Baked::SINGLETON_NORMALIZER_NFD_TABLES_V1.scalars24.const_len()
<= 0xFFF) {
{ ::core::panicking::panic_fmt(format_args!("future extension")); }
}assert!(
1686 provider::Baked::SINGLETON_NORMALIZER_NFD_TABLES_V1
1687 .scalars16
1688 .const_len()
1689 + provider::Baked::SINGLETON_NORMALIZER_NFD_TABLES_V1
1690 .scalars24
1691 .const_len()
1692 <= 0xFFF,
1693"future extension"
1694);
16951696DecomposingNormalizerBorrowed {
1697 decompositions: provider::Baked::SINGLETON_NORMALIZER_NFD_DATA_V1,
1698 tables: provider::Baked::SINGLETON_NORMALIZER_NFD_TABLES_V1,
1699 supplementary_tables: None,
1700 decomposition_passthrough_bound: 0xC0,
1701 composition_passthrough_bound: 0x0300,
1702 }
1703 }
17041705/// NFKD constructor using compiled data.
1706 ///
1707 /// ✨ *Enabled with the `compiled_data` Cargo feature.*
1708 ///
1709 /// [📚 Help choosing a constructor](icu_provider::constructors)
1710#[cfg(feature = "compiled_data")]
1711pub const fn new_nfkd() -> Self {
1712const _: () = if !(provider::Baked::SINGLETON_NORMALIZER_NFD_TABLES_V1.scalars16.const_len()
+
provider::Baked::SINGLETON_NORMALIZER_NFD_TABLES_V1.scalars24.const_len()
+
provider::Baked::SINGLETON_NORMALIZER_NFKD_TABLES_V1.scalars16.const_len()
+
provider::Baked::SINGLETON_NORMALIZER_NFKD_TABLES_V1.scalars24.const_len()
<= 0xFFF) {
{ ::core::panicking::panic_fmt(format_args!("future extension")); }
}assert!(
1713 provider::Baked::SINGLETON_NORMALIZER_NFD_TABLES_V1
1714 .scalars16
1715 .const_len()
1716 + provider::Baked::SINGLETON_NORMALIZER_NFD_TABLES_V1
1717 .scalars24
1718 .const_len()
1719 + provider::Baked::SINGLETON_NORMALIZER_NFKD_TABLES_V1
1720 .scalars16
1721 .const_len()
1722 + provider::Baked::SINGLETON_NORMALIZER_NFKD_TABLES_V1
1723 .scalars24
1724 .const_len()
1725 <= 0xFFF,
1726"future extension"
1727);
17281729const _: () = if !(provider::Baked::SINGLETON_NORMALIZER_NFKD_DATA_V1.passthrough_cap <=
0x0300) {
{ ::core::panicking::panic_fmt(format_args!("invalid")); }
}assert!(
1730 provider::Baked::SINGLETON_NORMALIZER_NFKD_DATA_V1.passthrough_cap <= 0x0300,
1731"invalid"
1732);
17331734let decomposition_capped =
1735if provider::Baked::SINGLETON_NORMALIZER_NFKD_DATA_V1.passthrough_cap < 0xC0 {
1736 provider::Baked::SINGLETON_NORMALIZER_NFKD_DATA_V1.passthrough_cap
1737 } else {
17380xC0
1739};
1740let composition_capped =
1741if provider::Baked::SINGLETON_NORMALIZER_NFKD_DATA_V1.passthrough_cap < 0x0300 {
1742 provider::Baked::SINGLETON_NORMALIZER_NFKD_DATA_V1.passthrough_cap
1743 } else {
17440x0300
1745};
17461747DecomposingNormalizerBorrowed {
1748 decompositions: provider::Baked::SINGLETON_NORMALIZER_NFKD_DATA_V1,
1749 tables: provider::Baked::SINGLETON_NORMALIZER_NFD_TABLES_V1,
1750 supplementary_tables: Some(provider::Baked::SINGLETON_NORMALIZER_NFKD_TABLES_V1),
1751 decomposition_passthrough_bound: decomposition_cappedas u8,
1752 composition_passthrough_bound: composition_capped,
1753 }
1754 }
17551756#[cfg(feature = "compiled_data")]
1757pub(crate) const fn new_uts46_decomposed() -> Self {
1758const _: () = if !(provider::Baked::SINGLETON_NORMALIZER_NFD_TABLES_V1.scalars16.const_len()
+
provider::Baked::SINGLETON_NORMALIZER_NFD_TABLES_V1.scalars24.const_len()
+
provider::Baked::SINGLETON_NORMALIZER_NFKD_TABLES_V1.scalars16.const_len()
+
provider::Baked::SINGLETON_NORMALIZER_NFKD_TABLES_V1.scalars24.const_len()
<= 0xFFF) {
{ ::core::panicking::panic_fmt(format_args!("future extension")); }
}assert!(
1759 provider::Baked::SINGLETON_NORMALIZER_NFD_TABLES_V1
1760 .scalars16
1761 .const_len()
1762 + provider::Baked::SINGLETON_NORMALIZER_NFD_TABLES_V1
1763 .scalars24
1764 .const_len()
1765 + provider::Baked::SINGLETON_NORMALIZER_NFKD_TABLES_V1
1766 .scalars16
1767 .const_len()
1768 + provider::Baked::SINGLETON_NORMALIZER_NFKD_TABLES_V1
1769 .scalars24
1770 .const_len()
1771 <= 0xFFF,
1772"future extension"
1773);
17741775const _: () = if !(provider::Baked::SINGLETON_NORMALIZER_UTS46_DATA_V1.passthrough_cap <=
0x0300) {
{ ::core::panicking::panic_fmt(format_args!("invalid")); }
}assert!(
1776 provider::Baked::SINGLETON_NORMALIZER_UTS46_DATA_V1.passthrough_cap <= 0x0300,
1777"invalid"
1778);
17791780let decomposition_capped =
1781if provider::Baked::SINGLETON_NORMALIZER_UTS46_DATA_V1.passthrough_cap < 0xC0 {
1782 provider::Baked::SINGLETON_NORMALIZER_UTS46_DATA_V1.passthrough_cap
1783 } else {
17840xC0
1785};
1786let composition_capped =
1787if provider::Baked::SINGLETON_NORMALIZER_UTS46_DATA_V1.passthrough_cap < 0x0300 {
1788 provider::Baked::SINGLETON_NORMALIZER_UTS46_DATA_V1.passthrough_cap
1789 } else {
17900x0300
1791};
17921793DecomposingNormalizerBorrowed {
1794 decompositions: provider::Baked::SINGLETON_NORMALIZER_UTS46_DATA_V1,
1795 tables: provider::Baked::SINGLETON_NORMALIZER_NFD_TABLES_V1,
1796 supplementary_tables: Some(provider::Baked::SINGLETON_NORMALIZER_NFKD_TABLES_V1),
1797 decomposition_passthrough_bound: decomposition_cappedas u8,
1798 composition_passthrough_bound: composition_capped,
1799 }
1800 }
1801}
18021803impl<'data> DecomposingNormalizerBorrowed<'data> {
1804/// NFD constructor using already-loaded data.
1805 ///
1806 /// This constructor is intended for use by collations.
1807 ///
1808 /// [📚 Help choosing a constructor](icu_provider::constructors)
1809#[doc(hidden)]
1810pub fn new_with_data(
1811 decompositions: &'data DecompositionData<'data>,
1812 tables: &'data DecompositionTables<'data>,
1813 ) -> Self {
1814Self {
1815decompositions,
1816tables,
1817 supplementary_tables: None,
1818 decomposition_passthrough_bound: 0xC0,
1819 composition_passthrough_bound: 0x0300,
1820 }
1821 }
18221823/// Wraps a delegate iterator into a decomposing iterator
1824 /// adapter by using the data already held by this normalizer.
1825pub fn normalize_iter<I: Iterator<Item = char>>(&self, iter: I) -> Decomposition<'data, I> {
1826Decomposition::new_with_supplements(
1827iter,
1828self.decompositions,
1829self.tables,
1830self.supplementary_tables,
1831self.decomposition_passthrough_bound,
1832 IgnorableBehavior::Unsupported,
1833 )
1834 }
18351836/// Normalize a string slice into a `Cow<'a, str>`.
pub fn normalize<'a>(&self, text: &'a str) -> Cow<'a, str> {
let (head, tail) = self.split_normalized(text);
if tail.is_empty() { return Cow::Borrowed(head); }
let mut ret = String::new();
ret.reserve(text.len());
ret.push_str(head);
let _ = self.normalize_to(tail, &mut ret);
Cow::Owned(ret)
}
/// Split a string slice into maximum normalized prefix and unnormalized suffix
/// such that the concatenation of the prefix and the normalization of the suffix
/// is the normalization of the whole input.
pub fn split_normalized<'a>(&self, text: &'a str) -> (&'a str, &'a str) {
let up_to = self.is_normalized_up_to(text);
text.split_at_checked(up_to).unwrap_or_else(||
{
if true {
if !false {
::core::panicking::panic("assertion failed: false")
};
};
("", text)
})
}
/// Return the index a string slice is normalized up to.
fn is_normalized_up_to(&self, text: &str) -> usize {
let mut sink = IsNormalizedSinkStr::new(text);
let _ = self.normalize_to(text, &mut sink);
text.len() - sink.remaining_len()
}
/// Check whether a string slice is normalized.
pub fn is_normalized(&self, text: &str) -> bool {
self.is_normalized_up_to(text) == text.len()
}normalizer_methods!();
18371838#[doc = r" Normalize a string slice into a `Write` sink."]
pub fn normalize_to<W: core::fmt::Write +
?Sized>(&self, text: &str, sink: &mut W) -> core::fmt::Result {
{}
let mut decomposition = self.normalize_iter(text.chars());
if true {
{
match (&decomposition.ignorable_behavior,
&IgnorableBehavior::Unsupported) {
(left_val, right_val) => {
if !(*left_val == *right_val) {
let kind = ::core::panicking::AssertKind::Eq;
::core::panicking::assert_failed(kind, &*left_val,
&*right_val, ::core::option::Option::None);
}
}
}
};
};
let decomposition_passthrough_bound =
decomposition.decomposition_passthrough_bound;
'outer: loop {
for cc in decomposition.buffer.drain(..) {
sink.write_char(cc.character())?;
}
if true {
{
match (&decomposition.buffer_pos, &0) {
(left_val, right_val) => {
if !(*left_val == *right_val) {
let kind = ::core::panicking::AssertKind::Eq;
::core::panicking::assert_failed(kind, &*left_val,
&*right_val, ::core::option::Option::None);
}
}
}
};
};
let mut undecomposed_starter =
if let Some(pending) = decomposition.pending.take() {
pending
} else { return Ok(()); };
if undecomposed_starter.starter_and_decomposes_to_self() {
sink.write_char(undecomposed_starter.character)?;
let pending_slice = decomposition.delegate.as_str();
{
let decomposition_passthrough_byte_bound =
if decomposition_passthrough_bound == 0xC0 {
0xC3u8
} else { decomposition_passthrough_bound.min(0x80) as u8 };
#[expect(clippy::unwrap_used)]
'fast: loop {
let mut code_unit_iter =
decomposition.delegate.as_str().as_bytes().iter();
'fastest: loop {
if let Some(&upcoming_byte) = code_unit_iter.next() {
if upcoming_byte < decomposition_passthrough_byte_bound {
continue 'fastest;
}
decomposition.delegate =
pending_slice[pending_slice.len() -
code_unit_iter.as_slice().len() - 1..].chars();
break 'fastest;
}
sink.write_str(pending_slice)?;
return Ok(());
}
let upcoming = decomposition.delegate.next().unwrap();
let upcoming_with_trie_value =
decomposition.attach_trie_value(upcoming);
if upcoming_with_trie_value.starter_and_decomposes_to_self()
{
continue 'fast;
}
let consumed_so_far_slice =
&pending_slice[..pending_slice.len() -
decomposition.delegate.as_str().len() -
upcoming.len_utf8()];
sink.write_str(consumed_so_far_slice)?;
if decomposition_starts_with_non_starter(upcoming_with_trie_value.trie_val)
{
decomposition.pending = Some(upcoming_with_trie_value);
decomposition.gather_and_sort_combining(0);
continue 'outer;
}
undecomposed_starter = upcoming_with_trie_value;
if true {
if !decomposition.pending.is_none() {
::core::panicking::panic("assertion failed: decomposition.pending.is_none()")
};
};
break 'fast;
}
}
}
let starter = decomposition.decomposing_next(undecomposed_starter);
sink.write_char(starter)?;
}
}decomposing_normalize_to!(
1839/// Normalize a string slice into a `Write` sink.
1840,
1841 normalize_to,
1842core::fmt::Write,
1843&str,
1844 {
1845 },
1846 as_str,
1847 {
1848let decomposition_passthrough_byte_bound = if decomposition_passthrough_bound == 0xC0 {
18490xC3u8
1850} else {
1851 decomposition_passthrough_bound.min(0x80) as u8
1852 };
1853// The attribute belongs on an inner statement, but Rust doesn't allow it there.
1854#[expect(clippy::unwrap_used)]
1855'fast: loop {
1856let mut code_unit_iter = decomposition.delegate.as_str().as_bytes().iter();
1857'fastest: loop {
1858if let Some(&upcoming_byte) = code_unit_iter.next() {
1859if upcoming_byte < decomposition_passthrough_byte_bound {
1860// Fast-track succeeded!
1861continue 'fastest;
1862 }
1863// This deliberately isn't panic-free, since the code pattern
1864 // that was OK for the composing counterpart regressed
1865 // English and French performance if done here, too.
1866decomposition.delegate = pending_slice[pending_slice.len() - code_unit_iter.as_slice().len() - 1..].chars();
1867break 'fastest;
1868 }
1869// End of stream
1870sink.write_str(pending_slice)?;
1871return Ok(());
1872 }
18731874// `unwrap()` OK, because the slice is valid UTF-8 and we know there
1875 // is an upcoming byte.
1876let upcoming = decomposition.delegate.next().unwrap();
1877let upcoming_with_trie_value = decomposition.attach_trie_value(upcoming);
1878if upcoming_with_trie_value.starter_and_decomposes_to_self() {
1879continue 'fast;
1880 }
1881let consumed_so_far_slice = &pending_slice[..pending_slice.len()
1882 - decomposition.delegate.as_str().len()
1883 - upcoming.len_utf8()];
1884 sink.write_str(consumed_so_far_slice)?;
18851886// Now let's figure out if we got a starter or a non-starter.
1887if decomposition_starts_with_non_starter(
1888 upcoming_with_trie_value.trie_val,
1889 ) {
1890// Let this trie value to be reprocessed in case it is
1891 // one of the rare decomposing ones.
1892decomposition.pending = Some(upcoming_with_trie_value);
1893 decomposition.gather_and_sort_combining(0);
1894continue 'outer;
1895 }
1896 undecomposed_starter = upcoming_with_trie_value;
1897debug_assert!(decomposition.pending.is_none());
1898break 'fast;
1899 }
1900 },
1901 text,
1902 sink,
1903 decomposition,
1904 decomposition_passthrough_bound,
1905 undecomposed_starter,
1906 pending_slice,
1907'outer,
1908 );
19091910decomposing_normalize_to!(
1911/// Normalize a slice of potentially-invalid UTF-8 into a `Write` sink.
1912 ///
1913 /// Ill-formed byte sequences are mapped to the REPLACEMENT CHARACTER
1914 /// according to the WHATWG Encoding Standard.
1915 ///
1916 /// ✨ *Enabled with the `utf8_iter` Cargo feature.*
1917#[cfg(feature = "utf8_iter")]
1918,
1919 normalize_utf8_to,
1920 core::fmt::Write,
1921&[u8],
1922 {
1923 },
1924 as_slice,
1925 {
1926let decomposition_passthrough_byte_bound = decomposition_passthrough_bound.min(0x80) as u8;
1927'fast: loop {
1928let mut code_unit_iter = decomposition.delegate.as_slice().iter();
1929'fastest: loop {
1930if let Some(&upcoming_byte) = code_unit_iter.next() {
1931if upcoming_byte < decomposition_passthrough_byte_bound {
1932// Fast-track succeeded!
1933continue 'fastest;
1934 }
1935break 'fastest;
1936 }
1937// End of stream
1938sink.write_str(unsafe { str::from_utf8_unchecked(pending_slice) })?;
1939return Ok(());
1940 }
1941#[expect(clippy::indexing_slicing)]
1942{decomposition.delegate = pending_slice[pending_slice.len() - code_unit_iter.as_slice().len() - 1..].chars();}
19431944// `unwrap()` OK, because the slice is valid UTF-8 and we know there
1945 // is an upcoming byte.
1946#[expect(clippy::unwrap_used)]
1947let upcoming = decomposition.delegate.next().unwrap();
1948let upcoming_with_trie_value = decomposition.attach_trie_value(upcoming);
1949if upcoming_with_trie_value.starter_and_decomposes_to_self_except_replacement() {
1950// Note: The trie value of the REPLACEMENT CHARACTER is
1951 // intentionally formatted to fail the
1952 // `starter_and_decomposes_to_self` test even though it
1953 // really is a starter that decomposes to self. This
1954 // Allows moving the branch on REPLACEMENT CHARACTER
1955 // below this `continue`.
1956continue 'fast;
1957 }
19581959// TODO: Annotate as unlikely.
1960if upcoming == char::REPLACEMENT_CHARACTER {
1961// We might have an error, so fall out of the fast path.
19621963 // Since the U+FFFD might signify an error, we can't
1964 // assume `upcoming.len_utf8()` for the backoff length.
1965#[expect(clippy::indexing_slicing)]
1966let mut consumed_so_far = pending_slice[..pending_slice.len() - decomposition.delegate.as_slice().len()].chars();
1967let back = consumed_so_far.next_back();
1968debug_assert_eq!(back, Some(char::REPLACEMENT_CHARACTER));
1969let consumed_so_far_slice = consumed_so_far.as_slice();
1970 sink.write_str(unsafe { str::from_utf8_unchecked(consumed_so_far_slice) } )?;
19711972// We could call `gather_and_sort_combining` here and
1973 // `continue 'outer`, but this should be better for code
1974 // size.
1975undecomposed_starter = upcoming_with_trie_value;
1976debug_assert!(decomposition.pending.is_none());
1977break 'fast;
1978 }
19791980#[expect(clippy::indexing_slicing)]
1981let consumed_so_far_slice = &pending_slice[..pending_slice.len()
1982 - decomposition.delegate.as_slice().len()
1983 - upcoming.len_utf8()];
1984 sink.write_str(unsafe { str::from_utf8_unchecked(consumed_so_far_slice) } )?;
19851986// Now let's figure out if we got a starter or a non-starter.
1987if decomposition_starts_with_non_starter(
1988 upcoming_with_trie_value.trie_val,
1989 ) {
1990// Let this trie value to be reprocessed in case it is
1991 // one of the rare decomposing ones.
1992decomposition.pending = Some(upcoming_with_trie_value);
1993 decomposition.gather_and_sort_combining(0);
1994continue 'outer;
1995 }
1996 undecomposed_starter = upcoming_with_trie_value;
1997debug_assert!(decomposition.pending.is_none());
1998break 'fast;
1999 }
2000 },
2001 text,
2002 sink,
2003 decomposition,
2004 decomposition_passthrough_bound,
2005 undecomposed_starter,
2006 pending_slice,
2007'outer,
2008 );
20092010decomposing_normalize_to!(
2011/// Normalize a slice of potentially-invalid UTF-16 into a `Write16` sink.
2012 ///
2013 /// Unpaired surrogates are mapped to the REPLACEMENT CHARACTER
2014 /// before normalizing.
2015 ///
2016 /// ✨ *Enabled with the `utf16_iter` Cargo feature.*
2017#[cfg(feature = "utf16_iter")]
2018,
2019 normalize_utf16_to,
2020 write16::Write16,
2021&[u16],
2022 {
2023 sink.size_hint(text.len())?;
2024 },
2025 as_slice,
2026 {
2027// This loop is only broken out of as goto forward and only as release-build recovery from
2028 // detecting an internal bug without panic. (In debug builds, internal bugs panic instead.)
2029#[expect(clippy::never_loop)]
2030'fastwrap: loop {
2031// Commented out `code_unit_iter` and used `ptr` and `end` to
2032 // work around https://github.com/rust-lang/rust/issues/144684 .
2033 //
2034 // let mut code_unit_iter = decomposition.delegate.as_slice().iter();
2035let delegate_as_slice = decomposition.delegate.as_slice();
2036let mut ptr: *const u16 = delegate_as_slice.as_ptr();
2037// SAFETY: materializing a pointer immediately past the end of an
2038 // allocation is OK.
2039let end: *const u16 = unsafe { ptr.add(delegate_as_slice.len()) };
2040'fast: loop {
2041// if let Some(&upcoming_code_unit) = code_unit_iter.next() {
2042if ptr != end {
2043// SAFETY: We just checked that `ptr` has not reached `end`.
2044 // `ptr` always advances by one, and we always have a check
2045 // per advancement.
2046let upcoming_code_unit = unsafe { *ptr };
2047// SAFETY: Since `ptr` hadn't reached `end`, yet, advancing
2048 // by one points to the same allocation or to immediately
2049 // after, which is OK.
2050ptr = unsafe { ptr.add(1) };
20512052let mut upcoming32 = u32::from(upcoming_code_unit);
2053// The performance of what logically is supposed to be this
2054 // branch is _incredibly_ brittle and what LLVM ends up doing
2055 // that affects the performance of what's logically about this
2056 // decision can swing to double/halve the throughput for Basic
2057 // Latin in ways that are completely unintuitive. Basically _any_
2058 // change to _any_ code that participates in how LLVM sees the
2059 // code around here can make the perf fall over. In seems that
2060 // manually annotating this branch as likely has worse effects
2061 // on non-Basic-Latin input that the case where LLVM just happens to
2062 // do the right thing.
2063 //
2064 // What happens with this branch may depend on what sink type
2065 // this code is monomorphized over.
2066 //
2067 // What a terrible sink of developer time!
2068if upcoming32 < decomposition_passthrough_bound {
2069continue 'fast;
2070 }
2071// We might be doing a trie lookup by surrogate. Surrogates get
2072 // a decomposition to U+FFFD.
2073let mut trie_value = decomposition.trie.get16(upcoming_code_unit);
2074if starter_and_decomposes_to_self_impl(trie_value) {
2075continue 'fast;
2076 }
2077// We might now be looking at a surrogate.
2078 // The loop is only broken out of as goto forward
2079#[expect(clippy::never_loop)]
2080'surrogateloop: loop {
2081// LLVM's optimizations are incredibly brittle for the code _above_,
2082 // and using `likely` _below_ without using it _above_ helps!
2083 // What a massive sink of developer time!
2084 // Seriously, the effect of these annotations is massively
2085 // unintuitive. Measure everything!
2086 // Notably, the `if likely(...)` formulation optimizes differently
2087 // than just putting `cold_path()` on the `else` path!
2088let surrogate_base = upcoming32.wrapping_sub(0xD800);
2089if likely(surrogate_base > (0xDFFF - 0xD800)) {
2090// Not surrogate
2091break 'surrogateloop;
2092 }
2093if likely(surrogate_base <= (0xDBFF - 0xD800)) {
2094// let iter_backup = code_unit_iter.clone();
2095 // if let Some(&low) = code_unit_iter.next() {
2096if ptr != end {
2097// SAFETY: We just checked that `ptr` has not reached `end`.
2098 // `ptr` always advances by one, and we always have a check
2099 // per advancement.
2100let low = unsafe { *ptr };
2101if likely(in_inclusive_range16(low, 0xDC00, 0xDFFF)) {
2102// SAFETY: Since `ptr` hadn't reached `end`, yet, advancing
2103 // by one points to the same allocation or to immediately
2104 // after, which is OK.
2105ptr = unsafe { ptr.add(1) };
21062107 upcoming32 = (upcoming32 << 10) + u32::from(low)
2108 - (((0xD800u32 << 10) - 0x10000u32) + 0xDC00u32);
2109// Successfully-paired surrogate. Read from the trie again.
2110trie_value = {
2111// Semantically, this bit of conditional compilation makes no sense.
2112 // The purpose is to keep LLVM seeing the untyped trie case the way
2113 // it did before so as not to regress the performance of the untyped
2114 // case due to unintuitive optimizer effects. If you care about the
2115 // perf of the untyped trie case and have better ideas, please try
2116 // something better.
2117#[cfg(not(icu4x_unstable_fast_trie_only))]
2118{decomposition.trie.get32(upcoming32)}
2119#[cfg(icu4x_unstable_fast_trie_only)]
2120{decomposition.trie.get32_supplementary(upcoming32)}
2121 };
2122if likely(starter_and_decomposes_to_self_impl(trie_value)) {
2123continue 'fast;
2124 }
2125break 'surrogateloop;
2126// } else {
2127 // code_unit_iter = iter_backup;
2128}
2129 }
2130 }
2131// unpaired surrogate
2132upcoming32 = 0xFFFD; // Safe value for `char::from_u32_unchecked` and matches later potential error check.
2133 // trie_value already holds a decomposition to U+FFFD.
2134break 'surrogateloop;
2135 }
21362137let upcoming = unsafe { char::from_u32_unchecked(upcoming32) };
2138let upcoming_with_trie_value = CharacterAndTrieValue::new(upcoming, trie_value);
213921402141let Some(consumed_so_far_slice) = pending_slice.get(..pending_slice.len() -
2142// code_unit_iter.as_slice().len()
2143 // SAFETY: `ptr` and `end` have been derived from the same allocation
2144 // and `ptr` is never greater than `end`.
2145unsafe { end.offset_from(ptr) as usize }
2146 - upcoming.len_utf16()) else {
2147// If we ever come here, it's a bug, but let's avoid panic code paths in release builds.
2148debug_assert!(false);
2149// Throw away the results of the fast path.
2150break 'fastwrap;
2151 };
2152 sink.write_slice(consumed_so_far_slice)?;
21532154if decomposition_starts_with_non_starter(
2155 upcoming_with_trie_value.trie_val,
2156 ) {
2157// Sync with main iterator
2158 // decomposition.delegate = code_unit_iter.as_slice().chars();
2159 // SAFETY: `ptr` and `end` have been derived from the same allocation
2160 // and `ptr` is never greater than `end`.
2161decomposition.delegate = unsafe { core::slice::from_raw_parts(ptr, end.offset_from(ptr) as usize) }.chars();
2162// Let this trie value to be reprocessed in case it is
2163 // one of the rare decomposing ones.
2164decomposition.pending = Some(upcoming_with_trie_value);
2165 decomposition.gather_and_sort_combining(0);
2166continue 'outer;
2167 }
2168 undecomposed_starter = upcoming_with_trie_value;
2169debug_assert!(decomposition.pending.is_none());
2170break 'fast;
2171 }
2172// End of stream
2173sink.write_slice(pending_slice)?;
2174return Ok(());
2175 }
2176// Sync the main iterator
2177 // decomposition.delegate = code_unit_iter.as_slice().chars();
2178 // SAFETY: `ptr` and `end` have been derived from the same allocation
2179 // and `ptr` is never greater than `end`.
2180decomposition.delegate = unsafe { core::slice::from_raw_parts(ptr, end.offset_from(ptr) as usize) }.chars();
2181break 'fastwrap;
2182 }
2183 },
2184 text,
2185 sink,
2186 decomposition,
2187 decomposition_passthrough_bound,
2188 undecomposed_starter,
2189 pending_slice,
2190'outer,
2191 );
2192}
21932194/// A normalizer for performing decomposing normalization.
2195#[derive(#[automatically_derived]
impl ::core::fmt::Debug for DecomposingNormalizer {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field5_finish(f,
"DecomposingNormalizer", "decompositions", &self.decompositions,
"tables", &self.tables, "supplementary_tables",
&self.supplementary_tables, "decomposition_passthrough_bound",
&self.decomposition_passthrough_bound,
"composition_passthrough_bound",
&&self.composition_passthrough_bound)
}
}Debug)]
2196pub struct DecomposingNormalizer {
2197 decompositions: DataPayload<NormalizerNfdDataV1>,
2198 tables: DataPayload<NormalizerNfdTablesV1>,
2199 supplementary_tables: Option<DataPayload<NormalizerNfkdTablesV1>>,
2200 decomposition_passthrough_bound: u8, // never above 0xC0
2201composition_passthrough_bound: u16, // never above 0x0300
2202}
22032204impl DecomposingNormalizer {
2205/// Constructs a borrowed version of this type for more efficient querying.
2206pub fn as_borrowed(&self) -> DecomposingNormalizerBorrowed<'_> {
2207DecomposingNormalizerBorrowed {
2208 decompositions: self.decompositions.get(),
2209 tables: self.tables.get(),
2210 supplementary_tables: self.supplementary_tables.as_ref().map(|s| s.get()),
2211 decomposition_passthrough_bound: self.decomposition_passthrough_bound,
2212 composition_passthrough_bound: self.composition_passthrough_bound,
2213 }
2214 }
22152216/// NFD constructor using compiled data.
2217 ///
2218 /// ✨ *Enabled with the `compiled_data` Cargo feature.*
2219 ///
2220 /// [📚 Help choosing a constructor](icu_provider::constructors)
2221#[cfg(feature = "compiled_data")]
2222pub const fn new_nfd() -> DecomposingNormalizerBorrowed<'static> {
2223DecomposingNormalizerBorrowed::new_nfd()
2224 }
22252226icu_provider::gen_buffer_data_constructors!(
2227 () -> error: DataError,
2228 functions: [
2229 new_nfd: skip,
2230 try_new_nfd_with_buffer_provider,
2231 try_new_nfd_unstable,
2232Self,
2233 ]
2234 );
22352236#[doc = "A version of [`Self::new_nfd`] that uses custom data provided by a [`DataProvider`].\n\n[\u{1f4da} Help choosing a constructor](icu_provider::constructors)\n\n<div class=\"stab unstable\">\u{26a0}\u{fe0f} The bounds on <tt>provider</tt> may change over time, including in SemVer minor releases.</div>"icu_provider::gen_buffer_unstable_docs!(UNSTABLE, Self::new_nfd)]
2237pub fn try_new_nfd_unstable<D>(provider: &D) -> Result<Self, DataError>
2238where
2239D: DataProvider<NormalizerNfdDataV1> + DataProvider<NormalizerNfdTablesV1> + ?Sized,
2240 {
2241let decompositions: DataPayload<NormalizerNfdDataV1> =
2242 provider.load(Default::default())?.payload;
2243let tables: DataPayload<NormalizerNfdTablesV1> = provider.load(Default::default())?.payload;
22442245if tables.get().scalars16.len() + tables.get().scalars24.len() > 0xFFF {
2246// The data is from a future where there exists a normalization flavor whose
2247 // complex decompositions take more than 0xFFF but fewer than 0x1FFF code points
2248 // of space. If a good use case from such a decomposition flavor arises, we can
2249 // dynamically change the bit masks so that the length mask becomes 0x1FFF instead
2250 // of 0xFFF and the all-non-starters mask becomes 0 instead of 0x1000. However,
2251 // since for now the masks are hard-coded, error out.
2252return Err(
2253DataError::custom("future extension").with_marker(NormalizerNfdTablesV1::INFO)
2254 );
2255 }
22562257let cap = decompositions.get().passthrough_cap;
2258if cap > 0x0300 {
2259return Err(DataError::custom("invalid").with_marker(NormalizerNfdDataV1::INFO));
2260 }
2261let decomposition_capped = cap.min(0xC0);
2262let composition_capped = cap.min(0x0300);
22632264Ok(DecomposingNormalizer {
2265decompositions,
2266tables,
2267 supplementary_tables: None,
2268 decomposition_passthrough_bound: decomposition_cappedas u8,
2269 composition_passthrough_bound: composition_capped,
2270 })
2271 }
22722273icu_provider::gen_buffer_data_constructors!(
2274 () -> error: DataError,
2275 functions: [
2276 new_nfkd: skip,
2277 try_new_nfkd_with_buffer_provider,
2278 try_new_nfkd_unstable,
2279Self,
2280 ]
2281 );
22822283/// NFKD constructor using compiled data.
2284 ///
2285 /// ✨ *Enabled with the `compiled_data` Cargo feature.*
2286 ///
2287 /// [📚 Help choosing a constructor](icu_provider::constructors)
2288#[cfg(feature = "compiled_data")]
2289pub const fn new_nfkd() -> DecomposingNormalizerBorrowed<'static> {
2290DecomposingNormalizerBorrowed::new_nfkd()
2291 }
22922293#[doc = "A version of [`Self::new_nfkd`] that uses custom data provided by a [`DataProvider`].\n\n[\u{1f4da} Help choosing a constructor](icu_provider::constructors)\n\n<div class=\"stab unstable\">\u{26a0}\u{fe0f} The bounds on <tt>provider</tt> may change over time, including in SemVer minor releases.</div>"icu_provider::gen_buffer_unstable_docs!(UNSTABLE, Self::new_nfkd)]
2294pub fn try_new_nfkd_unstable<D>(provider: &D) -> Result<Self, DataError>
2295where
2296D: DataProvider<NormalizerNfkdDataV1>
2297 + DataProvider<NormalizerNfdTablesV1>
2298 + DataProvider<NormalizerNfkdTablesV1>
2299 + ?Sized,
2300 {
2301let decompositions: DataPayload<NormalizerNfkdDataV1> =
2302 provider.load(Default::default())?.payload;
2303let tables: DataPayload<NormalizerNfdTablesV1> = provider.load(Default::default())?.payload;
2304let supplementary_tables: DataPayload<NormalizerNfkdTablesV1> =
2305 provider.load(Default::default())?.payload;
23062307if tables.get().scalars16.len()
2308 + tables.get().scalars24.len()
2309 + supplementary_tables.get().scalars16.len()
2310 + supplementary_tables.get().scalars24.len()
2311 > 0xFFF
2312{
2313// The data is from a future where there exists a normalization flavor whose
2314 // complex decompositions take more than 0xFFF but fewer than 0x1FFF code points
2315 // of space. If a good use case from such a decomposition flavor arises, we can
2316 // dynamically change the bit masks so that the length mask becomes 0x1FFF instead
2317 // of 0xFFF and the all-non-starters mask becomes 0 instead of 0x1000. However,
2318 // since for now the masks are hard-coded, error out.
2319return Err(
2320DataError::custom("future extension").with_marker(NormalizerNfdTablesV1::INFO)
2321 );
2322 }
23232324let cap = decompositions.get().passthrough_cap;
2325if cap > 0x0300 {
2326return Err(DataError::custom("invalid").with_marker(NormalizerNfkdDataV1::INFO));
2327 }
2328let decomposition_capped = cap.min(0xC0);
2329let composition_capped = cap.min(0x0300);
23302331Ok(DecomposingNormalizer {
2332 decompositions: decompositions.cast(),
2333tables,
2334 supplementary_tables: Some(supplementary_tables),
2335 decomposition_passthrough_bound: decomposition_cappedas u8,
2336 composition_passthrough_bound: composition_capped,
2337 })
2338 }
23392340/// UTS 46 decomposed constructor (testing only)
2341 ///
2342 /// This is a special building block normalization for IDNA. It is the decomposed counterpart of
2343 /// ICU4C's UTS 46 normalization with two exceptions: characters that UTS 46 disallows and
2344 /// ICU4C maps to U+FFFD and characters that UTS 46 maps to the empty string normalize as in
2345 /// NFD in this normalization. In both cases, the previous UTS 46 processing before using
2346 /// normalization is expected to deal with these characters. Making the disallowed characters
2347 /// behave like this is beneficial to data size, and this normalizer implementation cannot
2348 /// deal with a character normalizing to the empty string, which doesn't happen in NFD or
2349 /// NFKD as of Unicode 14.
2350 ///
2351 /// Warning: In this normalization, U+0345 COMBINING GREEK YPOGEGRAMMENI exhibits a behavior
2352 /// that no character in Unicode exhibits in NFD, NFKD, NFC, or NFKC: Case folding turns
2353 /// U+0345 from a reordered character into a non-reordered character before reordering happens.
2354 /// Therefore, the output of this normalization may differ for different inputs that are
2355 /// canonically equivalent with each other if they differ by how U+0345 is ordered relative
2356 /// to other reorderable characters.
2357pub(crate) fn try_new_uts46_decomposed_unstable<D>(provider: &D) -> Result<Self, DataError>
2358where
2359D: DataProvider<NormalizerUts46DataV1>
2360 + DataProvider<NormalizerNfdTablesV1>
2361 + DataProvider<NormalizerNfkdTablesV1>
2362// UTS 46 tables merged into CompatibilityDecompositionTablesV1
2363+ ?Sized,
2364 {
2365let decompositions: DataPayload<NormalizerUts46DataV1> =
2366 provider.load(Default::default())?.payload;
2367let tables: DataPayload<NormalizerNfdTablesV1> = provider.load(Default::default())?.payload;
2368let supplementary_tables: DataPayload<NormalizerNfkdTablesV1> =
2369 provider.load(Default::default())?.payload;
23702371if tables.get().scalars16.len()
2372 + tables.get().scalars24.len()
2373 + supplementary_tables.get().scalars16.len()
2374 + supplementary_tables.get().scalars24.len()
2375 > 0xFFF
2376{
2377// The data is from a future where there exists a normalization flavor whose
2378 // complex decompositions take more than 0xFFF but fewer than 0x1FFF code points
2379 // of space. If a good use case from such a decomposition flavor arises, we can
2380 // dynamically change the bit masks so that the length mask becomes 0x1FFF instead
2381 // of 0xFFF and the all-non-starters mask becomes 0 instead of 0x1000. However,
2382 // since for now the masks are hard-coded, error out.
2383return Err(
2384DataError::custom("future extension").with_marker(NormalizerNfdTablesV1::INFO)
2385 );
2386 }
23872388let cap = decompositions.get().passthrough_cap;
2389if cap > 0x0300 {
2390return Err(DataError::custom("invalid").with_marker(NormalizerUts46DataV1::INFO));
2391 }
2392let decomposition_capped = cap.min(0xC0);
2393let composition_capped = cap.min(0x0300);
23942395Ok(DecomposingNormalizer {
2396 decompositions: decompositions.cast(),
2397tables,
2398 supplementary_tables: Some(supplementary_tables),
2399 decomposition_passthrough_bound: decomposition_cappedas u8,
2400 composition_passthrough_bound: composition_capped,
2401 })
2402 }
2403}
24042405/// Borrowed version of a normalizer for performing composing normalization.
2406#[derive(#[automatically_derived]
impl<'a> ::core::fmt::Debug for ComposingNormalizerBorrowed<'a> {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field2_finish(f,
"ComposingNormalizerBorrowed", "decomposing_normalizer",
&self.decomposing_normalizer, "canonical_compositions",
&&self.canonical_compositions)
}
}Debug)]
2407pub struct ComposingNormalizerBorrowed<'a> {
2408 decomposing_normalizer: DecomposingNormalizerBorrowed<'a>,
2409 canonical_compositions: &'a CanonicalCompositions<'a>,
2410}
24112412impl ComposingNormalizerBorrowed<'static> {
2413/// Cheaply converts a [`ComposingNormalizerBorrowed<'static>`] into a [`ComposingNormalizer`].
2414 ///
2415 /// Note: Due to branching and indirection, using [`ComposingNormalizer`] might inhibit some
2416 /// compile-time optimizations that are possible with [`ComposingNormalizerBorrowed`].
2417pub const fn static_to_owned(self) -> ComposingNormalizer {
2418ComposingNormalizer {
2419 decomposing_normalizer: self.decomposing_normalizer.static_to_owned(),
2420 canonical_compositions: DataPayload::from_static_ref(self.canonical_compositions),
2421 }
2422 }
24232424/// NFC constructor using compiled data.
2425 ///
2426 /// ✨ *Enabled with the `compiled_data` Cargo feature.*
2427 ///
2428 /// [📚 Help choosing a constructor](icu_provider::constructors)
2429#[cfg(feature = "compiled_data")]
2430pub const fn new_nfc() -> Self {
2431ComposingNormalizerBorrowed {
2432 decomposing_normalizer: DecomposingNormalizerBorrowed::new_nfd(),
2433 canonical_compositions: provider::Baked::SINGLETON_NORMALIZER_NFC_V1,
2434 }
2435 }
24362437/// NFKC constructor using compiled data.
2438 ///
2439 /// ✨ *Enabled with the `compiled_data` Cargo feature.*
2440 ///
2441 /// [📚 Help choosing a constructor](icu_provider::constructors)
2442#[cfg(feature = "compiled_data")]
2443pub const fn new_nfkc() -> Self {
2444ComposingNormalizerBorrowed {
2445 decomposing_normalizer: DecomposingNormalizerBorrowed::new_nfkd(),
2446 canonical_compositions: provider::Baked::SINGLETON_NORMALIZER_NFC_V1,
2447 }
2448 }
24492450/// This is a special building block normalization for IDNA that implements parts of the Map
2451 /// step and the following Normalize step.
2452 ///
2453 /// Warning: In this normalization, U+0345 COMBINING GREEK YPOGEGRAMMENI exhibits a behavior
2454 /// that no character in Unicode exhibits in NFD, NFKD, NFC, or NFKC: Case folding turns
2455 /// U+0345 from a reordered character into a non-reordered character before reordering happens.
2456 /// Therefore, the output of this normalization may differ for different inputs that are
2457 /// canonically equivalents with each other if they differ by how U+0345 is ordered relative
2458 /// to other reorderable characters.
2459#[cfg(feature = "compiled_data")]
2460pub(crate) const fn new_uts46() -> Self {
2461ComposingNormalizerBorrowed {
2462 decomposing_normalizer: DecomposingNormalizerBorrowed::new_uts46_decomposed(),
2463 canonical_compositions: provider::Baked::SINGLETON_NORMALIZER_NFC_V1,
2464 }
2465 }
2466}
24672468impl<'data> ComposingNormalizerBorrowed<'data> {
2469/// Wraps a delegate iterator into a composing iterator
2470 /// adapter by using the data already held by this normalizer.
2471pub fn normalize_iter<I: Iterator<Item = char>>(&self, iter: I) -> Composition<'data, I> {
2472self.normalize_iter_private(iter, IgnorableBehavior::Unsupported)
2473 }
24742475fn normalize_iter_private<I: Iterator<Item = char>>(
2476&self,
2477 iter: I,
2478 ignorable_behavior: IgnorableBehavior,
2479 ) -> Composition<'data, I> {
2480Composition::new(
2481Decomposition::new_with_supplements(
2482iter,
2483self.decomposing_normalizer.decompositions,
2484self.decomposing_normalizer.tables,
2485self.decomposing_normalizer.supplementary_tables,
2486self.decomposing_normalizer.decomposition_passthrough_bound,
2487ignorable_behavior,
2488 ),
2489self.canonical_compositions.canonical_compositions.clone(),
2490self.decomposing_normalizer.composition_passthrough_bound,
2491 )
2492 }
24932494/// Normalize a string slice into a `Cow<'a, str>`.
pub fn normalize<'a>(&self, text: &'a str) -> Cow<'a, str> {
let (head, tail) = self.split_normalized(text);
if tail.is_empty() { return Cow::Borrowed(head); }
let mut ret = String::new();
ret.reserve(text.len());
ret.push_str(head);
let _ = self.normalize_to(tail, &mut ret);
Cow::Owned(ret)
}
/// Split a string slice into maximum normalized prefix and unnormalized suffix
/// such that the concatenation of the prefix and the normalization of the suffix
/// is the normalization of the whole input.
pub fn split_normalized<'a>(&self, text: &'a str) -> (&'a str, &'a str) {
let up_to = self.is_normalized_up_to(text);
text.split_at_checked(up_to).unwrap_or_else(||
{
if true {
if !false {
::core::panicking::panic("assertion failed: false")
};
};
("", text)
})
}
/// Return the index a string slice is normalized up to.
fn is_normalized_up_to(&self, text: &str) -> usize {
let mut sink = IsNormalizedSinkStr::new(text);
let _ = self.normalize_to(text, &mut sink);
text.len() - sink.remaining_len()
}
/// Check whether a string slice is normalized.
pub fn is_normalized(&self, text: &str) -> bool {
self.is_normalized_up_to(text) == text.len()
}normalizer_methods!();
24952496#[doc = r" Normalize a string slice into a `Write` sink."]
pub fn normalize_to<W: core::fmt::Write +
?Sized>(&self, text: &str, sink: &mut W) -> core::fmt::Result {
{}
let mut composition = self.normalize_iter(text.chars());
if true {
{
match (&composition.decomposition.ignorable_behavior,
&IgnorableBehavior::Unsupported) {
(left_val, right_val) => {
if !(*left_val == *right_val) {
let kind = ::core::panicking::AssertKind::Eq;
::core::panicking::assert_failed(kind, &*left_val,
&*right_val, ::core::option::Option::None);
}
}
}
};
};
for cc in composition.decomposition.buffer.drain(..) {
sink.write_char(cc.character())?;
}
let composition_passthrough_bound =
composition.composition_passthrough_bound;
'outer: loop {
if true {
{
match (&composition.decomposition.buffer_pos, &0) {
(left_val, right_val) => {
if !(*left_val == *right_val) {
let kind = ::core::panicking::AssertKind::Eq;
::core::panicking::assert_failed(kind, &*left_val,
&*right_val, ::core::option::Option::None);
}
}
}
};
};
let mut undecomposed_starter =
if let Some(pending) = composition.decomposition.pending.take() {
pending
} else { return Ok(()); };
if u32::from(undecomposed_starter.character) <
composition_passthrough_bound ||
undecomposed_starter.potential_passthrough() {
if true ||
undecomposed_starter.character !=
char::REPLACEMENT_CHARACTER {
let pending_slice =
&text[text.len() -
composition.decomposition.delegate.as_str().len() -
undecomposed_starter.character.len_utf8()..];
{
let composition_passthrough_byte_bound =
if composition_passthrough_bound == 0x300 {
0xCCu8
} else { composition_passthrough_bound.min(0x80) as u8 };
#[expect(clippy::unwrap_used)]
'fast: loop {
let mut code_unit_iter =
composition.decomposition.delegate.as_str().as_bytes().iter();
'fastest: loop {
if let Some(&upcoming_byte) = code_unit_iter.next() {
if upcoming_byte < composition_passthrough_byte_bound {
continue 'fastest;
}
let Some(remaining_slice) =
pending_slice.get(pending_slice.len() -
code_unit_iter.as_slice().len() -
1..) else {
if true {
if !false {
::core::panicking::panic("assertion failed: false")
};
};
break 'fastest;
};
composition.decomposition.delegate =
remaining_slice.chars();
break 'fastest;
}
sink.write_str(pending_slice)?;
return Ok(());
}
let upcoming =
composition.decomposition.delegate.next().unwrap();
let upcoming_with_trie_value =
composition.decomposition.attach_trie_value(upcoming);
if upcoming_with_trie_value.potential_passthrough_and_cannot_combine_backwards()
{
continue 'fast;
}
composition.decomposition.pending =
Some(upcoming_with_trie_value);
let mut consumed_so_far =
pending_slice[..pending_slice.len() -
composition.decomposition.delegate.as_str().len() -
upcoming.len_utf8()].chars();
undecomposed_starter =
composition.decomposition.attach_trie_value(consumed_so_far.next_back().unwrap());
let consumed_so_far_slice = consumed_so_far.as_str();
sink.write_str(consumed_so_far_slice)?;
break 'fast;
}
}
}
}
let mut starter =
composition.decomposition.decomposing_next(undecomposed_starter);
'bufferloop: loop {
loop {
let (character, ccc) =
if let Some((character, ccc)) =
composition.decomposition.buffer.get(composition.decomposition.buffer_pos).map(|c|
c.character_and_ccc()) {
(character, ccc)
} else {
composition.decomposition.buffer.clear();
composition.decomposition.buffer_pos = 0;
break;
};
if let Some(composed) =
composition.compose(starter, character) {
starter = composed;
composition.decomposition.buffer_pos += 1;
continue;
}
let mut most_recent_skipped_ccc = ccc;
if most_recent_skipped_ccc ==
CanonicalCombiningClass::NotReordered {
sink.write_char(starter)?;
starter = character;
composition.decomposition.buffer_pos += 1;
continue 'bufferloop;
} else {
{
let _ =
composition.decomposition.buffer.drain(0..composition.decomposition.buffer_pos);
}
composition.decomposition.buffer_pos = 0;
}
let mut i = 1;
while let Some((character, ccc)) =
composition.decomposition.buffer.get(i).map(|c|
c.character_and_ccc()) {
if ccc == CanonicalCombiningClass::NotReordered {
sink.write_char(starter)?;
for cc in composition.decomposition.buffer.drain(..i) {
sink.write_char(cc.character())?;
}
starter = character;
{
let removed = composition.decomposition.buffer.remove(0);
if true {
{
match (&starter, &removed.character()) {
(left_val, right_val) => {
if !(*left_val == *right_val) {
let kind = ::core::panicking::AssertKind::Eq;
::core::panicking::assert_failed(kind, &*left_val,
&*right_val, ::core::option::Option::None);
}
}
}
};
};
}
if true {
{
match (&composition.decomposition.buffer_pos, &0) {
(left_val, right_val) => {
if !(*left_val == *right_val) {
let kind = ::core::panicking::AssertKind::Eq;
::core::panicking::assert_failed(kind, &*left_val,
&*right_val, ::core::option::Option::None);
}
}
}
};
};
continue 'bufferloop;
}
if true {
if !(ccc >= most_recent_skipped_ccc) {
::core::panicking::panic("assertion failed: ccc >= most_recent_skipped_ccc")
};
};
if ccc != most_recent_skipped_ccc {
if let Some(composed) =
composition.compose_non_hangul(starter, character) {
composition.decomposition.buffer.remove(i);
starter = composed;
continue;
}
}
most_recent_skipped_ccc = ccc;
i += 1;
}
break;
}
if true {
{
match (&composition.decomposition.buffer_pos, &0) {
(left_val, right_val) => {
if !(*left_val == *right_val) {
let kind = ::core::panicking::AssertKind::Eq;
::core::panicking::assert_failed(kind, &*left_val,
&*right_val, ::core::option::Option::None);
}
}
}
};
};
if !composition.decomposition.buffer.is_empty() {
sink.write_char(starter)?;
for cc in composition.decomposition.buffer.drain(..) {
sink.write_char(cc.character())?;
}
continue 'outer;
}
if composition.decomposition.pending.is_some() {
let pending =
composition.decomposition.pending.as_ref().unwrap();
if u32::from(pending.character) <
composition.composition_passthrough_bound ||
!pending.can_combine_backwards() {
sink.write_char(starter)?;
continue 'outer;
}
let pending_starter =
composition.decomposition.pending.take().unwrap();
let decomposed =
composition.decomposition.decomposing_next(pending_starter);
if let Some(composed) =
composition.compose(starter, decomposed) {
starter = composed;
} else { sink.write_char(starter)?; starter = decomposed; }
continue 'bufferloop;
}
sink.write_char(starter)?;
return Ok(());
}
}
}composing_normalize_to!(
2497/// Normalize a string slice into a `Write` sink.
2498,
2499 normalize_to,
2500core::fmt::Write,
2501&str,
2502 {},
2503true,
2504 as_str,
2505 {
2506// Let's hope LICM hoists this outside `'outer`.
2507let composition_passthrough_byte_bound = if composition_passthrough_bound == 0x300 {
25080xCCu8
2509} else {
2510// We can make this fancy if a normalization other than NFC where looking at
2511 // non-ASCII lead bytes is worthwhile is ever introduced.
2512composition_passthrough_bound.min(0x80) as u8
2513 };
2514// Attributes have to be on blocks, so hoisting all the way here.
2515#[expect(clippy::unwrap_used)]
2516'fast: loop {
2517let mut code_unit_iter = composition.decomposition.delegate.as_str().as_bytes().iter();
2518'fastest: loop {
2519if let Some(&upcoming_byte) = code_unit_iter.next() {
2520if upcoming_byte < composition_passthrough_byte_bound {
2521// Fast-track succeeded!
2522continue 'fastest;
2523 }
2524let Some(remaining_slice) = pending_slice.get(pending_slice.len() - code_unit_iter.as_slice().len() - 1..) else {
2525// If we ever come here, it's an internal bug. Let's avoid panic code paths in release builds.
2526debug_assert!(false);
2527// Throw away the fastest-path result in case of an internal bug.
2528break 'fastest;
2529 };
2530 composition.decomposition.delegate = remaining_slice.chars();
2531break 'fastest;
2532 }
2533// End of stream
2534sink.write_str(pending_slice)?;
2535return Ok(());
2536 }
2537// `unwrap()` OK, because the slice is valid UTF-8 and we know there
2538 // is an upcoming byte.
2539let upcoming = composition.decomposition.delegate.next().unwrap();
2540let upcoming_with_trie_value = composition.decomposition.attach_trie_value(upcoming);
2541if upcoming_with_trie_value.potential_passthrough_and_cannot_combine_backwards() {
2542// Can't combine backwards, hence a plain (non-backwards-combining)
2543 // starter albeit past `composition_passthrough_bound`
25442545 // Fast-track succeeded!
2546continue 'fast;
2547 }
2548// We need to fall off the fast path.
2549composition.decomposition.pending = Some(upcoming_with_trie_value);
25502551// slicing and unwrap OK, because we've just evidently read enough previously.
2552let mut consumed_so_far = pending_slice[..pending_slice.len() - composition.decomposition.delegate.as_str().len() - upcoming.len_utf8()].chars();
2553// `unwrap` OK, because we've previously manage to read the previous character
2554undecomposed_starter = composition.decomposition.attach_trie_value(consumed_so_far.next_back().unwrap());
2555let consumed_so_far_slice = consumed_so_far.as_str();
2556 sink.write_str(consumed_so_far_slice)?;
2557break 'fast;
2558 }
2559 },
2560 text,
2561 sink,
2562 composition,
2563 composition_passthrough_bound,
2564 undecomposed_starter,
2565 pending_slice,
2566 len_utf8,
2567 );
25682569composing_normalize_to!(
2570/// Normalize a slice of potentially-invalid UTF-8 into a `Write` sink.
2571 ///
2572 /// Ill-formed byte sequences are mapped to the REPLACEMENT CHARACTER
2573 /// according to the WHATWG Encoding Standard.
2574 ///
2575 /// ✨ *Enabled with the `utf8_iter` Cargo feature.*
2576#[cfg(feature = "utf8_iter")]
2577,
2578 normalize_utf8_to,
2579 core::fmt::Write,
2580&[u8],
2581 {},
2582false,
2583 as_slice,
2584 {
2585'fast: loop {
2586if let Some(upcoming) = composition.decomposition.delegate.next() {
2587if u32::from(upcoming) < composition_passthrough_bound {
2588// Fast-track succeeded!
2589continue 'fast;
2590 }
2591// TODO: Be statically aware of fast/small trie.
2592let upcoming_with_trie_value = composition.decomposition.attach_trie_value(upcoming);
2593if upcoming_with_trie_value.potential_passthrough_and_cannot_combine_backwards() {
2594// Note: The trie value of the REPLACEMENT CHARACTER is
2595 // intentionally formatted to fail the
2596 // `potential_passthrough_and_cannot_combine_backwards`
2597 // test even though it really is a starter that decomposes
2598 // to self and cannot combine backwards. This
2599 // Allows moving the branch on REPLACEMENT CHARACTER
2600 // below this `continue`.
2601continue 'fast;
2602 }
2603// We need to fall off the fast path.
26042605 // TODO(#2006): Annotate as unlikely
2606if upcoming == char::REPLACEMENT_CHARACTER {
2607// Can't tell if this is an error or a literal U+FFFD in
2608 // the input. Assuming the former to be sure.
26092610 // Since the U+FFFD might signify an error, we can't
2611 // assume `upcoming.len_utf8()` for the backoff length.
2612#[expect(clippy::indexing_slicing)]
2613let mut consumed_so_far = pending_slice[..pending_slice.len() - composition.decomposition.delegate.as_slice().len()].chars();
2614let back = consumed_so_far.next_back();
2615debug_assert_eq!(back, Some(char::REPLACEMENT_CHARACTER));
2616let consumed_so_far_slice = consumed_so_far.as_slice();
2617 sink.write_str(unsafe { str::from_utf8_unchecked(consumed_so_far_slice) })?;
2618 undecomposed_starter = CharacterAndTrieValue::new(char::REPLACEMENT_CHARACTER, 0);
2619 composition.decomposition.pending = None;
2620break 'fast;
2621 }
26222623 composition.decomposition.pending = Some(upcoming_with_trie_value);
2624// slicing and unwrap OK, because we've just evidently read enough previously.
2625 // `unwrap` OK, because we've previously manage to read the previous character
2626#[expect(clippy::indexing_slicing)]
2627let mut consumed_so_far = pending_slice[..pending_slice.len() - composition.decomposition.delegate.as_slice().len() - upcoming.len_utf8()].chars();
2628#[expect(clippy::unwrap_used)]
2629{
2630// TODO: If the previous character was below the passthrough bound,
2631 // we really need to read from the trie. Otherwise, we could maintain
2632 // the most-recent trie value. Need to measure what's more expensive:
2633 // Remembering the trie value on each iteration or re-reading the
2634 // last one after the fast-track run.
2635undecomposed_starter = composition.decomposition.attach_trie_value(consumed_so_far.next_back().unwrap());
2636 }
2637let consumed_so_far_slice = consumed_so_far.as_slice();
2638 sink.write_str(unsafe { str::from_utf8_unchecked(consumed_so_far_slice)})?;
2639break 'fast;
2640 }
2641// End of stream
2642sink.write_str(unsafe { str::from_utf8_unchecked(pending_slice) })?;
2643return Ok(());
2644 }
2645 },
2646 text,
2647 sink,
2648 composition,
2649 composition_passthrough_bound,
2650 undecomposed_starter,
2651 pending_slice,
2652 len_utf8,
2653 );
26542655composing_normalize_to!(
2656/// Normalize a slice of potentially-invalid UTF-16 into a `Write16` sink.
2657 ///
2658 /// Unpaired surrogates are mapped to the REPLACEMENT CHARACTER
2659 /// before normalizing.
2660 ///
2661 /// ✨ *Enabled with the `utf16_iter` Cargo feature.*
2662#[cfg(feature = "utf16_iter")]
2663,
2664 normalize_utf16_to,
2665 write16::Write16,
2666&[u16],
2667 {
2668 sink.size_hint(text.len())?;
2669 },
2670false,
2671 as_slice,
2672 {
2673// This loop is only broken out of as goto forward and only as release-build recovery from
2674 // detecting an internal bug without panic. (In debug builds, internal bugs panic instead.)
2675#[expect(clippy::never_loop)]
2676'fastwrap: loop {
2677// Commented out `code_unit_iter` and used `ptr` and `end` to
2678 // work around https://github.com/rust-lang/rust/issues/144684 .
2679 //
2680 // let mut code_unit_iter = composition.decomposition.delegate.as_slice().iter();
2681let delegate_as_slice = composition.decomposition.delegate.as_slice();
2682let mut ptr: *const u16 = delegate_as_slice.as_ptr();
2683// SAFETY: materializing a pointer immediately past the end of an
2684 // allocation is OK.
2685let end: *const u16 = unsafe { ptr.add(delegate_as_slice.len()) };
26862687'fast: loop {
2688// if let Some(&upcoming_code_unit) = code_unit_iter.next() {
2689if ptr != end {
2690// SAFETY: We just checked that `ptr` has not reached `end`.
2691 // `ptr` always advances by one, and we always have a check
2692 // per advancement.
2693let upcoming_code_unit = unsafe { *ptr };
2694// SAFETY: Since `ptr` hadn't reached `end`, yet, advancing
2695 // by one points to the same allocation or to immediately
2696 // after, which is OK.
2697ptr = unsafe { ptr.add(1) };
26982699let mut upcoming32 = u32::from(upcoming_code_unit); // may be surrogate
2700 // The performance of what logically is supposed to be this
2701 // branch is somewhat brittle and what LLVM ends up doing
2702 // that affects the performance of what's logically about this
2703 // decision can swing to double/halve the throughput for Basic
2704 // Latin in ways that are completely unintuitive. Basically _any_
2705 // change to _any_ code that participates in how LLVM sees the
2706 // code around here can make the perf fall over. In seems that
2707 // manually annotating this branch as likely has worse effects
2708 // on non-Basic-Latin input that the case where LLVM just happens to
2709 // do the right thing.
2710 //
2711 // What happens with this branch may depend on what sink type
2712 // this code is monomorphized over.
2713 //
2714 // What a terrible sink of developer time!
2715if upcoming32 < composition_passthrough_bound {
2716// No need for surrogate or U+FFFD check, because
2717 // `composition_passthrough_bound` cannot be higher than
2718 // U+0300.
2719 // Fast-track succeeded!
2720continue 'fast;
2721 }
2722// We might be doing a trie lookup by surrogate. Surrogates get
2723 // a decomposition to U+FFFD.
2724let mut trie_value = composition.decomposition.trie.get16(upcoming_code_unit);
2725if potential_passthrough_and_cannot_combine_backwards_impl(trie_value) {
2726// Can't combine backwards, hence a plain (non-backwards-combining)
2727 // starter albeit past `composition_passthrough_bound`
27282729 // Fast-track succeeded!
2730continue 'fast;
2731 }
27322733// We might now be looking at a surrogate.
2734 // The loop is only broken out of as goto forward
2735#[expect(clippy::never_loop)]
2736'surrogateloop: loop {
2737// The `likely` annotations _below_ exist to make the code _above_
2738 // go faster!
2739let surrogate_base = upcoming32.wrapping_sub(0xD800);
2740if likely(surrogate_base > (0xDFFF - 0xD800)) {
2741// Not surrogate
2742break 'surrogateloop;
2743 }
2744if likely(surrogate_base <= (0xDBFF - 0xD800)) {
2745// let iter_backup = code_unit_iter.clone();
2746 // if let Some(&low) = code_unit_iter.next() {
2747if ptr != end {
2748// SAFETY: We just checked that `ptr` has not reached `end`.
2749 // `ptr` always advances by one, and we always have a check
2750 // per advancement.
2751let low = unsafe { *ptr };
2752if likely(in_inclusive_range16(low, 0xDC00, 0xDFFF)) {
2753// SAFETY: Since `ptr` hadn't reached `end`, yet, advancing
2754 // by one points to the same allocation or to immediately
2755 // after, which is OK.
2756ptr = unsafe { ptr.add(1) };
27572758 upcoming32 = (upcoming32 << 10) + u32::from(low)
2759 - (((0xD800u32 << 10) - 0x10000u32) + 0xDC00u32);
2760// Successfully-paired surrogate. Read from the trie again.
2761trie_value = {
2762// Semantically, this bit of conditional compilation makes no sense.
2763 // The purpose is to keep LLVM seeing the untyped trie case the way
2764 // it did before so as not to regress the performance of the untyped
2765 // case due to unintuitive optimizer effects. If you care about the
2766 // perf of the untyped trie case and have better ideas, please try
2767 // something better.
2768#[cfg(not(icu4x_unstable_fast_trie_only))]
2769{composition.decomposition.trie.get32(upcoming32)}
2770#[cfg(icu4x_unstable_fast_trie_only)]
2771{composition.decomposition.trie.get32_supplementary(upcoming32)}
2772 };
2773if likely(potential_passthrough_and_cannot_combine_backwards_impl(trie_value)) {
2774// Fast-track succeeded!
2775continue 'fast;
2776 }
2777break 'surrogateloop;
2778// } else {
2779 // code_unit_iter = iter_backup;
2780}
2781 }
2782 }
2783// unpaired surrogate
2784upcoming32 = 0xFFFD; // Safe value for `char::from_u32_unchecked` and matches later potential error check.
2785 // trie_value already holds a decomposition to U+FFFD.
2786debug_assert_eq!(trie_value, NON_ROUND_TRIP_MARKER | BACKWARD_COMBINING_MARKER | 0xFFFD);
2787break 'surrogateloop;
2788 }
27892790// SAFETY: upcoming32 can no longer be a surrogate.
2791let upcoming = unsafe { char::from_u32_unchecked(upcoming32) };
2792let upcoming_with_trie_value = CharacterAndTrieValue::new(upcoming, trie_value);
2793// We need to fall off the fast path.
2794composition.decomposition.pending = Some(upcoming_with_trie_value);
2795let Some(consumed_so_far_slice) = pending_slice.get(..pending_slice.len() -
2796// code_unit_iter.as_slice().len()
2797 // SAFETY: `ptr` and `end` have been derived from the same allocation
2798 // and `ptr` is never greater than `end`.
2799unsafe { end.offset_from(ptr) as usize }
2800 - upcoming.len_utf16()) else {
2801// If we ever come here, it's a bug, but let's avoid panic code paths in release builds.
2802debug_assert!(false);
2803// Throw away the results of the fast path.
2804break 'fastwrap;
2805 };
2806let mut consumed_so_far = consumed_so_far_slice.chars();
2807let Some(c_from_back) = consumed_so_far.next_back() else {
2808// If we ever come here, it's a bug, but let's avoid panic code paths in release builds.
2809debug_assert!(false);
2810// Throw away the results of the fast path.
2811break 'fastwrap;
2812 };
2813// TODO: If the previous character was below the passthrough bound,
2814 // we really need to read from the trie. Otherwise, we could maintain
2815 // the most-recent trie value. Need to measure what's more expensive:
2816 // Remembering the trie value on each iteration or re-reading the
2817 // last one after the fast-track run.
2818undecomposed_starter = composition.decomposition.attach_trie_value(c_from_back);
2819 sink.write_slice(consumed_so_far.as_slice())?;
2820break 'fast;
2821 }
2822// End of stream
2823sink.write_slice(pending_slice)?;
2824return Ok(());
2825 }
2826// Sync the main iterator
2827 // composition.decomposition.delegate = code_unit_iter.as_slice().chars();
2828 // SAFETY: `ptr` and `end` have been derive from the same allocation
2829 // and `ptr` is never greater than `end`.
2830composition.decomposition.delegate = unsafe { core::slice::from_raw_parts(ptr, end.offset_from(ptr) as usize) }.chars();
2831break 'fastwrap;
2832 }
2833 },
2834 text,
2835 sink,
2836 composition,
2837 composition_passthrough_bound,
2838 undecomposed_starter,
2839 pending_slice,
2840 len_utf16,
2841 );
2842}
28432844/// A normalizer for performing composing normalization.
2845#[derive(#[automatically_derived]
impl ::core::fmt::Debug for ComposingNormalizer {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field2_finish(f,
"ComposingNormalizer", "decomposing_normalizer",
&self.decomposing_normalizer, "canonical_compositions",
&&self.canonical_compositions)
}
}Debug)]
2846pub struct ComposingNormalizer {
2847 decomposing_normalizer: DecomposingNormalizer,
2848 canonical_compositions: DataPayload<NormalizerNfcV1>,
2849}
28502851impl ComposingNormalizer {
2852/// Constructs a borrowed version of this type for more efficient querying.
2853pub fn as_borrowed(&self) -> ComposingNormalizerBorrowed<'_> {
2854ComposingNormalizerBorrowed {
2855 decomposing_normalizer: self.decomposing_normalizer.as_borrowed(),
2856 canonical_compositions: self.canonical_compositions.get(),
2857 }
2858 }
28592860/// NFC constructor using compiled data.
2861 ///
2862 /// ✨ *Enabled with the `compiled_data` Cargo feature.*
2863 ///
2864 /// [📚 Help choosing a constructor](icu_provider::constructors)
2865#[cfg(feature = "compiled_data")]
2866pub const fn new_nfc() -> ComposingNormalizerBorrowed<'static> {
2867ComposingNormalizerBorrowed::new_nfc()
2868 }
28692870icu_provider::gen_buffer_data_constructors!(
2871 () -> error: DataError,
2872 functions: [
2873 new_nfc: skip,
2874 try_new_nfc_with_buffer_provider,
2875 try_new_nfc_unstable,
2876Self,
2877 ]
2878 );
28792880#[doc = "A version of [`Self::new_nfc`] that uses custom data provided by a [`DataProvider`].\n\n[\u{1f4da} Help choosing a constructor](icu_provider::constructors)\n\n<div class=\"stab unstable\">\u{26a0}\u{fe0f} The bounds on <tt>provider</tt> may change over time, including in SemVer minor releases.</div>"icu_provider::gen_buffer_unstable_docs!(UNSTABLE, Self::new_nfc)]
2881pub fn try_new_nfc_unstable<D>(provider: &D) -> Result<Self, DataError>
2882where
2883D: DataProvider<NormalizerNfdDataV1>
2884 + DataProvider<NormalizerNfdTablesV1>
2885 + DataProvider<NormalizerNfcV1>
2886 + ?Sized,
2887 {
2888let decomposing_normalizer = DecomposingNormalizer::try_new_nfd_unstable(provider)?;
28892890let canonical_compositions: DataPayload<NormalizerNfcV1> =
2891 provider.load(Default::default())?.payload;
28922893Ok(ComposingNormalizer {
2894decomposing_normalizer,
2895canonical_compositions,
2896 })
2897 }
28982899/// NFKC constructor using compiled data.
2900 ///
2901 /// ✨ *Enabled with the `compiled_data` Cargo feature.*
2902 ///
2903 /// [📚 Help choosing a constructor](icu_provider::constructors)
2904#[cfg(feature = "compiled_data")]
2905pub const fn new_nfkc() -> ComposingNormalizerBorrowed<'static> {
2906ComposingNormalizerBorrowed::new_nfkc()
2907 }
29082909icu_provider::gen_buffer_data_constructors!(
2910 () -> error: DataError,
2911 functions: [
2912 new_nfkc: skip,
2913 try_new_nfkc_with_buffer_provider,
2914 try_new_nfkc_unstable,
2915Self,
2916 ]
2917 );
29182919#[doc = "A version of [`Self::new_nfkc`] that uses custom data provided by a [`DataProvider`].\n\n[\u{1f4da} Help choosing a constructor](icu_provider::constructors)\n\n<div class=\"stab unstable\">\u{26a0}\u{fe0f} The bounds on <tt>provider</tt> may change over time, including in SemVer minor releases.</div>"icu_provider::gen_buffer_unstable_docs!(UNSTABLE, Self::new_nfkc)]
2920pub fn try_new_nfkc_unstable<D>(provider: &D) -> Result<Self, DataError>
2921where
2922D: DataProvider<NormalizerNfkdDataV1>
2923 + DataProvider<NormalizerNfdTablesV1>
2924 + DataProvider<NormalizerNfkdTablesV1>
2925 + DataProvider<NormalizerNfcV1>
2926 + ?Sized,
2927 {
2928let decomposing_normalizer = DecomposingNormalizer::try_new_nfkd_unstable(provider)?;
29292930let canonical_compositions: DataPayload<NormalizerNfcV1> =
2931 provider.load(Default::default())?.payload;
29322933Ok(ComposingNormalizer {
2934decomposing_normalizer,
2935canonical_compositions,
2936 })
2937 }
29382939#[doc = "A version of [`Self::new_uts46`] that uses custom data provided by a [`DataProvider`].\n\n[\u{1f4da} Help choosing a constructor](icu_provider::constructors)\n\n<div class=\"stab unstable\">\u{26a0}\u{fe0f} The bounds on <tt>provider</tt> may change over time, including in SemVer minor releases.</div>"icu_provider::gen_buffer_unstable_docs!(UNSTABLE, Self::new_uts46)]
2940pub(crate) fn try_new_uts46_unstable<D>(provider: &D) -> Result<Self, DataError>
2941where
2942D: DataProvider<NormalizerUts46DataV1>
2943 + DataProvider<NormalizerNfdTablesV1>
2944 + DataProvider<NormalizerNfkdTablesV1>
2945// UTS 46 tables merged into CompatibilityDecompositionTablesV1
2946+ DataProvider<NormalizerNfcV1>
2947 + ?Sized,
2948 {
2949let decomposing_normalizer =
2950 DecomposingNormalizer::try_new_uts46_decomposed_unstable(provider)?;
29512952let canonical_compositions: DataPayload<NormalizerNfcV1> =
2953 provider.load(Default::default())?.payload;
29542955Ok(ComposingNormalizer {
2956decomposing_normalizer,
2957canonical_compositions,
2958 })
2959 }
2960}
29612962#[cfg(feature = "utf16_iter")]
2963struct IsNormalizedSinkUtf16<'a> {
2964 expect: &'a [u16],
2965}
29662967#[cfg(feature = "utf16_iter")]
2968impl<'a> IsNormalizedSinkUtf16<'a> {
2969pub fn new(slice: &'a [u16]) -> Self {
2970 IsNormalizedSinkUtf16 { expect: slice }
2971 }
2972pub fn remaining_len(&self) -> usize {
2973self.expect.len()
2974 }
2975}
29762977#[cfg(feature = "utf16_iter")]
2978impl write16::Write16 for IsNormalizedSinkUtf16<'_> {
2979fn write_slice(&mut self, s: &[u16]) -> core::fmt::Result {
2980// We know that if we get a slice, it's a pass-through,
2981 // so we can compare addresses. Indexing is OK, because
2982 // an indexing failure would be a code bug rather than
2983 // an input or data issue.
2984#[expect(clippy::indexing_slicing)]
2985if core::ptr::eq(s.as_ptr(), self.expect.as_ptr()) {
2986self.expect = &self.expect[s.len()..];
2987Ok(())
2988 } else {
2989Err(core::fmt::Error {})
2990 }
2991 }
29922993fn write_char(&mut self, c: char) -> core::fmt::Result {
2994let mut iter = self.expect.chars();
2995if iter.next() == Some(c) {
2996self.expect = iter.as_slice();
2997Ok(())
2998 } else {
2999Err(core::fmt::Error {})
3000 }
3001 }
3002}
30033004#[cfg(feature = "utf8_iter")]
3005struct IsNormalizedSinkUtf8<'a> {
3006 expect: &'a [u8],
3007}
30083009#[cfg(feature = "utf8_iter")]
3010impl<'a> IsNormalizedSinkUtf8<'a> {
3011pub fn new(slice: &'a [u8]) -> Self {
3012 IsNormalizedSinkUtf8 { expect: slice }
3013 }
3014pub fn remaining_len(&self) -> usize {
3015self.expect.len()
3016 }
3017}
30183019#[cfg(feature = "utf8_iter")]
3020impl core::fmt::Write for IsNormalizedSinkUtf8<'_> {
3021fn write_str(&mut self, s: &str) -> core::fmt::Result {
3022// We know that if we get a slice, it's a pass-through,
3023 // so we can compare addresses. Indexing is OK, because
3024 // an indexing failure would be a code bug rather than
3025 // an input or data issue.
3026#[expect(clippy::indexing_slicing)]
3027if core::ptr::eq(s.as_ptr(), self.expect.as_ptr()) {
3028self.expect = &self.expect[s.len()..];
3029Ok(())
3030 } else {
3031Err(core::fmt::Error {})
3032 }
3033 }
30343035fn write_char(&mut self, c: char) -> core::fmt::Result {
3036let mut iter = self.expect.chars();
3037if iter.next() == Some(c) {
3038self.expect = iter.as_slice();
3039Ok(())
3040 } else {
3041Err(core::fmt::Error {})
3042 }
3043 }
3044}
30453046struct IsNormalizedSinkStr<'a> {
3047 expect: &'a str,
3048}
30493050impl<'a> IsNormalizedSinkStr<'a> {
3051pub fn new(slice: &'a str) -> Self {
3052IsNormalizedSinkStr { expect: slice }
3053 }
3054pub fn remaining_len(&self) -> usize {
3055self.expect.len()
3056 }
3057}
30583059impl core::fmt::Writefor IsNormalizedSinkStr<'_> {
3060fn write_str(&mut self, s: &str) -> core::fmt::Result {
3061// We know that if we get a slice, it's a pass-through,
3062 // so we can compare addresses. Indexing is OK, because
3063 // an indexing failure would be a code bug rather than
3064 // an input or data issue.
3065if core::ptr::eq(s.as_ptr(), self.expect.as_ptr()) {
3066self.expect = &self.expect[s.len()..];
3067Ok(())
3068 } else {
3069Err(core::fmt::Error {})
3070 }
3071 }
30723073fn write_char(&mut self, c: char) -> core::fmt::Result {
3074let mut iter = self.expect.chars();
3075if iter.next() == Some(c) {
3076self.expect = iter.as_str();
3077Ok(())
3078 } else {
3079Err(core::fmt::Error {})
3080 }
3081 }
3082}