1use std::num::ParseFloatError;
7
8#[cfg(feature = "parser")]
9use crate::macros::*;
10use crate::{
11 codegen::format::FormatAsRustCode,
12 corety::AzString,
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]
46 pub const fn is_initial(&self) -> bool {
47 self.inner.number == 0
48 }
49}
50
51impl PrintAsCssValue for StyleExclusionMargin {
52 fn print_as_css_value(&self) -> String {
53 format!("{}", self.inner.get())
54 }
55}
56
57impl FormatAsCssValue for StyleExclusionMargin {
58 fn format_as_css_value(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59 write!(f, "{}", self.inner.get())
60 }
61}
62
63impl FormatAsRustCode for StyleExclusionMargin {
64 fn format_as_rust_code(&self, _tabs: usize) -> String {
65 format!(
66 "StyleExclusionMargin {{ inner: FloatValue::const_new({}) }}",
67 self.inner.get()
68 )
69 }
70}
71
72#[cfg(feature = "parser")]
73#[derive(Clone, PartialEq, Eq)]
74pub enum StyleExclusionMarginParseError {
75 FloatValue(ParseFloatError),
76}
77
78#[cfg(feature = "parser")]
79impl_debug_as_display!(StyleExclusionMarginParseError);
80
81#[cfg(feature = "parser")]
82impl_display! { StyleExclusionMarginParseError, {
83 FloatValue(e) => format!("Invalid -azul-exclusion-margin value: {}", e),
84}}
85
86#[cfg(feature = "parser")]
87impl_from!(ParseFloatError, StyleExclusionMarginParseError::FloatValue);
88
89#[cfg(feature = "parser")]
90#[derive(Debug, Clone, PartialEq, Eq)]
91#[repr(C, u8)]
92pub enum StyleExclusionMarginParseErrorOwned {
93 FloatValue(AzString),
94}
95
96#[cfg(feature = "parser")]
97impl StyleExclusionMarginParseError {
98 #[must_use]
99 pub fn to_contained(&self) -> StyleExclusionMarginParseErrorOwned {
100 match self {
101 Self::FloatValue(e) => {
102 StyleExclusionMarginParseErrorOwned::FloatValue(format!("{e}").into())
103 }
104 }
105 }
106}
107
108#[cfg(feature = "parser")]
109impl StyleExclusionMarginParseErrorOwned {
110 #[must_use]
111 pub fn to_shared(&self) -> StyleExclusionMarginParseError {
112 match self {
113 Self::FloatValue(_) => {
114 StyleExclusionMarginParseError::FloatValue("".parse::<f32>().unwrap_err())
117 }
118 }
119 }
120}
121
122#[cfg(feature = "parser")]
123pub fn parse_style_exclusion_margin(
127 input: &str,
128) -> Result<StyleExclusionMargin, StyleExclusionMarginParseError> {
129 parse_float_value(input)
130 .map(|inner| StyleExclusionMargin { inner })
131 .map_err(StyleExclusionMarginParseError::FloatValue)
132}
133
134#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
146#[repr(C)]
147pub struct StyleHyphenationLanguage {
148 pub inner: AzString,
149}
150
151impl Default for StyleHyphenationLanguage {
152 fn default() -> Self {
153 Self {
154 inner: AzString::from_const_str("en-US"),
155 }
156 }
157}
158
159impl StyleHyphenationLanguage {
160 #[must_use]
161 pub fn is_initial(&self) -> bool {
162 self.inner.as_str() == "en-US"
163 }
164}
165
166impl PrintAsCssValue for StyleHyphenationLanguage {
167 fn print_as_css_value(&self) -> String {
168 format!("\"{}\"", self.inner.as_str())
169 }
170}
171
172impl FormatAsCssValue for StyleHyphenationLanguage {
173 fn format_as_css_value(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
174 write!(f, "\"{}\"", self.inner.as_str())
175 }
176}
177
178impl FormatAsRustCode for StyleHyphenationLanguage {
179 fn format_as_rust_code(&self, _tabs: usize) -> String {
180 format!(
181 "StyleHyphenationLanguage {{ inner: AzString::from_const_str(\"{}\") }}",
182 self.inner.as_str()
183 )
184 }
185}
186
187#[cfg(feature = "parser")]
188#[derive(Clone, PartialEq, Eq)]
189pub enum StyleHyphenationLanguageParseError {
190 InvalidString(String),
191}
192
193#[cfg(feature = "parser")]
194impl_debug_as_display!(StyleHyphenationLanguageParseError);
195
196#[cfg(feature = "parser")]
197impl_display! { StyleHyphenationLanguageParseError, {
198 InvalidString(e) => format!("Invalid -azul-hyphenation-language value: {}", e),
199}}
200
201#[cfg(feature = "parser")]
202#[derive(Debug, Clone, PartialEq, Eq)]
203#[repr(C, u8)]
204pub enum StyleHyphenationLanguageParseErrorOwned {
205 InvalidString(AzString),
206}
207
208#[cfg(feature = "parser")]
209impl StyleHyphenationLanguageParseError {
210 #[must_use]
211 pub fn to_contained(&self) -> StyleHyphenationLanguageParseErrorOwned {
212 match self {
213 Self::InvalidString(e) => {
214 StyleHyphenationLanguageParseErrorOwned::InvalidString(e.clone().into())
215 }
216 }
217 }
218}
219
220#[cfg(feature = "parser")]
221impl StyleHyphenationLanguageParseErrorOwned {
222 #[must_use]
223 pub fn to_shared(&self) -> StyleHyphenationLanguageParseError {
224 match self {
225 Self::InvalidString(e) => {
226 StyleHyphenationLanguageParseError::InvalidString(e.to_string())
227 }
228 }
229 }
230}
231
232#[cfg(feature = "parser")]
233pub fn parse_style_hyphenation_language(
237 input: &str,
238) -> Result<StyleHyphenationLanguage, StyleHyphenationLanguageParseError> {
239 let trimmed = input.trim();
243 let unquoted = if trimmed.len() >= 2
244 && ((trimmed.starts_with('"') && trimmed.ends_with('"'))
245 || (trimmed.starts_with('\'') && trimmed.ends_with('\'')))
246 {
247 &trimmed[1..trimmed.len() - 1]
248 } else {
249 trimmed
250 };
251
252 if unquoted.is_empty()
254 || !unquoted
255 .bytes()
256 .all(|b| b.is_ascii_alphanumeric() || b == b'-')
257 || unquoted.starts_with('-')
258 || unquoted.ends_with('-')
259 {
260 return Err(StyleHyphenationLanguageParseError::InvalidString(
261 unquoted.to_string(),
262 ));
263 }
264
265 Ok(StyleHyphenationLanguage {
266 inner: AzString::from_string(unquoted.to_string()),
267 })
268}
269
270#[cfg(test)]
271mod tests {
272 #![allow(clippy::float_cmp)]
274 use super::*;
275
276 #[test]
277 fn test_parse_exclusion_margin() {
278 let margin = parse_style_exclusion_margin("10.5").unwrap();
279 assert_eq!(margin.inner.get(), 10.5);
280
281 let margin = parse_style_exclusion_margin("0").unwrap();
282 assert_eq!(margin.inner.get(), 0.0);
283 }
284
285 #[test]
286 fn test_parse_hyphenation_language() {
287 let lang = parse_style_hyphenation_language("\"en-US\"").unwrap();
288 assert_eq!(lang.inner.as_str(), "en-US");
289
290 let lang = parse_style_hyphenation_language("'de-DE'").unwrap();
291 assert_eq!(lang.inner.as_str(), "de-DE");
292
293 let lang = parse_style_hyphenation_language("fr-FR").unwrap();
294 assert_eq!(lang.inner.as_str(), "fr-FR");
295
296 let lang = parse_style_hyphenation_language("zh").unwrap();
297 assert_eq!(lang.inner.as_str(), "zh");
298
299 let lang = parse_style_hyphenation_language("sr-Latn-RS").unwrap();
300 assert_eq!(lang.inner.as_str(), "sr-Latn-RS");
301
302 let lang = parse_style_hyphenation_language("en--US").unwrap();
304 assert_eq!(lang.inner.as_str(), "en--US");
305 }
306
307 #[test]
308 fn test_parse_hyphenation_language_invalid() {
309 assert!(matches!(
310 parse_style_hyphenation_language(""),
311 Err(StyleHyphenationLanguageParseError::InvalidString(_))
312 ));
313 assert!(matches!(
314 parse_style_hyphenation_language("-en"),
315 Err(StyleHyphenationLanguageParseError::InvalidString(_))
316 ));
317 assert!(matches!(
318 parse_style_hyphenation_language("en-"),
319 Err(StyleHyphenationLanguageParseError::InvalidString(_))
320 ));
321 assert!(matches!(
322 parse_style_hyphenation_language("en_US"),
323 Err(StyleHyphenationLanguageParseError::InvalidString(_))
324 ));
325 assert!(matches!(
326 parse_style_hyphenation_language("日本語"),
327 Err(StyleHyphenationLanguageParseError::InvalidString(_))
328 ));
329 }
330
331 #[test]
332 fn test_exclusion_margin_default() {
333 let margin = StyleExclusionMargin::default();
334 assert_eq!(margin.inner.get(), 0.0);
335 assert!(margin.is_initial());
336 }
337
338 #[test]
339 fn test_hyphenation_language_default() {
340 let lang = StyleHyphenationLanguage::default();
341 assert_eq!(lang.inner.as_str(), "en-US");
342 }
343}
344
345#[cfg(test)]
346mod autotest_generated {
347 #![allow(clippy::float_cmp)]
352
353 use super::*;
354
355 struct AsCss<'a, T: FormatAsCssValue>(&'a T);
357
358 impl<T: FormatAsCssValue> std::fmt::Display for AsCss<'_, T> {
359 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
360 self.0.format_as_css_value(f)
361 }
362 }
363
364 #[test]
369 fn exclusion_margin_is_initial_true_and_false() {
370 assert!(StyleExclusionMargin::default().is_initial());
371 assert!(StyleExclusionMargin {
372 inner: FloatValue::const_new(0)
373 }
374 .is_initial());
375 assert!(!StyleExclusionMargin {
376 inner: FloatValue::const_new(1)
377 }
378 .is_initial());
379 assert!(!StyleExclusionMargin {
380 inner: FloatValue::const_new(-1)
381 }
382 .is_initial());
383 }
384
385 #[test]
386 fn exclusion_margin_is_initial_on_boundary_encodings() {
387 for v in [0.0_f32, -0.0, 0.0004, -0.0004, f32::MIN_POSITIVE, 1e-30] {
389 let m = StyleExclusionMargin {
390 inner: FloatValue::new(v),
391 };
392 assert!(m.is_initial(), "{v} should encode to the initial value");
393 assert_eq!(m.inner.get(), 0.0);
394 }
395
396 let nan = StyleExclusionMargin {
398 inner: FloatValue::new(f32::NAN),
399 };
400 assert!(nan.is_initial());
401 assert!(!nan.inner.get().is_nan());
402
403 for v in [f32::INFINITY, f32::NEG_INFINITY, f32::MAX, f32::MIN] {
405 let m = StyleExclusionMargin {
406 inner: FloatValue::new(v),
407 };
408 assert!(!m.is_initial(), "{v} must not be reported as initial");
409 assert!(m.inner.get().is_finite());
410 }
411 }
412
413 #[test]
418 fn hyphenation_is_initial_true_and_false() {
419 assert!(StyleHyphenationLanguage::default().is_initial());
420 assert!(StyleHyphenationLanguage {
421 inner: AzString::from_const_str("en-US"),
422 }
423 .is_initial());
424
425 for not_initial in [
427 "",
428 " ",
429 "en-us",
430 "EN-US",
431 "en-US ",
432 "en",
433 "de-DE",
434 "en\u{0}US",
435 ] {
436 assert!(
437 !StyleHyphenationLanguage {
438 inner: AzString::from_string(not_initial.to_string()),
439 }
440 .is_initial(),
441 "{not_initial:?} must not be reported as initial"
442 );
443 }
444 }
445
446 #[test]
447 fn hyphenation_is_initial_on_extreme_strings_does_not_panic() {
448 for s in [
449 "\u{1F600}".to_string(),
450 "e\u{0301}n-US".to_string(), "en-US\u{200B}".to_string(), "a".repeat(1_000_000),
453 ] {
454 let lang = StyleHyphenationLanguage {
455 inner: AzString::from_string(s.clone()),
456 };
457 assert!(!lang.is_initial(), "{s:?} must not be reported as initial");
458 }
459 }
460
461 #[test]
466 fn exclusion_margin_print_and_format_agree() {
467 for v in [0.0_f32, 10.5, -3.25, 123.456, f32::INFINITY, f32::NAN] {
468 let m = StyleExclusionMargin {
469 inner: FloatValue::new(v),
470 };
471 let printed = m.print_as_css_value();
472 assert_eq!(printed, AsCss(&m).to_string());
473 assert!(!printed.contains("NaN") && !printed.contains("inf"));
475 assert!(m.format_as_rust_code(0).contains(&printed));
476 }
477 }
478
479 #[test]
480 fn hyphenation_print_and_format_agree() {
481 for s in ["en-US", "", "a", "\u{1F600}", "quote\"inside"] {
482 let lang = StyleHyphenationLanguage {
483 inner: AzString::from_string(s.to_string()),
484 };
485 let printed = lang.print_as_css_value();
486 assert_eq!(printed, AsCss(&lang).to_string());
487 assert_eq!(printed, format!("\"{s}\""));
488 assert!(lang.format_as_rust_code(0).contains(s));
489 }
490 }
491
492 #[test]
493 fn exclusion_margin_ord_and_hash_are_consistent_with_value() {
494 use std::{
495 collections::hash_map::DefaultHasher,
496 hash::{Hash, Hasher},
497 };
498
499 let hash = |m: &StyleExclusionMargin| {
500 let mut h = DefaultHasher::new();
501 m.hash(&mut h);
502 h.finish()
503 };
504
505 let a = StyleExclusionMargin {
506 inner: FloatValue::new(1.5),
507 };
508 let b = StyleExclusionMargin {
509 inner: FloatValue::new(1.5),
510 };
511 let c = StyleExclusionMargin {
512 inner: FloatValue::new(2.5),
513 };
514
515 assert_eq!(a, b);
516 assert_eq!(hash(&a), hash(&b));
517 assert!(a < c);
518 assert!(a.inner.get() < c.inner.get());
519
520 let d = StyleExclusionMargin {
522 inner: FloatValue::new(1.5004),
523 };
524 assert_eq!(a, d);
525 assert_eq!(hash(&a), hash(&d));
526 }
527
528 #[cfg(feature = "parser")]
533 #[test]
534 fn parse_exclusion_margin_valid_minimal() {
535 assert_eq!(
536 parse_style_exclusion_margin("0").unwrap(),
537 StyleExclusionMargin::default()
538 );
539 assert_eq!(parse_style_exclusion_margin("1").unwrap().inner.get(), 1.0);
540 }
541
542 #[cfg(feature = "parser")]
543 #[test]
544 fn parse_exclusion_margin_empty_and_whitespace_only() {
545 for input in ["", " ", " ", "\t\n", "\r\n\t ", "\u{00A0}"] {
546 assert!(
547 parse_style_exclusion_margin(input).is_err(),
548 "{input:?} must not parse"
549 );
550 }
551 }
552
553 #[cfg(feature = "parser")]
554 #[test]
555 fn parse_exclusion_margin_garbage_is_rejected() {
556 for input in [
557 "abc",
558 "px",
559 "10px",
560 "1,5",
561 "1_000",
562 "0x10",
563 "--5",
564 "5-",
565 "+-1",
566 ".",
567 "-",
568 "+",
569 "e5",
570 "1e",
571 "1.2.3",
572 "null",
573 "None",
574 "{}",
575 "()",
576 "/*10*/",
577 "10 20",
578 "\0",
579 "\u{7}\u{1b}[0m",
580 ] {
581 assert!(
582 parse_style_exclusion_margin(input).is_err(),
583 "{input:?} must be rejected"
584 );
585 }
586 }
587
588 #[cfg(feature = "parser")]
589 #[test]
590 fn parse_exclusion_margin_leading_trailing_junk() {
591 assert_eq!(
593 parse_style_exclusion_margin(" 10.5 ")
594 .unwrap()
595 .inner
596 .get(),
597 10.5
598 );
599 assert_eq!(
600 parse_style_exclusion_margin("\n\t-3.25\t\n")
601 .unwrap()
602 .inner
603 .get(),
604 -3.25
605 );
606 for input in [
608 "10.5px",
609 "10.5;garbage",
610 "10.5 !important",
611 "10.5;",
612 "10.5%",
613 ] {
614 assert!(
615 parse_style_exclusion_margin(input).is_err(),
616 "{input:?} must be rejected, not truncated to a number"
617 );
618 }
619 }
620
621 #[cfg(feature = "parser")]
622 #[test]
623 fn parse_exclusion_margin_boundary_numbers() {
624 for input in ["0", "-0", "+0", "0.0", "-0.0", "0e0"] {
626 let m = parse_style_exclusion_margin(input).unwrap();
627 assert_eq!(m.inner.number, 0, "{input:?} should encode to zero");
628 assert!(m.is_initial());
629 }
630
631 for input in ["0.0004", "-0.0004", "1e-30", "-1e-30"] {
633 let m = parse_style_exclusion_margin(input).unwrap();
634 assert_eq!(m.inner.number, 0, "{input:?} should truncate to zero");
635 }
636
637 for input in [
640 i64::MAX.to_string(),
641 i64::MIN.to_string(),
642 f32::MAX.to_string(),
643 format!("{}", f32::MIN),
644 "1e38".to_string(),
645 "-1e38".to_string(),
646 ] {
647 let m = parse_style_exclusion_margin(&input).unwrap();
648 assert!(
649 m.inner.get().is_finite(),
650 "{input:?} must decode to a finite value, got {}",
651 m.inner.get()
652 );
653 }
654
655 assert_eq!(
656 parse_style_exclusion_margin(&f32::MAX.to_string())
657 .unwrap()
658 .inner
659 .number,
660 isize::MAX
661 );
662 assert_eq!(
663 parse_style_exclusion_margin(&format!("{}", f32::MIN))
664 .unwrap()
665 .inner
666 .number,
667 isize::MIN
668 );
669 }
670
671 #[cfg(feature = "parser")]
672 #[test]
673 fn parse_exclusion_margin_nan_and_infinity_never_escape() {
674 let nan = parse_style_exclusion_margin("NaN").unwrap();
677 assert_eq!(nan.inner.number, 0);
678 assert!(!nan.inner.get().is_nan());
679 assert!(nan.is_initial());
680 assert_eq!(parse_style_exclusion_margin("nan").unwrap().inner.number, 0);
681 assert_eq!(
682 parse_style_exclusion_margin("-NaN").unwrap().inner.number,
683 0
684 );
685
686 for input in ["inf", "infinity", "+inf", "INF", "Infinity", "1e400"] {
687 let m = parse_style_exclusion_margin(input).unwrap();
688 assert_eq!(m.inner.number, isize::MAX, "{input:?} must saturate");
689 assert!(m.inner.get().is_finite());
690 }
691 for input in ["-inf", "-infinity", "-INF", "-1e400"] {
692 let m = parse_style_exclusion_margin(input).unwrap();
693 assert_eq!(m.inner.number, isize::MIN, "{input:?} must saturate");
694 assert!(m.inner.get().is_finite());
695 }
696 }
697
698 #[cfg(feature = "parser")]
699 #[test]
700 fn parse_exclusion_margin_unicode_input() {
701 for input in [
703 "\u{1F600}", "\u{FF15}", "1\u{0301}", "\u{202E}10.5", "\u{FEFF}10.5", "١٢٣", "10.5\u{1F4A9}", ] {
711 assert!(
712 parse_style_exclusion_margin(input).is_err(),
713 "{input:?} must be rejected"
714 );
715 }
716
717 let r = parse_style_exclusion_margin("\u{00A0}10.5\u{2003}");
720 assert!(r.is_err() || r.unwrap().inner.get() == 10.5);
721 }
722
723 #[cfg(feature = "parser")]
724 #[test]
725 fn parse_exclusion_margin_extremely_long_input() {
726 let huge = "9".repeat(1_000_000);
728 let m = parse_style_exclusion_margin(&huge).unwrap();
729 assert_eq!(m.inner.number, isize::MAX);
730 assert!(m.inner.get().is_finite());
731
732 let long_fraction = format!("1.{}", "0".repeat(1_000_000));
734 assert_eq!(
735 parse_style_exclusion_margin(&long_fraction)
736 .unwrap()
737 .inner
738 .get(),
739 1.0
740 );
741
742 assert!(parse_style_exclusion_margin(&"z".repeat(1_000_000)).is_err());
744 assert!(parse_style_exclusion_margin(&" ".repeat(1_000_000)).is_err());
745 }
746
747 #[cfg(feature = "parser")]
748 #[test]
749 fn parse_exclusion_margin_deeply_nested_input_does_not_stack_overflow() {
750 let nested = format!("{}{}", "(".repeat(10_000), ")".repeat(10_000));
751 assert!(parse_style_exclusion_margin(&nested).is_err());
752
753 let nested_number = format!("{}1{}", "(".repeat(10_000), ")".repeat(10_000));
754 assert!(parse_style_exclusion_margin(&nested_number).is_err());
755
756 assert!(parse_style_exclusion_margin(&"-".repeat(10_000)).is_err());
757 }
758
759 #[cfg(feature = "parser")]
760 #[test]
761 fn parse_exclusion_margin_round_trips_through_css_and_rust_code() {
762 for input in [
763 "0", "1", "10.5", "-3.25", "0.001", "123.456", "-0.5", "1000",
764 ] {
765 let parsed = parse_style_exclusion_margin(input).unwrap();
766
767 let printed = parsed.print_as_css_value();
769 let reparsed = parse_style_exclusion_margin(&printed).unwrap();
770 assert_eq!(
771 parsed, reparsed,
772 "{input:?} printed as {printed:?} did not round-trip"
773 );
774 assert_eq!(printed, reparsed.print_as_css_value());
775
776 assert!(parsed.format_as_rust_code(0).contains(&printed));
778 }
779 }
780
781 #[cfg(feature = "parser")]
786 #[test]
787 fn exclusion_margin_error_to_contained_carries_the_message() {
788 let err = parse_style_exclusion_margin("garbage").unwrap_err();
789 let StyleExclusionMarginParseErrorOwned::FloatValue(msg) = err.to_contained();
790 assert!(!msg.as_str().is_empty());
791 assert_eq!(format!("{err:?}"), format!("{err}"));
794 assert!(format!("{err}").contains("-azul-exclusion-margin"));
795 assert!(format!("{err}").contains(msg.as_str()));
796 }
797
798 #[cfg(feature = "parser")]
799 #[test]
800 fn exclusion_margin_error_to_shared_is_lossy_but_total() {
801 let empty_err_msg: AzString = format!("{}", "".parse::<f32>().unwrap_err()).into();
806
807 for msg in [
808 String::new(),
809 "invalid float literal".to_string(),
810 "\u{1F600}".to_string(),
811 "x".repeat(1_000_000),
812 ] {
813 let owned = StyleExclusionMarginParseErrorOwned::FloatValue(msg.clone().into());
814 let shared = owned.to_shared();
815 assert_eq!(
816 shared.to_contained(),
817 StyleExclusionMarginParseErrorOwned::FloatValue(empty_err_msg.clone()),
818 "to_shared() should normalise {msg:?} onto the empty-string error"
819 );
820 }
821
822 let empty = parse_style_exclusion_margin("").unwrap_err();
826 assert_eq!(empty.to_contained().to_shared(), empty);
827 assert_eq!(
828 empty.to_contained().to_shared().to_contained(),
829 empty.to_contained()
830 );
831 }
832
833 #[cfg(feature = "parser")]
838 #[test]
839 fn parse_hyphenation_language_valid_minimal() {
840 let lang = parse_style_hyphenation_language("en-US").unwrap();
841 assert_eq!(lang.inner.as_str(), "en-US");
842 assert!(lang.is_initial());
843 assert_eq!(
844 parse_style_hyphenation_language("a")
845 .unwrap()
846 .inner
847 .as_str(),
848 "a"
849 );
850 }
851
852 #[cfg(feature = "parser")]
853 #[test]
854 fn parse_hyphenation_language_empty_and_whitespace_only() {
855 for input in ["", " ", " ", "\t\n", "\r\n\t ", "\"\"", "''", "\" \""] {
856 assert!(
857 parse_style_hyphenation_language(input).is_err(),
858 "{input:?} must not parse"
859 );
860 }
861 }
862
863 #[cfg(feature = "parser")]
864 #[test]
865 fn parse_hyphenation_language_garbage_is_rejected() {
866 for input in [
867 "en_US", "-en", "en-", "en US", "\" en-US \"", "en;US",
873 "en/US",
874 "en.US",
875 "en*",
876 "<script>",
877 "en\0US", "en\nUS",
879 "'en-US\"", "\"en-US'",
881 "\"en-US", "en-US\"",
883 ] {
884 assert!(
885 matches!(
886 parse_style_hyphenation_language(input),
887 Err(StyleHyphenationLanguageParseError::InvalidString(_))
888 ),
889 "{input:?} must be rejected"
890 );
891 }
892 }
893
894 #[cfg(feature = "parser")]
895 #[test]
896 fn parse_hyphenation_language_leading_trailing_junk() {
897 assert_eq!(
899 parse_style_hyphenation_language(" en-US ")
900 .unwrap()
901 .inner
902 .as_str(),
903 "en-US"
904 );
905 assert_eq!(
906 parse_style_hyphenation_language("\t\"de-DE\"\n")
907 .unwrap()
908 .inner
909 .as_str(),
910 "de-DE"
911 );
912 for input in ["en-US;", "en-US !important", "\"en-US\";", "en-US /*c*/"] {
914 assert!(
915 parse_style_hyphenation_language(input).is_err(),
916 "{input:?} must be rejected, not truncated"
917 );
918 }
919 }
920
921 #[cfg(feature = "parser")]
922 #[test]
923 fn parse_hyphenation_language_unicode_input() {
924 for input in [
925 "日本語",
926 "\u{1F600}",
927 "\"\u{1F600}\"",
928 "e\u{0301}n-US", "en-US\u{200B}", "\u{FEFF}en-US", "en-US", "ру-RU",
933 ] {
934 assert!(
935 matches!(
936 parse_style_hyphenation_language(input),
937 Err(StyleHyphenationLanguageParseError::InvalidString(_))
938 ),
939 "{input:?} must be rejected without panicking"
940 );
941 }
942
943 let r = parse_style_hyphenation_language("\"日本語\"");
945 assert!(r.is_err());
946 }
947
948 #[cfg(feature = "parser")]
949 #[test]
950 fn parse_hyphenation_language_accepts_any_ascii_alphanumeric_tag() {
951 for input in [
955 "0",
956 "123",
957 "NaN",
958 "inf",
959 "zzzzzz",
960 "sr-Latn-RS",
961 "x-private",
962 ] {
963 let lang = parse_style_hyphenation_language(input).unwrap();
964 assert_eq!(lang.inner.as_str(), input);
965 }
966 assert_eq!(
967 parse_style_hyphenation_language(&i64::MAX.to_string())
968 .unwrap()
969 .inner
970 .as_str(),
971 i64::MAX.to_string()
972 );
973 assert!(parse_style_hyphenation_language(&i64::MIN.to_string()).is_err());
975 assert!(parse_style_hyphenation_language("-0").is_err());
976 }
977
978 #[cfg(feature = "parser")]
979 #[test]
980 fn parse_hyphenation_language_extremely_long_input() {
981 let huge = "a".repeat(1_000_000);
982 assert_eq!(
983 parse_style_hyphenation_language(&huge)
984 .unwrap()
985 .inner
986 .as_str(),
987 huge
988 );
989
990 let huge_quoted = format!("\"{huge}\"");
991 assert_eq!(
992 parse_style_hyphenation_language(&huge_quoted)
993 .unwrap()
994 .inner
995 .as_str(),
996 huge
997 );
998
999 assert!(parse_style_hyphenation_language(&"-".repeat(1_000_000)).is_err());
1001 assert!(parse_style_hyphenation_language(&"é".repeat(1_000_000)).is_err());
1003 }
1004
1005 #[cfg(feature = "parser")]
1006 #[test]
1007 fn parse_hyphenation_language_deeply_nested_input_does_not_stack_overflow() {
1008 let nested = format!("{}en-US{}", "(".repeat(10_000), ")".repeat(10_000));
1009 assert!(parse_style_hyphenation_language(&nested).is_err());
1010
1011 let nested_quotes = format!("{}en-US{}", "\"".repeat(10_000), "\"".repeat(10_000));
1012 assert!(parse_style_hyphenation_language(&nested_quotes).is_err());
1013 }
1014
1015 #[cfg(feature = "parser")]
1016 #[test]
1017 fn parse_hyphenation_language_round_trips_through_css_and_rust_code() {
1018 for input in [
1019 "en-US",
1020 "de-DE",
1021 "zh",
1022 "sr-Latn-RS",
1023 "en--US",
1024 "x-private",
1025 "0",
1026 ] {
1027 let parsed = parse_style_hyphenation_language(input).unwrap();
1028
1029 let printed = parsed.print_as_css_value();
1032 assert_eq!(printed, format!("\"{input}\""));
1033 let reparsed = parse_style_hyphenation_language(&printed).unwrap();
1034 assert_eq!(parsed, reparsed, "{input:?} did not round-trip");
1035 assert_eq!(printed, reparsed.print_as_css_value());
1036
1037 assert_eq!(
1039 parse_style_hyphenation_language(&format!("'{input}'")).unwrap(),
1040 parsed
1041 );
1042
1043 assert!(parsed.format_as_rust_code(0).contains(input));
1044 }
1045 }
1046
1047 #[cfg(feature = "parser")]
1052 #[test]
1053 fn hyphenation_error_to_contained_carries_the_offending_string() {
1054 let err = parse_style_hyphenation_language("en_US").unwrap_err();
1055 let StyleHyphenationLanguageParseErrorOwned::InvalidString(msg) = err.to_contained();
1056 assert_eq!(msg.as_str(), "en_US");
1057 assert_eq!(format!("{err:?}"), format!("{err}"));
1058 assert!(format!("{err}").contains("-azul-hyphenation-language"));
1059 assert!(format!("{err}").contains("en_US"));
1060 }
1061
1062 #[cfg(feature = "parser")]
1063 #[test]
1064 fn hyphenation_error_round_trips_losslessly() {
1065 for msg in [
1066 String::new(),
1067 "en_US".to_string(),
1068 "\u{1F600}".to_string(),
1069 "\0".to_string(),
1070 "x".repeat(1_000_000),
1071 ] {
1072 let shared = StyleHyphenationLanguageParseError::InvalidString(msg.clone());
1073 let owned = shared.to_contained();
1074 assert_eq!(owned.to_shared(), shared, "{msg:?} lost data on round-trip");
1075 assert_eq!(owned.to_shared().to_contained(), owned);
1076
1077 let owned_direct =
1078 StyleHyphenationLanguageParseErrorOwned::InvalidString(msg.clone().into());
1079 assert_eq!(owned_direct, owned);
1080 let StyleHyphenationLanguageParseErrorOwned::InvalidString(inner) = owned_direct;
1081 assert_eq!(inner.as_str(), msg);
1082 }
1083 }
1084
1085 #[cfg(feature = "parser")]
1086 #[test]
1087 fn hyphenation_error_for_empty_input_reports_the_unquoted_string() {
1088 let err = parse_style_hyphenation_language("\"\"").unwrap_err();
1090 assert_eq!(
1091 err.to_contained(),
1092 StyleHyphenationLanguageParseErrorOwned::InvalidString(AzString::from_const_str(""))
1093 );
1094 assert!(err.to_contained().to_shared() == err);
1095 }
1096
1097 #[cfg(feature = "parser")]
1110 #[test]
1111 fn parse_hyphenation_language_lone_quote_must_not_panic() {
1112 for input in ["\"", "'", " \" ", "\t'\n"] {
1113 assert!(
1114 parse_style_hyphenation_language(input).is_err(),
1115 "{input:?} must return Err, not panic"
1116 );
1117 }
1118 }
1119}