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
42impl PrintAsCssValue for LayoutTableLayout {
43 fn print_as_css_value(&self) -> String {
44 match self {
45 Self::Auto => "auto".to_string(),
46 Self::Fixed => "fixed".to_string(),
47 }
48 }
49}
50
51impl FormatAsRustCode for LayoutTableLayout {
52 fn format_as_rust_code(&self, _tabs: usize) -> String {
53 match self {
54 Self::Auto => "LayoutTableLayout::Auto".to_string(),
55 Self::Fixed => "LayoutTableLayout::Fixed".to_string(),
56 }
57 }
58}
59
60#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
68#[repr(C)]
69#[derive(Default)]
70pub enum StyleBorderCollapse {
71 #[default]
74 Separate,
75 Collapse,
78}
79
80impl PrintAsCssValue for StyleBorderCollapse {
81 fn print_as_css_value(&self) -> String {
82 match self {
83 Self::Separate => "separate".to_string(),
84 Self::Collapse => "collapse".to_string(),
85 }
86 }
87}
88
89impl FormatAsRustCode for StyleBorderCollapse {
90 fn format_as_rust_code(&self, _tabs: usize) -> String {
91 match self {
92 Self::Separate => "StyleBorderCollapse::Separate".to_string(),
93 Self::Collapse => "StyleBorderCollapse::Collapse".to_string(),
94 }
95 }
96}
97
98#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
109#[repr(C)]
110pub struct LayoutBorderSpacing {
111 pub horizontal: PixelValue,
113 pub vertical: PixelValue,
115}
116
117impl Default for LayoutBorderSpacing {
118 fn default() -> Self {
119 Self {
121 horizontal: PixelValue::const_px(0),
122 vertical: PixelValue::const_px(0),
123 }
124 }
125}
126
127impl LayoutBorderSpacing {
128 #[must_use]
130 pub const fn new(spacing: PixelValue) -> Self {
131 Self {
132 horizontal: spacing,
133 vertical: spacing,
134 }
135 }
136
137 #[must_use]
139 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
186impl PrintAsCssValue for StyleCaptionSide {
187 fn print_as_css_value(&self) -> String {
188 match self {
189 Self::Top => "top".to_string(),
190 Self::Bottom => "bottom".to_string(),
191 }
192 }
193}
194
195impl FormatAsRustCode for StyleCaptionSide {
196 fn format_as_rust_code(&self, _tabs: usize) -> String {
197 match self {
198 Self::Top => "StyleCaptionSide::Top".to_string(),
199 Self::Bottom => "StyleCaptionSide::Bottom".to_string(),
200 }
201 }
202}
203
204#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
211#[repr(C)]
212#[derive(Default)]
213pub enum StyleEmptyCells {
214 #[default]
216 Show,
217 Hide,
219}
220
221impl PrintAsCssValue for StyleEmptyCells {
222 fn print_as_css_value(&self) -> String {
223 match self {
224 Self::Show => "show".to_string(),
225 Self::Hide => "hide".to_string(),
226 }
227 }
228}
229
230impl FormatAsRustCode for StyleEmptyCells {
231 fn format_as_rust_code(&self, _tabs: usize) -> String {
232 match self {
233 Self::Show => "StyleEmptyCells::Show".to_string(),
234 Self::Hide => "StyleEmptyCells::Hide".to_string(),
235 }
236 }
237}
238
239#[derive(Debug, Clone, PartialEq)]
243pub(crate) enum LayoutTableLayoutParseError<'a> {
244 InvalidKeyword(&'a str),
245}
246
247pub(crate) fn parse_table_layout(
249 input: &str,
250) -> Result<LayoutTableLayout, LayoutTableLayoutParseError<'_>> {
251 match input.trim() {
252 "auto" => Ok(LayoutTableLayout::Auto),
253 "fixed" => Ok(LayoutTableLayout::Fixed),
254 other => Err(LayoutTableLayoutParseError::InvalidKeyword(other)),
255 }
256}
257
258#[derive(Debug, Clone, PartialEq)]
260pub(crate) enum StyleBorderCollapseParseError<'a> {
261 InvalidKeyword(&'a str),
262}
263
264pub(crate) fn parse_border_collapse(
266 input: &str,
267) -> Result<StyleBorderCollapse, StyleBorderCollapseParseError<'_>> {
268 match input.trim() {
269 "separate" => Ok(StyleBorderCollapse::Separate),
270 "collapse" => Ok(StyleBorderCollapse::Collapse),
271 other => Err(StyleBorderCollapseParseError::InvalidKeyword(other)),
272 }
273}
274
275#[derive(Debug, Clone, PartialEq)]
277pub(crate) enum LayoutBorderSpacingParseError<'a> {
278 PixelValue(CssPixelValueParseError<'a>),
279 InvalidFormat,
280}
281
282pub(crate) fn parse_border_spacing(
285 input: &str,
286) -> Result<LayoutBorderSpacing, LayoutBorderSpacingParseError<'_>> {
287 use crate::props::basic::parse_pixel_value;
288
289 let parts: Vec<&str> = input.split_whitespace().collect();
290
291 match parts.len() {
292 1 => {
293 let value =
295 parse_pixel_value(parts[0]).map_err(LayoutBorderSpacingParseError::PixelValue)?;
296 Ok(LayoutBorderSpacing::new(value))
297 }
298 2 => {
299 let horizontal =
301 parse_pixel_value(parts[0]).map_err(LayoutBorderSpacingParseError::PixelValue)?;
302 let vertical =
303 parse_pixel_value(parts[1]).map_err(LayoutBorderSpacingParseError::PixelValue)?;
304 Ok(LayoutBorderSpacing::new_separate(horizontal, vertical))
305 }
306 _ => Err(LayoutBorderSpacingParseError::InvalidFormat),
307 }
308}
309
310#[derive(Debug, Clone, PartialEq)]
312pub(crate) enum StyleCaptionSideParseError<'a> {
313 InvalidKeyword(&'a str),
314}
315
316pub(crate) fn parse_caption_side(
318 input: &str,
319) -> Result<StyleCaptionSide, StyleCaptionSideParseError<'_>> {
320 match input.trim() {
321 "top" => Ok(StyleCaptionSide::Top),
322 "bottom" => Ok(StyleCaptionSide::Bottom),
323 other => Err(StyleCaptionSideParseError::InvalidKeyword(other)),
324 }
325}
326
327#[derive(Debug, Clone, PartialEq)]
329pub(crate) enum StyleEmptyCellsParseError<'a> {
330 InvalidKeyword(&'a str),
331}
332
333pub(crate) fn parse_empty_cells(
335 input: &str,
336) -> Result<StyleEmptyCells, StyleEmptyCellsParseError<'_>> {
337 match input.trim() {
338 "show" => Ok(StyleEmptyCells::Show),
339 "hide" => Ok(StyleEmptyCells::Hide),
340 other => Err(StyleEmptyCellsParseError::InvalidKeyword(other)),
341 }
342}
343
344#[cfg(test)]
345mod tests {
346 use super::*;
347
348 #[test]
349 fn test_parse_table_layout() {
350 assert_eq!(parse_table_layout("auto").unwrap(), LayoutTableLayout::Auto);
351 assert_eq!(
352 parse_table_layout("fixed").unwrap(),
353 LayoutTableLayout::Fixed
354 );
355 assert!(parse_table_layout("invalid").is_err());
356 }
357
358 #[test]
359 fn test_parse_border_collapse() {
360 assert_eq!(
361 parse_border_collapse("separate").unwrap(),
362 StyleBorderCollapse::Separate
363 );
364 assert_eq!(
365 parse_border_collapse("collapse").unwrap(),
366 StyleBorderCollapse::Collapse
367 );
368 assert!(parse_border_collapse("invalid").is_err());
369 }
370
371 #[test]
372 fn test_parse_border_spacing() {
373 let spacing1 = parse_border_spacing("5px").unwrap();
374 assert_eq!(spacing1.horizontal, PixelValue::const_px(5));
375 assert_eq!(spacing1.vertical, PixelValue::const_px(5));
376
377 let spacing2 = parse_border_spacing("5px 10px").unwrap();
378 assert_eq!(spacing2.horizontal, PixelValue::const_px(5));
379 assert_eq!(spacing2.vertical, PixelValue::const_px(10));
380 }
381
382 #[test]
383 fn test_parse_caption_side() {
384 assert_eq!(parse_caption_side("top").unwrap(), StyleCaptionSide::Top);
385 assert_eq!(
386 parse_caption_side("bottom").unwrap(),
387 StyleCaptionSide::Bottom
388 );
389 assert!(parse_caption_side("invalid").is_err());
390 }
391
392 #[test]
393 fn test_parse_empty_cells() {
394 assert_eq!(parse_empty_cells("show").unwrap(), StyleEmptyCells::Show);
395 assert_eq!(parse_empty_cells("hide").unwrap(), StyleEmptyCells::Hide);
396 assert!(parse_empty_cells("invalid").is_err());
397 }
398}
399
400#[cfg(test)]
401#[allow(clippy::float_cmp)] mod autotest_generated {
403 use crate::props::basic::SizeMetric;
404
405 use super::*;
406
407 fn raw(p: PixelValue) -> isize {
411 p.number.number()
412 }
413
414 fn table_layout_ok(s: &str) -> bool {
415 parse_table_layout(s).is_ok()
416 }
417 fn border_collapse_ok(s: &str) -> bool {
418 parse_border_collapse(s).is_ok()
419 }
420 fn caption_side_ok(s: &str) -> bool {
421 parse_caption_side(s).is_ok()
422 }
423 fn empty_cells_ok(s: &str) -> bool {
424 parse_empty_cells(s).is_ok()
425 }
426
427 type KeywordParser = (&'static str, fn(&str) -> bool);
430 const KEYWORD_PARSERS: &[KeywordParser] = &[
431 ("table-layout", table_layout_ok),
432 ("border-collapse", border_collapse_ok),
433 ("caption-side", caption_side_ok),
434 ("empty-cells", empty_cells_ok),
435 ];
436
437 fn assert_all_keyword_parsers_reject(input: &str, why: &str) {
438 for (prop, parse) in KEYWORD_PARSERS {
439 assert!(
440 !parse(input),
441 "{prop}: expected {input:?} to be rejected ({why}), but it parsed"
442 );
443 }
444 }
445
446 #[test]
451 fn new_applies_the_same_spacing_to_both_axes() {
452 let s = LayoutBorderSpacing::new(PixelValue::const_px(7));
453 assert_eq!(s.horizontal, PixelValue::const_px(7));
454 assert_eq!(s.vertical, PixelValue::const_px(7));
455 assert_eq!(s.horizontal, s.vertical);
456 }
457
458 #[test]
459 fn new_separate_preserves_argument_order() {
460 let s =
463 LayoutBorderSpacing::new_separate(PixelValue::const_px(3), PixelValue::percent(50.0));
464 assert_eq!(s.horizontal, PixelValue::const_px(3));
465 assert_eq!(s.vertical, PixelValue::percent(50.0));
466 assert_ne!(s.horizontal, s.vertical);
467 }
468
469 #[test]
470 fn constructors_do_not_panic_on_extreme_floats() {
471 let extremes = [
472 0.0f32,
473 -0.0,
474 1.0,
475 -1.0,
476 f32::EPSILON,
477 f32::MIN_POSITIVE,
478 -f32::MIN_POSITIVE,
479 f32::MAX,
480 f32::MIN,
481 f32::INFINITY,
482 f32::NEG_INFINITY,
483 f32::NAN,
484 ];
485
486 for v in extremes {
487 let same = LayoutBorderSpacing::new(PixelValue::px(v));
488 assert_eq!(same.horizontal, same.vertical, "new({v}) must be symmetric");
489
490 for w in extremes {
491 let sep = LayoutBorderSpacing::new_separate(PixelValue::px(v), PixelValue::em(w));
492 assert_eq!(sep.horizontal.metric, SizeMetric::Px);
493 assert_eq!(sep.vertical.metric, SizeMetric::Em);
494 assert!(sep.horizontal.number.get().is_finite());
497 assert!(sep.vertical.number.get().is_finite());
498 }
499 }
500 }
501
502 #[test]
503 fn nan_spacing_is_flattened_to_zero_and_stays_comparable() {
504 let nan = LayoutBorderSpacing::new(PixelValue::px(f32::NAN));
508 assert_eq!(raw(nan.horizontal), 0);
509 assert_eq!(raw(nan.vertical), 0);
510
511 assert_eq!(nan, nan);
513 assert_eq!(nan, LayoutBorderSpacing::new(PixelValue::px(f32::NAN)));
514 assert_eq!(nan, LayoutBorderSpacing::new(PixelValue::px(0.0)));
515 assert_eq!(nan, LayoutBorderSpacing::default());
516 }
517
518 #[test]
519 fn infinite_spacing_saturates_instead_of_wrapping() {
520 for input in [f32::INFINITY, f32::MAX, 1e38] {
523 let s = LayoutBorderSpacing::new(PixelValue::px(input));
524 assert_eq!(raw(s.horizontal), isize::MAX, "{input} must saturate high");
525 assert!(s.horizontal.number.get().is_finite());
526 }
527 for input in [f32::NEG_INFINITY, f32::MIN, -1e38] {
528 let s = LayoutBorderSpacing::new(PixelValue::px(input));
529 assert_eq!(raw(s.horizontal), isize::MIN, "{input} must saturate low");
530 assert!(s.horizontal.number.get().is_finite());
531 }
532 }
533
534 #[test]
535 fn equal_border_spacings_hash_equal() {
536 use std::{
537 collections::hash_map::DefaultHasher,
538 hash::{Hash, Hasher},
539 };
540
541 fn hash(s: LayoutBorderSpacing) -> u64 {
542 let mut h = DefaultHasher::new();
543 s.hash(&mut h);
544 h.finish()
545 }
546
547 assert_eq!(
550 hash(LayoutBorderSpacing::new(PixelValue::px(f32::NAN))),
551 hash(LayoutBorderSpacing::default())
552 );
553 assert_eq!(
554 hash(LayoutBorderSpacing::new(PixelValue::const_px(4))),
555 hash(LayoutBorderSpacing::new_separate(
556 PixelValue::const_px(4),
557 PixelValue::const_px(4)
558 ))
559 );
560 }
561
562 #[test]
563 fn defaults_are_the_css_initial_values() {
564 assert_eq!(LayoutTableLayout::default(), LayoutTableLayout::Auto);
565 assert_eq!(
566 StyleBorderCollapse::default(),
567 StyleBorderCollapse::Separate
568 );
569 assert_eq!(StyleCaptionSide::default(), StyleCaptionSide::Top);
570 assert_eq!(StyleEmptyCells::default(), StyleEmptyCells::Show);
571
572 let d = LayoutBorderSpacing::default();
573 assert_eq!(d, LayoutBorderSpacing::new(PixelValue::const_px(0)));
574 assert_eq!(raw(d.horizontal), 0);
575 assert_eq!(raw(d.vertical), 0);
576 assert_eq!(d.horizontal.metric, SizeMetric::Px);
577 }
578
579 #[test]
584 fn keyword_parsers_accept_every_variant() {
585 assert_eq!(parse_table_layout("auto").unwrap(), LayoutTableLayout::Auto);
587 assert_eq!(
588 parse_table_layout("fixed").unwrap(),
589 LayoutTableLayout::Fixed
590 );
591 assert_eq!(
592 parse_border_collapse("separate").unwrap(),
593 StyleBorderCollapse::Separate
594 );
595 assert_eq!(
596 parse_border_collapse("collapse").unwrap(),
597 StyleBorderCollapse::Collapse
598 );
599 assert_eq!(parse_caption_side("top").unwrap(), StyleCaptionSide::Top);
600 assert_eq!(
601 parse_caption_side("bottom").unwrap(),
602 StyleCaptionSide::Bottom
603 );
604 assert_eq!(parse_empty_cells("show").unwrap(), StyleEmptyCells::Show);
605 assert_eq!(parse_empty_cells("hide").unwrap(), StyleEmptyCells::Hide);
606 }
607
608 #[test]
609 fn keyword_parsers_reject_empty_input() {
610 assert_all_keyword_parsers_reject("", "empty input");
611 assert_eq!(
612 parse_table_layout(""),
613 Err(LayoutTableLayoutParseError::InvalidKeyword(""))
614 );
615 assert_eq!(
616 parse_border_collapse(""),
617 Err(StyleBorderCollapseParseError::InvalidKeyword(""))
618 );
619 assert_eq!(
620 parse_caption_side(""),
621 Err(StyleCaptionSideParseError::InvalidKeyword(""))
622 );
623 assert_eq!(
624 parse_empty_cells(""),
625 Err(StyleEmptyCellsParseError::InvalidKeyword(""))
626 );
627 }
628
629 #[test]
630 fn keyword_parsers_reject_whitespace_only_input() {
631 for input in [" ", "\t", "\n", "\r\n", "\t\n \x0c", "\u{a0}", "\u{2028}"] {
632 assert_all_keyword_parsers_reject(input, "whitespace only");
633 }
634 assert_eq!(
637 parse_table_layout(" \t\n "),
638 Err(LayoutTableLayoutParseError::InvalidKeyword(""))
639 );
640 }
641
642 #[test]
643 fn keyword_parsers_trim_surrounding_whitespace() {
644 assert_eq!(
645 parse_table_layout(" auto\t\n").unwrap(),
646 LayoutTableLayout::Auto
647 );
648 assert_eq!(
649 parse_border_collapse("\r\n collapse ").unwrap(),
650 StyleBorderCollapse::Collapse
651 );
652 assert_eq!(
653 parse_caption_side("\t bottom \t").unwrap(),
654 StyleCaptionSide::Bottom
655 );
656 assert_eq!(
657 parse_empty_cells("\n hide \n").unwrap(),
658 StyleEmptyCells::Hide
659 );
660 }
661
662 #[test]
663 fn keyword_parsers_also_trim_non_css_unicode_whitespace() {
664 assert_eq!(
669 parse_table_layout("\u{a0}auto\u{a0}").unwrap(),
670 LayoutTableLayout::Auto
671 );
672 assert_eq!(
673 parse_empty_cells("\u{2028}show\u{2029}").unwrap(),
674 StyleEmptyCells::Show
675 );
676 }
677
678 #[test]
679 fn keyword_parse_errors_carry_the_trimmed_input() {
680 assert_eq!(
683 parse_table_layout(" bogus "),
684 Err(LayoutTableLayoutParseError::InvalidKeyword("bogus"))
685 );
686 assert_eq!(
687 parse_border_collapse(" separate collapse "),
688 Err(StyleBorderCollapseParseError::InvalidKeyword(
689 "separate collapse"
690 ))
691 );
692 assert_eq!(
693 parse_caption_side("\ttop;\t"),
694 Err(StyleCaptionSideParseError::InvalidKeyword("top;"))
695 );
696 assert_eq!(
697 parse_empty_cells(" show hide "),
698 Err(StyleEmptyCellsParseError::InvalidKeyword("show hide"))
699 );
700 }
701
702 #[test]
703 fn keyword_parsers_are_case_sensitive() {
704 for input in [
708 "AUTO", "Auto", "aUtO", "FIXED", "SEPARATE", "Collapse", "TOP", "Bottom", "SHOW",
709 "Hide",
710 ] {
711 assert_all_keyword_parsers_reject(input, "keyword matching is case-sensitive");
712 }
713 }
714
715 #[test]
716 fn keyword_parsers_reject_garbage() {
717 for input in [
718 "invalid",
719 "auto fixed",
720 "auto;",
721 "auto;garbage",
722 "auto/*c*/",
723 "/*auto*/",
724 "au to",
725 "a\0uto",
726 "\0",
727 "\u{7f}",
728 "-->",
729 "<!--",
730 "!important",
731 "\\61 uto",
732 "'auto'",
733 "\"auto\"",
734 "auto()",
735 "url(auto)",
736 ] {
737 assert_all_keyword_parsers_reject(input, "garbage");
738 }
739 }
740
741 #[test]
742 fn keyword_parsers_reject_boundary_numeric_strings() {
743 for input in [
746 "0",
747 "-0",
748 "0.0",
749 "1",
750 "-1",
751 "9223372036854775807", "-9223372036854775808", "18446744073709551616", "1e400",
755 "-1e400",
756 "3.4028235e38",
757 "1.17549435e-38",
758 "NaN",
759 "nan",
760 "inf",
761 "-inf",
762 "infinity",
763 ] {
764 assert_all_keyword_parsers_reject(input, "numeric input for a keyword property");
765 }
766 }
767
768 #[test]
769 fn keyword_parsers_reject_leading_and_trailing_junk() {
770 assert_all_keyword_parsers_reject("xauto", "leading junk");
771 assert_all_keyword_parsers_reject("autox", "trailing junk");
772 assert_all_keyword_parsers_reject("auto!", "trailing junk");
773 assert_all_keyword_parsers_reject("(fixed)", "wrapped in parens");
774 assert_all_keyword_parsers_reject("collapse,", "trailing comma");
775 assert_eq!(
777 parse_table_layout("auto ").unwrap(),
778 LayoutTableLayout::Auto
779 );
780 }
781
782 #[test]
783 fn keyword_parsers_survive_unicode_input() {
784 for input in [
785 "\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}",
793 "\u{10FFFF}", ] {
795 assert_all_keyword_parsers_reject(input, "non-ASCII input");
796 }
797 assert_eq!(
799 parse_table_layout(" \u{1F600} "),
800 Err(LayoutTableLayoutParseError::InvalidKeyword("\u{1F600}"))
801 );
802 }
803
804 #[test]
805 fn keyword_parsers_survive_extremely_long_input() {
806 let long = "a".repeat(1_000_000);
807 assert_all_keyword_parsers_reject(&long, "1M-char input");
808 assert_eq!(
809 parse_table_layout(&long),
810 Err(LayoutTableLayoutParseError::InvalidKeyword(long.as_str()))
811 );
812
813 let repeated = "auto".repeat(250_000);
815 assert_all_keyword_parsers_reject(&repeated, "repeated valid token");
816
817 let padded = format!("{}auto{}", " ".repeat(500_000), "\n".repeat(500_000));
819 assert_eq!(
820 parse_table_layout(&padded).unwrap(),
821 LayoutTableLayout::Auto
822 );
823 }
824
825 #[test]
826 fn keyword_parsers_survive_deeply_nested_input() {
827 let nested = format!("{}auto{}", "(".repeat(10_000), ")".repeat(10_000));
831 assert_all_keyword_parsers_reject(&nested, "10k nested brackets");
832
833 let braces = "{".repeat(50_000);
834 assert_all_keyword_parsers_reject(&braces, "50k open braces");
835 }
836
837 #[test]
842 fn border_spacing_single_value_applies_to_both_axes() {
843 let s = parse_border_spacing("5px").unwrap();
844 assert_eq!(s.horizontal, PixelValue::const_px(5));
845 assert_eq!(s.vertical, PixelValue::const_px(5));
846 assert_eq!(s, LayoutBorderSpacing::new(PixelValue::const_px(5)));
847 }
848
849 #[test]
850 fn border_spacing_two_values_are_horizontal_then_vertical() {
851 let s = parse_border_spacing("5px 10px").unwrap();
852 assert_eq!(s.horizontal, PixelValue::const_px(5));
853 assert_eq!(s.vertical, PixelValue::const_px(10));
854 assert_ne!(s, parse_border_spacing("10px 5px").unwrap());
856 }
857
858 #[test]
859 fn border_spacing_accepts_mixed_metrics_per_axis() {
860 let s = parse_border_spacing("1em 50%").unwrap();
861 assert_eq!(s.horizontal.metric, SizeMetric::Em);
862 assert_eq!(raw(s.horizontal), 1000);
863 assert_eq!(s.vertical.metric, SizeMetric::Percent);
864 assert_eq!(raw(s.vertical), 50_000);
865
866 assert_eq!(
868 parse_border_spacing("2rem").unwrap().horizontal.metric,
869 SizeMetric::Rem
870 );
871 assert_eq!(
873 parse_border_spacing("2vmax 3vmin")
874 .unwrap()
875 .horizontal
876 .metric,
877 SizeMetric::Vmax
878 );
879 }
880
881 #[test]
882 fn border_spacing_rejects_empty_and_whitespace_only_input() {
883 for input in ["", " ", "\t\n", "\r\n\x0c ", "\u{a0}", "\u{2028}\u{2029}"] {
885 assert_eq!(
886 parse_border_spacing(input),
887 Err(LayoutBorderSpacingParseError::InvalidFormat),
888 "expected InvalidFormat for {input:?}"
889 );
890 }
891 }
892
893 #[test]
894 fn border_spacing_rejects_more_than_two_components() {
895 for input in ["5px 10px 15px", "1px 2px 3px 4px", "0 0 0"] {
896 assert_eq!(
897 parse_border_spacing(input),
898 Err(LayoutBorderSpacingParseError::InvalidFormat),
899 "expected InvalidFormat for {input:?}"
900 );
901 }
902 }
903
904 #[test]
905 fn border_spacing_collapses_arbitrary_internal_whitespace() {
906 let expected =
907 LayoutBorderSpacing::new_separate(PixelValue::const_px(5), PixelValue::const_px(10));
908 for input in [
909 "5px 10px",
910 " 5px 10px ",
911 "\t5px\n10px\r\n",
912 "5px\x0c10px",
913 ] {
914 assert_eq!(parse_border_spacing(input).unwrap(), expected, "{input:?}");
915 }
916 }
917
918 #[test]
919 fn border_spacing_splits_on_non_css_unicode_whitespace() {
920 let s = parse_border_spacing("5px\u{a0}10px").unwrap();
925 assert_eq!(s.horizontal, PixelValue::const_px(5));
926 assert_eq!(s.vertical, PixelValue::const_px(10));
927 }
928
929 #[test]
930 fn border_spacing_propagates_pixel_value_errors() {
931 assert!(matches!(
933 parse_border_spacing("px"),
934 Err(LayoutBorderSpacingParseError::PixelValue(
935 CssPixelValueParseError::NoValueGiven("px", SizeMetric::Px)
936 ))
937 ));
938 for input in ["abc", "5foo", "5px;", "#5px", "5 .. px"] {
940 assert!(
941 parse_border_spacing(input).is_err(),
942 "expected {input:?} to be rejected"
943 );
944 }
945 assert!(matches!(
947 parse_border_spacing("5 px"),
948 Err(LayoutBorderSpacingParseError::PixelValue(
949 CssPixelValueParseError::NoValueGiven("px", SizeMetric::Px)
950 ))
951 ));
952 assert!(matches!(
954 parse_border_spacing("5px zzz"),
955 Err(LayoutBorderSpacingParseError::PixelValue(
956 CssPixelValueParseError::InvalidPixelValue("zzz")
957 ))
958 ));
959 }
960
961 #[test]
962 fn border_spacing_accepts_unitless_numbers() {
963 assert_eq!(
967 parse_border_spacing("0").unwrap(),
968 LayoutBorderSpacing::new(PixelValue::const_px(0))
969 );
970 assert_eq!(
971 parse_border_spacing("5").unwrap(),
972 LayoutBorderSpacing::new(PixelValue::const_px(5))
973 );
974 assert_eq!(raw(parse_border_spacing("-0").unwrap().horizontal), 0);
976 assert_eq!(
977 parse_border_spacing("-0").unwrap(),
978 parse_border_spacing("0").unwrap()
979 );
980 }
981
982 #[test]
983 fn border_spacing_accepts_negative_values() {
984 let s = parse_border_spacing("-5px -10px").unwrap();
987 assert_eq!(raw(s.horizontal), -5000);
988 assert_eq!(raw(s.vertical), -10_000);
989 }
990
991 #[test]
992 fn border_spacing_flattens_nan_to_zero() {
993 let s = parse_border_spacing("NaNpx").unwrap();
997 assert_eq!(raw(s.horizontal), 0);
998 assert_eq!(raw(s.vertical), 0);
999 assert!(s.horizontal.number.get().is_finite());
1000 assert_eq!(s, LayoutBorderSpacing::default());
1001
1002 let mixed = parse_border_spacing("NaNpx 4px").unwrap();
1003 assert_eq!(raw(mixed.horizontal), 0);
1004 assert_eq!(raw(mixed.vertical), 4000);
1005 }
1006
1007 #[test]
1008 fn border_spacing_saturates_infinite_and_overflowing_values() {
1009 for input in ["infpx", "infinitypx", "1e40px", "3.5e38px"] {
1012 let s = parse_border_spacing(input).unwrap();
1013 assert_eq!(raw(s.horizontal), isize::MAX, "{input} must saturate high");
1014 assert!(
1015 s.horizontal.number.get().is_finite(),
1016 "{input} must be finite"
1017 );
1018 }
1019 for input in ["-infpx", "-1e40px", "-3.5e38px"] {
1020 let s = parse_border_spacing(input).unwrap();
1021 assert_eq!(raw(s.horizontal), isize::MIN, "{input} must saturate low");
1022 assert!(
1023 s.horizontal.number.get().is_finite(),
1024 "{input} must be finite"
1025 );
1026 }
1027 let s = parse_border_spacing("infpx -infpx").unwrap();
1029 assert_eq!(raw(s.horizontal), isize::MAX);
1030 assert_eq!(raw(s.vertical), isize::MIN);
1031 assert_ne!(s.horizontal, s.vertical);
1032 }
1033
1034 #[test]
1035 fn border_spacing_truncates_below_milli_precision() {
1036 assert_eq!(raw(parse_border_spacing("0.0005px").unwrap().horizontal), 0);
1039 assert_eq!(
1040 raw(parse_border_spacing("0.9999px").unwrap().horizontal),
1041 999
1042 );
1043 assert_eq!(
1044 raw(parse_border_spacing("1.9999px").unwrap().horizontal),
1045 1999
1046 );
1047 let denormal = parse_border_spacing("1.17549435e-38px").unwrap();
1049 assert_eq!(raw(denormal.horizontal), 0);
1050 }
1051
1052 #[test]
1053 fn border_spacing_survives_extremely_long_input() {
1054 let long_token = "z".repeat(1_000_000);
1057 assert!(parse_border_spacing(&long_token).is_err());
1058
1059 let many = "5px ".repeat(200_000);
1061 assert_eq!(
1062 parse_border_spacing(&many),
1063 Err(LayoutBorderSpacingParseError::InvalidFormat)
1064 );
1065
1066 let padded = format!("{}5px{}", " ".repeat(500_000), " ".repeat(500_000));
1068 assert_eq!(
1069 parse_border_spacing(&padded).unwrap(),
1070 LayoutBorderSpacing::new(PixelValue::const_px(5))
1071 );
1072 }
1073
1074 #[test]
1075 fn border_spacing_survives_deeply_nested_input() {
1076 let nested = format!("{}5px{}", "(".repeat(10_000), ")".repeat(10_000));
1077 assert!(
1078 parse_border_spacing(&nested).is_err(),
1079 "10k nested brackets must be rejected, not stack-overflow"
1080 );
1081 }
1082
1083 #[test]
1084 fn border_spacing_unicode_input_does_not_panic() {
1085 for input in [
1086 "\u{1F600}",
1087 "5\u{1F600}px",
1088 "5px \u{1F600}",
1089 "5px", "5\u{0301}px", "\u{FEFF}5px", ] {
1093 assert!(
1094 parse_border_spacing(input).is_err(),
1095 "expected {input:?} to be rejected"
1096 );
1097 }
1098 }
1099
1100 #[test]
1105 fn keyword_enums_roundtrip_through_css() {
1106 for v in [LayoutTableLayout::Auto, LayoutTableLayout::Fixed] {
1107 assert_eq!(parse_table_layout(&v.print_as_css_value()).unwrap(), v);
1108 }
1109 for v in [StyleBorderCollapse::Separate, StyleBorderCollapse::Collapse] {
1110 assert_eq!(parse_border_collapse(&v.print_as_css_value()).unwrap(), v);
1111 }
1112 for v in [StyleCaptionSide::Top, StyleCaptionSide::Bottom] {
1113 assert_eq!(parse_caption_side(&v.print_as_css_value()).unwrap(), v);
1114 }
1115 for v in [StyleEmptyCells::Show, StyleEmptyCells::Hide] {
1116 assert_eq!(parse_empty_cells(&v.print_as_css_value()).unwrap(), v);
1117 }
1118 assert_eq!(
1120 parse_table_layout(&LayoutTableLayout::default().print_as_css_value()).unwrap(),
1121 LayoutTableLayout::default()
1122 );
1123 }
1124
1125 #[test]
1126 fn keyword_enum_css_spellings_are_distinct() {
1127 assert_ne!(
1131 LayoutTableLayout::Auto.print_as_css_value(),
1132 LayoutTableLayout::Fixed.print_as_css_value()
1133 );
1134 assert_ne!(
1135 StyleBorderCollapse::Separate.print_as_css_value(),
1136 StyleBorderCollapse::Collapse.print_as_css_value()
1137 );
1138 assert_ne!(
1139 StyleCaptionSide::Top.print_as_css_value(),
1140 StyleCaptionSide::Bottom.print_as_css_value()
1141 );
1142 assert_ne!(
1143 StyleEmptyCells::Show.print_as_css_value(),
1144 StyleEmptyCells::Hide.print_as_css_value()
1145 );
1146 }
1147
1148 #[test]
1149 fn border_spacing_roundtrips_through_css() {
1150 let values = [
1151 PixelValue::const_px(0),
1152 PixelValue::const_px(5),
1153 PixelValue::const_px(-5),
1154 PixelValue::px(1.5),
1155 PixelValue::px(-0.125),
1156 PixelValue::em(2.0),
1157 PixelValue::rem(0.5),
1158 PixelValue::percent(50.0),
1159 PixelValue::from_metric(SizeMetric::Pt, 12.0),
1160 PixelValue::from_metric(SizeMetric::Vmin, 3.25),
1161 ];
1162
1163 for h in values {
1164 let same = LayoutBorderSpacing::new(h);
1166 assert_eq!(
1167 parse_border_spacing(&same.print_as_css_value()).unwrap(),
1168 same,
1169 "single-value round-trip failed for {h:?}"
1170 );
1171
1172 for v in values {
1174 let sep = LayoutBorderSpacing::new_separate(h, v);
1175 assert_eq!(
1176 parse_border_spacing(&sep.print_as_css_value()).unwrap(),
1177 sep,
1178 "two-value round-trip failed for {h:?} / {v:?}"
1179 );
1180 }
1181 }
1182 }
1183
1184 #[test]
1185 fn border_spacing_prints_one_value_only_when_both_axes_match() {
1186 assert_eq!(
1187 LayoutBorderSpacing::new(PixelValue::const_px(5)).print_as_css_value(),
1188 "5px"
1189 );
1190 assert_eq!(
1191 LayoutBorderSpacing::new_separate(PixelValue::const_px(5), PixelValue::const_px(10))
1192 .print_as_css_value(),
1193 "5px 10px"
1194 );
1195 assert_eq!(
1197 LayoutBorderSpacing::new_separate(PixelValue::const_px(0), PixelValue::percent(0.0))
1198 .print_as_css_value(),
1199 "0px 0%"
1200 );
1201 assert_eq!(
1202 LayoutBorderSpacing::default().print_as_css_value(),
1203 "0px",
1204 "the default must not print as a two-value form"
1205 );
1206 }
1207
1208 #[test]
1209 fn border_spacing_saturated_value_roundtrips_stably() {
1210 let saturated = parse_border_spacing("infpx").unwrap();
1214 let printed = saturated.print_as_css_value();
1215 assert_eq!(parse_border_spacing(&printed).unwrap(), saturated);
1216
1217 let low = parse_border_spacing("-infpx").unwrap();
1218 assert_eq!(
1219 parse_border_spacing(&low.print_as_css_value()).unwrap(),
1220 low
1221 );
1222 }
1223
1224 #[test]
1225 fn border_spacing_parse_print_is_idempotent_after_quantization() {
1226 for input in ["0.0005px", "0.9999px", "0.1px", "1e40px", "NaNpx", "-0"] {
1229 let once = parse_border_spacing(input).unwrap();
1230 let printed = once.print_as_css_value();
1231 let twice = parse_border_spacing(&printed).unwrap();
1232 assert_eq!(once, twice, "not idempotent for {input:?}");
1233 assert_eq!(
1234 printed,
1235 twice.print_as_css_value(),
1236 "not stable for {input:?}"
1237 );
1238 }
1239 }
1240
1241 #[test]
1246 fn format_as_rust_code_emits_the_variant_paths() {
1247 assert_eq!(
1248 LayoutTableLayout::Fixed.format_as_rust_code(0),
1249 "LayoutTableLayout::Fixed"
1250 );
1251 assert_eq!(
1252 StyleBorderCollapse::Collapse.format_as_rust_code(0),
1253 "StyleBorderCollapse::Collapse"
1254 );
1255 assert_eq!(
1256 StyleCaptionSide::Bottom.format_as_rust_code(0),
1257 "StyleCaptionSide::Bottom"
1258 );
1259 assert_eq!(
1260 StyleEmptyCells::Hide.format_as_rust_code(0),
1261 "StyleEmptyCells::Hide"
1262 );
1263
1264 assert_eq!(
1266 LayoutTableLayout::Auto.format_as_rust_code(0),
1267 LayoutTableLayout::Auto.format_as_rust_code(usize::MAX)
1268 );
1269 }
1270
1271 #[test]
1272 fn format_as_rust_code_for_border_spacing_is_total() {
1273 assert_eq!(
1274 LayoutBorderSpacing::new(PixelValue::const_px(5)).format_as_rust_code(0),
1275 "LayoutBorderSpacing { horizontal: \
1276 PixelValue::const_from_metric_fractional(SizeMetric::Px, 5, 0), vertical: \
1277 PixelValue::const_from_metric_fractional(SizeMetric::Px, 5, 0) }"
1278 );
1279
1280 for v in [
1282 f32::NAN,
1283 f32::INFINITY,
1284 f32::NEG_INFINITY,
1285 f32::MAX,
1286 f32::MIN,
1287 -2.5,
1288 ] {
1289 let code = LayoutBorderSpacing::new(PixelValue::px(v)).format_as_rust_code(0);
1290 assert!(code.starts_with("LayoutBorderSpacing {"), "{v}: {code}");
1291 assert!(code.ends_with('}'), "{v}: {code}");
1292 }
1293 }
1294}