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's item_completed content 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/1 computes 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 (](url without 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

mode()

@type mode() :: :sealed | :streaming

Functions

mode_for_seal(atom)

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.

provisional_close(text)

@spec provisional_close(String.t()) :: String.t()

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).

render(markdown_text, context \\ %{})

@spec render(term(), map()) :: map()

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.

render_sealed(markdown_text, width)

@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).

render_streaming(markdown_text, width)

@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.