mod chrome;
mod classes;
mod drawing;
mod labels;
mod page;
mod scale;
pub(crate) mod scene;
mod tables;
mod titleblock;
pub(crate) mod types;
use crate::error::Error;
use crate::ledger::defaults::root_defaults;
use crate::resolve::NodeKind;
use crate::span::Span;
use crate::syntax::ast::{
Child, Decl, File, Link, Node, Rule, SelUnit, Selector, StyleItem, Value,
};
use classes::{class_defs, is_lini_class, lini_class, merge_decls, worn_classes};
use std::collections::{BTreeSet, HashMap};
use tables::{
column_count, distribute_cell_alignment, header_node, wrap_body_cells, wrap_header_row,
};
use types::{Types, is_template};
type Bodies = HashMap<String, (Vec<Child>, Vec<Link>)>;
pub fn desugar(file: &File) -> Result<File, Error> {
let types = Types::build(file)?;
let mut element_rules: HashMap<String, Vec<Decl>> = HashMap::new();
let mut bodies: Bodies = HashMap::new();
let mut extra_order: Vec<String> = Vec::new();
let mut user_root: Vec<Decl> = Vec::new();
let mut user_vars: Vec<Decl> = Vec::new();
let mut user_rules: Vec<Rule> = Vec::new();
let mut user_funcs: Vec<crate::syntax::ast::FuncDef> = Vec::new();
for item in &file.stylesheet {
match item {
StyleItem::RootDecl(d) => user_root.push(d.clone()),
StyleItem::Binding(f) => user_funcs.push(f.clone()),
StyleItem::Var(d) => user_vars.push(d.clone()),
StyleItem::Define(d) => {
element_rules
.entry(d.name.clone())
.or_default()
.extend(d.style.iter().cloned());
bodies.insert(d.name.clone(), (d.children.clone(), d.links.clone()));
push_unique(&mut extra_order, &d.name);
}
StyleItem::Rule(r) => match r.selector.units.as_slice() {
[SelUnit::Type { name, id: None }] => element_rules
.entry(name.clone())
.or_default()
.extend(r.decls.iter().cloned()),
[SelUnit::Class(c)]
if is_lini_class(c) && (c == "lini-link" || c == "lini-dimension") =>
{
user_rules.push(rewrite_selector(r, &types)?)
}
[SelUnit::Class(c)] if is_lini_class(c) => {
let x = c.strip_prefix("lini-").unwrap().to_string();
element_rules
.entry(x.clone())
.or_default()
.extend(r.decls.iter().cloned());
if NodeKind::parse(&x).is_none() && !is_template(&x) {
push_unique(&mut extra_order, &x);
}
}
_ => user_rules.push(rewrite_selector(r, &types)?),
},
}
}
let root_drawing = root_layout(&user_root) == Some("drawing");
let mut instances = Vec::new();
for child in &file.instances {
instances.push(lower_child(child, &types, &bodies, root_drawing)?);
}
if !root_drawing {
let declared = scene::declared_ids(&instances);
let mut root_msgs: Vec<&Link> = file.links.iter().collect();
root_msgs.extend(gather_frame_messages(&instances));
for (id, span) in scene::auto_created_ids(&root_msgs, &declared) {
instances.push(Child::Box(lower_node(
&scene::auto_box(&id, span),
&types,
&bodies,
false,
)?));
}
}
scale::fold(&mut instances, &mut user_root, root_drawing)?;
let mut present: BTreeSet<String> = BTreeSet::new();
for c in &instances {
mark_present(c, &mut present);
}
let mut stylesheet: Vec<StyleItem> = Vec::new();
let mut layout_defaults =
crate::ledger::defaults::root_layout_defaults(root_layout(&user_root));
let only_pages = !instances.is_empty()
&& instances
.iter()
.all(|c| matches!(c, Child::Box(n) if n.classes.iter().any(|k| k == "lini-page")));
if only_pages {
layout_defaults.push(decl("padding", vec![Value::Number(0.0)]));
}
let base = merge_decls(root_defaults(), &layout_defaults);
for d in merge_decls(base, &user_root) {
stylesheet.push(StyleItem::RootDecl(d));
}
for d in user_vars {
stylesheet.push(StyleItem::Var(d));
}
for f in user_funcs {
stylesheet.push(StyleItem::Binding(f));
}
let synthesizes_shapes = ["chart", "pie", "sequence"]
.iter()
.any(|t| present.contains(*t))
|| root_layout(&user_root) == Some("sequence");
for r in class_defs(&present, &element_rules, &extra_order, synthesizes_shapes) {
stylesheet.push(StyleItem::Rule(r));
}
for r in classes::scoped_note_rules(&present, &user_rules) {
stylesheet.push(StyleItem::Rule(r));
}
for r in user_rules {
stylesheet.push(StyleItem::Rule(r));
}
Ok(File {
stylesheet,
stylesheet_span: Span::empty(),
instances,
links: file
.links
.iter()
.flat_map(labels::split_chain)
.map(|w| labels::lower_link(&w))
.collect(),
})
}
pub(crate) const FRAME_TYPES: [&str; 4] = ["loop", "opt", "alt", "else"];
fn is_frame_classes(classes: &[String]) -> bool {
classes.iter().any(|c| {
c.strip_prefix("lini-")
.is_some_and(|x| FRAME_TYPES.contains(&x))
})
}
fn gather_frame_messages(children: &[Child]) -> Vec<&Link> {
let mut out = Vec::new();
for c in children {
if let Child::Box(n) = c
&& is_frame_classes(&n.classes)
{
out.extend(n.links.iter());
out.extend(gather_frame_messages(&n.children));
}
}
out
}
fn lower_child(
child: &Child,
types: &Types,
bodies: &Bodies,
in_drawing: bool,
) -> Result<Child, Error> {
match child {
Child::Box(n) => Ok(Child::Box(lower_node(n, types, bodies, in_drawing)?)),
Child::Text(t) => Ok(Child::Text(t.clone())),
}
}
fn decl(name: &str, values: Vec<Value>) -> Decl {
Decl {
name: name.into(),
groups: vec![values],
span: Span::empty(),
}
}
fn lower_node(
node: &Node,
types: &Types,
bodies: &Bodies,
in_drawing: bool,
) -> Result<Node, Error> {
let ty = node.ty.as_deref().unwrap_or("box");
let info = types.resolve(ty, node.span)?;
let kind = info.kind;
let is_drawing = is_drawing_body(&info.chain, &node.style);
let child_in_drawing =
is_drawing || (in_drawing && !seals_drawing_scope(&info.chain, &node.style));
let already = NodeKind::parse(ty).is_some()
&& node.classes.iter().any(|c| *c == lini_class(kind.as_str()));
let classes = if already {
node.classes.clone()
} else {
let mut cs = worn_classes(&info);
cs.extend(node.classes.iter().cloned());
cs
};
let new_ty = if already {
node.ty.clone()
} else {
Some(kind.as_str().to_string())
};
let mut children = Vec::new();
if !already {
for name in &info.chain {
if let Some((body, _)) = bodies.get(name) {
for c in body {
children.push(lower_child(c, types, bodies, child_in_drawing)?);
}
}
}
}
for c in &node.children {
children.push(lower_child(c, types, bodies, child_in_drawing)?);
}
if !already && in_drawing {
for ch in drawing::chrome_children(node, kind, &info.chain) {
children.push(Child::Box(lower_node(&ch, types, bodies, false)?));
}
}
let is_page = info.chain.iter().any(|n| n == "page");
let mut page_style: Option<Vec<Decl>> = None;
if is_page {
let mut s = node.style.clone();
page::expand_sheet(&mut s)?;
page::default_direction(&mut s, node.span);
page_style = Some(s);
}
if !already && is_page {
for ch in page::chrome_children(page_style.as_deref().expect("a page"), node.span) {
children.push(Child::Box(lower_node(&ch, types, bodies, false)?));
}
}
if is_page {
for child in &mut children {
if let Child::Box(n) = child
&& n.classes.iter().any(|c| c == "lini-title-block")
&& !n.style.iter().any(|d| d.name == "pin")
{
n.style.push(decl(
"pin",
vec![Value::Ident("bottom".into()), Value::Ident("right".into())],
));
}
}
}
let is_entity = info.chain.iter().any(|n| n == "entity");
let is_table = !is_entity && info.chain.iter().any(|n| n == "table");
let cols = column_count(&node.style, &info.chain);
if is_table && let Some(cols) = cols {
wrap_header_row(&mut children, cols, types, bodies)?;
}
if is_table || is_entity {
wrap_body_cells(&mut children, types, bodies)?;
}
if (is_table || is_entity)
&& let Some(cols) = cols
{
distribute_cell_alignment(&mut children, &node.style, cols, is_entity)?;
}
let text_capable = !matches!(
kind,
NodeKind::Line | NodeKind::Poly | NodeKind::Path | NodeKind::Image
);
let is_icon = kind == NodeKind::Icon;
let is_container = info.chain.iter().any(|n| n == "group");
let mut style: Vec<Decl> = if is_table || is_entity {
node.style
.iter()
.filter(|d| d.name != "align" && d.name != "justify")
.cloned()
.collect()
} else if let Some(expanded) = page_style {
expanded
} else {
node.style.clone()
};
if in_drawing && info.chain.iter().any(|t| t == "plane") {
style.push(decl("chrome", vec![Value::Ident("plane".into())]));
}
let is_title_block = info.chain.iter().any(|t| t == "title-block");
let mut label = node.label.as_ref().filter(|l| !l.text.is_empty());
if is_title_block
&& let Some(l) = label.take()
&& !style.iter().any(|d| d.name == "title")
{
style.push(Decl {
name: "title".into(),
groups: vec![vec![Value::String(l.text.clone())]],
span: l.span,
});
}
if is_title_block && titleblock::has_fields(&style) {
for cell in titleblock::expand_fields(&mut style, node.span) {
children.push(Child::Box(lower_node(&cell, types, bodies, false)?));
}
}
let mut kept_label = None;
if let Some(label) = label {
if is_icon {
if style.iter().any(|d| d.name == "symbol") {
return Err(Error::at(
node.span,
"an icon's symbol is its label or 'symbol:', not both",
));
}
style.push(labels::symbol_decl(&label.text, node.span));
} else if is_entity {
let title = header_node(label, Some(cols.unwrap_or(2)));
children.insert(0, Child::Box(lower_node(&title, types, bodies, false)?));
} else if is_drawing {
let title = lower_node(&labels::footnote_node(label), types, bodies, false)?;
children.insert(0, Child::Box(title));
} else if is_container {
let caption = lower_node(&labels::caption_node(label), types, bodies, false)?;
children.insert(0, Child::Box(caption));
} else if text_capable {
children.insert(0, Child::Text(label.clone()));
} else {
kept_label = Some(label.clone());
}
}
if is_drawing
&& node.style.iter().any(|d| d.name == "of")
&& node.label.as_ref().filter(|l| !l.text.is_empty()).is_none()
{
let foot = labels::of_footnote(node.span);
children.insert(0, Child::Box(lower_node(&foot, types, bodies, false)?));
}
if is_entity && let Some(cols) = cols {
for child in &mut children {
if let Child::Box(n) = child
&& n.classes
.iter()
.any(|c| c == "lini-header" || c == "lini-footer")
&& !n.style.iter().any(|d| d.name == "span")
{
n.style.push(decl("span", vec![Value::Number(cols as f64)]));
}
}
}
let mut links = Vec::new();
if !already {
for name in &info.chain {
if let Some((_, body)) = bodies.get(name) {
for w in body {
links.extend(labels::split_chain(w).iter().map(labels::lower_link));
}
}
}
}
for w in &node.links {
links.extend(labels::split_chain(w).iter().map(labels::lower_link));
}
if !already && !is_frame_classes(&classes) && !is_drawing_body(&info.chain, &node.style) {
let declared = scene::declared_ids(&children);
let to_create = {
let mut msgs: Vec<&Link> = node.links.iter().collect();
msgs.extend(gather_frame_messages(&children));
scene::auto_created_ids(&msgs, &declared)
};
for (auto_id, auto_span) in to_create {
let created = lower_node(&scene::auto_box(&auto_id, auto_span), types, bodies, false)?;
children.push(Child::Box(created));
}
}
Ok(Node {
id: node.id.clone(),
ty: new_ty,
label: kept_label,
classes,
style,
style_span: node.style_span,
children,
links,
span: node.span,
})
}
fn rewrite_selector(rule: &Rule, types: &Types) -> Result<Rule, Error> {
let mut units = Vec::with_capacity(rule.selector.units.len());
for unit in &rule.selector.units {
match unit {
SelUnit::Type { name, id } => {
let class = if is_lini_class(name) {
name.clone()
} else if types.is_known(name) {
lini_class(name)
} else {
return Err(Error::at(
rule.span,
format!("unknown type '{}' in selector", name),
));
};
match id {
Some(_) => units.push(SelUnit::Type {
name: class,
id: id.clone(),
}),
None => units.push(SelUnit::Class(class)),
}
}
SelUnit::Class(c) => units.push(SelUnit::Class(c.clone())),
SelUnit::Id(i) => units.push(SelUnit::Id(i.clone())),
SelUnit::Link => units.push(SelUnit::Class(lini_class("link"))),
SelUnit::Dimension => units.push(SelUnit::Class(lini_class("dimension"))),
}
}
Ok(Rule {
selector: Selector { units },
decls: rule.decls.clone(),
span: rule.span,
})
}
fn mark_present(child: &Child, present: &mut BTreeSet<String>) {
if let Child::Box(n) = child {
for c in &n.classes {
if let Some(x) = c.strip_prefix("lini-") {
present.insert(x.to_string());
}
}
for ch in &n.children {
mark_present(ch, present);
}
}
}
fn is_drawing_body(chain: &[String], style: &[Decl]) -> bool {
chain.iter().any(|t| t == "drawing") || root_layout(style) == Some("drawing")
}
fn seals_drawing_scope(chain: &[String], style: &[Decl]) -> bool {
chain.iter().any(|t| {
matches!(
t.as_str(),
"row" | "column" | "grid" | "table" | "entity" | "chart" | "pie" | "sequence"
)
}) || style
.iter()
.any(|d| d.name == "layout" || d.name == "direction")
}
fn root_layout(user_root: &[Decl]) -> Option<&str> {
user_root
.iter()
.rev()
.find(|d| d.name == "layout")
.and_then(|d| match d.groups.as_slice() {
[group] => match group.as_slice() {
[Value::Ident(s)] => Some(s.as_str()),
_ => None,
},
_ => None,
})
}
fn push_unique(v: &mut Vec<String>, name: &str) {
if !v.iter().any(|x| x == name) {
v.push(name.to_string());
}
}