use crate::{
Doc, IndentKind,
options::{LineBreak, PrintOptions},
};
#[derive(Clone, Copy)]
enum Mode {
Flat,
Break,
}
type Action<'a, 'b> = (usize, Mode, &'b Doc<'a>);
pub fn print<'a>(doc: &Doc<'a>, options: &'a PrintOptions) -> String {
assert!(options.tab_size > 0);
let mut printer = Printer::new(options);
let mut out = String::with_capacity(1024);
printer.print_to((0, Mode::Break, doc), &mut out);
out
}
struct Printer<'a> {
options: &'a PrintOptions,
cols: usize,
}
impl<'a> Printer<'a> {
fn new(options: &'a PrintOptions) -> Self {
Self { options, cols: 0 }
}
fn print_to<'b>(&mut self, init_action: Action<'a, 'b>, out: &mut String) -> bool {
let line_break = match self.options.line_break {
LineBreak::Lf => "\n",
LineBreak::Crlf => "\r\n",
};
let mut actions = Vec::with_capacity(128);
actions.push(init_action);
let mut fits = true;
while let Some((indent, mode, doc)) = actions.pop() {
match doc {
Doc::Nil => {}
Doc::Alt(doc_flat, doc_break) => match mode {
Mode::Flat => actions.push((indent, mode, doc_flat)),
Mode::Break => actions.push((indent, mode, doc_break)),
},
Doc::Union(attempt, alternate) => {
let original_cols = self.cols;
let mut buf = String::new();
if self.print_to((indent, mode, attempt), &mut buf) {
unsafe {
out.as_mut_vec().append(buf.as_mut_vec());
}
} else {
self.cols = original_cols;
actions.push((indent, mode, alternate));
}
}
Doc::Nest(offset, doc) => {
actions.push((indent + offset, mode, doc));
}
Doc::Text(text) => {
self.cols += measure_text_width(text);
out.push_str(text);
fits &= self.cols <= self.options.width;
}
Doc::NewLine => {
self.cols = indent;
out.push_str(line_break);
match self.options.indent_kind {
IndentKind::Space => {
out.push_str(&" ".repeat(indent));
}
IndentKind::Tab => {
out.push_str(&"\t".repeat(indent / self.options.tab_size));
out.push_str(&" ".repeat(indent % self.options.tab_size));
}
}
fits &= self.cols <= self.options.width;
}
Doc::EmptyLine => {
out.push_str(line_break);
}
Doc::Break(spaces, offset) => {
match mode {
Mode::Flat => {
self.cols += spaces;
out.push_str(&" ".repeat(*spaces));
}
Mode::Break => {
self.cols = indent + offset;
out.push_str(line_break);
match self.options.indent_kind {
IndentKind::Space => {
out.push_str(&" ".repeat(self.cols));
}
IndentKind::Tab => {
out.push_str(&"\t".repeat(self.cols / self.options.tab_size));
out.push_str(&" ".repeat(self.cols % self.options.tab_size));
}
}
}
};
fits &= self.cols <= self.options.width;
}
Doc::Group(docs) => match mode {
Mode::Flat => {
actions.extend(docs.iter().map(|doc| (indent, Mode::Flat, doc)).rev());
}
Mode::Break => {
let fitting_actions = docs
.iter()
.map(|doc| (indent, Mode::Flat, doc))
.rev()
.collect();
let mode = if fitting(
fitting_actions,
actions.iter().rev(),
self.cols,
self.options.width,
) {
Mode::Flat
} else {
Mode::Break
};
actions.extend(docs.iter().map(|doc| (indent, mode, doc)).rev());
}
},
Doc::List(docs) => {
actions.extend(docs.iter().map(|doc| (indent, mode, doc)).rev());
}
Doc::Slice(docs) => {
actions.extend(docs.iter().map(|doc| (indent, mode, doc)).rev());
}
}
}
fits
}
}
fn fitting<'a, 'b>(
mut actions: Vec<Action<'a, 'b>>,
mut best_actions: impl Iterator<Item = &'b Action<'a, 'b>>,
mut cols: usize,
width: usize,
) -> bool {
while let Some((indent, mode, doc)) = actions.pop().or_else(|| best_actions.next().copied()) {
match doc {
Doc::Nil => {}
Doc::Alt(doc_flat, doc_break) => match mode {
Mode::Flat => actions.push((indent, mode, doc_flat)),
Mode::Break => actions.push((indent, mode, doc_break)),
},
Doc::Union(attempt, alternate) => match mode {
Mode::Flat => actions.push((indent, mode, attempt)),
Mode::Break => actions.push((indent, mode, alternate)),
},
Doc::Nest(offset, doc) => {
actions.push((indent + offset, mode, doc));
}
Doc::Text(text) => {
cols += measure_text_width(text);
}
Doc::Break(spaces, _) => match mode {
Mode::Flat => cols += spaces,
Mode::Break => return true,
},
Doc::NewLine => {
return matches!(mode, Mode::Break);
}
Doc::EmptyLine => {}
Doc::Group(docs) => {
actions.extend(docs.iter().map(|doc| (indent, mode, doc)).rev());
}
Doc::List(docs) => {
actions.extend(docs.iter().map(|doc| (indent, mode, doc)).rev());
}
Doc::Slice(docs) => {
actions.extend(docs.iter().map(|doc| (indent, mode, doc)).rev());
}
}
if cols > width {
return false;
}
}
true
}
#[cfg(not(feature = "unicode-width"))]
fn measure_text_width(text: &str) -> usize {
text.len()
}
#[cfg(feature = "unicode-width")]
fn measure_text_width(text: &str) -> usize {
use unicode_width::UnicodeWidthStr;
text.width()
}