1use alloc::string::{String, ToString};
4use core::fmt;
5use crate::corety::AzString;
6
7use crate::{codegen::format::FormatAsRustCode, props::formatter::PrintAsCssValue};
8
9#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
13#[repr(C)]
14#[derive(Default)]
15pub enum StyleListStyleType {
16 None,
17 #[default]
18 Disc,
19 Circle,
20 Square,
21 Decimal,
22 DecimalLeadingZero,
23 LowerRoman,
24 UpperRoman,
25 LowerGreek,
26 UpperGreek,
27 LowerAlpha,
28 UpperAlpha,
29}
30
31
32impl PrintAsCssValue for StyleListStyleType {
33 fn print_as_css_value(&self) -> String {
34 use StyleListStyleType::{None, Disc, Circle, Square, Decimal, DecimalLeadingZero, LowerRoman, UpperRoman, LowerGreek, UpperGreek, LowerAlpha, UpperAlpha};
35 String::from(match self {
36 None => "none",
37 Disc => "disc",
38 Circle => "circle",
39 Square => "square",
40 Decimal => "decimal",
41 DecimalLeadingZero => "decimal-leading-zero",
42 LowerRoman => "lower-roman",
43 UpperRoman => "upper-roman",
44 LowerGreek => "lower-greek",
45 UpperGreek => "upper-greek",
46 LowerAlpha => "lower-alpha",
47 UpperAlpha => "upper-alpha",
48 })
49 }
50}
51
52impl FormatAsRustCode for StyleListStyleType {
53 fn format_as_rust_code(&self, _tabs: usize) -> String {
54 use StyleListStyleType::{None, Disc, Circle, Square, Decimal, DecimalLeadingZero, LowerRoman, UpperRoman, LowerGreek, UpperGreek, LowerAlpha, UpperAlpha};
55 format!(
56 "StyleListStyleType::{}",
57 match self {
58 None => "None",
59 Disc => "Disc",
60 Circle => "Circle",
61 Square => "Square",
62 Decimal => "Decimal",
63 DecimalLeadingZero => "DecimalLeadingZero",
64 LowerRoman => "LowerRoman",
65 UpperRoman => "UpperRoman",
66 LowerGreek => "LowerGreek",
67 UpperGreek => "UpperGreek",
68 LowerAlpha => "LowerAlpha",
69 UpperAlpha => "UpperAlpha",
70 }
71 )
72 }
73}
74
75impl fmt::Display for StyleListStyleType {
76 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77 write!(f, "{}", self.print_as_css_value())
78 }
79}
80
81#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
85#[repr(C)]
86#[derive(Default)]
87pub enum StyleListStylePosition {
88 Inside,
89 #[default]
90 Outside,
91}
92
93
94impl PrintAsCssValue for StyleListStylePosition {
95 fn print_as_css_value(&self) -> String {
96 use StyleListStylePosition::{Inside, Outside};
97 String::from(match self {
98 Inside => "inside",
99 Outside => "outside",
100 })
101 }
102}
103
104impl FormatAsRustCode for StyleListStylePosition {
105 fn format_as_rust_code(&self, _tabs: usize) -> String {
106 use StyleListStylePosition::{Inside, Outside};
107 format!(
108 "StyleListStylePosition::{}",
109 match self {
110 Inside => "Inside",
111 Outside => "Outside",
112 }
113 )
114 }
115}
116
117impl fmt::Display for StyleListStylePosition {
118 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119 write!(f, "{}", self.print_as_css_value())
120 }
121}
122
123#[cfg(feature = "parser")]
126#[derive(Clone, PartialEq, Eq)]
127pub enum StyleListStyleTypeParseError<'a> {
128 InvalidValue(&'a str),
129}
130
131#[cfg(feature = "parser")]
132impl_debug_as_display!(StyleListStyleTypeParseError<'a>);
133
134#[cfg(feature = "parser")]
135impl_display! { StyleListStyleTypeParseError<'a>, {
136 InvalidValue(val) => format!("Invalid list-style-type value: \"{}\"", val),
137}}
138
139#[cfg(feature = "parser")]
140#[derive(Debug, Clone, PartialEq, Eq)]
141#[repr(C, u8)]
142pub enum StyleListStyleTypeParseErrorOwned {
143 InvalidValue(AzString),
144}
145
146#[cfg(feature = "parser")]
147impl StyleListStyleTypeParseError<'_> {
148 #[must_use] pub fn to_contained(&self) -> StyleListStyleTypeParseErrorOwned {
149 match self {
150 Self::InvalidValue(s) => StyleListStyleTypeParseErrorOwned::InvalidValue((*s).to_string().into()),
151 }
152 }
153}
154
155#[cfg(feature = "parser")]
156impl StyleListStyleTypeParseErrorOwned {
157 #[must_use] pub fn to_shared(&self) -> StyleListStyleTypeParseError<'_> {
158 match self {
159 Self::InvalidValue(s) => StyleListStyleTypeParseError::InvalidValue(s.as_str()),
160 }
161 }
162}
163
164#[cfg(feature = "parser")]
166pub fn parse_style_list_style_type(
170 input: &str,
171) -> Result<StyleListStyleType, StyleListStyleTypeParseError<'_>> {
172 let input = input.trim();
173 match input {
174 "none" => Ok(StyleListStyleType::None),
175 "disc" => Ok(StyleListStyleType::Disc),
176 "circle" => Ok(StyleListStyleType::Circle),
177 "square" => Ok(StyleListStyleType::Square),
178 "decimal" => Ok(StyleListStyleType::Decimal),
179 "decimal-leading-zero" => Ok(StyleListStyleType::DecimalLeadingZero),
180 "lower-roman" => Ok(StyleListStyleType::LowerRoman),
181 "upper-roman" => Ok(StyleListStyleType::UpperRoman),
182 "lower-greek" => Ok(StyleListStyleType::LowerGreek),
183 "upper-greek" => Ok(StyleListStyleType::UpperGreek),
184 "lower-alpha" | "lower-latin" => Ok(StyleListStyleType::LowerAlpha),
185 "upper-alpha" | "upper-latin" => Ok(StyleListStyleType::UpperAlpha),
186 _ => Err(StyleListStyleTypeParseError::InvalidValue(input)),
187 }
188}
189
190#[cfg(feature = "parser")]
191#[derive(Clone, PartialEq, Eq)]
192pub enum StyleListStylePositionParseError<'a> {
193 InvalidValue(&'a str),
194}
195
196#[cfg(feature = "parser")]
197impl_debug_as_display!(StyleListStylePositionParseError<'a>);
198
199#[cfg(feature = "parser")]
200impl_display! { StyleListStylePositionParseError<'a>, {
201 InvalidValue(val) => format!("Invalid list-style-position value: \"{}\"", val),
202}}
203
204#[cfg(feature = "parser")]
205#[derive(Debug, Clone, PartialEq, Eq)]
206#[repr(C, u8)]
207pub enum StyleListStylePositionParseErrorOwned {
208 InvalidValue(AzString),
209}
210
211#[cfg(feature = "parser")]
212impl StyleListStylePositionParseError<'_> {
213 #[must_use] pub fn to_contained(&self) -> StyleListStylePositionParseErrorOwned {
214 match self {
215 Self::InvalidValue(s) => {
216 StyleListStylePositionParseErrorOwned::InvalidValue((*s).to_string().into())
217 }
218 }
219 }
220}
221
222#[cfg(feature = "parser")]
223impl StyleListStylePositionParseErrorOwned {
224 #[must_use] pub fn to_shared(&self) -> StyleListStylePositionParseError<'_> {
225 match self {
226 Self::InvalidValue(s) => StyleListStylePositionParseError::InvalidValue(s.as_str()),
227 }
228 }
229}
230
231#[cfg(feature = "parser")]
233pub fn parse_style_list_style_position(
237 input: &str,
238) -> Result<StyleListStylePosition, StyleListStylePositionParseError<'_>> {
239 let input = input.trim();
240 match input {
241 "inside" => Ok(StyleListStylePosition::Inside),
242 "outside" => Ok(StyleListStylePosition::Outside),
243 _ => Err(StyleListStylePositionParseError::InvalidValue(input)),
244 }
245}
246
247#[cfg(test)]
248mod autotest_generated {
249 use std::collections::BTreeSet;
261
262 use super::*;
263
264 const ALL_TYPES: [StyleListStyleType; 12] = [
265 StyleListStyleType::None,
266 StyleListStyleType::Disc,
267 StyleListStyleType::Circle,
268 StyleListStyleType::Square,
269 StyleListStyleType::Decimal,
270 StyleListStyleType::DecimalLeadingZero,
271 StyleListStyleType::LowerRoman,
272 StyleListStyleType::UpperRoman,
273 StyleListStyleType::LowerGreek,
274 StyleListStyleType::UpperGreek,
275 StyleListStyleType::LowerAlpha,
276 StyleListStyleType::UpperAlpha,
277 ];
278
279 const ALL_POSITIONS: [StyleListStylePosition; 2] =
280 [StyleListStylePosition::Inside, StyleListStylePosition::Outside];
281
282 #[cfg(feature = "parser")]
284 fn hostile_inputs() -> Vec<String> {
285 let mut v = vec![
286 String::new(),
287 " ".to_string(),
288 " \t\n\r".to_string(),
289 "\u{0c}\u{0b}".to_string(),
290 "\0".to_string(),
291 "disc\0".to_string(),
292 "\0disc".to_string(),
293 "-".to_string(),
294 "--".to_string(),
295 ";".to_string(),
296 "{}".to_string(),
297 "/* disc */".to_string(),
298 "disc;garbage".to_string(),
299 "disc disc".to_string(),
300 "disc,circle".to_string(),
301 "disc!important".to_string(),
302 "inside;".to_string(),
303 "list-style-type: disc".to_string(),
304 "lower_roman".to_string(),
305 "lower - roman".to_string(),
306 "lowerroman".to_string(),
307 "0".to_string(),
309 "-0".to_string(),
310 "NaN".to_string(),
311 "nan".to_string(),
312 "inf".to_string(),
313 "-inf".to_string(),
314 "infinity".to_string(),
315 i64::MAX.to_string(),
316 i64::MIN.to_string(),
317 u64::MAX.to_string(),
318 f64::MAX.to_string(),
319 f64::MIN_POSITIVE.to_string(),
320 "1e308".to_string(),
321 "-1e-308".to_string(),
322 "\u{1F600}".to_string(),
324 "disc\u{301}".to_string(),
325 "\u{301}".to_string(),
326 "\u{202E}disc".to_string(),
327 "di\u{200B}sc".to_string(),
328 "DISС".to_string(), "disc".to_string(), "круг".to_string(),
331 "\u{FFFD}".to_string(),
332 ];
333 v.push("(".repeat(10_000));
336 v.push("[".repeat(10_000));
337 v.push("disc(".repeat(10_000));
338 v.push(format!("{}disc{}", "(".repeat(10_000), ")".repeat(10_000)));
339 v
340 }
341
342 #[test]
345 fn css_values_are_well_formed_for_every_type() {
346 for v in ALL_TYPES {
347 let s = v.print_as_css_value();
348 assert!(!s.is_empty(), "{v:?} serialized to an empty CSS value");
349 assert!(
350 s.chars()
351 .all(|c| c.is_ascii_lowercase() || c == '-'),
352 "{v:?} serialized to {s:?}, which is not a bare lowercase CSS keyword"
353 );
354 assert!(!s.starts_with('-') && !s.ends_with('-'), "{v:?} -> {s:?}");
355 }
356 }
357
358 #[test]
359 fn css_values_are_well_formed_for_every_position() {
360 for v in ALL_POSITIONS {
361 let s = v.print_as_css_value();
362 assert!(!s.is_empty(), "{v:?} serialized to an empty CSS value");
363 assert!(s.chars().all(|c| c.is_ascii_lowercase()), "{v:?} -> {s:?}");
364 }
365 }
366
367 #[test]
368 fn css_values_are_unique() {
369 let types: BTreeSet<String> = ALL_TYPES.iter().map(PrintAsCssValue::print_as_css_value).collect();
370 assert_eq!(types.len(), ALL_TYPES.len(), "two list-style-type variants share a CSS keyword");
371
372 let positions: BTreeSet<String> =
373 ALL_POSITIONS.iter().map(PrintAsCssValue::print_as_css_value).collect();
374 assert_eq!(positions.len(), ALL_POSITIONS.len());
375 }
376
377 #[test]
378 fn display_agrees_with_print_as_css_value() {
379 for v in ALL_TYPES {
380 assert_eq!(v.to_string(), v.print_as_css_value(), "Display diverged for {v:?}");
381 }
382 for v in ALL_POSITIONS {
383 assert_eq!(v.to_string(), v.print_as_css_value(), "Display diverged for {v:?}");
384 }
385 }
386
387 #[test]
388 fn display_of_default_is_the_css_initial_value() {
389 assert_eq!(StyleListStyleType::default(), StyleListStyleType::Disc);
391 assert_eq!(StyleListStyleType::default().to_string(), "disc");
392 assert_eq!(StyleListStylePosition::default(), StyleListStylePosition::Outside);
393 assert_eq!(StyleListStylePosition::default().to_string(), "outside");
394 }
395
396 #[test]
397 fn rust_code_names_the_variant_and_ignores_the_tab_argument() {
398 for v in ALL_TYPES {
399 let expected = format!("StyleListStyleType::{v:?}");
401 assert_eq!(v.format_as_rust_code(0), expected);
402 assert_eq!(v.format_as_rust_code(usize::MAX), expected);
404 }
405 for v in ALL_POSITIONS {
406 let expected = format!("StyleListStylePosition::{v:?}");
407 assert_eq!(v.format_as_rust_code(0), expected);
408 assert_eq!(v.format_as_rust_code(usize::MAX), expected);
409 }
410 }
411
412 #[test]
415 #[cfg(feature = "parser")]
416 fn every_type_round_trips_through_its_css_value() {
417 for v in ALL_TYPES {
418 let printed = v.print_as_css_value();
419 assert_eq!(
420 parse_style_list_style_type(&printed),
421 Ok(v),
422 "{v:?} serialized to {printed:?}, which does not parse back"
423 );
424 }
425 }
426
427 #[test]
428 #[cfg(feature = "parser")]
429 fn every_position_round_trips_through_its_css_value() {
430 for v in ALL_POSITIONS {
431 let printed = v.print_as_css_value();
432 assert_eq!(parse_style_list_style_position(&printed), Ok(v), "{v:?} -> {printed:?}");
433 }
434 }
435
436 #[test]
437 #[cfg(feature = "parser")]
438 fn parsing_is_idempotent_through_reserialization() {
439 for input in [
442 "disc", "none", "circle", "square", "decimal", "decimal-leading-zero", "lower-roman",
443 "upper-roman", "lower-greek", "upper-greek", "lower-alpha", "upper-alpha",
444 "lower-latin", "upper-latin",
445 ] {
446 let first = parse_style_list_style_type(input).expect("known-good keyword");
447 let printed = first.print_as_css_value();
448 let second = parse_style_list_style_type(&printed).expect("reserialized value must reparse");
449 assert_eq!(first, second, "{input:?} was not idempotent (printed {printed:?})");
450 }
451 }
452
453 #[test]
456 #[cfg(feature = "parser")]
457 fn valid_keywords_map_to_the_expected_variants() {
458 let table = [
459 ("none", StyleListStyleType::None),
460 ("disc", StyleListStyleType::Disc),
461 ("circle", StyleListStyleType::Circle),
462 ("square", StyleListStyleType::Square),
463 ("decimal", StyleListStyleType::Decimal),
464 ("decimal-leading-zero", StyleListStyleType::DecimalLeadingZero),
465 ("lower-roman", StyleListStyleType::LowerRoman),
466 ("upper-roman", StyleListStyleType::UpperRoman),
467 ("lower-greek", StyleListStyleType::LowerGreek),
468 ("upper-greek", StyleListStyleType::UpperGreek),
469 ("lower-alpha", StyleListStyleType::LowerAlpha),
470 ("upper-alpha", StyleListStyleType::UpperAlpha),
471 ("lower-latin", StyleListStyleType::LowerAlpha),
473 ("upper-latin", StyleListStyleType::UpperAlpha),
474 ];
475 for (input, expected) in table {
476 assert_eq!(parse_style_list_style_type(input), Ok(expected), "input {input:?}");
477 }
478
479 assert_eq!(parse_style_list_style_position("inside"), Ok(StyleListStylePosition::Inside));
480 assert_eq!(parse_style_list_style_position("outside"), Ok(StyleListStylePosition::Outside));
481 }
482
483 #[test]
484 #[cfg(feature = "parser")]
485 fn ascii_whitespace_padding_is_trimmed() {
486 for padded in [" disc", "disc\n", "\t disc \r\n", "\u{0c}disc\u{0c}"] {
487 assert_eq!(
488 parse_style_list_style_type(padded),
489 Ok(StyleListStyleType::Disc),
490 "padding was not trimmed from {padded:?}"
491 );
492 }
493 assert_eq!(
494 parse_style_list_style_position("\n\t outside \t\n"),
495 Ok(StyleListStylePosition::Outside)
496 );
497 }
498
499 #[test]
500 #[cfg(feature = "parser")]
501 fn parsers_agree_with_str_trim_on_unicode_whitespace() {
502 for pad in ["\u{a0}", "\u{2003}", "\u{3000}"] {
507 let padded = format!("{pad}disc{pad}");
508 assert_eq!(
509 parse_style_list_style_type(&padded).is_ok(),
510 padded.trim() == "disc",
511 "parser and str::trim disagree on {padded:?}"
512 );
513 }
514 }
515
516 #[test]
519 #[cfg(feature = "parser")]
520 fn hostile_input_is_rejected_without_panicking() {
521 for input in hostile_inputs() {
522 let ty = parse_style_list_style_type(&input);
523 assert!(ty.is_err(), "list-style-type accepted hostile input {input:?} as {ty:?}");
524
525 let pos = parse_style_list_style_position(&input);
526 assert!(pos.is_err(), "list-style-position accepted hostile input {input:?} as {pos:?}");
527 }
528 }
529
530 #[test]
531 #[cfg(feature = "parser")]
532 fn keyword_matching_is_case_sensitive() {
533 for input in ["Disc", "DISC", "dIsC", "LOWER-ROMAN", "Decimal-Leading-Zero", "NONE"] {
537 assert!(
538 parse_style_list_style_type(input).is_err(),
539 "unexpectedly case-insensitive for {input:?}"
540 );
541 }
542 for input in ["Inside", "OUTSIDE", "OuTsIdE"] {
543 assert!(
544 parse_style_list_style_position(input).is_err(),
545 "unexpectedly case-insensitive for {input:?}"
546 );
547 }
548 }
549
550 #[test]
551 #[cfg(feature = "parser")]
552 fn megabyte_sized_input_terminates_and_is_rejected() {
553 let long = "a".repeat(1_000_000);
554 assert!(parse_style_list_style_type(&long).is_err());
555 assert!(parse_style_list_style_position(&long).is_err());
556
557 let repeated = "disc".repeat(250_000);
559 assert!(parse_style_list_style_type(&repeated).is_err());
560
561 let padded = format!("{}disc{}", " ".repeat(1_000_000), "\n".repeat(1_000_000));
563 assert_eq!(parse_style_list_style_type(&padded), Ok(StyleListStyleType::Disc));
564 }
565
566 #[test]
567 #[cfg(feature = "parser")]
568 fn multibyte_input_is_rejected_without_slicing_panics() {
569 for input in ["🙂", " 🙂 ", "日本語", "e\u{301}", "\u{1F600}\u{1F600}\u{1F600}"] {
572 match parse_style_list_style_type(input) {
573 Ok(v) => panic!("multibyte input {input:?} parsed as {v:?}"),
574 Err(StyleListStyleTypeParseError::InvalidValue(s)) => {
575 assert_eq!(s, input.trim(), "error payload was mangled for {input:?}");
576 }
577 }
578 }
579 }
580
581 #[test]
584 #[cfg(feature = "parser")]
585 fn error_payload_is_the_trimmed_input() {
586 match parse_style_list_style_type(" bogus-keyword ") {
587 Err(StyleListStyleTypeParseError::InvalidValue(s)) => assert_eq!(s, "bogus-keyword"),
588 other => panic!("expected InvalidValue, got {other:?}"),
589 }
590 match parse_style_list_style_position("\t bogus \n") {
591 Err(StyleListStylePositionParseError::InvalidValue(s)) => assert_eq!(s, "bogus"),
592 other => panic!("expected InvalidValue, got {other:?}"),
593 }
594 match parse_style_list_style_type(" \t\n") {
596 Err(StyleListStyleTypeParseError::InvalidValue(s)) => assert!(s.is_empty()),
597 other => panic!("expected InvalidValue(\"\"), got {other:?}"),
598 }
599 }
600
601 #[test]
602 #[cfg(feature = "parser")]
603 fn error_display_quotes_the_offending_value() {
604 let err = parse_style_list_style_type("🙂").unwrap_err();
605 let msg = err.to_string();
606 assert!(msg.contains("list-style-type"), "{msg:?}");
607 assert!(msg.contains('🙂'), "error message dropped the offending value: {msg:?}");
608
609 let err = parse_style_list_style_position("").unwrap_err();
610 let msg = err.to_string();
611 assert!(msg.contains("list-style-position"), "{msg:?}");
612 assert!(!msg.is_empty());
613 }
614
615 #[test]
618 #[cfg(feature = "parser")]
619 fn type_error_survives_a_contained_shared_round_trip() {
620 let payloads = [
621 "",
622 " ",
623 "bogus",
624 "🙂 combining\u{301}",
625 "\0embedded nul\0",
626 "line\nbreak\r\n",
627 "-",
628 ];
629 for payload in payloads {
630 let shared = StyleListStyleTypeParseError::InvalidValue(payload);
631 let owned = shared.to_contained();
632 match &owned {
633 StyleListStyleTypeParseErrorOwned::InvalidValue(s) => {
634 assert_eq!(s.as_str(), payload, "to_contained mangled {payload:?}");
635 }
636 }
637 assert_eq!(owned.to_shared(), shared, "round-trip lost data for {payload:?}");
638 }
639 }
640
641 #[test]
642 #[cfg(feature = "parser")]
643 fn position_error_survives_a_contained_shared_round_trip() {
644 for payload in ["", "bogus", "🙂", "\0", " interior spaces "] {
645 let shared = StyleListStylePositionParseError::InvalidValue(payload);
646 let owned = shared.to_contained();
647 match &owned {
648 StyleListStylePositionParseErrorOwned::InvalidValue(s) => {
649 assert_eq!(s.as_str(), payload);
650 }
651 }
652 assert_eq!(owned.to_shared(), shared, "round-trip lost data for {payload:?}");
653 }
654 }
655
656 #[test]
657 #[cfg(feature = "parser")]
658 fn owned_errors_constructed_directly_convert_back_to_shared() {
659 let owned = StyleListStyleTypeParseErrorOwned::InvalidValue(String::new().into());
660 match owned.to_shared() {
661 StyleListStyleTypeParseError::InvalidValue(s) => assert_eq!(s, ""),
662 }
663
664 let owned = StyleListStylePositionParseErrorOwned::InvalidValue(String::from("x").into());
665 match owned.to_shared() {
666 StyleListStylePositionParseError::InvalidValue(s) => assert_eq!(s, "x"),
667 }
668 }
669
670 #[test]
671 #[cfg(feature = "parser")]
672 fn huge_error_payload_is_carried_without_truncation() {
673 let huge = "z".repeat(100_000);
674 let err = parse_style_list_style_type(&huge).unwrap_err();
675 let owned = err.to_contained();
676 match &owned {
677 StyleListStyleTypeParseErrorOwned::InvalidValue(s) => {
678 assert_eq!(s.as_str().len(), 100_000, "payload was truncated");
679 }
680 }
681 assert_eq!(owned.to_shared(), StyleListStyleTypeParseError::InvalidValue(&huge));
682 }
683
684 #[test]
685 #[cfg(feature = "parser")]
686 fn to_contained_is_repeatable_and_does_not_consume_the_error() {
687 let err = parse_style_list_style_position("nope").unwrap_err();
688 let a = err.to_contained();
689 let b = err.to_contained();
690 assert_eq!(a, b);
691 assert_eq!(err, StyleListStylePositionParseError::InvalidValue("nope"));
692 }
693}