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