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 =
156            self.selector.matches(':').count() - self.selector.matches("::").count();
157        specificity += ((class_count + attribute_count + pseudo_class_count) as u32) * 10;
158
159        // Count element selectors (1 point each)
160        let element_count = self
161            .selector
162            .split_whitespace()
163            .filter(|s| {
164                !s.starts_with('.')
165                    && !s.starts_with('#')
166                    && !s.starts_with('[')
167                    && !s.starts_with(':')
168            })
169            .count();
170        specificity += element_count as u32;
171
172        specificity
173    }
174
175    /// Check if rule matches a selector
176    pub fn matches_selector(&self, target_selector: &str) -> bool {
177        self.selector == target_selector
178    }
179
180    /// Add a declaration to the rule
181    pub fn add_declaration(&mut self, property: String, value: String, important: bool) {
182        let declaration = CSSDeclaration {
183            property,
184            value,
185            important,
186            position: None,
187        };
188        self.declarations.push(declaration);
189    }
190
191    /// Remove a declaration by property name
192    pub fn remove_declaration(&mut self, property: &str) {
193        self.declarations.retain(|decl| decl.property != property);
194    }
195
196    /// Get a declaration by property name
197    pub fn get_declaration(&self, property: &str) -> Option<&CSSDeclaration> {
198        self.declarations
199            .iter()
200            .find(|decl| decl.property == property)
201    }
202
203    /// Check if rule has a specific property
204    pub fn has_property(&self, property: &str) -> bool {
205        self.declarations
206            .iter()
207            .any(|decl| decl.property == property)
208    }
209}
210
211impl CSSDeclaration {
212    /// Create a new declaration
213    pub fn new(property: String, value: String) -> Self {
214        Self {
215            property,
216            value,
217            important: false,
218            position: None,
219        }
220    }
221
222    /// Create a new important declaration
223    pub fn new_important(property: String, value: String) -> Self {
224        Self {
225            property,
226            value,
227            important: true,
228            position: None,
229        }
230    }
231
232    /// Set the declaration as important
233    pub fn set_important(&mut self) {
234        self.important = true;
235    }
236
237    /// Check if declaration is important
238    pub fn is_important(&self) -> bool {
239        self.important
240    }
241}
242
243impl CSSAtRule {
244    /// Create a new at-rule
245    pub fn new(name: String, params: String) -> Self {
246        Self {
247            name,
248            params,
249            body: Vec::new(),
250            position: None,
251        }
252    }
253
254    /// Add a nested rule to the at-rule
255    pub fn add_rule(&mut self, rule: CSSRule) {
256        self.body.push(CSSNode::Rule(rule));
257    }
258
259    /// Add a declaration to the at-rule
260    pub fn add_declaration(&mut self, declaration: CSSDeclaration) {
261        self.body.push(CSSNode::Declaration(declaration));
262    }
263}
264
265/// AST manipulation utilities
266impl CSSNode {
267    /// Get all rules from a stylesheet
268    pub fn get_rules(&self) -> Vec<&CSSRule> {
269        match self {
270            CSSNode::Stylesheet(rules) => rules.iter().collect(),
271            CSSNode::Rule(rule) => vec![rule],
272            _ => Vec::new(),
273        }
274    }
275
276    /// Get all declarations from a node
277    pub fn get_declarations(&self) -> Vec<&CSSDeclaration> {
278        match self {
279            CSSNode::Rule(rule) => rule.declarations.iter().collect(),
280            CSSNode::Declaration(decl) => vec![decl],
281            _ => Vec::new(),
282        }
283    }
284
285    /// Find rules by selector
286    pub fn find_rules_by_selector(&self, selector: &str) -> Vec<&CSSRule> {
287        self.get_rules()
288            .into_iter()
289            .filter(|rule| rule.matches_selector(selector))
290            .collect()
291    }
292
293    /// Find rules by property
294    pub fn find_rules_by_property(&self, property: &str) -> Vec<&CSSRule> {
295        self.get_rules()
296            .into_iter()
297            .filter(|rule| rule.has_property(property))
298            .collect()
299    }
300}
301
302#[cfg(test)]
303mod tests {
304    use super::*;
305
306    #[test]
307    fn test_css_rule_creation() {
308        let rule = CSSRule {
309            selector: ".test".to_string(),
310            declarations: vec![
311                CSSDeclaration::new("color".to_string(), "red".to_string()),
312                CSSDeclaration::new("font-size".to_string(), "16px".to_string()),
313            ],
314            nested_rules: Vec::new(),
315            media_query: None,
316            specificity: 0,
317            position: None,
318        };
319
320        assert_eq!(rule.selector, ".test");
321        assert_eq!(rule.declarations.len(), 2);
322        assert!(rule.has_property("color"));
323        assert!(!rule.has_property("background"));
324    }
325
326    #[test]
327    fn test_specificity_calculation() {
328        let rule = CSSRule {
329            selector: "#id .class div".to_string(),
330            declarations: Vec::new(),
331            nested_rules: Vec::new(),
332            media_query: None,
333            specificity: 0,
334            position: None,
335        };
336
337        let specificity = rule.calculate_specificity();
338        // 1 ID (#id) = 100, 1 class (.class) = 10, 1 element (div) = 1
339        assert_eq!(specificity, 111);
340    }
341
342    #[test]
343    fn test_declaration_creation() {
344        let decl = CSSDeclaration::new_important("color".to_string(), "red".to_string());
345        assert_eq!(decl.property, "color");
346        assert_eq!(decl.value, "red");
347        assert!(decl.is_important());
348    }
349
350    #[test]
351    fn test_at_rule_creation() {
352        let mut at_rule = CSSAtRule::new("media".to_string(), "(max-width: 768px)".to_string());
353        at_rule.add_rule(CSSRule {
354            selector: ".mobile".to_string(),
355            declarations: vec![CSSDeclaration::new(
356                "display".to_string(),
357                "block".to_string(),
358            )],
359            nested_rules: Vec::new(),
360            media_query: None,
361            specificity: 0,
362            position: None,
363        });
364
365        assert_eq!(at_rule.name, "media");
366        assert_eq!(at_rule.params, "(max-width: 768px)");
367        assert_eq!(at_rule.body.len(), 1);
368    }
369}