use std::borrow::Cow;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Doc {
Nil,
Text(Cow<'static, str>),
Line,
SoftLine,
HardLine,
Concat(Vec<Doc>),
Nest(usize, Box<Doc>),
Group(Box<Doc>),
}
impl Doc {
pub fn text(s: impl Into<Cow<'static, str>>) -> Doc {
Doc::Text(s.into())
}
}
pub fn nil() -> Doc {
Doc::Nil
}
pub fn line() -> Doc {
Doc::Line
}
pub fn softline() -> Doc {
Doc::SoftLine
}
pub fn hardline() -> Doc {
Doc::HardLine
}
pub fn concat(docs: impl IntoIterator<Item = Doc>) -> Doc {
let parts: Vec<Doc> = docs
.into_iter()
.filter(|d| !matches!(d, Doc::Nil))
.collect();
match parts.len() {
0 => Doc::Nil,
1 => parts.into_iter().next().expect("len checked"),
_ => Doc::Concat(parts),
}
}
pub fn nest(width: usize, doc: Doc) -> Doc {
if matches!(doc, Doc::Nil) {
return Doc::Nil;
}
Doc::Nest(width, Box::new(doc))
}
pub fn group(doc: Doc) -> Doc {
if matches!(doc, Doc::Nil) {
return Doc::Nil;
}
Doc::Group(Box::new(doc))
}
pub fn join(sep: Doc, items: impl IntoIterator<Item = Doc>) -> Doc {
let mut out = Vec::new();
for item in items {
if matches!(item, Doc::Nil) {
continue;
}
if !out.is_empty() {
out.push(sep.clone());
}
out.push(item);
}
concat(out)
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Mode {
Flat,
Break,
}
fn text_width(s: &str) -> usize {
s.chars().count()
}
fn fits(
mut remaining: isize,
group_indent: usize,
group_doc: &Doc,
rest: &[(usize, Mode, &Doc)],
) -> bool {
let mut local: Vec<(usize, Mode, &Doc)> = vec![(group_indent, Mode::Flat, group_doc)];
let mut rest_top = rest.len();
loop {
if remaining < 0 {
return false;
}
let (indent, mode, doc) = match local.pop() {
Some(cmd) => cmd,
None => {
if rest_top == 0 {
return true;
}
rest_top -= 1;
rest[rest_top]
}
};
match doc {
Doc::Nil => {}
Doc::Text(s) => remaining -= text_width(s) as isize,
Doc::Concat(parts) => {
for part in parts.iter().rev() {
local.push((indent, mode, part));
}
}
Doc::Nest(width, inner) => local.push((indent + width, mode, inner)),
Doc::Group(inner) => local.push((indent, Mode::Flat, inner)),
Doc::Line => match mode {
Mode::Flat => remaining -= 1,
Mode::Break => return true,
},
Doc::SoftLine => match mode {
Mode::Flat => {}
Mode::Break => return true,
},
Doc::HardLine => match mode {
Mode::Flat => return false,
Mode::Break => return true,
},
}
}
}
pub fn layout(doc: &Doc, width: usize) -> String {
let mut out = String::new();
let mut pos: isize = 0;
let mut stack: Vec<(usize, Mode, &Doc)> = vec![(0, Mode::Break, doc)];
while let Some((indent, mode, doc)) = stack.pop() {
match doc {
Doc::Nil => {}
Doc::Text(s) => {
out.push_str(s);
pos += text_width(s) as isize;
}
Doc::Concat(parts) => {
for part in parts.iter().rev() {
stack.push((indent, mode, part));
}
}
Doc::Nest(w, inner) => stack.push((indent + w, mode, inner)),
Doc::Group(inner) => {
let flat = mode == Mode::Flat || fits(width as isize - pos, indent, inner, &stack);
let inner_mode = if flat { Mode::Flat } else { Mode::Break };
stack.push((indent, inner_mode, inner));
}
Doc::Line => match mode {
Mode::Flat => {
out.push(' ');
pos += 1;
}
Mode::Break => {
new_line(&mut out, indent);
pos = indent as isize;
}
},
Doc::SoftLine => match mode {
Mode::Flat => {}
Mode::Break => {
new_line(&mut out, indent);
pos = indent as isize;
}
},
Doc::HardLine => {
new_line(&mut out, indent);
pos = indent as isize;
}
}
}
out
}
fn new_line(out: &mut String, indent: usize) {
while out.ends_with(' ') {
out.pop();
}
out.push('\n');
for _ in 0..indent {
out.push(' ');
}
}
#[cfg(test)]
mod tests {
use super::*;
fn list_doc() -> Doc {
group(concat([
Doc::text("SELECT"),
nest(
2,
concat([
line(),
Doc::text("a,"),
line(),
Doc::text("b,"),
line(),
Doc::text("c"),
]),
),
]))
}
#[test]
fn group_stays_flat_when_it_fits() {
assert_eq!(layout(&list_doc(), 80), "SELECT a, b, c");
}
#[test]
fn group_breaks_when_too_wide() {
assert_eq!(layout(&list_doc(), 10), "SELECT\n a,\n b,\n c");
}
#[test]
fn hardline_forces_enclosing_group_to_break() {
let doc = group(concat([Doc::text("a"), hardline(), Doc::text("b")]));
assert_eq!(layout(&doc, 80), "a\nb");
}
#[test]
fn nested_group_breaks_independently() {
let inner = group(concat([
Doc::text("("),
nest(
2,
concat([softline(), Doc::text("x"), line(), Doc::text("y")]),
),
softline(),
Doc::text(")"),
]));
let doc = concat([Doc::text("SELECT"), hardline(), inner]);
assert_eq!(layout(&doc, 80), "SELECT\n(x y)");
}
#[test]
fn softline_vanishes_when_flat_and_breaks_when_wide() {
let doc = group(concat([
Doc::text("("),
nest(2, concat([softline(), Doc::text("longcontent")])),
softline(),
Doc::text(")"),
]));
assert_eq!(layout(&doc, 80), "(longcontent)");
assert_eq!(layout(&doc, 5), "(\n longcontent\n)");
}
#[test]
fn trailing_spaces_are_trimmed_at_breaks() {
let doc = concat([Doc::text("a"), Doc::HardLine, Doc::text("b")]);
assert_eq!(layout(&doc, 80), "a\nb");
}
}