Liking cljdoc? Tell your friends :D

clj-string-layout

CI Status Release Clojars License

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.

Installation

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.

Quick Start

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 Config

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:

KeyDefaultMeaning
:layoutrequiredA map with :cols and optional :rows.
:width80Target width when the layout contains fill markers. Layouts can still exceed this if the data and literals are wider.
:align-charspaceCharacter used to pad aligned data cells.
:fill-charspaceCharacter used for f fill markers unless overridden by a row layout.
:word-split-charspaceCharacter used to split string input into words.
:row-split-charnewlineCharacter used to split string input into rows.
:display-widthcountFunction from string to display width. Override this for terminal-width-aware alignment of wide glyphs.
:col-widthsnilOptional explicit column display widths. Useful for fixed schemas and streaming large data sets.
:row-countnilOptional data row count for lazy output with row layouts.
:raw?falseReturn 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.

The Layout Language

Layout strings are made from four pieces:

PieceExampleMeaning
Literal delimiter text" | ", "</td>", "┌"Text emitted exactly as part of the output.
Column marker[L], [C], [R], [V]Placeholder for a data column.
Fill markerf or FExpands 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

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:

MarkerMeaning
[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

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:

PredicateMatches
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

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:

PredicateMatches
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

Built-in layouts are available in clj-string-layout.layout.

Box-drawing layouts:

VarAlignmentFill-aware
layout-ascii-box-leftLeftNo
layout-ascii-box-centerCenterNo
layout-ascii-box-rightRightNo
layout-ascii-box-fill-leftLeftYes
layout-ascii-box-fill-centerCenterYes
layout-ascii-box-fill-rightRightYes

Norton Commander-style layouts:

VarAlignmentFill-aware
layout-norton-commander-leftLeftNo
layout-norton-commander-centerCenterNo
layout-norton-commander-rightRightNo
layout-norton-commander-fill-leftLeftYes
layout-norton-commander-fill-centerCenterYes
layout-norton-commander-fill-rightRightYes

Markdown layouts:

VarAlignmentFill-aware
layout-markdown-leftLeftNo
layout-markdown-centerCenterNo
layout-markdown-rightRightNo
layout-markdown-fill-leftLeftYes
layout-markdown-fill-centerCenterYes
layout-markdown-fill-rightRightYes

HTML layouts:

VarBehavior
layout-html-tableEmits <table>, one <tr> per input row, and verbatim <td> contents.
layout-html-table-readableSame 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>&lt;Alice&gt;</td><td>tea &amp; cake</td></tr>"
;;     "</table>"]

(layout (escape/map-cells escape/markdown-cell [["name" "a|b"]])
        layouts/layout-markdown-left)
;; => ["| name | a\\|b |"
;;     "|:---- |:----- |"]

Raw Output

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.

Large Data Sets

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?]]}})

Convenience And Diagnostics

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, ...}}

Development

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

Release Process

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:

SecretMeaning
CLOJARS_USERNAMEClojars account name used for deployment.
CLOJARS_PASSWORDClojars 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.

Design Notes

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.

License

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

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close