Something went wrong. Try again.
we (web engine): Experimental web browser project to understand the limits of Claude
Something went wrong. Try again.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543//! CSS parser per CSS Syntax Module Level 3 §5.//!//! Consumes tokens from the tokenizer and produces a structured stylesheet.
use crate::animations::{Keyframe, KeyframeSelector};use crate::media::{parse_media_query_list, MediaQueryList};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), Keyframes(KeyframesRule), FontFace(FontFaceRule),}
/// A `@keyframes` rule with a name and a list of keyframes.#[derive(Debug, Clone, PartialEq)]pub struct KeyframesRule { pub name: String, pub keyframes: Vec<Keyframe>,}
/// A style rule: selector list + declarations.#[derive(Debug, Clone, PartialEq)]pub struct StyleRule { pub selectors: SelectorList, pub declarations: Vec<Declaration>,}
/// A `@media` rule with parsed media queries and nested rules.#[derive(Debug, Clone, PartialEq)]pub struct MediaRule { pub queries: MediaQueryList, pub rules: Vec<Rule>,}
/// An `@import` rule with a URL.#[derive(Debug, Clone, PartialEq)]pub struct ImportRule { pub url: String,}
/// A `@font-face` rule describing a web font.#[derive(Debug, Clone, PartialEq)]pub struct FontFaceRule { /// The font family name declared for this font face. pub family: String, /// `src` descriptor: list of font sources. pub sources: Vec<FontFaceSource>, /// `font-weight`: numeric weight (100–900). Defaults to 400. pub weight: u16, /// `font-style`: normal, italic, or oblique. pub style: FontFaceStyle, /// `font-display`: controls rendering behavior during font load. pub display: FontDisplay,}
/// A single source in a `@font-face` `src` descriptor.#[derive(Debug, Clone, PartialEq)]pub struct FontFaceSource { /// Source value: either a URL to fetch or a local font face name. pub source: FontFaceSourceKind, /// Optional `format()` hint (e.g., "truetype", "opentype", "woff2"). pub format: Option<String>,}
/// The source kind for a single `@font-face src` entry.#[derive(Debug, Clone, PartialEq)]pub enum FontFaceSourceKind { /// A network or relative URL source. Url(String), /// A platform-local font face name from `local(...)`. Local(String),}
/// `font-style` descriptor values for `@font-face`.#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub enum FontFaceStyle { Normal, Italic, Oblique,}
/// `font-display` descriptor values for `@font-face`.#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub enum FontDisplay { Auto, Block, Swap, Fallback, Optional,}
/// 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), PseudoElement(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) -> bool { let mut skipped = false; while matches!(self.peek(), Token::Whitespace) { skipped = true; self.advance(); } skipped }
// -- 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(), "keyframes" | "-webkit-keyframes" | "-moz-keyframes" => self.parse_keyframes_rule(), "font-face" => self.parse_font_face_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 for structured media query parsing. let mut prelude_tokens = Vec::new(); loop { match self.peek() { Token::LeftBrace | Token::Eof => break, _ => { let tok = self.advance(); prelude_tokens.push(tok); } } }
let queries = parse_media_query_list(&prelude_tokens);
// 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 { queries, 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 parse_keyframes_rule(&mut self) -> Option<Rule> { self.skip_whitespace();
// Parse the animation name (identifier or string) let name = match self.peek() { Token::Ident(_) => { if let Token::Ident(s) = self.advance() { s } else { unreachable!() } } Token::String(_) => { if let Token::String(s) = self.advance() { s } else { unreachable!() } } _ => { self.skip_at_rule_body(); return None; } };
self.skip_whitespace();
// Expect `{` if !matches!(self.peek(), Token::LeftBrace) { self.skip_at_rule_body(); return None; } self.advance();
// Parse keyframe blocks let mut keyframes = Vec::new(); loop { self.skip_whitespace(); if self.is_eof() { break; } if matches!(self.peek(), Token::RightBrace) { self.advance(); break; }
if let Some(kf) = self.parse_keyframe_block() { keyframes.push(kf); } }
Some(Rule::Keyframes(KeyframesRule { name, keyframes })) }
fn parse_font_face_rule(&mut self) -> Option<Rule> { self.skip_whitespace();
// Expect `{` if !matches!(self.peek(), Token::LeftBrace) { self.skip_at_rule_body(); return None; } self.advance();
// Parse descriptors inside the block. let declarations = self.parse_declaration_list_until_brace();
// Consume `}` if matches!(self.peek(), Token::RightBrace) { self.advance(); }
// Extract font-family descriptor (required). let mut family = None; let mut sources = Vec::new(); let mut weight: u16 = 400; let mut style = FontFaceStyle::Normal; let mut display = FontDisplay::Auto;
for decl in &declarations { match decl.property.as_str() { "font-family" => { family = extract_font_face_family(&decl.value); } "src" => { sources = parse_font_face_src(&decl.value); } "font-weight" => { weight = parse_font_face_weight(&decl.value); } "font-style" => { style = parse_font_face_style(&decl.value); } "font-display" => { display = parse_font_face_display(&decl.value); } _ => {} } }
let family = family?; if sources.is_empty() { return None; }
Some(Rule::FontFace(FontFaceRule { family, sources, weight, style, display, })) }
fn parse_keyframe_block(&mut self) -> Option<Keyframe> { // Parse keyframe selectors (comma-separated: `from`, `to`, or percentages) let mut selectors = Vec::new(); loop { self.skip_whitespace(); match self.peek() { Token::LeftBrace | Token::Eof | Token::RightBrace => break, Token::Comma => { self.advance(); continue; } Token::Ident(s) if s.eq_ignore_ascii_case("from") => { selectors.push(KeyframeSelector::From); self.advance(); } Token::Ident(s) if s.eq_ignore_ascii_case("to") => { selectors.push(KeyframeSelector::To); self.advance(); } Token::Percentage(_) => { if let Token::Percentage(p) = self.advance() { selectors.push(KeyframeSelector::Percentage(p)); } } Token::Number(n, _) if *n == 0.0 => { // Allow bare `0` as 0% self.advance(); selectors.push(KeyframeSelector::Percentage(0.0)); // Check for optional `%` sign if matches!(self.peek(), Token::Delim('%')) { self.advance(); } } _ => { // Unknown token in keyframe selector — skip to next block self.skip_at_rule_body(); return None; } } }
if selectors.is_empty() { return None; }
// Expect `{` if !matches!(self.peek(), Token::LeftBrace) { return None; } self.advance();
let declarations = self.parse_declaration_list_until_brace();
// Consume `}` if matches!(self.peek(), Token::RightBrace) { self.advance(); }
Some(Keyframe { selectors, declarations, }) }
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); } else { return SelectorList { selectors }; }
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); } else { return SelectorList { selectors: Vec::new(), }; } }
SelectorList { selectors } }
fn parse_selector(&mut self) -> Option<Selector> { let mut components = Vec::new(); let mut expecting_compound = true;
loop { let had_whitespace = self.skip_whitespace();
// Check for end of selector. match self.peek() { Token::Comma | Token::LeftBrace | Token::RightBrace | Token::Semicolon | Token::Eof => break, _ => {} }
// 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 expecting_compound { return None; } components.push(SelectorComponent::Combinator(comb)); expecting_compound = true; self.skip_whitespace(); continue; }
// Implicit descendant combinator if there was whitespace. if had_whitespace && !expecting_compound && starts_compound_selector_token(self.peek()) { components.push(SelectorComponent::Combinator(Combinator::Descendant)); expecting_compound = true; }
// Two compounds without whitespace or an explicit combinator are malformed. // `divspan` is one ident, but forms such as `div*` should be rejected. if !expecting_compound { return None; }
// Parse compound selector. let compound = self.parse_compound_selector()?; components.push(SelectorComponent::Compound(compound)); expecting_compound = false; }
if components.is_empty() || expecting_compound { 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.peek() { Token::Ident(_) => { if let Token::Ident(name) = self.advance() { simple.push(SimpleSelector::Class(name)); } } _ => return None, } } // ID Token::Hash(_, HashType::Id) => { if let Token::Hash(name, _) = self.advance() { simple.push(SimpleSelector::Id(name)); } } // Attribute Token::LeftBracket => { self.advance(); let attr = self.parse_attribute_selector()?; simple.push(SimpleSelector::Attribute(attr)); } // Pseudo-class / pseudo-element Token::Colon => { self.advance(); let is_pseudo_element = if matches!(self.peek(), Token::Colon) { self.advance(); true } else { false }; match self.advance() { Token::Ident(name) => { let name = name.to_ascii_lowercase(); if is_pseudo_element || name == "before" || name == "after" { simple.push(SimpleSelector::PseudoElement(name)); } else { simple.push(SimpleSelector::PseudoClass(name)); } } 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)); } _ => return None, } } // 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(); self.consume_required_attribute_equals()?; AttributeOp::Includes } Token::Delim('|') => { self.advance(); self.consume_required_attribute_equals()?; AttributeOp::DashMatch } Token::Delim('^') => { self.advance(); self.consume_required_attribute_equals()?; AttributeOp::Prefix } Token::Delim('$') => { self.advance(); self.consume_required_attribute_equals()?; AttributeOp::Suffix } Token::Delim('*') => { self.advance(); self.consume_required_attribute_equals()?; 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 consume_required_attribute_equals(&mut self) -> Option<()> { if matches!(self.peek(), Token::Delim('=')) { self.advance(); Some(()) } else { self.skip_to_bracket_close(); None } }
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();
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); } } _ => { if let Some(cv) = self.parse_component_value() { values.push(cv); } } } }
// Trim trailing whitespace while matches!(values.last(), Some(ComponentValue::Whitespace)) { values.pop(); }
let important = strip_trailing_important(&mut values);
(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::Url(s) => Some(ComponentValue::Function( "url".into(), vec![ComponentValue::String(s)], )), 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 starts_compound_selector_token(token: &Token) -> bool { matches!( token, Token::Ident(_) | Token::Delim('*') | Token::Delim('.') | Token::Hash(_, HashType::Id) | Token::LeftBracket | Token::Colon )}
fn strip_trailing_important(values: &mut Vec<ComponentValue>) -> bool { let Some(last_non_ws) = values .iter() .rposition(|v| !matches!(v, ComponentValue::Whitespace)) else { return false; };
let ComponentValue::Ident(name) = &values[last_non_ws] else { return false; }; if !name.eq_ignore_ascii_case("important") { return false; }
let Some(bang_pos) = values[..last_non_ws] .iter() .rposition(|v| !matches!(v, ComponentValue::Whitespace)) else { return false; }; if !matches!(values[bang_pos], ComponentValue::Delim('!')) { return false; }
if values[bang_pos + 1..last_non_ws] .iter() .any(|v| !matches!(v, ComponentValue::Whitespace)) { return false; }
values.truncate(bang_pos); while matches!(values.last(), Some(ComponentValue::Whitespace)) { values.pop(); } true}
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(), }}
// ---------------------------------------------------------------------------// @font-face descriptor helpers// ---------------------------------------------------------------------------
/// Extract the family name from a `font-family` descriptor value.fn extract_font_face_family(values: &[ComponentValue]) -> Option<String> { extract_css_family_name(values)}
/// Parse the `src` descriptor into a list of font sources.////// Supports `url("...")`, `local("...")`, and optional `format("...")`/// syntax. Multiple sources are comma-separated.fn parse_font_face_src(values: &[ComponentValue]) -> Vec<FontFaceSource> { let mut sources = Vec::new(); let mut i = 0;
while i < values.len() { match &values[i] { ComponentValue::Function(name, args) if name.eq_ignore_ascii_case("url") => { let url = extract_string_from_args(args); i += 1; // Check for optional format() hint. let format = skip_ws_and_parse_format(values, &mut i); if let Some(url) = url { sources.push(FontFaceSource { source: FontFaceSourceKind::Url(url), format, }); } } ComponentValue::Function(name, args) if name.eq_ignore_ascii_case("local") => { let local_name = extract_string_from_args(args); i += 1; let format = skip_ws_and_parse_format(values, &mut i); if let Some(local_name) = local_name { sources.push(FontFaceSource { source: FontFaceSourceKind::Local(local_name), format, }); } } ComponentValue::Comma => { i += 1; } _ => { i += 1; } } }
sources}
/// Extract a string or ident from function arguments.fn extract_string_from_args(args: &[ComponentValue]) -> Option<String> { extract_css_family_name(args)}
fn extract_css_family_name(values: &[ComponentValue]) -> Option<String> { let mut parts = Vec::new(); let mut saw_ident = false;
for value in values { match value { ComponentValue::String(s) => { if !s.is_empty() { return Some(s.clone()); } } ComponentValue::Ident(s) => { if !s.is_empty() { parts.push(s.clone()); saw_ident = true; } } ComponentValue::Whitespace if saw_ident => {} ComponentValue::Comma if saw_ident => break, _ if saw_ident => break, _ => {} } }
if parts.is_empty() { None } else { Some(parts.join(" ")) }}
/// Skip whitespace tokens and try to parse a `format()` function.fn skip_ws_and_parse_format(values: &[ComponentValue], i: &mut usize) -> Option<String> { // Skip whitespace. while *i < values.len() && matches!(values[*i], ComponentValue::Whitespace) { *i += 1; } // Check for format(). if *i < values.len() { if let ComponentValue::Function(name, args) = &values[*i] { if name.eq_ignore_ascii_case("format") { *i += 1; return extract_string_from_args(args); } } } None}
/// Parse `font-weight` descriptor: `normal`, `bold`, or numeric (100–900).fn parse_font_face_weight(values: &[ComponentValue]) -> u16 { for v in values { match v { ComponentValue::Ident(s) if s.eq_ignore_ascii_case("normal") => return 400, ComponentValue::Ident(s) if s.eq_ignore_ascii_case("bold") => return 700, ComponentValue::Number(n, _) => { let w = *n as u16; return w.clamp(1, 1000); } _ => {} } } 400}
/// Parse `font-style` descriptor: `normal`, `italic`, `oblique`.fn parse_font_face_style(values: &[ComponentValue]) -> FontFaceStyle { for v in values { if let ComponentValue::Ident(s) = v { if s.eq_ignore_ascii_case("italic") { return FontFaceStyle::Italic; } if s.eq_ignore_ascii_case("oblique") { return FontFaceStyle::Oblique; } if s.eq_ignore_ascii_case("normal") { return FontFaceStyle::Normal; } } } FontFaceStyle::Normal}
/// Parse `font-display` descriptor.fn parse_font_face_display(values: &[ComponentValue]) -> FontDisplay { for v in values { if let ComponentValue::Ident(s) = v { match s.to_ascii_lowercase().as_str() { "auto" => return FontDisplay::Auto, "block" => return FontDisplay::Block, "swap" => return FontDisplay::Swap, "fallback" => return FontDisplay::Fallback, "optional" => return FontDisplay::Optional, _ => {} } } } FontDisplay::Auto}
// ---------------------------------------------------------------------------// 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_trailing_comma_rejects_rule() { let ss = Parser::parse("h1, { color: red; } p { color: blue; }"); assert_eq!(ss.rules.len(), 1); 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::Type("p".into())); } _ => panic!("expected compound"), } }
#[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_invalid_attribute_operator_rejects_rule() { let ss = Parser::parse("div[a~b] { color: red; } p { color: blue; }"); assert_eq!(ss.rules.len(), 1); 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::Type("p".into())); } _ => panic!("expected compound"), } }
#[test] fn test_invalid_attribute_selector_rejects_compound() { let ss = Parser::parse("div[=x] { color: red; } p { color: blue; }"); assert_eq!(ss.rules.len(), 1); 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::Type("p".into())); } _ => 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_element() { let ss = Parser::parse("div::before, span:after { }"); let rule = match &ss.rules[0] { Rule::Style(r) => r, _ => panic!("expected style rule"), };
let before = &rule.selectors.selectors[0]; match &before.components[0] { SelectorComponent::Compound(c) => { assert_eq!(c.simple[0], SimpleSelector::Type("div".into())); assert_eq!(c.simple[1], SimpleSelector::PseudoElement("before".into())); } _ => panic!("expected compound"), }
let after = &rule.selectors.selectors[1]; match &after.components[0] { SelectorComponent::Compound(c) => { assert_eq!(c.simple[0], SimpleSelector::Type("span".into())); assert_eq!(c.simple[1], SimpleSelector::PseudoElement("after".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"), } }
#[test] fn test_malformed_class_selector_rejects_rule() { let ss = Parser::parse("div..foo { color: red; } p { color: blue; }"); assert_eq!(ss.rules.len(), 1); 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::Type("p".into())); } _ => panic!("expected compound"), } }
#[test] fn test_leading_combinator_rejects_rule() { let ss = Parser::parse("> p { color: red; } p { color: blue; }"); assert_eq!(ss.rules.len(), 1); let rule = match &ss.rules[0] { Rule::Style(r) => r, _ => panic!("expected style rule"), }; assert_eq!(rule.declarations[0].property, "color"); assert_eq!( rule.declarations[0].value[0], ComponentValue::Ident("blue".into()) ); }
#[test] fn test_trailing_combinator_rejects_rule() { let ss = Parser::parse("div > { color: red; } p { color: blue; }"); assert_eq!(ss.rules.len(), 1); 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::Type("p".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_important_requires_trailing_flag() { let ss = Parser::parse("p { color: red !important blue; }"); let rule = match &ss.rules[0] { Rule::Style(r) => r, _ => panic!("expected style rule"), }; assert!(!rule.declarations[0].important); assert!(rule.declarations[0] .value .contains(&ComponentValue::Ident("important".into()))); }
#[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_unquoted_url() { let ss = Parser::parse("p { background-image: url(image.png); }"); let rule = match &ss.rules[0] { Rule::Style(r) => r, _ => panic!("expected style rule"), }; assert_eq!(rule.declarations[0].property, "background-image"); match &rule.declarations[0].value[0] { ComponentValue::Function(name, args) => { assert_eq!(name, "url"); assert_eq!( args.as_slice(), &[ComponentValue::String("image.png".into())] ); } _ => panic!("expected url 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() { use crate::media::{MediaType, Modifier}; 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.queries.queries.len(), 1); assert_eq!(m.queries.queries[0].media_type, MediaType::Screen); assert_eq!(m.queries.queries[0].modifier, Modifier::None); assert_eq!(m.rules.len(), 1); } _ => panic!("expected media rule"), } }
#[test] fn test_media_rule_complex_query() { use crate::media::{Comparison, MediaFeature, MediaType}; let ss = Parser::parse("@media screen and (max-width: 600px) { p { font-size: 14px; } }"); match &ss.rules[0] { Rule::Media(m) => { assert_eq!(m.queries.queries[0].media_type, MediaType::Screen); assert_eq!(m.queries.queries[0].features.len(), 1); assert_eq!( m.queries.queries[0].features[0], MediaFeature::Width(Comparison::Lte, 600.0) ); 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); }
// -- @keyframes tests --
#[test] fn test_keyframes_from_to() { let ss = Parser::parse("@keyframes slidein { from { opacity: 0; } to { opacity: 1; } }"); assert_eq!(ss.rules.len(), 1); let kf = match &ss.rules[0] { Rule::Keyframes(k) => k, _ => panic!("expected keyframes rule"), }; assert_eq!(kf.name, "slidein"); assert_eq!(kf.keyframes.len(), 2); assert_eq!(kf.keyframes[0].selectors, vec![KeyframeSelector::From]); assert_eq!(kf.keyframes[1].selectors, vec![KeyframeSelector::To]); assert_eq!(kf.keyframes[0].declarations.len(), 1); assert_eq!(kf.keyframes[0].declarations[0].property, "opacity"); }
#[test] fn test_keyframes_percentages() { let ss = Parser::parse( "@keyframes fade { 0% { opacity: 0; } 50% { opacity: 1; } 100% { opacity: 0.5; } }", ); let kf = match &ss.rules[0] { Rule::Keyframes(k) => k, _ => panic!("expected keyframes rule"), }; assert_eq!(kf.name, "fade"); assert_eq!(kf.keyframes.len(), 3); assert_eq!( kf.keyframes[0].selectors, vec![KeyframeSelector::Percentage(0.0)] ); assert_eq!( kf.keyframes[1].selectors, vec![KeyframeSelector::Percentage(50.0)] ); assert_eq!( kf.keyframes[2].selectors, vec![KeyframeSelector::Percentage(100.0)] ); }
#[test] fn test_keyframes_multiple_selectors() { let ss = Parser::parse("@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }"); let kf = match &ss.rules[0] { Rule::Keyframes(k) => k, _ => panic!("expected keyframes rule"), }; assert_eq!(kf.keyframes.len(), 2); assert_eq!( kf.keyframes[0].selectors, vec![ KeyframeSelector::Percentage(0.0), KeyframeSelector::Percentage(100.0), ] ); }
#[test] fn test_keyframes_string_name() { let ss = Parser::parse( "@keyframes \"my animation\" { from { color: red; } to { color: blue; } }", ); let kf = match &ss.rules[0] { Rule::Keyframes(k) => k, _ => panic!("expected keyframes rule"), }; assert_eq!(kf.name, "my animation"); }
#[test] fn test_keyframes_webkit_prefix() { let ss = Parser::parse("@-webkit-keyframes spin { from { opacity: 0; } to { opacity: 1; } }"); assert_eq!(ss.rules.len(), 1); let kf = match &ss.rules[0] { Rule::Keyframes(k) => k, _ => panic!("expected keyframes rule"), }; assert_eq!(kf.name, "spin"); }
#[test] fn test_keyframes_multiple_declarations() { let ss = Parser::parse( "@keyframes move { from { left: 0px; top: 0px; } to { left: 100px; top: 50px; } }", ); let kf = match &ss.rules[0] { Rule::Keyframes(k) => k, _ => panic!("expected keyframes rule"), }; assert_eq!(kf.keyframes[0].declarations.len(), 2); assert_eq!(kf.keyframes[1].declarations.len(), 2); }
#[test] fn test_keyframes_with_style_rules() { let ss = Parser::parse( r#" .box { color: red; } @keyframes fade { from { opacity: 0; } to { opacity: 1; } } p { font-size: 16px; } "#, ); assert_eq!(ss.rules.len(), 3); assert!(matches!(ss.rules[0], Rule::Style(_))); assert!(matches!(ss.rules[1], Rule::Keyframes(_))); assert!(matches!(ss.rules[2], Rule::Style(_))); }
// -- @font-face -----------------------------------------------------------
#[test] fn font_face_basic() { let ss = Parser::parse( r#"@font-face { font-family: "MyFont"; src: url("myfont.ttf"); }"#, ); assert_eq!(ss.rules.len(), 1); let ff = match &ss.rules[0] { Rule::FontFace(ff) => ff, _ => panic!("expected FontFace rule"), }; assert_eq!(ff.family, "MyFont"); assert_eq!(ff.sources.len(), 1); assert_eq!( ff.sources[0].source, FontFaceSourceKind::Url("myfont.ttf".to_string()) ); assert!(ff.sources[0].format.is_none()); assert_eq!(ff.weight, 400); assert_eq!(ff.style, FontFaceStyle::Normal); assert_eq!(ff.display, FontDisplay::Auto); }
#[test] fn font_face_all_descriptors() { let ss = Parser::parse( r#"@font-face { font-family: "WebFont"; src: url("webfont.woff2") format("woff2"); font-weight: 700; font-style: italic; font-display: swap; }"#, ); assert_eq!(ss.rules.len(), 1); let ff = match &ss.rules[0] { Rule::FontFace(ff) => ff, _ => panic!("expected FontFace rule"), }; assert_eq!(ff.family, "WebFont"); assert_eq!(ff.sources.len(), 1); assert_eq!( ff.sources[0].source, FontFaceSourceKind::Url("webfont.woff2".to_string()) ); assert_eq!(ff.sources[0].format.as_deref(), Some("woff2")); assert_eq!(ff.weight, 700); assert_eq!(ff.style, FontFaceStyle::Italic); assert_eq!(ff.display, FontDisplay::Swap); }
#[test] fn font_face_bold_keyword() { let ss = Parser::parse( r#"@font-face { font-family: "BoldFont"; src: url("bold.ttf"); font-weight: bold; }"#, ); let ff = match &ss.rules[0] { Rule::FontFace(ff) => ff, _ => panic!("expected FontFace rule"), }; assert_eq!(ff.weight, 700); }
#[test] fn font_face_normal_weight_keyword() { let ss = Parser::parse( r#"@font-face { font-family: "NormalFont"; src: url("normal.ttf"); font-weight: normal; }"#, ); let ff = match &ss.rules[0] { Rule::FontFace(ff) => ff, _ => panic!("expected FontFace rule"), }; assert_eq!(ff.weight, 400); }
#[test] fn font_face_oblique_style() { let ss = Parser::parse( r#"@font-face { font-family: "ObliqueFont"; src: url("oblique.ttf"); font-style: oblique; }"#, ); let ff = match &ss.rules[0] { Rule::FontFace(ff) => ff, _ => panic!("expected FontFace rule"), }; assert_eq!(ff.style, FontFaceStyle::Oblique); }
#[test] fn font_face_multiple_sources() { let ss = Parser::parse( r#"@font-face { font-family: "MultiFmt"; src: url("font.woff2") format("woff2"), url("font.ttf") format("truetype"); }"#, ); let ff = match &ss.rules[0] { Rule::FontFace(ff) => ff, _ => panic!("expected FontFace rule"), }; assert_eq!(ff.sources.len(), 2); assert_eq!( ff.sources[0].source, FontFaceSourceKind::Url("font.woff2".to_string()) ); assert_eq!(ff.sources[0].format.as_deref(), Some("woff2")); assert_eq!( ff.sources[1].source, FontFaceSourceKind::Url("font.ttf".to_string()) ); assert_eq!(ff.sources[1].format.as_deref(), Some("truetype")); }
#[test] fn font_face_local_source() { let ss = Parser::parse( r#"@font-face { font-family: "Fallback"; src: local("Arial Bold"), url("fallback.ttf") format("truetype"); font-weight: 700; }"#, ); let ff = match &ss.rules[0] { Rule::FontFace(ff) => ff, _ => panic!("expected FontFace rule"), }; assert_eq!(ff.sources.len(), 2); assert_eq!( ff.sources[0].source, FontFaceSourceKind::Local("Arial Bold".to_string()) ); assert!(ff.sources[0].format.is_none()); assert_eq!( ff.sources[1].source, FontFaceSourceKind::Url("fallback.ttf".to_string()) ); assert_eq!(ff.sources[1].format.as_deref(), Some("truetype")); assert_eq!(ff.weight, 700); }
#[test] fn font_face_display_values() { for (keyword, expected) in [ ("auto", FontDisplay::Auto), ("block", FontDisplay::Block), ("swap", FontDisplay::Swap), ("fallback", FontDisplay::Fallback), ("optional", FontDisplay::Optional), ] { let css = format!( r#"@font-face {{ font-family: "F"; src: url("f.ttf"); font-display: {}; }}"#, keyword ); let ss = Parser::parse(&css); let ff = match &ss.rules[0] { Rule::FontFace(ff) => ff, _ => panic!("expected FontFace rule"), }; assert_eq!(ff.display, expected, "font-display: {}", keyword); } }
#[test] fn font_face_missing_family_skipped() { let ss = Parser::parse( r#"@font-face { src: url("nofamily.ttf"); }"#, ); assert!( ss.rules.is_empty(), "should skip @font-face without font-family" ); }
#[test] fn font_face_missing_src_skipped() { let ss = Parser::parse( r#"@font-face { font-family: "NoSrc"; }"#, ); assert!(ss.rules.is_empty(), "should skip @font-face without src"); }
#[test] fn font_face_unquoted_family() { let ss = Parser::parse( r#"@font-face { font-family: CustomFont; src: url("custom.ttf"); }"#, ); assert_eq!(ss.rules.len(), 1); let ff = match &ss.rules[0] { Rule::FontFace(ff) => ff, _ => panic!("expected FontFace rule"), }; assert_eq!(ff.family, "CustomFont"); }
#[test] fn font_face_unquoted_multi_word_family() { let ss = Parser::parse( r#"@font-face { font-family: Be Vietnam Pro; src: url("be-vietnam-pro.woff"); }"#, ); assert_eq!(ss.rules.len(), 1); let ff = match &ss.rules[0] { Rule::FontFace(ff) => ff, _ => panic!("expected FontFace rule"), }; assert_eq!(ff.family, "Be Vietnam Pro"); }
#[test] fn font_face_unquoted_url_source() { let ss = Parser::parse( r#"@font-face { font-family: "PlainUrl"; src: url(plain.ttf); }"#, ); assert_eq!(ss.rules.len(), 1); let ff = match &ss.rules[0] { Rule::FontFace(ff) => ff, _ => panic!("expected FontFace rule"), }; assert_eq!(ff.sources.len(), 1); assert_eq!( ff.sources[0].source, FontFaceSourceKind::Url("plain.ttf".to_string()) ); }
#[test] fn font_face_unquoted_multi_word_local_source() { let ss = Parser::parse( r#"@font-face { font-family: "Fallback"; src: local(Arial Bold), url("fallback.ttf") format("truetype"); font-weight: 700; }"#, ); let ff = match &ss.rules[0] { Rule::FontFace(ff) => ff, _ => panic!("expected FontFace rule"), }; assert_eq!( ff.sources[0].source, FontFaceSourceKind::Local("Arial Bold".to_string()) ); assert_eq!( ff.sources[1].source, FontFaceSourceKind::Url("fallback.ttf".to_string()) ); }
#[test] fn font_face_among_other_rules() { let ss = Parser::parse( r#" body { color: black; } @font-face { font-family: "TestFont"; src: url("test.ttf"); } p { margin: 0; } "#, ); assert_eq!(ss.rules.len(), 3); assert!(matches!(ss.rules[0], Rule::Style(_))); assert!(matches!(ss.rules[1], Rule::FontFace(_))); assert!(matches!(ss.rules[2], Rule::Style(_))); }
#[test] fn font_face_numeric_weight() { let ss = Parser::parse( r#"@font-face { font-family: "W300"; src: url("w300.ttf"); font-weight: 300; }"#, ); let ff = match &ss.rules[0] { Rule::FontFace(ff) => ff, _ => panic!("expected FontFace rule"), }; assert_eq!(ff.weight, 300); }}