A Java implementation of Philip Wadler's "A prettier printer", a pretty-printing algorithm for laying out hierarchical documents as text.
This algorithm is particularly suitable for formatting source code (see for example Prettier).
prettier4j is published for Java 8 and above.
Gradle (build.gradle / build.gradle.kts):
implementation("com.opencastsoftware:prettier4j:0.3.1")
Maven (pom.xml):
<dependency>
<groupId>com.opencastsoftware</groupId>
<artifactId>prettier4j</artifactId>
<version>0.3.1</version>
</dependency>
To render documents using this library you must use com.opencastsoftware.prettier4j.Doc.
In order to create documents, check out the static methods of that class, especially:
- empty() - creates an empty
Doc
. - text(String) - creates a
Doc
from aString
. These are used as the atomic text nodes of a document.
To render documents, the render(int) instance method is provided. The argument to this method declares a target line width when laying out the document.
It's not always possible for documents to fit within this target width. For example, a single Doc.text
node may be longer than the target width if the argument String
is long enough.
To concatenate documents, the append(Doc) instance method and related methods providing different separators are provided.
As a general rule, the best way to construct documents using this algorithm is to construct your document by concatenating text nodes, while declaring each place where a line break could be added if necessary.
The layout algorithm uses the concept of "flattened" layouts - layouts which are used when they are able to fit within the remaining space on the current line. In other words, they are "flattened" onto a single line.
The lineOrSpace(), lineOrEmpty() and related static methods are used to declare line breaks which may be replaced with alternative content if the current Doc
is flattened.
The line() static method creates a line break which may not be flattened.
However, none of these primitives create flattened layouts on their own.
In order to declare how documents can be flattened, you must declare groups within a document which are all flattened together.
For example, the following documents each render to the same content:
Doc.text("one")
.appendLineOrSpace(Doc.text("two"))
.appendLineOrSpace(Doc.text("three"))
.render(30);
// ===> "one\ntwo\nthree"
Doc.text("one")
.appendLineOrSpace(Doc.text("two"))
.appendLineOrSpace(Doc.text("three"))
.render(5);
// ===> "one\ntwo\nthree"
However, if we declare each of those documents as a group using the static method group(Doc), they are rendered differently:
Doc.group(
Doc.text("one")
.appendLineOrSpace(Doc.text("two"))
.appendLineOrSpace(Doc.text("three")))
.render(30);
// ===> "one two three"
Doc.group(
Doc.text("one")
.appendLineOrSpace(Doc.text("two"))
.appendLineOrSpace(Doc.text("three")))
.render(5);
// ===> "one\ntwo\nthree"
By declaring a group, we have specified that the contents of each group can be flattened onto a single line if there is enough space.
However, if there is not enough space for all three words on the line, they must be rendered using their expanded layout.
As a result, the first call to render
renders a space-separated list, whereas the second call renders as a newline separated list. The width of 5 characters provided to the render method in the second call does not allow enough space for the entire group to render on a single line.
As of version 0.2.0, there is support for rendering text with ANSI escape code sequences.
This enables text styles like foreground and background colours, underlines and bold font styling to be applied to a Doc
.
To do this, the styled(Styles.StylesOperator...) method of the Doc
class can be used.
The styles that can be applied can be found in the Styles class.
For example:
Doc.text("one").styled(Style.fg(Color.red()))
.appendLineOrSpace(Doc.text("two").styled(Style.fg(Color.green())))
.appendLineOrSpace(Doc.text("three").styled(Style.fg(Color.blue())))
.render(30);
// ===> "\u001b[31mone\u001b[0m \u001b[32mtwo\u001b[0m \u001b[34mthree\u001b[0m"
As of version 0.3.0, there is support for declaring parameters in documents via the param(String) method of the Doc
class.
Parameters are named, and a named parameter may appear multiple times in the same document.
All parameters must be bound to a Doc
value before rendering.
Binding parameters is exactly equivalent to inlining the argument values into the original document.
For example:
Doc.param("one")
.appendLineOrSpace(Doc.param("two"))
.appendLineOrSpace(Doc.param("three"))
.bind(
"one", Doc.text("1"),
"two", Doc.text("2"),
"three", Doc.text("3"))
.render(30);
// ===> "1 2 3"
is exactly equivalent to:
Doc.text("1")
.appendLineOrSpace(Doc.text("2"))
.appendLineOrSpace(Doc.text("3"))
.render(30);
// ===> "1 2 3"
The code in this repository is a pretty direct port of the paper's Haskell code to Java.
However, the names relating to line breaks (lineOrSpace
, lineOrEmpty
etc.) in this project are inspired by those used in typelevel/paiges, an excellent Scala port of the same algorithm.
All code in this repository is licensed under the Apache License, Version 2.0. See LICENSE.