Skip to content

Commit

Permalink
More docs and examples
Browse files Browse the repository at this point in the history
  • Loading branch information
deusaquilus committed Jan 25, 2024
1 parent a9513fc commit d21d74e
Show file tree
Hide file tree
Showing 2 changed files with 332 additions and 6 deletions.
262 changes: 260 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,266 @@ println(pprint(p).plainText)

## Extending PPrint

TODO
In order to extend pprint, subclass the PPrinter class and override the `treeify` function.
For example:
```kotlin
class CustomPPrinter1(config: PPrinterConfig) : PPrinter(config) {
override fun treeify(x: Any?, escapeUnicode: Boolean, showFieldNames: Boolean): Tree =
when (x) {
is java.time.LocalDate -> Tree.Literal(x.format(DateTimeFormatter.ofPattern("MM/dd/YYYY")))
else -> super.treeify(x, escapeUnicode, showFieldNames)
}
}

data class Person(val name: String, val born: LocalDate)
val pp = CustomPPrinter1(PPrinterConfig())
val joe = Person("Joe", LocalDate.of(1981, 1, 1))

println(pp.invoke(joe))
//> Person(name = "Joe", born = 01/01/1981)
```

For nested objects use Tree.Apply and recursively call the treeify method.
```kotlin
// A class that wouldn't normally print the right thing with pprint...
class MyJavaBean(val a: String, val b: Int) {
fun getValueA() = a
fun getValueB() = b
}

// Create the custom printer
class CustomPPrinter2(config: PPrinterConfig) : PPrinter(config) {
override fun treeify(x: Any?, esc: Boolean, names: Boolean): Tree =
when (x) {
// List through the properties of 'MyJavaBean' and recursively call treeify on them.
// (Note that Tree.Apply takes an iterator of properties so that the interface is lazy)
is MyJavaBean -> Tree.Apply("MyJavaBean", listOf(x.getValueA(), x.getValueB()).map { treeify(it, esc, names) }.iterator())
else -> super.treeify(x, esc, names)
}
}

val bean = MyJavaBean("abc", 123)
val pp = CustomPPrinter2(PPrinterConfig())
println(pp.invoke(bean))
//> MyJavaBean("abc", 123)
```

To print field-names you use Tree.KeyValue:
```kotlin
class CustomPPrinter3(config: PPrinterConfig) : PPrinter(config) {
override fun treeify(x: Any?, escapeUnicode: Boolean, showFieldNames: Boolean): Tree {
// function to make recursive calls shorter
fun rec(x: Any?) = treeify(x, escapeUnicode, showFieldNames)
return when (x) {
// Recurse on the values, pass result into Tree.KeyValue.
is MyJavaBean ->
Tree.Apply(
"MyJavaBean",
listOf(Tree.KeyValue("a", rec(x.getValueA())), Tree.KeyValue("b", rec(x.getValueB()))).iterator()
)
else ->
super.treeify(x, esc, names)
}
}
}

val bean = MyJavaBean("abc", 123)
val pp = CustomPPrinter2(PPrinterConfig())
println(pp.invoke(bean))
//> MyJavaBean(a = "abc", b = 123)
```

Often it is a good idea to honor the `showFieldNames` parameter only display key-values if it is enabled:
```kotlin
class CustomPPrinter4(config: PPrinterConfig) : PPrinter(config) {
override fun treeify(x: Any?, escapeUnicode: Boolean, showFieldNames: Boolean): Tree {
// function to make recursive calls shorter
fun rec(x: Any?) = treeify(x, escapeUnicode, showFieldNames)
fun field(fieldName: String, value: Any?) =
if (showFieldNames) Tree.KeyValue(fieldName, rec(value)) else rec(value)
return when (x) {
// Recurse on the values, pass result into Tree.KeyValue.
is MyJavaBean ->
Tree.Apply("MyJavaBean", listOf(field("a", x.getValueA()), field("b", x.getValueB())).iterator())
else ->
super.treeify(x, escapeUnicode, showFieldNames)
}
}
}

val bean = MyJavaBean("abc", 123)
println(CustomPPrinter4(PPrinterConfig()).invoke(bean))
//> MyJavaBean(a = "abc", b = 123)
println(CustomPPrinter4(PPrinterConfig(defaultShowFieldNames = false)).invoke(bean))
//> MyJavaBean("abc", 123)
```


## Fansi

TODO
> NOTE. Most of this is taken from the original Fansi documentation [here](https://raw.githubusercontent.com/com-lihaoyi/fansi/master/readme.md)
Fansi is a Kotlin library (ported from Scala) that was designed make it easy to deal with fancy colored Ansi
strings within your command-line programs.

While "normal" use of Ansi escapes with `java.lang.String`, you find yourself
concatenating colors:

```scala
val colored = Console.RED + "Hello World Ansi!" + Console.RESET
```

To build your colored string. This works the first time, but is error prone
on larger strings: e.g. did you remember to put a `Console.RESET` where it's
necessary? Do you need to end with one to avoid leaking the color to the entire
console after printing it?

Furthermore, some operations are fundamentally difficult or error-prone with
this approach. For example,

```scala
val colored: String = Console.RED + "Hello World Ansi!" + Console.RESET

// How to efficiently get the length of this string on-screen? We could try
// using regexes to remove and Ansi codes, but that's slow and inefficient.
// And it's easy to accidentally call `colored.length` and get a invalid length
val length = ???

// How to make the word `World` blue, while preserving the coloring of the
// `Ansi!` text after? What if the string came from somewhere else and you
// don't know what color that text was originally?
val blueWorld = ???

// What if I want to underline "World" instead of changing it's color, while
// still preserving the original color?
val underlinedWorld = ???

// What if I want to apply underlines to "World" and the two characters on
// either side, after I had already turned "World" blue?
val underlinedBlue = ???
```

While simple to describe, these tasks are all error-prone and difficult to
do using normal `java.lang.String`s containing Ansi color codes. This is
especially so if, unlike the toy example above, `colored` is coming from some
other part of your program and you're not sure what or how-many Ansi color
codes it already contains.

With Fansi, doing all these tasks is simple, error-proof and efficient:

```scala
val colored: fansi.Str = fansi.Color.Red("Hello World Ansi!")
// Or fansi.Str("Hello World Ansi!").overlay(fansi.Color.Red)

val length = colored.length // Fast and returns the non-colored length of string

val blueWorld = colored.overlay(fansi.Color.Blue, 6, 11)

val underlinedWorld = colored.overlay(fansi.Underlined.On, 6, 11)

val underlinedBlue = blueWorld.overlay(fansi.Underlined.On, 4, 13)
```

And it just works:

![LandingExample](docs/LandingExample.png)

Why Fansi?
----------

Unlike normal `java.lang.String`s with Ansi escapes embedded inside,
`fansi.Str` allows you to perform a range of operations in an efficient
manner:

- Extracting the non-Ansi `plainText` version of the string

- Get the non-Ansi `length`

- Concatenate colored Ansi strings without worrying about leaking
colors between them

- Applying colors to certain portions of an existing `fansi.Str`,
and ensuring that the newly-applied colors get properly terminated
while existing colors are unchanged

- Splitting colored Ansi strings at a `plainText` index

- Rendering to colored `java.lang.String`s with Ansi escapes embedded,
which can be passed around or concatenated without worrying about
leaking colors.

These are tasks which are possible to do with normal `java.lang.String`,
but are tedious, error-prone and typically inefficient. Often, you can get
by with adding copious amounts of `Console.RESET`s when working with colored
`java.lang.String`s, but even that easily results in errors when you `RESET`
too much and stomp over colors that already exist:

![StringError](docs/StringError.png)

`fansi.Str` allows you to perform these tasks safely and easily:

![FansiRocks](docs/FansiRocks.png)

Fansi is also very efficient: `fansi.Str` uses just 3x as much memory as
`java.lang.String` to hold all the additional formatting information.
Its operations are probably about the same factor slower, as they are all
implemented using fast `arraycopy`s and while-loops similar to
`java.lang.String`. That means that - unlike fiddling with Ansi-codes using
regexes - you generally do not need to worry about performance when dealing with
`fansi.Str`s. Just treat them as you would `java.lang.String`s: splitting them,
`substring`ing them, and applying or removing colors or other styles at-will.

Fansi was originally a part of the [Ammonite REPL](http://www.lihaoyi.com/Ammonite/),
but is now a standalone zero-dependency library anyone can use if they want
to easily and efficiently deal with colored Ansi strings.

Using Fansi
-----------

The main operations you need to know are:

- `fansi.Str(raw: CharSequence): fansi.String`, to construct colored
Ansi strings from a `java.lang.String`, with or without existing Ansi
color codes inside it.

- `fansi.Str`, the primary data-type that you will use to pass-around
colored Ansi strings and manipulate them: concatenating, splitting,
applying or removing colors, etc.

![fansi.Str](docs/Str.png)

- `fansi.Attr`s are the individual modifications you can make to an
`fansi.Str`'s formatting. Examples are:
- `fansi.Bold.{On, Off}`
- `fansi.Reversed.{On, Off}`
- `fansi.Underlined.{On, Off}`
- `fansi.Color.*`
- `fansi.Back.*`
- `fansi.Attr.Reset`

![fansi.Attr](docs/Attr.png)

- `fansi.Attrs` represents a group of zero or more `fansi.Attr`s.
These that can be passed around together, combined via `++` or applied
to `fansi.Str`s all at once. Any individual `fansi.Attr` can be used
when `fansi.Attrs` is required, as can `fansi.Attrs.empty`.

![fansi.Attrs](docs/Attrs.png)

- Using any of the `fansi.Attr` or `fansi.Attrs` mentioned above, e.g.
`fansi.Color.Red`, using `fansi.Color.Red("hello world ansi!")` to create a
`fansi.Str` with that text and color, or
`fansi.Str("hello world ansi!").overlay(fansi.Color.Blue, 6, 11)`

- `.render` to convert a `fansi.Str` back into a `java.lang.String` with all
necessary Ansi color codes within it

Fansi also supports 8-bit 256-colors through `fansi.Color.Full` and
`fansi.Back.Full`, as well as 24-bit 16-million-colors through
`fansi.Color.True` and `fansi.Back.True`:

![docs/TrueColor.png](docs/TrueColor.png)

Note that Fansi only performs the rendering of the colors to an ANSI-encoded
string. Final rendering will depend on whichever terminal you print the string
to, whether it is able to display these sets of colors or not.
76 changes: 72 additions & 4 deletions src/test/kotlin/io/exoquery/pprint/Examples.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package io.exoquery.pprint

import io.exoquery.fansi.Str
import io.exoquery.pprint
import java.time.format.DateTimeFormatter.ISO_LOCAL_DATE
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.time.format.DateTimeFormatter.*

data class Name(val first: String, val last: String)
data class Person(val name: Name, val age: Int)
Expand Down Expand Up @@ -61,18 +63,84 @@ fun ex5() = run {
class CustomPPrinter1(config: PPrinterConfig) : PPrinter(config) {
override fun treeify(x: Any?, escapeUnicode: Boolean, showFieldNames: Boolean): Tree =
when (x) {
is java.time.LocalDate -> Tree.Literal(x.format(ISO_LOCAL_DATE))
is java.time.LocalDate -> Tree.Literal(x.format(DateTimeFormatter.ofPattern("MM/dd/YYYY")))
else -> super.treeify(x, escapeUnicode, showFieldNames)
}
}

fun ex7() = run {
data class Person(val name: String, val born: LocalDate)
val pp = CustomPPrinter1(PPrinterConfig())
println(pp.invoke(java.time.LocalDate.now()))
println(pp.invoke(Person("Joe", LocalDate.of(1981, 1, 1))))
}

class MyJavaBean(val a: String, val b: Int) {
fun getValueA() = a
fun getValueB() = b
}

class CustomPPrinter2(config: PPrinterConfig) : PPrinter(config) {
override fun treeify(x: Any?, esc: Boolean, names: Boolean): Tree =
when (x) {
is MyJavaBean -> Tree.Apply("MyJavaBean", listOf(x.getValueA(), x.getValueB()).map { treeify(it, esc, names) }.iterator())
else -> super.treeify(x, esc, names)
}
}

fun ex8() = run {
val bean = MyJavaBean("abc", 123)
val pp = CustomPPrinter2(PPrinterConfig())
println(pp.invoke(bean))
}

class CustomPPrinter3(config: PPrinterConfig) : PPrinter(config) {
override fun treeify(x: Any?, esc: Boolean, names: Boolean): Tree {
// function to make recursive calls shorter
fun rec(x: Any?) = treeify(x, esc, names)
return when (x) {
// Recurse on the values, pass result into Tree.KeyValue.
is MyJavaBean ->
Tree.Apply(
"MyJavaBean",
listOf(Tree.KeyValue("a", rec(x.getValueA())), Tree.KeyValue("b", rec(x.getValueB()))).iterator()
)
else ->
super.treeify(x, esc, names)
}
}
}

fun ex9() = run {
val bean = MyJavaBean("abc", 123)
val pp = CustomPPrinter3(PPrinterConfig())
println(pp.invoke(bean))
}


class CustomPPrinter4(config: PPrinterConfig) : PPrinter(config) {
override fun treeify(x: Any?, escapeUnicode: Boolean, showFieldNames: Boolean): Tree {
// function to make recursive calls shorter
fun rec(x: Any?) = treeify(x, escapeUnicode, showFieldNames)
fun field(fieldName: String, value: Any?) =
if (showFieldNames) Tree.KeyValue(fieldName, rec(value)) else rec(value)
return when (x) {
// Recurse on the values, pass result into Tree.KeyValue.
is MyJavaBean ->
Tree.Apply("MyJavaBean", listOf(field("a", x.getValueA()), field("b", x.getValueB())).iterator())
else ->
super.treeify(x, escapeUnicode, showFieldNames)
}
}
}

fun ex10() = run {
val bean = MyJavaBean("abc", 123)
println(CustomPPrinter4(PPrinterConfig()).invoke(bean))
println(CustomPPrinter4(PPrinterConfig(defaultShowFieldNames = false)).invoke(bean))
}

fun main() {
ex7()
ex10()


//val seq = generateSequence { "foo" }
Expand Down

0 comments on commit d21d74e

Please sign in to comment.