use crate::error::Error;
use crate::lexer;
use crate::span::Span;
use crate::syntax::ast::{
Child, Decl, Define, Endpoint, File, Link, Node, Rule, SelUnit, Selector, StyleItem, TextNode,
Value,
};
use crate::syntax::parser;
mod trivia;
use trivia::{Trivia, TriviaToken, scan_trivia};
const INDENT: &str = " ";
const MAX_LINE: usize = 80;
pub fn format(src: &str) -> Result<String, Error> {
let tokens = lexer::lex(src)?;
let file = parser::parse(src, &tokens)?;
let trivia = scan_trivia(src);
let mut out = String::new();
Emitter {
trivia: &trivia,
cursor: 0,
out: &mut out,
terse: true,
}
.emit_file(&file, src.len());
Ok(out)
}
pub(crate) fn print_file(file: &File) -> String {
let mut out = String::new();
Emitter {
trivia: &[],
cursor: 0,
out: &mut out,
terse: false,
}
.emit_file(file, 0);
out
}
struct Emitter<'a> {
trivia: &'a [TriviaToken],
cursor: usize,
out: &'a mut String,
terse: bool,
}
impl Emitter<'_> {
fn emit_file(&mut self, file: &File, src_len: usize) {
let mut phases = 0;
if !file.stylesheet.is_empty() {
self.emit_stylesheet(file);
phases += 1;
}
if phased(&file.instances, &file.links) {
if !file.instances.is_empty() {
self.section_break(phases);
self.emit_ordered(&file.instances, &[], 0);
phases += 1;
}
if !file.links.is_empty() {
self.section_break(phases);
self.emit_ordered(&[], &file.links, 0);
}
} else {
self.section_break(phases);
self.emit_ordered(&file.instances, &file.links, 0);
}
self.emit_trivia_before(src_len, 0);
if self.out.is_empty() {
return;
}
if !self.out.ends_with('\n') {
self.out.push('\n');
}
}
fn section_break(&mut self, phases_emitted: usize) {
if phases_emitted > 0 && !self.out.is_empty() && !self.out.ends_with("\n\n") {
self.out.push('\n');
}
}
fn emit_stylesheet(&mut self, file: &File) {
let span = file.stylesheet_span;
self.emit_trivia_before(span.start, 0);
self.out.push_str("{\n");
self.cursor = span.start.saturating_add(1);
let items = &file.stylesheet;
let mut i = 0;
while i < items.len() {
if matches!(items[i], StyleItem::RootDecl(_)) {
let start = i;
while i < items.len() && matches!(items[i], StyleItem::RootDecl(_)) {
i += 1;
}
let run: Vec<&Decl> = items[start..i]
.iter()
.map(|it| match it {
StyleItem::RootDecl(d) => d,
_ => unreachable!(),
})
.collect();
self.emit_grouped_decls(&run, 1);
} else {
self.emit_trivia_before(style_item_span(&items[i]).start, 1);
self.emit_style_item(&items[i], 1);
self.cursor = style_item_span(&items[i]).end;
i += 1;
}
}
self.emit_trivia_before(span.end.saturating_sub(1), 1);
self.out.push_str("}\n");
self.cursor = span.end;
}
fn emit_style_item(&mut self, item: &StyleItem, depth: usize) {
match item {
StyleItem::RootDecl(d) => {
self.indent(depth);
self.emit_decl(d, false);
self.out.push('\n');
}
StyleItem::Var(d) => {
self.indent(depth);
self.emit_decl(d, true);
self.out.push('\n');
}
StyleItem::Rule(r) => self.emit_rule(r, depth),
StyleItem::Define(d) => self.emit_define(d, depth),
StyleItem::Func(f) => {
self.indent(depth);
self.out.push_str(&f.name);
if !f.params.is_empty() {
self.out.push('(');
self.out.push_str(&f.params.join(", "));
self.out.push(')');
}
self.out.push_str(" = ");
let atomic = crate::expr::Expr::parse(&f.body)
.map(|e| e.is_atomic())
.unwrap_or(false);
if atomic {
self.out.push_str(f.body.trim());
} else {
self.out.push('(');
self.out.push_str(f.body.trim());
self.out.push(')');
}
self.out.push_str(";\n");
}
}
}
fn emit_rule(&mut self, rule: &Rule, depth: usize) {
self.indent(depth);
self.emit_selector(&rule.selector);
self.emit_style_block(&rule.decls, rule.span.end, depth, true);
self.out.push('\n');
}
fn emit_define(&mut self, def: &Define, depth: usize) {
self.indent(depth);
self.out.push('|');
self.out.push_str(&def.name);
self.out.push_str("::");
self.out.push_str(&def.base);
self.out.push('|');
if !def.style.is_empty() {
let end = def.style_span.map_or(def.span.end, |s| s.end);
self.emit_style_block(&def.style, end, depth, false);
}
self.emit_body(&def.children, &def.links, def.span.end, depth);
self.out.push('\n');
}
fn emit_selector(&mut self, sel: &Selector) {
for (i, unit) in sel.units.iter().enumerate() {
if i > 0 {
self.out.push(' ');
}
match unit {
SelUnit::Type { name, id } => {
self.out.push('|');
self.out.push_str(name);
if let Some(id) = id {
self.out.push('#');
self.out.push_str(id);
}
self.out.push('|');
}
SelUnit::Class(c) => {
self.out.push('.');
self.out.push_str(c);
}
SelUnit::Id(i) => {
self.out.push('#');
self.out.push_str(i);
}
SelUnit::Link => self.out.push_str("|-|"),
SelUnit::Dimension => self.out.push_str("(-)"),
}
}
}
fn emit_ordered(&mut self, children: &[Child], links: &[Link], depth: usize) {
enum Item<'a> {
Child(&'a Child),
Link(&'a Link),
}
let mut items: Vec<Item> = Vec::with_capacity(children.len() + links.len());
items.extend(children.iter().map(Item::Child));
items.extend(links.iter().map(Item::Link));
items.sort_by_key(|it| match it {
Item::Child(c) => child_span(c).start,
Item::Link(w) => w.span.start,
});
for it in items {
let (start, end) = match &it {
Item::Child(c) => {
let s = child_span(c);
(s.start, s.end)
}
Item::Link(w) => (w.span.start, w.span.end),
};
self.emit_trivia_before(start, depth);
match it {
Item::Child(Child::Box(n)) => self.emit_node(n, depth),
Item::Child(Child::Text(t)) => {
self.indent(depth);
self.emit_text_node(t, depth);
}
Item::Link(w) => self.emit_link(w, depth),
}
self.out.push('\n');
self.cursor = end;
}
}
fn emit_node(&mut self, node: &Node, depth: usize) {
self.indent(depth);
self.out.push_str(&identity_bars(node));
if let Some(label) = &node.label {
self.out.push(' ');
self.emit_string(&label.text);
}
if !node.classes.is_empty() {
self.out.push(' ');
self.out.push_str(&class_str(&node.classes));
}
if !node.style.is_empty() {
let end = node.style_span.map_or(node.span.end, |s| s.end);
self.emit_style_block(&node.style, end, depth, false);
}
self.emit_content(node, &node.children, depth);
}
fn emit_content(&mut self, node: &Node, body: &[Child], depth: usize) {
if body.is_empty() && node.links.is_empty() {
return;
}
let end = node.span.end;
if node.links.is_empty() {
if let Some(cols) = self.table_cols(body, &node.style) {
self.out.push_str(" [\n");
self.emit_aligned_cells(body, cols, depth + 1);
self.emit_trivia_before(end.saturating_sub(1), depth + 1);
self.indent(depth);
self.out.push(']');
return;
}
let text_only = !body.is_empty()
&& body
.iter()
.all(|c| matches!(c, Child::Text(t) if t.style.is_empty()));
if text_only
&& !self.has_trivia_between(self.cursor, end)
&& self.try_inline_text(body, end)
{
return;
}
}
self.emit_body(body, &node.links, end, depth);
}
fn try_inline_text(&mut self, children: &[Child], end: usize) -> bool {
let line_start = self.out.rfind('\n').map_or(0, |i| i + 1);
let saved = self.out.len();
self.out.push_str(" [ ");
for (i, c) in children.iter().enumerate() {
if i > 0 {
self.out.push(' ');
}
if let Child::Text(t) = c {
self.emit_text_node(t, 0);
}
}
self.out.push_str(" ]");
if self.out.len() - line_start <= MAX_LINE {
self.cursor = end;
true
} else {
self.out.truncate(saved);
false
}
}
fn emit_body(&mut self, children: &[Child], links: &[Link], end: usize, depth: usize) {
if children.is_empty() && links.is_empty() && !self.has_comment_in(self.cursor, end) {
return;
}
self.out.push_str(" [\n");
self.emit_ordered(children, links, depth + 1);
self.emit_trivia_before(end.saturating_sub(1), depth + 1);
self.indent(depth);
self.out.push(']');
}
fn table_cols(&self, cells: &[Child], style: &[Decl]) -> Option<usize> {
if cells.is_empty() || !cells.iter().all(|c| matches!(c, Child::Text(_))) {
return None;
}
let start = child_span(&cells[0]).start;
let end = child_span(cells.last().unwrap()).end;
if self.has_trivia_between(start, end) {
return None;
}
count_columns(style)
}
fn emit_aligned_cells(&mut self, cells: &[Child], cols: usize, depth: usize) {
let texts: Vec<String> = cells
.iter()
.map(|c| match c {
Child::Text(t) => quoted(&t.text),
Child::Box(_) => String::new(),
})
.collect();
let mut widths = vec![0usize; cols];
for (i, s) in texts.iter().enumerate() {
widths[i % cols] = widths[i % cols].max(s.len());
}
for (i, s) in texts.iter().enumerate() {
let col = i % cols;
if col == 0 {
self.indent(depth);
} else {
self.out.push(' ');
}
self.out.push_str(s);
if col == cols - 1 || i == texts.len() - 1 {
self.out.push('\n');
} else {
pad(self.out, widths[col] - s.len());
}
}
self.cursor = child_span(cells.last().unwrap()).end;
}
fn emit_grouped_decls(&mut self, decls: &[&Decl], depth: usize) {
let mut mid_line = false;
for d in decls {
if mid_line && (d.name == "draw" || self.has_trivia_between(self.cursor, d.span.start))
{
self.out.push('\n');
mid_line = false;
}
self.emit_trivia_before(d.span.start, depth);
if mid_line {
self.out.push(' ');
} else {
self.indent(depth);
}
if d.name == "draw" {
self.emit_draw_decl(d, depth);
self.cursor = d.span.end;
self.out.push('\n');
mid_line = false;
continue;
}
self.emit_decl(d, false);
self.cursor = d.span.end;
mid_line = true;
}
if mid_line {
self.out.push('\n');
}
}
fn emit_draw_decl(&mut self, d: &Decl, depth: usize) {
self.out.push_str("draw: ");
let pad = " ".repeat(INDENT.len() * depth + "draw: ".len());
let mut first = true;
for v in d.groups.iter().flatten() {
let start = self.out.len();
let new_subpath = matches!(v, Value::Call(c) if c.name == "move");
if !first {
if new_subpath {
self.out.push('\n');
self.out.push_str(&pad);
} else {
self.out.push(' ');
}
}
self.emit_value(v);
let line_start = self.out.rfind('\n').map_or(0, |i| i + 1);
if !first && !new_subpath && self.out.len() - line_start >= MAX_LINE {
self.out.truncate(start);
self.out.push('\n');
self.out.push_str(&pad);
self.emit_value(v);
}
first = false;
}
self.out.push(';');
}
fn has_trivia_between(&self, start: usize, end: usize) -> bool {
self.trivia.iter().any(|t| t.pos >= start && t.pos < end)
}
fn emit_style_block(&mut self, decls: &[Decl], end: usize, depth: usize, keep_empty: bool) {
if decls.is_empty() {
if keep_empty && !self.has_comment_in(self.cursor, end) {
self.out.push_str(" {}");
self.cursor = end;
}
return;
}
if self.try_inline_decls(decls, end) {
return;
}
self.out.push_str(" {\n");
let refs: Vec<&Decl> = decls.iter().collect();
self.emit_grouped_decls(&refs, depth + 1);
self.emit_trivia_before(end.saturating_sub(1), depth + 1);
self.indent(depth);
self.out.push('}');
}
fn try_inline_decls(&mut self, decls: &[Decl], end: usize) -> bool {
if self.has_trivia_between(self.cursor, end) {
return false;
}
let multi_subpath = |d: &Decl| {
d.name == "draw"
&& d.groups
.iter()
.flatten()
.filter(|v| matches!(v, Value::Call(c) if c.name == "move"))
.count()
> 1
};
if decls.iter().any(multi_subpath) {
return false;
}
let line_start = self.out.rfind('\n').map_or(0, |i| i + 1);
let saved = self.out.len();
self.out.push_str(" { ");
for (i, d) in decls.iter().enumerate() {
if i > 0 {
self.out.push(' ');
}
self.emit_decl(d, false);
}
self.out.push_str(" }");
if self.out.len() - line_start <= MAX_LINE {
self.cursor = end;
true
} else {
self.out.truncate(saved);
false
}
}
fn emit_link(&mut self, w: &Link, depth: usize) {
self.indent(depth);
for (i, group) in w.chain.iter().enumerate() {
if i > 0 {
self.out.push(' ');
self.out.push_str(&w.op.spelling());
self.out.push(' ');
}
for (j, ep) in group.endpoints.iter().enumerate() {
if j > 0 {
self.out.push_str(" & ");
}
self.emit_endpoint(ep);
}
}
if w.chain.len() == 1 {
self.out.push(' ');
self.out.push_str(&w.op.spelling());
}
let all: Vec<&TextNode> = w.label.iter().chain(w.labels.iter()).collect();
let styled = all.iter().any(|t| !t.style.is_empty());
let head_label = (self.terse && all.len() == 1 && !styled).then(|| all[0]);
if let Some(label) = head_label {
self.out.push(' ');
self.emit_text_node(label, depth);
}
if !w.classes.is_empty() {
self.out.push(' ');
self.out.push_str(&class_str(&w.classes));
}
if !w.style.is_empty() {
let end = w.style_span.map_or(w.span.end, |s| s.end);
self.emit_style_block(&w.style, end, depth, false);
}
if head_label.is_none() && !all.is_empty() {
self.out.push_str(" [ ");
for (i, label) in all.iter().enumerate() {
if i > 0 {
self.out.push(' ');
}
self.emit_text_node(label, depth);
}
self.out.push_str(" ]");
}
}
fn emit_text_node(&mut self, t: &TextNode, depth: usize) {
self.emit_string(&t.text);
if !t.style.is_empty() {
let end = t.style_span.map_or(t.span.end, |s| s.end);
self.emit_style_block(&t.style, end, depth, false);
}
}
fn emit_endpoint(&mut self, ep: &Endpoint) {
self.out.push_str(&ep.path.join("."));
if let Some(point) = &ep.point {
self.out.push(':');
self.out.push_str(&point.name);
}
}
fn emit_decl(&mut self, decl: &Decl, is_var: bool) {
if is_var {
self.out.push_str("--");
}
self.out.push_str(&decl.name);
self.out.push_str(": ");
for (i, group) in decl.groups.iter().enumerate() {
if i > 0 {
self.out.push_str(", ");
}
for (j, v) in group.iter().enumerate() {
if j > 0 {
self.out.push(' ');
}
self.emit_value(v);
}
}
self.out.push(';');
}
fn emit_value(&mut self, v: &Value) {
match v {
Value::Number(n) => self.out.push_str(&format_number(*n)),
Value::Percent(n) => {
self.out.push_str(&format_number(*n));
self.out.push('%');
}
Value::String(s) => self.emit_string(s),
Value::Hex(h) => {
self.out.push('#');
self.out.push_str(h);
}
Value::Ident(s) => self.out.push_str(s),
Value::Var(name) => {
self.out.push_str("--");
self.out.push_str(name);
}
Value::Call(c) => {
self.out.push_str(&c.name);
self.out.push('(');
for (i, arg) in c.args.iter().enumerate() {
if i > 0 {
self.out.push_str(", ");
}
self.emit_arg(arg);
}
self.out.push(')');
}
Value::Expr(s) => {
self.out.push('(');
self.out.push_str(s.trim());
self.out.push(')');
}
Value::NamedCall(c, name) => {
self.emit_value(&Value::Call(c.clone()));
self.out.push(':');
self.out.push_str(name);
}
Value::Group(items) => {
for (i, item) in items.iter().enumerate() {
if i > 0 {
self.out.push(' ');
}
self.emit_value(item);
}
}
}
}
fn emit_arg(&mut self, v: &Value) {
match v {
Value::Expr(s) => self.out.push_str(s.trim()),
_ => self.emit_value(v),
}
}
fn emit_string(&mut self, s: &str) {
self.out.push_str("ed(s));
}
fn indent(&mut self, depth: usize) {
for _ in 0..depth {
self.out.push_str(INDENT);
}
}
fn has_comment_in(&self, start: usize, end: usize) -> bool {
self.trivia
.iter()
.any(|t| matches!(t.kind, Trivia::Comment(_)) && t.pos >= start && t.pos < end)
}
fn emit_trivia_before(&mut self, until: usize, depth: usize) {
let mut last_was_blank = false;
for t in self.trivia {
if t.pos < self.cursor {
continue;
}
if t.pos >= until {
break;
}
match &t.kind {
Trivia::Comment(text) => {
self.indent(depth);
self.out.push_str(text);
self.out.push('\n');
last_was_blank = false;
}
Trivia::BlankLine => {
if !last_was_blank && !self.out.is_empty() && !self.out.ends_with("\n\n") {
self.out.push('\n');
last_was_blank = true;
}
}
}
}
self.cursor = until;
}
}
fn style_item_span(item: &StyleItem) -> Span {
match item {
StyleItem::RootDecl(d) | StyleItem::Var(d) => d.span,
StyleItem::Rule(r) => r.span,
StyleItem::Define(d) => d.span,
StyleItem::Func(f) => f.span,
}
}
fn identity_bars(node: &Node) -> String {
let mut s = String::from("|");
if let Some(t) = &node.ty {
s.push_str(t);
}
if let Some(id) = &node.id {
s.push('#');
s.push_str(id);
}
s.push('|');
s
}
fn class_str(classes: &[String]) -> String {
let mut s = String::new();
for c in classes {
s.push('.');
s.push_str(c);
}
s
}
fn pad(out: &mut String, n: usize) {
for _ in 0..n {
out.push(' ');
}
}
fn child_span(c: &Child) -> Span {
match c {
Child::Box(n) => n.span,
Child::Text(t) => t.span,
}
}
fn phased(instances: &[Child], links: &[Link]) -> bool {
match (
instances.iter().map(|c| child_span(c).start).max(),
links.iter().map(|w| w.span.start).min(),
) {
(Some(last_instance), Some(first_link)) => last_instance < first_link,
_ => true,
}
}
fn quoted(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\t' => out.push_str("\\t"),
_ => out.push(c),
}
}
out.push('"');
out
}
fn count_columns(decls: &[Decl]) -> Option<usize> {
let d = decls.iter().find(|d| d.name == "columns")?;
let n: usize = d
.groups
.iter()
.flatten()
.map(|v| match v {
Value::Call(c) if c.name == "repeat" => c
.args
.first()
.and_then(|a| match a {
Value::Number(x) if *x >= 1.0 => Some(*x as usize),
_ => None,
})
.unwrap_or(1),
_ => 1,
})
.sum();
(n > 0).then_some(n)
}
fn format_number(n: f64) -> String {
if n.fract() == 0.0 && n.is_finite() && n.abs() < 1e15 {
format!("{}", n as i64)
} else {
format!("{}", n)
}
}
#[cfg(test)]
mod tests;