Liking cljdoc? Tell your friends :D

com.blockether.vis.ext.channel-tui.markdown-layout

TUI layout walker over a transient parsed Markdown tree (com.blockether.vis.internal.render/->ast).

Pure data: IR -> vector of lines. Each line is a vector of styled runs. The screen layer turns runs into ANSI / Lanterna cells.

Why a dedicated walker (and not ir/render :plain):

  • We need styling metadata per character run (bold / italic / inline code background / link target) so the TUI can paint colours and attach click + selection regions.
  • We need word-wrap at an arbitrary terminal width with hanging indent inside lists and quotes.
  • We need stable per-node identifiers so selection / hover / click can map a glyph back to its IR node.

Canonical-IR invariants we rely on (see internal.render docstring):

  • text only inside :span / raw bodies of :code / :c / :kbd;
  • no '\n' inside :span;
  • hard breaks are explicit [:br {}];
  • attrs map present on every vector node.

Run shape: {:text String ; never empty, never contains '\n' :style #{:bold :italic :code :dim :link :heading :marker :quote} :href String? ; present iff :link in style :node any?} ; opaque node identity for click/select

Public API: (ast->lines ir width) ; total walker (ast->lines ir width opts) Opts: :heading-prefix? bool ; unused: H1/H2 get an underline rule and ; H3-H6 a gutter mark (see heading->lines) :code-fence? bool ; render ``` lines around code blocks (default false) :max-lines int ; hard cap (default unlimited)

TUI layout walker over a transient parsed Markdown tree (`com.blockether.vis.internal.render/->ast`).

Pure data: IR -> vector of lines. Each line is a vector of styled
runs. The screen layer turns runs into ANSI / Lanterna cells.

Why a dedicated walker (and not `ir/render :plain`):
- We need styling metadata per character run (bold / italic / inline
  code background / link target) so the TUI can paint colours and
  attach click + selection regions.
- We need word-wrap at an arbitrary terminal width with hanging
  indent inside lists and quotes.
- We need stable per-node identifiers so selection / hover / click
  can map a glyph back to its IR node.

Canonical-IR invariants we rely on (see internal.render docstring):
- text only inside `:span` / raw bodies of `:code` / `:c` / `:kbd`;
- no '\n' inside `:span`;
- hard breaks are explicit `[:br {}]`;
- attrs map present on every vector node.

Run shape:
  {:text   String          ; never empty, never contains '\n'
   :style  #{:bold :italic :code :dim :link :heading :marker :quote}
   :href   String?         ; present iff :link in style
   :node   any?}           ; opaque node identity for click/select

Public API:
  (ast->lines ir width)             ; total walker
  (ast->lines ir width opts)
Opts:
  :heading-prefix? bool   ; unused: H1/H2 get an underline rule and
                          ; H3-H6 a gutter mark (see `heading->lines`)
  :code-fence?     bool   ; render ``` lines around code blocks (default false)
  :max-lines       int    ; hard cap (default unlimited)
raw docstring

ast->entriesclj

(ast->entries ir width)
(ast->entries ir width opts)

Drop-in replacement for the legacy render/markdown->entries. Returns a vector of {:line :meta} maps where :line is the sentinel-prefixed string the bubble painter consumes, and :meta is per-line click-region metadata. nil :meta for ordinary content lines.

:mode selects the marker set: :answer (default) — answer-zone PUA chars (answer-bg paint) :thinking — thinking-zone PUA chars (iter-header-bg + italic) :channel — in-place channel/tool IR; plain paragraphs use no background marker, structural rows keep explicit styling

:justify? full-justifies the prose: every line the wrapper broke on overflow (:wrap?) is stretched flush to both margins, while paragraph- and block-terminal lines stay ragged-right (stretching a four-word last line is the mega-hole bug) and code lines are never touched — their columns ARE the content.

This is the IR-side analogue of markdown->entries. Every bubble rendering path that used to parse the rendered markdown back into entries should call this directly on the canonical IR — no markdown round-trip.

Drop-in replacement for the legacy `render/markdown->entries`.
Returns a vector of `{:line :meta}` maps where `:line` is the
sentinel-prefixed string the bubble painter consumes, and `:meta`
is per-line click-region metadata. `nil` `:meta` for ordinary content
lines.

`:mode` selects the marker set:
  `:answer`   (default) — answer-zone PUA chars (answer-bg paint)
  `:thinking` — thinking-zone PUA chars (iter-header-bg + italic)
  `:channel`  — in-place channel/tool IR; plain paragraphs use no
                background marker, structural rows keep explicit styling

`:justify?` full-justifies the prose: every line the wrapper broke on
overflow (`:wrap?`) is stretched flush to both margins, while paragraph- and
block-terminal lines stay ragged-right (stretching a four-word last line is
the mega-hole bug) and code lines are never touched — their columns ARE the
content.

This is the IR-side analogue of `markdown->entries`. Every
bubble rendering path that used to parse the rendered markdown
back into entries should call this directly on the canonical IR
— no markdown round-trip.
sourceraw docstring

ast->inline-sentinel-stringclj

(ast->inline-sentinel-string ir)

Flatten a canonical IR into a single sentinel-wrapped inline string suitable for chrome-row labels. NO block markers — just inline sentinels (INLINE_BOLD_ON/OFF, INLINE_CODE_ON/OFF, etc.) wrapping styled runs. Hard breaks [:br] become spaces (single line contract). Used by chrome labels and other surfaces that previously ran through markdown->inline.

Flatten a canonical IR into a single sentinel-wrapped inline string
suitable for chrome-row labels. NO block markers — just inline
sentinels (`INLINE_BOLD_ON/OFF`, `INLINE_CODE_ON/OFF`, etc.) wrapping
styled runs. Hard breaks `[:br]` become spaces (single line
contract). Used by chrome labels and other surfaces that previously
ran through `markdown->inline`.
sourceraw docstring

ast->linesclj

(ast->lines input width)
(ast->lines input width opts)

Walk canonical IR (or any input that ir/->ast accepts) at a given terminal width and return a vector of styled lines.

Walk canonical IR (or any input that `ir/->ast` accepts) at a given
terminal `width` and return a vector of styled lines.
sourceraw docstring

ast->lines-tailclj

(ast->lines-tail input width tail-n)
(ast->lines-tail input width tail-n opts)

Render only the last tail-n styled lines of the IR.

Walks the top-level blocks BACKWARD, accumulating a cheap per-block line estimate (text-chars / content-width), until accumulated estimate ≥ tail-n * 2 (slack covers blank-collapse

  • per-block trailing blanks). Then renders ONLY those tail blocks via the normal ast->lines and (take-last tail-n).

Per-frame cost = O(visible-tail), independent of total body length. The semantically-correct path for an auto-scrolled tail-pinned bubble (vs the broken :max-lines cap which would give the FIRST tail-n).

tail-n must be positive. If the body has fewer total estimated lines than the budget, this falls back to a full walk.

Render only the last `tail-n` styled lines of the IR.

Walks the top-level blocks BACKWARD, accumulating a cheap
per-block line estimate (text-chars / content-width), until
accumulated estimate ≥ `tail-n * 2` (slack covers blank-collapse
+ per-block trailing blanks). Then renders ONLY those tail blocks
via the normal `ast->lines` and `(take-last tail-n)`.

Per-frame cost = O(visible-tail), independent of total body
length. The semantically-correct path for an auto-scrolled
tail-pinned bubble (vs the broken `:max-lines` cap which would
give the FIRST tail-n).

`tail-n` must be positive. If the body has fewer total estimated
lines than the budget, this falls back to a full walk.
sourceraw docstring

ast->lines-windowclj

(ast->lines-window input width start num)
(ast->lines-window input width start num opts)

Render only rows [start, start+num) of the IR.

Internally walks blocks forward with :max-lines (start + num + slack) so the walker short-circuits via the A1 blocks->lines path - work is O(start + num), not O(body). Output is bit-identical to (subvec (ast->lines ir w opts) start (+ start num)) when both are in range; result is clamped if the body has fewer lines.

Use cases:

  • start = 0: equivalent to (:max-lines num) head-cap.
  • start > 0: scrolled into a long bubble; only render the window. Note: skipping is still done by the walker producing start lines then discarding them - true zero-cost prefix skip would require per-block estimate-based skipping which this fn does not do.

num must be positive. start must be non-negative.

Render only rows `[start, start+num)` of the IR.

Internally walks blocks forward with `:max-lines (start + num +
slack)` so the walker short-circuits via the A1 `blocks->lines`
path - work is O(start + num), not O(body). Output is
bit-identical to `(subvec (ast->lines ir w opts) start (+ start
num))` when both are in range; result is clamped if the body has
fewer lines.

Use cases:
  - `start = 0`: equivalent to `(:max-lines num)` head-cap.
  - `start > 0`: scrolled into a long bubble; only render the
    window. Note: skipping is still done by the walker producing
    `start` lines then discarding them - true zero-cost prefix
    skip would require per-block estimate-based skipping which
    this fn does not do.

`num` must be positive. `start` must be non-negative.
sourceraw docstring

ast->sentinel-stringsclj

(ast->sentinel-strings ir width)
(ast->sentinel-strings ir width opts)

One-shot helper: canonical IR → vector of sentinel-prefixed strings ready for the existing bubble painter. Composes ast->lines with the sentinel adapter. :mode (:answer, :thinking, or :channel) selects the marker set.

One-shot helper: canonical IR → vector of sentinel-prefixed strings
ready for the existing bubble painter. Composes `ast->lines` with
the sentinel adapter. `:mode` (`:answer`, `:thinking`, or `:channel`)
selects the marker set.
sourceraw docstring

justify-line-runsclj

(justify-line-runs runs width)

Full-justify ONE soft-wrapped walker line's runs to width display columns by widening the inter-word whitespace INSIDE its text runs. The styled twin of primitives/justify-line, run on the IR before the runs are flattened to inline sentinels (so PUA style markers can never be counted as text).

The gap arithmetic is NOT re-implemented here. The line's content text is handed to lanterna's TerminalTextUtils/justifyLine (through p/justify-line) and the widened gaps it hands back are copied onto the runs in order, so the TUI justifies on the SAME grapheme/EAW-aware column engine that measured and wrapped the line — not on a second, subtly different one.

Only CONTENT gaps stretch: leading structural runs — a list :marker (-, , 1.) or the pure-whitespace hanging indent of a continuation row — keep their exact width, else justification blows a hole right after the bullet (- foo). So lanterna is only ever shown the content slice, justified to width MINUS that prefix.

The runs come back untouched when there is nothing to stretch (no gap, or no slack because an over-wide atom was force-fit past width), when the line is not ALREADY near-full (at slack >= gap-count every single gap would grow by at least a column, which is the river, not justification — lanterna itself has no such cap, that policy is ours), and whenever the justified text is not word-for-word the text we sent.

This is the ONE justifier in the TUI: components routes both its plain and its styled rows through it (a plain string is a single run), so no surface keeps a second copy of the gap arithmetic.

Full-justify ONE soft-wrapped walker line's `runs` to `width` display columns
by widening the inter-word whitespace INSIDE its text runs. The styled twin
of `primitives/justify-line`, run on the IR before the runs are flattened to
inline sentinels (so PUA style markers can never be counted as text).

The gap arithmetic is NOT re-implemented here. The line's content text is
handed to lanterna's `TerminalTextUtils/justifyLine` (through
`p/justify-line`) and the widened gaps it hands back are copied onto the runs
in order, so the TUI justifies on the SAME grapheme/EAW-aware column engine
that measured and wrapped the line — not on a second, subtly different one.

Only CONTENT gaps stretch: leading structural runs — a list `:marker`
(`- `, `• `, `1. `) or the pure-whitespace hanging indent of a continuation
row — keep their exact width, else justification blows a hole right after
the bullet (`-      foo`). So lanterna is only ever shown the content slice,
justified to `width` MINUS that prefix.

The runs come back untouched when there is nothing to stretch (no gap, or no
slack because an over-wide atom was force-fit past `width`), when the line is
not ALREADY near-full (at `slack >= gap-count` every single gap would grow by
at least a column, which is the river, not justification — lanterna itself
has no such cap, that policy is ours), and whenever the justified text is not
word-for-word the text we sent.

This is the ONE justifier in the TUI: `components` routes both its plain and
its styled rows through it (a plain string is a single run), so no surface
keeps a second copy of the gap arithmetic.
sourceraw docstring

lines->plainclj

(lines->plain lines)

Concatenate the text of every run in lines. Useful for tests + clipboard fallback (preferred clipboard path: ir/render :markdown).

Concatenate the text of every run in `lines`. Useful for tests +
clipboard fallback (preferred clipboard path: `ir/render :markdown`).
sourceraw docstring

lines->sentinel-stringsclj

(lines->sentinel-strings lines)
(lines->sentinel-strings lines opts)

Convert walker output (vector of {:runs :block-tag :block-level?} maps) into the painter's sentinel-prefixed string contract. Each line: <block-marker><inline-sentinel-wrapped body>. :mode selects the marker set (:answer default, :thinking, or :channel).

Convert walker output (vector of `{:runs :block-tag :block-level?}`
maps) into the painter's sentinel-prefixed string contract. Each
line: `<block-marker><inline-sentinel-wrapped body>`. `:mode`
selects the marker set (`:answer` default, `:thinking`, or `:channel`).
sourceraw docstring

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