///|
/// Selectors, from a rule's prelude tokens.
///
/// The prelude arrives as a token slice rather than being read from the main
/// cursor, because a rule's extent is decided by brace matching before anything
/// tries to understand it. That ordering is what makes recovery work: a
/// selector nobody can parse still has a known end, so the block after it can
/// be parsed normally instead of being swallowed.
///
/// Whitespace is significant here and nowhere else in CSS: it is the descendant
/// combinator. That is why the tokenizer keeps it.
///|
/// A comma-separated selector list.
fn Parser::parse_selector_list(
self : Parser,
toks : ArrayView[@token.Token],
start : Int,
) -> Array[@ast.Selector] raise @err.CssError {
let out : Array[@ast.Selector] = []
let mut from = 0
let mut depth = 0
let mut i = 0
while i < toks.length() {
let k = toks[i].kind
if k.is_opener() {
depth = depth + 1
} else if k == RParen || k == RBracket || k == RBrace {
if depth > 0 {
depth = depth - 1
}
} else if k == Comma && depth == 0 {
out.push(self.parse_selector(toks[from:i], start))
from = i + 1
}
i = i + 1
}
// A trailing comma leaves a run with nothing in it. That is an error, and it
// is reported -- but it must not also become a `Bogus` SELECTOR, because
// printing one emits the dangling comma again and the output stops being a
// fixed point.
let tail = toks[from:]
if only_trivia_toks(tail) && out.length() > 0 {
let sp = span_of_view(tail, start)
self.error(BadSelector, sp)
} else {
out.push(self.parse_selector(tail, start))
}
if out.length() == 0 {
out.push(Bogus(self.bogus(BadSelector, start, start)))
}
out
}
///|
fn only_trivia_toks(toks : ArrayView[@token.Token]) -> Bool {
for t in toks {
if !t.is_trivia() {
return false
}
}
true
}
///|
/// One selector: compounds joined by combinators.
fn Parser::parse_selector(
self : Parser,
toks : ArrayView[@token.Token],
start : Int,
) -> @ast.Selector raise @err.CssError {
let s = { toks, pos: 0, owner: self, }
s.skip_ws()
if s.at_end() {
let sp = span_of_view(toks, start)
return Bogus(self.bogus(BadSelector, sp.start, sp.end))
}
// A nested rule may open with a combinator: `> .child`.
match s.read_combinator(leading=true) {
Some(c) => {
let inner = s.parse_rest(start)
return Relative(c, inner)
}
None => ()
}
s.parse_rest(start)
}
///|
/// A cursor over one selector's tokens.
///
/// Separate from `Parser` because it moves through a slice rather than the
/// whole file, and giving it its own position keeps the two from being confused
/// -- a bug that would be invisible until a selector containing a function
/// desynchronised the main cursor.
priv struct SelCursor {
toks : ArrayView[@token.Token]
mut pos : Int
owner : Parser
}
///|
fn SelCursor::at_end(self : SelCursor) -> Bool {
self.pos >= self.toks.length()
}
///|
fn SelCursor::peek(self : SelCursor) -> @token.TokenKind {
if self.at_end() {
Eof
} else {
self.toks[self.pos].kind
}
}
///|
fn SelCursor::peek_at(self : SelCursor, n : Int) -> @token.TokenKind {
let i = self.pos + n
if i >= self.toks.length() || i < 0 {
Eof
} else {
self.toks[i].kind
}
}
///|
fn SelCursor::skip_ws(self : SelCursor) -> Unit {
while !self.at_end() && self.toks[self.pos].is_trivia() {
self.pos = self.pos + 1
}
}
///|
fn SelCursor::here(self : SelCursor) -> Int {
if self.at_end() {
if self.toks.length() == 0 {
0
} else {
self.toks[self.toks.length() - 1].span.end
}
} else {
self.toks[self.pos].span.start
}
}
///|
fn span_of_view(toks : ArrayView[@token.Token], fallback : Int) -> @span.Span {
if toks.length() == 0 {
@span.Span::at(fallback)
} else {
@span.Span::new(toks[0].span.start, toks[toks.length() - 1].span.end)
}
}
///|
fn SelCursor::parse_rest(
self : SelCursor,
start : Int,
) -> @ast.Selector raise @err.CssError {
let mut acc : @ast.Selector = Simple(self.parse_compound(start))
while true {
match self.read_combinator() {
None => break
Some(c) => {
if self.at_end() {
let at = self.here()
let b = self.owner.bogus(DanglingCombinator, at, at)
return Complex(acc, c, {
type_sel: None,
quals: [Bogus(b)],
span: @span.Span::at(at),
})
}
acc = Complex(acc, c, self.parse_compound(start))
}
}
}
acc
}
///|
/// A combinator, if one is next.
///
/// `leading` distinguishes the start of a nested relative selector, where a
/// bare space means nothing, from a position between compounds, where it means
/// descendant.
fn SelCursor::read_combinator(
self : SelCursor,
leading? : Bool = false,
) -> @ast.Combinator? {
let save = self.pos
let mut saw_ws = false
while !self.at_end() && self.toks[self.pos].is_trivia() {
saw_ws = true
self.pos = self.pos + 1
}
if self.at_end() {
self.pos = save
return None
}
let c : @ast.Combinator? = match self.peek() {
Delim('>') => {
self.pos = self.pos + 1
Some(Child)
}
Delim('+') => {
self.pos = self.pos + 1
Some(NextSibling)
}
Delim('~') =>
// `~` alone is the sibling combinator; `~=` only ever appears inside
// `[ ]`, which this loop never sees at the top level.
if self.peek_at(1) == Delim('=') {
None
} else {
self.pos = self.pos + 1
Some(SubsequentSibling)
}
Delim('|') =>
if self.peek_at(1) == Delim('|') {
self.pos = self.pos + 2
Some(Column)
} else {
None
}
_ => None
}
match c {
Some(_) => {
self.skip_ws()
c
}
None =>
if saw_ws && !leading {
Some(Descendant)
} else {
self.pos = save
None
}
}
}
///|
/// One element's worth: an optional type selector, then qualifiers.
fn SelCursor::parse_compound(
self : SelCursor,
start : Int,
) -> @ast.Compound raise @err.CssError {
let from = self.here()
let quals : Array[@ast.Qualifier] = []
let mut type_sel : @ast.TypeSelector? = None
// A namespace prefix binds to the type selector, so it has to be read first
// -- and `a|b` is only a namespace when the `|` is not `||` and not `|=`.
let ns = self.read_ns_prefix()
match self.peek() {
Ident(name) => {
self.pos = self.pos + 1
type_sel = Some(Named(ns, name))
}
Delim('*') => {
self.pos = self.pos + 1
type_sel = Some(Universal(ns))
}
_ =>
match ns {
// A prefix with nothing to qualify.
Some(_) => {
let at = self.here()
quals.push(Bogus(self.owner.bogus(BadSelector, at, at)))
}
None => ()
}
}
while !self.at_end() {
match self.peek() {
Delim('.') =>
match self.peek_at(1) {
Ident(n) => {
self.pos = self.pos + 2
quals.push(Class(n))
}
_ => {
let at = self.here()
self.pos = self.pos + 1
quals.push(Bogus(self.owner.bogus(BadSelector, at, at + 1)))
}
}
Hash(n, k) => {
self.pos = self.pos + 1
if k == Id {
quals.push(Id(n))
} else {
let at = self.here()
quals.push(Bogus(self.owner.bogus(BadSelector, at, at)))
}
}
Delim('&') => {
self.pos = self.pos + 1
quals.push(Nesting)
}
LBracket => quals.push(self.parse_attr(start))
Colon => quals.push(self.parse_pseudo(start))
_ => break
}
}
{ type_sel, quals, span: @span.Span::new(from, self.here()), }
}
///|
/// A namespace prefix, if the `|` here is one.
fn SelCursor::read_ns_prefix(self : SelCursor) -> @ast.NsPrefix? {
match self.peek() {
Delim('|') =>
if self.peek_at(1) == Delim('|') {
None
} else {
self.pos = self.pos + 1
Some(None_)
}
Delim('*') =>
if self.peek_at(1) == Delim('|') && self.peek_at(2) != Delim('|') {
self.pos = self.pos + 2
Some(Any)
} else {
None
}
Ident(n) =>
if self.peek_at(1) == Delim('|') &&
self.peek_at(2) != Delim('|') &&
self.peek_at(2) != Delim('=') {
self.pos = self.pos + 2
Some(Named(n))
} else {
None
}
_ => None
}
}
///|
/// `[attr]`, `[attr=value]`, `[attr^="v" i]`.
fn SelCursor::parse_attr(
self : SelCursor,
start : Int,
) -> @ast.Qualifier raise @err.CssError {
let from = self.here()
self.pos = self.pos + 1
self.skip_ws()
let ns = self.read_ns_prefix()
let name = match self.peek() {
Ident(n) => {
self.pos = self.pos + 1
n
}
_ => return self.attr_bogus(from, start)
}
self.skip_ws()
if self.peek() == RBracket {
self.pos = self.pos + 1
return Attr({ ns, name, matcher: None, case_: Default, })
}
// The match operators are a delim pair; the tokenizer does not fuse them,
// because outside `[ ]` those characters mean unrelated things.
let op : @ast.AttrOp? = match (self.peek(), self.peek_at(1)) {
(Delim('='), _) => {
self.pos = self.pos + 1
Some(Exact)
}
(Delim('~'), Delim('=')) => {
self.pos = self.pos + 2
Some(Includes)
}
(Delim('|'), Delim('=')) => {
self.pos = self.pos + 2
Some(DashMatch)
}
(Delim('^'), Delim('=')) => {
self.pos = self.pos + 2
Some(Prefix)
}
(Delim('$'), Delim('=')) => {
self.pos = self.pos + 2
Some(Suffix)
}
(Delim('*'), Delim('=')) => {
self.pos = self.pos + 2
Some(Substring)
}
_ => None
}
let op = match op {
Some(o) => o
None => return self.attr_bogus(from, start)
}
self.skip_ws()
let value : @ast.AttrValue = match self.peek() {
Str(s) => {
self.pos = self.pos + 1
Str(s)
}
Ident(s) => {
self.pos = self.pos + 1
Ident(s)
}
Number(r, _, _) => {
self.pos = self.pos + 1
Ident(r)
}
_ => return self.attr_bogus(from, start)
}
self.skip_ws()
let case_ : @ast.AttrCase = match self.peek() {
Ident(f) =>
if f.to_lower() == "i" {
self.pos = self.pos + 1
Insensitive
} else if f.to_lower() == "s" {
self.pos = self.pos + 1
Sensitive
} else {
Default
}
_ => Default
}
self.skip_ws()
if self.peek() == RBracket {
self.pos = self.pos + 1
Attr({ ns, name, matcher: Some((op, value)), case_, })
} else {
self.attr_bogus(from, start)
}
}
///|
/// Give up on an attribute selector, consuming through its `]` so that the rest
/// of the compound still parses.
fn SelCursor::attr_bogus(
self : SelCursor,
from : Int,
start : Int,
) -> @ast.Qualifier raise @err.CssError {
ignore(start)
while !self.at_end() && self.peek() != RBracket {
self.pos = self.pos + 1
}
if !self.at_end() {
self.pos = self.pos + 1
}
Bogus(self.owner.bogus(BadAttributeSelector, from, self.here()))
}
///|
/// `:hover`, `::before`, `:not(...)`, `:nth-child(2n+1 of .x)`.
fn SelCursor::parse_pseudo(
self : SelCursor,
start : Int,
) -> @ast.Qualifier raise @err.CssError {
let from = self.here()
self.pos = self.pos + 1
let is_element = if self.peek() == Colon {
self.pos = self.pos + 1
true
} else {
false
}
match self.peek() {
Ident(name) => {
self.pos = self.pos + 1
let lower = name.to_lower()
if is_element || is_legacy_element(lower) {
Element(Simple(name))
} else {
Pseudo(Simple(name))
}
}
Function(name) => {
self.pos = self.pos + 1
let args = self.take_group()
let lower = name.to_lower()
if is_element {
if takes_selector_list(lower) {
Element(Sub(name, self.owner.parse_selector_list(args, start)))
} else {
Element(Fn(name, self.owner.values_of(args)))
}
} else if takes_selector_list(lower) {
Pseudo(Sub(name, self.owner.parse_selector_list(args, start)))
} else if is_nth(lower) {
self.parse_nth(name, args, from, start)
} else if lower == "dir" {
match first_ident(args) {
Some(d) => Pseudo(Dir(d))
None => Pseudo(Unknown(name, self.owner.values_of(args)))
}
} else if lower == "lang" {
Pseudo(Lang(idents_of(args)))
} else {
Pseudo(Unknown(name, self.owner.values_of(args)))
}
}
_ => {
let at = self.here()
Bogus(self.owner.bogus(BadPseudoClass(""), from, at))
}
}
}
///|
/// The tokens of the group that a `Function` token opened, up to its `)`.
fn SelCursor::take_group(self : SelCursor) -> ArrayView[@token.Token] {
let from = self.pos
let mut depth = 1
while !self.at_end() {
let k = self.peek()
if k.is_opener() {
depth = depth + 1
} else if k == RParen {
depth = depth - 1
if depth == 0 {
let out = self.toks[from:self.pos]
self.pos = self.pos + 1
return out
}
} else if k == RBracket || k == RBrace {
if depth > 0 {
depth = depth - 1
}
}
self.pos = self.pos + 1
}
self.toks[from:self.pos]
}
///|
/// The four pseudo-elements CSS still spells with one colon, for compatibility
/// with stylesheets written before `::` existed.
fn is_legacy_element(name : String) -> Bool {
name == "before" ||
name == "after" ||
name == "first-line" ||
name == "first-letter"
}
///|
fn takes_selector_list(name : String) -> Bool {
name == "is" ||
name == "where" ||
name == "not" ||
name == "has" ||
name == "matches" ||
name == "any" ||
name == "host" ||
name == "host-context" ||
name == "slotted" ||
name == "current"
}
///|
fn is_nth(name : String) -> Bool {
name == "nth-child" ||
name == "nth-last-child" ||
name == "nth-of-type" ||
name == "nth-last-of-type" ||
name == "nth-col" ||
name == "nth-last-col"
}
///|
fn first_ident(toks : ArrayView[@token.Token]) -> String? {
for t in toks {
match t.kind {
Ident(s) => return Some(s)
_ => ()
}
}
None
}
///|
fn idents_of(toks : ArrayView[@token.Token]) -> Array[String] {
let out = []
for t in toks {
match t.kind {
Ident(s) => out.push(s)
Str(s) => out.push(s)
_ => ()
}
}
out
}