Skip to main content

textual_rs/css/
parser.rs

1//! TCSS stylesheet parser built on cssparser.
2
3use cssparser::{
4    AtRuleParser, ParseError, Parser, ParserInput, QualifiedRuleParser, StyleSheetParser,
5};
6
7use crate::css::property::{parse_declaration_block, PropertyParseError};
8use crate::css::selector::{Selector, SelectorParseError, SelectorParser};
9use crate::css::types::Declaration;
10
11/// A parsed TCSS rule: a selector list + declaration block.
12#[derive(Debug, Clone)]
13pub struct Rule {
14    /// The list of selectors this rule applies to.
15    pub selectors: Vec<Selector>,
16    /// The property declarations to apply when the selectors match.
17    pub declarations: Vec<Declaration>,
18}
19
20/// Custom parse error for TCSS.
21#[derive(Debug, Clone)]
22pub enum TcssParseError {
23    /// The selector string could not be parsed.
24    InvalidSelector(String),
25    /// An unrecognized CSS property was encountered.
26    InvalidProperty(String),
27    /// A property value could not be parsed.
28    InvalidValue(String),
29}
30
31impl From<SelectorParseError> for TcssParseError {
32    fn from(e: SelectorParseError) -> Self {
33        TcssParseError::InvalidSelector(e.0)
34    }
35}
36
37impl From<PropertyParseError> for TcssParseError {
38    fn from(e: PropertyParseError) -> Self {
39        match e {
40            PropertyParseError::UnknownProperty(s) => TcssParseError::InvalidProperty(s),
41            PropertyParseError::InvalidValue(s) => TcssParseError::InvalidValue(s),
42        }
43    }
44}
45
46/// Rule parser for the TCSS stylesheet parser.
47pub struct TcssRuleParser;
48
49impl<'i> AtRuleParser<'i> for TcssRuleParser {
50    type Prelude = ();
51    type AtRule = Rule;
52    type Error = TcssParseError;
53}
54
55impl<'i> QualifiedRuleParser<'i> for TcssRuleParser {
56    type Prelude = Vec<Selector>;
57    type QualifiedRule = Rule;
58    type Error = TcssParseError;
59
60    fn parse_prelude<'t>(
61        &mut self,
62        input: &mut Parser<'i, 't>,
63    ) -> Result<Self::Prelude, ParseError<'i, Self::Error>> {
64        SelectorParser::parse_selector_list(input).map_err(|e| e.into::<TcssParseError>())
65    }
66
67    fn parse_block<'t>(
68        &mut self,
69        prelude: Self::Prelude,
70        _start: &cssparser::ParserState,
71        input: &mut Parser<'i, 't>,
72    ) -> Result<Self::QualifiedRule, ParseError<'i, Self::Error>> {
73        let declarations =
74            parse_declaration_block(input).map_err(|e| e.into::<TcssParseError>())?;
75        Ok(Rule {
76            selectors: prelude,
77            declarations,
78        })
79    }
80}
81
82/// Parse a TCSS stylesheet string into rules and error messages.
83///
84/// Returns `(rules, errors)` where errors contain line-number information.
85pub fn parse_stylesheet(css: &str) -> (Vec<Rule>, Vec<String>) {
86    let mut input = ParserInput::new(css);
87    let mut parser = Parser::new(&mut input);
88    let mut rule_parser = TcssRuleParser;
89
90    let mut rules = Vec::new();
91    let mut errors = Vec::new();
92
93    let sheet_parser = StyleSheetParser::new(&mut parser, &mut rule_parser);
94    for result in sheet_parser {
95        match result {
96            Ok(rule) => rules.push(rule),
97            Err((parse_error, slice)) => {
98                let loc = parse_error.location;
99                // loc.line is 0-indexed; add 1 for human-readable line numbers
100                let msg = format!(
101                    "CSS parse error at line {}, column {}: {:?} (near {:?})",
102                    loc.line + 1,
103                    loc.column,
104                    parse_error.kind,
105                    slice
106                );
107                errors.push(msg);
108            }
109        }
110    }
111
112    (rules, errors)
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118    use crate::css::selector::Selector;
119    use crate::css::types::{TcssColor, TcssValue};
120
121    #[test]
122    fn parse_stylesheet_single_rule() {
123        let (rules, errors) = parse_stylesheet("Button { color: red; }");
124        assert!(errors.is_empty(), "unexpected errors: {:?}", errors);
125        assert_eq!(rules.len(), 1);
126        assert_eq!(
127            rules[0].selectors,
128            vec![Selector::Type("Button".to_string())]
129        );
130        assert_eq!(rules[0].declarations.len(), 1);
131        assert_eq!(rules[0].declarations[0].property, "color");
132        assert!(matches!(
133            rules[0].declarations[0].value,
134            TcssValue::Color(TcssColor::Rgb(255, 0, 0))
135        ));
136    }
137
138    #[test]
139    fn parse_stylesheet_three_rules() {
140        let css = r#"
141            Button { color: red; }
142            .active { display: block; }
143            #sidebar { width: 20; }
144        "#;
145        let (rules, errors) = parse_stylesheet(css);
146        assert!(errors.is_empty(), "unexpected errors: {:?}", errors);
147        assert_eq!(rules.len(), 3);
148    }
149
150    #[test]
151    fn parse_stylesheet_syntax_error_collects_line_number() {
152        // Second rule has a syntax error (invalid selector with $)
153        let css = r#"Button { color: red; }
154$invalid { color: blue; }
155Label { display: flex; }"#;
156        let (rules, errors) = parse_stylesheet(css);
157        // Valid rules should still be parsed
158        assert!(!errors.is_empty(), "expected at least one error");
159        // Error should contain line info
160        assert!(
161            errors[0].contains("line 2") || errors[0].contains("2"),
162            "error should mention line number: {}",
163            errors[0]
164        );
165        // The valid rules before/after the error should still be parsed
166        assert!(
167            rules.len() >= 1,
168            "should have parsed at least one valid rule"
169        );
170    }
171
172    #[test]
173    fn parse_stylesheet_empty_returns_empty() {
174        let (rules, errors) = parse_stylesheet("");
175        assert!(rules.is_empty());
176        assert!(errors.is_empty());
177    }
178
179    #[test]
180    fn parse_stylesheet_multiple_declarations() {
181        let css = "Button { color: red; display: block; opacity: 0.5; }";
182        let (rules, errors) = parse_stylesheet(css);
183        assert!(errors.is_empty(), "unexpected errors: {:?}", errors);
184        assert_eq!(rules.len(), 1);
185        assert_eq!(rules[0].declarations.len(), 3);
186    }
187}