Sign in

    pretty

    Wadler-style pretty printer for MoonBit: documents, groups and layout that fits a line width

    pretty-printer
    wadler
    layout
    document
    Download zip
    Author
    Version
    0.2.0
    License
    Apache-2.0
    Last updated
    17 hours ago
    Downloads
    4K

    Dependencies

    #moonrockz/pretty

    A Wadler-style pretty printer for MoonBit. Build a document from text, line breaks and groups; render lays it out to fit a line width.

    let items = [@pretty.text("a"), @pretty.text("b"), @pretty.text("c")]
    let doc = @pretty.group(
    @pretty.text("[") +
    @pretty.nest(2, @pretty.line() + @pretty.join(items, @pretty.text(",") + @pretty.line())) +
    @pretty.line() +
    @pretty.text("]"),
    )
    @pretty.render(doc) // "[ a, b, c ]"
    @pretty.render(doc, width=6) // "[\n a,\n b,\n c\n]"

    FunctionFlatBroken
    text(s)ss
    verbatim(s)ss, no indentation after its line feeds
    line()a spacea line break
    softline()nothinga line break
    hardline()always a line break
    nest(n, d)dd, indented by n more
    align(d)dd, indented to its start column
    tab(n, d)dd, indented to the next multiple of n
    group(d)flat if it fitselse broken
    if_break(b, f)fb
    group_alone(d)flat if d alone fitselse broken
    break_(b, u)ub, then a line break
    flex_break(b, u)uu when the rest fits, else b and a line break
    lines(n)n line breaks (does not force the group)
    zero_width(s)s, width 0s, width 0
    raw_text(s)s as one atom: line feeds kept, no indentation, every cluster counts
    force_break(d)d; its group never fits
    next_break_fits(d, m)d; Enabled lets the group fit up to the first break in d
    nest_if_broken(n, d)dd, indented by n more
    set_nesting(n, d)dd, indented to column n

    • A group is flat when it fits together with the rest of its line, up to the next possible break (the fit rule of Wadler's "A prettier printer").
    • Evaluation is strict (Lindig, "Strictly Pretty") and uses explicit stacks, so deep documents render on wasm, wasm-gc, js and native.
    • The engine writes indentation and the spaces of flat lines only before the next text, so a line break never leaves trailing whitespace and an empty line has no indentation. (A break_ whose unbroken text ends in a space, followed by a lines, keeps that space: it is text.) Width counts extended grapheme clusters (UAX #29, Unicode 17.0.0; @grapheme.count).
    • render(doc, width?, start?): start=Unbroken keeps a line outside every group flat, as the Gleam compiler's pretty-arena does. The forms group_alone, break_, flex_break, lines, force_break, next_break_fits, nest_if_broken, set_nesting and zero_width have pretty-arena's semantics; group keeps Wadler's rule.

    Doc

    type Doc

    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.
    impl Add for Doc

    NextBreakFits

    pub(all) enum NextBreakFits {
    Enabled
    Disabled
    } derive(Eq,
    Debug
    )

    Whether a next_break_fits document is in force: Enabled makes the first break inside it count as a fit; Disabled turns off every enabled next_break_fits inside it.

    Start

    pub(all) enum Start {
    Broken
    Unbroken
    } derive(Eq,
    Debug
    )

    The mode render starts in. Broken (the default) breaks a line that is outside every group; Unbroken keeps it flat, as pretty-arena's pretty_print does.

    align

    fn align(d : Doc) -> Doc

    d with the indentation of its line breaks set to the column where d starts.

    break_

    fn break_(broken : String, unbroken : String) -> Doc

    A strict break (pretty-arena's break_): unbroken when the enclosing group is flat; broken, then a line feed and the indentation, when it is broken. break_("", " ") renders like line(); break_(",", ", ") writes a trailing comma when broken. The enclosing group decides the mode (group with Wadler's rule, group_alone with pretty-arena's).

    test {
    let d = @pretty.group_alone(
    @pretty.text("a") + @pretty.break_(",", ", ") + @pretty.text("b"),
    )
    inspect(@pretty.render(d, width=80), content="a, b")
    inspect(@pretty.render(d, width=3), content="a,\nb")
    }

    concat

    fn concat(docs : Array[Doc]) -> Doc

    The documents one after the other.

    empty

    fn empty() -> Doc

    The empty document. empty() + d and d + empty() render as d.

    flex_break

    fn flex_break(broken : String, unbroken : String) -> Doc

    A flex break (pretty-arena's flex_break): unbroken when the rest of the line up to the next break fits after it, or when the mode is Unbroken; otherwise broken, then a line feed and the indentation. Each flex break decides on its own, so a list of flex breaks fills lines.

    force_break

    fn force_break(d : Doc) -> Doc

    d, and the enclosing group never fits: it breaks. Only next_break_fits(_, Enabled) outside it lets a group stay flat up to the forced break. The render of d does not change.

    group

    fn group(d : Doc) -> Doc

    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.

    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")
    }

    group_alone

    fn group_alone(d : Doc) -> Doc

    d on one line when d alone fits in the rest of the width (the Gleam compiler's pretty-arena rule), otherwise d with its breaks broken. Unlike group, the text after the group on the same line is not counted, and the check runs again for a nested group_alone even when the outer one is flat. A hardline inside it does not force it to break: the check stops there, as at a lines.

    test {
    let body = @pretty.text("a") + @pretty.line() + @pretty.text("b")
    inspect(
    @pretty.render(@pretty.group_alone(body) + @pretty.text("cc"), width=3),
    content="a bcc",
    )
    inspect(
    @pretty.render(@pretty.group(body) + @pretty.text("cc"), width=3),
    content="a\nbcc",
    )
    let with_hardline = @pretty.group_alone(
    body + @pretty.hardline() + @pretty.text("c"),
    )
    inspect(@pretty.render(with_hardline, width=80), content="a b\nc")
    }

    hardline

    fn hardline() -> Doc

    Always a line break and the indentation. Every group that holds it breaks; a group_alone treats it as a lines(1) (the text before it decides whether the group fits).

    if_break

    fn if_break(broken : Doc, flat : Doc) -> Doc

    broken when the enclosing group breaks, flat when it is flat. Only flat decides whether the group can be flat.

    join

    fn join(docs : Array[Doc], sep : Doc) -> Doc

    The documents with sep between them.

    line

    fn line() -> Doc

    A space in flat mode; a line break and the indentation in break mode.

    lines

    fn lines(n : Int) -> Doc

    n line feeds, then the indentation (pretty-arena's Line(n)). Unlike hardline, it does not force the enclosing group to break: the group is flat when the text up to the lines fits. lines(0) is empty().

    test {
    inspect(
    @pretty.render(
    @pretty.nest(2, @pretty.text("a") + @pretty.lines(2) + @pretty.text("b")),
    ),
    content="a\n\n b",
    )
    }

    nest

    fn nest(n : Int, d : Doc) -> Doc

    d with the indentation of its line breaks increased by n.

    nest_if_broken

    fn nest_if_broken(n : Int, d : Doc) -> Doc

    d with the indentation of its line breaks increased by n only when the enclosing group is broken (pretty-arena's nest_if_broken). In a flat group the indentation does not change, so a lines inside d keeps the outer indentation.

    next_break_fits

    fn next_break_fits(d : Doc, mode : NextBreakFits) -> Doc

    d with a changed fit check, as pretty-arena's next_break_fits. Enabled: the first break inside d ends the check of the enclosing group with "fits", even past a force_break, so a call whose last argument is a block keeps f({ on one line. Disabled: every enabled next_break_fits inside d is ignored. The render of d does not change.

    test {
    let block = @pretty.group_alone(
    @pretty.force_break(
    @pretty.text("{") +
    @pretty.nest(2, @pretty.break_("", " ") + @pretty.text("x")) +
    @pretty.break_("", " ") +
    @pretty.text("}"),
    ),
    )
    let call = @pretty.group_alone(
    @pretty.text("f(") +
    @pretty.nest(
    2,
    @pretty.break_("", "") + @pretty.next_break_fits(block, Enabled),
    ) +
    @pretty.break_("", "") +
    @pretty.text(")"),
    )
    inspect(@pretty.render(call), content="f({\n x\n })")
    }

    raw_text

    fn raw_text(s : String) -> Doc

    The text s as one atom (pretty-arena's Str): a line feed in s is written as it is, with no indentation after it, and the width of s counts every grapheme cluster, line feeds included, as if s were on one line. It does not force the enclosing group to break. Use it for a multi-line string literal that a formatter must keep byte for byte while it lays out the code around it as the Gleam compiler does.

    test {
    let d = @pretty.nest(
    2,
    @pretty.raw_text("ab\ncd") + @pretty.flex_break("", " ") + @pretty.text("x"),
    )
    inspect(@pretty.render(d, width=7), content="ab\ncd x")
    inspect(@pretty.render(d, width=6), content="ab\ncd\n x")
    }

    render

    fn render(doc : Doc, width? : Int, start? : Start) -> String

    Lay out doc in lines of at most width columns where its groups allow. A group is flat when it fits, together with the rest of its line up to the next possible break. Text wider than width stays whole. start is the mode outside every group: Broken (the default) breaks a bare line, Unbroken keeps it flat. Uses explicit stacks, so deep documents render on every target.

    test {
    let items = [@pretty.text("a"), @pretty.text("b")]
    let d = @pretty.group(
    @pretty.text("[") +
    @pretty.nest(
    2,
    @pretty.line() + @pretty.join(items, @pretty.text(",") + @pretty.line()),
    ) +
    @pretty.line() +
    @pretty.text("]"),
    )
    inspect(@pretty.render(d), content="[ a, b ]")
    inspect(@pretty.render(d, width=4), content="[\n a,\n b\n]")
    }

    set_nesting

    fn set_nesting(n : Int, d : Doc) -> Doc

    d with the indentation of its line breaks set to n columns (pretty-arena's set_nesting).

    softline

    fn softline() -> Doc

    Nothing in flat mode; a line break and the indentation in break mode.

    tab

    fn tab(n : Int, d : Doc) -> Doc

    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.

    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",
    )
    }

    text

    fn text(s : String) -> Doc

    The text s. A line feed in s is a hardline(), so the lines after it get the current indentation. Width counts extended grapheme clusters (UAX #29): a character with combining marks, a flag or an emoji sequence is one column.

    test {
    inspect(
    @pretty.render(@pretty.nest(2, @pretty.text("a\nb"))),
    content="a\n b",
    )
    }

    verbatim

    fn verbatim(s : String) -> Doc

    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.

    zero_width

    fn zero_width(s : String) -> Doc

    The text s with width 0: it is written, but it does not count toward the line width. Use it for text the layout must not react to, such as a comment. s must hold no line feed.

    Source Files

    Powered by MoonBit

    Site sourceReport issuePackagesBuild queueSkillsStatistics

    © 2026 mooncakes.io