1
//! CSS properties for writing modes and clearing.
2
//!
3
//! Key types:
4
//! - [`LayoutWritingMode`] — `writing-mode` (`horizontal-tb`, `vertical-rl`, `vertical-lr`)
5
//! - [`LayoutClear`] — `clear` (`none`, `left`, `right`, `both`)
6
//!
7
//! Parse functions are gated behind the `parser` feature and are consumed
8
//! by the CSS property system in `property.rs`.
9

            
10
use alloc::string::{String, ToString};
11
use crate::corety::AzString;
12

            
13
use crate::props::formatter::PrintAsCssValue;
14

            
15
// --- writing-mode (LayoutWritingMode) ---
16

            
17
// +spec:writing-modes:ec496c - writing-mode property: horizontal-tb, vertical-rl, vertical-lr block flow directions
18
// +spec:writing-modes:fdc4cc - writing-mode property: horizontal-tb | vertical-rl | vertical-lr
19
// +spec:writing-modes:aeb9bb - writing-mode property determines block flow direction
20
/// Represents a `writing-mode` attribute
21
// +spec:writing-modes:a7f174 - line orientation: in vertical-lr the line-over (ascender) side is block-end, not block-start
22
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
23
#[repr(C)]
24
// +spec:block-formatting-context:387117 - writing-mode specifies horizontal/vertical line layout and block progression direction
25
// +spec:block-formatting-context:3815e7 - vertical-rl writing mode supported via VerticalRl variant
26
// +spec:block-formatting-context:9d7cd4 - vertical writing mode support (VerticalRl, VerticalLr)
27
#[derive(Default)]
28
pub enum LayoutWritingMode {
29
    /// Top-to-bottom block flow, left-to-right inline direction (Latin, etc.).
30
    #[default]
31
    HorizontalTb,
32
    /// Right-to-left block flow, top-to-bottom inline direction (CJK vertical).
33
    VerticalRl,
34
    // +spec:writing-modes:f35728 - vertical-lr writing mode for left-to-right block flow (Manchu, Mongolian)
35
    /// Left-to-right block flow, top-to-bottom inline direction (Mongolian).
36
    VerticalLr,
37
}
38

            
39

            
40
impl LayoutWritingMode {
41
    /// Returns true if the writing mode is vertical (`VerticalRl` or `VerticalLr`)
42
42
    #[must_use] pub const fn is_vertical(self) -> bool {
43
42
        matches!(self, Self::VerticalRl | Self::VerticalLr)
44
42
    }
45
}
46

            
47
impl core::fmt::Debug for LayoutWritingMode {
48
1578
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
49
1578
        write!(f, "{}", self.print_as_css_value())
50
1578
    }
51
}
52

            
53
impl core::fmt::Display for LayoutWritingMode {
54
7
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
55
7
        write!(f, "{}", self.print_as_css_value())
56
7
    }
57
}
58

            
59
impl PrintAsCssValue for LayoutWritingMode {
60
1601
    fn print_as_css_value(&self) -> String {
61
1601
        match self {
62
1582
            Self::HorizontalTb => "horizontal-tb".to_string(),
63
10
            Self::VerticalRl => "vertical-rl".to_string(),
64
9
            Self::VerticalLr => "vertical-lr".to_string(),
65
        }
66
1601
    }
67
}
68

            
69
#[cfg(feature = "parser")]
70
#[derive(Clone, PartialEq, Eq)]
71
pub enum LayoutWritingModeParseError<'a> {
72
    InvalidValue(&'a str),
73
}
74

            
75
#[cfg(feature = "parser")]
76
impl_debug_as_display!(LayoutWritingModeParseError<'a>);
77
#[cfg(feature = "parser")]
78
impl_display! { LayoutWritingModeParseError<'a>, {
79
    InvalidValue(e) => format!("Invalid writing-mode value: \"{}\"", e),
80
}}
81

            
82
#[cfg(feature = "parser")]
83
#[derive(Debug, Clone, PartialEq, Eq)]
84
#[repr(C, u8)]
85
pub enum LayoutWritingModeParseErrorOwned {
86
    InvalidValue(AzString),
87
}
88

            
89
#[cfg(feature = "parser")]
90
impl LayoutWritingModeParseError<'_> {
91
17
    #[must_use] pub fn to_contained(&self) -> LayoutWritingModeParseErrorOwned {
92
17
        match self {
93
17
            LayoutWritingModeParseError::InvalidValue(s) => {
94
17
                LayoutWritingModeParseErrorOwned::InvalidValue((*s).to_string().into())
95
            }
96
        }
97
17
    }
98
}
99

            
100
#[cfg(feature = "parser")]
101
impl LayoutWritingModeParseErrorOwned {
102
17
    #[must_use] pub fn to_shared(&self) -> LayoutWritingModeParseError<'_> {
103
17
        match self {
104
17
            Self::InvalidValue(s) => {
105
17
                LayoutWritingModeParseError::InvalidValue(s.as_str())
106
            }
107
        }
108
17
    }
109
}
110

            
111
#[cfg(feature = "parser")]
112
/// # Errors
113
///
114
/// Returns an error if `input` is not a valid CSS `writing-mode` value.
115
117
pub fn parse_layout_writing_mode(
116
117
    input: &str,
117
117
) -> Result<LayoutWritingMode, LayoutWritingModeParseError<'_>> {
118
117
    let input = input.trim();
119
117
    match input {
120
117
        "horizontal-tb" => Ok(LayoutWritingMode::HorizontalTb),
121
112
        "vertical-rl" => Ok(LayoutWritingMode::VerticalRl),
122
        // +spec:writing-modes:23147f - SVG1.1 tb-lr maps to vertical-lr
123
68
        "vertical-lr" | "tb-lr" => Ok(LayoutWritingMode::VerticalLr),
124
62
        _ => Err(LayoutWritingModeParseError::InvalidValue(input)),
125
    }
126
117
}
127

            
128
// --- clear (LayoutClear) ---
129

            
130
/// Represents a `clear` attribute
131
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
132
#[repr(C)]
133
#[derive(Default)]
134
pub enum LayoutClear {
135
    /// No clearing; element is not moved below preceding floats.
136
    #[default]
137
    None,
138
    /// Element is moved below preceding left floats.
139
    Left,
140
    /// Element is moved below preceding right floats.
141
    Right,
142
    /// Element is moved below all preceding floats.
143
    Both,
144
}
145

            
146

            
147
impl core::fmt::Debug for LayoutClear {
148
30536
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
149
30536
        write!(f, "{}", self.print_as_css_value())
150
30536
    }
151
}
152

            
153
impl core::fmt::Display for LayoutClear {
154
10
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
155
10
        write!(f, "{}", self.print_as_css_value())
156
10
    }
157
}
158

            
159
impl PrintAsCssValue for LayoutClear {
160
30563
    fn print_as_css_value(&self) -> String {
161
30563
        match self {
162
30429
            Self::None => "none".to_string(),
163
68
            Self::Left => "left".to_string(),
164
32
            Self::Right => "right".to_string(),
165
34
            Self::Both => "both".to_string(),
166
        }
167
30563
    }
168
}
169

            
170
#[cfg(feature = "parser")]
171
#[derive(Clone, PartialEq, Eq)]
172
pub enum LayoutClearParseError<'a> {
173
    InvalidValue(&'a str),
174
}
175

            
176
#[cfg(feature = "parser")]
177
impl_debug_as_display!(LayoutClearParseError<'a>);
178
#[cfg(feature = "parser")]
179
impl_display! { LayoutClearParseError<'a>, {
180
    InvalidValue(e) => format!("Invalid clear value: \"{}\"", e),
181
}}
182

            
183
#[cfg(feature = "parser")]
184
#[derive(Debug, Clone, PartialEq, Eq)]
185
#[repr(C, u8)]
186
pub enum LayoutClearParseErrorOwned {
187
    InvalidValue(AzString),
188
}
189

            
190
#[cfg(feature = "parser")]
191
impl LayoutClearParseError<'_> {
192
31
    #[must_use] pub fn to_contained(&self) -> LayoutClearParseErrorOwned {
193
31
        match self {
194
31
            LayoutClearParseError::InvalidValue(s) => {
195
31
                LayoutClearParseErrorOwned::InvalidValue((*s).to_string().into())
196
            }
197
        }
198
31
    }
199
}
200

            
201
#[cfg(feature = "parser")]
202
impl LayoutClearParseErrorOwned {
203
31
    #[must_use] pub fn to_shared(&self) -> LayoutClearParseError<'_> {
204
31
        match self {
205
31
            Self::InvalidValue(s) => {
206
31
                LayoutClearParseError::InvalidValue(s.as_str())
207
            }
208
        }
209
31
    }
210
}
211

            
212
#[cfg(feature = "parser")]
213
/// # Errors
214
///
215
/// Returns an error if `input` is not a valid CSS `clear` value.
216
171
pub fn parse_layout_clear(input: &str) -> Result<LayoutClear, LayoutClearParseError<'_>> {
217
171
    let input = input.trim();
218
171
    match input {
219
171
        "none" => Ok(LayoutClear::None),
220
166
        "left" => Ok(LayoutClear::Left),
221
114
        "right" => Ok(LayoutClear::Right),
222
98
        "both" => Ok(LayoutClear::Both),
223
67
        _ => Err(LayoutClearParseError::InvalidValue(input)),
224
    }
225
171
}
226

            
227
#[cfg(all(test, feature = "parser"))]
228
mod tests {
229
    use super::*;
230

            
231
    // LayoutWritingMode tests
232
    #[test]
233
1
    fn test_parse_writing_mode_horizontal_tb() {
234
1
        assert_eq!(
235
1
            parse_layout_writing_mode("horizontal-tb").unwrap(),
236
            LayoutWritingMode::HorizontalTb
237
        );
238
1
    }
239

            
240
    #[test]
241
1
    fn test_parse_writing_mode_vertical_rl() {
242
1
        assert_eq!(
243
1
            parse_layout_writing_mode("vertical-rl").unwrap(),
244
            LayoutWritingMode::VerticalRl
245
        );
246
1
    }
247

            
248
    #[test]
249
1
    fn test_parse_writing_mode_vertical_lr() {
250
1
        assert_eq!(
251
1
            parse_layout_writing_mode("vertical-lr").unwrap(),
252
            LayoutWritingMode::VerticalLr
253
        );
254
1
    }
255

            
256
    #[test]
257
1
    fn test_parse_writing_mode_invalid() {
258
1
        assert!(parse_layout_writing_mode("invalid").is_err());
259
1
        assert!(parse_layout_writing_mode("horizontal").is_err());
260
1
    }
261

            
262
    #[test]
263
1
    fn test_parse_writing_mode_whitespace() {
264
1
        assert_eq!(
265
1
            parse_layout_writing_mode("  vertical-rl  ").unwrap(),
266
            LayoutWritingMode::VerticalRl
267
        );
268
1
    }
269

            
270
    // LayoutClear tests
271
    #[test]
272
1
    fn test_parse_layout_clear_none() {
273
1
        assert_eq!(parse_layout_clear("none").unwrap(), LayoutClear::None);
274
1
    }
275

            
276
    #[test]
277
1
    fn test_parse_layout_clear_left() {
278
1
        assert_eq!(parse_layout_clear("left").unwrap(), LayoutClear::Left);
279
1
    }
280

            
281
    #[test]
282
1
    fn test_parse_layout_clear_right() {
283
1
        assert_eq!(parse_layout_clear("right").unwrap(), LayoutClear::Right);
284
1
    }
285

            
286
    #[test]
287
1
    fn test_parse_layout_clear_both() {
288
1
        assert_eq!(parse_layout_clear("both").unwrap(), LayoutClear::Both);
289
1
    }
290

            
291
    #[test]
292
1
    fn test_parse_layout_clear_invalid() {
293
1
        assert!(parse_layout_clear("invalid").is_err());
294
1
        assert!(parse_layout_clear("all").is_err());
295
1
    }
296

            
297
    #[test]
298
1
    fn test_parse_layout_clear_whitespace() {
299
1
        assert_eq!(parse_layout_clear("  both  ").unwrap(), LayoutClear::Both);
300
1
    }
301

            
302
    // Print tests
303
    #[test]
304
1
    fn test_print_writing_mode() {
305
1
        assert_eq!(
306
1
            LayoutWritingMode::HorizontalTb.print_as_css_value(),
307
            "horizontal-tb"
308
        );
309
1
        assert_eq!(
310
1
            LayoutWritingMode::VerticalRl.print_as_css_value(),
311
            "vertical-rl"
312
        );
313
1
        assert_eq!(
314
1
            LayoutWritingMode::VerticalLr.print_as_css_value(),
315
            "vertical-lr"
316
        );
317
1
    }
318

            
319
    #[test]
320
1
    fn test_print_layout_clear() {
321
1
        assert_eq!(LayoutClear::None.print_as_css_value(), "none");
322
1
        assert_eq!(LayoutClear::Left.print_as_css_value(), "left");
323
1
        assert_eq!(LayoutClear::Right.print_as_css_value(), "right");
324
1
        assert_eq!(LayoutClear::Both.print_as_css_value(), "both");
325
1
    }
326
}
327

            
328
#[cfg(test)]
329
mod autotest_generated {
330
    use super::*;
331

            
332
    const ALL_WRITING_MODES: [LayoutWritingMode; 3] = [
333
        LayoutWritingMode::HorizontalTb,
334
        LayoutWritingMode::VerticalRl,
335
        LayoutWritingMode::VerticalLr,
336
    ];
337

            
338
    const ALL_CLEARS: [LayoutClear; 4] = [
339
        LayoutClear::None,
340
        LayoutClear::Left,
341
        LayoutClear::Right,
342
        LayoutClear::Both,
343
    ];
344

            
345
    // --- LayoutWritingMode::is_vertical (predicate) ---
346

            
347
    #[test]
348
    fn is_vertical_known_true_and_false() {
349
        assert!(!LayoutWritingMode::HorizontalTb.is_vertical());
350
        assert!(LayoutWritingMode::VerticalRl.is_vertical());
351
        assert!(LayoutWritingMode::VerticalLr.is_vertical());
352
    }
353

            
354
    #[test]
355
    fn is_vertical_default_is_horizontal() {
356
        assert!(!LayoutWritingMode::default().is_vertical());
357
        assert_eq!(LayoutWritingMode::default(), LayoutWritingMode::HorizontalTb);
358
    }
359

            
360
    #[test]
361
    fn is_vertical_is_const_evaluable() {
362
        const HORIZONTAL: bool = LayoutWritingMode::HorizontalTb.is_vertical();
363
        const VERTICAL: bool = LayoutWritingMode::VerticalRl.is_vertical();
364
        const _: () = assert!(!HORIZONTAL && VERTICAL);
365
    }
366

            
367
    #[test]
368
    fn is_vertical_agrees_with_css_keyword_prefix() {
369
        // Invariant: is_vertical() must agree with the serialized keyword, so the
370
        // predicate can never drift away from the CSS value it claims to describe.
371
        for mode in ALL_WRITING_MODES {
372
            let css = mode.print_as_css_value();
373
            assert_eq!(
374
                mode.is_vertical(),
375
                css.starts_with("vertical-"),
376
                "is_vertical() disagrees with keyword {css}"
377
            );
378
        }
379
    }
380

            
381
    #[test]
382
    fn is_vertical_is_pure_and_does_not_consume() {
383
        // `self`-by-value on a Copy enum: repeated calls must be stable.
384
        let mode = LayoutWritingMode::VerticalLr;
385
        assert!(mode.is_vertical());
386
        assert!(mode.is_vertical());
387
        assert_eq!(mode, LayoutWritingMode::VerticalLr);
388
    }
389

            
390
    // --- Debug / Display / PrintAsCssValue (serializers) ---
391

            
392
    #[test]
393
    fn writing_mode_debug_display_and_css_value_all_agree() {
394
        for mode in ALL_WRITING_MODES {
395
            let css = mode.print_as_css_value();
396
            assert!(!css.is_empty());
397
            assert_eq!(format!("{mode:?}"), css);
398
            assert_eq!(format!("{mode}"), css);
399
        }
400
    }
401

            
402
    #[test]
403
    fn clear_debug_display_and_css_value_all_agree() {
404
        for clear in ALL_CLEARS {
405
            let css = clear.print_as_css_value();
406
            assert!(!css.is_empty());
407
            assert_eq!(format!("{clear:?}"), css);
408
            assert_eq!(format!("{clear}"), css);
409
        }
410
    }
411

            
412
    #[test]
413
    fn serializing_defaults_does_not_panic() {
414
        assert_eq!(LayoutWritingMode::default().print_as_css_value(), "horizontal-tb");
415
        assert_eq!(LayoutClear::default().print_as_css_value(), "none");
416
    }
417

            
418
    #[test]
419
    fn serialized_keywords_are_distinct() {
420
        // Two variants sharing a keyword would silently collapse on re-parse.
421
        let modes: alloc::collections::BTreeSet<String> = ALL_WRITING_MODES
422
            .iter()
423
            .map(PrintAsCssValue::print_as_css_value)
424
            .collect();
425
        assert_eq!(modes.len(), ALL_WRITING_MODES.len());
426

            
427
        let clears: alloc::collections::BTreeSet<String> = ALL_CLEARS
428
            .iter()
429
            .map(PrintAsCssValue::print_as_css_value)
430
            .collect();
431
        assert_eq!(clears.len(), ALL_CLEARS.len());
432
    }
433

            
434
    #[test]
435
    fn display_impl_ignores_width_and_precision_flags() {
436
        // The Display impl forwards through `write!(f, "{}", String)`, which writes
437
        // straight to the underlying buffer and drops the outer formatter's flags.
438
        // Padding/truncation therefore does NOT apply — pinning the real behaviour so
439
        // callers never build a stylesheet assuming `{:>20}` aligns.
440
        assert_eq!(format!("{:>20}", LayoutClear::Both), "both");
441
        assert_eq!(format!("{:.2}", LayoutClear::Both), "both");
442
        assert_eq!(format!("{:>20}", LayoutWritingMode::VerticalRl), "vertical-rl");
443
    }
444

            
445
    // --- ordering / hashing invariants on the plain enums ---
446

            
447
    #[test]
448
    fn writing_mode_ord_follows_declaration_order() {
449
        assert!(LayoutWritingMode::HorizontalTb < LayoutWritingMode::VerticalRl);
450
        assert!(LayoutWritingMode::VerticalRl < LayoutWritingMode::VerticalLr);
451
    }
452

            
453
    #[test]
454
    fn clear_ord_follows_declaration_order() {
455
        assert!(LayoutClear::None < LayoutClear::Left);
456
        assert!(LayoutClear::Left < LayoutClear::Right);
457
        assert!(LayoutClear::Right < LayoutClear::Both);
458
    }
459

            
460
    #[test]
461
    fn equal_values_hash_equally() {
462
        use core::hash::{Hash, Hasher};
463
        use std::collections::hash_map::DefaultHasher;
464

            
465
        fn hash_of<T: Hash>(value: &T) -> u64 {
466
            let mut hasher = DefaultHasher::new();
467
            value.hash(&mut hasher);
468
            hasher.finish()
469
        }
470

            
471
        // Hash must agree with Eq: independently-constructed equal values hash alike.
472
        assert_eq!(
473
            hash_of(&LayoutWritingMode::VerticalRl),
474
            hash_of(&ALL_WRITING_MODES[1])
475
        );
476
        assert_eq!(hash_of(&LayoutClear::Both), hash_of(&ALL_CLEARS[3]));
477
        assert_ne!(
478
            hash_of(&LayoutWritingMode::HorizontalTb),
479
            hash_of(&LayoutWritingMode::VerticalRl)
480
        );
481
    }
482

            
483
    // --- parsers ---
484

            
485
    #[cfg(feature = "parser")]
486
    #[test]
487
    fn parse_writing_mode_valid_minimal() {
488
        assert_eq!(
489
            parse_layout_writing_mode("horizontal-tb"),
490
            Ok(LayoutWritingMode::HorizontalTb)
491
        );
492
    }
493

            
494
    #[cfg(feature = "parser")]
495
    #[test]
496
    fn parse_writing_mode_accepts_svg_tb_lr_alias() {
497
        // SVG 1.1 `tb-lr` is an accepted alias for `vertical-lr`.
498
        assert_eq!(
499
            parse_layout_writing_mode("tb-lr"),
500
            Ok(LayoutWritingMode::VerticalLr)
501
        );
502
        // ...but the other SVG 1.1 writing-mode aliases are NOT mapped.
503
        for unsupported in ["lr", "lr-tb", "rl", "rl-tb", "tb"] {
504
            assert!(
505
                parse_layout_writing_mode(unsupported).is_err(),
506
                "{unsupported} unexpectedly parsed"
507
            );
508
        }
509
    }
510

            
511
    #[cfg(feature = "parser")]
512
    #[test]
513
    fn parse_clear_valid_minimal() {
514
        assert_eq!(parse_layout_clear("none"), Ok(LayoutClear::None));
515
    }
516

            
517
    #[cfg(feature = "parser")]
518
    #[test]
519
    fn parse_empty_input_errors_with_empty_payload() {
520
        let err = parse_layout_writing_mode("").unwrap_err();
521
        assert_eq!(err, LayoutWritingModeParseError::InvalidValue(""));
522
        let err = parse_layout_clear("").unwrap_err();
523
        assert_eq!(err, LayoutClearParseError::InvalidValue(""));
524
    }
525

            
526
    #[cfg(feature = "parser")]
527
    #[test]
528
    fn parse_whitespace_only_input_errors_after_trimming() {
529
        for blank in ["   ", "\t\n", "\r\n\r\n", "\u{0c}", " \t \n \r "] {
530
            assert_eq!(
531
                parse_layout_writing_mode(blank).unwrap_err(),
532
                LayoutWritingModeParseError::InvalidValue(""),
533
                "whitespace {blank:?} should trim to the empty payload"
534
            );
535
            assert_eq!(
536
                parse_layout_clear(blank).unwrap_err(),
537
                LayoutClearParseError::InvalidValue("")
538
            );
539
        }
540
    }
541

            
542
    #[cfg(feature = "parser")]
543
    #[test]
544
    fn parse_garbage_returns_err_and_echoes_trimmed_input() {
545
        for garbage in [
546
            "invalid",
547
            "!!!",
548
            "\u{0}",
549
            "none\u{0}",
550
            "{}[]();",
551
            "-",
552
            "--",
553
            "vertical-",
554
            "-rl",
555
            "vertical rl",
556
            "vertical - rl",
557
            "vertical_rl",
558
            "verticalrl",
559
        ] {
560
            let err = parse_layout_writing_mode(garbage).unwrap_err();
561
            assert_eq!(err, LayoutWritingModeParseError::InvalidValue(garbage.trim()));
562
            let err = parse_layout_clear(garbage).unwrap_err();
563
            assert_eq!(err, LayoutClearParseError::InvalidValue(garbage.trim()));
564
        }
565
    }
566

            
567
    #[cfg(feature = "parser")]
568
    #[test]
569
    fn parse_rejects_css_wide_keywords() {
570
        // These are handled (if at all) by the caller in property.rs, not here —
571
        // the value parser itself must not silently accept them as keywords.
572
        for wide in ["inherit", "initial", "unset", "revert", "revert-layer"] {
573
            assert!(parse_layout_writing_mode(wide).is_err(), "{wide} accepted");
574
            assert!(parse_layout_clear(wide).is_err(), "{wide} accepted");
575
        }
576
    }
577

            
578
    #[cfg(feature = "parser")]
579
    #[test]
580
    fn parse_is_ascii_case_sensitive() {
581
        // NOTE: CSS keywords are ASCII case-insensitive per css-values-4, but these
582
        // parsers match case-sensitively. Locking in the CURRENT behaviour; see the
583
        // report for the spec deviation.
584
        for upper in ["HORIZONTAL-TB", "Vertical-Rl", "VERTICAL-LR", "TB-LR"] {
585
            assert!(
586
                parse_layout_writing_mode(upper).is_err(),
587
                "{upper} parsed — case-insensitive matching was added, update this test"
588
            );
589
        }
590
        for upper in ["NONE", "Left", "RIGHT", "Both"] {
591
            assert!(
592
                parse_layout_clear(upper).is_err(),
593
                "{upper} parsed — case-insensitive matching was added, update this test"
594
            );
595
        }
596
    }
597

            
598
    #[cfg(feature = "parser")]
599
    #[test]
600
    fn parse_leading_trailing_ascii_whitespace_is_trimmed() {
601
        assert_eq!(
602
            parse_layout_writing_mode("  vertical-rl  "),
603
            Ok(LayoutWritingMode::VerticalRl)
604
        );
605
        assert_eq!(
606
            parse_layout_writing_mode("\t\nvertical-lr\r\n"),
607
            Ok(LayoutWritingMode::VerticalLr)
608
        );
609
        assert_eq!(parse_layout_clear("\t both \n"), Ok(LayoutClear::Both));
610
    }
611

            
612
    #[cfg(feature = "parser")]
613
    #[test]
614
    fn parse_rejects_trailing_junk_after_valid_keyword() {
615
        for junk in ["both;garbage", "both;", "both both", "both!important", "both,"] {
616
            assert_eq!(
617
                parse_layout_clear(junk).unwrap_err(),
618
                LayoutClearParseError::InvalidValue(junk.trim())
619
            );
620
        }
621
        assert!(parse_layout_writing_mode("vertical-rl;garbage").is_err());
622
    }
623

            
624
    #[cfg(feature = "parser")]
625
    #[test]
626
    fn parse_trims_unicode_whitespace_beyond_css_whitespace() {
627
        // `str::trim` uses the Unicode White_Space property, which is a SUPERSET of
628
        // CSS whitespace (space, tab, LF, CR, FF). So NBSP-padded keywords parse
629
        // here even though a spec-conformant CSS tokenizer would reject them.
630
        // Asserting the CURRENT behaviour; see the report.
631
        assert_eq!(
632
            parse_layout_clear("\u{00a0}both\u{00a0}"),
633
            Ok(LayoutClear::Both)
634
        );
635
        assert_eq!(
636
            parse_layout_writing_mode("\u{2003}vertical-rl\u{2003}"),
637
            Ok(LayoutWritingMode::VerticalRl)
638
        );
639
        // Zero-width space and BOM are NOT White_Space, so they stay in the payload.
640
        assert_eq!(
641
            parse_layout_clear("\u{200b}both").unwrap_err(),
642
            LayoutClearParseError::InvalidValue("\u{200b}both")
643
        );
644
        assert_eq!(
645
            parse_layout_clear("\u{feff}both").unwrap_err(),
646
            LayoutClearParseError::InvalidValue("\u{feff}both")
647
        );
648
    }
649

            
650
    #[cfg(feature = "parser")]
651
    #[test]
652
    fn parse_unicode_input_does_not_panic_on_char_boundaries() {
653
        for weird in [
654
            "\u{1F600}",
655
            "🙂🙃",
656
            "e\u{0301}",              // combining acute
657
            "n\u{0303}one",           // combining tilde inside a keyword
658
            "none",               // fullwidth
659
            "نص",                     // RTL
660
            "\u{202E}both",           // RTL override
661
            "both\u{0301}",
662
        ] {
663
            let err = parse_layout_writing_mode(weird).unwrap_err();
664
            assert_eq!(err, LayoutWritingModeParseError::InvalidValue(weird.trim()));
665
            let err = parse_layout_clear(weird).unwrap_err();
666
            assert_eq!(err, LayoutClearParseError::InvalidValue(weird.trim()));
667
            // The borrowed payload must still be valid UTF-8 we can round-trip.
668
            assert_eq!(err.to_contained().to_shared(), err);
669
        }
670
    }
671

            
672
    #[cfg(feature = "parser")]
673
    #[test]
674
    fn parse_boundary_numeric_strings_are_rejected() {
675
        for numeric in [
676
            "0",
677
            "-0",
678
            "0.0",
679
            "NaN",
680
            "nan",
681
            "inf",
682
            "-inf",
683
            "infinity",
684
            "1e999",
685
            "-1e-999",
686
            "9223372036854775807",  // i64::MAX
687
            "-9223372036854775808", // i64::MIN
688
            "18446744073709551616", // u64::MAX + 1
689
            "179769313486231570000000000000000000000000000000000000000000000000000000000000000",
690
        ] {
691
            assert!(parse_layout_writing_mode(numeric).is_err(), "{numeric} parsed");
692
            assert!(parse_layout_clear(numeric).is_err(), "{numeric} parsed");
693
        }
694
    }
695

            
696
    #[cfg(feature = "parser")]
697
    #[test]
698
    fn parse_extremely_long_input_does_not_panic_or_hang() {
699
        let long = "a".repeat(1_000_000);
700
        let err = parse_layout_clear(&long).unwrap_err();
701
        assert_eq!(err, LayoutClearParseError::InvalidValue(long.as_str()));
702

            
703
        // A megabyte of whitespace must trim down to the empty payload, not hang.
704
        let blank = " ".repeat(1_000_000);
705
        assert_eq!(
706
            parse_layout_writing_mode(&blank).unwrap_err(),
707
            LayoutWritingModeParseError::InvalidValue("")
708
        );
709

            
710
        // A valid keyword buried in a megabyte of padding still parses.
711
        let padded = format!("{blank}vertical-rl{blank}");
712
        assert_eq!(
713
            parse_layout_writing_mode(&padded),
714
            Ok(LayoutWritingMode::VerticalRl)
715
        );
716

            
717
        // A near-miss keyword repeated: still exactly one Err, no quadratic blowup.
718
        let repeated = "both ".repeat(200_000);
719
        assert!(parse_layout_clear(&repeated).is_err());
720
    }
721

            
722
    #[cfg(feature = "parser")]
723
    #[test]
724
    fn parse_deeply_nested_input_does_not_stack_overflow() {
725
        let nested = "(".repeat(10_000) + &")".repeat(10_000);
726
        assert!(parse_layout_clear(&nested).is_err());
727
        assert!(parse_layout_writing_mode(&nested).is_err());
728

            
729
        let brackets = "[".repeat(10_000);
730
        assert_eq!(
731
            parse_layout_clear(&brackets).unwrap_err(),
732
            LayoutClearParseError::InvalidValue(brackets.as_str())
733
        );
734
    }
735

            
736
    #[cfg(feature = "parser")]
737
    #[test]
738
    fn parse_error_payload_borrows_the_trimmed_slice_not_the_whole_input() {
739
        let input = "   bogus-value   ";
740
        let LayoutClearParseError::InvalidValue(payload) = parse_layout_clear(input).unwrap_err();
741
        // The error echoes the TRIMMED slice, not the raw input.
742
        assert_eq!(payload, "bogus-value");
743
        assert_eq!(payload.len(), 11);
744
        // ...and it is a borrowed subslice of the original buffer, not a copy.
745
        assert!(input.as_ptr() <= payload.as_ptr());
746
    }
747

            
748
    // --- round-trip: serialize -> parse -> same value ---
749

            
750
    #[test]
751
    #[cfg(feature = "parser")]
752
    fn writing_mode_round_trips_through_css_value() {
753
        for mode in ALL_WRITING_MODES {
754
            let css = mode.print_as_css_value();
755
            assert_eq!(parse_layout_writing_mode(&css), Ok(mode), "round-trip {css}");
756
            // Display and Debug are the same wire format, so they round-trip too.
757
            assert_eq!(parse_layout_writing_mode(&format!("{mode}")), Ok(mode));
758
            assert_eq!(parse_layout_writing_mode(&format!("{mode:?}")), Ok(mode));
759
        }
760
    }
761

            
762
    #[test]
763
    #[cfg(feature = "parser")]
764
    fn clear_round_trips_through_css_value() {
765
        for clear in ALL_CLEARS {
766
            let css = clear.print_as_css_value();
767
            assert_eq!(parse_layout_clear(&css), Ok(clear), "round-trip {css}");
768
            assert_eq!(parse_layout_clear(&format!("{clear}")), Ok(clear));
769
            assert_eq!(parse_layout_clear(&format!("{clear:?}")), Ok(clear));
770
        }
771
    }
772

            
773
    // --- error getters: to_contained / to_shared ---
774

            
775
    #[cfg(feature = "parser")]
776
    #[test]
777
    fn writing_mode_error_to_contained_preserves_the_value() {
778
        let err = parse_layout_writing_mode("bogus").unwrap_err();
779
        assert_eq!(
780
            err.to_contained(),
781
            LayoutWritingModeParseErrorOwned::InvalidValue("bogus".into())
782
        );
783
    }
784

            
785
    #[cfg(feature = "parser")]
786
    #[test]
787
    fn clear_error_to_contained_preserves_the_value() {
788
        let err = parse_layout_clear("bogus").unwrap_err();
789
        assert_eq!(
790
            err.to_contained(),
791
            LayoutClearParseErrorOwned::InvalidValue("bogus".into())
792
        );
793
    }
794

            
795
    #[cfg(feature = "parser")]
796
    #[test]
797
    fn error_to_contained_outlives_the_parsed_input() {
798
        // The whole point of to_contained(): escape the input's lifetime.
799
        let owned = {
800
            let scratch = String::from("temporary-garbage");
801
            parse_layout_clear(&scratch).unwrap_err().to_contained()
802
        };
803
        assert_eq!(
804
            owned,
805
            LayoutClearParseErrorOwned::InvalidValue("temporary-garbage".into())
806
        );
807
        assert_eq!(
808
            owned.to_shared(),
809
            LayoutClearParseError::InvalidValue("temporary-garbage")
810
        );
811
    }
812

            
813
    #[cfg(feature = "parser")]
814
    #[test]
815
    fn error_shared_owned_round_trip_is_lossless() {
816
        for value in [
817
            "",
818
            " ",
819
            "\u{0}",
820
            "🙂",
821
            "e\u{0301}",
822
            "quote\"inside",
823
            "back\\slash",
824
            "new\nline",
825
        ] {
826
            let shared = LayoutClearParseError::InvalidValue(value);
827
            let owned = shared.to_contained();
828
            assert_eq!(owned.to_shared(), shared, "clear round-trip {value:?}");
829
            // to_contained -> to_shared -> to_contained is idempotent.
830
            assert_eq!(owned.to_shared().to_contained(), owned);
831

            
832
            let shared = LayoutWritingModeParseError::InvalidValue(value);
833
            let owned = shared.to_contained();
834
            assert_eq!(owned.to_shared(), shared, "writing-mode round-trip {value:?}");
835
            assert_eq!(owned.to_shared().to_contained(), owned);
836
        }
837
    }
838

            
839
    #[cfg(feature = "parser")]
840
    #[test]
841
    fn error_round_trip_survives_a_huge_payload() {
842
        let huge = "x".repeat(100_000);
843
        let shared = LayoutClearParseError::InvalidValue(huge.as_str());
844
        let owned = shared.to_contained();
845
        let LayoutClearParseError::InvalidValue(back) = owned.to_shared();
846
        assert_eq!(back.len(), 100_000);
847
        assert_eq!(back, huge.as_str());
848
    }
849

            
850
    #[cfg(feature = "parser")]
851
    #[test]
852
    fn error_to_shared_on_default_azstring_does_not_panic() {
853
        // AzString::default() is the empty &'static str — the degenerate case for
854
        // the unchecked from_utf8 inside AzString::as_str().
855
        let owned = LayoutClearParseErrorOwned::InvalidValue(AzString::default());
856
        assert_eq!(owned.to_shared(), LayoutClearParseError::InvalidValue(""));
857

            
858
        let owned = LayoutWritingModeParseErrorOwned::InvalidValue(AzString::default());
859
        assert_eq!(
860
            owned.to_shared(),
861
            LayoutWritingModeParseError::InvalidValue("")
862
        );
863
    }
864

            
865
    #[cfg(feature = "parser")]
866
    #[test]
867
    fn error_display_and_debug_include_the_offending_value() {
868
        let err = parse_layout_writing_mode("  bogus  ").unwrap_err();
869
        let display = format!("{err}");
870
        assert_eq!(display, "Invalid writing-mode value: \"bogus\"");
871
        // impl_debug_as_display!: Debug must be identical to Display.
872
        assert_eq!(format!("{err:?}"), display);
873

            
874
        let err = parse_layout_clear("bogus").unwrap_err();
875
        let display = format!("{err}");
876
        assert_eq!(display, "Invalid clear value: \"bogus\"");
877
        assert_eq!(format!("{err:?}"), display);
878
    }
879

            
880
    #[cfg(feature = "parser")]
881
    #[test]
882
    fn error_display_does_not_panic_on_empty_or_unicode_payloads() {
883
        for value in ["", "\u{0}", "🙂", "\u{202E}"] {
884
            let err = LayoutClearParseError::InvalidValue(value);
885
            assert!(format!("{err}").contains(value));
886
            let owned = err.to_contained();
887
            assert!(format!("{:?}", owned.to_shared()).contains(value));
888
        }
889
    }
890
}