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