clj-string-layout turns rows of strings into aligned text — Markdown
tables, box-drawing terminal output, CSV, HTML, psql, Org mode, and
anything you can build out of the underlying layout DSL.
Describe the data once:
(require '[clj-string-layout.table :as table])
(def items
[{:item "apple" :qty 12 :price 1.50}
{:item "pear" :qty 4 :price 2.00}
{:item "kiwi" :qty 8 :price 0.75}])
(def cols
[:item
{:from :qty :as "Qty" :align :right}
{:from :price :as "Price" :align :right
:formatter #(format "$%.2f" %)}])
Render it any way you need.
(table/table-str {:format :box :columns cols :rows items})
┌───────┬─────┬───────┐
│ item │ Qty │ Price │
├───────┼─────┼───────┤
│ apple │ 12 │ $1.50 │
├───────┼─────┼───────┤
│ pear │ 4 │ $2.00 │
├───────┼─────┼───────┤
│ kiwi │ 8 │ $0.75 │
└───────┴─────┴───────┘
(table/table-str {:format :markdown :columns cols :rows items})
| item | Qty | Price |
|:----- | ---:| -----:|
| apple | 12 | $1.50 |
| pear | 4 | $2.00 |
| kiwi | 8 | $0.75 |
(table/table-str {:format :html :columns cols :rows items})
<table>
<tr><th>item</th><th>Qty</th><th>Price</th></tr>
<tr><td>apple</td><td>12</td><td>$1.50</td></tr>
<tr><td>pear</td><td>4</td><td>$2.00</td></tr>
<tr><td>kiwi</td><td>8</td><td>$0.75</td></tr>
</table>
(table/table-str {:format :csv :columns cols :rows items})
item,Qty,Price
apple,12,$1.50
pear,4,$2.00
kiwi,8,$0.75
The same cols and items also render as :double-box, :ascii-grid,
:psql, :org, :rst, :tsv, :pipe, :plain, and the three
alignment-specific :markdown-* variants. The
examples gallery shows every named format
side by side with the same data.
;; deps.edn
{:deps {io.github.mbjarland/clj-string-layout {:mvn/version "2.1.1"}}}
No third-party Clojure runtime dependencies — a Babashka script can require it directly:
#!/usr/bin/env bb
(require '[babashka.deps :as deps])
(deps/add-deps '{:deps {io.github.mbjarland/clj-string-layout
{:mvn/version "2.1.1"}}})
(require '[clj-string-layout.table :as table])
(println (table/table-str {:format :box
:headers ["Name" "Qty"]
:rows [["apple" 12] ["pear" 4]]}))
Tested on Java 11, 17, and 21.
The example above is the high-level table API. Underneath it sits a small layout DSL — the engine every named format is built on, and the thing you reach for when you want text to line up in ways the table API doesn't quite cover (custom borders, ANSI dashboards, log lines with aligned timestamps, key/value pairs, anything).
┌─ HIGH ────────────────────────────────────────────────────────────────┐
│ clj-string-layout.table │
│ (table/table {:format :box :columns [...] :rows [...]}) │
│ → Most callers start (and stop) here. │
├───────────────────────────────────────────────────────────────────────┤
│ clj-string-layout.core/layout │
│ (layout rows {:layout {:cols ["[L] [R]"]}}) │
│ → For anything the table API doesn't reach. │
│ Pre-canned configs in clj-string-layout.presets. │
└─ LOW ─────────────────────────────────────────────────────────────────┘
Deeper docs: table API · examples gallery · layout language · recipes · preset catalog · CLI · errors.
Four moving parts.
A vector of maps:
:rows [{:item "apple" :qty 12} {:item "pear" :qty 4}]
…or a vector of positional vectors:
:rows [["apple" 12] ["pear" 4]]
Two shapes:
:qty ;; defaults
{:from :qty :as "Qty" :align :right ;; explicit
:formatter #(format "%s units" %)
:width 10 :overflow :ellipsis}
Map keys:
| Key | Means |
|---|---|
:from | Row map key. Omit for vector rows — the column's source is its position. |
:as | Header label. Defaults to the keyword name of :from. |
:align | :left, :center, :right, or :verbatim. |
:formatter | One-arg function applied to the cell value before rendering. |
:width | Maximum cell width. |
:overflow | :none, :clip, :ellipsis, :wrap, :error. |
A keyword. One of:
:plain · :markdown · :markdown-left · :markdown-center ·
:markdown-right · :box · :double-box · :ascii-grid · :csv ·
:tsv · :pipe · :psql · :org · :rst · :html.
| Key | Adds |
|---|---|
:title | Centered caption above text formats; <caption> for HTML. |
:footers | Trailing rows that share column treatment. |
:cell-fn | Per-cell decoration callback — pair with :display-width for ANSI styling. |
:width + :fill? | Expand the rendered table toward a target width. |
:raw? | Return vectors of pieces instead of joined strings. |
See the table API guide for the full surface.
When the named formats don't reach, drop down to the layout DSL. The
column layout is a string made of column markers ([L] [C] [R]
[V]), fill regions (f), and optional repeat groups ({…}):
(require '[clj-string-layout.core :refer [layout]])
(layout [["left" "right"]]
{:width 30
:fill-char \.
:layout {:cols ["[L]f[R]"]}})
;; => ["left.....................right"]
The string above has two column markers ([L] and [R]) and one fill
region (f) between them — extra width is absorbed by the fill so the
right-hand value sits flush against the target width. The DSL also
handles virtual row layouts (top/middle/bottom rules), repeat groups
for variable column counts, raw-piece output for cell decoration, and
streaming via layout-seq. See the
layout language reference for the grammar
and the recipe book for paste-able examples.
clojure -M:cli -- --input csv --format markdown --headers data.csv
bb bb-format --input tsv --format box < data.tsv
bb bb-format runs natively under Babashka in ~50 ms instead of the
JVM path's ~700 ms. See the CLI guide for the full
option list and the programmatic cli/render entry point.
Local checks, code expectations, and the release process live in CONTRIBUTING.md. Documentation is rebuilt on every release at cljdoc.org.
Copyright © 2017-2026 Matias Bjarland · Eclipse Public License 1.0
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |