The clj-string-layout.table namespace is the easiest entry point for common
tables. It lets you choose a named format, pass headers and rows, and describe
columns without writing layout DSL strings.
(require '[clj-string-layout.table :as table])
(table/table {:format :markdown
:headers ["Name" "Qty"]
:rows [["apple" 12]
["pear" 4]]})
;; => ["| Name | Qty |"
;; "|:----- |:--- |"
;; "| apple | 12 |"
;; "| pear | 4 |"]
Use table-str when the consumer wants one string:
(table/table-str {:format :plain
:headers ["Name" "Qty"]
:rows [["apple" 12]]})
;; => "Name Qty\napple 12 "
Available formats are discoverable at runtime:
(table/formats)
;; => #{:plain :markdown :markdown-left :markdown-center :markdown-right
;; :box :double-box :unicode-box :unicode-double-box
;; :ascii-box :ascii-double-box :ascii-grid
;; :csv :tsv :pipe :psql :org :rst :html}
| Format | Output |
|---|---|
:plain | Whitespace-separated aligned columns. |
:markdown | Markdown pipe table. |
:markdown-left | Markdown pipe table with left-aligned columns. |
:markdown-center | Markdown pipe table with center-aligned columns. |
:markdown-right | Markdown pipe table with right-aligned columns. |
:box | Unicode box-drawing table with ┌─┬─┐ borders. Aliases: :unicode-box, :ascii-box. |
:double-box | Unicode box-drawing table with ╔═╦═╗ borders. Aliases: :unicode-double-box, :ascii-double-box. |
:ascii-grid | ASCII +---+ table. |
:csv | Comma-separated values, CSV-escaped by default. |
:tsv | Tab-separated values. |
:pipe | Compact pipe-separated values. |
:psql | PostgreSQL psql-style terminal table. |
:org | Org mode table. |
:rst | reStructuredText simple table. |
:html | HTML table with optional header row. |
The Markdown formats only emit the |:---| rule row when a header is present
(via :headers or :columns). Headerless data renders as plain pipe rows.
Column specs let you render maps directly and control titles, alignment, formatting, widths, and overflow.
(table/table {:format :markdown
:columns [{:key :name :title "Name"}
{:key :qty :title "Qty" :align :right}]
:rows [{:name "apple" :qty 12}
{:name "pear" :qty 4}]})
;; => ["| Name | Qty |"
;; "|:----- | ---:|"
;; "| apple | 12 |"
;; "| pear | 4 |"]
Column keys for vector rows can be numeric indexes:
{:columns [{:key 0 :title "Name"}
{:key 1 :title "Qty" :align :right}]}
Use :format on a column to transform values before rendering:
(table/table {:format :plain
:columns [{:key :name :title "Name"}
{:key :price :title "Price"
:align :right
:format #(format "$%.2f" (double %))}]
:rows [{:name "apple" :price 1.5}]})
;; => ["Name Price"
;; "apple $1.50"]
Use :width with :overflow to constrain cell text before layout.
| Policy | Behavior |
|---|---|
:none | Default. Leave long values unchanged. |
:clip | Cut values at :width. |
:ellipsis | Cut values and append ... where possible. |
:wrap | Split values into multiple physical table rows. |
:error | Throw ex-info when a value exceeds :width. |
(table/table {:format :plain
:columns [{:key 0 :title "Text" :width 4
:overflow :ellipsis}]
:rows [["abcdef"]]})
;; => ["Text"
;; "a..."]
Wrapping creates additional rows:
(table/table {:format :plain
:columns [{:key 0 :title "Txt" :width 3
:overflow :wrap}]
:rows [["abcdef"]]})
;; => ["Txt"
;; "abc"
;; "def"]
The table API escapes by default for formats where raw values commonly break the syntax:
| Format | Escaper |
|---|---|
:markdown | escape/markdown-cell |
:csv | escape/csv-cell |
:tsv | escape/tsv-cell |
:org | escape/org-cell |
:rst | escape/rst-cell |
:html | escape/html |
Set :escape? false if you already escaped values yourself.
(table/table {:format :html
:headers ["<Name>"]
:rows [["a&b"]]})
;; => ["<table>"
;; " <tr><th><Name></th></tr>"
;; " <tr><td>a&b</td></tr>"
;; "</table>"]
Pass :cell-fn to wrap each cell value before the layout engine pads it.
The callback receives a context map and must return a string:
| Context key | Meaning |
|---|---|
:section | One of :header, :data, :footer. |
:row | Zero-based row index within the section. |
:col | Column index. |
:column | The column spec map. |
:value | The post-format/escape value, ready to wrap. |
(table/table {:format :markdown
:headers ["Name" "Qty"]
:rows [["apple" "12"]]
:cell-fn (fn [{:keys [section value]}]
(if (= :header section)
(str "**" value "**")
value))})
;; => ["| **Name** | **Qty** |"
;; "|:-------- |:------- |"
;; "| apple | 12 |"]
When the callback adds non-printing characters such as ANSI color codes,
also pass :display-width clj-string-layout.width/ansi-width so the layout
engine pads using the original visible width rather than the byte length of
the wrapped value.
Pass :footers (a vector of rows in the same shape as :rows) to render
trailing rows below the data. Footers are useful for totals, summaries, or
any row that should be styled the same as data but always appear last.
(table/table {:format :box
:headers ["Item" "Qty"]
:rows [["apple" 12] ["pear" 4]]
:footers [["Total" 16]]})
;; ┌───────┬─────┐
;; │ Item │ Qty │
;; ├───────┼─────┤
;; │ apple │ 12 │
;; ├───────┼─────┤
;; │ pear │ 4 │
;; ├───────┼─────┤
;; │ Total │ 16 │
;; └───────┴─────┘
Box-drawing formats automatically separate footers from the data with the
same interior rule used between data rows. For :html, footers are emitted
as <tr><td> rows after the data; wrap them in <tfoot> post-render if you
need the structural element.
Pass :title to render a caption above the table. For text formats the title
is centered to the rendered width and emitted as a single line; for :html
it becomes a <caption> element inside the <table>.
(table/table {:format :box
:title "Inventory"
:headers ["Name" "Qty"]
:rows [["apple" "12"]]})
;; =>
;; Inventory
;; ┌───────┬─────┐
;; │ Name │ Qty │
;; ├───────┼─────┤
;; │ apple │ 12 │
;; └───────┴─────┘
The title is escaped with the same per-format escaper unless :escape? false
is set on the spec.
:width and :display-width are forwarded to the layout engine for every
format that emits visually padded text (plain, markdown, the box variants,
ascii-grid, psql, org, and rst). They are intentionally ignored for :html,
since HTML output is structural markup rather than padded text.
Pass :fill? true together with :width to make the generated formats
expand their column padding toward that width:
(table/table {:format :box
:fill? true
:width 25
:headers ["Name" "Qty"]
:rows [["apple" "12"]]})
;; ┌────────────┬──────────┐
;; │ Name │ Qty │
;; ├────────────┼──────────┤
;; │ apple │ 12 │
;; └────────────┴──────────┘
:raw? is honored for every format. For visually padded formats it returns
each row as a vector of pieces ready for cell decoration; for :html each
output line becomes a single-piece vector so callers can wrap or annotate
specific lines without re-parsing the rendered string.
Use the high-level table API when you need common output formats quickly. Use
clj-string-layout.core/layout directly when you need custom borders, multiple
repeat groups, custom row predicates, or a format not represented by the table
registry.
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 |