use super::ir::{ResolvedValue, VarTable};
use super::value::resolve_groups;
use crate::error::Error;
use crate::syntax::ast::{Rule, SelPart};
use std::collections::HashSet;
struct CompiledRule {
selector: Vec<SelPart>,
decls: Vec<(String, ResolvedValue)>,
}
pub struct Stylesheet {
rules: Vec<CompiledRule>,
classes: HashSet<String>,
}
impl Stylesheet {
pub fn build(rules: &[&Rule], vars: &VarTable) -> Result<Self, Error> {
let mut compiled = Vec::with_capacity(rules.len());
let mut classes = HashSet::new();
for rule in rules {
for part in &rule.selector.parts {
if let SelPart::Class(c) = part {
classes.insert(c.clone());
}
}
let mut decls = Vec::with_capacity(rule.decls.len());
for d in &rule.decls {
decls.push((d.name.clone(), resolve_groups(&d.groups, d.span, vars)?));
}
compiled.push(CompiledRule {
selector: rule.selector.parts.clone(),
decls,
});
}
Ok(Self {
rules: compiled,
classes,
})
}
pub fn defines_class(&self, name: &str) -> bool {
self.classes.contains(name)
}
pub fn class_decls(&self, class: &str) -> Vec<(String, ResolvedValue)> {
let mut out = Vec::new();
for rule in &self.rules {
if let [SelPart::Class(c)] = rule.selector.as_slice()
&& c == class
{
out.extend(rule.decls.iter().cloned());
}
}
out
}
pub fn node_layers(
&self,
ancestors: &[NodeFacts],
node: &NodeFacts,
) -> Vec<(String, ResolvedValue)> {
let mut out = Vec::new();
for rule in &self.rules {
if rule.selector.len() > 1 && selector_matches(&rule.selector, ancestors, node) {
out.extend(rule.decls.iter().cloned());
}
}
for rule in &self.rules {
if let [SelPart::Class(c)] = rule.selector.as_slice()
&& !c.starts_with("lini-")
&& node.classes.iter().any(|x| x == c)
{
out.extend(rule.decls.iter().cloned());
}
}
out
}
}
pub struct NodeFacts {
pub classes: Vec<String>,
}
pub fn selector_matches(parts: &[SelPart], ancestors: &[NodeFacts], node: &NodeFacts) -> bool {
let Some((last, prefix)) = parts.split_last() else {
return false;
};
if !part_matches(last, node) {
return false;
}
let mut next = 0;
for part in prefix {
match ancestors[next..].iter().position(|a| part_matches(part, a)) {
Some(offset) => next += offset + 1,
None => return false,
}
}
true
}
fn part_matches(part: &SelPart, facts: &NodeFacts) -> bool {
match part {
SelPart::Class(c) => facts.classes.iter().any(|x| x == c),
SelPart::Type(_) => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn facts(classes: &[&str]) -> NodeFacts {
NodeFacts {
classes: classes.iter().map(|s| s.to_string()).collect(),
}
}
fn cls(name: &str) -> SelPart {
SelPart::Class(name.into())
}
#[test]
fn class_selector_matches_a_worn_class() {
let node = facts(&["lini-box", "hot"]);
assert!(selector_matches(&[cls("lini-box")], &[], &node));
assert!(selector_matches(&[cls("hot")], &[], &node));
assert!(!selector_matches(&[cls("cold")], &[], &node));
}
#[test]
fn descendant_selector_needs_a_matching_ancestor() {
let node = facts(&["lini-box"]);
let ancestors = [
facts(&["lini-table", "lini-group", "lini-box"]),
facts(&["lini-row"]),
];
assert!(selector_matches(
&[cls("lini-table"), cls("lini-box")],
&ancestors,
&node
));
}
#[test]
fn descendant_selector_fails_without_the_ancestor() {
let node = facts(&["lini-box"]);
let ancestors = [facts(&["lini-group", "lini-box"])];
assert!(!selector_matches(
&[cls("lini-table"), cls("lini-box")],
&ancestors,
&node
));
}
#[test]
fn descendant_combinator_is_not_adjacency() {
let node = facts(&["lini-box"]);
let ancestors = [
facts(&["lini-group", "lini-box", "sidebar"]),
facts(&["lini-row"]),
];
assert!(selector_matches(
&[cls("sidebar"), cls("lini-box")],
&ancestors,
&node
));
}
fn sheet(src: &str) -> Stylesheet {
use crate::syntax::ast::{Rule, StyleItem};
let toks = crate::lexer::lex(src).expect("lex");
let file = crate::syntax::parser::parse(&toks).expect("parse");
let lowered = crate::desugar::desugar(&file).expect("desugar");
let rules: Vec<&Rule> = lowered
.stylesheet
.iter()
.filter_map(|it| match it {
StyleItem::Rule(r) => Some(r),
_ => None,
})
.collect();
Stylesheet::build(&rules, &VarTable::new()).expect("build")
}
fn names(decls: &[(String, ResolvedValue)]) -> Vec<&str> {
decls.iter().map(|(n, _)| n.as_str()).collect()
}
#[test]
fn class_decls_returns_the_generated_type_class() {
let s = sheet("x |box|\n");
assert!(names(&s.class_decls("lini-box")).contains(&"radius"));
assert!(s.class_decls("lini-oval").is_empty());
}
#[test]
fn node_layers_put_descendant_rules_before_class_rules_and_skip_lini() {
let s = sheet(
"{ .hot { stroke: red; }\n|group box| { fill: gray; } }\ng |group| .hot [\n a |box| .hot\n]\n",
);
let node = facts(&["lini-box", "hot"]);
let ancestors = [facts(&["lini-group", "lini-box", "hot"])];
assert_eq!(
names(&s.node_layers(&ancestors, &node)),
vec!["fill", "stroke"]
);
}
#[test]
fn defines_class_covers_every_referenced_user_class() {
let s = sheet("{ .hot { stroke: red; } }\nx |box| .hot\n");
assert!(s.defines_class("hot"));
assert!(!s.defines_class("cold"));
}
}