use super::bundles::{primitive_bundle, template_bundle};
use super::types::{TEMPLATES, TypeInfo};
use crate::resolve::NodeKind;
use crate::span::Span;
use crate::syntax::ast::{Decl, Rule, SelUnit, Selector};
use std::collections::{BTreeSet, HashMap, HashSet};
pub fn lini_class(name: &str) -> String {
format!("lini-{name}")
}
pub fn is_lini_class(name: &str) -> bool {
name.starts_with("lini-")
}
pub fn worn_classes(info: &TypeInfo) -> Vec<String> {
let mut out: Vec<String> = info.chain.iter().rev().map(|n| lini_class(n)).collect();
out.push(lini_class(info.kind.as_str()));
out
}
pub fn class_defs(
present: &BTreeSet<String>,
element_rules: &HashMap<String, Vec<Decl>>,
extra_order: &[String],
) -> Vec<Rule> {
let mut rules = Vec::new();
let mut emit = |name: &str, bundle: Vec<Decl>| {
if !present.contains(name) {
return;
}
let decls = match element_rules.get(name) {
Some(extra) => merge_decls(bundle, extra),
None => bundle,
};
if decls.is_empty() {
return;
}
rules.push(class_rule(name, decls));
};
for kind in NodeKind::ALL {
emit(kind.as_str(), primitive_bundle(kind));
}
for (name, _) in TEMPLATES {
emit(name, template_bundle(name));
}
let mut seen = HashSet::new();
for name in extra_order {
if seen.insert(name.as_str()) {
emit(name, Vec::new());
}
}
rules
}
pub(super) fn merge_decls(mut base: Vec<Decl>, extra: &[Decl]) -> Vec<Decl> {
for d in extra {
if let Some(slot) = base.iter_mut().find(|x| x.name == d.name) {
*slot = d.clone();
} else {
base.push(d.clone());
}
}
base
}
fn class_rule(name: &str, decls: Vec<Decl>) -> Rule {
Rule {
selector: Selector {
units: vec![SelUnit::Class(lini_class(name))],
},
decls,
span: Span::empty(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::syntax::ast::Value;
fn decl(name: &str, v: f64) -> Decl {
Decl {
name: name.into(),
groups: vec![vec![Value::Number(v)]],
span: Span::empty(),
}
}
fn sel_class(sel: &Selector) -> &str {
match sel.units.as_slice() {
[SelUnit::Class(c)] => c,
_ => "",
}
}
#[test]
fn worn_chain_is_render_order_derived_to_base_then_primitive() {
let info = TypeInfo {
kind: NodeKind::Block,
chain: vec!["group".into(), "table".into()],
};
assert_eq!(
worn_classes(&info),
vec!["lini-table", "lini-group", "lini-block"]
);
}
#[test]
fn reserved_prefix_test() {
assert!(is_lini_class("lini-box"));
assert!(!is_lini_class("hot"));
}
#[test]
fn class_def_merges_bundle_then_element_rule() {
let mut present = BTreeSet::new();
present.insert("box".to_string());
let mut el: HashMap<String, Vec<Decl>> = HashMap::new();
el.insert("box".to_string(), vec![decl("radius", 4.0)]);
let defs = class_defs(&present, &el, &[]);
let boxdef = defs
.iter()
.find(|r| sel_class(&r.selector) == "lini-box")
.expect("box def");
let last_radius = boxdef
.decls
.iter()
.rev()
.find(|d| d.name == "radius")
.unwrap();
assert!(matches!(last_radius.groups[0][0], Value::Number(n) if n == 4.0));
}
#[test]
fn empty_bundle_type_emits_no_rule() {
let mut present = BTreeSet::new();
present.insert("image".to_string());
let defs = class_defs(&present, &HashMap::new(), &[]);
assert!(defs.iter().all(|r| sel_class(&r.selector) != "lini-image"));
}
}