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