1use std::num::ParseFloatError;
7
8#[cfg(feature = "parser")]
9use crate::macros::*;
10use crate::{
11 corety::AzString,
12 codegen::format::FormatAsRustCode,
13 props::{
14 basic::{length::parse_float_value, FloatValue},
15 formatter::{FormatAsCssValue, PrintAsCssValue},
16 },
17};
18
19#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
31#[repr(C)]
32pub struct StyleExclusionMargin {
33 pub inner: FloatValue,
34}
35
36impl Default for StyleExclusionMargin {
37 fn default() -> Self {
38 Self {
39 inner: FloatValue::const_new(0),
40 }
41 }
42}
43
44impl StyleExclusionMargin {
45 #[must_use] pub const fn is_initial(&self) -> bool {
46 self.inner.number == 0
47 }
48}
49
50impl PrintAsCssValue for StyleExclusionMargin {
51 fn print_as_css_value(&self) -> String {
52 format!("{}", self.inner.get())
53 }
54}
55
56impl FormatAsCssValue for StyleExclusionMargin {
57 fn format_as_css_value(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 write!(f, "{}", self.inner.get())
59 }
60}
61
62impl FormatAsRustCode for StyleExclusionMargin {
63 fn format_as_rust_code(&self, _tabs: usize) -> String {
64 format!(
65 "StyleExclusionMargin {{ inner: FloatValue::const_new({}) }}",
66 self.inner.get()
67 )
68 }
69}
70
71#[cfg(feature = "parser")]
72#[derive(Clone, PartialEq, Eq)]
73pub enum StyleExclusionMarginParseError {
74 FloatValue(ParseFloatError),
75}
76
77#[cfg(feature = "parser")]
78impl_debug_as_display!(StyleExclusionMarginParseError);
79
80#[cfg(feature = "parser")]
81impl_display! { StyleExclusionMarginParseError, {
82 FloatValue(e) => format!("Invalid -azul-exclusion-margin value: {}", e),
83}}
84
85#[cfg(feature = "parser")]
86impl_from!(ParseFloatError, StyleExclusionMarginParseError::FloatValue);
87
88#[cfg(feature = "parser")]
89#[derive(Debug, Clone, PartialEq, Eq)]
90#[repr(C, u8)]
91pub enum StyleExclusionMarginParseErrorOwned {
92 FloatValue(AzString),
93}
94
95#[cfg(feature = "parser")]
96impl StyleExclusionMarginParseError {
97 #[must_use] pub fn to_contained(&self) -> StyleExclusionMarginParseErrorOwned {
98 match self {
99 Self::FloatValue(e) => {
100 StyleExclusionMarginParseErrorOwned::FloatValue(format!("{e}").into())
101 }
102 }
103 }
104}
105
106#[cfg(feature = "parser")]
107impl StyleExclusionMarginParseErrorOwned {
108 #[must_use] pub fn to_shared(&self) -> StyleExclusionMarginParseError {
109 match self {
110 Self::FloatValue(_) => {
111 StyleExclusionMarginParseError::FloatValue("".parse::<f32>().unwrap_err())
114 }
115 }
116 }
117}
118
119#[cfg(feature = "parser")]
120pub fn parse_style_exclusion_margin(
124 input: &str,
125) -> Result<StyleExclusionMargin, StyleExclusionMarginParseError> {
126 parse_float_value(input)
127 .map(|inner| StyleExclusionMargin { inner })
128 .map_err(StyleExclusionMarginParseError::FloatValue)
129}
130
131#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
143#[repr(C)]
144pub struct StyleHyphenationLanguage {
145 pub inner: AzString,
146}
147
148impl Default for StyleHyphenationLanguage {
149 fn default() -> Self {
150 Self {
151 inner: AzString::from_const_str("en-US"),
152 }
153 }
154}
155
156impl StyleHyphenationLanguage {
157 #[must_use] pub fn is_initial(&self) -> bool {
158 self.inner.as_str() == "en-US"
159 }
160}
161
162impl PrintAsCssValue for StyleHyphenationLanguage {
163 fn print_as_css_value(&self) -> String {
164 format!("\"{}\"", self.inner.as_str())
165 }
166}
167
168impl FormatAsCssValue for StyleHyphenationLanguage {
169 fn format_as_css_value(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170 write!(f, "\"{}\"", self.inner.as_str())
171 }
172}
173
174impl FormatAsRustCode for StyleHyphenationLanguage {
175 fn format_as_rust_code(&self, _tabs: usize) -> String {
176 format!(
177 "StyleHyphenationLanguage {{ inner: AzString::from_const_str(\"{}\") }}",
178 self.inner.as_str()
179 )
180 }
181}
182
183#[cfg(feature = "parser")]
184#[derive(Clone, PartialEq, Eq)]
185pub enum StyleHyphenationLanguageParseError {
186 InvalidString(String),
187}
188
189#[cfg(feature = "parser")]
190impl_debug_as_display!(StyleHyphenationLanguageParseError);
191
192#[cfg(feature = "parser")]
193impl_display! { StyleHyphenationLanguageParseError, {
194 InvalidString(e) => format!("Invalid -azul-hyphenation-language value: {}", e),
195}}
196
197#[cfg(feature = "parser")]
198#[derive(Debug, Clone, PartialEq, Eq)]
199#[repr(C, u8)]
200pub enum StyleHyphenationLanguageParseErrorOwned {
201 InvalidString(AzString),
202}
203
204#[cfg(feature = "parser")]
205impl StyleHyphenationLanguageParseError {
206 #[must_use] pub fn to_contained(&self) -> StyleHyphenationLanguageParseErrorOwned {
207 match self {
208 Self::InvalidString(e) => {
209 StyleHyphenationLanguageParseErrorOwned::InvalidString(e.clone().into())
210 }
211 }
212 }
213}
214
215#[cfg(feature = "parser")]
216impl StyleHyphenationLanguageParseErrorOwned {
217 #[must_use] pub fn to_shared(&self) -> StyleHyphenationLanguageParseError {
218 match self {
219 Self::InvalidString(e) => StyleHyphenationLanguageParseError::InvalidString(e.to_string()),
220 }
221 }
222}
223
224#[cfg(feature = "parser")]
225pub fn parse_style_hyphenation_language(
229 input: &str,
230) -> Result<StyleHyphenationLanguage, StyleHyphenationLanguageParseError> {
231 let trimmed = input.trim();
235 let unquoted = if trimmed.len() >= 2
236 && ((trimmed.starts_with('"') && trimmed.ends_with('"'))
237 || (trimmed.starts_with('\'') && trimmed.ends_with('\'')))
238 {
239 &trimmed[1..trimmed.len() - 1]
240 } else {
241 trimmed
242 };
243
244 if unquoted.is_empty()
246 || !unquoted.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-')
247 || unquoted.starts_with('-')
248 || unquoted.ends_with('-')
249 {
250 return Err(StyleHyphenationLanguageParseError::InvalidString(
251 unquoted.to_string(),
252 ));
253 }
254
255 Ok(StyleHyphenationLanguage {
256 inner: AzString::from_string(unquoted.to_string()),
257 })
258}
259
260#[cfg(test)]
261mod tests {
262 #![allow(clippy::float_cmp)]
264 use super::*;
265
266 #[test]
267 fn test_parse_exclusion_margin() {
268 let margin = parse_style_exclusion_margin("10.5").unwrap();
269 assert_eq!(margin.inner.get(), 10.5);
270
271 let margin = parse_style_exclusion_margin("0").unwrap();
272 assert_eq!(margin.inner.get(), 0.0);
273 }
274
275 #[test]
276 fn test_parse_hyphenation_language() {
277 let lang = parse_style_hyphenation_language("\"en-US\"").unwrap();
278 assert_eq!(lang.inner.as_str(), "en-US");
279
280 let lang = parse_style_hyphenation_language("'de-DE'").unwrap();
281 assert_eq!(lang.inner.as_str(), "de-DE");
282
283 let lang = parse_style_hyphenation_language("fr-FR").unwrap();
284 assert_eq!(lang.inner.as_str(), "fr-FR");
285
286 let lang = parse_style_hyphenation_language("zh").unwrap();
287 assert_eq!(lang.inner.as_str(), "zh");
288
289 let lang = parse_style_hyphenation_language("sr-Latn-RS").unwrap();
290 assert_eq!(lang.inner.as_str(), "sr-Latn-RS");
291
292 let lang = parse_style_hyphenation_language("en--US").unwrap();
294 assert_eq!(lang.inner.as_str(), "en--US");
295 }
296
297 #[test]
298 fn test_parse_hyphenation_language_invalid() {
299 assert!(matches!(
300 parse_style_hyphenation_language(""),
301 Err(StyleHyphenationLanguageParseError::InvalidString(_))
302 ));
303 assert!(matches!(
304 parse_style_hyphenation_language("-en"),
305 Err(StyleHyphenationLanguageParseError::InvalidString(_))
306 ));
307 assert!(matches!(
308 parse_style_hyphenation_language("en-"),
309 Err(StyleHyphenationLanguageParseError::InvalidString(_))
310 ));
311 assert!(matches!(
312 parse_style_hyphenation_language("en_US"),
313 Err(StyleHyphenationLanguageParseError::InvalidString(_))
314 ));
315 assert!(matches!(
316 parse_style_hyphenation_language("日本語"),
317 Err(StyleHyphenationLanguageParseError::InvalidString(_))
318 ));
319 }
320
321 #[test]
322 fn test_exclusion_margin_default() {
323 let margin = StyleExclusionMargin::default();
324 assert_eq!(margin.inner.get(), 0.0);
325 assert!(margin.is_initial());
326 }
327
328 #[test]
329 fn test_hyphenation_language_default() {
330 let lang = StyleHyphenationLanguage::default();
331 assert_eq!(lang.inner.as_str(), "en-US");
332 }
333}
334
335#[cfg(test)]
336mod autotest_generated {
337 #![allow(clippy::float_cmp)]
342
343 use super::*;
344
345 struct AsCss<'a, T: FormatAsCssValue>(&'a T);
347
348 impl<T: FormatAsCssValue> std::fmt::Display for AsCss<'_, T> {
349 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
350 self.0.format_as_css_value(f)
351 }
352 }
353
354 #[test]
359 fn exclusion_margin_is_initial_true_and_false() {
360 assert!(StyleExclusionMargin::default().is_initial());
361 assert!(StyleExclusionMargin {
362 inner: FloatValue::const_new(0)
363 }
364 .is_initial());
365 assert!(!StyleExclusionMargin {
366 inner: FloatValue::const_new(1)
367 }
368 .is_initial());
369 assert!(!StyleExclusionMargin {
370 inner: FloatValue::const_new(-1)
371 }
372 .is_initial());
373 }
374
375 #[test]
376 fn exclusion_margin_is_initial_on_boundary_encodings() {
377 for v in [0.0_f32, -0.0, 0.0004, -0.0004, f32::MIN_POSITIVE, 1e-30] {
379 let m = StyleExclusionMargin {
380 inner: FloatValue::new(v),
381 };
382 assert!(m.is_initial(), "{v} should encode to the initial value");
383 assert_eq!(m.inner.get(), 0.0);
384 }
385
386 let nan = StyleExclusionMargin {
388 inner: FloatValue::new(f32::NAN),
389 };
390 assert!(nan.is_initial());
391 assert!(!nan.inner.get().is_nan());
392
393 for v in [f32::INFINITY, f32::NEG_INFINITY, f32::MAX, f32::MIN] {
395 let m = StyleExclusionMargin {
396 inner: FloatValue::new(v),
397 };
398 assert!(!m.is_initial(), "{v} must not be reported as initial");
399 assert!(m.inner.get().is_finite());
400 }
401 }
402
403 #[test]
408 fn hyphenation_is_initial_true_and_false() {
409 assert!(StyleHyphenationLanguage::default().is_initial());
410 assert!(StyleHyphenationLanguage {
411 inner: AzString::from_const_str("en-US"),
412 }
413 .is_initial());
414
415 for not_initial in ["", " ", "en-us", "EN-US", "en-US ", "en", "de-DE", "en\u{0}US"] {
417 assert!(
418 !StyleHyphenationLanguage {
419 inner: AzString::from_string(not_initial.to_string()),
420 }
421 .is_initial(),
422 "{not_initial:?} must not be reported as initial"
423 );
424 }
425 }
426
427 #[test]
428 fn hyphenation_is_initial_on_extreme_strings_does_not_panic() {
429 for s in [
430 "\u{1F600}".to_string(),
431 "e\u{0301}n-US".to_string(), "en-US\u{200B}".to_string(), "a".repeat(1_000_000),
434 ] {
435 let lang = StyleHyphenationLanguage {
436 inner: AzString::from_string(s.clone()),
437 };
438 assert!(!lang.is_initial(), "{s:?} must not be reported as initial");
439 }
440 }
441
442 #[test]
447 fn exclusion_margin_print_and_format_agree() {
448 for v in [0.0_f32, 10.5, -3.25, 123.456, f32::INFINITY, f32::NAN] {
449 let m = StyleExclusionMargin {
450 inner: FloatValue::new(v),
451 };
452 let printed = m.print_as_css_value();
453 assert_eq!(printed, AsCss(&m).to_string());
454 assert!(!printed.contains("NaN") && !printed.contains("inf"));
456 assert!(m.format_as_rust_code(0).contains(&printed));
457 }
458 }
459
460 #[test]
461 fn hyphenation_print_and_format_agree() {
462 for s in ["en-US", "", "a", "\u{1F600}", "quote\"inside"] {
463 let lang = StyleHyphenationLanguage {
464 inner: AzString::from_string(s.to_string()),
465 };
466 let printed = lang.print_as_css_value();
467 assert_eq!(printed, AsCss(&lang).to_string());
468 assert_eq!(printed, format!("\"{s}\""));
469 assert!(lang.format_as_rust_code(0).contains(s));
470 }
471 }
472
473 #[test]
474 fn exclusion_margin_ord_and_hash_are_consistent_with_value() {
475 use std::{
476 collections::hash_map::DefaultHasher,
477 hash::{Hash, Hasher},
478 };
479
480 let hash = |m: &StyleExclusionMargin| {
481 let mut h = DefaultHasher::new();
482 m.hash(&mut h);
483 h.finish()
484 };
485
486 let a = StyleExclusionMargin {
487 inner: FloatValue::new(1.5),
488 };
489 let b = StyleExclusionMargin {
490 inner: FloatValue::new(1.5),
491 };
492 let c = StyleExclusionMargin {
493 inner: FloatValue::new(2.5),
494 };
495
496 assert_eq!(a, b);
497 assert_eq!(hash(&a), hash(&b));
498 assert!(a < c);
499 assert!(a.inner.get() < c.inner.get());
500
501 let d = StyleExclusionMargin {
503 inner: FloatValue::new(1.5004),
504 };
505 assert_eq!(a, d);
506 assert_eq!(hash(&a), hash(&d));
507 }
508
509 #[cfg(feature = "parser")]
514 #[test]
515 fn parse_exclusion_margin_valid_minimal() {
516 assert_eq!(
517 parse_style_exclusion_margin("0").unwrap(),
518 StyleExclusionMargin::default()
519 );
520 assert_eq!(parse_style_exclusion_margin("1").unwrap().inner.get(), 1.0);
521 }
522
523 #[cfg(feature = "parser")]
524 #[test]
525 fn parse_exclusion_margin_empty_and_whitespace_only() {
526 for input in ["", " ", " ", "\t\n", "\r\n\t ", "\u{00A0}"] {
527 assert!(
528 parse_style_exclusion_margin(input).is_err(),
529 "{input:?} must not parse"
530 );
531 }
532 }
533
534 #[cfg(feature = "parser")]
535 #[test]
536 fn parse_exclusion_margin_garbage_is_rejected() {
537 for input in [
538 "abc", "px", "10px", "1,5", "1_000", "0x10", "--5", "5-", "+-1", ".", "-", "+", "e5",
539 "1e", "1.2.3", "null", "None", "{}", "()", "/*10*/", "10 20", "\0", "\u{7}\u{1b}[0m",
540 ] {
541 assert!(
542 parse_style_exclusion_margin(input).is_err(),
543 "{input:?} must be rejected"
544 );
545 }
546 }
547
548 #[cfg(feature = "parser")]
549 #[test]
550 fn parse_exclusion_margin_leading_trailing_junk() {
551 assert_eq!(
553 parse_style_exclusion_margin(" 10.5 ").unwrap().inner.get(),
554 10.5
555 );
556 assert_eq!(
557 parse_style_exclusion_margin("\n\t-3.25\t\n")
558 .unwrap()
559 .inner
560 .get(),
561 -3.25
562 );
563 for input in ["10.5px", "10.5;garbage", "10.5 !important", "10.5;", "10.5%"] {
565 assert!(
566 parse_style_exclusion_margin(input).is_err(),
567 "{input:?} must be rejected, not truncated to a number"
568 );
569 }
570 }
571
572 #[cfg(feature = "parser")]
573 #[test]
574 fn parse_exclusion_margin_boundary_numbers() {
575 for input in ["0", "-0", "+0", "0.0", "-0.0", "0e0"] {
577 let m = parse_style_exclusion_margin(input).unwrap();
578 assert_eq!(m.inner.number, 0, "{input:?} should encode to zero");
579 assert!(m.is_initial());
580 }
581
582 for input in ["0.0004", "-0.0004", "1e-30", "-1e-30"] {
584 let m = parse_style_exclusion_margin(input).unwrap();
585 assert_eq!(m.inner.number, 0, "{input:?} should truncate to zero");
586 }
587
588 for input in [
591 i64::MAX.to_string(),
592 i64::MIN.to_string(),
593 f32::MAX.to_string(),
594 format!("{}", f32::MIN),
595 "1e38".to_string(),
596 "-1e38".to_string(),
597 ] {
598 let m = parse_style_exclusion_margin(&input).unwrap();
599 assert!(
600 m.inner.get().is_finite(),
601 "{input:?} must decode to a finite value, got {}",
602 m.inner.get()
603 );
604 }
605
606 assert_eq!(
607 parse_style_exclusion_margin(&f32::MAX.to_string())
608 .unwrap()
609 .inner
610 .number,
611 isize::MAX
612 );
613 assert_eq!(
614 parse_style_exclusion_margin(&format!("{}", f32::MIN))
615 .unwrap()
616 .inner
617 .number,
618 isize::MIN
619 );
620 }
621
622 #[cfg(feature = "parser")]
623 #[test]
624 fn parse_exclusion_margin_nan_and_infinity_never_escape() {
625 let nan = parse_style_exclusion_margin("NaN").unwrap();
628 assert_eq!(nan.inner.number, 0);
629 assert!(!nan.inner.get().is_nan());
630 assert!(nan.is_initial());
631 assert_eq!(parse_style_exclusion_margin("nan").unwrap().inner.number, 0);
632 assert_eq!(parse_style_exclusion_margin("-NaN").unwrap().inner.number, 0);
633
634 for input in ["inf", "infinity", "+inf", "INF", "Infinity", "1e400"] {
635 let m = parse_style_exclusion_margin(input).unwrap();
636 assert_eq!(m.inner.number, isize::MAX, "{input:?} must saturate");
637 assert!(m.inner.get().is_finite());
638 }
639 for input in ["-inf", "-infinity", "-INF", "-1e400"] {
640 let m = parse_style_exclusion_margin(input).unwrap();
641 assert_eq!(m.inner.number, isize::MIN, "{input:?} must saturate");
642 assert!(m.inner.get().is_finite());
643 }
644 }
645
646 #[cfg(feature = "parser")]
647 #[test]
648 fn parse_exclusion_margin_unicode_input() {
649 for input in [
651 "\u{1F600}", "\u{FF15}", "1\u{0301}", "\u{202E}10.5", "\u{FEFF}10.5", "١٢٣", "10.5\u{1F4A9}", ] {
659 assert!(
660 parse_style_exclusion_margin(input).is_err(),
661 "{input:?} must be rejected"
662 );
663 }
664
665 let r = parse_style_exclusion_margin("\u{00A0}10.5\u{2003}");
668 assert!(r.is_err() || r.unwrap().inner.get() == 10.5);
669 }
670
671 #[cfg(feature = "parser")]
672 #[test]
673 fn parse_exclusion_margin_extremely_long_input() {
674 let huge = "9".repeat(1_000_000);
676 let m = parse_style_exclusion_margin(&huge).unwrap();
677 assert_eq!(m.inner.number, isize::MAX);
678 assert!(m.inner.get().is_finite());
679
680 let long_fraction = format!("1.{}", "0".repeat(1_000_000));
682 assert_eq!(
683 parse_style_exclusion_margin(&long_fraction).unwrap().inner.get(),
684 1.0
685 );
686
687 assert!(parse_style_exclusion_margin(&"z".repeat(1_000_000)).is_err());
689 assert!(parse_style_exclusion_margin(&" ".repeat(1_000_000)).is_err());
690 }
691
692 #[cfg(feature = "parser")]
693 #[test]
694 fn parse_exclusion_margin_deeply_nested_input_does_not_stack_overflow() {
695 let nested = format!("{}{}", "(".repeat(10_000), ")".repeat(10_000));
696 assert!(parse_style_exclusion_margin(&nested).is_err());
697
698 let nested_number = format!("{}1{}", "(".repeat(10_000), ")".repeat(10_000));
699 assert!(parse_style_exclusion_margin(&nested_number).is_err());
700
701 assert!(parse_style_exclusion_margin(&"-".repeat(10_000)).is_err());
702 }
703
704 #[cfg(feature = "parser")]
705 #[test]
706 fn parse_exclusion_margin_round_trips_through_css_and_rust_code() {
707 for input in ["0", "1", "10.5", "-3.25", "0.001", "123.456", "-0.5", "1000"] {
708 let parsed = parse_style_exclusion_margin(input).unwrap();
709
710 let printed = parsed.print_as_css_value();
712 let reparsed = parse_style_exclusion_margin(&printed).unwrap();
713 assert_eq!(
714 parsed, reparsed,
715 "{input:?} printed as {printed:?} did not round-trip"
716 );
717 assert_eq!(printed, reparsed.print_as_css_value());
718
719 assert!(parsed.format_as_rust_code(0).contains(&printed));
721 }
722 }
723
724 #[cfg(feature = "parser")]
729 #[test]
730 fn exclusion_margin_error_to_contained_carries_the_message() {
731 let err = parse_style_exclusion_margin("garbage").unwrap_err();
732 let StyleExclusionMarginParseErrorOwned::FloatValue(msg) = err.to_contained();
733 assert!(!msg.as_str().is_empty());
734 assert_eq!(format!("{err:?}"), format!("{err}"));
737 assert!(format!("{err}").contains("-azul-exclusion-margin"));
738 assert!(format!("{err}").contains(msg.as_str()));
739 }
740
741 #[cfg(feature = "parser")]
742 #[test]
743 fn exclusion_margin_error_to_shared_is_lossy_but_total() {
744 let empty_err_msg: AzString = format!("{}", "".parse::<f32>().unwrap_err()).into();
749
750 for msg in [
751 String::new(),
752 "invalid float literal".to_string(),
753 "\u{1F600}".to_string(),
754 "x".repeat(1_000_000),
755 ] {
756 let owned = StyleExclusionMarginParseErrorOwned::FloatValue(msg.clone().into());
757 let shared = owned.to_shared();
758 assert_eq!(
759 shared.to_contained(),
760 StyleExclusionMarginParseErrorOwned::FloatValue(empty_err_msg.clone()),
761 "to_shared() should normalise {msg:?} onto the empty-string error"
762 );
763 }
764
765 let empty = parse_style_exclusion_margin("").unwrap_err();
769 assert_eq!(empty.to_contained().to_shared(), empty);
770 assert_eq!(empty.to_contained().to_shared().to_contained(), empty.to_contained());
771 }
772
773 #[cfg(feature = "parser")]
778 #[test]
779 fn parse_hyphenation_language_valid_minimal() {
780 let lang = parse_style_hyphenation_language("en-US").unwrap();
781 assert_eq!(lang.inner.as_str(), "en-US");
782 assert!(lang.is_initial());
783 assert_eq!(parse_style_hyphenation_language("a").unwrap().inner.as_str(), "a");
784 }
785
786 #[cfg(feature = "parser")]
787 #[test]
788 fn parse_hyphenation_language_empty_and_whitespace_only() {
789 for input in ["", " ", " ", "\t\n", "\r\n\t ", "\"\"", "''", "\" \""] {
790 assert!(
791 parse_style_hyphenation_language(input).is_err(),
792 "{input:?} must not parse"
793 );
794 }
795 }
796
797 #[cfg(feature = "parser")]
798 #[test]
799 fn parse_hyphenation_language_garbage_is_rejected() {
800 for input in [
801 "en_US", "-en", "en-", "en US", "\" en-US \"", "en;US",
807 "en/US",
808 "en.US",
809 "en*",
810 "<script>",
811 "en\0US", "en\nUS",
813 "'en-US\"", "\"en-US'",
815 "\"en-US", "en-US\"",
817 ] {
818 assert!(
819 matches!(
820 parse_style_hyphenation_language(input),
821 Err(StyleHyphenationLanguageParseError::InvalidString(_))
822 ),
823 "{input:?} must be rejected"
824 );
825 }
826 }
827
828 #[cfg(feature = "parser")]
829 #[test]
830 fn parse_hyphenation_language_leading_trailing_junk() {
831 assert_eq!(
833 parse_style_hyphenation_language(" en-US ").unwrap().inner.as_str(),
834 "en-US"
835 );
836 assert_eq!(
837 parse_style_hyphenation_language("\t\"de-DE\"\n").unwrap().inner.as_str(),
838 "de-DE"
839 );
840 for input in ["en-US;", "en-US !important", "\"en-US\";", "en-US /*c*/"] {
842 assert!(
843 parse_style_hyphenation_language(input).is_err(),
844 "{input:?} must be rejected, not truncated"
845 );
846 }
847 }
848
849 #[cfg(feature = "parser")]
850 #[test]
851 fn parse_hyphenation_language_unicode_input() {
852 for input in [
853 "日本語",
854 "\u{1F600}",
855 "\"\u{1F600}\"",
856 "e\u{0301}n-US", "en-US\u{200B}", "\u{FEFF}en-US", "en-US", "ру-RU",
861 ] {
862 assert!(
863 matches!(
864 parse_style_hyphenation_language(input),
865 Err(StyleHyphenationLanguageParseError::InvalidString(_))
866 ),
867 "{input:?} must be rejected without panicking"
868 );
869 }
870
871 let r = parse_style_hyphenation_language("\"日本語\"");
873 assert!(r.is_err());
874 }
875
876 #[cfg(feature = "parser")]
877 #[test]
878 fn parse_hyphenation_language_accepts_any_ascii_alphanumeric_tag() {
879 for input in ["0", "123", "NaN", "inf", "zzzzzz", "sr-Latn-RS", "x-private"] {
883 let lang = parse_style_hyphenation_language(input).unwrap();
884 assert_eq!(lang.inner.as_str(), input);
885 }
886 assert_eq!(
887 parse_style_hyphenation_language(&i64::MAX.to_string())
888 .unwrap()
889 .inner
890 .as_str(),
891 i64::MAX.to_string()
892 );
893 assert!(parse_style_hyphenation_language(&i64::MIN.to_string()).is_err());
895 assert!(parse_style_hyphenation_language("-0").is_err());
896 }
897
898 #[cfg(feature = "parser")]
899 #[test]
900 fn parse_hyphenation_language_extremely_long_input() {
901 let huge = "a".repeat(1_000_000);
902 assert_eq!(
903 parse_style_hyphenation_language(&huge).unwrap().inner.as_str(),
904 huge
905 );
906
907 let huge_quoted = format!("\"{huge}\"");
908 assert_eq!(
909 parse_style_hyphenation_language(&huge_quoted)
910 .unwrap()
911 .inner
912 .as_str(),
913 huge
914 );
915
916 assert!(parse_style_hyphenation_language(&"-".repeat(1_000_000)).is_err());
918 assert!(parse_style_hyphenation_language(&"é".repeat(1_000_000)).is_err());
920 }
921
922 #[cfg(feature = "parser")]
923 #[test]
924 fn parse_hyphenation_language_deeply_nested_input_does_not_stack_overflow() {
925 let nested = format!("{}en-US{}", "(".repeat(10_000), ")".repeat(10_000));
926 assert!(parse_style_hyphenation_language(&nested).is_err());
927
928 let nested_quotes = format!("{}en-US{}", "\"".repeat(10_000), "\"".repeat(10_000));
929 assert!(parse_style_hyphenation_language(&nested_quotes).is_err());
930 }
931
932 #[cfg(feature = "parser")]
933 #[test]
934 fn parse_hyphenation_language_round_trips_through_css_and_rust_code() {
935 for input in ["en-US", "de-DE", "zh", "sr-Latn-RS", "en--US", "x-private", "0"] {
936 let parsed = parse_style_hyphenation_language(input).unwrap();
937
938 let printed = parsed.print_as_css_value();
941 assert_eq!(printed, format!("\"{input}\""));
942 let reparsed = parse_style_hyphenation_language(&printed).unwrap();
943 assert_eq!(parsed, reparsed, "{input:?} did not round-trip");
944 assert_eq!(printed, reparsed.print_as_css_value());
945
946 assert_eq!(
948 parse_style_hyphenation_language(&format!("'{input}'")).unwrap(),
949 parsed
950 );
951
952 assert!(parsed.format_as_rust_code(0).contains(input));
953 }
954 }
955
956 #[cfg(feature = "parser")]
961 #[test]
962 fn hyphenation_error_to_contained_carries_the_offending_string() {
963 let err = parse_style_hyphenation_language("en_US").unwrap_err();
964 let StyleHyphenationLanguageParseErrorOwned::InvalidString(msg) = err.to_contained();
965 assert_eq!(msg.as_str(), "en_US");
966 assert_eq!(format!("{err:?}"), format!("{err}"));
967 assert!(format!("{err}").contains("-azul-hyphenation-language"));
968 assert!(format!("{err}").contains("en_US"));
969 }
970
971 #[cfg(feature = "parser")]
972 #[test]
973 fn hyphenation_error_round_trips_losslessly() {
974 for msg in [
975 String::new(),
976 "en_US".to_string(),
977 "\u{1F600}".to_string(),
978 "\0".to_string(),
979 "x".repeat(1_000_000),
980 ] {
981 let shared = StyleHyphenationLanguageParseError::InvalidString(msg.clone());
982 let owned = shared.to_contained();
983 assert_eq!(owned.to_shared(), shared, "{msg:?} lost data on round-trip");
984 assert_eq!(owned.to_shared().to_contained(), owned);
985
986 let owned_direct =
987 StyleHyphenationLanguageParseErrorOwned::InvalidString(msg.clone().into());
988 assert_eq!(owned_direct, owned);
989 let StyleHyphenationLanguageParseErrorOwned::InvalidString(inner) = owned_direct;
990 assert_eq!(inner.as_str(), msg);
991 }
992 }
993
994 #[cfg(feature = "parser")]
995 #[test]
996 fn hyphenation_error_for_empty_input_reports_the_unquoted_string() {
997 let err = parse_style_hyphenation_language("\"\"").unwrap_err();
999 assert_eq!(
1000 err.to_contained(),
1001 StyleHyphenationLanguageParseErrorOwned::InvalidString(AzString::from_const_str(""))
1002 );
1003 assert!(err.to_contained().to_shared() == err);
1004 }
1005
1006 #[cfg(feature = "parser")]
1019 #[test]
1020 fn parse_hyphenation_language_lone_quote_must_not_panic() {
1021 for input in ["\"", "'", " \" ", "\t'\n"] {
1022 assert!(
1023 parse_style_hyphenation_language(input).is_err(),
1024 "{input:?} must return Err, not panic"
1025 );
1026 }
1027 }
1028}