SRFI 159: Combinator Formatting
This SRFI is currently in final status. Here is an explanation of each status that a SRFI can hold. To provide input on this SRFI, please send email to
firstname.lastname@example.org. To subscribe to the list, follow these instructions. You can access previous messages via the mailing list archive.
A library of procedures for formatting Scheme objects to text in various ways, and for easily concatenating, composing and extending these formatters efficiently without resorting to capturing and manipulating intermediate strings.
There are several approaches to text formatting. Concatenating
strings to display is not acceptable, since it doesn't scale to very
large output. The simplest realistic idea, and what people resort to
in typical portable Scheme, is to interleave
write and manual loops, but this is both extremely
verbose and doesn't compose well. A simple concept such as padding
space can't be achieved directly without somehow capturing intermediate
The traditional approach in other languages is to use templates -
typically strings, though in theory any object could be used and
indeed Emacs's mode-line format templates allow arbitrary
sexps. Templates can use either escape sequences (as in C's
and Common Lisp's
format) or pattern matching (as in Visual Basic's
form, and SQL date formats). The primary
disadvantage of templates is the relative difficulty (usually
impossibility) of extending them, their opaqueness, and the
unreadability that arises with complex formats. Templates are not
without their advantages, but they are already addressed by other
libraries such as SRFI 28
and SRFI 48.
Another important aspect of formatting is state. Common Lisp
provides a "fresh-line" format spec which outputs a newline only if
the output stream is not already at the beginning of a line. C++
iostreams allow changing the radix and floating-point precision for
numeric output, not just for a single value but as a persistent
setting for all future output. Custom formatters which could
manipulate their own state would allow for many new possibilities.
This SRFI takes a combinator approach to solving both problems. Formatters are defined, which are called to produce their output as needed, composed with other formatters, and refer to and update arbitrary state. The primary goal of this SRFI is to have a maximally expressive and extensible formatting library. The next most important goal is scalability - to be able to handle arbitrarily large output and not build intermediate results except where necessary. The third goal is brevity and ease of use.
Base show each each-in-list displayed written written-simply pretty pretty-simply escaped maybe-escaped numeric numeric/comma numeric/si numeric/fitted nl fl space-to tab-to nothing joined joined/prefix joined/suffix joined/last joined/dot joined/range padded padded/right padded/both trimmed trimmed/right trimmed/both trimmed/lazy fitted fitted/right fitted/both fn with with! forked call-with-output port row col width output writer string-width pad-char ellipsis radix precision decimal-sep decimal-align Columnar columnar tabular wrapped wrapped/list wrapped/char justified from-file line-numbers Unicode as-unicode unicode-terminal-width Color as-red as-blue as-green as-cyan as-yellow as-magenta as-white as-black as-bold as-underline
We introduce a new type,
formatter, which is
disjoint from any type except possibly procedures.
In the prototypes below the following naming conventions imply type restrictions:
The naming of formatters and mappers is generally chosen such that they read as adjectives or adverbs describing how the objects they act on are formatted. This provides a natural reading of the code, and allows for a simple mapping between standard operations and their formatting counterparts:
The SRFI is divided into a core implementation and three utility libraries, which could be defined portably in terms of the core but are provided as convenience extensions. The libraries are as follows:
(srfi 159) ; composite of all of the following (srfi 159 base) ; all bindings not in one of the following (srfi 159 columnar) ; all bindings in Columnar Formatting (srfi 159 unicode) ; all bindings in Unicode (srfi 159 color) ; all bindings in Formatting with Color
showoutput-dest fmt ...)
The entry point for all formatting. Applies the fmt formatters in
sequence, accumulating the output to output-dest. As with SRFI
format, output-dest can be an output port,
indicate the current output port, or
#f to accumulate the
output into a string and return that as the result of
Each fmt should be a formatter as discussed below. As a
convenience, non-formatter arguments are also allowed and are
formatted as if wrapped with
displayed, described below, so
(show #f "π = " (with ((precision 2)) (acos -1)) nl)would return the string
"π = 3.14\n".
As mentioned, formatters are an opaque type and cannot directly be
applied outside of
show. Custom formatters are built on the
existing formatters, and as first class objects may be named or
computed dynamically, so that:
(let ((~.2f (lambda (x) (with ((precision 2)) x)))) (show #f "π = " (~.2f (acos -1)) nl))produces the same result. For typical uses you only need to combine the existing high level formatters described in the succeeding sections, but see the section Higher Order Formatters and State for control flow and state manipulation primitives.
The return value of
show is the accumulated string if
#f and unspecified otherwise.
If obj is a formatter, returns obj as is.
Otherwise, outputs obj using
Specifically, strings are output as if by
characters are written as if by
write-char. Other objects
are output as with
written (including nested strings and chars
inside obj). This is the default behavior for top-level formats
each and most other high-level formatters.
Outputs obj using
write semantics. Uses the current
numeric formatting settings to the extent that the written
result can still
be passed to
read, possibly with loss of precision.
the current radix is used if set to any of 2, 8, 10 or 16, and the
fixed point precision is used if specified and the
radix is 10.
(show #f (written (cons 0 1))) => "(0 . 1)"
(show #f 1.5 " " (with ((precision 0)) 1.5)) => "1.5 2"
(show #f 1/7 " " (with ((precision 3)) 1/7) " " (with ((precision 20)) 1/7)) => "1/7 0.143 0.14285714285714285714"
Implementations should allow arbitrary precision for exact rational
numbers, for example, using
string-segment from SRFI 152, the
following code returns the first 100 Fibonacci numbers:
(map string->number (string-segment (show #f (with ((precision 2500)) (/ 1000 (- #e1e50 #e1e25 1)))) 25))
As above, but doesn't handle shared structures. Infinite loops can
still be avoided if used inside a formatter that truncates data
Pretty-prints obj. The result should be identical to
written except possibly for differences in whitespace to make
the output resemble formatted source code. Implementations should
print vectors and data lists (lists that don't begin with a (nested)
symbol) in a tabular format when possible to reduce vertical space.
As above but without sharing.
escapedstr [quote-ch esc-ch renamer])
Outputs the string str, escaping any quote or escape characters.
If esc-ch, which defaults to
escapes only the quote-ch, which defaults to
by doubling it, as in SQL strings and CSV values. If renamer is
provided, it should be a procedure of one character which maps that
character to its escape value, e.g.
#\newline => #\n,
#f if there is no escape value.
(show #f (escaped "hi, bob!")) => "hi, bob!" (show #f (escaped "hi, \"bob!\"")) => "hi, \"bob!\""
maybe-escapedstr pred [quote-ch esc-ch renamer])
escaped, but first checks if any quoting is required (by
the existence of either any quote or escape characters, or any
pred), and if so outputs the string in
quotes and with escapes. Otherwise outputs the string as is. This
is useful for quoting symbols and CSV output, etc.
(show #f (maybe-escaped "foo" char-whitespace? #\")) => "foo"
(show #f (maybe-escaped "foo bar" char-whitespace? #\")) => "\"foo bar\""
(show #f (maybe-escaped "foo\"bar\"baz" char-whitespace? #\")) => "\"foo\"bar\"baz\""
numericnum [radix precision sign comma comma-sep decimal-sep])
Formats a single number num. You can optionally specify any radix from 2 to 36 (even if num isn't an integer). precision forces a fixed-point format.
A sign of
#t indicates to output a plus sign (+) for
positive integers. However, if sign is a pair of two strings, it
means to wrap negative numbers with the two strings. For example,
("(" . ")") prints negative numbers in parentheses, financial
-1.99 => (1.99).
comma is an integer specifying the number of digits between commas.
comma-sep is the character to use for commas, defaulting to
decimal-sep is the character to use for decimals, defaulting
#\., or to
#\, (European style) if comma-sep is
These parameters may seem unwieldy, but they can also take their defaults from state variables, described below.
numeric/commanum [base precision sign])
numeric to print with commas.
(show #f (numeric/comma 1234567)) => "1,234,567"
numeric/sinum [base separator])
Abbreviates num with an SI suffix as in the
option to many GNU commands. The base defaults to 1024, using
suffix names like Ki, Mi, Gi, etc. Other bases (e.g. the standard
1000) have the suffixes k, M, G, etc. If separator is
provided, it is inserted after the number, before any suffix.
(show #f (numeric/si 608)) => "608"
(show #f (numeric/si 608) "B") => "608B"
(show #f (numeric/si 608 " ") "B") => "608 B"
(show #f (numeric/si 3986)) => "3.9Ki"
(show #f (numeric/si 3986 1000) "B") => "4kB"
(show #f (numeric/si 1.23e-6 1000) "m") => "1.23μm"
(show #f (numeric/si 1.23e-6 1000 " ") "m") => "1.23 μm"
See https://en.wikipedia.org/wiki/Metric_prefix for the complete list of abbreviations.
numeric/fittedwidth n . args)
numeric, but if the result doesn't fit in
width using the current
instead a string of hashes rather than showing an incorrectly
truncated number. For example
(show #f (with ((precision 2)) (numeric/fitted 4 1.25))) => "1.25"
(show #f (with ((precision 2)) (numeric/fitted 4 12.345))) => "#.##"
Outputs a newline.
(show #f nl) => "\n"
Short for "fresh line," outputs a newline only if we're not already at the start of a line.
(show #f fl) => ""
(show #f "hi" fl) => "hi\n"
(show #f "hi" nl fl) => "hi\n"
Outputs spaces up to the given column. If the current column is already >= column, does nothing. The character used for spacing is the current value of pad-char, described below, which defaults to space. Columns are zero-based.
(show #f "a" (space-to 5) "b") => "a b"
(show #f "a" (space-to 0) "b") => "ab"
Outputs spaces up to the next tab stop, using tab stops of width
tab-width, which defaults to 8. If already on a tab stop,
does nothing. If you want to ensure you always tab at least one
space, you can use
(each " " (tab-to width)). Columns
(show #f (tab-to 5) "b") => "b"
(show #f "a" (tab-to 5) "b") => "a b"
(show #f "abcdefghi" (tab-to 5) "b") => "abcdefghi b"
Outputs nothing (useful in combinators and as a default noop in conditionals).
(show #f "a" nothing "b") => "ab"
Applies each fmt in sequence, as in the top-level of
(show #f (each "a" "b")) => "ab"
(apply each list-of-fmts) but may be more efficient.
joinedmapper list [sep])
Formats each element elt of list with
inserting sep in between. sep defaults to the empty string, but
can be any format or string.
(show #f (joined displayed '(a b c) ", ")) => "a, b, c"
joined/prefixmapper list [sep])
joined/suffixmapper list [sep])
(show #f (joined/prefix displayed '(usr local bin) "/")) => "/usr/local/bin"
(show #f (joined/suffix displayed '(1 2 3) nl)) => "1\n2\n3\n"As
joined, but inserts sep before/after every element.
joined/lastmapper last-mapper list [sep])
joined, but the last element of the list is formatted with
(show #f (joined/last displayed (lambda (last) (each "and " last)) '(lions tigers bears) ", ")) => "lions, tigers, and bears"
joined/dotmapper dot-mapper list [sep])
joined, but if the list is a dotted list, then formats the
dotted value with dot-mapper instead.
(show #f "(" (joined/dot displayed (lambda (dot) (each ". " dot)) '(1 2 . 3) " ") ")") => "(1 2 . 3)"
joined/rangemapper start [end sep])
joined, but counts from start (inclusive) to end
(exclusive), formatting each integer in the range with mapper. If
#f or unspecified, produces an infinite stream of
(show #f (joined/range displayed 0 5 " ")) => "0 1 2 3 4"
paddedwidth fmt ...)
padded/rightwidth fmt ...)
padded/bothwidth fmt ...)
Analogs of SRFI 13
string-pad, these add extra space to the
left, right or both sides of the output generated by the fmts
to pad it to width. If width is exceeded, has no effect.
padded/both will include one more extra space on the right
side of the output if the difference is odd.
padded/right is guaranteed not to accumulate any intermediate
Note these are column-oriented padders, so won't necessarily work with multi-line output (padding doesn't seem a likely operation for multi-line output).
(show #f (padded 5 "abc")) => " abc"
(show #f (padded/right 5 "abc")) => "abc "
(show #f (padded/both 5 "abc")) => " abc "
trimmedwidth fmt ...)
trimmed/rightwidth fmt ...)
trimmed/bothwidth fmt ...)
Analogs of SRFI 13
string-trim, these truncate the output of the
fmts to force it in under width columns. As soon as any of
the fmts exceeds width, stop formatting and truncate the
result, returning control to whoever called trimmed. If width
is not exceeded, is equivalent to
If a truncation ellipsis is set, then when any truncation occurs
trimmed/right will prepend and append the
trimmed/both will both prepend and
append. The length of the ellipsis will be considered when
truncating the original string, so that the total width will never
be longer than width. It is an error if width is
less than the length of ellipsis, or double the length for /both.
(show #f (with ((ellipsis "...")) (trimmed 5 "abcde"))) => "abcde"
(show #f (with ((ellipses "...")) (trimmed 5 "abcdef"))) => "ab..."It is an error if width is shorter than the width of the ellipsis.
trimmed/lazywidth fmt ...)
A variant of
trimmed which generates each fmt in
left to right order, and truncates and terminates
immediately if more than width characters are generated.
Thus this is safe to use with an infinite amount of output,
written-simply on an infinite list.
fittedwidth fmt ...)
fitted/rightwidth fmt ...)
fitted/bothwidth fmt ...)
A combination of
trimmed that ensures that the output
width is exactly width, truncating if it goes over and padding if
it goes under.
(srfi 159 columnar)library.
space-to and padding/trimming can be
used to manually align columns to produce table-like output, these
can be tedious to use. The optional extensions in this section make
Formats each column side-by-side, i.e. as though each were formatted separately and then the individual lines concatenated together. The current line width (from the width state variable) is divided evenly among the columns, and all but the last column are right-padded. For example
(show #t (columnar (displayed "abc\ndef\n") (displayed "123\n456\n")))outputs
abc 123 def 456assuming a 16-char width (the left side gets half the width, or 8 spaces, and is left aligned). Note that we explicitly use
displayedinstead of the strings directly. This is because
columnartreats raw strings as literals inserted into the given location on every line, to be used as borders, for example:
(show #t (columnar "/* " (displayed "abc\ndef\n") " | " (displayed "123\n456\n") " */"))would output
/* abc | 123 */ /* def | 456 */You may also prefix any column with any of the symbols 'left, 'right or 'center to control the justification. The symbol 'infinite can be used to indicate the column generates an infinite stream of output.
You can further prefix any column with a width modifier. Any positive integer is treated as a fixed width, ignoring the available width. Any real number between 0 and 1 indicates a fraction of the available width (after subtracting out any fixed widths). Columns with unspecified width divide up the remaining width evenly. If the extra space does not divide evenly, it is allocated column-wise left to right, e.g. if the width of 78 is divided among 5 columns, the column widths become 16, 16, 16, 15, 15 in order.
columnar builds its output incrementally,
interleaving calls to the generators until each has produced a line,
then concatenating that line together and outputting it. This is
important because as noted above, some columns may produce an
infinite stream of output, and in general you may want to format
data larger than can fit into memory. Thus columnar would be
suitable for line numbering a file of arbitrary size, or
implementing the Unix
yes(1) command, etc.
columnar except that each column is padded at
least to the minimum width required on any of its lines. Thus
(show #t (tabular "|" (each "a\nbc\ndef\n") "|" (each "123\n45\n6\n") "|"))outputs
|a |123| |bc |45 | |def|6 |This makes it easier to generate tables without knowing widths in advance. However, because it requires generating the entire output in advance to determine the correct column widths,
tabularcannot format a table larger than would fit in memory.
each, except text is accumulated and lines are
wrapped to fit in the current width as in the Unix
command. Specifically, words are tokenized by splitting on all
characters which satisfy the predicate in the parameter
word-separator?, which defaults to
Words are grouped into lines separating them by space, and line
breaks are introduced to minimize the
sum of the cube of trailing whitespace on every line.
Like wrapped, but taking a pre-tokenized list of strings.
wrapped, but splits simply on individual characters exactly
as the current width is reached on each line. Thus there is
nothing to optimize and this formatter doesn't buffer output.
wrapped except the lines are full-justified.
(define func '(define (fold kons knil ls) (let lp ((ls ls) (acc knil)) (if (null? ls) acc (lp (cdr ls) (kons (car ls) acc)))))) (define doc (string-append "The fundamental list iterator. Applies KONS to each " "element of LS and the result of the previous application, " "beginning with KNIL. With KONS as CONS and KNIL as '(), " "equivalent to REVERSE.")) (show #t (columnar (pretty func) " ; " (justified doc)))outputs
(define (fold kons knil ls) ; The fundamental list iterator. (let lp ((ls ls) (acc knil)) ; Applies KONS to each element of (if (null? ls) ; LS and the result of the previous acc ; application, beginning with KNIL. (lp (cdr ls) ; With KONS as CONS and KNIL as '(), (kons (car ls) acc))))) ; equivalent to REVERSE.
Displays the contents of the file pathname one line at a time, so
that in typical formatters such as
columnar only constant
memory is consumed, making this suitable for formatting files of
A convenience utility, just formats an infinite stream of numbers (in the current radix) beginning with start, which defaults to 1.
nl(1) utility could be implemented as:
(show #t (columnar 4 'right 'infinite (line-numbers) " " (from-file "read-line.scm")))which might output:
1 2 (define (read-line . o) 3 (let ((port (if (pair? o) (car o) (current-input-port)))) 4 (let lp ((res '())) 5 (let ((c (read-char port))) 6 (if (or (eof-object? c) (eqv? c #\newline)) 7 (list->string (reverse res)) 8 (lp (cons c res)))))))
(srfi 159 color)library.
Outputs the formatters colored or (boldened or underline) with ANSI escapes, for use when formatting to a terminal.
(srfi 159 unicode)library.
(with ((string-width unicode-terminal-width)) fmt ...)Padding, trimming and tabbing, etc. will generally not do the right thing in the presence of zero-width and double-width Unicode characters. This formatter overrides the string-width state var used in column tracking to do the right thing in such cases, considering Unicode double or full width characters as 2 characters wide (as they typically are in fixed-width terminals), while treating combining and non-spacing characters as 0 characters wide.
;; 3 characters padded to 5 (show #f (with ((pad-char #\〜)) (padded/both 5 "日本語"))) => "〜日本語〜" ;; the 3 characters have a terminal width of 6 so are not padded (show #f (as-unicode (with ((pad-char #\〜)) (padded/both 5 "日本語")))) => "日本語"
A utility function which returns the integer number of columns str would require in a terminal, according to the following rules:
Implementations should support the properties from at least the current Unicode specification at time of writing this SRFI, 10.0.0.
Formatters up to this point have been simple accumulators of output,
with no control flow or handling of state. Both of these are
with for getting and setting
A formatter is essentially an environment monad, although the underlying implementation is unspecified.
fn((id state-var) ...) expr ... fmt)
Short for "function," this is the analog to
lambda. Returns a
formatter which on application evaluates each expr and
fmt in left-to-right order, in a lexical
environment extended with each identifier id bound to the current
value of the state variable named by the symbol state-var. The
result of the fmt is then applied as a formatter.
As a convenience, any
(id state-var) list may be abbreviated
id, indicating id is bound to the state
variable of the same (symbol) name.
(show #f "column: " (fn (col) col)) => "column: 8" (show #f "column: " (fn ((col1 col)) (each col1 ", " (fn ((col2 col)) col2)))) => "column: 8, 11"
The trivial case of no state variables is often useful to allow for lazy applications of formatters, needed for conditional formatting and loops. For example:
(show #t (let lp ((ls ls)) (if (pair? ls) (each (car ls) (lp (cdr ls))) nothing)))would eagerly create a formatter concatenating every element of ls before starting to accumulate any output, whereas
(show #t (let lp ((ls ls)) (if (pair? ls) (each (car ls) (fn () (lp (cdr ls)))) nothing)))would lazily apply the formatters one at a time.
with((state-var value) ...) fmt ...)
Conceptually the formatting equivalent of
temporarily altering state variables. Applies each of the
formatters fmt with each state-var bound to the corresponding
value. The resulting state is then updated to restore each
state-var to its original value.
with!(state-var value) ...)
with but does not restore the original values,
changing the value of each state-var for any remaining formatters
in a sequence.
Calls fmt1 on (a conceptual copy of) the current state, then fmt2 on the same original state as though fmt1 had not been called.
A utility, calls formatter on a copy of the current state (as with
forked), accumulating the results into a string. Then calls
the formatter resulting from
on the original state.
The following state variables have predefined meanings with the formatters in this SRFI.
The textual port output is written to, this can be overridden to capture intermediate output.
The current row of output.
The current column of output, used for padding and spacing, etc.
The current line width, used for wrapping, pretty-printing, and columnar formatting. The default is implementation-defined.
The underlying standard formatter for writing a single string. The default value outputs the string while tracking the current row and col. This can be overridden both to capture intermediate output and perform transformations on strings before outputting, but should generally wrap the existing output to preserve expected behavior.
The mapper for automatic formatting of non-string/char values in
each and other formatters. Defaults to
A function of a single string, it returns the length in columns of that string, used by the default output.
The character used by
tab-to and other padding
(define (print-table-of-contents alist) (define (print-line x) (each (car x) (space-to 72) (padded 3 (cdr x)))) (show #t (with ((pad-char #\.)) (joined/suffix print-line alist nl)))) (print-table-of-contents '(("An Unexpected Party" . 29) ("Roast Mutton" . 60) ("A Short Rest" . 87) ("Over Hill and Under Hill" . 100) ("Riddles in the Dark" . 115)))would output
An Unexpected Party.....................................................29 Roast Mutton............................................................60 A Short Rest............................................................87 Over Hill and Under Hill...............................................100 Riddles in the Dark....................................................115
The string used when truncating as described in
The radix for numeric output, defaulting to 10, as used in
The precision for numeric output, as described in
written. The precision
specifies the number of digits written after the decimal point. If
the numeric value to be written out requires more digits to
represent it than precision, the written representation is chosen
which is closest to the numeric value and representable with the
specified precision. If the numeric value falls on the midpoint of
two such representations, it is implementation dependent which
representation is chosen.
When the numeric value is an inexact floating-point number, there is
more than one interpretation of this "rounding". One is to take
the effective value the floating-point number represents (e.g. if we
use binary floating-point numbers, we take the value of
sign mantissa (expt 2 exponent))), and
compare it to the two closest numeric representations of the given
precision. Another way is to obtain the default notation of the
floating-point number and apply rounding to it. The former (we call
it effective rounding) is consistent with most floating-point number
operations, but may lead to a more non-intuitive result than the latter (we
call it notational rounding). For example, 5.015 can't be represented
exactly in binary floating-point numbers. With IEEE754 floating-point
numbers, the floating point number closest to 5.015 is smaller than
exact 5.015, i.e.
(< 5.015 5015/1000) => #t. With
effective rounding with precision 2, it should result in "5.01".
However, users who look at the notation may be confused by "5.015"
not being rounded up as they usually expect. With notational rounding
the implementation chooses "5.02" (if it also adopts
round-half-to-infinity or round-half-up rule). It is up to the
implementation to choose which interpretation to adopt.
The decimal separator for floating point output, default ".".
Specifies an alignment for the decimal place when formatting numbers, and is useful for outputting tables of numbers.
(define (print-angles x) (joined numeric (list x (sin x) (cos x) (tan x)) " ")) (show #t (with ((decimal-align 5) (precision 3)) (joined/suffix print-angles (iota 5) nl)))would output
0.000 0.000 1.000 0.000 1.000 0.842 0.540 1.557 2.000 0.909 -0.416 -2.185 3.000 0.141 -0.990 -0.142 4.000 -0.757 -0.654 1.158
A character predicate used to tokenize words for
justify. Defaults to
char-whitespace?. More flexibility is
A reference implementation in portable R7RS is available at https://github.com/ashinn/chibi-scheme/blob/master/lib/chibi/show.sld, and included files, depending on SRFI 1, 69, 117, 130.
The reference implementation is an environment monad, with a
pluggable backend allowing either passing explicit state or
maintaining all state variables in parameters. Note
trimmed/lazy rely on first-class continuations, however an
implementation written in CPS-style would not require this.
Alex Shinn, John Cowan, Arthur Gleckler, Revised^7 Report on the Algorithmic Language Scheme https://bitbucket.org/cowan/r7rs/src/draft-10/rnrs/r7rs.pdf
Guy L. Steele Jr., Common Lisp Hyperspec http://www.lispworks.com/documentation/common-lisp.html
Scott G. Miller, SRFI 28 - Basic Format Strings https://srfi.schemers.org/srfi-28/
Ken Dickey, SRFI 48 - Intermediate Format Strings https://srfi.schemers.org/srfi-48/
C++ iomanip http://www.cplusplus.com/reference/iomanip/
Damian Conway, Perl6 Exegesis 7 - formatting https://www.perl.com/pub/2004/02/27/exegesis7.html/
Alex Shinn, fmt - Combinator Formatting http://synthcode.com/scheme/fmt/
Ken Lunde, Unicode® Standard Annex #11 - East Asian Width http://www.unicode.org/reports/tr11/
Copyright (C) Alex Shinn 2017. All Rights Reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.