Liking cljdoc? Tell your friends :D

com.blockether.vis.internal.render

Vis answer IR — Hiccup-EDN, MDAST-equivalent, with strict canonical form post-->ast.

Public surface: (->ast input) ; soft-normalize any input → [:ir & blocks] (render input flavor opts) ; one of :html :markdown :plain (extract-code ast) ; for vis --code (extract-text ast) ; for voice TTS (session->markdown db session-ref opts?)

Tags: ROOT :ir BLOCKS (11) :p :h{:level 1-6} :code{:lang} :ul :ol{:start} :li :quote :table :tr :th :td INLINES (11) :span{:preserve-ws? :nowrap?} :br :strong :em :c :a{:href} :img{:src :alt} :kbd :mark :sup :sub

Disclosure/collapsible answer blocks are intentionally unsupported: no :details, no :summary, no HTML <details>/<summary> in answer IR.

─── Canonical form (invariant after ->ast) ───────────────────────────────

  1. Every vector node has its attrs map at index 1 ({} when absent).
  2. :ir children are exclusively block nodes.
  3. Text lives ONLY in:
    • :span body (single string, no '\n').
    • raw bodies of :code, :c, :kbd (single string, ws preserved). Anywhere else, vector children only — no bare strings in the tree.
  4. Hard line breaks are explicit [:br {}] nodes.
  5. Soft breaks (any '\n' inside a non-preserve-ws string) are collapsed to a single space during canonicalization. This is the structural fix for LLM output that emits cosmetic mid-paragraph indentation (e.g. " \n continuation").
  6. :li children are either all blocks OR a single :p wrapping the inline run.
  7. :ul/:ol children are exclusively :li.
  8. :table children are :tr; :tr children are :th/:td; :th/:td children are inline.

─── Coercion rules at the boundary ─────────────────────────────────────────

->ast is total and pure. Accepted inputs: [:ir ...] — re-canonicalized (idempotent) [:tag ...] (Hiccup, non-:ir) — wrapped in [:ir <node>] "text" — wrapped in [:ir [:p [:span text]]] sequential / vector of mixed — element-by-element coercion anything else — surfaced as [:code {:lang "edn"} pr-str]

Vis answer IR — Hiccup-EDN, MDAST-equivalent, with strict canonical
form post-`->ast`.

Public surface:
  (->ast input)                 ; soft-normalize any input → [:ir & blocks]
  (render input flavor opts)    ; one of :html :markdown :plain
  (extract-code ast)            ; for vis --code
  (extract-text ast)            ; for voice TTS
  (session->markdown db session-ref opts?)

Tags:
  ROOT             :ir
  BLOCKS    (11)   :p :h{:level 1-6} :code{:lang} :ul :ol{:start} :li
                   :quote :table :tr :th :td
  INLINES   (11)   :span{:preserve-ws? :nowrap?} :br
                   :strong :em :c :a{:href}
                   :img{:src :alt} :kbd :mark :sup :sub

Disclosure/collapsible answer blocks are intentionally unsupported:
no `:details`, no `:summary`, no HTML `<details>/<summary>` in answer IR.

─── Canonical form (invariant after `->ast`) ───────────────────────────────

1. Every vector node has its attrs map at index 1 ({} when absent).
2. `:ir` children are exclusively block nodes.
3. Text lives ONLY in:
     - `:span` body (single string, no '\n').
     - raw bodies of `:code`, `:c`, `:kbd` (single string, ws preserved).
   Anywhere else, vector children only — no bare strings in the tree.
4. Hard line breaks are explicit `[:br {}]` nodes.
5. Soft breaks (any '\n' inside a non-preserve-ws string) are collapsed
   to a single space during canonicalization. This is the structural fix
   for LLM output that emits cosmetic mid-paragraph indentation
   (e.g. `" \n   continuation"`).
6. `:li` children are either all blocks OR a single `:p` wrapping the
   inline run.
7. `:ul`/`:ol` children are exclusively `:li`.
8. `:table` children are `:tr`; `:tr` children are `:th`/`:td`;
   `:th`/`:td` children are inline.

─── Coercion rules at the boundary ─────────────────────────────────────────

`->ast` is total and pure. Accepted inputs:
  [:ir ...]                    — re-canonicalized (idempotent)
  [:tag ...] (Hiccup, non-:ir) — wrapped in [:ir <node>]
  "text"                       — wrapped in [:ir [:p [:span text]]]
  sequential / vector of mixed — element-by-element coercion
  anything else                — surfaced as [:code {:lang "edn"} pr-str]
raw docstring

*soft-break*clj

How a CommonMark SoftLineBreak (a bare between two non-blank lines) is lifted into IR. CommonMark prose semantics collapse a soft break to a single space, which is correct for model-authored answers and thinking. User-typed / pasted input is line-oriented (an input box, not a prose document): a literal newline is intent, so we lift it to [:br] to preserve the pasted shape (code, line-numbered dumps, tables). Bound to :hard via (markdown->ir text {:soft-break :hard}). Default :space keeps every existing caller unchanged.

How a CommonMark SoftLineBreak (a bare `
` between two non-blank
   lines) is lifted into IR. CommonMark prose semantics collapse a soft
   break to a single space, which is correct for model-authored answers
   and thinking. User-typed / pasted input is line-oriented (an input
   box, not a prose document): a literal newline is intent, so we lift
   it to `[:br]` to preserve the pasted shape (code, line-numbered
   dumps, tables). Bound to `:hard` via `(markdown->ir text {:soft-break
   :hard})`. Default `:space` keeps every existing caller unchanged.
sourceraw docstring

->astclj

(->ast v)

Soft-normalize any answer-input value into canonical [:ir & blocks]. Pure, total, idempotent.

Identity-preserving: when the input already satisfies the canonical invariants (canonical?), the return value is the SAME object. This keeps downstream System/identityHashCode caches (format-answer-with-thinking-data, etc.) hot across repeated render passes — walker output is computed once per canonical IR identity, not once per equal-but-fresh allocation.

Before canonicalization, Hiccup child positions are walked with clojure+.walk semantics and non-vector sequential values (notably lazy seqs from (map ...) inside answer IR) are safely realized to at most 100 items, then replaced with an explicit … many more marker when truncated. This avoids persisting Java LazySeq identity strings and avoids hanging on infinite seqs.

See namespace docstring for the full canonical-form invariants.

Soft-normalize any answer-input value into canonical [:ir & blocks].
Pure, total, idempotent.

Identity-preserving: when the input already satisfies the canonical
invariants (`canonical?`), the return value is the SAME object.
This keeps downstream `System/identityHashCode` caches
(`format-answer-with-thinking-data`, etc.) hot across repeated
render passes — walker output is computed once per canonical IR
identity, not once per equal-but-fresh allocation.

Before canonicalization, Hiccup child positions are walked with
`clojure+.walk` semantics and non-vector sequential values (notably
lazy seqs from `(map ...)` inside answer IR) are safely realized to
at most 100 items, then replaced with an explicit `… many more`
marker when truncated. This avoids persisting Java LazySeq identity
strings and avoids hanging on infinite seqs.

See namespace docstring for the full canonical-form invariants.
sourceraw docstring

answer->irclj

(answer->ir answer)

Lift a final-answer value into canonical IR.

The Markdown-answer pipeline produces exactly two final-answer shapes:

  • {:answer markdown} - plain-prose answer
  • {:vis/answer-mode :needs-input :answer/text string} - needs-input gate

Returns canonical [:ir & blocks]. nil yields [:ir {}]. Anything outside the two canonical shapes is an upstream bug.

Lift a final-answer value into canonical IR.

The Markdown-answer pipeline produces exactly two final-answer shapes:
  - `{:answer markdown}`                                  - plain-prose answer
  - `{:vis/answer-mode :needs-input :answer/text string}` - needs-input gate

Returns canonical `[:ir & blocks]`. nil yields `[:ir {}]`.
Anything outside the two canonical shapes is an upstream bug.
sourceraw docstring

block-structurally-silent?clj

(block-structurally-silent? form-source)

True when the block source carries NO :code segment — it is purely structural engine chrome (a bare title call). The engine stamps such a block + its stream chunk with :vis/silent? true so channels drop the whole entry from display (web folds it on :silent, the TUI hides the form slot). Any block that DOES carry a :code segment flows through and its raw source paints unconditionally.

True when the block source carries NO `:code` segment — it is purely
structural engine chrome (a bare title call). The engine stamps such a
block + its stream chunk with `:vis/silent? true` so channels drop the
whole entry from display (web folds it on `:silent`, the TUI hides the
form slot). Any block that DOES carry a `:code` segment flows through and
its raw source paints unconditionally.
sourceraw docstring

canonical?clj

(canonical? x)

Cheap structural check: x is already a canonical [:ir & blocks] AST. When true, (->ast x) is the identity (returns the same object), so downstream caches keyed on System/identityHashCode hit cleanly across repeated render passes.

Cheap structural check: `x` is already a canonical `[:ir & blocks]`
AST. When true, `(->ast x)` is the identity (returns the same
object), so downstream caches keyed on `System/identityHashCode`
hit cleanly across repeated render passes.
sourceraw docstring

diff-line-kindclj

(diff-line-kind line)

Classify ONE unified-diff line for channel-neutral colouring: :meta (file headers ---/+++), :hunk (@@), :add (+), :del (-), or :ctx (context / unchanged). The SINGLE classifier both channels share — the TUI maps the kind to an ANSI colour, the web to a CSS class — so a diff fence colours IDENTICALLY in both, from one source of truth (no per-channel copy to drift).

Classify ONE unified-diff line for channel-neutral colouring: `:meta` (file
headers `---`/`+++`), `:hunk` (`@@`), `:add` (`+`), `:del` (`-`), or `:ctx`
(context / unchanged). The SINGLE classifier both channels share — the TUI maps
the kind to an ANSI colour, the web to a CSS class — so a `diff` fence colours
IDENTICALLY in both, from one source of truth (no per-channel copy to drift).
sourceraw docstring

extract-codeclj

(extract-code input)

Walk the AST and return a vector of strings, one per [:code ...] block, in source order. Used by vis --code.

Walk the AST and return a vector of strings, one per [:code ...] block,
in source order. Used by `vis --code`.
sourceraw docstring

extract-textclj

(extract-text input)

Walk the AST and return concatenated plain-text content of all [:p] blocks (inline content stripped). Used by voice TTS.

Walk the AST and return concatenated plain-text content of all [:p]
blocks (inline content stripped). Used by voice TTS.
sourceraw docstring

ir?clj

(ir? x)

True when x is a canonical [:ir ...] AST.

True when x is a canonical [:ir ...] AST.
sourceraw docstring

markdown->irclj

(markdown->ir text)
(markdown->ir text {:keys [soft-break]})

Parse a Markdown string into canonical answer-IR. Idempotent: when the input is already canonical IR, returns it unchanged (identical? preserved — cache-friendly).

This is the SINGLE entry point for turning Markdown source into IR. Used by:

  • the final-answer pipeline (model's plain-prose Markdown answer)
  • thinking text from the model
  • user-typed messages from the TUI input box

Returns canonical [:ir & blocks] directly (no further ->ast round-trip needed). Empty / nil input yields [:ir {}].

Implementation: commonmark-java parser + GFM tables / strikethrough extensions, then a faithful Node→IR walker. Soft line breaks collapse to a single space; hard line breaks become [:br].

opts (2-arity) currently understands {:soft-break :hard}, which lifts every bare newline to [:br] instead of a space — used for line-oriented user/pasted input so the rendered bubble keeps the exact line structure the user typed. Default keeps prose semantics.

Parse a Markdown string into canonical answer-IR.
Idempotent: when the input is already canonical IR, returns it
unchanged (`identical?` preserved — cache-friendly).

This is the SINGLE entry point for turning Markdown source into IR.
Used by:
  - the final-answer pipeline (model's plain-prose Markdown answer)
  - thinking text from the model
  - user-typed messages from the TUI input box

Returns canonical `[:ir & blocks]` directly (no further `->ast`
round-trip needed). Empty / nil input yields `[:ir {}]`.

Implementation: commonmark-java parser + GFM tables / strikethrough
extensions, then a faithful Node→IR walker. Soft line breaks collapse
to a single space; hard line breaks become `[:br]`.

`opts` (2-arity) currently understands `{:soft-break :hard}`, which
lifts every bare newline to `[:br]` instead of a space — used for
line-oriented user/pasted input so the rendered bubble keeps the
exact line structure the user typed. Default keeps prose semantics.
sourceraw docstring

normalize-reasoningclj

(normalize-reasoning text)

Canonical normalization for model reasoning / thinking text before it is rendered as a trace. Reasoning streams carry whitespace-padded blank rows (trailing spaces/tabs the model emits) and paragraph-style double newlines that make the compact thinking block look ragged (glm-5.2 especially). Strip per-line trailing whitespace, collapse every run of newlines down to ONE line break, then give the trace BREATHING ROOM: a line that ENDS A SENTENCE (./!/?/, optionally closed by a quote/paren/bracket) and is followed by more text gets a blank line after it, so consecutive sentences read as separate paragraphs instead of a wall. Finally trim. Shared by every channel so the TUI bubble and the web thinking card normalize identically.

Canonical normalization for model reasoning / thinking text before it is
rendered as a trace. Reasoning streams carry whitespace-padded blank rows
(trailing spaces/tabs the model emits) and paragraph-style double newlines
that make the compact thinking block look ragged (glm-5.2 especially). Strip
per-line trailing whitespace, collapse every run of newlines down to ONE line
break, then give the trace BREATHING ROOM: a line that ENDS A SENTENCE
(`.`/`!`/`?`/`…`, optionally closed by a quote/paren/bracket) and is
followed by more text gets a blank line after it, so consecutive sentences
read as separate paragraphs instead of a wall. Finally trim. Shared by every
channel so the TUI bubble and the web thinking card normalize identically.
sourceraw docstring

parse-block-displayclj

(parse-block-display form-source)

Return the model's authored source as ONE verbatim :code segment.

The engine is full-Python: the source is Python and we keep it VERBATIM here — exactly what the model wrote, no splitting, no classification. This is the canonical segment (tests + the model's own context depend on it being the raw bytes). Channels that want a beautified view call prettify-python at paint time; the IR stays raw.

Pure helper. Never throws. Blank / nil input returns [].

Return the model's authored source as ONE verbatim `:code` segment.

The engine is full-Python: the source is Python and we keep it VERBATIM here —
exactly what the model wrote, no splitting, no classification. This is the
canonical segment (tests + the model's own context depend on it being the raw
bytes). Channels that want a beautified view call `prettify-python` at paint
time; the IR stays raw.

Pure helper. Never throws. Blank / nil input returns `[]`.
sourceraw docstring

prettify-pythonclj

(prettify-python src)

Beautify Python src via ruff (long calls/collections wrapped multiline, black-style) for DISPLAY at a CHANNEL boundary. NOT applied to the canonical :code IR (which stays VERBATIM — the executed/stored source, and the model's exact bytes), so channels opt in when painting. Cached (bounded LRU) and verbatim-safe: format-or returns src unchanged when ruff is unavailable or the snippet isn't valid Python (partial streams, prose), so it never changes meaning and never throws. ruff runs in-process via clj-ruff — ONE process-wide cdylib, leak-free (confined arena + ruff_free_string per call).

Beautify Python `src` via ruff (long calls/collections wrapped multiline,
black-style) for DISPLAY at a CHANNEL boundary. NOT applied to the canonical
`:code` IR (which stays VERBATIM — the executed/stored source, and the
model's exact bytes), so channels opt in when painting. Cached (bounded LRU)
and verbatim-safe: `format-or` returns `src` unchanged when ruff is
unavailable or the snippet isn't valid Python (partial streams, prose), so it
never changes meaning and never throws. ruff runs in-process via clj-ruff —
ONE process-wide cdylib, leak-free (confined arena + `ruff_free_string` per call).
sourceraw docstring

reasoning->irclj

(reasoning->ir text)

Reasoning / thinking text -> canonical answer-IR. The SINGLE shared entry point for rendering a model's thinking trace (TUI thinking bubble AND the web thinking card), so both channels paint the SAME structure. Reasoning is line-oriented (a trace, not flowing prose): normalize via normalize-reasoning then lift every bare newline to a HARD break ([:br]) via {:soft-break :hard}. Without the hard break a bold heading collapses onto its body line (the TUI **heading** body bug); with it the heading keeps its own line, matching the web ticker's marked({:breaks true}).

Reasoning / thinking text -> canonical answer-IR. The SINGLE shared entry
point for rendering a model's thinking trace (TUI thinking bubble AND the web
thinking card), so both channels paint the SAME structure. Reasoning is
line-oriented (a trace, not flowing prose): normalize via `normalize-reasoning`
then lift every bare newline to a HARD break (`[:br]`) via `{:soft-break
:hard}`. Without the hard break a bold heading collapses onto its body line
(the TUI `**heading** body` bug); with it the heading keeps its own line,
matching the web ticker's `marked({:breaks true})`.
sourceraw docstring

reasoning-collapse-min-hiddenclj

Minimum number of HIDDEN reasoning rows required before a thinking trace is COLLAPSED behind a disclosure at all. If the remainder beyond the preview is smaller than this, the whole trace renders inline — a +1 more toggle that only reveals one extra line is pure friction. One source of truth shared by the TUI thinking bubble and the web thinking card.

Minimum number of HIDDEN reasoning rows required before a thinking trace is
COLLAPSED behind a disclosure at all. If the remainder beyond the preview is
smaller than this, the whole trace renders inline — a `+1 more` toggle that
only reveals one extra line is pure friction. One source of truth shared by
the TUI thinking bubble and the web thinking card.
sourceraw docstring

reasoning-preview-line-limitclj

Canonical reasoning PREVIEW height shared by every channel. Up to this many rows/lines of a thinking trace stay visible; the remainder folds behind a +N more disclosure (web) / ▸ THINKING +N more toggle (TUI). One source of truth so the TUI bubble and the web card clamp reasoning to the SAME height.

Canonical reasoning PREVIEW height shared by every channel. Up to this many
rows/lines of a thinking trace stay visible; the remainder folds behind a
`+N more` disclosure (web) / `▸ THINKING +N more` toggle (TUI). One
source of truth so the TUI bubble and the web card clamp reasoning to the
SAME height.
sourceraw docstring

renderclj

(render input flavor)
(render input flavor opts)

Render any answer input into a flavor.

Input: string | Hiccup vector | [:ir ...] AST | sequential of mixed Flavor: :html | :markdown | :plain Opts: {:context #{:answer :thinking :status :error} :max-length int - hard cap; truncate at paragraph boundary}

Render any answer input into a flavor.

Input:  string | Hiccup vector | [:ir ...] AST | sequential of mixed
Flavor: :html | :markdown | :plain
Opts:   {:context    #{:answer :thinking :status :error}
         :max-length int  - hard cap; truncate at paragraph boundary}
sourceraw docstring

search-textclj

(search-text v)

Universal plain-text projection for full-text search / clipboard / logging. Accepts canonical IR, a markdown string, or anything ->ast can coerce; returns a single concatenated string suitable for FT5 indexing or substring matching.

IR-side rendering: all prose/list/quote/table text collapses to spaces; :code/:c bodies are included verbatim (often the highest-signal text for search). Strings are lifted via markdown->ir so search hits the same shape regardless of upstream contract.

Idempotent on canonical input via the markdown->ir shortcut.

Universal plain-text projection for full-text search / clipboard /
logging. Accepts canonical IR, a markdown string, or anything
`->ast` can coerce; returns a single concatenated string suitable
for FT5 indexing or substring matching.

IR-side rendering: all prose/list/quote/table text collapses to spaces;
`:code`/`:c` bodies are included verbatim (often the highest-signal text
for search).
Strings are lifted via `markdown->ir` so search hits the same shape
regardless of upstream contract.

Idempotent on canonical input via the `markdown->ir` shortcut.
sourceraw docstring

session->markdownclj

(session->markdown db-info session-ref)
(session->markdown db-info session-ref opts)

Project a full session as a Markdown document on top of the IR pipeline.

Project a full session as a Markdown document on top of the IR
pipeline.
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