1use alloc::string::{String, ToString};
7use core::fmt;
8use crate::corety::AzString;
9
10use crate::{codegen::format::FormatAsRustCode, props::formatter::PrintAsCssValue};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
14#[repr(C)]
15#[derive(Default)]
16pub enum LayoutTextJustify {
17 #[default]
18 Auto,
19 None,
20 InterWord,
21 InterCharacter,
22 Distribute,
25}
26
27
28impl PrintAsCssValue for LayoutTextJustify {
29 fn print_as_css_value(&self) -> String {
30 match self {
31 Self::Auto => "auto",
32 Self::None => "none",
33 Self::InterWord => "inter-word",
34 Self::InterCharacter => "inter-character",
35 Self::Distribute => "distribute",
36 }
37 .to_string()
38 }
39}
40
41impl FormatAsRustCode for LayoutTextJustify {
42 fn format_as_rust_code(&self, _tabs: usize) -> String {
43 format!("LayoutTextJustify::{self:?}")
44 }
45}
46
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub enum TextJustifyParseError<'a> {
49 InvalidValue(&'a str),
50}
51
52impl fmt::Display for TextJustifyParseError<'_> {
53 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54 match self {
55 TextJustifyParseError::InvalidValue(s) => {
56 write!(f, "Invalid text-justify value: '{s}'.")
57 }
58 }
59 }
60}
61
62#[derive(Debug, Clone, PartialEq, Eq)]
63#[repr(C, u8)]
64pub enum TextJustifyParseErrorOwned {
65 InvalidValue(AzString),
66}
67
68impl TextJustifyParseError<'_> {
69 #[must_use] pub fn to_owned(&self) -> TextJustifyParseErrorOwned {
70 match self {
71 TextJustifyParseError::InvalidValue(s) => {
72 TextJustifyParseErrorOwned::InvalidValue((*s).to_string().into())
73 }
74 }
75 }
76}
77
78impl TextJustifyParseErrorOwned {
79 #[must_use] pub fn to_borrowed(&self) -> TextJustifyParseError<'_> {
80 match self {
81 Self::InvalidValue(s) => {
82 TextJustifyParseError::InvalidValue(s.as_str())
83 }
84 }
85 }
86}
87
88pub fn parse_layout_text_justify(
93 input: &str,
94) -> Result<LayoutTextJustify, TextJustifyParseError<'_>> {
95 match input.trim() {
96 "auto" => Ok(LayoutTextJustify::Auto),
97 "none" => Ok(LayoutTextJustify::None),
98 "inter-word" => Ok(LayoutTextJustify::InterWord),
99 "inter-character" | "distribute" => Ok(LayoutTextJustify::InterCharacter),
102 other => Err(TextJustifyParseError::InvalidValue(other)),
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109 #[test]
110 fn test_parse_layout_text_justify() {
111 assert_eq!(
112 parse_layout_text_justify("auto"),
113 Ok(LayoutTextJustify::Auto)
114 );
115 assert_eq!(
116 parse_layout_text_justify("none"),
117 Ok(LayoutTextJustify::None)
118 );
119 assert_eq!(
120 parse_layout_text_justify("inter-word"),
121 Ok(LayoutTextJustify::InterWord)
122 );
123 assert_eq!(
124 parse_layout_text_justify("inter-character"),
125 Ok(LayoutTextJustify::InterCharacter)
126 );
127 assert_eq!(
128 parse_layout_text_justify("distribute"),
129 Ok(LayoutTextJustify::InterCharacter)
130 );
131 assert!(parse_layout_text_justify("invalid").is_err());
132 }
133}
134
135#[cfg(test)]
136mod autotest_generated {
137 use super::*;
138
139 const ALL_JUSTIFY: [LayoutTextJustify; 5] = [
147 LayoutTextJustify::Auto,
148 LayoutTextJustify::None,
149 LayoutTextJustify::InterWord,
150 LayoutTextJustify::InterCharacter,
151 LayoutTextJustify::Distribute,
152 ];
153
154 const fn justify_variant_index(j: LayoutTextJustify) -> usize {
155 match j {
156 LayoutTextJustify::Auto => 0,
157 LayoutTextJustify::None => 1,
158 LayoutTextJustify::InterWord => 2,
159 LayoutTextJustify::InterCharacter => 3,
160 LayoutTextJustify::Distribute => 4,
161 }
162 }
163
164 #[test]
165 fn all_justify_lists_every_variant_exactly_once() {
166 for (i, j) in ALL_JUSTIFY.iter().enumerate() {
167 assert_eq!(
168 justify_variant_index(*j),
169 i,
170 "ALL_JUSTIFY is out of sync at index {i} ({j:?})"
171 );
172 }
173 }
174
175 #[test]
180 fn every_documented_keyword_parses_to_its_variant() {
181 assert_eq!(
182 parse_layout_text_justify("auto"),
183 Ok(LayoutTextJustify::Auto)
184 );
185 assert_eq!(
186 parse_layout_text_justify("none"),
187 Ok(LayoutTextJustify::None)
188 );
189 assert_eq!(
190 parse_layout_text_justify("inter-word"),
191 Ok(LayoutTextJustify::InterWord)
192 );
193 assert_eq!(
194 parse_layout_text_justify("inter-character"),
195 Ok(LayoutTextJustify::InterCharacter)
196 );
197 assert_eq!(
199 parse_layout_text_justify("distribute"),
200 Ok(LayoutTextJustify::InterCharacter)
201 );
202 }
203
204 #[test]
208 fn no_input_ever_parses_to_the_legacy_distribute_variant() {
209 let candidates = [
210 "distribute",
211 " distribute ",
212 "inter-character",
213 "auto",
214 "none",
215 "inter-word",
216 ];
217 for input in candidates {
218 assert_ne!(
219 parse_layout_text_justify(input),
220 Ok(LayoutTextJustify::Distribute),
221 "{input:?} parsed to the FFI-only Distribute variant"
222 );
223 }
224 }
225
226 #[test]
231 fn empty_and_whitespace_only_input_is_rejected_with_an_empty_payload() {
232 for input in ["", " ", " ", "\t", "\n", "\r\n", " \t\r\n\u{b}\u{c} "] {
235 assert_eq!(
236 parse_layout_text_justify(input),
237 Err(TextJustifyParseError::InvalidValue("")),
238 "whitespace-only input {input:?}"
239 );
240 }
241 }
242
243 #[test]
244 fn surrounding_ascii_whitespace_is_trimmed_before_matching() {
245 for (input, expected) in [
246 (" auto ", LayoutTextJustify::Auto),
247 ("\tnone\t", LayoutTextJustify::None),
248 ("\n inter-word \r\n", LayoutTextJustify::InterWord),
249 ("\r\n\tdistribute\r\n\t", LayoutTextJustify::InterCharacter),
250 ] {
251 assert_eq!(parse_layout_text_justify(input), Ok(expected), "{input:?}");
252 }
253 }
254
255 #[test]
260 fn unicode_whitespace_padding_is_also_trimmed() {
261 assert_eq!(
262 parse_layout_text_justify("\u{a0}auto\u{a0}"),
263 Ok(LayoutTextJustify::Auto),
264 "NBSP-padded keyword"
265 );
266 assert_eq!(
267 parse_layout_text_justify("\u{3000}none\u{3000}"),
268 Ok(LayoutTextJustify::None),
269 "ideographic-space-padded keyword"
270 );
271 assert_eq!(
272 parse_layout_text_justify("\u{85}inter-word\u{85}"),
273 Ok(LayoutTextJustify::InterWord),
274 "NEL-padded keyword"
275 );
276 assert_eq!(
279 parse_layout_text_justify("\u{200b}auto"),
280 Err(TextJustifyParseError::InvalidValue("\u{200b}auto")),
281 "zero-width space is not whitespace"
282 );
283 assert_eq!(
284 parse_layout_text_justify("\u{feff}auto"),
285 Err(TextJustifyParseError::InvalidValue("\u{feff}auto")),
286 "BOM is not whitespace"
287 );
288 }
289
290 #[test]
291 fn interior_whitespace_is_never_collapsed() {
292 for input in [
293 "inter word",
294 "inter -word",
295 "inter- word",
296 "inter - character",
297 "auto auto",
298 "auto none",
299 "au to",
300 ] {
301 assert_eq!(
302 parse_layout_text_justify(input),
303 Err(TextJustifyParseError::InvalidValue(input.trim())),
304 "interior whitespace in {input:?} must not be collapsed away"
305 );
306 }
307 }
308
309 #[test]
318 fn keyword_matching_is_case_sensitive() {
319 for input in [
320 "AUTO",
321 "Auto",
322 "aUtO",
323 "NONE",
324 "Inter-Word",
325 "INTER-CHARACTER",
326 "Distribute",
327 ] {
328 assert!(
329 parse_layout_text_justify(input).is_err(),
330 "{input:?} unexpectedly parsed"
331 );
332 }
333 }
334
335 #[test]
336 fn garbage_and_near_miss_input_is_rejected_without_panicking() {
337 for input in [
338 "invalid",
339 "interword",
340 "inter_word",
341 "inter–word", "inter-",
343 "-character",
344 "inter-characters",
345 "autos",
346 "aut",
347 "auto;",
348 "auto;garbage",
349 "auto !important",
350 "auto/**/",
351 "/*auto*/",
352 "url(auto)",
353 "attr(auto)",
354 "\"auto\"",
355 "'auto'",
356 ";",
357 "{}",
358 "\\",
359 "-",
360 "--",
361 "\0",
362 "auto\0",
363 "\0auto",
364 "initial",
365 "inherit",
366 "unset",
367 "revert",
368 ] {
369 assert_eq!(
370 parse_layout_text_justify(input),
371 Err(TextJustifyParseError::InvalidValue(input.trim())),
372 "{input:?} must be rejected verbatim"
373 );
374 }
375 }
376
377 #[test]
378 fn boundary_numeric_strings_are_rejected() {
379 let big = i64::MAX.to_string();
380 let small = i64::MIN.to_string();
381 let umax = u64::MAX.to_string();
382 let fmax = f64::MAX.to_string();
383 let ftiny = f64::MIN_POSITIVE.to_string();
384 let inputs = [
385 "0",
386 "-0",
387 "+0",
388 "0.0",
389 "1",
390 "1e400",
391 "-1e-400",
392 "NaN",
393 "nan",
394 "inf",
395 "-inf",
396 "Infinity",
397 big.as_str(),
398 small.as_str(),
399 umax.as_str(),
400 fmax.as_str(),
401 ftiny.as_str(),
402 ];
403 for input in inputs {
404 assert_eq!(
405 parse_layout_text_justify(input),
406 Err(TextJustifyParseError::InvalidValue(input)),
407 "numeric-looking input {input:?} must not parse as a keyword"
408 );
409 }
410 }
411
412 #[test]
413 fn unicode_input_is_rejected_and_preserved_verbatim_in_the_error() {
414 for input in [
415 "\u{1F600}", "auto\u{301}", "\u{301}\u{301}\u{301}", "auto", "аuto", "\u{202e}auto", "🙂🙃🙂",
422 "日本語",
423 "\u{fffd}", ] {
425 assert_eq!(
426 parse_layout_text_justify(input),
427 Err(TextJustifyParseError::InvalidValue(input)),
428 "unicode input {input:?}"
429 );
430 let err = parse_layout_text_justify(input).unwrap_err();
433 assert_eq!(err.to_owned().to_borrowed(), err);
434 }
435 }
436
437 #[test]
438 fn extremely_long_input_neither_panics_nor_hangs() {
439 let million = "a".repeat(1_000_000);
440 assert_eq!(
441 parse_layout_text_justify(&million),
442 Err(TextJustifyParseError::InvalidValue(million.as_str()))
443 );
444
445 let repeated = "auto".repeat(250_000);
447 assert!(parse_layout_text_justify(&repeated).is_err());
448
449 let padded = format!("{}auto{}", " ".repeat(500_000), "\n".repeat(500_000));
451 assert_eq!(
452 parse_layout_text_justify(&padded),
453 Ok(LayoutTextJustify::Auto)
454 );
455
456 let padded_garbage = format!("{}garbage{}", " ".repeat(500_000), " ".repeat(500_000));
459 let err = parse_layout_text_justify(&padded_garbage).unwrap_err();
460 assert_eq!(err, TextJustifyParseError::InvalidValue("garbage"));
461 }
462
463 #[test]
464 fn deeply_nested_brackets_do_not_stack_overflow() {
465 let nested = format!("{}auto{}", "(".repeat(10_000), ")".repeat(10_000));
468 assert!(parse_layout_text_justify(&nested).is_err());
469
470 let braces = "{".repeat(50_000);
471 assert_eq!(
472 parse_layout_text_justify(&braces),
473 Err(TextJustifyParseError::InvalidValue(braces.as_str()))
474 );
475 }
476
477 #[test]
480 fn error_payload_is_a_borrowed_subslice_of_the_input() {
481 let input = " bogus-value ";
482 let Err(TextJustifyParseError::InvalidValue(slice)) = parse_layout_text_justify(input)
483 else {
484 panic!("expected an error for {input:?}");
485 };
486 assert_eq!(slice, "bogus-value");
487 assert_eq!(slice, input.trim());
488
489 let base = input.as_ptr() as usize;
490 let borrowed = slice.as_ptr() as usize;
491 assert!(
492 borrowed >= base && borrowed + slice.len() <= base + input.len(),
493 "error payload does not point into the input buffer"
494 );
495 }
496
497 #[test]
502 fn every_printed_value_parses_back_to_the_same_variant() {
503 for j in ALL_JUSTIFY {
504 let printed = j.print_as_css_value();
505 let reparsed = parse_layout_text_justify(&printed);
506 let expected = if j == LayoutTextJustify::Distribute {
507 LayoutTextJustify::InterCharacter
511 } else {
512 j
513 };
514 assert_eq!(reparsed, Ok(expected), "round trip of {j:?} via {printed:?}");
515 }
516 }
517
518 #[test]
519 fn distribute_round_trip_is_lossy_by_design() {
520 let printed = LayoutTextJustify::Distribute.print_as_css_value();
521 assert_eq!(printed, "distribute");
522 assert_eq!(
523 parse_layout_text_justify(&printed),
524 Ok(LayoutTextJustify::InterCharacter)
525 );
526 assert_ne!(
527 parse_layout_text_justify(&printed),
528 Ok(LayoutTextJustify::Distribute)
529 );
530 }
531
532 #[test]
533 fn printed_values_are_distinct_well_formed_css_idents() {
534 let mut seen: Vec<String> = Vec::new();
535 for j in ALL_JUSTIFY {
536 let printed = j.print_as_css_value();
537 assert!(!printed.is_empty(), "{j:?} printed an empty value");
538 assert_eq!(printed.trim(), printed, "{j:?} printed padded value");
539 assert!(
540 !printed.contains(char::is_whitespace),
541 "{j:?} printed interior whitespace: {printed:?}"
542 );
543 assert!(
544 printed
545 .chars()
546 .all(|c| c.is_ascii_lowercase() || c == '-'),
547 "{j:?} printed a non-ident value: {printed:?}"
548 );
549 assert!(
550 !seen.contains(&printed),
551 "{j:?} printed a value already used by another variant: {printed:?}"
552 );
553 seen.push(printed);
554 }
555 assert_eq!(seen.len(), ALL_JUSTIFY.len());
556 }
557
558 #[test]
563 fn format_as_rust_code_is_a_variant_path_and_ignores_indentation() {
564 for j in ALL_JUSTIFY {
565 let code = j.format_as_rust_code(0);
566 assert_eq!(code, format!("LayoutTextJustify::{j:?}"));
567 assert!(code.starts_with("LayoutTextJustify::"));
568 assert!(!code.contains(char::is_whitespace), "{code:?}");
569 assert_eq!(j.format_as_rust_code(usize::MAX), code);
572 assert_eq!(j.format_as_rust_code(usize::MIN), code);
573 }
574 }
575
576 #[test]
581 fn default_is_auto() {
582 assert_eq!(LayoutTextJustify::default(), LayoutTextJustify::Auto);
583 assert_eq!(
584 parse_layout_text_justify(&LayoutTextJustify::default().print_as_css_value()),
585 Ok(LayoutTextJustify::default())
586 );
587 }
588
589 #[test]
594 fn repr_c_discriminants_are_ffi_stable() {
595 assert_eq!(LayoutTextJustify::Auto as u8, 0);
596 assert_eq!(LayoutTextJustify::None as u8, 1);
597 assert_eq!(LayoutTextJustify::InterWord as u8, 2);
598 assert_eq!(LayoutTextJustify::InterCharacter as u8, 3);
599 assert_eq!(LayoutTextJustify::Distribute as u8, 4);
600 }
601
602 #[test]
603 fn derived_ord_follows_declaration_order() {
604 for (i, a) in ALL_JUSTIFY.iter().enumerate() {
605 for (k, b) in ALL_JUSTIFY.iter().enumerate() {
606 assert_eq!(
607 a.cmp(b),
608 i.cmp(&k),
609 "Ord disagrees with declaration order for {a:?} vs {b:?}"
610 );
611 assert_eq!(a.partial_cmp(b), Some(a.cmp(b)));
612 assert_eq!(a == b, i == k);
613 }
614 }
615 }
616
617 #[test]
618 fn equal_variants_hash_equally_and_distinct_variants_do_not_collide() {
619 use std::collections::hash_map::DefaultHasher;
620 use std::hash::{Hash, Hasher};
621
622 fn hash_of(j: LayoutTextJustify) -> u64 {
623 let mut hasher = DefaultHasher::new();
624 j.hash(&mut hasher);
625 hasher.finish()
626 }
627
628 for j in ALL_JUSTIFY {
629 assert_eq!(hash_of(j), hash_of(j), "{j:?} hashes unstably");
630 }
631 let mut hashes: Vec<u64> = ALL_JUSTIFY.iter().copied().map(hash_of).collect();
632 hashes.sort_unstable();
633 hashes.dedup();
634 assert_eq!(
635 hashes.len(),
636 ALL_JUSTIFY.len(),
637 "two variants share a hash — HashMap<LayoutTextJustify, _> would be needlessly slow"
638 );
639 }
640
641 const NASTY: [&str; 9] = [
647 "",
648 "auto",
649 "\0",
650 "line\nbreak",
651 "🙂",
652 "a\u{301}",
653 "{}",
654 "{0} {1} {{}}",
655 "%s %d %n",
656 ];
657
658 #[test]
659 fn display_is_non_empty_and_quotes_the_offending_value() {
660 for value in NASTY {
661 let msg = TextJustifyParseError::InvalidValue(value).to_string();
662 assert!(!msg.is_empty(), "empty message for {value:?}");
663 assert!(
664 msg.starts_with("Invalid text-justify value: '"),
665 "unexpected prefix: {msg:?}"
666 );
667 assert!(msg.ends_with("'."), "unexpected suffix: {msg:?}");
668 assert!(
669 msg.contains(value),
670 "message {msg:?} dropped the offending value {value:?}"
671 );
672 }
673 }
674
675 #[test]
678 fn display_does_not_interpret_braces_in_the_payload() {
679 let msg = TextJustifyParseError::InvalidValue("{0} {{}} %s").to_string();
680 assert_eq!(msg, "Invalid text-justify value: '{0} {{}} %s'.");
681 }
682
683 #[test]
684 fn display_survives_empty_and_megabyte_payloads() {
685 assert_eq!(
686 TextJustifyParseError::InvalidValue("").to_string(),
687 "Invalid text-justify value: ''."
688 );
689
690 let huge = "x".repeat(1_000_000);
691 let msg = TextJustifyParseError::InvalidValue(huge.as_str()).to_string();
692 assert_eq!(msg.len(), "Invalid text-justify value: ''.".len() + huge.len());
694 assert!(msg.contains(huge.as_str()));
695 }
696
697 #[test]
698 fn to_owned_then_to_borrowed_is_the_identity() {
699 for value in NASTY {
700 let borrowed = TextJustifyParseError::InvalidValue(value);
701 let owned = borrowed.to_owned();
702 assert_eq!(
703 owned,
704 TextJustifyParseErrorOwned::InvalidValue(String::from(value).into())
705 );
706 assert_eq!(owned.to_borrowed(), borrowed, "round trip of {value:?}");
707 assert_eq!(owned.to_borrowed().to_string(), borrowed.to_string());
709 }
710 }
711
712 #[test]
713 fn to_owned_survives_a_large_multibyte_payload_and_keeps_its_length() {
714 let huge = "\u{1F600}".repeat(100_000); let borrowed = TextJustifyParseError::InvalidValue(huge.as_str());
716 let owned = borrowed.to_owned();
717 let TextJustifyParseErrorOwned::InvalidValue(s) = &owned;
718 assert_eq!(s.as_str().len(), huge.len());
719 assert_eq!(owned.to_borrowed(), borrowed);
720 }
721
722 #[test]
725 fn parse_error_payload_round_trips_through_the_owned_form() {
726 for input in [" 💥 ", "\tnot-a-keyword\n", "", " ", "\0\0"] {
727 let err = parse_layout_text_justify(input).unwrap_err();
728 let TextJustifyParseError::InvalidValue(borrowed) = &err;
729 assert_eq!(*borrowed, input.trim());
730
731 let owned = err.to_owned();
732 let TextJustifyParseErrorOwned::InvalidValue(s) = &owned;
733 assert_eq!(s.as_str(), input.trim());
734 assert_eq!(owned.to_borrowed(), err);
735 }
736 }
737
738 #[test]
739 fn owned_error_is_independent_of_the_input_buffer() {
740 let owned = {
743 let scratch = String::from(" transient-garbage ");
744 parse_layout_text_justify(&scratch).unwrap_err().to_owned()
745 };
746 let TextJustifyParseErrorOwned::InvalidValue(s) = &owned;
747 assert_eq!(s.as_str(), "transient-garbage");
748 assert_eq!(
749 owned.to_borrowed().to_string(),
750 "Invalid text-justify value: 'transient-garbage'."
751 );
752 }
753}