Something went wrong. Try again.
we (web engine): Experimental web browser project to understand the limits of Claude
Something went wrong. Try again.
Rust
at url-parser
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478//! 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() }
// -- 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); }}