1
//! CSS properties for styling text.
2
//!
3
//! Each property type implements `PrintAsCssValue` for CSS serialization and
4
//! (behind the `parser` feature) has a corresponding `parse_style_*` function
5
//! with borrowed/owned error type pairs.
6

            
7
use alloc::string::{String, ToString};
8
use core::fmt;
9
use crate::corety::AzString;
10

            
11
use crate::{
12
    codegen::format::FormatAsRustCode,
13
    props::{
14
        basic::{
15
            error::{InvalidValueErr, InvalidValueErrOwned},
16
            length::{PercentageParseError, PercentageParseErrorOwned, PercentageValue},
17
            pixel::{CssPixelValueParseError, CssPixelValueParseErrorOwned, PixelValue},
18
            ColorU, CssDuration,
19
        },
20
        formatter::PrintAsCssValue,
21
        macros::PixelValueTaker,
22
    },
23
};
24

            
25
// -- StyleTextColor (color property) --
26
// NOTE: `color` is a text property, but the `ColorU` type itself is in `basic/color.rs`.
27
// This is a newtype wrapper for type safety.
28

            
29
/// Represents a `color` attribute.
30
#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
31
#[repr(C)]
32
pub struct StyleTextColor {
33
    pub inner: ColorU,
34
}
35

            
36
impl fmt::Debug for StyleTextColor {
37
264
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38
264
        write!(f, "{}", self.print_as_css_value())
39
264
    }
40
}
41

            
42
impl StyleTextColor {
43
728
    #[must_use] pub fn interpolate(&self, other: &Self, t: f32) -> Self {
44
728
        Self {
45
728
            inner: self.inner.interpolate(&other.inner, t),
46
728
        }
47
728
    }
48
}
49

            
50
impl PrintAsCssValue for StyleTextColor {
51
1585
    fn print_as_css_value(&self) -> String {
52
1585
        self.inner.to_hash()
53
1585
    }
54
}
55

            
56
// -- StyleTextAlign --
57

            
58
/// Horizontal text alignment enum (left, center, right) - default: `Left`
59
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
60
#[repr(C)]
61
pub enum StyleTextAlign {
62
    Left,
63
    Center,
64
    Right,
65
    Justify,
66
    #[default]
67
    Start,
68
    End,
69
}
70

            
71
impl_option!(
72
    StyleTextAlign,
73
    OptionStyleTextAlign,
74
    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
75
);
76

            
77
impl PrintAsCssValue for StyleTextAlign {
78
6
    fn print_as_css_value(&self) -> String {
79
6
        String::from(match self {
80
1
            Self::Left => "left",
81
1
            Self::Center => "center",
82
1
            Self::Right => "right",
83
1
            Self::Justify => "justify",
84
1
            Self::Start => "start",
85
1
            Self::End => "end",
86
        })
87
6
    }
88
}
89

            
90
// -- StyleLetterSpacing --
91

            
92
/// Represents a `letter-spacing` attribute
93
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
94
#[repr(C)]
95
pub struct StyleLetterSpacing {
96
    pub inner: PixelValue,
97
}
98

            
99
impl fmt::Debug for StyleLetterSpacing {
100
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101
        write!(f, "{}", self.inner)
102
    }
103
}
104
impl Default for StyleLetterSpacing {
105
    fn default() -> Self {
106
        Self {
107
            inner: PixelValue::const_px(0),
108
        }
109
    }
110
}
111
impl_pixel_value!(StyleLetterSpacing);
112
impl PixelValueTaker for StyleLetterSpacing {
113
    fn from_pixel_value(inner: PixelValue) -> Self {
114
        Self { inner }
115
    }
116
}
117
impl PrintAsCssValue for StyleLetterSpacing {
118
6
    fn print_as_css_value(&self) -> String {
119
6
        format!("{}", self.inner)
120
6
    }
121
}
122

            
123
// -- StyleWordSpacing --
124

            
125
/// Represents a `word-spacing` attribute
126
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
127
#[repr(C)]
128
pub struct StyleWordSpacing {
129
    pub inner: PixelValue,
130
}
131

            
132
impl fmt::Debug for StyleWordSpacing {
133
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134
        write!(f, "{}", self.inner)
135
    }
136
}
137
impl Default for StyleWordSpacing {
138
    fn default() -> Self {
139
        Self {
140
            inner: PixelValue::const_px(0),
141
        }
142
    }
143
}
144
impl_pixel_value!(StyleWordSpacing);
145
impl PixelValueTaker for StyleWordSpacing {
146
    fn from_pixel_value(inner: PixelValue) -> Self {
147
        Self { inner }
148
    }
149
}
150
impl PrintAsCssValue for StyleWordSpacing {
151
6
    fn print_as_css_value(&self) -> String {
152
6
        format!("{}", self.inner)
153
6
    }
154
}
155

            
156
// -- StyleLineHeight --
157

            
158
/// Represents a `line-height` attribute
159
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
160
#[repr(C)]
161
pub struct StyleLineHeight {
162
    pub inner: PercentageValue,
163
}
164
impl Default for StyleLineHeight {
165
297409
    fn default() -> Self {
166
297409
        Self {
167
297409
            inner: PercentageValue::const_new(120),
168
297409
        }
169
297409
    }
170
}
171
impl_percentage_value!(StyleLineHeight);
172
impl PrintAsCssValue for StyleLineHeight {
173
1
    fn print_as_css_value(&self) -> String {
174
1
        format!("{}", self.inner)
175
1
    }
176
}
177

            
178
// -- StyleTabSize --
179

            
180
/// Represents a `tab-size` attribute
181
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
182
#[repr(C)]
183
pub struct StyleTabSize {
184
    pub inner: PixelValue, // Can be a number (space characters, em-based) or a length
185
}
186

            
187
impl fmt::Debug for StyleTabSize {
188
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189
        write!(f, "{}", self.inner)
190
    }
191
}
192
impl Default for StyleTabSize {
193
1
    fn default() -> Self {
194
1
        Self {
195
1
            inner: PixelValue::em(8.0),
196
1
        }
197
1
    }
198
}
199
impl_pixel_value!(StyleTabSize);
200
impl PixelValueTaker for StyleTabSize {
201
    fn from_pixel_value(inner: PixelValue) -> Self {
202
        Self { inner }
203
    }
204
}
205
impl PrintAsCssValue for StyleTabSize {
206
1
    fn print_as_css_value(&self) -> String {
207
1
        format!("{}", self.inner)
208
1
    }
209
}
210

            
211
// -- StyleWhiteSpace --
212

            
213
/// How to handle white space inside an element.
214
/// 
215
/// CSS Text Level 3: <https://www.w3.org/TR/css-text-3/#white-space-property>
216
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
217
#[repr(C)]
218
#[derive(Default)]
219
pub enum StyleWhiteSpace {
220
    /// Collapse whitespace, wrap lines
221
    #[default]
222
    Normal,
223
    /// Preserve whitespace, no wrap (except for explicit breaks)
224
    Pre,
225
    /// Collapse whitespace, no wrap
226
    Nowrap,
227
    /// Preserve whitespace, wrap lines
228
    PreWrap,
229
    /// Collapse whitespace (except newlines), wrap lines
230
    PreLine,
231
    /// Preserve whitespace, allow breaking at spaces
232
    BreakSpaces,
233
}
234
impl_option!(
235
    StyleWhiteSpace,
236
    OptionStyleWhiteSpace,
237
    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
238
);
239
impl PrintAsCssValue for StyleWhiteSpace {
240
6
    fn print_as_css_value(&self) -> String {
241
6
        String::from(match self {
242
1
            Self::Normal => "normal",
243
1
            Self::Pre => "pre",
244
1
            Self::Nowrap => "nowrap",
245
1
            Self::PreWrap => "pre-wrap",
246
1
            Self::PreLine => "pre-line",
247
1
            Self::BreakSpaces => "break-spaces",
248
        })
249
6
    }
250
}
251

            
252
// -- StyleHyphens --
253

            
254
/// Hyphenation rules.
255
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
256
#[repr(C)]
257
#[derive(Default)]
258
pub enum StyleHyphens {
259
    /// No hyphenation: words are not broken at hyphenation opportunities.
260
    None,
261
    /// Manual hyphenation: words are only broken at explicit soft hyphens (U+00AD)
262
    /// or unconditional hyphens (U+2010).
263
    #[default]
264
    Manual,
265
    /// Automatic hyphenation: words may be broken at automatic hyphenation
266
    /// opportunities determined by a language-appropriate hyphenation resource,
267
    /// in addition to explicit opportunities.
268
    Auto,
269
}
270
impl_option!(
271
    StyleHyphens,
272
    OptionStyleHyphens,
273
    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
274
);
275
impl PrintAsCssValue for StyleHyphens {
276
3
    fn print_as_css_value(&self) -> String {
277
3
        String::from(match self {
278
1
            Self::None => "none",
279
1
            Self::Manual => "manual",
280
1
            Self::Auto => "auto",
281
        })
282
3
    }
283
}
284

            
285
// -- StyleLineBreak --
286

            
287
/// Controls the strictness of line breaking rules.
288
///
289
/// CSS Text Level 3: <https://www.w3.org/TR/css-text-3/#line-break-property>
290
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
291
#[repr(C)]
292
#[derive(Default)]
293
pub enum StyleLineBreak {
294
    /// The browser determines the set of line-breaking restrictions to use.
295
    #[default]
296
    Auto,
297
    /// Breaks text using the least restrictive set of line-breaking rules.
298
    Loose,
299
    /// Breaks text using the most common set of line-breaking rules.
300
    Normal,
301
    /// Breaks text using the most stringent set of line-breaking rules.
302
    Strict,
303
    /// There is a soft wrap opportunity around every typographic character unit,
304
    /// including around any punctuation character or preserved white spaces,
305
    /// or in the middle of words, disregarding any prohibition against line breaks.
306
    Anywhere,
307
}
308
impl_option!(
309
    StyleLineBreak,
310
    OptionStyleLineBreak,
311
    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
312
);
313
impl PrintAsCssValue for StyleLineBreak {
314
5
    fn print_as_css_value(&self) -> String {
315
5
        String::from(match self {
316
1
            Self::Auto => "auto",
317
1
            Self::Loose => "loose",
318
1
            Self::Normal => "normal",
319
1
            Self::Strict => "strict",
320
1
            Self::Anywhere => "anywhere",
321
        })
322
5
    }
323
}
324

            
325
// -- StyleWordBreak --
326

            
327
/// Controls line breaking rules within words.
328
///
329
/// CSS Text Level 3 §5.2: <https://www.w3.org/TR/css-text-3/#word-break-property>
330
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
331
#[repr(C)]
332
#[derive(Default)]
333
pub enum StyleWordBreak {
334
    /// Use default line break rules.
335
    #[default]
336
    Normal,
337
    /// Allow break opportunities between any two characters (CJK and non-CJK).
338
    BreakAll,
339
    /// Forbid break opportunities within CJK character sequences.
340
    KeepAll,
341
    // +spec:line-breaking:815882 - deprecated break-word keyword: same as normal + overflow-wrap: anywhere
342
    /// Deprecated: equivalent to word-break: normal and overflow-wrap: anywhere.
343
    BreakWord,
344
}
345
impl_option!(
346
    StyleWordBreak,
347
    OptionStyleWordBreak,
348
    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
349
);
350
impl PrintAsCssValue for StyleWordBreak {
351
4
    fn print_as_css_value(&self) -> String {
352
4
        String::from(match self {
353
1
            Self::Normal => "normal",
354
1
            Self::BreakAll => "break-all",
355
1
            Self::KeepAll => "keep-all",
356
1
            Self::BreakWord => "break-word",
357
        })
358
4
    }
359
}
360

            
361
// -- StyleOverflowWrap --
362

            
363
/// Controls whether the browser may break at otherwise disallowed points
364
/// to prevent overflow.
365
///
366
/// CSS Text Level 3 §3.3: <https://www.w3.org/TR/css-text-3/#overflow-wrap-property>
367
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
368
#[repr(C)]
369
#[derive(Default)]
370
pub enum StyleOverflowWrap {
371
    /// Lines may only break at allowed break points.
372
    #[default]
373
    Normal,
374
    /// An otherwise unbreakable sequence may be broken at an arbitrary point
375
    /// if there are no otherwise acceptable break points.
376
    Anywhere,
377
    /// Same as `anywhere` but soft wrap opportunities introduced are not
378
    /// considered when calculating min-content intrinsic sizes.
379
    BreakWord,
380
}
381
impl_option!(
382
    StyleOverflowWrap,
383
    OptionStyleOverflowWrap,
384
    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
385
);
386
impl PrintAsCssValue for StyleOverflowWrap {
387
3
    fn print_as_css_value(&self) -> String {
388
3
        String::from(match self {
389
1
            Self::Normal => "normal",
390
1
            Self::Anywhere => "anywhere",
391
1
            Self::BreakWord => "break-word",
392
        })
393
3
    }
394
}
395

            
396
// -- StyleTextAlignLast --
397

            
398
/// Controls alignment of the last line of a block or a line right before
399
/// a forced line break.
400
///
401
/// CSS Text Level 3 §7.2: <https://www.w3.org/TR/css-text-3/#text-align-last-property>
402
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
403
#[repr(C)]
404
#[derive(Default)]
405
pub enum StyleTextAlignLast {
406
    /// Alignment of the last line is determined by text-align (or start if justify).
407
    #[default]
408
    Auto,
409
    /// Align to the start edge of the line box.
410
    Start,
411
    /// Align to the end edge of the line box.
412
    End,
413
    /// Align to the line left.
414
    Left,
415
    /// Align to the line right.
416
    Right,
417
    /// Center the content.
418
    Center,
419
    /// Justify the content.
420
    Justify,
421
}
422
impl_option!(
423
    StyleTextAlignLast,
424
    OptionStyleTextAlignLast,
425
    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
426
);
427
impl PrintAsCssValue for StyleTextAlignLast {
428
7
    fn print_as_css_value(&self) -> String {
429
7
        String::from(match self {
430
1
            Self::Auto => "auto",
431
1
            Self::Start => "start",
432
1
            Self::End => "end",
433
1
            Self::Left => "left",
434
1
            Self::Right => "right",
435
1
            Self::Center => "center",
436
1
            Self::Justify => "justify",
437
        })
438
7
    }
439
}
440

            
441
// -- StyleTextTransform --
442

            
443
/// Controls capitalization of a text run (applied before shaping).
444
///
445
/// CSS Text Level 3 §2.1: <https://www.w3.org/TR/css-text-3/#text-transform-property>
446
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
447
#[repr(C)]
448
#[derive(Default)]
449
pub enum StyleTextTransform {
450
    /// No capitalization effect.
451
    #[default]
452
    None,
453
    /// Uppercase the first typographic letter unit of each word.
454
    Capitalize,
455
    /// Uppercase every typographic letter unit.
456
    Uppercase,
457
    /// Lowercase every typographic letter unit.
458
    Lowercase,
459
    /// Map to the full-width form where available.
460
    FullWidth,
461
}
462
impl_option!(
463
    StyleTextTransform,
464
    OptionStyleTextTransform,
465
    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
466
);
467
impl PrintAsCssValue for StyleTextTransform {
468
5
    fn print_as_css_value(&self) -> String {
469
5
        String::from(match self {
470
1
            Self::None => "none",
471
1
            Self::Capitalize => "capitalize",
472
1
            Self::Uppercase => "uppercase",
473
1
            Self::Lowercase => "lowercase",
474
1
            Self::FullWidth => "full-width",
475
        })
476
5
    }
477
}
478

            
479
// -- StyleDirection --
480

            
481
/// Text direction.
482
// +spec:writing-modes:46fed3 - direction property provides explicit bidi controls in CSS
483
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
484
#[repr(C)]
485
#[derive(Default)]
486
pub enum StyleDirection {
487
    /// Left-to-right text direction
488
    #[default]
489
    Ltr,
490
    /// Right-to-left text direction
491
    Rtl,
492
}
493
impl_option!(
494
    StyleDirection,
495
    OptionStyleDirection,
496
    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
497
);
498
impl PrintAsCssValue for StyleDirection {
499
2
    fn print_as_css_value(&self) -> String {
500
2
        String::from(match self {
501
1
            Self::Ltr => "ltr",
502
1
            Self::Rtl => "rtl",
503
        })
504
2
    }
505
}
506

            
507
// -- StyleUserSelect --
508

            
509
/// Controls whether the user can select text.
510
/// Used to prevent accidental text selection on UI controls like buttons.
511
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
512
#[repr(C)]
513
#[derive(Default)]
514
pub enum StyleUserSelect {
515
    /// Browser determines selectability (default)
516
    #[default]
517
    Auto,
518
    /// Text is selectable
519
    Text,
520
    /// Text is not selectable
521
    None,
522
    /// User can select all text with a single action
523
    All,
524
}
525
impl_option!(
526
    StyleUserSelect,
527
    OptionStyleUserSelect,
528
    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
529
);
530
impl PrintAsCssValue for StyleUserSelect {
531
4
    fn print_as_css_value(&self) -> String {
532
4
        String::from(match self {
533
1
            Self::Auto => "auto",
534
1
            Self::Text => "text",
535
1
            Self::None => "none",
536
1
            Self::All => "all",
537
        })
538
4
    }
539
}
540

            
541
// -- StyleTextDecoration --
542

            
543
/// Text decoration (underline, overline, line-through).
544
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
545
#[repr(C)]
546
#[derive(Default)]
547
pub enum StyleTextDecoration {
548
    /// No decoration
549
    #[default]
550
    None,
551
    /// Underline
552
    Underline,
553
    /// Line above text
554
    Overline,
555
    /// Strike-through line
556
    LineThrough,
557
}
558
impl_option!(
559
    StyleTextDecoration,
560
    OptionStyleTextDecoration,
561
    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
562
);
563
impl PrintAsCssValue for StyleTextDecoration {
564
4
    fn print_as_css_value(&self) -> String {
565
4
        String::from(match self {
566
1
            Self::None => "none",
567
1
            Self::Underline => "underline",
568
1
            Self::Overline => "overline",
569
1
            Self::LineThrough => "line-through",
570
        })
571
4
    }
572
}
573

            
574
// -- StyleVerticalAlign --
575

            
576
/// CSS 2.2 §10.8.1 vertical-align property values
577
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
578
#[repr(C, u8)]
579
#[derive(Default)]
580
pub enum StyleVerticalAlign {
581
    /// CSS default - align baselines
582
    #[default]
583
    Baseline,
584
    /// Align top of element with top of line box
585
    Top,
586
    /// Align middle of element with baseline + half x-height
587
    Middle,
588
    /// Align bottom of element with bottom of line box
589
    Bottom,
590
    /// Align baseline with parent's subscript baseline
591
    Sub,
592
    /// Align baseline with parent's superscript baseline
593
    Superscript,
594
    /// Align top with top of parent's font
595
    TextTop,
596
    /// Align bottom with bottom of parent's font
597
    TextBottom,
598
    /// <percentage> refers to line-height of the element itself
599
    Percentage(PercentageValue),
600
    /// <length> offset from baseline
601
    Length(PixelValue),
602
}
603

            
604
impl_option!(
605
    StyleVerticalAlign,
606
    OptionStyleVerticalAlign,
607
    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
608
);
609

            
610
impl PrintAsCssValue for StyleVerticalAlign {
611
12
    fn print_as_css_value(&self) -> String {
612
12
        match self {
613
1
            Self::Baseline => String::from("baseline"),
614
1
            Self::Top => String::from("top"),
615
1
            Self::Middle => String::from("middle"),
616
1
            Self::Bottom => String::from("bottom"),
617
1
            Self::Sub => String::from("sub"),
618
1
            Self::Superscript => String::from("super"),
619
1
            Self::TextTop => String::from("text-top"),
620
1
            Self::TextBottom => String::from("text-bottom"),
621
2
            Self::Percentage(p) => format!("{}%", p.normalized() * 100.0),
622
2
            Self::Length(l) => l.print_as_css_value(),
623
        }
624
12
    }
625
}
626

            
627
impl FormatAsRustCode for StyleVerticalAlign {
628
    fn format_as_rust_code(&self, indent: usize) -> String {
629
        match self {
630
            Self::Baseline => "StyleVerticalAlign::Baseline".to_string(),
631
            Self::Top => "StyleVerticalAlign::Top".to_string(),
632
            Self::Middle => "StyleVerticalAlign::Middle".to_string(),
633
            Self::Bottom => "StyleVerticalAlign::Bottom".to_string(),
634
            Self::Sub => "StyleVerticalAlign::Sub".to_string(),
635
            Self::Superscript => "StyleVerticalAlign::Superscript".to_string(),
636
            Self::TextTop => "StyleVerticalAlign::TextTop".to_string(),
637
            Self::TextBottom => "StyleVerticalAlign::TextBottom".to_string(),
638
            Self::Percentage(p) => format!("StyleVerticalAlign::Percentage(PercentageValue::new({}))", p.normalized() * 100.0),
639
            Self::Length(l) => format!("StyleVerticalAlign::Length({l})"),
640
        }
641
    }
642
}
643

            
644
// --- PARSERS ---
645

            
646
#[cfg(feature = "parser")]
647
use crate::props::basic::{
648
    color::{parse_css_color, CssColorParseError, CssColorParseErrorOwned},
649
    DurationParseError,
650
};
651

            
652
#[cfg(feature = "parser")]
653
#[derive(Clone, PartialEq)]
654
pub enum StyleTextColorParseError<'a> {
655
    ColorParseError(CssColorParseError<'a>),
656
}
657
#[cfg(feature = "parser")]
658
impl_debug_as_display!(StyleTextColorParseError<'a>);
659
#[cfg(feature = "parser")]
660
impl_display! { StyleTextColorParseError<'a>, {
661
    ColorParseError(e) => format!("Invalid color: {}", e),
662
}}
663
#[cfg(feature = "parser")]
664
impl_from!(
665
    CssColorParseError<'a>,
666
    StyleTextColorParseError::ColorParseError
667
);
668

            
669
#[cfg(feature = "parser")]
670
#[derive(Debug, Clone, PartialEq)]
671
#[repr(C, u8)]
672
pub enum StyleTextColorParseErrorOwned {
673
    ColorParseError(CssColorParseErrorOwned),
674
}
675

            
676
#[cfg(feature = "parser")]
677
impl StyleTextColorParseError<'_> {
678
2
    #[must_use] pub fn to_contained(&self) -> StyleTextColorParseErrorOwned {
679
2
        match self {
680
2
            Self::ColorParseError(e) => {
681
2
                StyleTextColorParseErrorOwned::ColorParseError(e.to_contained())
682
            }
683
        }
684
2
    }
685
}
686

            
687
#[cfg(feature = "parser")]
688
impl StyleTextColorParseErrorOwned {
689
2
    #[must_use] pub fn to_shared(&self) -> StyleTextColorParseError<'_> {
690
2
        match self {
691
2
            Self::ColorParseError(e) => StyleTextColorParseError::ColorParseError(e.to_shared()),
692
        }
693
2
    }
694
}
695

            
696
#[cfg(feature = "parser")]
697
/// # Errors
698
///
699
/// Returns an error if `input` is not a valid CSS `text-color` value.
700
63086
pub fn parse_style_text_color(input: &str) -> Result<StyleTextColor, StyleTextColorParseError<'_>> {
701
63086
    parse_css_color(input)
702
63086
        .map(|inner| StyleTextColor { inner })
703
63086
        .map_err(StyleTextColorParseError::ColorParseError)
704
63086
}
705

            
706
#[cfg(feature = "parser")]
707
#[derive(Clone, PartialEq, Eq)]
708
pub enum StyleTextAlignParseError<'a> {
709
    InvalidValue(InvalidValueErr<'a>),
710
}
711
#[cfg(feature = "parser")]
712
impl_debug_as_display!(StyleTextAlignParseError<'a>);
713
#[cfg(feature = "parser")]
714
impl_display! { StyleTextAlignParseError<'a>, {
715
    InvalidValue(e) => format!("Invalid text-align value: \"{}\"", e.0),
716
}}
717
#[cfg(feature = "parser")]
718
impl_from!(InvalidValueErr<'a>, StyleTextAlignParseError::InvalidValue);
719

            
720
#[cfg(feature = "parser")]
721
#[derive(Debug, Clone, PartialEq, Eq)]
722
#[repr(C, u8)]
723
pub enum StyleTextAlignParseErrorOwned {
724
    InvalidValue(InvalidValueErrOwned),
725
}
726

            
727
#[cfg(feature = "parser")]
728
impl StyleTextAlignParseError<'_> {
729
4
    #[must_use] pub fn to_contained(&self) -> StyleTextAlignParseErrorOwned {
730
4
        match self {
731
4
            Self::InvalidValue(e) => StyleTextAlignParseErrorOwned::InvalidValue(e.to_contained()),
732
        }
733
4
    }
734
}
735

            
736
#[cfg(feature = "parser")]
737
impl StyleTextAlignParseErrorOwned {
738
4
    #[must_use] pub fn to_shared(&self) -> StyleTextAlignParseError<'_> {
739
4
        match self {
740
4
            Self::InvalidValue(e) => StyleTextAlignParseError::InvalidValue(e.to_shared()),
741
        }
742
4
    }
743
}
744

            
745
#[cfg(feature = "parser")]
746
/// # Errors
747
///
748
/// Returns an error if `input` is not a valid CSS `text-align` value.
749
142
pub fn parse_style_text_align(input: &str) -> Result<StyleTextAlign, StyleTextAlignParseError<'_>> {
750
142
    match input.trim() {
751
142
        "left" => Ok(StyleTextAlign::Left),
752
120
        "center" => Ok(StyleTextAlign::Center),
753
49
        "right" => Ok(StyleTextAlign::Right),
754
46
        "justify" => Ok(StyleTextAlign::Justify),
755
31
        "start" => Ok(StyleTextAlign::Start),
756
28
        "end" => Ok(StyleTextAlign::End),
757
25
        other => Err(StyleTextAlignParseError::InvalidValue(InvalidValueErr(
758
25
            other,
759
25
        ))),
760
    }
761
142
}
762

            
763
#[cfg(feature = "parser")]
764
#[derive(Clone, PartialEq, Eq)]
765
pub enum StyleLetterSpacingParseError<'a> {
766
    PixelValue(CssPixelValueParseError<'a>),
767
}
768
#[cfg(feature = "parser")]
769
impl_debug_as_display!(StyleLetterSpacingParseError<'a>);
770
#[cfg(feature = "parser")]
771
impl_display! { StyleLetterSpacingParseError<'a>, {
772
    PixelValue(e) => format!("Invalid letter-spacing value: {}", e),
773
}}
774
#[cfg(feature = "parser")]
775
impl_from!(
776
    CssPixelValueParseError<'a>,
777
    StyleLetterSpacingParseError::PixelValue
778
);
779

            
780
#[cfg(feature = "parser")]
781
#[derive(Debug, Clone, PartialEq, Eq)]
782
#[repr(C, u8)]
783
pub enum StyleLetterSpacingParseErrorOwned {
784
    PixelValue(CssPixelValueParseErrorOwned),
785
}
786

            
787
#[cfg(feature = "parser")]
788
impl StyleLetterSpacingParseError<'_> {
789
5
    #[must_use] pub fn to_contained(&self) -> StyleLetterSpacingParseErrorOwned {
790
5
        match self {
791
5
            Self::PixelValue(e) => StyleLetterSpacingParseErrorOwned::PixelValue(e.to_contained()),
792
        }
793
5
    }
794
}
795

            
796
#[cfg(feature = "parser")]
797
impl StyleLetterSpacingParseErrorOwned {
798
5
    #[must_use] pub fn to_shared(&self) -> StyleLetterSpacingParseError<'_> {
799
5
        match self {
800
5
            Self::PixelValue(e) => StyleLetterSpacingParseError::PixelValue(e.to_shared()),
801
        }
802
5
    }
803
}
804

            
805
#[cfg(feature = "parser")]
806
/// # Errors
807
///
808
/// Returns an error if `input` is not a valid CSS `letter-spacing` value.
809
79
pub fn parse_style_letter_spacing(
810
79
    input: &str,
811
79
) -> Result<StyleLetterSpacing, StyleLetterSpacingParseError<'_>> {
812
79
    crate::props::basic::pixel::parse_pixel_value(input)
813
79
        .map(|inner| StyleLetterSpacing { inner })
814
79
        .map_err(StyleLetterSpacingParseError::PixelValue)
815
79
}
816

            
817
// -- StyleTextIndent (text-indent property) --
818

            
819
/// Represents a `text-indent` attribute (indentation of first line in a block).
820
#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
821
#[repr(C)]
822
pub struct StyleTextIndent {
823
    pub inner: PixelValue,
824
    /// `each-line` keyword: indent first line of each block container
825
    /// AND each line after a forced line break (but not after soft wrap).
826
    pub each_line: bool,
827
    /// `hanging` keyword: inverts which lines are affected by the indent.
828
    pub hanging: bool,
829
}
830

            
831
impl fmt::Debug for StyleTextIndent {
832
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
833
        write!(f, "{}", self.print_as_css_value())
834
    }
835
}
836

            
837
impl StyleTextIndent {
838
    #[inline]
839
4
    #[must_use] pub const fn zero() -> Self {
840
4
        Self { inner: PixelValue::zero(), each_line: false, hanging: false }
841
4
    }
842
    #[inline]
843
8
    #[must_use] pub const fn const_px(value: isize) -> Self {
844
8
        Self { inner: PixelValue::const_px(value), each_line: false, hanging: false }
845
8
    }
846
    #[inline]
847
5
    #[must_use] pub const fn const_em(value: isize) -> Self {
848
5
        Self { inner: PixelValue::const_em(value), each_line: false, hanging: false }
849
5
    }
850
    #[inline]
851
5
    #[must_use] pub const fn const_pt(value: isize) -> Self {
852
5
        Self { inner: PixelValue::const_pt(value), each_line: false, hanging: false }
853
5
    }
854
    #[inline]
855
5
    #[must_use] pub const fn const_percent(value: isize) -> Self {
856
5
        Self { inner: PixelValue::const_percent(value), each_line: false, hanging: false }
857
5
    }
858
    #[inline]
859
5
    #[must_use] pub const fn const_in(value: isize) -> Self {
860
5
        Self { inner: PixelValue::const_in(value), each_line: false, hanging: false }
861
5
    }
862
    #[inline]
863
6
    #[must_use] pub const fn const_cm(value: isize) -> Self {
864
6
        Self { inner: PixelValue::const_cm(value), each_line: false, hanging: false }
865
6
    }
866
    #[inline]
867
6
    #[must_use] pub const fn const_mm(value: isize) -> Self {
868
6
        Self { inner: PixelValue::const_mm(value), each_line: false, hanging: false }
869
6
    }
870
    #[inline]
871
3
    #[must_use] pub const fn const_from_metric(metric: crate::props::basic::length::SizeMetric, value: isize) -> Self {
872
3
        Self { inner: PixelValue::const_from_metric(metric, value), each_line: false, hanging: false }
873
3
    }
874
    #[inline]
875
17
    #[must_use] pub fn px(value: f32) -> Self {
876
17
        Self { inner: PixelValue::px(value), each_line: false, hanging: false }
877
17
    }
878
    #[inline]
879
3
    #[must_use] pub fn em(value: f32) -> Self {
880
3
        Self { inner: PixelValue::em(value), each_line: false, hanging: false }
881
3
    }
882
    #[inline]
883
3
    #[must_use] pub fn pt(value: f32) -> Self {
884
3
        Self { inner: PixelValue::pt(value), each_line: false, hanging: false }
885
3
    }
886
    #[inline]
887
2
    #[must_use] pub fn percent(value: f32) -> Self {
888
2
        Self { inner: PixelValue::percent(value), each_line: false, hanging: false }
889
2
    }
890
    #[inline]
891
5
    #[must_use] pub fn from_metric(metric: crate::props::basic::length::SizeMetric, value: f32) -> Self {
892
5
        Self { inner: PixelValue::from_metric(metric, value), each_line: false, hanging: false }
893
5
    }
894
    #[inline]
895
17
    #[must_use] pub fn interpolate(&self, other: &Self, t: f32) -> Self {
896
17
        Self { inner: self.inner.interpolate(&other.inner, t), each_line: self.each_line, hanging: self.hanging }
897
17
    }
898
}
899

            
900
impl PrintAsCssValue for StyleTextIndent {
901
13
    fn print_as_css_value(&self) -> String {
902
13
        let mut s = self.inner.to_string();
903
13
        if self.hanging {
904
6
            s.push_str(" hanging");
905
7
        }
906
13
        if self.each_line {
907
6
            s.push_str(" each-line");
908
7
        }
909
13
        s
910
13
    }
911
}
912

            
913
impl FormatAsRustCode for StyleTextIndent {
914
    fn format_as_rust_code(&self, _tabs: usize) -> String {
915
        format!(
916
            "StyleTextIndent {{ inner: {}, each_line: {}, hanging: {} }}",
917
            self.inner.format_as_rust_code(0), self.each_line, self.hanging
918
        )
919
    }
920
}
921

            
922
#[cfg(feature = "parser")]
923
#[derive(Clone, PartialEq, Eq)]
924
pub enum StyleTextIndentParseError<'a> {
925
    PixelValue(CssPixelValueParseError<'a>),
926
}
927
#[cfg(feature = "parser")]
928
impl_debug_as_display!(StyleTextIndentParseError<'a>);
929
#[cfg(feature = "parser")]
930
impl_display! { StyleTextIndentParseError<'a>, {
931
    PixelValue(e) => format!("Invalid text-indent value: {}", e),
932
}}
933
#[cfg(feature = "parser")]
934
impl_from!(
935
    CssPixelValueParseError<'a>,
936
    StyleTextIndentParseError::PixelValue
937
);
938

            
939
#[cfg(feature = "parser")]
940
#[derive(Debug, Clone, PartialEq, Eq)]
941
#[repr(C, u8)]
942
pub enum StyleTextIndentParseErrorOwned {
943
    PixelValue(CssPixelValueParseErrorOwned),
944
}
945

            
946
#[cfg(feature = "parser")]
947
impl StyleTextIndentParseError<'_> {
948
2
    #[must_use] pub fn to_contained(&self) -> StyleTextIndentParseErrorOwned {
949
2
        match self {
950
2
            Self::PixelValue(e) => StyleTextIndentParseErrorOwned::PixelValue(e.to_contained()),
951
        }
952
2
    }
953
}
954

            
955
#[cfg(feature = "parser")]
956
impl StyleTextIndentParseErrorOwned {
957
2
    #[must_use] pub fn to_shared(&self) -> StyleTextIndentParseError<'_> {
958
2
        match self {
959
2
            Self::PixelValue(e) => StyleTextIndentParseError::PixelValue(e.to_shared()),
960
        }
961
2
    }
962
}
963

            
964
#[cfg(feature = "parser")]
965
/// # Errors
966
///
967
/// Returns an error if `input` is not a valid CSS `text-indent` value.
968
36
pub fn parse_style_text_indent(input: &str) -> Result<StyleTextIndent, StyleTextIndentParseError<'_>> {
969
36
    let mut each_line = false;
970
36
    let mut hanging = false;
971
36
    let mut pixel_part: Option<&str> = None;
972

            
973
50
    for token in input.split_whitespace() {
974
50
        match token {
975
50
            "each-line" => each_line = true,
976
43
            "hanging" => hanging = true,
977
34
            _ => {
978
34
                pixel_part = Some(token);
979
34
            }
980
        }
981
    }
982

            
983
36
    let pixel_str = pixel_part.unwrap_or("0px");
984

            
985
36
    crate::props::basic::pixel::parse_pixel_value(pixel_str)
986
36
        .map(|inner| StyleTextIndent { inner, each_line, hanging })
987
36
        .map_err(StyleTextIndentParseError::PixelValue)
988
36
}
989

            
990
/// initial-letter property for drop caps
991
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
992
#[repr(C)]
993
pub struct StyleInitialLetter {
994
    pub size: u32,
995
    pub sink: crate::corety::OptionU32,
996
}
997

            
998
impl FormatAsRustCode for StyleInitialLetter {
999
    fn format_as_rust_code(&self, _tabs: usize) -> String {
        format!("{self:?}")
    }
}
impl PrintAsCssValue for StyleInitialLetter {
1
    fn print_as_css_value(&self) -> String {
1
        if let crate::corety::OptionU32::Some(sink) = self.sink {
1
            format!("{} {}", self.size, sink)
        } else {
            format!("{}", self.size)
        }
1
    }
}
#[cfg(feature = "parser")]
#[derive(Clone, PartialEq, Eq)]
pub enum StyleInitialLetterParseError<'a> {
    InvalidFormat(&'a str),
    InvalidSize(&'a str),
    InvalidSink(&'a str),
}
#[cfg(feature = "parser")]
impl_debug_as_display!(StyleInitialLetterParseError<'a>);
#[cfg(feature = "parser")]
impl_display! { StyleInitialLetterParseError<'a>, {
    InvalidFormat(e) => format!("Invalid initial-letter format: {}", e),
    InvalidSize(e) => format!("Invalid initial-letter size: {}", e),
    InvalidSink(e) => format!("Invalid initial-letter sink: {}", e),
}}
#[cfg(feature = "parser")]
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(C, u8)]
pub enum StyleInitialLetterParseErrorOwned {
    InvalidFormat(AzString),
    InvalidSize(AzString),
    InvalidSink(AzString),
}
#[cfg(feature = "parser")]
impl StyleInitialLetterParseError<'_> {
4
    #[must_use] pub fn to_contained(&self) -> StyleInitialLetterParseErrorOwned {
4
        match self {
1
            Self::InvalidFormat(s) => {
1
                StyleInitialLetterParseErrorOwned::InvalidFormat((*s).to_string().into())
            }
2
            Self::InvalidSize(s) => StyleInitialLetterParseErrorOwned::InvalidSize((*s).to_string().into()),
1
            Self::InvalidSink(s) => StyleInitialLetterParseErrorOwned::InvalidSink((*s).to_string().into()),
        }
4
    }
}
#[cfg(feature = "parser")]
impl StyleInitialLetterParseErrorOwned {
4
    #[must_use] pub fn to_shared(&self) -> StyleInitialLetterParseError<'_> {
4
        match self {
1
            Self::InvalidFormat(s) => StyleInitialLetterParseError::InvalidFormat(s.as_str()),
2
            Self::InvalidSize(s) => StyleInitialLetterParseError::InvalidSize(s.as_str()),
1
            Self::InvalidSink(s) => StyleInitialLetterParseError::InvalidSink(s.as_str()),
        }
4
    }
}
#[cfg(feature = "parser")]
impl From<StyleInitialLetterParseError<'_>> for StyleInitialLetterParseErrorOwned {
    fn from(e: StyleInitialLetterParseError<'_>) -> Self {
        match e {
            StyleInitialLetterParseError::InvalidFormat(s) => {
                Self::InvalidFormat(s.to_string().into())
            }
            StyleInitialLetterParseError::InvalidSize(s) => {
                Self::InvalidSize(s.to_string().into())
            }
            StyleInitialLetterParseError::InvalidSink(s) => {
                Self::InvalidSink(s.to_string().into())
            }
        }
    }
}
#[cfg(feature = "parser")]
impl_display! { StyleInitialLetterParseErrorOwned, {
    InvalidFormat(e) => format!("Invalid initial-letter format: {}", e),
    InvalidSize(e) => format!("Invalid initial-letter size: {}", e),
    InvalidSink(e) => format!("Invalid initial-letter sink: {}", e),
}}
#[cfg(feature = "parser")]
/// # Errors
///
/// Returns an error if `input` is not a valid CSS `initial-letter` value.
19
pub fn parse_style_initial_letter(
19
    input: &str,
19
) -> Result<StyleInitialLetter, StyleInitialLetterParseError<'_>> {
19
    let input = input.trim();
19
    let parts: Vec<&str> = input.split_whitespace().collect();
19
    if parts.is_empty() {
3
        return Err(StyleInitialLetterParseError::InvalidFormat(input));
16
    }
    // Parse size (required)
16
    let size = parts[0]
16
        .parse::<u32>()
16
        .map_err(|_| StyleInitialLetterParseError::InvalidSize(parts[0]))?;
10
    if size == 0 {
2
        return Err(StyleInitialLetterParseError::InvalidSize(parts[0]));
8
    }
    // Parse sink (optional)
8
    let sink = if parts.len() > 1 {
        crate::corety::OptionU32::Some(
6
            parts[1]
6
                .parse::<u32>()
6
                .map_err(|_| StyleInitialLetterParseError::InvalidSink(parts[1]))?,
        )
    } else {
2
        crate::corety::OptionU32::None
    };
5
    Ok(StyleInitialLetter { size, sink })
19
}
/// line-clamp property for limiting visible lines
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub struct StyleLineClamp {
    pub max_lines: usize,
}
impl FormatAsRustCode for StyleLineClamp {
    fn format_as_rust_code(&self, _tabs: usize) -> String {
        format!("{self:?}")
    }
}
impl PrintAsCssValue for StyleLineClamp {
1
    fn print_as_css_value(&self) -> String {
1
        format!("{}", self.max_lines)
1
    }
}
#[cfg(feature = "parser")]
#[derive(Clone, PartialEq, Eq)]
pub enum StyleLineClampParseError<'a> {
    InvalidValue(&'a str),
    ZeroValue,
}
#[cfg(feature = "parser")]
impl_debug_as_display!(StyleLineClampParseError<'a>);
#[cfg(feature = "parser")]
impl_display! { StyleLineClampParseError<'a>, {
    InvalidValue(e) => format!("Invalid line-clamp value: {}", e),
    ZeroValue => format!("line-clamp cannot be zero"),
}}
#[allow(variant_size_differences)] // repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
#[cfg(feature = "parser")]
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(C, u8)]
pub enum StyleLineClampParseErrorOwned {
    InvalidValue(AzString),
    ZeroValue,
}
#[cfg(feature = "parser")]
impl StyleLineClampParseError<'_> {
4
    #[must_use] pub fn to_contained(&self) -> StyleLineClampParseErrorOwned {
4
        match self {
3
            Self::InvalidValue(s) => StyleLineClampParseErrorOwned::InvalidValue((*s).to_string().into()),
1
            Self::ZeroValue => StyleLineClampParseErrorOwned::ZeroValue,
        }
4
    }
}
#[cfg(feature = "parser")]
impl StyleLineClampParseErrorOwned {
4
    #[must_use] pub fn to_shared(&self) -> StyleLineClampParseError<'_> {
4
        match self {
3
            Self::InvalidValue(s) => StyleLineClampParseError::InvalidValue(s.as_str()),
1
            Self::ZeroValue => StyleLineClampParseError::ZeroValue,
        }
4
    }
}
#[cfg(feature = "parser")]
impl From<StyleLineClampParseError<'_>> for StyleLineClampParseErrorOwned {
    fn from(e: StyleLineClampParseError<'_>) -> Self {
        e.to_contained()
    }
}
#[cfg(feature = "parser")]
impl_display! { StyleLineClampParseErrorOwned, {
    InvalidValue(e) => format!("Invalid line-clamp value: {}", e),
    ZeroValue => format!("line-clamp cannot be zero"),
}}
#[cfg(feature = "parser")]
/// # Errors
///
/// Returns an error if `input` is not a valid CSS `line-clamp` value.
17
pub fn parse_style_line_clamp(
17
    input: &str,
17
) -> Result<StyleLineClamp, StyleLineClampParseError<'_>> {
17
    let input = input.trim();
17
    let max_lines = input
17
        .parse::<usize>()
17
        .map_err(|_| StyleLineClampParseError::InvalidValue(input))?;
6
    if max_lines == 0 {
2
        return Err(StyleLineClampParseError::ZeroValue);
4
    }
4
    Ok(StyleLineClamp { max_lines })
17
}
/// hanging-punctuation property for hanging punctuation marks
///
/// CSS Text 3 §8: `none | [ first || [ force-end | allow-end ] || last ]`
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
#[derive(Default)]
pub struct StyleHangingPunctuation {
    pub first: bool,
    pub force_end: bool,
    pub allow_end: bool,
    pub last: bool,
}
impl StyleHangingPunctuation {
67
    #[must_use] pub const fn is_enabled(&self) -> bool {
67
        self.first || self.force_end || self.allow_end || self.last
67
    }
}
impl FormatAsRustCode for StyleHangingPunctuation {
    fn format_as_rust_code(&self, _tabs: usize) -> String {
        format!("{self:?}")
    }
}
impl PrintAsCssValue for StyleHangingPunctuation {
34
    fn print_as_css_value(&self) -> String {
34
        if !self.is_enabled() {
2
            return "none".to_string();
32
        }
32
        let mut parts = Vec::new();
32
        if self.first { parts.push("first"); }
32
        if self.force_end { parts.push("force-end"); }
32
        if self.allow_end { parts.push("allow-end"); }
32
        if self.last { parts.push("last"); }
32
        parts.join(" ")
34
    }
}
#[cfg(feature = "parser")]
#[derive(Clone, PartialEq, Eq)]
pub enum StyleHangingPunctuationParseError<'a> {
    InvalidValue(&'a str),
}
#[cfg(feature = "parser")]
impl_debug_as_display!(StyleHangingPunctuationParseError<'a>);
#[cfg(feature = "parser")]
impl_display! { StyleHangingPunctuationParseError<'a>, {
    InvalidValue(e) => format!("Invalid hanging-punctuation value: {}", e),
}}
#[cfg(feature = "parser")]
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(C, u8)]
pub enum StyleHangingPunctuationParseErrorOwned {
    InvalidValue(AzString),
}
#[cfg(feature = "parser")]
impl StyleHangingPunctuationParseError<'_> {
3
    #[must_use] pub fn to_contained(&self) -> StyleHangingPunctuationParseErrorOwned {
3
        match self {
3
            Self::InvalidValue(s) => {
3
                StyleHangingPunctuationParseErrorOwned::InvalidValue((*s).to_string().into())
            }
        }
3
    }
}
#[cfg(feature = "parser")]
impl StyleHangingPunctuationParseErrorOwned {
3
    #[must_use] pub fn to_shared(&self) -> StyleHangingPunctuationParseError<'_> {
3
        match self {
3
            Self::InvalidValue(s) => StyleHangingPunctuationParseError::InvalidValue(s.as_str()),
        }
3
    }
}
#[cfg(feature = "parser")]
impl From<StyleHangingPunctuationParseError<'_>> for StyleHangingPunctuationParseErrorOwned {
    fn from(e: StyleHangingPunctuationParseError<'_>) -> Self {
        e.to_contained()
    }
}
#[cfg(feature = "parser")]
impl_display! { StyleHangingPunctuationParseErrorOwned, {
    InvalidValue(e) => format!("Invalid hanging-punctuation value: {}", e),
}}
#[cfg(feature = "parser")]
/// # Errors
///
/// Returns an error if `input` is not a valid CSS `hanging-punctuation` value.
27
pub fn parse_style_hanging_punctuation(
27
    input: &str,
27
) -> Result<StyleHangingPunctuation, StyleHangingPunctuationParseError<'_>> {
27
    let input = input.trim();
27
    if input.eq_ignore_ascii_case("none") {
1
        return Ok(StyleHangingPunctuation::default());
26
    }
26
    let mut first = false;
26
    let mut force_end = false;
26
    let mut allow_end = false;
26
    let mut last = false;
44
    for token in input.split_whitespace() {
44
        match token.to_lowercase().as_str() {
44
            "first" => first = true,
32
            "force-end" => force_end = true,
23
            "allow-end" => allow_end = true,
14
            "last" => last = true,
6
            _ => return Err(StyleHangingPunctuationParseError::InvalidValue(input)),
        }
    }
20
    if force_end && allow_end {
5
        return Err(StyleHangingPunctuationParseError::InvalidValue(input));
15
    }
15
    Ok(StyleHangingPunctuation { first, force_end, allow_end, last })
27
}
/// text-combine-upright property for combining horizontal text in vertical layout
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C, u8)]
#[derive(Default)]
pub enum StyleTextCombineUpright {
    #[default]
    None,
    All,
    Digits(u8),
}
impl FormatAsRustCode for StyleTextCombineUpright {
    fn format_as_rust_code(&self, _tabs: usize) -> String {
        format!("{self:?}")
    }
}
impl PrintAsCssValue for StyleTextCombineUpright {
3
    fn print_as_css_value(&self) -> String {
3
        match self {
            Self::None => "none".to_string(),
            Self::All => "all".to_string(),
3
            Self::Digits(n) => format!("digits {n}"),
        }
3
    }
}
#[cfg(feature = "parser")]
#[derive(Clone, PartialEq, Eq)]
pub enum StyleTextCombineUprightParseError<'a> {
    InvalidValue(&'a str),
    InvalidDigits(&'a str),
}
#[cfg(feature = "parser")]
impl_debug_as_display!(StyleTextCombineUprightParseError<'a>);
#[cfg(feature = "parser")]
impl_display! { StyleTextCombineUprightParseError<'a>, {
    InvalidValue(e) => format!("Invalid text-combine-upright value: {}", e),
    InvalidDigits(e) => format!("Invalid text-combine-upright digits: {}", e),
}}
#[cfg(feature = "parser")]
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(C, u8)]
pub enum StyleTextCombineUprightParseErrorOwned {
    InvalidValue(AzString),
    InvalidDigits(AzString),
}
#[cfg(feature = "parser")]
impl StyleTextCombineUprightParseError<'_> {
2
    #[must_use] pub fn to_contained(&self) -> StyleTextCombineUprightParseErrorOwned {
2
        match self {
1
            Self::InvalidValue(s) => {
1
                StyleTextCombineUprightParseErrorOwned::InvalidValue((*s).to_string().into())
            }
1
            Self::InvalidDigits(s) => {
1
                StyleTextCombineUprightParseErrorOwned::InvalidDigits((*s).to_string().into())
            }
        }
2
    }
}
#[cfg(feature = "parser")]
impl StyleTextCombineUprightParseErrorOwned {
2
    #[must_use] pub fn to_shared(&self) -> StyleTextCombineUprightParseError<'_> {
2
        match self {
1
            Self::InvalidValue(s) => StyleTextCombineUprightParseError::InvalidValue(s.as_str()),
1
            Self::InvalidDigits(s) => StyleTextCombineUprightParseError::InvalidDigits(s.as_str()),
        }
2
    }
}
#[cfg(feature = "parser")]
impl From<StyleTextCombineUprightParseError<'_>> for StyleTextCombineUprightParseErrorOwned {
    fn from(e: StyleTextCombineUprightParseError<'_>) -> Self {
        e.to_contained()
    }
}
#[cfg(feature = "parser")]
impl_display! { StyleTextCombineUprightParseErrorOwned, {
    InvalidValue(e) => format!("Invalid text-combine-upright value: {}", e),
    InvalidDigits(e) => format!("Invalid text-combine-upright digits: {}", e),
}}
#[cfg(feature = "parser")]
/// # Errors
///
/// Returns an error if `input` is not a valid CSS `text-combine-upright` value.
19
pub fn parse_style_text_combine_upright(
19
    input: &str,
19
) -> Result<StyleTextCombineUpright, StyleTextCombineUprightParseError<'_>> {
19
    let trimmed = input.trim();
19
    if trimmed.eq_ignore_ascii_case("none") {
2
        Ok(StyleTextCombineUpright::None)
17
    } else if trimmed.eq_ignore_ascii_case("all") {
1
        Ok(StyleTextCombineUpright::All)
16
    } else if trimmed.starts_with("digits") {
13
        let parts: Vec<&str> = trimmed.split_whitespace().collect();
13
        if parts.len() == 2 {
10
            let n = parts[1]
10
                .parse::<u8>()
10
                .map_err(|_| StyleTextCombineUprightParseError::InvalidDigits(input))?;
8
            if (2..=4).contains(&n) {
3
                Ok(StyleTextCombineUpright::Digits(n))
            } else {
5
                Err(StyleTextCombineUprightParseError::InvalidDigits(input))
            }
        } else {
            // Default to "digits 2"
3
            Ok(StyleTextCombineUpright::Digits(2))
        }
    } else {
3
        Err(StyleTextCombineUprightParseError::InvalidValue(input))
    }
19
}
#[cfg(feature = "parser")]
#[derive(Clone, PartialEq, Eq)]
pub enum StyleWordSpacingParseError<'a> {
    PixelValue(CssPixelValueParseError<'a>),
}
#[cfg(feature = "parser")]
impl_debug_as_display!(StyleWordSpacingParseError<'a>);
#[cfg(feature = "parser")]
impl_display! { StyleWordSpacingParseError<'a>, {
    PixelValue(e) => format!("Invalid word-spacing value: {}", e),
}}
#[cfg(feature = "parser")]
impl_from!(
    CssPixelValueParseError<'a>,
    StyleWordSpacingParseError::PixelValue
);
#[cfg(feature = "parser")]
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(C, u8)]
pub enum StyleWordSpacingParseErrorOwned {
    PixelValue(CssPixelValueParseErrorOwned),
}
#[cfg(feature = "parser")]
impl StyleWordSpacingParseError<'_> {
4
    #[must_use] pub fn to_contained(&self) -> StyleWordSpacingParseErrorOwned {
4
        match self {
4
            Self::PixelValue(e) => StyleWordSpacingParseErrorOwned::PixelValue(e.to_contained()),
        }
4
    }
}
#[cfg(feature = "parser")]
impl StyleWordSpacingParseErrorOwned {
4
    #[must_use] pub fn to_shared(&self) -> StyleWordSpacingParseError<'_> {
4
        match self {
4
            Self::PixelValue(e) => StyleWordSpacingParseError::PixelValue(e.to_shared()),
        }
4
    }
}
#[cfg(feature = "parser")]
/// # Errors
///
/// Returns an error if `input` is not a valid CSS `word-spacing` value.
34
pub fn parse_style_word_spacing(
34
    input: &str,
34
) -> Result<StyleWordSpacing, StyleWordSpacingParseError<'_>> {
34
    crate::props::basic::pixel::parse_pixel_value(input)
34
        .map(|inner| StyleWordSpacing { inner })
34
        .map_err(StyleWordSpacingParseError::PixelValue)
34
}
#[cfg(feature = "parser")]
#[derive(Clone, PartialEq, Eq)]
#[repr(C, u8)]
pub enum StyleLineHeightParseError {
    Percentage(PercentageParseError),
}
#[cfg(feature = "parser")]
impl_debug_as_display!(StyleLineHeightParseError);
#[cfg(feature = "parser")]
impl_display! { StyleLineHeightParseError, {
    Percentage(e) => format!("Invalid line-height value: {}", e),
}}
#[cfg(feature = "parser")]
impl_from!(PercentageParseError, StyleLineHeightParseError::Percentage);
#[cfg(feature = "parser")]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StyleLineHeightParseErrorOwned {
    Percentage(PercentageParseErrorOwned),
}
#[cfg(feature = "parser")]
impl StyleLineHeightParseError {
3
    #[must_use] pub fn to_contained(&self) -> StyleLineHeightParseErrorOwned {
3
        match self {
3
            Self::Percentage(e) => StyleLineHeightParseErrorOwned::Percentage(e.to_contained()),
        }
3
    }
}
#[cfg(feature = "parser")]
impl StyleLineHeightParseErrorOwned {
3
    #[must_use] pub fn to_shared(&self) -> StyleLineHeightParseError {
3
        match self {
3
            Self::Percentage(e) => StyleLineHeightParseError::Percentage(e.to_shared()),
        }
3
    }
}
#[cfg(feature = "parser")]
/// # Errors
///
/// Returns an error if `input` is not a valid CSS `line-height` value.
936
pub fn parse_style_line_height(input: &str) -> Result<StyleLineHeight, StyleLineHeightParseError> {
    // Try <number> or <percentage> first (multiplier of font-size)
936
    if let Ok(inner) = crate::props::basic::length::parse_percentage_value(input) {
712
        return Ok(StyleLineHeight { inner });
224
    }
    // Try <length> (e.g., "50px") — store as NEGATIVE PercentageValue to signal absolute px.
    // Convention: negative normalized() = absolute pixel value (CSS line-height can't be negative).
    // Resolved at layout time in fc.rs where font_size is known.
224
    if let Ok(px) = crate::props::basic::pixel::parse_pixel_value(input) {
213
        if px.metric == crate::props::basic::length::SizeMetric::Px {
209
            let px_val = px.number.get();
209
            return Ok(StyleLineHeight {
209
                inner: PercentageValue::new(-px_val * 100.0),
209
            });
4
        }
11
    }
15
    Err(StyleLineHeightParseError::Percentage(
15
        PercentageParseError::InvalidUnit(String::new().into()),
15
    ))
936
}
#[cfg(feature = "parser")]
#[derive(Clone, PartialEq, Eq)]
pub enum StyleTabSizeParseError<'a> {
    PixelValue(CssPixelValueParseError<'a>),
}
#[cfg(feature = "parser")]
impl_debug_as_display!(StyleTabSizeParseError<'a>);
#[cfg(feature = "parser")]
impl_display! { StyleTabSizeParseError<'a>, {
    PixelValue(e) => format!("Invalid tab-size value: {}", e),
}}
#[cfg(feature = "parser")]
impl_from!(
    CssPixelValueParseError<'a>,
    StyleTabSizeParseError::PixelValue
);
#[cfg(feature = "parser")]
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(C, u8)]
pub enum StyleTabSizeParseErrorOwned {
    PixelValue(CssPixelValueParseErrorOwned),
}
#[cfg(feature = "parser")]
impl StyleTabSizeParseError<'_> {
3
    #[must_use] pub fn to_contained(&self) -> StyleTabSizeParseErrorOwned {
3
        match self {
3
            Self::PixelValue(e) => StyleTabSizeParseErrorOwned::PixelValue(e.to_contained()),
        }
3
    }
}
#[cfg(feature = "parser")]
impl StyleTabSizeParseErrorOwned {
3
    #[must_use] pub fn to_shared(&self) -> StyleTabSizeParseError<'_> {
3
        match self {
3
            Self::PixelValue(e) => StyleTabSizeParseError::PixelValue(e.to_shared()),
        }
3
    }
}
#[cfg(feature = "parser")]
/// # Errors
///
/// Returns an error if `input` is not a valid CSS `tab-size` value.
11
pub fn parse_style_tab_size(input: &str) -> Result<StyleTabSize, StyleTabSizeParseError<'_>> {
11
    input.trim().parse::<f32>().map_or_else(
7
        |_| {
7
            crate::props::basic::pixel::parse_pixel_value(input)
7
                .map(|v| StyleTabSize { inner: v })
7
                .map_err(StyleTabSizeParseError::PixelValue)
7
        },
4
        |number| {
4
            Ok(StyleTabSize {
4
                inner: PixelValue::em(number),
4
            })
4
        },
    )
11
}
#[cfg(feature = "parser")]
#[derive(Clone, PartialEq, Eq)]
pub enum StyleWhiteSpaceParseError<'a> {
    InvalidValue(InvalidValueErr<'a>),
}
#[cfg(feature = "parser")]
impl_debug_as_display!(StyleWhiteSpaceParseError<'a>);
#[cfg(feature = "parser")]
impl_display! { StyleWhiteSpaceParseError<'a>, {
    InvalidValue(e) => format!("Invalid white-space value: \"{}\"", e.0),
}}
#[cfg(feature = "parser")]
impl_from!(InvalidValueErr<'a>, StyleWhiteSpaceParseError::InvalidValue);
#[cfg(feature = "parser")]
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(C, u8)]
pub enum StyleWhiteSpaceParseErrorOwned {
    InvalidValue(InvalidValueErrOwned),
}
#[cfg(feature = "parser")]
impl StyleWhiteSpaceParseError<'_> {
2
    #[must_use] pub fn to_contained(&self) -> StyleWhiteSpaceParseErrorOwned {
2
        match self {
2
            Self::InvalidValue(e) => StyleWhiteSpaceParseErrorOwned::InvalidValue(e.to_contained()),
        }
2
    }
}
#[cfg(feature = "parser")]
impl StyleWhiteSpaceParseErrorOwned {
2
    #[must_use] pub fn to_shared(&self) -> StyleWhiteSpaceParseError<'_> {
2
        match self {
2
            Self::InvalidValue(e) => StyleWhiteSpaceParseError::InvalidValue(e.to_shared()),
        }
2
    }
}
#[cfg(feature = "parser")]
/// # Errors
///
/// Returns an error if `input` is not a valid CSS `white-space` value.
460
pub fn parse_style_white_space(input: &str) -> Result<StyleWhiteSpace, StyleWhiteSpaceParseError<'_>> {
460
    match input.trim() {
460
        "normal" => Ok(StyleWhiteSpace::Normal),
361
        "pre" => Ok(StyleWhiteSpace::Pre),
214
        "nowrap" | "no-wrap" => Ok(StyleWhiteSpace::Nowrap),
135
        "pre-wrap" => Ok(StyleWhiteSpace::PreWrap),
96
        "pre-line" => Ok(StyleWhiteSpace::PreLine),
34
        "break-spaces" => Ok(StyleWhiteSpace::BreakSpaces),
20
        other => Err(StyleWhiteSpaceParseError::InvalidValue(InvalidValueErr(
20
            other,
20
        ))),
    }
460
}
#[cfg(feature = "parser")]
#[derive(Clone, PartialEq, Eq)]
pub enum StyleHyphensParseError<'a> {
    InvalidValue(InvalidValueErr<'a>),
}
#[cfg(feature = "parser")]
impl_debug_as_display!(StyleHyphensParseError<'a>);
#[cfg(feature = "parser")]
impl_display! { StyleHyphensParseError<'a>, {
    InvalidValue(e) => format!("Invalid hyphens value: \"{}\"", e.0),
}}
#[cfg(feature = "parser")]
impl_from!(InvalidValueErr<'a>, StyleHyphensParseError::InvalidValue);
#[cfg(feature = "parser")]
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(C, u8)]
pub enum StyleHyphensParseErrorOwned {
    InvalidValue(InvalidValueErrOwned),
}
#[cfg(feature = "parser")]
impl StyleHyphensParseError<'_> {
2
    #[must_use] pub fn to_contained(&self) -> StyleHyphensParseErrorOwned {
2
        match self {
2
            Self::InvalidValue(e) => StyleHyphensParseErrorOwned::InvalidValue(e.to_contained()),
        }
2
    }
}
#[cfg(feature = "parser")]
impl StyleHyphensParseErrorOwned {
2
    #[must_use] pub fn to_shared(&self) -> StyleHyphensParseError<'_> {
2
        match self {
2
            Self::InvalidValue(e) => StyleHyphensParseError::InvalidValue(e.to_shared()),
        }
2
    }
}
#[cfg(feature = "parser")]
/// # Errors
///
/// Returns an error if `input` is not a valid CSS `hyphens` value.
24
pub fn parse_style_hyphens(input: &str) -> Result<StyleHyphens, StyleHyphensParseError<'_>> {
24
    match input.trim() {
24
        "none" => Ok(StyleHyphens::None),
22
        "manual" => Ok(StyleHyphens::Manual),
20
        "auto" => Ok(StyleHyphens::Auto),
18
        other => Err(StyleHyphensParseError::InvalidValue(InvalidValueErr(other))),
    }
24
}
// -- StyleLineBreak parse --
#[cfg(feature = "parser")]
#[derive(Clone, PartialEq, Eq)]
pub enum StyleLineBreakParseError<'a> {
    InvalidValue(InvalidValueErr<'a>),
}
#[cfg(feature = "parser")]
impl_debug_as_display!(StyleLineBreakParseError<'a>);
#[cfg(feature = "parser")]
impl_display! { StyleLineBreakParseError<'a>, {
    InvalidValue(e) => format!("Invalid line-break value: \"{}\"", e.0),
}}
#[cfg(feature = "parser")]
impl_from!(InvalidValueErr<'a>, StyleLineBreakParseError::InvalidValue);
#[cfg(feature = "parser")]
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(C, u8)]
pub enum StyleLineBreakParseErrorOwned {
    InvalidValue(InvalidValueErrOwned),
}
#[cfg(feature = "parser")]
impl StyleLineBreakParseError<'_> {
2
    #[must_use] pub fn to_contained(&self) -> StyleLineBreakParseErrorOwned {
2
        match self {
2
            Self::InvalidValue(e) => StyleLineBreakParseErrorOwned::InvalidValue(e.to_contained()),
        }
2
    }
}
#[cfg(feature = "parser")]
impl StyleLineBreakParseErrorOwned {
2
    #[must_use] pub fn to_shared(&self) -> StyleLineBreakParseError<'_> {
2
        match self {
2
            Self::InvalidValue(e) => StyleLineBreakParseError::InvalidValue(e.to_shared()),
        }
2
    }
}
#[cfg(feature = "parser")]
/// # Errors
///
/// Returns an error if `input` is not a valid CSS `line-break` value.
28
pub fn parse_style_line_break(input: &str) -> Result<StyleLineBreak, StyleLineBreakParseError<'_>> {
28
    match input.trim() {
28
        "auto" => Ok(StyleLineBreak::Auto),
26
        "loose" => Ok(StyleLineBreak::Loose),
24
        "normal" => Ok(StyleLineBreak::Normal),
22
        "strict" => Ok(StyleLineBreak::Strict),
20
        "anywhere" => Ok(StyleLineBreak::Anywhere),
18
        other => Err(StyleLineBreakParseError::InvalidValue(InvalidValueErr(other))),
    }
28
}
// -- StyleWordBreak parse --
#[cfg(feature = "parser")]
#[derive(Clone, PartialEq, Eq)]
pub enum StyleWordBreakParseError<'a> {
    InvalidValue(InvalidValueErr<'a>),
}
#[cfg(feature = "parser")]
impl_debug_as_display!(StyleWordBreakParseError<'a>);
#[cfg(feature = "parser")]
impl_display! { StyleWordBreakParseError<'a>, {
    InvalidValue(e) => format!("Invalid word-break value: \"{}\"", e.0),
}}
#[cfg(feature = "parser")]
impl_from!(InvalidValueErr<'a>, StyleWordBreakParseError::InvalidValue);
#[cfg(feature = "parser")]
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(C, u8)]
pub enum StyleWordBreakParseErrorOwned {
    InvalidValue(InvalidValueErrOwned),
}
#[cfg(feature = "parser")]
impl StyleWordBreakParseError<'_> {
2
    #[must_use] pub fn to_contained(&self) -> StyleWordBreakParseErrorOwned {
2
        match self {
2
            Self::InvalidValue(e) => StyleWordBreakParseErrorOwned::InvalidValue(e.to_contained()),
        }
2
    }
}
#[cfg(feature = "parser")]
impl StyleWordBreakParseErrorOwned {
2
    #[must_use] pub fn to_shared(&self) -> StyleWordBreakParseError<'_> {
2
        match self {
2
            Self::InvalidValue(e) => StyleWordBreakParseError::InvalidValue(e.to_shared()),
        }
2
    }
}
#[cfg(feature = "parser")]
/// # Errors
///
/// Returns an error if `input` is not a valid CSS `word-break` value.
26
pub fn parse_style_word_break(input: &str) -> Result<StyleWordBreak, StyleWordBreakParseError<'_>> {
26
    match input.trim() {
26
        "normal" => Ok(StyleWordBreak::Normal),
24
        "break-all" => Ok(StyleWordBreak::BreakAll),
22
        "keep-all" => Ok(StyleWordBreak::KeepAll),
20
        "break-word" => Ok(StyleWordBreak::BreakWord),
18
        other => Err(StyleWordBreakParseError::InvalidValue(InvalidValueErr(other))),
    }
26
}
// -- StyleOverflowWrap parse --
#[cfg(feature = "parser")]
#[derive(Clone, PartialEq, Eq)]
pub enum StyleOverflowWrapParseError<'a> {
    InvalidValue(InvalidValueErr<'a>),
}
#[cfg(feature = "parser")]
impl_debug_as_display!(StyleOverflowWrapParseError<'a>);
#[cfg(feature = "parser")]
impl_display! { StyleOverflowWrapParseError<'a>, {
    InvalidValue(e) => format!("Invalid overflow-wrap value: \"{}\"", e.0),
}}
#[cfg(feature = "parser")]
impl_from!(InvalidValueErr<'a>, StyleOverflowWrapParseError::InvalidValue);
#[cfg(feature = "parser")]
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(C, u8)]
pub enum StyleOverflowWrapParseErrorOwned {
    InvalidValue(InvalidValueErrOwned),
}
#[cfg(feature = "parser")]
impl StyleOverflowWrapParseError<'_> {
2
    #[must_use] pub fn to_contained(&self) -> StyleOverflowWrapParseErrorOwned {
2
        match self {
2
            Self::InvalidValue(e) => StyleOverflowWrapParseErrorOwned::InvalidValue(e.to_contained()),
        }
2
    }
}
#[cfg(feature = "parser")]
impl StyleOverflowWrapParseErrorOwned {
2
    #[must_use] pub fn to_shared(&self) -> StyleOverflowWrapParseError<'_> {
2
        match self {
2
            Self::InvalidValue(e) => StyleOverflowWrapParseError::InvalidValue(e.to_shared()),
        }
2
    }
}
#[cfg(feature = "parser")]
/// # Errors
///
/// Returns an error if `input` is not a valid CSS `overflow-wrap` value.
36
pub fn parse_style_overflow_wrap(input: &str) -> Result<StyleOverflowWrap, StyleOverflowWrapParseError<'_>> {
36
    match input.trim() {
36
        "normal" => Ok(StyleOverflowWrap::Normal),
34
        "anywhere" => Ok(StyleOverflowWrap::Anywhere),
32
        "break-word" => Ok(StyleOverflowWrap::BreakWord),
18
        other => Err(StyleOverflowWrapParseError::InvalidValue(InvalidValueErr(other))),
    }
36
}
// -- StyleTextAlignLast parse --
#[cfg(feature = "parser")]
#[derive(Clone, PartialEq, Eq)]
pub enum StyleTextAlignLastParseError<'a> {
    InvalidValue(InvalidValueErr<'a>),
}
#[cfg(feature = "parser")]
impl_debug_as_display!(StyleTextAlignLastParseError<'a>);
#[cfg(feature = "parser")]
impl_display! { StyleTextAlignLastParseError<'a>, {
    InvalidValue(e) => format!("Invalid text-align-last value: \"{}\"", e.0),
}}
#[cfg(feature = "parser")]
impl_from!(InvalidValueErr<'a>, StyleTextAlignLastParseError::InvalidValue);
#[cfg(feature = "parser")]
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(C, u8)]
pub enum StyleTextAlignLastParseErrorOwned {
    InvalidValue(InvalidValueErrOwned),
}
#[cfg(feature = "parser")]
impl StyleTextAlignLastParseError<'_> {
2
    #[must_use] pub fn to_contained(&self) -> StyleTextAlignLastParseErrorOwned {
2
        match self {
2
            Self::InvalidValue(e) => StyleTextAlignLastParseErrorOwned::InvalidValue(e.to_contained()),
        }
2
    }
}
#[cfg(feature = "parser")]
impl StyleTextAlignLastParseErrorOwned {
2
    #[must_use] pub fn to_shared(&self) -> StyleTextAlignLastParseError<'_> {
2
        match self {
2
            Self::InvalidValue(e) => StyleTextAlignLastParseError::InvalidValue(e.to_shared()),
        }
2
    }
}
#[cfg(feature = "parser")]
/// # Errors
///
/// Returns an error if `input` is not a valid CSS `text-align-last` value.
32
pub fn parse_style_text_align_last(input: &str) -> Result<StyleTextAlignLast, StyleTextAlignLastParseError<'_>> {
32
    match input.trim() {
32
        "auto" => Ok(StyleTextAlignLast::Auto),
30
        "start" => Ok(StyleTextAlignLast::Start),
28
        "end" => Ok(StyleTextAlignLast::End),
26
        "left" => Ok(StyleTextAlignLast::Left),
24
        "right" => Ok(StyleTextAlignLast::Right),
22
        "center" => Ok(StyleTextAlignLast::Center),
20
        "justify" => Ok(StyleTextAlignLast::Justify),
18
        other => Err(StyleTextAlignLastParseError::InvalidValue(InvalidValueErr(other))),
    }
32
}
// -- StyleTextTransform parse --
#[cfg(feature = "parser")]
#[derive(Clone, PartialEq, Eq)]
pub enum StyleTextTransformParseError<'a> {
    InvalidValue(InvalidValueErr<'a>),
}
#[cfg(feature = "parser")]
impl_debug_as_display!(StyleTextTransformParseError<'a>);
#[cfg(feature = "parser")]
impl_display! { StyleTextTransformParseError<'a>, {
    InvalidValue(e) => format!("Invalid text-transform value: \"{}\"", e.0),
}}
#[cfg(feature = "parser")]
impl_from!(InvalidValueErr<'a>, StyleTextTransformParseError::InvalidValue);
#[cfg(feature = "parser")]
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(C, u8)]
pub enum StyleTextTransformParseErrorOwned {
    InvalidValue(InvalidValueErrOwned),
}
#[cfg(feature = "parser")]
impl StyleTextTransformParseError<'_> {
2
    #[must_use] pub fn to_contained(&self) -> StyleTextTransformParseErrorOwned {
2
        match self {
2
            Self::InvalidValue(e) => StyleTextTransformParseErrorOwned::InvalidValue(e.to_contained()),
        }
2
    }
}
#[cfg(feature = "parser")]
impl StyleTextTransformParseErrorOwned {
2
    #[must_use] pub fn to_shared(&self) -> StyleTextTransformParseError<'_> {
2
        match self {
2
            Self::InvalidValue(e) => StyleTextTransformParseError::InvalidValue(e.to_shared()),
        }
2
    }
}
#[cfg(feature = "parser")]
/// # Errors
///
/// Returns an error if `input` is not a valid CSS `text-transform` value.
52
pub fn parse_style_text_transform(input: &str) -> Result<StyleTextTransform, StyleTextTransformParseError<'_>> {
52
    match input.trim() {
52
        "none" => Ok(StyleTextTransform::None),
50
        "capitalize" => Ok(StyleTextTransform::Capitalize),
48
        "uppercase" => Ok(StyleTextTransform::Uppercase),
34
        "lowercase" => Ok(StyleTextTransform::Lowercase),
20
        "full-width" => Ok(StyleTextTransform::FullWidth),
18
        other => Err(StyleTextTransformParseError::InvalidValue(InvalidValueErr(other))),
    }
52
}
#[cfg(feature = "parser")]
#[derive(Clone, PartialEq, Eq)]
pub enum StyleDirectionParseError<'a> {
    InvalidValue(InvalidValueErr<'a>),
}
#[cfg(feature = "parser")]
impl_debug_as_display!(StyleDirectionParseError<'a>);
#[cfg(feature = "parser")]
impl_display! { StyleDirectionParseError<'a>, {
    InvalidValue(e) => format!("Invalid direction value: \"{}\"", e.0),
}}
#[cfg(feature = "parser")]
impl_from!(InvalidValueErr<'a>, StyleDirectionParseError::InvalidValue);
#[cfg(feature = "parser")]
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(C, u8)]
pub enum StyleDirectionParseErrorOwned {
    InvalidValue(InvalidValueErrOwned),
}
#[cfg(feature = "parser")]
impl StyleDirectionParseError<'_> {
2
    #[must_use] pub fn to_contained(&self) -> StyleDirectionParseErrorOwned {
2
        match self {
2
            Self::InvalidValue(e) => StyleDirectionParseErrorOwned::InvalidValue(e.to_contained()),
        }
2
    }
}
#[cfg(feature = "parser")]
impl StyleDirectionParseErrorOwned {
2
    #[must_use] pub fn to_shared(&self) -> StyleDirectionParseError<'_> {
2
        match self {
2
            Self::InvalidValue(e) => StyleDirectionParseError::InvalidValue(e.to_shared()),
        }
2
    }
}
#[cfg(feature = "parser")]
/// # Errors
///
/// Returns an error if `input` is not a valid CSS `direction` value.
59
pub fn parse_style_direction(input: &str) -> Result<StyleDirection, StyleDirectionParseError<'_>> {
59
    match input.trim() {
59
        "ltr" => Ok(StyleDirection::Ltr),
57
        "rtl" => Ok(StyleDirection::Rtl),
19
        other => Err(StyleDirectionParseError::InvalidValue(InvalidValueErr(
19
            other,
19
        ))),
    }
59
}
#[cfg(feature = "parser")]
#[derive(Clone, PartialEq, Eq)]
pub enum StyleUserSelectParseError<'a> {
    InvalidValue(InvalidValueErr<'a>),
}
#[cfg(feature = "parser")]
impl_debug_as_display!(StyleUserSelectParseError<'a>);
#[cfg(feature = "parser")]
impl_display! { StyleUserSelectParseError<'a>, {
    InvalidValue(e) => format!("Invalid user-select value: \"{}\"", e.0),
}}
#[cfg(feature = "parser")]
impl_from!(InvalidValueErr<'a>, StyleUserSelectParseError::InvalidValue);
#[cfg(feature = "parser")]
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(C, u8)]
pub enum StyleUserSelectParseErrorOwned {
    InvalidValue(InvalidValueErrOwned),
}
#[cfg(feature = "parser")]
impl StyleUserSelectParseError<'_> {
2
    #[must_use] pub fn to_contained(&self) -> StyleUserSelectParseErrorOwned {
2
        match self {
2
            Self::InvalidValue(e) => StyleUserSelectParseErrorOwned::InvalidValue(e.to_contained()),
        }
2
    }
}
#[cfg(feature = "parser")]
impl StyleUserSelectParseErrorOwned {
2
    #[must_use] pub fn to_shared(&self) -> StyleUserSelectParseError<'_> {
2
        match self {
2
            Self::InvalidValue(e) => StyleUserSelectParseError::InvalidValue(e.to_shared()),
        }
2
    }
}
#[cfg(feature = "parser")]
/// # Errors
///
/// Returns an error if `input` is not a valid CSS `user-select` value.
146
pub fn parse_style_user_select(input: &str) -> Result<StyleUserSelect, StyleUserSelectParseError<'_>> {
146
    match input.trim() {
146
        "auto" => Ok(StyleUserSelect::Auto),
144
        "text" => Ok(StyleUserSelect::Text),
130
        "none" => Ok(StyleUserSelect::None),
20
        "all" => Ok(StyleUserSelect::All),
18
        other => Err(StyleUserSelectParseError::InvalidValue(InvalidValueErr(
18
            other,
18
        ))),
    }
146
}
#[cfg(feature = "parser")]
#[derive(Clone, PartialEq, Eq)]
pub enum StyleTextDecorationParseError<'a> {
    InvalidValue(InvalidValueErr<'a>),
}
#[cfg(feature = "parser")]
impl_debug_as_display!(StyleTextDecorationParseError<'a>);
#[cfg(feature = "parser")]
impl_display! { StyleTextDecorationParseError<'a>, {
    InvalidValue(e) => format!("Invalid text-decoration value: \"{}\"", e.0),
}}
#[cfg(feature = "parser")]
impl_from!(
    InvalidValueErr<'a>,
    StyleTextDecorationParseError::InvalidValue
);
#[cfg(feature = "parser")]
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(C, u8)]
pub enum StyleTextDecorationParseErrorOwned {
    InvalidValue(InvalidValueErrOwned),
}
#[cfg(feature = "parser")]
impl StyleTextDecorationParseError<'_> {
2
    #[must_use] pub fn to_contained(&self) -> StyleTextDecorationParseErrorOwned {
2
        match self {
2
            Self::InvalidValue(e) => {
2
                StyleTextDecorationParseErrorOwned::InvalidValue(e.to_contained())
            }
        }
2
    }
}
#[cfg(feature = "parser")]
impl StyleTextDecorationParseErrorOwned {
2
    #[must_use] pub fn to_shared(&self) -> StyleTextDecorationParseError<'_> {
2
        match self {
2
            Self::InvalidValue(e) => StyleTextDecorationParseError::InvalidValue(e.to_shared()),
        }
2
    }
}
#[cfg(feature = "parser")]
/// # Errors
///
/// Returns an error if `input` is not a valid CSS `text-decoration` value.
26
pub fn parse_style_text_decoration(
26
    input: &str,
26
) -> Result<StyleTextDecoration, StyleTextDecorationParseError<'_>> {
26
    match input.trim() {
26
        "none" => Ok(StyleTextDecoration::None),
24
        "underline" => Ok(StyleTextDecoration::Underline),
22
        "overline" => Ok(StyleTextDecoration::Overline),
20
        "line-through" => Ok(StyleTextDecoration::LineThrough),
18
        other => Err(StyleTextDecorationParseError::InvalidValue(
18
            InvalidValueErr(other),
18
        )),
    }
26
}
#[cfg(feature = "parser")]
#[derive(Clone, PartialEq, Eq)]
pub enum StyleVerticalAlignParseError<'a> {
    InvalidValue(InvalidValueErr<'a>),
}
#[cfg(feature = "parser")]
impl_debug_as_display!(StyleVerticalAlignParseError<'a>);
#[cfg(feature = "parser")]
impl_display! { StyleVerticalAlignParseError<'a>, {
    InvalidValue(e) => format!("Invalid vertical-align value: \"{}\"", e.0),
}}
#[cfg(feature = "parser")]
impl_from!(
    InvalidValueErr<'a>,
    StyleVerticalAlignParseError::InvalidValue
);
#[cfg(feature = "parser")]
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(C, u8)]
pub enum StyleVerticalAlignParseErrorOwned {
    InvalidValue(InvalidValueErrOwned),
}
#[cfg(feature = "parser")]
impl StyleVerticalAlignParseError<'_> {
2
    #[must_use] pub fn to_contained(&self) -> StyleVerticalAlignParseErrorOwned {
2
        match self {
2
            Self::InvalidValue(e) => {
2
                StyleVerticalAlignParseErrorOwned::InvalidValue(e.to_contained())
            }
        }
2
    }
}
#[cfg(feature = "parser")]
impl StyleVerticalAlignParseErrorOwned {
2
    #[must_use] pub fn to_shared(&self) -> StyleVerticalAlignParseError<'_> {
2
        match self {
2
            Self::InvalidValue(e) => StyleVerticalAlignParseError::InvalidValue(e.to_shared()),
        }
2
    }
}
#[cfg(feature = "parser")]
/// # Errors
///
/// Returns an error if `input` is not a valid CSS `vertical-align` value.
43
pub fn parse_style_vertical_align(
43
    input: &str,
43
) -> Result<StyleVerticalAlign, StyleVerticalAlignParseError<'_>> {
43
    match input.trim() {
43
        "baseline" => Ok(StyleVerticalAlign::Baseline),
42
        "top" => Ok(StyleVerticalAlign::Top),
41
        "middle" => Ok(StyleVerticalAlign::Middle),
40
        "bottom" => Ok(StyleVerticalAlign::Bottom),
39
        "sub" => Ok(StyleVerticalAlign::Sub),
26
        "super" => Ok(StyleVerticalAlign::Superscript),
13
        "text-top" => Ok(StyleVerticalAlign::TextTop),
12
        "text-bottom" => Ok(StyleVerticalAlign::TextBottom),
11
        other if other.ends_with('%') => {
4
            let num_str = other.trim_end_matches('%').trim();
4
            num_str.parse::<f32>().map_or_else(
2
                |_| Err(StyleVerticalAlignParseError::InvalidValue(InvalidValueErr(other))),
2
                |val| Ok(StyleVerticalAlign::Percentage(PercentageValue::new(val))),
            )
        }
7
        other => crate::props::basic::pixel::parse_pixel_value(other).map_or_else(
5
            |_| Err(StyleVerticalAlignParseError::InvalidValue(InvalidValueErr(other))),
2
            |pv| Ok(StyleVerticalAlign::Length(pv)),
        ),
    }
43
}
// --- CaretColor ---
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub struct CaretColor {
    pub inner: ColorU,
}
impl Default for CaretColor {
    fn default() -> Self {
        Self {
            inner: ColorU::BLACK,
        }
    }
}
impl PrintAsCssValue for CaretColor {
    fn print_as_css_value(&self) -> String {
        self.inner.to_hash()
    }
}
impl FormatAsRustCode for CaretColor {
    fn format_as_rust_code(&self, _tabs: usize) -> String {
        format!(
            "CaretColor {{ inner: {} }}",
            crate::codegen::format::format_color_value(&self.inner)
        )
    }
}
#[cfg(feature = "parser")]
/// # Errors
///
/// Returns an error if `input` is not a valid CSS `caret-color` value.
3
pub fn parse_caret_color(input: &str) -> Result<CaretColor, CssColorParseError<'_>> {
3
    parse_css_color(input).map(|inner| CaretColor { inner })
3
}
// --- CaretAnimationDuration ---
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub struct CaretAnimationDuration {
    pub inner: CssDuration,
}
impl Default for CaretAnimationDuration {
    fn default() -> Self {
        Self {
            inner: CssDuration::from_millis(500),
        } // Default 500ms blink time
    }
}
impl PrintAsCssValue for CaretAnimationDuration {
    fn print_as_css_value(&self) -> String {
        self.inner.print_as_css_value()
    }
}
impl FormatAsRustCode for CaretAnimationDuration {
    fn format_as_rust_code(&self, _tabs: usize) -> String {
        format!(
            "CaretAnimationDuration {{ inner: {} }}",
            self.inner.format_as_rust_code(0)
        )
    }
}
#[cfg(feature = "parser")]
/// # Errors
///
/// Returns an error if `input` is not a valid CSS `caret-animation-duration` value.
98
pub fn parse_caret_animation_duration(
98
    input: &str,
98
) -> Result<CaretAnimationDuration, DurationParseError<'_>> {
    use crate::props::basic::parse_duration;
98
    parse_duration(input).map(|inner| CaretAnimationDuration { inner })
98
}
// --- CaretWidth ---
/// Width of the text cursor (caret) in pixels.
/// CSS doesn't have a standard property for this, so we use `-azul-caret-width`.
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub struct CaretWidth {
    pub inner: PixelValue,
}
impl Default for CaretWidth {
    fn default() -> Self {
        Self {
            inner: PixelValue::px(2.0), // Default 2px caret width
        }
    }
}
impl PrintAsCssValue for CaretWidth {
    fn print_as_css_value(&self) -> String {
        self.inner.print_as_css_value()
    }
}
impl FormatAsRustCode for CaretWidth {
    fn format_as_rust_code(&self, _tabs: usize) -> String {
        format!(
            "CaretWidth {{ inner: {} }}",
            self.inner.format_as_rust_code(0)
        )
    }
}
#[cfg(feature = "parser")]
/// # Errors
///
/// Returns an error if `input` is not a valid CSS `caret-width` value.
2
pub fn parse_caret_width(input: &str) -> Result<CaretWidth, CssPixelValueParseError<'_>> {
    use crate::props::basic::pixel::parse_pixel_value;
2
    parse_pixel_value(input).map(|inner| CaretWidth { inner })
2
}
// --- From implementations for CssProperty ---
impl From<StyleUserSelect> for crate::props::property::CssProperty {
120
    fn from(value: StyleUserSelect) -> Self {
        use crate::props::property::CssProperty;
120
        Self::user_select(value)
120
    }
}
impl From<StyleTextDecoration> for crate::props::property::CssProperty {
    fn from(value: StyleTextDecoration) -> Self {
        use crate::props::property::CssProperty;
        Self::text_decoration(value)
    }
}
#[cfg(all(test, feature = "parser"))]
mod tests {
    use super::*;
    use crate::props::basic::{color::ColorU, length::PercentageValue, pixel::PixelValue};
    #[test]
1
    fn test_parse_style_text_color() {
1
        assert_eq!(
1
            parse_style_text_color("red").unwrap().inner,
1
            ColorU::new_rgb(255, 0, 0)
        );
1
        assert_eq!(
1
            parse_style_text_color("#aabbcc").unwrap().inner,
1
            ColorU::new_rgb(170, 187, 204)
        );
1
        assert!(parse_style_text_color("not-a-color").is_err());
1
    }
    #[test]
1
    fn test_parse_style_text_align() {
1
        assert_eq!(
1
            parse_style_text_align("left").unwrap(),
            StyleTextAlign::Left
        );
1
        assert_eq!(
1
            parse_style_text_align("center").unwrap(),
            StyleTextAlign::Center
        );
1
        assert_eq!(
1
            parse_style_text_align("right").unwrap(),
            StyleTextAlign::Right
        );
1
        assert_eq!(
1
            parse_style_text_align("justify").unwrap(),
            StyleTextAlign::Justify
        );
1
        assert_eq!(
1
            parse_style_text_align("start").unwrap(),
            StyleTextAlign::Start
        );
1
        assert_eq!(parse_style_text_align("end").unwrap(), StyleTextAlign::End);
1
        assert!(parse_style_text_align("middle").is_err());
1
    }
    #[test]
1
    fn test_parse_spacing() {
1
        assert_eq!(
1
            parse_style_letter_spacing("2px").unwrap().inner,
1
            PixelValue::px(2.0)
        );
1
        assert_eq!(
1
            parse_style_letter_spacing("-0.1em").unwrap().inner,
1
            PixelValue::em(-0.1)
        );
1
        assert_eq!(
1
            parse_style_word_spacing("0.5em").unwrap().inner,
1
            PixelValue::em(0.5)
        );
1
    }
    #[test]
1
    fn test_parse_line_height() {
1
        assert_eq!(
1
            parse_style_line_height("1.5").unwrap().inner,
1
            PercentageValue::new(150.0)
        );
1
        assert_eq!(
1
            parse_style_line_height("120%").unwrap().inner,
1
            PercentageValue::new(120.0)
        );
        // px values stored as negative PercentageValue (convention: negative = absolute px)
1
        assert_eq!(
1
            parse_style_line_height("20px").unwrap().inner,
1
            PercentageValue::new(-20.0 * 100.0)
        );
1
    }
    #[test]
1
    fn test_parse_tab_size() {
        // Unitless number is treated as `em`
1
        assert_eq!(
1
            parse_style_tab_size("4").unwrap().inner,
1
            PixelValue::em(4.0)
        );
1
        assert_eq!(
1
            parse_style_tab_size("20px").unwrap().inner,
1
            PixelValue::px(20.0)
        );
1
    }
    #[test]
1
    fn test_parse_white_space() {
1
        assert_eq!(
1
            parse_style_white_space("normal").unwrap(),
            StyleWhiteSpace::Normal
        );
1
        assert_eq!(
1
            parse_style_white_space("pre").unwrap(),
            StyleWhiteSpace::Pre
        );
1
        assert_eq!(
1
            parse_style_white_space("nowrap").unwrap(),
            StyleWhiteSpace::Nowrap
        );
1
        assert_eq!(
1
            parse_style_white_space("pre-wrap").unwrap(),
            StyleWhiteSpace::PreWrap
        );
1
    }
}
// -- StyleUnicodeBidi --
/// Represents the `unicode-bidi` CSS property.
///
/// Controls how bidirectional text is handled within an element.
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
#[derive(Default)]
pub enum StyleUnicodeBidi {
    /// No additional level of embedding
    #[default]
    Normal,
    /// Open an additional level of embedding
    Embed,
    /// Isolate the element from surrounding bidirectional text
    Isolate,
    /// Override the bidirectional algorithm for inline content
    BidiOverride,
    /// Combine isolation and override
    IsolateOverride,
    /// Determine paragraph direction from content without bidi algorithm
    Plaintext,
}
impl_option!(
    StyleUnicodeBidi,
    OptionStyleUnicodeBidi,
    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
);
impl PrintAsCssValue for StyleUnicodeBidi {
6
    fn print_as_css_value(&self) -> String {
6
        String::from(match self {
1
            Self::Normal => "normal",
1
            Self::Embed => "embed",
1
            Self::Isolate => "isolate",
1
            Self::BidiOverride => "bidi-override",
1
            Self::IsolateOverride => "isolate-override",
1
            Self::Plaintext => "plaintext",
        })
6
    }
}
#[cfg(feature = "parser")]
#[derive(Clone, PartialEq, Eq)]
pub enum StyleUnicodeBidiParseError<'a> {
    InvalidValue(InvalidValueErr<'a>),
}
#[cfg(feature = "parser")]
impl_debug_as_display!(StyleUnicodeBidiParseError<'a>);
#[cfg(feature = "parser")]
impl_display! { StyleUnicodeBidiParseError<'a>, {
    InvalidValue(e) => format!("Invalid unicode-bidi value: \"{}\"", e.0),
}}
#[cfg(feature = "parser")]
impl_from!(InvalidValueErr<'a>, StyleUnicodeBidiParseError::InvalidValue);
#[cfg(feature = "parser")]
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(C, u8)]
pub enum StyleUnicodeBidiParseErrorOwned {
    InvalidValue(InvalidValueErrOwned),
}
#[cfg(feature = "parser")]
impl StyleUnicodeBidiParseError<'_> {
2
    #[must_use] pub fn to_contained(&self) -> StyleUnicodeBidiParseErrorOwned {
2
        match self {
2
            Self::InvalidValue(e) => StyleUnicodeBidiParseErrorOwned::InvalidValue(e.to_contained()),
        }
2
    }
}
#[cfg(feature = "parser")]
impl StyleUnicodeBidiParseErrorOwned {
2
    #[must_use] pub fn to_shared(&self) -> StyleUnicodeBidiParseError<'_> {
2
        match self {
2
            Self::InvalidValue(e) => StyleUnicodeBidiParseError::InvalidValue(e.to_shared()),
        }
2
    }
}
#[cfg(feature = "parser")]
/// # Errors
///
/// Returns an error if `input` is not a valid CSS `unicode-bidi` value.
30
pub fn parse_style_unicode_bidi(input: &str) -> Result<StyleUnicodeBidi, StyleUnicodeBidiParseError<'_>> {
30
    match input.trim() {
30
        "normal" => Ok(StyleUnicodeBidi::Normal),
28
        "embed" => Ok(StyleUnicodeBidi::Embed),
26
        "isolate" => Ok(StyleUnicodeBidi::Isolate),
24
        "bidi-override" => Ok(StyleUnicodeBidi::BidiOverride),
22
        "isolate-override" => Ok(StyleUnicodeBidi::IsolateOverride),
20
        "plaintext" => Ok(StyleUnicodeBidi::Plaintext),
18
        other => Err(StyleUnicodeBidiParseError::InvalidValue(InvalidValueErr(other))),
    }
30
}
// -- StyleTextBoxTrim --
/// Represents the `text-box-trim` CSS property.
///
/// Controls whether the leading is trimmed at the start/end of a block container.
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
#[derive(Default)]
pub enum StyleTextBoxTrim {
    /// No trimming
    #[default]
    None,
    /// Trim leading over the first formatted line
    TrimStart,
    /// Trim leading under the last formatted line
    TrimEnd,
    /// Trim both start and end
    TrimBoth,
}
impl_option!(
    StyleTextBoxTrim,
    OptionStyleTextBoxTrim,
    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
);
impl PrintAsCssValue for StyleTextBoxTrim {
4
    fn print_as_css_value(&self) -> String {
4
        String::from(match self {
1
            Self::None => "none",
1
            Self::TrimStart => "trim-start",
1
            Self::TrimEnd => "trim-end",
1
            Self::TrimBoth => "trim-both",
        })
4
    }
}
#[cfg(feature = "parser")]
#[derive(Clone, PartialEq, Eq)]
pub enum StyleTextBoxTrimParseError<'a> {
    InvalidValue(InvalidValueErr<'a>),
}
#[cfg(feature = "parser")]
impl_debug_as_display!(StyleTextBoxTrimParseError<'a>);
#[cfg(feature = "parser")]
impl_display! { StyleTextBoxTrimParseError<'a>, {
    InvalidValue(e) => format!("Invalid text-box-trim value: \"{}\"", e.0),
}}
#[cfg(feature = "parser")]
impl_from!(InvalidValueErr<'a>, StyleTextBoxTrimParseError::InvalidValue);
#[cfg(feature = "parser")]
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(C, u8)]
pub enum StyleTextBoxTrimParseErrorOwned {
    InvalidValue(InvalidValueErrOwned),
}
#[cfg(feature = "parser")]
impl StyleTextBoxTrimParseError<'_> {
2
    #[must_use] pub fn to_contained(&self) -> StyleTextBoxTrimParseErrorOwned {
2
        match self {
2
            Self::InvalidValue(e) => StyleTextBoxTrimParseErrorOwned::InvalidValue(e.to_contained()),
        }
2
    }
}
#[cfg(feature = "parser")]
impl StyleTextBoxTrimParseErrorOwned {
2
    #[must_use] pub fn to_shared(&self) -> StyleTextBoxTrimParseError<'_> {
2
        match self {
2
            Self::InvalidValue(e) => StyleTextBoxTrimParseError::InvalidValue(e.to_shared()),
        }
2
    }
}
#[cfg(feature = "parser")]
/// # Errors
///
/// Returns an error if `input` is not a valid CSS `text-box-trim` value.
86
pub fn parse_style_text_box_trim(input: &str) -> Result<StyleTextBoxTrim, StyleTextBoxTrimParseError<'_>> {
86
    match input.trim() {
86
        "none" => Ok(StyleTextBoxTrim::None),
84
        "trim-start" => Ok(StyleTextBoxTrim::TrimStart),
82
        "trim-end" => Ok(StyleTextBoxTrim::TrimEnd),
80
        "trim-both" => Ok(StyleTextBoxTrim::TrimBoth),
54
        other => Err(StyleTextBoxTrimParseError::InvalidValue(InvalidValueErr(other))),
    }
86
}
// -- StyleTextBoxEdge --
/// The OVER edge metric of `text-box-edge` (first value).
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
#[repr(C)]
pub enum TextBoxEdgeOver {
    // +spec:line-height:cc03df - Auto uses line-fit-edge value, interpreting leading (initial) as text
    /// Use the line-fit-edge value (initial: text). `auto` is single-value
    /// only: it cannot be paired with an under keyword.
    #[default]
    Auto,
    /// Use the text-over baseline
    Text,
    /// Use the cap-height baseline
    Cap,
    /// Use the x-height baseline
    Ex,
    /// Use the ideographic-over baseline
    Ideographic,
    /// Use the ideographic-ink-over baseline
    IdeographicInk,
}
/// The UNDER edge metric of `text-box-edge` (second value).
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
#[repr(C)]
pub enum TextBoxEdgeUnder {
    /// Follows the over edge's `auto` (single-value form only).
    #[default]
    Auto,
    /// Use the text-under baseline
    Text,
    /// Use the alphabetic baseline
    Alphabetic,
    /// Use the ideographic-under baseline
    Ideographic,
    /// Use the ideographic-ink-under baseline
    IdeographicInk,
}
/// Represents the `text-box-edge` CSS property.
///
/// Specifies the metrics used for determining the over/under edges of text
/// for the purposes of `text-box-trim`.
// +spec:writing-modes:daad86 - first value = over edge, second = under edge; single value applies to both (else "text" assumed for missing)
///
/// Grammar: `auto | [ text | cap | ex | ideographic | ideographic-ink ]
/// [ text | alphabetic | ideographic | ideographic-ink ]?`. With one value,
/// both edges take that keyword when it exists on both axes (`text`,
/// `ideographic`, `ideographic-ink`); otherwise (`cap`, `ex`) the under edge
/// is assumed `text`.
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
#[repr(C)]
pub struct StyleTextBoxEdge {
    pub over: TextBoxEdgeOver,
    pub under: TextBoxEdgeUnder,
}
impl StyleTextBoxEdge {
    /// The initial value: `auto` (over and under follow line-fit-edge).
    pub const AUTO: Self = Self {
        over: TextBoxEdgeOver::Auto,
        under: TextBoxEdgeUnder::Auto,
    };
}
impl_option!(
    StyleTextBoxEdge,
    OptionStyleTextBoxEdge,
    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
);
impl PrintAsCssValue for StyleTextBoxEdge {
8
    fn print_as_css_value(&self) -> String {
8
        let over = match self.over {
1
            TextBoxEdgeOver::Auto => return String::from("auto"),
2
            TextBoxEdgeOver::Text => "text",
2
            TextBoxEdgeOver::Cap => "cap",
1
            TextBoxEdgeOver::Ex => "ex",
1
            TextBoxEdgeOver::Ideographic => "ideographic",
1
            TextBoxEdgeOver::IdeographicInk => "ideographic-ink",
        };
7
        let under = match self.under {
3
            TextBoxEdgeUnder::Auto | TextBoxEdgeUnder::Text => "text",
1
            TextBoxEdgeUnder::Alphabetic => "alphabetic",
2
            TextBoxEdgeUnder::Ideographic => "ideographic",
1
            TextBoxEdgeUnder::IdeographicInk => "ideographic-ink",
        };
        // Serialize the shortest form: omit the under edge when the single-
        // value form round-trips to the same pair.
7
        let single_round_trips = match self.over {
2
            TextBoxEdgeOver::Text => matches!(self.under, TextBoxEdgeUnder::Text | TextBoxEdgeUnder::Auto),
            TextBoxEdgeOver::Cap | TextBoxEdgeOver::Ex => {
3
                matches!(self.under, TextBoxEdgeUnder::Text | TextBoxEdgeUnder::Auto)
            }
1
            TextBoxEdgeOver::Ideographic => matches!(self.under, TextBoxEdgeUnder::Ideographic),
            TextBoxEdgeOver::IdeographicInk => {
1
                matches!(self.under, TextBoxEdgeUnder::IdeographicInk)
            }
            TextBoxEdgeOver::Auto => unreachable!(),
        };
7
        if single_round_trips {
5
            String::from(over)
        } else {
2
            alloc::format!("{over} {under}")
        }
8
    }
}
#[cfg(feature = "parser")]
#[derive(Clone, PartialEq, Eq)]
pub enum StyleTextBoxEdgeParseError<'a> {
    InvalidValue(InvalidValueErr<'a>),
}
#[cfg(feature = "parser")]
impl_debug_as_display!(StyleTextBoxEdgeParseError<'a>);
#[cfg(feature = "parser")]
impl_display! { StyleTextBoxEdgeParseError<'a>, {
    InvalidValue(e) => format!("Invalid text-box-edge value: \"{}\"", e.0),
}}
#[cfg(feature = "parser")]
impl_from!(InvalidValueErr<'a>, StyleTextBoxEdgeParseError::InvalidValue);
#[cfg(feature = "parser")]
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(C, u8)]
pub enum StyleTextBoxEdgeParseErrorOwned {
    InvalidValue(InvalidValueErrOwned),
}
#[cfg(feature = "parser")]
impl StyleTextBoxEdgeParseError<'_> {
2
    #[must_use] pub fn to_contained(&self) -> StyleTextBoxEdgeParseErrorOwned {
2
        match self {
2
            Self::InvalidValue(e) => StyleTextBoxEdgeParseErrorOwned::InvalidValue(e.to_contained()),
        }
2
    }
}
#[cfg(feature = "parser")]
impl StyleTextBoxEdgeParseErrorOwned {
2
    #[must_use] pub fn to_shared(&self) -> StyleTextBoxEdgeParseError<'_> {
2
        match self {
2
            Self::InvalidValue(e) => StyleTextBoxEdgeParseError::InvalidValue(e.to_shared()),
        }
2
    }
}
#[cfg(feature = "parser")]
/// # Errors
///
/// Returns an error if `input` is not a valid CSS `text-box-edge` value.
46
pub fn parse_style_text_box_edge(input: &str) -> Result<StyleTextBoxEdge, StyleTextBoxEdgeParseError<'_>> {
46
    let trimmed = input.trim();
46
    let mut parts = trimmed.split_whitespace();
46
    let first = parts.next().unwrap_or("");
46
    let second = parts.next();
46
    if parts.next().is_some() {
        return Err(StyleTextBoxEdgeParseError::InvalidValue(InvalidValueErr(input)));
46
    }
46
    let over = match first {
46
        "auto" => {
            // `auto` is single-value only.
3
            if second.is_some() {
1
                return Err(StyleTextBoxEdgeParseError::InvalidValue(InvalidValueErr(input)));
2
            }
2
            return Ok(StyleTextBoxEdge::AUTO);
        }
43
        "text" => TextBoxEdgeOver::Text,
27
        "cap" => TextBoxEdgeOver::Cap,
10
        "ex" => TextBoxEdgeOver::Ex,
8
        "ideographic" => TextBoxEdgeOver::Ideographic,
6
        "ideographic-ink" => TextBoxEdgeOver::IdeographicInk,
4
        other => return Err(StyleTextBoxEdgeParseError::InvalidValue(InvalidValueErr(other))),
    };
39
    let under = match second {
17
        Some("text") => TextBoxEdgeUnder::Text,
17
        Some("alphabetic") => TextBoxEdgeUnder::Alphabetic,
3
        Some("ideographic") => TextBoxEdgeUnder::Ideographic,
1
        Some("ideographic-ink") => TextBoxEdgeUnder::IdeographicInk,
1
        Some(other) => {
1
            return Err(StyleTextBoxEdgeParseError::InvalidValue(InvalidValueErr(other)))
        }
        // Single-value form: both edges take the keyword when it exists on
        // both axes; otherwise `text` is assumed for the missing under edge.
22
        None => match over {
2
            TextBoxEdgeOver::Ideographic => TextBoxEdgeUnder::Ideographic,
2
            TextBoxEdgeOver::IdeographicInk => TextBoxEdgeUnder::IdeographicInk,
18
            _ => TextBoxEdgeUnder::Text,
        },
    };
38
    Ok(StyleTextBoxEdge { over, under })
46
}
// -- StyleDominantBaseline --
/// Represents the `dominant-baseline` CSS property.
///
/// Specifies the dominant baseline used to align inline-level contents.
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
#[derive(Default)]
pub enum StyleDominantBaseline {
    /// Use the dominant baseline of the parent
    #[default]
    Auto,
    /// Use the text-under baseline
    TextBottom,
    /// Use the alphabetic baseline
    Alphabetic,
    /// Use the ideographic baseline
    Ideographic,
    /// Use the middle baseline
    Middle,
    /// Use the central baseline
    Central,
    /// Use the mathematical baseline
    Mathematical,
    /// Use the hanging baseline
    Hanging,
    /// Use the text-over baseline
    TextTop,
}
impl_option!(
    StyleDominantBaseline,
    OptionStyleDominantBaseline,
    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
);
impl PrintAsCssValue for StyleDominantBaseline {
9
    fn print_as_css_value(&self) -> String {
9
        String::from(match self {
1
            Self::Auto => "auto",
1
            Self::TextBottom => "text-bottom",
1
            Self::Alphabetic => "alphabetic",
1
            Self::Ideographic => "ideographic",
1
            Self::Middle => "middle",
1
            Self::Central => "central",
1
            Self::Mathematical => "mathematical",
1
            Self::Hanging => "hanging",
1
            Self::TextTop => "text-top",
        })
9
    }
}
#[cfg(feature = "parser")]
#[derive(Clone, PartialEq, Eq)]
pub enum StyleDominantBaselineParseError<'a> {
    InvalidValue(InvalidValueErr<'a>),
}
#[cfg(feature = "parser")]
impl_debug_as_display!(StyleDominantBaselineParseError<'a>);
#[cfg(feature = "parser")]
impl_display! { StyleDominantBaselineParseError<'a>, {
    InvalidValue(e) => format!("Invalid dominant-baseline value: \"{}\"", e.0),
}}
#[cfg(feature = "parser")]
impl_from!(InvalidValueErr<'a>, StyleDominantBaselineParseError::InvalidValue);
#[cfg(feature = "parser")]
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(C, u8)]
pub enum StyleDominantBaselineParseErrorOwned {
    InvalidValue(InvalidValueErrOwned),
}
#[cfg(feature = "parser")]
impl StyleDominantBaselineParseError<'_> {
2
    #[must_use] pub fn to_contained(&self) -> StyleDominantBaselineParseErrorOwned {
2
        match self {
2
            Self::InvalidValue(e) => StyleDominantBaselineParseErrorOwned::InvalidValue(e.to_contained()),
        }
2
    }
}
#[cfg(feature = "parser")]
impl StyleDominantBaselineParseErrorOwned {
2
    #[must_use] pub fn to_shared(&self) -> StyleDominantBaselineParseError<'_> {
2
        match self {
2
            Self::InvalidValue(e) => StyleDominantBaselineParseError::InvalidValue(e.to_shared()),
        }
2
    }
}
#[cfg(feature = "parser")]
/// # Errors
///
/// Returns an error if `input` is not a valid CSS `dominant-baseline` value.
36
pub fn parse_style_dominant_baseline(input: &str) -> Result<StyleDominantBaseline, StyleDominantBaselineParseError<'_>> {
36
    match input.trim() {
36
        "auto" => Ok(StyleDominantBaseline::Auto),
34
        "text-bottom" => Ok(StyleDominantBaseline::TextBottom),
32
        "alphabetic" => Ok(StyleDominantBaseline::Alphabetic),
30
        "ideographic" => Ok(StyleDominantBaseline::Ideographic),
28
        "middle" => Ok(StyleDominantBaseline::Middle),
26
        "central" => Ok(StyleDominantBaseline::Central),
24
        "mathematical" => Ok(StyleDominantBaseline::Mathematical),
22
        "hanging" => Ok(StyleDominantBaseline::Hanging),
20
        "text-top" => Ok(StyleDominantBaseline::TextTop),
18
        other => Err(StyleDominantBaselineParseError::InvalidValue(InvalidValueErr(other))),
    }
36
}
// -- StyleAlignmentBaseline --
// +spec:display-property:c90924 - alignment-baseline property: values, initial value, and applies-to per CSS Inline 3 §4.2.2
// +spec:font-metrics:fa4489 - alignment-baseline property: specifies box's alignment baseline used before post-alignment shift
// +spec:inline-block:939f05 - alignment-baseline property definition with all spec values (baseline, text-bottom, alphabetic, ideographic, middle, central, mathematical, text-top)
/// Represents the `alignment-baseline` CSS property.
///
/// Specifies which baseline of the element is aligned with the dominant baseline.
// +spec:writing-modes:cc8e70 - alignment-baseline values for inline baseline alignment
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
#[derive(Default)]
pub enum StyleAlignmentBaseline {
    /// Use the dominant baseline of the parent
    #[default]
    Baseline,
    /// Align to the text-under baseline
    TextBottom,
    /// Align to the alphabetic baseline
    Alphabetic,
    /// Align to the ideographic baseline
    Ideographic,
    /// Align to the middle baseline
    Middle,
    /// Align to the central baseline
    Central,
    /// Align to the mathematical baseline
    Mathematical,
    /// Align to the text-over baseline
    TextTop,
}
impl_option!(
    StyleAlignmentBaseline,
    OptionStyleAlignmentBaseline,
    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
);
impl PrintAsCssValue for StyleAlignmentBaseline {
8
    fn print_as_css_value(&self) -> String {
8
        String::from(match self {
1
            Self::Baseline => "baseline",
1
            Self::TextBottom => "text-bottom",
1
            Self::Alphabetic => "alphabetic",
1
            Self::Ideographic => "ideographic",
1
            Self::Middle => "middle",
1
            Self::Central => "central",
1
            Self::Mathematical => "mathematical",
1
            Self::TextTop => "text-top",
        })
8
    }
}
#[cfg(feature = "parser")]
#[derive(Clone, PartialEq, Eq)]
pub enum StyleAlignmentBaselineParseError<'a> {
    InvalidValue(InvalidValueErr<'a>),
}
#[cfg(feature = "parser")]
impl_debug_as_display!(StyleAlignmentBaselineParseError<'a>);
#[cfg(feature = "parser")]
impl_display! { StyleAlignmentBaselineParseError<'a>, {
    InvalidValue(e) => format!("Invalid alignment-baseline value: \"{}\"", e.0),
}}
#[cfg(feature = "parser")]
impl_from!(InvalidValueErr<'a>, StyleAlignmentBaselineParseError::InvalidValue);
#[cfg(feature = "parser")]
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(C, u8)]
pub enum StyleAlignmentBaselineParseErrorOwned {
    InvalidValue(InvalidValueErrOwned),
}
#[cfg(feature = "parser")]
impl StyleAlignmentBaselineParseError<'_> {
2
    #[must_use] pub fn to_contained(&self) -> StyleAlignmentBaselineParseErrorOwned {
2
        match self {
2
            Self::InvalidValue(e) => StyleAlignmentBaselineParseErrorOwned::InvalidValue(e.to_contained()),
        }
2
    }
}
#[cfg(feature = "parser")]
impl StyleAlignmentBaselineParseErrorOwned {
2
    #[must_use] pub fn to_shared(&self) -> StyleAlignmentBaselineParseError<'_> {
2
        match self {
2
            Self::InvalidValue(e) => StyleAlignmentBaselineParseError::InvalidValue(e.to_shared()),
        }
2
    }
}
#[cfg(feature = "parser")]
/// # Errors
///
/// Returns an error if `input` is not a valid CSS `alignment-baseline` value.
34
pub fn parse_style_alignment_baseline(input: &str) -> Result<StyleAlignmentBaseline, StyleAlignmentBaselineParseError<'_>> {
34
    match input.trim() {
34
        "baseline" => Ok(StyleAlignmentBaseline::Baseline),
32
        "text-bottom" => Ok(StyleAlignmentBaseline::TextBottom),
30
        "alphabetic" => Ok(StyleAlignmentBaseline::Alphabetic),
28
        "ideographic" => Ok(StyleAlignmentBaseline::Ideographic),
26
        "middle" => Ok(StyleAlignmentBaseline::Middle),
24
        "central" => Ok(StyleAlignmentBaseline::Central),
22
        "mathematical" => Ok(StyleAlignmentBaseline::Mathematical),
20
        "text-top" => Ok(StyleAlignmentBaseline::TextTop),
18
        other => Err(StyleAlignmentBaselineParseError::InvalidValue(InvalidValueErr(other))),
    }
34
}
// -- StyleBaselineSource --
// +spec:inline-block:939f05 - baseline-source longhand: auto | first | last (auto = last baseline for inline-block / IFC roots, first baseline otherwise)
/// Represents the `baseline-source` CSS property.
///
/// Selects which of the box's baselines is used as its baseline in the parent's
/// baseline alignment (CSS Inline Layout Module Level 3 §5.2).
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
#[derive(Default)]
pub enum StyleBaselineSource {
    /// `auto`: last baseline for inline-block / IFC roots, first baseline otherwise.
    #[default]
    Auto,
    /// `first`: use the first baseline set.
    First,
    /// `last`: use the last baseline set.
    Last,
}
impl_option!(
    StyleBaselineSource,
    OptionStyleBaselineSource,
    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
);
impl PrintAsCssValue for StyleBaselineSource {
3
    fn print_as_css_value(&self) -> String {
3
        String::from(match self {
1
            Self::Auto => "auto",
1
            Self::First => "first",
1
            Self::Last => "last",
        })
3
    }
}
#[cfg(feature = "parser")]
#[derive(Clone, PartialEq, Eq)]
pub enum StyleBaselineSourceParseError<'a> {
    InvalidValue(InvalidValueErr<'a>),
}
#[cfg(feature = "parser")]
impl_debug_as_display!(StyleBaselineSourceParseError<'a>);
#[cfg(feature = "parser")]
impl_display! { StyleBaselineSourceParseError<'a>, {
    InvalidValue(e) => format!("Invalid baseline-source value: \"{}\"", e.0),
}}
#[cfg(feature = "parser")]
impl_from!(InvalidValueErr<'a>, StyleBaselineSourceParseError::InvalidValue);
#[cfg(feature = "parser")]
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(C, u8)]
pub enum StyleBaselineSourceParseErrorOwned {
    InvalidValue(InvalidValueErrOwned),
}
#[cfg(feature = "parser")]
impl StyleBaselineSourceParseError<'_> {
2
    #[must_use] pub fn to_contained(&self) -> StyleBaselineSourceParseErrorOwned {
2
        match self {
2
            Self::InvalidValue(e) => StyleBaselineSourceParseErrorOwned::InvalidValue(e.to_contained()),
        }
2
    }
}
#[cfg(feature = "parser")]
impl StyleBaselineSourceParseErrorOwned {
2
    #[must_use] pub fn to_shared(&self) -> StyleBaselineSourceParseError<'_> {
2
        match self {
2
            Self::InvalidValue(e) => StyleBaselineSourceParseError::InvalidValue(e.to_shared()),
        }
2
    }
}
#[cfg(feature = "parser")]
/// # Errors
///
/// Returns an error if `input` is not a valid CSS `baseline-source` value.
24
pub fn parse_style_baseline_source(input: &str) -> Result<StyleBaselineSource, StyleBaselineSourceParseError<'_>> {
24
    match input.trim() {
24
        "auto" => Ok(StyleBaselineSource::Auto),
22
        "first" => Ok(StyleBaselineSource::First),
20
        "last" => Ok(StyleBaselineSource::Last),
18
        other => Err(StyleBaselineSourceParseError::InvalidValue(InvalidValueErr(other))),
    }
24
}
// -- StyleLineFitEdge --
// +spec:line-height:cc03df - line-fit-edge selects the over/under metrics that size a line box; initial `leading` uses the line-height leading model
// +spec:box-model:0e75c1 - with line-fit-edge:leading (initial), margin/border/padding do not contribute to inline layout bounds
/// Represents the `line-fit-edge` CSS property.
///
/// Selects which font metrics determine the over/under edges used when fitting an
/// inline box into its line box (CSS Inline Layout Module Level 3 §5). `Auto` on
/// `text-box-edge` defers to this value.
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
#[derive(Default)]
pub enum StyleLineFitEdge {
    /// `leading` (initial): size the line box using the line-height leading model.
    #[default]
    Leading,
    /// `text`: use the text-over / text-under baselines.
    Text,
    /// `cap`: use the cap-height baseline for the over edge.
    Cap,
    /// `ex`: use the x-height baseline for the over edge.
    Ex,
    /// `ideographic`: use the ideographic-em baseline.
    Ideographic,
    /// `ideographic-ink`: use the ideographic-ink baseline.
    IdeographicInk,
    /// `alphabetic`: use the alphabetic baseline for the under edge.
    Alphabetic,
}
impl_option!(
    StyleLineFitEdge,
    OptionStyleLineFitEdge,
    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
);
impl PrintAsCssValue for StyleLineFitEdge {
7
    fn print_as_css_value(&self) -> String {
7
        String::from(match self {
1
            Self::Leading => "leading",
1
            Self::Text => "text",
1
            Self::Cap => "cap",
1
            Self::Ex => "ex",
1
            Self::Ideographic => "ideographic",
1
            Self::IdeographicInk => "ideographic-ink",
1
            Self::Alphabetic => "alphabetic",
        })
7
    }
}
#[cfg(feature = "parser")]
#[derive(Clone, PartialEq, Eq)]
pub enum StyleLineFitEdgeParseError<'a> {
    InvalidValue(InvalidValueErr<'a>),
}
#[cfg(feature = "parser")]
impl_debug_as_display!(StyleLineFitEdgeParseError<'a>);
#[cfg(feature = "parser")]
impl_display! { StyleLineFitEdgeParseError<'a>, {
    InvalidValue(e) => format!("Invalid line-fit-edge value: \"{}\"", e.0),
}}
#[cfg(feature = "parser")]
impl_from!(InvalidValueErr<'a>, StyleLineFitEdgeParseError::InvalidValue);
#[cfg(feature = "parser")]
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(C, u8)]
pub enum StyleLineFitEdgeParseErrorOwned {
    InvalidValue(InvalidValueErrOwned),
}
#[cfg(feature = "parser")]
impl StyleLineFitEdgeParseError<'_> {
2
    #[must_use] pub fn to_contained(&self) -> StyleLineFitEdgeParseErrorOwned {
2
        match self {
2
            Self::InvalidValue(e) => StyleLineFitEdgeParseErrorOwned::InvalidValue(e.to_contained()),
        }
2
    }
}
#[cfg(feature = "parser")]
impl StyleLineFitEdgeParseErrorOwned {
2
    #[must_use] pub fn to_shared(&self) -> StyleLineFitEdgeParseError<'_> {
2
        match self {
2
            Self::InvalidValue(e) => StyleLineFitEdgeParseError::InvalidValue(e.to_shared()),
        }
2
    }
}
#[cfg(feature = "parser")]
/// # Errors
///
/// Returns an error if `input` is not a valid CSS `line-fit-edge` value.
32
pub fn parse_style_line_fit_edge(input: &str) -> Result<StyleLineFitEdge, StyleLineFitEdgeParseError<'_>> {
32
    match input.trim() {
32
        "leading" => Ok(StyleLineFitEdge::Leading),
30
        "text" => Ok(StyleLineFitEdge::Text),
28
        "cap" => Ok(StyleLineFitEdge::Cap),
26
        "ex" => Ok(StyleLineFitEdge::Ex),
24
        "ideographic" => Ok(StyleLineFitEdge::Ideographic),
22
        "ideographic-ink" => Ok(StyleLineFitEdge::IdeographicInk),
20
        "alphabetic" => Ok(StyleLineFitEdge::Alphabetic),
18
        other => Err(StyleLineFitEdgeParseError::InvalidValue(InvalidValueErr(other))),
    }
32
}
// -- StyleInitialLetterAlign --
/// Represents the `initial-letter-align` CSS property.
///
/// Specifies the alignment points used to align an initial letter.
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
#[derive(Default)]
pub enum StyleInitialLetterAlign {
    /// Automatically determine alignment based on script
    #[default]
    Auto,
    /// Align to the alphabetic baseline
    Alphabetic,
    /// Align to the hanging baseline
    Hanging,
    /// Align to the ideographic baseline
    Ideographic,
}
impl_option!(
    StyleInitialLetterAlign,
    OptionStyleInitialLetterAlign,
    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
);
impl PrintAsCssValue for StyleInitialLetterAlign {
4
    fn print_as_css_value(&self) -> String {
4
        String::from(match self {
1
            Self::Auto => "auto",
1
            Self::Alphabetic => "alphabetic",
1
            Self::Hanging => "hanging",
1
            Self::Ideographic => "ideographic",
        })
4
    }
}
#[cfg(feature = "parser")]
#[derive(Clone, PartialEq, Eq)]
pub enum StyleInitialLetterAlignParseError<'a> {
    InvalidValue(InvalidValueErr<'a>),
}
#[cfg(feature = "parser")]
impl_debug_as_display!(StyleInitialLetterAlignParseError<'a>);
#[cfg(feature = "parser")]
impl_display! { StyleInitialLetterAlignParseError<'a>, {
    InvalidValue(e) => format!("Invalid initial-letter-align value: \"{}\"", e.0),
}}
#[cfg(feature = "parser")]
impl_from!(InvalidValueErr<'a>, StyleInitialLetterAlignParseError::InvalidValue);
#[cfg(feature = "parser")]
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(C, u8)]
pub enum StyleInitialLetterAlignParseErrorOwned {
    InvalidValue(InvalidValueErrOwned),
}
#[cfg(feature = "parser")]
impl StyleInitialLetterAlignParseError<'_> {
2
    #[must_use] pub fn to_contained(&self) -> StyleInitialLetterAlignParseErrorOwned {
2
        match self {
2
            Self::InvalidValue(e) => StyleInitialLetterAlignParseErrorOwned::InvalidValue(e.to_contained()),
        }
2
    }
}
#[cfg(feature = "parser")]
impl StyleInitialLetterAlignParseErrorOwned {
2
    #[must_use] pub fn to_shared(&self) -> StyleInitialLetterAlignParseError<'_> {
2
        match self {
2
            Self::InvalidValue(e) => StyleInitialLetterAlignParseError::InvalidValue(e.to_shared()),
        }
2
    }
}
#[cfg(feature = "parser")]
/// # Errors
///
/// Returns an error if `input` is not a valid CSS `initial-letter-align` value.
26
pub fn parse_style_initial_letter_align(input: &str) -> Result<StyleInitialLetterAlign, StyleInitialLetterAlignParseError<'_>> {
26
    match input.trim() {
26
        "auto" => Ok(StyleInitialLetterAlign::Auto),
24
        "alphabetic" => Ok(StyleInitialLetterAlign::Alphabetic),
22
        "hanging" => Ok(StyleInitialLetterAlign::Hanging),
20
        "ideographic" => Ok(StyleInitialLetterAlign::Ideographic),
18
        other => Err(StyleInitialLetterAlignParseError::InvalidValue(InvalidValueErr(other))),
    }
26
}
// -- StyleInitialLetterWrap --
/// Represents the `initial-letter-wrap` CSS property.
///
/// Specifies how text adjacent to an initial letter wraps.
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
#[derive(Default)]
pub enum StyleInitialLetterWrap {
    /// No special wrapping around the initial letter
    #[default]
    None,
    /// Wrap only the first line adjacent to the initial letter
    First,
    /// Wrap all lines adjacent to the initial letter
    All,
    /// Wrap using a grid-based layout
    Grid,
}
impl_option!(
    StyleInitialLetterWrap,
    OptionStyleInitialLetterWrap,
    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
);
impl PrintAsCssValue for StyleInitialLetterWrap {
4
    fn print_as_css_value(&self) -> String {
4
        String::from(match self {
1
            Self::None => "none",
1
            Self::First => "first",
1
            Self::All => "all",
1
            Self::Grid => "grid",
        })
4
    }
}
#[cfg(feature = "parser")]
#[derive(Clone, PartialEq, Eq)]
pub enum StyleInitialLetterWrapParseError<'a> {
    InvalidValue(InvalidValueErr<'a>),
}
#[cfg(feature = "parser")]
impl_debug_as_display!(StyleInitialLetterWrapParseError<'a>);
#[cfg(feature = "parser")]
impl_display! { StyleInitialLetterWrapParseError<'a>, {
    InvalidValue(e) => format!("Invalid initial-letter-wrap value: \"{}\"", e.0),
}}
#[cfg(feature = "parser")]
impl_from!(InvalidValueErr<'a>, StyleInitialLetterWrapParseError::InvalidValue);
#[cfg(feature = "parser")]
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(C, u8)]
pub enum StyleInitialLetterWrapParseErrorOwned {
    InvalidValue(InvalidValueErrOwned),
}
#[cfg(feature = "parser")]
impl StyleInitialLetterWrapParseError<'_> {
2
    #[must_use] pub fn to_contained(&self) -> StyleInitialLetterWrapParseErrorOwned {
2
        match self {
2
            Self::InvalidValue(e) => StyleInitialLetterWrapParseErrorOwned::InvalidValue(e.to_contained()),
        }
2
    }
}
#[cfg(feature = "parser")]
impl StyleInitialLetterWrapParseErrorOwned {
2
    #[must_use] pub fn to_shared(&self) -> StyleInitialLetterWrapParseError<'_> {
2
        match self {
2
            Self::InvalidValue(e) => StyleInitialLetterWrapParseError::InvalidValue(e.to_shared()),
        }
2
    }
}
#[cfg(feature = "parser")]
/// # Errors
///
/// Returns an error if `input` is not a valid CSS `initial-letter-wrap` value.
26
pub fn parse_style_initial_letter_wrap(input: &str) -> Result<StyleInitialLetterWrap, StyleInitialLetterWrapParseError<'_>> {
26
    match input.trim() {
26
        "none" => Ok(StyleInitialLetterWrap::None),
24
        "first" => Ok(StyleInitialLetterWrap::First),
22
        "all" => Ok(StyleInitialLetterWrap::All),
20
        "grid" => Ok(StyleInitialLetterWrap::Grid),
18
        other => Err(StyleInitialLetterWrapParseError::InvalidValue(InvalidValueErr(other))),
    }
26
}
#[cfg(test)]
#[allow(clippy::float_cmp, clippy::too_many_lines, clippy::cast_precision_loss)]
mod autotest_generated {
    use super::*;
    use crate::props::basic::length::SizeMetric;
    const OPAQUE_BLACK: ColorU = ColorU { r: 0, g: 0, b: 0, a: 255 };
    const OPAQUE_WHITE: ColorU = ColorU { r: 255, g: 255, b: 255, a: 255 };
    const TRANSPARENT_BLACK: ColorU = ColorU { r: 0, g: 0, b: 0, a: 0 };
    /// `FloatValue` encodes an f32 as `isize` scaled by this factor.
    const SCALE: isize = 1000;
    // =====================================================================
    // StyleTextIndent — constructors + numeric edges
    // =====================================================================
    #[test]
    fn text_indent_zero_is_the_neutral_element() {
        let z = StyleTextIndent::zero();
        assert_eq!(z, StyleTextIndent::default());
        assert_eq!(z.inner.metric, SizeMetric::Px);
        assert_eq!(z.inner.number.get(), 0.0);
        assert!(!z.each_line);
        assert!(!z.hanging);
        assert_eq!(z.print_as_css_value(), "0px");
    }
    #[test]
    fn text_indent_const_ctors_pin_metric_and_value() {
        // (constructed value, expected metric) for 0 / positive / negative.
        for v in [0_isize, 1, -1, 42, -42] {
            let cases = [
                (StyleTextIndent::const_px(v), SizeMetric::Px),
                (StyleTextIndent::const_em(v), SizeMetric::Em),
                (StyleTextIndent::const_pt(v), SizeMetric::Pt),
                (StyleTextIndent::const_percent(v), SizeMetric::Percent),
                (StyleTextIndent::const_in(v), SizeMetric::In),
                (StyleTextIndent::const_cm(v), SizeMetric::Cm),
                (StyleTextIndent::const_mm(v), SizeMetric::Mm),
            ];
            for (got, metric) in cases {
                assert_eq!(got.inner.metric, metric, "metric for {v}");
                assert_eq!(got.inner.number.get(), v as f32, "value for {v} {metric:?}");
                // The keyword flags are never set by the numeric constructors.
                assert!(!got.each_line && !got.hanging);
            }
        }
    }
    #[test]
    fn text_indent_const_ctors_hold_the_whole_encodable_isize_range() {
        // The isize encoding is `value * 1000`, so the representable input range is
        // isize::MIN/1000 ..= isize::MAX/1000. Pin the scale, then both ends of it.
        assert_eq!(StyleTextIndent::const_px(1).inner.number.number(), SCALE);
        let max = isize::MAX / SCALE;
        let min = isize::MIN / SCALE;
        assert_eq!(StyleTextIndent::const_px(max).inner.number.number(), max * SCALE);
        assert_eq!(StyleTextIndent::const_px(min).inner.number.number(), min * SCALE);
        assert_eq!(
            StyleTextIndent::const_from_metric(SizeMetric::Em, max).inner.number.number(),
            max * SCALE
        );
        // NOTE: one step past those bounds (e.g. `const_px(isize::MAX)`) overflows the
        // `value * 1000` multiply and panics in debug. See the report — not asserted here
        // because the behaviour differs between debug (panic) and release (wrap).
    }
    #[test]
    fn text_indent_float_ctors_saturate_on_nan_and_infinity() {
        // f32 -> isize is a saturating `as` cast: NaN -> 0, +inf -> MAX, -inf -> MIN.
        assert_eq!(StyleTextIndent::px(f32::NAN).inner.number.get(), 0.0);
        assert_eq!(StyleTextIndent::em(f32::NAN).inner.number.get(), 0.0);
        assert_eq!(StyleTextIndent::px(f32::INFINITY).inner.number.number(), isize::MAX);
        assert_eq!(StyleTextIndent::px(f32::NEG_INFINITY).inner.number.number(), isize::MIN);
        assert_eq!(StyleTextIndent::pt(f32::MAX).inner.number.number(), isize::MAX);
        assert_eq!(StyleTextIndent::pt(-f32::MAX).inner.number.number(), isize::MIN);
        // Sub-precision magnitudes collapse to zero rather than trapping.
        assert_eq!(StyleTextIndent::percent(f32::MIN_POSITIVE).inner.number.number(), 0);
        assert_eq!(StyleTextIndent::px(-0.0).inner.number.number(), 0);
        // Every saturated result is still a finite, readable f32.
        for v in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, f32::MAX, -f32::MAX] {
            assert!(StyleTextIndent::px(v).inner.number.get().is_finite());
        }
    }
    #[test]
    fn text_indent_from_metric_agrees_with_the_typed_ctors() {
        assert_eq!(StyleTextIndent::from_metric(SizeMetric::Px, 1.5), StyleTextIndent::px(1.5));
        assert_eq!(StyleTextIndent::from_metric(SizeMetric::Em, -2.5), StyleTextIndent::em(-2.5));
        assert_eq!(StyleTextIndent::from_metric(SizeMetric::Pt, 0.0), StyleTextIndent::pt(0.0));
        assert_eq!(
            StyleTextIndent::from_metric(SizeMetric::Percent, 50.0),
            StyleTextIndent::percent(50.0)
        );
        assert_eq!(StyleTextIndent::const_from_metric(SizeMetric::Cm, 3), StyleTextIndent::const_cm(3));
        assert_eq!(StyleTextIndent::const_from_metric(SizeMetric::Mm, -3), StyleTextIndent::const_mm(-3));
        // A metric with no typed ctor still round-trips through from_metric.
        let vw = StyleTextIndent::from_metric(SizeMetric::Vw, 10.0);
        assert_eq!(vw.inner.metric, SizeMetric::Vw);
        assert_eq!(vw.inner.number.get(), 10.0);
    }
    #[test]
    fn text_indent_interpolate_endpoints_and_extrapolation() {
        let a = StyleTextIndent::px(0.0);
        let b = StyleTextIndent::px(100.0);
        assert_eq!(a.interpolate(&b, 0.0).inner.number.get(), 0.0);
        assert_eq!(a.interpolate(&b, 1.0).inner.number.get(), 100.0);
        assert_eq!(a.interpolate(&b, 0.5).inner.number.get(), 50.0);
        // t outside [0,1] extrapolates rather than clamping.
        assert_eq!(a.interpolate(&b, -1.0).inner.number.get(), -100.0);
        assert_eq!(a.interpolate(&b, 2.0).inner.number.get(), 200.0);
        // Interpolating a value with itself is the identity for any finite t.
        assert_eq!(b.interpolate(&b, 0.25), b);
    }
    #[test]
    fn text_indent_interpolate_with_nonfinite_t_is_defined() {
        let a = StyleTextIndent::px(0.0);
        let b = StyleTextIndent::px(100.0);
        // NaN propagates into the f32 -> isize cast, which saturates NaN to 0.
        assert_eq!(a.interpolate(&b, f32::NAN).inner.number.get(), 0.0);
        // +/-inf saturate to the isize bounds instead of panicking.
        assert_eq!(a.interpolate(&b, f32::INFINITY).inner.number.number(), isize::MAX);
        assert_eq!(a.interpolate(&b, f32::NEG_INFINITY).inner.number.number(), isize::MIN);
        // 0 * inf is NaN, so interpolating equal endpoints by inf collapses to zero.
        assert_eq!(b.interpolate(&b, f32::INFINITY).inner.number.get(), 0.0);
        for t in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, f32::MAX] {
            assert!(a.interpolate(&b, t).inner.number.get().is_finite());
        }
    }
    #[test]
    fn text_indent_interpolate_keeps_self_flags_and_normalizes_mixed_metrics() {
        let a = StyleTextIndent { each_line: true, hanging: true, ..StyleTextIndent::px(0.0) };
        let b = StyleTextIndent { each_line: false, hanging: false, ..StyleTextIndent::px(10.0) };
        // Flags are taken from `self`, never blended.
        let mid = a.interpolate(&b, 0.5);
        assert!(mid.each_line && mid.hanging);
        assert!(!b.interpolate(&a, 0.5).each_line);
        // Mismatched metrics are resolved into px.
        let mixed = StyleTextIndent::px(10.0).interpolate(&StyleTextIndent::em(2.0), 0.5);
        assert_eq!(mixed.inner.metric, SizeMetric::Px);
        assert!(mixed.inner.number.get().is_finite());
    }
    // =====================================================================
    // StyleTextColor::interpolate
    // =====================================================================
    #[test]
    fn text_color_interpolate_endpoints_are_exact() {
        let a = StyleTextColor { inner: OPAQUE_BLACK };
        let b = StyleTextColor { inner: OPAQUE_WHITE };
        assert_eq!(a.interpolate(&b, 0.0), a);
        assert_eq!(a.interpolate(&b, 1.0), b);
        // 0 + 255*0.5 = 127.5, rounded half-away-from-zero.
        assert_eq!(a.interpolate(&b, 0.5).inner, ColorU { r: 128, g: 128, b: 128, a: 255 });
        assert_eq!(a.print_as_css_value(), "#000000ff");
    }
    #[test]
    fn text_color_interpolate_saturates_out_of_range_t() {
        let a = StyleTextColor { inner: OPAQUE_BLACK };
        let b = StyleTextColor { inner: OPAQUE_WHITE };
        // 0 + 255*2 = 510 -> clamped to 255 by the saturating u8 cast (no wrap to 254).
        assert_eq!(a.interpolate(&b, 2.0).inner, OPAQUE_WHITE);
        // 0 + 255*-1 = -255 -> clamped to 0 (no wrap to 1).
        assert_eq!(a.interpolate(&b, -1.0).inner, OPAQUE_BLACK);
    }
    #[test]
    fn text_color_interpolate_with_nonfinite_t_is_defined() {
        let a = StyleTextColor { inner: OPAQUE_BLACK };
        let b = StyleTextColor { inner: OPAQUE_WHITE };
        // NaN saturates to 0 in every channel — including alpha, so the result is
        // transparent black rather than a panic or garbage.
        assert_eq!(a.interpolate(&b, f32::NAN).inner, TRANSPARENT_BLACK);
        // With t = +inf the changing channels saturate to 255, but alpha is *equal* in
        // both endpoints, so it computes 255 + (0 * inf) = NaN and saturates to 0.
        // A fully-opaque pair therefore interpolates to a fully-transparent colour.
        assert_eq!(
            a.interpolate(&b, f32::INFINITY).inner,
            ColorU { r: 255, g: 255, b: 255, a: 0 }
        );
        assert_eq!(
            a.interpolate(&b, f32::NEG_INFINITY).inner,
            ColorU { r: 0, g: 0, b: 0, a: 0 }
        );
    }
    // =====================================================================
    // StyleHangingPunctuation::is_enabled (predicate invariants)
    // =====================================================================
    #[test]
    fn hanging_punctuation_is_enabled_matches_its_flags_exhaustively() {
        assert!(!StyleHangingPunctuation::default().is_enabled());
        for bits in 0_u8..16 {
            let hp = StyleHangingPunctuation {
                first: bits & 1 != 0,
                force_end: bits & 2 != 0,
                allow_end: bits & 4 != 0,
                last: bits & 8 != 0,
            };
            assert_eq!(hp.is_enabled(), bits != 0, "bits={bits}");
            // is_enabled() is exactly the "prints as something other than none" predicate.
            assert_eq!(hp.print_as_css_value() == "none", !hp.is_enabled(), "bits={bits}");
        }
    }
    #[test]
    fn hanging_punctuation_prints_flags_in_spec_order() {
        let all = StyleHangingPunctuation { first: true, force_end: true, allow_end: true, last: true };
        assert_eq!(all.print_as_css_value(), "first force-end allow-end last");
        assert_eq!(
            StyleHangingPunctuation { last: true, ..Default::default() }.print_as_css_value(),
            "last"
        );
    }
    // =====================================================================
    // Parser-gated tests
    // =====================================================================
    #[cfg(feature = "parser")]
    mod parser {
        use super::super::*;
        use crate::props::basic::length::SizeMetric;
        const GARBAGE: &[&str] = &[
            "",
            "   ",
            "\t\n",
            "!!!",
            "\0\0",
            "0",
            "-0",
            "9223372036854775807",
            "1e400",
            "NaN",
            "inf",
            "\u{1F600}",
            "e\u{0301}",
            "\u{202E}left",
            "left;garbage",
            "left garbage",
        ];
        /// Every keyword parser must reject junk, and accept its own printed form.
        macro_rules! assert_keyword_round_trip {
            ($parse:ident, $variants:expr) => {{
                for v in $variants {
                    let printed = v.print_as_css_value();
                    assert_eq!($parse(&printed).as_ref(), Ok(&v), "round-trip of {printed:?}");
                    // Surrounding whitespace is trimmed, not rejected.
                    assert_eq!($parse(&format!("  {printed}  ")).as_ref(), Ok(&v));
                }
                for g in GARBAGE.iter().copied() {
                    assert!($parse(g).is_err(), "{} accepted garbage {g:?}", stringify!($parse));
                }
            }};
        }
        #[test]
        fn keyword_parsers_round_trip_every_variant_and_reject_garbage() {
            type TA = StyleTextAlign;
            assert_keyword_round_trip!(
                parse_style_text_align,
                [TA::Left, TA::Center, TA::Right, TA::Justify, TA::Start, TA::End]
            );
            type WS = StyleWhiteSpace;
            assert_keyword_round_trip!(
                parse_style_white_space,
                [WS::Normal, WS::Pre, WS::Nowrap, WS::PreWrap, WS::PreLine, WS::BreakSpaces]
            );
            type H = StyleHyphens;
            assert_keyword_round_trip!(parse_style_hyphens, [H::None, H::Manual, H::Auto]);
            type LB = StyleLineBreak;
            assert_keyword_round_trip!(
                parse_style_line_break,
                [LB::Auto, LB::Loose, LB::Normal, LB::Strict, LB::Anywhere]
            );
            type WB = StyleWordBreak;
            assert_keyword_round_trip!(
                parse_style_word_break,
                [WB::Normal, WB::BreakAll, WB::KeepAll, WB::BreakWord]
            );
            type OW = StyleOverflowWrap;
            assert_keyword_round_trip!(
                parse_style_overflow_wrap,
                [OW::Normal, OW::Anywhere, OW::BreakWord]
            );
            type Tal = StyleTextAlignLast;
            assert_keyword_round_trip!(
                parse_style_text_align_last,
                [Tal::Auto, Tal::Start, Tal::End, Tal::Left, Tal::Right, Tal::Center, Tal::Justify]
            );
            type TT = StyleTextTransform;
            assert_keyword_round_trip!(
                parse_style_text_transform,
                [TT::None, TT::Capitalize, TT::Uppercase, TT::Lowercase, TT::FullWidth]
            );
            type D = StyleDirection;
            assert_keyword_round_trip!(parse_style_direction, [D::Ltr, D::Rtl]);
            type US = StyleUserSelect;
            assert_keyword_round_trip!(
                parse_style_user_select,
                [US::Auto, US::Text, US::None, US::All]
            );
            type TD = StyleTextDecoration;
            assert_keyword_round_trip!(
                parse_style_text_decoration,
                [TD::None, TD::Underline, TD::Overline, TD::LineThrough]
            );
            type UB = StyleUnicodeBidi;
            assert_keyword_round_trip!(
                parse_style_unicode_bidi,
                [UB::Normal, UB::Embed, UB::Isolate, UB::BidiOverride, UB::IsolateOverride, UB::Plaintext]
            );
            type Tbt = StyleTextBoxTrim;
            assert_keyword_round_trip!(
                parse_style_text_box_trim,
                [Tbt::None, Tbt::TrimStart, Tbt::TrimEnd, Tbt::TrimBoth]
            );
            {
                // text-box-edge is a two-value property now; round-trip the
                // grammar by hand instead of via the single-keyword macro.
                use crate::props::style::text::{TextBoxEdgeOver as O, TextBoxEdgeUnder as U};
                let cases = [
                    ("auto", StyleTextBoxEdge::AUTO),
                    ("text", StyleTextBoxEdge { over: O::Text, under: U::Text }),
                    ("cap", StyleTextBoxEdge { over: O::Cap, under: U::Text }),
                    ("ex", StyleTextBoxEdge { over: O::Ex, under: U::Text }),
                    ("ideographic", StyleTextBoxEdge { over: O::Ideographic, under: U::Ideographic }),
                    ("ideographic-ink", StyleTextBoxEdge { over: O::IdeographicInk, under: U::IdeographicInk }),
                    ("cap alphabetic", StyleTextBoxEdge { over: O::Cap, under: U::Alphabetic }),
                    ("text ideographic", StyleTextBoxEdge { over: O::Text, under: U::Ideographic }),
                ];
                for (input, expected) in cases {
                    let parsed = parse_style_text_box_edge(input).unwrap_or_else(|e| {
                        panic!("`{input}` must parse: {e:?}")
                    });
                    assert_eq!(parsed, expected, "parse of `{input}`");
                    let printed = parsed.print_as_css_value();
                    let reparsed = parse_style_text_box_edge(&printed)
                        .unwrap_or_else(|e| panic!("reprint `{printed}` must parse: {e:?}"));
                    assert_eq!(reparsed, parsed, "print/parse round trip via `{printed}`");
                }
                // `auto` cannot take a second value; junk is rejected.
                assert!(parse_style_text_box_edge("auto text").is_err());
                assert!(parse_style_text_box_edge("cap cap").is_err(), "cap is over-only");
                assert!(parse_style_text_box_edge("alphabetic").is_err(), "alphabetic is under-only");
                assert!(parse_style_text_box_edge("bogus").is_err());
            }
            type DB = StyleDominantBaseline;
            assert_keyword_round_trip!(
                parse_style_dominant_baseline,
                [
                    DB::Auto, DB::TextBottom, DB::Alphabetic, DB::Ideographic, DB::Middle,
                    DB::Central, DB::Mathematical, DB::Hanging, DB::TextTop
                ]
            );
            type AB = StyleAlignmentBaseline;
            assert_keyword_round_trip!(
                parse_style_alignment_baseline,
                [
                    AB::Baseline, AB::TextBottom, AB::Alphabetic, AB::Ideographic, AB::Middle,
                    AB::Central, AB::Mathematical, AB::TextTop
                ]
            );
            type Bs = StyleBaselineSource;
            assert_keyword_round_trip!(
                parse_style_baseline_source,
                [Bs::Auto, Bs::First, Bs::Last]
            );
            type Lfe = StyleLineFitEdge;
            assert_keyword_round_trip!(
                parse_style_line_fit_edge,
                [
                    Lfe::Leading, Lfe::Text, Lfe::Cap, Lfe::Ex, Lfe::Ideographic,
                    Lfe::IdeographicInk, Lfe::Alphabetic
                ]
            );
            type Ila = StyleInitialLetterAlign;
            assert_keyword_round_trip!(
                parse_style_initial_letter_align,
                [Ila::Auto, Ila::Alphabetic, Ila::Hanging, Ila::Ideographic]
            );
            type Ilw = StyleInitialLetterWrap;
            assert_keyword_round_trip!(
                parse_style_initial_letter_wrap,
                [Ilw::None, Ilw::First, Ilw::All, Ilw::Grid]
            );
        }
        #[test]
        fn keyword_parsers_are_case_sensitive() {
            // BUG: CSS keywords are ASCII case-insensitive (CSS Syntax 3 §3.1), but every
            // `match input.trim()` parser in this file compares exactly, so `text-align: LEFT`
            // is rejected. `hanging-punctuation` / `text-combine-upright` *do* fold case, so
            // the file is internally inconsistent too. Pinned as-is; see the report.
            assert!(parse_style_text_align("LEFT").is_err());
            assert!(parse_style_text_align("Left").is_err());
            assert!(parse_style_white_space("Normal").is_err());
            assert!(parse_style_direction("LTR").is_err());
            // ...whereas these two fold case as the spec requires:
            assert!(parse_style_hanging_punctuation("FIRST").is_ok());
            assert!(parse_style_text_combine_upright("NONE").is_ok());
        }
        #[test]
        fn extremely_long_and_deeply_nested_input_terminates_with_err() {
            let long = "a".repeat(1_000_000);
            assert!(parse_style_text_align(&long).is_err());
            assert!(parse_style_white_space(&long).is_err());
            assert!(parse_style_text_color(&long).is_err());
            assert!(parse_style_letter_spacing(&long).is_err());
            assert!(parse_style_word_spacing(&long).is_err());
            assert!(parse_style_tab_size(&long).is_err());
            assert!(parse_style_line_height(&long).is_err());
            assert!(parse_style_text_indent(&long).is_err());
            assert!(parse_style_hanging_punctuation(&long).is_err());
            assert!(parse_style_initial_letter(&long).is_err());
            assert!(parse_style_line_clamp(&long).is_err());
            assert!(parse_style_vertical_align(&long).is_err());
            // A 1000-digit integer overflows every numeric target -> Err, never a wrap.
            let huge_number = "9".repeat(1000);
            assert!(parse_style_line_clamp(&huge_number).is_err());
            assert!(parse_style_initial_letter(&huge_number).is_err());
            // No parser here recurses, so nesting cannot blow the stack.
            let nested = format!("{}{}", "(".repeat(10_000), ")".repeat(10_000));
            assert!(parse_style_text_align(&nested).is_err());
            assert!(parse_style_letter_spacing(&nested).is_err());
            assert!(parse_style_text_indent(&nested).is_err());
            assert!(parse_style_line_height(&nested).is_err());
        }
        // -- pixel-valued properties -------------------------------------------
        #[test]
        fn spacing_and_tab_size_round_trip_through_their_printed_form() {
            for pv in [
                PixelValue::px(2.0),
                PixelValue::px(-3.0),
                PixelValue::em(0.5),
                PixelValue::pt(12.0),
                PixelValue::percent(10.0),
                PixelValue::zero(),
            ] {
                let ls = StyleLetterSpacing { inner: pv };
                assert_eq!(parse_style_letter_spacing(&ls.print_as_css_value()).unwrap(), ls);
                let ws = StyleWordSpacing { inner: pv };
                assert_eq!(parse_style_word_spacing(&ws.print_as_css_value()).unwrap(), ws);
            }
            // tab-size: unitless numbers mean `em`, lengths keep their unit.
            let ts = StyleTabSize::default();
            assert_eq!(ts.inner, PixelValue::em(8.0));
            assert_eq!(parse_style_tab_size(&ts.print_as_css_value()).unwrap(), ts);
            assert_eq!(parse_style_tab_size("4").unwrap().inner, PixelValue::em(4.0));
            assert_eq!(parse_style_tab_size("20px").unwrap().inner, PixelValue::px(20.0));
        }
        #[test]
        fn pixel_parsers_reject_empty_and_unit_only_input() {
            for bad in ["", "   ", "\t\n", "px", "em", "abc", "px px", "10pxx", "\u{1F600}"] {
                assert!(parse_style_letter_spacing(bad).is_err(), "letter-spacing {bad:?}");
                assert!(parse_style_word_spacing(bad).is_err(), "word-spacing {bad:?}");
            }
            // BUG: the shared pixel parser trims *after* stripping the unit suffix, so a space
            // between the number and its unit is accepted even though CSS forbids it.
            assert_eq!(parse_style_letter_spacing("10 px").unwrap().inner, PixelValue::px(10.0));
        }
        #[test]
        fn pixel_parsers_accept_float_keywords_and_saturate_instead_of_panicking() {
            // BUG: `f32::from_str` accepts "NaN"/"inf"/"1e400", so these are *not* rejected
            // as CSS lengths. They cannot panic (the isize cast saturates), but they should
            // be Err. Pinned as-is; see the report.
            assert_eq!(parse_style_letter_spacing("NaN").unwrap().inner.number.number(), 0);
            assert_eq!(
                parse_style_letter_spacing("inf").unwrap().inner.number.number(),
                isize::MAX
            );
            assert_eq!(
                parse_style_letter_spacing("-inf").unwrap().inner.number.number(),
                isize::MIN
            );
            assert_eq!(
                parse_style_word_spacing("1e400px").unwrap().inner.number.number(),
                isize::MAX
            );
            // Same story via the tab-size unitless branch.
            assert_eq!(parse_style_tab_size("NaN").unwrap().inner, PixelValue::em(0.0));
            assert!(parse_style_tab_size("inf").unwrap().inner.number.get().is_finite());
            // Boundary numeric strings that *are* legal still parse to the right value.
            assert_eq!(parse_style_letter_spacing("0").unwrap().inner, PixelValue::px(0.0));
            assert_eq!(parse_style_letter_spacing("-0").unwrap().inner.number.number(), 0);
            assert!(parse_style_letter_spacing("9223372036854775807px").unwrap().inner.number.get().is_finite());
        }
        // -- text-indent --------------------------------------------------------
        #[test]
        fn text_indent_round_trips_length_and_keywords() {
            for pv in [PixelValue::px(10.0), PixelValue::em(-2.0), PixelValue::percent(50.0)] {
                for (each_line, hanging) in [(false, false), (true, false), (false, true), (true, true)] {
                    let ti = StyleTextIndent { inner: pv, each_line, hanging };
                    let printed = ti.print_as_css_value();
                    assert_eq!(parse_style_text_indent(&printed).unwrap(), ti, "{printed:?}");
                }
            }
            assert!(parse_style_text_indent("10px hanging").unwrap().hanging);
            assert!(parse_style_text_indent("each-line 10px").unwrap().each_line);
        }
        #[test]
        fn text_indent_defaults_missing_length_to_zero_and_keeps_the_last_one() {
            // BUG: `text-indent` requires a <length-percentage>; these should all be Err.
            // Instead an absent length silently defaults to 0px, so empty/whitespace/keyword-
            // only input parses Ok. Pinned as-is; see the report.
            assert_eq!(parse_style_text_indent("").unwrap(), StyleTextIndent::zero());
            assert_eq!(parse_style_text_indent("   ").unwrap(), StyleTextIndent::zero());
            assert_eq!(
                parse_style_text_indent("hanging").unwrap(),
                StyleTextIndent { hanging: true, ..StyleTextIndent::zero() }
            );
            // BUG: repeated lengths are not rejected — the last token silently wins.
            assert_eq!(parse_style_text_indent("10px 20px").unwrap().inner, PixelValue::px(20.0));
            // Junk in the length slot is still rejected.
            assert!(parse_style_text_indent("garbage").is_err());
            assert!(parse_style_text_indent("hanging garbage").is_err());
        }
        // -- initial-letter -----------------------------------------------------
        #[test]
        fn initial_letter_parses_size_and_optional_sink() {
            assert_eq!(
                parse_style_initial_letter("3").unwrap(),
                StyleInitialLetter { size: 3, sink: crate::corety::OptionU32::None }
            );
            let with_sink = StyleInitialLetter { size: 3, sink: crate::corety::OptionU32::Some(2) };
            assert_eq!(parse_style_initial_letter("3 2").unwrap(), with_sink);
            // Printed form round-trips.
            assert_eq!(parse_style_initial_letter(&with_sink.print_as_css_value()).unwrap(), with_sink);
        }
        #[test]
        fn initial_letter_rejects_zero_negative_and_overflowing_sizes() {
            assert!(parse_style_initial_letter("0").is_err(), "size 0 must be rejected");
            assert!(parse_style_initial_letter("-1").is_err());
            assert!(parse_style_initial_letter("1.5").is_err());
            assert!(parse_style_initial_letter("4294967296").is_err(), "u32::MAX + 1");
            assert!(parse_style_initial_letter("3 -1").is_err(), "negative sink");
            assert!(parse_style_initial_letter("3 x").is_err());
            assert!(parse_style_initial_letter("").is_err());
            assert!(parse_style_initial_letter("   ").is_err());
            // u32::MAX itself is in range.
            assert_eq!(parse_style_initial_letter("4294967295").unwrap().size, u32::MAX);
            // BUG: a third component should be a parse error, but it is silently dropped.
            assert_eq!(parse_style_initial_letter("3 2 9").unwrap(), StyleInitialLetter {
                size: 3,
                sink: crate::corety::OptionU32::Some(2),
            });
        }
        // -- line-clamp ---------------------------------------------------------
        #[test]
        fn line_clamp_rejects_zero_and_out_of_range_values() {
            assert_eq!(parse_style_line_clamp("3").unwrap(), StyleLineClamp { max_lines: 3 });
            assert_eq!(parse_style_line_clamp("  7  ").unwrap().max_lines, 7);
            assert_eq!(
                parse_style_line_clamp("0").unwrap_err(),
                StyleLineClampParseError::ZeroValue
            );
            assert!(parse_style_line_clamp("-1").is_err());
            assert!(parse_style_line_clamp("1.0").is_err());
            assert!(parse_style_line_clamp("").is_err());
            assert!(parse_style_line_clamp("   ").is_err());
            assert!(parse_style_line_clamp("\u{1F600}").is_err());
            // Saturating/wrapping never happens: an out-of-range integer is an error.
            assert!(parse_style_line_clamp("99999999999999999999999").is_err());
            let max = usize::MAX.to_string();
            assert_eq!(parse_style_line_clamp(&max).unwrap().max_lines, usize::MAX);
            // Printed form round-trips.
            let lc = StyleLineClamp { max_lines: 42 };
            assert_eq!(parse_style_line_clamp(&lc.print_as_css_value()).unwrap(), lc);
        }
        // -- hanging-punctuation ------------------------------------------------
        #[test]
        fn hanging_punctuation_round_trips_and_enforces_mutual_exclusion() {
            for bits in 0_u8..16 {
                let hp = StyleHangingPunctuation {
                    first: bits & 1 != 0,
                    force_end: bits & 2 != 0,
                    allow_end: bits & 4 != 0,
                    last: bits & 8 != 0,
                };
                let printed = hp.print_as_css_value();
                if hp.force_end && hp.allow_end {
                    // `force-end` and `allow-end` are mutually exclusive per CSS Text 3 §8.
                    assert!(parse_style_hanging_punctuation(&printed).is_err(), "{printed:?}");
                } else {
                    assert_eq!(parse_style_hanging_punctuation(&printed).unwrap(), hp, "{printed:?}");
                }
            }
            assert!(parse_style_hanging_punctuation("first bogus").is_err());
            assert!(parse_style_hanging_punctuation("\u{1F600}").is_err());
            assert!(parse_style_hanging_punctuation("none first").is_err());
        }
        #[test]
        fn hanging_punctuation_accepts_empty_input_as_none() {
            // BUG: empty / whitespace-only input has no tokens, so the loop body never runs
            // and the parser returns Ok(none) instead of Err. Pinned as-is; see the report.
            assert_eq!(
                parse_style_hanging_punctuation("").unwrap(),
                StyleHangingPunctuation::default()
            );
            assert_eq!(
                parse_style_hanging_punctuation("   ").unwrap(),
                StyleHangingPunctuation::default()
            );
            // Duplicate keywords are also accepted (idempotent flag set).
            assert!(parse_style_hanging_punctuation("first first").unwrap().first);
        }
        // -- text-combine-upright -----------------------------------------------
        #[test]
        fn text_combine_upright_bounds_the_digits_operand() {
            assert_eq!(parse_style_text_combine_upright("none").unwrap(), StyleTextCombineUpright::None);
            assert_eq!(parse_style_text_combine_upright("all").unwrap(), StyleTextCombineUpright::All);
            for n in 2_u8..=4 {
                let v = StyleTextCombineUpright::Digits(n);
                assert_eq!(parse_style_text_combine_upright(&v.print_as_css_value()).unwrap(), v);
            }
            // Outside the spec'd 2..=4 range -> Err, not a silent clamp or wrap.
            for bad in ["digits 0", "digits 1", "digits 5", "digits 255", "digits 256", "digits -1"] {
                assert!(parse_style_text_combine_upright(bad).is_err(), "{bad:?} accepted");
            }
            assert!(parse_style_text_combine_upright("").is_err());
            assert!(parse_style_text_combine_upright("bogus").is_err());
        }
        #[test]
        fn text_combine_upright_accepts_garbage_after_the_digits_prefix() {
            // BUG: the `digits` branch is chosen by `starts_with("digits")` with no word
            // boundary, and any token count != 2 falls back to `digits 2`. So junk that
            // merely starts with "digits" parses Ok. Pinned as-is; see the report.
            assert_eq!(
                parse_style_text_combine_upright("digits").unwrap(),
                StyleTextCombineUpright::Digits(2)
            );
            assert_eq!(
                parse_style_text_combine_upright("digitsgarbage").unwrap(),
                StyleTextCombineUpright::Digits(2)
            );
            assert_eq!(
                parse_style_text_combine_upright("digits 2 3").unwrap(),
                StyleTextCombineUpright::Digits(2)
            );
        }
        // -- line-height --------------------------------------------------------
        #[test]
        fn line_height_parses_numbers_percentages_and_px() {
            assert_eq!(parse_style_line_height("1.5").unwrap().inner, PercentageValue::new(150.0));
            assert_eq!(parse_style_line_height("120%").unwrap().inner, PercentageValue::new(120.0));
            // px lengths are encoded as a *negative* percentage (documented convention).
            assert_eq!(parse_style_line_height("20px").unwrap().inner, PercentageValue::new(-2000.0));
            assert!(parse_style_line_height("").is_err());
            assert!(parse_style_line_height("   ").is_err());
            assert!(parse_style_line_height("abc").is_err());
            assert!(parse_style_line_height("\u{1F600}").is_err());
            // Printed form round-trips as a value.
            let lh = StyleLineHeight::default();
            assert_eq!(parse_style_line_height(&lh.print_as_css_value()).unwrap(), lh);
        }
        #[test]
        fn line_height_negative_numbers_alias_absolute_px_lengths() {
            // BUG: negative values are the internal marker for "absolute px", but the number
            // branch happily parses a negative <number>, so `line-height: -1` and
            // `line-height: 1px` produce the *same* value and are indistinguishable
            // downstream. A negative line-height is invalid CSS and should be Err.
            assert_eq!(
                parse_style_line_height("-1").unwrap(),
                parse_style_line_height("1px").unwrap()
            );
            assert_eq!(
                parse_style_line_height("-100%").unwrap(),
                parse_style_line_height("1px").unwrap()
            );
        }
        #[test]
        fn line_height_rejects_em_and_other_length_units() {
            // BUG: `line-height: 1.5em` (and rem/pt/...) is valid CSS but only Px survives
            // the length branch, so every other unit is rejected. Pinned as-is.
            assert!(parse_style_line_height("1.5em").is_err());
            assert!(parse_style_line_height("12pt").is_err());
            assert!(parse_style_line_height("2rem").is_err());
        }
        #[test]
        fn line_height_rejects_non_ascii_numerals_without_panicking() {
            // `char::is_numeric()` is true for U+FF15 FULLWIDTH DIGIT FIVE, so
            // parse_percentage_value sets split_pos = idx + 1 = 1 and then slices
            // `input[1..]` — a byte index inside a 3-byte char -> panic.
            // Any of these is a CSS-reachable crash:
            assert!(parse_style_line_height("\u{FF15}").is_err()); // fullwidth 5
            assert!(parse_style_line_height("\u{0665}").is_err()); // arabic-indic 5
            assert!(parse_style_line_height("1\u{00B2}").is_err()); // superscript 2
        }
        // -- vertical-align -----------------------------------------------------
        #[test]
        fn vertical_align_round_trips_keywords_percentages_and_lengths() {
            type VA = StyleVerticalAlign;
            for v in [
                VA::Baseline, VA::Top, VA::Middle, VA::Bottom, VA::Sub, VA::Superscript,
                VA::TextTop, VA::TextBottom,
                VA::Percentage(PercentageValue::new(50.0)),
                VA::Percentage(PercentageValue::new(-25.0)),
                VA::Length(PixelValue::px(12.0)),
                VA::Length(PixelValue::em(1.5)),
            ] {
                let printed = v.print_as_css_value();
                assert_eq!(parse_style_vertical_align(&printed).unwrap(), v, "{printed:?}");
            }
            assert!(parse_style_vertical_align("").is_err());
            assert!(parse_style_vertical_align("%").is_err());
            assert!(parse_style_vertical_align("bogus%").is_err());
            assert!(parse_style_vertical_align("\u{1F600}").is_err());
        }
        // -- caret-* helpers ----------------------------------------------------
        #[test]
        fn caret_parsers_reject_garbage_and_accept_minimal_input() {
            assert_eq!(parse_caret_color("red").unwrap().inner, parse_style_text_color("red").unwrap().inner);
            assert!(parse_caret_color("").is_err());
            assert!(parse_caret_color("not-a-color").is_err());
            assert_eq!(parse_caret_width("2px").unwrap().inner, PixelValue::px(2.0));
            assert!(parse_caret_width("").is_err());
            assert!(parse_caret_animation_duration("bogus").is_err());
            assert!(parse_caret_animation_duration("500ms").is_ok());
        }
        // -- error type getters: to_contained / to_shared ------------------------
        /// Owned<->shared conversion must be lossless for every error family here.
        macro_rules! assert_error_round_trip {
            ($parse:ident, $($bad:expr),+ $(,)?) => {{
                $(
                    let e = $parse($bad).expect_err(concat!(stringify!($parse), " accepted ", $bad));
                    assert_eq!(e.to_contained().to_shared(), e, "{:?} via {}", $bad, stringify!($parse));
                )+
            }};
        }
        #[test]
        fn invalid_value_errors_round_trip_through_their_owned_form() {
            assert_error_round_trip!(parse_style_text_align, "", "middle", "\u{1F600}");
            assert_error_round_trip!(parse_style_white_space, "", "wrap");
            assert_error_round_trip!(parse_style_hyphens, "", "always");
            assert_error_round_trip!(parse_style_line_break, "", "tight");
            assert_error_round_trip!(parse_style_word_break, "", "break");
            assert_error_round_trip!(parse_style_overflow_wrap, "", "wrap");
            assert_error_round_trip!(parse_style_text_align_last, "", "middle");
            assert_error_round_trip!(parse_style_text_transform, "", "smallcaps");
            assert_error_round_trip!(parse_style_direction, "", "sideways");
            assert_error_round_trip!(parse_style_user_select, "", "some");
            assert_error_round_trip!(parse_style_text_decoration, "", "blink");
            assert_error_round_trip!(parse_style_vertical_align, "", "bogus");
            assert_error_round_trip!(parse_style_unicode_bidi, "", "override");
            assert_error_round_trip!(parse_style_text_box_trim, "", "trim");
            assert_error_round_trip!(parse_style_text_box_edge, "", "edge");
            assert_error_round_trip!(parse_style_dominant_baseline, "", "bogus");
            assert_error_round_trip!(parse_style_alignment_baseline, "", "bogus");
            assert_error_round_trip!(parse_style_baseline_source, "", "bogus");
            assert_error_round_trip!(parse_style_line_fit_edge, "", "bogus");
            assert_error_round_trip!(parse_style_initial_letter_align, "", "bogus");
            assert_error_round_trip!(parse_style_initial_letter_wrap, "", "bogus");
        }
        #[test]
        fn pixel_and_numeric_errors_round_trip_through_their_owned_form() {
            // EmptyString / ValueParseErr / NoValueGiven / InvalidPixelValue variants.
            assert_error_round_trip!(parse_style_letter_spacing, "", "abcpx", "px", "zz");
            assert_error_round_trip!(parse_style_word_spacing, "", "abcem", "em", "zz");
            assert_error_round_trip!(parse_style_text_indent, "abcpx", "zz");
            assert_error_round_trip!(parse_style_tab_size, "", "abcpx", "zz");
            assert_error_round_trip!(parse_style_line_height, "", "abc", "1.5em");
            assert_error_round_trip!(parse_style_initial_letter, "", "x", "0", "3 x");
            assert_error_round_trip!(parse_style_line_clamp, "", "x", "0");
            assert_error_round_trip!(parse_style_hanging_punctuation, "bogus", "force-end allow-end");
            assert_error_round_trip!(parse_style_text_combine_upright, "bogus", "digits 9");
        }
        #[test]
        fn text_color_error_round_trips_and_preserves_its_message() {
            let e = parse_style_text_color("not-a-color").unwrap_err();
            let owned = e.to_contained();
            let round_tripped = owned.to_shared();
            assert_eq!(format!("{e}"), format!("{round_tripped}"));
            assert!(parse_style_text_color("").is_err());
            assert!(parse_style_text_color("#gggggg").is_err());
            assert!(parse_style_text_color("\u{1F600}").is_err());
            // Positive control.
            assert_eq!(parse_style_text_color("#aabbcc").unwrap().inner.to_hash(), "#aabbccff");
        }
        #[test]
        fn error_types_survive_a_default_ish_extreme_instance() {
            // to_contained/to_shared must not panic on empty or huge payloads.
            let long = "z".repeat(100_000);
            let e = parse_style_text_align(&long).unwrap_err();
            assert_eq!(e.to_contained().to_shared(), e);
            let e = parse_style_line_clamp(&long).unwrap_err();
            assert_eq!(e.to_contained().to_shared(), e);
            let e = parse_style_hanging_punctuation(&long).unwrap_err();
            assert_eq!(e.to_contained().to_shared(), e);
            // Empty payload.
            let e = parse_style_letter_spacing("").unwrap_err();
            assert_eq!(e.to_contained().to_shared(), e);
        }
        // -- metric coverage ----------------------------------------------------
        #[test]
        fn letter_spacing_accepts_every_size_metric_it_prints() {
            for (unit, metric) in [
                ("px", SizeMetric::Px),
                ("pt", SizeMetric::Pt),
                ("em", SizeMetric::Em),
                ("rem", SizeMetric::Rem),
                ("in", SizeMetric::In),
                ("cm", SizeMetric::Cm),
                ("mm", SizeMetric::Mm),
                ("%", SizeMetric::Percent),
                ("vw", SizeMetric::Vw),
                ("vh", SizeMetric::Vh),
                ("vmax", SizeMetric::Vmax),
                // FIXED: `vmin` used to be unreachable — the suffix table tried "in"
                // before "vmin", so "1vmin" was stripped to "1vm" and failed to parse,
                // making every parse_pixel_value-backed property (letter-spacing,
                // word-spacing, text-indent, tab-size, vertical-align) reject a valid
                // CSS unit. The table now puts "vmin" ahead of "in".
                ("vmin", SizeMetric::Vmin),
            ] {
                let parsed = parse_style_letter_spacing(&format!("1{unit}")).unwrap();
                assert_eq!(parsed.inner.metric, metric, "unit {unit}");
                assert_eq!(parsed.inner.number.get(), 1.0);
            }
        }
    }
}