pub mod other;
pub mod private;
pub mod transform;
pub mod unicode;
use core::cmp::Ordering;
use other::Other;
use private::{Private, PRIVATE_EXT_CHAR};
use transform::{Transform, TRANSFORM_EXT_CHAR};
use unicode::{Unicode, UNICODE_EXT_CHAR};
#[cfg(feature = "alloc")]
use alloc::vec::Vec;
use crate::parser::ParseError;
#[cfg(feature = "alloc")]
use crate::parser::SubtagIterator;
use crate::subtags;
#[derive(Debug, PartialEq, Eq, Clone, Hash, PartialOrd, Ord, Copy)]
#[non_exhaustive]
pub enum ExtensionType {
Transform,
Unicode,
Private,
Other(u8),
}
impl ExtensionType {
#[allow(dead_code)]
pub(crate) const fn try_from_byte_slice(key: &[u8]) -> Result<Self, ParseError> {
if let [b] = key {
Self::try_from_byte(*b)
} else {
Err(ParseError::InvalidExtension)
}
}
pub(crate) const fn try_from_byte(key: u8) -> Result<Self, ParseError> {
let key = key.to_ascii_lowercase();
match key as char {
UNICODE_EXT_CHAR => Ok(Self::Unicode),
TRANSFORM_EXT_CHAR => Ok(Self::Transform),
PRIVATE_EXT_CHAR => Ok(Self::Private),
'a'..='z' => Ok(Self::Other(key)),
_ => Err(ParseError::InvalidExtension),
}
}
pub(crate) const fn try_from_utf8(code_units: &[u8]) -> Result<Self, ParseError> {
let &[first] = code_units else {
return Err(ParseError::InvalidExtension);
};
Self::try_from_byte(first)
}
}
#[derive(Debug, Default, PartialEq, Eq, Clone, Hash)]
#[non_exhaustive]
pub struct Extensions {
pub unicode: Unicode,
pub transform: Transform,
pub private: Private,
#[cfg(feature = "alloc")]
pub other: Vec<Other>,
#[cfg(not(feature = "alloc"))]
pub other: &'static [Other],
}
impl Extensions {
#[inline]
pub const fn new() -> Self {
Self {
unicode: Unicode::new(),
transform: Transform::new(),
private: Private::new(),
#[cfg(feature = "alloc")]
other: Vec::new(),
#[cfg(not(feature = "alloc"))]
other: &[],
}
}
#[inline]
pub const fn from_unicode(unicode: Unicode) -> Self {
Self {
unicode,
transform: Transform::new(),
private: Private::new(),
#[cfg(feature = "alloc")]
other: Vec::new(),
#[cfg(not(feature = "alloc"))]
other: &[],
}
}
pub fn is_empty(&self) -> bool {
self.unicode.is_empty()
&& self.transform.is_empty()
&& self.private.is_empty()
&& self.other.is_empty()
}
#[expect(clippy::type_complexity)]
#[cfg_attr(not(feature = "alloc"), expect(clippy::needless_borrow))]
pub(crate) fn as_tuple(
&self,
) -> (
(&unicode::Attributes, &unicode::Keywords),
(
Option<(
subtags::Language,
Option<subtags::Script>,
Option<subtags::Region>,
&subtags::Variants,
)>,
&transform::Fields,
),
&Private,
&[Other],
) {
(
self.unicode.as_tuple(),
self.transform.as_tuple(),
&self.private,
&self.other,
)
}
pub fn total_cmp(&self, other: &Self) -> Ordering {
self.as_tuple().cmp(&other.as_tuple())
}
#[cfg(feature = "alloc")]
pub fn retain_by_type<F>(&mut self, mut predicate: F)
where
F: FnMut(ExtensionType) -> bool,
{
if !predicate(ExtensionType::Unicode) {
self.unicode.clear();
}
if !predicate(ExtensionType::Transform) {
self.transform.clear();
}
if !predicate(ExtensionType::Private) {
self.private.clear();
}
#[cfg(feature = "alloc")]
self.other
.retain(|o| predicate(ExtensionType::Other(o.get_ext_byte())));
}
#[cfg(feature = "alloc")]
pub(crate) fn try_from_iter(iter: &mut SubtagIterator) -> Result<Self, ParseError> {
let mut unicode = None;
let mut transform = None;
let mut private = None;
let mut other = Vec::new();
while let Some(subtag) = iter.next() {
if subtag.is_empty() {
return Err(ParseError::InvalidExtension);
}
let &[subtag] = subtag else {
return Err(ParseError::InvalidExtension);
};
match ExtensionType::try_from_byte(subtag) {
Ok(ExtensionType::Unicode) => {
if unicode.is_some() {
return Err(ParseError::DuplicatedExtension);
}
unicode = Some(Unicode::try_from_iter(iter)?);
}
Ok(ExtensionType::Transform) => {
if transform.is_some() {
return Err(ParseError::DuplicatedExtension);
}
transform = Some(Transform::try_from_iter(iter)?);
}
Ok(ExtensionType::Private) => {
if private.is_some() {
return Err(ParseError::DuplicatedExtension);
}
private = Some(Private::try_from_iter(iter)?);
}
Ok(ExtensionType::Other(ext)) => {
if other.iter().any(|o: &Other| o.get_ext_byte() == ext) {
return Err(ParseError::DuplicatedExtension);
}
let parsed = Other::try_from_iter(ext, iter)?;
if let Err(idx) = other.binary_search(&parsed) {
other.insert(idx, parsed);
} else {
return Err(ParseError::InvalidExtension);
}
}
_ => return Err(ParseError::InvalidExtension),
}
}
Ok(Self {
unicode: unicode.unwrap_or_default(),
transform: transform.unwrap_or_default(),
private: private.unwrap_or_default(),
other,
})
}
pub(crate) fn for_each_subtag_str<E, F>(&self, f: &mut F) -> Result<(), E>
where
F: FnMut(&str) -> Result<(), E>,
{
let mut wrote_tu = false;
self.other.iter().try_for_each(|other| {
if other.get_ext() > TRANSFORM_EXT_CHAR && !wrote_tu {
self.transform.for_each_subtag_str(f, true)?;
self.unicode.for_each_subtag_str(f, true)?;
wrote_tu = true;
}
other.for_each_subtag_str(f, true)?;
Ok(())
})?;
if !wrote_tu {
self.transform.for_each_subtag_str(f, true)?;
self.unicode.for_each_subtag_str(f, true)?;
}
self.private.for_each_subtag_str(f, true)?;
Ok(())
}
}
impl_writeable_for_each_subtag_str_no_test!(Extensions);
#[test]
fn test_writeable() {
use crate::Locale;
use writeable::assert_writeable_eq;
assert_writeable_eq!(Extensions::new(), "");
assert_writeable_eq!(
"my-t-my-d0-zawgyi".parse::<Locale>().unwrap().extensions,
"t-my-d0-zawgyi",
);
assert_writeable_eq!(
"ar-SA-u-ca-islamic-civil"
.parse::<Locale>()
.unwrap()
.extensions,
"u-ca-islamic-civil",
);
assert_writeable_eq!(
"en-001-x-foo-bar".parse::<Locale>().unwrap().extensions,
"x-foo-bar",
);
assert_writeable_eq!(
"und-t-m0-true".parse::<Locale>().unwrap().extensions,
"t-m0-true",
);
assert_writeable_eq!(
"und-a-foo-t-foo-u-foo-w-foo-z-foo-x-foo"
.parse::<Locale>()
.unwrap()
.extensions,
"a-foo-t-foo-u-foo-w-foo-z-foo-x-foo",
);
}