we (web engine): Experimental web browser project to understand the limits of Claude
1//! CSS parser per CSS Syntax Module Level 3 §5.
2//!
3//! Consumes tokens from the tokenizer and produces a structured stylesheet.
4
5use crate::tokenizer::{HashType, NumericType, Token, Tokenizer};
6
7// ---------------------------------------------------------------------------
8// AST types
9// ---------------------------------------------------------------------------
10
11/// A parsed CSS stylesheet: a list of rules.
12#[derive(Debug, Clone, PartialEq)]
13pub struct Stylesheet {
14 pub rules: Vec<Rule>,
15}
16
17/// A top-level rule: either a style rule or an at-rule.
18#[derive(Debug, Clone, PartialEq)]
19pub enum Rule {
20 Style(StyleRule),
21 Media(MediaRule),
22 Import(ImportRule),
23}
24
25/// A style rule: selector list + declarations.
26#[derive(Debug, Clone, PartialEq)]
27pub struct StyleRule {
28 pub selectors: SelectorList,
29 pub declarations: Vec<Declaration>,
30}
31
32/// A `@media` rule with a prelude (media query text) and nested rules.
33#[derive(Debug, Clone, PartialEq)]
34pub struct MediaRule {
35 pub query: String,
36 pub rules: Vec<Rule>,
37}
38
39/// An `@import` rule with a URL.
40#[derive(Debug, Clone, PartialEq)]
41pub struct ImportRule {
42 pub url: String,
43}
44
45/// A comma-separated list of selectors.
46#[derive(Debug, Clone, PartialEq)]
47pub struct SelectorList {
48 pub selectors: Vec<Selector>,
49}
50
51/// A complex selector: a chain of compound selectors joined by combinators.
52#[derive(Debug, Clone, PartialEq)]
53pub struct Selector {
54 pub components: Vec<SelectorComponent>,
55}
56
57/// Either a compound selector or a combinator between compounds.
58#[derive(Debug, Clone, PartialEq)]
59pub enum SelectorComponent {
60 Compound(CompoundSelector),
61 Combinator(Combinator),
62}
63
64/// A compound selector: a sequence of simple selectors with no combinator.
65#[derive(Debug, Clone, PartialEq)]
66pub struct CompoundSelector {
67 pub simple: Vec<SimpleSelector>,
68}
69
70/// A simple selector.
71#[derive(Debug, Clone, PartialEq)]
72pub enum SimpleSelector {
73 Type(String),
74 Universal,
75 Class(String),
76 Id(String),
77 Attribute(AttributeSelector),
78 PseudoClass(String),
79}
80
81/// An attribute selector.
82#[derive(Debug, Clone, PartialEq)]
83pub struct AttributeSelector {
84 pub name: String,
85 pub op: Option<AttributeOp>,
86 pub value: Option<String>,
87}
88
89/// Attribute matching operator.
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub enum AttributeOp {
92 /// `=`
93 Exact,
94 /// `~=`
95 Includes,
96 /// `|=`
97 DashMatch,
98 /// `^=`
99 Prefix,
100 /// `$=`
101 Suffix,
102 /// `*=`
103 Substring,
104}
105
106/// A combinator between compound selectors.
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub enum Combinator {
109 /// Descendant (whitespace)
110 Descendant,
111 /// Child (`>`)
112 Child,
113 /// Adjacent sibling (`+`)
114 AdjacentSibling,
115 /// General sibling (`~`)
116 GeneralSibling,
117}
118
119/// A CSS property declaration.
120#[derive(Debug, Clone, PartialEq)]
121pub struct Declaration {
122 pub property: String,
123 pub value: Vec<ComponentValue>,
124 pub important: bool,
125}
126
127/// A component value in a declaration value. We store these as typed tokens
128/// so downstream code can interpret them without re-tokenizing.
129#[derive(Debug, Clone, PartialEq)]
130pub enum ComponentValue {
131 Ident(String),
132 String(String),
133 Number(f64, NumericType),
134 Percentage(f64),
135 Dimension(f64, NumericType, String),
136 Hash(String, HashType),
137 Function(String, Vec<ComponentValue>),
138 Delim(char),
139 Comma,
140 Whitespace,
141}
142
143// ---------------------------------------------------------------------------
144// Parser
145// ---------------------------------------------------------------------------
146
147/// CSS parser. Consumes tokens and produces AST.
148pub struct Parser {
149 tokens: Vec<Token>,
150 pos: usize,
151}
152
153impl Parser {
154 /// Parse a full stylesheet from CSS source text.
155 pub fn parse(input: &str) -> Stylesheet {
156 let tokens = Tokenizer::tokenize(input);
157 let mut parser = Self { tokens, pos: 0 };
158 parser.parse_stylesheet()
159 }
160
161 /// Parse a list of declarations (for `style` attribute values).
162 pub fn parse_declarations(input: &str) -> Vec<Declaration> {
163 let tokens = Tokenizer::tokenize(input);
164 let mut parser = Self { tokens, pos: 0 };
165 parser.parse_declaration_list()
166 }
167
168 /// Parse a selector list from a string (for `querySelector`/`querySelectorAll`).
169 pub fn parse_selectors(input: &str) -> SelectorList {
170 let tokens = Tokenizer::tokenize(input);
171 let mut parser = Self { tokens, pos: 0 };
172 parser.parse_selector_list()
173 }
174
175 // -- Token access -------------------------------------------------------
176
177 fn peek(&self) -> &Token {
178 self.tokens.get(self.pos).unwrap_or(&Token::Eof)
179 }
180
181 fn advance(&mut self) -> Token {
182 let tok = self.tokens.get(self.pos).cloned().unwrap_or(Token::Eof);
183 if self.pos < self.tokens.len() {
184 self.pos += 1;
185 }
186 tok
187 }
188
189 fn is_eof(&self) -> bool {
190 self.pos >= self.tokens.len()
191 }
192
193 fn skip_whitespace(&mut self) {
194 while matches!(self.peek(), Token::Whitespace) {
195 self.advance();
196 }
197 }
198
199 // -- Stylesheet parsing -------------------------------------------------
200
201 fn parse_stylesheet(&mut self) -> Stylesheet {
202 let mut rules = Vec::new();
203 loop {
204 self.skip_whitespace();
205 if self.is_eof() {
206 break;
207 }
208 match self.peek() {
209 Token::AtKeyword(_) => {
210 if let Some(rule) = self.parse_at_rule() {
211 rules.push(rule);
212 }
213 }
214 Token::Cdo | Token::Cdc => {
215 self.advance();
216 }
217 _ => {
218 if let Some(rule) = self.parse_style_rule() {
219 rules.push(Rule::Style(rule));
220 }
221 }
222 }
223 }
224 Stylesheet { rules }
225 }
226
227 // -- At-rules -----------------------------------------------------------
228
229 fn parse_at_rule(&mut self) -> Option<Rule> {
230 let name = match self.advance() {
231 Token::AtKeyword(name) => name,
232 _ => return None,
233 };
234
235 match name.to_ascii_lowercase().as_str() {
236 "media" => self.parse_media_rule(),
237 "import" => self.parse_import_rule(),
238 _ => {
239 // Unknown at-rule: skip to end of block or semicolon
240 self.skip_at_rule_body();
241 None
242 }
243 }
244 }
245
246 fn parse_media_rule(&mut self) -> Option<Rule> {
247 self.skip_whitespace();
248
249 // Collect prelude tokens as the media query string
250 let mut query = String::new();
251 loop {
252 match self.peek() {
253 Token::LeftBrace | Token::Eof => break,
254 Token::Whitespace => {
255 if !query.is_empty() {
256 query.push(' ');
257 }
258 self.advance();
259 }
260 _ => {
261 let tok = self.advance();
262 query.push_str(&token_to_string(&tok));
263 }
264 }
265 }
266
267 // Expect `{`
268 if !matches!(self.peek(), Token::LeftBrace) {
269 return None;
270 }
271 self.advance();
272
273 // Parse nested rules until `}`
274 let mut rules = Vec::new();
275 loop {
276 self.skip_whitespace();
277 if self.is_eof() {
278 break;
279 }
280 if matches!(self.peek(), Token::RightBrace) {
281 self.advance();
282 break;
283 }
284 match self.peek() {
285 Token::AtKeyword(_) => {
286 if let Some(rule) = self.parse_at_rule() {
287 rules.push(rule);
288 }
289 }
290 _ => {
291 if let Some(rule) = self.parse_style_rule() {
292 rules.push(Rule::Style(rule));
293 }
294 }
295 }
296 }
297
298 Some(Rule::Media(MediaRule {
299 query: query.trim().to_string(),
300 rules,
301 }))
302 }
303
304 fn parse_import_rule(&mut self) -> Option<Rule> {
305 self.skip_whitespace();
306 let url = match self.peek() {
307 Token::String(_) => {
308 if let Token::String(s) = self.advance() {
309 s
310 } else {
311 unreachable!()
312 }
313 }
314 Token::Url(_) => {
315 if let Token::Url(s) = self.advance() {
316 s
317 } else {
318 unreachable!()
319 }
320 }
321 Token::Function(name) if name.eq_ignore_ascii_case("url") => {
322 self.advance();
323 self.skip_whitespace();
324 let url = match self.advance() {
325 Token::String(s) => s,
326 _ => {
327 self.skip_to_semicolon();
328 return None;
329 }
330 };
331 self.skip_whitespace();
332 // consume closing paren
333 if matches!(self.peek(), Token::RightParen) {
334 self.advance();
335 }
336 url
337 }
338 _ => {
339 self.skip_to_semicolon();
340 return None;
341 }
342 };
343 // Consume optional trailing tokens until semicolon
344 self.skip_to_semicolon();
345 Some(Rule::Import(ImportRule { url }))
346 }
347
348 fn skip_at_rule_body(&mut self) {
349 let mut brace_depth = 0;
350 loop {
351 match self.peek() {
352 Token::Eof => return,
353 Token::Semicolon if brace_depth == 0 => {
354 self.advance();
355 return;
356 }
357 Token::LeftBrace => {
358 brace_depth += 1;
359 self.advance();
360 }
361 Token::RightBrace => {
362 if brace_depth == 0 {
363 self.advance();
364 return;
365 }
366 brace_depth -= 1;
367 self.advance();
368 if brace_depth == 0 {
369 return;
370 }
371 }
372 _ => {
373 self.advance();
374 }
375 }
376 }
377 }
378
379 fn skip_to_semicolon(&mut self) {
380 loop {
381 match self.peek() {
382 Token::Eof | Token::Semicolon => {
383 if matches!(self.peek(), Token::Semicolon) {
384 self.advance();
385 }
386 return;
387 }
388 _ => {
389 self.advance();
390 }
391 }
392 }
393 }
394
395 // -- Style rules --------------------------------------------------------
396
397 fn parse_style_rule(&mut self) -> Option<StyleRule> {
398 let selectors = self.parse_selector_list();
399
400 // Expect `{`
401 self.skip_whitespace();
402 if !matches!(self.peek(), Token::LeftBrace) {
403 // Error recovery: skip to end of block or next rule
404 self.skip_at_rule_body();
405 return None;
406 }
407 self.advance();
408
409 let declarations = self.parse_declaration_list_until_brace();
410
411 // Consume `}`
412 if matches!(self.peek(), Token::RightBrace) {
413 self.advance();
414 }
415
416 if selectors.selectors.is_empty() {
417 return None;
418 }
419
420 Some(StyleRule {
421 selectors,
422 declarations,
423 })
424 }
425
426 // -- Selector parsing ---------------------------------------------------
427
428 fn parse_selector_list(&mut self) -> SelectorList {
429 let mut selectors = Vec::new();
430 self.skip_whitespace();
431
432 if let Some(sel) = self.parse_selector() {
433 selectors.push(sel);
434 }
435
436 loop {
437 self.skip_whitespace();
438 if !matches!(self.peek(), Token::Comma) {
439 break;
440 }
441 self.advance(); // consume comma
442 self.skip_whitespace();
443 if let Some(sel) = self.parse_selector() {
444 selectors.push(sel);
445 }
446 }
447
448 SelectorList { selectors }
449 }
450
451 fn parse_selector(&mut self) -> Option<Selector> {
452 let mut components = Vec::new();
453 let mut last_was_compound = false;
454 let mut had_whitespace = false;
455
456 loop {
457 // Check for end of selector
458 match self.peek() {
459 Token::Comma
460 | Token::LeftBrace
461 | Token::RightBrace
462 | Token::Semicolon
463 | Token::Eof => break,
464 Token::Whitespace => {
465 had_whitespace = true;
466 self.advance();
467 continue;
468 }
469 _ => {}
470 }
471
472 // Check for explicit combinator
473 let combinator = match self.peek() {
474 Token::Delim('>') => {
475 self.advance();
476 Some(Combinator::Child)
477 }
478 Token::Delim('+') => {
479 self.advance();
480 Some(Combinator::AdjacentSibling)
481 }
482 Token::Delim('~') => {
483 self.advance();
484 Some(Combinator::GeneralSibling)
485 }
486 _ => None,
487 };
488
489 if let Some(comb) = combinator {
490 if last_was_compound {
491 components.push(SelectorComponent::Combinator(comb));
492 last_was_compound = false;
493 had_whitespace = false;
494 }
495 self.skip_whitespace();
496 continue;
497 }
498
499 // Implicit descendant combinator if there was whitespace
500 if had_whitespace && last_was_compound {
501 components.push(SelectorComponent::Combinator(Combinator::Descendant));
502 had_whitespace = false;
503 }
504
505 // Parse compound selector
506 if let Some(compound) = self.parse_compound_selector() {
507 components.push(SelectorComponent::Compound(compound));
508 last_was_compound = true;
509 } else {
510 break;
511 }
512 }
513
514 if components.is_empty() {
515 None
516 } else {
517 Some(Selector { components })
518 }
519 }
520
521 fn parse_compound_selector(&mut self) -> Option<CompoundSelector> {
522 let mut simple = Vec::new();
523
524 loop {
525 match self.peek() {
526 // Type or universal
527 Token::Ident(_) if simple.is_empty() || !has_type_or_universal(&simple) => {
528 if let Token::Ident(name) = self.advance() {
529 simple.push(SimpleSelector::Type(name.to_ascii_lowercase()));
530 }
531 }
532 Token::Delim('*') if simple.is_empty() || !has_type_or_universal(&simple) => {
533 self.advance();
534 simple.push(SimpleSelector::Universal);
535 }
536 // Class
537 Token::Delim('.') => {
538 self.advance();
539 match self.advance() {
540 Token::Ident(name) => simple.push(SimpleSelector::Class(name)),
541 _ => break,
542 }
543 }
544 // ID
545 Token::Hash(_, HashType::Id) => {
546 if let Token::Hash(name, _) = self.advance() {
547 simple.push(SimpleSelector::Id(name));
548 }
549 }
550 // Attribute
551 Token::LeftBracket => {
552 self.advance();
553 if let Some(attr) = self.parse_attribute_selector() {
554 simple.push(SimpleSelector::Attribute(attr));
555 }
556 }
557 // Pseudo-class
558 Token::Colon => {
559 self.advance();
560 match self.advance() {
561 Token::Ident(name) => {
562 simple.push(SimpleSelector::PseudoClass(name.to_ascii_lowercase()))
563 }
564 Token::Function(name) => {
565 // Parse pseudo-class with arguments: skip to closing paren
566 let mut pname = name.to_ascii_lowercase();
567 pname.push('(');
568 let mut depth = 1;
569 loop {
570 match self.peek() {
571 Token::Eof => break,
572 Token::LeftParen => {
573 depth += 1;
574 pname.push('(');
575 self.advance();
576 }
577 Token::RightParen => {
578 depth -= 1;
579 if depth == 0 {
580 self.advance();
581 break;
582 }
583 pname.push(')');
584 self.advance();
585 }
586 _ => {
587 let tok = self.advance();
588 pname.push_str(&token_to_string(&tok));
589 }
590 }
591 }
592 pname.push(')');
593 simple.push(SimpleSelector::PseudoClass(pname));
594 }
595 _ => break,
596 }
597 }
598 // Ident after already having a type selector → new compound starts
599 Token::Ident(_) => break,
600 _ => break,
601 }
602 }
603
604 if simple.is_empty() {
605 None
606 } else {
607 Some(CompoundSelector { simple })
608 }
609 }
610
611 fn parse_attribute_selector(&mut self) -> Option<AttributeSelector> {
612 self.skip_whitespace();
613
614 let name = match self.advance() {
615 Token::Ident(name) => name.to_ascii_lowercase(),
616 _ => {
617 self.skip_to_bracket_close();
618 return None;
619 }
620 };
621
622 self.skip_whitespace();
623
624 // Check for close bracket (presence-only selector)
625 if matches!(self.peek(), Token::RightBracket) {
626 self.advance();
627 return Some(AttributeSelector {
628 name,
629 op: None,
630 value: None,
631 });
632 }
633
634 // Parse operator
635 let op = match self.peek() {
636 Token::Delim('=') => {
637 self.advance();
638 AttributeOp::Exact
639 }
640 Token::Delim('~') => {
641 self.advance();
642 if matches!(self.peek(), Token::Delim('=')) {
643 self.advance();
644 }
645 AttributeOp::Includes
646 }
647 Token::Delim('|') => {
648 self.advance();
649 if matches!(self.peek(), Token::Delim('=')) {
650 self.advance();
651 }
652 AttributeOp::DashMatch
653 }
654 Token::Delim('^') => {
655 self.advance();
656 if matches!(self.peek(), Token::Delim('=')) {
657 self.advance();
658 }
659 AttributeOp::Prefix
660 }
661 Token::Delim('$') => {
662 self.advance();
663 if matches!(self.peek(), Token::Delim('=')) {
664 self.advance();
665 }
666 AttributeOp::Suffix
667 }
668 Token::Delim('*') => {
669 self.advance();
670 if matches!(self.peek(), Token::Delim('=')) {
671 self.advance();
672 }
673 AttributeOp::Substring
674 }
675 _ => {
676 self.skip_to_bracket_close();
677 return None;
678 }
679 };
680
681 self.skip_whitespace();
682
683 // Parse value
684 let value = match self.advance() {
685 Token::Ident(v) => v,
686 Token::String(v) => v,
687 _ => {
688 self.skip_to_bracket_close();
689 return None;
690 }
691 };
692
693 self.skip_whitespace();
694
695 // Consume closing bracket
696 if matches!(self.peek(), Token::RightBracket) {
697 self.advance();
698 }
699
700 Some(AttributeSelector {
701 name,
702 op: Some(op),
703 value: Some(value),
704 })
705 }
706
707 fn skip_to_bracket_close(&mut self) {
708 loop {
709 match self.peek() {
710 Token::RightBracket | Token::Eof => {
711 if matches!(self.peek(), Token::RightBracket) {
712 self.advance();
713 }
714 return;
715 }
716 _ => {
717 self.advance();
718 }
719 }
720 }
721 }
722
723 // -- Declaration parsing ------------------------------------------------
724
725 fn parse_declaration_list(&mut self) -> Vec<Declaration> {
726 let mut declarations = Vec::new();
727 loop {
728 self.skip_whitespace();
729 if self.is_eof() {
730 break;
731 }
732 if matches!(self.peek(), Token::Semicolon) {
733 self.advance();
734 continue;
735 }
736 if let Some(decl) = self.parse_declaration() {
737 declarations.push(decl);
738 }
739 }
740 declarations
741 }
742
743 fn parse_declaration_list_until_brace(&mut self) -> Vec<Declaration> {
744 let mut declarations = Vec::new();
745 loop {
746 self.skip_whitespace();
747 if self.is_eof() || matches!(self.peek(), Token::RightBrace) {
748 break;
749 }
750 if matches!(self.peek(), Token::Semicolon) {
751 self.advance();
752 continue;
753 }
754 if let Some(decl) = self.parse_declaration() {
755 declarations.push(decl);
756 }
757 }
758 declarations
759 }
760
761 fn parse_declaration(&mut self) -> Option<Declaration> {
762 // Property name
763 let property = match self.peek() {
764 Token::Ident(_) => {
765 if let Token::Ident(name) = self.advance() {
766 name.to_ascii_lowercase()
767 } else {
768 unreachable!()
769 }
770 }
771 _ => {
772 // Error recovery: skip to next semicolon or closing brace
773 self.skip_declaration_error();
774 return None;
775 }
776 };
777
778 self.skip_whitespace();
779
780 // Expect colon
781 if !matches!(self.peek(), Token::Colon) {
782 self.skip_declaration_error();
783 return None;
784 }
785 self.advance();
786
787 self.skip_whitespace();
788
789 // Parse value
790 let (value, important) = self.parse_declaration_value();
791
792 if value.is_empty() {
793 return None;
794 }
795
796 Some(Declaration {
797 property,
798 value,
799 important,
800 })
801 }
802
803 fn parse_declaration_value(&mut self) -> (Vec<ComponentValue>, bool) {
804 let mut values = Vec::new();
805 let mut important = false;
806
807 loop {
808 match self.peek() {
809 Token::Semicolon | Token::RightBrace | Token::Eof => break,
810 Token::Whitespace => {
811 self.advance();
812 // Only add whitespace if there are already values and we're
813 // not at the end of the declaration
814 if !values.is_empty()
815 && !matches!(
816 self.peek(),
817 Token::Semicolon | Token::RightBrace | Token::Eof
818 )
819 {
820 values.push(ComponentValue::Whitespace);
821 }
822 }
823 Token::Delim('!') => {
824 self.advance();
825 self.skip_whitespace();
826 if let Token::Ident(ref s) = self.peek() {
827 if s.eq_ignore_ascii_case("important") {
828 important = true;
829 self.advance();
830 }
831 }
832 }
833 _ => {
834 if let Some(cv) = self.parse_component_value() {
835 values.push(cv);
836 }
837 }
838 }
839 }
840
841 // Trim trailing whitespace
842 while matches!(values.last(), Some(ComponentValue::Whitespace)) {
843 values.pop();
844 }
845
846 (values, important)
847 }
848
849 fn parse_component_value(&mut self) -> Option<ComponentValue> {
850 match self.advance() {
851 Token::Ident(s) => Some(ComponentValue::Ident(s)),
852 Token::String(s) => Some(ComponentValue::String(s)),
853 Token::Number(n, t) => Some(ComponentValue::Number(n, t)),
854 Token::Percentage(n) => Some(ComponentValue::Percentage(n)),
855 Token::Dimension(n, t, u) => Some(ComponentValue::Dimension(n, t, u)),
856 Token::Hash(s, ht) => Some(ComponentValue::Hash(s, ht)),
857 Token::Comma => Some(ComponentValue::Comma),
858 Token::Delim(c) => Some(ComponentValue::Delim(c)),
859 Token::Function(name) => {
860 let args = self.parse_function_args();
861 Some(ComponentValue::Function(name, args))
862 }
863 _ => None,
864 }
865 }
866
867 fn parse_function_args(&mut self) -> Vec<ComponentValue> {
868 let mut args = Vec::new();
869 loop {
870 match self.peek() {
871 Token::RightParen | Token::Eof => {
872 if matches!(self.peek(), Token::RightParen) {
873 self.advance();
874 }
875 break;
876 }
877 Token::Whitespace => {
878 self.advance();
879 if !args.is_empty() && !matches!(self.peek(), Token::RightParen | Token::Eof) {
880 args.push(ComponentValue::Whitespace);
881 }
882 }
883 _ => {
884 if let Some(cv) = self.parse_component_value() {
885 args.push(cv);
886 }
887 }
888 }
889 }
890 args
891 }
892
893 fn skip_declaration_error(&mut self) {
894 loop {
895 match self.peek() {
896 Token::Semicolon => {
897 self.advance();
898 return;
899 }
900 Token::RightBrace | Token::Eof => return,
901 _ => {
902 self.advance();
903 }
904 }
905 }
906 }
907}
908
909// ---------------------------------------------------------------------------
910// Helpers
911// ---------------------------------------------------------------------------
912
913fn has_type_or_universal(selectors: &[SimpleSelector]) -> bool {
914 selectors
915 .iter()
916 .any(|s| matches!(s, SimpleSelector::Type(_) | SimpleSelector::Universal))
917}
918
919fn token_to_string(token: &Token) -> String {
920 match token {
921 Token::Ident(s) => s.clone(),
922 Token::Function(s) => format!("{s}("),
923 Token::AtKeyword(s) => format!("@{s}"),
924 Token::Hash(s, _) => format!("#{s}"),
925 Token::String(s) => format!("\"{s}\""),
926 Token::Url(s) => format!("url({s})"),
927 Token::Number(n, _) => format!("{n}"),
928 Token::Percentage(n) => format!("{n}%"),
929 Token::Dimension(n, _, u) => format!("{n}{u}"),
930 Token::Whitespace => " ".to_string(),
931 Token::Colon => ":".to_string(),
932 Token::Semicolon => ";".to_string(),
933 Token::Comma => ",".to_string(),
934 Token::LeftBracket => "[".to_string(),
935 Token::RightBracket => "]".to_string(),
936 Token::LeftParen => "(".to_string(),
937 Token::RightParen => ")".to_string(),
938 Token::LeftBrace => "{".to_string(),
939 Token::RightBrace => "}".to_string(),
940 Token::Delim(c) => c.to_string(),
941 Token::Cdo => "<!--".to_string(),
942 Token::Cdc => "-->".to_string(),
943 Token::BadString | Token::BadUrl | Token::Eof => String::new(),
944 }
945}
946
947// ---------------------------------------------------------------------------
948// Tests
949// ---------------------------------------------------------------------------
950
951#[cfg(test)]
952mod tests {
953 use super::*;
954
955 // -- Selector tests -----------------------------------------------------
956
957 #[test]
958 fn test_type_selector() {
959 let ss = Parser::parse("div { }");
960 assert_eq!(ss.rules.len(), 1);
961 let rule = match &ss.rules[0] {
962 Rule::Style(r) => r,
963 _ => panic!("expected style rule"),
964 };
965 assert_eq!(rule.selectors.selectors.len(), 1);
966 let sel = &rule.selectors.selectors[0];
967 assert_eq!(sel.components.len(), 1);
968 match &sel.components[0] {
969 SelectorComponent::Compound(c) => {
970 assert_eq!(c.simple.len(), 1);
971 assert_eq!(c.simple[0], SimpleSelector::Type("div".into()));
972 }
973 _ => panic!("expected compound"),
974 }
975 }
976
977 #[test]
978 fn test_universal_selector() {
979 let ss = Parser::parse("* { }");
980 let rule = match &ss.rules[0] {
981 Rule::Style(r) => r,
982 _ => panic!("expected style rule"),
983 };
984 let sel = &rule.selectors.selectors[0];
985 match &sel.components[0] {
986 SelectorComponent::Compound(c) => {
987 assert_eq!(c.simple[0], SimpleSelector::Universal);
988 }
989 _ => panic!("expected compound"),
990 }
991 }
992
993 #[test]
994 fn test_class_selector() {
995 let ss = Parser::parse(".foo { }");
996 let rule = match &ss.rules[0] {
997 Rule::Style(r) => r,
998 _ => panic!("expected style rule"),
999 };
1000 let sel = &rule.selectors.selectors[0];
1001 match &sel.components[0] {
1002 SelectorComponent::Compound(c) => {
1003 assert_eq!(c.simple[0], SimpleSelector::Class("foo".into()));
1004 }
1005 _ => panic!("expected compound"),
1006 }
1007 }
1008
1009 #[test]
1010 fn test_id_selector() {
1011 let ss = Parser::parse("#main { }");
1012 let rule = match &ss.rules[0] {
1013 Rule::Style(r) => r,
1014 _ => panic!("expected style rule"),
1015 };
1016 let sel = &rule.selectors.selectors[0];
1017 match &sel.components[0] {
1018 SelectorComponent::Compound(c) => {
1019 assert_eq!(c.simple[0], SimpleSelector::Id("main".into()));
1020 }
1021 _ => panic!("expected compound"),
1022 }
1023 }
1024
1025 #[test]
1026 fn test_compound_selector() {
1027 let ss = Parser::parse("div.foo#bar { }");
1028 let rule = match &ss.rules[0] {
1029 Rule::Style(r) => r,
1030 _ => panic!("expected style rule"),
1031 };
1032 let sel = &rule.selectors.selectors[0];
1033 match &sel.components[0] {
1034 SelectorComponent::Compound(c) => {
1035 assert_eq!(c.simple.len(), 3);
1036 assert_eq!(c.simple[0], SimpleSelector::Type("div".into()));
1037 assert_eq!(c.simple[1], SimpleSelector::Class("foo".into()));
1038 assert_eq!(c.simple[2], SimpleSelector::Id("bar".into()));
1039 }
1040 _ => panic!("expected compound"),
1041 }
1042 }
1043
1044 #[test]
1045 fn test_selector_list() {
1046 let ss = Parser::parse("h1, h2, h3 { }");
1047 let rule = match &ss.rules[0] {
1048 Rule::Style(r) => r,
1049 _ => panic!("expected style rule"),
1050 };
1051 assert_eq!(rule.selectors.selectors.len(), 3);
1052 }
1053
1054 #[test]
1055 fn test_descendant_combinator() {
1056 let ss = Parser::parse("div p { }");
1057 let rule = match &ss.rules[0] {
1058 Rule::Style(r) => r,
1059 _ => panic!("expected style rule"),
1060 };
1061 let sel = &rule.selectors.selectors[0];
1062 assert_eq!(sel.components.len(), 3);
1063 assert!(matches!(
1064 sel.components[1],
1065 SelectorComponent::Combinator(Combinator::Descendant)
1066 ));
1067 }
1068
1069 #[test]
1070 fn test_child_combinator() {
1071 let ss = Parser::parse("div > p { }");
1072 let rule = match &ss.rules[0] {
1073 Rule::Style(r) => r,
1074 _ => panic!("expected style rule"),
1075 };
1076 let sel = &rule.selectors.selectors[0];
1077 assert_eq!(sel.components.len(), 3);
1078 assert!(matches!(
1079 sel.components[1],
1080 SelectorComponent::Combinator(Combinator::Child)
1081 ));
1082 }
1083
1084 #[test]
1085 fn test_adjacent_sibling_combinator() {
1086 let ss = Parser::parse("h1 + p { }");
1087 let rule = match &ss.rules[0] {
1088 Rule::Style(r) => r,
1089 _ => panic!("expected style rule"),
1090 };
1091 let sel = &rule.selectors.selectors[0];
1092 assert!(matches!(
1093 sel.components[1],
1094 SelectorComponent::Combinator(Combinator::AdjacentSibling)
1095 ));
1096 }
1097
1098 #[test]
1099 fn test_general_sibling_combinator() {
1100 let ss = Parser::parse("h1 ~ p { }");
1101 let rule = match &ss.rules[0] {
1102 Rule::Style(r) => r,
1103 _ => panic!("expected style rule"),
1104 };
1105 let sel = &rule.selectors.selectors[0];
1106 assert!(matches!(
1107 sel.components[1],
1108 SelectorComponent::Combinator(Combinator::GeneralSibling)
1109 ));
1110 }
1111
1112 #[test]
1113 fn test_attribute_presence() {
1114 let ss = Parser::parse("[disabled] { }");
1115 let rule = match &ss.rules[0] {
1116 Rule::Style(r) => r,
1117 _ => panic!("expected style rule"),
1118 };
1119 let sel = &rule.selectors.selectors[0];
1120 match &sel.components[0] {
1121 SelectorComponent::Compound(c) => match &c.simple[0] {
1122 SimpleSelector::Attribute(attr) => {
1123 assert_eq!(attr.name, "disabled");
1124 assert!(attr.op.is_none());
1125 assert!(attr.value.is_none());
1126 }
1127 _ => panic!("expected attribute selector"),
1128 },
1129 _ => panic!("expected compound"),
1130 }
1131 }
1132
1133 #[test]
1134 fn test_attribute_exact() {
1135 let ss = Parser::parse("[type=\"text\"] { }");
1136 let rule = match &ss.rules[0] {
1137 Rule::Style(r) => r,
1138 _ => panic!("expected style rule"),
1139 };
1140 let sel = &rule.selectors.selectors[0];
1141 match &sel.components[0] {
1142 SelectorComponent::Compound(c) => match &c.simple[0] {
1143 SimpleSelector::Attribute(attr) => {
1144 assert_eq!(attr.name, "type");
1145 assert_eq!(attr.op, Some(AttributeOp::Exact));
1146 assert_eq!(attr.value, Some("text".into()));
1147 }
1148 _ => panic!("expected attribute selector"),
1149 },
1150 _ => panic!("expected compound"),
1151 }
1152 }
1153
1154 #[test]
1155 fn test_attribute_operators() {
1156 let ops = vec![
1157 ("[a~=b] { }", AttributeOp::Includes),
1158 ("[a|=b] { }", AttributeOp::DashMatch),
1159 ("[a^=b] { }", AttributeOp::Prefix),
1160 ("[a$=b] { }", AttributeOp::Suffix),
1161 ("[a*=b] { }", AttributeOp::Substring),
1162 ];
1163 for (input, expected_op) in ops {
1164 let ss = Parser::parse(input);
1165 let rule = match &ss.rules[0] {
1166 Rule::Style(r) => r,
1167 _ => panic!("expected style rule"),
1168 };
1169 let sel = &rule.selectors.selectors[0];
1170 match &sel.components[0] {
1171 SelectorComponent::Compound(c) => match &c.simple[0] {
1172 SimpleSelector::Attribute(attr) => {
1173 assert_eq!(attr.op, Some(expected_op), "failed for {input}");
1174 }
1175 _ => panic!("expected attribute selector"),
1176 },
1177 _ => panic!("expected compound"),
1178 }
1179 }
1180 }
1181
1182 #[test]
1183 fn test_pseudo_class() {
1184 let ss = Parser::parse("a:hover { }");
1185 let rule = match &ss.rules[0] {
1186 Rule::Style(r) => r,
1187 _ => panic!("expected style rule"),
1188 };
1189 let sel = &rule.selectors.selectors[0];
1190 match &sel.components[0] {
1191 SelectorComponent::Compound(c) => {
1192 assert_eq!(c.simple.len(), 2);
1193 assert_eq!(c.simple[0], SimpleSelector::Type("a".into()));
1194 assert_eq!(c.simple[1], SimpleSelector::PseudoClass("hover".into()));
1195 }
1196 _ => panic!("expected compound"),
1197 }
1198 }
1199
1200 #[test]
1201 fn test_pseudo_class_first_child() {
1202 let ss = Parser::parse("p:first-child { }");
1203 let rule = match &ss.rules[0] {
1204 Rule::Style(r) => r,
1205 _ => panic!("expected style rule"),
1206 };
1207 let sel = &rule.selectors.selectors[0];
1208 match &sel.components[0] {
1209 SelectorComponent::Compound(c) => {
1210 assert_eq!(
1211 c.simple[1],
1212 SimpleSelector::PseudoClass("first-child".into())
1213 );
1214 }
1215 _ => panic!("expected compound"),
1216 }
1217 }
1218
1219 // -- Declaration tests --------------------------------------------------
1220
1221 #[test]
1222 fn test_simple_declaration() {
1223 let ss = Parser::parse("p { color: red; }");
1224 let rule = match &ss.rules[0] {
1225 Rule::Style(r) => r,
1226 _ => panic!("expected style rule"),
1227 };
1228 assert_eq!(rule.declarations.len(), 1);
1229 assert_eq!(rule.declarations[0].property, "color");
1230 assert_eq!(rule.declarations[0].value.len(), 1);
1231 assert_eq!(
1232 rule.declarations[0].value[0],
1233 ComponentValue::Ident("red".into())
1234 );
1235 assert!(!rule.declarations[0].important);
1236 }
1237
1238 #[test]
1239 fn test_multiple_declarations() {
1240 let ss = Parser::parse("p { color: red; font-size: 16px; }");
1241 let rule = match &ss.rules[0] {
1242 Rule::Style(r) => r,
1243 _ => panic!("expected style rule"),
1244 };
1245 assert_eq!(rule.declarations.len(), 2);
1246 assert_eq!(rule.declarations[0].property, "color");
1247 assert_eq!(rule.declarations[1].property, "font-size");
1248 }
1249
1250 #[test]
1251 fn test_important_declaration() {
1252 let ss = Parser::parse("p { color: red !important; }");
1253 let rule = match &ss.rules[0] {
1254 Rule::Style(r) => r,
1255 _ => panic!("expected style rule"),
1256 };
1257 assert!(rule.declarations[0].important);
1258 }
1259
1260 #[test]
1261 fn test_declaration_with_function() {
1262 let ss = Parser::parse("p { color: rgb(255, 0, 0); }");
1263 let rule = match &ss.rules[0] {
1264 Rule::Style(r) => r,
1265 _ => panic!("expected style rule"),
1266 };
1267 assert_eq!(rule.declarations[0].property, "color");
1268 match &rule.declarations[0].value[0] {
1269 ComponentValue::Function(name, args) => {
1270 assert_eq!(name, "rgb");
1271 // 255, 0, 0 → Number, Comma, WS, Number, Comma, WS, Number
1272 assert!(!args.is_empty());
1273 }
1274 _ => panic!("expected function value"),
1275 }
1276 }
1277
1278 #[test]
1279 fn test_declaration_with_hash_color() {
1280 let ss = Parser::parse("p { color: #ff0000; }");
1281 let rule = match &ss.rules[0] {
1282 Rule::Style(r) => r,
1283 _ => panic!("expected style rule"),
1284 };
1285 assert_eq!(
1286 rule.declarations[0].value[0],
1287 ComponentValue::Hash("ff0000".into(), HashType::Id)
1288 );
1289 }
1290
1291 #[test]
1292 fn test_declaration_with_dimension() {
1293 let ss = Parser::parse("p { margin: 10px; }");
1294 let rule = match &ss.rules[0] {
1295 Rule::Style(r) => r,
1296 _ => panic!("expected style rule"),
1297 };
1298 assert_eq!(
1299 rule.declarations[0].value[0],
1300 ComponentValue::Dimension(10.0, NumericType::Integer, "px".into())
1301 );
1302 }
1303
1304 #[test]
1305 fn test_declaration_with_percentage() {
1306 let ss = Parser::parse("p { width: 50%; }");
1307 let rule = match &ss.rules[0] {
1308 Rule::Style(r) => r,
1309 _ => panic!("expected style rule"),
1310 };
1311 assert_eq!(
1312 rule.declarations[0].value[0],
1313 ComponentValue::Percentage(50.0)
1314 );
1315 }
1316
1317 #[test]
1318 fn test_parse_inline_style() {
1319 let decls = Parser::parse_declarations("color: red; font-size: 16px");
1320 assert_eq!(decls.len(), 2);
1321 assert_eq!(decls[0].property, "color");
1322 assert_eq!(decls[1].property, "font-size");
1323 }
1324
1325 // -- Error recovery tests -----------------------------------------------
1326
1327 #[test]
1328 fn test_invalid_declaration_skipped() {
1329 let ss = Parser::parse("p { ??? ; color: red; }");
1330 let rule = match &ss.rules[0] {
1331 Rule::Style(r) => r,
1332 _ => panic!("expected style rule"),
1333 };
1334 // The invalid declaration should be skipped, color should remain
1335 assert_eq!(rule.declarations.len(), 1);
1336 assert_eq!(rule.declarations[0].property, "color");
1337 }
1338
1339 #[test]
1340 fn test_missing_colon_skipped() {
1341 let ss = Parser::parse("p { color red; font-size: 16px; }");
1342 let rule = match &ss.rules[0] {
1343 Rule::Style(r) => r,
1344 _ => panic!("expected style rule"),
1345 };
1346 assert_eq!(rule.declarations.len(), 1);
1347 assert_eq!(rule.declarations[0].property, "font-size");
1348 }
1349
1350 #[test]
1351 fn test_empty_stylesheet() {
1352 let ss = Parser::parse("");
1353 assert_eq!(ss.rules.len(), 0);
1354 }
1355
1356 #[test]
1357 fn test_empty_rule() {
1358 let ss = Parser::parse("p { }");
1359 let rule = match &ss.rules[0] {
1360 Rule::Style(r) => r,
1361 _ => panic!("expected style rule"),
1362 };
1363 assert_eq!(rule.declarations.len(), 0);
1364 }
1365
1366 #[test]
1367 fn test_multiple_rules() {
1368 let ss = Parser::parse("h1 { color: blue; } p { color: red; }");
1369 assert_eq!(ss.rules.len(), 2);
1370 }
1371
1372 // -- @-rule tests -------------------------------------------------------
1373
1374 #[test]
1375 fn test_import_rule_string() {
1376 let ss = Parser::parse("@import \"style.css\";");
1377 assert_eq!(ss.rules.len(), 1);
1378 match &ss.rules[0] {
1379 Rule::Import(r) => assert_eq!(r.url, "style.css"),
1380 _ => panic!("expected import rule"),
1381 }
1382 }
1383
1384 #[test]
1385 fn test_import_rule_url() {
1386 let ss = Parser::parse("@import url(style.css);");
1387 assert_eq!(ss.rules.len(), 1);
1388 match &ss.rules[0] {
1389 Rule::Import(r) => assert_eq!(r.url, "style.css"),
1390 _ => panic!("expected import rule"),
1391 }
1392 }
1393
1394 #[test]
1395 fn test_media_rule() {
1396 let ss = Parser::parse("@media screen { p { color: red; } }");
1397 assert_eq!(ss.rules.len(), 1);
1398 match &ss.rules[0] {
1399 Rule::Media(m) => {
1400 assert_eq!(m.query, "screen");
1401 assert_eq!(m.rules.len(), 1);
1402 }
1403 _ => panic!("expected media rule"),
1404 }
1405 }
1406
1407 #[test]
1408 fn test_media_rule_complex_query() {
1409 let ss = Parser::parse("@media screen and (max-width: 600px) { p { font-size: 14px; } }");
1410 match &ss.rules[0] {
1411 Rule::Media(m) => {
1412 assert!(m.query.contains("screen"));
1413 assert!(m.query.contains("max-width"));
1414 assert_eq!(m.rules.len(), 1);
1415 }
1416 _ => panic!("expected media rule"),
1417 }
1418 }
1419
1420 #[test]
1421 fn test_unknown_at_rule_skipped() {
1422 let ss = Parser::parse("@charset \"UTF-8\"; p { color: red; }");
1423 assert_eq!(ss.rules.len(), 1);
1424 match &ss.rules[0] {
1425 Rule::Style(r) => assert_eq!(r.declarations[0].property, "color"),
1426 _ => panic!("expected style rule"),
1427 }
1428 }
1429
1430 // -- Integration tests --------------------------------------------------
1431
1432 #[test]
1433 fn test_real_css() {
1434 let css = r#"
1435 body {
1436 margin: 0;
1437 font-family: sans-serif;
1438 background-color: #fff;
1439 }
1440 h1 {
1441 color: #333;
1442 font-size: 24px;
1443 }
1444 .container {
1445 max-width: 960px;
1446 margin: 0 auto;
1447 }
1448 a:hover {
1449 color: blue;
1450 text-decoration: underline;
1451 }
1452 "#;
1453 let ss = Parser::parse(css);
1454 assert_eq!(ss.rules.len(), 4);
1455 }
1456
1457 #[test]
1458 fn test_declaration_no_trailing_semicolon() {
1459 let ss = Parser::parse("p { color: red }");
1460 let rule = match &ss.rules[0] {
1461 Rule::Style(r) => r,
1462 _ => panic!("expected style rule"),
1463 };
1464 assert_eq!(rule.declarations.len(), 1);
1465 assert_eq!(rule.declarations[0].property, "color");
1466 }
1467
1468 #[test]
1469 fn test_multi_value_declaration() {
1470 let ss = Parser::parse("p { margin: 10px 20px 30px 40px; }");
1471 let rule = match &ss.rules[0] {
1472 Rule::Style(r) => r,
1473 _ => panic!("expected style rule"),
1474 };
1475 // 10px WS 20px WS 30px WS 40px
1476 assert_eq!(rule.declarations[0].value.len(), 7);
1477 }
1478
1479 #[test]
1480 fn test_cdo_cdc_ignored() {
1481 let ss = Parser::parse("<!-- p { color: red; } -->");
1482 assert_eq!(ss.rules.len(), 1);
1483 }
1484}