Transient Markdown parsing and renderer projections.
Canonical answers are role-labelled, string-keyed content blocks from
com.blockether.vis.internal.content. Parsed Markdown trees are created only
inside renderers and are never transported or persisted.
markdown->ast parses prose for renderer-local layout. render,
extract-code, extract-text, and session->markdown are disposable
projections; none of their intermediate trees are canonical message data.
Transient Markdown parsing and renderer projections. Canonical answers are role-labelled, string-keyed content blocks from `com.blockether.vis.internal.content`. Parsed Markdown trees are created only inside renderers and are never transported or persisted. `markdown->ast` parses prose for renderer-local layout. `render`, `extract-code`, `extract-text`, and `session->markdown` are disposable projections; none of their intermediate trees are canonical message data.
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->ast 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->ast text {:soft-break
:hard})`. Default `:space` keeps every existing caller unchanged.(->ast v)Soft-normalize any answer-input value into canonical [:ast & 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 renderer input) 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 [:ast & 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 renderer input) 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.
(ast? x)True when x is a canonical [:ast ...] AST.
True when x is a canonical [:ast ...] AST.
(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.
(canonical? x)Cheap structural check: x is already a canonical [:ast & 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 `[:ast & 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.
(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).
(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`.
(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.
(markdown->ast text)(markdown->ast text {:keys [soft-break]})Parse a Markdown string into canonical transient Markdown tree.
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:
Returns canonical [:ast & blocks] directly (no further ->ast
round-trip needed). Empty / nil input yields [:ast {}].
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 transient Markdown tree.
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 `[:ast & blocks]` directly (no further `->ast`
round-trip needed). Empty / nil input yields `[:ast {}]`.
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.(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.
(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 `[]`.
(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).
(reasoning->ast text)Reasoning / thinking text -> canonical transient Markdown tree. 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 transient Markdown tree. 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})`.Minimum HIDDEN reasoning rows required before a thinking trace collapses. One means the preview limit is a hard maximum in every channel.
Minimum HIDDEN reasoning rows required before a thinking trace collapses. One means the preview limit is a hard maximum in every channel.
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.
(render input flavor)(render input flavor opts)Render any answer input into a flavor.
Input: string | Hiccup vector | [:ast ...] 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 | [:ast ...] AST | sequential of mixed
Flavor: :html | :markdown | :plain
Opts: {:context #{:answer :thinking :status :error}
:max-length int - hard cap; truncate at paragraph boundary}(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 parsed via markdown->ast so search sees the rendered shape
regardless of upstream contract.
Idempotent on parsed input via the markdown->ast 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 parsed via `markdown->ast` so search sees the rendered shape regardless of upstream contract. Idempotent on parsed input via the `markdown->ast` shortcut.
(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.
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 |