The pretty
package exports one class: Document
.
A document can be seen as a set of different layouts for the same text.
Calling render(n)
on a document picks
the best of its layouts that fit in n
columns.
doc.render(80); // tries to fit doc in 80 columns
It is returned as a string. There's also an efficient
renderToSink
method if needed.
There aren't many ways to create a document. Namely, a document has one of the following forms:
empty
line
text(str)
doc + doc
doc.nest(n)
doc.group
Informally,
empty
is the empty documentline
is a new linetext(str)
is the text str
doc1 + doc2
is doc1
concatenated to doc2
doc.nest(n)
is doc
where every new line is indented by n
spacesdoc.group
is doc
where line breaks are allowed to render as " "
insteadThe rest of this document illustrates these forms with interactive examples of increasing complexity.
We start by defining four text documents that will be used throughout the examples.
Document a = text("aaa");
Document b = text("bbb");
Document c = text("ccc");
Document d = text("ddd");
Remember that Document
s are immutable so it's safe to reuse
them. For instance, we can can create a document that renders as
"aaaaaa"
by concatenating a
with itself.
a + a
text
, empty
, line
and +
are pretty self-explanatory and don't need much
introduction. Let's just note that +
is associative, that
empty
is neutral for +
and that
line
is equivalent to text("\n")
as long as
nest
and group
aren't involved.
As an example, here is a Dart expression using these primitives:
a + empty + b + line + c
And here is how it renders:
The red line can be moved to adjust the desired maximum number of columns
but it won't have any effect on this document since it doesn't use
group
. The + and - minus buttons change the font size.
Note that this document is equivalent to any parenthezising of the
original expression, e.g. (a + empty) + (b + (line + c))
.
It is also equivalent to a + b + line + c
.
The method nest
indents all lines after a line break inside
the document it applies to. For instance the following document:
a + (line + b + line + c).nest(2) + line + d
renders as follows:
Here, nest
applies to the two occurrences of
line
inside the parenthesis, but not to the one outside,
as is made clear by the structure of the expression. Note that the
following document renders exactly the same:
(a + line + b + line + c).nest(2) + line + d
In both cases, nest
only affects lines appearing
after a line break.
Nesting is compositional: a document with a nested subdocument can in turn be nested as part of a bigger document. For example the following document:
a + (line + b + (line + c).nest(2) + line + d).nest(2)
renders as follows:
This document is still "static": the desired number of columns has no effect on the rendering. Yet it demonstrate one interesting aspect of the library already: documents can be composed without having to worry about any global indentation level. This makes it easy to write composable pretty-printing functions.
The method group
is what makes documents "responsive": it
tells the renderer when it is OK to not render a line
as
a line break, but as a space instead. Depending on the available amount of
horizontal space, the renderer will decide for each group whether it is
better flattened or not.
For instance the following expression:
(a + line + (b + line + c).group + line + d).group
renders as follows:
Moving the red line with the mouse reveals the effect of
group
. As long as enough horizontal space is available, the
outer group (and a fortiori the inner group) is flattened and all
occurrences of line
are replaced by spaces. When less than 15
columns are available, the outer group stops being flattened but the inner
group still fits on one line. For 6 columns or less, all occurrences of
line
translate to line breaks.
Note that group
integrates seamlessly with nest
:
flatten groups ignore nesting altogether since there is no line break.
Otherwise, nesting applies as usual. For instance the following
expression:
(a + (line + (b + line + c).group).nest(2) + line + d).group
renders as follows:
As long as 15 colums or more are available, the document renders as before. But as soon as the outer group breaks, the nesting becomes apparent.
Let us now apply the libtary to a full-fledged example: a pretty-printer of rose trees. We define trees as follows.
class Tree {
final String name;
final List<Tree> children;
Tree(this.name, this.children);
}
We want pretty-printed trees to look something like:
foo { bar, baz { qux, bar }, baz }
We pretty-print a tree by prepending its name to its pretty-printed children, and then group the result:
Document prettyTree(Tree tree) {
final prettyChildren = /* TODO */
return (text(tree.name) + prettyChildren).group;
}
If tree
has no children then prettyChildren
is
the empty document. Otherwise it will have something to do with
prettyTree
mapped over tree.children
:
final prettyChildren = tree.children.isEmpty
? empty
: /* SOMETHING SOMETHING tree.children.map(prettyTree) */;
In order to flesh out this something, we start by defining an auxiliary function that puts curly braces around a document and indents it by 2 spaces.
Document brackets(Document doc) {
return (text(" {") + line + doc).nest(2) + line + text("}");
}
We will apply it to the result of gluing the pretty-printed subtrees together.
final prettyChildren = tree.children.isEmpty
? empty
: brackets(/* tree.children.map(prettyTree) GLUED TOGETHER */);
There is actually a facility method just for that in the
Document
class: join
.
The expression doc.join([doc1, ..., docn])
is equivalent to
doc1 + doc + ... + doc + docn
. In our case, we want to join
the prettified children with a comma followed by a line break.
final prettyChildren = tree.children.isEmpty
? empty
: brackets((text(",") + line).join(tree.children.map(prettyTree)));
Finally, the full pretty-printing code is reproduced below.
Document prettyTree(Tree tree) {
final prettyChildren = tree.children.isEmpty
? empty
: brackets((text(",") + line).join(tree.children.map(prettyTree)));
return (text(tree.name) + prettyChildren).group;
}
Document brackets(Document doc) {
return (text(" {") + line + doc).nest(2) + line + text("}");
}
And here is how it renders on a random tree:
The reload button generates a new random tree.
Finally, let us write a "real world" pretty-printer: a pretty-printer of JSON documents. JSON document being trees, it will be very similar to the pretty-printer of the previous section.
The only novelty is the usage of a not yet introduced function:
lineOr
. The expression lineOr(str)
renders as
str
if there is enough horizontal space, as a line break
otherwise. It is a generalisation of line
, which is actually
a shortcut for lineOr(" ")
. Since we want empty JSON objects
to render as {}
and not as { }
, yet to break
after the first brace if required, we use lineOr("")
instead of line
.
Other than that the code, reproduced below, is unsurprising.
Document comma = text(",") + line;
Document between(String left, String right, Document doc) {
return (text(left) + (lineOr('') + doc).nest(2) + lineOr('') + text(right)).group;
}
Document prettyJson(Object json) {
if (json is List) {
return between("[", "]", comma.join(json.map(prettyJson)));
} else if (json is Map) {
final children = [];
json.forEach((key, value) {
children.add(escape(key) + text(": ") + prettyJson(value));
});
return between("{", "}", comma.join(children));
} else {
return escape(json);
}
}
Document escape(Object leaf) {
return text(const JsonEncoder().convert(leaf));
}
And here is how it renders on a random JSON document:
That is all there is to the pretty library. Enjoy!