1use alloc::string::{String, ToString};
12
13use crate::{
14 codegen::format::FormatAsRustCode,
15 props::{
16 basic::pixel::{CssPixelValueParseError, PixelValue},
17 formatter::PrintAsCssValue,
18 },
19};
20
21#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
29#[repr(C)]
30#[derive(Default)]
31pub enum LayoutTableLayout {
32 #[default]
35 Auto,
36 Fixed,
40}
41
42
43impl PrintAsCssValue for LayoutTableLayout {
44 fn print_as_css_value(&self) -> String {
45 match self {
46 Self::Auto => "auto".to_string(),
47 Self::Fixed => "fixed".to_string(),
48 }
49 }
50}
51
52impl FormatAsRustCode for LayoutTableLayout {
53 fn format_as_rust_code(&self, _tabs: usize) -> String {
54 match self {
55 Self::Auto => "LayoutTableLayout::Auto".to_string(),
56 Self::Fixed => "LayoutTableLayout::Fixed".to_string(),
57 }
58 }
59}
60
61#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
69#[repr(C)]
70#[derive(Default)]
71pub enum StyleBorderCollapse {
72 #[default]
75 Separate,
76 Collapse,
79}
80
81
82impl PrintAsCssValue for StyleBorderCollapse {
83 fn print_as_css_value(&self) -> String {
84 match self {
85 Self::Separate => "separate".to_string(),
86 Self::Collapse => "collapse".to_string(),
87 }
88 }
89}
90
91impl FormatAsRustCode for StyleBorderCollapse {
92 fn format_as_rust_code(&self, _tabs: usize) -> String {
93 match self {
94 Self::Separate => "StyleBorderCollapse::Separate".to_string(),
95 Self::Collapse => "StyleBorderCollapse::Collapse".to_string(),
96 }
97 }
98}
99
100#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
111#[repr(C)]
112pub struct LayoutBorderSpacing {
113 pub horizontal: PixelValue,
115 pub vertical: PixelValue,
117}
118
119impl Default for LayoutBorderSpacing {
120 fn default() -> Self {
121 Self {
123 horizontal: PixelValue::const_px(0),
124 vertical: PixelValue::const_px(0),
125 }
126 }
127}
128
129impl LayoutBorderSpacing {
130 #[must_use] pub const fn new(spacing: PixelValue) -> Self {
132 Self {
133 horizontal: spacing,
134 vertical: spacing,
135 }
136 }
137
138 #[must_use] pub const fn new_separate(horizontal: PixelValue, vertical: PixelValue) -> Self {
140 Self {
141 horizontal,
142 vertical,
143 }
144 }
145}
146
147impl PrintAsCssValue for LayoutBorderSpacing {
148 fn print_as_css_value(&self) -> String {
149 if self.horizontal == self.vertical {
150 self.horizontal.to_string()
152 } else {
153 format!("{} {}", self.horizontal, self.vertical)
155 }
156 }
157}
158
159impl FormatAsRustCode for LayoutBorderSpacing {
160 fn format_as_rust_code(&self, _tabs: usize) -> String {
161 use crate::codegen::format::format_pixel_value;
162 format!(
163 "LayoutBorderSpacing {{ horizontal: {}, vertical: {} }}",
164 format_pixel_value(&self.horizontal),
165 format_pixel_value(&self.vertical)
166 )
167 }
168}
169
170#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
176#[repr(C)]
177#[derive(Default)]
178pub enum StyleCaptionSide {
179 #[default]
181 Top,
182 Bottom,
184}
185
186
187impl PrintAsCssValue for StyleCaptionSide {
188 fn print_as_css_value(&self) -> String {
189 match self {
190 Self::Top => "top".to_string(),
191 Self::Bottom => "bottom".to_string(),
192 }
193 }
194}
195
196impl FormatAsRustCode for StyleCaptionSide {
197 fn format_as_rust_code(&self, _tabs: usize) -> String {
198 match self {
199 Self::Top => "StyleCaptionSide::Top".to_string(),
200 Self::Bottom => "StyleCaptionSide::Bottom".to_string(),
201 }
202 }
203}
204
205#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
212#[repr(C)]
213#[derive(Default)]
214pub enum StyleEmptyCells {
215 #[default]
217 Show,
218 Hide,
220}
221
222
223impl PrintAsCssValue for StyleEmptyCells {
224 fn print_as_css_value(&self) -> String {
225 match self {
226 Self::Show => "show".to_string(),
227 Self::Hide => "hide".to_string(),
228 }
229 }
230}
231
232impl FormatAsRustCode for StyleEmptyCells {
233 fn format_as_rust_code(&self, _tabs: usize) -> String {
234 match self {
235 Self::Show => "StyleEmptyCells::Show".to_string(),
236 Self::Hide => "StyleEmptyCells::Hide".to_string(),
237 }
238 }
239}
240
241#[derive(Debug, Clone, PartialEq)]
245pub(crate) enum LayoutTableLayoutParseError<'a> {
246 InvalidKeyword(&'a str),
247}
248
249pub(crate) fn parse_table_layout(
251 input: &str,
252) -> Result<LayoutTableLayout, LayoutTableLayoutParseError<'_>> {
253 match input.trim() {
254 "auto" => Ok(LayoutTableLayout::Auto),
255 "fixed" => Ok(LayoutTableLayout::Fixed),
256 other => Err(LayoutTableLayoutParseError::InvalidKeyword(other)),
257 }
258}
259
260#[derive(Debug, Clone, PartialEq)]
262pub(crate) enum StyleBorderCollapseParseError<'a> {
263 InvalidKeyword(&'a str),
264}
265
266pub(crate) fn parse_border_collapse(
268 input: &str,
269) -> Result<StyleBorderCollapse, StyleBorderCollapseParseError<'_>> {
270 match input.trim() {
271 "separate" => Ok(StyleBorderCollapse::Separate),
272 "collapse" => Ok(StyleBorderCollapse::Collapse),
273 other => Err(StyleBorderCollapseParseError::InvalidKeyword(other)),
274 }
275}
276
277#[derive(Debug, Clone, PartialEq)]
279pub(crate) enum LayoutBorderSpacingParseError<'a> {
280 PixelValue(CssPixelValueParseError<'a>),
281 InvalidFormat,
282}
283
284pub(crate) fn parse_border_spacing(
287 input: &str,
288) -> Result<LayoutBorderSpacing, LayoutBorderSpacingParseError<'_>> {
289 use crate::props::basic::parse_pixel_value;
290
291 let parts: Vec<&str> = input.split_whitespace().collect();
292
293 match parts.len() {
294 1 => {
295 let value =
297 parse_pixel_value(parts[0]).map_err(LayoutBorderSpacingParseError::PixelValue)?;
298 Ok(LayoutBorderSpacing::new(value))
299 }
300 2 => {
301 let horizontal =
303 parse_pixel_value(parts[0]).map_err(LayoutBorderSpacingParseError::PixelValue)?;
304 let vertical =
305 parse_pixel_value(parts[1]).map_err(LayoutBorderSpacingParseError::PixelValue)?;
306 Ok(LayoutBorderSpacing::new_separate(horizontal, vertical))
307 }
308 _ => Err(LayoutBorderSpacingParseError::InvalidFormat),
309 }
310}
311
312#[derive(Debug, Clone, PartialEq)]
314pub(crate) enum StyleCaptionSideParseError<'a> {
315 InvalidKeyword(&'a str),
316}
317
318pub(crate) fn parse_caption_side(
320 input: &str,
321) -> Result<StyleCaptionSide, StyleCaptionSideParseError<'_>> {
322 match input.trim() {
323 "top" => Ok(StyleCaptionSide::Top),
324 "bottom" => Ok(StyleCaptionSide::Bottom),
325 other => Err(StyleCaptionSideParseError::InvalidKeyword(other)),
326 }
327}
328
329#[derive(Debug, Clone, PartialEq)]
331pub(crate) enum StyleEmptyCellsParseError<'a> {
332 InvalidKeyword(&'a str),
333}
334
335pub(crate) fn parse_empty_cells(
337 input: &str,
338) -> Result<StyleEmptyCells, StyleEmptyCellsParseError<'_>> {
339 match input.trim() {
340 "show" => Ok(StyleEmptyCells::Show),
341 "hide" => Ok(StyleEmptyCells::Hide),
342 other => Err(StyleEmptyCellsParseError::InvalidKeyword(other)),
343 }
344}
345
346#[cfg(test)]
347mod tests {
348 use super::*;
349
350 #[test]
351 fn test_parse_table_layout() {
352 assert_eq!(parse_table_layout("auto").unwrap(), LayoutTableLayout::Auto);
353 assert_eq!(
354 parse_table_layout("fixed").unwrap(),
355 LayoutTableLayout::Fixed
356 );
357 assert!(parse_table_layout("invalid").is_err());
358 }
359
360 #[test]
361 fn test_parse_border_collapse() {
362 assert_eq!(
363 parse_border_collapse("separate").unwrap(),
364 StyleBorderCollapse::Separate
365 );
366 assert_eq!(
367 parse_border_collapse("collapse").unwrap(),
368 StyleBorderCollapse::Collapse
369 );
370 assert!(parse_border_collapse("invalid").is_err());
371 }
372
373 #[test]
374 fn test_parse_border_spacing() {
375 let spacing1 = parse_border_spacing("5px").unwrap();
376 assert_eq!(spacing1.horizontal, PixelValue::const_px(5));
377 assert_eq!(spacing1.vertical, PixelValue::const_px(5));
378
379 let spacing2 = parse_border_spacing("5px 10px").unwrap();
380 assert_eq!(spacing2.horizontal, PixelValue::const_px(5));
381 assert_eq!(spacing2.vertical, PixelValue::const_px(10));
382 }
383
384 #[test]
385 fn test_parse_caption_side() {
386 assert_eq!(parse_caption_side("top").unwrap(), StyleCaptionSide::Top);
387 assert_eq!(
388 parse_caption_side("bottom").unwrap(),
389 StyleCaptionSide::Bottom
390 );
391 assert!(parse_caption_side("invalid").is_err());
392 }
393
394 #[test]
395 fn test_parse_empty_cells() {
396 assert_eq!(parse_empty_cells("show").unwrap(), StyleEmptyCells::Show);
397 assert_eq!(parse_empty_cells("hide").unwrap(), StyleEmptyCells::Hide);
398 assert!(parse_empty_cells("invalid").is_err());
399 }
400}
401
402#[cfg(test)]
403#[allow(clippy::float_cmp)] mod autotest_generated {
405 use crate::props::basic::SizeMetric;
406
407 use super::*;
408
409 fn raw(p: PixelValue) -> isize {
413 p.number.number()
414 }
415
416 fn table_layout_ok(s: &str) -> bool {
417 parse_table_layout(s).is_ok()
418 }
419 fn border_collapse_ok(s: &str) -> bool {
420 parse_border_collapse(s).is_ok()
421 }
422 fn caption_side_ok(s: &str) -> bool {
423 parse_caption_side(s).is_ok()
424 }
425 fn empty_cells_ok(s: &str) -> bool {
426 parse_empty_cells(s).is_ok()
427 }
428
429 type KeywordParser = (&'static str, fn(&str) -> bool);
432 const KEYWORD_PARSERS: &[KeywordParser] = &[
433 ("table-layout", table_layout_ok),
434 ("border-collapse", border_collapse_ok),
435 ("caption-side", caption_side_ok),
436 ("empty-cells", empty_cells_ok),
437 ];
438
439 fn assert_all_keyword_parsers_reject(input: &str, why: &str) {
440 for (prop, parse) in KEYWORD_PARSERS {
441 assert!(
442 !parse(input),
443 "{prop}: expected {input:?} to be rejected ({why}), but it parsed"
444 );
445 }
446 }
447
448 #[test]
453 fn new_applies_the_same_spacing_to_both_axes() {
454 let s = LayoutBorderSpacing::new(PixelValue::const_px(7));
455 assert_eq!(s.horizontal, PixelValue::const_px(7));
456 assert_eq!(s.vertical, PixelValue::const_px(7));
457 assert_eq!(s.horizontal, s.vertical);
458 }
459
460 #[test]
461 fn new_separate_preserves_argument_order() {
462 let s = LayoutBorderSpacing::new_separate(
465 PixelValue::const_px(3),
466 PixelValue::percent(50.0),
467 );
468 assert_eq!(s.horizontal, PixelValue::const_px(3));
469 assert_eq!(s.vertical, PixelValue::percent(50.0));
470 assert_ne!(s.horizontal, s.vertical);
471 }
472
473 #[test]
474 fn constructors_do_not_panic_on_extreme_floats() {
475 let extremes = [
476 0.0f32,
477 -0.0,
478 1.0,
479 -1.0,
480 f32::EPSILON,
481 f32::MIN_POSITIVE,
482 -f32::MIN_POSITIVE,
483 f32::MAX,
484 f32::MIN,
485 f32::INFINITY,
486 f32::NEG_INFINITY,
487 f32::NAN,
488 ];
489
490 for v in extremes {
491 let same = LayoutBorderSpacing::new(PixelValue::px(v));
492 assert_eq!(same.horizontal, same.vertical, "new({v}) must be symmetric");
493
494 for w in extremes {
495 let sep =
496 LayoutBorderSpacing::new_separate(PixelValue::px(v), PixelValue::em(w));
497 assert_eq!(sep.horizontal.metric, SizeMetric::Px);
498 assert_eq!(sep.vertical.metric, SizeMetric::Em);
499 assert!(sep.horizontal.number.get().is_finite());
502 assert!(sep.vertical.number.get().is_finite());
503 }
504 }
505 }
506
507 #[test]
508 fn nan_spacing_is_flattened_to_zero_and_stays_comparable() {
509 let nan = LayoutBorderSpacing::new(PixelValue::px(f32::NAN));
513 assert_eq!(raw(nan.horizontal), 0);
514 assert_eq!(raw(nan.vertical), 0);
515
516 assert_eq!(nan, nan);
518 assert_eq!(nan, LayoutBorderSpacing::new(PixelValue::px(f32::NAN)));
519 assert_eq!(nan, LayoutBorderSpacing::new(PixelValue::px(0.0)));
520 assert_eq!(nan, LayoutBorderSpacing::default());
521 }
522
523 #[test]
524 fn infinite_spacing_saturates_instead_of_wrapping() {
525 for input in [f32::INFINITY, f32::MAX, 1e38] {
528 let s = LayoutBorderSpacing::new(PixelValue::px(input));
529 assert_eq!(raw(s.horizontal), isize::MAX, "{input} must saturate high");
530 assert!(s.horizontal.number.get().is_finite());
531 }
532 for input in [f32::NEG_INFINITY, f32::MIN, -1e38] {
533 let s = LayoutBorderSpacing::new(PixelValue::px(input));
534 assert_eq!(raw(s.horizontal), isize::MIN, "{input} must saturate low");
535 assert!(s.horizontal.number.get().is_finite());
536 }
537 }
538
539 #[test]
540 fn equal_border_spacings_hash_equal() {
541 use std::{
542 collections::hash_map::DefaultHasher,
543 hash::{Hash, Hasher},
544 };
545
546 fn hash(s: LayoutBorderSpacing) -> u64 {
547 let mut h = DefaultHasher::new();
548 s.hash(&mut h);
549 h.finish()
550 }
551
552 assert_eq!(
555 hash(LayoutBorderSpacing::new(PixelValue::px(f32::NAN))),
556 hash(LayoutBorderSpacing::default())
557 );
558 assert_eq!(
559 hash(LayoutBorderSpacing::new(PixelValue::const_px(4))),
560 hash(LayoutBorderSpacing::new_separate(
561 PixelValue::const_px(4),
562 PixelValue::const_px(4)
563 ))
564 );
565 }
566
567 #[test]
568 fn defaults_are_the_css_initial_values() {
569 assert_eq!(LayoutTableLayout::default(), LayoutTableLayout::Auto);
570 assert_eq!(StyleBorderCollapse::default(), StyleBorderCollapse::Separate);
571 assert_eq!(StyleCaptionSide::default(), StyleCaptionSide::Top);
572 assert_eq!(StyleEmptyCells::default(), StyleEmptyCells::Show);
573
574 let d = LayoutBorderSpacing::default();
575 assert_eq!(d, LayoutBorderSpacing::new(PixelValue::const_px(0)));
576 assert_eq!(raw(d.horizontal), 0);
577 assert_eq!(raw(d.vertical), 0);
578 assert_eq!(d.horizontal.metric, SizeMetric::Px);
579 }
580
581 #[test]
586 fn keyword_parsers_accept_every_variant() {
587 assert_eq!(parse_table_layout("auto").unwrap(), LayoutTableLayout::Auto);
589 assert_eq!(parse_table_layout("fixed").unwrap(), LayoutTableLayout::Fixed);
590 assert_eq!(
591 parse_border_collapse("separate").unwrap(),
592 StyleBorderCollapse::Separate
593 );
594 assert_eq!(
595 parse_border_collapse("collapse").unwrap(),
596 StyleBorderCollapse::Collapse
597 );
598 assert_eq!(parse_caption_side("top").unwrap(), StyleCaptionSide::Top);
599 assert_eq!(parse_caption_side("bottom").unwrap(), StyleCaptionSide::Bottom);
600 assert_eq!(parse_empty_cells("show").unwrap(), StyleEmptyCells::Show);
601 assert_eq!(parse_empty_cells("hide").unwrap(), StyleEmptyCells::Hide);
602 }
603
604 #[test]
605 fn keyword_parsers_reject_empty_input() {
606 assert_all_keyword_parsers_reject("", "empty input");
607 assert_eq!(
608 parse_table_layout(""),
609 Err(LayoutTableLayoutParseError::InvalidKeyword(""))
610 );
611 assert_eq!(
612 parse_border_collapse(""),
613 Err(StyleBorderCollapseParseError::InvalidKeyword(""))
614 );
615 assert_eq!(
616 parse_caption_side(""),
617 Err(StyleCaptionSideParseError::InvalidKeyword(""))
618 );
619 assert_eq!(
620 parse_empty_cells(""),
621 Err(StyleEmptyCellsParseError::InvalidKeyword(""))
622 );
623 }
624
625 #[test]
626 fn keyword_parsers_reject_whitespace_only_input() {
627 for input in [" ", "\t", "\n", "\r\n", "\t\n \x0c", "\u{a0}", "\u{2028}"] {
628 assert_all_keyword_parsers_reject(input, "whitespace only");
629 }
630 assert_eq!(
633 parse_table_layout(" \t\n "),
634 Err(LayoutTableLayoutParseError::InvalidKeyword(""))
635 );
636 }
637
638 #[test]
639 fn keyword_parsers_trim_surrounding_whitespace() {
640 assert_eq!(
641 parse_table_layout(" auto\t\n").unwrap(),
642 LayoutTableLayout::Auto
643 );
644 assert_eq!(
645 parse_border_collapse("\r\n collapse ").unwrap(),
646 StyleBorderCollapse::Collapse
647 );
648 assert_eq!(
649 parse_caption_side("\t bottom \t").unwrap(),
650 StyleCaptionSide::Bottom
651 );
652 assert_eq!(parse_empty_cells("\n hide \n").unwrap(), StyleEmptyCells::Hide);
653 }
654
655 #[test]
656 fn keyword_parsers_also_trim_non_css_unicode_whitespace() {
657 assert_eq!(
662 parse_table_layout("\u{a0}auto\u{a0}").unwrap(),
663 LayoutTableLayout::Auto
664 );
665 assert_eq!(
666 parse_empty_cells("\u{2028}show\u{2029}").unwrap(),
667 StyleEmptyCells::Show
668 );
669 }
670
671 #[test]
672 fn keyword_parse_errors_carry_the_trimmed_input() {
673 assert_eq!(
676 parse_table_layout(" bogus "),
677 Err(LayoutTableLayoutParseError::InvalidKeyword("bogus"))
678 );
679 assert_eq!(
680 parse_border_collapse(" separate collapse "),
681 Err(StyleBorderCollapseParseError::InvalidKeyword(
682 "separate collapse"
683 ))
684 );
685 assert_eq!(
686 parse_caption_side("\ttop;\t"),
687 Err(StyleCaptionSideParseError::InvalidKeyword("top;"))
688 );
689 assert_eq!(
690 parse_empty_cells(" show hide "),
691 Err(StyleEmptyCellsParseError::InvalidKeyword("show hide"))
692 );
693 }
694
695 #[test]
696 fn keyword_parsers_are_case_sensitive() {
697 for input in [
701 "AUTO", "Auto", "aUtO", "FIXED", "SEPARATE", "Collapse", "TOP", "Bottom",
702 "SHOW", "Hide",
703 ] {
704 assert_all_keyword_parsers_reject(input, "keyword matching is case-sensitive");
705 }
706 }
707
708 #[test]
709 fn keyword_parsers_reject_garbage() {
710 for input in [
711 "invalid",
712 "auto fixed",
713 "auto;",
714 "auto;garbage",
715 "auto/*c*/",
716 "/*auto*/",
717 "au to",
718 "a\0uto",
719 "\0",
720 "\u{7f}",
721 "-->",
722 "<!--",
723 "!important",
724 "\\61 uto",
725 "'auto'",
726 "\"auto\"",
727 "auto()",
728 "url(auto)",
729 ] {
730 assert_all_keyword_parsers_reject(input, "garbage");
731 }
732 }
733
734 #[test]
735 fn keyword_parsers_reject_boundary_numeric_strings() {
736 for input in [
739 "0",
740 "-0",
741 "0.0",
742 "1",
743 "-1",
744 "9223372036854775807", "-9223372036854775808", "18446744073709551616", "1e400",
748 "-1e400",
749 "3.4028235e38",
750 "1.17549435e-38",
751 "NaN",
752 "nan",
753 "inf",
754 "-inf",
755 "infinity",
756 ] {
757 assert_all_keyword_parsers_reject(input, "numeric input for a keyword property");
758 }
759 }
760
761 #[test]
762 fn keyword_parsers_reject_leading_and_trailing_junk() {
763 assert_all_keyword_parsers_reject("xauto", "leading junk");
764 assert_all_keyword_parsers_reject("autox", "trailing junk");
765 assert_all_keyword_parsers_reject("auto!", "trailing junk");
766 assert_all_keyword_parsers_reject("(fixed)", "wrapped in parens");
767 assert_all_keyword_parsers_reject("collapse,", "trailing comma");
768 assert_eq!(parse_table_layout("auto ").unwrap(), LayoutTableLayout::Auto);
770 }
771
772 #[test]
773 fn keyword_parsers_survive_unicode_input() {
774 for input in [
775 "\u{1F600}", "auto\u{0301}", "\u{FF41}\u{FF55}\u{FF54}\u{FF4F}", "\u{202E}auto", "\u{FEFF}auto", "аuto", "🇩🇪", "e\u{0301}\u{0301}\u{0301}",
783 "\u{10FFFF}", ] {
785 assert_all_keyword_parsers_reject(input, "non-ASCII input");
786 }
787 assert_eq!(
789 parse_table_layout(" \u{1F600} "),
790 Err(LayoutTableLayoutParseError::InvalidKeyword("\u{1F600}"))
791 );
792 }
793
794 #[test]
795 fn keyword_parsers_survive_extremely_long_input() {
796 let long = "a".repeat(1_000_000);
797 assert_all_keyword_parsers_reject(&long, "1M-char input");
798 assert_eq!(
799 parse_table_layout(&long),
800 Err(LayoutTableLayoutParseError::InvalidKeyword(long.as_str()))
801 );
802
803 let repeated = "auto".repeat(250_000);
805 assert_all_keyword_parsers_reject(&repeated, "repeated valid token");
806
807 let padded = format!("{}auto{}", " ".repeat(500_000), "\n".repeat(500_000));
809 assert_eq!(parse_table_layout(&padded).unwrap(), LayoutTableLayout::Auto);
810 }
811
812 #[test]
813 fn keyword_parsers_survive_deeply_nested_input() {
814 let nested = format!("{}auto{}", "(".repeat(10_000), ")".repeat(10_000));
818 assert_all_keyword_parsers_reject(&nested, "10k nested brackets");
819
820 let braces = "{".repeat(50_000);
821 assert_all_keyword_parsers_reject(&braces, "50k open braces");
822 }
823
824 #[test]
829 fn border_spacing_single_value_applies_to_both_axes() {
830 let s = parse_border_spacing("5px").unwrap();
831 assert_eq!(s.horizontal, PixelValue::const_px(5));
832 assert_eq!(s.vertical, PixelValue::const_px(5));
833 assert_eq!(s, LayoutBorderSpacing::new(PixelValue::const_px(5)));
834 }
835
836 #[test]
837 fn border_spacing_two_values_are_horizontal_then_vertical() {
838 let s = parse_border_spacing("5px 10px").unwrap();
839 assert_eq!(s.horizontal, PixelValue::const_px(5));
840 assert_eq!(s.vertical, PixelValue::const_px(10));
841 assert_ne!(s, parse_border_spacing("10px 5px").unwrap());
843 }
844
845 #[test]
846 fn border_spacing_accepts_mixed_metrics_per_axis() {
847 let s = parse_border_spacing("1em 50%").unwrap();
848 assert_eq!(s.horizontal.metric, SizeMetric::Em);
849 assert_eq!(raw(s.horizontal), 1000);
850 assert_eq!(s.vertical.metric, SizeMetric::Percent);
851 assert_eq!(raw(s.vertical), 50_000);
852
853 assert_eq!(
855 parse_border_spacing("2rem").unwrap().horizontal.metric,
856 SizeMetric::Rem
857 );
858 assert_eq!(
860 parse_border_spacing("2vmax 3vmin").unwrap().horizontal.metric,
861 SizeMetric::Vmax
862 );
863 }
864
865 #[test]
866 fn border_spacing_rejects_empty_and_whitespace_only_input() {
867 for input in ["", " ", "\t\n", "\r\n\x0c ", "\u{a0}", "\u{2028}\u{2029}"] {
869 assert_eq!(
870 parse_border_spacing(input),
871 Err(LayoutBorderSpacingParseError::InvalidFormat),
872 "expected InvalidFormat for {input:?}"
873 );
874 }
875 }
876
877 #[test]
878 fn border_spacing_rejects_more_than_two_components() {
879 for input in ["5px 10px 15px", "1px 2px 3px 4px", "0 0 0"] {
880 assert_eq!(
881 parse_border_spacing(input),
882 Err(LayoutBorderSpacingParseError::InvalidFormat),
883 "expected InvalidFormat for {input:?}"
884 );
885 }
886 }
887
888 #[test]
889 fn border_spacing_collapses_arbitrary_internal_whitespace() {
890 let expected = LayoutBorderSpacing::new_separate(
891 PixelValue::const_px(5),
892 PixelValue::const_px(10),
893 );
894 for input in [
895 "5px 10px",
896 " 5px 10px ",
897 "\t5px\n10px\r\n",
898 "5px\x0c10px",
899 ] {
900 assert_eq!(parse_border_spacing(input).unwrap(), expected, "{input:?}");
901 }
902 }
903
904 #[test]
905 fn border_spacing_splits_on_non_css_unicode_whitespace() {
906 let s = parse_border_spacing("5px\u{a0}10px").unwrap();
911 assert_eq!(s.horizontal, PixelValue::const_px(5));
912 assert_eq!(s.vertical, PixelValue::const_px(10));
913 }
914
915 #[test]
916 fn border_spacing_propagates_pixel_value_errors() {
917 assert!(matches!(
919 parse_border_spacing("px"),
920 Err(LayoutBorderSpacingParseError::PixelValue(
921 CssPixelValueParseError::NoValueGiven("px", SizeMetric::Px)
922 ))
923 ));
924 for input in ["abc", "5foo", "5px;", "#5px", "5 .. px"] {
926 assert!(
927 parse_border_spacing(input).is_err(),
928 "expected {input:?} to be rejected"
929 );
930 }
931 assert!(matches!(
933 parse_border_spacing("5 px"),
934 Err(LayoutBorderSpacingParseError::PixelValue(
935 CssPixelValueParseError::NoValueGiven("px", SizeMetric::Px)
936 ))
937 ));
938 assert!(matches!(
940 parse_border_spacing("5px zzz"),
941 Err(LayoutBorderSpacingParseError::PixelValue(
942 CssPixelValueParseError::InvalidPixelValue("zzz")
943 ))
944 ));
945 }
946
947 #[test]
948 fn border_spacing_accepts_unitless_numbers() {
949 assert_eq!(
953 parse_border_spacing("0").unwrap(),
954 LayoutBorderSpacing::new(PixelValue::const_px(0))
955 );
956 assert_eq!(
957 parse_border_spacing("5").unwrap(),
958 LayoutBorderSpacing::new(PixelValue::const_px(5))
959 );
960 assert_eq!(raw(parse_border_spacing("-0").unwrap().horizontal), 0);
962 assert_eq!(
963 parse_border_spacing("-0").unwrap(),
964 parse_border_spacing("0").unwrap()
965 );
966 }
967
968 #[test]
969 fn border_spacing_accepts_negative_values() {
970 let s = parse_border_spacing("-5px -10px").unwrap();
973 assert_eq!(raw(s.horizontal), -5000);
974 assert_eq!(raw(s.vertical), -10_000);
975 }
976
977 #[test]
978 fn border_spacing_flattens_nan_to_zero() {
979 let s = parse_border_spacing("NaNpx").unwrap();
983 assert_eq!(raw(s.horizontal), 0);
984 assert_eq!(raw(s.vertical), 0);
985 assert!(s.horizontal.number.get().is_finite());
986 assert_eq!(s, LayoutBorderSpacing::default());
987
988 let mixed = parse_border_spacing("NaNpx 4px").unwrap();
989 assert_eq!(raw(mixed.horizontal), 0);
990 assert_eq!(raw(mixed.vertical), 4000);
991 }
992
993 #[test]
994 fn border_spacing_saturates_infinite_and_overflowing_values() {
995 for input in ["infpx", "infinitypx", "1e40px", "3.5e38px"] {
998 let s = parse_border_spacing(input).unwrap();
999 assert_eq!(raw(s.horizontal), isize::MAX, "{input} must saturate high");
1000 assert!(s.horizontal.number.get().is_finite(), "{input} must be finite");
1001 }
1002 for input in ["-infpx", "-1e40px", "-3.5e38px"] {
1003 let s = parse_border_spacing(input).unwrap();
1004 assert_eq!(raw(s.horizontal), isize::MIN, "{input} must saturate low");
1005 assert!(s.horizontal.number.get().is_finite(), "{input} must be finite");
1006 }
1007 let s = parse_border_spacing("infpx -infpx").unwrap();
1009 assert_eq!(raw(s.horizontal), isize::MAX);
1010 assert_eq!(raw(s.vertical), isize::MIN);
1011 assert_ne!(s.horizontal, s.vertical);
1012 }
1013
1014 #[test]
1015 fn border_spacing_truncates_below_milli_precision() {
1016 assert_eq!(raw(parse_border_spacing("0.0005px").unwrap().horizontal), 0);
1019 assert_eq!(raw(parse_border_spacing("0.9999px").unwrap().horizontal), 999);
1020 assert_eq!(raw(parse_border_spacing("1.9999px").unwrap().horizontal), 1999);
1021 let denormal = parse_border_spacing("1.17549435e-38px").unwrap();
1023 assert_eq!(raw(denormal.horizontal), 0);
1024 }
1025
1026 #[test]
1027 fn border_spacing_survives_extremely_long_input() {
1028 let long_token = "z".repeat(1_000_000);
1031 assert!(parse_border_spacing(&long_token).is_err());
1032
1033 let many = "5px ".repeat(200_000);
1035 assert_eq!(
1036 parse_border_spacing(&many),
1037 Err(LayoutBorderSpacingParseError::InvalidFormat)
1038 );
1039
1040 let padded = format!("{}5px{}", " ".repeat(500_000), " ".repeat(500_000));
1042 assert_eq!(
1043 parse_border_spacing(&padded).unwrap(),
1044 LayoutBorderSpacing::new(PixelValue::const_px(5))
1045 );
1046 }
1047
1048 #[test]
1049 fn border_spacing_survives_deeply_nested_input() {
1050 let nested = format!("{}5px{}", "(".repeat(10_000), ")".repeat(10_000));
1051 assert!(
1052 parse_border_spacing(&nested).is_err(),
1053 "10k nested brackets must be rejected, not stack-overflow"
1054 );
1055 }
1056
1057 #[test]
1058 fn border_spacing_unicode_input_does_not_panic() {
1059 for input in [
1060 "\u{1F600}",
1061 "5\u{1F600}px",
1062 "5px \u{1F600}",
1063 "5px", "5\u{0301}px", "\u{FEFF}5px", ] {
1067 assert!(
1068 parse_border_spacing(input).is_err(),
1069 "expected {input:?} to be rejected"
1070 );
1071 }
1072 }
1073
1074 #[test]
1079 fn keyword_enums_roundtrip_through_css() {
1080 for v in [LayoutTableLayout::Auto, LayoutTableLayout::Fixed] {
1081 assert_eq!(parse_table_layout(&v.print_as_css_value()).unwrap(), v);
1082 }
1083 for v in [StyleBorderCollapse::Separate, StyleBorderCollapse::Collapse] {
1084 assert_eq!(parse_border_collapse(&v.print_as_css_value()).unwrap(), v);
1085 }
1086 for v in [StyleCaptionSide::Top, StyleCaptionSide::Bottom] {
1087 assert_eq!(parse_caption_side(&v.print_as_css_value()).unwrap(), v);
1088 }
1089 for v in [StyleEmptyCells::Show, StyleEmptyCells::Hide] {
1090 assert_eq!(parse_empty_cells(&v.print_as_css_value()).unwrap(), v);
1091 }
1092 assert_eq!(
1094 parse_table_layout(&LayoutTableLayout::default().print_as_css_value()).unwrap(),
1095 LayoutTableLayout::default()
1096 );
1097 }
1098
1099 #[test]
1100 fn keyword_enum_css_spellings_are_distinct() {
1101 assert_ne!(
1105 LayoutTableLayout::Auto.print_as_css_value(),
1106 LayoutTableLayout::Fixed.print_as_css_value()
1107 );
1108 assert_ne!(
1109 StyleBorderCollapse::Separate.print_as_css_value(),
1110 StyleBorderCollapse::Collapse.print_as_css_value()
1111 );
1112 assert_ne!(
1113 StyleCaptionSide::Top.print_as_css_value(),
1114 StyleCaptionSide::Bottom.print_as_css_value()
1115 );
1116 assert_ne!(
1117 StyleEmptyCells::Show.print_as_css_value(),
1118 StyleEmptyCells::Hide.print_as_css_value()
1119 );
1120 }
1121
1122 #[test]
1123 fn border_spacing_roundtrips_through_css() {
1124 let values = [
1125 PixelValue::const_px(0),
1126 PixelValue::const_px(5),
1127 PixelValue::const_px(-5),
1128 PixelValue::px(1.5),
1129 PixelValue::px(-0.125),
1130 PixelValue::em(2.0),
1131 PixelValue::rem(0.5),
1132 PixelValue::percent(50.0),
1133 PixelValue::from_metric(SizeMetric::Pt, 12.0),
1134 PixelValue::from_metric(SizeMetric::Vmin, 3.25),
1135 ];
1136
1137 for h in values {
1138 let same = LayoutBorderSpacing::new(h);
1140 assert_eq!(
1141 parse_border_spacing(&same.print_as_css_value()).unwrap(),
1142 same,
1143 "single-value round-trip failed for {h:?}"
1144 );
1145
1146 for v in values {
1148 let sep = LayoutBorderSpacing::new_separate(h, v);
1149 assert_eq!(
1150 parse_border_spacing(&sep.print_as_css_value()).unwrap(),
1151 sep,
1152 "two-value round-trip failed for {h:?} / {v:?}"
1153 );
1154 }
1155 }
1156 }
1157
1158 #[test]
1159 fn border_spacing_prints_one_value_only_when_both_axes_match() {
1160 assert_eq!(
1161 LayoutBorderSpacing::new(PixelValue::const_px(5)).print_as_css_value(),
1162 "5px"
1163 );
1164 assert_eq!(
1165 LayoutBorderSpacing::new_separate(
1166 PixelValue::const_px(5),
1167 PixelValue::const_px(10)
1168 )
1169 .print_as_css_value(),
1170 "5px 10px"
1171 );
1172 assert_eq!(
1174 LayoutBorderSpacing::new_separate(
1175 PixelValue::const_px(0),
1176 PixelValue::percent(0.0)
1177 )
1178 .print_as_css_value(),
1179 "0px 0%"
1180 );
1181 assert_eq!(
1182 LayoutBorderSpacing::default().print_as_css_value(),
1183 "0px",
1184 "the default must not print as a two-value form"
1185 );
1186 }
1187
1188 #[test]
1189 fn border_spacing_saturated_value_roundtrips_stably() {
1190 let saturated = parse_border_spacing("infpx").unwrap();
1194 let printed = saturated.print_as_css_value();
1195 assert_eq!(parse_border_spacing(&printed).unwrap(), saturated);
1196
1197 let low = parse_border_spacing("-infpx").unwrap();
1198 assert_eq!(
1199 parse_border_spacing(&low.print_as_css_value()).unwrap(),
1200 low
1201 );
1202 }
1203
1204 #[test]
1205 fn border_spacing_parse_print_is_idempotent_after_quantization() {
1206 for input in ["0.0005px", "0.9999px", "0.1px", "1e40px", "NaNpx", "-0"] {
1209 let once = parse_border_spacing(input).unwrap();
1210 let printed = once.print_as_css_value();
1211 let twice = parse_border_spacing(&printed).unwrap();
1212 assert_eq!(once, twice, "not idempotent for {input:?}");
1213 assert_eq!(printed, twice.print_as_css_value(), "not stable for {input:?}");
1214 }
1215 }
1216
1217 #[test]
1222 fn format_as_rust_code_emits_the_variant_paths() {
1223 assert_eq!(
1224 LayoutTableLayout::Fixed.format_as_rust_code(0),
1225 "LayoutTableLayout::Fixed"
1226 );
1227 assert_eq!(
1228 StyleBorderCollapse::Collapse.format_as_rust_code(0),
1229 "StyleBorderCollapse::Collapse"
1230 );
1231 assert_eq!(
1232 StyleCaptionSide::Bottom.format_as_rust_code(0),
1233 "StyleCaptionSide::Bottom"
1234 );
1235 assert_eq!(StyleEmptyCells::Hide.format_as_rust_code(0), "StyleEmptyCells::Hide");
1236
1237 assert_eq!(
1239 LayoutTableLayout::Auto.format_as_rust_code(0),
1240 LayoutTableLayout::Auto.format_as_rust_code(usize::MAX)
1241 );
1242 }
1243
1244 #[test]
1245 fn format_as_rust_code_for_border_spacing_is_total() {
1246 assert_eq!(
1247 LayoutBorderSpacing::new(PixelValue::const_px(5)).format_as_rust_code(0),
1248 "LayoutBorderSpacing { horizontal: \
1249 PixelValue::const_from_metric_fractional(SizeMetric::Px, 5, 0), vertical: \
1250 PixelValue::const_from_metric_fractional(SizeMetric::Px, 5, 0) }"
1251 );
1252
1253 for v in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, f32::MAX, f32::MIN, -2.5] {
1255 let code = LayoutBorderSpacing::new(PixelValue::px(v)).format_as_rust_code(0);
1256 assert!(code.starts_with("LayoutBorderSpacing {"), "{v}: {code}");
1257 assert!(code.ends_with('}'), "{v}: {code}");
1258 }
1259 }
1260}