use super::types::{TEMPLATES, TypeInfo};
use crate::ledger::defaults::{primitive_bundle, template_bundle};
use crate::resolve::NodeKind;
use crate::span::Span;
use crate::syntax::ast::{Decl, Rule, SelUnit, Selector, Value};
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],
synthesizes_shapes: bool,
) -> Vec<Rule> {
let mut rules = Vec::new();
let mut emit = |name: &str, bundle: Vec<Decl>, force: bool| {
if !force && !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 {
let synthesized = synthesizes_shapes && matches!(kind, NodeKind::Line | NodeKind::Block);
emit(kind.as_str(), primitive_bundle(kind), synthesized);
}
for (name, _) in TEMPLATES {
emit(name, template_bundle(name), false);
}
let mut seen = HashSet::new();
for name in extra_order {
if seen.insert(name.as_str()) {
emit(name, Vec::new(), false);
}
}
for (name, prop, value) in ALIGN_CLASSES {
if present.contains(name) {
rules.push(align_class_rule(name, prop, value));
}
}
rules
}
pub(super) const ALIGN_CLASSES: [(&str, &str, &str); 4] = [
("align-start", "align", "start"),
("align-end", "align", "end"),
("justify-start", "justify", "start"),
("justify-end", "justify", "end"),
];
fn align_class_rule(name: &str, prop: &str, value: &str) -> Rule {
Rule {
selector: Selector {
units: vec![SelUnit::Class(lini_class(name))],
},
decls: vec![Decl {
name: prop.to_string(),
groups: vec![vec![Value::Ident(value.to_string())]],
span: Span::empty(),
}],
span: Span::empty(),
}
}
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
}
pub fn scoped_note_rules(present: &BTreeSet<String>, user_rules: &[Rule]) -> Vec<Rule> {
if !present.contains("note") {
return Vec::new();
}
let number = |name: &str, ns: &[f64]| Decl {
name: name.to_string(),
groups: vec![ns.iter().map(|n| Value::Number(*n)).collect()],
span: Span::empty(),
};
["sequence", "drawing"]
.iter()
.map(|scope| Rule {
selector: Selector {
units: vec![
SelUnit::Class(format!("lini-{scope}")),
SelUnit::Class("lini-note".to_string()),
],
},
decls: vec![
number("padding", &[6.0, 10.0]),
number("font-size", &[13.0]),
],
span: Span::empty(),
})
.filter(|r| {
!user_rules
.iter()
.any(|u| match u.selector.units.as_slice() {
[SelUnit::Class(a), SelUnit::Class(b)] => {
matches!(r.selector.units.as_slice(),
[SelUnit::Class(x), SelUnit::Class(y)] if a == x && b == y)
}
_ => false,
})
})
.collect()
}
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, &[], false);
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(), &[], false);
assert!(defs.iter().all(|r| sel_class(&r.selector) != "lini-image"));
}
}