///|
/// CSS Selector Parser
/// Parses CSS tokens into Selector structures
///|
/// Parser state
priv struct SelectorParser {
tokens : Array[@token.Token]
mut pos : Int
}
///|
fn SelectorParser::new(tokens : Array[@token.Token]) -> SelectorParser {
{ tokens, pos: 0 }
}
///|
fn SelectorParser::peek(self : SelectorParser) -> @token.Token {
if self.pos >= self.tokens.length() {
// Return a safe fallback - this should not happen if tokenize() adds EOF
self.tokens[self.tokens.length() - 1]
} else {
self.tokens[self.pos]
}
}
///|
fn SelectorParser::is_at_end(self : SelectorParser) -> Bool {
self.pos >= self.tokens.length() ||
(match self.tokens[self.pos] {
EOF => true
_ => false
})
}
///|
fn SelectorParser::advance(self : SelectorParser) -> @token.Token {
let tok = self.peek()
self.pos = self.pos + 1
tok
}
///|
fn SelectorParser::skip_whitespace(self : SelectorParser) -> Unit {
while !self.is_at_end() {
match self.peek() {
Whitespace => {
let _ = self.advance()
}
_ => break
}
}
}
///|
/// Parse a selector list (comma-separated)
fn parse_selector_list(tokens : Array[@token.Token]) -> SelectorList? {
let parser = SelectorParser::new(tokens)
parser.skip_whitespace()
let selectors : Array[ComplexSelector] = []
// Parse first selector
match parser.parse_complex_selector() {
Some(sel) => selectors.push(sel)
None => return None
}
// Parse remaining selectors
while !parser.is_at_end() {
parser.skip_whitespace()
match parser.peek() {
Comma => {
let _ = parser.advance()
parser.skip_whitespace()
match parser.parse_complex_selector() {
Some(sel) => selectors.push(sel)
None => return None
}
}
_ => break
}
}
parser.skip_whitespace()
if !parser.is_at_end() {
return None
}
Some(SelectorList::{ selectors, })
}
///|
/// Parse a single selector (without commas)
fn parse_selector(tokens : Array[@token.Token]) -> ComplexSelector? {
let parser = SelectorParser::new(tokens)
parser.skip_whitespace()
match parser.parse_complex_selector() {
None => None
Some(sel) => {
parser.skip_whitespace()
if parser.is_at_end() {
Some(sel)
} else {
None
}
}
}
}
///|
fn SelectorParser::parse_complex_selector(
self : SelectorParser,
) -> ComplexSelector? {
// Parse the first compound selector
let first = match self.parse_compound_selector() {
None => return None
Some(f) => f
}
let tail : Array[ComplexSelectorStep] = []
let mut current = first
while !self.is_at_end() {
// Check for combinator (maybe preceded by whitespace)
let had_whitespace = match self.peek() {
Whitespace => {
let _ = self.advance()
true
}
_ => false
}
// Check for explicit combinator
let combinator : Combinator? = match self.peek() {
Delim('>') => {
let _ = self.advance()
self.skip_whitespace()
Some(Child)
}
Delim('+') => {
let _ = self.advance()
self.skip_whitespace()
Some(NextSibling)
}
Delim('~') => {
let _ = self.advance()
self.skip_whitespace()
Some(SubsequentSibling)
}
_ =>
if had_whitespace {
// Descendant combinator (whitespace)
Some(Descendant)
} else {
None
}
}
match combinator {
None => break
Some(comb) =>
// Try to parse next compound selector
match self.parse_compound_selector() {
None => return None
Some(next) => {
// Build chain: push current to tail with combinator, next becomes current
tail.push({ combinator: comb, selector: current })
current = next
}
}
}
}
// Reverse tail so it goes from right to left
let reversed_tail : Array[ComplexSelectorStep] = []
for i = tail.length() - 1; i >= 0; i = i - 1 {
reversed_tail.push(tail[i])
}
Some({ head: current, tail: reversed_tail })
}
///|
fn SelectorParser::parse_compound_selector(
self : SelectorParser,
) -> CompoundSelector? {
let mut type_selector : SimpleSelector? = None
let subclasses : Array[SimpleSelector] = []
// First, try to parse type selector or universal
match self.peek() {
Ident(name) => {
let _ = self.advance()
type_selector = Some(Type(name.to_lower()))
}
Delim('*') => {
let _ = self.advance()
type_selector = Some(Universal)
}
_ => ()
}
// Parse subclass selectors
while !self.is_at_end() {
match self.peek() {
// ID selector
Hash(id, _) => {
let _ = self.advance()
subclasses.push(Id(id))
}
// Class selector
Delim('.') => {
let _ = self.advance()
match self.peek() {
Ident(class_name) => {
let _ = self.advance()
subclasses.push(Class(class_name))
}
_ => break
}
}
// Attribute selector
LeftBracket =>
match self.parse_attribute_selector() {
Some(attr) => subclasses.push(Attribute(attr))
None => break
}
// Pseudo-class or pseudo-element
Colon => {
let _ = self.advance()
match self.peek() {
// Double colon - pseudo-element
Colon => {
let _ = self.advance()
match self.parse_pseudo_element() {
Some(pe) => subclasses.push(PseudoElement(pe))
None => return None
}
}
// Single colon - pseudo-class (or legacy pseudo-element)
_ =>
// First try to parse as pseudo-class
match self.parse_pseudo_class() {
Some(pc) => subclasses.push(PseudoClass(pc))
None =>
// Try legacy pseudo-element syntax (:before, :after)
match self.parse_pseudo_element() {
Some(pe) => subclasses.push(PseudoElement(pe))
None => return None
}
}
}
}
_ => break
}
}
// Must have at least one selector component
if type_selector is None && subclasses.is_empty() {
return None
}
Some({ type_selector, subclasses })
}
///|
fn SelectorParser::parse_attribute_selector(
self : SelectorParser,
) -> AttributeSelector? {
// Consume '['
match self.peek() {
LeftBracket => {
let _ = self.advance()
}
_ => return None
}
self.skip_whitespace()
// Get attribute name
let name = match self.peek() {
Ident(n) => {
let _ = self.advance()
n
}
_ => return None
}
self.skip_whitespace()
// Check for match operator or end
let match_type : AttributeMatch = match self.peek() {
RightBracket => {
let _ = self.advance()
return Some({ name, match_type: Exists, case_insensitive: false })
}
Delim('=') => {
let _ = self.advance()
self.skip_whitespace()
match self.parse_attr_value() {
Some(v) => Exact(v)
None => return None
}
}
Delim('~') => {
let _ = self.advance()
match self.peek() {
Delim('=') => {
let _ = self.advance()
}
_ => return None
}
self.skip_whitespace()
match self.parse_attr_value() {
Some(v) => Includes(v)
None => return None
}
}
Delim('|') => {
let _ = self.advance()
match self.peek() {
Delim('=') => {
let _ = self.advance()
}
_ => return None
}
self.skip_whitespace()
match self.parse_attr_value() {
Some(v) => DashMatch(v)
None => return None
}
}
Delim('^') => {
let _ = self.advance()
match self.peek() {
Delim('=') => {
let _ = self.advance()
}
_ => return None
}
self.skip_whitespace()
match self.parse_attr_value() {
Some(v) => Prefix(v)
None => return None
}
}
Delim('$') => {
let _ = self.advance()
match self.peek() {
Delim('=') => {
let _ = self.advance()
}
_ => return None
}
self.skip_whitespace()
match self.parse_attr_value() {
Some(v) => Suffix(v)
None => return None
}
}
Delim('*') => {
let _ = self.advance()
match self.peek() {
Delim('=') => {
let _ = self.advance()
}
_ => return None
}
self.skip_whitespace()
match self.parse_attr_value() {
Some(v) => Substring(v)
None => return None
}
}
_ => return None
}
self.skip_whitespace()
// Check for case-insensitive flag
let case_insensitive = match self.peek() {
Ident(flag) =>
if flag.to_lower() == "i" {
let _ = self.advance()
self.skip_whitespace()
true
} else if flag.to_lower() == "s" {
let _ = self.advance()
self.skip_whitespace()
false
} else {
false
}
_ => false
}
// Consume ']'
match self.peek() {
RightBracket => {
let _ = self.advance()
}
_ => return None
}
Some({ name, match_type, case_insensitive })
}
///|
fn SelectorParser::parse_attr_value(self : SelectorParser) -> String? {
match self.peek() {
Ident(v) => {
let _ = self.advance()
Some(v)
}
@token.Token::String(v) => {
let _ = self.advance()
Some(v)
}
_ => None
}
}
///|
fn SelectorParser::parse_pseudo_element(
self : SelectorParser,
) -> PseudoElement? {
match self.peek() {
Ident(name) => {
let _ = self.advance()
match name.to_lower() {
"before" => Some(Before)
"after" => Some(After)
"first-line" => Some(FirstLine)
"first-letter" => Some(FirstLetter)
"marker" => Some(Marker)
"placeholder" => Some(Placeholder)
"selection" => Some(Selection)
_ => None
}
}
_ => None
}
}
///|
fn SelectorParser::parse_pseudo_class(self : SelectorParser) -> PseudoClass? {
match self.peek() {
Ident(name) => {
// Check if it's a known pseudo-class BEFORE consuming the token
let result = match name.to_lower() {
"first-child" => Some(FirstChild)
"last-child" => Some(LastChild)
"only-child" => Some(OnlyChild)
"first-of-type" => Some(FirstOfType)
"last-of-type" => Some(LastOfType)
"only-of-type" => Some(OnlyOfType)
"root" => Some(Root)
"empty" => Some(Empty)
"hover" => Some(Hover)
"active" => Some(Active)
"focus" => Some(Focus)
"focus-visible" => Some(FocusVisible)
"focus-within" => Some(FocusWithin)
"link" => Some(Link)
"visited" => Some(Visited)
"any-link" => Some(AnyLink)
"enabled" => Some(Enabled)
"disabled" => Some(Disabled)
"checked" => Some(Checked)
"indeterminate" => Some(Indeterminate)
"required" => Some(Required)
"optional" => Some(Optional)
"valid" => Some(Valid)
"invalid" => Some(Invalid)
"read-only" => Some(ReadOnly)
"read-write" => Some(ReadWrite)
_ => None
}
// Only consume the token if it matched
match result {
Some(_) => {
let _ = self.advance()
result
}
None => None
}
}
Function(name) => {
let _ = self.advance() // Consume the Function token
let name_lower = name.to_lower()
match name_lower {
"nth-child" => {
let expr = self.parse_nth_expr()
match self.peek() {
RightParen => {
let _ = self.advance()
match expr {
Some(e) => Some(NthChild(e))
None => None
}
}
_ => None
}
}
"nth-last-child" => {
let expr = self.parse_nth_expr()
match self.peek() {
RightParen => {
let _ = self.advance()
match expr {
Some(e) => Some(NthLastChild(e))
None => None
}
}
_ => None
}
}
"nth-of-type" => {
let expr = self.parse_nth_expr()
match self.peek() {
RightParen => {
let _ = self.advance()
match expr {
Some(e) => Some(NthOfType(e))
None => None
}
}
_ => None
}
}
"nth-last-of-type" => {
let expr = self.parse_nth_expr()
match self.peek() {
RightParen => {
let _ = self.advance()
match expr {
Some(e) => Some(NthLastOfType(e))
None => None
}
}
_ => None
}
}
"not" => {
self.skip_whitespace()
let selectors = self.parse_compound_selector_list_until_paren()
match self.peek() {
RightParen => {
let _ = self.advance()
Some(Not(selectors))
}
_ => None
}
}
"is" => {
self.skip_whitespace()
let selectors = self.parse_complex_selector_list_until_paren()
match self.peek() {
RightParen => {
let _ = self.advance()
Some(Is(selectors))
}
_ => None
}
}
"where" => {
self.skip_whitespace()
let selectors = self.parse_complex_selector_list_until_paren()
match self.peek() {
RightParen => {
let _ = self.advance()
Some(Where(selectors))
}
_ => None
}
}
"has" => {
self.skip_whitespace()
let selectors = self.parse_relative_selector_list_until_paren()
match self.peek() {
RightParen => {
let _ = self.advance()
Some(Has(selectors))
}
_ => None
}
}
"state" => {
self.skip_whitespace()
let state = self.parse_attr_value()
match self.peek() {
RightParen => {
let _ = self.advance()
match state {
Some(token) => Some(State(token))
None => None
}
}
_ => None
}
}
_ => None
}
}
_ => None
}
}
///|
fn SelectorParser::parse_nth_expr(self : SelectorParser) -> NthExpr? {
self.skip_whitespace()
// Handle keywords: odd, even
match self.peek() {
Ident(kw) => {
let kw_lower = kw.to_lower()
if kw_lower == "odd" {
let _ = self.advance()
return Some({ a: 2, b: 1 })
}
if kw_lower == "even" {
let _ = self.advance()
return Some({ a: 2, b: 0 })
}
if kw_lower == "n" {
let _ = self.advance()
// Just 'n' = 1n+0
self.skip_whitespace()
let b = self.parse_nth_b()
return Some({ a: 1, b })
}
if kw_lower == "-n" {
let _ = self.advance()
self.skip_whitespace()
let b = self.parse_nth_b()
return Some({ a: -1, b })
}
return None
}
Dimension(num, unit) => {
let unit_lower = unit.to_lower()
if unit_lower == "n" {
let _ = self.advance()
let a = num.to_int()
self.skip_whitespace()
let b = self.parse_nth_b()
return Some({ a, b })
}
if unit_lower.length() > 1 && unit_lower[0] == 'n' {
let suffix = unit_lower.unsafe_substring(
start=1,
end=unit_lower.length(),
)
let b = @string.parse_int(suffix) catch {
_ =>
if suffix.length() > 1 && suffix[0] == '+' {
@string.parse_int(
suffix.unsafe_substring(start=1, end=suffix.length()),
) catch {
_ => 0
}
} else {
0
}
}
let _ = self.advance()
let a = num.to_int()
return Some({ a, b })
}
return None
}
Number(num, _) => {
let _ = self.advance()
// Just a number = 0n+B
return Some({ a: 0, b: num.to_int() })
}
Delim('+') => {
let _ = self.advance()
self.skip_whitespace()
match self.peek() {
Ident(kw) =>
if kw.to_lower() == "n" {
let _ = self.advance()
self.skip_whitespace()
let b = self.parse_nth_b()
return Some({ a: 1, b })
} else {
return None
}
Number(num, _) => {
let _ = self.advance()
return Some({ a: 0, b: num.to_int() })
}
_ => return None
}
}
Delim('-') => {
let _ = self.advance()
self.skip_whitespace()
match self.peek() {
Ident(kw) =>
if kw.to_lower() == "n" {
let _ = self.advance()
self.skip_whitespace()
let b = self.parse_nth_b()
return Some({ a: -1, b })
} else {
return None
}
Number(num, _) => {
let _ = self.advance()
return Some({ a: 0, b: -num.to_int() })
}
_ => return None
}
}
_ => return None
}
}
///|
fn SelectorParser::parse_nth_b(self : SelectorParser) -> Int {
match self.peek() {
Delim('+') => {
let _ = self.advance()
self.skip_whitespace()
match self.peek() {
Number(num, _) => {
let _ = self.advance()
num.to_int()
}
_ => 0
}
}
Delim('-') => {
let _ = self.advance()
self.skip_whitespace()
match self.peek() {
Number(num, _) => {
let _ = self.advance()
-num.to_int()
}
_ => 0
}
}
Number(num, _) => {
// Number might include sign
let _ = self.advance()
num.to_int()
}
_ => 0
}
}
///|
fn SelectorParser::parse_compound_selector_list_until_paren(
self : SelectorParser,
) -> Array[CompoundSelector] {
let result : Array[CompoundSelector] = []
while !self.is_at_end() {
self.skip_whitespace()
match self.peek() {
RightParen | EOF => break
Comma => {
let _ = self.advance()
}
_ =>
match self.parse_compound_selector() {
Some(sel) => result.push(sel)
None => break
}
}
}
result
}
///|
fn SelectorParser::parse_complex_selector_list_until_paren(
self : SelectorParser,
) -> Array[ComplexSelector] {
let result : Array[ComplexSelector] = []
while !self.is_at_end() {
self.skip_whitespace()
match self.peek() {
RightParen | EOF => break
Comma => {
let _ = self.advance()
}
_ =>
match self.parse_complex_selector() {
Some(sel) => result.push(sel)
None => break
}
}
}
result
}
///|
fn SelectorParser::parse_relative_selector_list_until_paren(
self : SelectorParser,
) -> Array[RelativeSelector] {
let result : Array[RelativeSelector] = []
while !self.is_at_end() {
self.skip_whitespace()
match self.peek() {
RightParen | EOF => break
Comma => {
let _ = self.advance()
}
_ => {
// Check for optional leading combinator
let combinator : Combinator? = match self.peek() {
Delim('>') => {
let _ = self.advance()
self.skip_whitespace()
Some(Child)
}
Delim('+') => {
let _ = self.advance()
self.skip_whitespace()
Some(NextSibling)
}
Delim('~') => {
let _ = self.advance()
self.skip_whitespace()
Some(SubsequentSibling)
}
_ => None
}
match self.parse_complex_selector() {
Some(sel) => result.push({ combinator, selector: sel })
None => break
}
}
}
}
result
}
///|
/// Convenience function to parse a selector from CSS text
pub fn parse_selector_text(css : String) -> ComplexSelector? {
let tokens = @token.tokenize(css)
parse_selector(tokens)
}
///|
/// Convenience function to parse a selector list from CSS text
pub fn parse_selector_list_text(css : String) -> SelectorList? {
let tokens = @token.tokenize(css)
parse_selector_list(tokens)
}