use crate::ast::{LinkOp, Side};
use crate::error::Error;
use crate::lexer;
use crate::span::Span;
use crate::syntax::ast::{
Child, Decl, Define, Endpoint, File, Link, Node, Rule, SelPart, 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(&tokens)?;
let trivia = scan_trivia(src);
let mut out = String::new();
Emitter {
trivia: &trivia,
cursor: 0,
out: &mut out,
align: true,
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,
align: false,
terse: false,
}
.emit_file(file, 0);
out
}
struct Emitter<'a> {
trivia: &'a [TriviaToken],
cursor: usize,
out: &'a mut String,
align: bool,
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 !file.instances.is_empty() {
self.section_break(phases);
self.emit_children(&file.instances, 0);
phases += 1;
}
if !file.links.is_empty() {
self.section_break(phases);
for w in &file.links {
self.emit_trivia_before(w.span.start, 0);
self.emit_link(w, 0);
self.out.push('\n');
self.cursor = w.span.end;
}
}
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),
}
}
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) {
if let [SelPart::Class(c)] = sel.parts.as_slice() {
self.out.push('.');
self.out.push_str(c);
return;
}
self.out.push('|');
for (i, part) in sel.parts.iter().enumerate() {
if i > 0 {
self.out.push(' ');
}
match part {
SelPart::Type(t) => self.out.push_str(t),
SelPart::Class(c) => {
self.out.push('.');
self.out.push_str(c);
}
}
}
self.out.push('|');
}
fn emit_children(&mut self, children: &[Child], depth: usize) {
let widths = if self.align {
align::child_widths(children, self.trivia)
} else {
vec![align::NodeWidths::default(); children.len()]
};
for (i, c) in children.iter().enumerate() {
let span = child_span(c);
self.emit_trivia_before(span.start, depth);
match c {
Child::Box(n) => self.emit_node(n, depth, widths[i]),
Child::Text(t) => {
self.indent(depth);
self.emit_text_node(t, depth);
}
}
self.out.push('\n');
self.cursor = span.end;
}
}
fn emit_node(&mut self, node: &Node, depth: usize, w: align::NodeWidths) {
self.indent(depth);
let bars = type_bars(&node.ty);
let classes = class_str(&node.classes);
let has_block = !node.style.is_empty();
let has_body = !node.children.is_empty() || !node.links.is_empty();
let id = node.id.as_deref().unwrap_or("");
let id_w = if id.is_empty() && depth == 0 { 0 } else { w.id };
let after_id = !bars.is_empty() || !classes.is_empty() || has_block || has_body;
let after_ty = !classes.is_empty() || has_block || has_body;
let mut wrote = self.emit_col(id, id_w, after_id, false);
wrote = self.emit_col(&bars, w.ty, after_ty, wrote);
if !classes.is_empty() {
self.space_if(wrote);
self.out.push_str(&classes);
}
if has_block {
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, depth);
}
fn emit_col(&mut self, seg: &str, width: usize, follows: bool, preceded: bool) -> bool {
if seg.is_empty() && (width == 0 || !follows) {
return preceded;
}
self.space_if(preceded);
self.out.push_str(seg);
if follows {
pad(self.out, width.saturating_sub(seg.len()));
}
true
}
fn emit_content(&mut self, node: &Node, depth: usize) {
if node.children.is_empty() && node.links.is_empty() {
return;
}
let end = node.span.end;
if let Some(cols) = self.table_cols(node) {
self.out.push_str(" [\n");
self.emit_aligned_cells(&node.children, cols, depth + 1);
self.emit_trivia_before(end.saturating_sub(1), depth + 1);
self.indent(depth);
self.out.push(']');
return;
}
let text_only = node.links.is_empty()
&& node
.children
.iter()
.all(|c| matches!(c, Child::Text(t) if t.style.is_empty()));
if text_only && !self.has_trivia_between(self.cursor, end) {
if self.terse {
for c in &node.children {
if let Child::Text(t) = c {
self.out.push(' ');
self.emit_text_node(t, depth);
}
}
self.cursor = end;
return;
}
if self.try_inline_text(&node.children, end) {
return;
}
}
self.emit_body(&node.children, &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_children(children, depth + 1);
for w in links {
self.emit_trivia_before(w.span.start, depth + 1);
self.emit_link(w, depth + 1);
self.out.push('\n');
self.cursor = w.span.end;
}
self.emit_trivia_before(end.saturating_sub(1), depth + 1);
self.indent(depth);
self.out.push(']');
}
fn table_cols(&self, node: &Node) -> Option<usize> {
let cells = &node.children;
if cells.is_empty()
|| !node.links.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(&node.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 space_if(&mut self, cond: bool) {
if cond {
self.out.push(' ');
}
}
fn emit_grouped_decls(&mut self, decls: &[&Decl], depth: usize) {
let mut mid_line = false;
for d in decls {
if mid_line && 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);
}
self.emit_decl(d, false);
self.cursor = d.span.end;
mid_line = true;
}
if mid_line {
self.out.push('\n');
}
}
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 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(&link_op_str(w.op));
self.out.push(' ');
}
for (j, ep) in group.endpoints.iter().enumerate() {
if j > 0 {
self.out.push_str(" & ");
}
self.emit_endpoint(ep);
}
}
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 !w.labels.is_empty() {
let bare = w.labels.iter().all(|t| t.style.is_empty());
if self.terse && bare {
for label in &w.labels {
self.out.push(' ');
self.emit_text_node(label, depth);
}
} else {
self.out.push_str(" [ ");
for (i, label) in w.labels.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(side) = ep.side {
self.out.push('.');
self.out.push_str(side_str(side));
}
}
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_value(arg);
}
self.out.push(')');
}
}
}
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,
}
}
fn type_bars(ty: &Option<String>) -> String {
match ty {
Some(t) => format!("|{t}|"),
None => String::new(),
}
}
fn class_str(classes: &[String]) -> String {
let mut s = String::new();
for c in classes {
s.push('.');
s.push_str(c);
}
s
}
fn link_op_str(op: LinkOp) -> String {
format!(
"{}{}{}",
op.start.start_str(),
op.line.as_str(),
op.end.end_str()
)
}
fn side_str(s: Side) -> &'static str {
match s {
Side::Top => "top",
Side::Bottom => "bottom",
Side::Left => "left",
Side::Right => "right",
}
}
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 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)
}
}
mod align;
#[cfg(test)]
mod tests;