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::animations::{Keyframe, KeyframeSelector};
6use crate::media::{parse_media_query_list, MediaQueryList};
7use crate::tokenizer::{HashType, NumericType, Token, Tokenizer};
8
9// ---------------------------------------------------------------------------
10// AST types
11// ---------------------------------------------------------------------------
12
13/// A parsed CSS stylesheet: a list of rules.
14#[derive(Debug, Clone, PartialEq)]
15pub struct Stylesheet {
16 pub rules: Vec<Rule>,
17}
18
19/// A top-level rule: either a style rule or an at-rule.
20#[derive(Debug, Clone, PartialEq)]
21pub enum Rule {
22 Style(StyleRule),
23 Media(MediaRule),
24 Import(ImportRule),
25 Keyframes(KeyframesRule),
26 FontFace(FontFaceRule),
27}
28
29/// A `@keyframes` rule with a name and a list of keyframes.
30#[derive(Debug, Clone, PartialEq)]
31pub struct KeyframesRule {
32 pub name: String,
33 pub keyframes: Vec<Keyframe>,
34}
35
36/// A style rule: selector list + declarations.
37#[derive(Debug, Clone, PartialEq)]
38pub struct StyleRule {
39 pub selectors: SelectorList,
40 pub declarations: Vec<Declaration>,
41}
42
43/// A `@media` rule with parsed media queries and nested rules.
44#[derive(Debug, Clone, PartialEq)]
45pub struct MediaRule {
46 pub queries: MediaQueryList,
47 pub rules: Vec<Rule>,
48}
49
50/// An `@import` rule with a URL.
51#[derive(Debug, Clone, PartialEq)]
52pub struct ImportRule {
53 pub url: String,
54}
55
56/// A `@font-face` rule describing a web font.
57#[derive(Debug, Clone, PartialEq)]
58pub struct FontFaceRule {
59 /// The font family name declared for this font face.
60 pub family: String,
61 /// `src` descriptor: list of font sources (currently only `url()` references).
62 pub sources: Vec<FontFaceSource>,
63 /// `font-weight`: numeric weight (100–900). Defaults to 400.
64 pub weight: u16,
65 /// `font-style`: normal, italic, or oblique.
66 pub style: FontFaceStyle,
67 /// `font-display`: controls rendering behavior during font load.
68 pub display: FontDisplay,
69}
70
71/// A single source in a `@font-face` `src` descriptor.
72#[derive(Debug, Clone, PartialEq)]
73pub struct FontFaceSource {
74 /// URL to the font file.
75 pub url: String,
76 /// Optional `format()` hint (e.g., "truetype", "opentype", "woff2").
77 pub format: Option<String>,
78}
79
80/// `font-style` descriptor values for `@font-face`.
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub enum FontFaceStyle {
83 Normal,
84 Italic,
85 Oblique,
86}
87
88/// `font-display` descriptor values for `@font-face`.
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub enum FontDisplay {
91 Auto,
92 Block,
93 Swap,
94 Fallback,
95 Optional,
96}
97
98/// A comma-separated list of selectors.
99#[derive(Debug, Clone, PartialEq)]
100pub struct SelectorList {
101 pub selectors: Vec<Selector>,
102}
103
104/// A complex selector: a chain of compound selectors joined by combinators.
105#[derive(Debug, Clone, PartialEq)]
106pub struct Selector {
107 pub components: Vec<SelectorComponent>,
108}
109
110/// Either a compound selector or a combinator between compounds.
111#[derive(Debug, Clone, PartialEq)]
112pub enum SelectorComponent {
113 Compound(CompoundSelector),
114 Combinator(Combinator),
115}
116
117/// A compound selector: a sequence of simple selectors with no combinator.
118#[derive(Debug, Clone, PartialEq)]
119pub struct CompoundSelector {
120 pub simple: Vec<SimpleSelector>,
121}
122
123/// A simple selector.
124#[derive(Debug, Clone, PartialEq)]
125pub enum SimpleSelector {
126 Type(String),
127 Universal,
128 Class(String),
129 Id(String),
130 Attribute(AttributeSelector),
131 PseudoClass(String),
132}
133
134/// An attribute selector.
135#[derive(Debug, Clone, PartialEq)]
136pub struct AttributeSelector {
137 pub name: String,
138 pub op: Option<AttributeOp>,
139 pub value: Option<String>,
140}
141
142/// Attribute matching operator.
143#[derive(Debug, Clone, Copy, PartialEq, Eq)]
144pub enum AttributeOp {
145 /// `=`
146 Exact,
147 /// `~=`
148 Includes,
149 /// `|=`
150 DashMatch,
151 /// `^=`
152 Prefix,
153 /// `$=`
154 Suffix,
155 /// `*=`
156 Substring,
157}
158
159/// A combinator between compound selectors.
160#[derive(Debug, Clone, Copy, PartialEq, Eq)]
161pub enum Combinator {
162 /// Descendant (whitespace)
163 Descendant,
164 /// Child (`>`)
165 Child,
166 /// Adjacent sibling (`+`)
167 AdjacentSibling,
168 /// General sibling (`~`)
169 GeneralSibling,
170}
171
172/// A CSS property declaration.
173#[derive(Debug, Clone, PartialEq)]
174pub struct Declaration {
175 pub property: String,
176 pub value: Vec<ComponentValue>,
177 pub important: bool,
178}
179
180/// A component value in a declaration value. We store these as typed tokens
181/// so downstream code can interpret them without re-tokenizing.
182#[derive(Debug, Clone, PartialEq)]
183pub enum ComponentValue {
184 Ident(String),
185 String(String),
186 Number(f64, NumericType),
187 Percentage(f64),
188 Dimension(f64, NumericType, String),
189 Hash(String, HashType),
190 Function(String, Vec<ComponentValue>),
191 Delim(char),
192 Comma,
193 Whitespace,
194}
195
196// ---------------------------------------------------------------------------
197// Parser
198// ---------------------------------------------------------------------------
199
200/// CSS parser. Consumes tokens and produces AST.
201pub struct Parser {
202 tokens: Vec<Token>,
203 pos: usize,
204}
205
206impl Parser {
207 /// Parse a full stylesheet from CSS source text.
208 pub fn parse(input: &str) -> Stylesheet {
209 let tokens = Tokenizer::tokenize(input);
210 let mut parser = Self { tokens, pos: 0 };
211 parser.parse_stylesheet()
212 }
213
214 /// Parse a list of declarations (for `style` attribute values).
215 pub fn parse_declarations(input: &str) -> Vec<Declaration> {
216 let tokens = Tokenizer::tokenize(input);
217 let mut parser = Self { tokens, pos: 0 };
218 parser.parse_declaration_list()
219 }
220
221 /// Parse a selector list from a string (for `querySelector`/`querySelectorAll`).
222 pub fn parse_selectors(input: &str) -> SelectorList {
223 let tokens = Tokenizer::tokenize(input);
224 let mut parser = Self { tokens, pos: 0 };
225 parser.parse_selector_list()
226 }
227
228 // -- Token access -------------------------------------------------------
229
230 fn peek(&self) -> &Token {
231 self.tokens.get(self.pos).unwrap_or(&Token::Eof)
232 }
233
234 fn advance(&mut self) -> Token {
235 let tok = self.tokens.get(self.pos).cloned().unwrap_or(Token::Eof);
236 if self.pos < self.tokens.len() {
237 self.pos += 1;
238 }
239 tok
240 }
241
242 fn is_eof(&self) -> bool {
243 self.pos >= self.tokens.len()
244 }
245
246 fn skip_whitespace(&mut self) {
247 while matches!(self.peek(), Token::Whitespace) {
248 self.advance();
249 }
250 }
251
252 // -- Stylesheet parsing -------------------------------------------------
253
254 fn parse_stylesheet(&mut self) -> Stylesheet {
255 let mut rules = Vec::new();
256 loop {
257 self.skip_whitespace();
258 if self.is_eof() {
259 break;
260 }
261 match self.peek() {
262 Token::AtKeyword(_) => {
263 if let Some(rule) = self.parse_at_rule() {
264 rules.push(rule);
265 }
266 }
267 Token::Cdo | Token::Cdc => {
268 self.advance();
269 }
270 _ => {
271 if let Some(rule) = self.parse_style_rule() {
272 rules.push(Rule::Style(rule));
273 }
274 }
275 }
276 }
277 Stylesheet { rules }
278 }
279
280 // -- At-rules -----------------------------------------------------------
281
282 fn parse_at_rule(&mut self) -> Option<Rule> {
283 let name = match self.advance() {
284 Token::AtKeyword(name) => name,
285 _ => return None,
286 };
287
288 match name.to_ascii_lowercase().as_str() {
289 "media" => self.parse_media_rule(),
290 "import" => self.parse_import_rule(),
291 "keyframes" | "-webkit-keyframes" | "-moz-keyframes" => self.parse_keyframes_rule(),
292 "font-face" => self.parse_font_face_rule(),
293 _ => {
294 // Unknown at-rule: skip to end of block or semicolon
295 self.skip_at_rule_body();
296 None
297 }
298 }
299 }
300
301 fn parse_media_rule(&mut self) -> Option<Rule> {
302 self.skip_whitespace();
303
304 // Collect prelude tokens for structured media query parsing.
305 let mut prelude_tokens = Vec::new();
306 loop {
307 match self.peek() {
308 Token::LeftBrace | Token::Eof => break,
309 _ => {
310 let tok = self.advance();
311 prelude_tokens.push(tok);
312 }
313 }
314 }
315
316 let queries = parse_media_query_list(&prelude_tokens);
317
318 // Expect `{`
319 if !matches!(self.peek(), Token::LeftBrace) {
320 return None;
321 }
322 self.advance();
323
324 // Parse nested rules until `}`
325 let mut rules = Vec::new();
326 loop {
327 self.skip_whitespace();
328 if self.is_eof() {
329 break;
330 }
331 if matches!(self.peek(), Token::RightBrace) {
332 self.advance();
333 break;
334 }
335 match self.peek() {
336 Token::AtKeyword(_) => {
337 if let Some(rule) = self.parse_at_rule() {
338 rules.push(rule);
339 }
340 }
341 _ => {
342 if let Some(rule) = self.parse_style_rule() {
343 rules.push(Rule::Style(rule));
344 }
345 }
346 }
347 }
348
349 Some(Rule::Media(MediaRule { queries, rules }))
350 }
351
352 fn parse_import_rule(&mut self) -> Option<Rule> {
353 self.skip_whitespace();
354 let url = match self.peek() {
355 Token::String(_) => {
356 if let Token::String(s) = self.advance() {
357 s
358 } else {
359 unreachable!()
360 }
361 }
362 Token::Url(_) => {
363 if let Token::Url(s) = self.advance() {
364 s
365 } else {
366 unreachable!()
367 }
368 }
369 Token::Function(name) if name.eq_ignore_ascii_case("url") => {
370 self.advance();
371 self.skip_whitespace();
372 let url = match self.advance() {
373 Token::String(s) => s,
374 _ => {
375 self.skip_to_semicolon();
376 return None;
377 }
378 };
379 self.skip_whitespace();
380 // consume closing paren
381 if matches!(self.peek(), Token::RightParen) {
382 self.advance();
383 }
384 url
385 }
386 _ => {
387 self.skip_to_semicolon();
388 return None;
389 }
390 };
391 // Consume optional trailing tokens until semicolon
392 self.skip_to_semicolon();
393 Some(Rule::Import(ImportRule { url }))
394 }
395
396 fn parse_keyframes_rule(&mut self) -> Option<Rule> {
397 self.skip_whitespace();
398
399 // Parse the animation name (identifier or string)
400 let name = match self.peek() {
401 Token::Ident(_) => {
402 if let Token::Ident(s) = self.advance() {
403 s
404 } else {
405 unreachable!()
406 }
407 }
408 Token::String(_) => {
409 if let Token::String(s) = self.advance() {
410 s
411 } else {
412 unreachable!()
413 }
414 }
415 _ => {
416 self.skip_at_rule_body();
417 return None;
418 }
419 };
420
421 self.skip_whitespace();
422
423 // Expect `{`
424 if !matches!(self.peek(), Token::LeftBrace) {
425 self.skip_at_rule_body();
426 return None;
427 }
428 self.advance();
429
430 // Parse keyframe blocks
431 let mut keyframes = Vec::new();
432 loop {
433 self.skip_whitespace();
434 if self.is_eof() {
435 break;
436 }
437 if matches!(self.peek(), Token::RightBrace) {
438 self.advance();
439 break;
440 }
441
442 if let Some(kf) = self.parse_keyframe_block() {
443 keyframes.push(kf);
444 }
445 }
446
447 Some(Rule::Keyframes(KeyframesRule { name, keyframes }))
448 }
449
450 fn parse_font_face_rule(&mut self) -> Option<Rule> {
451 self.skip_whitespace();
452
453 // Expect `{`
454 if !matches!(self.peek(), Token::LeftBrace) {
455 self.skip_at_rule_body();
456 return None;
457 }
458 self.advance();
459
460 // Parse descriptors inside the block.
461 let declarations = self.parse_declaration_list_until_brace();
462
463 // Consume `}`
464 if matches!(self.peek(), Token::RightBrace) {
465 self.advance();
466 }
467
468 // Extract font-family descriptor (required).
469 let mut family = None;
470 let mut sources = Vec::new();
471 let mut weight: u16 = 400;
472 let mut style = FontFaceStyle::Normal;
473 let mut display = FontDisplay::Auto;
474
475 for decl in &declarations {
476 match decl.property.as_str() {
477 "font-family" => {
478 family = extract_font_face_family(&decl.value);
479 }
480 "src" => {
481 sources = parse_font_face_src(&decl.value);
482 }
483 "font-weight" => {
484 weight = parse_font_face_weight(&decl.value);
485 }
486 "font-style" => {
487 style = parse_font_face_style(&decl.value);
488 }
489 "font-display" => {
490 display = parse_font_face_display(&decl.value);
491 }
492 _ => {}
493 }
494 }
495
496 let family = family?;
497 if sources.is_empty() {
498 return None;
499 }
500
501 Some(Rule::FontFace(FontFaceRule {
502 family,
503 sources,
504 weight,
505 style,
506 display,
507 }))
508 }
509
510 fn parse_keyframe_block(&mut self) -> Option<Keyframe> {
511 // Parse keyframe selectors (comma-separated: `from`, `to`, or percentages)
512 let mut selectors = Vec::new();
513 loop {
514 self.skip_whitespace();
515 match self.peek() {
516 Token::LeftBrace | Token::Eof | Token::RightBrace => break,
517 Token::Comma => {
518 self.advance();
519 continue;
520 }
521 Token::Ident(s) if s.eq_ignore_ascii_case("from") => {
522 selectors.push(KeyframeSelector::From);
523 self.advance();
524 }
525 Token::Ident(s) if s.eq_ignore_ascii_case("to") => {
526 selectors.push(KeyframeSelector::To);
527 self.advance();
528 }
529 Token::Percentage(_) => {
530 if let Token::Percentage(p) = self.advance() {
531 selectors.push(KeyframeSelector::Percentage(p));
532 }
533 }
534 Token::Number(n, _) if *n == 0.0 => {
535 // Allow bare `0` as 0%
536 self.advance();
537 selectors.push(KeyframeSelector::Percentage(0.0));
538 // Check for optional `%` sign
539 if matches!(self.peek(), Token::Delim('%')) {
540 self.advance();
541 }
542 }
543 _ => {
544 // Unknown token in keyframe selector — skip to next block
545 self.skip_at_rule_body();
546 return None;
547 }
548 }
549 }
550
551 if selectors.is_empty() {
552 return None;
553 }
554
555 // Expect `{`
556 if !matches!(self.peek(), Token::LeftBrace) {
557 return None;
558 }
559 self.advance();
560
561 let declarations = self.parse_declaration_list_until_brace();
562
563 // Consume `}`
564 if matches!(self.peek(), Token::RightBrace) {
565 self.advance();
566 }
567
568 Some(Keyframe {
569 selectors,
570 declarations,
571 })
572 }
573
574 fn skip_at_rule_body(&mut self) {
575 let mut brace_depth = 0;
576 loop {
577 match self.peek() {
578 Token::Eof => return,
579 Token::Semicolon if brace_depth == 0 => {
580 self.advance();
581 return;
582 }
583 Token::LeftBrace => {
584 brace_depth += 1;
585 self.advance();
586 }
587 Token::RightBrace => {
588 if brace_depth == 0 {
589 self.advance();
590 return;
591 }
592 brace_depth -= 1;
593 self.advance();
594 if brace_depth == 0 {
595 return;
596 }
597 }
598 _ => {
599 self.advance();
600 }
601 }
602 }
603 }
604
605 fn skip_to_semicolon(&mut self) {
606 loop {
607 match self.peek() {
608 Token::Eof | Token::Semicolon => {
609 if matches!(self.peek(), Token::Semicolon) {
610 self.advance();
611 }
612 return;
613 }
614 _ => {
615 self.advance();
616 }
617 }
618 }
619 }
620
621 // -- Style rules --------------------------------------------------------
622
623 fn parse_style_rule(&mut self) -> Option<StyleRule> {
624 let selectors = self.parse_selector_list();
625
626 // Expect `{`
627 self.skip_whitespace();
628 if !matches!(self.peek(), Token::LeftBrace) {
629 // Error recovery: skip to end of block or next rule
630 self.skip_at_rule_body();
631 return None;
632 }
633 self.advance();
634
635 let declarations = self.parse_declaration_list_until_brace();
636
637 // Consume `}`
638 if matches!(self.peek(), Token::RightBrace) {
639 self.advance();
640 }
641
642 if selectors.selectors.is_empty() {
643 return None;
644 }
645
646 Some(StyleRule {
647 selectors,
648 declarations,
649 })
650 }
651
652 // -- Selector parsing ---------------------------------------------------
653
654 fn parse_selector_list(&mut self) -> SelectorList {
655 let mut selectors = Vec::new();
656 self.skip_whitespace();
657
658 if let Some(sel) = self.parse_selector() {
659 selectors.push(sel);
660 }
661
662 loop {
663 self.skip_whitespace();
664 if !matches!(self.peek(), Token::Comma) {
665 break;
666 }
667 self.advance(); // consume comma
668 self.skip_whitespace();
669 if let Some(sel) = self.parse_selector() {
670 selectors.push(sel);
671 }
672 }
673
674 SelectorList { selectors }
675 }
676
677 fn parse_selector(&mut self) -> Option<Selector> {
678 let mut components = Vec::new();
679 let mut last_was_compound = false;
680 let mut had_whitespace = false;
681
682 loop {
683 // Check for end of selector
684 match self.peek() {
685 Token::Comma
686 | Token::LeftBrace
687 | Token::RightBrace
688 | Token::Semicolon
689 | Token::Eof => break,
690 Token::Whitespace => {
691 had_whitespace = true;
692 self.advance();
693 continue;
694 }
695 _ => {}
696 }
697
698 // Check for explicit combinator
699 let combinator = match self.peek() {
700 Token::Delim('>') => {
701 self.advance();
702 Some(Combinator::Child)
703 }
704 Token::Delim('+') => {
705 self.advance();
706 Some(Combinator::AdjacentSibling)
707 }
708 Token::Delim('~') => {
709 self.advance();
710 Some(Combinator::GeneralSibling)
711 }
712 _ => None,
713 };
714
715 if let Some(comb) = combinator {
716 if last_was_compound {
717 components.push(SelectorComponent::Combinator(comb));
718 last_was_compound = false;
719 had_whitespace = false;
720 }
721 self.skip_whitespace();
722 continue;
723 }
724
725 // Implicit descendant combinator if there was whitespace
726 if had_whitespace && last_was_compound {
727 components.push(SelectorComponent::Combinator(Combinator::Descendant));
728 had_whitespace = false;
729 }
730
731 // Parse compound selector
732 if let Some(compound) = self.parse_compound_selector() {
733 components.push(SelectorComponent::Compound(compound));
734 last_was_compound = true;
735 } else {
736 break;
737 }
738 }
739
740 if components.is_empty() {
741 None
742 } else {
743 Some(Selector { components })
744 }
745 }
746
747 fn parse_compound_selector(&mut self) -> Option<CompoundSelector> {
748 let mut simple = Vec::new();
749
750 loop {
751 match self.peek() {
752 // Type or universal
753 Token::Ident(_) if simple.is_empty() || !has_type_or_universal(&simple) => {
754 if let Token::Ident(name) = self.advance() {
755 simple.push(SimpleSelector::Type(name.to_ascii_lowercase()));
756 }
757 }
758 Token::Delim('*') if simple.is_empty() || !has_type_or_universal(&simple) => {
759 self.advance();
760 simple.push(SimpleSelector::Universal);
761 }
762 // Class
763 Token::Delim('.') => {
764 self.advance();
765 match self.advance() {
766 Token::Ident(name) => simple.push(SimpleSelector::Class(name)),
767 _ => break,
768 }
769 }
770 // ID
771 Token::Hash(_, HashType::Id) => {
772 if let Token::Hash(name, _) = self.advance() {
773 simple.push(SimpleSelector::Id(name));
774 }
775 }
776 // Attribute
777 Token::LeftBracket => {
778 self.advance();
779 if let Some(attr) = self.parse_attribute_selector() {
780 simple.push(SimpleSelector::Attribute(attr));
781 }
782 }
783 // Pseudo-class
784 Token::Colon => {
785 self.advance();
786 match self.advance() {
787 Token::Ident(name) => {
788 simple.push(SimpleSelector::PseudoClass(name.to_ascii_lowercase()))
789 }
790 Token::Function(name) => {
791 // Parse pseudo-class with arguments: skip to closing paren
792 let mut pname = name.to_ascii_lowercase();
793 pname.push('(');
794 let mut depth = 1;
795 loop {
796 match self.peek() {
797 Token::Eof => break,
798 Token::LeftParen => {
799 depth += 1;
800 pname.push('(');
801 self.advance();
802 }
803 Token::RightParen => {
804 depth -= 1;
805 if depth == 0 {
806 self.advance();
807 break;
808 }
809 pname.push(')');
810 self.advance();
811 }
812 _ => {
813 let tok = self.advance();
814 pname.push_str(&token_to_string(&tok));
815 }
816 }
817 }
818 pname.push(')');
819 simple.push(SimpleSelector::PseudoClass(pname));
820 }
821 _ => break,
822 }
823 }
824 // Ident after already having a type selector → new compound starts
825 Token::Ident(_) => break,
826 _ => break,
827 }
828 }
829
830 if simple.is_empty() {
831 None
832 } else {
833 Some(CompoundSelector { simple })
834 }
835 }
836
837 fn parse_attribute_selector(&mut self) -> Option<AttributeSelector> {
838 self.skip_whitespace();
839
840 let name = match self.advance() {
841 Token::Ident(name) => name.to_ascii_lowercase(),
842 _ => {
843 self.skip_to_bracket_close();
844 return None;
845 }
846 };
847
848 self.skip_whitespace();
849
850 // Check for close bracket (presence-only selector)
851 if matches!(self.peek(), Token::RightBracket) {
852 self.advance();
853 return Some(AttributeSelector {
854 name,
855 op: None,
856 value: None,
857 });
858 }
859
860 // Parse operator
861 let op = match self.peek() {
862 Token::Delim('=') => {
863 self.advance();
864 AttributeOp::Exact
865 }
866 Token::Delim('~') => {
867 self.advance();
868 if matches!(self.peek(), Token::Delim('=')) {
869 self.advance();
870 }
871 AttributeOp::Includes
872 }
873 Token::Delim('|') => {
874 self.advance();
875 if matches!(self.peek(), Token::Delim('=')) {
876 self.advance();
877 }
878 AttributeOp::DashMatch
879 }
880 Token::Delim('^') => {
881 self.advance();
882 if matches!(self.peek(), Token::Delim('=')) {
883 self.advance();
884 }
885 AttributeOp::Prefix
886 }
887 Token::Delim('$') => {
888 self.advance();
889 if matches!(self.peek(), Token::Delim('=')) {
890 self.advance();
891 }
892 AttributeOp::Suffix
893 }
894 Token::Delim('*') => {
895 self.advance();
896 if matches!(self.peek(), Token::Delim('=')) {
897 self.advance();
898 }
899 AttributeOp::Substring
900 }
901 _ => {
902 self.skip_to_bracket_close();
903 return None;
904 }
905 };
906
907 self.skip_whitespace();
908
909 // Parse value
910 let value = match self.advance() {
911 Token::Ident(v) => v,
912 Token::String(v) => v,
913 _ => {
914 self.skip_to_bracket_close();
915 return None;
916 }
917 };
918
919 self.skip_whitespace();
920
921 // Consume closing bracket
922 if matches!(self.peek(), Token::RightBracket) {
923 self.advance();
924 }
925
926 Some(AttributeSelector {
927 name,
928 op: Some(op),
929 value: Some(value),
930 })
931 }
932
933 fn skip_to_bracket_close(&mut self) {
934 loop {
935 match self.peek() {
936 Token::RightBracket | Token::Eof => {
937 if matches!(self.peek(), Token::RightBracket) {
938 self.advance();
939 }
940 return;
941 }
942 _ => {
943 self.advance();
944 }
945 }
946 }
947 }
948
949 // -- Declaration parsing ------------------------------------------------
950
951 fn parse_declaration_list(&mut self) -> Vec<Declaration> {
952 let mut declarations = Vec::new();
953 loop {
954 self.skip_whitespace();
955 if self.is_eof() {
956 break;
957 }
958 if matches!(self.peek(), Token::Semicolon) {
959 self.advance();
960 continue;
961 }
962 if let Some(decl) = self.parse_declaration() {
963 declarations.push(decl);
964 }
965 }
966 declarations
967 }
968
969 fn parse_declaration_list_until_brace(&mut self) -> Vec<Declaration> {
970 let mut declarations = Vec::new();
971 loop {
972 self.skip_whitespace();
973 if self.is_eof() || matches!(self.peek(), Token::RightBrace) {
974 break;
975 }
976 if matches!(self.peek(), Token::Semicolon) {
977 self.advance();
978 continue;
979 }
980 if let Some(decl) = self.parse_declaration() {
981 declarations.push(decl);
982 }
983 }
984 declarations
985 }
986
987 fn parse_declaration(&mut self) -> Option<Declaration> {
988 // Property name
989 let property = match self.peek() {
990 Token::Ident(_) => {
991 if let Token::Ident(name) = self.advance() {
992 name.to_ascii_lowercase()
993 } else {
994 unreachable!()
995 }
996 }
997 _ => {
998 // Error recovery: skip to next semicolon or closing brace
999 self.skip_declaration_error();
1000 return None;
1001 }
1002 };
1003
1004 self.skip_whitespace();
1005
1006 // Expect colon
1007 if !matches!(self.peek(), Token::Colon) {
1008 self.skip_declaration_error();
1009 return None;
1010 }
1011 self.advance();
1012
1013 self.skip_whitespace();
1014
1015 // Parse value
1016 let (value, important) = self.parse_declaration_value();
1017
1018 if value.is_empty() {
1019 return None;
1020 }
1021
1022 Some(Declaration {
1023 property,
1024 value,
1025 important,
1026 })
1027 }
1028
1029 fn parse_declaration_value(&mut self) -> (Vec<ComponentValue>, bool) {
1030 let mut values = Vec::new();
1031 let mut important = false;
1032
1033 loop {
1034 match self.peek() {
1035 Token::Semicolon | Token::RightBrace | Token::Eof => break,
1036 Token::Whitespace => {
1037 self.advance();
1038 // Only add whitespace if there are already values and we're
1039 // not at the end of the declaration
1040 if !values.is_empty()
1041 && !matches!(
1042 self.peek(),
1043 Token::Semicolon | Token::RightBrace | Token::Eof
1044 )
1045 {
1046 values.push(ComponentValue::Whitespace);
1047 }
1048 }
1049 Token::Delim('!') => {
1050 self.advance();
1051 self.skip_whitespace();
1052 if let Token::Ident(ref s) = self.peek() {
1053 if s.eq_ignore_ascii_case("important") {
1054 important = true;
1055 self.advance();
1056 }
1057 }
1058 }
1059 _ => {
1060 if let Some(cv) = self.parse_component_value() {
1061 values.push(cv);
1062 }
1063 }
1064 }
1065 }
1066
1067 // Trim trailing whitespace
1068 while matches!(values.last(), Some(ComponentValue::Whitespace)) {
1069 values.pop();
1070 }
1071
1072 (values, important)
1073 }
1074
1075 fn parse_component_value(&mut self) -> Option<ComponentValue> {
1076 match self.advance() {
1077 Token::Ident(s) => Some(ComponentValue::Ident(s)),
1078 Token::String(s) => Some(ComponentValue::String(s)),
1079 Token::Number(n, t) => Some(ComponentValue::Number(n, t)),
1080 Token::Percentage(n) => Some(ComponentValue::Percentage(n)),
1081 Token::Dimension(n, t, u) => Some(ComponentValue::Dimension(n, t, u)),
1082 Token::Hash(s, ht) => Some(ComponentValue::Hash(s, ht)),
1083 Token::Comma => Some(ComponentValue::Comma),
1084 Token::Delim(c) => Some(ComponentValue::Delim(c)),
1085 Token::Function(name) => {
1086 let args = self.parse_function_args();
1087 Some(ComponentValue::Function(name, args))
1088 }
1089 _ => None,
1090 }
1091 }
1092
1093 fn parse_function_args(&mut self) -> Vec<ComponentValue> {
1094 let mut args = Vec::new();
1095 loop {
1096 match self.peek() {
1097 Token::RightParen | Token::Eof => {
1098 if matches!(self.peek(), Token::RightParen) {
1099 self.advance();
1100 }
1101 break;
1102 }
1103 Token::Whitespace => {
1104 self.advance();
1105 if !args.is_empty() && !matches!(self.peek(), Token::RightParen | Token::Eof) {
1106 args.push(ComponentValue::Whitespace);
1107 }
1108 }
1109 _ => {
1110 if let Some(cv) = self.parse_component_value() {
1111 args.push(cv);
1112 }
1113 }
1114 }
1115 }
1116 args
1117 }
1118
1119 fn skip_declaration_error(&mut self) {
1120 loop {
1121 match self.peek() {
1122 Token::Semicolon => {
1123 self.advance();
1124 return;
1125 }
1126 Token::RightBrace | Token::Eof => return,
1127 _ => {
1128 self.advance();
1129 }
1130 }
1131 }
1132 }
1133}
1134
1135// ---------------------------------------------------------------------------
1136// Helpers
1137// ---------------------------------------------------------------------------
1138
1139fn has_type_or_universal(selectors: &[SimpleSelector]) -> bool {
1140 selectors
1141 .iter()
1142 .any(|s| matches!(s, SimpleSelector::Type(_) | SimpleSelector::Universal))
1143}
1144
1145fn token_to_string(token: &Token) -> String {
1146 match token {
1147 Token::Ident(s) => s.clone(),
1148 Token::Function(s) => format!("{s}("),
1149 Token::AtKeyword(s) => format!("@{s}"),
1150 Token::Hash(s, _) => format!("#{s}"),
1151 Token::String(s) => format!("\"{s}\""),
1152 Token::Url(s) => format!("url({s})"),
1153 Token::Number(n, _) => format!("{n}"),
1154 Token::Percentage(n) => format!("{n}%"),
1155 Token::Dimension(n, _, u) => format!("{n}{u}"),
1156 Token::Whitespace => " ".to_string(),
1157 Token::Colon => ":".to_string(),
1158 Token::Semicolon => ";".to_string(),
1159 Token::Comma => ",".to_string(),
1160 Token::LeftBracket => "[".to_string(),
1161 Token::RightBracket => "]".to_string(),
1162 Token::LeftParen => "(".to_string(),
1163 Token::RightParen => ")".to_string(),
1164 Token::LeftBrace => "{".to_string(),
1165 Token::RightBrace => "}".to_string(),
1166 Token::Delim(c) => c.to_string(),
1167 Token::Cdo => "<!--".to_string(),
1168 Token::Cdc => "-->".to_string(),
1169 Token::BadString | Token::BadUrl | Token::Eof => String::new(),
1170 }
1171}
1172
1173// ---------------------------------------------------------------------------
1174// @font-face descriptor helpers
1175// ---------------------------------------------------------------------------
1176
1177/// Extract the family name from a `font-family` descriptor value.
1178fn extract_font_face_family(values: &[ComponentValue]) -> Option<String> {
1179 for v in values {
1180 match v {
1181 ComponentValue::String(s) => return Some(s.clone()),
1182 ComponentValue::Ident(s) => return Some(s.clone()),
1183 _ => {}
1184 }
1185 }
1186 None
1187}
1188
1189/// Parse the `src` descriptor into a list of font sources.
1190///
1191/// Supports `url("...")` and `url("...") format("...")` syntax.
1192/// Multiple sources are comma-separated.
1193fn parse_font_face_src(values: &[ComponentValue]) -> Vec<FontFaceSource> {
1194 let mut sources = Vec::new();
1195 let mut i = 0;
1196
1197 while i < values.len() {
1198 match &values[i] {
1199 ComponentValue::Function(name, args) if name.eq_ignore_ascii_case("url") => {
1200 let url = extract_string_from_args(args);
1201 i += 1;
1202 // Check for optional format() hint.
1203 let format = skip_ws_and_parse_format(values, &mut i);
1204 if let Some(url) = url {
1205 sources.push(FontFaceSource { url, format });
1206 }
1207 }
1208 ComponentValue::Comma => {
1209 i += 1;
1210 }
1211 _ => {
1212 i += 1;
1213 }
1214 }
1215 }
1216
1217 sources
1218}
1219
1220/// Extract a string or ident from function arguments.
1221fn extract_string_from_args(args: &[ComponentValue]) -> Option<String> {
1222 for arg in args {
1223 match arg {
1224 ComponentValue::String(s) => return Some(s.clone()),
1225 ComponentValue::Ident(s) => return Some(s.clone()),
1226 _ => {}
1227 }
1228 }
1229 None
1230}
1231
1232/// Skip whitespace tokens and try to parse a `format()` function.
1233fn skip_ws_and_parse_format(values: &[ComponentValue], i: &mut usize) -> Option<String> {
1234 // Skip whitespace.
1235 while *i < values.len() && matches!(values[*i], ComponentValue::Whitespace) {
1236 *i += 1;
1237 }
1238 // Check for format().
1239 if *i < values.len() {
1240 if let ComponentValue::Function(name, args) = &values[*i] {
1241 if name.eq_ignore_ascii_case("format") {
1242 *i += 1;
1243 return extract_string_from_args(args);
1244 }
1245 }
1246 }
1247 None
1248}
1249
1250/// Parse `font-weight` descriptor: `normal`, `bold`, or numeric (100–900).
1251fn parse_font_face_weight(values: &[ComponentValue]) -> u16 {
1252 for v in values {
1253 match v {
1254 ComponentValue::Ident(s) if s.eq_ignore_ascii_case("normal") => return 400,
1255 ComponentValue::Ident(s) if s.eq_ignore_ascii_case("bold") => return 700,
1256 ComponentValue::Number(n, _) => {
1257 let w = *n as u16;
1258 return w.clamp(1, 1000);
1259 }
1260 _ => {}
1261 }
1262 }
1263 400
1264}
1265
1266/// Parse `font-style` descriptor: `normal`, `italic`, `oblique`.
1267fn parse_font_face_style(values: &[ComponentValue]) -> FontFaceStyle {
1268 for v in values {
1269 if let ComponentValue::Ident(s) = v {
1270 if s.eq_ignore_ascii_case("italic") {
1271 return FontFaceStyle::Italic;
1272 }
1273 if s.eq_ignore_ascii_case("oblique") {
1274 return FontFaceStyle::Oblique;
1275 }
1276 if s.eq_ignore_ascii_case("normal") {
1277 return FontFaceStyle::Normal;
1278 }
1279 }
1280 }
1281 FontFaceStyle::Normal
1282}
1283
1284/// Parse `font-display` descriptor.
1285fn parse_font_face_display(values: &[ComponentValue]) -> FontDisplay {
1286 for v in values {
1287 if let ComponentValue::Ident(s) = v {
1288 match s.to_ascii_lowercase().as_str() {
1289 "auto" => return FontDisplay::Auto,
1290 "block" => return FontDisplay::Block,
1291 "swap" => return FontDisplay::Swap,
1292 "fallback" => return FontDisplay::Fallback,
1293 "optional" => return FontDisplay::Optional,
1294 _ => {}
1295 }
1296 }
1297 }
1298 FontDisplay::Auto
1299}
1300
1301// ---------------------------------------------------------------------------
1302// Tests
1303// ---------------------------------------------------------------------------
1304
1305#[cfg(test)]
1306mod tests {
1307 use super::*;
1308
1309 // -- Selector tests -----------------------------------------------------
1310
1311 #[test]
1312 fn test_type_selector() {
1313 let ss = Parser::parse("div { }");
1314 assert_eq!(ss.rules.len(), 1);
1315 let rule = match &ss.rules[0] {
1316 Rule::Style(r) => r,
1317 _ => panic!("expected style rule"),
1318 };
1319 assert_eq!(rule.selectors.selectors.len(), 1);
1320 let sel = &rule.selectors.selectors[0];
1321 assert_eq!(sel.components.len(), 1);
1322 match &sel.components[0] {
1323 SelectorComponent::Compound(c) => {
1324 assert_eq!(c.simple.len(), 1);
1325 assert_eq!(c.simple[0], SimpleSelector::Type("div".into()));
1326 }
1327 _ => panic!("expected compound"),
1328 }
1329 }
1330
1331 #[test]
1332 fn test_universal_selector() {
1333 let ss = Parser::parse("* { }");
1334 let rule = match &ss.rules[0] {
1335 Rule::Style(r) => r,
1336 _ => panic!("expected style rule"),
1337 };
1338 let sel = &rule.selectors.selectors[0];
1339 match &sel.components[0] {
1340 SelectorComponent::Compound(c) => {
1341 assert_eq!(c.simple[0], SimpleSelector::Universal);
1342 }
1343 _ => panic!("expected compound"),
1344 }
1345 }
1346
1347 #[test]
1348 fn test_class_selector() {
1349 let ss = Parser::parse(".foo { }");
1350 let rule = match &ss.rules[0] {
1351 Rule::Style(r) => r,
1352 _ => panic!("expected style rule"),
1353 };
1354 let sel = &rule.selectors.selectors[0];
1355 match &sel.components[0] {
1356 SelectorComponent::Compound(c) => {
1357 assert_eq!(c.simple[0], SimpleSelector::Class("foo".into()));
1358 }
1359 _ => panic!("expected compound"),
1360 }
1361 }
1362
1363 #[test]
1364 fn test_id_selector() {
1365 let ss = Parser::parse("#main { }");
1366 let rule = match &ss.rules[0] {
1367 Rule::Style(r) => r,
1368 _ => panic!("expected style rule"),
1369 };
1370 let sel = &rule.selectors.selectors[0];
1371 match &sel.components[0] {
1372 SelectorComponent::Compound(c) => {
1373 assert_eq!(c.simple[0], SimpleSelector::Id("main".into()));
1374 }
1375 _ => panic!("expected compound"),
1376 }
1377 }
1378
1379 #[test]
1380 fn test_compound_selector() {
1381 let ss = Parser::parse("div.foo#bar { }");
1382 let rule = match &ss.rules[0] {
1383 Rule::Style(r) => r,
1384 _ => panic!("expected style rule"),
1385 };
1386 let sel = &rule.selectors.selectors[0];
1387 match &sel.components[0] {
1388 SelectorComponent::Compound(c) => {
1389 assert_eq!(c.simple.len(), 3);
1390 assert_eq!(c.simple[0], SimpleSelector::Type("div".into()));
1391 assert_eq!(c.simple[1], SimpleSelector::Class("foo".into()));
1392 assert_eq!(c.simple[2], SimpleSelector::Id("bar".into()));
1393 }
1394 _ => panic!("expected compound"),
1395 }
1396 }
1397
1398 #[test]
1399 fn test_selector_list() {
1400 let ss = Parser::parse("h1, h2, h3 { }");
1401 let rule = match &ss.rules[0] {
1402 Rule::Style(r) => r,
1403 _ => panic!("expected style rule"),
1404 };
1405 assert_eq!(rule.selectors.selectors.len(), 3);
1406 }
1407
1408 #[test]
1409 fn test_descendant_combinator() {
1410 let ss = Parser::parse("div p { }");
1411 let rule = match &ss.rules[0] {
1412 Rule::Style(r) => r,
1413 _ => panic!("expected style rule"),
1414 };
1415 let sel = &rule.selectors.selectors[0];
1416 assert_eq!(sel.components.len(), 3);
1417 assert!(matches!(
1418 sel.components[1],
1419 SelectorComponent::Combinator(Combinator::Descendant)
1420 ));
1421 }
1422
1423 #[test]
1424 fn test_child_combinator() {
1425 let ss = Parser::parse("div > p { }");
1426 let rule = match &ss.rules[0] {
1427 Rule::Style(r) => r,
1428 _ => panic!("expected style rule"),
1429 };
1430 let sel = &rule.selectors.selectors[0];
1431 assert_eq!(sel.components.len(), 3);
1432 assert!(matches!(
1433 sel.components[1],
1434 SelectorComponent::Combinator(Combinator::Child)
1435 ));
1436 }
1437
1438 #[test]
1439 fn test_adjacent_sibling_combinator() {
1440 let ss = Parser::parse("h1 + p { }");
1441 let rule = match &ss.rules[0] {
1442 Rule::Style(r) => r,
1443 _ => panic!("expected style rule"),
1444 };
1445 let sel = &rule.selectors.selectors[0];
1446 assert!(matches!(
1447 sel.components[1],
1448 SelectorComponent::Combinator(Combinator::AdjacentSibling)
1449 ));
1450 }
1451
1452 #[test]
1453 fn test_general_sibling_combinator() {
1454 let ss = Parser::parse("h1 ~ p { }");
1455 let rule = match &ss.rules[0] {
1456 Rule::Style(r) => r,
1457 _ => panic!("expected style rule"),
1458 };
1459 let sel = &rule.selectors.selectors[0];
1460 assert!(matches!(
1461 sel.components[1],
1462 SelectorComponent::Combinator(Combinator::GeneralSibling)
1463 ));
1464 }
1465
1466 #[test]
1467 fn test_attribute_presence() {
1468 let ss = Parser::parse("[disabled] { }");
1469 let rule = match &ss.rules[0] {
1470 Rule::Style(r) => r,
1471 _ => panic!("expected style rule"),
1472 };
1473 let sel = &rule.selectors.selectors[0];
1474 match &sel.components[0] {
1475 SelectorComponent::Compound(c) => match &c.simple[0] {
1476 SimpleSelector::Attribute(attr) => {
1477 assert_eq!(attr.name, "disabled");
1478 assert!(attr.op.is_none());
1479 assert!(attr.value.is_none());
1480 }
1481 _ => panic!("expected attribute selector"),
1482 },
1483 _ => panic!("expected compound"),
1484 }
1485 }
1486
1487 #[test]
1488 fn test_attribute_exact() {
1489 let ss = Parser::parse("[type=\"text\"] { }");
1490 let rule = match &ss.rules[0] {
1491 Rule::Style(r) => r,
1492 _ => panic!("expected style rule"),
1493 };
1494 let sel = &rule.selectors.selectors[0];
1495 match &sel.components[0] {
1496 SelectorComponent::Compound(c) => match &c.simple[0] {
1497 SimpleSelector::Attribute(attr) => {
1498 assert_eq!(attr.name, "type");
1499 assert_eq!(attr.op, Some(AttributeOp::Exact));
1500 assert_eq!(attr.value, Some("text".into()));
1501 }
1502 _ => panic!("expected attribute selector"),
1503 },
1504 _ => panic!("expected compound"),
1505 }
1506 }
1507
1508 #[test]
1509 fn test_attribute_operators() {
1510 let ops = vec![
1511 ("[a~=b] { }", AttributeOp::Includes),
1512 ("[a|=b] { }", AttributeOp::DashMatch),
1513 ("[a^=b] { }", AttributeOp::Prefix),
1514 ("[a$=b] { }", AttributeOp::Suffix),
1515 ("[a*=b] { }", AttributeOp::Substring),
1516 ];
1517 for (input, expected_op) in ops {
1518 let ss = Parser::parse(input);
1519 let rule = match &ss.rules[0] {
1520 Rule::Style(r) => r,
1521 _ => panic!("expected style rule"),
1522 };
1523 let sel = &rule.selectors.selectors[0];
1524 match &sel.components[0] {
1525 SelectorComponent::Compound(c) => match &c.simple[0] {
1526 SimpleSelector::Attribute(attr) => {
1527 assert_eq!(attr.op, Some(expected_op), "failed for {input}");
1528 }
1529 _ => panic!("expected attribute selector"),
1530 },
1531 _ => panic!("expected compound"),
1532 }
1533 }
1534 }
1535
1536 #[test]
1537 fn test_pseudo_class() {
1538 let ss = Parser::parse("a:hover { }");
1539 let rule = match &ss.rules[0] {
1540 Rule::Style(r) => r,
1541 _ => panic!("expected style rule"),
1542 };
1543 let sel = &rule.selectors.selectors[0];
1544 match &sel.components[0] {
1545 SelectorComponent::Compound(c) => {
1546 assert_eq!(c.simple.len(), 2);
1547 assert_eq!(c.simple[0], SimpleSelector::Type("a".into()));
1548 assert_eq!(c.simple[1], SimpleSelector::PseudoClass("hover".into()));
1549 }
1550 _ => panic!("expected compound"),
1551 }
1552 }
1553
1554 #[test]
1555 fn test_pseudo_class_first_child() {
1556 let ss = Parser::parse("p:first-child { }");
1557 let rule = match &ss.rules[0] {
1558 Rule::Style(r) => r,
1559 _ => panic!("expected style rule"),
1560 };
1561 let sel = &rule.selectors.selectors[0];
1562 match &sel.components[0] {
1563 SelectorComponent::Compound(c) => {
1564 assert_eq!(
1565 c.simple[1],
1566 SimpleSelector::PseudoClass("first-child".into())
1567 );
1568 }
1569 _ => panic!("expected compound"),
1570 }
1571 }
1572
1573 // -- Declaration tests --------------------------------------------------
1574
1575 #[test]
1576 fn test_simple_declaration() {
1577 let ss = Parser::parse("p { color: red; }");
1578 let rule = match &ss.rules[0] {
1579 Rule::Style(r) => r,
1580 _ => panic!("expected style rule"),
1581 };
1582 assert_eq!(rule.declarations.len(), 1);
1583 assert_eq!(rule.declarations[0].property, "color");
1584 assert_eq!(rule.declarations[0].value.len(), 1);
1585 assert_eq!(
1586 rule.declarations[0].value[0],
1587 ComponentValue::Ident("red".into())
1588 );
1589 assert!(!rule.declarations[0].important);
1590 }
1591
1592 #[test]
1593 fn test_multiple_declarations() {
1594 let ss = Parser::parse("p { color: red; font-size: 16px; }");
1595 let rule = match &ss.rules[0] {
1596 Rule::Style(r) => r,
1597 _ => panic!("expected style rule"),
1598 };
1599 assert_eq!(rule.declarations.len(), 2);
1600 assert_eq!(rule.declarations[0].property, "color");
1601 assert_eq!(rule.declarations[1].property, "font-size");
1602 }
1603
1604 #[test]
1605 fn test_important_declaration() {
1606 let ss = Parser::parse("p { color: red !important; }");
1607 let rule = match &ss.rules[0] {
1608 Rule::Style(r) => r,
1609 _ => panic!("expected style rule"),
1610 };
1611 assert!(rule.declarations[0].important);
1612 }
1613
1614 #[test]
1615 fn test_declaration_with_function() {
1616 let ss = Parser::parse("p { color: rgb(255, 0, 0); }");
1617 let rule = match &ss.rules[0] {
1618 Rule::Style(r) => r,
1619 _ => panic!("expected style rule"),
1620 };
1621 assert_eq!(rule.declarations[0].property, "color");
1622 match &rule.declarations[0].value[0] {
1623 ComponentValue::Function(name, args) => {
1624 assert_eq!(name, "rgb");
1625 // 255, 0, 0 → Number, Comma, WS, Number, Comma, WS, Number
1626 assert!(!args.is_empty());
1627 }
1628 _ => panic!("expected function value"),
1629 }
1630 }
1631
1632 #[test]
1633 fn test_declaration_with_hash_color() {
1634 let ss = Parser::parse("p { color: #ff0000; }");
1635 let rule = match &ss.rules[0] {
1636 Rule::Style(r) => r,
1637 _ => panic!("expected style rule"),
1638 };
1639 assert_eq!(
1640 rule.declarations[0].value[0],
1641 ComponentValue::Hash("ff0000".into(), HashType::Id)
1642 );
1643 }
1644
1645 #[test]
1646 fn test_declaration_with_dimension() {
1647 let ss = Parser::parse("p { margin: 10px; }");
1648 let rule = match &ss.rules[0] {
1649 Rule::Style(r) => r,
1650 _ => panic!("expected style rule"),
1651 };
1652 assert_eq!(
1653 rule.declarations[0].value[0],
1654 ComponentValue::Dimension(10.0, NumericType::Integer, "px".into())
1655 );
1656 }
1657
1658 #[test]
1659 fn test_declaration_with_percentage() {
1660 let ss = Parser::parse("p { width: 50%; }");
1661 let rule = match &ss.rules[0] {
1662 Rule::Style(r) => r,
1663 _ => panic!("expected style rule"),
1664 };
1665 assert_eq!(
1666 rule.declarations[0].value[0],
1667 ComponentValue::Percentage(50.0)
1668 );
1669 }
1670
1671 #[test]
1672 fn test_parse_inline_style() {
1673 let decls = Parser::parse_declarations("color: red; font-size: 16px");
1674 assert_eq!(decls.len(), 2);
1675 assert_eq!(decls[0].property, "color");
1676 assert_eq!(decls[1].property, "font-size");
1677 }
1678
1679 // -- Error recovery tests -----------------------------------------------
1680
1681 #[test]
1682 fn test_invalid_declaration_skipped() {
1683 let ss = Parser::parse("p { ??? ; color: red; }");
1684 let rule = match &ss.rules[0] {
1685 Rule::Style(r) => r,
1686 _ => panic!("expected style rule"),
1687 };
1688 // The invalid declaration should be skipped, color should remain
1689 assert_eq!(rule.declarations.len(), 1);
1690 assert_eq!(rule.declarations[0].property, "color");
1691 }
1692
1693 #[test]
1694 fn test_missing_colon_skipped() {
1695 let ss = Parser::parse("p { color red; font-size: 16px; }");
1696 let rule = match &ss.rules[0] {
1697 Rule::Style(r) => r,
1698 _ => panic!("expected style rule"),
1699 };
1700 assert_eq!(rule.declarations.len(), 1);
1701 assert_eq!(rule.declarations[0].property, "font-size");
1702 }
1703
1704 #[test]
1705 fn test_empty_stylesheet() {
1706 let ss = Parser::parse("");
1707 assert_eq!(ss.rules.len(), 0);
1708 }
1709
1710 #[test]
1711 fn test_empty_rule() {
1712 let ss = Parser::parse("p { }");
1713 let rule = match &ss.rules[0] {
1714 Rule::Style(r) => r,
1715 _ => panic!("expected style rule"),
1716 };
1717 assert_eq!(rule.declarations.len(), 0);
1718 }
1719
1720 #[test]
1721 fn test_multiple_rules() {
1722 let ss = Parser::parse("h1 { color: blue; } p { color: red; }");
1723 assert_eq!(ss.rules.len(), 2);
1724 }
1725
1726 // -- @-rule tests -------------------------------------------------------
1727
1728 #[test]
1729 fn test_import_rule_string() {
1730 let ss = Parser::parse("@import \"style.css\";");
1731 assert_eq!(ss.rules.len(), 1);
1732 match &ss.rules[0] {
1733 Rule::Import(r) => assert_eq!(r.url, "style.css"),
1734 _ => panic!("expected import rule"),
1735 }
1736 }
1737
1738 #[test]
1739 fn test_import_rule_url() {
1740 let ss = Parser::parse("@import url(style.css);");
1741 assert_eq!(ss.rules.len(), 1);
1742 match &ss.rules[0] {
1743 Rule::Import(r) => assert_eq!(r.url, "style.css"),
1744 _ => panic!("expected import rule"),
1745 }
1746 }
1747
1748 #[test]
1749 fn test_media_rule() {
1750 use crate::media::{MediaType, Modifier};
1751 let ss = Parser::parse("@media screen { p { color: red; } }");
1752 assert_eq!(ss.rules.len(), 1);
1753 match &ss.rules[0] {
1754 Rule::Media(m) => {
1755 assert_eq!(m.queries.queries.len(), 1);
1756 assert_eq!(m.queries.queries[0].media_type, MediaType::Screen);
1757 assert_eq!(m.queries.queries[0].modifier, Modifier::None);
1758 assert_eq!(m.rules.len(), 1);
1759 }
1760 _ => panic!("expected media rule"),
1761 }
1762 }
1763
1764 #[test]
1765 fn test_media_rule_complex_query() {
1766 use crate::media::{Comparison, MediaFeature, MediaType};
1767 let ss = Parser::parse("@media screen and (max-width: 600px) { p { font-size: 14px; } }");
1768 match &ss.rules[0] {
1769 Rule::Media(m) => {
1770 assert_eq!(m.queries.queries[0].media_type, MediaType::Screen);
1771 assert_eq!(m.queries.queries[0].features.len(), 1);
1772 assert_eq!(
1773 m.queries.queries[0].features[0],
1774 MediaFeature::Width(Comparison::Lte, 600.0)
1775 );
1776 assert_eq!(m.rules.len(), 1);
1777 }
1778 _ => panic!("expected media rule"),
1779 }
1780 }
1781
1782 #[test]
1783 fn test_unknown_at_rule_skipped() {
1784 let ss = Parser::parse("@charset \"UTF-8\"; p { color: red; }");
1785 assert_eq!(ss.rules.len(), 1);
1786 match &ss.rules[0] {
1787 Rule::Style(r) => assert_eq!(r.declarations[0].property, "color"),
1788 _ => panic!("expected style rule"),
1789 }
1790 }
1791
1792 // -- Integration tests --------------------------------------------------
1793
1794 #[test]
1795 fn test_real_css() {
1796 let css = r#"
1797 body {
1798 margin: 0;
1799 font-family: sans-serif;
1800 background-color: #fff;
1801 }
1802 h1 {
1803 color: #333;
1804 font-size: 24px;
1805 }
1806 .container {
1807 max-width: 960px;
1808 margin: 0 auto;
1809 }
1810 a:hover {
1811 color: blue;
1812 text-decoration: underline;
1813 }
1814 "#;
1815 let ss = Parser::parse(css);
1816 assert_eq!(ss.rules.len(), 4);
1817 }
1818
1819 #[test]
1820 fn test_declaration_no_trailing_semicolon() {
1821 let ss = Parser::parse("p { color: red }");
1822 let rule = match &ss.rules[0] {
1823 Rule::Style(r) => r,
1824 _ => panic!("expected style rule"),
1825 };
1826 assert_eq!(rule.declarations.len(), 1);
1827 assert_eq!(rule.declarations[0].property, "color");
1828 }
1829
1830 #[test]
1831 fn test_multi_value_declaration() {
1832 let ss = Parser::parse("p { margin: 10px 20px 30px 40px; }");
1833 let rule = match &ss.rules[0] {
1834 Rule::Style(r) => r,
1835 _ => panic!("expected style rule"),
1836 };
1837 // 10px WS 20px WS 30px WS 40px
1838 assert_eq!(rule.declarations[0].value.len(), 7);
1839 }
1840
1841 #[test]
1842 fn test_cdo_cdc_ignored() {
1843 let ss = Parser::parse("<!-- p { color: red; } -->");
1844 assert_eq!(ss.rules.len(), 1);
1845 }
1846
1847 // -- @keyframes tests --
1848
1849 #[test]
1850 fn test_keyframes_from_to() {
1851 let ss = Parser::parse("@keyframes slidein { from { opacity: 0; } to { opacity: 1; } }");
1852 assert_eq!(ss.rules.len(), 1);
1853 let kf = match &ss.rules[0] {
1854 Rule::Keyframes(k) => k,
1855 _ => panic!("expected keyframes rule"),
1856 };
1857 assert_eq!(kf.name, "slidein");
1858 assert_eq!(kf.keyframes.len(), 2);
1859 assert_eq!(kf.keyframes[0].selectors, vec![KeyframeSelector::From]);
1860 assert_eq!(kf.keyframes[1].selectors, vec![KeyframeSelector::To]);
1861 assert_eq!(kf.keyframes[0].declarations.len(), 1);
1862 assert_eq!(kf.keyframes[0].declarations[0].property, "opacity");
1863 }
1864
1865 #[test]
1866 fn test_keyframes_percentages() {
1867 let ss = Parser::parse(
1868 "@keyframes fade { 0% { opacity: 0; } 50% { opacity: 1; } 100% { opacity: 0.5; } }",
1869 );
1870 let kf = match &ss.rules[0] {
1871 Rule::Keyframes(k) => k,
1872 _ => panic!("expected keyframes rule"),
1873 };
1874 assert_eq!(kf.name, "fade");
1875 assert_eq!(kf.keyframes.len(), 3);
1876 assert_eq!(
1877 kf.keyframes[0].selectors,
1878 vec![KeyframeSelector::Percentage(0.0)]
1879 );
1880 assert_eq!(
1881 kf.keyframes[1].selectors,
1882 vec![KeyframeSelector::Percentage(50.0)]
1883 );
1884 assert_eq!(
1885 kf.keyframes[2].selectors,
1886 vec![KeyframeSelector::Percentage(100.0)]
1887 );
1888 }
1889
1890 #[test]
1891 fn test_keyframes_multiple_selectors() {
1892 let ss =
1893 Parser::parse("@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }");
1894 let kf = match &ss.rules[0] {
1895 Rule::Keyframes(k) => k,
1896 _ => panic!("expected keyframes rule"),
1897 };
1898 assert_eq!(kf.keyframes.len(), 2);
1899 assert_eq!(
1900 kf.keyframes[0].selectors,
1901 vec![
1902 KeyframeSelector::Percentage(0.0),
1903 KeyframeSelector::Percentage(100.0),
1904 ]
1905 );
1906 }
1907
1908 #[test]
1909 fn test_keyframes_string_name() {
1910 let ss = Parser::parse(
1911 "@keyframes \"my animation\" { from { color: red; } to { color: blue; } }",
1912 );
1913 let kf = match &ss.rules[0] {
1914 Rule::Keyframes(k) => k,
1915 _ => panic!("expected keyframes rule"),
1916 };
1917 assert_eq!(kf.name, "my animation");
1918 }
1919
1920 #[test]
1921 fn test_keyframes_webkit_prefix() {
1922 let ss =
1923 Parser::parse("@-webkit-keyframes spin { from { opacity: 0; } to { opacity: 1; } }");
1924 assert_eq!(ss.rules.len(), 1);
1925 let kf = match &ss.rules[0] {
1926 Rule::Keyframes(k) => k,
1927 _ => panic!("expected keyframes rule"),
1928 };
1929 assert_eq!(kf.name, "spin");
1930 }
1931
1932 #[test]
1933 fn test_keyframes_multiple_declarations() {
1934 let ss = Parser::parse(
1935 "@keyframes move { from { left: 0px; top: 0px; } to { left: 100px; top: 50px; } }",
1936 );
1937 let kf = match &ss.rules[0] {
1938 Rule::Keyframes(k) => k,
1939 _ => panic!("expected keyframes rule"),
1940 };
1941 assert_eq!(kf.keyframes[0].declarations.len(), 2);
1942 assert_eq!(kf.keyframes[1].declarations.len(), 2);
1943 }
1944
1945 #[test]
1946 fn test_keyframes_with_style_rules() {
1947 let ss = Parser::parse(
1948 r#"
1949 .box { color: red; }
1950 @keyframes fade { from { opacity: 0; } to { opacity: 1; } }
1951 p { font-size: 16px; }
1952 "#,
1953 );
1954 assert_eq!(ss.rules.len(), 3);
1955 assert!(matches!(ss.rules[0], Rule::Style(_)));
1956 assert!(matches!(ss.rules[1], Rule::Keyframes(_)));
1957 assert!(matches!(ss.rules[2], Rule::Style(_)));
1958 }
1959
1960 // -- @font-face -----------------------------------------------------------
1961
1962 #[test]
1963 fn font_face_basic() {
1964 let ss = Parser::parse(
1965 r#"@font-face {
1966 font-family: "MyFont";
1967 src: url("myfont.ttf");
1968 }"#,
1969 );
1970 assert_eq!(ss.rules.len(), 1);
1971 let ff = match &ss.rules[0] {
1972 Rule::FontFace(ff) => ff,
1973 _ => panic!("expected FontFace rule"),
1974 };
1975 assert_eq!(ff.family, "MyFont");
1976 assert_eq!(ff.sources.len(), 1);
1977 assert_eq!(ff.sources[0].url, "myfont.ttf");
1978 assert!(ff.sources[0].format.is_none());
1979 assert_eq!(ff.weight, 400);
1980 assert_eq!(ff.style, FontFaceStyle::Normal);
1981 assert_eq!(ff.display, FontDisplay::Auto);
1982 }
1983
1984 #[test]
1985 fn font_face_all_descriptors() {
1986 let ss = Parser::parse(
1987 r#"@font-face {
1988 font-family: "WebFont";
1989 src: url("webfont.woff2") format("woff2");
1990 font-weight: 700;
1991 font-style: italic;
1992 font-display: swap;
1993 }"#,
1994 );
1995 assert_eq!(ss.rules.len(), 1);
1996 let ff = match &ss.rules[0] {
1997 Rule::FontFace(ff) => ff,
1998 _ => panic!("expected FontFace rule"),
1999 };
2000 assert_eq!(ff.family, "WebFont");
2001 assert_eq!(ff.sources.len(), 1);
2002 assert_eq!(ff.sources[0].url, "webfont.woff2");
2003 assert_eq!(ff.sources[0].format.as_deref(), Some("woff2"));
2004 assert_eq!(ff.weight, 700);
2005 assert_eq!(ff.style, FontFaceStyle::Italic);
2006 assert_eq!(ff.display, FontDisplay::Swap);
2007 }
2008
2009 #[test]
2010 fn font_face_bold_keyword() {
2011 let ss = Parser::parse(
2012 r#"@font-face {
2013 font-family: "BoldFont";
2014 src: url("bold.ttf");
2015 font-weight: bold;
2016 }"#,
2017 );
2018 let ff = match &ss.rules[0] {
2019 Rule::FontFace(ff) => ff,
2020 _ => panic!("expected FontFace rule"),
2021 };
2022 assert_eq!(ff.weight, 700);
2023 }
2024
2025 #[test]
2026 fn font_face_normal_weight_keyword() {
2027 let ss = Parser::parse(
2028 r#"@font-face {
2029 font-family: "NormalFont";
2030 src: url("normal.ttf");
2031 font-weight: normal;
2032 }"#,
2033 );
2034 let ff = match &ss.rules[0] {
2035 Rule::FontFace(ff) => ff,
2036 _ => panic!("expected FontFace rule"),
2037 };
2038 assert_eq!(ff.weight, 400);
2039 }
2040
2041 #[test]
2042 fn font_face_oblique_style() {
2043 let ss = Parser::parse(
2044 r#"@font-face {
2045 font-family: "ObliqueFont";
2046 src: url("oblique.ttf");
2047 font-style: oblique;
2048 }"#,
2049 );
2050 let ff = match &ss.rules[0] {
2051 Rule::FontFace(ff) => ff,
2052 _ => panic!("expected FontFace rule"),
2053 };
2054 assert_eq!(ff.style, FontFaceStyle::Oblique);
2055 }
2056
2057 #[test]
2058 fn font_face_multiple_sources() {
2059 let ss = Parser::parse(
2060 r#"@font-face {
2061 font-family: "MultiFmt";
2062 src: url("font.woff2") format("woff2"), url("font.ttf") format("truetype");
2063 }"#,
2064 );
2065 let ff = match &ss.rules[0] {
2066 Rule::FontFace(ff) => ff,
2067 _ => panic!("expected FontFace rule"),
2068 };
2069 assert_eq!(ff.sources.len(), 2);
2070 assert_eq!(ff.sources[0].url, "font.woff2");
2071 assert_eq!(ff.sources[0].format.as_deref(), Some("woff2"));
2072 assert_eq!(ff.sources[1].url, "font.ttf");
2073 assert_eq!(ff.sources[1].format.as_deref(), Some("truetype"));
2074 }
2075
2076 #[test]
2077 fn font_face_display_values() {
2078 for (keyword, expected) in [
2079 ("auto", FontDisplay::Auto),
2080 ("block", FontDisplay::Block),
2081 ("swap", FontDisplay::Swap),
2082 ("fallback", FontDisplay::Fallback),
2083 ("optional", FontDisplay::Optional),
2084 ] {
2085 let css = format!(
2086 r#"@font-face {{
2087 font-family: "F";
2088 src: url("f.ttf");
2089 font-display: {};
2090 }}"#,
2091 keyword
2092 );
2093 let ss = Parser::parse(&css);
2094 let ff = match &ss.rules[0] {
2095 Rule::FontFace(ff) => ff,
2096 _ => panic!("expected FontFace rule"),
2097 };
2098 assert_eq!(ff.display, expected, "font-display: {}", keyword);
2099 }
2100 }
2101
2102 #[test]
2103 fn font_face_missing_family_skipped() {
2104 let ss = Parser::parse(
2105 r#"@font-face {
2106 src: url("nofamily.ttf");
2107 }"#,
2108 );
2109 assert!(
2110 ss.rules.is_empty(),
2111 "should skip @font-face without font-family"
2112 );
2113 }
2114
2115 #[test]
2116 fn font_face_missing_src_skipped() {
2117 let ss = Parser::parse(
2118 r#"@font-face {
2119 font-family: "NoSrc";
2120 }"#,
2121 );
2122 assert!(ss.rules.is_empty(), "should skip @font-face without src");
2123 }
2124
2125 #[test]
2126 fn font_face_unquoted_family() {
2127 let ss = Parser::parse(
2128 r#"@font-face {
2129 font-family: CustomFont;
2130 src: url("custom.ttf");
2131 }"#,
2132 );
2133 assert_eq!(ss.rules.len(), 1);
2134 let ff = match &ss.rules[0] {
2135 Rule::FontFace(ff) => ff,
2136 _ => panic!("expected FontFace rule"),
2137 };
2138 assert_eq!(ff.family, "CustomFont");
2139 }
2140
2141 #[test]
2142 fn font_face_among_other_rules() {
2143 let ss = Parser::parse(
2144 r#"
2145 body { color: black; }
2146 @font-face {
2147 font-family: "TestFont";
2148 src: url("test.ttf");
2149 }
2150 p { margin: 0; }
2151 "#,
2152 );
2153 assert_eq!(ss.rules.len(), 3);
2154 assert!(matches!(ss.rules[0], Rule::Style(_)));
2155 assert!(matches!(ss.rules[1], Rule::FontFace(_)));
2156 assert!(matches!(ss.rules[2], Rule::Style(_)));
2157 }
2158
2159 #[test]
2160 fn font_face_numeric_weight() {
2161 let ss = Parser::parse(
2162 r#"@font-face {
2163 font-family: "W300";
2164 src: url("w300.ttf");
2165 font-weight: 300;
2166 }"#,
2167 );
2168 let ff = match &ss.rules[0] {
2169 Rule::FontFace(ff) => ff,
2170 _ => panic!("expected FontFace rule"),
2171 };
2172 assert_eq!(ff.weight, 300);
2173 }
2174}