Skip to content

opencastsoftware/prettier4j

Repository files navigation

prettier4j

CI codecov Maven Central javadoc License

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).

Installation

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>

Usage

Basics

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 a String. 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.

ANSI styled text

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"

Parameterized documents

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"

Acknowlegements

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.

License

All code in this repository is licensed under the Apache License, Version 2.0. See LICENSE.