///|
/// Variant names accepted by `@custom-variant`, matching the upstream grammar.
fn is_valid_variant_name(name : String) -> Bool {
let body = if name.has_prefix("@") { name[1:].to_owned() } else { name }
if body == "" {
return false
}
let first = body[0]
if !((first >= 'a' && first <= 'z') || (first >= '0' && first <= '9')) {
return false
}
for character in body {
if !((character >= 'a' && character <= 'z') ||
(character >= 'A' && character <= 'Z') ||
(character >= '0' && character <= '9') ||
character == '_' ||
character == '-') {
return false
}
}
!body.has_suffix("_") && !body.has_suffix("-")
}
///|
/// Build the template of a `@custom-variant name (…)` selector list.
///
/// Entries that start with `@` become at-rules wrapping `@slot`, and the
/// remaining selectors are joined into a single rule, as upstream does.
fn custom_variant_selector_template(
name : String,
raw : String,
) -> CustomVariantTemplate raise CompileError {
let entries = split_top_level(raw, ',').map(trim)
if entries.is_empty() || entries.contains("") {
raise InvalidCss("`@custom-variant \{name} (\{raw})` selector is invalid.")
}
let selectors = entries.filter(fn(entry) { !entry.has_prefix("@") })
let at_rules = entries.filter(fn(entry) { entry.has_prefix("@") })
if at_rules.is_empty() {
return CustomVariantSelector(selectors.join(", "))
}
let span : SourceSpan = { start: 0, end: 0 }
let slot : CssNode = AtRule(name="@slot", params="", nodes=None, span~)
let template : Array[CssNode] = []
if !selectors.is_empty() {
template.push(Rule(selector=selectors.join(", "), nodes=[slot], span~))
}
for entry in at_rules {
let (at_name, params) = match entry.split_once(" ") {
Some((at_name, params)) => (at_name.to_owned(), trim(params.to_owned()))
None => (entry, "")
}
template.push(AtRule(name=at_name, params~, nodes=Some([slot]), span~))
}
CustomVariantNodes(template)
}
///|
fn parse_custom_variants(
nodes : ArrayView[CssNode],
) -> Map[String, CustomVariantTemplate] raise CompileError {
let variants : Map[String, CustomVariantTemplate] = Map([])
for node in nodes {
match node {
AtRule(name="@custom-variant", params~, nodes=None, ..) => {
let (name, raw_selector) = match params.split_once(" ") {
Some((name, rest)) => (trim(name.to_owned()), trim(rest.to_owned()))
None => (trim(params), "")
}
if !is_valid_variant_name(name) {
raise InvalidCss(
"`@custom-variant \{name}` defines an invalid variant name.",
)
}
if raw_selector == "" {
raise InvalidCss("`@custom-variant \{name}` has no selector or body.")
}
let selector = if raw_selector.has_prefix("(") &&
raw_selector.has_suffix(")") {
raw_selector[1:raw_selector.length() - 1].to_owned()
} else {
raw_selector
}
variants[name] = custom_variant_selector_template(name, selector)
}
AtRule(name="@custom-variant", params~, nodes=Some(children), ..) => {
let variant_name = trim(params)
if variant_name.contains(" ") {
raise InvalidCss(
"`@custom-variant \{variant_name}` cannot have both a selector and a body.",
)
}
if !is_valid_variant_name(variant_name) {
raise InvalidCss(
"`@custom-variant \{variant_name}` defines an invalid variant name.",
)
}
if children.is_empty() {
raise InvalidCss("`@custom-variant \{variant_name}` is empty.")
}
variants[variant_name] = CustomVariantNodes(children.copy())
}
Rule(nodes~, ..) | AtRule(nodes=Some(nodes), ..) | Context(nodes~, ..) => {
let nested = parse_custom_variants(nodes)
for name, selector in nested {
variants[name] = selector
}
}
_ => ()
}
}
variants
}
///|
fn remove_custom_variant_nodes(
nodes : ArrayView[CssNode],
) -> (Array[CssNode], Bool) {
let output : Array[CssNode] = []
let mut changed = false
for node in nodes {
match node {
AtRule(name="@custom-variant", ..) => changed = true
Rule(selector~, nodes~, span~) => {
let (children, child_changed) = remove_custom_variant_nodes(nodes)
changed = changed || child_changed
output.push(Rule(selector~, nodes=children, span~))
}
AtRule(name~, params~, nodes=Some(nodes), span~) => {
let (children, child_changed) = remove_custom_variant_nodes(nodes)
changed = changed || child_changed
output.push(AtRule(name~, params~, nodes=Some(children), span~))
}
Context(values~, nodes=children, span~) => {
let (inner, inner_changed) = remove_custom_variant_nodes(children)
changed = changed || inner_changed
output.push(Context(values~, nodes=inner, span~))
}
_ => output.push(node)
}
}
(output, changed)
}