///|
/// A document: pieces of text and places where `render` may break a line.
/// Build documents with the functions of this package and join them with
/// `+`. The representation is private, so it can change without a breaking
/// release.
struct Doc {
node : DocNode
// Whether the flat form holds a hard line. A group that holds one always
// breaks.
has_hardline : Bool
}
///|
priv enum DocNode {
Empty
Text(String, Int) // the text and its width in code points
Line(Int) // the width of the flat form: 1 for `line`, 0 for `softline`
HardLine
VerbatimLine // a line break with no indentation
Nest(Int, Doc)
Align(Doc)
Tab(Int, Doc) // indentation: the next multiple of the Int
Group(Doc)
IfBreak(Doc, Doc) // broken, flat
Concat(Doc, Doc)
}
///|
fn make(node : DocNode, has_hardline : Bool) -> Doc {
{ node, has_hardline, }
}
///|
/// The empty document. `empty() + d` and `d + empty()` render as `d`.
pub fn empty() -> Doc {
make(Empty, false)
}
///|
fn columns(s : String) -> Int {
let mut n = 0
for _ in s {
n += 1
}
n
}
///|
fn atom(s : String) -> Doc {
if s.is_empty() {
empty()
} else {
make(Text(s, columns(s)), false)
}
}
///|
/// The text `s`. A line feed in `s` is a `hardline()`, so the lines after it
/// get the current indentation. Width counts code points.
///
/// ```mbt check
/// test {
/// inspect(
/// @pretty.render(@pretty.nest(2, @pretty.text("a\nb"))),
/// content="a\n b",
/// )
/// }
/// ```
pub fn text(s : String) -> Doc {
let parts = s.split("\n").map(p => atom(p.to_owned())).collect()
join(parts, hardline())
}
///|
/// The text `s` as it is: a line feed in `s` starts a new line with no
/// indentation. Use it for text whose bytes must not change, such as a doc
/// comment. A group that holds a line feed this way always breaks.
pub fn verbatim(s : String) -> Doc {
let parts = s.split("\n").map(p => atom(p.to_owned())).collect()
join(parts, make(VerbatimLine, true))
}
///|
/// A space in flat mode; a line break and the indentation in break mode.
pub fn line() -> Doc {
make(Line(1), false)
}
///|
/// Nothing in flat mode; a line break and the indentation in break mode.
pub fn softline() -> Doc {
make(Line(0), false)
}
///|
/// Always a line break and the indentation. Every group that holds it
/// breaks.
pub fn hardline() -> Doc {
make(HardLine, true)
}
///|
/// `d` with the indentation of its line breaks increased by `n`.
pub fn nest(n : Int, d : Doc) -> Doc {
make(Nest(n, d), d.has_hardline)
}
///|
/// `d` with the indentation of its line breaks set to the next multiple of
/// `n` that is greater than the current indentation (a tab stop). With
/// `n <= 0` the indentation does not change. Use it after `align` to indent
/// to a fixed grid: indentation 6 becomes 8, indentation 8 becomes 12.
///
/// ```mbt check
/// test {
/// let body = @pretty.text("x") +
/// @pretty.tab(4, @pretty.hardline() + @pretty.text("y"))
/// inspect(
/// @pretty.render(@pretty.text("ab") + @pretty.align(body)),
/// content="abx\n y",
/// )
/// inspect(
/// @pretty.render(@pretty.text("abcd") + @pretty.align(body)),
/// content="abcdx\n y",
/// )
/// }
/// ```
pub fn tab(n : Int, d : Doc) -> Doc {
make(Tab(n, d), d.has_hardline)
}
///|
/// `d` with the indentation of its line breaks set to the column where `d`
/// starts.
pub fn align(d : Doc) -> Doc {
make(Align(d), d.has_hardline)
}
///|
/// `d` on one line when it fits (with the rest of the line up to the next
/// possible break), otherwise `d` with its line breaks.
///
/// ```mbt check
/// test {
/// let d = @pretty.group(@pretty.text("a") + @pretty.line() + @pretty.text("b"))
/// inspect(@pretty.render(d, width=3), content="a b")
/// inspect(@pretty.render(d, width=2), content="a\nb")
/// }
/// ```
pub fn group(d : Doc) -> Doc {
make(Group(d), d.has_hardline)
}
///|
/// `broken` when the enclosing group breaks, `flat` when it is flat. Only
/// `flat` decides whether the group can be flat.
pub fn if_break(broken : Doc, flat : Doc) -> Doc {
make(IfBreak(broken, flat), flat.has_hardline)
}
///|
fn concat2(a : Doc, b : Doc) -> Doc {
match (a.node, b.node) {
(Empty, _) => b
(_, Empty) => a
_ => make(Concat(a, b), a.has_hardline || b.has_hardline)
}
}
///|
/// The documents one after the other.
pub fn concat(docs : Array[Doc]) -> Doc {
let mut d = empty()
for x in docs {
d = concat2(d, x)
}
d
}
///|
/// The documents with `sep` between them.
pub fn join(docs : Array[Doc], sep : Doc) -> Doc {
let mut d = empty()
for i, x in docs {
d = if i == 0 { x } else { concat2(concat2(d, sep), x) }
}
d
}
///|
pub impl Add for Doc with fn add(self, other) {
concat2(self, other)
}