A linear time pretty printing library
npm install pretty-fast-pretty-printerFor an introduction to the Pretty Fast Pretty Printer and how to use it, please
see the guide.
This readme is the reference manual.
------
_Pretty printing_ is an approach to printing source code that can adapt how
things are printed to fit within a maximum line width.
To use this library:
1. Convert the source code you want to print into a _document_ (instance of Doc), using the
constructors below. The Doc will encode _all possible ways_ that the source can be printed.
2. Print the Doc using doc.display(width), where width is the maximum allowed line width. This
will return a list of lines.
There are a variety of pretty-printing algorithms. The
pretty-fast-pretty-printer uses a custom algorithm
that displays a document in time linear in the number of distinct nodes in
the document. (Note that this is better than linear in the _size_ of the
document: if a document contains multiple references to a single sub-document,
that sub-document is only counted _once_. This can be an exponential
improvement.)
The algorithm takes inspiration from:
1. Wadler's
Prettier Printer,
2. Bernardy's
Pretty but not Greedy Printer, and
3. Ben Lerner's
Pyret pretty printer
This pretty printer was developed as part of
Codemirror Blocks.
Don't let this stop you from using it for your own project, though: it's a
general purpose library!
Documents are constructed out of six basic _combinators_:
txt(string) simply displays string. The string cannot contain newlines.txt("") is an empty document.
For example,
``js`
txt("Hello, world")
.display(80);
produces:
Hello, world
All other combinators will automatically wrap string arguments in txt."a string"
As a result, you can almost always write instead of txt("a string").
vert(doc1, doc2, ...) vertically concatenates documents, from top to bottom.
(I.e., it joins them with newlines). The vertical concatenation of two
documents looks like this:
For example,
`js`
vert("Hello,", "world!")
.display(80)
produces:
Hello,
world!
Vertical concatenation is associative. Thus:
vert(X, Y, Z)
= vert(X, vert(Y, Z))
= vert(vert(X, Y), Z)
horz(doc1, doc2, ...) horizontally concatenates documents. The second document
is indented to match the last line of the first document (and so forth for the
third document, etc.). The horizontal concatention of two documents looks like
this:
!Horizontal concatenation image
For example,
`js`
horz("BEGIN ", vert("first line", "second line"))
.display(80)
produces:
BEGIN first line
second line
Horizontal concatenation is associative. Thus:
horz(X, Y, Z)
= horz(X, horz(Y, Z))
= horz(horz(X, Y), Z)
horzArray(docArray) is a variant of horz that takes a single argument thathorz.apply(null, docArray)
is an array of documents. It is equivalent to .
concat(doc1, doc2, ...) naively concatenates documents from left to right. Ithorz
is similar to , except that the indentation level is kept _fixed_ for
all of the documents. The simple concatenation of two documents looks like this:
You should almost always prefer horz over concat.
As an example,
`js`
concat("BEGIN ", vert("first line", "second line"))
.display(80))
produces:
BEGIN first line
second line
concatArray(docArray) is a variant of concat that takes a single argumentconcat.apply(null, docArray)
that is an array of documents. It is equivalent to .
ifFlat(doc1, doc2) chooses between two documents. It will use doc1 if itdoc2
fits entirely on the current line, otherwise it will use . More precisely,doc1 will be used iff:
1. It can be rendered flat. A "flat" document has no newlines,
i.e., no vert. And,ifFlat
2. When rendered flat, it fits on the current line without going over
the pretty printing width. _NOTE:_ this counts only the portion of the current line contained in
the and to its left; it does not count anything to its right. Mostly this doesn't matterifFlat
much, but it can result in a document being printed over multiple lines when one would do. If
this ends up being an issue, try moving more of the Doc into the choice.
Finally, fullLine(doc) ensures that nothing is placed after doc, if at all possible. MoreifFlat
specifically, choices will be made such that nothing appears to the right of thefullLine(doc), if at all possible. However, if something is displayed _unconditionally_ to thefullLine
right of the , this library will have no choice but to put it there.
This is helpful for line comments. For example, fullLine("// comment") will
ensure that (if at all possible) nothing is placed after the comment.
Besides the combinators, there are some other useful "utility" constructors.
These constructors don't provide any extra power, as they are all defined in
terms of the combinators described above. But they capture some useful patterns.
There is also a
string template
shorthand for building a doc, called pretty. It accepts template strings thatvert
may contain newlines. It combines the lines with , and the parts of eachhorz
line with , returning a Doc. For example, this template:
`js
let c = "a == b";
let t = "a << 2";
let e = "a + b";
prettyif (${c}) {\n ${t}\n} else {\n ${e}\n}`
.display(80)
pretty prints an if statement across multiple lines:
if (a == b) {
a << 2
} else {
a + b
}
sepBy(items, sep, vertSep="") will display either:
items[0] sep items[1] sep ... items[n]
if it fits on one line, or:
items[0] vertSep \n items[1] vertSep \n ... items[n]
otherwise. (Without the extra spaces; those are there for readability.)
Neither sep nor vertSep may contain newlines.
wrap(words, sep=" ", vertSep="") does word wrapping. It combines the words withsep when they fit on the same line, or vertSep\n when they don't.
For simple word wrapping, you would use:
wrap(words, " ", "") // or just wrap(words)
For word-wrapping a comma-separated list, you would use:
wrap(words, ", ", ",")
Neither sep nor vertSep may contain newlines.
There are also some constructors for common kinds of s-expressions:
standardSexpr(func, args) is rendered like this:
(func args ... args)
or like this:
(func
args
...
args)
lambdaLikeSexpr(keyword, defn, body) is rendered like this:
(keyword defn body)
or like this:
(keyword defn
body)
beginLikeSexpr(keyword, bodies)` is rendered like this:
(keyword
bodies
...
bodies)