use crate::error::Error;
use crate::span::Span;
use crate::syntax::ast::{Decl, Node, TextNode, Value};
const SIZES: &[(&str, f64, f64, bool)] = &[
("a0", 841.0, 1189.0, true),
("a1", 594.0, 841.0, true),
("a2", 420.0, 594.0, true),
("a3", 297.0, 420.0, true),
("a4", 210.0, 297.0, false),
("a5", 148.0, 210.0, false),
("a", 215.9, 279.4, false),
("b", 279.4, 431.8, true),
("c", 431.8, 558.8, true),
("d", 558.8, 863.6, true),
("e", 863.6, 1117.6, true),
];
pub(super) const DEFAULT: (f64, f64) = (210.0, 297.0);
pub(super) fn expand_sheet(style: &mut Vec<Decl>) -> Result<(), Error> {
let Some(at) = style.iter().position(|d| d.name == "sheet") else {
return Ok(());
};
let d = &style[at];
let (span, values) = (d.span, d.groups.first().cloned().unwrap_or_default());
let bad = |got: &str| {
let mut msg =
"'sheet' takes a size — a5…a0 (ISO) or a…e (ANSI) — and an optional portrait / landscape"
.to_string();
let candidates = [
"a0",
"a1",
"a2",
"a3",
"a4",
"a5",
"a",
"b",
"c",
"d",
"e",
"portrait",
"landscape",
];
if let Some(near) = candidates
.iter()
.filter(|c| edit_distance(got, c) <= 2)
.min_by_key(|c| edit_distance(got, c))
{
msg.push_str(&format!(" — did you mean '{near}'?"));
}
Error::at(span, msg)
};
let mut size: Option<(f64, f64, bool)> = None;
let mut orient: Option<&str> = None;
for v in &values {
let Value::Ident(word) = v else {
return Err(bad("…"));
};
if let Some(&(_, w, h, land)) = SIZES.iter().find(|(n, ..)| n == word) {
size = Some((w, h, land));
} else if word == "portrait" || word == "landscape" {
orient = Some(word);
} else {
return Err(bad(word));
}
}
let Some((pw, ph, default_landscape)) = size else {
return Err(bad(""));
};
let landscape = match orient {
Some(o) => o == "landscape",
None => default_landscape,
};
let (w, h) = if landscape { (ph, pw) } else { (pw, ph) };
let decl = |name: &str, v: f64| Decl {
name: name.into(),
groups: vec![vec![Value::Number(v)]],
span,
};
style.splice(at..=at, [decl("width", w), decl("height", h)]);
Ok(())
}
fn edit_distance(a: &str, b: &str) -> usize {
let (a, b): (Vec<char>, Vec<char>) = (a.chars().collect(), b.chars().collect());
let mut row: Vec<usize> = (0..=b.len()).collect();
for (i, ca) in a.iter().enumerate() {
let mut prev = row[0];
row[0] = i + 1;
for (j, cb) in b.iter().enumerate() {
let cur = row[j + 1];
row[j + 1] = (prev + usize::from(ca != cb)).min(row[j] + 1).min(cur + 1);
prev = cur;
}
}
row[b.len()]
}
fn dims(style: &[Decl]) -> (f64, f64) {
let num = |name: &str| {
style.iter().rev().find(|d| d.name == name).and_then(|d| {
match d.groups.first()?.first()? {
Value::Number(n) => Some(*n),
_ => None,
}
})
};
(
num("width").unwrap_or(DEFAULT.0),
num("height").unwrap_or(DEFAULT.1),
)
}
fn zone_count(mm: f64) -> usize {
let r = mm / 50.0;
let lo = ((r / 2.0).floor() * 2.0).max(2.0);
let hi = lo + 2.0;
if (r - lo) <= (hi - r) {
lo as usize
} else {
hi as usize
}
}
pub(super) fn chrome_children(style: &[Decl], at: Span) -> Vec<Node> {
let (w, h) = dims(style);
let (cols, rows) = (zone_count(w), zone_count(h));
let mut out = vec![chrome(
at,
"frame",
vec![Value::Ident("frame".into())],
None,
)];
for (edge, n) in [("top", cols), ("bottom", cols)] {
for i in (1..n).filter(|i| *i != n / 2) {
out.push(tick(at, edge, i));
}
}
for (edge, n) in [("left", rows), ("right", rows)] {
for i in (1..n).filter(|i| *i != n / 2) {
out.push(tick(at, edge, i));
}
}
for edge in ["top", "bottom", "left", "right"] {
out.push(chrome(
at,
"tick",
vec![Value::Ident("mark".into()), Value::Ident(edge.into())],
Some(points_placeholder(at)),
));
}
for (edge, n) in [("top", cols), ("bottom", cols)] {
for i in 0..n {
out.push(zone(at, edge, i, (i + 1).to_string()));
}
}
for (edge, n) in [("left", rows), ("right", rows)] {
for i in 0..n {
let letter = char::from(b'A' + (i % 26) as u8).to_string();
out.push(zone(at, edge, i, letter));
}
}
out
}
fn tick(at: Span, edge: &str, i: usize) -> Node {
chrome(
at,
"tick",
vec![
Value::Ident("tick".into()),
Value::Ident(edge.into()),
Value::Number(i as f64),
],
Some(points_placeholder(at)),
)
}
fn zone(at: Span, edge: &str, i: usize, label: String) -> Node {
let tail = Span::new(at.end, at.end);
let mut n = chrome(
at,
"zone",
vec![
Value::Ident("zone".into()),
Value::Ident(edge.into()),
Value::Number(i as f64),
],
None,
);
n.label = Some(TextNode {
text: label,
style: Vec::new(),
style_span: None,
span: tail,
});
n
}
fn points_placeholder(at: Span) -> Decl {
let tail = Span::new(at.end, at.end);
Decl {
name: "points".into(),
groups: vec![
vec![Value::Number(0.0), Value::Number(0.0)],
vec![Value::Number(0.0), Value::Number(0.0)],
],
span: tail,
}
}
fn chrome(at: Span, ty: &str, marker: Vec<Value>, extra: Option<Decl>) -> Node {
let tail = Span::new(at.end, at.end);
let mut style = vec![
Decl {
name: "chrome".into(),
groups: vec![marker],
span: tail,
},
Decl {
name: "pin".into(),
groups: vec![vec![Value::Ident("center".into())]],
span: tail,
},
];
style.extend(extra);
Node {
id: None,
ty: Some(ty.into()),
label: None,
classes: Vec::new(),
style,
style_span: None,
children: Vec::new(),
links: Vec::new(),
span: tail,
}
}