tcss_core/
generator.rs

1//! CSS Generator for TCSS
2//!
3//! This module converts a processed AST (with evaluated expressions) into valid CSS output.
4
5use crate::ast::{ASTNode, Expr, Program, Property};
6use crate::executor::Executor;
7
8/// Options for CSS generation
9#[derive(Debug, Clone)]
10pub struct GeneratorOptions {
11    /// Minify the output (remove whitespace)
12    pub minify: bool,
13    
14    /// Indentation string (e.g., "  " or "\t")
15    pub indent: String,
16    
17    /// Add newline at end of file
18    pub trailing_newline: bool,
19}
20
21impl Default for GeneratorOptions {
22    fn default() -> Self {
23        Self {
24            minify: false,
25            indent: "  ".to_string(),
26            trailing_newline: true,
27        }
28    }
29}
30
31/// CSS Generator
32pub struct Generator {
33    options: GeneratorOptions,
34    executor: Executor,
35}
36
37impl Generator {
38    /// Create a new generator with default options
39    pub fn new() -> Self {
40        Self {
41            options: GeneratorOptions::default(),
42            executor: Executor::new(),
43        }
44    }
45
46    /// Create a new generator with custom options
47    pub fn with_options(options: GeneratorOptions) -> Self {
48        Self {
49            options,
50            executor: Executor::new(),
51        }
52    }
53
54    /// Generate CSS from a TCSS program
55    pub fn generate(&mut self, program: &Program) -> Result<String, String> {
56        // First, execute the program to evaluate all expressions
57        self.executor.execute(program)?;
58
59        // Then, generate CSS from CSS rules
60        let mut css = String::new();
61
62        for node in &program.nodes {
63            if let ASTNode::CSSRule { selector, properties } = node {
64                let rule = self.generate_css_rule(selector, properties)?;
65                css.push_str(&rule);
66                
67                if !self.options.minify {
68                    css.push('\n');
69                }
70            }
71        }
72
73        // Add trailing newline if requested
74        if self.options.trailing_newline && !css.is_empty() && !css.ends_with('\n') {
75            css.push('\n');
76        }
77
78        Ok(css)
79    }
80
81    /// Generate a CSS rule
82    fn generate_css_rule(&mut self, selector: &str, properties: &[Property]) -> Result<String, String> {
83        let mut css = String::new();
84
85        // Selector
86        css.push_str(selector);
87        
88        if self.options.minify {
89            css.push('{');
90        } else {
91            css.push_str(" {\n");
92        }
93
94        // Properties
95        for (_i, property) in properties.iter().enumerate() {
96            if !self.options.minify {
97                css.push_str(&self.options.indent);
98            }
99
100            css.push_str(&property.name);
101            css.push(':');
102            
103            if !self.options.minify {
104                css.push(' ');
105            }
106
107            // Evaluate the property value
108            let value = self.evaluate_property_value(&property.value)?;
109            css.push_str(&value);
110            css.push(';');
111
112            if !self.options.minify {
113                css.push('\n');
114            }
115        }
116
117        // Closing brace
118        css.push('}');
119        
120        if !self.options.minify {
121            css.push('\n');
122        }
123
124        Ok(css)
125    }
126
127    /// Evaluate a property value expression
128    fn evaluate_property_value(&mut self, expr: &Expr) -> Result<String, String> {
129        let value = self.executor.evaluate_expr(expr)?;
130        Ok(value.to_css_string())
131    }
132
133    /// Generate minified CSS from a TCSS program
134    pub fn generate_minified(&mut self, program: &Program) -> Result<String, String> {
135        let original_minify = self.options.minify;
136        self.options.minify = true;
137        let result = self.generate(program);
138        self.options.minify = original_minify;
139        result
140    }
141
142    /// Generate formatted CSS from a TCSS program
143    pub fn generate_formatted(&mut self, program: &Program) -> Result<String, String> {
144        let original_minify = self.options.minify;
145        self.options.minify = false;
146        let result = self.generate(program);
147        self.options.minify = original_minify;
148        result
149    }
150
151    /// Get a reference to the executor (for testing)
152    pub fn executor(&self) -> &Executor {
153        &self.executor
154    }
155
156    /// Get a mutable reference to the executor
157    pub fn executor_mut(&mut self) -> &mut Executor {
158        &mut self.executor
159    }
160
161    /// Get the current options
162    pub fn options(&self) -> &GeneratorOptions {
163        &self.options
164    }
165
166    /// Set new options
167    pub fn set_options(&mut self, options: GeneratorOptions) {
168        self.options = options;
169    }
170}
171
172impl Default for Generator {
173    fn default() -> Self {
174        Self::new()
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181    use crate::lexer::Lexer;
182    use crate::parser::Parser;
183
184    fn generate_css(source: &str) -> Result<String, String> {
185        let mut lexer = Lexer::new(source);
186        let tokens = lexer.tokenize()?;
187        let mut parser = Parser::new(tokens);
188        let program = parser.parse()?;
189        let mut generator = Generator::new();
190        generator.generate(&program)
191    }
192
193    #[test]
194    fn test_simple_css_rule() {
195        let source = ".button:\n    padding: 16px\n    color: #fff";
196        let css = generate_css(source).unwrap();
197        assert!(css.contains(".button"));
198        assert!(css.contains("padding: 16px;"));
199        assert!(css.contains("color: #fff;"));
200    }
201
202    #[test]
203    fn test_with_variable() {
204        let source = "@var primary: #3498db\n\n.button:\n    background: primary";
205        let css = generate_css(source).unwrap();
206        assert!(css.contains("background: #3498db;"));
207    }
208
209    #[test]
210    fn test_with_function() {
211        let source = r#"
212@fn spacing(x):
213    return x * 16px
214
215.container:
216    padding: spacing(2)
217"#;
218        let css = generate_css(source).unwrap();
219        assert!(css.contains("padding: 32px;"));
220    }
221
222    #[test]
223    fn test_minified_output() {
224        let source = ".button:\n    padding: 16px\n    color: #fff";
225        let mut lexer = Lexer::new(source);
226        let tokens = lexer.tokenize().unwrap();
227        let mut parser = Parser::new(tokens);
228        let program = parser.parse().unwrap();
229
230        let mut generator = Generator::with_options(GeneratorOptions {
231            minify: true,
232            indent: "  ".to_string(),
233            trailing_newline: false,
234        });
235
236        let css = generator.generate(&program).unwrap();
237        assert_eq!(css, ".button{padding:16px;color:#fff;}");
238    }
239
240    #[test]
241    fn test_formatted_output() {
242        let source = ".button:\n    padding: 16px";
243        let css = generate_css(source).unwrap();
244        assert!(css.contains("  padding: 16px;"));
245        assert!(css.contains("{\n"));
246        assert!(css.contains("}\n"));
247    }
248
249    #[test]
250    fn test_multiple_rules() {
251        let source = r#"
252.button:
253    padding: 16px
254
255.container:
256    margin: 20px
257"#;
258        let css = generate_css(source).unwrap();
259        assert!(css.contains(".button"));
260        assert!(css.contains(".container"));
261        assert!(css.contains("padding: 16px;"));
262        assert!(css.contains("margin: 20px;"));
263    }
264
265    #[test]
266    fn test_complex_expressions() {
267        let source = r#"
268@var base: 16px
269@var multiplier: 2
270
271.container:
272    padding: base * multiplier
273    margin: base + 8px
274"#;
275        let css = generate_css(source).unwrap();
276        assert!(css.contains("padding: 32px;"));
277        assert!(css.contains("margin: 24px;"));
278    }
279
280    #[test]
281    fn test_string_values() {
282        let source = r#"
283@var font: "Arial"
284
285.text:
286    font-family: font
287"#;
288        let css = generate_css(source).unwrap();
289        assert!(css.contains("font-family: Arial;"));
290    }
291
292    #[test]
293    fn test_nested_function_calls() {
294        let source = r#"
295@fn double(x):
296    return x * 2
297
298@fn quadruple(x):
299    return double(double(x))
300
301.box:
302    width: quadruple(10px)
303"#;
304        let css = generate_css(source).unwrap();
305        assert!(css.contains("width: 40px;"));
306    }
307}
308