use super::ir::{ResolvedValue, VarTable};
use super::value::resolve_property;
use crate::error::Error;
use crate::expr::FuncTable;
use crate::syntax::ast::{Rule, SelUnit};
use std::collections::HashSet;
struct CompiledRule {
selector: Vec<SelUnit>,
decls: Vec<(String, ResolvedValue)>,
}
pub struct Stylesheet {
rules: Vec<CompiledRule>,
classes: HashSet<String>,
}
impl Stylesheet {
pub fn build(rules: &[&Rule], vars: &VarTable, funcs: &FuncTable) -> Result<Self, Error> {
let mut compiled = Vec::with_capacity(rules.len());
let mut classes = HashSet::new();
for rule in rules {
for unit in &rule.selector.units {
if let SelUnit::Class(c) = unit {
classes.insert(c.clone());
}
}
let mut decls = Vec::with_capacity(rule.decls.len());
for d in &rule.decls {
decls.push((
d.name.clone(),
resolve_property(&d.name, &d.groups, d.span, vars, funcs)?,
));
}
compiled.push(CompiledRule {
selector: rule.selector.units.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 [SelUnit::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 [SelUnit::Class(c)] = rule.selector.as_slice()
&& !c.starts_with("lini-")
&& node.classes.iter().any(|x| x == c)
{
out.extend(rule.decls.iter().cloned());
}
}
for rule in &self.rules {
if selector_is_id(&rule.selector) && selector_matches(&rule.selector, ancestors, node) {
out.extend(rule.decls.iter().cloned());
}
}
out
}
}
fn selector_is_id(sel: &[SelUnit]) -> bool {
matches!(sel, [SelUnit::Id(_)] | [SelUnit::Type { id: Some(_), .. }])
}
pub struct NodeFacts {
pub classes: Vec<String>,
pub id: Option<String>,
}
pub fn selector_matches(units: &[SelUnit], ancestors: &[NodeFacts], node: &NodeFacts) -> bool {
let Some((last, prefix)) = units.split_last() else {
return false;
};
if !unit_matches(last, node) {
return false;
}
let mut next = 0;
for unit in prefix {
match ancestors[next..].iter().position(|a| unit_matches(unit, a)) {
Some(offset) => next += offset + 1,
None => return false,
}
}
true
}
fn unit_matches(unit: &SelUnit, facts: &NodeFacts) -> bool {
match unit {
SelUnit::Class(c) => facts.classes.iter().any(|x| x == c),
SelUnit::Id(i) => facts.id.as_deref() == Some(i.as_str()),
SelUnit::Type { name, id } => {
facts.classes.iter().any(|x| x == name)
&& id.as_deref().is_none_or(|i| facts.id.as_deref() == Some(i))
}
SelUnit::Link => unreachable!("'|-|' is lowered to '.lini-link' before resolve"),
SelUnit::Dimension => {
unreachable!("'(-)' is lowered to '.lini-dimension' before resolve")
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn facts(classes: &[&str]) -> NodeFacts {
NodeFacts {
classes: classes.iter().map(|s| s.to_string()).collect(),
id: None,
}
}
fn facts_id(classes: &[&str], id: &str) -> NodeFacts {
NodeFacts {
classes: classes.iter().map(|s| s.to_string()).collect(),
id: Some(id.to_string()),
}
}
fn cls(name: &str) -> SelUnit {
SelUnit::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
));
}
#[test]
fn id_selector_matches_the_node_id() {
let node = facts_id(&["lini-box"], "hero");
assert!(selector_matches(&[SelUnit::Id("hero".into())], &[], &node));
assert!(!selector_matches(
&[SelUnit::Id("other".into())],
&[],
&node
));
}
#[test]
fn id_pinned_type_needs_both_class_and_id() {
let unit = SelUnit::Type {
name: "lini-table".into(),
id: Some("main".into()),
};
assert!(selector_matches(
std::slice::from_ref(&unit),
&[],
&facts_id(&["lini-table"], "main")
));
assert!(!selector_matches(
std::slice::from_ref(&unit),
&[],
&facts_id(&["lini-table"], "other")
));
assert!(!selector_matches(
std::slice::from_ref(&unit),
&[],
&facts_id(&["lini-box"], "main")
));
}
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(), &FuncTable::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("|box#x|\n");
assert!(names(&s.class_decls("lini-box")).contains(&"radius"));
assert!(s.class_decls("lini-oval").is_empty());
}
#[test]
fn node_layers_order_descendant_then_class_then_id() {
let s = sheet(
"{ .hot { stroke: red; }\n|group| |box| { fill: gray; }\n#a { opacity: 0.5; } }\n\
|group#g| .hot [\n |box#a| .hot\n]\n",
);
let node = facts_id(&["lini-box", "hot"], "a");
let ancestors = [facts(&["lini-group", "lini-box", "hot"])];
assert_eq!(
names(&s.node_layers(&ancestors, &node)),
vec!["fill", "stroke", "opacity"]
);
}
#[test]
fn defines_class_covers_every_referenced_user_class() {
let s = sheet("{ .hot { stroke: red; } }\n|box#x| .hot\n");
assert!(s.defines_class("hot"));
assert!(!s.defines_class("cold"));
}
}