Current section

Files

Jump to
pprint src pprint.gleam
Raw

src/pprint.gleam

import gleam/io
import gleam/int
import gleam/bool
import gleam/list
import gleam/float
import gleam/dict.{type Dict}
import gleam/string
import gleam/dynamic.{type Dynamic}
import gleam/bit_array
import glam/doc.{type Document}
import pprint/decoder
// --- PUBLIC API --------------------------------------------------------------
/// Configuration for the pretty printer.
///
pub type Config {
Config(
style_mode: StyleMode,
bit_array_mode: BitArrayMode,
label_mode: LabelMode,
)
}
/// Styling can be configured with `StyleMode`.
///
pub type StyleMode {
/// Data structures are styled with ANSI style codes.
Styled
/// Everything remains unstyled.
Unstyled
}
/// Since Erlang handles BitArrays differently than JavaScript does, the
/// `BitArraysAsString` config option enables compatibility between the two targets.
///
/// These options only affect the JS target, which does not convert bit arrays to
/// strings by default like Erlang does.
///
pub type BitArrayMode {
/// Bit arrays will be converted to strings when pretty printed.
BitArraysAsString
/// Bit arrays will be kept the same.
KeepBitArrays
}
/// This option only affects the JavaScript target since Erlang has a different
/// runtime representation of custom types that omits labels.
///
pub type LabelMode {
/// Show field labels in custom types.
/// ```
/// Foo(42, bar: "bar", baz: "baz")
/// ```
Labels
/// Leave out field labels.
/// ```
/// Foo(42, "bar", "baz")
/// ```
NoLabels
}
const max_width = 40
/// Pretty print a value with the config below to stderr for debugging purposes.
/// The value is returned back from the function so it can be used in pipelines.
///
/// ```
/// Config(Styled, KeepBitArrays, Labels)
/// ```
///
/// # Examples
///
/// ```
/// pprint.debug([1, 2, 3])
/// |> list.each(pprint.debug)
///
/// // Prints:
/// // [1, 2, 3]
/// // 1
/// // 2
/// // 3
/// ```
///
pub fn debug(value: a) -> a {
value
|> with_config(Config(Styled, KeepBitArrays, Labels))
|> io.println_error
value
}
/// Pretty print a value as a string with the following config:
/// ```
/// Config(Unstyled, BitArraysAsString, NoLabels)
/// ```
/// This function behaves identically on both targets so it can be relied upon
/// for snapshot testing.
///
pub fn format(value: a) -> String {
with_config(value, Config(Unstyled, BitArraysAsString, NoLabels))
}
/// Pretty print a value as a string with the following config:
/// ```
/// Config(Styled, BitArraysAsString, NoLabels)
/// ```
/// This function behaves identically on both targets so it can be relied upon
/// for snapshot testing.
///
pub fn styled(value: a) -> String {
with_config(value, Config(Styled, BitArraysAsString, NoLabels))
}
/// Pretty print a value as a string with a custom config.
///
/// # Examples
///
/// ```
/// [1, 2, 3, 4]
/// |> pprint.with_config(Config(Color, KeepBitArrays, Labels))
/// ```
///
pub fn with_config(value: a, config: Config) -> String {
value
|> dynamic.from
|> pretty_dynamic(config)
|> doc.to_string(max_width)
}
// ---- PRETTY PRINTING --------------------------------------------------------
fn pretty_type(value: decoder.Type, config: Config) -> Document {
case value {
decoder.TString(s) -> pretty_string(s, config)
decoder.TInt(i) ->
int.to_string(i)
|> ansi(yellow, config)
decoder.TFloat(f) ->
float.to_string(f)
|> ansi(yellow, config)
decoder.TBool(b) ->
bool.to_string(b)
|> ansi(blue, config)
decoder.TBitArray(b) ->
case config.bit_array_mode {
KeepBitArrays -> pretty_bit_array(b, config)
BitArraysAsString ->
case bit_array.to_string(b) {
Ok(s) -> pretty_string(s, config)
Error(Nil) -> pretty_bit_array(b, config)
}
}
decoder.TNil -> ansi("Nil", blue, config)
decoder.TList(items) -> pretty_list(items, config)
decoder.TDict(d) -> pretty_dict(d, config)
decoder.TTuple(items) -> pretty_tuple(items, config)
decoder.TCustom(name, fields) -> pretty_custom_type(name, fields, config)
decoder.TForeign(f) -> ansi(f, dim, config)
}
}
fn pretty_dynamic(value: Dynamic, config: Config) -> Document {
value
|> decoder.classify
|> pretty_type(config)
}
fn pretty_string(string: String, config: Config) -> Document {
{ "\"" <> string <> "\"" }
|> ansi(green, config)
}
fn pretty_bit_array(bits: BitArray, config: Config) -> Document {
string.inspect(bits)
|> ansi(magenta, config)
}
fn pretty_list(items: List(Dynamic), config: Config) -> Document {
let items = list.map(items, decoder.classify)
// When the list consists only of numbers, the values are joined with flex spaces
// instead of normal ones.
let space = case items {
[decoder.TInt(_), ..] | [decoder.TFloat(_), ..] -> doc.flex_space
_ -> doc.space
}
list.map(items, pretty_type(_, config))
|> doc.concat_join([doc.from_string(","), space])
|> wrap(doc.from_string("["), doc.from_string("]"), trailing: ",")
}
fn pretty_dict(d: Dict(decoder.Type, decoder.Type), config: Config) -> Document {
dict.to_list(d)
|> list.sort(fn(one_field, other_field) {
// We need to sort dicts so that those always have a consistent order.
let #(one_key, _one_value) = one_field
let #(other_key, _other_value) = other_field
string.compare(string.inspect(one_key), string.inspect(other_key))
})
|> list.map(fn(field) {
// Format the dict's items into tuple literals
[
doc.from_string("#("),
pretty_type(field.0, config),
doc.from_string(", "),
pretty_type(field.1, config),
doc.from_string(")"),
]
|> doc.concat
})
|> doc.concat_join([doc.from_string(","), doc.space])
|> wrap(
doc.from_string("dict.from_list(["),
doc.from_string("])"),
trailing: ",",
)
}
fn pretty_tuple(items: List(Dynamic), config: Config) -> Document {
list.map(items, pretty_dynamic(_, config))
|> doc.concat_join([doc.from_string(","), doc.space])
|> wrap(doc.from_string("#("), doc.from_string(")"), trailing: ",")
}
fn pretty_custom_type(
name: String,
fields: List(decoder.Field),
config: Config,
) -> Document {
// Common built-in constructor names are styled
let style = case name {
"Ok" | "Error" | "Some" | "None" -> bold
_ -> ""
}
let fields =
list.map(fields, fn(field) {
case field, config.label_mode {
decoder.Positional(value), Labels
| decoder.Positional(value), NoLabels
| decoder.Labelled(_, value), NoLabels -> pretty_dynamic(value, config)
decoder.Labelled(label, value), Labels ->
doc.concat([
ansi(label <> ": ", dim, config),
pretty_dynamic(value, config),
])
}
})
let open = doc.concat([ansi(name, style, config), doc.from_string("(")])
let close = doc.from_string(")")
case fields {
[] -> doc.from_string(name)
// If the constructor has only one field, it is formatted without indenting
// its field to improve readability. In other words, it is formatted like this
//
// Ok([
// // ...
// ])
//
// instead of this:
//
// Ok(
// [
// // ...
// ]
// )
//
[single] -> doc.concat([open, single, close])
// However, multiple fields are indented because they would look weird otherwise.
_ ->
fields
|> doc.concat_join([doc.from_string(","), doc.space])
|> wrap(open, close, trailing: ",")
}
}
// ---- ANSI -------------------------------------------------------------------
// Sadly packages like `gleam_community_ansi` cannot be used with Glam since ANSI
// escape codes need to be wrapped with `doc.zero_width_string` calls.
const reset = "\u{001b}[0m"
const green = "\u{001b}[38;5;2m"
const yellow = "\u{001b}[38;5;3m"
const blue = "\u{001b}[38;5;4m"
const magenta = "\u{001b}[38;5;5m"
const bold = "\u{001b}[1m"
const dim = "\u{001b}[2m"
fn ansi(text: String, code: String, config: Config) -> Document {
let text_doc = doc.from_string(text)
case config.style_mode {
Unstyled -> text_doc
Styled ->
doc.concat([
doc.zero_width_string(code),
text_doc,
doc.zero_width_string(reset),
])
}
}
// ---- UTILS ------------------------------------------------------------------
fn wrap(
document: Document,
open: Document,
close: Document,
trailing trailing: String,
) -> Document {
document
|> doc.prepend_docs([open, doc.soft_break])
|> doc.nest(by: 2)
|> doc.append_docs([doc.break("", trailing), close])
|> doc.group
}