Raxol. UI. Components. Harness. MarkdownBody
(Raxol v2.6.1)
View Source
Renders a message/reasoning block's Markdown content for the harness transcript.
Extends Raxol.UI.Components.MarkdownRenderer (does not change its
existing behavior -- see that module's own additive GFM table support)
with a streaming-aware render policy: while a message is still being
typed/streamed, its trailing text may contain an incomplete construct,
and this module renders a provisional, closed-up view of that tail
without ever touching the real (still-growing) source buffer.
Two modes
:sealed-- the block'sitem_completedcontent is final; render it through a plain full parse (Raxol.UI.Components.MarkdownRenderer, no transformation).:streaming-- the block is still live; the accumulated tail text may contain an incomplete construct (an unclosed fence, an unpaired**/_/`, a half-written link).provisional_close/1computes a RENDER-ONLY closed copy of the text -- the caller's source buffer is never touched, this module takes a string by value and returns a view, nothing more. Discarded on the next delta / at seal: the sealed render is always a full parse of the true final content, never a leftover of a provisional close.
Provisional-close constructs handled
A single left-to-right scan of the "prose" portion of the text (fenced code content is excluded -- markdown markers inside a code span/block are literal, never auto-closed as emphasis) tracks a stack of open constructs and appends whatever is still open, in LIFO order, at the very end of the text:
- fenced code block (an odd count of
```/~~~fence lines -- closed by appending a fence line of the SAME marker type that opened it; a fence only closes on a matching marker, so a stray~~~line inside a```block never prematurely ends it, and vice versa; nothing else is scanned once the buffer ends inside an unterminated fence) - inline code span (a single
`) - bold (
**/__) - italic (
*/_) - link text (
[without a matching]) and link URL (](urlwithout a matching))
The renderer this module hands off to uses a flat (non-recursive)
regex grammar -- it can represent at most ONE active emphasis/code/link
run at a time, never genuine parent-child nesting of different marker
kinds. So when the truncation point falls inside real nesting (e.g. a
bold span containing an unfinished italic run), only the OUTERMOST
still-open construct is closed; any INNER opener that never got a
chance to close before the cut has its own marker characters stripped
from the render-only copy -- the real text it introduced is preserved,
now attributed to the surviving outer construct. Emitting a closer for
every open frame (LIFO, unconditionally) would produce a marker
sequence -- like ***, ___, or `* -- the flat grammar can't
parse back out, which is exactly the raw-marker leak this guards
against. This is the same "strip a dangling opener rather than leak
it" idea the module already applies to a content-free opener, just
widened to cover an un-nestable one too.
A half-written table row needs no closer here.
Raxol.UI.Components.MarkdownRenderer's table detector fires only once
a header line and a separator-shaped line (|---|...) both exist as
complete lines in the buffer, so a lone in-progress header renders as
plain text (harmless literal | characters, not a broken frame). Two
benign streaming shapes follow from that: a partial separator on the
still-streaming last line (e.g. |--, which the lenient detector
accepts) renders a premature but well-formed frame -- header +
separator, no body rows yet; and a still-in-progress last body row
renders with a short/ragged final cell. Neither is a broken frame, and
a narrow width never collapses a column to zero (columns clamp to a
minimum and the row overflows/wraps like any long line -- see
MarkdownRenderer's table renderer).
Known limitation
Backslash-escaped markers (e.g. \*) are not treated as CommonMark
escapes -- the backslash is scanned as ordinary text and the following
marker character still toggles/opens its construct normally. Full
escape handling would need to thread an "escaped" flag through the
scan; left as a documented gap rather than expanded scope here.
Summary
Functions
The single vocabulary bridge from a block's seal state (:live | :sealed, see Raxol.UI.Components.Harness.Block) to this module's
render mode: :live -> :streaming, :sealed -> :sealed. Every call
site that glues block seal to Markdown rendering (BodyProvider's
:message props, Block.render/2's context[:markdown] path) MUST
use this rather than re-deriving the mapping -- two names for the same
binary state is already one too many.
Render-only provisional close of incomplete Markdown constructs in
text. Pure: returns a NEW string, never mutates or reads any external
state. Safe on arbitrary input (garbage, invalid UTF-8, empty string) --
never raises. Runs in a single pass, time linear in byte_size(text)
(above 262144 bytes the scan is skipped and text is
returned unchanged -- a degradation ceiling, not the bound).
Renders markdown_text per context[:mode] (:sealed default) and
context[:width] (defaults to Raxol.Core.Defaults.terminal_width/0).
Never raises -- arbitrary/garbage/invalid-UTF-8 input always yields
some safe view.
Full parse -- the seal-time render. markdown_text is trusted to be
the item's final, complete content (item_completed.content, never a
concatenation of deltas).
Streaming render: provisional_close/1 on a copy of markdown_text,
then a full parse of THAT -- never the original. The caller's buffer is
untouched; this function takes a value and returns a value.
Types
Functions
@spec mode_for_seal(Raxol.UI.Components.Harness.Block.seal_state()) :: mode()
The single vocabulary bridge from a block's seal state (:live | :sealed, see Raxol.UI.Components.Harness.Block) to this module's
render mode: :live -> :streaming, :sealed -> :sealed. Every call
site that glues block seal to Markdown rendering (BodyProvider's
:message props, Block.render/2's context[:markdown] path) MUST
use this rather than re-deriving the mapping -- two names for the same
binary state is already one too many.
Render-only provisional close of incomplete Markdown constructs in
text. Pure: returns a NEW string, never mutates or reads any external
state. Safe on arbitrary input (garbage, invalid UTF-8, empty string) --
never raises. Runs in a single pass, time linear in byte_size(text)
(above 262144 bytes the scan is skipped and text is
returned unchanged -- a degradation ceiling, not the bound).
Renders markdown_text per context[:mode] (:sealed default) and
context[:width] (defaults to Raxol.Core.Defaults.terminal_width/0).
Never raises -- arbitrary/garbage/invalid-UTF-8 input always yields
some safe view.
@spec render_sealed(term(), pos_integer()) :: map()
Full parse -- the seal-time render. markdown_text is trusted to be
the item's final, complete content (item_completed.content, never a
concatenation of deltas).
@spec render_streaming(term(), pos_integer()) :: map()
Streaming render: provisional_close/1 on a copy of markdown_text,
then a full parse of THAT -- never the original. The caller's buffer is
untouched; this function takes a value and returns a value.