///|
/// Split only top-level simple selectors. Quoted attributes and functional
/// pseudo classes stay intact, and escaped punctuation never becomes a token.
fn selector_units(text : String) -> Array[String] raise ParseError {
let chars = text.to_array()
let out : Array[String] = []
let mut i = 0
while i < chars.length() {
if [' ', '\t', '\n', '\r'].contains(chars[i]) {
if !out.is_empty() && out.last() != Some(" ") {
out.push(" ")
}
i += 1
continue
}
if ['>', '+', '~', '|'].contains(chars[i]) {
out.push(chars[i].to_string())
i += 1
continue
}
let start = i
if ['.', '#', '%', ':'].contains(chars[i]) {
let c = chars[i]
i += 1
if c == ':' && i < chars.length() && chars[i] == ':' {
i += 1
}
}
let mut quote = '\u0000'
let mut level = 0
while i < chars.length() {
let c = chars[i]
if c == '\\' {
i += if i + 1 < chars.length() { 2 } else { 1 }
continue
}
if quote != '\u0000' {
if c == quote {
quote = '\u0000'
}
} else if c == '"' || c == '\'' {
quote = c
} else if c == '[' && level == 0 && i > start {
break
} else if c == '(' || c == '[' {
level += 1
} else if c == ')' || c == ']' {
level -= 1
} else if level == 0 &&
[' ', '\t', '\n', '\r', '.', '#', '%', ':', '>', '+', '~', '|', ','].contains(
c,
) {
break
}
i += 1
}
if i == start || level != 0 || quote != '\u0000' {
raise Invalid("invalid selector")
}
out.push(String::from_array(chars[start:i]))
}
if out.last() == Some(" ") {
ignore(out.pop())
}
out
}
///|
fn selector_separator(text : String) -> Bool {
[" ", ">", "+", "~", "|"].contains(text)
}
///|
fn selector_placeholder(text : String) -> Bool {
let units = selector_units(text) catch { _ => return true }
units.any(s => s.has_prefix("%"))
}
///|
fn unify_compound(
left : Array[String],
right : Array[String],
) -> Array[String]? {
let out : Array[String] = []
let mut type_name : String? = None
let mut id : String? = None
let mut pseudo_element : String? = None
for value in [..left, ..right] {
if value == "*" {
continue
}
if value.has_prefix("#") {
if id is Some(old) && old != value {
return None
}
id = Some(value)
} else if value.has_prefix("::") {
if pseudo_element is Some(old) && old != value {
return None
}
pseudo_element = Some(value)
} else if !value.has_prefix(".") &&
!value.has_prefix("%") &&
!value.has_prefix(":") &&
!value.has_prefix("[") {
if type_name is Some(old) && old != value {
return None
}
type_name = Some(value)
}
if !out.contains(value) {
out.push(value)
}
}
let ordered : Array[String] = []
if type_name is Some(value) {
ordered.push(value)
}
for value in out {
if Some(value) != type_name && !value.has_prefix(":") {
ordered.push(value)
}
}
for value in out {
if value.has_prefix(":") {
ordered.push(value)
}
}
if ordered.is_empty() {
ordered.push("*")
}
Some(ordered)
}
///|
fn extend_selector(
original : String,
target : String,
replacement : String,
) -> (Bool, Array[String]) raise ParseError {
let units = selector_units(original)
let extender = selector_units(replacement)
let mut compound_start = extender.length()
while compound_start > 0 && !selector_separator(extender[compound_start - 1]) {
compound_start -= 1
}
let lead = extender[:compound_start].to_owned()
let tail = extender[compound_start:].to_owned()
let out : Array[String] = []
let mut matched = false
for i in 0.. 0 && !selector_separator(units[start - 1]) {
start -= 1
}
while end < units.length() && !selector_separator(units[end]) {
end += 1
}
let rest = [..units[start:i], ..units[i + 1:end]]
if unify_compound(rest, tail) is Some(compound) {
let prefix = units[:start].to_owned()
let suffix = units[end:].to_owned().join("")
let merged = compound.join("") + suffix
if prefix == lead || lead.is_empty() {
out.push(prefix.join("") + merged)
} else if prefix.is_empty() {
out.push(lead.join("") + merged)
} else {
// Two independent descendant paths may appear in either order.
// Combinator weaving requires a full selector algebra and is rejected.
if lead.any(s => s != " " && selector_separator(s)) {
raise Invalid("complex extender combinators are not supported")
}
out.push(prefix.join("") + lead.join("") + merged)
out.push(lead.join("") + prefix.join("") + merged)
}
}
}
(matched, out)
}
///|
fn Emitter::add_extension(
self : Emitter,
source : String,
parents : Array[String],
) -> Unit raise ParseError {
if parents.is_empty() {
raise Invalid("extend outside rule")
}
let optional = source.has_suffix("!optional")
let text = if optional {
source[:source.length() - 9].trim().to_owned()
} else {
source
}
for target in split_top(text, ',') {
let tokens = selector_units(target)
if tokens.length() != 1 ||
selector_separator(tokens[0]) ||
target.contains("&") ||
target.is_empty() {
raise Invalid("extend requires a simple selector")
}
self.extensions.push({
target,
selectors: parents.copy(),
context: self.context.copy(),
path: self.path,
optional,
matched: false,
})
if self.extensions.length() > 4096 {
raise Invalid("extension limit")
}
}
}