clj-string-layout is a small Clojure library for turning rows of strings into aligned text layouts: simple columns, box-drawing tables, Markdown tables, HTML table snippets, and custom formats defined with a compact layout language.
The core idea is that column layouts describe how each data cell is aligned, while row layouts describe virtual rows inserted around or between the data rows. Repeating layout groups make the same layout work for any number of columns.
For more copy-and-paste examples, see the recipe book.
Add the library to deps.edn:
{:deps {io.github.mbjarland/clj-string-layout {:mvn/version "1.0.4"}}}
Versions before 1.0.4 used the older com.github.mbjarland Maven group.
Use io.github.mbjarland for new installations.
Require the namespaces you need:
(require '[clj-string-layout.core :refer [layout layout-seq]]
'[clj-string-layout.escape :as escape]
'[clj-string-layout.layout :as layouts]
'[clj-string-layout.predicates :as pred])
The library is tested on Java 11, 17, and 21. Java 11 is the intended minimum runtime.
Input can be a string split into rows and words:
(def data (str "Alice, why is\n"
"a raven like\n"
"a writing desk?"))
(layout data {:layout {:cols ["[L] [L] [L]"]}})
;; => ["Alice, why is "
;; "a raven like "
;; "a writing desk?"]
Input can also be a vector of rows when you want exact control over cell boundaries:
(layout [["Alice," "why" "is"]
["a" "raven" "like"]
["a" "writing" "desk?"]]
{:layout {:cols ["[L] [L] [L]"]}})
Use a built-in layout for common output formats:
(layout data layouts/layout-ascii-box-center)
;; => ["┌────────┬─────────┬───────┐"
;; "│ Alice, │ why │ is │"
;; "├────────┼─────────┼───────┤"
;; "│ a │ raven │ like │"
;; "├────────┼─────────┼───────┤"
;; "│ a │ writing │ desk? │"
;; "└────────┴─────────┴───────┘"]
layout takes rows and a layout config map.
{:align-char \space
:fill-char \space
:word-split-char \space
:row-split-char \newline
:display-width count
:col-widths nil
:row-count nil
:width 80
:raw? false
:layout {:cols ["[L] [C] [R]"]
:rows [["|[-]|[-]|[-]|" :apply-for layouts/first-row?]]}}
Options:
| Key | Default | Meaning |
|---|---|---|
:layout | required | A map with :cols and optional :rows. |
:width | 80 | Target width when the layout contains fill markers. Layouts can still exceed this if the data and literals are wider. |
:align-char | space | Character used to pad aligned data cells. |
:fill-char | space | Character used for f fill markers unless overridden by a row layout. |
:word-split-char | space | Character used to split string input into words. |
:row-split-char | newline | Character used to split string input into rows. |
:display-width | count | Function from string to display width. Override this for terminal-width-aware alignment of wide glyphs. |
:col-widths | nil | Optional explicit column display widths. Useful for fixed schemas and streaming large data sets. |
:row-count | nil | Optional data row count for lazy output with row layouts. |
:raw? | false | Return each output row as a vector of pieces instead of joined strings. Useful when post-processing cells, for example adding ANSI colors. |
By default, widths are measured with Clojure's count, preserving plain string
length behavior. For monospace terminal output containing wide glyphs, pass a
:display-width function that returns a non-negative integer for each string.
The function is used for cell values, literal delimiters, padding, and fill
width calculations. Alignment and fill characters should occupy one display
column.
Use layout-seq with :col-widths for large data sets when the schema widths
are known ahead of time. Without explicit widths, exact alignment still needs to
scan all rows before the first output row can be rendered. If the layout inserts
virtual rows, pass :row-count as well so row predicates can identify the last
virtual row without counting the input. Use escape/map-cell-seq instead of
escape/map-cells when escaping a large lazy input.
Layout strings are made from four pieces:
| Piece | Example | Meaning |
|---|---|---|
| Literal delimiter text | " | ", "</td>", "┌" | Text emitted exactly as part of the output. |
| Column marker | [L], [C], [R], [V] | Placeholder for a data column. |
| Fill marker | f or F | Expands to absorb remaining width. |
| Repeat group | { [L] |} | Repeats a sub-layout for a variable number of columns. |
| Escaped literal | \f, \{, \] | Emits the following character literally. |
The current grammar is:
layout = delim? ((col | repeat) delim?)*
repeat = <'{'> delim? (col delim?)* <'}'>
delim = (escaped | fill | #'[^\\\\\\[\\]{}fF]+')+
escaped = <'\\\\'> #'.'
fill = <'F'> (#'[\\d]+')?
col = <'['> fill? align fill? <']'>
Column layouts and row layouts share this structure, but they interpret align differently.
Use a backslash when delimiter text needs a reserved character literally:
(layout "a" {:layout {:cols ["\\f[L]\\F"]}})
;; => ["faF"]
Column layouts live at [:layout :cols]. The value is a vector whose first item is the layout string. Additional key/value pairs configure repeating groups.
{:layout {:cols ["[L] [C] [R]"]}}
Supported column alignments:
| Marker | Meaning |
|---|---|
[L] | Left-align the cell within the column width. |
[C] | Center-align the cell within the column width. |
[R] | Right-align the cell within the column width. |
[V] | Verbatim output. Do not pad the cell to the computed column width. |
Examples:
(layout "name price\napple 12\npear 4"
{:layout {:cols ["[L] [R]"]}})
;; => ["name price"
;; "apple 12"
;; "pear 4"]
Use f when extra width should be distributed into the layout instead of ignored:
(layout "left right"
{:width 20
:layout {:cols ["[L]f[R]"]}})
;; => ["left right"]
Fill markers may appear in delimiters or inside column brackets:
"[L]f[R]" ;; all extra width between two columns
"f[R] [L]f" ;; split extra width before and after the row
"[Lf] [Rf]" ;; expand the column padding itself
When multiple fill markers are present, remaining width is distributed across them as evenly as possible. Any remainder is assigned from left to right according to the existing fill algorithm.
Repeat groups make layouts adapt to the number of input columns. They are wrapped in {...}.
{:layout {:cols ["|{ [L] |}" :repeat-for [pred/all-cols?]]}}
For three columns, the repeating section is expanded three times:
| [L] | [L] | [L] |
Repeat groups are useful for table-like formats where the same cell pattern should be reused for every column:
(layout "a b c\n1 2 3"
{:layout {:cols ["|{ [C] |}" :repeat-for [pred/all-cols?]]}})
;; => ["| a | b | c |"
;; "| 1 | 2 | 3 |"]
The :repeat-for value controls which columns a repeat group handles. The predicate receives [idx last-idx]. The older :apply-for key is still accepted on column layouts for compatibility, but :repeat-for is clearer because row layouts also use :apply-for for virtual row predicates.
Column predicates are supplied by clj-string-layout.predicates and are also re-exported by clj-string-layout.layout for compatibility:
| Predicate | Matches |
|---|---|
first-col? | First column. |
second-col? | Second column. |
last-col? | Last column. |
not-first-col? | Every column except the first. |
not-last-col? | Every column except the last. |
interior-col? | Columns that are neither first nor last. |
not-interior-col? | First or last column. |
all-cols? | Every column. |
You can also pass your own predicate:
(defn even-col? [[idx _]] (even? idx))
Row layouts live at [:layout :rows]. They insert virtual rows before, between, or after data rows.
{:layout {:cols ["│{ [L] │}" :repeat-for [pred/all-cols?]]
:rows [["┌{─[─]─┬}─[─]─┐" :apply-for layouts/first-row?]
["├{─[─]─┼}─[─]─┤" :apply-for layouts/interior-row?]
["└{─[─]─┴}─[─]─┘" :apply-for layouts/last-row?]]}}
Row layout column markers use the character inside brackets as a drawing character, not a cell alignment. For example, [─] emits enough ─ characters to match the corresponding data column width. [=f] uses = and can include fill expansion.
Row predicates receive [idx last-idx], where the indexes refer to virtual row positions. With three data rows, the virtual row positions are 0, 1, 2, and 3. 0 is before the first data row, 3 is after the last data row, and the interior positions are between data rows.
Row predicates are supplied by clj-string-layout.predicates and are also re-exported by clj-string-layout.layout for compatibility:
| Predicate | Matches |
|---|---|
first-row? | Virtual row before the first data row. |
second-row? | Virtual row after the first data row. Useful for Markdown header separators. |
last-row? | Virtual row after the last data row. |
not-first-row? | Every virtual row except the first. |
not-last-row? | Every virtual row except the last. |
interior-row? | Virtual rows between data rows. |
not-interior-row? | First or last virtual row. |
all-rows? | Every virtual row. |
Example Markdown table:
(layout (str "name qty\n"
"apple 12\n"
"pear 4")
layouts/layout-markdown-left)
;; => ["| name | qty |"
;; "|:----- |:--- |"
;; "| apple | 12 |"
;; "| pear | 4 |"]
Built-in layouts are available in clj-string-layout.layout.
Box-drawing layouts:
| Var | Alignment | Fill-aware |
|---|---|---|
layout-ascii-box-left | Left | No |
layout-ascii-box-center | Center | No |
layout-ascii-box-right | Right | No |
layout-ascii-box-fill-left | Left | Yes |
layout-ascii-box-fill-center | Center | Yes |
layout-ascii-box-fill-right | Right | Yes |
Norton Commander-style layouts:
| Var | Alignment | Fill-aware |
|---|---|---|
layout-norton-commander-left | Left | No |
layout-norton-commander-center | Center | No |
layout-norton-commander-right | Right | No |
layout-norton-commander-fill-left | Left | Yes |
layout-norton-commander-fill-center | Center | Yes |
layout-norton-commander-fill-right | Right | Yes |
Markdown layouts:
| Var | Alignment | Fill-aware |
|---|---|---|
layout-markdown-left | Left | No |
layout-markdown-center | Center | No |
layout-markdown-right | Right | No |
layout-markdown-fill-left | Left | Yes |
layout-markdown-fill-center | Center | Yes |
layout-markdown-fill-right | Right | Yes |
HTML layouts:
| Var | Behavior |
|---|---|
layout-html-table | Emits <table>, one <tr> per input row, and verbatim <td> contents. |
layout-html-table-readable | Same shape, but left-aligns cell contents for more readable source output. |
HTML example:
(layout "Alice why\na raven" layouts/layout-html-table)
;; => ["<table>"
;; " <tr><td>Alice</td><td>why</td></tr>"
;; " <tr><td>a</td><td>raven</td></tr>"
;; "</table>"]
HTML and Markdown presets emit cell contents verbatim by default. Escape input cells before rendering when the data is not already safe for the target format:
(layout (escape/map-cells escape/html [["<Alice>" "tea & cake"]])
layouts/layout-html-table)
;; => ["<table>"
;; " <tr><td><Alice></td><td>tea & cake</td></tr>"
;; "</table>"]
(layout (escape/map-cells escape/markdown-cell [["name" "a|b"]])
layouts/layout-markdown-left)
;; => ["| name | a\\|b |"
;; "|:---- |:----- |"]
Set :raw? true if you need the pieces before they are joined:
(layout "a b" {:raw? true
:layout {:cols ["| [L] | [R] |"]}})
;; => [["| " "a" " | " "b" " |"]]
This is useful when a later step needs to decorate specific cells without re-parsing the final string.
Automatic column widths require all rows to be inspected before output can be
rendered. For very large data sets with known schema widths, use layout-seq
and provide :col-widths to render rows lazily:
(def rows (map vector ["a" "bb" "ccc"]))
(take 2 (layout-seq rows {:col-widths [3]
:layout {:cols ["[L]"]}}))
;; => ("a " "bb ")
If the layout has virtual rows, pass :row-count so predicates such as
last-row? work without counting the input first:
(layout-seq rows {:col-widths [3]
:row-count 3
:layout {:cols ["[L]"]
:rows [["[-]" :apply-for layouts/all-rows?]]}})
Use layout-str when you want a single newline-delimited string:
(layout-str "a b\naa bb" {:layout {:cols ["[L] [R]"]}})
;; => "a b\naa bb"
Use parse-layout or explain-layout while developing custom layout strings:
(parse-layout "[L]f[R]")
;; => [{:type :column, :align :l, ...} {:type :fill} ...]
(explain-layout "[x]")
;; => {:valid? false, :message "...", :data {:type :layout-parse-error, ...}}
Run the test suite:
clojure -M:test
Run the linter:
clojure -M:lint
Build the jar:
clojure -T:build jar
Install locally:
clojure -T:build install
Deploy to Clojars after setting Clojars credentials for deps-deploy:
clojure -T:deploy deploy
Releases are published by GitHub Actions when a version tag is pushed. The tag
must be prefixed with v and must match version.edn. For example,
version.edn containing {:version "1.0.4"} must be released with tag
v1.0.4.
Required repository secrets:
| Secret | Meaning |
|---|---|
CLOJARS_USERNAME | Clojars account name used for deployment. |
CLOJARS_PASSWORD | Clojars deploy token or password used by deps-deploy. |
Release steps:
git tag -a v1.0.4 -m "Release v1.0.4"
git push origin v1.0.4
The release workflow then runs linting, tests, and jar builds on Java 11, 17, and 21. After verification passes, it rebuilds the jar on Java 11, deploys to Clojars, and creates a GitHub Release with the jar attached.
The library intentionally keeps the public API small. Most users need only clj-string-layout.core/layout, reusable predicates in clj-string-layout.predicates, and preset layouts in clj-string-layout.layout.
The f and F characters are reserved as fill markers in layout delimiter positions. Use escaped literals such as \f or \F when delimiter text needs those characters literally.
Copyright © 2017-2026 Matias Bjarland
Distributed under the 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 |