Overview
Formatting in two layers: interpolation of scalars, and a composable layout of blocks.
(import (std prelude))
(import (std fmt))
;; Layer 1: interpolation
(println* ["Hello, " 42 ", ok? " #t])
;; Layer 2: layout
(show ["Padding: [" (padded/left 10 ["foo"]) "]\n"])
(show ["Number: " (numeric/comma 1234567.89) "\n"])
(show ["Columns:\n"
(columnar [["first long text string" "second text string"]
["col 2" "more text for col 2"]])])
Interpolation combines scalars of mixed types into text. It
works on a (Vec Disp): a vec literal whose elements are strings,
ints, doubles, chars and bools, each elaborated into the Disp case
that holds it. println* prints one.
Layout is a block algebra, for padding, aligning, wrapping
paragraphs and laying out tables. The unit is a Block: a string,
int or char, which needs no wrapping in a vec literal, or a BRun,
a function that writes to an output sink and threads the current column
through. Every combinator takes its content as a (Vec Block) and
answers a Block, so they nest. Nothing is written until the blocks
are run by show, render or render->string.
A combinator of your own is a BRun and the two functions that run
blocks, run-block and run-blocks. It is the same kind of
thing as the ones in this module:
(: boxed (-> (Vec Block) Block))
(defun (boxed bs)
(BRun (fun (out col)
(run-blocks out col ["[" (render->string bs) "]"]))))
Settings are parameters, so they are dynamically scoped:
with-fmt binds one around some blocks while they run, and it is
restored when they end. The ones here are:
| Parameter | Default | What it sets |
|---|---|---|
current-pad | #\space | The character padding is made of. |
current-width | 78 | The right margin of wrapped, justified and columnar. |
current-ellipsis | "" | What replaces the part a trim cuts off. |
current-string-width | the character count | How wide a string is, in columns. |
current-precision | -1 | Digits after the decimal separator; negative is as many as needed. |
current-comma | 0 | Digits per group; zero is no grouping. |
current-comma-sep | #\, | The character between groups. |
current-decimal-sep | #\. | The decimal separator. |
current-signed | #f | Whether a positive number gets a leading +. |
A setting of your own works the same way: with-fmt takes the
parameter as an argument. Read it with parameter-ref inside the
BRun’s function, when the block runs. Read where the block is
built, it is read before any enclosing with-fmt has bound it.
See also: println*, show, render->string, with-fmt, Block
Reference
capturedcolumnareachfitted/bothfitted/leftfitted/rightjoinedjoined/lastjoined/prefixjoined/suffixjustifiednumericnumeric/commapadded/bothpadded/leftpadded/rightprintln*renderrender->stringrun-blockrun-blocksshowspace-totab-totabulartrimmed/bothtrimmed/lefttrimmed/rightwidth-ofwith-fmtwith-padwrapped
Types
Disp
union
A scalar to interpolate: a string, int, double, char or bool.
DStr- A string, as it is.
DInt- An int, by int->string.
DDouble- A double, by double->string.
DChar- A char, as one character.
DBool- A bool, as #t or #f.
A vec literal of mixed scalars, such as ["n = " 42 ", ok? " #t],
elaborates each element into the case that can hold it, so the cases are
rarely written.
See also: println*
Sink
type alias
Where a block writes its text: a function given each piece of it.
Block
union
A piece of layout: a scalar, or a function that writes and answers the column it left.
BStr- A string, written as it is.
BInt- An int, written in decimal.
BChar- A char.
BRun- A function of the sink and the column it starts at, which writes and answers the column it ends at.
(render->string ["x = " 7 #\!]) ; "x = 7!": the leaves need no wrapping
The column is counted in characters since the last newline written. It
is the one thing threaded through every block, and what lets fl,
space-to and wrapped know where the cursor is.
Functions
println*
function
(: println* (-> (Vec Disp) void))
(println* ds)
Prints scalars of mixed types, then a newline.
ds- The scalars, written as a vec literal.
(println* ["n = " 42 ", ok? " #t ", " #\x ", " 1.5])
render
function
(: render (-> TextOutputPort (Vec Block) void))
(render port bs)
Writes blocks to a port.
port- The port to write to.
bs- The blocks, run from column 0.
It is #:sync, and cannot be otherwise: the sink is stored in a
BRun, and a stored function’s colour is its type, so no
suspending sink can go there. To render from a fiber, use
render->string and write the string yourself.
See also: show, render->string
show
function
(: show (-> (Vec Block) void))
(show bs)
Writes blocks to the current output port.
bs- The blocks, run from column 0.
(show ["Padding: [" (padded/left 10 ["foo"]) "]\n"])
render to current-output-port, and #:sync for the
same reason.
See also: render, render->string
render->string
function
(: render->string (-> (Vec Block) string))
(render->string bs)
The text blocks write, as a string.
bs- The blocks, run from column 0.
- returns
- Everything the blocks wrote.
(render->string [(padded/left 5 ["7"])]) ; " 7"
run-block
function
(: run-block (-> Sink int Block int))
(run-block out col b)
Runs one block: the engine’s step, for writing combinators.
out- The sink to write to.
col- The column the block starts at.
b- The block.
- returns
- The column it ends at.
See also: run-blocks, Block
run-blocks
function
(: run-blocks (-> Sink int (Vec Block) int))
(run-blocks out col bs)
Runs blocks one after another, threading the column.
out- The sink to write to.
col- The column the first block starts at.
bs- The blocks.
- returns
- The column the last one ends at.
See also: run-block
with-fmt
function
(: with-fmt (-> (Param %a) %a (Vec Block) Block))
(with-fmt p v bs)
Runs blocks with a parameter bound to a value.
%a- The parameter's type.
p- The parameter: one of this module's settings, or one of your own.
v- Its value while the blocks run.
bs- The blocks.
- returns
- A block. The parameter is bound when it runs, not when it is built, and restored when it ends.
(show [(with-fmt current-precision 2 [(numeric 3.14159)])]) ; 3.14
See also: with-pad, current-width
with-pad
function
(: with-pad (-> char (Vec Block) Block))
(with-pad c bs)
Runs blocks with another padding character.
c- The padding character.
bs- The blocks.
- returns
- A block:
(with-fmt current-pad c bs).
(show [(with-pad #\. ["ab" (space-to 6) "c"])]) ; ab....c, a dot leader
See also: current-pad, space-to
each
function
(: each (-> (Vec Block) Block))
(each bs)
Many blocks as one.
bs- The blocks.
- returns
- A block that runs them in order.
captured
function
(: captured (-> (-> string Block) (Vec Block) Block))
(captured f bs)
Renders blocks to a string and hands it to a function that answers a block.
f- Given the text the blocks wrote; its block is run in their place.
bs- The blocks to render.
- returns
- A block.
(captured (fun (s) (BInt (string-count s))) ["abcd"]) ; writes 4
How a combinator that needs the whole text, rather than writing as it
goes, is made: padding, trimming and wrapping are all written with it.
The blocks are rendered from column 0, and f’s block runs at the
current column.
See also: render->string
width-of
function
(: width-of (-> string int))
(width-of s)
How many columns a string takes, by current-string-width.
s- The string.
- returns
- Its width.
See also: current-string-width
space-to
function
(: space-to (-> int Block))
(space-to goal)
Pads to a column.
goal- The column to move to.
- returns
- A block that writes padding up to goal, and nothing if the cursor is already at or past it.
(render->string ["ab" (space-to 5) "c"]) ; "ab c"
It fills with current-pad, so under with-pad it is a
leader. It never moves backwards.
tab-to
function
(: tab-to (-> (#:width int) Block))
(tab-to #:width 8)
Pads to the next tab stop.
#:width- The distance between tab stops. 8 unless given.
- returns
- A block that pads to the next multiple of width.
(render->string ["ab" (tab-to #:width 4) "c"]) ; "ab c"
(render->string ["abcd" (tab-to #:width 4) "c"]) ; "abcd c"
It always moves: at a tab stop already, it pads to the next one.
See also: space-to
padded/left
function
(: padded/left (-> int (Vec Block) Block))
(padded/left w bs)
Pads content on the left to a width: aligned right.
w- The width, in columns.
bs- The content.
- returns
- A block. Content wider than w is left as it is: padding never cuts.
(render->string [(padded/left 6 ["x=" 7])]) ; " x=7"
See also: padded/right, padded/both, fitted/left, current-pad
padded/right
function
(: padded/right (-> int (Vec Block) Block))
(padded/right w bs)
Pads content on the right to a width: aligned left.
w- The width, in columns.
bs- The content.
- returns
- A block. Content wider than w is left as it is.
See also: padded/left, padded/both, fitted/right
padded/both
function
(: padded/both (-> int (Vec Block) Block))
(padded/both w bs)
Pads content on both sides to a width: centred.
w- The width, in columns.
bs- The content.
- returns
- A block, with the odd column, if there is one, on the right. Content wider than w is left as it is.
See also: padded/left, padded/right, fitted/both
trimmed/left
function
(: trimmed/left (-> int (Vec Block) Block))
(trimmed/left w bs)
Cuts content to a width, keeping its end.
w- The width, in columns.
bs- The content.
- returns
- A block. Content that fits is left as it is; otherwise current-ellipsis stands at the start, where the cut was.
(render->string [(trimmed/left 3 ["abcdef"])]) ; "def"
See also: trimmed/right, trimmed/both, fitted/left, current-ellipsis
trimmed/right
function
(: trimmed/right (-> int (Vec Block) Block))
(trimmed/right w bs)
Cuts content to a width, keeping its start.
w- The width, in columns.
bs- The content.
- returns
- A block. Content that fits is left as it is; otherwise current-ellipsis stands at the end.
(render->string [(trimmed/right 3 ["abcdef"])]) ; "abc"
See also: trimmed/left, trimmed/both, fitted/right, current-ellipsis
trimmed/both
function
(: trimmed/both (-> int (Vec Block) Block))
(trimmed/both w bs)
Cuts content to a width, keeping its middle.
w- The width, in columns.
bs- The content.
- returns
- A block. Content that fits is left as it is; otherwise current-ellipsis stands at both ends.
(render->string [(trimmed/both 4 ["abcdefgh"])]) ; "cdef"
See also: trimmed/left, trimmed/right, fitted/both
fitted/left
function
(: fitted/left (-> int (Vec Block) Block))
(fitted/left w bs)
Content at exactly a width, aligned right: padded if short, cut if long.
w- The width, in columns.
bs- The content.
- returns
- A block: trimmed/left, then padded/left.
See also: padded/left, trimmed/left
fitted/right
function
(: fitted/right (-> int (Vec Block) Block))
(fitted/right w bs)
Content at exactly a width, aligned left: padded if short, cut if long.
w- The width, in columns.
bs- The content.
- returns
- A block: trimmed/right, then padded/right.
(render->string [(fitted/right 5 ["ab"]) "|"]) ; "ab |"
(render->string [(fitted/right 3 ["abcdef"]) "|"]) ; "abc|"
See also: padded/right, trimmed/right
fitted/both
function
(: fitted/both (-> int (Vec Block) Block))
(fitted/both w bs)
Content at exactly a width, centred: padded if short, cut in the middle if long.
w- The width, in columns.
bs- The content.
- returns
- A block: trimmed/both, then padded/both.
See also: padded/both, trimmed/both
joined
function
(: joined (-> (-> %a Block) (List %a) (Vec Block) Block))
(joined f xs sep)
Every element of a list, formatted, with a separator between them.
%a- The elements' type.
f- Formats one element.
xs- The elements.
sep- The separator, itself blocks.
- returns
- A block. One element has no separator, and none is nothing.
(show [(joined (fun (n) (BInt n)) (list 1 2 3) [", "])]) ; 1, 2, 3
See also: joined/prefix, joined/suffix, joined/last
joined/prefix
function
(: joined/prefix (-> (-> %a Block) (List %a) (Vec Block) Block))
(joined/prefix f xs sep)
Every element of a list, formatted, with a separator before each, the first included.
%a- The elements' type.
f- Formats one element.
xs- The elements.
sep- The separator, itself blocks.
- returns
- A block.
(show [(joined/prefix (fun (s) (BStr s)) (list "a" "b") [","])]) ; ,a,b
See also: joined, joined/suffix
joined/suffix
function
(: joined/suffix (-> (-> %a Block) (List %a) (Vec Block) Block))
(joined/suffix f xs sep)
Every element of a list, formatted, with a separator after each, the last included.
%a- The elements' type.
f- Formats one element.
xs- The elements.
sep- The separator, itself blocks.
- returns
- A block.
(show [(joined/suffix (fun (s) (BStr s)) (list "a" "b") [","])]) ; a,b,
See also: joined, joined/prefix
joined/last
function
(: joined/last (-> (-> %a Block) (-> %a Block) (List %a) (Vec Block) Block))
(joined/last f last-f xs sep)
Like joined, with the last element formatted by a function of its own.
%a- The elements' type.
f- Formats every element but the last.
last-f- Formats the last element.
xs- The elements.
sep- The separator, itself blocks.
- returns
- A block. A list of one is formatted by last-f alone.
(show [(joined/last (fun (s) (BStr s))
(fun (s) (each ["and " (BStr s)]))
(list "a" "b" "c")
[", "])])
;; a, b, and c
The separator is written before the last element too, so what
last-f adds is a word, not a comma.
See also: joined
numeric
function
(: numeric (-> double (#:precision int) (#:comma int) (#:comma-sep char) (#:decimal-sep char) (#:signed bool) Block))
(numeric x #:precision -1 #:comma -1 #:comma-sep #\nul
#:decimal-sep #\nul #:signed #f)
A number, with a precision, grouped digits and separators of your choosing.
x- The number. An int is formatted as (cast double n).
#:precision- Digits after the decimal separator. Negative, the default, is current-precision.
#:comma- Digits per group; 0 is no grouping. Negative, the default, is current-comma.
#:comma-sep- The character between groups. #\nul, the default, is current-comma-sep.
#:decimal-sep- The decimal separator. #\nul, the default, is current-decimal-sep.
#:signed- #t gives a positive number a leading +. #f, the default, is current-signed.
- returns
- A block.
(numeric 2.0 #:precision 2) ; 2.00
(numeric -3.14159 #:precision 3) ; -3.142
(numeric 1234567.891 #:precision 2 #:comma 3) ; 1,234,567.89
(numeric 42.0 #:signed #t) ; +42
(numeric 1234.5 #:comma 3 #:comma-sep #\space #:decimal-sep #\,) ; 1 234,5
The digits are the invariant culture’s and the separators are settings,
so the machine’s locale never shows. A keyword not given defaults to its
parameter when the block runs. Since a keyword’s default is what stands
for the parameter, a keyword cannot ask for that default against a
parameter bound otherwise: #:signed #f under a
current-signed of #t still signs, and a negative
#:precision still reads current-precision.
See also: numeric/comma, current-precision, current-comma
numeric/comma
function
(: numeric/comma (-> double Block))
(numeric/comma x)
A number with its digits grouped in thousands: the common case.
x- The number.
- returns
- A block:
(numeric x #:comma 3).
(numeric/comma 1234567.0) ; 1,234,567
(numeric/comma 42.0) ; 42
See also: numeric
wrapped
function
(: wrapped (-> (Vec Block) Block))
(wrapped bs)
Content filled to the right margin, ragged on the right.
bs- The content. Every run of whitespace in it, newlines too, is a break between words.
- returns
- A block.
(with-fmt current-width 12 [(wrapped ["the quick brown fox jumps"])])
;; the quick
;; brown fox
;; jumps
The first line starts wherever the cursor is, and has only the room left
before current-width. Words are separated by one
current-pad. A word wider than a line gets a line of its own
rather than being cut.
See also: justified, current-width
justified
function
(: justified (-> (Vec Block) Block))
(justified bs)
Content filled to the right margin and flush against both, the last line excepted.
bs- The content. Every run of whitespace in it is a break between words.
- returns
- A block.
(with-fmt current-width 12 [(justified ["the quick brown fox jumps"])])
;; the quick
;; brown fox
;; jumps
Lines are broken as wrapped breaks them, then the gaps are
widened to fill the line, the leftmost ones taking any extra column. The
last line, and a line of one word, are not stretched.
See also: wrapped, current-width
columnar
function
(: columnar (-> (Vec (Vec Block)) (#:sep string) Block))
(columnar cols #:sep " ")
Columns side by side, dividing current-width evenly and cutting what does not fit.
cols- The columns, each a vec of blocks; a newline in one starts its next row.
#:sep- Written between the columns. A space unless given.
- returns
- A block.
(show [(with-fmt current-width 11
[(columnar [["abc" nl "def"]
["123" nl "456"]])])])
;; abc 123
;; def 456
Each column is rendered on its own and the lines are laid out together. The width left once the separators are taken out is shared equally, and every cell is cut and padded to its share, the last column cut but not padded. A column with fewer lines than the others contributes blanks. The table starts on a fresh line, and every row ends with a newline.
See also: tabular, current-width
tabular
function
(: tabular (-> (Vec (Vec Block)) (#:sep string) Block))
(tabular cols #:sep " ")
Columns side by side, each as wide as its widest line, cutting nothing.
cols- The columns, each a vec of blocks; a newline in one starts its next row.
#:sep- Written between the columns. A space unless given.
- returns
- A block.
(show [(tabular [["a" nl "bbb"]
["1" nl "2"]])])
;; a 1
;; bbb 2
(show [(tabular [["a" nl "bbb"]
["1" nl "2"]]
#:sep " | ")])
;; a | 1
;; bbb | 2
Laid out as columnar lays them out, but with each column padded
to the width its own content asks for, the last not padded at all. A
column with fewer lines than the others contributes blanks. The table
starts on a fresh line, and every row ends with a newline.
See also: columnar
Values
current-pad
value
(: current-pad (Param char))
The character padding is made of. A space unless set.
Padding that is not a whole number of pad characters wide is as many as
fit. Words in wrapped and justified are separated by it
too.
See also: with-pad, padded/left, space-to
current-width
value
(: current-width (Param int))
The right margin: what wrapped, justified and columnar fill up to. 78 unless set.
current-ellipsis
value
(: current-ellipsis (Param string))
What replaces the part a trim cuts off. Empty, which cuts silently, unless set.
(with-fmt current-ellipsis "..." [(trimmed/right 6 ["abcdefgh"])]) ; abc...
It takes its room out of the content, so a trimmed text is never wider than asked for. An ellipsis wider than that is cut itself.
See also: trimmed/right
current-string-width
value
(: current-string-width (Param (-> string int)))
How many columns a string takes. Its character count unless set.
;; An astral character takes two columns on a terminal.
(defun (terminal-width s)
(string-fold (fun (ch acc) (+ acc (if (> (char->integer ch) 65535) 2 1))) 0 s))
(with-fmt current-string-width terminal-width [(padded/left 5 ["😀"])])
Everything that measures or cuts goes through it: padding, trimming,
wrapping and tables. The column a BRun is handed is not: it
counts characters.
See also: width-of
current-precision
value
(: current-precision (Param int))
Digits after the decimal separator. Negative, the default, is as many as the number needs.
As many as it needs is what double->string would give. A
precision rounds.
See also: numeric
current-comma
value
(: current-comma (Param int))
Digits per group in the integer part. Zero, the default, is no grouping.
See also: numeric, numeric/comma, current-comma-sep
current-comma-sep
value
(: current-comma-sep (Param char))
The character between groups of digits. A comma unless set.
See also: current-comma, numeric
current-decimal-sep
value
(: current-decimal-sep (Param char))
The decimal separator. A full stop unless set.
See also: numeric, current-comma-sep
current-signed
value
(: current-signed (Param bool))
Whether a number with no sign of its own gets a leading +. #f unless set.
See also: numeric
nothing
value
(: nothing Block)
A block that writes nothing: what a conditional arm answers when it has nothing to say.
(render->string ["a" (if verbose? nl nothing) "b"])
See also: each
fl
value
(: fl Block)
A fresh line: a newline, unless the cursor is already at the start of one.
(render->string ["a" nl fl "b"]) ; "a\nb"
(render->string [fl "a"]) ; "a"
See also: nl