1use crate::ast::{CSSAtRule, CSSDeclaration, CSSNode, CSSRule, SourcePosition};
7use crate::error::{PostCSSError, Result};
8use serde::{Deserialize, Serialize};
9#[derive(Debug, Clone)]
13pub struct CSSParser {
14 options: ParseOptions,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct ParseOptions {
20 pub track_positions: bool,
22 pub strict_mode: bool,
24 pub custom_properties: bool,
26 pub nesting: bool,
28 pub container_queries: bool,
30 pub cascade_layers: bool,
32 pub max_nesting_depth: usize,
34 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 pub fn new(options: ParseOptions) -> Self {
56 Self { options }
57 }
58
59 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 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 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 if let Ok(rule) = self.parse_rule(state) {
98 rules.push(rule);
99 }
100 }
101 }
102
103 Ok(CSSNode::Stylesheet(rules))
104 }
105
106 fn parse_rule(&self, state: &mut ParserState) -> Result<CSSRule> {
108 let start_pos = state.position();
109
110 let selector = self.parse_selector(state)?;
112 state.skip_whitespace();
113
114 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(); 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 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(); 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 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 fn parse_declaration(&self, state: &mut ParserState) -> Result<CSSDeclaration> {
181 let start_pos = state.position();
182
183 let property = self.parse_property_name(state)?;
185 state.skip_whitespace();
186
187 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(); state.skip_whitespace();
197
198 let value = self.parse_property_value(state)?;
200 state.skip_whitespace();
201
202 let mut important = false;
204 if state.peek() == Some('!') {
205 state.advance(); 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 if state.peek() == Some(';') {
216 state.advance(); }
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 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 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 fn parse_at_rule(&self, state: &mut ParserState) -> Result<CSSAtRule> {
290 let start_pos = state.position();
291
292 state.advance();
294
295 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 let params = if state.peek() == Some('{') {
309 String::new()
310 } else {
311 self.parse_at_rule_params(state)?
312 };
313
314 let mut body = Vec::new();
316 if state.peek() == Some('{') {
317 state.advance(); 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(); }
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 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#[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 self.advance(); self.advance(); while !self.is_eof() {
434 if self.peek() == Some('*') && self.peek_ahead(1) == Some('/') {
435 self.advance(); self.advance(); 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}