Skip to main content

azul_css/props/style/
text.rs

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
7use crate::corety::AzString;
8use alloc::string::{String, ToString};
9use core::fmt;
10
11use 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)]
32pub struct StyleTextColor {
33    pub inner: ColorU,
34}
35
36impl fmt::Debug for StyleTextColor {
37    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38        write!(f, "{}", self.print_as_css_value())
39    }
40}
41
42impl StyleTextColor {
43    #[must_use]
44    pub fn interpolate(&self, other: &Self, t: f32) -> Self {
45        Self {
46            inner: self.inner.interpolate(&other.inner, t),
47        }
48    }
49}
50
51impl PrintAsCssValue for StyleTextColor {
52    fn print_as_css_value(&self) -> String {
53        self.inner.to_hash()
54    }
55}
56
57// -- StyleTextAlign --
58
59/// Horizontal text alignment enum (left, center, right) - default: `Left`
60#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
61#[repr(C)]
62pub enum StyleTextAlign {
63    Left,
64    Center,
65    Right,
66    Justify,
67    #[default]
68    Start,
69    End,
70}
71
72impl_option!(
73    StyleTextAlign,
74    OptionStyleTextAlign,
75    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
76);
77
78impl PrintAsCssValue for StyleTextAlign {
79    fn print_as_css_value(&self) -> String {
80        String::from(match self {
81            Self::Left => "left",
82            Self::Center => "center",
83            Self::Right => "right",
84            Self::Justify => "justify",
85            Self::Start => "start",
86            Self::End => "end",
87        })
88    }
89}
90
91// -- StyleLetterSpacing --
92
93/// Represents a `letter-spacing` attribute
94#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
95#[repr(C)]
96pub struct StyleLetterSpacing {
97    pub inner: PixelValue,
98}
99
100impl fmt::Debug for StyleLetterSpacing {
101    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102        write!(f, "{}", self.inner)
103    }
104}
105impl Default for StyleLetterSpacing {
106    fn default() -> Self {
107        Self {
108            inner: PixelValue::const_px(0),
109        }
110    }
111}
112impl_pixel_value!(StyleLetterSpacing);
113impl PixelValueTaker for StyleLetterSpacing {
114    fn from_pixel_value(inner: PixelValue) -> Self {
115        Self { inner }
116    }
117}
118impl PrintAsCssValue for StyleLetterSpacing {
119    fn print_as_css_value(&self) -> String {
120        format!("{}", self.inner)
121    }
122}
123
124// -- StyleWordSpacing --
125
126/// Represents a `word-spacing` attribute
127#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
128#[repr(C)]
129pub struct StyleWordSpacing {
130    pub inner: PixelValue,
131}
132
133impl fmt::Debug for StyleWordSpacing {
134    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135        write!(f, "{}", self.inner)
136    }
137}
138impl Default for StyleWordSpacing {
139    fn default() -> Self {
140        Self {
141            inner: PixelValue::const_px(0),
142        }
143    }
144}
145impl_pixel_value!(StyleWordSpacing);
146impl PixelValueTaker for StyleWordSpacing {
147    fn from_pixel_value(inner: PixelValue) -> Self {
148        Self { inner }
149    }
150}
151impl PrintAsCssValue for StyleWordSpacing {
152    fn print_as_css_value(&self) -> String {
153        format!("{}", self.inner)
154    }
155}
156
157// -- StyleLineHeight --
158
159/// Represents a `line-height` attribute
160#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
161#[repr(C)]
162pub struct StyleLineHeight {
163    pub inner: PercentageValue,
164}
165impl Default for StyleLineHeight {
166    fn default() -> Self {
167        Self {
168            inner: PercentageValue::const_new(120),
169        }
170    }
171}
172impl_percentage_value!(StyleLineHeight);
173impl PrintAsCssValue for StyleLineHeight {
174    fn print_as_css_value(&self) -> String {
175        format!("{}", self.inner)
176    }
177}
178
179// -- StyleTabSize --
180
181/// Represents a `tab-size` attribute
182#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
183#[repr(C)]
184pub struct StyleTabSize {
185    pub inner: PixelValue, // Can be a number (space characters, em-based) or a length
186}
187
188impl fmt::Debug for StyleTabSize {
189    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190        write!(f, "{}", self.inner)
191    }
192}
193impl Default for StyleTabSize {
194    fn default() -> Self {
195        Self {
196            inner: PixelValue::em(8.0),
197        }
198    }
199}
200impl_pixel_value!(StyleTabSize);
201impl PixelValueTaker for StyleTabSize {
202    fn from_pixel_value(inner: PixelValue) -> Self {
203        Self { inner }
204    }
205}
206impl PrintAsCssValue for StyleTabSize {
207    fn print_as_css_value(&self) -> String {
208        format!("{}", self.inner)
209    }
210}
211
212// -- StyleWhiteSpace --
213
214/// How to handle white space inside an element.
215///
216/// CSS Text Level 3: <https://www.w3.org/TR/css-text-3/#white-space-property>
217#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
218#[repr(C)]
219#[derive(Default)]
220pub enum StyleWhiteSpace {
221    /// Collapse whitespace, wrap lines
222    #[default]
223    Normal,
224    /// Preserve whitespace, no wrap (except for explicit breaks)
225    Pre,
226    /// Collapse whitespace, no wrap
227    Nowrap,
228    /// Preserve whitespace, wrap lines
229    PreWrap,
230    /// Collapse whitespace (except newlines), wrap lines
231    PreLine,
232    /// Preserve whitespace, allow breaking at spaces
233    BreakSpaces,
234}
235impl_option!(
236    StyleWhiteSpace,
237    OptionStyleWhiteSpace,
238    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
239);
240impl PrintAsCssValue for StyleWhiteSpace {
241    fn print_as_css_value(&self) -> String {
242        String::from(match self {
243            Self::Normal => "normal",
244            Self::Pre => "pre",
245            Self::Nowrap => "nowrap",
246            Self::PreWrap => "pre-wrap",
247            Self::PreLine => "pre-line",
248            Self::BreakSpaces => "break-spaces",
249        })
250    }
251}
252
253// -- StyleHyphens --
254
255/// Hyphenation rules.
256#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
257#[repr(C)]
258#[derive(Default)]
259pub enum StyleHyphens {
260    /// No hyphenation: words are not broken at hyphenation opportunities.
261    None,
262    /// Manual hyphenation: words are only broken at explicit soft hyphens (U+00AD)
263    /// or unconditional hyphens (U+2010).
264    #[default]
265    Manual,
266    /// Automatic hyphenation: words may be broken at automatic hyphenation
267    /// opportunities determined by a language-appropriate hyphenation resource,
268    /// in addition to explicit opportunities.
269    Auto,
270}
271impl_option!(
272    StyleHyphens,
273    OptionStyleHyphens,
274    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
275);
276impl PrintAsCssValue for StyleHyphens {
277    fn print_as_css_value(&self) -> String {
278        String::from(match self {
279            Self::None => "none",
280            Self::Manual => "manual",
281            Self::Auto => "auto",
282        })
283    }
284}
285
286// -- StyleLineBreak --
287
288/// Controls the strictness of line breaking rules.
289///
290/// CSS Text Level 3: <https://www.w3.org/TR/css-text-3/#line-break-property>
291#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
292#[repr(C)]
293#[derive(Default)]
294pub enum StyleLineBreak {
295    /// The browser determines the set of line-breaking restrictions to use.
296    #[default]
297    Auto,
298    /// Breaks text using the least restrictive set of line-breaking rules.
299    Loose,
300    /// Breaks text using the most common set of line-breaking rules.
301    Normal,
302    /// Breaks text using the most stringent set of line-breaking rules.
303    Strict,
304    /// There is a soft wrap opportunity around every typographic character unit,
305    /// including around any punctuation character or preserved white spaces,
306    /// or in the middle of words, disregarding any prohibition against line breaks.
307    Anywhere,
308}
309impl_option!(
310    StyleLineBreak,
311    OptionStyleLineBreak,
312    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
313);
314impl PrintAsCssValue for StyleLineBreak {
315    fn print_as_css_value(&self) -> String {
316        String::from(match self {
317            Self::Auto => "auto",
318            Self::Loose => "loose",
319            Self::Normal => "normal",
320            Self::Strict => "strict",
321            Self::Anywhere => "anywhere",
322        })
323    }
324}
325
326// -- StyleWordBreak --
327
328/// Controls line breaking rules within words.
329///
330/// CSS Text Level 3 §5.2: <https://www.w3.org/TR/css-text-3/#word-break-property>
331#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
332#[repr(C)]
333#[derive(Default)]
334pub enum StyleWordBreak {
335    /// Use default line break rules.
336    #[default]
337    Normal,
338    /// Allow break opportunities between any two characters (CJK and non-CJK).
339    BreakAll,
340    /// Forbid break opportunities within CJK character sequences.
341    KeepAll,
342    // +spec:line-breaking:815882 - deprecated break-word keyword: same as normal + overflow-wrap: anywhere
343    /// Deprecated: equivalent to word-break: normal and overflow-wrap: anywhere.
344    BreakWord,
345}
346impl_option!(
347    StyleWordBreak,
348    OptionStyleWordBreak,
349    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
350);
351impl PrintAsCssValue for StyleWordBreak {
352    fn print_as_css_value(&self) -> String {
353        String::from(match self {
354            Self::Normal => "normal",
355            Self::BreakAll => "break-all",
356            Self::KeepAll => "keep-all",
357            Self::BreakWord => "break-word",
358        })
359    }
360}
361
362// -- StyleOverflowWrap --
363
364/// Controls whether the browser may break at otherwise disallowed points
365/// to prevent overflow.
366///
367/// CSS Text Level 3 §3.3: <https://www.w3.org/TR/css-text-3/#overflow-wrap-property>
368#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
369#[repr(C)]
370#[derive(Default)]
371pub enum StyleOverflowWrap {
372    /// Lines may only break at allowed break points.
373    #[default]
374    Normal,
375    /// An otherwise unbreakable sequence may be broken at an arbitrary point
376    /// if there are no otherwise acceptable break points.
377    Anywhere,
378    /// Same as `anywhere` but soft wrap opportunities introduced are not
379    /// considered when calculating min-content intrinsic sizes.
380    BreakWord,
381}
382impl_option!(
383    StyleOverflowWrap,
384    OptionStyleOverflowWrap,
385    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
386);
387impl PrintAsCssValue for StyleOverflowWrap {
388    fn print_as_css_value(&self) -> String {
389        String::from(match self {
390            Self::Normal => "normal",
391            Self::Anywhere => "anywhere",
392            Self::BreakWord => "break-word",
393        })
394    }
395}
396
397// -- StyleTextAlignLast --
398
399/// Controls alignment of the last line of a block or a line right before
400/// a forced line break.
401///
402/// CSS Text Level 3 §7.2: <https://www.w3.org/TR/css-text-3/#text-align-last-property>
403#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
404#[repr(C)]
405#[derive(Default)]
406pub enum StyleTextAlignLast {
407    /// Alignment of the last line is determined by text-align (or start if justify).
408    #[default]
409    Auto,
410    /// Align to the start edge of the line box.
411    Start,
412    /// Align to the end edge of the line box.
413    End,
414    /// Align to the line left.
415    Left,
416    /// Align to the line right.
417    Right,
418    /// Center the content.
419    Center,
420    /// Justify the content.
421    Justify,
422}
423impl_option!(
424    StyleTextAlignLast,
425    OptionStyleTextAlignLast,
426    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
427);
428impl PrintAsCssValue for StyleTextAlignLast {
429    fn print_as_css_value(&self) -> String {
430        String::from(match self {
431            Self::Auto => "auto",
432            Self::Start => "start",
433            Self::End => "end",
434            Self::Left => "left",
435            Self::Right => "right",
436            Self::Center => "center",
437            Self::Justify => "justify",
438        })
439    }
440}
441
442// -- StyleTextTransform --
443
444/// Controls capitalization of a text run (applied before shaping).
445///
446/// CSS Text Level 3 §2.1: <https://www.w3.org/TR/css-text-3/#text-transform-property>
447#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
448#[repr(C)]
449#[derive(Default)]
450pub enum StyleTextTransform {
451    /// No capitalization effect.
452    #[default]
453    None,
454    /// Uppercase the first typographic letter unit of each word.
455    Capitalize,
456    /// Uppercase every typographic letter unit.
457    Uppercase,
458    /// Lowercase every typographic letter unit.
459    Lowercase,
460    /// Map to the full-width form where available.
461    FullWidth,
462}
463impl_option!(
464    StyleTextTransform,
465    OptionStyleTextTransform,
466    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
467);
468impl PrintAsCssValue for StyleTextTransform {
469    fn print_as_css_value(&self) -> String {
470        String::from(match self {
471            Self::None => "none",
472            Self::Capitalize => "capitalize",
473            Self::Uppercase => "uppercase",
474            Self::Lowercase => "lowercase",
475            Self::FullWidth => "full-width",
476        })
477    }
478}
479
480// -- StyleDirection --
481
482/// Text direction.
483// +spec:writing-modes:46fed3 - direction property provides explicit bidi controls in CSS
484#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
485#[repr(C)]
486#[derive(Default)]
487pub enum StyleDirection {
488    /// Left-to-right text direction
489    #[default]
490    Ltr,
491    /// Right-to-left text direction
492    Rtl,
493}
494impl_option!(
495    StyleDirection,
496    OptionStyleDirection,
497    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
498);
499impl PrintAsCssValue for StyleDirection {
500    fn print_as_css_value(&self) -> String {
501        String::from(match self {
502            Self::Ltr => "ltr",
503            Self::Rtl => "rtl",
504        })
505    }
506}
507
508// -- StyleUserSelect --
509
510/// Controls whether the user can select text.
511/// Used to prevent accidental text selection on UI controls like buttons.
512#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
513#[repr(C)]
514#[derive(Default)]
515pub enum StyleUserSelect {
516    /// Browser determines selectability (default)
517    #[default]
518    Auto,
519    /// Text is selectable
520    Text,
521    /// Text is not selectable
522    None,
523    /// User can select all text with a single action
524    All,
525}
526impl_option!(
527    StyleUserSelect,
528    OptionStyleUserSelect,
529    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
530);
531impl PrintAsCssValue for StyleUserSelect {
532    fn print_as_css_value(&self) -> String {
533        String::from(match self {
534            Self::Auto => "auto",
535            Self::Text => "text",
536            Self::None => "none",
537            Self::All => "all",
538        })
539    }
540}
541
542// -- StyleTextDecoration --
543
544/// Text decoration (underline, overline, line-through).
545#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
546#[repr(C)]
547#[derive(Default)]
548pub enum StyleTextDecoration {
549    /// No decoration
550    #[default]
551    None,
552    /// Underline
553    Underline,
554    /// Line above text
555    Overline,
556    /// Strike-through line
557    LineThrough,
558}
559impl_option!(
560    StyleTextDecoration,
561    OptionStyleTextDecoration,
562    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
563);
564impl PrintAsCssValue for StyleTextDecoration {
565    fn print_as_css_value(&self) -> String {
566        String::from(match self {
567            Self::None => "none",
568            Self::Underline => "underline",
569            Self::Overline => "overline",
570            Self::LineThrough => "line-through",
571        })
572    }
573}
574
575// -- StyleVerticalAlign --
576
577/// CSS 2.2 §10.8.1 vertical-align property values
578#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
579#[repr(C, u8)]
580#[derive(Default)]
581pub enum StyleVerticalAlign {
582    /// CSS default - align baselines
583    #[default]
584    Baseline,
585    /// Align top of element with top of line box
586    Top,
587    /// Align middle of element with baseline + half x-height
588    Middle,
589    /// Align bottom of element with bottom of line box
590    Bottom,
591    /// Align baseline with parent's subscript baseline
592    Sub,
593    /// Align baseline with parent's superscript baseline
594    Superscript,
595    /// Align top with top of parent's font
596    TextTop,
597    /// Align bottom with bottom of parent's font
598    TextBottom,
599    /// <percentage> refers to line-height of the element itself
600    Percentage(PercentageValue),
601    /// <length> offset from baseline
602    Length(PixelValue),
603}
604
605impl_option!(
606    StyleVerticalAlign,
607    OptionStyleVerticalAlign,
608    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
609);
610
611impl PrintAsCssValue for StyleVerticalAlign {
612    fn print_as_css_value(&self) -> String {
613        match self {
614            Self::Baseline => String::from("baseline"),
615            Self::Top => String::from("top"),
616            Self::Middle => String::from("middle"),
617            Self::Bottom => String::from("bottom"),
618            Self::Sub => String::from("sub"),
619            Self::Superscript => String::from("super"),
620            Self::TextTop => String::from("text-top"),
621            Self::TextBottom => String::from("text-bottom"),
622            Self::Percentage(p) => format!("{}%", p.normalized() * 100.0),
623            Self::Length(l) => l.print_as_css_value(),
624        }
625    }
626}
627
628impl FormatAsRustCode for StyleVerticalAlign {
629    fn format_as_rust_code(&self, indent: usize) -> String {
630        match self {
631            Self::Baseline => "StyleVerticalAlign::Baseline".to_string(),
632            Self::Top => "StyleVerticalAlign::Top".to_string(),
633            Self::Middle => "StyleVerticalAlign::Middle".to_string(),
634            Self::Bottom => "StyleVerticalAlign::Bottom".to_string(),
635            Self::Sub => "StyleVerticalAlign::Sub".to_string(),
636            Self::Superscript => "StyleVerticalAlign::Superscript".to_string(),
637            Self::TextTop => "StyleVerticalAlign::TextTop".to_string(),
638            Self::TextBottom => "StyleVerticalAlign::TextBottom".to_string(),
639            Self::Percentage(p) => format!(
640                "StyleVerticalAlign::Percentage(PercentageValue::new({}))",
641                p.normalized() * 100.0
642            ),
643            Self::Length(l) => format!("StyleVerticalAlign::Length({l})"),
644        }
645    }
646}
647
648// --- PARSERS ---
649
650#[cfg(feature = "parser")]
651use crate::props::basic::{
652    color::{parse_css_color, CssColorParseError, CssColorParseErrorOwned},
653    DurationParseError,
654};
655
656#[cfg(feature = "parser")]
657#[derive(Clone, PartialEq)]
658pub enum StyleTextColorParseError<'a> {
659    ColorParseError(CssColorParseError<'a>),
660}
661#[cfg(feature = "parser")]
662impl_debug_as_display!(StyleTextColorParseError<'a>);
663#[cfg(feature = "parser")]
664impl_display! { StyleTextColorParseError<'a>, {
665    ColorParseError(e) => format!("Invalid color: {}", e),
666}}
667#[cfg(feature = "parser")]
668impl_from!(
669    CssColorParseError<'a>,
670    StyleTextColorParseError::ColorParseError
671);
672
673#[cfg(feature = "parser")]
674#[derive(Debug, Clone, PartialEq)]
675#[repr(C, u8)]
676pub enum StyleTextColorParseErrorOwned {
677    ColorParseError(CssColorParseErrorOwned),
678}
679
680#[cfg(feature = "parser")]
681impl StyleTextColorParseError<'_> {
682    #[must_use]
683    pub fn to_contained(&self) -> StyleTextColorParseErrorOwned {
684        match self {
685            Self::ColorParseError(e) => {
686                StyleTextColorParseErrorOwned::ColorParseError(e.to_contained())
687            }
688        }
689    }
690}
691
692#[cfg(feature = "parser")]
693impl StyleTextColorParseErrorOwned {
694    #[must_use]
695    pub fn to_shared(&self) -> StyleTextColorParseError<'_> {
696        match self {
697            Self::ColorParseError(e) => StyleTextColorParseError::ColorParseError(e.to_shared()),
698        }
699    }
700}
701
702#[cfg(feature = "parser")]
703/// # Errors
704///
705/// Returns an error if `input` is not a valid CSS `text-color` value.
706pub fn parse_style_text_color(input: &str) -> Result<StyleTextColor, StyleTextColorParseError<'_>> {
707    parse_css_color(input)
708        .map(|inner| StyleTextColor { inner })
709        .map_err(StyleTextColorParseError::ColorParseError)
710}
711
712#[cfg(feature = "parser")]
713#[derive(Clone, PartialEq, Eq)]
714pub enum StyleTextAlignParseError<'a> {
715    InvalidValue(InvalidValueErr<'a>),
716}
717#[cfg(feature = "parser")]
718impl_debug_as_display!(StyleTextAlignParseError<'a>);
719#[cfg(feature = "parser")]
720impl_display! { StyleTextAlignParseError<'a>, {
721    InvalidValue(e) => format!("Invalid text-align value: \"{}\"", e.0),
722}}
723#[cfg(feature = "parser")]
724impl_from!(InvalidValueErr<'a>, StyleTextAlignParseError::InvalidValue);
725
726#[cfg(feature = "parser")]
727#[derive(Debug, Clone, PartialEq, Eq)]
728#[repr(C, u8)]
729pub enum StyleTextAlignParseErrorOwned {
730    InvalidValue(InvalidValueErrOwned),
731}
732
733#[cfg(feature = "parser")]
734impl StyleTextAlignParseError<'_> {
735    #[must_use]
736    pub fn to_contained(&self) -> StyleTextAlignParseErrorOwned {
737        match self {
738            Self::InvalidValue(e) => StyleTextAlignParseErrorOwned::InvalidValue(e.to_contained()),
739        }
740    }
741}
742
743#[cfg(feature = "parser")]
744impl StyleTextAlignParseErrorOwned {
745    #[must_use]
746    pub fn to_shared(&self) -> StyleTextAlignParseError<'_> {
747        match self {
748            Self::InvalidValue(e) => StyleTextAlignParseError::InvalidValue(e.to_shared()),
749        }
750    }
751}
752
753#[cfg(feature = "parser")]
754/// # Errors
755///
756/// Returns an error if `input` is not a valid CSS `text-align` value.
757pub fn parse_style_text_align(input: &str) -> Result<StyleTextAlign, StyleTextAlignParseError<'_>> {
758    match input.trim() {
759        "left" => Ok(StyleTextAlign::Left),
760        "center" => Ok(StyleTextAlign::Center),
761        "right" => Ok(StyleTextAlign::Right),
762        "justify" => Ok(StyleTextAlign::Justify),
763        "start" => Ok(StyleTextAlign::Start),
764        "end" => Ok(StyleTextAlign::End),
765        other => Err(StyleTextAlignParseError::InvalidValue(InvalidValueErr(
766            other,
767        ))),
768    }
769}
770
771#[cfg(feature = "parser")]
772#[derive(Clone, PartialEq, Eq)]
773pub enum StyleLetterSpacingParseError<'a> {
774    PixelValue(CssPixelValueParseError<'a>),
775}
776#[cfg(feature = "parser")]
777impl_debug_as_display!(StyleLetterSpacingParseError<'a>);
778#[cfg(feature = "parser")]
779impl_display! { StyleLetterSpacingParseError<'a>, {
780    PixelValue(e) => format!("Invalid letter-spacing value: {}", e),
781}}
782#[cfg(feature = "parser")]
783impl_from!(
784    CssPixelValueParseError<'a>,
785    StyleLetterSpacingParseError::PixelValue
786);
787
788#[cfg(feature = "parser")]
789#[derive(Debug, Clone, PartialEq, Eq)]
790#[repr(C, u8)]
791pub enum StyleLetterSpacingParseErrorOwned {
792    PixelValue(CssPixelValueParseErrorOwned),
793}
794
795#[cfg(feature = "parser")]
796impl StyleLetterSpacingParseError<'_> {
797    #[must_use]
798    pub fn to_contained(&self) -> StyleLetterSpacingParseErrorOwned {
799        match self {
800            Self::PixelValue(e) => StyleLetterSpacingParseErrorOwned::PixelValue(e.to_contained()),
801        }
802    }
803}
804
805#[cfg(feature = "parser")]
806impl StyleLetterSpacingParseErrorOwned {
807    #[must_use]
808    pub fn to_shared(&self) -> StyleLetterSpacingParseError<'_> {
809        match self {
810            Self::PixelValue(e) => StyleLetterSpacingParseError::PixelValue(e.to_shared()),
811        }
812    }
813}
814
815#[cfg(feature = "parser")]
816/// # Errors
817///
818/// Returns an error if `input` is not a valid CSS `letter-spacing` value.
819pub fn parse_style_letter_spacing(
820    input: &str,
821) -> Result<StyleLetterSpacing, StyleLetterSpacingParseError<'_>> {
822    crate::props::basic::pixel::parse_pixel_value(input)
823        .map(|inner| StyleLetterSpacing { inner })
824        .map_err(StyleLetterSpacingParseError::PixelValue)
825}
826
827// -- StyleTextIndent (text-indent property) --
828
829/// Represents a `text-indent` attribute (indentation of first line in a block).
830#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
831#[repr(C)]
832pub struct StyleTextIndent {
833    pub inner: PixelValue,
834    /// `each-line` keyword: indent first line of each block container
835    /// AND each line after a forced line break (but not after soft wrap).
836    pub each_line: bool,
837    /// `hanging` keyword: inverts which lines are affected by the indent.
838    pub hanging: bool,
839}
840
841impl fmt::Debug for StyleTextIndent {
842    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
843        write!(f, "{}", self.print_as_css_value())
844    }
845}
846
847impl StyleTextIndent {
848    #[inline]
849    #[must_use]
850    pub const fn zero() -> Self {
851        Self {
852            inner: PixelValue::zero(),
853            each_line: false,
854            hanging: false,
855        }
856    }
857    #[inline]
858    #[must_use]
859    pub const fn const_px(value: isize) -> Self {
860        Self {
861            inner: PixelValue::const_px(value),
862            each_line: false,
863            hanging: false,
864        }
865    }
866    #[inline]
867    #[must_use]
868    pub const fn const_em(value: isize) -> Self {
869        Self {
870            inner: PixelValue::const_em(value),
871            each_line: false,
872            hanging: false,
873        }
874    }
875    #[inline]
876    #[must_use]
877    pub const fn const_pt(value: isize) -> Self {
878        Self {
879            inner: PixelValue::const_pt(value),
880            each_line: false,
881            hanging: false,
882        }
883    }
884    #[inline]
885    #[must_use]
886    pub const fn const_percent(value: isize) -> Self {
887        Self {
888            inner: PixelValue::const_percent(value),
889            each_line: false,
890            hanging: false,
891        }
892    }
893    #[inline]
894    #[must_use]
895    pub const fn const_in(value: isize) -> Self {
896        Self {
897            inner: PixelValue::const_in(value),
898            each_line: false,
899            hanging: false,
900        }
901    }
902    #[inline]
903    #[must_use]
904    pub const fn const_cm(value: isize) -> Self {
905        Self {
906            inner: PixelValue::const_cm(value),
907            each_line: false,
908            hanging: false,
909        }
910    }
911    #[inline]
912    #[must_use]
913    pub const fn const_mm(value: isize) -> Self {
914        Self {
915            inner: PixelValue::const_mm(value),
916            each_line: false,
917            hanging: false,
918        }
919    }
920    #[inline]
921    #[must_use]
922    pub const fn const_from_metric(
923        metric: crate::props::basic::length::SizeMetric,
924        value: isize,
925    ) -> Self {
926        Self {
927            inner: PixelValue::const_from_metric(metric, value),
928            each_line: false,
929            hanging: false,
930        }
931    }
932    #[inline]
933    #[must_use]
934    pub fn px(value: f32) -> Self {
935        Self {
936            inner: PixelValue::px(value),
937            each_line: false,
938            hanging: false,
939        }
940    }
941    #[inline]
942    #[must_use]
943    pub fn em(value: f32) -> Self {
944        Self {
945            inner: PixelValue::em(value),
946            each_line: false,
947            hanging: false,
948        }
949    }
950    #[inline]
951    #[must_use]
952    pub fn pt(value: f32) -> Self {
953        Self {
954            inner: PixelValue::pt(value),
955            each_line: false,
956            hanging: false,
957        }
958    }
959    #[inline]
960    #[must_use]
961    pub fn percent(value: f32) -> Self {
962        Self {
963            inner: PixelValue::percent(value),
964            each_line: false,
965            hanging: false,
966        }
967    }
968    #[inline]
969    #[must_use]
970    pub fn from_metric(metric: crate::props::basic::length::SizeMetric, value: f32) -> Self {
971        Self {
972            inner: PixelValue::from_metric(metric, value),
973            each_line: false,
974            hanging: false,
975        }
976    }
977    #[inline]
978    #[must_use]
979    pub fn interpolate(&self, other: &Self, t: f32) -> Self {
980        Self {
981            inner: self.inner.interpolate(&other.inner, t),
982            each_line: self.each_line,
983            hanging: self.hanging,
984        }
985    }
986}
987
988impl PrintAsCssValue for StyleTextIndent {
989    fn print_as_css_value(&self) -> String {
990        let mut s = self.inner.to_string();
991        if self.hanging {
992            s.push_str(" hanging");
993        }
994        if self.each_line {
995            s.push_str(" each-line");
996        }
997        s
998    }
999}
1000
1001impl FormatAsRustCode for StyleTextIndent {
1002    fn format_as_rust_code(&self, _tabs: usize) -> String {
1003        format!(
1004            "StyleTextIndent {{ inner: {}, each_line: {}, hanging: {} }}",
1005            self.inner.format_as_rust_code(0),
1006            self.each_line,
1007            self.hanging
1008        )
1009    }
1010}
1011
1012#[cfg(feature = "parser")]
1013#[derive(Clone, PartialEq, Eq)]
1014pub enum StyleTextIndentParseError<'a> {
1015    PixelValue(CssPixelValueParseError<'a>),
1016}
1017#[cfg(feature = "parser")]
1018impl_debug_as_display!(StyleTextIndentParseError<'a>);
1019#[cfg(feature = "parser")]
1020impl_display! { StyleTextIndentParseError<'a>, {
1021    PixelValue(e) => format!("Invalid text-indent value: {}", e),
1022}}
1023#[cfg(feature = "parser")]
1024impl_from!(
1025    CssPixelValueParseError<'a>,
1026    StyleTextIndentParseError::PixelValue
1027);
1028
1029#[cfg(feature = "parser")]
1030#[derive(Debug, Clone, PartialEq, Eq)]
1031#[repr(C, u8)]
1032pub enum StyleTextIndentParseErrorOwned {
1033    PixelValue(CssPixelValueParseErrorOwned),
1034}
1035
1036#[cfg(feature = "parser")]
1037impl StyleTextIndentParseError<'_> {
1038    #[must_use]
1039    pub fn to_contained(&self) -> StyleTextIndentParseErrorOwned {
1040        match self {
1041            Self::PixelValue(e) => StyleTextIndentParseErrorOwned::PixelValue(e.to_contained()),
1042        }
1043    }
1044}
1045
1046#[cfg(feature = "parser")]
1047impl StyleTextIndentParseErrorOwned {
1048    #[must_use]
1049    pub fn to_shared(&self) -> StyleTextIndentParseError<'_> {
1050        match self {
1051            Self::PixelValue(e) => StyleTextIndentParseError::PixelValue(e.to_shared()),
1052        }
1053    }
1054}
1055
1056#[cfg(feature = "parser")]
1057/// # Errors
1058///
1059/// Returns an error if `input` is not a valid CSS `text-indent` value.
1060pub fn parse_style_text_indent(
1061    input: &str,
1062) -> Result<StyleTextIndent, StyleTextIndentParseError<'_>> {
1063    let mut each_line = false;
1064    let mut hanging = false;
1065    let mut pixel_part: Option<&str> = None;
1066
1067    for token in input.split_whitespace() {
1068        match token {
1069            "each-line" => each_line = true,
1070            "hanging" => hanging = true,
1071            _ => {
1072                pixel_part = Some(token);
1073            }
1074        }
1075    }
1076
1077    let pixel_str = pixel_part.unwrap_or("0px");
1078
1079    crate::props::basic::pixel::parse_pixel_value(pixel_str)
1080        .map(|inner| StyleTextIndent {
1081            inner,
1082            each_line,
1083            hanging,
1084        })
1085        .map_err(StyleTextIndentParseError::PixelValue)
1086}
1087
1088/// initial-letter property for drop caps
1089#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1090#[repr(C)]
1091pub struct StyleInitialLetter {
1092    pub size: u32,
1093    pub sink: crate::corety::OptionU32,
1094}
1095
1096impl FormatAsRustCode for StyleInitialLetter {
1097    fn format_as_rust_code(&self, _tabs: usize) -> String {
1098        format!("{self:?}")
1099    }
1100}
1101
1102impl PrintAsCssValue for StyleInitialLetter {
1103    fn print_as_css_value(&self) -> String {
1104        if let crate::corety::OptionU32::Some(sink) = self.sink {
1105            format!("{} {}", self.size, sink)
1106        } else {
1107            format!("{}", self.size)
1108        }
1109    }
1110}
1111
1112#[cfg(feature = "parser")]
1113#[derive(Clone, PartialEq, Eq)]
1114pub enum StyleInitialLetterParseError<'a> {
1115    InvalidFormat(&'a str),
1116    InvalidSize(&'a str),
1117    InvalidSink(&'a str),
1118}
1119#[cfg(feature = "parser")]
1120impl_debug_as_display!(StyleInitialLetterParseError<'a>);
1121#[cfg(feature = "parser")]
1122impl_display! { StyleInitialLetterParseError<'a>, {
1123    InvalidFormat(e) => format!("Invalid initial-letter format: {}", e),
1124    InvalidSize(e) => format!("Invalid initial-letter size: {}", e),
1125    InvalidSink(e) => format!("Invalid initial-letter sink: {}", e),
1126}}
1127
1128#[cfg(feature = "parser")]
1129#[derive(Debug, Clone, PartialEq, Eq)]
1130#[repr(C, u8)]
1131pub enum StyleInitialLetterParseErrorOwned {
1132    InvalidFormat(AzString),
1133    InvalidSize(AzString),
1134    InvalidSink(AzString),
1135}
1136
1137#[cfg(feature = "parser")]
1138impl StyleInitialLetterParseError<'_> {
1139    #[must_use]
1140    pub fn to_contained(&self) -> StyleInitialLetterParseErrorOwned {
1141        match self {
1142            Self::InvalidFormat(s) => {
1143                StyleInitialLetterParseErrorOwned::InvalidFormat((*s).to_string().into())
1144            }
1145            Self::InvalidSize(s) => {
1146                StyleInitialLetterParseErrorOwned::InvalidSize((*s).to_string().into())
1147            }
1148            Self::InvalidSink(s) => {
1149                StyleInitialLetterParseErrorOwned::InvalidSink((*s).to_string().into())
1150            }
1151        }
1152    }
1153}
1154
1155#[cfg(feature = "parser")]
1156impl StyleInitialLetterParseErrorOwned {
1157    #[must_use]
1158    pub fn to_shared(&self) -> StyleInitialLetterParseError<'_> {
1159        match self {
1160            Self::InvalidFormat(s) => StyleInitialLetterParseError::InvalidFormat(s.as_str()),
1161            Self::InvalidSize(s) => StyleInitialLetterParseError::InvalidSize(s.as_str()),
1162            Self::InvalidSink(s) => StyleInitialLetterParseError::InvalidSink(s.as_str()),
1163        }
1164    }
1165}
1166
1167#[cfg(feature = "parser")]
1168impl From<StyleInitialLetterParseError<'_>> for StyleInitialLetterParseErrorOwned {
1169    fn from(e: StyleInitialLetterParseError<'_>) -> Self {
1170        match e {
1171            StyleInitialLetterParseError::InvalidFormat(s) => {
1172                Self::InvalidFormat(s.to_string().into())
1173            }
1174            StyleInitialLetterParseError::InvalidSize(s) => Self::InvalidSize(s.to_string().into()),
1175            StyleInitialLetterParseError::InvalidSink(s) => Self::InvalidSink(s.to_string().into()),
1176        }
1177    }
1178}
1179
1180#[cfg(feature = "parser")]
1181impl_display! { StyleInitialLetterParseErrorOwned, {
1182    InvalidFormat(e) => format!("Invalid initial-letter format: {}", e),
1183    InvalidSize(e) => format!("Invalid initial-letter size: {}", e),
1184    InvalidSink(e) => format!("Invalid initial-letter sink: {}", e),
1185}}
1186
1187#[cfg(feature = "parser")]
1188/// # Errors
1189///
1190/// Returns an error if `input` is not a valid CSS `initial-letter` value.
1191pub fn parse_style_initial_letter(
1192    input: &str,
1193) -> Result<StyleInitialLetter, StyleInitialLetterParseError<'_>> {
1194    let input = input.trim();
1195    let parts: Vec<&str> = input.split_whitespace().collect();
1196
1197    if parts.is_empty() {
1198        return Err(StyleInitialLetterParseError::InvalidFormat(input));
1199    }
1200
1201    // Parse size (required)
1202    let size = parts[0]
1203        .parse::<u32>()
1204        .map_err(|_| StyleInitialLetterParseError::InvalidSize(parts[0]))?;
1205
1206    if size == 0 {
1207        return Err(StyleInitialLetterParseError::InvalidSize(parts[0]));
1208    }
1209
1210    // Parse sink (optional)
1211    let sink = if parts.len() > 1 {
1212        crate::corety::OptionU32::Some(
1213            parts[1]
1214                .parse::<u32>()
1215                .map_err(|_| StyleInitialLetterParseError::InvalidSink(parts[1]))?,
1216        )
1217    } else {
1218        crate::corety::OptionU32::None
1219    };
1220
1221    Ok(StyleInitialLetter { size, sink })
1222}
1223
1224/// line-clamp property for limiting visible lines
1225#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1226#[repr(C)]
1227pub struct StyleLineClamp {
1228    pub max_lines: usize,
1229}
1230
1231impl FormatAsRustCode for StyleLineClamp {
1232    fn format_as_rust_code(&self, _tabs: usize) -> String {
1233        format!("{self:?}")
1234    }
1235}
1236
1237impl PrintAsCssValue for StyleLineClamp {
1238    fn print_as_css_value(&self) -> String {
1239        format!("{}", self.max_lines)
1240    }
1241}
1242
1243#[cfg(feature = "parser")]
1244#[derive(Clone, PartialEq, Eq)]
1245pub enum StyleLineClampParseError<'a> {
1246    InvalidValue(&'a str),
1247    ZeroValue,
1248}
1249#[cfg(feature = "parser")]
1250impl_debug_as_display!(StyleLineClampParseError<'a>);
1251#[cfg(feature = "parser")]
1252impl_display! { StyleLineClampParseError<'a>, {
1253    InvalidValue(e) => format!("Invalid line-clamp value: {}", e),
1254    ZeroValue => format!("line-clamp cannot be zero"),
1255}}
1256#[allow(variant_size_differences)]
1257// repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
1258#[cfg(feature = "parser")]
1259#[derive(Debug, Clone, PartialEq, Eq)]
1260#[repr(C, u8)]
1261pub enum StyleLineClampParseErrorOwned {
1262    InvalidValue(AzString),
1263    ZeroValue,
1264}
1265
1266#[cfg(feature = "parser")]
1267impl StyleLineClampParseError<'_> {
1268    #[must_use]
1269    pub fn to_contained(&self) -> StyleLineClampParseErrorOwned {
1270        match self {
1271            Self::InvalidValue(s) => {
1272                StyleLineClampParseErrorOwned::InvalidValue((*s).to_string().into())
1273            }
1274            Self::ZeroValue => StyleLineClampParseErrorOwned::ZeroValue,
1275        }
1276    }
1277}
1278
1279#[cfg(feature = "parser")]
1280impl StyleLineClampParseErrorOwned {
1281    #[must_use]
1282    pub fn to_shared(&self) -> StyleLineClampParseError<'_> {
1283        match self {
1284            Self::InvalidValue(s) => StyleLineClampParseError::InvalidValue(s.as_str()),
1285            Self::ZeroValue => StyleLineClampParseError::ZeroValue,
1286        }
1287    }
1288}
1289
1290#[cfg(feature = "parser")]
1291impl From<StyleLineClampParseError<'_>> for StyleLineClampParseErrorOwned {
1292    fn from(e: StyleLineClampParseError<'_>) -> Self {
1293        e.to_contained()
1294    }
1295}
1296
1297#[cfg(feature = "parser")]
1298impl_display! { StyleLineClampParseErrorOwned, {
1299    InvalidValue(e) => format!("Invalid line-clamp value: {}", e),
1300    ZeroValue => format!("line-clamp cannot be zero"),
1301}}
1302
1303#[cfg(feature = "parser")]
1304/// # Errors
1305///
1306/// Returns an error if `input` is not a valid CSS `line-clamp` value.
1307pub fn parse_style_line_clamp(input: &str) -> Result<StyleLineClamp, StyleLineClampParseError<'_>> {
1308    let input = input.trim();
1309
1310    let max_lines = input
1311        .parse::<usize>()
1312        .map_err(|_| StyleLineClampParseError::InvalidValue(input))?;
1313
1314    if max_lines == 0 {
1315        return Err(StyleLineClampParseError::ZeroValue);
1316    }
1317
1318    Ok(StyleLineClamp { max_lines })
1319}
1320
1321/// hanging-punctuation property for hanging punctuation marks
1322///
1323/// CSS Text 3 §8: `none | [ first || [ force-end | allow-end ] || last ]`
1324#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1325#[repr(C)]
1326#[derive(Default)]
1327pub struct StyleHangingPunctuation {
1328    pub first: bool,
1329    pub force_end: bool,
1330    pub allow_end: bool,
1331    pub last: bool,
1332}
1333
1334impl StyleHangingPunctuation {
1335    #[must_use]
1336    pub const fn is_enabled(&self) -> bool {
1337        self.first || self.force_end || self.allow_end || self.last
1338    }
1339}
1340
1341impl FormatAsRustCode for StyleHangingPunctuation {
1342    fn format_as_rust_code(&self, _tabs: usize) -> String {
1343        format!("{self:?}")
1344    }
1345}
1346
1347impl PrintAsCssValue for StyleHangingPunctuation {
1348    fn print_as_css_value(&self) -> String {
1349        if !self.is_enabled() {
1350            return "none".to_string();
1351        }
1352        let mut parts = Vec::new();
1353        if self.first {
1354            parts.push("first");
1355        }
1356        if self.force_end {
1357            parts.push("force-end");
1358        }
1359        if self.allow_end {
1360            parts.push("allow-end");
1361        }
1362        if self.last {
1363            parts.push("last");
1364        }
1365        parts.join(" ")
1366    }
1367}
1368
1369#[cfg(feature = "parser")]
1370#[derive(Clone, PartialEq, Eq)]
1371pub enum StyleHangingPunctuationParseError<'a> {
1372    InvalidValue(&'a str),
1373}
1374#[cfg(feature = "parser")]
1375impl_debug_as_display!(StyleHangingPunctuationParseError<'a>);
1376#[cfg(feature = "parser")]
1377impl_display! { StyleHangingPunctuationParseError<'a>, {
1378    InvalidValue(e) => format!("Invalid hanging-punctuation value: {}", e),
1379}}
1380
1381#[cfg(feature = "parser")]
1382#[derive(Debug, Clone, PartialEq, Eq)]
1383#[repr(C, u8)]
1384pub enum StyleHangingPunctuationParseErrorOwned {
1385    InvalidValue(AzString),
1386}
1387
1388#[cfg(feature = "parser")]
1389impl StyleHangingPunctuationParseError<'_> {
1390    #[must_use]
1391    pub fn to_contained(&self) -> StyleHangingPunctuationParseErrorOwned {
1392        match self {
1393            Self::InvalidValue(s) => {
1394                StyleHangingPunctuationParseErrorOwned::InvalidValue((*s).to_string().into())
1395            }
1396        }
1397    }
1398}
1399
1400#[cfg(feature = "parser")]
1401impl StyleHangingPunctuationParseErrorOwned {
1402    #[must_use]
1403    pub fn to_shared(&self) -> StyleHangingPunctuationParseError<'_> {
1404        match self {
1405            Self::InvalidValue(s) => StyleHangingPunctuationParseError::InvalidValue(s.as_str()),
1406        }
1407    }
1408}
1409
1410#[cfg(feature = "parser")]
1411impl From<StyleHangingPunctuationParseError<'_>> for StyleHangingPunctuationParseErrorOwned {
1412    fn from(e: StyleHangingPunctuationParseError<'_>) -> Self {
1413        e.to_contained()
1414    }
1415}
1416
1417#[cfg(feature = "parser")]
1418impl_display! { StyleHangingPunctuationParseErrorOwned, {
1419    InvalidValue(e) => format!("Invalid hanging-punctuation value: {}", e),
1420}}
1421
1422#[cfg(feature = "parser")]
1423/// # Errors
1424///
1425/// Returns an error if `input` is not a valid CSS `hanging-punctuation` value.
1426pub fn parse_style_hanging_punctuation(
1427    input: &str,
1428) -> Result<StyleHangingPunctuation, StyleHangingPunctuationParseError<'_>> {
1429    let input = input.trim();
1430
1431    if input.eq_ignore_ascii_case("none") {
1432        return Ok(StyleHangingPunctuation::default());
1433    }
1434
1435    let mut first = false;
1436    let mut force_end = false;
1437    let mut allow_end = false;
1438    let mut last = false;
1439
1440    for token in input.split_whitespace() {
1441        match token.to_lowercase().as_str() {
1442            "first" => first = true,
1443            "force-end" => force_end = true,
1444            "allow-end" => allow_end = true,
1445            "last" => last = true,
1446            _ => return Err(StyleHangingPunctuationParseError::InvalidValue(input)),
1447        }
1448    }
1449
1450    if force_end && allow_end {
1451        return Err(StyleHangingPunctuationParseError::InvalidValue(input));
1452    }
1453
1454    Ok(StyleHangingPunctuation {
1455        first,
1456        force_end,
1457        allow_end,
1458        last,
1459    })
1460}
1461
1462/// text-combine-upright property for combining horizontal text in vertical layout
1463#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1464#[repr(C, u8)]
1465#[derive(Default)]
1466pub enum StyleTextCombineUpright {
1467    #[default]
1468    None,
1469    All,
1470    Digits(u8),
1471}
1472
1473impl FormatAsRustCode for StyleTextCombineUpright {
1474    fn format_as_rust_code(&self, _tabs: usize) -> String {
1475        format!("{self:?}")
1476    }
1477}
1478
1479impl PrintAsCssValue for StyleTextCombineUpright {
1480    fn print_as_css_value(&self) -> String {
1481        match self {
1482            Self::None => "none".to_string(),
1483            Self::All => "all".to_string(),
1484            Self::Digits(n) => format!("digits {n}"),
1485        }
1486    }
1487}
1488
1489#[cfg(feature = "parser")]
1490#[derive(Clone, PartialEq, Eq)]
1491pub enum StyleTextCombineUprightParseError<'a> {
1492    InvalidValue(&'a str),
1493    InvalidDigits(&'a str),
1494}
1495#[cfg(feature = "parser")]
1496impl_debug_as_display!(StyleTextCombineUprightParseError<'a>);
1497#[cfg(feature = "parser")]
1498impl_display! { StyleTextCombineUprightParseError<'a>, {
1499    InvalidValue(e) => format!("Invalid text-combine-upright value: {}", e),
1500    InvalidDigits(e) => format!("Invalid text-combine-upright digits: {}", e),
1501}}
1502
1503#[cfg(feature = "parser")]
1504#[derive(Debug, Clone, PartialEq, Eq)]
1505#[repr(C, u8)]
1506pub enum StyleTextCombineUprightParseErrorOwned {
1507    InvalidValue(AzString),
1508    InvalidDigits(AzString),
1509}
1510
1511#[cfg(feature = "parser")]
1512impl StyleTextCombineUprightParseError<'_> {
1513    #[must_use]
1514    pub fn to_contained(&self) -> StyleTextCombineUprightParseErrorOwned {
1515        match self {
1516            Self::InvalidValue(s) => {
1517                StyleTextCombineUprightParseErrorOwned::InvalidValue((*s).to_string().into())
1518            }
1519            Self::InvalidDigits(s) => {
1520                StyleTextCombineUprightParseErrorOwned::InvalidDigits((*s).to_string().into())
1521            }
1522        }
1523    }
1524}
1525
1526#[cfg(feature = "parser")]
1527impl StyleTextCombineUprightParseErrorOwned {
1528    #[must_use]
1529    pub fn to_shared(&self) -> StyleTextCombineUprightParseError<'_> {
1530        match self {
1531            Self::InvalidValue(s) => StyleTextCombineUprightParseError::InvalidValue(s.as_str()),
1532            Self::InvalidDigits(s) => StyleTextCombineUprightParseError::InvalidDigits(s.as_str()),
1533        }
1534    }
1535}
1536
1537#[cfg(feature = "parser")]
1538impl From<StyleTextCombineUprightParseError<'_>> for StyleTextCombineUprightParseErrorOwned {
1539    fn from(e: StyleTextCombineUprightParseError<'_>) -> Self {
1540        e.to_contained()
1541    }
1542}
1543
1544#[cfg(feature = "parser")]
1545impl_display! { StyleTextCombineUprightParseErrorOwned, {
1546    InvalidValue(e) => format!("Invalid text-combine-upright value: {}", e),
1547    InvalidDigits(e) => format!("Invalid text-combine-upright digits: {}", e),
1548}}
1549
1550#[cfg(feature = "parser")]
1551/// # Errors
1552///
1553/// Returns an error if `input` is not a valid CSS `text-combine-upright` value.
1554pub fn parse_style_text_combine_upright(
1555    input: &str,
1556) -> Result<StyleTextCombineUpright, StyleTextCombineUprightParseError<'_>> {
1557    let trimmed = input.trim();
1558
1559    if trimmed.eq_ignore_ascii_case("none") {
1560        Ok(StyleTextCombineUpright::None)
1561    } else if trimmed.eq_ignore_ascii_case("all") {
1562        Ok(StyleTextCombineUpright::All)
1563    } else if trimmed.starts_with("digits") {
1564        let parts: Vec<&str> = trimmed.split_whitespace().collect();
1565        if parts.len() == 2 {
1566            let n = parts[1]
1567                .parse::<u8>()
1568                .map_err(|_| StyleTextCombineUprightParseError::InvalidDigits(input))?;
1569            if (2..=4).contains(&n) {
1570                Ok(StyleTextCombineUpright::Digits(n))
1571            } else {
1572                Err(StyleTextCombineUprightParseError::InvalidDigits(input))
1573            }
1574        } else {
1575            // Default to "digits 2"
1576            Ok(StyleTextCombineUpright::Digits(2))
1577        }
1578    } else {
1579        Err(StyleTextCombineUprightParseError::InvalidValue(input))
1580    }
1581}
1582
1583#[cfg(feature = "parser")]
1584#[derive(Clone, PartialEq, Eq)]
1585pub enum StyleWordSpacingParseError<'a> {
1586    PixelValue(CssPixelValueParseError<'a>),
1587}
1588#[cfg(feature = "parser")]
1589impl_debug_as_display!(StyleWordSpacingParseError<'a>);
1590#[cfg(feature = "parser")]
1591impl_display! { StyleWordSpacingParseError<'a>, {
1592    PixelValue(e) => format!("Invalid word-spacing value: {}", e),
1593}}
1594#[cfg(feature = "parser")]
1595impl_from!(
1596    CssPixelValueParseError<'a>,
1597    StyleWordSpacingParseError::PixelValue
1598);
1599
1600#[cfg(feature = "parser")]
1601#[derive(Debug, Clone, PartialEq, Eq)]
1602#[repr(C, u8)]
1603pub enum StyleWordSpacingParseErrorOwned {
1604    PixelValue(CssPixelValueParseErrorOwned),
1605}
1606
1607#[cfg(feature = "parser")]
1608impl StyleWordSpacingParseError<'_> {
1609    #[must_use]
1610    pub fn to_contained(&self) -> StyleWordSpacingParseErrorOwned {
1611        match self {
1612            Self::PixelValue(e) => StyleWordSpacingParseErrorOwned::PixelValue(e.to_contained()),
1613        }
1614    }
1615}
1616
1617#[cfg(feature = "parser")]
1618impl StyleWordSpacingParseErrorOwned {
1619    #[must_use]
1620    pub fn to_shared(&self) -> StyleWordSpacingParseError<'_> {
1621        match self {
1622            Self::PixelValue(e) => StyleWordSpacingParseError::PixelValue(e.to_shared()),
1623        }
1624    }
1625}
1626
1627#[cfg(feature = "parser")]
1628/// # Errors
1629///
1630/// Returns an error if `input` is not a valid CSS `word-spacing` value.
1631pub fn parse_style_word_spacing(
1632    input: &str,
1633) -> Result<StyleWordSpacing, StyleWordSpacingParseError<'_>> {
1634    crate::props::basic::pixel::parse_pixel_value(input)
1635        .map(|inner| StyleWordSpacing { inner })
1636        .map_err(StyleWordSpacingParseError::PixelValue)
1637}
1638
1639#[cfg(feature = "parser")]
1640#[derive(Clone, PartialEq, Eq)]
1641#[repr(C, u8)]
1642pub enum StyleLineHeightParseError {
1643    Percentage(PercentageParseError),
1644}
1645#[cfg(feature = "parser")]
1646impl_debug_as_display!(StyleLineHeightParseError);
1647#[cfg(feature = "parser")]
1648impl_display! { StyleLineHeightParseError, {
1649    Percentage(e) => format!("Invalid line-height value: {}", e),
1650}}
1651#[cfg(feature = "parser")]
1652impl_from!(PercentageParseError, StyleLineHeightParseError::Percentage);
1653
1654#[cfg(feature = "parser")]
1655#[derive(Debug, Clone, PartialEq, Eq)]
1656pub enum StyleLineHeightParseErrorOwned {
1657    Percentage(PercentageParseErrorOwned),
1658}
1659
1660#[cfg(feature = "parser")]
1661impl StyleLineHeightParseError {
1662    #[must_use]
1663    pub fn to_contained(&self) -> StyleLineHeightParseErrorOwned {
1664        match self {
1665            Self::Percentage(e) => StyleLineHeightParseErrorOwned::Percentage(e.to_contained()),
1666        }
1667    }
1668}
1669
1670#[cfg(feature = "parser")]
1671impl StyleLineHeightParseErrorOwned {
1672    #[must_use]
1673    pub fn to_shared(&self) -> StyleLineHeightParseError {
1674        match self {
1675            Self::Percentage(e) => StyleLineHeightParseError::Percentage(e.to_shared()),
1676        }
1677    }
1678}
1679
1680#[cfg(feature = "parser")]
1681/// # Errors
1682///
1683/// Returns an error if `input` is not a valid CSS `line-height` value.
1684pub fn parse_style_line_height(input: &str) -> Result<StyleLineHeight, StyleLineHeightParseError> {
1685    // Try <number> or <percentage> first (multiplier of font-size)
1686    if let Ok(inner) = crate::props::basic::length::parse_percentage_value(input) {
1687        return Ok(StyleLineHeight { inner });
1688    }
1689    // Try <length> (e.g., "50px") — store as NEGATIVE PercentageValue to signal absolute px.
1690    // Convention: negative normalized() = absolute pixel value (CSS line-height can't be negative).
1691    // Resolved at layout time in fc.rs where font_size is known.
1692    if let Ok(px) = crate::props::basic::pixel::parse_pixel_value(input) {
1693        if px.metric == crate::props::basic::length::SizeMetric::Px {
1694            let px_val = px.number.get();
1695            return Ok(StyleLineHeight {
1696                inner: PercentageValue::new(-px_val * 100.0),
1697            });
1698        }
1699    }
1700    Err(StyleLineHeightParseError::Percentage(
1701        PercentageParseError::InvalidUnit(String::new().into()),
1702    ))
1703}
1704
1705#[cfg(feature = "parser")]
1706#[derive(Clone, PartialEq, Eq)]
1707pub enum StyleTabSizeParseError<'a> {
1708    PixelValue(CssPixelValueParseError<'a>),
1709}
1710#[cfg(feature = "parser")]
1711impl_debug_as_display!(StyleTabSizeParseError<'a>);
1712#[cfg(feature = "parser")]
1713impl_display! { StyleTabSizeParseError<'a>, {
1714    PixelValue(e) => format!("Invalid tab-size value: {}", e),
1715}}
1716#[cfg(feature = "parser")]
1717impl_from!(
1718    CssPixelValueParseError<'a>,
1719    StyleTabSizeParseError::PixelValue
1720);
1721
1722#[cfg(feature = "parser")]
1723#[derive(Debug, Clone, PartialEq, Eq)]
1724#[repr(C, u8)]
1725pub enum StyleTabSizeParseErrorOwned {
1726    PixelValue(CssPixelValueParseErrorOwned),
1727}
1728
1729#[cfg(feature = "parser")]
1730impl StyleTabSizeParseError<'_> {
1731    #[must_use]
1732    pub fn to_contained(&self) -> StyleTabSizeParseErrorOwned {
1733        match self {
1734            Self::PixelValue(e) => StyleTabSizeParseErrorOwned::PixelValue(e.to_contained()),
1735        }
1736    }
1737}
1738
1739#[cfg(feature = "parser")]
1740impl StyleTabSizeParseErrorOwned {
1741    #[must_use]
1742    pub fn to_shared(&self) -> StyleTabSizeParseError<'_> {
1743        match self {
1744            Self::PixelValue(e) => StyleTabSizeParseError::PixelValue(e.to_shared()),
1745        }
1746    }
1747}
1748
1749#[cfg(feature = "parser")]
1750/// # Errors
1751///
1752/// Returns an error if `input` is not a valid CSS `tab-size` value.
1753pub fn parse_style_tab_size(input: &str) -> Result<StyleTabSize, StyleTabSizeParseError<'_>> {
1754    input.trim().parse::<f32>().map_or_else(
1755        |_| {
1756            crate::props::basic::pixel::parse_pixel_value(input)
1757                .map(|v| StyleTabSize { inner: v })
1758                .map_err(StyleTabSizeParseError::PixelValue)
1759        },
1760        |number| {
1761            Ok(StyleTabSize {
1762                inner: PixelValue::em(number),
1763            })
1764        },
1765    )
1766}
1767
1768#[cfg(feature = "parser")]
1769#[derive(Clone, PartialEq, Eq)]
1770pub enum StyleWhiteSpaceParseError<'a> {
1771    InvalidValue(InvalidValueErr<'a>),
1772}
1773#[cfg(feature = "parser")]
1774impl_debug_as_display!(StyleWhiteSpaceParseError<'a>);
1775#[cfg(feature = "parser")]
1776impl_display! { StyleWhiteSpaceParseError<'a>, {
1777    InvalidValue(e) => format!("Invalid white-space value: \"{}\"", e.0),
1778}}
1779#[cfg(feature = "parser")]
1780impl_from!(InvalidValueErr<'a>, StyleWhiteSpaceParseError::InvalidValue);
1781
1782#[cfg(feature = "parser")]
1783#[derive(Debug, Clone, PartialEq, Eq)]
1784#[repr(C, u8)]
1785pub enum StyleWhiteSpaceParseErrorOwned {
1786    InvalidValue(InvalidValueErrOwned),
1787}
1788
1789#[cfg(feature = "parser")]
1790impl StyleWhiteSpaceParseError<'_> {
1791    #[must_use]
1792    pub fn to_contained(&self) -> StyleWhiteSpaceParseErrorOwned {
1793        match self {
1794            Self::InvalidValue(e) => StyleWhiteSpaceParseErrorOwned::InvalidValue(e.to_contained()),
1795        }
1796    }
1797}
1798
1799#[cfg(feature = "parser")]
1800impl StyleWhiteSpaceParseErrorOwned {
1801    #[must_use]
1802    pub fn to_shared(&self) -> StyleWhiteSpaceParseError<'_> {
1803        match self {
1804            Self::InvalidValue(e) => StyleWhiteSpaceParseError::InvalidValue(e.to_shared()),
1805        }
1806    }
1807}
1808
1809#[cfg(feature = "parser")]
1810/// # Errors
1811///
1812/// Returns an error if `input` is not a valid CSS `white-space` value.
1813pub fn parse_style_white_space(
1814    input: &str,
1815) -> Result<StyleWhiteSpace, StyleWhiteSpaceParseError<'_>> {
1816    match input.trim() {
1817        "normal" => Ok(StyleWhiteSpace::Normal),
1818        "pre" => Ok(StyleWhiteSpace::Pre),
1819        "nowrap" | "no-wrap" => Ok(StyleWhiteSpace::Nowrap),
1820        "pre-wrap" => Ok(StyleWhiteSpace::PreWrap),
1821        "pre-line" => Ok(StyleWhiteSpace::PreLine),
1822        "break-spaces" => Ok(StyleWhiteSpace::BreakSpaces),
1823        other => Err(StyleWhiteSpaceParseError::InvalidValue(InvalidValueErr(
1824            other,
1825        ))),
1826    }
1827}
1828
1829#[cfg(feature = "parser")]
1830#[derive(Clone, PartialEq, Eq)]
1831pub enum StyleHyphensParseError<'a> {
1832    InvalidValue(InvalidValueErr<'a>),
1833}
1834#[cfg(feature = "parser")]
1835impl_debug_as_display!(StyleHyphensParseError<'a>);
1836#[cfg(feature = "parser")]
1837impl_display! { StyleHyphensParseError<'a>, {
1838    InvalidValue(e) => format!("Invalid hyphens value: \"{}\"", e.0),
1839}}
1840#[cfg(feature = "parser")]
1841impl_from!(InvalidValueErr<'a>, StyleHyphensParseError::InvalidValue);
1842
1843#[cfg(feature = "parser")]
1844#[derive(Debug, Clone, PartialEq, Eq)]
1845#[repr(C, u8)]
1846pub enum StyleHyphensParseErrorOwned {
1847    InvalidValue(InvalidValueErrOwned),
1848}
1849
1850#[cfg(feature = "parser")]
1851impl StyleHyphensParseError<'_> {
1852    #[must_use]
1853    pub fn to_contained(&self) -> StyleHyphensParseErrorOwned {
1854        match self {
1855            Self::InvalidValue(e) => StyleHyphensParseErrorOwned::InvalidValue(e.to_contained()),
1856        }
1857    }
1858}
1859
1860#[cfg(feature = "parser")]
1861impl StyleHyphensParseErrorOwned {
1862    #[must_use]
1863    pub fn to_shared(&self) -> StyleHyphensParseError<'_> {
1864        match self {
1865            Self::InvalidValue(e) => StyleHyphensParseError::InvalidValue(e.to_shared()),
1866        }
1867    }
1868}
1869
1870#[cfg(feature = "parser")]
1871/// # Errors
1872///
1873/// Returns an error if `input` is not a valid CSS `hyphens` value.
1874pub fn parse_style_hyphens(input: &str) -> Result<StyleHyphens, StyleHyphensParseError<'_>> {
1875    match input.trim() {
1876        "none" => Ok(StyleHyphens::None),
1877        "manual" => Ok(StyleHyphens::Manual),
1878        "auto" => Ok(StyleHyphens::Auto),
1879        other => Err(StyleHyphensParseError::InvalidValue(InvalidValueErr(other))),
1880    }
1881}
1882
1883// -- StyleLineBreak parse --
1884
1885#[cfg(feature = "parser")]
1886#[derive(Clone, PartialEq, Eq)]
1887pub enum StyleLineBreakParseError<'a> {
1888    InvalidValue(InvalidValueErr<'a>),
1889}
1890#[cfg(feature = "parser")]
1891impl_debug_as_display!(StyleLineBreakParseError<'a>);
1892#[cfg(feature = "parser")]
1893impl_display! { StyleLineBreakParseError<'a>, {
1894    InvalidValue(e) => format!("Invalid line-break value: \"{}\"", e.0),
1895}}
1896#[cfg(feature = "parser")]
1897impl_from!(InvalidValueErr<'a>, StyleLineBreakParseError::InvalidValue);
1898
1899#[cfg(feature = "parser")]
1900#[derive(Debug, Clone, PartialEq, Eq)]
1901#[repr(C, u8)]
1902pub enum StyleLineBreakParseErrorOwned {
1903    InvalidValue(InvalidValueErrOwned),
1904}
1905
1906#[cfg(feature = "parser")]
1907impl StyleLineBreakParseError<'_> {
1908    #[must_use]
1909    pub fn to_contained(&self) -> StyleLineBreakParseErrorOwned {
1910        match self {
1911            Self::InvalidValue(e) => StyleLineBreakParseErrorOwned::InvalidValue(e.to_contained()),
1912        }
1913    }
1914}
1915
1916#[cfg(feature = "parser")]
1917impl StyleLineBreakParseErrorOwned {
1918    #[must_use]
1919    pub fn to_shared(&self) -> StyleLineBreakParseError<'_> {
1920        match self {
1921            Self::InvalidValue(e) => StyleLineBreakParseError::InvalidValue(e.to_shared()),
1922        }
1923    }
1924}
1925
1926#[cfg(feature = "parser")]
1927/// # Errors
1928///
1929/// Returns an error if `input` is not a valid CSS `line-break` value.
1930pub fn parse_style_line_break(input: &str) -> Result<StyleLineBreak, StyleLineBreakParseError<'_>> {
1931    match input.trim() {
1932        "auto" => Ok(StyleLineBreak::Auto),
1933        "loose" => Ok(StyleLineBreak::Loose),
1934        "normal" => Ok(StyleLineBreak::Normal),
1935        "strict" => Ok(StyleLineBreak::Strict),
1936        "anywhere" => Ok(StyleLineBreak::Anywhere),
1937        other => Err(StyleLineBreakParseError::InvalidValue(InvalidValueErr(
1938            other,
1939        ))),
1940    }
1941}
1942
1943// -- StyleWordBreak parse --
1944
1945#[cfg(feature = "parser")]
1946#[derive(Clone, PartialEq, Eq)]
1947pub enum StyleWordBreakParseError<'a> {
1948    InvalidValue(InvalidValueErr<'a>),
1949}
1950#[cfg(feature = "parser")]
1951impl_debug_as_display!(StyleWordBreakParseError<'a>);
1952#[cfg(feature = "parser")]
1953impl_display! { StyleWordBreakParseError<'a>, {
1954    InvalidValue(e) => format!("Invalid word-break value: \"{}\"", e.0),
1955}}
1956#[cfg(feature = "parser")]
1957impl_from!(InvalidValueErr<'a>, StyleWordBreakParseError::InvalidValue);
1958
1959#[cfg(feature = "parser")]
1960#[derive(Debug, Clone, PartialEq, Eq)]
1961#[repr(C, u8)]
1962pub enum StyleWordBreakParseErrorOwned {
1963    InvalidValue(InvalidValueErrOwned),
1964}
1965
1966#[cfg(feature = "parser")]
1967impl StyleWordBreakParseError<'_> {
1968    #[must_use]
1969    pub fn to_contained(&self) -> StyleWordBreakParseErrorOwned {
1970        match self {
1971            Self::InvalidValue(e) => StyleWordBreakParseErrorOwned::InvalidValue(e.to_contained()),
1972        }
1973    }
1974}
1975
1976#[cfg(feature = "parser")]
1977impl StyleWordBreakParseErrorOwned {
1978    #[must_use]
1979    pub fn to_shared(&self) -> StyleWordBreakParseError<'_> {
1980        match self {
1981            Self::InvalidValue(e) => StyleWordBreakParseError::InvalidValue(e.to_shared()),
1982        }
1983    }
1984}
1985
1986#[cfg(feature = "parser")]
1987/// # Errors
1988///
1989/// Returns an error if `input` is not a valid CSS `word-break` value.
1990pub fn parse_style_word_break(input: &str) -> Result<StyleWordBreak, StyleWordBreakParseError<'_>> {
1991    match input.trim() {
1992        "normal" => Ok(StyleWordBreak::Normal),
1993        "break-all" => Ok(StyleWordBreak::BreakAll),
1994        "keep-all" => Ok(StyleWordBreak::KeepAll),
1995        "break-word" => Ok(StyleWordBreak::BreakWord),
1996        other => Err(StyleWordBreakParseError::InvalidValue(InvalidValueErr(
1997            other,
1998        ))),
1999    }
2000}
2001
2002// -- StyleOverflowWrap parse --
2003
2004#[cfg(feature = "parser")]
2005#[derive(Clone, PartialEq, Eq)]
2006pub enum StyleOverflowWrapParseError<'a> {
2007    InvalidValue(InvalidValueErr<'a>),
2008}
2009#[cfg(feature = "parser")]
2010impl_debug_as_display!(StyleOverflowWrapParseError<'a>);
2011#[cfg(feature = "parser")]
2012impl_display! { StyleOverflowWrapParseError<'a>, {
2013    InvalidValue(e) => format!("Invalid overflow-wrap value: \"{}\"", e.0),
2014}}
2015#[cfg(feature = "parser")]
2016impl_from!(
2017    InvalidValueErr<'a>,
2018    StyleOverflowWrapParseError::InvalidValue
2019);
2020
2021#[cfg(feature = "parser")]
2022#[derive(Debug, Clone, PartialEq, Eq)]
2023#[repr(C, u8)]
2024pub enum StyleOverflowWrapParseErrorOwned {
2025    InvalidValue(InvalidValueErrOwned),
2026}
2027
2028#[cfg(feature = "parser")]
2029impl StyleOverflowWrapParseError<'_> {
2030    #[must_use]
2031    pub fn to_contained(&self) -> StyleOverflowWrapParseErrorOwned {
2032        match self {
2033            Self::InvalidValue(e) => {
2034                StyleOverflowWrapParseErrorOwned::InvalidValue(e.to_contained())
2035            }
2036        }
2037    }
2038}
2039
2040#[cfg(feature = "parser")]
2041impl StyleOverflowWrapParseErrorOwned {
2042    #[must_use]
2043    pub fn to_shared(&self) -> StyleOverflowWrapParseError<'_> {
2044        match self {
2045            Self::InvalidValue(e) => StyleOverflowWrapParseError::InvalidValue(e.to_shared()),
2046        }
2047    }
2048}
2049
2050#[cfg(feature = "parser")]
2051/// # Errors
2052///
2053/// Returns an error if `input` is not a valid CSS `overflow-wrap` value.
2054pub fn parse_style_overflow_wrap(
2055    input: &str,
2056) -> Result<StyleOverflowWrap, StyleOverflowWrapParseError<'_>> {
2057    match input.trim() {
2058        "normal" => Ok(StyleOverflowWrap::Normal),
2059        "anywhere" => Ok(StyleOverflowWrap::Anywhere),
2060        "break-word" => Ok(StyleOverflowWrap::BreakWord),
2061        other => Err(StyleOverflowWrapParseError::InvalidValue(InvalidValueErr(
2062            other,
2063        ))),
2064    }
2065}
2066
2067// -- StyleTextAlignLast parse --
2068
2069#[cfg(feature = "parser")]
2070#[derive(Clone, PartialEq, Eq)]
2071pub enum StyleTextAlignLastParseError<'a> {
2072    InvalidValue(InvalidValueErr<'a>),
2073}
2074#[cfg(feature = "parser")]
2075impl_debug_as_display!(StyleTextAlignLastParseError<'a>);
2076#[cfg(feature = "parser")]
2077impl_display! { StyleTextAlignLastParseError<'a>, {
2078    InvalidValue(e) => format!("Invalid text-align-last value: \"{}\"", e.0),
2079}}
2080#[cfg(feature = "parser")]
2081impl_from!(
2082    InvalidValueErr<'a>,
2083    StyleTextAlignLastParseError::InvalidValue
2084);
2085
2086#[cfg(feature = "parser")]
2087#[derive(Debug, Clone, PartialEq, Eq)]
2088#[repr(C, u8)]
2089pub enum StyleTextAlignLastParseErrorOwned {
2090    InvalidValue(InvalidValueErrOwned),
2091}
2092
2093#[cfg(feature = "parser")]
2094impl StyleTextAlignLastParseError<'_> {
2095    #[must_use]
2096    pub fn to_contained(&self) -> StyleTextAlignLastParseErrorOwned {
2097        match self {
2098            Self::InvalidValue(e) => {
2099                StyleTextAlignLastParseErrorOwned::InvalidValue(e.to_contained())
2100            }
2101        }
2102    }
2103}
2104
2105#[cfg(feature = "parser")]
2106impl StyleTextAlignLastParseErrorOwned {
2107    #[must_use]
2108    pub fn to_shared(&self) -> StyleTextAlignLastParseError<'_> {
2109        match self {
2110            Self::InvalidValue(e) => StyleTextAlignLastParseError::InvalidValue(e.to_shared()),
2111        }
2112    }
2113}
2114
2115#[cfg(feature = "parser")]
2116/// # Errors
2117///
2118/// Returns an error if `input` is not a valid CSS `text-align-last` value.
2119pub fn parse_style_text_align_last(
2120    input: &str,
2121) -> Result<StyleTextAlignLast, StyleTextAlignLastParseError<'_>> {
2122    match input.trim() {
2123        "auto" => Ok(StyleTextAlignLast::Auto),
2124        "start" => Ok(StyleTextAlignLast::Start),
2125        "end" => Ok(StyleTextAlignLast::End),
2126        "left" => Ok(StyleTextAlignLast::Left),
2127        "right" => Ok(StyleTextAlignLast::Right),
2128        "center" => Ok(StyleTextAlignLast::Center),
2129        "justify" => Ok(StyleTextAlignLast::Justify),
2130        other => Err(StyleTextAlignLastParseError::InvalidValue(InvalidValueErr(
2131            other,
2132        ))),
2133    }
2134}
2135
2136// -- StyleTextTransform parse --
2137
2138#[cfg(feature = "parser")]
2139#[derive(Clone, PartialEq, Eq)]
2140pub enum StyleTextTransformParseError<'a> {
2141    InvalidValue(InvalidValueErr<'a>),
2142}
2143#[cfg(feature = "parser")]
2144impl_debug_as_display!(StyleTextTransformParseError<'a>);
2145#[cfg(feature = "parser")]
2146impl_display! { StyleTextTransformParseError<'a>, {
2147    InvalidValue(e) => format!("Invalid text-transform value: \"{}\"", e.0),
2148}}
2149#[cfg(feature = "parser")]
2150impl_from!(
2151    InvalidValueErr<'a>,
2152    StyleTextTransformParseError::InvalidValue
2153);
2154
2155#[cfg(feature = "parser")]
2156#[derive(Debug, Clone, PartialEq, Eq)]
2157#[repr(C, u8)]
2158pub enum StyleTextTransformParseErrorOwned {
2159    InvalidValue(InvalidValueErrOwned),
2160}
2161
2162#[cfg(feature = "parser")]
2163impl StyleTextTransformParseError<'_> {
2164    #[must_use]
2165    pub fn to_contained(&self) -> StyleTextTransformParseErrorOwned {
2166        match self {
2167            Self::InvalidValue(e) => {
2168                StyleTextTransformParseErrorOwned::InvalidValue(e.to_contained())
2169            }
2170        }
2171    }
2172}
2173
2174#[cfg(feature = "parser")]
2175impl StyleTextTransformParseErrorOwned {
2176    #[must_use]
2177    pub fn to_shared(&self) -> StyleTextTransformParseError<'_> {
2178        match self {
2179            Self::InvalidValue(e) => StyleTextTransformParseError::InvalidValue(e.to_shared()),
2180        }
2181    }
2182}
2183
2184#[cfg(feature = "parser")]
2185/// # Errors
2186///
2187/// Returns an error if `input` is not a valid CSS `text-transform` value.
2188pub fn parse_style_text_transform(
2189    input: &str,
2190) -> Result<StyleTextTransform, StyleTextTransformParseError<'_>> {
2191    match input.trim() {
2192        "none" => Ok(StyleTextTransform::None),
2193        "capitalize" => Ok(StyleTextTransform::Capitalize),
2194        "uppercase" => Ok(StyleTextTransform::Uppercase),
2195        "lowercase" => Ok(StyleTextTransform::Lowercase),
2196        "full-width" => Ok(StyleTextTransform::FullWidth),
2197        other => Err(StyleTextTransformParseError::InvalidValue(InvalidValueErr(
2198            other,
2199        ))),
2200    }
2201}
2202
2203#[cfg(feature = "parser")]
2204#[derive(Clone, PartialEq, Eq)]
2205pub enum StyleDirectionParseError<'a> {
2206    InvalidValue(InvalidValueErr<'a>),
2207}
2208#[cfg(feature = "parser")]
2209impl_debug_as_display!(StyleDirectionParseError<'a>);
2210#[cfg(feature = "parser")]
2211impl_display! { StyleDirectionParseError<'a>, {
2212    InvalidValue(e) => format!("Invalid direction value: \"{}\"", e.0),
2213}}
2214#[cfg(feature = "parser")]
2215impl_from!(InvalidValueErr<'a>, StyleDirectionParseError::InvalidValue);
2216
2217#[cfg(feature = "parser")]
2218#[derive(Debug, Clone, PartialEq, Eq)]
2219#[repr(C, u8)]
2220pub enum StyleDirectionParseErrorOwned {
2221    InvalidValue(InvalidValueErrOwned),
2222}
2223
2224#[cfg(feature = "parser")]
2225impl StyleDirectionParseError<'_> {
2226    #[must_use]
2227    pub fn to_contained(&self) -> StyleDirectionParseErrorOwned {
2228        match self {
2229            Self::InvalidValue(e) => StyleDirectionParseErrorOwned::InvalidValue(e.to_contained()),
2230        }
2231    }
2232}
2233
2234#[cfg(feature = "parser")]
2235impl StyleDirectionParseErrorOwned {
2236    #[must_use]
2237    pub fn to_shared(&self) -> StyleDirectionParseError<'_> {
2238        match self {
2239            Self::InvalidValue(e) => StyleDirectionParseError::InvalidValue(e.to_shared()),
2240        }
2241    }
2242}
2243
2244#[cfg(feature = "parser")]
2245/// # Errors
2246///
2247/// Returns an error if `input` is not a valid CSS `direction` value.
2248pub fn parse_style_direction(input: &str) -> Result<StyleDirection, StyleDirectionParseError<'_>> {
2249    match input.trim() {
2250        "ltr" => Ok(StyleDirection::Ltr),
2251        "rtl" => Ok(StyleDirection::Rtl),
2252        other => Err(StyleDirectionParseError::InvalidValue(InvalidValueErr(
2253            other,
2254        ))),
2255    }
2256}
2257
2258#[cfg(feature = "parser")]
2259#[derive(Clone, PartialEq, Eq)]
2260pub enum StyleUserSelectParseError<'a> {
2261    InvalidValue(InvalidValueErr<'a>),
2262}
2263#[cfg(feature = "parser")]
2264impl_debug_as_display!(StyleUserSelectParseError<'a>);
2265#[cfg(feature = "parser")]
2266impl_display! { StyleUserSelectParseError<'a>, {
2267    InvalidValue(e) => format!("Invalid user-select value: \"{}\"", e.0),
2268}}
2269#[cfg(feature = "parser")]
2270impl_from!(InvalidValueErr<'a>, StyleUserSelectParseError::InvalidValue);
2271
2272#[cfg(feature = "parser")]
2273#[derive(Debug, Clone, PartialEq, Eq)]
2274#[repr(C, u8)]
2275pub enum StyleUserSelectParseErrorOwned {
2276    InvalidValue(InvalidValueErrOwned),
2277}
2278
2279#[cfg(feature = "parser")]
2280impl StyleUserSelectParseError<'_> {
2281    #[must_use]
2282    pub fn to_contained(&self) -> StyleUserSelectParseErrorOwned {
2283        match self {
2284            Self::InvalidValue(e) => StyleUserSelectParseErrorOwned::InvalidValue(e.to_contained()),
2285        }
2286    }
2287}
2288
2289#[cfg(feature = "parser")]
2290impl StyleUserSelectParseErrorOwned {
2291    #[must_use]
2292    pub fn to_shared(&self) -> StyleUserSelectParseError<'_> {
2293        match self {
2294            Self::InvalidValue(e) => StyleUserSelectParseError::InvalidValue(e.to_shared()),
2295        }
2296    }
2297}
2298
2299#[cfg(feature = "parser")]
2300/// # Errors
2301///
2302/// Returns an error if `input` is not a valid CSS `user-select` value.
2303pub fn parse_style_user_select(
2304    input: &str,
2305) -> Result<StyleUserSelect, StyleUserSelectParseError<'_>> {
2306    match input.trim() {
2307        "auto" => Ok(StyleUserSelect::Auto),
2308        "text" => Ok(StyleUserSelect::Text),
2309        "none" => Ok(StyleUserSelect::None),
2310        "all" => Ok(StyleUserSelect::All),
2311        other => Err(StyleUserSelectParseError::InvalidValue(InvalidValueErr(
2312            other,
2313        ))),
2314    }
2315}
2316
2317#[cfg(feature = "parser")]
2318#[derive(Clone, PartialEq, Eq)]
2319pub enum StyleTextDecorationParseError<'a> {
2320    InvalidValue(InvalidValueErr<'a>),
2321}
2322#[cfg(feature = "parser")]
2323impl_debug_as_display!(StyleTextDecorationParseError<'a>);
2324#[cfg(feature = "parser")]
2325impl_display! { StyleTextDecorationParseError<'a>, {
2326    InvalidValue(e) => format!("Invalid text-decoration value: \"{}\"", e.0),
2327}}
2328#[cfg(feature = "parser")]
2329impl_from!(
2330    InvalidValueErr<'a>,
2331    StyleTextDecorationParseError::InvalidValue
2332);
2333
2334#[cfg(feature = "parser")]
2335#[derive(Debug, Clone, PartialEq, Eq)]
2336#[repr(C, u8)]
2337pub enum StyleTextDecorationParseErrorOwned {
2338    InvalidValue(InvalidValueErrOwned),
2339}
2340
2341#[cfg(feature = "parser")]
2342impl StyleTextDecorationParseError<'_> {
2343    #[must_use]
2344    pub fn to_contained(&self) -> StyleTextDecorationParseErrorOwned {
2345        match self {
2346            Self::InvalidValue(e) => {
2347                StyleTextDecorationParseErrorOwned::InvalidValue(e.to_contained())
2348            }
2349        }
2350    }
2351}
2352
2353#[cfg(feature = "parser")]
2354impl StyleTextDecorationParseErrorOwned {
2355    #[must_use]
2356    pub fn to_shared(&self) -> StyleTextDecorationParseError<'_> {
2357        match self {
2358            Self::InvalidValue(e) => StyleTextDecorationParseError::InvalidValue(e.to_shared()),
2359        }
2360    }
2361}
2362
2363#[cfg(feature = "parser")]
2364/// # Errors
2365///
2366/// Returns an error if `input` is not a valid CSS `text-decoration` value.
2367pub fn parse_style_text_decoration(
2368    input: &str,
2369) -> Result<StyleTextDecoration, StyleTextDecorationParseError<'_>> {
2370    match input.trim() {
2371        "none" => Ok(StyleTextDecoration::None),
2372        "underline" => Ok(StyleTextDecoration::Underline),
2373        "overline" => Ok(StyleTextDecoration::Overline),
2374        "line-through" => Ok(StyleTextDecoration::LineThrough),
2375        other => Err(StyleTextDecorationParseError::InvalidValue(
2376            InvalidValueErr(other),
2377        )),
2378    }
2379}
2380
2381#[cfg(feature = "parser")]
2382#[derive(Clone, PartialEq, Eq)]
2383pub enum StyleVerticalAlignParseError<'a> {
2384    InvalidValue(InvalidValueErr<'a>),
2385}
2386#[cfg(feature = "parser")]
2387impl_debug_as_display!(StyleVerticalAlignParseError<'a>);
2388#[cfg(feature = "parser")]
2389impl_display! { StyleVerticalAlignParseError<'a>, {
2390    InvalidValue(e) => format!("Invalid vertical-align value: \"{}\"", e.0),
2391}}
2392#[cfg(feature = "parser")]
2393impl_from!(
2394    InvalidValueErr<'a>,
2395    StyleVerticalAlignParseError::InvalidValue
2396);
2397
2398#[cfg(feature = "parser")]
2399#[derive(Debug, Clone, PartialEq, Eq)]
2400#[repr(C, u8)]
2401pub enum StyleVerticalAlignParseErrorOwned {
2402    InvalidValue(InvalidValueErrOwned),
2403}
2404
2405#[cfg(feature = "parser")]
2406impl StyleVerticalAlignParseError<'_> {
2407    #[must_use]
2408    pub fn to_contained(&self) -> StyleVerticalAlignParseErrorOwned {
2409        match self {
2410            Self::InvalidValue(e) => {
2411                StyleVerticalAlignParseErrorOwned::InvalidValue(e.to_contained())
2412            }
2413        }
2414    }
2415}
2416
2417#[cfg(feature = "parser")]
2418impl StyleVerticalAlignParseErrorOwned {
2419    #[must_use]
2420    pub fn to_shared(&self) -> StyleVerticalAlignParseError<'_> {
2421        match self {
2422            Self::InvalidValue(e) => StyleVerticalAlignParseError::InvalidValue(e.to_shared()),
2423        }
2424    }
2425}
2426
2427#[cfg(feature = "parser")]
2428/// # Errors
2429///
2430/// Returns an error if `input` is not a valid CSS `vertical-align` value.
2431pub fn parse_style_vertical_align(
2432    input: &str,
2433) -> Result<StyleVerticalAlign, StyleVerticalAlignParseError<'_>> {
2434    match input.trim() {
2435        "baseline" => Ok(StyleVerticalAlign::Baseline),
2436        "top" => Ok(StyleVerticalAlign::Top),
2437        "middle" => Ok(StyleVerticalAlign::Middle),
2438        "bottom" => Ok(StyleVerticalAlign::Bottom),
2439        "sub" => Ok(StyleVerticalAlign::Sub),
2440        "super" => Ok(StyleVerticalAlign::Superscript),
2441        "text-top" => Ok(StyleVerticalAlign::TextTop),
2442        "text-bottom" => Ok(StyleVerticalAlign::TextBottom),
2443        other if other.ends_with('%') => {
2444            let num_str = other.trim_end_matches('%').trim();
2445            num_str.parse::<f32>().map_or_else(
2446                |_| {
2447                    Err(StyleVerticalAlignParseError::InvalidValue(InvalidValueErr(
2448                        other,
2449                    )))
2450                },
2451                |val| Ok(StyleVerticalAlign::Percentage(PercentageValue::new(val))),
2452            )
2453        }
2454        other => crate::props::basic::pixel::parse_pixel_value(other).map_or_else(
2455            |_| {
2456                Err(StyleVerticalAlignParseError::InvalidValue(InvalidValueErr(
2457                    other,
2458                )))
2459            },
2460            |pv| Ok(StyleVerticalAlign::Length(pv)),
2461        ),
2462    }
2463}
2464
2465// --- CaretColor ---
2466
2467#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2468#[repr(C)]
2469pub struct CaretColor {
2470    pub inner: ColorU,
2471}
2472
2473impl Default for CaretColor {
2474    fn default() -> Self {
2475        Self {
2476            inner: ColorU::BLACK,
2477        }
2478    }
2479}
2480
2481impl PrintAsCssValue for CaretColor {
2482    fn print_as_css_value(&self) -> String {
2483        self.inner.to_hash()
2484    }
2485}
2486
2487impl FormatAsRustCode for CaretColor {
2488    fn format_as_rust_code(&self, _tabs: usize) -> String {
2489        format!(
2490            "CaretColor {{ inner: {} }}",
2491            crate::codegen::format::format_color_value(&self.inner)
2492        )
2493    }
2494}
2495
2496#[cfg(feature = "parser")]
2497/// # Errors
2498///
2499/// Returns an error if `input` is not a valid CSS `caret-color` value.
2500pub fn parse_caret_color(input: &str) -> Result<CaretColor, CssColorParseError<'_>> {
2501    parse_css_color(input).map(|inner| CaretColor { inner })
2502}
2503
2504// --- CaretAnimationDuration ---
2505
2506#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2507#[repr(C)]
2508pub struct CaretAnimationDuration {
2509    pub inner: CssDuration,
2510}
2511
2512impl Default for CaretAnimationDuration {
2513    fn default() -> Self {
2514        Self {
2515            inner: CssDuration::from_millis(500),
2516        } // Default 500ms blink time
2517    }
2518}
2519
2520impl PrintAsCssValue for CaretAnimationDuration {
2521    fn print_as_css_value(&self) -> String {
2522        self.inner.print_as_css_value()
2523    }
2524}
2525
2526impl FormatAsRustCode for CaretAnimationDuration {
2527    fn format_as_rust_code(&self, _tabs: usize) -> String {
2528        format!(
2529            "CaretAnimationDuration {{ inner: {} }}",
2530            self.inner.format_as_rust_code(0)
2531        )
2532    }
2533}
2534
2535#[cfg(feature = "parser")]
2536/// # Errors
2537///
2538/// Returns an error if `input` is not a valid CSS `caret-animation-duration` value.
2539pub fn parse_caret_animation_duration(
2540    input: &str,
2541) -> Result<CaretAnimationDuration, DurationParseError<'_>> {
2542    use crate::props::basic::parse_duration;
2543
2544    parse_duration(input).map(|inner| CaretAnimationDuration { inner })
2545}
2546
2547// --- CaretWidth ---
2548
2549/// Width of the text cursor (caret) in pixels.
2550/// CSS doesn't have a standard property for this, so we use `-azul-caret-width`.
2551#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2552#[repr(C)]
2553pub struct CaretWidth {
2554    pub inner: PixelValue,
2555}
2556
2557impl Default for CaretWidth {
2558    fn default() -> Self {
2559        Self {
2560            inner: PixelValue::px(2.0), // Default 2px caret width
2561        }
2562    }
2563}
2564
2565impl PrintAsCssValue for CaretWidth {
2566    fn print_as_css_value(&self) -> String {
2567        self.inner.print_as_css_value()
2568    }
2569}
2570
2571impl FormatAsRustCode for CaretWidth {
2572    fn format_as_rust_code(&self, _tabs: usize) -> String {
2573        format!(
2574            "CaretWidth {{ inner: {} }}",
2575            self.inner.format_as_rust_code(0)
2576        )
2577    }
2578}
2579
2580#[cfg(feature = "parser")]
2581/// # Errors
2582///
2583/// Returns an error if `input` is not a valid CSS `caret-width` value.
2584pub fn parse_caret_width(input: &str) -> Result<CaretWidth, CssPixelValueParseError<'_>> {
2585    use crate::props::basic::pixel::parse_pixel_value;
2586
2587    parse_pixel_value(input).map(|inner| CaretWidth { inner })
2588}
2589
2590// --- From implementations for CssProperty ---
2591
2592impl From<StyleUserSelect> for crate::props::property::CssProperty {
2593    fn from(value: StyleUserSelect) -> Self {
2594        use crate::props::property::CssProperty;
2595        Self::user_select(value)
2596    }
2597}
2598
2599impl From<StyleTextDecoration> for crate::props::property::CssProperty {
2600    fn from(value: StyleTextDecoration) -> Self {
2601        use crate::props::property::CssProperty;
2602        Self::text_decoration(value)
2603    }
2604}
2605
2606#[cfg(all(test, feature = "parser"))]
2607mod tests {
2608    use super::*;
2609    use crate::props::basic::{color::ColorU, length::PercentageValue, pixel::PixelValue};
2610
2611    #[test]
2612    fn test_parse_style_text_color() {
2613        assert_eq!(
2614            parse_style_text_color("red").unwrap().inner,
2615            ColorU::new_rgb(255, 0, 0)
2616        );
2617        assert_eq!(
2618            parse_style_text_color("#aabbcc").unwrap().inner,
2619            ColorU::new_rgb(170, 187, 204)
2620        );
2621        assert!(parse_style_text_color("not-a-color").is_err());
2622    }
2623
2624    #[test]
2625    fn test_parse_style_text_align() {
2626        assert_eq!(
2627            parse_style_text_align("left").unwrap(),
2628            StyleTextAlign::Left
2629        );
2630        assert_eq!(
2631            parse_style_text_align("center").unwrap(),
2632            StyleTextAlign::Center
2633        );
2634        assert_eq!(
2635            parse_style_text_align("right").unwrap(),
2636            StyleTextAlign::Right
2637        );
2638        assert_eq!(
2639            parse_style_text_align("justify").unwrap(),
2640            StyleTextAlign::Justify
2641        );
2642        assert_eq!(
2643            parse_style_text_align("start").unwrap(),
2644            StyleTextAlign::Start
2645        );
2646        assert_eq!(parse_style_text_align("end").unwrap(), StyleTextAlign::End);
2647        assert!(parse_style_text_align("middle").is_err());
2648    }
2649
2650    #[test]
2651    fn test_parse_spacing() {
2652        assert_eq!(
2653            parse_style_letter_spacing("2px").unwrap().inner,
2654            PixelValue::px(2.0)
2655        );
2656        assert_eq!(
2657            parse_style_letter_spacing("-0.1em").unwrap().inner,
2658            PixelValue::em(-0.1)
2659        );
2660        assert_eq!(
2661            parse_style_word_spacing("0.5em").unwrap().inner,
2662            PixelValue::em(0.5)
2663        );
2664    }
2665
2666    #[test]
2667    fn test_parse_line_height() {
2668        assert_eq!(
2669            parse_style_line_height("1.5").unwrap().inner,
2670            PercentageValue::new(150.0)
2671        );
2672        assert_eq!(
2673            parse_style_line_height("120%").unwrap().inner,
2674            PercentageValue::new(120.0)
2675        );
2676        // px values stored as negative PercentageValue (convention: negative = absolute px)
2677        assert_eq!(
2678            parse_style_line_height("20px").unwrap().inner,
2679            PercentageValue::new(-20.0 * 100.0)
2680        );
2681    }
2682
2683    #[test]
2684    fn test_parse_tab_size() {
2685        // Unitless number is treated as `em`
2686        assert_eq!(
2687            parse_style_tab_size("4").unwrap().inner,
2688            PixelValue::em(4.0)
2689        );
2690        assert_eq!(
2691            parse_style_tab_size("20px").unwrap().inner,
2692            PixelValue::px(20.0)
2693        );
2694    }
2695
2696    #[test]
2697    fn test_parse_white_space() {
2698        assert_eq!(
2699            parse_style_white_space("normal").unwrap(),
2700            StyleWhiteSpace::Normal
2701        );
2702        assert_eq!(
2703            parse_style_white_space("pre").unwrap(),
2704            StyleWhiteSpace::Pre
2705        );
2706        assert_eq!(
2707            parse_style_white_space("nowrap").unwrap(),
2708            StyleWhiteSpace::Nowrap
2709        );
2710        assert_eq!(
2711            parse_style_white_space("pre-wrap").unwrap(),
2712            StyleWhiteSpace::PreWrap
2713        );
2714    }
2715}
2716
2717// -- StyleUnicodeBidi --
2718
2719/// Represents the `unicode-bidi` CSS property.
2720///
2721/// Controls how bidirectional text is handled within an element.
2722#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2723#[repr(C)]
2724#[derive(Default)]
2725pub enum StyleUnicodeBidi {
2726    /// No additional level of embedding
2727    #[default]
2728    Normal,
2729    /// Open an additional level of embedding
2730    Embed,
2731    /// Isolate the element from surrounding bidirectional text
2732    Isolate,
2733    /// Override the bidirectional algorithm for inline content
2734    BidiOverride,
2735    /// Combine isolation and override
2736    IsolateOverride,
2737    /// Determine paragraph direction from content without bidi algorithm
2738    Plaintext,
2739}
2740impl_option!(
2741    StyleUnicodeBidi,
2742    OptionStyleUnicodeBidi,
2743    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
2744);
2745impl PrintAsCssValue for StyleUnicodeBidi {
2746    fn print_as_css_value(&self) -> String {
2747        String::from(match self {
2748            Self::Normal => "normal",
2749            Self::Embed => "embed",
2750            Self::Isolate => "isolate",
2751            Self::BidiOverride => "bidi-override",
2752            Self::IsolateOverride => "isolate-override",
2753            Self::Plaintext => "plaintext",
2754        })
2755    }
2756}
2757
2758#[cfg(feature = "parser")]
2759#[derive(Clone, PartialEq, Eq)]
2760pub enum StyleUnicodeBidiParseError<'a> {
2761    InvalidValue(InvalidValueErr<'a>),
2762}
2763#[cfg(feature = "parser")]
2764impl_debug_as_display!(StyleUnicodeBidiParseError<'a>);
2765#[cfg(feature = "parser")]
2766impl_display! { StyleUnicodeBidiParseError<'a>, {
2767    InvalidValue(e) => format!("Invalid unicode-bidi value: \"{}\"", e.0),
2768}}
2769#[cfg(feature = "parser")]
2770impl_from!(
2771    InvalidValueErr<'a>,
2772    StyleUnicodeBidiParseError::InvalidValue
2773);
2774
2775#[cfg(feature = "parser")]
2776#[derive(Debug, Clone, PartialEq, Eq)]
2777#[repr(C, u8)]
2778pub enum StyleUnicodeBidiParseErrorOwned {
2779    InvalidValue(InvalidValueErrOwned),
2780}
2781
2782#[cfg(feature = "parser")]
2783impl StyleUnicodeBidiParseError<'_> {
2784    #[must_use]
2785    pub fn to_contained(&self) -> StyleUnicodeBidiParseErrorOwned {
2786        match self {
2787            Self::InvalidValue(e) => {
2788                StyleUnicodeBidiParseErrorOwned::InvalidValue(e.to_contained())
2789            }
2790        }
2791    }
2792}
2793
2794#[cfg(feature = "parser")]
2795impl StyleUnicodeBidiParseErrorOwned {
2796    #[must_use]
2797    pub fn to_shared(&self) -> StyleUnicodeBidiParseError<'_> {
2798        match self {
2799            Self::InvalidValue(e) => StyleUnicodeBidiParseError::InvalidValue(e.to_shared()),
2800        }
2801    }
2802}
2803
2804#[cfg(feature = "parser")]
2805/// # Errors
2806///
2807/// Returns an error if `input` is not a valid CSS `unicode-bidi` value.
2808pub fn parse_style_unicode_bidi(
2809    input: &str,
2810) -> Result<StyleUnicodeBidi, StyleUnicodeBidiParseError<'_>> {
2811    match input.trim() {
2812        "normal" => Ok(StyleUnicodeBidi::Normal),
2813        "embed" => Ok(StyleUnicodeBidi::Embed),
2814        "isolate" => Ok(StyleUnicodeBidi::Isolate),
2815        "bidi-override" => Ok(StyleUnicodeBidi::BidiOverride),
2816        "isolate-override" => Ok(StyleUnicodeBidi::IsolateOverride),
2817        "plaintext" => Ok(StyleUnicodeBidi::Plaintext),
2818        other => Err(StyleUnicodeBidiParseError::InvalidValue(InvalidValueErr(
2819            other,
2820        ))),
2821    }
2822}
2823
2824// -- StyleTextBoxTrim --
2825
2826/// Represents the `text-box-trim` CSS property.
2827///
2828/// Controls whether the leading is trimmed at the start/end of a block container.
2829#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2830#[repr(C)]
2831#[derive(Default)]
2832pub enum StyleTextBoxTrim {
2833    /// No trimming
2834    #[default]
2835    None,
2836    /// Trim leading over the first formatted line
2837    TrimStart,
2838    /// Trim leading under the last formatted line
2839    TrimEnd,
2840    /// Trim both start and end
2841    TrimBoth,
2842}
2843impl_option!(
2844    StyleTextBoxTrim,
2845    OptionStyleTextBoxTrim,
2846    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
2847);
2848impl PrintAsCssValue for StyleTextBoxTrim {
2849    fn print_as_css_value(&self) -> String {
2850        String::from(match self {
2851            Self::None => "none",
2852            Self::TrimStart => "trim-start",
2853            Self::TrimEnd => "trim-end",
2854            Self::TrimBoth => "trim-both",
2855        })
2856    }
2857}
2858
2859#[cfg(feature = "parser")]
2860#[derive(Clone, PartialEq, Eq)]
2861pub enum StyleTextBoxTrimParseError<'a> {
2862    InvalidValue(InvalidValueErr<'a>),
2863}
2864#[cfg(feature = "parser")]
2865impl_debug_as_display!(StyleTextBoxTrimParseError<'a>);
2866#[cfg(feature = "parser")]
2867impl_display! { StyleTextBoxTrimParseError<'a>, {
2868    InvalidValue(e) => format!("Invalid text-box-trim value: \"{}\"", e.0),
2869}}
2870#[cfg(feature = "parser")]
2871impl_from!(
2872    InvalidValueErr<'a>,
2873    StyleTextBoxTrimParseError::InvalidValue
2874);
2875
2876#[cfg(feature = "parser")]
2877#[derive(Debug, Clone, PartialEq, Eq)]
2878#[repr(C, u8)]
2879pub enum StyleTextBoxTrimParseErrorOwned {
2880    InvalidValue(InvalidValueErrOwned),
2881}
2882
2883#[cfg(feature = "parser")]
2884impl StyleTextBoxTrimParseError<'_> {
2885    #[must_use]
2886    pub fn to_contained(&self) -> StyleTextBoxTrimParseErrorOwned {
2887        match self {
2888            Self::InvalidValue(e) => {
2889                StyleTextBoxTrimParseErrorOwned::InvalidValue(e.to_contained())
2890            }
2891        }
2892    }
2893}
2894
2895#[cfg(feature = "parser")]
2896impl StyleTextBoxTrimParseErrorOwned {
2897    #[must_use]
2898    pub fn to_shared(&self) -> StyleTextBoxTrimParseError<'_> {
2899        match self {
2900            Self::InvalidValue(e) => StyleTextBoxTrimParseError::InvalidValue(e.to_shared()),
2901        }
2902    }
2903}
2904
2905#[cfg(feature = "parser")]
2906/// # Errors
2907///
2908/// Returns an error if `input` is not a valid CSS `text-box-trim` value.
2909pub fn parse_style_text_box_trim(
2910    input: &str,
2911) -> Result<StyleTextBoxTrim, StyleTextBoxTrimParseError<'_>> {
2912    match input.trim() {
2913        "none" => Ok(StyleTextBoxTrim::None),
2914        "trim-start" => Ok(StyleTextBoxTrim::TrimStart),
2915        "trim-end" => Ok(StyleTextBoxTrim::TrimEnd),
2916        "trim-both" => Ok(StyleTextBoxTrim::TrimBoth),
2917        other => Err(StyleTextBoxTrimParseError::InvalidValue(InvalidValueErr(
2918            other,
2919        ))),
2920    }
2921}
2922
2923// -- StyleTextBoxEdge --
2924
2925/// The OVER edge metric of `text-box-edge` (first value).
2926#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
2927#[repr(C)]
2928pub enum TextBoxEdgeOver {
2929    // +spec:line-height:cc03df - Auto uses line-fit-edge value, interpreting leading (initial) as text
2930    /// Use the line-fit-edge value (initial: text). `auto` is single-value
2931    /// only: it cannot be paired with an under keyword.
2932    #[default]
2933    Auto,
2934    /// Use the text-over baseline
2935    Text,
2936    /// Use the cap-height baseline
2937    Cap,
2938    /// Use the x-height baseline
2939    Ex,
2940    /// Use the ideographic-over baseline
2941    Ideographic,
2942    /// Use the ideographic-ink-over baseline
2943    IdeographicInk,
2944}
2945
2946/// The UNDER edge metric of `text-box-edge` (second value).
2947#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
2948#[repr(C)]
2949pub enum TextBoxEdgeUnder {
2950    /// Follows the over edge's `auto` (single-value form only).
2951    #[default]
2952    Auto,
2953    /// Use the text-under baseline
2954    Text,
2955    /// Use the alphabetic baseline
2956    Alphabetic,
2957    /// Use the ideographic-under baseline
2958    Ideographic,
2959    /// Use the ideographic-ink-under baseline
2960    IdeographicInk,
2961}
2962
2963/// Represents the `text-box-edge` CSS property.
2964///
2965/// Specifies the metrics used for determining the over/under edges of text
2966/// for the purposes of `text-box-trim`.
2967// +spec:writing-modes:daad86 - first value = over edge, second = under edge; single value applies to both (else "text" assumed for missing)
2968///
2969/// Grammar: `auto | [ text | cap | ex | ideographic | ideographic-ink ]
2970/// [ text | alphabetic | ideographic | ideographic-ink ]?`. With one value,
2971/// both edges take that keyword when it exists on both axes (`text`,
2972/// `ideographic`, `ideographic-ink`); otherwise (`cap`, `ex`) the under edge
2973/// is assumed `text`.
2974#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
2975#[repr(C)]
2976pub struct StyleTextBoxEdge {
2977    pub over: TextBoxEdgeOver,
2978    pub under: TextBoxEdgeUnder,
2979}
2980
2981impl StyleTextBoxEdge {
2982    /// The initial value: `auto` (over and under follow line-fit-edge).
2983    pub const AUTO: Self = Self {
2984        over: TextBoxEdgeOver::Auto,
2985        under: TextBoxEdgeUnder::Auto,
2986    };
2987}
2988impl_option!(
2989    StyleTextBoxEdge,
2990    OptionStyleTextBoxEdge,
2991    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
2992);
2993impl PrintAsCssValue for StyleTextBoxEdge {
2994    fn print_as_css_value(&self) -> String {
2995        let over = match self.over {
2996            TextBoxEdgeOver::Auto => return String::from("auto"),
2997            TextBoxEdgeOver::Text => "text",
2998            TextBoxEdgeOver::Cap => "cap",
2999            TextBoxEdgeOver::Ex => "ex",
3000            TextBoxEdgeOver::Ideographic => "ideographic",
3001            TextBoxEdgeOver::IdeographicInk => "ideographic-ink",
3002        };
3003        let under = match self.under {
3004            TextBoxEdgeUnder::Auto | TextBoxEdgeUnder::Text => "text",
3005            TextBoxEdgeUnder::Alphabetic => "alphabetic",
3006            TextBoxEdgeUnder::Ideographic => "ideographic",
3007            TextBoxEdgeUnder::IdeographicInk => "ideographic-ink",
3008        };
3009        // Serialize the shortest form: omit the under edge when the single-
3010        // value form round-trips to the same pair.
3011        let single_round_trips = match self.over {
3012            TextBoxEdgeOver::Text => {
3013                matches!(self.under, TextBoxEdgeUnder::Text | TextBoxEdgeUnder::Auto)
3014            }
3015            TextBoxEdgeOver::Cap | TextBoxEdgeOver::Ex => {
3016                matches!(self.under, TextBoxEdgeUnder::Text | TextBoxEdgeUnder::Auto)
3017            }
3018            TextBoxEdgeOver::Ideographic => matches!(self.under, TextBoxEdgeUnder::Ideographic),
3019            TextBoxEdgeOver::IdeographicInk => {
3020                matches!(self.under, TextBoxEdgeUnder::IdeographicInk)
3021            }
3022            TextBoxEdgeOver::Auto => unreachable!(),
3023        };
3024        if single_round_trips {
3025            String::from(over)
3026        } else {
3027            alloc::format!("{over} {under}")
3028        }
3029    }
3030}
3031
3032#[cfg(feature = "parser")]
3033#[derive(Clone, PartialEq, Eq)]
3034pub enum StyleTextBoxEdgeParseError<'a> {
3035    InvalidValue(InvalidValueErr<'a>),
3036}
3037#[cfg(feature = "parser")]
3038impl_debug_as_display!(StyleTextBoxEdgeParseError<'a>);
3039#[cfg(feature = "parser")]
3040impl_display! { StyleTextBoxEdgeParseError<'a>, {
3041    InvalidValue(e) => format!("Invalid text-box-edge value: \"{}\"", e.0),
3042}}
3043#[cfg(feature = "parser")]
3044impl_from!(
3045    InvalidValueErr<'a>,
3046    StyleTextBoxEdgeParseError::InvalidValue
3047);
3048
3049#[cfg(feature = "parser")]
3050#[derive(Debug, Clone, PartialEq, Eq)]
3051#[repr(C, u8)]
3052pub enum StyleTextBoxEdgeParseErrorOwned {
3053    InvalidValue(InvalidValueErrOwned),
3054}
3055
3056#[cfg(feature = "parser")]
3057impl StyleTextBoxEdgeParseError<'_> {
3058    #[must_use]
3059    pub fn to_contained(&self) -> StyleTextBoxEdgeParseErrorOwned {
3060        match self {
3061            Self::InvalidValue(e) => {
3062                StyleTextBoxEdgeParseErrorOwned::InvalidValue(e.to_contained())
3063            }
3064        }
3065    }
3066}
3067
3068#[cfg(feature = "parser")]
3069impl StyleTextBoxEdgeParseErrorOwned {
3070    #[must_use]
3071    pub fn to_shared(&self) -> StyleTextBoxEdgeParseError<'_> {
3072        match self {
3073            Self::InvalidValue(e) => StyleTextBoxEdgeParseError::InvalidValue(e.to_shared()),
3074        }
3075    }
3076}
3077
3078#[cfg(feature = "parser")]
3079/// # Errors
3080///
3081/// Returns an error if `input` is not a valid CSS `text-box-edge` value.
3082pub fn parse_style_text_box_edge(
3083    input: &str,
3084) -> Result<StyleTextBoxEdge, StyleTextBoxEdgeParseError<'_>> {
3085    let trimmed = input.trim();
3086    let mut parts = trimmed.split_whitespace();
3087    let first = parts.next().unwrap_or("");
3088    let second = parts.next();
3089    if parts.next().is_some() {
3090        return Err(StyleTextBoxEdgeParseError::InvalidValue(InvalidValueErr(
3091            input,
3092        )));
3093    }
3094
3095    let over = match first {
3096        "auto" => {
3097            // `auto` is single-value only.
3098            if second.is_some() {
3099                return Err(StyleTextBoxEdgeParseError::InvalidValue(InvalidValueErr(
3100                    input,
3101                )));
3102            }
3103            return Ok(StyleTextBoxEdge::AUTO);
3104        }
3105        "text" => TextBoxEdgeOver::Text,
3106        "cap" => TextBoxEdgeOver::Cap,
3107        "ex" => TextBoxEdgeOver::Ex,
3108        "ideographic" => TextBoxEdgeOver::Ideographic,
3109        "ideographic-ink" => TextBoxEdgeOver::IdeographicInk,
3110        other => {
3111            return Err(StyleTextBoxEdgeParseError::InvalidValue(InvalidValueErr(
3112                other,
3113            )))
3114        }
3115    };
3116
3117    let under = match second {
3118        Some("text") => TextBoxEdgeUnder::Text,
3119        Some("alphabetic") => TextBoxEdgeUnder::Alphabetic,
3120        Some("ideographic") => TextBoxEdgeUnder::Ideographic,
3121        Some("ideographic-ink") => TextBoxEdgeUnder::IdeographicInk,
3122        Some(other) => {
3123            return Err(StyleTextBoxEdgeParseError::InvalidValue(InvalidValueErr(
3124                other,
3125            )))
3126        }
3127        // Single-value form: both edges take the keyword when it exists on
3128        // both axes; otherwise `text` is assumed for the missing under edge.
3129        None => match over {
3130            TextBoxEdgeOver::Ideographic => TextBoxEdgeUnder::Ideographic,
3131            TextBoxEdgeOver::IdeographicInk => TextBoxEdgeUnder::IdeographicInk,
3132            _ => TextBoxEdgeUnder::Text,
3133        },
3134    };
3135
3136    Ok(StyleTextBoxEdge { over, under })
3137}
3138
3139// -- StyleDominantBaseline --
3140
3141/// Represents the `dominant-baseline` CSS property.
3142///
3143/// Specifies the dominant baseline used to align inline-level contents.
3144#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
3145#[repr(C)]
3146#[derive(Default)]
3147pub enum StyleDominantBaseline {
3148    /// Use the dominant baseline of the parent
3149    #[default]
3150    Auto,
3151    /// Use the text-under baseline
3152    TextBottom,
3153    /// Use the alphabetic baseline
3154    Alphabetic,
3155    /// Use the ideographic baseline
3156    Ideographic,
3157    /// Use the middle baseline
3158    Middle,
3159    /// Use the central baseline
3160    Central,
3161    /// Use the mathematical baseline
3162    Mathematical,
3163    /// Use the hanging baseline
3164    Hanging,
3165    /// Use the text-over baseline
3166    TextTop,
3167}
3168impl_option!(
3169    StyleDominantBaseline,
3170    OptionStyleDominantBaseline,
3171    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
3172);
3173impl PrintAsCssValue for StyleDominantBaseline {
3174    fn print_as_css_value(&self) -> String {
3175        String::from(match self {
3176            Self::Auto => "auto",
3177            Self::TextBottom => "text-bottom",
3178            Self::Alphabetic => "alphabetic",
3179            Self::Ideographic => "ideographic",
3180            Self::Middle => "middle",
3181            Self::Central => "central",
3182            Self::Mathematical => "mathematical",
3183            Self::Hanging => "hanging",
3184            Self::TextTop => "text-top",
3185        })
3186    }
3187}
3188
3189#[cfg(feature = "parser")]
3190#[derive(Clone, PartialEq, Eq)]
3191pub enum StyleDominantBaselineParseError<'a> {
3192    InvalidValue(InvalidValueErr<'a>),
3193}
3194#[cfg(feature = "parser")]
3195impl_debug_as_display!(StyleDominantBaselineParseError<'a>);
3196#[cfg(feature = "parser")]
3197impl_display! { StyleDominantBaselineParseError<'a>, {
3198    InvalidValue(e) => format!("Invalid dominant-baseline value: \"{}\"", e.0),
3199}}
3200#[cfg(feature = "parser")]
3201impl_from!(
3202    InvalidValueErr<'a>,
3203    StyleDominantBaselineParseError::InvalidValue
3204);
3205
3206#[cfg(feature = "parser")]
3207#[derive(Debug, Clone, PartialEq, Eq)]
3208#[repr(C, u8)]
3209pub enum StyleDominantBaselineParseErrorOwned {
3210    InvalidValue(InvalidValueErrOwned),
3211}
3212
3213#[cfg(feature = "parser")]
3214impl StyleDominantBaselineParseError<'_> {
3215    #[must_use]
3216    pub fn to_contained(&self) -> StyleDominantBaselineParseErrorOwned {
3217        match self {
3218            Self::InvalidValue(e) => {
3219                StyleDominantBaselineParseErrorOwned::InvalidValue(e.to_contained())
3220            }
3221        }
3222    }
3223}
3224
3225#[cfg(feature = "parser")]
3226impl StyleDominantBaselineParseErrorOwned {
3227    #[must_use]
3228    pub fn to_shared(&self) -> StyleDominantBaselineParseError<'_> {
3229        match self {
3230            Self::InvalidValue(e) => StyleDominantBaselineParseError::InvalidValue(e.to_shared()),
3231        }
3232    }
3233}
3234
3235#[cfg(feature = "parser")]
3236/// # Errors
3237///
3238/// Returns an error if `input` is not a valid CSS `dominant-baseline` value.
3239pub fn parse_style_dominant_baseline(
3240    input: &str,
3241) -> Result<StyleDominantBaseline, StyleDominantBaselineParseError<'_>> {
3242    match input.trim() {
3243        "auto" => Ok(StyleDominantBaseline::Auto),
3244        "text-bottom" => Ok(StyleDominantBaseline::TextBottom),
3245        "alphabetic" => Ok(StyleDominantBaseline::Alphabetic),
3246        "ideographic" => Ok(StyleDominantBaseline::Ideographic),
3247        "middle" => Ok(StyleDominantBaseline::Middle),
3248        "central" => Ok(StyleDominantBaseline::Central),
3249        "mathematical" => Ok(StyleDominantBaseline::Mathematical),
3250        "hanging" => Ok(StyleDominantBaseline::Hanging),
3251        "text-top" => Ok(StyleDominantBaseline::TextTop),
3252        other => Err(StyleDominantBaselineParseError::InvalidValue(
3253            InvalidValueErr(other),
3254        )),
3255    }
3256}
3257
3258// -- StyleAlignmentBaseline --
3259
3260// +spec:display-property:c90924 - alignment-baseline property: values, initial value, and applies-to per CSS Inline 3 §4.2.2
3261// +spec:font-metrics:fa4489 - alignment-baseline property: specifies box's alignment baseline used before post-alignment shift
3262// +spec:inline-block:939f05 - alignment-baseline property definition with all spec values (baseline, text-bottom, alphabetic, ideographic, middle, central, mathematical, text-top)
3263/// Represents the `alignment-baseline` CSS property.
3264///
3265/// Specifies which baseline of the element is aligned with the dominant baseline.
3266// +spec:writing-modes:cc8e70 - alignment-baseline values for inline baseline alignment
3267#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
3268#[repr(C)]
3269#[derive(Default)]
3270pub enum StyleAlignmentBaseline {
3271    /// Use the dominant baseline of the parent
3272    #[default]
3273    Baseline,
3274    /// Align to the text-under baseline
3275    TextBottom,
3276    /// Align to the alphabetic baseline
3277    Alphabetic,
3278    /// Align to the ideographic baseline
3279    Ideographic,
3280    /// Align to the middle baseline
3281    Middle,
3282    /// Align to the central baseline
3283    Central,
3284    /// Align to the mathematical baseline
3285    Mathematical,
3286    /// Align to the text-over baseline
3287    TextTop,
3288}
3289impl_option!(
3290    StyleAlignmentBaseline,
3291    OptionStyleAlignmentBaseline,
3292    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
3293);
3294impl PrintAsCssValue for StyleAlignmentBaseline {
3295    fn print_as_css_value(&self) -> String {
3296        String::from(match self {
3297            Self::Baseline => "baseline",
3298            Self::TextBottom => "text-bottom",
3299            Self::Alphabetic => "alphabetic",
3300            Self::Ideographic => "ideographic",
3301            Self::Middle => "middle",
3302            Self::Central => "central",
3303            Self::Mathematical => "mathematical",
3304            Self::TextTop => "text-top",
3305        })
3306    }
3307}
3308
3309#[cfg(feature = "parser")]
3310#[derive(Clone, PartialEq, Eq)]
3311pub enum StyleAlignmentBaselineParseError<'a> {
3312    InvalidValue(InvalidValueErr<'a>),
3313}
3314#[cfg(feature = "parser")]
3315impl_debug_as_display!(StyleAlignmentBaselineParseError<'a>);
3316#[cfg(feature = "parser")]
3317impl_display! { StyleAlignmentBaselineParseError<'a>, {
3318    InvalidValue(e) => format!("Invalid alignment-baseline value: \"{}\"", e.0),
3319}}
3320#[cfg(feature = "parser")]
3321impl_from!(
3322    InvalidValueErr<'a>,
3323    StyleAlignmentBaselineParseError::InvalidValue
3324);
3325
3326#[cfg(feature = "parser")]
3327#[derive(Debug, Clone, PartialEq, Eq)]
3328#[repr(C, u8)]
3329pub enum StyleAlignmentBaselineParseErrorOwned {
3330    InvalidValue(InvalidValueErrOwned),
3331}
3332
3333#[cfg(feature = "parser")]
3334impl StyleAlignmentBaselineParseError<'_> {
3335    #[must_use]
3336    pub fn to_contained(&self) -> StyleAlignmentBaselineParseErrorOwned {
3337        match self {
3338            Self::InvalidValue(e) => {
3339                StyleAlignmentBaselineParseErrorOwned::InvalidValue(e.to_contained())
3340            }
3341        }
3342    }
3343}
3344
3345#[cfg(feature = "parser")]
3346impl StyleAlignmentBaselineParseErrorOwned {
3347    #[must_use]
3348    pub fn to_shared(&self) -> StyleAlignmentBaselineParseError<'_> {
3349        match self {
3350            Self::InvalidValue(e) => StyleAlignmentBaselineParseError::InvalidValue(e.to_shared()),
3351        }
3352    }
3353}
3354
3355#[cfg(feature = "parser")]
3356/// # Errors
3357///
3358/// Returns an error if `input` is not a valid CSS `alignment-baseline` value.
3359pub fn parse_style_alignment_baseline(
3360    input: &str,
3361) -> Result<StyleAlignmentBaseline, StyleAlignmentBaselineParseError<'_>> {
3362    match input.trim() {
3363        "baseline" => Ok(StyleAlignmentBaseline::Baseline),
3364        "text-bottom" => Ok(StyleAlignmentBaseline::TextBottom),
3365        "alphabetic" => Ok(StyleAlignmentBaseline::Alphabetic),
3366        "ideographic" => Ok(StyleAlignmentBaseline::Ideographic),
3367        "middle" => Ok(StyleAlignmentBaseline::Middle),
3368        "central" => Ok(StyleAlignmentBaseline::Central),
3369        "mathematical" => Ok(StyleAlignmentBaseline::Mathematical),
3370        "text-top" => Ok(StyleAlignmentBaseline::TextTop),
3371        other => Err(StyleAlignmentBaselineParseError::InvalidValue(
3372            InvalidValueErr(other),
3373        )),
3374    }
3375}
3376
3377// -- StyleBaselineSource --
3378
3379// +spec:inline-block:939f05 - baseline-source longhand: auto | first | last (auto = last baseline for inline-block / IFC roots, first baseline otherwise)
3380/// Represents the `baseline-source` CSS property.
3381///
3382/// Selects which of the box's baselines is used as its baseline in the parent's
3383/// baseline alignment (CSS Inline Layout Module Level 3 §5.2).
3384#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
3385#[repr(C)]
3386#[derive(Default)]
3387pub enum StyleBaselineSource {
3388    /// `auto`: last baseline for inline-block / IFC roots, first baseline otherwise.
3389    #[default]
3390    Auto,
3391    /// `first`: use the first baseline set.
3392    First,
3393    /// `last`: use the last baseline set.
3394    Last,
3395}
3396impl_option!(
3397    StyleBaselineSource,
3398    OptionStyleBaselineSource,
3399    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
3400);
3401impl PrintAsCssValue for StyleBaselineSource {
3402    fn print_as_css_value(&self) -> String {
3403        String::from(match self {
3404            Self::Auto => "auto",
3405            Self::First => "first",
3406            Self::Last => "last",
3407        })
3408    }
3409}
3410
3411#[cfg(feature = "parser")]
3412#[derive(Clone, PartialEq, Eq)]
3413pub enum StyleBaselineSourceParseError<'a> {
3414    InvalidValue(InvalidValueErr<'a>),
3415}
3416#[cfg(feature = "parser")]
3417impl_debug_as_display!(StyleBaselineSourceParseError<'a>);
3418#[cfg(feature = "parser")]
3419impl_display! { StyleBaselineSourceParseError<'a>, {
3420    InvalidValue(e) => format!("Invalid baseline-source value: \"{}\"", e.0),
3421}}
3422#[cfg(feature = "parser")]
3423impl_from!(
3424    InvalidValueErr<'a>,
3425    StyleBaselineSourceParseError::InvalidValue
3426);
3427
3428#[cfg(feature = "parser")]
3429#[derive(Debug, Clone, PartialEq, Eq)]
3430#[repr(C, u8)]
3431pub enum StyleBaselineSourceParseErrorOwned {
3432    InvalidValue(InvalidValueErrOwned),
3433}
3434
3435#[cfg(feature = "parser")]
3436impl StyleBaselineSourceParseError<'_> {
3437    #[must_use]
3438    pub fn to_contained(&self) -> StyleBaselineSourceParseErrorOwned {
3439        match self {
3440            Self::InvalidValue(e) => {
3441                StyleBaselineSourceParseErrorOwned::InvalidValue(e.to_contained())
3442            }
3443        }
3444    }
3445}
3446
3447#[cfg(feature = "parser")]
3448impl StyleBaselineSourceParseErrorOwned {
3449    #[must_use]
3450    pub fn to_shared(&self) -> StyleBaselineSourceParseError<'_> {
3451        match self {
3452            Self::InvalidValue(e) => StyleBaselineSourceParseError::InvalidValue(e.to_shared()),
3453        }
3454    }
3455}
3456
3457#[cfg(feature = "parser")]
3458/// # Errors
3459///
3460/// Returns an error if `input` is not a valid CSS `baseline-source` value.
3461pub fn parse_style_baseline_source(
3462    input: &str,
3463) -> Result<StyleBaselineSource, StyleBaselineSourceParseError<'_>> {
3464    match input.trim() {
3465        "auto" => Ok(StyleBaselineSource::Auto),
3466        "first" => Ok(StyleBaselineSource::First),
3467        "last" => Ok(StyleBaselineSource::Last),
3468        other => Err(StyleBaselineSourceParseError::InvalidValue(
3469            InvalidValueErr(other),
3470        )),
3471    }
3472}
3473
3474// -- StyleLineFitEdge --
3475
3476// +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
3477// +spec:box-model:0e75c1 - with line-fit-edge:leading (initial), margin/border/padding do not contribute to inline layout bounds
3478/// Represents the `line-fit-edge` CSS property.
3479///
3480/// Selects which font metrics determine the over/under edges used when fitting an
3481/// inline box into its line box (CSS Inline Layout Module Level 3 §5). `Auto` on
3482/// `text-box-edge` defers to this value.
3483#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
3484#[repr(C)]
3485#[derive(Default)]
3486pub enum StyleLineFitEdge {
3487    /// `leading` (initial): size the line box using the line-height leading model.
3488    #[default]
3489    Leading,
3490    /// `text`: use the text-over / text-under baselines.
3491    Text,
3492    /// `cap`: use the cap-height baseline for the over edge.
3493    Cap,
3494    /// `ex`: use the x-height baseline for the over edge.
3495    Ex,
3496    /// `ideographic`: use the ideographic-em baseline.
3497    Ideographic,
3498    /// `ideographic-ink`: use the ideographic-ink baseline.
3499    IdeographicInk,
3500    /// `alphabetic`: use the alphabetic baseline for the under edge.
3501    Alphabetic,
3502}
3503impl_option!(
3504    StyleLineFitEdge,
3505    OptionStyleLineFitEdge,
3506    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
3507);
3508impl PrintAsCssValue for StyleLineFitEdge {
3509    fn print_as_css_value(&self) -> String {
3510        String::from(match self {
3511            Self::Leading => "leading",
3512            Self::Text => "text",
3513            Self::Cap => "cap",
3514            Self::Ex => "ex",
3515            Self::Ideographic => "ideographic",
3516            Self::IdeographicInk => "ideographic-ink",
3517            Self::Alphabetic => "alphabetic",
3518        })
3519    }
3520}
3521
3522#[cfg(feature = "parser")]
3523#[derive(Clone, PartialEq, Eq)]
3524pub enum StyleLineFitEdgeParseError<'a> {
3525    InvalidValue(InvalidValueErr<'a>),
3526}
3527#[cfg(feature = "parser")]
3528impl_debug_as_display!(StyleLineFitEdgeParseError<'a>);
3529#[cfg(feature = "parser")]
3530impl_display! { StyleLineFitEdgeParseError<'a>, {
3531    InvalidValue(e) => format!("Invalid line-fit-edge value: \"{}\"", e.0),
3532}}
3533#[cfg(feature = "parser")]
3534impl_from!(
3535    InvalidValueErr<'a>,
3536    StyleLineFitEdgeParseError::InvalidValue
3537);
3538
3539#[cfg(feature = "parser")]
3540#[derive(Debug, Clone, PartialEq, Eq)]
3541#[repr(C, u8)]
3542pub enum StyleLineFitEdgeParseErrorOwned {
3543    InvalidValue(InvalidValueErrOwned),
3544}
3545
3546#[cfg(feature = "parser")]
3547impl StyleLineFitEdgeParseError<'_> {
3548    #[must_use]
3549    pub fn to_contained(&self) -> StyleLineFitEdgeParseErrorOwned {
3550        match self {
3551            Self::InvalidValue(e) => {
3552                StyleLineFitEdgeParseErrorOwned::InvalidValue(e.to_contained())
3553            }
3554        }
3555    }
3556}
3557
3558#[cfg(feature = "parser")]
3559impl StyleLineFitEdgeParseErrorOwned {
3560    #[must_use]
3561    pub fn to_shared(&self) -> StyleLineFitEdgeParseError<'_> {
3562        match self {
3563            Self::InvalidValue(e) => StyleLineFitEdgeParseError::InvalidValue(e.to_shared()),
3564        }
3565    }
3566}
3567
3568#[cfg(feature = "parser")]
3569/// # Errors
3570///
3571/// Returns an error if `input` is not a valid CSS `line-fit-edge` value.
3572pub fn parse_style_line_fit_edge(
3573    input: &str,
3574) -> Result<StyleLineFitEdge, StyleLineFitEdgeParseError<'_>> {
3575    match input.trim() {
3576        "leading" => Ok(StyleLineFitEdge::Leading),
3577        "text" => Ok(StyleLineFitEdge::Text),
3578        "cap" => Ok(StyleLineFitEdge::Cap),
3579        "ex" => Ok(StyleLineFitEdge::Ex),
3580        "ideographic" => Ok(StyleLineFitEdge::Ideographic),
3581        "ideographic-ink" => Ok(StyleLineFitEdge::IdeographicInk),
3582        "alphabetic" => Ok(StyleLineFitEdge::Alphabetic),
3583        other => Err(StyleLineFitEdgeParseError::InvalidValue(InvalidValueErr(
3584            other,
3585        ))),
3586    }
3587}
3588
3589// -- StyleInitialLetterAlign --
3590
3591/// Represents the `initial-letter-align` CSS property.
3592///
3593/// Specifies the alignment points used to align an initial letter.
3594#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
3595#[repr(C)]
3596#[derive(Default)]
3597pub enum StyleInitialLetterAlign {
3598    /// Automatically determine alignment based on script
3599    #[default]
3600    Auto,
3601    /// Align to the alphabetic baseline
3602    Alphabetic,
3603    /// Align to the hanging baseline
3604    Hanging,
3605    /// Align to the ideographic baseline
3606    Ideographic,
3607}
3608impl_option!(
3609    StyleInitialLetterAlign,
3610    OptionStyleInitialLetterAlign,
3611    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
3612);
3613impl PrintAsCssValue for StyleInitialLetterAlign {
3614    fn print_as_css_value(&self) -> String {
3615        String::from(match self {
3616            Self::Auto => "auto",
3617            Self::Alphabetic => "alphabetic",
3618            Self::Hanging => "hanging",
3619            Self::Ideographic => "ideographic",
3620        })
3621    }
3622}
3623
3624#[cfg(feature = "parser")]
3625#[derive(Clone, PartialEq, Eq)]
3626pub enum StyleInitialLetterAlignParseError<'a> {
3627    InvalidValue(InvalidValueErr<'a>),
3628}
3629#[cfg(feature = "parser")]
3630impl_debug_as_display!(StyleInitialLetterAlignParseError<'a>);
3631#[cfg(feature = "parser")]
3632impl_display! { StyleInitialLetterAlignParseError<'a>, {
3633    InvalidValue(e) => format!("Invalid initial-letter-align value: \"{}\"", e.0),
3634}}
3635#[cfg(feature = "parser")]
3636impl_from!(
3637    InvalidValueErr<'a>,
3638    StyleInitialLetterAlignParseError::InvalidValue
3639);
3640
3641#[cfg(feature = "parser")]
3642#[derive(Debug, Clone, PartialEq, Eq)]
3643#[repr(C, u8)]
3644pub enum StyleInitialLetterAlignParseErrorOwned {
3645    InvalidValue(InvalidValueErrOwned),
3646}
3647
3648#[cfg(feature = "parser")]
3649impl StyleInitialLetterAlignParseError<'_> {
3650    #[must_use]
3651    pub fn to_contained(&self) -> StyleInitialLetterAlignParseErrorOwned {
3652        match self {
3653            Self::InvalidValue(e) => {
3654                StyleInitialLetterAlignParseErrorOwned::InvalidValue(e.to_contained())
3655            }
3656        }
3657    }
3658}
3659
3660#[cfg(feature = "parser")]
3661impl StyleInitialLetterAlignParseErrorOwned {
3662    #[must_use]
3663    pub fn to_shared(&self) -> StyleInitialLetterAlignParseError<'_> {
3664        match self {
3665            Self::InvalidValue(e) => StyleInitialLetterAlignParseError::InvalidValue(e.to_shared()),
3666        }
3667    }
3668}
3669
3670#[cfg(feature = "parser")]
3671/// # Errors
3672///
3673/// Returns an error if `input` is not a valid CSS `initial-letter-align` value.
3674pub fn parse_style_initial_letter_align(
3675    input: &str,
3676) -> Result<StyleInitialLetterAlign, StyleInitialLetterAlignParseError<'_>> {
3677    match input.trim() {
3678        "auto" => Ok(StyleInitialLetterAlign::Auto),
3679        "alphabetic" => Ok(StyleInitialLetterAlign::Alphabetic),
3680        "hanging" => Ok(StyleInitialLetterAlign::Hanging),
3681        "ideographic" => Ok(StyleInitialLetterAlign::Ideographic),
3682        other => Err(StyleInitialLetterAlignParseError::InvalidValue(
3683            InvalidValueErr(other),
3684        )),
3685    }
3686}
3687
3688// -- StyleInitialLetterWrap --
3689
3690/// Represents the `initial-letter-wrap` CSS property.
3691///
3692/// Specifies how text adjacent to an initial letter wraps.
3693#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
3694#[repr(C)]
3695#[derive(Default)]
3696pub enum StyleInitialLetterWrap {
3697    /// No special wrapping around the initial letter
3698    #[default]
3699    None,
3700    /// Wrap only the first line adjacent to the initial letter
3701    First,
3702    /// Wrap all lines adjacent to the initial letter
3703    All,
3704    /// Wrap using a grid-based layout
3705    Grid,
3706}
3707impl_option!(
3708    StyleInitialLetterWrap,
3709    OptionStyleInitialLetterWrap,
3710    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
3711);
3712impl PrintAsCssValue for StyleInitialLetterWrap {
3713    fn print_as_css_value(&self) -> String {
3714        String::from(match self {
3715            Self::None => "none",
3716            Self::First => "first",
3717            Self::All => "all",
3718            Self::Grid => "grid",
3719        })
3720    }
3721}
3722
3723#[cfg(feature = "parser")]
3724#[derive(Clone, PartialEq, Eq)]
3725pub enum StyleInitialLetterWrapParseError<'a> {
3726    InvalidValue(InvalidValueErr<'a>),
3727}
3728#[cfg(feature = "parser")]
3729impl_debug_as_display!(StyleInitialLetterWrapParseError<'a>);
3730#[cfg(feature = "parser")]
3731impl_display! { StyleInitialLetterWrapParseError<'a>, {
3732    InvalidValue(e) => format!("Invalid initial-letter-wrap value: \"{}\"", e.0),
3733}}
3734#[cfg(feature = "parser")]
3735impl_from!(
3736    InvalidValueErr<'a>,
3737    StyleInitialLetterWrapParseError::InvalidValue
3738);
3739
3740#[cfg(feature = "parser")]
3741#[derive(Debug, Clone, PartialEq, Eq)]
3742#[repr(C, u8)]
3743pub enum StyleInitialLetterWrapParseErrorOwned {
3744    InvalidValue(InvalidValueErrOwned),
3745}
3746
3747#[cfg(feature = "parser")]
3748impl StyleInitialLetterWrapParseError<'_> {
3749    #[must_use]
3750    pub fn to_contained(&self) -> StyleInitialLetterWrapParseErrorOwned {
3751        match self {
3752            Self::InvalidValue(e) => {
3753                StyleInitialLetterWrapParseErrorOwned::InvalidValue(e.to_contained())
3754            }
3755        }
3756    }
3757}
3758
3759#[cfg(feature = "parser")]
3760impl StyleInitialLetterWrapParseErrorOwned {
3761    #[must_use]
3762    pub fn to_shared(&self) -> StyleInitialLetterWrapParseError<'_> {
3763        match self {
3764            Self::InvalidValue(e) => StyleInitialLetterWrapParseError::InvalidValue(e.to_shared()),
3765        }
3766    }
3767}
3768
3769#[cfg(feature = "parser")]
3770/// # Errors
3771///
3772/// Returns an error if `input` is not a valid CSS `initial-letter-wrap` value.
3773pub fn parse_style_initial_letter_wrap(
3774    input: &str,
3775) -> Result<StyleInitialLetterWrap, StyleInitialLetterWrapParseError<'_>> {
3776    match input.trim() {
3777        "none" => Ok(StyleInitialLetterWrap::None),
3778        "first" => Ok(StyleInitialLetterWrap::First),
3779        "all" => Ok(StyleInitialLetterWrap::All),
3780        "grid" => Ok(StyleInitialLetterWrap::Grid),
3781        other => Err(StyleInitialLetterWrapParseError::InvalidValue(
3782            InvalidValueErr(other),
3783        )),
3784    }
3785}
3786
3787#[cfg(test)]
3788#[allow(clippy::float_cmp, clippy::too_many_lines, clippy::cast_precision_loss)]
3789mod autotest_generated {
3790    use super::*;
3791    use crate::props::basic::length::SizeMetric;
3792
3793    const OPAQUE_BLACK: ColorU = ColorU {
3794        r: 0,
3795        g: 0,
3796        b: 0,
3797        a: 255,
3798    };
3799    const OPAQUE_WHITE: ColorU = ColorU {
3800        r: 255,
3801        g: 255,
3802        b: 255,
3803        a: 255,
3804    };
3805    const TRANSPARENT_BLACK: ColorU = ColorU {
3806        r: 0,
3807        g: 0,
3808        b: 0,
3809        a: 0,
3810    };
3811
3812    /// `FloatValue` encodes an f32 as `isize` scaled by this factor.
3813    const SCALE: isize = 1000;
3814
3815    // =====================================================================
3816    // StyleTextIndent — constructors + numeric edges
3817    // =====================================================================
3818
3819    #[test]
3820    fn text_indent_zero_is_the_neutral_element() {
3821        let z = StyleTextIndent::zero();
3822        assert_eq!(z, StyleTextIndent::default());
3823        assert_eq!(z.inner.metric, SizeMetric::Px);
3824        assert_eq!(z.inner.number.get(), 0.0);
3825        assert!(!z.each_line);
3826        assert!(!z.hanging);
3827        assert_eq!(z.print_as_css_value(), "0px");
3828    }
3829
3830    #[test]
3831    fn text_indent_const_ctors_pin_metric_and_value() {
3832        // (constructed value, expected metric) for 0 / positive / negative.
3833        for v in [0_isize, 1, -1, 42, -42] {
3834            let cases = [
3835                (StyleTextIndent::const_px(v), SizeMetric::Px),
3836                (StyleTextIndent::const_em(v), SizeMetric::Em),
3837                (StyleTextIndent::const_pt(v), SizeMetric::Pt),
3838                (StyleTextIndent::const_percent(v), SizeMetric::Percent),
3839                (StyleTextIndent::const_in(v), SizeMetric::In),
3840                (StyleTextIndent::const_cm(v), SizeMetric::Cm),
3841                (StyleTextIndent::const_mm(v), SizeMetric::Mm),
3842            ];
3843            for (got, metric) in cases {
3844                assert_eq!(got.inner.metric, metric, "metric for {v}");
3845                assert_eq!(got.inner.number.get(), v as f32, "value for {v} {metric:?}");
3846                // The keyword flags are never set by the numeric constructors.
3847                assert!(!got.each_line && !got.hanging);
3848            }
3849        }
3850    }
3851
3852    #[test]
3853    fn text_indent_const_ctors_hold_the_whole_encodable_isize_range() {
3854        // The isize encoding is `value * 1000`, so the representable input range is
3855        // isize::MIN/1000 ..= isize::MAX/1000. Pin the scale, then both ends of it.
3856        assert_eq!(StyleTextIndent::const_px(1).inner.number.number(), SCALE);
3857
3858        let max = isize::MAX / SCALE;
3859        let min = isize::MIN / SCALE;
3860        assert_eq!(
3861            StyleTextIndent::const_px(max).inner.number.number(),
3862            max * SCALE
3863        );
3864        assert_eq!(
3865            StyleTextIndent::const_px(min).inner.number.number(),
3866            min * SCALE
3867        );
3868        assert_eq!(
3869            StyleTextIndent::const_from_metric(SizeMetric::Em, max)
3870                .inner
3871                .number
3872                .number(),
3873            max * SCALE
3874        );
3875        // NOTE: one step past those bounds (e.g. `const_px(isize::MAX)`) overflows the
3876        // `value * 1000` multiply and panics in debug. See the report — not asserted here
3877        // because the behaviour differs between debug (panic) and release (wrap).
3878    }
3879
3880    #[test]
3881    fn text_indent_float_ctors_saturate_on_nan_and_infinity() {
3882        // f32 -> isize is a saturating `as` cast: NaN -> 0, +inf -> MAX, -inf -> MIN.
3883        assert_eq!(StyleTextIndent::px(f32::NAN).inner.number.get(), 0.0);
3884        assert_eq!(StyleTextIndent::em(f32::NAN).inner.number.get(), 0.0);
3885
3886        assert_eq!(
3887            StyleTextIndent::px(f32::INFINITY).inner.number.number(),
3888            isize::MAX
3889        );
3890        assert_eq!(
3891            StyleTextIndent::px(f32::NEG_INFINITY).inner.number.number(),
3892            isize::MIN
3893        );
3894        assert_eq!(
3895            StyleTextIndent::pt(f32::MAX).inner.number.number(),
3896            isize::MAX
3897        );
3898        assert_eq!(
3899            StyleTextIndent::pt(-f32::MAX).inner.number.number(),
3900            isize::MIN
3901        );
3902
3903        // Sub-precision magnitudes collapse to zero rather than trapping.
3904        assert_eq!(
3905            StyleTextIndent::percent(f32::MIN_POSITIVE)
3906                .inner
3907                .number
3908                .number(),
3909            0
3910        );
3911        assert_eq!(StyleTextIndent::px(-0.0).inner.number.number(), 0);
3912
3913        // Every saturated result is still a finite, readable f32.
3914        for v in [
3915            f32::NAN,
3916            f32::INFINITY,
3917            f32::NEG_INFINITY,
3918            f32::MAX,
3919            -f32::MAX,
3920        ] {
3921            assert!(StyleTextIndent::px(v).inner.number.get().is_finite());
3922        }
3923    }
3924
3925    #[test]
3926    fn text_indent_from_metric_agrees_with_the_typed_ctors() {
3927        assert_eq!(
3928            StyleTextIndent::from_metric(SizeMetric::Px, 1.5),
3929            StyleTextIndent::px(1.5)
3930        );
3931        assert_eq!(
3932            StyleTextIndent::from_metric(SizeMetric::Em, -2.5),
3933            StyleTextIndent::em(-2.5)
3934        );
3935        assert_eq!(
3936            StyleTextIndent::from_metric(SizeMetric::Pt, 0.0),
3937            StyleTextIndent::pt(0.0)
3938        );
3939        assert_eq!(
3940            StyleTextIndent::from_metric(SizeMetric::Percent, 50.0),
3941            StyleTextIndent::percent(50.0)
3942        );
3943        assert_eq!(
3944            StyleTextIndent::const_from_metric(SizeMetric::Cm, 3),
3945            StyleTextIndent::const_cm(3)
3946        );
3947        assert_eq!(
3948            StyleTextIndent::const_from_metric(SizeMetric::Mm, -3),
3949            StyleTextIndent::const_mm(-3)
3950        );
3951
3952        // A metric with no typed ctor still round-trips through from_metric.
3953        let vw = StyleTextIndent::from_metric(SizeMetric::Vw, 10.0);
3954        assert_eq!(vw.inner.metric, SizeMetric::Vw);
3955        assert_eq!(vw.inner.number.get(), 10.0);
3956    }
3957
3958    #[test]
3959    fn text_indent_interpolate_endpoints_and_extrapolation() {
3960        let a = StyleTextIndent::px(0.0);
3961        let b = StyleTextIndent::px(100.0);
3962
3963        assert_eq!(a.interpolate(&b, 0.0).inner.number.get(), 0.0);
3964        assert_eq!(a.interpolate(&b, 1.0).inner.number.get(), 100.0);
3965        assert_eq!(a.interpolate(&b, 0.5).inner.number.get(), 50.0);
3966        // t outside [0,1] extrapolates rather than clamping.
3967        assert_eq!(a.interpolate(&b, -1.0).inner.number.get(), -100.0);
3968        assert_eq!(a.interpolate(&b, 2.0).inner.number.get(), 200.0);
3969        // Interpolating a value with itself is the identity for any finite t.
3970        assert_eq!(b.interpolate(&b, 0.25), b);
3971    }
3972
3973    #[test]
3974    fn text_indent_interpolate_with_nonfinite_t_is_defined() {
3975        let a = StyleTextIndent::px(0.0);
3976        let b = StyleTextIndent::px(100.0);
3977
3978        // NaN propagates into the f32 -> isize cast, which saturates NaN to 0.
3979        assert_eq!(a.interpolate(&b, f32::NAN).inner.number.get(), 0.0);
3980        // +/-inf saturate to the isize bounds instead of panicking.
3981        assert_eq!(
3982            a.interpolate(&b, f32::INFINITY).inner.number.number(),
3983            isize::MAX
3984        );
3985        assert_eq!(
3986            a.interpolate(&b, f32::NEG_INFINITY).inner.number.number(),
3987            isize::MIN
3988        );
3989        // 0 * inf is NaN, so interpolating equal endpoints by inf collapses to zero.
3990        assert_eq!(b.interpolate(&b, f32::INFINITY).inner.number.get(), 0.0);
3991
3992        for t in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, f32::MAX] {
3993            assert!(a.interpolate(&b, t).inner.number.get().is_finite());
3994        }
3995    }
3996
3997    #[test]
3998    fn text_indent_interpolate_keeps_self_flags_and_normalizes_mixed_metrics() {
3999        let a = StyleTextIndent {
4000            each_line: true,
4001            hanging: true,
4002            ..StyleTextIndent::px(0.0)
4003        };
4004        let b = StyleTextIndent {
4005            each_line: false,
4006            hanging: false,
4007            ..StyleTextIndent::px(10.0)
4008        };
4009
4010        // Flags are taken from `self`, never blended.
4011        let mid = a.interpolate(&b, 0.5);
4012        assert!(mid.each_line && mid.hanging);
4013        assert!(!b.interpolate(&a, 0.5).each_line);
4014
4015        // Mismatched metrics are resolved into px.
4016        let mixed = StyleTextIndent::px(10.0).interpolate(&StyleTextIndent::em(2.0), 0.5);
4017        assert_eq!(mixed.inner.metric, SizeMetric::Px);
4018        assert!(mixed.inner.number.get().is_finite());
4019    }
4020
4021    // =====================================================================
4022    // StyleTextColor::interpolate
4023    // =====================================================================
4024
4025    #[test]
4026    fn text_color_interpolate_endpoints_are_exact() {
4027        let a = StyleTextColor {
4028            inner: OPAQUE_BLACK,
4029        };
4030        let b = StyleTextColor {
4031            inner: OPAQUE_WHITE,
4032        };
4033
4034        assert_eq!(a.interpolate(&b, 0.0), a);
4035        assert_eq!(a.interpolate(&b, 1.0), b);
4036        // 0 + 255*0.5 = 127.5, rounded half-away-from-zero.
4037        assert_eq!(
4038            a.interpolate(&b, 0.5).inner,
4039            ColorU {
4040                r: 128,
4041                g: 128,
4042                b: 128,
4043                a: 255
4044            }
4045        );
4046        assert_eq!(a.print_as_css_value(), "#000000ff");
4047    }
4048
4049    #[test]
4050    fn text_color_interpolate_saturates_out_of_range_t() {
4051        let a = StyleTextColor {
4052            inner: OPAQUE_BLACK,
4053        };
4054        let b = StyleTextColor {
4055            inner: OPAQUE_WHITE,
4056        };
4057
4058        // 0 + 255*2 = 510 -> clamped to 255 by the saturating u8 cast (no wrap to 254).
4059        assert_eq!(a.interpolate(&b, 2.0).inner, OPAQUE_WHITE);
4060        // 0 + 255*-1 = -255 -> clamped to 0 (no wrap to 1).
4061        assert_eq!(a.interpolate(&b, -1.0).inner, OPAQUE_BLACK);
4062    }
4063
4064    #[test]
4065    fn text_color_interpolate_with_nonfinite_t_is_defined() {
4066        let a = StyleTextColor {
4067            inner: OPAQUE_BLACK,
4068        };
4069        let b = StyleTextColor {
4070            inner: OPAQUE_WHITE,
4071        };
4072
4073        // NaN saturates to 0 in every channel — including alpha, so the result is
4074        // transparent black rather than a panic or garbage.
4075        assert_eq!(a.interpolate(&b, f32::NAN).inner, TRANSPARENT_BLACK);
4076
4077        // With t = +inf the changing channels saturate to 255, but alpha is *equal* in
4078        // both endpoints, so it computes 255 + (0 * inf) = NaN and saturates to 0.
4079        // A fully-opaque pair therefore interpolates to a fully-transparent colour.
4080        assert_eq!(
4081            a.interpolate(&b, f32::INFINITY).inner,
4082            ColorU {
4083                r: 255,
4084                g: 255,
4085                b: 255,
4086                a: 0
4087            }
4088        );
4089        assert_eq!(
4090            a.interpolate(&b, f32::NEG_INFINITY).inner,
4091            ColorU {
4092                r: 0,
4093                g: 0,
4094                b: 0,
4095                a: 0
4096            }
4097        );
4098    }
4099
4100    // =====================================================================
4101    // StyleHangingPunctuation::is_enabled (predicate invariants)
4102    // =====================================================================
4103
4104    #[test]
4105    fn hanging_punctuation_is_enabled_matches_its_flags_exhaustively() {
4106        assert!(!StyleHangingPunctuation::default().is_enabled());
4107
4108        for bits in 0_u8..16 {
4109            let hp = StyleHangingPunctuation {
4110                first: bits & 1 != 0,
4111                force_end: bits & 2 != 0,
4112                allow_end: bits & 4 != 0,
4113                last: bits & 8 != 0,
4114            };
4115            assert_eq!(hp.is_enabled(), bits != 0, "bits={bits}");
4116            // is_enabled() is exactly the "prints as something other than none" predicate.
4117            assert_eq!(
4118                hp.print_as_css_value() == "none",
4119                !hp.is_enabled(),
4120                "bits={bits}"
4121            );
4122        }
4123    }
4124
4125    #[test]
4126    fn hanging_punctuation_prints_flags_in_spec_order() {
4127        let all = StyleHangingPunctuation {
4128            first: true,
4129            force_end: true,
4130            allow_end: true,
4131            last: true,
4132        };
4133        assert_eq!(all.print_as_css_value(), "first force-end allow-end last");
4134        assert_eq!(
4135            StyleHangingPunctuation {
4136                last: true,
4137                ..Default::default()
4138            }
4139            .print_as_css_value(),
4140            "last"
4141        );
4142    }
4143
4144    // =====================================================================
4145    // Parser-gated tests
4146    // =====================================================================
4147
4148    #[cfg(feature = "parser")]
4149    mod parser {
4150        use super::super::*;
4151        use crate::props::basic::length::SizeMetric;
4152
4153        const GARBAGE: &[&str] = &[
4154            "",
4155            "   ",
4156            "\t\n",
4157            "!!!",
4158            "\0\0",
4159            "0",
4160            "-0",
4161            "9223372036854775807",
4162            "1e400",
4163            "NaN",
4164            "inf",
4165            "\u{1F600}",
4166            "e\u{0301}",
4167            "\u{202E}left",
4168            "left;garbage",
4169            "left garbage",
4170        ];
4171
4172        /// Every keyword parser must reject junk, and accept its own printed form.
4173        macro_rules! assert_keyword_round_trip {
4174            ($parse:ident, $variants:expr) => {{
4175                for v in $variants {
4176                    let printed = v.print_as_css_value();
4177                    assert_eq!(
4178                        $parse(&printed).as_ref(),
4179                        Ok(&v),
4180                        "round-trip of {printed:?}"
4181                    );
4182                    // Surrounding whitespace is trimmed, not rejected.
4183                    assert_eq!($parse(&format!("  {printed}  ")).as_ref(), Ok(&v));
4184                }
4185                for g in GARBAGE.iter().copied() {
4186                    assert!(
4187                        $parse(g).is_err(),
4188                        "{} accepted garbage {g:?}",
4189                        stringify!($parse)
4190                    );
4191                }
4192            }};
4193        }
4194
4195        #[test]
4196        fn keyword_parsers_round_trip_every_variant_and_reject_garbage() {
4197            type TA = StyleTextAlign;
4198            assert_keyword_round_trip!(
4199                parse_style_text_align,
4200                [
4201                    TA::Left,
4202                    TA::Center,
4203                    TA::Right,
4204                    TA::Justify,
4205                    TA::Start,
4206                    TA::End
4207                ]
4208            );
4209            type WS = StyleWhiteSpace;
4210            assert_keyword_round_trip!(
4211                parse_style_white_space,
4212                [
4213                    WS::Normal,
4214                    WS::Pre,
4215                    WS::Nowrap,
4216                    WS::PreWrap,
4217                    WS::PreLine,
4218                    WS::BreakSpaces
4219                ]
4220            );
4221            type H = StyleHyphens;
4222            assert_keyword_round_trip!(parse_style_hyphens, [H::None, H::Manual, H::Auto]);
4223            type LB = StyleLineBreak;
4224            assert_keyword_round_trip!(
4225                parse_style_line_break,
4226                [LB::Auto, LB::Loose, LB::Normal, LB::Strict, LB::Anywhere]
4227            );
4228            type WB = StyleWordBreak;
4229            assert_keyword_round_trip!(
4230                parse_style_word_break,
4231                [WB::Normal, WB::BreakAll, WB::KeepAll, WB::BreakWord]
4232            );
4233            type OW = StyleOverflowWrap;
4234            assert_keyword_round_trip!(
4235                parse_style_overflow_wrap,
4236                [OW::Normal, OW::Anywhere, OW::BreakWord]
4237            );
4238            type Tal = StyleTextAlignLast;
4239            assert_keyword_round_trip!(
4240                parse_style_text_align_last,
4241                [
4242                    Tal::Auto,
4243                    Tal::Start,
4244                    Tal::End,
4245                    Tal::Left,
4246                    Tal::Right,
4247                    Tal::Center,
4248                    Tal::Justify
4249                ]
4250            );
4251            type TT = StyleTextTransform;
4252            assert_keyword_round_trip!(
4253                parse_style_text_transform,
4254                [
4255                    TT::None,
4256                    TT::Capitalize,
4257                    TT::Uppercase,
4258                    TT::Lowercase,
4259                    TT::FullWidth
4260                ]
4261            );
4262            type D = StyleDirection;
4263            assert_keyword_round_trip!(parse_style_direction, [D::Ltr, D::Rtl]);
4264            type US = StyleUserSelect;
4265            assert_keyword_round_trip!(
4266                parse_style_user_select,
4267                [US::Auto, US::Text, US::None, US::All]
4268            );
4269            type TD = StyleTextDecoration;
4270            assert_keyword_round_trip!(
4271                parse_style_text_decoration,
4272                [TD::None, TD::Underline, TD::Overline, TD::LineThrough]
4273            );
4274            type UB = StyleUnicodeBidi;
4275            assert_keyword_round_trip!(
4276                parse_style_unicode_bidi,
4277                [
4278                    UB::Normal,
4279                    UB::Embed,
4280                    UB::Isolate,
4281                    UB::BidiOverride,
4282                    UB::IsolateOverride,
4283                    UB::Plaintext
4284                ]
4285            );
4286            type Tbt = StyleTextBoxTrim;
4287            assert_keyword_round_trip!(
4288                parse_style_text_box_trim,
4289                [Tbt::None, Tbt::TrimStart, Tbt::TrimEnd, Tbt::TrimBoth]
4290            );
4291            {
4292                // text-box-edge is a two-value property now; round-trip the
4293                // grammar by hand instead of via the single-keyword macro.
4294                use crate::props::style::text::{TextBoxEdgeOver as O, TextBoxEdgeUnder as U};
4295                let cases = [
4296                    ("auto", StyleTextBoxEdge::AUTO),
4297                    (
4298                        "text",
4299                        StyleTextBoxEdge {
4300                            over: O::Text,
4301                            under: U::Text,
4302                        },
4303                    ),
4304                    (
4305                        "cap",
4306                        StyleTextBoxEdge {
4307                            over: O::Cap,
4308                            under: U::Text,
4309                        },
4310                    ),
4311                    (
4312                        "ex",
4313                        StyleTextBoxEdge {
4314                            over: O::Ex,
4315                            under: U::Text,
4316                        },
4317                    ),
4318                    (
4319                        "ideographic",
4320                        StyleTextBoxEdge {
4321                            over: O::Ideographic,
4322                            under: U::Ideographic,
4323                        },
4324                    ),
4325                    (
4326                        "ideographic-ink",
4327                        StyleTextBoxEdge {
4328                            over: O::IdeographicInk,
4329                            under: U::IdeographicInk,
4330                        },
4331                    ),
4332                    (
4333                        "cap alphabetic",
4334                        StyleTextBoxEdge {
4335                            over: O::Cap,
4336                            under: U::Alphabetic,
4337                        },
4338                    ),
4339                    (
4340                        "text ideographic",
4341                        StyleTextBoxEdge {
4342                            over: O::Text,
4343                            under: U::Ideographic,
4344                        },
4345                    ),
4346                ];
4347                for (input, expected) in cases {
4348                    let parsed = parse_style_text_box_edge(input)
4349                        .unwrap_or_else(|e| panic!("`{input}` must parse: {e:?}"));
4350                    assert_eq!(parsed, expected, "parse of `{input}`");
4351                    let printed = parsed.print_as_css_value();
4352                    let reparsed = parse_style_text_box_edge(&printed)
4353                        .unwrap_or_else(|e| panic!("reprint `{printed}` must parse: {e:?}"));
4354                    assert_eq!(reparsed, parsed, "print/parse round trip via `{printed}`");
4355                }
4356                // `auto` cannot take a second value; junk is rejected.
4357                assert!(parse_style_text_box_edge("auto text").is_err());
4358                assert!(
4359                    parse_style_text_box_edge("cap cap").is_err(),
4360                    "cap is over-only"
4361                );
4362                assert!(
4363                    parse_style_text_box_edge("alphabetic").is_err(),
4364                    "alphabetic is under-only"
4365                );
4366                assert!(parse_style_text_box_edge("bogus").is_err());
4367            }
4368            type DB = StyleDominantBaseline;
4369            assert_keyword_round_trip!(
4370                parse_style_dominant_baseline,
4371                [
4372                    DB::Auto,
4373                    DB::TextBottom,
4374                    DB::Alphabetic,
4375                    DB::Ideographic,
4376                    DB::Middle,
4377                    DB::Central,
4378                    DB::Mathematical,
4379                    DB::Hanging,
4380                    DB::TextTop
4381                ]
4382            );
4383            type AB = StyleAlignmentBaseline;
4384            assert_keyword_round_trip!(
4385                parse_style_alignment_baseline,
4386                [
4387                    AB::Baseline,
4388                    AB::TextBottom,
4389                    AB::Alphabetic,
4390                    AB::Ideographic,
4391                    AB::Middle,
4392                    AB::Central,
4393                    AB::Mathematical,
4394                    AB::TextTop
4395                ]
4396            );
4397            type Bs = StyleBaselineSource;
4398            assert_keyword_round_trip!(
4399                parse_style_baseline_source,
4400                [Bs::Auto, Bs::First, Bs::Last]
4401            );
4402            type Lfe = StyleLineFitEdge;
4403            assert_keyword_round_trip!(
4404                parse_style_line_fit_edge,
4405                [
4406                    Lfe::Leading,
4407                    Lfe::Text,
4408                    Lfe::Cap,
4409                    Lfe::Ex,
4410                    Lfe::Ideographic,
4411                    Lfe::IdeographicInk,
4412                    Lfe::Alphabetic
4413                ]
4414            );
4415            type Ila = StyleInitialLetterAlign;
4416            assert_keyword_round_trip!(
4417                parse_style_initial_letter_align,
4418                [Ila::Auto, Ila::Alphabetic, Ila::Hanging, Ila::Ideographic]
4419            );
4420            type Ilw = StyleInitialLetterWrap;
4421            assert_keyword_round_trip!(
4422                parse_style_initial_letter_wrap,
4423                [Ilw::None, Ilw::First, Ilw::All, Ilw::Grid]
4424            );
4425        }
4426
4427        #[test]
4428        fn keyword_parsers_are_case_sensitive() {
4429            // BUG: CSS keywords are ASCII case-insensitive (CSS Syntax 3 §3.1), but every
4430            // `match input.trim()` parser in this file compares exactly, so `text-align: LEFT`
4431            // is rejected. `hanging-punctuation` / `text-combine-upright` *do* fold case, so
4432            // the file is internally inconsistent too. Pinned as-is; see the report.
4433            assert!(parse_style_text_align("LEFT").is_err());
4434            assert!(parse_style_text_align("Left").is_err());
4435            assert!(parse_style_white_space("Normal").is_err());
4436            assert!(parse_style_direction("LTR").is_err());
4437            // ...whereas these two fold case as the spec requires:
4438            assert!(parse_style_hanging_punctuation("FIRST").is_ok());
4439            assert!(parse_style_text_combine_upright("NONE").is_ok());
4440        }
4441
4442        #[test]
4443        fn extremely_long_and_deeply_nested_input_terminates_with_err() {
4444            let long = "a".repeat(1_000_000);
4445            assert!(parse_style_text_align(&long).is_err());
4446            assert!(parse_style_white_space(&long).is_err());
4447            assert!(parse_style_text_color(&long).is_err());
4448            assert!(parse_style_letter_spacing(&long).is_err());
4449            assert!(parse_style_word_spacing(&long).is_err());
4450            assert!(parse_style_tab_size(&long).is_err());
4451            assert!(parse_style_line_height(&long).is_err());
4452            assert!(parse_style_text_indent(&long).is_err());
4453            assert!(parse_style_hanging_punctuation(&long).is_err());
4454            assert!(parse_style_initial_letter(&long).is_err());
4455            assert!(parse_style_line_clamp(&long).is_err());
4456            assert!(parse_style_vertical_align(&long).is_err());
4457
4458            // A 1000-digit integer overflows every numeric target -> Err, never a wrap.
4459            let huge_number = "9".repeat(1000);
4460            assert!(parse_style_line_clamp(&huge_number).is_err());
4461            assert!(parse_style_initial_letter(&huge_number).is_err());
4462
4463            // No parser here recurses, so nesting cannot blow the stack.
4464            let nested = format!("{}{}", "(".repeat(10_000), ")".repeat(10_000));
4465            assert!(parse_style_text_align(&nested).is_err());
4466            assert!(parse_style_letter_spacing(&nested).is_err());
4467            assert!(parse_style_text_indent(&nested).is_err());
4468            assert!(parse_style_line_height(&nested).is_err());
4469        }
4470
4471        // -- pixel-valued properties -------------------------------------------
4472
4473        #[test]
4474        fn spacing_and_tab_size_round_trip_through_their_printed_form() {
4475            for pv in [
4476                PixelValue::px(2.0),
4477                PixelValue::px(-3.0),
4478                PixelValue::em(0.5),
4479                PixelValue::pt(12.0),
4480                PixelValue::percent(10.0),
4481                PixelValue::zero(),
4482            ] {
4483                let ls = StyleLetterSpacing { inner: pv };
4484                assert_eq!(
4485                    parse_style_letter_spacing(&ls.print_as_css_value()).unwrap(),
4486                    ls
4487                );
4488                let ws = StyleWordSpacing { inner: pv };
4489                assert_eq!(
4490                    parse_style_word_spacing(&ws.print_as_css_value()).unwrap(),
4491                    ws
4492                );
4493            }
4494            // tab-size: unitless numbers mean `em`, lengths keep their unit.
4495            let ts = StyleTabSize::default();
4496            assert_eq!(ts.inner, PixelValue::em(8.0));
4497            assert_eq!(parse_style_tab_size(&ts.print_as_css_value()).unwrap(), ts);
4498            assert_eq!(
4499                parse_style_tab_size("4").unwrap().inner,
4500                PixelValue::em(4.0)
4501            );
4502            assert_eq!(
4503                parse_style_tab_size("20px").unwrap().inner,
4504                PixelValue::px(20.0)
4505            );
4506        }
4507
4508        #[test]
4509        fn pixel_parsers_reject_empty_and_unit_only_input() {
4510            for bad in [
4511                "",
4512                "   ",
4513                "\t\n",
4514                "px",
4515                "em",
4516                "abc",
4517                "px px",
4518                "10pxx",
4519                "\u{1F600}",
4520            ] {
4521                assert!(
4522                    parse_style_letter_spacing(bad).is_err(),
4523                    "letter-spacing {bad:?}"
4524                );
4525                assert!(
4526                    parse_style_word_spacing(bad).is_err(),
4527                    "word-spacing {bad:?}"
4528                );
4529            }
4530            // BUG: the shared pixel parser trims *after* stripping the unit suffix, so a space
4531            // between the number and its unit is accepted even though CSS forbids it.
4532            assert_eq!(
4533                parse_style_letter_spacing("10 px").unwrap().inner,
4534                PixelValue::px(10.0)
4535            );
4536        }
4537
4538        #[test]
4539        fn pixel_parsers_accept_float_keywords_and_saturate_instead_of_panicking() {
4540            // BUG: `f32::from_str` accepts "NaN"/"inf"/"1e400", so these are *not* rejected
4541            // as CSS lengths. They cannot panic (the isize cast saturates), but they should
4542            // be Err. Pinned as-is; see the report.
4543            assert_eq!(
4544                parse_style_letter_spacing("NaN")
4545                    .unwrap()
4546                    .inner
4547                    .number
4548                    .number(),
4549                0
4550            );
4551            assert_eq!(
4552                parse_style_letter_spacing("inf")
4553                    .unwrap()
4554                    .inner
4555                    .number
4556                    .number(),
4557                isize::MAX
4558            );
4559            assert_eq!(
4560                parse_style_letter_spacing("-inf")
4561                    .unwrap()
4562                    .inner
4563                    .number
4564                    .number(),
4565                isize::MIN
4566            );
4567            assert_eq!(
4568                parse_style_word_spacing("1e400px")
4569                    .unwrap()
4570                    .inner
4571                    .number
4572                    .number(),
4573                isize::MAX
4574            );
4575            // Same story via the tab-size unitless branch.
4576            assert_eq!(
4577                parse_style_tab_size("NaN").unwrap().inner,
4578                PixelValue::em(0.0)
4579            );
4580            assert!(parse_style_tab_size("inf")
4581                .unwrap()
4582                .inner
4583                .number
4584                .get()
4585                .is_finite());
4586
4587            // Boundary numeric strings that *are* legal still parse to the right value.
4588            assert_eq!(
4589                parse_style_letter_spacing("0").unwrap().inner,
4590                PixelValue::px(0.0)
4591            );
4592            assert_eq!(
4593                parse_style_letter_spacing("-0")
4594                    .unwrap()
4595                    .inner
4596                    .number
4597                    .number(),
4598                0
4599            );
4600            assert!(parse_style_letter_spacing("9223372036854775807px")
4601                .unwrap()
4602                .inner
4603                .number
4604                .get()
4605                .is_finite());
4606        }
4607
4608        // -- text-indent --------------------------------------------------------
4609
4610        #[test]
4611        fn text_indent_round_trips_length_and_keywords() {
4612            for pv in [
4613                PixelValue::px(10.0),
4614                PixelValue::em(-2.0),
4615                PixelValue::percent(50.0),
4616            ] {
4617                for (each_line, hanging) in
4618                    [(false, false), (true, false), (false, true), (true, true)]
4619                {
4620                    let ti = StyleTextIndent {
4621                        inner: pv,
4622                        each_line,
4623                        hanging,
4624                    };
4625                    let printed = ti.print_as_css_value();
4626                    assert_eq!(
4627                        parse_style_text_indent(&printed).unwrap(),
4628                        ti,
4629                        "{printed:?}"
4630                    );
4631                }
4632            }
4633            assert!(parse_style_text_indent("10px hanging").unwrap().hanging);
4634            assert!(parse_style_text_indent("each-line 10px").unwrap().each_line);
4635        }
4636
4637        #[test]
4638        fn text_indent_defaults_missing_length_to_zero_and_keeps_the_last_one() {
4639            // BUG: `text-indent` requires a <length-percentage>; these should all be Err.
4640            // Instead an absent length silently defaults to 0px, so empty/whitespace/keyword-
4641            // only input parses Ok. Pinned as-is; see the report.
4642            assert_eq!(
4643                parse_style_text_indent("").unwrap(),
4644                StyleTextIndent::zero()
4645            );
4646            assert_eq!(
4647                parse_style_text_indent("   ").unwrap(),
4648                StyleTextIndent::zero()
4649            );
4650            assert_eq!(
4651                parse_style_text_indent("hanging").unwrap(),
4652                StyleTextIndent {
4653                    hanging: true,
4654                    ..StyleTextIndent::zero()
4655                }
4656            );
4657            // BUG: repeated lengths are not rejected — the last token silently wins.
4658            assert_eq!(
4659                parse_style_text_indent("10px 20px").unwrap().inner,
4660                PixelValue::px(20.0)
4661            );
4662
4663            // Junk in the length slot is still rejected.
4664            assert!(parse_style_text_indent("garbage").is_err());
4665            assert!(parse_style_text_indent("hanging garbage").is_err());
4666        }
4667
4668        // -- initial-letter -----------------------------------------------------
4669
4670        #[test]
4671        fn initial_letter_parses_size_and_optional_sink() {
4672            assert_eq!(
4673                parse_style_initial_letter("3").unwrap(),
4674                StyleInitialLetter {
4675                    size: 3,
4676                    sink: crate::corety::OptionU32::None
4677                }
4678            );
4679            let with_sink = StyleInitialLetter {
4680                size: 3,
4681                sink: crate::corety::OptionU32::Some(2),
4682            };
4683            assert_eq!(parse_style_initial_letter("3 2").unwrap(), with_sink);
4684            // Printed form round-trips.
4685            assert_eq!(
4686                parse_style_initial_letter(&with_sink.print_as_css_value()).unwrap(),
4687                with_sink
4688            );
4689        }
4690
4691        #[test]
4692        fn initial_letter_rejects_zero_negative_and_overflowing_sizes() {
4693            assert!(
4694                parse_style_initial_letter("0").is_err(),
4695                "size 0 must be rejected"
4696            );
4697            assert!(parse_style_initial_letter("-1").is_err());
4698            assert!(parse_style_initial_letter("1.5").is_err());
4699            assert!(
4700                parse_style_initial_letter("4294967296").is_err(),
4701                "u32::MAX + 1"
4702            );
4703            assert!(parse_style_initial_letter("3 -1").is_err(), "negative sink");
4704            assert!(parse_style_initial_letter("3 x").is_err());
4705            assert!(parse_style_initial_letter("").is_err());
4706            assert!(parse_style_initial_letter("   ").is_err());
4707            // u32::MAX itself is in range.
4708            assert_eq!(
4709                parse_style_initial_letter("4294967295").unwrap().size,
4710                u32::MAX
4711            );
4712            // BUG: a third component should be a parse error, but it is silently dropped.
4713            assert_eq!(
4714                parse_style_initial_letter("3 2 9").unwrap(),
4715                StyleInitialLetter {
4716                    size: 3,
4717                    sink: crate::corety::OptionU32::Some(2),
4718                }
4719            );
4720        }
4721
4722        // -- line-clamp ---------------------------------------------------------
4723
4724        #[test]
4725        fn line_clamp_rejects_zero_and_out_of_range_values() {
4726            assert_eq!(
4727                parse_style_line_clamp("3").unwrap(),
4728                StyleLineClamp { max_lines: 3 }
4729            );
4730            assert_eq!(parse_style_line_clamp("  7  ").unwrap().max_lines, 7);
4731            assert_eq!(
4732                parse_style_line_clamp("0").unwrap_err(),
4733                StyleLineClampParseError::ZeroValue
4734            );
4735            assert!(parse_style_line_clamp("-1").is_err());
4736            assert!(parse_style_line_clamp("1.0").is_err());
4737            assert!(parse_style_line_clamp("").is_err());
4738            assert!(parse_style_line_clamp("   ").is_err());
4739            assert!(parse_style_line_clamp("\u{1F600}").is_err());
4740            // Saturating/wrapping never happens: an out-of-range integer is an error.
4741            assert!(parse_style_line_clamp("99999999999999999999999").is_err());
4742            let max = usize::MAX.to_string();
4743            assert_eq!(parse_style_line_clamp(&max).unwrap().max_lines, usize::MAX);
4744            // Printed form round-trips.
4745            let lc = StyleLineClamp { max_lines: 42 };
4746            assert_eq!(
4747                parse_style_line_clamp(&lc.print_as_css_value()).unwrap(),
4748                lc
4749            );
4750        }
4751
4752        // -- hanging-punctuation ------------------------------------------------
4753
4754        #[test]
4755        fn hanging_punctuation_round_trips_and_enforces_mutual_exclusion() {
4756            for bits in 0_u8..16 {
4757                let hp = StyleHangingPunctuation {
4758                    first: bits & 1 != 0,
4759                    force_end: bits & 2 != 0,
4760                    allow_end: bits & 4 != 0,
4761                    last: bits & 8 != 0,
4762                };
4763                let printed = hp.print_as_css_value();
4764                if hp.force_end && hp.allow_end {
4765                    // `force-end` and `allow-end` are mutually exclusive per CSS Text 3 §8.
4766                    assert!(
4767                        parse_style_hanging_punctuation(&printed).is_err(),
4768                        "{printed:?}"
4769                    );
4770                } else {
4771                    assert_eq!(
4772                        parse_style_hanging_punctuation(&printed).unwrap(),
4773                        hp,
4774                        "{printed:?}"
4775                    );
4776                }
4777            }
4778            assert!(parse_style_hanging_punctuation("first bogus").is_err());
4779            assert!(parse_style_hanging_punctuation("\u{1F600}").is_err());
4780            assert!(parse_style_hanging_punctuation("none first").is_err());
4781        }
4782
4783        #[test]
4784        fn hanging_punctuation_accepts_empty_input_as_none() {
4785            // BUG: empty / whitespace-only input has no tokens, so the loop body never runs
4786            // and the parser returns Ok(none) instead of Err. Pinned as-is; see the report.
4787            assert_eq!(
4788                parse_style_hanging_punctuation("").unwrap(),
4789                StyleHangingPunctuation::default()
4790            );
4791            assert_eq!(
4792                parse_style_hanging_punctuation("   ").unwrap(),
4793                StyleHangingPunctuation::default()
4794            );
4795            // Duplicate keywords are also accepted (idempotent flag set).
4796            assert!(
4797                parse_style_hanging_punctuation("first first")
4798                    .unwrap()
4799                    .first
4800            );
4801        }
4802
4803        // -- text-combine-upright -----------------------------------------------
4804
4805        #[test]
4806        fn text_combine_upright_bounds_the_digits_operand() {
4807            assert_eq!(
4808                parse_style_text_combine_upright("none").unwrap(),
4809                StyleTextCombineUpright::None
4810            );
4811            assert_eq!(
4812                parse_style_text_combine_upright("all").unwrap(),
4813                StyleTextCombineUpright::All
4814            );
4815            for n in 2_u8..=4 {
4816                let v = StyleTextCombineUpright::Digits(n);
4817                assert_eq!(
4818                    parse_style_text_combine_upright(&v.print_as_css_value()).unwrap(),
4819                    v
4820                );
4821            }
4822            // Outside the spec'd 2..=4 range -> Err, not a silent clamp or wrap.
4823            for bad in [
4824                "digits 0",
4825                "digits 1",
4826                "digits 5",
4827                "digits 255",
4828                "digits 256",
4829                "digits -1",
4830            ] {
4831                assert!(
4832                    parse_style_text_combine_upright(bad).is_err(),
4833                    "{bad:?} accepted"
4834                );
4835            }
4836            assert!(parse_style_text_combine_upright("").is_err());
4837            assert!(parse_style_text_combine_upright("bogus").is_err());
4838        }
4839
4840        #[test]
4841        fn text_combine_upright_accepts_garbage_after_the_digits_prefix() {
4842            // BUG: the `digits` branch is chosen by `starts_with("digits")` with no word
4843            // boundary, and any token count != 2 falls back to `digits 2`. So junk that
4844            // merely starts with "digits" parses Ok. Pinned as-is; see the report.
4845            assert_eq!(
4846                parse_style_text_combine_upright("digits").unwrap(),
4847                StyleTextCombineUpright::Digits(2)
4848            );
4849            assert_eq!(
4850                parse_style_text_combine_upright("digitsgarbage").unwrap(),
4851                StyleTextCombineUpright::Digits(2)
4852            );
4853            assert_eq!(
4854                parse_style_text_combine_upright("digits 2 3").unwrap(),
4855                StyleTextCombineUpright::Digits(2)
4856            );
4857        }
4858
4859        // -- line-height --------------------------------------------------------
4860
4861        #[test]
4862        fn line_height_parses_numbers_percentages_and_px() {
4863            assert_eq!(
4864                parse_style_line_height("1.5").unwrap().inner,
4865                PercentageValue::new(150.0)
4866            );
4867            assert_eq!(
4868                parse_style_line_height("120%").unwrap().inner,
4869                PercentageValue::new(120.0)
4870            );
4871            // px lengths are encoded as a *negative* percentage (documented convention).
4872            assert_eq!(
4873                parse_style_line_height("20px").unwrap().inner,
4874                PercentageValue::new(-2000.0)
4875            );
4876            assert!(parse_style_line_height("").is_err());
4877            assert!(parse_style_line_height("   ").is_err());
4878            assert!(parse_style_line_height("abc").is_err());
4879            assert!(parse_style_line_height("\u{1F600}").is_err());
4880            // Printed form round-trips as a value.
4881            let lh = StyleLineHeight::default();
4882            assert_eq!(
4883                parse_style_line_height(&lh.print_as_css_value()).unwrap(),
4884                lh
4885            );
4886        }
4887
4888        #[test]
4889        fn line_height_negative_numbers_alias_absolute_px_lengths() {
4890            // BUG: negative values are the internal marker for "absolute px", but the number
4891            // branch happily parses a negative <number>, so `line-height: -1` and
4892            // `line-height: 1px` produce the *same* value and are indistinguishable
4893            // downstream. A negative line-height is invalid CSS and should be Err.
4894            assert_eq!(
4895                parse_style_line_height("-1").unwrap(),
4896                parse_style_line_height("1px").unwrap()
4897            );
4898            assert_eq!(
4899                parse_style_line_height("-100%").unwrap(),
4900                parse_style_line_height("1px").unwrap()
4901            );
4902        }
4903
4904        #[test]
4905        fn line_height_rejects_em_and_other_length_units() {
4906            // BUG: `line-height: 1.5em` (and rem/pt/...) is valid CSS but only Px survives
4907            // the length branch, so every other unit is rejected. Pinned as-is.
4908            assert!(parse_style_line_height("1.5em").is_err());
4909            assert!(parse_style_line_height("12pt").is_err());
4910            assert!(parse_style_line_height("2rem").is_err());
4911        }
4912
4913        #[test]
4914        fn line_height_rejects_non_ascii_numerals_without_panicking() {
4915            // `char::is_numeric()` is true for U+FF15 FULLWIDTH DIGIT FIVE, so
4916            // parse_percentage_value sets split_pos = idx + 1 = 1 and then slices
4917            // `input[1..]` — a byte index inside a 3-byte char -> panic.
4918            // Any of these is a CSS-reachable crash:
4919            assert!(parse_style_line_height("\u{FF15}").is_err()); // fullwidth 5
4920            assert!(parse_style_line_height("\u{0665}").is_err()); // arabic-indic 5
4921            assert!(parse_style_line_height("1\u{00B2}").is_err()); // superscript 2
4922        }
4923
4924        // -- vertical-align -----------------------------------------------------
4925
4926        #[test]
4927        fn vertical_align_round_trips_keywords_percentages_and_lengths() {
4928            type VA = StyleVerticalAlign;
4929            for v in [
4930                VA::Baseline,
4931                VA::Top,
4932                VA::Middle,
4933                VA::Bottom,
4934                VA::Sub,
4935                VA::Superscript,
4936                VA::TextTop,
4937                VA::TextBottom,
4938                VA::Percentage(PercentageValue::new(50.0)),
4939                VA::Percentage(PercentageValue::new(-25.0)),
4940                VA::Length(PixelValue::px(12.0)),
4941                VA::Length(PixelValue::em(1.5)),
4942            ] {
4943                let printed = v.print_as_css_value();
4944                assert_eq!(
4945                    parse_style_vertical_align(&printed).unwrap(),
4946                    v,
4947                    "{printed:?}"
4948                );
4949            }
4950            assert!(parse_style_vertical_align("").is_err());
4951            assert!(parse_style_vertical_align("%").is_err());
4952            assert!(parse_style_vertical_align("bogus%").is_err());
4953            assert!(parse_style_vertical_align("\u{1F600}").is_err());
4954        }
4955
4956        // -- caret-* helpers ----------------------------------------------------
4957
4958        #[test]
4959        fn caret_parsers_reject_garbage_and_accept_minimal_input() {
4960            assert_eq!(
4961                parse_caret_color("red").unwrap().inner,
4962                parse_style_text_color("red").unwrap().inner
4963            );
4964            assert!(parse_caret_color("").is_err());
4965            assert!(parse_caret_color("not-a-color").is_err());
4966            assert_eq!(parse_caret_width("2px").unwrap().inner, PixelValue::px(2.0));
4967            assert!(parse_caret_width("").is_err());
4968            assert!(parse_caret_animation_duration("bogus").is_err());
4969            assert!(parse_caret_animation_duration("500ms").is_ok());
4970        }
4971
4972        // -- error type getters: to_contained / to_shared ------------------------
4973
4974        /// Owned<->shared conversion must be lossless for every error family here.
4975        macro_rules! assert_error_round_trip {
4976            ($parse:ident, $($bad:expr),+ $(,)?) => {{
4977                $(
4978                    let e = $parse($bad).expect_err(concat!(stringify!($parse), " accepted ", $bad));
4979                    assert_eq!(e.to_contained().to_shared(), e, "{:?} via {}", $bad, stringify!($parse));
4980                )+
4981            }};
4982        }
4983
4984        #[test]
4985        fn invalid_value_errors_round_trip_through_their_owned_form() {
4986            assert_error_round_trip!(parse_style_text_align, "", "middle", "\u{1F600}");
4987            assert_error_round_trip!(parse_style_white_space, "", "wrap");
4988            assert_error_round_trip!(parse_style_hyphens, "", "always");
4989            assert_error_round_trip!(parse_style_line_break, "", "tight");
4990            assert_error_round_trip!(parse_style_word_break, "", "break");
4991            assert_error_round_trip!(parse_style_overflow_wrap, "", "wrap");
4992            assert_error_round_trip!(parse_style_text_align_last, "", "middle");
4993            assert_error_round_trip!(parse_style_text_transform, "", "smallcaps");
4994            assert_error_round_trip!(parse_style_direction, "", "sideways");
4995            assert_error_round_trip!(parse_style_user_select, "", "some");
4996            assert_error_round_trip!(parse_style_text_decoration, "", "blink");
4997            assert_error_round_trip!(parse_style_vertical_align, "", "bogus");
4998            assert_error_round_trip!(parse_style_unicode_bidi, "", "override");
4999            assert_error_round_trip!(parse_style_text_box_trim, "", "trim");
5000            assert_error_round_trip!(parse_style_text_box_edge, "", "edge");
5001            assert_error_round_trip!(parse_style_dominant_baseline, "", "bogus");
5002            assert_error_round_trip!(parse_style_alignment_baseline, "", "bogus");
5003            assert_error_round_trip!(parse_style_baseline_source, "", "bogus");
5004            assert_error_round_trip!(parse_style_line_fit_edge, "", "bogus");
5005            assert_error_round_trip!(parse_style_initial_letter_align, "", "bogus");
5006            assert_error_round_trip!(parse_style_initial_letter_wrap, "", "bogus");
5007        }
5008
5009        #[test]
5010        fn pixel_and_numeric_errors_round_trip_through_their_owned_form() {
5011            // EmptyString / ValueParseErr / NoValueGiven / InvalidPixelValue variants.
5012            assert_error_round_trip!(parse_style_letter_spacing, "", "abcpx", "px", "zz");
5013            assert_error_round_trip!(parse_style_word_spacing, "", "abcem", "em", "zz");
5014            assert_error_round_trip!(parse_style_text_indent, "abcpx", "zz");
5015            assert_error_round_trip!(parse_style_tab_size, "", "abcpx", "zz");
5016            assert_error_round_trip!(parse_style_line_height, "", "abc", "1.5em");
5017            assert_error_round_trip!(parse_style_initial_letter, "", "x", "0", "3 x");
5018            assert_error_round_trip!(parse_style_line_clamp, "", "x", "0");
5019            assert_error_round_trip!(
5020                parse_style_hanging_punctuation,
5021                "bogus",
5022                "force-end allow-end"
5023            );
5024            assert_error_round_trip!(parse_style_text_combine_upright, "bogus", "digits 9");
5025        }
5026
5027        #[test]
5028        fn text_color_error_round_trips_and_preserves_its_message() {
5029            let e = parse_style_text_color("not-a-color").unwrap_err();
5030            let owned = e.to_contained();
5031            let round_tripped = owned.to_shared();
5032            assert_eq!(format!("{e}"), format!("{round_tripped}"));
5033            assert!(parse_style_text_color("").is_err());
5034            assert!(parse_style_text_color("#gggggg").is_err());
5035            assert!(parse_style_text_color("\u{1F600}").is_err());
5036            // Positive control.
5037            assert_eq!(
5038                parse_style_text_color("#aabbcc").unwrap().inner.to_hash(),
5039                "#aabbccff"
5040            );
5041        }
5042
5043        #[test]
5044        fn error_types_survive_a_default_ish_extreme_instance() {
5045            // to_contained/to_shared must not panic on empty or huge payloads.
5046            let long = "z".repeat(100_000);
5047            let e = parse_style_text_align(&long).unwrap_err();
5048            assert_eq!(e.to_contained().to_shared(), e);
5049            let e = parse_style_line_clamp(&long).unwrap_err();
5050            assert_eq!(e.to_contained().to_shared(), e);
5051            let e = parse_style_hanging_punctuation(&long).unwrap_err();
5052            assert_eq!(e.to_contained().to_shared(), e);
5053            // Empty payload.
5054            let e = parse_style_letter_spacing("").unwrap_err();
5055            assert_eq!(e.to_contained().to_shared(), e);
5056        }
5057
5058        // -- metric coverage ----------------------------------------------------
5059
5060        #[test]
5061        fn letter_spacing_accepts_every_size_metric_it_prints() {
5062            for (unit, metric) in [
5063                ("px", SizeMetric::Px),
5064                ("pt", SizeMetric::Pt),
5065                ("em", SizeMetric::Em),
5066                ("rem", SizeMetric::Rem),
5067                ("in", SizeMetric::In),
5068                ("cm", SizeMetric::Cm),
5069                ("mm", SizeMetric::Mm),
5070                ("%", SizeMetric::Percent),
5071                ("vw", SizeMetric::Vw),
5072                ("vh", SizeMetric::Vh),
5073                ("vmax", SizeMetric::Vmax),
5074                // FIXED: `vmin` used to be unreachable — the suffix table tried "in"
5075                // before "vmin", so "1vmin" was stripped to "1vm" and failed to parse,
5076                // making every parse_pixel_value-backed property (letter-spacing,
5077                // word-spacing, text-indent, tab-size, vertical-align) reject a valid
5078                // CSS unit. The table now puts "vmin" ahead of "in".
5079                ("vmin", SizeMetric::Vmin),
5080            ] {
5081                let parsed = parse_style_letter_spacing(&format!("1{unit}")).unwrap();
5082                assert_eq!(parsed.inner.metric, metric, "unit {unit}");
5083                assert_eq!(parsed.inner.number.get(), 1.0);
5084            }
5085        }
5086    }
5087}