1use alloc::string::{String, ToString};
8use core::num::ParseIntError;
9
10use crate::props::{
11 basic::{
12 color::{parse_css_color, ColorU, CssColorParseError, CssColorParseErrorOwned},
13 pixel::{
14 parse_pixel_value, CssPixelValueParseError, CssPixelValueParseErrorOwned, PixelValue,
15 },
16 },
17 formatter::PrintAsCssValue,
18 style::border::{
19 parse_border_style, BorderStyle, CssBorderStyleParseError, CssBorderStyleParseErrorOwned,
20 },
21};
22
23#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
29#[repr(C, u8)]
30#[derive(Default)]
31pub enum ColumnCount {
32 #[default]
33 Auto,
34 Integer(u32),
35}
36
37impl PrintAsCssValue for ColumnCount {
38 fn print_as_css_value(&self) -> String {
39 match self {
40 Self::Auto => "auto".to_string(),
41 Self::Integer(i) => i.to_string(),
42 }
43 }
44}
45
46#[allow(variant_size_differences)]
48#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
53#[repr(C, u8)]
54#[derive(Default)]
55pub enum ColumnWidth {
56 #[default]
57 Auto,
58 Length(PixelValue),
59}
60
61impl PrintAsCssValue for ColumnWidth {
62 fn print_as_css_value(&self) -> String {
63 match self {
64 Self::Auto => "auto".to_string(),
65 Self::Length(px) => px.print_as_css_value(),
66 }
67 }
68}
69
70#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
76#[repr(C)]
77#[derive(Default)]
78pub enum ColumnSpan {
79 #[default]
80 None,
81 All,
82}
83
84impl PrintAsCssValue for ColumnSpan {
85 fn print_as_css_value(&self) -> String {
86 String::from(match self {
87 Self::None => "none",
88 Self::All => "all",
89 })
90 }
91}
92
93#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
99#[repr(C)]
100#[derive(Default)]
101pub enum ColumnFill {
102 Auto,
103 #[default]
104 Balance,
105}
106
107impl PrintAsCssValue for ColumnFill {
108 fn print_as_css_value(&self) -> String {
109 String::from(match self {
110 Self::Auto => "auto",
111 Self::Balance => "balance",
112 })
113 }
114}
115
116#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
122#[repr(C)]
123pub struct ColumnRuleWidth {
124 pub inner: PixelValue,
125}
126
127impl Default for ColumnRuleWidth {
128 fn default() -> Self {
129 Self {
130 inner: PixelValue::const_px(3),
131 }
132 }
133}
134
135impl PrintAsCssValue for ColumnRuleWidth {
136 fn print_as_css_value(&self) -> String {
137 self.inner.print_as_css_value()
138 }
139}
140
141#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
145#[repr(C)]
146pub struct ColumnRuleStyle {
147 pub inner: BorderStyle,
148}
149
150impl Default for ColumnRuleStyle {
151 fn default() -> Self {
152 Self {
153 inner: BorderStyle::None,
154 }
155 }
156}
157
158impl PrintAsCssValue for ColumnRuleStyle {
159 fn print_as_css_value(&self) -> String {
160 self.inner.print_as_css_value()
161 }
162}
163
164#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
170#[repr(C)]
171pub struct ColumnRuleColor {
172 pub inner: ColorU,
173}
174
175impl Default for ColumnRuleColor {
176 fn default() -> Self {
177 Self {
179 inner: ColorU::BLACK,
180 }
181 }
182}
183
184impl PrintAsCssValue for ColumnRuleColor {
185 fn print_as_css_value(&self) -> String {
186 self.inner.to_hash()
187 }
188}
189
190impl crate::codegen::format::FormatAsRustCode for ColumnCount {
192 fn format_as_rust_code(&self, _tabs: usize) -> String {
193 match self {
194 Self::Auto => String::from("ColumnCount::Auto"),
195 Self::Integer(i) => format!("ColumnCount::Integer({i})"),
196 }
197 }
198}
199
200impl crate::codegen::format::FormatAsRustCode for ColumnWidth {
201 fn format_as_rust_code(&self, _tabs: usize) -> String {
202 match self {
203 Self::Auto => String::from("ColumnWidth::Auto"),
204 Self::Length(px) => format!(
205 "ColumnWidth::Length({})",
206 crate::codegen::format::format_pixel_value(px)
207 ),
208 }
209 }
210}
211
212impl crate::codegen::format::FormatAsRustCode for ColumnSpan {
213 fn format_as_rust_code(&self, _tabs: usize) -> String {
214 match self {
215 Self::None => String::from("ColumnSpan::None"),
216 Self::All => String::from("ColumnSpan::All"),
217 }
218 }
219}
220
221impl crate::codegen::format::FormatAsRustCode for ColumnFill {
222 fn format_as_rust_code(&self, _tabs: usize) -> String {
223 match self {
224 Self::Auto => String::from("ColumnFill::Auto"),
225 Self::Balance => String::from("ColumnFill::Balance"),
226 }
227 }
228}
229
230impl crate::codegen::format::FormatAsRustCode for ColumnRuleWidth {
231 fn format_as_rust_code(&self, _tabs: usize) -> String {
232 format!(
233 "ColumnRuleWidth {{ inner: {} }}",
234 crate::codegen::format::format_pixel_value(&self.inner)
235 )
236 }
237}
238
239impl crate::codegen::format::FormatAsRustCode for ColumnRuleStyle {
240 fn format_as_rust_code(&self, tabs: usize) -> String {
241 format!(
242 "ColumnRuleStyle {{ inner: {} }}",
243 self.inner.format_as_rust_code(tabs)
244 )
245 }
246}
247
248impl crate::codegen::format::FormatAsRustCode for ColumnRuleColor {
249 fn format_as_rust_code(&self, _tabs: usize) -> String {
250 format!(
251 "ColumnRuleColor {{ inner: {} }}",
252 crate::codegen::format::format_color_value(&self.inner)
253 )
254 }
255}
256
257#[cfg(feature = "parser")]
260pub mod parser {
261 #[allow(clippy::wildcard_imports)]
262 use super::*;
264 use crate::corety::AzString;
265
266 #[derive(Clone, PartialEq, Eq)]
269 pub enum ColumnCountParseError<'a> {
270 InvalidValue(&'a str),
271 ParseInt(ParseIntError),
272 }
273
274 impl_debug_as_display!(ColumnCountParseError<'a>);
275 impl_display! { ColumnCountParseError<'a>, {
276 InvalidValue(v) => format!("Invalid column-count value: \"{}\"", v),
277 ParseInt(e) => format!("Invalid integer for column-count: {}", e),
278 }}
279
280 #[derive(Debug, Clone, PartialEq, Eq)]
281 #[repr(C, u8)]
282 pub enum ColumnCountParseErrorOwned {
283 InvalidValue(AzString),
284 ParseInt(AzString),
285 }
286
287 impl ColumnCountParseError<'_> {
288 #[must_use]
289 pub fn to_contained(&self) -> ColumnCountParseErrorOwned {
290 match self {
291 Self::InvalidValue(s) => {
292 ColumnCountParseErrorOwned::InvalidValue((*s).to_string().into())
293 }
294 Self::ParseInt(e) => ColumnCountParseErrorOwned::ParseInt(e.to_string().into()),
295 }
296 }
297 }
298
299 impl ColumnCountParseErrorOwned {
300 #[must_use]
301 pub fn to_shared(&self) -> ColumnCountParseError<'_> {
302 match self {
303 Self::InvalidValue(s) => ColumnCountParseError::InvalidValue(s),
304 Self::ParseInt(_) => ColumnCountParseError::InvalidValue("invalid integer"),
308 }
309 }
310 }
311
312 pub fn parse_column_count(input: &str) -> Result<ColumnCount, ColumnCountParseError<'_>> {
316 let trimmed = input.trim();
317 if trimmed == "auto" {
318 return Ok(ColumnCount::Auto);
319 }
320 let val: u32 = trimmed.parse().map_err(ColumnCountParseError::ParseInt)?;
321 Ok(ColumnCount::Integer(val))
322 }
323
324 #[derive(Clone, PartialEq, Eq)]
327 pub enum ColumnWidthParseError<'a> {
328 InvalidValue(&'a str),
329 PixelValue(CssPixelValueParseError<'a>),
330 }
331
332 impl_debug_as_display!(ColumnWidthParseError<'a>);
333 impl_display! { ColumnWidthParseError<'a>, {
334 InvalidValue(v) => format!("Invalid column-width value: \"{}\"", v),
335 PixelValue(e) => format!("{}", e),
336 }}
337 impl_from! { CssPixelValueParseError<'a>, ColumnWidthParseError::PixelValue }
338
339 #[derive(Debug, Clone, PartialEq, Eq)]
340 #[repr(C, u8)]
341 pub enum ColumnWidthParseErrorOwned {
342 InvalidValue(AzString),
343 PixelValue(CssPixelValueParseErrorOwned),
344 }
345
346 impl ColumnWidthParseError<'_> {
347 #[must_use]
348 pub fn to_contained(&self) -> ColumnWidthParseErrorOwned {
349 match self {
350 Self::InvalidValue(s) => {
351 ColumnWidthParseErrorOwned::InvalidValue((*s).to_string().into())
352 }
353 Self::PixelValue(e) => ColumnWidthParseErrorOwned::PixelValue(e.to_contained()),
354 }
355 }
356 }
357
358 impl ColumnWidthParseErrorOwned {
359 #[must_use]
360 pub fn to_shared(&self) -> ColumnWidthParseError<'_> {
361 match self {
362 Self::InvalidValue(s) => ColumnWidthParseError::InvalidValue(s),
363 Self::PixelValue(e) => ColumnWidthParseError::PixelValue(e.to_shared()),
364 }
365 }
366 }
367
368 pub fn parse_column_width(input: &str) -> Result<ColumnWidth, ColumnWidthParseError<'_>> {
372 let trimmed = input.trim();
373 if trimmed == "auto" {
374 return Ok(ColumnWidth::Auto);
375 }
376 Ok(ColumnWidth::Length(parse_pixel_value(trimmed)?))
377 }
378
379 macro_rules! define_simple_column_parser {
381 (
382 $fn_name:ident,
383 $struct_name:ident,
384 $error_name:ident,
385 $error_owned_name:ident,
386 $prop_name:expr,
387 $($val:expr => $variant:path),+
388 ) => {
389 #[derive(Clone, PartialEq, Eq)]
390 pub enum $error_name<'a> {
391 InvalidValue(&'a str),
392 }
393
394 impl_debug_as_display!($error_name<'a>);
395 impl_display! { $error_name<'a>, {
396 InvalidValue(v) => format!("Invalid {} value: \"{}\"", $prop_name, v),
397 }}
398
399 #[derive(Debug, Clone, PartialEq, Eq)]
400 #[repr(C, u8)]
401 pub enum $error_owned_name {
402 InvalidValue(AzString),
403 }
404
405 impl $error_name<'_> {
406 #[must_use] pub fn to_contained(&self) -> $error_owned_name {
407 match self {
408 Self::InvalidValue(s) => $error_owned_name::InvalidValue(s.to_string().into()),
409 }
410 }
411 }
412
413 impl $error_owned_name {
414 #[must_use] pub fn to_shared(&self) -> $error_name<'_> {
415 match self {
416 Self::InvalidValue(s) => $error_name::InvalidValue(s.as_str()),
417 }
418 }
419 }
420
421 pub fn $fn_name(input: &str) -> Result<$struct_name, $error_name<'_>> {
425 match input.trim() {
426 $( $val => Ok($variant), )+
427 _ => Err($error_name::InvalidValue(input)),
428 }
429 }
430 };
431 }
432
433 define_simple_column_parser!(
434 parse_column_span,
435 ColumnSpan,
436 ColumnSpanParseError,
437 ColumnSpanParseErrorOwned,
438 "column-span",
439 "none" => ColumnSpan::None,
440 "all" => ColumnSpan::All
441 );
442
443 define_simple_column_parser!(
444 parse_column_fill,
445 ColumnFill,
446 ColumnFillParseError,
447 ColumnFillParseErrorOwned,
448 "column-fill",
449 "auto" => ColumnFill::Auto,
450 "balance" => ColumnFill::Balance
451 );
452
453 #[derive(Clone, PartialEq, Eq)]
456 pub enum ColumnRuleWidthParseError<'a> {
457 Pixel(CssPixelValueParseError<'a>),
458 }
459 impl_debug_as_display!(ColumnRuleWidthParseError<'a>);
460 impl_display! { ColumnRuleWidthParseError<'a>, { Pixel(e) => format!("{}", e) }}
461 impl_from! { CssPixelValueParseError<'a>, ColumnRuleWidthParseError::Pixel }
462 #[derive(Debug, Clone, PartialEq, Eq)]
463 #[repr(C, u8)]
464 pub enum ColumnRuleWidthParseErrorOwned {
465 Pixel(CssPixelValueParseErrorOwned),
466 }
467 impl ColumnRuleWidthParseError<'_> {
468 #[must_use]
469 pub fn to_contained(&self) -> ColumnRuleWidthParseErrorOwned {
470 match self {
471 ColumnRuleWidthParseError::Pixel(e) => {
472 ColumnRuleWidthParseErrorOwned::Pixel(e.to_contained())
473 }
474 }
475 }
476 }
477 impl ColumnRuleWidthParseErrorOwned {
478 #[must_use]
479 pub fn to_shared(&self) -> ColumnRuleWidthParseError<'_> {
480 match self {
481 Self::Pixel(e) => ColumnRuleWidthParseError::Pixel(e.to_shared()),
482 }
483 }
484 }
485 pub fn parse_column_rule_width(
489 input: &str,
490 ) -> Result<ColumnRuleWidth, ColumnRuleWidthParseError<'_>> {
491 Ok(ColumnRuleWidth {
492 inner: parse_pixel_value(input)?,
493 })
494 }
495
496 #[derive(Clone, PartialEq, Eq)]
497 pub enum ColumnRuleStyleParseError<'a> {
498 Style(CssBorderStyleParseError<'a>),
499 }
500 impl_debug_as_display!(ColumnRuleStyleParseError<'a>);
501 impl_display! { ColumnRuleStyleParseError<'a>, { Style(e) => format!("{}", e) }}
502 impl_from! { CssBorderStyleParseError<'a>, ColumnRuleStyleParseError::Style }
503 #[derive(Debug, Clone, PartialEq, Eq)]
504 #[repr(C, u8)]
505 pub enum ColumnRuleStyleParseErrorOwned {
506 Style(CssBorderStyleParseErrorOwned),
507 }
508 impl ColumnRuleStyleParseError<'_> {
509 #[must_use]
510 pub fn to_contained(&self) -> ColumnRuleStyleParseErrorOwned {
511 match self {
512 ColumnRuleStyleParseError::Style(e) => {
513 ColumnRuleStyleParseErrorOwned::Style(e.to_contained())
514 }
515 }
516 }
517 }
518 impl ColumnRuleStyleParseErrorOwned {
519 #[must_use]
520 pub fn to_shared(&self) -> ColumnRuleStyleParseError<'_> {
521 match self {
522 Self::Style(e) => ColumnRuleStyleParseError::Style(e.to_shared()),
523 }
524 }
525 }
526 pub fn parse_column_rule_style(
530 input: &str,
531 ) -> Result<ColumnRuleStyle, ColumnRuleStyleParseError<'_>> {
532 Ok(ColumnRuleStyle {
533 inner: parse_border_style(input)?,
534 })
535 }
536
537 #[derive(Clone, PartialEq)]
538 pub enum ColumnRuleColorParseError<'a> {
539 Color(CssColorParseError<'a>),
540 }
541 impl_debug_as_display!(ColumnRuleColorParseError<'a>);
542 impl_display! { ColumnRuleColorParseError<'a>, { Color(e) => format!("{}", e) }}
543 impl_from! { CssColorParseError<'a>, ColumnRuleColorParseError::Color }
544 #[derive(Debug, Clone, PartialEq)]
545 #[repr(C, u8)]
546 pub enum ColumnRuleColorParseErrorOwned {
547 Color(CssColorParseErrorOwned),
548 }
549 impl ColumnRuleColorParseError<'_> {
550 #[must_use]
551 pub fn to_contained(&self) -> ColumnRuleColorParseErrorOwned {
552 match self {
553 ColumnRuleColorParseError::Color(e) => {
554 ColumnRuleColorParseErrorOwned::Color(e.to_contained())
555 }
556 }
557 }
558 }
559 impl ColumnRuleColorParseErrorOwned {
560 #[must_use]
561 pub fn to_shared(&self) -> ColumnRuleColorParseError<'_> {
562 match self {
563 Self::Color(e) => ColumnRuleColorParseError::Color(e.to_shared()),
564 }
565 }
566 }
567 pub fn parse_column_rule_color(
571 input: &str,
572 ) -> Result<ColumnRuleColor, ColumnRuleColorParseError<'_>> {
573 Ok(ColumnRuleColor {
574 inner: parse_css_color(input)?,
575 })
576 }
577}
578
579#[cfg(feature = "parser")]
580pub use parser::*;
581
582#[cfg(all(test, feature = "parser"))]
583mod tests {
584 use super::*;
585
586 #[test]
587 fn test_parse_column_count() {
588 assert_eq!(parse_column_count("auto").unwrap(), ColumnCount::Auto);
589 assert_eq!(parse_column_count("3").unwrap(), ColumnCount::Integer(3));
590 assert!(parse_column_count("none").is_err());
591 assert!(parse_column_count("2.5").is_err());
592 }
593
594 #[test]
595 fn test_parse_column_width() {
596 assert_eq!(parse_column_width("auto").unwrap(), ColumnWidth::Auto);
597 assert_eq!(
598 parse_column_width("200px").unwrap(),
599 ColumnWidth::Length(PixelValue::px(200.0))
600 );
601 assert_eq!(
602 parse_column_width("15em").unwrap(),
603 ColumnWidth::Length(PixelValue::em(15.0))
604 );
605 assert!(parse_column_width("50%").is_ok()); }
607
608 #[test]
609 fn test_parse_column_span() {
610 assert_eq!(parse_column_span("none").unwrap(), ColumnSpan::None);
611 assert_eq!(parse_column_span("all").unwrap(), ColumnSpan::All);
612 assert!(parse_column_span("2").is_err());
613 }
614
615 #[test]
616 fn test_parse_column_fill() {
617 assert_eq!(parse_column_fill("auto").unwrap(), ColumnFill::Auto);
618 assert_eq!(parse_column_fill("balance").unwrap(), ColumnFill::Balance);
619 assert!(parse_column_fill("none").is_err());
620 }
621
622 #[test]
623 fn test_parse_column_rule() {
624 assert_eq!(
625 parse_column_rule_width("5px").unwrap().inner,
626 PixelValue::px(5.0)
627 );
628 assert_eq!(
629 parse_column_rule_style("dotted").unwrap().inner,
630 BorderStyle::Dotted
631 );
632 assert_eq!(parse_column_rule_color("blue").unwrap().inner, ColorU::BLUE);
633 }
634}
635
636#[cfg(all(test, feature = "parser"))]
637#[allow(clippy::float_cmp)] mod autotest_generated {
639 use super::*;
640 use crate::{codegen::format::FormatAsRustCode, corety::AzString, props::basic::SizeMetric};
641
642 const LONG: usize = 1_000_000;
644
645 #[test]
650 fn column_count_valid_minimal_and_trimming() {
651 assert_eq!(parse_column_count("auto").unwrap(), ColumnCount::Auto);
652 assert_eq!(parse_column_count("1").unwrap(), ColumnCount::Integer(1));
653 assert_eq!(parse_column_count(" auto\t\n").unwrap(), ColumnCount::Auto);
655 assert_eq!(
656 parse_column_count(" \n 12 \t").unwrap(),
657 ColumnCount::Integer(12)
658 );
659 }
660
661 #[test]
662 fn column_count_rejects_empty_and_whitespace_only() {
663 assert!(parse_column_count("").is_err());
664 assert!(parse_column_count(" ").is_err());
665 assert!(parse_column_count("\t\n\r ").is_err());
666 }
667
668 #[test]
669 fn column_count_rejects_garbage_without_panicking() {
670 for bad in [
671 "none",
672 "auto auto",
673 "3px",
674 "3;garbage",
675 "3 4",
676 "2.5",
677 "0x10",
678 "1_000",
679 "--3",
680 "+-3",
681 "\0",
682 "3\0",
683 "١٢٣",
684 "Ù£",
685 "3",
686 "NaN",
687 "inf",
688 "-inf",
689 "e5",
690 "1e3",
691 ] {
692 assert!(
693 parse_column_count(bad).is_err(),
694 "column-count accepted garbage: {bad:?}"
695 );
696 }
697 }
698
699 #[test]
700 fn column_count_case_sensitivity_is_exact() {
701 assert!(parse_column_count("AUTO").is_err());
704 assert!(parse_column_count("Auto").is_err());
705 }
706
707 #[test]
708 fn column_count_u32_boundaries_saturate_into_err_not_wrap() {
709 assert_eq!(parse_column_count("0").unwrap(), ColumnCount::Integer(0));
711 assert_eq!(parse_column_count("+7").unwrap(), ColumnCount::Integer(7));
713 assert_eq!(
715 parse_column_count("4294967295").unwrap(),
716 ColumnCount::Integer(u32::MAX)
717 );
718 assert!(parse_column_count("4294967296").is_err());
719 assert!(parse_column_count("9223372036854775807").is_err());
721 assert!(parse_column_count("18446744073709551615").is_err());
722 assert!(parse_column_count("9999999999999999999999999999999999999999").is_err());
723 assert!(parse_column_count("-1").is_err());
725 assert!(parse_column_count("-0").is_err());
726 }
727
728 #[test]
729 fn column_count_extremely_long_input_terminates() {
730 let huge = "9".repeat(LONG);
731 assert!(parse_column_count(&huge).is_err());
732 let huge_keyword = "auto".repeat(LONG / 4);
733 assert!(parse_column_count(&huge_keyword).is_err());
734 let padded = format!("{}{}{}", " ".repeat(10_000), "7", " ".repeat(10_000));
736 assert_eq!(
737 parse_column_count(&padded).unwrap(),
738 ColumnCount::Integer(7)
739 );
740 }
741
742 #[test]
743 fn column_count_deeply_nested_input_does_not_stack_overflow() {
744 let nested = "(".repeat(10_000);
745 assert!(parse_column_count(&nested).is_err());
746 let balanced = format!("{}{}", "(".repeat(10_000), ")".repeat(10_000));
747 assert!(parse_column_count(&balanced).is_err());
748 }
749
750 #[test]
751 fn column_count_unicode_input_does_not_panic() {
752 for bad in [
753 "\u{1F600}",
754 "auto\u{1F600}",
755 "e\u{301}", "\u{202E}3", "\u{FEFF}auto", "au\u{0000}to",
759 ] {
760 assert!(
761 parse_column_count(bad).is_err(),
762 "column-count accepted unicode junk: {bad:?}"
763 );
764 }
765 }
766
767 #[test]
768 fn column_count_roundtrips_through_print_as_css_value() {
769 for value in [
770 ColumnCount::Auto,
771 ColumnCount::Integer(0),
772 ColumnCount::Integer(1),
773 ColumnCount::Integer(u32::MAX),
774 ] {
775 let printed = value.print_as_css_value();
776 assert_eq!(
777 parse_column_count(&printed).unwrap(),
778 value,
779 "round-trip failed for {value:?} (printed as {printed:?})"
780 );
781 }
782 }
783
784 #[test]
789 fn column_count_error_invalid_value_roundtrips_losslessly() {
790 for s in ["", "x", " padded ", "\u{1F600}"] {
791 let shared = ColumnCountParseError::InvalidValue(s);
792 let owned = shared.to_contained();
793 assert_eq!(
794 owned,
795 ColumnCountParseErrorOwned::InvalidValue(AzString::from(s))
796 );
797 assert_eq!(owned.to_shared(), shared);
798 }
799 }
800
801 #[test]
802 fn column_count_error_parse_int_roundtrip_is_lossy_but_safe() {
803 let err = parse_column_count("abc").unwrap_err();
804 assert_eq!(
805 err,
806 ColumnCountParseError::ParseInt("abc".parse::<u32>().unwrap_err())
807 );
808
809 let owned = err.to_contained();
810 match &owned {
811 ColumnCountParseErrorOwned::ParseInt(msg) => {
812 assert!(!msg.as_str().is_empty(), "ParseInt message was dropped");
813 }
814 other => panic!("expected ParseInt, got {other:?}"),
815 }
816
817 assert_eq!(
820 owned.to_shared(),
821 ColumnCountParseError::InvalidValue("invalid integer")
822 );
823 }
824
825 #[test]
826 fn column_count_error_debug_equals_display_and_keeps_input() {
827 let err = ColumnCountParseError::InvalidValue("weird\u{1F600}input");
828 assert_eq!(format!("{err:?}"), format!("{err}"));
829 assert!(format!("{err}").contains("weird\u{1F600}input"));
830
831 let overflow = parse_column_count("4294967296").unwrap_err();
833 assert!(!format!("{overflow}").is_empty());
834 assert!(!format!("{:?}", overflow.to_contained()).is_empty());
835 }
836
837 #[test]
842 fn column_width_valid_minimal_and_trimming() {
843 assert_eq!(parse_column_width("auto").unwrap(), ColumnWidth::Auto);
844 assert_eq!(parse_column_width(" auto ").unwrap(), ColumnWidth::Auto);
845 assert_eq!(
846 parse_column_width("\t1px\n").unwrap(),
847 ColumnWidth::Length(PixelValue::px(1.0))
848 );
849 }
850
851 #[test]
852 fn column_width_rejects_empty_whitespace_and_garbage() {
853 assert!(parse_column_width("").is_err());
854 assert!(parse_column_width(" ").is_err());
855 assert!(parse_column_width("\t\n").is_err());
856 for bad in [
857 "px",
858 "em",
859 "%",
860 "ten-px",
861 "200px;garbage",
862 "200 px extra",
863 "#200px",
864 "\u{1F600}",
865 "20\u{301}px",
866 "AUTO",
867 ] {
868 assert!(
869 parse_column_width(bad).is_err(),
870 "column-width accepted garbage: {bad:?}"
871 );
872 }
873 }
874
875 #[test]
876 fn column_width_non_finite_numbers_are_stored_finite() {
877 let nan = parse_column_width("NaN").unwrap();
881 assert_eq!(nan, ColumnWidth::Length(PixelValue::px(0.0)));
882
883 for input in ["inf", "Infinity", "-inf", "-Infinity", "1e40px", "-1e40px"] {
884 let parsed = parse_column_width(input).unwrap();
885 match parsed {
886 ColumnWidth::Length(px) => assert!(
887 px.number.get().is_finite(),
888 "{input:?} produced a non-finite PixelValue"
889 ),
890 ColumnWidth::Auto => panic!("{input:?} unexpectedly parsed as auto"),
891 }
892 }
893 }
894
895 #[test]
896 fn column_width_zero_and_subnormal_boundaries() {
897 assert_eq!(
898 parse_column_width("0").unwrap(),
899 ColumnWidth::Length(PixelValue::px(0.0))
900 );
901 assert_eq!(
903 parse_column_width("-0").unwrap(),
904 parse_column_width("0").unwrap()
905 );
906 assert_eq!(
907 parse_column_width("-0px").unwrap(),
908 ColumnWidth::Length(PixelValue::px(0.0))
909 );
910 assert_eq!(
912 parse_column_width("1e-45px").unwrap(),
913 ColumnWidth::Length(PixelValue::px(0.0))
914 );
915 match parse_column_width("-10px").unwrap() {
917 ColumnWidth::Length(px) => assert!(px.number.get() < 0.0),
918 ColumnWidth::Auto => panic!("-10px parsed as auto"),
919 }
920 }
921
922 #[test]
923 fn column_width_extremely_long_input_terminates() {
924 let huge_digits = format!("{}px", "9".repeat(LONG));
925 let parsed = parse_column_width(&huge_digits).unwrap();
926 match parsed {
927 ColumnWidth::Length(px) => assert!(px.number.get().is_finite()),
928 ColumnWidth::Auto => panic!("digit soup parsed as auto"),
929 }
930 assert!(parse_column_width(&"a".repeat(LONG)).is_err());
931 assert!(parse_column_width(&"auto".repeat(LONG / 4)).is_err());
932 }
933
934 #[test]
935 fn column_width_deeply_nested_input_does_not_stack_overflow() {
936 assert!(parse_column_width(&"(".repeat(10_000)).is_err());
937 let balanced = format!("calc{}{}", "(".repeat(10_000), ")".repeat(10_000));
938 assert!(parse_column_width(&balanced).is_err());
939 }
940
941 #[test]
942 fn column_width_roundtrips_through_print_as_css_value() {
943 let values = [
946 ColumnWidth::Auto,
947 ColumnWidth::Length(PixelValue::px(0.0)),
948 ColumnWidth::Length(PixelValue::px(200.0)),
949 ColumnWidth::Length(PixelValue::px(-12.5)),
950 ColumnWidth::Length(PixelValue::em(1.5)),
951 ColumnWidth::Length(PixelValue::rem(2.0)),
952 ColumnWidth::Length(PixelValue::pt(-20.0)),
953 ColumnWidth::Length(PixelValue::percent(50.0)),
954 ColumnWidth::Length(PixelValue::inch(1.0)),
955 ColumnWidth::Length(PixelValue::cm(3.0)),
956 ColumnWidth::Length(PixelValue::mm(10.0)),
957 ColumnWidth::Length(PixelValue::from_metric(SizeMetric::Vw, 10.0)),
958 ColumnWidth::Length(PixelValue::from_metric(SizeMetric::Vh, 10.0)),
959 ColumnWidth::Length(PixelValue::from_metric(SizeMetric::Vmax, 10.0)),
960 ];
961 for value in values {
962 let printed = value.print_as_css_value();
963 assert_eq!(
964 parse_column_width(&printed).unwrap(),
965 value,
966 "round-trip failed for {value:?} (printed as {printed:?})"
967 );
968 }
969 }
970
971 #[test]
976 fn column_width_error_roundtrips_through_owned() {
977 for s in ["", "bogus", "\u{1F600}"] {
980 let shared = ColumnWidthParseError::InvalidValue(s);
981 let owned = shared.to_contained();
982 assert_eq!(
983 owned,
984 ColumnWidthParseErrorOwned::InvalidValue(AzString::from(s))
985 );
986 assert_eq!(owned.to_shared(), shared);
987 }
988
989 for bad in ["", "ten-px", "px", "%"] {
991 let err = parse_column_width(bad).unwrap_err();
992 assert!(
993 matches!(err, ColumnWidthParseError::PixelValue(_)),
994 "{bad:?} produced an unexpected error variant: {err:?}"
995 );
996 let owned = err.to_contained();
997 assert_eq!(owned.to_shared(), err, "lossy round-trip for {bad:?}");
998 assert_eq!(format!("{err:?}"), format!("{err}"));
999 }
1000 }
1001
1002 #[test]
1007 fn column_span_and_fill_accept_only_their_keywords() {
1008 assert_eq!(parse_column_span("none").unwrap(), ColumnSpan::None);
1009 assert_eq!(parse_column_span(" all \t").unwrap(), ColumnSpan::All);
1010 assert_eq!(parse_column_fill("auto").unwrap(), ColumnFill::Auto);
1011 assert_eq!(
1012 parse_column_fill("\n balance ").unwrap(),
1013 ColumnFill::Balance
1014 );
1015
1016 for bad in [
1017 "",
1018 " ",
1019 "2",
1020 "ALL",
1021 "None",
1022 "all all",
1023 "all;",
1024 "\u{1F600}",
1025 "nonee",
1026 "\0",
1027 ] {
1028 assert!(
1029 parse_column_span(bad).is_err(),
1030 "column-span accepted {bad:?}"
1031 );
1032 }
1033 for bad in [
1034 "",
1035 " ",
1036 "none",
1037 "AUTO",
1038 "balanced",
1039 "auto balance",
1040 "\u{1F600}",
1041 ] {
1042 assert!(
1043 parse_column_fill(bad).is_err(),
1044 "column-fill accepted {bad:?}"
1045 );
1046 }
1047 assert!(parse_column_span("balance").is_err());
1049 assert!(parse_column_fill("all").is_err());
1050 }
1051
1052 #[test]
1053 fn column_span_and_fill_survive_long_and_nested_input() {
1054 let huge = "all".repeat(LONG / 3);
1055 assert!(parse_column_span(&huge).is_err());
1056 assert!(parse_column_fill(&huge).is_err());
1057 let nested = "(".repeat(10_000);
1058 assert!(parse_column_span(&nested).is_err());
1059 assert!(parse_column_fill(&nested).is_err());
1060 }
1061
1062 #[test]
1063 fn column_span_error_reports_the_untrimmed_input() {
1064 let err = parse_column_span(" bogus ").unwrap_err();
1067 match err {
1068 ColumnSpanParseError::InvalidValue(s) => assert_eq!(s, " bogus "),
1069 }
1070 assert_eq!(format!("{err:?}"), format!("{err}"));
1071 assert!(format!("{err}").contains("column-span"));
1072
1073 let owned = err.to_contained();
1074 assert_eq!(
1075 owned,
1076 ColumnSpanParseErrorOwned::InvalidValue(AzString::from(" bogus "))
1077 );
1078 assert_eq!(owned.to_shared(), err);
1079
1080 let fill_err = parse_column_fill("\u{1F600}").unwrap_err();
1081 let fill_owned = fill_err.to_contained();
1082 assert_eq!(fill_owned.to_shared(), fill_err);
1083 assert!(format!("{fill_err}").contains("column-fill"));
1084 }
1085
1086 #[test]
1091 fn column_rule_width_valid_and_invalid() {
1092 assert_eq!(
1093 parse_column_rule_width("5px").unwrap().inner,
1094 PixelValue::px(5.0)
1095 );
1096 assert_eq!(
1097 parse_column_rule_width(" 0 ").unwrap().inner,
1098 PixelValue::px(0.0)
1099 );
1100 for bad in [
1101 "",
1102 " ",
1103 "auto",
1104 "solid",
1105 "px",
1106 "\u{1F600}",
1107 "5px;5px",
1108 "5 px extra",
1109 ] {
1110 assert!(
1111 parse_column_rule_width(bad).is_err(),
1112 "column-rule-width accepted {bad:?}"
1113 );
1114 }
1115 }
1116
1117 #[test]
1118 fn column_rule_width_extremes_stay_finite() {
1119 for input in ["NaN", "inf", "-inf", "1e40px", "-1e40px", "1e-45px"] {
1120 let w = parse_column_rule_width(input).unwrap();
1121 assert!(
1122 w.inner.number.get().is_finite(),
1123 "{input:?} produced a non-finite column-rule-width"
1124 );
1125 }
1126 assert!(parse_column_rule_width(&"9".repeat(LONG))
1127 .unwrap()
1128 .inner
1129 .number
1130 .get()
1131 .is_finite());
1132 assert!(parse_column_rule_width(&"(".repeat(10_000)).is_err());
1133 }
1134
1135 #[test]
1136 fn column_rule_width_default_is_medium_and_roundtrips() {
1137 let default = ColumnRuleWidth::default();
1138 assert_eq!(default.inner, PixelValue::const_px(3));
1139 assert_eq!(default.print_as_css_value(), "3px");
1140 assert_eq!(parse_column_rule_width("3px").unwrap(), default);
1141
1142 for value in [
1143 ColumnRuleWidth::default(),
1144 ColumnRuleWidth {
1145 inner: PixelValue::px(0.0),
1146 },
1147 ColumnRuleWidth {
1148 inner: PixelValue::em(2.5),
1149 },
1150 ColumnRuleWidth {
1151 inner: PixelValue::percent(-25.0),
1152 },
1153 ] {
1154 let printed = value.print_as_css_value();
1155 assert_eq!(parse_column_rule_width(&printed).unwrap(), value);
1156 }
1157 }
1158
1159 #[test]
1160 fn column_rule_width_error_roundtrips_through_owned() {
1161 for bad in ["", "auto", "px"] {
1162 let err = parse_column_rule_width(bad).unwrap_err();
1163 let owned = err.to_contained();
1164 assert_eq!(owned.to_shared(), err, "lossy round-trip for {bad:?}");
1165 assert_eq!(format!("{err:?}"), format!("{err}"));
1166 assert!(!format!("{err}").is_empty());
1167 }
1168 }
1169
1170 #[test]
1175 fn column_rule_style_accepts_every_border_style_and_roundtrips() {
1176 for style in [
1177 BorderStyle::None,
1178 BorderStyle::Solid,
1179 BorderStyle::Double,
1180 BorderStyle::Dotted,
1181 BorderStyle::Dashed,
1182 BorderStyle::Hidden,
1183 BorderStyle::Groove,
1184 BorderStyle::Ridge,
1185 BorderStyle::Inset,
1186 BorderStyle::Outset,
1187 ] {
1188 let value = ColumnRuleStyle { inner: style };
1189 let printed = value.print_as_css_value();
1190 assert_eq!(
1191 parse_column_rule_style(&printed).unwrap(),
1192 value,
1193 "round-trip failed for {style:?} (printed as {printed:?})"
1194 );
1195 }
1196 }
1197
1198 #[test]
1199 fn column_rule_style_rejects_garbage_without_panicking() {
1200 for bad in [
1201 "",
1202 " ",
1203 "SOLID",
1204 "solidd",
1205 "solid solid",
1206 "3px",
1207 "\u{1F600}",
1208 "\0",
1209 "soli\u{0301}d",
1210 ] {
1211 assert!(
1212 parse_column_rule_style(bad).is_err(),
1213 "column-rule-style accepted {bad:?}"
1214 );
1215 }
1216 assert!(parse_column_rule_style(&"solid".repeat(LONG / 5)).is_err());
1217 assert!(parse_column_rule_style(&"(".repeat(10_000)).is_err());
1218 assert_eq!(
1220 parse_column_rule_style(" dotted \n").unwrap().inner,
1221 BorderStyle::Dotted
1222 );
1223 }
1224
1225 #[test]
1226 fn column_rule_style_error_roundtrips_through_owned() {
1227 let err = parse_column_rule_style("bogus").unwrap_err();
1228 let owned = err.to_contained();
1229 assert_eq!(owned.to_shared(), err);
1230 assert_eq!(format!("{err:?}"), format!("{err}"));
1231 assert!(format!("{err}").contains("bogus"));
1232
1233 let unicode_err = parse_column_rule_style("\u{1F600}").unwrap_err();
1234 let unicode_owned = unicode_err.to_contained();
1235 assert_eq!(unicode_owned.to_shared(), unicode_err);
1236 }
1237
1238 #[test]
1243 fn column_rule_color_accepts_names_hex_and_functions() {
1244 assert_eq!(parse_column_rule_color("blue").unwrap().inner, ColorU::BLUE);
1245 assert_eq!(
1247 parse_column_rule_color(" BLUE \t").unwrap().inner,
1248 ColorU::BLUE
1249 );
1250 assert_eq!(
1251 parse_column_rule_color("#000f").unwrap().inner,
1252 ColorU::BLACK
1253 );
1254 assert_eq!(
1255 parse_column_rule_color("#ff000080").unwrap().inner,
1256 ColorU::rgba(255, 0, 0, 128)
1257 );
1258 assert_eq!(parse_column_rule_color("transparent").unwrap().inner.a, 0);
1259 assert_eq!(
1260 parse_column_rule_color("rgba(255, 0, 0, 1.0)")
1261 .unwrap()
1262 .inner,
1263 ColorU::RED
1264 );
1265 }
1266
1267 #[test]
1268 fn column_rule_color_rejects_garbage_without_panicking() {
1269 for bad in [
1270 "",
1271 " ",
1272 "#",
1273 "#f",
1274 "#ff",
1275 "#fffff",
1276 "#zzzzzz",
1277 "notacolor",
1278 "rgb(",
1279 "rgb(1,2",
1280 "rgb()",
1281 "rgb(300)",
1282 "hsl(",
1283 "\u{1F600}",
1284 "#\u{1F600}",
1285 "\u{130}", ] {
1287 assert!(
1288 parse_column_rule_color(bad).is_err(),
1289 "column-rule-color accepted {bad:?}"
1290 );
1291 }
1292 }
1293
1294 #[test]
1295 fn column_rule_color_survives_long_and_nested_input() {
1296 assert!(parse_column_rule_color(&"a".repeat(100_000)).is_err());
1297 assert!(parse_column_rule_color(&"#".repeat(100_000)).is_err());
1298 assert!(parse_column_rule_color(&"(".repeat(10_000)).is_err());
1300 assert!(parse_column_rule_color(&format!("rgb{}", "(".repeat(10_000))).is_err());
1301 let nested = format!("rgb{}{}", "(".repeat(10_000), ")".repeat(10_000));
1302 assert!(parse_column_rule_color(&nested).is_err());
1303 }
1304
1305 #[test]
1306 fn column_rule_color_roundtrips_through_to_hash() {
1307 for color in [
1308 ColorU::BLACK,
1309 ColorU::WHITE,
1310 ColorU::RED,
1311 ColorU::BLUE,
1312 ColorU::TRANSPARENT,
1313 ColorU::rgba(1, 2, 3, 4),
1314 ColorU::rgba(255, 255, 255, 0),
1315 ColorU::rgba(0, 0, 0, 255),
1316 ] {
1317 let value = ColumnRuleColor { inner: color };
1318 let printed = value.print_as_css_value(); assert_eq!(printed.len(), 9, "unexpected hash form: {printed:?}");
1320 assert_eq!(
1321 parse_column_rule_color(&printed).unwrap(),
1322 value,
1323 "round-trip failed for {color:?} (printed as {printed:?})"
1324 );
1325 }
1326 }
1327
1328 #[test]
1329 fn column_rule_color_error_roundtrips_through_owned() {
1330 for bad in ["", "notacolor", "rgb(", "#zzzzzz"] {
1331 let err = parse_column_rule_color(bad).unwrap_err();
1332 let owned = err.to_contained();
1333 assert_eq!(owned.to_shared(), err, "lossy round-trip for {bad:?}");
1334 assert_eq!(format!("{err:?}"), format!("{err}"));
1335 assert!(!format!("{err}").is_empty());
1336 }
1337 }
1338
1339 #[test]
1344 fn defaults_match_the_documented_css_initial_values() {
1345 assert_eq!(ColumnCount::default(), ColumnCount::Auto);
1346 assert_eq!(ColumnWidth::default(), ColumnWidth::Auto);
1347 assert_eq!(ColumnSpan::default(), ColumnSpan::None);
1348 assert_eq!(ColumnFill::default(), ColumnFill::Balance);
1349 assert_eq!(ColumnRuleStyle::default().inner, BorderStyle::None);
1350 assert_eq!(ColumnRuleColor::default().inner, ColorU::BLACK);
1352
1353 assert_eq!(
1355 parse_column_count(&ColumnCount::default().print_as_css_value()).unwrap(),
1356 ColumnCount::default()
1357 );
1358 assert_eq!(
1359 parse_column_width(&ColumnWidth::default().print_as_css_value()).unwrap(),
1360 ColumnWidth::default()
1361 );
1362 assert_eq!(
1363 parse_column_span(&ColumnSpan::default().print_as_css_value()).unwrap(),
1364 ColumnSpan::default()
1365 );
1366 assert_eq!(
1367 parse_column_fill(&ColumnFill::default().print_as_css_value()).unwrap(),
1368 ColumnFill::default()
1369 );
1370 assert_eq!(
1371 parse_column_rule_width(&ColumnRuleWidth::default().print_as_css_value()).unwrap(),
1372 ColumnRuleWidth::default()
1373 );
1374 assert_eq!(
1375 parse_column_rule_style(&ColumnRuleStyle::default().print_as_css_value()).unwrap(),
1376 ColumnRuleStyle::default()
1377 );
1378 assert_eq!(
1379 parse_column_rule_color(&ColumnRuleColor::default().print_as_css_value()).unwrap(),
1380 ColumnRuleColor::default()
1381 );
1382 }
1383
1384 #[test]
1385 fn ord_and_hash_agree_with_eq() {
1386 use std::{
1387 collections::hash_map::DefaultHasher,
1388 hash::{Hash, Hasher},
1389 };
1390
1391 fn hash_of<T: Hash>(t: &T) -> u64 {
1392 let mut h = DefaultHasher::new();
1393 t.hash(&mut h);
1394 h.finish()
1395 }
1396
1397 assert!(ColumnCount::Auto < ColumnCount::Integer(0));
1399 assert!(ColumnCount::Integer(1) < ColumnCount::Integer(u32::MAX));
1400 assert!(ColumnWidth::Auto < ColumnWidth::Length(PixelValue::px(0.0)));
1401 assert!(ColumnSpan::None < ColumnSpan::All);
1402 assert!(ColumnFill::Auto < ColumnFill::Balance);
1403
1404 assert_eq!(
1406 hash_of(&ColumnCount::Integer(7)),
1407 hash_of(&parse_column_count("7").unwrap())
1408 );
1409 assert_eq!(
1410 hash_of(&ColumnWidth::Length(PixelValue::px(0.0))),
1411 hash_of(&parse_column_width("-0px").unwrap())
1412 );
1413 assert_ne!(hash_of(&ColumnFill::Auto), hash_of(&ColumnFill::Balance));
1414 }
1415
1416 #[test]
1417 fn format_as_rust_code_emits_constructible_snippets() {
1418 assert_eq!(
1419 ColumnCount::Auto.format_as_rust_code(0),
1420 "ColumnCount::Auto"
1421 );
1422 assert_eq!(
1423 ColumnCount::Integer(u32::MAX).format_as_rust_code(0),
1424 "ColumnCount::Integer(4294967295)"
1425 );
1426 assert_eq!(
1427 ColumnWidth::Auto.format_as_rust_code(0),
1428 "ColumnWidth::Auto"
1429 );
1430 assert_eq!(ColumnSpan::All.format_as_rust_code(0), "ColumnSpan::All");
1431 assert_eq!(ColumnSpan::None.format_as_rust_code(0), "ColumnSpan::None");
1432 assert_eq!(ColumnFill::Auto.format_as_rust_code(0), "ColumnFill::Auto");
1433 assert_eq!(
1434 ColumnFill::Balance.format_as_rust_code(0),
1435 "ColumnFill::Balance"
1436 );
1437
1438 let wide = ColumnWidth::Length(PixelValue::px(f32::MAX));
1440 assert!(wide
1441 .format_as_rust_code(0)
1442 .starts_with("ColumnWidth::Length("));
1443 let rule = ColumnRuleWidth {
1444 inner: PixelValue::px(-0.5),
1445 };
1446 assert!(rule.format_as_rust_code(0).starts_with("ColumnRuleWidth {"));
1447 let style = ColumnRuleStyle {
1448 inner: BorderStyle::Dotted,
1449 };
1450 assert!(style.format_as_rust_code(0).contains("Dotted"));
1451 let color = ColumnRuleColor {
1452 inner: ColorU::TRANSPARENT,
1453 };
1454 assert!(color
1455 .format_as_rust_code(0)
1456 .starts_with("ColumnRuleColor {"));
1457 }
1458}