tailwind_rs_postcss/
ast.rs

1//! CSS AST (Abstract Syntax Tree) definitions
2//!
3//! This module provides the core AST structures for representing CSS
4//! in a structured, manipulatable format.
5
6use serde::{Deserialize, Serialize};
7// use std::collections::HashMap;
8
9/// Root CSS AST node
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11pub enum CSSNode {
12    /// Stylesheet containing multiple rules
13    Stylesheet(Vec<CSSRule>),
14    /// Single CSS rule
15    Rule(CSSRule),
16    /// CSS declaration
17    Declaration(CSSDeclaration),
18    /// At-rule (e.g., @media, @keyframes)
19    AtRule(CSSAtRule),
20    /// Comment
21    Comment(String),
22}
23
24/// CSS rule representation
25#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
26pub struct CSSRule {
27    /// Rule selector (e.g., ".class", "#id", "div")
28    pub selector: String,
29    /// CSS declarations
30    pub declarations: Vec<CSSDeclaration>,
31    /// Nested rules (for complex selectors)
32    pub nested_rules: Vec<CSSRule>,
33    /// Media query (for responsive rules)
34    pub media_query: Option<String>,
35    /// Rule specificity
36    pub specificity: u32,
37    /// Source position
38    pub position: Option<SourcePosition>,
39}
40
41/// CSS declaration (property-value pair)
42#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
43pub struct CSSDeclaration {
44    /// CSS property name
45    pub property: String,
46    /// CSS property value
47    pub value: String,
48    /// Whether the declaration is marked as !important
49    pub important: bool,
50    /// Source position
51    pub position: Option<SourcePosition>,
52}
53
54/// CSS at-rule (e.g., @media, @keyframes, @import)
55#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
56pub struct CSSAtRule {
57    /// At-rule name (e.g., "media", "keyframes", "import")
58    pub name: String,
59    /// At-rule parameters
60    pub params: String,
61    /// Nested rules or declarations
62    pub body: Vec<CSSNode>,
63    /// Source position
64    pub position: Option<SourcePosition>,
65}
66
67/// Source position information
68#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
69pub struct SourcePosition {
70    /// Line number (1-based)
71    pub line: usize,
72    /// Column number (1-based)
73    pub column: usize,
74    /// Source file path
75    pub source: Option<String>,
76}
77
78/// CSS selector component
79#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
80pub enum SelectorComponent {
81    /// Class selector (.class)
82    Class(String),
83    /// ID selector (#id)
84    Id(String),
85    /// Element selector (div, span, etc.)
86    Element(String),
87    /// Attribute selector ([attr="value"])
88    Attribute(AttributeSelector),
89    /// Pseudo-class (:hover, :focus)
90    PseudoClass(String),
91    /// Pseudo-element (::before, ::after)
92    PseudoElement(String),
93    /// Universal selector (*)
94    Universal,
95    /// Combinator (>, +, ~, space)
96    Combinator(CombinatorType),
97    /// Group of selectors
98    Group(Vec<SelectorComponent>),
99}
100
101/// Attribute selector
102#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
103pub struct AttributeSelector {
104    pub name: String,
105    pub operator: AttributeOperator,
106    pub value: Option<String>,
107    pub case_sensitive: bool,
108}
109
110/// Attribute operator types
111#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
112pub enum AttributeOperator {
113    /// [attr]
114    Exists,
115    /// [attr="value"]
116    Equals,
117    /// [attr~="value"]
118    ContainsWord,
119    /// [attr|="value"]
120    StartsWith,
121    /// [attr^="value"]
122    StartsWithPrefix,
123    /// [attr$="value"]
124    EndsWith,
125    /// [attr*="value"]
126    Contains,
127}
128
129/// Combinator types
130#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
131pub enum CombinatorType {
132    /// Descendant combinator (space)
133    Descendant,
134    /// Child combinator (>)
135    Child,
136    /// Adjacent sibling combinator (+)
137    AdjacentSibling,
138    /// General sibling combinator (~)
139    GeneralSibling,
140}
141
142/// CSS specificity calculation
143impl CSSRule {
144    /// Calculate CSS specificity
145    pub fn calculate_specificity(&self) -> u32 {
146        let mut specificity = 0u32;
147        
148        // Count ID selectors (100 points each)
149        let id_count = self.selector.matches('#').count();
150        specificity += (id_count as u32) * 100;
151        
152        // Count class selectors, attribute selectors, and pseudo-classes (10 points each)
153        let class_count = self.selector.matches('.').count();
154        let attribute_count = self.selector.matches('[').count();
155        let pseudo_class_count = self.selector.matches(':').count() - self.selector.matches("::").count();
156        specificity += ((class_count + attribute_count + pseudo_class_count) as u32) * 10;
157        
158        // Count element selectors (1 point each)
159        let element_count = self.selector.split_whitespace()
160            .filter(|s| !s.starts_with('.') && !s.starts_with('#') && !s.starts_with('[') && !s.starts_with(':'))
161            .count();
162        specificity += element_count as u32;
163        
164        specificity
165    }
166    
167    /// Check if rule matches a selector
168    pub fn matches_selector(&self, target_selector: &str) -> bool {
169        self.selector == target_selector
170    }
171    
172    /// Add a declaration to the rule
173    pub fn add_declaration(&mut self, property: String, value: String, important: bool) {
174        let declaration = CSSDeclaration {
175            property,
176            value,
177            important,
178            position: None,
179        };
180        self.declarations.push(declaration);
181    }
182    
183    /// Remove a declaration by property name
184    pub fn remove_declaration(&mut self, property: &str) {
185        self.declarations.retain(|decl| decl.property != property);
186    }
187    
188    /// Get a declaration by property name
189    pub fn get_declaration(&self, property: &str) -> Option<&CSSDeclaration> {
190        self.declarations.iter().find(|decl| decl.property == property)
191    }
192    
193    /// Check if rule has a specific property
194    pub fn has_property(&self, property: &str) -> bool {
195        self.declarations.iter().any(|decl| decl.property == property)
196    }
197}
198
199impl CSSDeclaration {
200    /// Create a new declaration
201    pub fn new(property: String, value: String) -> Self {
202        Self {
203            property,
204            value,
205            important: false,
206            position: None,
207        }
208    }
209    
210    /// Create a new important declaration
211    pub fn new_important(property: String, value: String) -> Self {
212        Self {
213            property,
214            value,
215            important: true,
216            position: None,
217        }
218    }
219    
220    /// Set the declaration as important
221    pub fn set_important(&mut self) {
222        self.important = true;
223    }
224    
225    /// Check if declaration is important
226    pub fn is_important(&self) -> bool {
227        self.important
228    }
229}
230
231impl CSSAtRule {
232    /// Create a new at-rule
233    pub fn new(name: String, params: String) -> Self {
234        Self {
235            name,
236            params,
237            body: Vec::new(),
238            position: None,
239        }
240    }
241    
242    /// Add a nested rule to the at-rule
243    pub fn add_rule(&mut self, rule: CSSRule) {
244        self.body.push(CSSNode::Rule(rule));
245    }
246    
247    /// Add a declaration to the at-rule
248    pub fn add_declaration(&mut self, declaration: CSSDeclaration) {
249        self.body.push(CSSNode::Declaration(declaration));
250    }
251}
252
253/// AST manipulation utilities
254impl CSSNode {
255    /// Get all rules from a stylesheet
256    pub fn get_rules(&self) -> Vec<&CSSRule> {
257        match self {
258            CSSNode::Stylesheet(rules) => rules.iter().collect(),
259            CSSNode::Rule(rule) => vec![rule],
260            _ => Vec::new(),
261        }
262    }
263    
264    /// Get all declarations from a node
265    pub fn get_declarations(&self) -> Vec<&CSSDeclaration> {
266        match self {
267            CSSNode::Rule(rule) => rule.declarations.iter().collect(),
268            CSSNode::Declaration(decl) => vec![decl],
269            _ => Vec::new(),
270        }
271    }
272    
273    /// Find rules by selector
274    pub fn find_rules_by_selector(&self, selector: &str) -> Vec<&CSSRule> {
275        self.get_rules()
276            .into_iter()
277            .filter(|rule| rule.matches_selector(selector))
278            .collect()
279    }
280    
281    /// Find rules by property
282    pub fn find_rules_by_property(&self, property: &str) -> Vec<&CSSRule> {
283        self.get_rules()
284            .into_iter()
285            .filter(|rule| rule.has_property(property))
286            .collect()
287    }
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293
294    #[test]
295    fn test_css_rule_creation() {
296        let rule = CSSRule {
297            selector: ".test".to_string(),
298            declarations: vec![
299                CSSDeclaration::new("color".to_string(), "red".to_string()),
300                CSSDeclaration::new("font-size".to_string(), "16px".to_string()),
301            ],
302            nested_rules: Vec::new(),
303            media_query: None,
304            specificity: 0,
305            position: None,
306        };
307        
308        assert_eq!(rule.selector, ".test");
309        assert_eq!(rule.declarations.len(), 2);
310        assert!(rule.has_property("color"));
311        assert!(!rule.has_property("background"));
312    }
313
314    #[test]
315    fn test_specificity_calculation() {
316        let rule = CSSRule {
317            selector: "#id .class div".to_string(),
318            declarations: Vec::new(),
319            nested_rules: Vec::new(),
320            media_query: None,
321            specificity: 0,
322            position: None,
323        };
324        
325        let specificity = rule.calculate_specificity();
326        // 1 ID (#id) = 100, 1 class (.class) = 10, 1 element (div) = 1
327        assert_eq!(specificity, 111);
328    }
329
330    #[test]
331    fn test_declaration_creation() {
332        let decl = CSSDeclaration::new_important("color".to_string(), "red".to_string());
333        assert_eq!(decl.property, "color");
334        assert_eq!(decl.value, "red");
335        assert!(decl.is_important());
336    }
337
338    #[test]
339    fn test_at_rule_creation() {
340        let mut at_rule = CSSAtRule::new("media".to_string(), "(max-width: 768px)".to_string());
341        at_rule.add_rule(CSSRule {
342            selector: ".mobile".to_string(),
343            declarations: vec![CSSDeclaration::new("display".to_string(), "block".to_string())],
344            nested_rules: Vec::new(),
345            media_query: None,
346            specificity: 0,
347            position: None,
348        });
349        
350        assert_eq!(at_rule.name, "media");
351        assert_eq!(at_rule.params, "(max-width: 768px)");
352        assert_eq!(at_rule.body.len(), 1);
353    }
354}