use std::num::ParseFloatError;
#[cfg(feature = "parser")]
use crate::macros::*;
use crate::{
corety::AzString,
codegen::format::FormatAsRustCode,
props::{
basic::{length::parse_float_value, FloatValue},
formatter::{FormatAsCssValue, PrintAsCssValue},
},
};
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub struct StyleExclusionMargin {
pub inner: FloatValue,
}
impl Default for StyleExclusionMargin {
fn default() -> Self {
Self {
inner: FloatValue::const_new(0),
}
}
}
impl StyleExclusionMargin {
#[must_use] pub const fn is_initial(&self) -> bool {
self.inner.number == 0
}
}
impl PrintAsCssValue for StyleExclusionMargin {
fn print_as_css_value(&self) -> String {
format!("{}", self.inner.get())
}
}
impl FormatAsCssValue for StyleExclusionMargin {
fn format_as_css_value(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.inner.get())
}
}
impl FormatAsRustCode for StyleExclusionMargin {
fn format_as_rust_code(&self, _tabs: usize) -> String {
format!(
"StyleExclusionMargin {{ inner: FloatValue::const_new({}) }}",
self.inner.get()
)
}
}
#[cfg(feature = "parser")]
#[derive(Clone, PartialEq, Eq)]
pub enum StyleExclusionMarginParseError {
FloatValue(ParseFloatError),
}
#[cfg(feature = "parser")]
impl_debug_as_display!(StyleExclusionMarginParseError);
#[cfg(feature = "parser")]
impl_display! { StyleExclusionMarginParseError, {
FloatValue(e) => format!("Invalid -azul-exclusion-margin value: {}", e),
}}
#[cfg(feature = "parser")]
impl_from!(ParseFloatError, StyleExclusionMarginParseError::FloatValue);
#[cfg(feature = "parser")]
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(C, u8)]
pub enum StyleExclusionMarginParseErrorOwned {
FloatValue(AzString),
}
#[cfg(feature = "parser")]
impl StyleExclusionMarginParseError {
#[must_use] pub fn to_contained(&self) -> StyleExclusionMarginParseErrorOwned {
match self {
Self::FloatValue(e) => {
StyleExclusionMarginParseErrorOwned::FloatValue(format!("{e}").into())
}
}
}
}
#[cfg(feature = "parser")]
impl StyleExclusionMarginParseErrorOwned {
#[must_use] pub fn to_shared(&self) -> StyleExclusionMarginParseError {
match self {
Self::FloatValue(_) => {
StyleExclusionMarginParseError::FloatValue("".parse::<f32>().unwrap_err())
}
}
}
}
#[cfg(feature = "parser")]
pub fn parse_style_exclusion_margin(
input: &str,
) -> Result<StyleExclusionMargin, StyleExclusionMarginParseError> {
parse_float_value(input)
.map(|inner| StyleExclusionMargin { inner })
.map_err(StyleExclusionMarginParseError::FloatValue)
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub struct StyleHyphenationLanguage {
pub inner: AzString,
}
impl Default for StyleHyphenationLanguage {
fn default() -> Self {
Self {
inner: AzString::from_const_str("en-US"),
}
}
}
impl StyleHyphenationLanguage {
#[must_use] pub fn is_initial(&self) -> bool {
self.inner.as_str() == "en-US"
}
}
impl PrintAsCssValue for StyleHyphenationLanguage {
fn print_as_css_value(&self) -> String {
format!("\"{}\"", self.inner.as_str())
}
}
impl FormatAsCssValue for StyleHyphenationLanguage {
fn format_as_css_value(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "\"{}\"", self.inner.as_str())
}
}
impl FormatAsRustCode for StyleHyphenationLanguage {
fn format_as_rust_code(&self, _tabs: usize) -> String {
format!(
"StyleHyphenationLanguage {{ inner: AzString::from_const_str(\"{}\") }}",
self.inner.as_str()
)
}
}
#[cfg(feature = "parser")]
#[derive(Clone, PartialEq, Eq)]
pub enum StyleHyphenationLanguageParseError {
InvalidString(String),
}
#[cfg(feature = "parser")]
impl_debug_as_display!(StyleHyphenationLanguageParseError);
#[cfg(feature = "parser")]
impl_display! { StyleHyphenationLanguageParseError, {
InvalidString(e) => format!("Invalid -azul-hyphenation-language value: {}", e),
}}
#[cfg(feature = "parser")]
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(C, u8)]
pub enum StyleHyphenationLanguageParseErrorOwned {
InvalidString(AzString),
}
#[cfg(feature = "parser")]
impl StyleHyphenationLanguageParseError {
#[must_use] pub fn to_contained(&self) -> StyleHyphenationLanguageParseErrorOwned {
match self {
Self::InvalidString(e) => {
StyleHyphenationLanguageParseErrorOwned::InvalidString(e.clone().into())
}
}
}
}
#[cfg(feature = "parser")]
impl StyleHyphenationLanguageParseErrorOwned {
#[must_use] pub fn to_shared(&self) -> StyleHyphenationLanguageParseError {
match self {
Self::InvalidString(e) => StyleHyphenationLanguageParseError::InvalidString(e.to_string()),
}
}
}
#[cfg(feature = "parser")]
pub fn parse_style_hyphenation_language(
input: &str,
) -> Result<StyleHyphenationLanguage, StyleHyphenationLanguageParseError> {
let trimmed = input.trim();
let unquoted = if trimmed.len() >= 2
&& ((trimmed.starts_with('"') && trimmed.ends_with('"'))
|| (trimmed.starts_with('\'') && trimmed.ends_with('\'')))
{
&trimmed[1..trimmed.len() - 1]
} else {
trimmed
};
if unquoted.is_empty()
|| !unquoted.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-')
|| unquoted.starts_with('-')
|| unquoted.ends_with('-')
{
return Err(StyleHyphenationLanguageParseError::InvalidString(
unquoted.to_string(),
));
}
Ok(StyleHyphenationLanguage {
inner: AzString::from_string(unquoted.to_string()),
})
}
#[cfg(test)]
mod tests {
#![allow(clippy::float_cmp)]
use super::*;
#[test]
fn test_parse_exclusion_margin() {
let margin = parse_style_exclusion_margin("10.5").unwrap();
assert_eq!(margin.inner.get(), 10.5);
let margin = parse_style_exclusion_margin("0").unwrap();
assert_eq!(margin.inner.get(), 0.0);
}
#[test]
fn test_parse_hyphenation_language() {
let lang = parse_style_hyphenation_language("\"en-US\"").unwrap();
assert_eq!(lang.inner.as_str(), "en-US");
let lang = parse_style_hyphenation_language("'de-DE'").unwrap();
assert_eq!(lang.inner.as_str(), "de-DE");
let lang = parse_style_hyphenation_language("fr-FR").unwrap();
assert_eq!(lang.inner.as_str(), "fr-FR");
let lang = parse_style_hyphenation_language("zh").unwrap();
assert_eq!(lang.inner.as_str(), "zh");
let lang = parse_style_hyphenation_language("sr-Latn-RS").unwrap();
assert_eq!(lang.inner.as_str(), "sr-Latn-RS");
let lang = parse_style_hyphenation_language("en--US").unwrap();
assert_eq!(lang.inner.as_str(), "en--US");
}
#[test]
fn test_parse_hyphenation_language_invalid() {
assert!(matches!(
parse_style_hyphenation_language(""),
Err(StyleHyphenationLanguageParseError::InvalidString(_))
));
assert!(matches!(
parse_style_hyphenation_language("-en"),
Err(StyleHyphenationLanguageParseError::InvalidString(_))
));
assert!(matches!(
parse_style_hyphenation_language("en-"),
Err(StyleHyphenationLanguageParseError::InvalidString(_))
));
assert!(matches!(
parse_style_hyphenation_language("en_US"),
Err(StyleHyphenationLanguageParseError::InvalidString(_))
));
assert!(matches!(
parse_style_hyphenation_language("日本語"),
Err(StyleHyphenationLanguageParseError::InvalidString(_))
));
}
#[test]
fn test_exclusion_margin_default() {
let margin = StyleExclusionMargin::default();
assert_eq!(margin.inner.get(), 0.0);
assert!(margin.is_initial());
}
#[test]
fn test_hyphenation_language_default() {
let lang = StyleHyphenationLanguage::default();
assert_eq!(lang.inner.as_str(), "en-US");
}
}
#[cfg(test)]
mod autotest_generated {
#![allow(clippy::float_cmp)]
use super::*;
struct AsCss<'a, T: FormatAsCssValue>(&'a T);
impl<T: FormatAsCssValue> std::fmt::Display for AsCss<'_, T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.format_as_css_value(f)
}
}
#[test]
fn exclusion_margin_is_initial_true_and_false() {
assert!(StyleExclusionMargin::default().is_initial());
assert!(StyleExclusionMargin {
inner: FloatValue::const_new(0)
}
.is_initial());
assert!(!StyleExclusionMargin {
inner: FloatValue::const_new(1)
}
.is_initial());
assert!(!StyleExclusionMargin {
inner: FloatValue::const_new(-1)
}
.is_initial());
}
#[test]
fn exclusion_margin_is_initial_on_boundary_encodings() {
for v in [0.0_f32, -0.0, 0.0004, -0.0004, f32::MIN_POSITIVE, 1e-30] {
let m = StyleExclusionMargin {
inner: FloatValue::new(v),
};
assert!(m.is_initial(), "{v} should encode to the initial value");
assert_eq!(m.inner.get(), 0.0);
}
let nan = StyleExclusionMargin {
inner: FloatValue::new(f32::NAN),
};
assert!(nan.is_initial());
assert!(!nan.inner.get().is_nan());
for v in [f32::INFINITY, f32::NEG_INFINITY, f32::MAX, f32::MIN] {
let m = StyleExclusionMargin {
inner: FloatValue::new(v),
};
assert!(!m.is_initial(), "{v} must not be reported as initial");
assert!(m.inner.get().is_finite());
}
}
#[test]
fn hyphenation_is_initial_true_and_false() {
assert!(StyleHyphenationLanguage::default().is_initial());
assert!(StyleHyphenationLanguage {
inner: AzString::from_const_str("en-US"),
}
.is_initial());
for not_initial in ["", " ", "en-us", "EN-US", "en-US ", "en", "de-DE", "en\u{0}US"] {
assert!(
!StyleHyphenationLanguage {
inner: AzString::from_string(not_initial.to_string()),
}
.is_initial(),
"{not_initial:?} must not be reported as initial"
);
}
}
#[test]
fn hyphenation_is_initial_on_extreme_strings_does_not_panic() {
for s in [
"\u{1F600}".to_string(),
"e\u{0301}n-US".to_string(), "en-US\u{200B}".to_string(), "a".repeat(1_000_000),
] {
let lang = StyleHyphenationLanguage {
inner: AzString::from_string(s.clone()),
};
assert!(!lang.is_initial(), "{s:?} must not be reported as initial");
}
}
#[test]
fn exclusion_margin_print_and_format_agree() {
for v in [0.0_f32, 10.5, -3.25, 123.456, f32::INFINITY, f32::NAN] {
let m = StyleExclusionMargin {
inner: FloatValue::new(v),
};
let printed = m.print_as_css_value();
assert_eq!(printed, AsCss(&m).to_string());
assert!(!printed.contains("NaN") && !printed.contains("inf"));
assert!(m.format_as_rust_code(0).contains(&printed));
}
}
#[test]
fn hyphenation_print_and_format_agree() {
for s in ["en-US", "", "a", "\u{1F600}", "quote\"inside"] {
let lang = StyleHyphenationLanguage {
inner: AzString::from_string(s.to_string()),
};
let printed = lang.print_as_css_value();
assert_eq!(printed, AsCss(&lang).to_string());
assert_eq!(printed, format!("\"{s}\""));
assert!(lang.format_as_rust_code(0).contains(s));
}
}
#[test]
fn exclusion_margin_ord_and_hash_are_consistent_with_value() {
use std::{
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
};
let hash = |m: &StyleExclusionMargin| {
let mut h = DefaultHasher::new();
m.hash(&mut h);
h.finish()
};
let a = StyleExclusionMargin {
inner: FloatValue::new(1.5),
};
let b = StyleExclusionMargin {
inner: FloatValue::new(1.5),
};
let c = StyleExclusionMargin {
inner: FloatValue::new(2.5),
};
assert_eq!(a, b);
assert_eq!(hash(&a), hash(&b));
assert!(a < c);
assert!(a.inner.get() < c.inner.get());
let d = StyleExclusionMargin {
inner: FloatValue::new(1.5004),
};
assert_eq!(a, d);
assert_eq!(hash(&a), hash(&d));
}
#[cfg(feature = "parser")]
#[test]
fn parse_exclusion_margin_valid_minimal() {
assert_eq!(
parse_style_exclusion_margin("0").unwrap(),
StyleExclusionMargin::default()
);
assert_eq!(parse_style_exclusion_margin("1").unwrap().inner.get(), 1.0);
}
#[cfg(feature = "parser")]
#[test]
fn parse_exclusion_margin_empty_and_whitespace_only() {
for input in ["", " ", " ", "\t\n", "\r\n\t ", "\u{00A0}"] {
assert!(
parse_style_exclusion_margin(input).is_err(),
"{input:?} must not parse"
);
}
}
#[cfg(feature = "parser")]
#[test]
fn parse_exclusion_margin_garbage_is_rejected() {
for input in [
"abc", "px", "10px", "1,5", "1_000", "0x10", "--5", "5-", "+-1", ".", "-", "+", "e5",
"1e", "1.2.3", "null", "None", "{}", "()", "/*10*/", "10 20", "\0", "\u{7}\u{1b}[0m",
] {
assert!(
parse_style_exclusion_margin(input).is_err(),
"{input:?} must be rejected"
);
}
}
#[cfg(feature = "parser")]
#[test]
fn parse_exclusion_margin_leading_trailing_junk() {
assert_eq!(
parse_style_exclusion_margin(" 10.5 ").unwrap().inner.get(),
10.5
);
assert_eq!(
parse_style_exclusion_margin("\n\t-3.25\t\n")
.unwrap()
.inner
.get(),
-3.25
);
for input in ["10.5px", "10.5;garbage", "10.5 !important", "10.5;", "10.5%"] {
assert!(
parse_style_exclusion_margin(input).is_err(),
"{input:?} must be rejected, not truncated to a number"
);
}
}
#[cfg(feature = "parser")]
#[test]
fn parse_exclusion_margin_boundary_numbers() {
for input in ["0", "-0", "+0", "0.0", "-0.0", "0e0"] {
let m = parse_style_exclusion_margin(input).unwrap();
assert_eq!(m.inner.number, 0, "{input:?} should encode to zero");
assert!(m.is_initial());
}
for input in ["0.0004", "-0.0004", "1e-30", "-1e-30"] {
let m = parse_style_exclusion_margin(input).unwrap();
assert_eq!(m.inner.number, 0, "{input:?} should truncate to zero");
}
for input in [
i64::MAX.to_string(),
i64::MIN.to_string(),
f32::MAX.to_string(),
format!("{}", f32::MIN),
"1e38".to_string(),
"-1e38".to_string(),
] {
let m = parse_style_exclusion_margin(&input).unwrap();
assert!(
m.inner.get().is_finite(),
"{input:?} must decode to a finite value, got {}",
m.inner.get()
);
}
assert_eq!(
parse_style_exclusion_margin(&f32::MAX.to_string())
.unwrap()
.inner
.number,
isize::MAX
);
assert_eq!(
parse_style_exclusion_margin(&format!("{}", f32::MIN))
.unwrap()
.inner
.number,
isize::MIN
);
}
#[cfg(feature = "parser")]
#[test]
fn parse_exclusion_margin_nan_and_infinity_never_escape() {
let nan = parse_style_exclusion_margin("NaN").unwrap();
assert_eq!(nan.inner.number, 0);
assert!(!nan.inner.get().is_nan());
assert!(nan.is_initial());
assert_eq!(parse_style_exclusion_margin("nan").unwrap().inner.number, 0);
assert_eq!(parse_style_exclusion_margin("-NaN").unwrap().inner.number, 0);
for input in ["inf", "infinity", "+inf", "INF", "Infinity", "1e400"] {
let m = parse_style_exclusion_margin(input).unwrap();
assert_eq!(m.inner.number, isize::MAX, "{input:?} must saturate");
assert!(m.inner.get().is_finite());
}
for input in ["-inf", "-infinity", "-INF", "-1e400"] {
let m = parse_style_exclusion_margin(input).unwrap();
assert_eq!(m.inner.number, isize::MIN, "{input:?} must saturate");
assert!(m.inner.get().is_finite());
}
}
#[cfg(feature = "parser")]
#[test]
fn parse_exclusion_margin_unicode_input() {
for input in [
"\u{1F600}", "\u{FF15}", "1\u{0301}", "\u{202E}10.5", "\u{FEFF}10.5", "١٢٣", "10.5\u{1F4A9}", ] {
assert!(
parse_style_exclusion_margin(input).is_err(),
"{input:?} must be rejected"
);
}
let r = parse_style_exclusion_margin("\u{00A0}10.5\u{2003}");
assert!(r.is_err() || r.unwrap().inner.get() == 10.5);
}
#[cfg(feature = "parser")]
#[test]
fn parse_exclusion_margin_extremely_long_input() {
let huge = "9".repeat(1_000_000);
let m = parse_style_exclusion_margin(&huge).unwrap();
assert_eq!(m.inner.number, isize::MAX);
assert!(m.inner.get().is_finite());
let long_fraction = format!("1.{}", "0".repeat(1_000_000));
assert_eq!(
parse_style_exclusion_margin(&long_fraction).unwrap().inner.get(),
1.0
);
assert!(parse_style_exclusion_margin(&"z".repeat(1_000_000)).is_err());
assert!(parse_style_exclusion_margin(&" ".repeat(1_000_000)).is_err());
}
#[cfg(feature = "parser")]
#[test]
fn parse_exclusion_margin_deeply_nested_input_does_not_stack_overflow() {
let nested = format!("{}{}", "(".repeat(10_000), ")".repeat(10_000));
assert!(parse_style_exclusion_margin(&nested).is_err());
let nested_number = format!("{}1{}", "(".repeat(10_000), ")".repeat(10_000));
assert!(parse_style_exclusion_margin(&nested_number).is_err());
assert!(parse_style_exclusion_margin(&"-".repeat(10_000)).is_err());
}
#[cfg(feature = "parser")]
#[test]
fn parse_exclusion_margin_round_trips_through_css_and_rust_code() {
for input in ["0", "1", "10.5", "-3.25", "0.001", "123.456", "-0.5", "1000"] {
let parsed = parse_style_exclusion_margin(input).unwrap();
let printed = parsed.print_as_css_value();
let reparsed = parse_style_exclusion_margin(&printed).unwrap();
assert_eq!(
parsed, reparsed,
"{input:?} printed as {printed:?} did not round-trip"
);
assert_eq!(printed, reparsed.print_as_css_value());
assert!(parsed.format_as_rust_code(0).contains(&printed));
}
}
#[cfg(feature = "parser")]
#[test]
fn exclusion_margin_error_to_contained_carries_the_message() {
let err = parse_style_exclusion_margin("garbage").unwrap_err();
let StyleExclusionMarginParseErrorOwned::FloatValue(msg) = err.to_contained();
assert!(!msg.as_str().is_empty());
assert_eq!(format!("{err:?}"), format!("{err}"));
assert!(format!("{err}").contains("-azul-exclusion-margin"));
assert!(format!("{err}").contains(msg.as_str()));
}
#[cfg(feature = "parser")]
#[test]
fn exclusion_margin_error_to_shared_is_lossy_but_total() {
let empty_err_msg: AzString = format!("{}", "".parse::<f32>().unwrap_err()).into();
for msg in [
String::new(),
"invalid float literal".to_string(),
"\u{1F600}".to_string(),
"x".repeat(1_000_000),
] {
let owned = StyleExclusionMarginParseErrorOwned::FloatValue(msg.clone().into());
let shared = owned.to_shared();
assert_eq!(
shared.to_contained(),
StyleExclusionMarginParseErrorOwned::FloatValue(empty_err_msg.clone()),
"to_shared() should normalise {msg:?} onto the empty-string error"
);
}
let empty = parse_style_exclusion_margin("").unwrap_err();
assert_eq!(empty.to_contained().to_shared(), empty);
assert_eq!(empty.to_contained().to_shared().to_contained(), empty.to_contained());
}
#[cfg(feature = "parser")]
#[test]
fn parse_hyphenation_language_valid_minimal() {
let lang = parse_style_hyphenation_language("en-US").unwrap();
assert_eq!(lang.inner.as_str(), "en-US");
assert!(lang.is_initial());
assert_eq!(parse_style_hyphenation_language("a").unwrap().inner.as_str(), "a");
}
#[cfg(feature = "parser")]
#[test]
fn parse_hyphenation_language_empty_and_whitespace_only() {
for input in ["", " ", " ", "\t\n", "\r\n\t ", "\"\"", "''", "\" \""] {
assert!(
parse_style_hyphenation_language(input).is_err(),
"{input:?} must not parse"
);
}
}
#[cfg(feature = "parser")]
#[test]
fn parse_hyphenation_language_garbage_is_rejected() {
for input in [
"en_US", "-en", "en-", "en US", "\" en-US \"", "en;US",
"en/US",
"en.US",
"en*",
"<script>",
"en\0US", "en\nUS",
"'en-US\"", "\"en-US'",
"\"en-US", "en-US\"",
] {
assert!(
matches!(
parse_style_hyphenation_language(input),
Err(StyleHyphenationLanguageParseError::InvalidString(_))
),
"{input:?} must be rejected"
);
}
}
#[cfg(feature = "parser")]
#[test]
fn parse_hyphenation_language_leading_trailing_junk() {
assert_eq!(
parse_style_hyphenation_language(" en-US ").unwrap().inner.as_str(),
"en-US"
);
assert_eq!(
parse_style_hyphenation_language("\t\"de-DE\"\n").unwrap().inner.as_str(),
"de-DE"
);
for input in ["en-US;", "en-US !important", "\"en-US\";", "en-US /*c*/"] {
assert!(
parse_style_hyphenation_language(input).is_err(),
"{input:?} must be rejected, not truncated"
);
}
}
#[cfg(feature = "parser")]
#[test]
fn parse_hyphenation_language_unicode_input() {
for input in [
"日本語",
"\u{1F600}",
"\"\u{1F600}\"",
"e\u{0301}n-US", "en-US\u{200B}", "\u{FEFF}en-US", "en-US", "ру-RU",
] {
assert!(
matches!(
parse_style_hyphenation_language(input),
Err(StyleHyphenationLanguageParseError::InvalidString(_))
),
"{input:?} must be rejected without panicking"
);
}
let r = parse_style_hyphenation_language("\"日本語\"");
assert!(r.is_err());
}
#[cfg(feature = "parser")]
#[test]
fn parse_hyphenation_language_accepts_any_ascii_alphanumeric_tag() {
for input in ["0", "123", "NaN", "inf", "zzzzzz", "sr-Latn-RS", "x-private"] {
let lang = parse_style_hyphenation_language(input).unwrap();
assert_eq!(lang.inner.as_str(), input);
}
assert_eq!(
parse_style_hyphenation_language(&i64::MAX.to_string())
.unwrap()
.inner
.as_str(),
i64::MAX.to_string()
);
assert!(parse_style_hyphenation_language(&i64::MIN.to_string()).is_err());
assert!(parse_style_hyphenation_language("-0").is_err());
}
#[cfg(feature = "parser")]
#[test]
fn parse_hyphenation_language_extremely_long_input() {
let huge = "a".repeat(1_000_000);
assert_eq!(
parse_style_hyphenation_language(&huge).unwrap().inner.as_str(),
huge
);
let huge_quoted = format!("\"{huge}\"");
assert_eq!(
parse_style_hyphenation_language(&huge_quoted)
.unwrap()
.inner
.as_str(),
huge
);
assert!(parse_style_hyphenation_language(&"-".repeat(1_000_000)).is_err());
assert!(parse_style_hyphenation_language(&"é".repeat(1_000_000)).is_err());
}
#[cfg(feature = "parser")]
#[test]
fn parse_hyphenation_language_deeply_nested_input_does_not_stack_overflow() {
let nested = format!("{}en-US{}", "(".repeat(10_000), ")".repeat(10_000));
assert!(parse_style_hyphenation_language(&nested).is_err());
let nested_quotes = format!("{}en-US{}", "\"".repeat(10_000), "\"".repeat(10_000));
assert!(parse_style_hyphenation_language(&nested_quotes).is_err());
}
#[cfg(feature = "parser")]
#[test]
fn parse_hyphenation_language_round_trips_through_css_and_rust_code() {
for input in ["en-US", "de-DE", "zh", "sr-Latn-RS", "en--US", "x-private", "0"] {
let parsed = parse_style_hyphenation_language(input).unwrap();
let printed = parsed.print_as_css_value();
assert_eq!(printed, format!("\"{input}\""));
let reparsed = parse_style_hyphenation_language(&printed).unwrap();
assert_eq!(parsed, reparsed, "{input:?} did not round-trip");
assert_eq!(printed, reparsed.print_as_css_value());
assert_eq!(
parse_style_hyphenation_language(&format!("'{input}'")).unwrap(),
parsed
);
assert!(parsed.format_as_rust_code(0).contains(input));
}
}
#[cfg(feature = "parser")]
#[test]
fn hyphenation_error_to_contained_carries_the_offending_string() {
let err = parse_style_hyphenation_language("en_US").unwrap_err();
let StyleHyphenationLanguageParseErrorOwned::InvalidString(msg) = err.to_contained();
assert_eq!(msg.as_str(), "en_US");
assert_eq!(format!("{err:?}"), format!("{err}"));
assert!(format!("{err}").contains("-azul-hyphenation-language"));
assert!(format!("{err}").contains("en_US"));
}
#[cfg(feature = "parser")]
#[test]
fn hyphenation_error_round_trips_losslessly() {
for msg in [
String::new(),
"en_US".to_string(),
"\u{1F600}".to_string(),
"\0".to_string(),
"x".repeat(1_000_000),
] {
let shared = StyleHyphenationLanguageParseError::InvalidString(msg.clone());
let owned = shared.to_contained();
assert_eq!(owned.to_shared(), shared, "{msg:?} lost data on round-trip");
assert_eq!(owned.to_shared().to_contained(), owned);
let owned_direct =
StyleHyphenationLanguageParseErrorOwned::InvalidString(msg.clone().into());
assert_eq!(owned_direct, owned);
let StyleHyphenationLanguageParseErrorOwned::InvalidString(inner) = owned_direct;
assert_eq!(inner.as_str(), msg);
}
}
#[cfg(feature = "parser")]
#[test]
fn hyphenation_error_for_empty_input_reports_the_unquoted_string() {
let err = parse_style_hyphenation_language("\"\"").unwrap_err();
assert_eq!(
err.to_contained(),
StyleHyphenationLanguageParseErrorOwned::InvalidString(AzString::from_const_str(""))
);
assert!(err.to_contained().to_shared() == err);
}
#[cfg(feature = "parser")]
#[test]
fn parse_hyphenation_language_lone_quote_must_not_panic() {
for input in ["\"", "'", " \" ", "\t'\n"] {
assert!(
parse_style_hyphenation_language(input).is_err(),
"{input:?} must return Err, not panic"
);
}
}
}