web engine - experimental web browser

Implement CSS parser: selectors, declarations, and @-rules

Full CSS parser per CSS Syntax Module Level 3 §5 that consumes tokens from the
tokenizer and produces a structured stylesheet AST. Supports type, universal,
class, ID, attribute, and pseudo-class selectors with all combinators
(descendant, child, adjacent sibling, general sibling). Parses declarations
with !important, component values including functions, and @-rules (@media
with nested rules, @import). Includes error recovery for invalid declarations
and unknown at-rules. 37 unit tests covering all selector types, declarations,
@-rules, error recovery, and real CSS patterns.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+1478
+1
crates/css/src/lib.rs
··· 1 1 //! CSS tokenizer, parser, and CSSOM. 2 2 3 + pub mod parser; 3 4 pub mod tokenizer;
+1477
crates/css/src/parser.rs
··· 1 + //! CSS parser per CSS Syntax Module Level 3 §5. 2 + //! 3 + //! Consumes tokens from the tokenizer and produces a structured stylesheet. 4 + 5 + use 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)] 13 + pub 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)] 19 + pub 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)] 27 + pub 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)] 34 + pub 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)] 41 + pub struct ImportRule { 42 + pub url: String, 43 + } 44 + 45 + /// A comma-separated list of selectors. 46 + #[derive(Debug, Clone, PartialEq)] 47 + pub 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)] 53 + pub struct Selector { 54 + pub components: Vec<SelectorComponent>, 55 + } 56 + 57 + /// Either a compound selector or a combinator between compounds. 58 + #[derive(Debug, Clone, PartialEq)] 59 + pub 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)] 66 + pub struct CompoundSelector { 67 + pub simple: Vec<SimpleSelector>, 68 + } 69 + 70 + /// A simple selector. 71 + #[derive(Debug, Clone, PartialEq)] 72 + pub 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)] 83 + pub 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)] 91 + pub 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)] 108 + pub 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)] 121 + pub 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)] 130 + pub 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. 148 + pub struct Parser { 149 + tokens: Vec<Token>, 150 + pos: usize, 151 + } 152 + 153 + impl 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 + 906 + fn has_type_or_universal(selectors: &[SimpleSelector]) -> bool { 907 + selectors 908 + .iter() 909 + .any(|s| matches!(s, SimpleSelector::Type(_) | SimpleSelector::Universal)) 910 + } 911 + 912 + fn 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)] 945 + mod 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 + }