tailwind_rs_postcss/
parser.rs

1//! CSS Parser implementation
2//!
3//! This module provides CSS parsing functionality with support for
4//! modern CSS features and PostCSS compatibility.
5
6use crate::ast::{CSSAtRule, CSSDeclaration, CSSNode, CSSRule, SourcePosition};
7use crate::error::{PostCSSError, Result};
8use serde::{Deserialize, Serialize};
9// use std::collections::HashMap;
10
11/// CSS parser with configurable options
12#[derive(Debug, Clone)]
13pub struct CSSParser {
14    options: ParseOptions,
15}
16
17/// Parser configuration options
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct ParseOptions {
20    /// Enable source position tracking
21    pub track_positions: bool,
22    /// Enable strict parsing
23    pub strict_mode: bool,
24    /// Enable CSS custom properties support
25    pub custom_properties: bool,
26    /// Enable CSS nesting support
27    pub nesting: bool,
28    /// Enable CSS container queries support
29    pub container_queries: bool,
30    /// Enable CSS cascade layers support
31    pub cascade_layers: bool,
32    /// Maximum nesting depth
33    pub max_nesting_depth: usize,
34    /// Custom parser plugins
35    pub plugins: Vec<String>,
36}
37
38impl Default for ParseOptions {
39    fn default() -> Self {
40        Self {
41            track_positions: true,
42            strict_mode: false,
43            custom_properties: true,
44            nesting: true,
45            container_queries: true,
46            cascade_layers: true,
47            max_nesting_depth: 10,
48            plugins: Vec::new(),
49        }
50    }
51}
52
53impl CSSParser {
54    /// Create a new CSS parser with options
55    pub fn new(options: ParseOptions) -> Self {
56        Self { options }
57    }
58
59    /// Parse CSS input into AST
60    pub fn parse(&self, input: &str) -> Result<CSSNode> {
61        let mut parser_state = ParserState::new(input, &self.options);
62        self.parse_stylesheet(&mut parser_state)
63    }
64
65    /// Parse a stylesheet (root level)
66    fn parse_stylesheet(&self, state: &mut ParserState) -> Result<CSSNode> {
67        let mut rules = Vec::new();
68
69        while !state.is_eof() {
70            state.skip_whitespace();
71
72            if state.is_eof() {
73                break;
74            }
75
76            // Handle at-rules
77            if state.peek() == Some('@') {
78                if let Ok(at_rule) = self.parse_at_rule(state) {
79                    rules.push(CSSRule {
80                        selector: format!("@{}", at_rule.name),
81                        declarations: Vec::new(),
82                        nested_rules: vec![CSSRule {
83                            selector: at_rule.params.clone(),
84                            declarations: Vec::new(),
85                            nested_rules: Vec::new(),
86                            media_query: None,
87                            specificity: 0,
88                            position: at_rule.position.clone(),
89                        }],
90                        media_query: None,
91                        specificity: 0,
92                        position: at_rule.position.clone(),
93                    });
94                }
95            } else {
96                // Parse regular rule
97                if let Ok(rule) = self.parse_rule(state) {
98                    rules.push(rule);
99                }
100            }
101        }
102
103        Ok(CSSNode::Stylesheet(rules))
104    }
105
106    /// Parse a CSS rule
107    fn parse_rule(&self, state: &mut ParserState) -> Result<CSSRule> {
108        let start_pos = state.position();
109
110        // Parse selector
111        let selector = self.parse_selector(state)?;
112        state.skip_whitespace();
113
114        // Expect opening brace
115        if state.peek() != Some('{') {
116            return Err(PostCSSError::ParseError {
117                message: "Expected '{' after selector".to_string(),
118                line: state.line(),
119                column: state.column(),
120            });
121        }
122        state.advance(); // consume '{'
123
124        // Parse declarations
125        let mut declarations = Vec::new();
126        state.skip_whitespace();
127
128        while !state.is_eof() && state.peek() != Some('}') {
129            if let Ok(declaration) = self.parse_declaration(state) {
130                declarations.push(declaration);
131            }
132            state.skip_whitespace();
133        }
134
135        // Expect closing brace
136        if state.peek() != Some('}') {
137            return Err(PostCSSError::ParseError {
138                message: "Expected '}' after declarations".to_string(),
139                line: state.line(),
140                column: state.column(),
141            });
142        }
143        state.advance(); // consume '}'
144
145        Ok(CSSRule {
146            selector,
147            declarations,
148            nested_rules: Vec::new(),
149            media_query: None,
150            specificity: 0,
151            position: if self.options.track_positions {
152                Some(SourcePosition {
153                    line: start_pos.line,
154                    column: start_pos.column,
155                    source: None,
156                })
157            } else {
158                None
159            },
160        })
161    }
162
163    /// Parse a CSS selector
164    fn parse_selector(&self, state: &mut ParserState) -> Result<String> {
165        let mut selector = String::new();
166
167        while !state.is_eof() && state.peek() != Some('{') {
168            let ch = state.peek().unwrap();
169            if ch == ';' || ch == '}' {
170                break;
171            }
172            selector.push(ch);
173            state.advance();
174        }
175
176        Ok(selector.trim().to_string())
177    }
178
179    /// Parse a CSS declaration
180    fn parse_declaration(&self, state: &mut ParserState) -> Result<CSSDeclaration> {
181        let start_pos = state.position();
182
183        // Parse property name
184        let property = self.parse_property_name(state)?;
185        state.skip_whitespace();
186
187        // Expect colon
188        if state.peek() != Some(':') {
189            return Err(PostCSSError::ParseError {
190                message: "Expected ':' after property name".to_string(),
191                line: state.line(),
192                column: state.column(),
193            });
194        }
195        state.advance(); // consume ':'
196        state.skip_whitespace();
197
198        // Parse value
199        let value = self.parse_property_value(state)?;
200        state.skip_whitespace();
201
202        // Check for !important
203        let mut important = false;
204        if state.peek() == Some('!') {
205            state.advance(); // consume '!'
206            if state.peek() == Some('i') || state.peek() == Some('I') {
207                let important_str = state.read_while(|ch| ch.is_alphabetic());
208                if important_str.to_lowercase() == "important" {
209                    important = true;
210                }
211            }
212        }
213
214        // Expect semicolon or end of rule
215        if state.peek() == Some(';') {
216            state.advance(); // consume ';'
217        }
218
219        Ok(CSSDeclaration {
220            property,
221            value,
222            important,
223            position: if self.options.track_positions {
224                Some(SourcePosition {
225                    line: start_pos.line,
226                    column: start_pos.column,
227                    source: None,
228                })
229            } else {
230                None
231            },
232        })
233    }
234
235    /// Parse property name
236    fn parse_property_name(&self, state: &mut ParserState) -> Result<String> {
237        let name = state.read_while(|ch| ch.is_alphanumeric() || ch == '-');
238        if name.is_empty() {
239            return Err(PostCSSError::ParseError {
240                message: "Expected property name".to_string(),
241                line: state.line(),
242                column: state.column(),
243            });
244        }
245        Ok(name)
246    }
247
248    /// Parse property value
249    fn parse_property_value(&self, state: &mut ParserState) -> Result<String> {
250        let mut value = String::new();
251        let mut depth = 0;
252
253        while !state.is_eof() {
254            let ch = state.peek().unwrap();
255
256            match ch {
257                '(' | '[' | '{' => {
258                    depth += 1;
259                    value.push(ch);
260                    state.advance();
261                }
262                ')' | ']' | '}' => {
263                    if depth > 0 {
264                        depth -= 1;
265                        value.push(ch);
266                        state.advance();
267                    } else {
268                        break;
269                    }
270                }
271                ';' | '!' => {
272                    if depth == 0 {
273                        break;
274                    }
275                    value.push(ch);
276                    state.advance();
277                }
278                _ => {
279                    value.push(ch);
280                    state.advance();
281                }
282            }
283        }
284
285        Ok(value.trim().to_string())
286    }
287
288    /// Parse an at-rule
289    fn parse_at_rule(&self, state: &mut ParserState) -> Result<CSSAtRule> {
290        let start_pos = state.position();
291
292        // Consume '@'
293        state.advance();
294
295        // Parse at-rule name
296        let name = state.read_while(|ch| ch.is_alphanumeric() || ch == '-');
297        if name.is_empty() {
298            return Err(PostCSSError::ParseError {
299                message: "Expected at-rule name".to_string(),
300                line: state.line(),
301                column: state.column(),
302            });
303        }
304
305        state.skip_whitespace();
306
307        // Parse parameters
308        let params = if state.peek() == Some('{') {
309            String::new()
310        } else {
311            self.parse_at_rule_params(state)?
312        };
313
314        // Parse body if present
315        let mut body = Vec::new();
316        if state.peek() == Some('{') {
317            state.advance(); // consume '{'
318            state.skip_whitespace();
319
320            while !state.is_eof() && state.peek() != Some('}') {
321                if let Ok(rule) = self.parse_rule(state) {
322                    body.push(CSSNode::Rule(rule));
323                }
324                state.skip_whitespace();
325            }
326
327            if state.peek() == Some('}') {
328                state.advance(); // consume '}'
329            }
330        }
331
332        Ok(CSSAtRule {
333            name,
334            params,
335            body,
336            position: if self.options.track_positions {
337                Some(SourcePosition {
338                    line: start_pos.line,
339                    column: start_pos.column,
340                    source: None,
341                })
342            } else {
343                None
344            },
345        })
346    }
347
348    /// Parse at-rule parameters
349    fn parse_at_rule_params(&self, state: &mut ParserState) -> Result<String> {
350        let mut params = String::new();
351        let mut depth = 0;
352
353        while !state.is_eof() {
354            let ch = state.peek().unwrap();
355
356            match ch {
357                '(' | '[' | '{' => {
358                    depth += 1;
359                    params.push(ch);
360                    state.advance();
361                }
362                ')' | ']' | '}' => {
363                    if depth > 0 {
364                        depth -= 1;
365                        params.push(ch);
366                        state.advance();
367                    } else {
368                        break;
369                    }
370                }
371                _ => {
372                    params.push(ch);
373                    state.advance();
374                }
375            }
376        }
377
378        Ok(params.trim().to_string())
379    }
380}
381
382/// Parser state for tracking position and input
383#[derive(Debug)]
384struct ParserState<'a> {
385    input: &'a str,
386    position: usize,
387    line: usize,
388    column: usize,
389    options: &'a ParseOptions,
390}
391
392impl<'a> ParserState<'a> {
393    fn new(input: &'a str, options: &'a ParseOptions) -> Self {
394        Self {
395            input,
396            position: 0,
397            line: 1,
398            column: 1,
399            options,
400        }
401    }
402
403    fn is_eof(&self) -> bool {
404        self.position >= self.input.len()
405    }
406
407    fn peek(&self) -> Option<char> {
408        self.input.chars().nth(self.position)
409    }
410
411    fn advance(&mut self) {
412        if let Some(ch) = self.peek() {
413            if ch == '\n' {
414                self.line += 1;
415                self.column = 1;
416            } else {
417                self.column += 1;
418            }
419            self.position += 1;
420        }
421    }
422
423    fn skip_whitespace(&mut self) {
424        while !self.is_eof() {
425            match self.peek() {
426                Some(ch) if ch.is_whitespace() => {
427                    self.advance();
428                }
429                Some('/') if self.peek_ahead(1) == Some('*') => {
430                    // Skip CSS comments
431                    self.advance(); // consume '/'
432                    self.advance(); // consume '*'
433                    while !self.is_eof() {
434                        if self.peek() == Some('*') && self.peek_ahead(1) == Some('/') {
435                            self.advance(); // consume '*'
436                            self.advance(); // consume '/'
437                            break;
438                        }
439                        self.advance();
440                    }
441                }
442                _ => break,
443            }
444        }
445    }
446
447    fn peek_ahead(&self, offset: usize) -> Option<char> {
448        self.input.chars().nth(self.position + offset)
449    }
450
451    fn read_while<F>(&mut self, predicate: F) -> String
452    where
453        F: Fn(char) -> bool,
454    {
455        let mut result = String::new();
456        while !self.is_eof() {
457            if let Some(ch) = self.peek() {
458                if predicate(ch) {
459                    result.push(ch);
460                    self.advance();
461                } else {
462                    break;
463                }
464            } else {
465                break;
466            }
467        }
468        result
469    }
470
471    fn position(&self) -> SourcePosition {
472        SourcePosition {
473            line: self.line,
474            column: self.column,
475            source: None,
476        }
477    }
478
479    fn line(&self) -> usize {
480        self.line
481    }
482
483    fn column(&self) -> usize {
484        self.column
485    }
486}
487
488#[cfg(test)]
489mod tests {
490    use super::*;
491
492    #[test]
493    fn test_simple_css_parsing() {
494        let parser = CSSParser::new(ParseOptions::default());
495        let input = ".test { color: red; font-size: 16px; }";
496        let result = parser.parse(input);
497
498        assert!(result.is_ok());
499
500        if let Ok(CSSNode::Stylesheet(rules)) = result {
501            assert_eq!(rules.len(), 1);
502            assert_eq!(rules[0].selector, ".test");
503            assert_eq!(rules[0].declarations.len(), 2);
504            assert_eq!(rules[0].declarations[0].property, "color");
505            assert_eq!(rules[0].declarations[0].value, "red");
506        }
507    }
508
509    #[test]
510    fn test_at_rule_parsing() {
511        let parser = CSSParser::new(ParseOptions::default());
512        let input = "@media (max-width: 768px) { .mobile { display: block; } }";
513        let result = parser.parse(input);
514
515        assert!(result.is_ok());
516    }
517
518    #[test]
519    fn test_important_declaration() {
520        let parser = CSSParser::new(ParseOptions::default());
521        let input = ".test { color: red !important; }";
522        let result = parser.parse(input);
523
524        assert!(result.is_ok());
525
526        if let Ok(CSSNode::Stylesheet(rules)) = result {
527            assert!(rules[0].declarations[0].important);
528        }
529    }
530
531    #[test]
532    fn test_parser_state() {
533        let options = ParseOptions::default();
534        let mut state = ParserState::new("test input", &options);
535
536        assert!(!state.is_eof());
537        assert_eq!(state.peek(), Some('t'));
538
539        state.advance();
540        assert_eq!(state.peek(), Some('e'));
541        assert_eq!(state.line(), 1);
542        assert_eq!(state.column(), 2);
543    }
544}