Doc IR and printer¶
Language rules do not produce text. They produce a Doc: a small tree
describing text, possible line breaks, and indentation. The printer
(rainbow_fmt.core.printer) decides which line breaks to take so that the
output fits the configured width. This split is what lets one printer serve
every language (see architecture.md, section 4).
from rainbow_fmt.core.doc import group, indent, join, line, softline
from rainbow_fmt.core.printer import PrintOptions, print_doc
def bracketed(*items):
return group("[", indent(softline, join([",", line], items)), softline, "]")
print_doc(bracketed("1", "2"), PrintOptions())
# [1, 2]
print_doc(bracketed("1", "2"), PrintOptions(max_width=4, indent_size=2))
# [
# 1,
# 2
# ]
Docs are values¶
A Doc is immutable data: nodes are frozen dataclasses that compare and hash
by value (group("a") == group("a")). Nothing depends on object identity,
and the printer never calls back into rule code
(ADR 0002).
Wherever a Doc is expected, builders also accept:
| Argument | Means |
|---|---|
"text" |
text("text") |
[a, b] or (a, b) |
concat(a, b) |
Anything else raises TypeError.
Building blocks¶
| Builder | Flat (fits) | Broken |
|---|---|---|
text(s) |
s |
s (must not contain line breaks) |
concat(*parts) |
parts in order | parts in order |
line |
a space | a line break |
softline |
nothing | a line break |
hardline |
always a line break; breaks every enclosing group | |
break_parent |
prints nothing; breaks every enclosing group | |
group(*parts, should_break=False, id=None) |
contents flat if they fit | contents broken |
conditional_group(*states) |
the first state that fits, flat | the last state, broken (see below) |
indent(*parts) |
— | contents one level deeper (indent_size spaces or a tab) |
align(n, *parts) |
— | contents n spaces deeper, even when indenting with tabs |
join(sep, items) |
items separated by sep |
|
fill(items) |
alternating content and separators; each separator breaks only if the next content does not fit | |
if_break(broken, flat="", group_id=None) |
flat |
broken; follows the enclosing group, or the group with id == group_id |
line_suffix(*parts) |
contents moved to the end of the current line (trailing comments) | |
table(rows) |
each row on its own line; every cell except the last in a row padded to its column's widest cell | |
verbatim(s) |
s exactly: not re-indented, not trimmed |
A group is flat when its contents, plus everything after it up to the
next line break, fit in the remaining width. Otherwise it is broken, and
its line/softlines become line breaks. Nested groups are decided
independently, outermost first.
Forced breaks¶
A group cannot be flat if it contains a hardline, a break_parent, a
multi-line verbatim, or a table with more than one row. Each node
records this in a forced attribute, computed from its direct children
when it is built, so no separate pass over the tree is needed.
conditional_group: alternative layouts¶
function = concat("function () {", indent(hardline, "run()"), hardline, "}")
conditional_group(
concat("f(a, ", function, ")"), # hug the argument
group("f(", indent(softline, "a,", line, function), softline, ")", should_break=True),
)
# f(a, function () {
# run()
# })
Prettier's conditionalGroup: each state is tried in turn, flat (groups
with forced breaks inside it are still broken), and the first one whose
text up to its first line break, followed by the rest of the line, fits
is printed; if none fits, the last is printed broken. Forced breaks inside
a conditional group do not break the groups around it (its forced is
False); that is what lets a call hug a function argument whose body
spans lines. After a hardline printed inside a state, the groups that
follow are measured again rather than printed flat.
table: column alignment¶
table([["x", " = 1"], ["long_name", " = 2"]])
# x = 1
# long_name = 2
Cells are printed flat and must not contain forced breaks. Rows follow the current indentation. Widths are display widths (see below).
verbatim: exact source text¶
Used for preserve and rainbow: off regions, and for multi-line string
literals. Lines after the first are emitted as-is, without the current
indentation, and trailing whitespace inside is kept. Line breaks inside are
written as \n and printed with the configured newline (PrintOptions.newline).
Printing¶
print_doc(doc, PrintOptions(
max_width=80, # >= 1
indent_style="space", # "space" | "tab"
indent_size=4, # columns per level with spaces
tab_width=4, # columns a tab counts as
newline="\n", # "\n" | "\r\n"
))
- Width is measured in terminal columns: East Asian wide and fullwidth
characters count 2, combining marks and format characters (such as
zero-width space) count 0, and a tab counts
tab_width(rainbow_fmt.core.printer.display_width). - Trailing whitespace is removed from every line, except inside
verbatimtext. - Tabs and alignment: with
indent_style="tab",indentadds a tab andalignadds spaces ("smart tabs"). - The printer is iterative, so deeply nested or very long documents do not hit Python's recursion limit.
The algorithm is the Wadler/Leijen "prettier printer" as implemented by
Prettier (doc-printer.js), with table and verbatim added.