Skip to main content

hjkl_css/
parse.rs

1//! Convert a CSS-subset string to a [`Stylesheet`]. Backed by `cssparser`'s
2//! tokenizer + `StyleSheetParser`. Compound selectors with descendant (` `),
3//! child (`>`), adjacent-sibling (`+`), and general-sibling (`~`) combinators
4//! are supported.
5
6use cssparser::{
7    AtRuleParser, CowRcStr, DeclarationParser, ParseError as CssParseError, Parser, ParserState,
8    QualifiedRuleParser, RuleBodyItemParser, RuleBodyParser, StyleSheetParser, Token,
9    match_ignore_ascii_case,
10};
11
12use crate::ast::{
13    Combinator, Declaration, PseudoClass, Rule, Selector, SimpleSelector, Stylesheet,
14};
15use crate::error::{ParseError, ParseErrorOwned};
16use crate::value::{Color, Length, SideValue, Value};
17
18pub fn parse(input: &str) -> Result<Stylesheet, ParseError> {
19    let mut parser = Parser::new(input);
20    let mut rule_parser = StylesheetRuleParser;
21    let mut rules = Vec::new();
22    let iter = StyleSheetParser::new(&mut parser, &mut rule_parser);
23    for item in iter {
24        match item {
25            Ok(Some(rule)) => rules.push(rule),
26            // `None` here is an at-rule we chose to swallow (e.g. `@media`,
27            // `@charset`) — keep parsing, don't surface as an error.
28            Ok(None) => {}
29            // CSS spec: a single malformed rule must not invalidate the
30            // surrounding stylesheet. cssparser already skips the broken
31            // rule's tokens before yielding the next item, so we drop the
32            // error and keep collecting.
33            Err(_) => {}
34        }
35    }
36    Ok(Stylesheet { rules })
37}
38
39struct StylesheetRuleParser;
40
41impl<'i> QualifiedRuleParser<'i> for StylesheetRuleParser {
42    type Prelude = Vec<Selector>;
43    type QualifiedRule = Option<Rule>;
44    type Error = ParseErrorOwned;
45
46    fn parse_prelude(
47        &mut self,
48        parser: &mut Parser<'i>,
49    ) -> Result<Self::Prelude, CssParseError<Self::Error>> {
50        parse_selector_list(parser)
51    }
52
53    fn parse_block(
54        &mut self,
55        selectors: Self::Prelude,
56        _start: &ParserState,
57        parser: &mut Parser<'i>,
58    ) -> Result<Self::QualifiedRule, CssParseError<Self::Error>> {
59        let mut declarations = Vec::new();
60        let mut decl_parser = DeclParser;
61        let body = RuleBodyParser::new(parser, &mut decl_parser);
62        // CSS spec: a malformed declaration must not invalidate the
63        // surrounding rule. cssparser already advances past the broken
64        // declaration; we just swallow the error and keep collecting the
65        // rest.
66        for item in body {
67            match item {
68                Ok(decl) => declarations.push(decl),
69                Err(_) => continue,
70            }
71        }
72        Ok(Some(Rule {
73            selectors,
74            declarations,
75        }))
76    }
77}
78
79impl<'i> AtRuleParser<'i> for StylesheetRuleParser {
80    type Prelude = ();
81    type AtRule = Option<Rule>;
82    type Error = ParseErrorOwned;
83
84    // Consume an at-rule prelude (everything up to `;` or `{`) and discard
85    // it. Returning Ok signals "recognized"; the matching parse_block
86    // (for block at-rules) or rule-list parser (for statement at-rules)
87    // will skip the body. v1 doesn't implement any at-rule semantics, but
88    // we swallow them so a real-world stylesheet with `@charset` /
89    // `@media` doesn't blow up the whole parse.
90    fn parse_prelude(
91        &mut self,
92        _name: CowRcStr<'i>,
93        parser: &mut Parser<'i>,
94    ) -> Result<Self::Prelude, CssParseError<Self::Error>> {
95        while parser.next().is_ok() {}
96        Ok(())
97    }
98
99    fn rule_without_block(
100        &mut self,
101        _prelude: Self::Prelude,
102        _start: &ParserState,
103    ) -> Result<Self::AtRule, ()> {
104        Ok(None)
105    }
106
107    fn parse_block(
108        &mut self,
109        _prelude: Self::Prelude,
110        _start: &ParserState,
111        parser: &mut Parser<'i>,
112    ) -> Result<Self::AtRule, CssParseError<Self::Error>> {
113        // Drain the block — its contents (nested rules or declarations)
114        // are intentionally discarded in v1.
115        while parser.next().is_ok() {}
116        Ok(None)
117    }
118}
119
120fn parse_selector_list<'i>(
121    parser: &mut Parser<'i>,
122) -> Result<Vec<Selector>, CssParseError<ParseErrorOwned>> {
123    let mut selectors = Vec::new();
124    loop {
125        selectors.push(parse_compound_selector(parser)?);
126        if parser.expect_comma().is_err() {
127            break;
128        }
129    }
130    Ok(selectors)
131}
132
133/// Parse one [`SimpleSelector`] from the token stream. Consumes only
134/// tokens that belong to the simple selector (element, classes, pseudo).
135/// Stops — without consuming — at whitespace or any token that cannot
136/// continue the current simple selector.
137///
138/// `allow_type` controls whether a leading `Ident` token is accepted as a
139/// type selector. The very first part of a compound selector allows a type
140/// selector; subsequent parts (after an explicit combinator) also allow one
141/// since the combinator was already consumed. The only case that disallows
142/// a type selector is a non-first part reached via the whitespace/descendant
143/// path, where the Ident has already been put back by the caller.
144fn parse_simple_selector<'i>(
145    parser: &mut Parser<'i>,
146    allow_type: bool,
147) -> Result<SimpleSelector, CssParseError<ParseErrorOwned>> {
148    let mut sel = SimpleSelector::default();
149    let mut saw_anything = false;
150    loop {
151        let save = parser.state();
152        let next = parser.next_including_whitespace().cloned();
153        match next {
154            Ok(Token::Ident(name)) if allow_type && !saw_anything => {
155                sel.element = Some(name.to_string());
156                saw_anything = true;
157            }
158            Ok(Token::Delim('.')) => {
159                let name = parser.expect_ident()?.to_string();
160                sel.classes.push(name);
161                saw_anything = true;
162            }
163            Ok(Token::Colon) => {
164                let ident = parser.expect_ident_cloned()?;
165                let Some(pseudo) = PseudoClass::from_ident(&ident) else {
166                    return Err(CssParseError::custom(ParseErrorOwned(format!(
167                        "unknown pseudo-class :{ident}"
168                    ))));
169                };
170                if sel.pseudo.is_some() {
171                    return Err(CssParseError::custom(ParseErrorOwned(
172                        "multiple pseudo-classes per selector are not supported".to_string(),
173                    )));
174                }
175                sel.pseudo = Some(pseudo);
176                saw_anything = true;
177            }
178            _ => {
179                parser.reset(&save);
180                break;
181            }
182        }
183    }
184    if !saw_anything {
185        return Err(CssParseError::custom(ParseErrorOwned(
186            "empty selector".to_string(),
187        )));
188    }
189    Ok(sel)
190}
191
192/// Parse a compound selector: one or more [`SimpleSelector`]s joined by
193/// [`Combinator`]s. Handles ` ` (descendant), `>` (child), `+`
194/// (adjacent sibling), `~` (general sibling).
195fn parse_compound_selector<'i>(
196    parser: &mut Parser<'i>,
197) -> Result<Selector, CssParseError<ParseErrorOwned>> {
198    // Skip leading whitespace before the selector starts — cssparser keeps
199    // the whitespace token between e.g. `,` and the next selector.
200    parser.skip_whitespace();
201    let first = parse_simple_selector(parser, true)?;
202    let mut parts = vec![first];
203    let mut combinators = vec![];
204
205    loop {
206        // Peek at what follows: whitespace, explicit combinator token, or
207        // something that cannot continue a selector.
208        let save = parser.state();
209        let tok = parser.next_including_whitespace().cloned();
210
211        match tok {
212            // Explicit combinators: `>`, `+`, `~` (possibly with surrounding
213            // whitespace already consumed in the whitespace arm below).
214            Ok(Token::Delim('>')) => {
215                parser.skip_whitespace();
216                let next_simple = parse_simple_selector(parser, true)?;
217                parts.push(next_simple);
218                combinators.push(Combinator::Child);
219            }
220            Ok(Token::Delim('+')) => {
221                parser.skip_whitespace();
222                let next_simple = parse_simple_selector(parser, true)?;
223                parts.push(next_simple);
224                combinators.push(Combinator::AdjacentSibling);
225            }
226            Ok(Token::Delim('~')) => {
227                parser.skip_whitespace();
228                let next_simple = parse_simple_selector(parser, true)?;
229                parts.push(next_simple);
230                combinators.push(Combinator::GeneralSibling);
231            }
232            Ok(Token::WhiteSpace(_)) => {
233                // Could be a descendant combinator or just trailing
234                // whitespace before `{`. Peek further.
235                //
236                // Eat any *additional* whitespace tokens. cssparser emits
237                // one Token::WhiteSpace per contiguous whitespace run, but
238                // a CSS comment between two runs (e.g. `.a /* x */ .b`)
239                // separates them into two tokens. Without this skip the
240                // second whitespace falls into the `_` arm of the inner
241                // match, the loop breaks, and the trailing `.b` is left
242                // unconsumed — cssparser then drops the whole rule.
243                parser.skip_whitespace();
244                let after_ws = parser.state();
245                let next_tok = parser.next_including_whitespace().cloned();
246                match next_tok {
247                    // Explicit combinator after whitespace: ` > .b`, ` + .b`, ` ~ .b`.
248                    Ok(Token::Delim('>')) => {
249                        parser.skip_whitespace();
250                        let next_simple = parse_simple_selector(parser, true)?;
251                        parts.push(next_simple);
252                        combinators.push(Combinator::Child);
253                    }
254                    Ok(Token::Delim('+')) => {
255                        parser.skip_whitespace();
256                        let next_simple = parse_simple_selector(parser, true)?;
257                        parts.push(next_simple);
258                        combinators.push(Combinator::AdjacentSibling);
259                    }
260                    Ok(Token::Delim('~')) => {
261                        parser.skip_whitespace();
262                        let next_simple = parse_simple_selector(parser, true)?;
263                        parts.push(next_simple);
264                        combinators.push(Combinator::GeneralSibling);
265                    }
266                    // Tokens that can start a simple selector → descendant combinator.
267                    // Put the token back and re-parse — the Ident case needs
268                    // allow_type=true so `label span` works.
269                    Ok(Token::Ident(_)) | Ok(Token::Delim('.')) | Ok(Token::Colon) => {
270                        parser.reset(&after_ws);
271                        let next_simple = parse_simple_selector(parser, true)?;
272                        parts.push(next_simple);
273                        combinators.push(Combinator::Descendant);
274                    }
275                    _ => {
276                        // Trailing whitespace before `{` or end — not a
277                        // combinator. Back up past the whitespace too.
278                        parser.reset(&save);
279                        break;
280                    }
281                }
282            }
283            _ => {
284                parser.reset(&save);
285                break;
286            }
287        }
288    }
289
290    Ok(Selector { parts, combinators })
291}
292
293struct DeclParser;
294
295impl<'i> DeclarationParser<'i> for DeclParser {
296    type Declaration = Declaration;
297    type Error = ParseErrorOwned;
298
299    fn parse_value(
300        &mut self,
301        name: CowRcStr<'i>,
302        parser: &mut Parser<'i>,
303        _start: &ParserState,
304    ) -> Result<Self::Declaration, CssParseError<Self::Error>> {
305        let prop = name.to_string();
306        let (value, important) = parse_value(&prop, parser)?;
307        Ok(Declaration {
308            property: prop,
309            value,
310            important,
311        })
312    }
313}
314
315impl<'i> AtRuleParser<'i> for DeclParser {
316    type Prelude = ();
317    type AtRule = Declaration;
318    type Error = ParseErrorOwned;
319}
320
321impl<'i> QualifiedRuleParser<'i> for DeclParser {
322    type Prelude = ();
323    type QualifiedRule = Declaration;
324    type Error = ParseErrorOwned;
325}
326
327impl<'i> RuleBodyItemParser<'i, Declaration, ParseErrorOwned> for DeclParser {
328    fn parse_declarations(&self) -> bool {
329        true
330    }
331    fn parse_qualified(&self) -> bool {
332        false
333    }
334}
335
336fn parse_value<'i>(
337    prop: &str,
338    parser: &mut Parser<'i>,
339) -> Result<(Value, bool), CssParseError<ParseErrorOwned>> {
340    let value = parse_value_inner(prop, parser)?;
341    let important = consume_important(parser);
342    parser.expect_exhausted()?;
343    Ok((value, important))
344}
345
346fn parse_value_inner<'i>(
347    prop: &str,
348    parser: &mut Parser<'i>,
349) -> Result<Value, CssParseError<ParseErrorOwned>> {
350    // Per-property value type: each recognised property accepts only the
351    // value shape it actually means. This stops `color: nonsense` from
352    // silently parsing as a keyword and reaching the adapter.
353    match property_kind(prop) {
354        PropertyKind::Color => parse_color(parser).map(Value::Color),
355        PropertyKind::Length => parse_length(parser).map(Value::Length),
356        PropertyKind::LengthOrAuto => {
357            if parser.try_parse(expect_auto).is_ok() {
358                Ok(Value::Auto)
359            } else {
360                parse_length(parser).map(Value::Length)
361            }
362        }
363        PropertyKind::SideLengths => {
364            let mut lengths = Vec::new();
365            while let Ok(len) = parser.try_parse(parse_length) {
366                lengths.push(len);
367                if lengths.len() == 4 {
368                    break;
369                }
370            }
371            if lengths.is_empty() {
372                return Err(CssParseError::custom(ParseErrorOwned(format!(
373                    "expected length for `{prop}`"
374                ))));
375            }
376            Ok(Value::LengthSet(lengths))
377        }
378        PropertyKind::SideLengthsOrAuto => parse_side_lengths_or_auto(prop, parser),
379        PropertyKind::Keyword(allowed) => {
380            let ident = parser.try_parse(|p| p.expect_ident_cloned()).map_err(|_| {
381                CssParseError::custom(ParseErrorOwned(format!("expected keyword for `{prop}`")))
382            })?;
383            let kw = ident.to_ascii_lowercase();
384            if allowed.contains(&kw.as_str()) {
385                Ok(Value::Keyword(kw))
386            } else {
387                Err(CssParseError::custom(ParseErrorOwned(format!(
388                    "unknown keyword `{kw}` for `{prop}`"
389                ))))
390            }
391        }
392        PropertyKind::Number => {
393            let n = parser.try_parse(|p| p.expect_number()).map_err(|_| {
394                CssParseError::custom(ParseErrorOwned(format!("expected number for `{prop}`")))
395            })?;
396            // `flex-grow` / `flex-shrink` are spec-required to be >= 0;
397            // every property using `PropertyKind::Number` today inherits
398            // that constraint. If a future property needs signed numbers,
399            // split into a separate `SignedNumber` kind.
400            if n < 0.0 {
401                return Err(CssParseError::custom(ParseErrorOwned(format!(
402                    "negative number not allowed for `{prop}`"
403                ))));
404            }
405            Ok(Value::Number(f64::from(n)))
406        }
407        PropertyKind::NumberOrLength => {
408            // Try unitless number first (line-height: 1.5), then length.
409            // A dimension token like `24px` is NOT a plain Number in
410            // cssparser, so `expect_number` won't consume it — try in order.
411            if let Ok(n) = parser.try_parse(|p| {
412                let tok = p.next()?.clone();
413                match tok {
414                    // Accept only a pure Number token (no unit).
415                    Token::Number { value, .. } => Ok(value),
416                    other => Err(CssParseError::<ParseErrorOwned>::custom(ParseErrorOwned(
417                        format!("not a plain number: {other:?}"),
418                    ))),
419                }
420            }) {
421                Ok(Value::Number(f64::from(n)))
422            } else {
423                parse_length(parser).map(Value::Length)
424            }
425        }
426        PropertyKind::FontWeight => {
427            if let Ok(n) = parser.try_parse(|p| p.expect_number()) {
428                let n = f64::from(n);
429                // CSS spec: font-weight numeric values must be integers in 1..=1000.
430                if n.fract() != 0.0 || !(1.0..=1000.0).contains(&n) {
431                    return Err(CssParseError::custom(ParseErrorOwned(format!(
432                        "font-weight numeric value `{n}` is out of range (must be integer 1–1000)"
433                    ))));
434                }
435                Ok(Value::Number(n))
436            } else {
437                let ident = parser.try_parse(|p| p.expect_ident_cloned()).map_err(|_| {
438                    CssParseError::custom(ParseErrorOwned(
439                        "expected number or keyword for `font-weight`".to_string(),
440                    ))
441                })?;
442                let kw = ident.to_ascii_lowercase();
443                if ["normal", "bold"].contains(&kw.as_str()) {
444                    Ok(Value::Keyword(kw))
445                } else {
446                    Err(CssParseError::custom(ParseErrorOwned(format!(
447                        "unknown keyword `{kw}` for `font-weight`"
448                    ))))
449                }
450            }
451        }
452        PropertyKind::FontFamily => parse_font_family(parser),
453        PropertyKind::Border => parse_border_shorthand(prop, parser),
454        PropertyKind::Unknown => {
455            // Forward-compat: unknown properties (anything we'll grow into
456            // later) take whichever shape the value tokens fit. Try color,
457            // then length, then bare keyword.
458            if let Ok(c) = parser.try_parse(parse_color) {
459                return Ok(Value::Color(c));
460            }
461            if let Ok(len) = parser.try_parse(parse_length) {
462                return Ok(Value::Length(len));
463            }
464            if let Ok(ident) = parser.try_parse(|p| p.expect_ident_cloned()) {
465                // Match the lowercasing the strict keyword arm applies, so
466                // unknown-property keyword values cascade case-insensitively
467                // with each other.
468                return Ok(Value::Keyword(ident.to_ascii_lowercase()));
469            }
470            Err(CssParseError::custom(ParseErrorOwned(format!(
471                "could not parse value for `{prop}`"
472            ))))
473        }
474    }
475}
476
477// -- side-lengths-or-auto ----------------------------------------------------
478
479fn parse_side_lengths_or_auto<'i>(
480    prop: &str,
481    parser: &mut Parser<'i>,
482) -> Result<Value, CssParseError<ParseErrorOwned>> {
483    let mut sides: Vec<SideValue> = Vec::new();
484    loop {
485        if sides.len() == 4 {
486            break;
487        }
488        if let Ok(()) = parser.try_parse(expect_auto) {
489            sides.push(SideValue::Auto);
490        } else if let Ok(len) = parser.try_parse(parse_length) {
491            sides.push(SideValue::Length(len));
492        } else {
493            break;
494        }
495    }
496    if sides.is_empty() {
497        return Err(CssParseError::custom(ParseErrorOwned(format!(
498            "expected length or auto for `{prop}`"
499        ))));
500    }
501    // Single `auto` token → Value::Auto (matches `width: auto` semantics).
502    if sides == [SideValue::Auto] {
503        return Ok(Value::Auto);
504    }
505    // If every side is a plain length, downcast to LengthSet so consumers
506    // that only handle LengthSet still work.
507    let all_lengths: Option<Vec<Length>> = sides
508        .iter()
509        .map(|sv| match sv {
510            SideValue::Length(l) => Some(*l),
511            SideValue::Auto => None,
512        })
513        .collect();
514    if let Some(lengths) = all_lengths {
515        return Ok(Value::LengthSet(lengths));
516    }
517    Ok(Value::SideSet(sides))
518}
519
520// -- font-family -------------------------------------------------------------
521
522fn parse_font_family<'i>(parser: &mut Parser<'i>) -> Result<Value, CssParseError<ParseErrorOwned>> {
523    let mut families: Vec<String> = Vec::new();
524    let mut pending_comma = false;
525    loop {
526        // Accept a quoted string or one or more unquoted idents.
527        let pushed = if let Ok(s) = parser.try_parse(|p| p.expect_string_cloned()) {
528            families.push(s.to_string());
529            true
530        } else if let Ok(ident) = parser.try_parse(|p| p.expect_ident_cloned()) {
531            // Concatenate adjacent idents for multi-word names like `sans serif`.
532            // CSS spec: unquoted family name = sequence of idents.
533            let mut name = ident.to_string();
534            loop {
535                // peek: if next non-whitespace token is an ident (and no comma
536                // or EOF), keep appending.
537                let state = parser.state();
538                match parser.next_including_whitespace() {
539                    Ok(Token::WhiteSpace(_)) => {
540                        let state2 = parser.state();
541                        match parser.next_including_whitespace() {
542                            Ok(Token::Ident(next_ident)) => {
543                                name.push(' ');
544                                name.push_str(next_ident.as_ref());
545                            }
546                            _ => {
547                                parser.reset(&state2);
548                                break;
549                            }
550                        }
551                    }
552                    _ => {
553                        parser.reset(&state);
554                        break;
555                    }
556                }
557            }
558            families.push(name);
559            true
560        } else {
561            false
562        };
563        if !pushed {
564            if pending_comma {
565                // `font-family: "Hack",` — comma not followed by another
566                // family name. Reject so the malformed declaration is
567                // dropped.
568                return Err(CssParseError::custom(ParseErrorOwned(
569                    "trailing comma in font-family".into(),
570                )));
571            }
572            break;
573        }
574        if parser.try_parse(|p| p.expect_comma()).is_err() {
575            break;
576        }
577        pending_comma = true;
578    }
579    if families.is_empty() {
580        return Err(CssParseError::custom(ParseErrorOwned(
581            "expected font-family value".into(),
582        )));
583    }
584    Ok(Value::FontFamilyList(families))
585}
586
587// -- border shorthand --------------------------------------------------------
588
589fn parse_border_shorthand<'i>(
590    prop: &str,
591    parser: &mut Parser<'i>,
592) -> Result<Value, CssParseError<ParseErrorOwned>> {
593    // Accept `<length> [solid|none] <color>` in any order.
594    // `style` token if present must be `solid` or `none`; others reject.
595    // All three are required (width + color mandatory; style optional).
596    let mut width: Option<Length> = None;
597    let mut color: Option<Color> = None;
598    let mut saw_none_style = false;
599
600    // Try each token up to 3 times (at most: length, style, color).
601    for _ in 0..3 {
602        if width.is_none()
603            && let Ok(len) = parser.try_parse(parse_length)
604        {
605            width = Some(len);
606            continue;
607        }
608        if color.is_none()
609            && let Ok(c) = parser.try_parse(parse_color)
610        {
611            color = Some(c);
612            continue;
613        }
614        // style keyword: solid (accepted, ignored) or none (zero width).
615        if let Ok(ident) = parser.try_parse(|p| p.expect_ident_cloned()) {
616            let kw = ident.to_ascii_lowercase();
617            match kw.as_str() {
618                // `none` is special: it zeros the width when no width was
619                // given and makes the color optional.
620                "none" => {
621                    saw_none_style = true;
622                    continue;
623                }
624                // Every other style keyword (`solid`, `dashed`, `dotted`,
625                // `double`, `groove`, `ridge`, `inset`, `outset`, …) is
626                // accepted and ignored — floem has no border-style model,
627                // so we don't promote the choice into the AST. Erroring
628                // would drop the whole declaration including the width
629                // and color the user actually cares about.
630                _ => continue,
631            }
632        }
633        break;
634    }
635
636    let width = if saw_none_style {
637        // `none` style: width is optional. If the source gave an explicit
638        // width (e.g. `border: 1px none red`) keep it — the user is
639        // describing a transition-style hidden border. If they omitted the
640        // width (`border: none red` / `border: none`) treat the border as
641        // zero-thickness.
642        width.unwrap_or(Length::Px(0.0))
643    } else {
644        width.ok_or_else(|| {
645            CssParseError::custom(ParseErrorOwned(format!("missing width in `{prop}`")))
646        })?
647    };
648    // `border: none` (no color) is the most common CSS reset. Allow it
649    // when the user wrote `none`: fall back to transparent so the border
650    // is structurally present in the AST but visually invisible.
651    let color = match color {
652        Some(c) => c,
653        None if saw_none_style => Color::rgba(0, 0, 0, 0),
654        None => {
655            return Err(CssParseError::custom(ParseErrorOwned(format!(
656                "missing color in `{prop}`"
657            ))));
658        }
659    };
660
661    Ok(Value::Border { width, color })
662}
663
664// -- helpers -----------------------------------------------------------------
665
666fn expect_auto<'i>(parser: &mut Parser<'i>) -> Result<(), CssParseError<ParseErrorOwned>> {
667    let tok = parser.next()?.clone();
668    match &tok {
669        Token::Ident(name) if name.eq_ignore_ascii_case("auto") => Ok(()),
670        other => Err(CssParseError::custom(ParseErrorOwned(format!(
671            "expected `auto`, got {other:?}"
672        )))),
673    }
674}
675
676// -- property kind -----------------------------------------------------------
677
678#[derive(Debug, Clone)]
679enum PropertyKind {
680    Color,
681    Length,
682    /// `width`, `height`, `flex-basis`: length OR `auto`.
683    LengthOrAuto,
684    /// `padding`, `border-radius`: 1..=4 lengths only.
685    SideLengths,
686    /// `margin`: 1..=4 sides, each length or auto.
687    SideLengthsOrAuto,
688    /// Fixed set of keyword values.
689    Keyword(&'static [&'static str]),
690    /// Unitless number only.
691    Number,
692    /// Unitless number (yields `Value::Number`) OR length (yields `Value::Length`).
693    NumberOrLength,
694    FontFamily,
695    /// `font-weight`: integer in 1..=1000 OR keyword `normal`/`bold`.
696    FontWeight,
697    Border,
698    /// Forward-compat shape for properties not yet first-class.
699    Unknown,
700}
701
702fn property_kind(name: &str) -> PropertyKind {
703    match name {
704        "color" | "background-color" => PropertyKind::Color,
705
706        // Sizing
707        "width" | "height" | "flex-basis" => PropertyKind::LengthOrAuto,
708
709        // Box spacing
710        "padding" | "border-radius" => PropertyKind::SideLengths,
711        "margin" => PropertyKind::SideLengthsOrAuto,
712        "gap" | "row-gap" | "column-gap" => PropertyKind::Length,
713
714        // Layout
715        "display" => PropertyKind::Keyword(&["flex", "block", "none"]),
716        "flex-direction" => {
717            PropertyKind::Keyword(&["row", "column", "row-reverse", "column-reverse"])
718        }
719        "align-items" => PropertyKind::Keyword(&["start", "end", "center", "stretch", "baseline"]),
720        "justify-content" => PropertyKind::Keyword(&[
721            "start",
722            "end",
723            "center",
724            "space-between",
725            "space-around",
726            "space-evenly",
727        ]),
728        "flex-grow" | "flex-shrink" => PropertyKind::Number,
729
730        // Border shorthands
731        "border" | "border-top" | "border-right" | "border-bottom" | "border-left" | "outline" => {
732            PropertyKind::Border
733        }
734
735        // CSS spec: `border-width` is a 1..=4 length shorthand
736        // (top/right/bottom/left), same expansion rules as padding.
737        "border-width" => PropertyKind::SideLengths,
738        "border-color" => PropertyKind::Color,
739        "border-top-color" | "border-right-color" | "border-bottom-color" | "border-left-color" => {
740            PropertyKind::Color
741        }
742
743        // Typography
744        "font-family" => PropertyKind::FontFamily,
745        "font-size" => PropertyKind::Length,
746        "font-weight" => PropertyKind::FontWeight,
747        "font-style" => PropertyKind::Keyword(&["normal", "italic", "oblique"]),
748        "text-align" => PropertyKind::Keyword(&["left", "center", "right", "justify"]),
749        "line-height" => PropertyKind::NumberOrLength,
750
751        _ => PropertyKind::Unknown,
752    }
753}
754
755/// Consume a trailing `!important` if present. The flag is surfaced on
756/// the resulting [`Declaration`] and honoured by the cascade in
757/// [`crate::Stylesheet::resolve`] — important declarations beat any
758/// non-important declaration regardless of specificity, with source
759/// order breaking ties within either tier.
760fn consume_important(parser: &mut Parser<'_>) -> bool {
761    parser
762        .try_parse(|p| -> Result<(), CssParseError<ParseErrorOwned>> {
763            p.expect_delim('!')?;
764            let ident = p.expect_ident_cloned()?;
765            if ident.eq_ignore_ascii_case("important") {
766                Ok(())
767            } else {
768                Err(CssParseError::custom(ParseErrorOwned(
769                    "not !important".to_string(),
770                )))
771            }
772        })
773        .is_ok()
774}
775
776fn parse_length<'i>(parser: &mut Parser<'i>) -> Result<Length, CssParseError<ParseErrorOwned>> {
777    let token = parser.next()?.clone();
778    match token {
779        Token::Dimension { value, unit, .. } => match unit.as_ref() {
780            "px" => Ok(Length::Px(f64::from(value))),
781            other => Err(CssParseError::custom(ParseErrorOwned(format!(
782                // `em` / `rem` deferred to a later phase.
783                "unsupported length unit `{other}`"
784            )))),
785        },
786        Token::Percentage { unit_value, .. } => Ok(Length::Percent(f64::from(unit_value) * 100.0)),
787        Token::Number { value, .. } => Ok(Length::Px(f64::from(value))),
788        other => Err(CssParseError::custom(ParseErrorOwned(format!(
789            "expected length, got {other:?}"
790        )))),
791    }
792}
793
794fn parse_color<'i>(parser: &mut Parser<'i>) -> Result<Color, CssParseError<ParseErrorOwned>> {
795    let token = parser.next()?.clone();
796    match token {
797        // cssparser emits `Hash` when the value starts with a digit
798        // (e.g. `#1a2b3c`) and `IDHash` otherwise — both are valid CSS
799        // colour syntax, so collapse them into a single arm.
800        Token::IDHash(h) | Token::Hash(h) => parse_hex(h.as_ref())
801            .ok_or_else(|| CssParseError::custom(ParseErrorOwned(format!("bad hex color `#{h}`")))),
802        Token::Ident(name) => named_color(name.as_ref()).ok_or_else(|| {
803            CssParseError::custom(ParseErrorOwned(format!("unknown color name `{name}`")))
804        }),
805        Token::Function(name) => {
806            let name_lc = name.to_ascii_lowercase();
807            parser.parse_nested_block(|p| match name_lc.as_str() {
808                "rgb" => parse_rgb_args(p, false),
809                "rgba" => parse_rgb_args(p, true),
810                other => Err(CssParseError::custom(ParseErrorOwned(format!(
811                    "unsupported color function `{other}`"
812                )))),
813            })
814        }
815        other => Err(CssParseError::custom(ParseErrorOwned(format!(
816            "expected color, got {other:?}"
817        )))),
818    }
819}
820
821fn parse_rgb_args<'i>(
822    parser: &mut Parser<'i>,
823    expect_alpha: bool,
824) -> Result<Color, CssParseError<ParseErrorOwned>> {
825    let r = parse_u8_channel(parser)?;
826    parser.expect_comma()?;
827    let g = parse_u8_channel(parser)?;
828    parser.expect_comma()?;
829    let b = parse_u8_channel(parser)?;
830    let a = if expect_alpha {
831        parser.expect_comma()?;
832        let f = parser.expect_number()?;
833        (f.clamp(0.0, 1.0) * 255.0).round() as u8
834    } else {
835        0xff
836    };
837    parser.expect_exhausted()?;
838    Ok(Color::rgba(r, g, b, a))
839}
840
841fn parse_u8_channel<'i>(parser: &mut Parser<'i>) -> Result<u8, CssParseError<ParseErrorOwned>> {
842    let token = parser.next()?.clone();
843    let n = match token {
844        Token::Number { value, .. } => value,
845        Token::Percentage { unit_value, .. } => unit_value * 255.0,
846        other => {
847            return Err(CssParseError::custom(ParseErrorOwned(format!(
848                "expected channel, got {other:?}"
849            ))));
850        }
851    };
852    Ok(n.clamp(0.0, 255.0).round() as u8)
853}
854
855fn parse_hex(s: &str) -> Option<Color> {
856    let hex = |c: char| c.to_digit(16).map(|d| d as u8);
857    let chars: Vec<u8> = s.chars().filter_map(hex).collect();
858    if chars.len() != s.chars().count() {
859        return None;
860    }
861    let dup = |n: u8| (n << 4) | n;
862    Some(match chars.len() {
863        3 => Color::rgb(dup(chars[0]), dup(chars[1]), dup(chars[2])),
864        4 => Color::rgba(dup(chars[0]), dup(chars[1]), dup(chars[2]), dup(chars[3])),
865        6 => Color::rgb(
866            (chars[0] << 4) | chars[1],
867            (chars[2] << 4) | chars[3],
868            (chars[4] << 4) | chars[5],
869        ),
870        8 => Color::rgba(
871            (chars[0] << 4) | chars[1],
872            (chars[2] << 4) | chars[3],
873            (chars[4] << 4) | chars[5],
874            (chars[6] << 4) | chars[7],
875        ),
876        _ => return None,
877    })
878}
879
880fn named_color(name: &str) -> Option<Color> {
881    // CSS Color Module 4 canonical hex values.
882    Some(match_ignore_ascii_case! { name,
883        "transparent" => Color::rgba(0, 0, 0, 0),
884        // CSS Level 1 (16 colors)
885        "black"   => Color::rgb(0x00, 0x00, 0x00),
886        "silver"  => Color::rgb(0xc0, 0xc0, 0xc0),
887        "gray"    => Color::rgb(0x80, 0x80, 0x80),
888        "grey"    => Color::rgb(0x80, 0x80, 0x80),
889        "white"   => Color::rgb(0xff, 0xff, 0xff),
890        "maroon"  => Color::rgb(0x80, 0x00, 0x00),
891        "red"     => Color::rgb(0xff, 0x00, 0x00),
892        "purple"  => Color::rgb(0x80, 0x00, 0x80),
893        "fuchsia" => Color::rgb(0xff, 0x00, 0xff),
894        "green"   => Color::rgb(0x00, 0x80, 0x00),
895        "lime"    => Color::rgb(0x00, 0xff, 0x00),
896        "olive"   => Color::rgb(0x80, 0x80, 0x00),
897        "yellow"  => Color::rgb(0xff, 0xff, 0x00),
898        "navy"    => Color::rgb(0x00, 0x00, 0x80),
899        "blue"    => Color::rgb(0x00, 0x00, 0xff),
900        "teal"    => Color::rgb(0x00, 0x80, 0x80),
901        "aqua"    => Color::rgb(0x00, 0xff, 0xff),
902        // Common aliases / extras
903        "cyan"    => Color::rgb(0x00, 0xff, 0xff),
904        "magenta" => Color::rgb(0xff, 0x00, 0xff),
905        "orange"  => Color::rgb(0xff, 0xa5, 0x00),
906        "brown"   => Color::rgb(0xa5, 0x2a, 0x2a),
907        "pink"    => Color::rgb(0xff, 0xc0, 0xcb),
908        _ => return None,
909    })
910}