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
37
38impl PrintAsCssValue for ColumnCount {
39 fn print_as_css_value(&self) -> String {
40 match self {
41 Self::Auto => "auto".to_string(),
42 Self::Integer(i) => i.to_string(),
43 }
44 }
45}
46
47#[allow(variant_size_differences)] #[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
61
62impl PrintAsCssValue for ColumnWidth {
63 fn print_as_css_value(&self) -> String {
64 match self {
65 Self::Auto => "auto".to_string(),
66 Self::Length(px) => px.print_as_css_value(),
67 }
68 }
69}
70
71#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
77#[repr(C)]
78#[derive(Default)]
79pub enum ColumnSpan {
80 #[default]
81 None,
82 All,
83}
84
85
86impl PrintAsCssValue for ColumnSpan {
87 fn print_as_css_value(&self) -> String {
88 String::from(match self {
89 Self::None => "none",
90 Self::All => "all",
91 })
92 }
93}
94
95#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
101#[repr(C)]
102#[derive(Default)]
103pub enum ColumnFill {
104 Auto,
105 #[default]
106 Balance,
107}
108
109
110impl PrintAsCssValue for ColumnFill {
111 fn print_as_css_value(&self) -> String {
112 String::from(match self {
113 Self::Auto => "auto",
114 Self::Balance => "balance",
115 })
116 }
117}
118
119#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
125#[repr(C)]
126pub struct ColumnRuleWidth {
127 pub inner: PixelValue,
128}
129
130impl Default for ColumnRuleWidth {
131 fn default() -> Self {
132 Self {
133 inner: PixelValue::const_px(3),
134 }
135 }
136}
137
138impl PrintAsCssValue for ColumnRuleWidth {
139 fn print_as_css_value(&self) -> String {
140 self.inner.print_as_css_value()
141 }
142}
143
144#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
148#[repr(C)]
149pub struct ColumnRuleStyle {
150 pub inner: BorderStyle,
151}
152
153impl Default for ColumnRuleStyle {
154 fn default() -> Self {
155 Self {
156 inner: BorderStyle::None,
157 }
158 }
159}
160
161impl PrintAsCssValue for ColumnRuleStyle {
162 fn print_as_css_value(&self) -> String {
163 self.inner.print_as_css_value()
164 }
165}
166
167#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
173#[repr(C)]
174pub struct ColumnRuleColor {
175 pub inner: ColorU,
176}
177
178impl Default for ColumnRuleColor {
179 fn default() -> Self {
180 Self {
182 inner: ColorU::BLACK,
183 }
184 }
185}
186
187impl PrintAsCssValue for ColumnRuleColor {
188 fn print_as_css_value(&self) -> String {
189 self.inner.to_hash()
190 }
191}
192
193impl crate::codegen::format::FormatAsRustCode for ColumnCount {
195 fn format_as_rust_code(&self, _tabs: usize) -> String {
196 match self {
197 Self::Auto => String::from("ColumnCount::Auto"),
198 Self::Integer(i) => format!("ColumnCount::Integer({i})"),
199 }
200 }
201}
202
203impl crate::codegen::format::FormatAsRustCode for ColumnWidth {
204 fn format_as_rust_code(&self, _tabs: usize) -> String {
205 match self {
206 Self::Auto => String::from("ColumnWidth::Auto"),
207 Self::Length(px) => format!(
208 "ColumnWidth::Length({})",
209 crate::codegen::format::format_pixel_value(px)
210 ),
211 }
212 }
213}
214
215impl crate::codegen::format::FormatAsRustCode for ColumnSpan {
216 fn format_as_rust_code(&self, _tabs: usize) -> String {
217 match self {
218 Self::None => String::from("ColumnSpan::None"),
219 Self::All => String::from("ColumnSpan::All"),
220 }
221 }
222}
223
224impl crate::codegen::format::FormatAsRustCode for ColumnFill {
225 fn format_as_rust_code(&self, _tabs: usize) -> String {
226 match self {
227 Self::Auto => String::from("ColumnFill::Auto"),
228 Self::Balance => String::from("ColumnFill::Balance"),
229 }
230 }
231}
232
233impl crate::codegen::format::FormatAsRustCode for ColumnRuleWidth {
234 fn format_as_rust_code(&self, _tabs: usize) -> String {
235 format!(
236 "ColumnRuleWidth {{ inner: {} }}",
237 crate::codegen::format::format_pixel_value(&self.inner)
238 )
239 }
240}
241
242impl crate::codegen::format::FormatAsRustCode for ColumnRuleStyle {
243 fn format_as_rust_code(&self, tabs: usize) -> String {
244 format!(
245 "ColumnRuleStyle {{ inner: {} }}",
246 self.inner.format_as_rust_code(tabs)
247 )
248 }
249}
250
251impl crate::codegen::format::FormatAsRustCode for ColumnRuleColor {
252 fn format_as_rust_code(&self, _tabs: usize) -> String {
253 format!(
254 "ColumnRuleColor {{ inner: {} }}",
255 crate::codegen::format::format_color_value(&self.inner)
256 )
257 }
258}
259
260#[cfg(feature = "parser")]
263pub mod parser {
264 #[allow(clippy::wildcard_imports)] use super::*;
266 use crate::corety::AzString;
267
268 #[derive(Clone, PartialEq, Eq)]
271 pub enum ColumnCountParseError<'a> {
272 InvalidValue(&'a str),
273 ParseInt(ParseIntError),
274 }
275
276 impl_debug_as_display!(ColumnCountParseError<'a>);
277 impl_display! { ColumnCountParseError<'a>, {
278 InvalidValue(v) => format!("Invalid column-count value: \"{}\"", v),
279 ParseInt(e) => format!("Invalid integer for column-count: {}", e),
280 }}
281
282 #[derive(Debug, Clone, PartialEq, Eq)]
283 #[repr(C, u8)]
284 pub enum ColumnCountParseErrorOwned {
285 InvalidValue(AzString),
286 ParseInt(AzString),
287 }
288
289 impl ColumnCountParseError<'_> {
290 #[must_use] pub fn to_contained(&self) -> ColumnCountParseErrorOwned {
291 match self {
292 Self::InvalidValue(s) => ColumnCountParseErrorOwned::InvalidValue((*s).to_string().into()),
293 Self::ParseInt(e) => ColumnCountParseErrorOwned::ParseInt(e.to_string().into()),
294 }
295 }
296 }
297
298 impl ColumnCountParseErrorOwned {
299 #[must_use] pub fn to_shared(&self) -> ColumnCountParseError<'_> {
300 match self {
301 Self::InvalidValue(s) => ColumnCountParseError::InvalidValue(s),
302 Self::ParseInt(_) => ColumnCountParseError::InvalidValue("invalid integer"),
306 }
307 }
308 }
309
310 pub fn parse_column_count(
314 input: &str,
315 ) -> Result<ColumnCount, ColumnCountParseError<'_>> {
316 let trimmed = input.trim();
317 if trimmed == "auto" {
318 return Ok(ColumnCount::Auto);
319 }
320 let val: u32 = trimmed
321 .parse()
322 .map_err(ColumnCountParseError::ParseInt)?;
323 Ok(ColumnCount::Integer(val))
324 }
325
326 #[derive(Clone, PartialEq, Eq)]
329 pub enum ColumnWidthParseError<'a> {
330 InvalidValue(&'a str),
331 PixelValue(CssPixelValueParseError<'a>),
332 }
333
334 impl_debug_as_display!(ColumnWidthParseError<'a>);
335 impl_display! { ColumnWidthParseError<'a>, {
336 InvalidValue(v) => format!("Invalid column-width value: \"{}\"", v),
337 PixelValue(e) => format!("{}", e),
338 }}
339 impl_from! { CssPixelValueParseError<'a>, ColumnWidthParseError::PixelValue }
340
341 #[derive(Debug, Clone, PartialEq, Eq)]
342 #[repr(C, u8)]
343 pub enum ColumnWidthParseErrorOwned {
344 InvalidValue(AzString),
345 PixelValue(CssPixelValueParseErrorOwned),
346 }
347
348 impl ColumnWidthParseError<'_> {
349 #[must_use] pub fn to_contained(&self) -> ColumnWidthParseErrorOwned {
350 match self {
351 Self::InvalidValue(s) => ColumnWidthParseErrorOwned::InvalidValue((*s).to_string().into()),
352 Self::PixelValue(e) => ColumnWidthParseErrorOwned::PixelValue(e.to_contained()),
353 }
354 }
355 }
356
357 impl ColumnWidthParseErrorOwned {
358 #[must_use] pub fn to_shared(&self) -> ColumnWidthParseError<'_> {
359 match self {
360 Self::InvalidValue(s) => ColumnWidthParseError::InvalidValue(s),
361 Self::PixelValue(e) => ColumnWidthParseError::PixelValue(e.to_shared()),
362 }
363 }
364 }
365
366 pub fn parse_column_width(
370 input: &str,
371 ) -> 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] pub fn to_contained(&self) -> ColumnRuleWidthParseErrorOwned {
469 match self {
470 ColumnRuleWidthParseError::Pixel(e) => {
471 ColumnRuleWidthParseErrorOwned::Pixel(e.to_contained())
472 }
473 }
474 }
475 }
476 impl ColumnRuleWidthParseErrorOwned {
477 #[must_use] pub fn to_shared(&self) -> ColumnRuleWidthParseError<'_> {
478 match self {
479 Self::Pixel(e) => {
480 ColumnRuleWidthParseError::Pixel(e.to_shared())
481 }
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] pub fn to_contained(&self) -> ColumnRuleStyleParseErrorOwned {
510 match self {
511 ColumnRuleStyleParseError::Style(e) => {
512 ColumnRuleStyleParseErrorOwned::Style(e.to_contained())
513 }
514 }
515 }
516 }
517 impl ColumnRuleStyleParseErrorOwned {
518 #[must_use] pub fn to_shared(&self) -> ColumnRuleStyleParseError<'_> {
519 match self {
520 Self::Style(e) => {
521 ColumnRuleStyleParseError::Style(e.to_shared())
522 }
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] pub fn to_contained(&self) -> ColumnRuleColorParseErrorOwned {
551 match self {
552 ColumnRuleColorParseError::Color(e) => {
553 ColumnRuleColorParseErrorOwned::Color(e.to_contained())
554 }
555 }
556 }
557 }
558 impl ColumnRuleColorParseErrorOwned {
559 #[must_use] pub fn to_shared(&self) -> ColumnRuleColorParseError<'_> {
560 match self {
561 Self::Color(e) => {
562 ColumnRuleColorParseError::Color(e.to_shared())
563 }
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!(parse_column_count(" \n 12 \t").unwrap(), ColumnCount::Integer(12));
656 }
657
658 #[test]
659 fn column_count_rejects_empty_and_whitespace_only() {
660 assert!(parse_column_count("").is_err());
661 assert!(parse_column_count(" ").is_err());
662 assert!(parse_column_count("\t\n\r ").is_err());
663 }
664
665 #[test]
666 fn column_count_rejects_garbage_without_panicking() {
667 for bad in [
668 "none", "auto auto", "3px", "3;garbage", "3 4", "2.5", "0x10", "1_000", "--3", "+-3",
669 "\0", "3\0", "١٢٣", "٣", "3", "NaN", "inf", "-inf", "e5", "1e3",
670 ] {
671 assert!(
672 parse_column_count(bad).is_err(),
673 "column-count accepted garbage: {bad:?}"
674 );
675 }
676 }
677
678 #[test]
679 fn column_count_case_sensitivity_is_exact() {
680 assert!(parse_column_count("AUTO").is_err());
683 assert!(parse_column_count("Auto").is_err());
684 }
685
686 #[test]
687 fn column_count_u32_boundaries_saturate_into_err_not_wrap() {
688 assert_eq!(parse_column_count("0").unwrap(), ColumnCount::Integer(0));
690 assert_eq!(parse_column_count("+7").unwrap(), ColumnCount::Integer(7));
692 assert_eq!(
694 parse_column_count("4294967295").unwrap(),
695 ColumnCount::Integer(u32::MAX)
696 );
697 assert!(parse_column_count("4294967296").is_err());
698 assert!(parse_column_count("9223372036854775807").is_err());
700 assert!(parse_column_count("18446744073709551615").is_err());
701 assert!(parse_column_count("9999999999999999999999999999999999999999").is_err());
702 assert!(parse_column_count("-1").is_err());
704 assert!(parse_column_count("-0").is_err());
705 }
706
707 #[test]
708 fn column_count_extremely_long_input_terminates() {
709 let huge = "9".repeat(LONG);
710 assert!(parse_column_count(&huge).is_err());
711 let huge_keyword = "auto".repeat(LONG / 4);
712 assert!(parse_column_count(&huge_keyword).is_err());
713 let padded = format!("{}{}{}", " ".repeat(10_000), "7", " ".repeat(10_000));
715 assert_eq!(parse_column_count(&padded).unwrap(), ColumnCount::Integer(7));
716 }
717
718 #[test]
719 fn column_count_deeply_nested_input_does_not_stack_overflow() {
720 let nested = "(".repeat(10_000);
721 assert!(parse_column_count(&nested).is_err());
722 let balanced = format!("{}{}", "(".repeat(10_000), ")".repeat(10_000));
723 assert!(parse_column_count(&balanced).is_err());
724 }
725
726 #[test]
727 fn column_count_unicode_input_does_not_panic() {
728 for bad in [
729 "\u{1F600}",
730 "auto\u{1F600}",
731 "e\u{301}", "\u{202E}3", "\u{FEFF}auto", "au\u{0000}to",
735 ] {
736 assert!(
737 parse_column_count(bad).is_err(),
738 "column-count accepted unicode junk: {bad:?}"
739 );
740 }
741 }
742
743 #[test]
744 fn column_count_roundtrips_through_print_as_css_value() {
745 for value in [
746 ColumnCount::Auto,
747 ColumnCount::Integer(0),
748 ColumnCount::Integer(1),
749 ColumnCount::Integer(u32::MAX),
750 ] {
751 let printed = value.print_as_css_value();
752 assert_eq!(
753 parse_column_count(&printed).unwrap(),
754 value,
755 "round-trip failed for {value:?} (printed as {printed:?})"
756 );
757 }
758 }
759
760 #[test]
765 fn column_count_error_invalid_value_roundtrips_losslessly() {
766 for s in ["", "x", " padded ", "\u{1F600}"] {
767 let shared = ColumnCountParseError::InvalidValue(s);
768 let owned = shared.to_contained();
769 assert_eq!(owned, ColumnCountParseErrorOwned::InvalidValue(AzString::from(s)));
770 assert_eq!(owned.to_shared(), shared);
771 }
772 }
773
774 #[test]
775 fn column_count_error_parse_int_roundtrip_is_lossy_but_safe() {
776 let err = parse_column_count("abc").unwrap_err();
777 assert_eq!(
778 err,
779 ColumnCountParseError::ParseInt("abc".parse::<u32>().unwrap_err())
780 );
781
782 let owned = err.to_contained();
783 match &owned {
784 ColumnCountParseErrorOwned::ParseInt(msg) => {
785 assert!(!msg.as_str().is_empty(), "ParseInt message was dropped");
786 }
787 other => panic!("expected ParseInt, got {other:?}"),
788 }
789
790 assert_eq!(
793 owned.to_shared(),
794 ColumnCountParseError::InvalidValue("invalid integer")
795 );
796 }
797
798 #[test]
799 fn column_count_error_debug_equals_display_and_keeps_input() {
800 let err = ColumnCountParseError::InvalidValue("weird\u{1F600}input");
801 assert_eq!(format!("{err:?}"), format!("{err}"));
802 assert!(format!("{err}").contains("weird\u{1F600}input"));
803
804 let overflow = parse_column_count("4294967296").unwrap_err();
806 assert!(!format!("{overflow}").is_empty());
807 assert!(!format!("{:?}", overflow.to_contained()).is_empty());
808 }
809
810 #[test]
815 fn column_width_valid_minimal_and_trimming() {
816 assert_eq!(parse_column_width("auto").unwrap(), ColumnWidth::Auto);
817 assert_eq!(parse_column_width(" auto ").unwrap(), ColumnWidth::Auto);
818 assert_eq!(
819 parse_column_width("\t1px\n").unwrap(),
820 ColumnWidth::Length(PixelValue::px(1.0))
821 );
822 }
823
824 #[test]
825 fn column_width_rejects_empty_whitespace_and_garbage() {
826 assert!(parse_column_width("").is_err());
827 assert!(parse_column_width(" ").is_err());
828 assert!(parse_column_width("\t\n").is_err());
829 for bad in [
830 "px", "em", "%", "ten-px", "200px;garbage", "200 px extra", "#200px", "\u{1F600}",
831 "20\u{301}px", "AUTO",
832 ] {
833 assert!(
834 parse_column_width(bad).is_err(),
835 "column-width accepted garbage: {bad:?}"
836 );
837 }
838 }
839
840 #[test]
841 fn column_width_non_finite_numbers_are_stored_finite() {
842 let nan = parse_column_width("NaN").unwrap();
846 assert_eq!(nan, ColumnWidth::Length(PixelValue::px(0.0)));
847
848 for input in ["inf", "Infinity", "-inf", "-Infinity", "1e40px", "-1e40px"] {
849 let parsed = parse_column_width(input).unwrap();
850 match parsed {
851 ColumnWidth::Length(px) => assert!(
852 px.number.get().is_finite(),
853 "{input:?} produced a non-finite PixelValue"
854 ),
855 ColumnWidth::Auto => panic!("{input:?} unexpectedly parsed as auto"),
856 }
857 }
858 }
859
860 #[test]
861 fn column_width_zero_and_subnormal_boundaries() {
862 assert_eq!(
863 parse_column_width("0").unwrap(),
864 ColumnWidth::Length(PixelValue::px(0.0))
865 );
866 assert_eq!(
868 parse_column_width("-0").unwrap(),
869 parse_column_width("0").unwrap()
870 );
871 assert_eq!(
872 parse_column_width("-0px").unwrap(),
873 ColumnWidth::Length(PixelValue::px(0.0))
874 );
875 assert_eq!(
877 parse_column_width("1e-45px").unwrap(),
878 ColumnWidth::Length(PixelValue::px(0.0))
879 );
880 match parse_column_width("-10px").unwrap() {
882 ColumnWidth::Length(px) => assert!(px.number.get() < 0.0),
883 ColumnWidth::Auto => panic!("-10px parsed as auto"),
884 }
885 }
886
887 #[test]
888 fn column_width_extremely_long_input_terminates() {
889 let huge_digits = format!("{}px", "9".repeat(LONG));
890 let parsed = parse_column_width(&huge_digits).unwrap();
891 match parsed {
892 ColumnWidth::Length(px) => assert!(px.number.get().is_finite()),
893 ColumnWidth::Auto => panic!("digit soup parsed as auto"),
894 }
895 assert!(parse_column_width(&"a".repeat(LONG)).is_err());
896 assert!(parse_column_width(&"auto".repeat(LONG / 4)).is_err());
897 }
898
899 #[test]
900 fn column_width_deeply_nested_input_does_not_stack_overflow() {
901 assert!(parse_column_width(&"(".repeat(10_000)).is_err());
902 let balanced = format!("calc{}{}", "(".repeat(10_000), ")".repeat(10_000));
903 assert!(parse_column_width(&balanced).is_err());
904 }
905
906 #[test]
907 fn column_width_roundtrips_through_print_as_css_value() {
908 let values = [
911 ColumnWidth::Auto,
912 ColumnWidth::Length(PixelValue::px(0.0)),
913 ColumnWidth::Length(PixelValue::px(200.0)),
914 ColumnWidth::Length(PixelValue::px(-12.5)),
915 ColumnWidth::Length(PixelValue::em(1.5)),
916 ColumnWidth::Length(PixelValue::rem(2.0)),
917 ColumnWidth::Length(PixelValue::pt(-20.0)),
918 ColumnWidth::Length(PixelValue::percent(50.0)),
919 ColumnWidth::Length(PixelValue::inch(1.0)),
920 ColumnWidth::Length(PixelValue::cm(3.0)),
921 ColumnWidth::Length(PixelValue::mm(10.0)),
922 ColumnWidth::Length(PixelValue::from_metric(SizeMetric::Vw, 10.0)),
923 ColumnWidth::Length(PixelValue::from_metric(SizeMetric::Vh, 10.0)),
924 ColumnWidth::Length(PixelValue::from_metric(SizeMetric::Vmax, 10.0)),
925 ];
926 for value in values {
927 let printed = value.print_as_css_value();
928 assert_eq!(
929 parse_column_width(&printed).unwrap(),
930 value,
931 "round-trip failed for {value:?} (printed as {printed:?})"
932 );
933 }
934 }
935
936 #[test]
941 fn column_width_error_roundtrips_through_owned() {
942 for s in ["", "bogus", "\u{1F600}"] {
945 let shared = ColumnWidthParseError::InvalidValue(s);
946 let owned = shared.to_contained();
947 assert_eq!(owned, ColumnWidthParseErrorOwned::InvalidValue(AzString::from(s)));
948 assert_eq!(owned.to_shared(), shared);
949 }
950
951 for bad in ["", "ten-px", "px", "%"] {
953 let err = parse_column_width(bad).unwrap_err();
954 assert!(
955 matches!(err, ColumnWidthParseError::PixelValue(_)),
956 "{bad:?} produced an unexpected error variant: {err:?}"
957 );
958 let owned = err.to_contained();
959 assert_eq!(owned.to_shared(), err, "lossy round-trip for {bad:?}");
960 assert_eq!(format!("{err:?}"), format!("{err}"));
961 }
962 }
963
964 #[test]
969 fn column_span_and_fill_accept_only_their_keywords() {
970 assert_eq!(parse_column_span("none").unwrap(), ColumnSpan::None);
971 assert_eq!(parse_column_span(" all \t").unwrap(), ColumnSpan::All);
972 assert_eq!(parse_column_fill("auto").unwrap(), ColumnFill::Auto);
973 assert_eq!(parse_column_fill("\n balance ").unwrap(), ColumnFill::Balance);
974
975 for bad in ["", " ", "2", "ALL", "None", "all all", "all;", "\u{1F600}", "nonee", "\0"] {
976 assert!(parse_column_span(bad).is_err(), "column-span accepted {bad:?}");
977 }
978 for bad in ["", " ", "none", "AUTO", "balanced", "auto balance", "\u{1F600}"] {
979 assert!(parse_column_fill(bad).is_err(), "column-fill accepted {bad:?}");
980 }
981 assert!(parse_column_span("balance").is_err());
983 assert!(parse_column_fill("all").is_err());
984 }
985
986 #[test]
987 fn column_span_and_fill_survive_long_and_nested_input() {
988 let huge = "all".repeat(LONG / 3);
989 assert!(parse_column_span(&huge).is_err());
990 assert!(parse_column_fill(&huge).is_err());
991 let nested = "(".repeat(10_000);
992 assert!(parse_column_span(&nested).is_err());
993 assert!(parse_column_fill(&nested).is_err());
994 }
995
996 #[test]
997 fn column_span_error_reports_the_untrimmed_input() {
998 let err = parse_column_span(" bogus ").unwrap_err();
1001 match err {
1002 ColumnSpanParseError::InvalidValue(s) => assert_eq!(s, " bogus "),
1003 }
1004 assert_eq!(format!("{err:?}"), format!("{err}"));
1005 assert!(format!("{err}").contains("column-span"));
1006
1007 let owned = err.to_contained();
1008 assert_eq!(owned, ColumnSpanParseErrorOwned::InvalidValue(AzString::from(" bogus ")));
1009 assert_eq!(owned.to_shared(), err);
1010
1011 let fill_err = parse_column_fill("\u{1F600}").unwrap_err();
1012 let fill_owned = fill_err.to_contained();
1013 assert_eq!(fill_owned.to_shared(), fill_err);
1014 assert!(format!("{fill_err}").contains("column-fill"));
1015 }
1016
1017 #[test]
1022 fn column_rule_width_valid_and_invalid() {
1023 assert_eq!(
1024 parse_column_rule_width("5px").unwrap().inner,
1025 PixelValue::px(5.0)
1026 );
1027 assert_eq!(
1028 parse_column_rule_width(" 0 ").unwrap().inner,
1029 PixelValue::px(0.0)
1030 );
1031 for bad in ["", " ", "auto", "solid", "px", "\u{1F600}", "5px;5px", "5 px extra"] {
1032 assert!(
1033 parse_column_rule_width(bad).is_err(),
1034 "column-rule-width accepted {bad:?}"
1035 );
1036 }
1037 }
1038
1039 #[test]
1040 fn column_rule_width_extremes_stay_finite() {
1041 for input in ["NaN", "inf", "-inf", "1e40px", "-1e40px", "1e-45px"] {
1042 let w = parse_column_rule_width(input).unwrap();
1043 assert!(
1044 w.inner.number.get().is_finite(),
1045 "{input:?} produced a non-finite column-rule-width"
1046 );
1047 }
1048 assert!(parse_column_rule_width(&"9".repeat(LONG)).unwrap().inner.number.get().is_finite());
1049 assert!(parse_column_rule_width(&"(".repeat(10_000)).is_err());
1050 }
1051
1052 #[test]
1053 fn column_rule_width_default_is_medium_and_roundtrips() {
1054 let default = ColumnRuleWidth::default();
1055 assert_eq!(default.inner, PixelValue::const_px(3));
1056 assert_eq!(default.print_as_css_value(), "3px");
1057 assert_eq!(parse_column_rule_width("3px").unwrap(), default);
1058
1059 for value in [
1060 ColumnRuleWidth::default(),
1061 ColumnRuleWidth { inner: PixelValue::px(0.0) },
1062 ColumnRuleWidth { inner: PixelValue::em(2.5) },
1063 ColumnRuleWidth { inner: PixelValue::percent(-25.0) },
1064 ] {
1065 let printed = value.print_as_css_value();
1066 assert_eq!(parse_column_rule_width(&printed).unwrap(), value);
1067 }
1068 }
1069
1070 #[test]
1071 fn column_rule_width_error_roundtrips_through_owned() {
1072 for bad in ["", "auto", "px"] {
1073 let err = parse_column_rule_width(bad).unwrap_err();
1074 let owned = err.to_contained();
1075 assert_eq!(owned.to_shared(), err, "lossy round-trip for {bad:?}");
1076 assert_eq!(format!("{err:?}"), format!("{err}"));
1077 assert!(!format!("{err}").is_empty());
1078 }
1079 }
1080
1081 #[test]
1086 fn column_rule_style_accepts_every_border_style_and_roundtrips() {
1087 for style in [
1088 BorderStyle::None,
1089 BorderStyle::Solid,
1090 BorderStyle::Double,
1091 BorderStyle::Dotted,
1092 BorderStyle::Dashed,
1093 BorderStyle::Hidden,
1094 BorderStyle::Groove,
1095 BorderStyle::Ridge,
1096 BorderStyle::Inset,
1097 BorderStyle::Outset,
1098 ] {
1099 let value = ColumnRuleStyle { inner: style };
1100 let printed = value.print_as_css_value();
1101 assert_eq!(
1102 parse_column_rule_style(&printed).unwrap(),
1103 value,
1104 "round-trip failed for {style:?} (printed as {printed:?})"
1105 );
1106 }
1107 }
1108
1109 #[test]
1110 fn column_rule_style_rejects_garbage_without_panicking() {
1111 for bad in [
1112 "", " ", "SOLID", "solidd", "solid solid", "3px", "\u{1F600}", "\0", "soli\u{0301}d",
1113 ] {
1114 assert!(
1115 parse_column_rule_style(bad).is_err(),
1116 "column-rule-style accepted {bad:?}"
1117 );
1118 }
1119 assert!(parse_column_rule_style(&"solid".repeat(LONG / 5)).is_err());
1120 assert!(parse_column_rule_style(&"(".repeat(10_000)).is_err());
1121 assert_eq!(
1123 parse_column_rule_style(" dotted \n").unwrap().inner,
1124 BorderStyle::Dotted
1125 );
1126 }
1127
1128 #[test]
1129 fn column_rule_style_error_roundtrips_through_owned() {
1130 let err = parse_column_rule_style("bogus").unwrap_err();
1131 let owned = err.to_contained();
1132 assert_eq!(owned.to_shared(), err);
1133 assert_eq!(format!("{err:?}"), format!("{err}"));
1134 assert!(format!("{err}").contains("bogus"));
1135
1136 let unicode_err = parse_column_rule_style("\u{1F600}").unwrap_err();
1137 let unicode_owned = unicode_err.to_contained();
1138 assert_eq!(unicode_owned.to_shared(), unicode_err);
1139 }
1140
1141 #[test]
1146 fn column_rule_color_accepts_names_hex_and_functions() {
1147 assert_eq!(parse_column_rule_color("blue").unwrap().inner, ColorU::BLUE);
1148 assert_eq!(parse_column_rule_color(" BLUE \t").unwrap().inner, ColorU::BLUE);
1150 assert_eq!(parse_column_rule_color("#000f").unwrap().inner, ColorU::BLACK);
1151 assert_eq!(
1152 parse_column_rule_color("#ff000080").unwrap().inner,
1153 ColorU::rgba(255, 0, 0, 128)
1154 );
1155 assert_eq!(parse_column_rule_color("transparent").unwrap().inner.a, 0);
1156 assert_eq!(
1157 parse_column_rule_color("rgba(255, 0, 0, 1.0)").unwrap().inner,
1158 ColorU::RED
1159 );
1160 }
1161
1162 #[test]
1163 fn column_rule_color_rejects_garbage_without_panicking() {
1164 for bad in [
1165 "",
1166 " ",
1167 "#",
1168 "#f",
1169 "#ff",
1170 "#fffff",
1171 "#zzzzzz",
1172 "notacolor",
1173 "rgb(",
1174 "rgb(1,2",
1175 "rgb()",
1176 "rgb(300)",
1177 "hsl(",
1178 "\u{1F600}",
1179 "#\u{1F600}",
1180 "\u{130}", ] {
1182 assert!(
1183 parse_column_rule_color(bad).is_err(),
1184 "column-rule-color accepted {bad:?}"
1185 );
1186 }
1187 }
1188
1189 #[test]
1190 fn column_rule_color_survives_long_and_nested_input() {
1191 assert!(parse_column_rule_color(&"a".repeat(100_000)).is_err());
1192 assert!(parse_column_rule_color(&"#".repeat(100_000)).is_err());
1193 assert!(parse_column_rule_color(&"(".repeat(10_000)).is_err());
1195 assert!(parse_column_rule_color(&format!("rgb{}", "(".repeat(10_000))).is_err());
1196 let nested = format!("rgb{}{}", "(".repeat(10_000), ")".repeat(10_000));
1197 assert!(parse_column_rule_color(&nested).is_err());
1198 }
1199
1200 #[test]
1201 fn column_rule_color_roundtrips_through_to_hash() {
1202 for color in [
1203 ColorU::BLACK,
1204 ColorU::WHITE,
1205 ColorU::RED,
1206 ColorU::BLUE,
1207 ColorU::TRANSPARENT,
1208 ColorU::rgba(1, 2, 3, 4),
1209 ColorU::rgba(255, 255, 255, 0),
1210 ColorU::rgba(0, 0, 0, 255),
1211 ] {
1212 let value = ColumnRuleColor { inner: color };
1213 let printed = value.print_as_css_value(); assert_eq!(printed.len(), 9, "unexpected hash form: {printed:?}");
1215 assert_eq!(
1216 parse_column_rule_color(&printed).unwrap(),
1217 value,
1218 "round-trip failed for {color:?} (printed as {printed:?})"
1219 );
1220 }
1221 }
1222
1223 #[test]
1224 fn column_rule_color_error_roundtrips_through_owned() {
1225 for bad in ["", "notacolor", "rgb(", "#zzzzzz"] {
1226 let err = parse_column_rule_color(bad).unwrap_err();
1227 let owned = err.to_contained();
1228 assert_eq!(owned.to_shared(), err, "lossy round-trip for {bad:?}");
1229 assert_eq!(format!("{err:?}"), format!("{err}"));
1230 assert!(!format!("{err}").is_empty());
1231 }
1232 }
1233
1234 #[test]
1239 fn defaults_match_the_documented_css_initial_values() {
1240 assert_eq!(ColumnCount::default(), ColumnCount::Auto);
1241 assert_eq!(ColumnWidth::default(), ColumnWidth::Auto);
1242 assert_eq!(ColumnSpan::default(), ColumnSpan::None);
1243 assert_eq!(ColumnFill::default(), ColumnFill::Balance);
1244 assert_eq!(ColumnRuleStyle::default().inner, BorderStyle::None);
1245 assert_eq!(ColumnRuleColor::default().inner, ColorU::BLACK);
1247
1248 assert_eq!(
1250 parse_column_count(&ColumnCount::default().print_as_css_value()).unwrap(),
1251 ColumnCount::default()
1252 );
1253 assert_eq!(
1254 parse_column_width(&ColumnWidth::default().print_as_css_value()).unwrap(),
1255 ColumnWidth::default()
1256 );
1257 assert_eq!(
1258 parse_column_span(&ColumnSpan::default().print_as_css_value()).unwrap(),
1259 ColumnSpan::default()
1260 );
1261 assert_eq!(
1262 parse_column_fill(&ColumnFill::default().print_as_css_value()).unwrap(),
1263 ColumnFill::default()
1264 );
1265 assert_eq!(
1266 parse_column_rule_width(&ColumnRuleWidth::default().print_as_css_value()).unwrap(),
1267 ColumnRuleWidth::default()
1268 );
1269 assert_eq!(
1270 parse_column_rule_style(&ColumnRuleStyle::default().print_as_css_value()).unwrap(),
1271 ColumnRuleStyle::default()
1272 );
1273 assert_eq!(
1274 parse_column_rule_color(&ColumnRuleColor::default().print_as_css_value()).unwrap(),
1275 ColumnRuleColor::default()
1276 );
1277 }
1278
1279 #[test]
1280 fn ord_and_hash_agree_with_eq() {
1281 use std::{
1282 collections::hash_map::DefaultHasher,
1283 hash::{Hash, Hasher},
1284 };
1285
1286 fn hash_of<T: Hash>(t: &T) -> u64 {
1287 let mut h = DefaultHasher::new();
1288 t.hash(&mut h);
1289 h.finish()
1290 }
1291
1292 assert!(ColumnCount::Auto < ColumnCount::Integer(0));
1294 assert!(ColumnCount::Integer(1) < ColumnCount::Integer(u32::MAX));
1295 assert!(ColumnWidth::Auto < ColumnWidth::Length(PixelValue::px(0.0)));
1296 assert!(ColumnSpan::None < ColumnSpan::All);
1297 assert!(ColumnFill::Auto < ColumnFill::Balance);
1298
1299 assert_eq!(
1301 hash_of(&ColumnCount::Integer(7)),
1302 hash_of(&parse_column_count("7").unwrap())
1303 );
1304 assert_eq!(
1305 hash_of(&ColumnWidth::Length(PixelValue::px(0.0))),
1306 hash_of(&parse_column_width("-0px").unwrap())
1307 );
1308 assert_ne!(hash_of(&ColumnFill::Auto), hash_of(&ColumnFill::Balance));
1309 }
1310
1311 #[test]
1312 fn format_as_rust_code_emits_constructible_snippets() {
1313 assert_eq!(ColumnCount::Auto.format_as_rust_code(0), "ColumnCount::Auto");
1314 assert_eq!(
1315 ColumnCount::Integer(u32::MAX).format_as_rust_code(0),
1316 "ColumnCount::Integer(4294967295)"
1317 );
1318 assert_eq!(ColumnWidth::Auto.format_as_rust_code(0), "ColumnWidth::Auto");
1319 assert_eq!(ColumnSpan::All.format_as_rust_code(0), "ColumnSpan::All");
1320 assert_eq!(ColumnSpan::None.format_as_rust_code(0), "ColumnSpan::None");
1321 assert_eq!(ColumnFill::Auto.format_as_rust_code(0), "ColumnFill::Auto");
1322 assert_eq!(ColumnFill::Balance.format_as_rust_code(0), "ColumnFill::Balance");
1323
1324 let wide = ColumnWidth::Length(PixelValue::px(f32::MAX));
1326 assert!(wide.format_as_rust_code(0).starts_with("ColumnWidth::Length("));
1327 let rule = ColumnRuleWidth { inner: PixelValue::px(-0.5) };
1328 assert!(rule.format_as_rust_code(0).starts_with("ColumnRuleWidth {"));
1329 let style = ColumnRuleStyle { inner: BorderStyle::Dotted };
1330 assert!(style.format_as_rust_code(0).contains("Dotted"));
1331 let color = ColumnRuleColor { inner: ColorU::TRANSPARENT };
1332 assert!(color.format_as_rust_code(0).starts_with("ColumnRuleColor {"));
1333 }
1334}