1use alloc::string::{String, ToString};
8
9use crate::{corety::AzString, props::formatter::PrintAsCssValue};
10
11#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
20#[repr(C)]
21pub struct Content {
22 pub inner: AzString,
23}
24
25impl Default for Content {
26 fn default() -> Self {
27 Self {
28 inner: "normal".into(),
29 }
30 }
31}
32
33impl PrintAsCssValue for Content {
34 fn print_as_css_value(&self) -> String {
35 self.inner.as_str().to_string()
36 }
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
41#[repr(C)]
42pub struct CounterReset {
43 pub counter_name: AzString,
44 pub value: i32,
45}
46
47impl CounterReset {
48 #[must_use]
49 pub const fn new(counter_name: AzString, value: i32) -> Self {
50 Self {
51 counter_name,
52 value,
53 }
54 }
55
56 #[must_use]
57 pub const fn none() -> Self {
58 Self {
59 counter_name: AzString::from_const_str("none"),
60 value: 0,
61 }
62 }
63
64 #[must_use]
65 pub const fn list_item() -> Self {
66 Self {
67 counter_name: AzString::from_const_str("list-item"),
68 value: 0,
69 }
70 }
71}
72
73impl Default for CounterReset {
74 fn default() -> Self {
75 Self::none()
76 }
77}
78
79impl PrintAsCssValue for CounterReset {
80 fn print_as_css_value(&self) -> String {
81 if self.counter_name.as_str() == "none" {
82 "none".to_string()
83 } else {
84 alloc::format!("{} {}", self.counter_name.as_str(), self.value)
85 }
86 }
87}
88
89#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
91#[repr(C)]
92pub struct CounterIncrement {
93 pub counter_name: AzString,
94 pub value: i32,
95}
96
97impl CounterIncrement {
98 #[must_use]
99 pub const fn new(counter_name: AzString, value: i32) -> Self {
100 Self {
101 counter_name,
102 value,
103 }
104 }
105
106 #[must_use]
107 pub const fn none() -> Self {
108 Self {
109 counter_name: AzString::from_const_str("none"),
110 value: 0,
111 }
112 }
113
114 #[must_use]
115 pub const fn list_item() -> Self {
116 Self {
117 counter_name: AzString::from_const_str("list-item"),
118 value: 1,
119 }
120 }
121}
122
123impl Default for CounterIncrement {
124 fn default() -> Self {
125 Self::none()
126 }
127}
128
129impl PrintAsCssValue for CounterIncrement {
130 fn print_as_css_value(&self) -> String {
131 if self.counter_name.as_str() == "none" {
132 "none".to_string()
133 } else {
134 alloc::format!("{} {}", self.counter_name.as_str(), self.value)
135 }
136 }
137}
138
139#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
143#[repr(C)]
144pub struct StringSet {
145 pub inner: AzString,
146}
147
148impl Default for StringSet {
149 fn default() -> Self {
150 Self {
151 inner: "none".into(),
152 }
153 }
154}
155
156impl PrintAsCssValue for StringSet {
157 fn print_as_css_value(&self) -> String {
158 self.inner.as_str().to_string()
159 }
160}
161
162impl crate::codegen::format::FormatAsRustCode for Content {
164 fn format_as_rust_code(&self, _tabs: usize) -> String {
165 format!("Content {{ inner: String::from({:?}) }}", self.inner)
166 }
167}
168
169impl crate::codegen::format::FormatAsRustCode for CounterReset {
170 fn format_as_rust_code(&self, _tabs: usize) -> String {
171 alloc::format!(
172 "CounterReset {{ counter_name: AzString::from_const_str({:?}), value: {} }}",
173 self.counter_name.as_str(),
174 self.value
175 )
176 }
177}
178
179impl crate::codegen::format::FormatAsRustCode for CounterIncrement {
180 fn format_as_rust_code(&self, _tabs: usize) -> String {
181 alloc::format!(
182 "CounterIncrement {{ counter_name: AzString::from_const_str({:?}), value: {} }}",
183 self.counter_name.as_str(),
184 self.value
185 )
186 }
187}
188
189impl crate::codegen::format::FormatAsRustCode for StringSet {
190 fn format_as_rust_code(&self, _tabs: usize) -> String {
191 format!("StringSet {{ inner: String::from({:?}) }}", self.inner)
192 }
193}
194
195#[cfg(feature = "parser")]
198pub mod parser {
199 #[allow(clippy::wildcard_imports)]
200 use super::*;
202
203 pub fn parse_content(input: &str) -> Result<Content, ()> {
208 Ok(Content {
209 inner: input.trim().into(),
210 })
211 }
212
213 fn parse_counter_name_value(input: &str, default_value: i32) -> Result<(AzString, i32), ()> {
214 let trimmed = input.trim();
215
216 if trimmed == "none" {
217 return Ok((AzString::from_const_str("none"), 0));
218 }
219
220 let parts: Vec<&str> = trimmed.split_whitespace().collect();
221
222 if parts.is_empty() {
223 return Err(());
224 }
225
226 let counter_name = parts[0].into();
227 let value = if parts.len() > 1 {
228 parts[1].parse::<i32>().map_err(|_| ())?
229 } else {
230 default_value
231 };
232
233 Ok((counter_name, value))
234 }
235
236 pub fn parse_counter_reset(input: &str) -> Result<CounterReset, ()> {
240 let (counter_name, value) = parse_counter_name_value(input, 0)?;
241 Ok(CounterReset::new(counter_name, value))
242 }
243
244 pub fn parse_counter_increment(input: &str) -> Result<CounterIncrement, ()> {
248 let (counter_name, value) = parse_counter_name_value(input, 1)?;
249 Ok(CounterIncrement::new(counter_name, value))
250 }
251
252 pub fn parse_string_set(input: &str) -> Result<StringSet, ()> {
256 Ok(StringSet {
257 inner: input.trim().into(),
258 })
259 }
260}
261
262#[cfg(feature = "parser")]
263pub use parser::*;
264
265#[cfg(all(test, feature = "parser"))]
266mod tests {
267 use super::*;
268
269 #[test]
270 fn test_simple_content_parser() {
271 assert_eq!(parse_content("'Hello'").unwrap().inner.as_str(), "'Hello'");
272
273 let reset = parse_counter_reset("page 1").unwrap();
275 assert_eq!(reset.counter_name.as_str(), "page");
276 assert_eq!(reset.value, 1);
277
278 let reset = parse_counter_reset("list-item 0").unwrap();
279 assert_eq!(reset.counter_name.as_str(), "list-item");
280 assert_eq!(reset.value, 0);
281
282 let reset = parse_counter_reset("none").unwrap();
283 assert_eq!(reset.counter_name.as_str(), "none");
284
285 let inc = parse_counter_increment("section").unwrap();
287 assert_eq!(inc.counter_name.as_str(), "section");
288 assert_eq!(inc.value, 1); let inc = parse_counter_increment("list-item 2").unwrap();
291 assert_eq!(inc.counter_name.as_str(), "list-item");
292 assert_eq!(inc.value, 2);
293
294 assert_eq!(
295 parse_string_set("chapter-title content()")
296 .unwrap()
297 .inner
298 .as_str(),
299 "chapter-title content()"
300 );
301 }
302}
303
304#[cfg(all(test, feature = "parser"))]
305mod autotest_generated {
306 use std::{
307 collections::hash_map::DefaultHasher,
308 hash::{Hash, Hasher},
309 };
310
311 use super::*;
312 use crate::codegen::format::FormatAsRustCode;
313
314 fn hash_of<T: Hash>(t: &T) -> u64 {
315 let mut h = DefaultHasher::new();
316 t.hash(&mut h);
317 h.finish()
318 }
319
320 fn hostile_corpus() -> Vec<String> {
322 alloc::vec![
323 String::new(),
324 " ".to_string(),
325 "\t\n\r\u{b}\u{c}".to_string(),
326 "\u{a0}".to_string(), "\u{200b}".to_string(), "\0".to_string(), "\0page\0 1\0".to_string(),
330 ";".to_string(),
331 "}{".to_string(),
332 "counter(section) \". \"".to_string(),
333 "-".to_string(),
334 "--".to_string(),
335 "+".to_string(),
336 "e".to_string(),
337 "NaN".to_string(),
338 "inf".to_string(),
339 "-inf".to_string(),
340 "\u{1F600}".to_string(),
341 "a\u{0301}\u{0301}\u{0301}".to_string(), "\u{202e}reversed".to_string(), "page 1 page 2 page 3".to_string(),
344 "page \u{fffd}".to_string(),
345 "\"unterminated".to_string(),
346 "url(".to_string(),
347 ]
348 }
349
350 #[test]
356 fn counter_constructors_preserve_fields_at_i32_extremes() {
357 for value in [i32::MIN, i32::MIN + 1, -1, 0, 1, i32::MAX - 1, i32::MAX] {
358 let r = CounterReset::new(AzString::from_const_str("c"), value);
359 assert_eq!(r.counter_name.as_str(), "c");
360 assert_eq!(r.value, value);
361
362 let i = CounterIncrement::new(AzString::from_const_str("c"), value);
363 assert_eq!(i.counter_name.as_str(), "c");
364 assert_eq!(i.value, value);
365 }
366 }
367
368 #[test]
369 fn counter_constructors_accept_degenerate_names_verbatim() {
370 let huge: String = "x".repeat(100_000);
373 let names = ["", " ", "a b", "\u{1F600}\u{1F600}", "none", huge.as_str()];
374
375 for name in names {
376 let r = CounterReset::new(name.into(), i32::MIN);
377 assert_eq!(r.counter_name.as_str(), name);
378 assert_eq!(r.counter_name.as_str().len(), name.len());
379 assert_eq!(r.value, i32::MIN);
380
381 let i = CounterIncrement::new(name.into(), i32::MAX);
382 assert_eq!(i.counter_name.as_str(), name);
383 assert_eq!(i.counter_name.as_str().len(), name.len());
384 assert_eq!(i.value, i32::MAX);
385 }
386 }
387
388 #[test]
389 fn counter_none_and_list_item_constants() {
390 assert_eq!(CounterReset::none().counter_name.as_str(), "none");
391 assert_eq!(CounterReset::none().value, 0);
392 assert_eq!(CounterReset::default(), CounterReset::none());
393
394 assert_eq!(CounterIncrement::none().counter_name.as_str(), "none");
395 assert_eq!(CounterIncrement::none().value, 0);
396 assert_eq!(CounterIncrement::default(), CounterIncrement::none());
397
398 assert_eq!(CounterReset::list_item().counter_name.as_str(), "list-item");
401 assert_eq!(CounterReset::list_item().value, 0);
402 assert_eq!(
403 CounterIncrement::list_item().counter_name.as_str(),
404 "list-item"
405 );
406 assert_eq!(CounterIncrement::list_item().value, 1);
407 }
408
409 #[test]
410 fn content_and_string_set_defaults() {
411 assert_eq!(Content::default().inner.as_str(), "normal");
412 assert_eq!(StringSet::default().inner.as_str(), "none");
413 }
414
415 #[test]
420 fn content_and_string_set_are_pure_trim_and_never_error() {
421 for input in hostile_corpus() {
426 let c = parse_content(&input).expect("parse_content never errors");
427 assert_eq!(c.inner.as_str(), input.trim());
428
429 let s = parse_string_set(&input).expect("parse_string_set never errors");
430 assert_eq!(s.inner.as_str(), input.trim());
431 }
432
433 assert_eq!(parse_content("").unwrap().inner.as_str(), "");
434 assert_eq!(parse_content(" \t\n ").unwrap().inner.as_str(), "");
435 assert_eq!(parse_string_set("").unwrap().inner.as_str(), "");
436 assert_eq!(parse_content("\u{a0}x\u{a0}").unwrap().inner.as_str(), "x");
438 assert_eq!(
440 parse_content(" \u{200b} ").unwrap().inner.as_str(),
441 "\u{200b}"
442 );
443 }
444
445 #[test]
446 fn content_positive_control_and_inner_junk_is_kept() {
447 assert_eq!(parse_content("'Hi'").unwrap().inner.as_str(), "'Hi'");
448 assert_eq!(parse_content(" 'Hi' ").unwrap().inner.as_str(), "'Hi'");
449 assert_eq!(
451 parse_content("'Hi';garbage").unwrap().inner.as_str(),
452 "'Hi';garbage"
453 );
454 assert_eq!(
455 parse_string_set("chapter content()")
456 .unwrap()
457 .inner
458 .as_str(),
459 "chapter content()"
460 );
461 }
462
463 #[test]
464 fn content_extremely_long_input_does_not_hang() {
465 let huge = "a".repeat(1_000_000);
466 let padded = alloc::format!(" {huge}\n");
467
468 let c = parse_content(&padded).unwrap();
469 assert_eq!(c.inner.as_str().len(), 1_000_000);
470 assert!(c.inner.as_str().starts_with("aa"));
471
472 let s = parse_string_set(&padded).unwrap();
473 assert_eq!(s.inner.as_str().len(), 1_000_000);
474 }
475
476 #[test]
477 fn content_deeply_nested_input_does_not_stack_overflow() {
478 let nested = alloc::format!("{}{}", "(".repeat(10_000), ")".repeat(10_000));
480 let c = parse_content(&nested).unwrap();
481 assert_eq!(c.inner.as_str().len(), 20_000);
482
483 let deep = alloc::format!("{}x{}", "counter(".repeat(5_000), ")".repeat(5_000));
484 assert!(parse_content(&deep).is_ok());
485 assert!(parse_string_set(&deep).is_ok());
486 }
487
488 #[test]
489 fn content_preserves_multibyte_unicode_byte_for_byte() {
490 let input = " \u{1F600}a\u{0301}\u{4e2d}\u{6587} ";
491 let c = parse_content(input).unwrap();
492 assert_eq!(c.inner.as_str(), input.trim());
493 assert_eq!(c.inner.as_str().len(), input.trim().len());
494 assert_eq!(c.inner.as_str().chars().count(), 5);
498 }
499
500 #[test]
501 fn content_boundary_number_strings_are_accepted_as_plain_text() {
502 for n in [
504 "0",
505 "-0",
506 "9223372036854775807",
507 "-9223372036854775808",
508 "1e309",
509 "NaN",
510 "inf",
511 ] {
512 assert_eq!(parse_content(n).unwrap().inner.as_str(), n);
513 assert_eq!(parse_string_set(n).unwrap().inner.as_str(), n);
514 }
515 }
516
517 #[test]
518 fn content_print_parse_roundtrips_for_parsed_values() {
519 for input in hostile_corpus() {
520 let c = parse_content(&input).unwrap();
521 let reparsed = parse_content(&c.print_as_css_value()).unwrap();
522 assert_eq!(reparsed, c, "content round-trip failed for {input:?}");
523
524 let s = parse_string_set(&input).unwrap();
525 let re_s = parse_string_set(&s.print_as_css_value()).unwrap();
526 assert_eq!(re_s, s, "string-set round-trip failed for {input:?}");
527 }
528
529 let d = Content::default();
530 assert_eq!(parse_content(&d.print_as_css_value()).unwrap(), d);
531 let d = StringSet::default();
532 assert_eq!(parse_string_set(&d.print_as_css_value()).unwrap(), d);
533 }
534
535 #[test]
536 fn content_roundtrip_is_lossy_for_untrimmed_handbuilt_values() {
537 let padded = Content {
540 inner: " x ".into(),
541 };
542 assert_eq!(padded.print_as_css_value(), " x ");
543 assert_ne!(parse_content(&padded.print_as_css_value()).unwrap(), padded);
544 assert_eq!(
545 parse_content(&padded.print_as_css_value())
546 .unwrap()
547 .inner
548 .as_str(),
549 "x"
550 );
551 }
552
553 #[test]
560 fn counter_empty_and_whitespace_only_input_is_rejected() {
561 for input in ["", " ", " ", "\t\n", "\r\n\t ", "\u{a0}", "\u{2003}"] {
562 assert!(
563 parse_counter_reset(input).is_err(),
564 "counter-reset accepted whitespace-only {input:?}"
565 );
566 assert!(
567 parse_counter_increment(input).is_err(),
568 "counter-increment accepted whitespace-only {input:?}"
569 );
570 }
571 }
572
573 #[test]
574 fn counter_missing_value_uses_per_property_default() {
575 let r = parse_counter_reset("section").unwrap();
576 assert_eq!(r.counter_name.as_str(), "section");
577 assert_eq!(r.value, 0);
578
579 let i = parse_counter_increment("section").unwrap();
580 assert_eq!(i.counter_name.as_str(), "section");
581 assert_eq!(i.value, 1);
582 }
583
584 #[test]
585 fn counter_none_keyword_is_case_sensitive() {
586 let r = parse_counter_reset("NONE").unwrap();
589 assert_eq!(r.counter_name.as_str(), "NONE");
590 assert_eq!(r.value, 0);
591
592 let i = parse_counter_increment("None").unwrap();
593 assert_eq!(i.counter_name.as_str(), "None");
594 assert_eq!(i.value, 1, "uppercase `None` took the counter-name branch");
595
596 assert_eq!(parse_counter_increment("none").unwrap().value, 0);
598 assert_eq!(parse_counter_increment(" none ").unwrap().value, 0);
599 }
600
601 #[test]
602 fn counter_none_with_value_keeps_value_but_prints_as_bare_none() {
603 let r = parse_counter_reset("none 5").unwrap();
607 assert_eq!(r.counter_name.as_str(), "none");
608 assert_eq!(r.value, 5);
609 assert_eq!(r.print_as_css_value(), "none");
610
611 let reparsed = parse_counter_reset(&r.print_as_css_value()).unwrap();
612 assert_eq!(reparsed.value, 0);
613 assert_ne!(reparsed, r, "value 5 silently vanished across a round-trip");
614 }
615
616 #[test]
617 fn counter_value_at_i32_boundaries_parses_exactly() {
618 assert_eq!(parse_counter_reset("c 2147483647").unwrap().value, i32::MAX);
619 assert_eq!(
620 parse_counter_reset("c -2147483648").unwrap().value,
621 i32::MIN
622 );
623 assert_eq!(parse_counter_reset("c 0").unwrap().value, 0);
624 assert_eq!(parse_counter_reset("c -0").unwrap().value, 0);
625 assert_eq!(parse_counter_reset("c +7").unwrap().value, 7);
626 assert_eq!(parse_counter_reset("c 007").unwrap().value, 7);
627 assert_eq!(
628 parse_counter_increment("c -2147483648").unwrap().value,
629 i32::MIN
630 );
631 }
632
633 #[test]
634 fn counter_value_overflowing_i32_is_rejected_not_wrapped() {
635 for over in [
636 "c 2147483648", "c -2147483649", "c 9223372036854775807", "c -9223372036854775808", "c 340282366920938463463374607431768211456",
641 ] {
642 assert!(
643 parse_counter_reset(over).is_err(),
644 "overflowing value silently accepted: {over:?}"
645 );
646 assert!(parse_counter_increment(over).is_err());
647 }
648 }
649
650 #[test]
651 fn counter_non_integer_values_are_rejected() {
652 for bad in [
653 "c 1.0",
654 "c 1.5",
655 "c 1e3",
656 "c NaN",
657 "c nan",
658 "c inf",
659 "c -inf",
660 "c 0x10",
661 "c 1_000",
662 "c 1,",
663 "c one",
664 "c -",
665 "c +",
666 "c ٣", "c 1", "c 1\u{200b}",
669 ] {
670 assert!(
671 parse_counter_reset(bad).is_err(),
672 "counter-reset accepted non-integer {bad:?}"
673 );
674 assert!(
675 parse_counter_increment(bad).is_err(),
676 "counter-increment accepted non-integer {bad:?}"
677 );
678 }
679 }
680
681 #[test]
682 fn counter_extra_tokens_after_the_first_pair_are_silently_dropped() {
683 let r = parse_counter_reset("a 1 b 2 c 3").unwrap();
686 assert_eq!(r.counter_name.as_str(), "a");
687 assert_eq!(r.value, 1);
688
689 parse_counter_increment("a b").unwrap_err();
692 }
693
694 #[test]
695 fn counter_arbitrary_whitespace_forms_are_normalized() {
696 for input in [
697 "\t page \n 42 \r",
698 "page\u{a0}42", "page 42", "\u{2003}page 42", ] {
702 let r = parse_counter_reset(input).unwrap();
703 assert_eq!(r.counter_name.as_str(), "page", "for {input:?}");
704 assert_eq!(r.value, 42, "for {input:?}");
705 }
706 }
707
708 #[test]
709 fn counter_unicode_names_are_preserved() {
710 let r = parse_counter_reset("\u{7ae0}\u{8282} 3").unwrap();
711 assert_eq!(r.counter_name.as_str(), "\u{7ae0}\u{8282}");
712 assert_eq!(r.value, 3);
713
714 let i = parse_counter_increment("\u{1F600}").unwrap();
715 assert_eq!(i.counter_name.as_str(), "\u{1F600}");
716 assert_eq!(i.value, 1);
717
718 let z = parse_counter_reset("\u{200b}").unwrap();
720 assert_eq!(z.counter_name.as_str(), "\u{200b}");
721 assert_eq!(z.value, 0);
722 }
723
724 #[test]
725 fn counter_hostile_corpus_never_panics() {
726 for input in hostile_corpus() {
727 let _ = parse_counter_reset(&input);
728 let _ = parse_counter_increment(&input);
729 }
730 }
731
732 #[test]
733 fn counter_extremely_long_input_does_not_hang() {
734 let long_name = "n".repeat(1_000_000);
735 let r = parse_counter_reset(&long_name).unwrap();
736 assert_eq!(r.counter_name.as_str().len(), 1_000_000);
737 assert_eq!(r.value, 0);
738
739 let with_value = alloc::format!("{long_name} 5");
740 assert_eq!(parse_counter_increment(&with_value).unwrap().value, 5);
741
742 let long_number = alloc::format!("c {}", "9".repeat(1_000_000));
744 assert!(parse_counter_reset(&long_number).is_err());
745
746 assert!(parse_counter_reset(&" ".repeat(1_000_000)).is_err());
748 }
749
750 #[test]
751 fn counter_deeply_nested_input_does_not_stack_overflow() {
752 let nested = alloc::format!("{}{}", "(".repeat(10_000), ")".repeat(10_000));
753 let r = parse_counter_reset(&nested).unwrap();
754 assert_eq!(r.counter_name.as_str().len(), 20_000);
755 assert_eq!(r.value, 0);
756
757 let nested_with_value = alloc::format!("{nested} 1");
758 assert_eq!(
759 parse_counter_increment(&nested_with_value).unwrap().value,
760 1
761 );
762 }
763
764 #[test]
769 fn counter_print_parse_roundtrips_for_well_formed_values() {
770 for (name, value) in [
771 ("page", 0),
772 ("page", 1),
773 ("section", -1),
774 ("list-item", i32::MAX),
775 ("list-item", i32::MIN),
776 ("\u{7ae0}", 7),
777 ] {
778 let r = CounterReset::new(name.into(), value);
779 assert_eq!(parse_counter_reset(&r.print_as_css_value()).unwrap(), r);
780
781 let i = CounterIncrement::new(name.into(), value);
782 assert_eq!(parse_counter_increment(&i.print_as_css_value()).unwrap(), i);
783 }
784
785 let n = CounterReset::none();
787 assert_eq!(n.print_as_css_value(), "none");
788 assert_eq!(parse_counter_reset(&n.print_as_css_value()).unwrap(), n);
789
790 let li = CounterIncrement::list_item();
791 assert_eq!(li.print_as_css_value(), "list-item 1");
792 assert_eq!(
793 parse_counter_increment(&li.print_as_css_value()).unwrap(),
794 li
795 );
796
797 assert_eq!(
798 CounterReset::list_item().print_as_css_value(),
799 "list-item 0"
800 );
801 assert_eq!(CounterIncrement::none().print_as_css_value(), "none");
802 }
803
804 #[test]
805 fn counter_print_of_empty_name_reparses_into_a_different_counter() {
806 let r = CounterReset::new(AzString::from_const_str(""), 5);
809 assert_eq!(r.print_as_css_value(), " 5");
810
811 let reparsed = parse_counter_reset(&r.print_as_css_value()).unwrap();
812 assert_eq!(reparsed.counter_name.as_str(), "5");
813 assert_eq!(reparsed.value, 0);
814 assert_ne!(reparsed, r);
815 }
816
817 #[test]
818 fn counter_print_of_name_containing_space_fails_to_reparse() {
819 let r = CounterReset::new("a b".into(), 5);
822 assert_eq!(r.print_as_css_value(), "a b 5");
823 assert!(parse_counter_reset(&r.print_as_css_value()).is_err());
824
825 let i = CounterIncrement::new("a b".into(), 5);
826 assert!(parse_counter_increment(&i.print_as_css_value()).is_err());
827 }
828
829 #[test]
834 fn counter_ord_is_name_then_value_and_hash_agrees_with_eq() {
835 let a1 = CounterReset::new("a".into(), 1);
836 let a2 = CounterReset::new("a".into(), 2);
837 let b_min = CounterReset::new("b".into(), i32::MIN);
838
839 assert!(a1 < a2, "equal names must order by value");
840 assert!(a2 < b_min, "name must dominate value in the ordering");
841 assert!(CounterReset::new("a".into(), i32::MAX) < b_min);
842
843 let owned = CounterReset::new(String::from("a").into(), 1);
845 assert_eq!(owned, a1);
846 assert_eq!(hash_of(&owned), hash_of(&a1));
847 assert_ne!(a1, a2);
848
849 assert_eq!(
850 hash_of(&Content { inner: "x".into() }),
851 hash_of(&parse_content(" x ").unwrap())
852 );
853 }
854
855 #[test]
856 fn format_as_rust_code_escapes_quotes_and_control_chars() {
857 let c = Content {
858 inner: "a\"b\\c\nd".into(),
859 };
860 assert_eq!(
861 c.format_as_rust_code(0),
862 r#"Content { inner: String::from("a\"b\\c\nd") }"#
863 );
864
865 let s = StringSet {
866 inner: "\"q\"".into(),
867 };
868 assert_eq!(
869 s.format_as_rust_code(0),
870 r#"StringSet { inner: String::from("\"q\"") }"#
871 );
872
873 let r = CounterReset::new("a\"b".into(), i32::MIN);
874 assert_eq!(
875 r.format_as_rust_code(0),
876 r#"CounterReset { counter_name: AzString::from_const_str("a\"b"), value: -2147483648 }"#
877 );
878
879 let i = CounterIncrement::new("".into(), i32::MAX);
880 assert_eq!(
881 i.format_as_rust_code(0),
882 r#"CounterIncrement { counter_name: AzString::from_const_str(""), value: 2147483647 }"#
883 );
884 }
885
886 #[test]
887 fn format_as_rust_code_ignores_the_tab_argument() {
888 let c = Content::default();
889 assert_eq!(c.format_as_rust_code(0), c.format_as_rust_code(usize::MAX));
890
891 let r = CounterReset::list_item();
892 assert_eq!(r.format_as_rust_code(0), r.format_as_rust_code(usize::MAX));
893 }
894}