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//! ```
33mod fields;
34mod key;
35mod value;
36
37pub use fields::Fields;
38#[doc(inline)]
39pub use key::{key, Key};
40pub use value::Value;
41
42use crate::helpers::ShortSlice;
43use crate::parser::SubtagIterator;
44use crate::parser::{parse_language_identifier_from_iter, ParserError, ParserMode};
45use crate::subtags::Language;
46use crate::LanguageIdentifier;
47use 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
77pub 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
85impl 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
205writeable::impl_display_with_writeable!(Transform);
206
207impl 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