///|
/// At-rules.
///
/// An at-rule is a call whose name is in the table, in statement position. It
/// is recognised only there and only in that shape, so a type selector that
/// happens to be called `media` is merely unspellable rather than silently
/// reinterpreted -- `tag(media)` is the way out.
///|
fn Lowerer::at_rule(
self : Lowerer,
g : @sast.Node,
name : String,
args : ArrayView[@sast.Node],
block : @sast.Node?,
ctx : Ctx,
) -> @ast.CssRule? raise @err.ShrubCssError {
let span = cspan(g.span)
// The inner context of a conditional group is the one it sits in: `media()`
// at the top of a file holds rules, and nested inside a rule it holds
// declarations too. Getting this wrong is the easiest mistake in the whole
// lowering, so it is named once here.
let inner = match ctx {
Top => Ctx::Top
_ => Ctx::Nested
}
match (name, block) {
("media", Some(b)) =>
Some(
Media({
queries: self.media_list(args),
body: self.block_items(b, inner),
span,
}),
)
("supports", Some(b)) =>
Some(
Supports({
condition: self.condition(args, g.span, Supports),
body: self.block_items(b, inner),
span,
}),
)
("container", Some(b)) => {
let (cname, rest) = split_container(args)
Some(
Container({
name: cname,
condition: self.condition(rest, g.span, Query),
body: self.block_items(b, inner),
span,
}),
)
}
("keyframes", Some(b)) => {
// A quoted name and a bare one are different rules in CSS, so the
// string is checked first rather than being folded into an identifier.
let kname : @ast.KeyframesName = match literal_string(args) {
Some(s) => Str(s)
None =>
match single_ident(args) {
Some(n) => Ident(@names.unkebab(n))
None => {
self.error(BadCallShape("keyframes"), g.span)
Ident("")
}
}
}
Some(Keyframes({ name: kname, frames: self.frames(b), span, }))
}
("font-face", Some(b)) => Some(FontFace(self.decl_block(b)))
("counter-style", Some(b)) =>
Some(CounterStyle(name_arg(args).unwrap_or(""), self.decl_block(b)))
("property", Some(b)) => {
let pname = match as_dashed(args_items(args)) {
Some(n) => n
None => name_arg(args).unwrap_or("")
}
Some(Property({ name: pname, decls: self.decl_block(b), span, }))
}
("page", Some(b)) =>
Some(
Page({
selectors: page_selectors(args),
// Not `DeclOnly`: a page body may hold margin at-rules -- `@top-left`
// and its fifteen siblings -- as well as declarations.
body: self.block_items(b, Nested),
span,
}),
)
("starting-style", Some(b)) =>
Some(StartingStyle(self.block_items(b, inner), span))
("scope", Some(b)) => {
let (s, e) = self.scope_parts(args, g.span)
Some(Scope({ start: s, end: e, body: self.block_items(b, inner), span, }))
}
("layer", Some(b)) =>
Some(
Layer({
names: layer_names(args),
body: Some(self.block_items(b, inner)),
span,
}),
)
("layer", None) =>
Some(Layer({ names: layer_names(args), body: None, span, }))
("charset", None) =>
match literal_string(args) {
Some(s) => Some(Charset(s, span))
None => {
self.error(BadCallShape("charset"), g.span)
Some(self.bogus_rule(BadCallShape("charset"), g))
}
}
("namespace", None) => {
let gs = arg_groups(args)
match gs.length() {
1 =>
match gs[0] {
[{ it: Lit(Str(u)), .. }] =>
Some(Namespace({ prefix: None, url: u, span, }))
_ => Some(self.bogus_rule(BadCallShape("namespace"), g))
}
2 =>
match (gs[0], gs[1]) {
([{ it: Id(p), .. }], [{ it: Lit(Str(u)), .. }]) =>
Some(Namespace({ prefix: Some(p), url: u, span, }))
_ => Some(self.bogus_rule(BadCallShape("namespace"), g))
}
_ => Some(self.bogus_rule(BadCallShape("namespace"), g))
}
}
("import", None) => self.import_of(args, g, span)
// `at("name", prelude)`: the escape for an at-rule this library does not
// know. Without a marked form an unknown `@`-rule reads back as a selector
// containing an unknown pseudo-class, which is a whole rule silently
// becoming one that matches nothing.
("at", _) => {
let gs = arg_groups(args)
if gs.length() == 0 {
return Some(self.bogus_rule(BadCallShape("at"), g))
}
let at_name = match gs[0] {
[{ it: Lit(Str(s)), .. }] => s
[{ it: Id(n), .. }] => @names.unkebab(n)
_ => return Some(self.bogus_rule(BadCallShape("at"), g))
}
let prelude : Array[@ast.ComponentValue] = []
let mut i = 1
while i < gs.length() {
if i > 1 {
prelude.push(Comma)
}
let (vs, _) = self.values(gs[i])
for v in vs {
prelude.push(v)
}
i = i + 1
}
// `Nested`, not the enclosing context: `at(...)` is by definition an
// at-rule nobody knows, so nothing can say whether its body holds
// declarations or rules, and `Nested` is the context that admits both.
let body = match block {
Some(b) => Some(self.block_items(b, Nested))
None => None
}
Some(Unknown({ name: at_name, prelude, block: body, span, }))
}
(_, blk) => {
// A known at-rule in the wrong shape, or one the table does not know.
self.error(BadCallShape(name), g.span)
// `Nested`, not `inner`: nothing here knows what this at-rule holds, and
// `Nested` is the context that admits both declarations and rules. Using
// the enclosing context would make `@styleset { nice-style: 12 }` at the
// top of a file read its declaration as a rule.
let body = match blk {
Some(b) => Some(self.block_items(b, Nested))
None => None
}
Some(
Unknown({ name, prelude: self.comma_list(args), block: body, span, }),
)
}
}
}
///|
/// The items of a single-group argument list, for a call whose argument is not
/// a name but a shape -- `property(--brand)`.
fn args_items(args : ArrayView[@sast.Node]) -> ArrayView[@sast.Node] {
match args {
[{ it: Group(xs), .. }] => xs[:]
_ => args
}
}
///|
fn Lowerer::import_of(
self : Lowerer,
args : ArrayView[@sast.Node],
g : @sast.Node,
span : @cspan.Span,
) -> @ast.CssRule? raise @err.ShrubCssError {
let gs = arg_groups(args)
if gs.length() == 0 {
return Some(self.bogus_rule(BadCallShape("import"), g))
}
let url = match gs[0] {
[{ it: Lit(Str(s)), .. }] => s
_ => return Some(self.bogus_rule(BadCallShape("import"), g))
}
let mut layer : @ast.LayerName?? = None
let mut supports : @ast.Condition? = None
let media : Array[@sast.Node] = []
let mut i = 1
while i < gs.length() {
let items = gs[i]
match as_call(items) {
Some((n, a, _)) if n == "layer" =>
layer = Some(Some({ parts: layer_parts(a), }))
Some((n, a, _)) if n == "supports" =>
supports = Some(self.condition(a, g.span, Supports))
_ =>
match items {
[{ it: Id("layer"), .. }] => layer = Some(None)
_ =>
// Whatever is left is a media query.
for n in items {
media.push(n)
}
}
}
i = i + 1
}
let queries = if media.length() == 0 {
[]
} else {
[self.media_query(media[:], g.span)]
}
Some(Import({ url, layer, supports, media: queries, span, }))
}
// -------------------------------------------------------- media and conditions
///|
fn Lowerer::media_list(
self : Lowerer,
args : ArrayView[@sast.Node],
) -> Array[@ast.MediaQuery] raise @err.ShrubCssError {
let out : Array[@ast.MediaQuery] = []
for g in args {
let items = match g.it {
Group(xs) => xs[:]
_ => one(g)
}
out.push(self.media_query(items, g.span))
}
out
}
///|
/// `screen and (width <= rem(40))`, `only print`, or a bare condition.
fn Lowerer::media_query(
self : Lowerer,
items : ArrayView[@sast.Node],
at : @basic.Span,
) -> @ast.MediaQuery raise @err.ShrubCssError {
let mut rest = items
let mut qualifier : @ast.MediaQualifier? = None
let mut media_type : String? = None
match rest {
[{ it: Id("only"), .. }, { it: Id(t), .. }, ..] => {
qualifier = Some(Only)
media_type = Some(@names.unkebab(t))
rest = rest[2:]
}
[{ it: Id("not"), .. }, { it: Id(t), .. }, ..] if !is_logical(t) => {
qualifier = Some(Not)
media_type = Some(@names.unkebab(t))
rest = rest[2:]
}
[{ it: Id(t), .. }, ..] if !is_logical(t) && !is_feature_head(rest) => {
media_type = Some(@names.unkebab(t))
rest = rest[1:]
}
_ => ()
}
// A leading `and` joins the type to the condition and carries no meaning of
// its own.
match rest {
[{ it: Id("and"), .. }, ..] => rest = rest[1:]
_ => ()
}
let condition = if rest.length() == 0 {
None
} else {
Some(self.condition_items(rest, at, Query))
}
{ qualifier, media_type, condition, }
}
///|
/// Whether a run begins a feature test rather than a media type.
///
/// `screen` is a type; `width <= rem(40)` is a feature whose first token also
/// happens to be a bare identifier. The difference is what follows it.
fn is_feature_head(items : ArrayView[@sast.Node]) -> Bool {
if items.length() < 2 {
return false
}
match items[1].it {
Op(_) => true
Block(_) => true
_ => false
}
}
///|
fn is_logical(w : String) -> Bool {
w == "and" || w == "or" || w == "not"
}
///|
fn Lowerer::condition(
self : Lowerer,
args : ArrayView[@sast.Node],
at : @basic.Span,
kind : CondKind,
) -> @ast.Condition raise @err.ShrubCssError {
match args {
[{ it: Group(xs), .. }] => self.condition_items(xs[:], at, kind)
_ => self.condition_items(args, at, kind)
}
}
///|
/// What a `name: value` inside a condition means.
///
/// The same shrubbery, two readings, and CSS makes the same distinction: in
/// `@supports` it is a declaration being tested for support, and in `@media`
/// or `@container` it is a plain feature test. Nothing in the text tells them
/// apart, so the context has to.
priv enum CondKind {
Query
Supports
} derive(Eq)
///|
/// A condition: features and parenthesised groups, joined by `and`/`or`.
fn Lowerer::condition_items(
self : Lowerer,
items : ArrayView[@sast.Node],
at : @basic.Span,
kind : CondKind,
) -> @ast.Condition raise @err.ShrubCssError {
if items.length() == 0 {
return Bogus(self.bogus_at(BadSelector, at))
}
let ands = split_word(items, "and")
let ors = split_word(items, "or")
if ands.length() > 1 && ors.length() > 1 {
// CSS requires the parentheses too, so this is a diagnostic rather than a
// precedence decision.
self.error(Unsupported("`and` and `or` mixed without parentheses"), at)
return Bogus(self.bogus_at(BadSelector, at))
}
if ands.length() > 1 {
let cs : Array[@ast.Condition] = []
for part in ands {
flatten(cs, self.condition_items(part, at, kind), And)
}
return Operation(And, cs)
}
if ors.length() > 1 {
let cs : Array[@ast.Condition] = []
for part in ors {
flatten(cs, self.condition_items(part, at, kind), Or)
}
return Operation(Or, cs)
}
match items {
[{ it: Id("not"), .. }, ..] =>
return Operation(Not, [self.condition_items(items[1:], at, kind)])
_ => ()
}
// A parenthesised group is a nested condition; anything else is a leaf.
match items {
[{ it: Parens(gs), .. }] =>
match gs {
[g] => {
let inner = match g.it {
Group(xs) => xs[:]
_ => one(g)
}
if has_logical(inner) {
return self.condition_items(inner, at, kind)
}
return self.feature(inner, at, kind)
}
_ => ()
}
_ => ()
}
match as_call(items) {
Some((n, a, _)) if n == "selector" => {
let sels : Array[@ast.Selector] = []
for g in a {
let sitems = match g.it {
Group(xs) => xs[:]
_ => one(g)
}
sels.push(self.selector(sitems, g.span))
}
return SelectorFn(sels)
}
_ => ()
}
self.feature(items, at, kind)
}
///|
fn flatten(
out : Array[@ast.Condition],
c : @ast.Condition,
op : @ast.LogicalOp,
) -> Unit {
match c {
Operation(o, inner) if o == op =>
for x in inner {
out.push(x)
}
_ => out.push(c)
}
}
///|
fn has_logical(items : ArrayView[@sast.Node]) -> Bool {
for n in items {
match n.it {
Id(w) => if is_logical(w) { return true }
Parens(_) => return true
_ => ()
}
}
false
}
///|
/// A feature test, in the four shapes CSS gives it.
///
/// `=` is the canonical spelling for the plain form, because the `:` form opens
/// a block and is one step from the semicolon hazard. Both are accepted.
fn Lowerer::feature(
self : Lowerer,
items : ArrayView[@sast.Node],
at : @basic.Span,
kind : CondKind,
) -> @ast.Condition raise @err.ShrubCssError {
// `(display: grid)` inside `supports` is a declaration, not a feature, and
// its property may be the `ident("...")` escape as well as a bare name --
// the same two spellings a declaration has anywhere else.
let head_len = items.length() - 1
if head_len > 0 {
match items[head_len].it {
Block(gs) =>
if gs.length() == 1 {
let name : @ast.PropertyName? = match items[0:head_len] {
[{ it: Id(p), .. }] => Some(Ident(@names.unkebab(p)))
_ =>
match as_dashed(items[0:head_len]) {
Some(n) => Some(Custom(n))
None =>
match as_call(items[0:head_len]) {
Some((f, a, rest)) =>
if f == "ident" && rest.length() == 0 {
match literal_string(a) {
Some(lit) => Some(Ident(lit))
None => None
}
} else {
None
}
None => None
}
}
}
match name {
Some(property) => {
let (value, important) = self.value_of(gs[0])
// The same shrubbery, two readings, and CSS makes the same
// distinction: in `@supports` it is a declaration being tested,
// and in `@media` it is a plain feature.
return if kind == Supports {
Decl({ property, value, important, span: cspan(at), })
} else {
Feature(Plain(property.text(), value))
}
}
None => ()
}
}
_ => ()
}
}
// `width = rem(40)`, `width <= rem(40)`.
let ops = range_ops(items)
if ops.length() == 1 {
let (i, op) = ops[0]
let left = items[0:i]
let right = items[i + 1:]
// The name may be a custom property -- `style(--responsive: true)` -- which
// is two nodes, not one. It has to be recognised here, before the fallback
// decides the name must be on the right and flips the comparison round.
let left_name : String? = match left {
[{ it: Id(n), .. }] => Some(@names.unkebab(n))
_ => as_dashed(left)
}
match left_name {
Some(n) => {
let (vs, _) = self.values(right)
// `=` is a range test comparing for equality; the plain form is the
// block spelling, handled above.
return Feature(Range(n, op, vs))
}
None =>
match right {
[{ it: Id(n), .. }] => {
let (vs, _) = self.values(left)
return Feature(Range(@names.unkebab(n), flip(op), vs))
}
_ => ()
}
}
}
if ops.length() == 2 {
let (i1, op1) = ops[0]
let (i2, op2) = ops[1]
match items[i1 + 1:i2] {
[{ it: Id(n), .. }] => {
let (lo, _) = self.values(items[0:i1])
let (hi, _) = self.values(items[i2 + 1:])
return Feature(Interval(lo, op1, @names.unkebab(n), op2, hi))
}
_ => ()
}
}
match items {
[{ it: Id(n), .. }] => Feature(Boolean(@names.unkebab(n)))
_ => {
let (vs, _) = self.values(items)
Unknown(vs)
}
}
}
///|
/// The comparison operators at the top of a run, with their positions.
fn range_ops(items : ArrayView[@sast.Node]) -> Array[(Int, @ast.RangeOp)] {
let out : Array[(Int, @ast.RangeOp)] = []
for i, n in items {
match n.it {
Op("<") => out.push((i, Lt))
Op("<=") => out.push((i, Le))
Op(">") => out.push((i, Gt))
Op(">=") => out.push((i, Ge))
Op("=") => out.push((i, Eq))
_ => ()
}
}
out
}
///|
fn flip(op : @ast.RangeOp) -> @ast.RangeOp {
match op {
Lt => Gt
Le => Ge
Gt => Lt
Ge => Le
Eq => Eq
}
}
///|
fn split_word(
items : ArrayView[@sast.Node],
word : String,
) -> Array[ArrayView[@sast.Node]] {
let out : Array[ArrayView[@sast.Node]] = []
let mut from = 0
for i, n in items {
match n.it {
Id(w) =>
if w == word {
out.push(items[from:i])
from = i + 1
}
_ => ()
}
}
out.push(items[from:])
out
}
// ---------------------------------------------------------- small preludes
///|
fn split_container(
args : ArrayView[@sast.Node],
) -> (String?, ArrayView[@sast.Node]) {
let gs = arg_groups(args)
if gs.length() >= 2 {
match gs[0] {
[{ it: Id(n), .. }] if !is_logical(n) =>
return (Some(@names.unkebab(n)), args[1:])
_ => ()
}
}
(None, args)
}
///|
fn Lowerer::frames(
self : Lowerer,
block : @sast.Node,
) -> Array[@ast.KeyframeBlock] raise @err.ShrubCssError {
let out : Array[@ast.KeyframeBlock] = []
let groups = match block.it {
Block(gs) => gs
_ => return out
}
for g in groups {
let items = match g.it {
Group(xs) => xs[:]
_ => continue
}
let (head, blk, _) = split_block(items)
match blk {
None => {
self.error(ExpectedBlock, g.span)
continue
}
Some(b) => {
let selectors : Array[@ast.KeyframeSelector] = []
for sel_items in comma_runs(head) {
match self.keyframe_selector(sel_items, g.span) {
Some(s) => selectors.push(s)
None => ()
}
}
out.push({ selectors, decls: self.decl_block(b), span: cspan(g.span), })
}
}
}
out
}
///|
fn Lowerer::keyframe_selector(
self : Lowerer,
items : ArrayView[@sast.Node],
at : @basic.Span,
) -> @ast.KeyframeSelector? raise @err.ShrubCssError {
match items {
[{ it: Id("from"), .. }] => Some(From)
[{ it: Id("to"), .. }] => Some(To)
_ =>
match as_call(items) {
Some((n, args, _)) if n == "percent" =>
match single_number(args) {
Some(num) => Some(Percentage(num))
None => {
self.error(BadCallShape("percent"), at)
None
}
}
_ => {
self.error(Unsupported("this keyframe selector"), at)
None
}
}
}
}
///|
/// A head split on brackets, for the comma-separated case.
fn comma_runs(head : ArrayView[@sast.Node]) -> Array[ArrayView[@sast.Node]] {
match head {
[{ it: Brackets(gs), .. }] => {
let out : Array[ArrayView[@sast.Node]] = []
for g in gs {
match g.it {
Group(xs) => out.push(xs[:])
_ => out.push(one(g))
}
}
out
}
_ => [head]
}
}
///|
fn layer_names(args : ArrayView[@sast.Node]) -> Array[@ast.LayerName] {
let out : Array[@ast.LayerName] = []
for g in args {
let items = match g.it {
Group(xs) => xs[:]
_ => one(g)
}
let parts = layer_parts_items(items)
if parts.length() > 0 {
out.push({ parts, })
}
}
out
}
///|
fn layer_parts(args : ArrayView[@sast.Node]) -> Array[String] {
match args {
[{ it: Group(xs), .. }] => layer_parts_items(xs[:])
_ => layer_parts_items(args)
}
}
///|
/// `base.theme` is `Id . Id`, which is a dotted layer name.
fn layer_parts_items(items : ArrayView[@sast.Node]) -> Array[String] {
let out : Array[String] = []
for n in items {
match n.it {
Id(p) => out.push(@names.unkebab(p))
_ => ()
}
}
out
}
///|
fn page_selectors(args : ArrayView[@sast.Node]) -> Array[String] {
let out : Array[String] = []
for g in args {
let items = match g.it {
Group(xs) => xs[:]
_ => one(g)
}
// A run of calls is a run of pseudo-pages -- `page(first() right())` is
// `@page :first:right` -- and a bare name is the page's own name.
let b = StringBuilder()
let mut i = 0
while i < items.length() {
match as_call(items[i:]) {
Some((name, _, _)) => {
b.write_string(":")
b.write_string(@names.unkebab(name))
i = i + 2
}
None => {
match items[i].it {
Id(name) => b.write_string(@names.unkebab(name))
_ => ()
}
i = i + 1
}
}
}
let s = b.to_string()
if s != "" {
out.push(s)
}
}
out
}
///|
fn Lowerer::scope_parts(
self : Lowerer,
args : ArrayView[@sast.Node],
at : @basic.Span,
) -> (Array[@ast.Selector]?, Array[@ast.Selector]?) raise @err.ShrubCssError {
let gs = arg_groups(args)
let mut start : Array[@ast.Selector]? = None
let mut end : Array[@ast.Selector]? = None
for items in gs {
// `~to` splits the two halves.
let mut split = -1
for i, n in items {
match n.it {
Kw("to") => split = i
_ => ()
}
}
if split >= 0 {
if split > 0 {
start = Some([self.selector(items[0:split], at)])
}
if split + 1 < items.length() {
end = Some([self.selector(items[split + 1:], at)])
}
} else if start is None {
start = Some([self.selector(items, at)])
} else {
end = Some([self.selector(items, at)])
}
}
(start, end)
}