1 | // This file is part of ICU4X. For terms of use, please see the file |
2 | // called LICENSE at the top level of the ICU4X source tree |
3 | // (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). |
4 | |
5 | //! Transform Extensions provide information on content transformations in a given locale. |
6 | //! |
7 | //! The main struct for this extension is [`Transform`] which contains [`Fields`] and an |
8 | //! optional [`LanguageIdentifier`]. |
9 | //! |
10 | //! [`LanguageIdentifier`]: super::super::LanguageIdentifier |
11 | //! |
12 | //! # Examples |
13 | //! |
14 | //! ``` |
15 | //! use icu::locid::extensions::transform::{Fields, Key, Transform, Value}; |
16 | //! use icu::locid::{LanguageIdentifier, Locale}; |
17 | //! |
18 | //! let mut loc: Locale = |
19 | //! "en-US-t-es-AR-h0-hybrid" .parse().expect("Parsing failed." ); |
20 | //! |
21 | //! let lang: LanguageIdentifier = |
22 | //! "es-AR" .parse().expect("Parsing LanguageIdentifier failed." ); |
23 | //! |
24 | //! let key: Key = "h0" .parse().expect("Parsing key failed." ); |
25 | //! let value: Value = "hybrid" .parse().expect("Parsing value failed." ); |
26 | //! |
27 | //! assert_eq!(loc.extensions.transform.lang, Some(lang)); |
28 | //! assert!(loc.extensions.transform.fields.contains_key(&key)); |
29 | //! assert_eq!(loc.extensions.transform.fields.get(&key), Some(&value)); |
30 | //! |
31 | //! assert_eq!(&loc.extensions.transform.to_string(), "t-es-AR-h0-hybrid" ); |
32 | //! ``` |
33 | mod fields; |
34 | mod key; |
35 | mod value; |
36 | |
37 | pub use fields::Fields; |
38 | #[doc (inline)] |
39 | pub use key::{key, Key}; |
40 | pub use value::Value; |
41 | |
42 | use crate::helpers::ShortSlice; |
43 | use crate::parser::SubtagIterator; |
44 | use crate::parser::{parse_language_identifier_from_iter, ParserError, ParserMode}; |
45 | use crate::subtags::Language; |
46 | use crate::LanguageIdentifier; |
47 | use litemap::LiteMap; |
48 | |
49 | /// A list of [`Unicode BCP47 T Extensions`] as defined in [`Unicode Locale |
50 | /// Identifier`] specification. |
51 | /// |
52 | /// Transform extension carries information about source language or script of |
53 | /// transformed content, including content that has been transliterated, transcribed, |
54 | /// or translated, or in some other way influenced by the source (See [`RFC 6497`] for details). |
55 | /// |
56 | /// # Examples |
57 | /// |
58 | /// ``` |
59 | /// use icu::locid::extensions::transform::{Key, Value}; |
60 | /// use icu::locid::{LanguageIdentifier, Locale}; |
61 | /// |
62 | /// let mut loc: Locale = |
63 | /// "de-t-en-US-h0-hybrid" .parse().expect("Parsing failed." ); |
64 | /// |
65 | /// let en_us: LanguageIdentifier = "en-US" .parse().expect("Parsing failed." ); |
66 | /// |
67 | /// assert_eq!(loc.extensions.transform.lang, Some(en_us)); |
68 | /// let key: Key = "h0" .parse().expect("Parsing key failed." ); |
69 | /// let value: Value = "hybrid" .parse().expect("Parsing value failed." ); |
70 | /// assert_eq!(loc.extensions.transform.fields.get(&key), Some(&value)); |
71 | /// ``` |
72 | /// [`Unicode BCP47 T Extensions`]: https://unicode.org/reports/tr35/#t_Extension |
73 | /// [`RFC 6497`]: https://www.ietf.org/rfc/rfc6497.txt |
74 | /// [`Unicode Locale Identifier`]: https://unicode.org/reports/tr35/#Unicode_locale_identifier |
75 | #[derive (Clone, PartialEq, Eq, Debug, Default, Hash)] |
76 | #[allow (clippy::exhaustive_structs)] // spec-backed stable datastructure |
77 | pub struct Transform { |
78 | /// The [`LanguageIdentifier`] specified with this locale extension, or `None` if not present. |
79 | pub lang: Option<LanguageIdentifier>, |
80 | /// The key-value pairs present in this locale extension, with each extension key subtag |
81 | /// associated to its provided value subtag. |
82 | pub fields: Fields, |
83 | } |
84 | |
85 | impl Transform { |
86 | /// Returns a new empty map of Transform extensions. Same as [`default()`](Default::default()), but is `const`. |
87 | /// |
88 | /// # Examples |
89 | /// |
90 | /// ``` |
91 | /// use icu::locid::extensions::transform::Transform; |
92 | /// |
93 | /// assert_eq!(Transform::new(), Transform::default()); |
94 | /// ``` |
95 | #[inline ] |
96 | pub const fn new() -> Self { |
97 | Self { |
98 | lang: None, |
99 | fields: Fields::new(), |
100 | } |
101 | } |
102 | |
103 | /// Returns `true` if there are no tfields and no tlang in the `TransformExtensionList`. |
104 | /// |
105 | /// # Examples |
106 | /// |
107 | /// ``` |
108 | /// use icu::locid::Locale; |
109 | /// |
110 | /// let mut loc: Locale = "en-US-t-es-AR" .parse().expect("Parsing failed." ); |
111 | /// |
112 | /// assert!(!loc.extensions.transform.is_empty()); |
113 | /// ``` |
114 | pub fn is_empty(&self) -> bool { |
115 | self.lang.is_none() && self.fields.is_empty() |
116 | } |
117 | |
118 | /// Clears the transform extension, effectively removing it from the locale. |
119 | /// |
120 | /// # Examples |
121 | /// |
122 | /// ``` |
123 | /// use icu::locid::Locale; |
124 | /// |
125 | /// let mut loc: Locale = "en-US-t-es-AR" .parse().unwrap(); |
126 | /// loc.extensions.transform.clear(); |
127 | /// assert_eq!(loc, "en-US" .parse().unwrap()); |
128 | /// ``` |
129 | pub fn clear(&mut self) { |
130 | self.lang = None; |
131 | self.fields.clear(); |
132 | } |
133 | |
134 | pub(crate) fn try_from_iter(iter: &mut SubtagIterator) -> Result<Self, ParserError> { |
135 | let mut tlang = None; |
136 | let mut tfields = LiteMap::new(); |
137 | |
138 | if let Some(subtag) = iter.peek() { |
139 | if Language::try_from_bytes(subtag).is_ok() { |
140 | tlang = Some(parse_language_identifier_from_iter( |
141 | iter, |
142 | ParserMode::Partial, |
143 | )?); |
144 | } |
145 | } |
146 | |
147 | let mut current_tkey = None; |
148 | let mut current_tvalue = ShortSlice::new(); |
149 | let mut has_current_tvalue = false; |
150 | |
151 | while let Some(subtag) = iter.peek() { |
152 | if let Some(tkey) = current_tkey { |
153 | if let Ok(val) = Value::parse_subtag(subtag) { |
154 | has_current_tvalue = true; |
155 | if let Some(val) = val { |
156 | current_tvalue.push(val); |
157 | } |
158 | } else { |
159 | if !has_current_tvalue { |
160 | return Err(ParserError::InvalidExtension); |
161 | } |
162 | tfields.try_insert(tkey, Value::from_short_slice_unchecked(current_tvalue)); |
163 | current_tkey = None; |
164 | current_tvalue = ShortSlice::new(); |
165 | has_current_tvalue = false; |
166 | continue; |
167 | } |
168 | } else if let Ok(tkey) = Key::try_from_bytes(subtag) { |
169 | current_tkey = Some(tkey); |
170 | } else { |
171 | break; |
172 | } |
173 | |
174 | iter.next(); |
175 | } |
176 | |
177 | if let Some(tkey) = current_tkey { |
178 | if !has_current_tvalue { |
179 | return Err(ParserError::InvalidExtension); |
180 | } |
181 | tfields.try_insert(tkey, Value::from_short_slice_unchecked(current_tvalue)); |
182 | } |
183 | |
184 | Ok(Self { |
185 | lang: tlang, |
186 | fields: tfields.into(), |
187 | }) |
188 | } |
189 | |
190 | pub(crate) fn for_each_subtag_str<E, F>(&self, f: &mut F) -> Result<(), E> |
191 | where |
192 | F: FnMut(&str) -> Result<(), E>, |
193 | { |
194 | if self.is_empty() { |
195 | return Ok(()); |
196 | } |
197 | f("t" )?; |
198 | if let Some(lang) = &self.lang { |
199 | lang.for_each_subtag_str(f)?; |
200 | } |
201 | self.fields.for_each_subtag_str(f) |
202 | } |
203 | } |
204 | |
205 | writeable::impl_display_with_writeable!(Transform); |
206 | |
207 | impl writeable::Writeable for Transform { |
208 | fn write_to<W: core::fmt::Write + ?Sized>(&self, sink: &mut W) -> core::fmt::Result { |
209 | if self.is_empty() { |
210 | return Ok(()); |
211 | } |
212 | sink.write_str("t" )?; |
213 | if let Some(lang) = &self.lang { |
214 | sink.write_char('-' )?; |
215 | writeable::Writeable::write_to(lang, sink)?; |
216 | } |
217 | if !self.fields.is_empty() { |
218 | sink.write_char('-' )?; |
219 | writeable::Writeable::write_to(&self.fields, sink)?; |
220 | } |
221 | Ok(()) |
222 | } |
223 | |
224 | fn writeable_length_hint(&self) -> writeable::LengthHint { |
225 | if self.is_empty() { |
226 | return writeable::LengthHint::exact(0); |
227 | } |
228 | let mut result = writeable::LengthHint::exact(1); |
229 | if let Some(lang) = &self.lang { |
230 | result += writeable::Writeable::writeable_length_hint(lang) + 1; |
231 | } |
232 | if !self.fields.is_empty() { |
233 | result += writeable::Writeable::writeable_length_hint(&self.fields) + 1; |
234 | } |
235 | result |
236 | } |
237 | } |
238 | |