Something went wrong. Try again.
we (web engine): Experimental web browser project to understand the limits of Claude
Something went wrong. Try again.
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485//! CSS parser per CSS Syntax Module Level 3 §5.//!//! Consumes tokens from the tokenizer and produces a structured stylesheet.
use crate::tokenizer::{HashType, NumericType, Token, Tokenizer};
// ---------------------------------------------------------------------------// AST types// ---------------------------------------------------------------------------
/// A parsed CSS stylesheet: a list of rules.#[derive(Debug, Clone, PartialEq)]pub struct Stylesheet { pub rules: Vec<Rule>,}
/// A top-level rule: either a style rule or an at-rule.#[derive(Debug, Clone, PartialEq)]pub enum Rule { Style(StyleRule), Media(MediaRule), Import(ImportRule),}
/// A style rule: selector list + declarations.#[derive(Debug, Clone, PartialEq)]pub struct StyleRule { pub selectors: SelectorList, pub declarations: Vec<Declaration>,}
/// A `@media` rule with a prelude (media query text) and nested rules.#[derive(Debug, Clone, PartialEq)]pub struct MediaRule { pub query: String, pub rules: Vec<Rule>,}
/// An `@import` rule with a URL.#[derive(Debug, Clone, PartialEq)]pub struct ImportRule { pub url: String,}
/// A comma-separated list of selectors.#[derive(Debug, Clone, PartialEq)]pub struct SelectorList { pub selectors: Vec<Selector>,}
/// A complex selector: a chain of compound selectors joined by combinators.#[derive(Debug, Clone, PartialEq)]pub struct Selector { pub components: Vec<SelectorComponent>,}
/// Either a compound selector or a combinator between compounds.#[derive(Debug, Clone, PartialEq)]pub enum SelectorComponent { Compound(CompoundSelector), Combinator(Combinator),}
/// A compound selector: a sequence of simple selectors with no combinator.#[derive(Debug, Clone, PartialEq)]pub struct CompoundSelector { pub simple: Vec<SimpleSelector>,}
/// A simple selector.#[derive(Debug, Clone, PartialEq)]pub enum SimpleSelector { Type(String), Universal, Class(String), Id(String), Attribute(AttributeSelector), PseudoClass(String),}
/// An attribute selector.#[derive(Debug, Clone, PartialEq)]pub struct AttributeSelector { pub name: String, pub op: Option<AttributeOp>, pub value: Option<String>,}
/// Attribute matching operator.#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub enum AttributeOp { /// `=` Exact, /// `~=` Includes, /// `|=` DashMatch, /// `^=` Prefix, /// `$=` Suffix, /// `*=` Substring,}
/// A combinator between compound selectors.#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub enum Combinator { /// Descendant (whitespace) Descendant, /// Child (`>`) Child, /// Adjacent sibling (`+`) AdjacentSibling, /// General sibling (`~`) GeneralSibling,}
/// A CSS property declaration.#[derive(Debug, Clone, PartialEq)]pub struct Declaration { pub property: String, pub value: Vec<ComponentValue>, pub important: bool,}
/// A component value in a declaration value. We store these as typed tokens/// so downstream code can interpret them without re-tokenizing.#[derive(Debug, Clone, PartialEq)]pub enum ComponentValue { Ident(String), String(String), Number(f64, NumericType), Percentage(f64), Dimension(f64, NumericType, String), Hash(String, HashType), Function(String, Vec<ComponentValue>), Delim(char), Comma, Whitespace,}
// ---------------------------------------------------------------------------// Parser// ---------------------------------------------------------------------------
/// CSS parser. Consumes tokens and produces AST.pub struct Parser { tokens: Vec<Token>, pos: usize,}
impl Parser { /// Parse a full stylesheet from CSS source text. pub fn parse(input: &str) -> Stylesheet { let tokens = Tokenizer::tokenize(input); let mut parser = Self { tokens, pos: 0 }; parser.parse_stylesheet() }
/// Parse a list of declarations (for `style` attribute values). pub fn parse_declarations(input: &str) -> Vec<Declaration> { let tokens = Tokenizer::tokenize(input); let mut parser = Self { tokens, pos: 0 }; parser.parse_declaration_list() }
/// Parse a selector list from a string (for `querySelector`/`querySelectorAll`). pub fn parse_selectors(input: &str) -> SelectorList { let tokens = Tokenizer::tokenize(input); let mut parser = Self { tokens, pos: 0 }; parser.parse_selector_list() }
// -- Token access -------------------------------------------------------
fn peek(&self) -> &Token { self.tokens.get(self.pos).unwrap_or(&Token::Eof) }
fn advance(&mut self) -> Token { let tok = self.tokens.get(self.pos).cloned().unwrap_or(Token::Eof); if self.pos < self.tokens.len() { self.pos += 1; } tok }
fn is_eof(&self) -> bool { self.pos >= self.tokens.len() }
fn skip_whitespace(&mut self) { while matches!(self.peek(), Token::Whitespace) { self.advance(); } }
// -- Stylesheet parsing -------------------------------------------------
fn parse_stylesheet(&mut self) -> Stylesheet { let mut rules = Vec::new(); loop { self.skip_whitespace(); if self.is_eof() { break; } match self.peek() { Token::AtKeyword(_) => { if let Some(rule) = self.parse_at_rule() { rules.push(rule); } } Token::Cdo | Token::Cdc => { self.advance(); } _ => { if let Some(rule) = self.parse_style_rule() { rules.push(Rule::Style(rule)); } } } } Stylesheet { rules } }
// -- At-rules -----------------------------------------------------------
fn parse_at_rule(&mut self) -> Option<Rule> { let name = match self.advance() { Token::AtKeyword(name) => name, _ => return None, };
match name.to_ascii_lowercase().as_str() { "media" => self.parse_media_rule(), "import" => self.parse_import_rule(), _ => { // Unknown at-rule: skip to end of block or semicolon self.skip_at_rule_body(); None } } }
fn parse_media_rule(&mut self) -> Option<Rule> { self.skip_whitespace();
// Collect prelude tokens as the media query string let mut query = String::new(); loop { match self.peek() { Token::LeftBrace | Token::Eof => break, Token::Whitespace => { if !query.is_empty() { query.push(' '); } self.advance(); } _ => { let tok = self.advance(); query.push_str(&token_to_string(&tok)); } } }
// Expect `{` if !matches!(self.peek(), Token::LeftBrace) { return None; } self.advance();
// Parse nested rules until `}` let mut rules = Vec::new(); loop { self.skip_whitespace(); if self.is_eof() { break; } if matches!(self.peek(), Token::RightBrace) { self.advance(); break; } match self.peek() { Token::AtKeyword(_) => { if let Some(rule) = self.parse_at_rule() { rules.push(rule); } } _ => { if let Some(rule) = self.parse_style_rule() { rules.push(Rule::Style(rule)); } } } }
Some(Rule::Media(MediaRule { query: query.trim().to_string(), rules, })) }
fn parse_import_rule(&mut self) -> Option<Rule> { self.skip_whitespace(); let url = match self.peek() { Token::String(_) => { if let Token::String(s) = self.advance() { s } else { unreachable!() } } Token::Url(_) => { if let Token::Url(s) = self.advance() { s } else { unreachable!() } } Token::Function(name) if name.eq_ignore_ascii_case("url") => { self.advance(); self.skip_whitespace(); let url = match self.advance() { Token::String(s) => s, _ => { self.skip_to_semicolon(); return None; } }; self.skip_whitespace(); // consume closing paren if matches!(self.peek(), Token::RightParen) { self.advance(); } url } _ => { self.skip_to_semicolon(); return None; } }; // Consume optional trailing tokens until semicolon self.skip_to_semicolon(); Some(Rule::Import(ImportRule { url })) }
fn skip_at_rule_body(&mut self) { let mut brace_depth = 0; loop { match self.peek() { Token::Eof => return, Token::Semicolon if brace_depth == 0 => { self.advance(); return; } Token::LeftBrace => { brace_depth += 1; self.advance(); } Token::RightBrace => { if brace_depth == 0 { self.advance(); return; } brace_depth -= 1; self.advance(); if brace_depth == 0 { return; } } _ => { self.advance(); } } } }
fn skip_to_semicolon(&mut self) { loop { match self.peek() { Token::Eof | Token::Semicolon => { if matches!(self.peek(), Token::Semicolon) { self.advance(); } return; } _ => { self.advance(); } } } }
// -- Style rules --------------------------------------------------------
fn parse_style_rule(&mut self) -> Option<StyleRule> { let selectors = self.parse_selector_list();
// Expect `{` self.skip_whitespace(); if !matches!(self.peek(), Token::LeftBrace) { // Error recovery: skip to end of block or next rule self.skip_at_rule_body(); return None; } self.advance();
let declarations = self.parse_declaration_list_until_brace();
// Consume `}` if matches!(self.peek(), Token::RightBrace) { self.advance(); }
if selectors.selectors.is_empty() { return None; }
Some(StyleRule { selectors, declarations, }) }
// -- Selector parsing ---------------------------------------------------
fn parse_selector_list(&mut self) -> SelectorList { let mut selectors = Vec::new(); self.skip_whitespace();
if let Some(sel) = self.parse_selector() { selectors.push(sel); }
loop { self.skip_whitespace(); if !matches!(self.peek(), Token::Comma) { break; } self.advance(); // consume comma self.skip_whitespace(); if let Some(sel) = self.parse_selector() { selectors.push(sel); } }
SelectorList { selectors } }
fn parse_selector(&mut self) -> Option<Selector> { let mut components = Vec::new(); let mut last_was_compound = false; let mut had_whitespace = false;
loop { // Check for end of selector match self.peek() { Token::Comma | Token::LeftBrace | Token::RightBrace | Token::Semicolon | Token::Eof => break, Token::Whitespace => { had_whitespace = true; self.advance(); continue; } _ => {} }
// Check for explicit combinator let combinator = match self.peek() { Token::Delim('>') => { self.advance(); Some(Combinator::Child) } Token::Delim('+') => { self.advance(); Some(Combinator::AdjacentSibling) } Token::Delim('~') => { self.advance(); Some(Combinator::GeneralSibling) } _ => None, };
if let Some(comb) = combinator { if last_was_compound { components.push(SelectorComponent::Combinator(comb)); last_was_compound = false; had_whitespace = false; } self.skip_whitespace(); continue; }
// Implicit descendant combinator if there was whitespace if had_whitespace && last_was_compound { components.push(SelectorComponent::Combinator(Combinator::Descendant)); had_whitespace = false; }
// Parse compound selector if let Some(compound) = self.parse_compound_selector() { components.push(SelectorComponent::Compound(compound)); last_was_compound = true; } else { break; } }
if components.is_empty() { None } else { Some(Selector { components }) } }
fn parse_compound_selector(&mut self) -> Option<CompoundSelector> { let mut simple = Vec::new();
loop { match self.peek() { // Type or universal Token::Ident(_) if simple.is_empty() || !has_type_or_universal(&simple) => { if let Token::Ident(name) = self.advance() { simple.push(SimpleSelector::Type(name.to_ascii_lowercase())); } } Token::Delim('*') if simple.is_empty() || !has_type_or_universal(&simple) => { self.advance(); simple.push(SimpleSelector::Universal); } // Class Token::Delim('.') => { self.advance(); match self.advance() { Token::Ident(name) => simple.push(SimpleSelector::Class(name)), _ => break, } } // ID Token::Hash(_, HashType::Id) => { if let Token::Hash(name, _) = self.advance() { simple.push(SimpleSelector::Id(name)); } } // Attribute Token::LeftBracket => { self.advance(); if let Some(attr) = self.parse_attribute_selector() { simple.push(SimpleSelector::Attribute(attr)); } } // Pseudo-class Token::Colon => { self.advance(); match self.advance() { Token::Ident(name) => { simple.push(SimpleSelector::PseudoClass(name.to_ascii_lowercase())) } Token::Function(name) => { // Parse pseudo-class with arguments: skip to closing paren let mut pname = name.to_ascii_lowercase(); pname.push('('); let mut depth = 1; loop { match self.peek() { Token::Eof => break, Token::LeftParen => { depth += 1; pname.push('('); self.advance(); } Token::RightParen => { depth -= 1; if depth == 0 { self.advance(); break; } pname.push(')'); self.advance(); } _ => { let tok = self.advance(); pname.push_str(&token_to_string(&tok)); } } } pname.push(')'); simple.push(SimpleSelector::PseudoClass(pname)); } _ => break, } } // Ident after already having a type selector → new compound starts Token::Ident(_) => break, _ => break, } }
if simple.is_empty() { None } else { Some(CompoundSelector { simple }) } }
fn parse_attribute_selector(&mut self) -> Option<AttributeSelector> { self.skip_whitespace();
let name = match self.advance() { Token::Ident(name) => name.to_ascii_lowercase(), _ => { self.skip_to_bracket_close(); return None; } };
self.skip_whitespace();
// Check for close bracket (presence-only selector) if matches!(self.peek(), Token::RightBracket) { self.advance(); return Some(AttributeSelector { name, op: None, value: None, }); }
// Parse operator let op = match self.peek() { Token::Delim('=') => { self.advance(); AttributeOp::Exact } Token::Delim('~') => { self.advance(); if matches!(self.peek(), Token::Delim('=')) { self.advance(); } AttributeOp::Includes } Token::Delim('|') => { self.advance(); if matches!(self.peek(), Token::Delim('=')) { self.advance(); } AttributeOp::DashMatch } Token::Delim('^') => { self.advance(); if matches!(self.peek(), Token::Delim('=')) { self.advance(); } AttributeOp::Prefix } Token::Delim('$') => { self.advance(); if matches!(self.peek(), Token::Delim('=')) { self.advance(); } AttributeOp::Suffix } Token::Delim('*') => { self.advance(); if matches!(self.peek(), Token::Delim('=')) { self.advance(); } AttributeOp::Substring } _ => { self.skip_to_bracket_close(); return None; } };
self.skip_whitespace();
// Parse value let value = match self.advance() { Token::Ident(v) => v, Token::String(v) => v, _ => { self.skip_to_bracket_close(); return None; } };
self.skip_whitespace();
// Consume closing bracket if matches!(self.peek(), Token::RightBracket) { self.advance(); }
Some(AttributeSelector { name, op: Some(op), value: Some(value), }) }
fn skip_to_bracket_close(&mut self) { loop { match self.peek() { Token::RightBracket | Token::Eof => { if matches!(self.peek(), Token::RightBracket) { self.advance(); } return; } _ => { self.advance(); } } } }
// -- Declaration parsing ------------------------------------------------
fn parse_declaration_list(&mut self) -> Vec<Declaration> { let mut declarations = Vec::new(); loop { self.skip_whitespace(); if self.is_eof() { break; } if matches!(self.peek(), Token::Semicolon) { self.advance(); continue; } if let Some(decl) = self.parse_declaration() { declarations.push(decl); } } declarations }
fn parse_declaration_list_until_brace(&mut self) -> Vec<Declaration> { let mut declarations = Vec::new(); loop { self.skip_whitespace(); if self.is_eof() || matches!(self.peek(), Token::RightBrace) { break; } if matches!(self.peek(), Token::Semicolon) { self.advance(); continue; } if let Some(decl) = self.parse_declaration() { declarations.push(decl); } } declarations }
fn parse_declaration(&mut self) -> Option<Declaration> { // Property name let property = match self.peek() { Token::Ident(_) => { if let Token::Ident(name) = self.advance() { name.to_ascii_lowercase() } else { unreachable!() } } _ => { // Error recovery: skip to next semicolon or closing brace self.skip_declaration_error(); return None; } };
self.skip_whitespace();
// Expect colon if !matches!(self.peek(), Token::Colon) { self.skip_declaration_error(); return None; } self.advance();
self.skip_whitespace();
// Parse value let (value, important) = self.parse_declaration_value();
if value.is_empty() { return None; }
Some(Declaration { property, value, important, }) }
fn parse_declaration_value(&mut self) -> (Vec<ComponentValue>, bool) { let mut values = Vec::new(); let mut important = false;
loop { match self.peek() { Token::Semicolon | Token::RightBrace | Token::Eof => break, Token::Whitespace => { self.advance(); // Only add whitespace if there are already values and we're // not at the end of the declaration if !values.is_empty() && !matches!( self.peek(), Token::Semicolon | Token::RightBrace | Token::Eof ) { values.push(ComponentValue::Whitespace); } } Token::Delim('!') => { self.advance(); self.skip_whitespace(); if let Token::Ident(ref s) = self.peek() { if s.eq_ignore_ascii_case("important") { important = true; self.advance(); } } } _ => { if let Some(cv) = self.parse_component_value() { values.push(cv); } } } }
// Trim trailing whitespace while matches!(values.last(), Some(ComponentValue::Whitespace)) { values.pop(); }
(values, important) }
fn parse_component_value(&mut self) -> Option<ComponentValue> { match self.advance() { Token::Ident(s) => Some(ComponentValue::Ident(s)), Token::String(s) => Some(ComponentValue::String(s)), Token::Number(n, t) => Some(ComponentValue::Number(n, t)), Token::Percentage(n) => Some(ComponentValue::Percentage(n)), Token::Dimension(n, t, u) => Some(ComponentValue::Dimension(n, t, u)), Token::Hash(s, ht) => Some(ComponentValue::Hash(s, ht)), Token::Comma => Some(ComponentValue::Comma), Token::Delim(c) => Some(ComponentValue::Delim(c)), Token::Function(name) => { let args = self.parse_function_args(); Some(ComponentValue::Function(name, args)) } _ => None, } }
fn parse_function_args(&mut self) -> Vec<ComponentValue> { let mut args = Vec::new(); loop { match self.peek() { Token::RightParen | Token::Eof => { if matches!(self.peek(), Token::RightParen) { self.advance(); } break; } Token::Whitespace => { self.advance(); if !args.is_empty() && !matches!(self.peek(), Token::RightParen | Token::Eof) { args.push(ComponentValue::Whitespace); } } _ => { if let Some(cv) = self.parse_component_value() { args.push(cv); } } } } args }
fn skip_declaration_error(&mut self) { loop { match self.peek() { Token::Semicolon => { self.advance(); return; } Token::RightBrace | Token::Eof => return, _ => { self.advance(); } } } }}
// ---------------------------------------------------------------------------// Helpers// ---------------------------------------------------------------------------
fn has_type_or_universal(selectors: &[SimpleSelector]) -> bool { selectors .iter() .any(|s| matches!(s, SimpleSelector::Type(_) | SimpleSelector::Universal))}
fn token_to_string(token: &Token) -> String { match token { Token::Ident(s) => s.clone(), Token::Function(s) => format!("{s}("), Token::AtKeyword(s) => format!("@{s}"), Token::Hash(s, _) => format!("#{s}"), Token::String(s) => format!("\"{s}\""), Token::Url(s) => format!("url({s})"), Token::Number(n, _) => format!("{n}"), Token::Percentage(n) => format!("{n}%"), Token::Dimension(n, _, u) => format!("{n}{u}"), Token::Whitespace => " ".to_string(), Token::Colon => ":".to_string(), Token::Semicolon => ";".to_string(), Token::Comma => ",".to_string(), Token::LeftBracket => "[".to_string(), Token::RightBracket => "]".to_string(), Token::LeftParen => "(".to_string(), Token::RightParen => ")".to_string(), Token::LeftBrace => "{".to_string(), Token::RightBrace => "}".to_string(), Token::Delim(c) => c.to_string(), Token::Cdo => "<!--".to_string(), Token::Cdc => "-->".to_string(), Token::BadString | Token::BadUrl | Token::Eof => String::new(), }}
// ---------------------------------------------------------------------------// Tests// ---------------------------------------------------------------------------
#[cfg(test)]mod tests { use super::*;
// -- Selector tests -----------------------------------------------------
#[test] fn test_type_selector() { let ss = Parser::parse("div { }"); assert_eq!(ss.rules.len(), 1); let rule = match &ss.rules[0] { Rule::Style(r) => r, _ => panic!("expected style rule"), }; assert_eq!(rule.selectors.selectors.len(), 1); let sel = &rule.selectors.selectors[0]; assert_eq!(sel.components.len(), 1); match &sel.components[0] { SelectorComponent::Compound(c) => { assert_eq!(c.simple.len(), 1); assert_eq!(c.simple[0], SimpleSelector::Type("div".into())); } _ => panic!("expected compound"), } }
#[test] fn test_universal_selector() { let ss = Parser::parse("* { }"); let rule = match &ss.rules[0] { Rule::Style(r) => r, _ => panic!("expected style rule"), }; let sel = &rule.selectors.selectors[0]; match &sel.components[0] { SelectorComponent::Compound(c) => { assert_eq!(c.simple[0], SimpleSelector::Universal); } _ => panic!("expected compound"), } }
#[test] fn test_class_selector() { let ss = Parser::parse(".foo { }"); let rule = match &ss.rules[0] { Rule::Style(r) => r, _ => panic!("expected style rule"), }; let sel = &rule.selectors.selectors[0]; match &sel.components[0] { SelectorComponent::Compound(c) => { assert_eq!(c.simple[0], SimpleSelector::Class("foo".into())); } _ => panic!("expected compound"), } }
#[test] fn test_id_selector() { let ss = Parser::parse("#main { }"); let rule = match &ss.rules[0] { Rule::Style(r) => r, _ => panic!("expected style rule"), }; let sel = &rule.selectors.selectors[0]; match &sel.components[0] { SelectorComponent::Compound(c) => { assert_eq!(c.simple[0], SimpleSelector::Id("main".into())); } _ => panic!("expected compound"), } }
#[test] fn test_compound_selector() { let ss = Parser::parse("div.foo#bar { }"); let rule = match &ss.rules[0] { Rule::Style(r) => r, _ => panic!("expected style rule"), }; let sel = &rule.selectors.selectors[0]; match &sel.components[0] { SelectorComponent::Compound(c) => { assert_eq!(c.simple.len(), 3); assert_eq!(c.simple[0], SimpleSelector::Type("div".into())); assert_eq!(c.simple[1], SimpleSelector::Class("foo".into())); assert_eq!(c.simple[2], SimpleSelector::Id("bar".into())); } _ => panic!("expected compound"), } }
#[test] fn test_selector_list() { let ss = Parser::parse("h1, h2, h3 { }"); let rule = match &ss.rules[0] { Rule::Style(r) => r, _ => panic!("expected style rule"), }; assert_eq!(rule.selectors.selectors.len(), 3); }
#[test] fn test_descendant_combinator() { let ss = Parser::parse("div p { }"); let rule = match &ss.rules[0] { Rule::Style(r) => r, _ => panic!("expected style rule"), }; let sel = &rule.selectors.selectors[0]; assert_eq!(sel.components.len(), 3); assert!(matches!( sel.components[1], SelectorComponent::Combinator(Combinator::Descendant) )); }
#[test] fn test_child_combinator() { let ss = Parser::parse("div > p { }"); let rule = match &ss.rules[0] { Rule::Style(r) => r, _ => panic!("expected style rule"), }; let sel = &rule.selectors.selectors[0]; assert_eq!(sel.components.len(), 3); assert!(matches!( sel.components[1], SelectorComponent::Combinator(Combinator::Child) )); }
#[test] fn test_adjacent_sibling_combinator() { let ss = Parser::parse("h1 + p { }"); let rule = match &ss.rules[0] { Rule::Style(r) => r, _ => panic!("expected style rule"), }; let sel = &rule.selectors.selectors[0]; assert!(matches!( sel.components[1], SelectorComponent::Combinator(Combinator::AdjacentSibling) )); }
#[test] fn test_general_sibling_combinator() { let ss = Parser::parse("h1 ~ p { }"); let rule = match &ss.rules[0] { Rule::Style(r) => r, _ => panic!("expected style rule"), }; let sel = &rule.selectors.selectors[0]; assert!(matches!( sel.components[1], SelectorComponent::Combinator(Combinator::GeneralSibling) )); }
#[test] fn test_attribute_presence() { let ss = Parser::parse("[disabled] { }"); let rule = match &ss.rules[0] { Rule::Style(r) => r, _ => panic!("expected style rule"), }; let sel = &rule.selectors.selectors[0]; match &sel.components[0] { SelectorComponent::Compound(c) => match &c.simple[0] { SimpleSelector::Attribute(attr) => { assert_eq!(attr.name, "disabled"); assert!(attr.op.is_none()); assert!(attr.value.is_none()); } _ => panic!("expected attribute selector"), }, _ => panic!("expected compound"), } }
#[test] fn test_attribute_exact() { let ss = Parser::parse("[type=\"text\"] { }"); let rule = match &ss.rules[0] { Rule::Style(r) => r, _ => panic!("expected style rule"), }; let sel = &rule.selectors.selectors[0]; match &sel.components[0] { SelectorComponent::Compound(c) => match &c.simple[0] { SimpleSelector::Attribute(attr) => { assert_eq!(attr.name, "type"); assert_eq!(attr.op, Some(AttributeOp::Exact)); assert_eq!(attr.value, Some("text".into())); } _ => panic!("expected attribute selector"), }, _ => panic!("expected compound"), } }
#[test] fn test_attribute_operators() { let ops = vec![ ("[a~=b] { }", AttributeOp::Includes), ("[a|=b] { }", AttributeOp::DashMatch), ("[a^=b] { }", AttributeOp::Prefix), ("[a$=b] { }", AttributeOp::Suffix), ("[a*=b] { }", AttributeOp::Substring), ]; for (input, expected_op) in ops { let ss = Parser::parse(input); let rule = match &ss.rules[0] { Rule::Style(r) => r, _ => panic!("expected style rule"), }; let sel = &rule.selectors.selectors[0]; match &sel.components[0] { SelectorComponent::Compound(c) => match &c.simple[0] { SimpleSelector::Attribute(attr) => { assert_eq!(attr.op, Some(expected_op), "failed for {input}"); } _ => panic!("expected attribute selector"), }, _ => panic!("expected compound"), } } }
#[test] fn test_pseudo_class() { let ss = Parser::parse("a:hover { }"); let rule = match &ss.rules[0] { Rule::Style(r) => r, _ => panic!("expected style rule"), }; let sel = &rule.selectors.selectors[0]; match &sel.components[0] { SelectorComponent::Compound(c) => { assert_eq!(c.simple.len(), 2); assert_eq!(c.simple[0], SimpleSelector::Type("a".into())); assert_eq!(c.simple[1], SimpleSelector::PseudoClass("hover".into())); } _ => panic!("expected compound"), } }
#[test] fn test_pseudo_class_first_child() { let ss = Parser::parse("p:first-child { }"); let rule = match &ss.rules[0] { Rule::Style(r) => r, _ => panic!("expected style rule"), }; let sel = &rule.selectors.selectors[0]; match &sel.components[0] { SelectorComponent::Compound(c) => { assert_eq!( c.simple[1], SimpleSelector::PseudoClass("first-child".into()) ); } _ => panic!("expected compound"), } }
// -- Declaration tests --------------------------------------------------
#[test] fn test_simple_declaration() { let ss = Parser::parse("p { color: red; }"); let rule = match &ss.rules[0] { Rule::Style(r) => r, _ => panic!("expected style rule"), }; assert_eq!(rule.declarations.len(), 1); assert_eq!(rule.declarations[0].property, "color"); assert_eq!(rule.declarations[0].value.len(), 1); assert_eq!( rule.declarations[0].value[0], ComponentValue::Ident("red".into()) ); assert!(!rule.declarations[0].important); }
#[test] fn test_multiple_declarations() { let ss = Parser::parse("p { color: red; font-size: 16px; }"); let rule = match &ss.rules[0] { Rule::Style(r) => r, _ => panic!("expected style rule"), }; assert_eq!(rule.declarations.len(), 2); assert_eq!(rule.declarations[0].property, "color"); assert_eq!(rule.declarations[1].property, "font-size"); }
#[test] fn test_important_declaration() { let ss = Parser::parse("p { color: red !important; }"); let rule = match &ss.rules[0] { Rule::Style(r) => r, _ => panic!("expected style rule"), }; assert!(rule.declarations[0].important); }
#[test] fn test_declaration_with_function() { let ss = Parser::parse("p { color: rgb(255, 0, 0); }"); let rule = match &ss.rules[0] { Rule::Style(r) => r, _ => panic!("expected style rule"), }; assert_eq!(rule.declarations[0].property, "color"); match &rule.declarations[0].value[0] { ComponentValue::Function(name, args) => { assert_eq!(name, "rgb"); // 255, 0, 0 → Number, Comma, WS, Number, Comma, WS, Number assert!(!args.is_empty()); } _ => panic!("expected function value"), } }
#[test] fn test_declaration_with_hash_color() { let ss = Parser::parse("p { color: #ff0000; }"); let rule = match &ss.rules[0] { Rule::Style(r) => r, _ => panic!("expected style rule"), }; assert_eq!( rule.declarations[0].value[0], ComponentValue::Hash("ff0000".into(), HashType::Id) ); }
#[test] fn test_declaration_with_dimension() { let ss = Parser::parse("p { margin: 10px; }"); let rule = match &ss.rules[0] { Rule::Style(r) => r, _ => panic!("expected style rule"), }; assert_eq!( rule.declarations[0].value[0], ComponentValue::Dimension(10.0, NumericType::Integer, "px".into()) ); }
#[test] fn test_declaration_with_percentage() { let ss = Parser::parse("p { width: 50%; }"); let rule = match &ss.rules[0] { Rule::Style(r) => r, _ => panic!("expected style rule"), }; assert_eq!( rule.declarations[0].value[0], ComponentValue::Percentage(50.0) ); }
#[test] fn test_parse_inline_style() { let decls = Parser::parse_declarations("color: red; font-size: 16px"); assert_eq!(decls.len(), 2); assert_eq!(decls[0].property, "color"); assert_eq!(decls[1].property, "font-size"); }
// -- Error recovery tests -----------------------------------------------
#[test] fn test_invalid_declaration_skipped() { let ss = Parser::parse("p { ??? ; color: red; }"); let rule = match &ss.rules[0] { Rule::Style(r) => r, _ => panic!("expected style rule"), }; // The invalid declaration should be skipped, color should remain assert_eq!(rule.declarations.len(), 1); assert_eq!(rule.declarations[0].property, "color"); }
#[test] fn test_missing_colon_skipped() { let ss = Parser::parse("p { color red; font-size: 16px; }"); let rule = match &ss.rules[0] { Rule::Style(r) => r, _ => panic!("expected style rule"), }; assert_eq!(rule.declarations.len(), 1); assert_eq!(rule.declarations[0].property, "font-size"); }
#[test] fn test_empty_stylesheet() { let ss = Parser::parse(""); assert_eq!(ss.rules.len(), 0); }
#[test] fn test_empty_rule() { let ss = Parser::parse("p { }"); let rule = match &ss.rules[0] { Rule::Style(r) => r, _ => panic!("expected style rule"), }; assert_eq!(rule.declarations.len(), 0); }
#[test] fn test_multiple_rules() { let ss = Parser::parse("h1 { color: blue; } p { color: red; }"); assert_eq!(ss.rules.len(), 2); }
// -- @-rule tests -------------------------------------------------------
#[test] fn test_import_rule_string() { let ss = Parser::parse("@import \"style.css\";"); assert_eq!(ss.rules.len(), 1); match &ss.rules[0] { Rule::Import(r) => assert_eq!(r.url, "style.css"), _ => panic!("expected import rule"), } }
#[test] fn test_import_rule_url() { let ss = Parser::parse("@import url(style.css);"); assert_eq!(ss.rules.len(), 1); match &ss.rules[0] { Rule::Import(r) => assert_eq!(r.url, "style.css"), _ => panic!("expected import rule"), } }
#[test] fn test_media_rule() { let ss = Parser::parse("@media screen { p { color: red; } }"); assert_eq!(ss.rules.len(), 1); match &ss.rules[0] { Rule::Media(m) => { assert_eq!(m.query, "screen"); assert_eq!(m.rules.len(), 1); } _ => panic!("expected media rule"), } }
#[test] fn test_media_rule_complex_query() { let ss = Parser::parse("@media screen and (max-width: 600px) { p { font-size: 14px; } }"); match &ss.rules[0] { Rule::Media(m) => { assert!(m.query.contains("screen")); assert!(m.query.contains("max-width")); assert_eq!(m.rules.len(), 1); } _ => panic!("expected media rule"), } }
#[test] fn test_unknown_at_rule_skipped() { let ss = Parser::parse("@charset \"UTF-8\"; p { color: red; }"); assert_eq!(ss.rules.len(), 1); match &ss.rules[0] { Rule::Style(r) => assert_eq!(r.declarations[0].property, "color"), _ => panic!("expected style rule"), } }
// -- Integration tests --------------------------------------------------
#[test] fn test_real_css() { let css = r#" body { margin: 0; font-family: sans-serif; background-color: #fff; } h1 { color: #333; font-size: 24px; } .container { max-width: 960px; margin: 0 auto; } a:hover { color: blue; text-decoration: underline; } "#; let ss = Parser::parse(css); assert_eq!(ss.rules.len(), 4); }
#[test] fn test_declaration_no_trailing_semicolon() { let ss = Parser::parse("p { color: red }"); let rule = match &ss.rules[0] { Rule::Style(r) => r, _ => panic!("expected style rule"), }; assert_eq!(rule.declarations.len(), 1); assert_eq!(rule.declarations[0].property, "color"); }
#[test] fn test_multi_value_declaration() { let ss = Parser::parse("p { margin: 10px 20px 30px 40px; }"); let rule = match &ss.rules[0] { Rule::Style(r) => r, _ => panic!("expected style rule"), }; // 10px WS 20px WS 30px WS 40px assert_eq!(rule.declarations[0].value.len(), 7); }
#[test] fn test_cdo_cdc_ignored() { let ss = Parser::parse("<!-- p { color: red; } -->"); assert_eq!(ss.rules.len(), 1); }}