use std::borrow::Cow;
use unicode_width::UnicodeWidthStr;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Doc {
Text(Cow<'static, str>, usize),
SourceCodeSlice(Cow<'static, str>, usize),
Line(LineKind),
Concat(Vec<Doc>),
BestFitting(Vec<Doc>),
Group {
content: Box<Doc>,
expand: bool,
must_break: bool,
},
IfBreak { broken: Box<Doc>, flat: Box<Doc> },
Indent(Box<Doc>),
LineSuffix(Box<Doc>),
BreakParent,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LineKind {
Space,
Soft,
Hard,
}
pub fn text(s: impl Into<Cow<'static, str>>) -> Doc {
let s = s.into();
let width = text_width(&s);
Doc::Text(s, width)
}
pub fn source_code_slice(s: impl Into<Cow<'static, str>>) -> Doc {
let s = s.into();
let width = last_line_width(&s);
Doc::SourceCodeSlice(s, width)
}
pub fn concat(parts: Vec<Doc>) -> Doc {
Doc::Concat(parts)
}
pub fn group(inner: Doc) -> Doc {
let must_break = has_forced_break(&inner);
Doc::Group {
content: Box::new(inner),
expand: false,
must_break,
}
}
pub fn group_expanded(inner: Doc) -> Doc {
Doc::Group {
content: Box::new(inner),
expand: true,
must_break: true,
}
}
pub fn indent(inner: Doc) -> Doc {
Doc::Indent(Box::new(inner))
}
#[doc(hidden)]
pub fn block_indent(inner: Doc) -> Doc {
concat(vec![indent(concat(vec![hard_line(), inner])), hard_line()])
}
#[doc(hidden)]
pub fn if_group_breaks(broken: Doc, flat: Doc) -> Doc {
Doc::IfBreak {
broken: Box::new(broken),
flat: Box::new(flat),
}
}
pub fn line() -> Doc {
Doc::Line(LineKind::Space)
}
pub fn soft_line() -> Doc {
Doc::Line(LineKind::Soft)
}
pub fn hard_line() -> Doc {
Doc::Line(LineKind::Hard)
}
pub fn space() -> Doc {
Doc::Text(Cow::Borrowed(" "), 1)
}
pub fn empty() -> Doc {
Doc::Concat(Vec::new())
}
pub fn line_suffix(inner: Doc) -> Doc {
Doc::LineSuffix(Box::new(inner))
}
pub fn break_parent() -> Doc {
Doc::BreakParent
}
pub fn join(sep: Doc, items: Vec<Doc>) -> Doc {
let mut parts = Vec::with_capacity(items.len().saturating_mul(2));
for (i, item) in items.into_iter().enumerate() {
if i > 0 {
parts.push(sep.clone());
}
parts.push(item);
}
Doc::Concat(parts)
}
#[doc(hidden)]
pub fn best_fitting(candidates: Vec<Doc>) -> Doc {
Doc::BestFitting(candidates)
}
#[doc(hidden)]
#[derive(Default)]
pub struct DocBuffer {
parts: Vec<Doc>,
}
impl DocBuffer {
pub fn new() -> Self {
DocBuffer { parts: Vec::new() }
}
pub fn write_element(&mut self, doc: Doc) {
self.parts.push(doc);
}
pub fn write_fmt_value<T: Format + ?Sized>(&mut self, value: &T) {
value.fmt(self);
}
pub fn write_with_rule<T: ?Sized, R: FormatRule<T>>(&mut self, rule: &R, item: &T) {
rule.fmt(item, self);
}
pub fn finish(mut self) -> Doc {
if self.parts.len() == 1 {
self.parts.pop().expect("len checked == 1")
} else {
Doc::Concat(self.parts)
}
}
}
#[doc(hidden)]
pub trait Format {
fn fmt(&self, buffer: &mut DocBuffer);
}
#[doc(hidden)]
pub trait FormatRule<T: ?Sized> {
fn fmt(&self, item: &T, buffer: &mut DocBuffer);
}
impl Format for Doc {
fn fmt(&self, buffer: &mut DocBuffer) {
buffer.write_element(self.clone());
}
}
impl<T: Format + ?Sized> Format for &T {
fn fmt(&self, buffer: &mut DocBuffer) {
(**self).fmt(buffer);
}
}
#[doc(hidden)]
pub fn format_value<T: Format + ?Sized>(value: &T) -> Doc {
let mut buffer = DocBuffer::new();
buffer.write_fmt_value(value);
buffer.finish()
}
#[doc(hidden)]
#[macro_export]
macro_rules! doc_write {
($buffer:expr, [ $( $element:expr ),* $(,)? ]) => {{
$( $buffer.write_fmt_value(&$element); )*
}};
}
#[doc(inline)]
pub use crate::doc_write;
#[derive(Clone, Copy, Debug)]
pub struct PrintOptions {
pub line_width: usize,
pub indent_width: usize,
}
impl Default for PrintOptions {
fn default() -> Self {
PrintOptions {
line_width: 100,
indent_width: 4,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Mode {
Flat,
Break,
}
#[derive(Clone, Copy)]
struct Cmd<'a> {
indent: usize,
mode: Mode,
doc: &'a Doc,
}
fn text_width(s: &str) -> usize {
if s.is_ascii() {
return s.len();
}
UnicodeWidthStr::width(s)
}
fn last_line_width(s: &str) -> usize {
match s.rfind('\n') {
Some(nl) => text_width(&s[nl + 1..]),
None => text_width(s),
}
}
fn has_forced_break(doc: &Doc) -> bool {
match doc {
Doc::Line(LineKind::Hard) | Doc::BreakParent => true,
Doc::Line(_) | Doc::Text(..) | Doc::SourceCodeSlice(..) => false,
Doc::LineSuffix(_) => false,
Doc::IfBreak { flat, .. } => has_forced_break(flat),
Doc::Concat(parts) => parts.iter().any(has_forced_break),
Doc::BestFitting(candidates) => {
!candidates.is_empty() && candidates.iter().all(has_forced_break)
}
Doc::Indent(inner) => has_forced_break(inner),
Doc::Group { must_break, .. } => *must_break,
}
}
fn fits(mut remaining: isize, rest: &[Cmd], next: Cmd, opts: &PrintOptions) -> bool {
if remaining < 0 {
return false;
}
let mut stack: Vec<Cmd> = vec![next];
let mut rest_idx = rest.len();
loop {
let cmd = match stack.pop() {
Some(cmd) => cmd,
None => {
if rest_idx == 0 {
return true;
}
rest_idx -= 1;
rest[rest_idx]
}
};
match cmd.doc {
Doc::Text(_, width) => {
remaining -= *width as isize;
if remaining < 0 {
return false;
}
}
Doc::SourceCodeSlice(s, _) => {
let mut pieces = s.split('\n');
let first = pieces.next().unwrap_or_default();
remaining -= text_width(first) as isize;
if remaining < 0 {
return false;
}
for piece in pieces {
remaining =
opts.line_width as isize - cmd.indent as isize - text_width(piece) as isize;
if remaining < 0 {
return false;
}
}
}
Doc::IfBreak { broken, flat } => {
let chosen = if cmd.mode == Mode::Break {
broken
} else {
flat
};
stack.push(Cmd { doc: chosen, ..cmd });
}
Doc::Concat(parts) => {
for part in parts.iter().rev() {
stack.push(Cmd { doc: part, ..cmd });
}
}
Doc::BestFitting(candidates) => {
if let Some(candidate) = candidates.first() {
stack.push(Cmd {
doc: candidate,
..cmd
});
}
}
Doc::Indent(inner) => stack.push(Cmd {
indent: cmd.indent + opts.indent_width,
doc: inner,
mode: cmd.mode,
}),
Doc::Group {
content,
must_break,
..
} => {
let mode = if *must_break { Mode::Break } else { Mode::Flat };
stack.push(Cmd {
indent: cmd.indent,
mode,
doc: content,
});
}
Doc::LineSuffix(_) | Doc::BreakParent => {}
Doc::Line(kind) => match cmd.mode {
Mode::Break => return true,
Mode::Flat => match kind {
LineKind::Hard => return true,
LineKind::Soft => {}
LineKind::Space => {
remaining -= 1;
if remaining < 0 {
return false;
}
}
},
},
}
}
}
pub fn print(doc: &Doc, opts: &PrintOptions) -> String {
let mut out = String::new();
let mut col = 0usize;
let mut cmds: Vec<Cmd> = vec![Cmd {
indent: 0,
mode: Mode::Break,
doc,
}];
let mut line_suffixes: Vec<Cmd> = Vec::new();
loop {
let cmd = match cmds.pop() {
Some(cmd) => cmd,
None if !line_suffixes.is_empty() => {
while let Some(suffix) = line_suffixes.pop() {
cmds.push(suffix);
}
continue;
}
None => break,
};
match cmd.doc {
Doc::Text(s, width) => {
out.push_str(s);
col += *width;
}
Doc::SourceCodeSlice(s, width) => {
let mut first = true;
for piece in s.split('\n') {
if !first {
out.push('\n');
for _ in 0..cmd.indent {
out.push(' ');
}
}
out.push_str(piece);
first = false;
}
col = if s.contains('\n') {
cmd.indent + *width
} else {
col + *width
};
}
Doc::IfBreak { broken, flat } => {
let chosen = if cmd.mode == Mode::Break {
broken
} else {
flat
};
cmds.push(Cmd { doc: chosen, ..cmd });
}
Doc::Concat(parts) => {
for part in parts.iter().rev() {
cmds.push(Cmd { doc: part, ..cmd });
}
}
Doc::BestFitting(candidates) => {
if candidates.is_empty() {
continue;
}
let mut chosen = candidates.last().expect("non-empty candidates");
for candidate in candidates {
if fits(
opts.line_width as isize - col as isize,
&cmds,
Cmd {
indent: cmd.indent,
mode: cmd.mode,
doc: candidate,
},
opts,
) {
chosen = candidate;
break;
}
}
cmds.push(Cmd { doc: chosen, ..cmd });
}
Doc::LineSuffix(inner) => line_suffixes.push(Cmd { doc: inner, ..cmd }),
Doc::BreakParent => {}
Doc::Indent(inner) => cmds.push(Cmd {
indent: cmd.indent + opts.indent_width,
doc: inner,
mode: cmd.mode,
}),
Doc::Group {
content,
must_break,
..
} => {
let mode = if *must_break {
Mode::Break
} else if fits(
opts.line_width as isize - col as isize,
&cmds,
Cmd {
indent: cmd.indent,
mode: Mode::Flat,
doc: content,
},
opts,
) {
Mode::Flat
} else {
Mode::Break
};
cmds.push(Cmd {
indent: cmd.indent,
mode,
doc: content,
});
}
Doc::Line(kind) => {
let newline = match cmd.mode {
Mode::Break => true,
Mode::Flat => match kind {
LineKind::Hard => true,
LineKind::Space => {
out.push(' ');
col += 1;
false
}
LineKind::Soft => false,
},
};
if newline {
if !line_suffixes.is_empty() {
cmds.push(cmd);
while let Some(suffix) = line_suffixes.pop() {
cmds.push(suffix);
}
continue;
}
out.push('\n');
for _ in 0..cmd.indent {
out.push(' ');
}
col = cmd.indent;
}
}
}
}
finalize(out)
}
fn finalize(raw: String) -> String {
let mut out = String::with_capacity(raw.len());
let mut started = false;
let mut pending_blanks = 0usize;
for line in raw.lines() {
let line = line.trim_end();
if line.is_empty() {
if started {
pending_blanks += 1;
}
continue;
}
if started {
out.push('\n');
for _ in 0..pending_blanks {
out.push('\n');
}
}
pending_blanks = 0;
out.push_str(line);
started = true;
}
if started {
out.push('\n');
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn p(doc: &Doc, width: usize) -> String {
print(
doc,
&PrintOptions {
line_width: width,
indent_width: 4,
},
)
}
#[test]
fn text_and_concat() {
let doc = concat(vec![text("a"), space(), text("b")]);
assert_eq!(p(&doc, 80), "a b\n");
}
#[test]
fn cjk_text_is_measured_double_width() {
assert_eq!(text_width("abc"), 3);
assert_eq!(text_width("長芋"), 4); assert_eq!(text_width("a長"), 3); let doc = group(concat(vec![text("長"), line(), text("芋")]));
assert_eq!(p(&doc, 4), "長\n芋\n");
assert_eq!(p(&doc, 5), "長 芋\n");
}
#[test]
fn non_ascii_width_uses_unicode_sequence_width() {
assert_eq!(text_width("e\u{301}"), 1); assert_eq!(text_width("👩\u{200d}💻"), 2);
let doc = group(concat(vec![text("e\u{301}"), line(), text("👩\u{200d}💻")]));
assert_eq!(p(&doc, 3), "e\u{301}\n👩\u{200d}💻\n");
assert_eq!(p(&doc, 4), "e\u{301} 👩\u{200d}💻\n");
}
#[test]
fn group_stays_flat_when_it_fits() {
let doc = group(concat(vec![text("a"), line(), text("b")]));
assert_eq!(p(&doc, 80), "a b\n");
}
#[test]
fn group_breaks_when_too_wide() {
let doc = group(concat(vec![
text("aaaa"),
indent(concat(vec![line(), text("bbbb")])),
]));
assert_eq!(p(&doc, 5), "aaaa\n bbbb\n");
}
#[test]
fn hard_line_forces_enclosing_group_to_break() {
let doc = group(concat(vec![text("a"), hard_line(), text("b")]));
assert_eq!(p(&doc, 80), "a\nb\n");
}
#[test]
fn soft_line_is_nothing_when_flat() {
let doc = group(concat(vec![text("a"), soft_line(), text("b")]));
assert_eq!(p(&doc, 80), "ab\n");
}
#[test]
fn join_interleaves_separator() {
let doc = join(text(", "), vec![text("a"), text("b"), text("c")]);
assert_eq!(p(&doc, 80), "a, b, c\n");
}
#[test]
fn best_fitting_picks_first_candidate_that_fits() {
let doc = best_fitting(vec![
text("short"),
concat(vec![text("fallback"), hard_line(), text("layout")]),
]);
assert_eq!(p(&doc, 10), "short\n");
}
#[test]
fn best_fitting_falls_back_to_last_candidate() {
let doc = best_fitting(vec![
text("too-wide"),
concat(vec![text("fallback"), hard_line(), text("layout")]),
]);
assert_eq!(p(&doc, 4), "fallback\nlayout\n");
}
#[test]
fn nested_indent_accumulates() {
let doc = concat(vec![
text("a"),
indent(concat(vec![
hard_line(),
text("b"),
indent(concat(vec![hard_line(), text("c")])),
])),
]);
assert_eq!(p(&doc, 80), "a\n b\n c\n");
}
#[test]
fn expanded_group_breaks_even_when_it_fits() {
let doc = group_expanded(concat(vec![
text("("),
indent(concat(vec![soft_line(), text("a")])),
soft_line(),
text(")"),
]));
assert_eq!(p(&doc, 80), "(\n a\n)\n");
}
#[test]
fn expanded_inner_group_forces_outer_to_break() {
let inner = group_expanded(concat(vec![
text("("),
indent(concat(vec![soft_line(), text("x")])),
soft_line(),
text(")"),
]));
let outer = group(concat(vec![text("a"), line(), inner]));
assert_eq!(p(&outer, 80), "a\n(\n x\n)\n");
}
#[test]
fn line_suffix_defers_content_to_the_end_of_the_line() {
let doc = concat(vec![
line_suffix(text(" -- note")),
text("code"),
hard_line(),
text("next"),
]);
assert_eq!(p(&doc, 80), "code -- note\nnext\n");
}
#[test]
fn line_suffix_flushes_at_end_of_document() {
let doc = concat(vec![line_suffix(text(" -- note")), text("code")]);
assert_eq!(p(&doc, 80), "code -- note\n");
}
#[test]
fn break_parent_forces_its_group_to_break() {
let doc = group(concat(vec![
text("a"),
break_parent(),
indent(concat(vec![line(), text("b")])),
]));
assert_eq!(p(&doc, 80), "a\n b\n");
}
#[test]
fn empty_document_prints_nothing() {
assert_eq!(p(&empty(), 80), "");
}
#[test]
fn trailing_whitespace_is_trimmed() {
let doc = concat(vec![text("a"), indent(hard_line()), hard_line(), text("b")]);
assert_eq!(p(&doc, 80), "a\n\nb\n");
}
#[test]
fn text_caches_its_display_width() {
for s in ["select", "a, b, c", "長芋", "a長b", ""] {
match text(s) {
Doc::Text(stored, width) => {
assert_eq!(stored, s);
assert_eq!(width, text_width(s));
}
other => panic!("text() should build a Text node, got {other:?}"),
}
}
assert_eq!(
text_width("SELECT a, b"),
UnicodeWidthStr::width("SELECT a, b")
);
}
#[test]
fn group_precomputes_its_forced_break() {
match group(concat(vec![text("a"), line(), text("b")])) {
Doc::Group { must_break, .. } => assert!(!must_break),
other => panic!("expected a group, got {other:?}"),
}
match group(concat(vec![text("a"), hard_line(), text("b")])) {
Doc::Group { must_break, .. } => assert!(must_break),
other => panic!("expected a group, got {other:?}"),
}
match group_expanded(text("x")) {
Doc::Group { must_break, .. } => assert!(must_break),
other => panic!("expected a group, got {other:?}"),
}
}
#[test]
fn finalize_drops_leading_and_trailing_blank_lines_but_keeps_interior() {
let raw = String::from("\n \na \n\n\nb \n\n \n");
assert_eq!(finalize(raw), "a\n\n\nb\n");
assert_eq!(finalize(String::new()), "");
assert_eq!(finalize(String::from(" \n ")), "");
assert_eq!(finalize(String::from("only")), "only\n");
}
#[test]
fn source_code_slice_single_line_behaves_like_text() {
let doc = concat(vec![text("a "), source_code_slice("b = c"), text(" d")]);
assert_eq!(p(&doc, 80), "a b = c d\n");
}
#[test]
fn source_code_slice_caches_its_last_line_width() {
match source_code_slice("alpha\nbeta\nlong-tail") {
Doc::SourceCodeSlice(s, width) => {
assert_eq!(s, "alpha\nbeta\nlong-tail");
assert_eq!(width, "long-tail".len());
}
other => panic!("expected a source slice, got {other:?}"),
}
match source_code_slice("x\n") {
Doc::SourceCodeSlice(_, width) => assert_eq!(width, 0),
other => panic!("expected a source slice, got {other:?}"),
}
}
#[test]
fn source_code_slice_reindents_continuation_lines_under_indent() {
let doc = concat(vec![
text("header"),
indent(concat(vec![hard_line(), source_code_slice("v1\nv2\nv3")])),
hard_line(),
text("footer"),
]);
assert_eq!(p(&doc, 80), "header\n v1\n v2\n v3\nfooter\n");
}
#[test]
fn source_code_slice_tail_width_participates_in_fits() {
let doc = group(concat(vec![
source_code_slice("aa\nbbbb"),
line(),
text("c"),
]));
assert_eq!(p(&doc, 6), "aa\nbbbb c\n");
assert_eq!(p(&doc, 5), "aa\nbbbb\nc\n");
}
#[test]
fn if_group_breaks_selects_layout_by_group_mode() {
let doc = group(concat(vec![
text("a"),
if_group_breaks(text(","), empty()),
line(),
text("b"),
]));
assert_eq!(p(&doc, 80), "a b\n");
assert_eq!(p(&doc, 1), "a,\nb\n");
}
#[test]
fn if_group_breaks_flat_arm_forced_break_propagates_but_broken_arm_does_not() {
assert!(has_forced_break(&if_group_breaks(empty(), hard_line())));
assert!(!has_forced_break(&if_group_breaks(hard_line(), empty())));
}
#[test]
fn block_indent_frames_content_on_its_own_indented_line() {
let doc = concat(vec![text("{"), block_indent(text("body")), text("}")]);
assert_eq!(p(&doc, 80), "{\n body\n}\n");
}
#[test]
fn doc_write_composes_format_values_in_order() {
let mut f = DocBuffer::new();
doc_write!(f, [text("a"), text(" = "), text("b")]);
assert_eq!(p(&group(f.finish()), 80), "a = b\n");
}
#[test]
fn format_rule_decouples_layout_from_data() {
struct AssignRule;
impl FormatRule<str> for AssignRule {
fn fmt(&self, item: &str, buffer: &mut DocBuffer) {
doc_write!(
buffer,
[text("x => \""), text(item.to_string()), text("\"")]
);
}
}
let mut f = DocBuffer::new();
f.write_with_rule(&AssignRule, "v");
assert_eq!(p(&f.finish(), 80), "x => \"v\"\n");
}
}