registry:ui · Data Display
markdown — the streaming face, AST mapped to first-party parts
One component turns a markdown string into jixoai surfaces: fenced code lands in code-card (generation-guard repaint keeps partial code readable mid-stream), tables land in the registry table under a data-kind wrapper, and every prose construct lands on a first-party reading-content part — Blockquote (with GitHub alert detection), Heading, List, the text family, Link, InlineCode, Separator — escaping the jx-pure face exactly where the box-owning blocks need it and composing with it everywhere else. Streaming is keyed-block memoized — the frozen prefix keeps its DOM while append-only chunks re-render only the trailing block — and streaming=false (the default) is a static, final document. The parser core is stream-markdown-parser; the renderer, the mapping vocabulary and the security floor (html:false, validateLink, image sanitize, zero raw-HTML injection) are 100% first-party.
$npx jixoai-ui add markdown
https://ui.jixoai.com/r/markdown.json
usage
Usage
Source is a value-domain payload (the code-card precedent) — a runtime string, never inlined markup. streaming opts into the cursor + tail-key face; components overrides are trusted application code (the same trust level as any component you render by hand — the security floor covers the DEFAULT map, overrides own their own rendering).
<script lang="ts">
import Markdown from '@ui/markdown';
</script>
<!-- static face (the default): source parses FINAL — unclosed fences
settle, no cursor, nothing loads -->
<Markdown {source} />
<!-- streaming face: append-only source updates re-render only the
trailing block; the cursor rides the tail while it streams -->
<Markdown {source} streaming />
<!-- per-node-type overrides: TRUSTED application code — the same trust
level as any component you render by hand; overrides receive
{ node } raw and delegate children through the exported
MarkdownNode. The default map's security floor (html:false,
validateLink, image sanitize) covers the DEFAULT renderer. -->
<Markdown {source} components={{ link: DocLink }} />examples
Examples
One ability per demo: the GFM vocabulary in one pass, the GitHub alerts matrix, the streaming simulation (a live chunk feed over the public props), the components override seam, and the static document face.
The markdown component exists to render headings — every demo mounts it inside the
lint-sanctioned headings-ok scope, and demo sources never
mint an h1 (this page owns exactly one). The simulation is page-owned state over public
component behavior — zero registry edits, zero API bypasses.
the map
Component coverage map
The default map is first-party end to end (markdown-coverage-components §3): every markdown construct lands on a registry part, uniformly managed. Box-owning block surfaces escape the jx-pure face (no-jx-pure on the root) so it stops double-painting them; face-composing inline members never escape — an inline escape would virally descope the code chips and nested marks a link legitimately contains. The override seam is still consulted first; the map's growth changes nothing about its trust class.
| Node type | Renders | Escape |
|---|---|---|
code_block | CodeCard (unchanged — partial code + loading state mid-fence) | carrier div (existing) |
table | Table in div[data-kind="table"] (unchanged) | carrier (existing) |
blockquote | Blockquote — GitHub-alert detection first (§ the alerts demo) | root no-jx-pure |
heading | <Heading level> (the em ladder) | root no-jx-pure |
list | <List ordered start> (list_item stays native li) | root no-jx-pure |
paragraph / inline | <P> | face-composing |
strong / emphasis / strikethrough / highlight / insert / subscript / superscript | <Strong> <Em> <Del> <Mark> <Ins> <Sub> <Sup> | face-composing |
link | <Link> (absolute http(s) opens externally) | face-composing |
inline_code | <InlineCode> riding the component's lang="auto" default (children recursed inside) | root escape |
thematic_break | <Separator /> | carrier div + root escape |
checkbox (task items) | native disabled input — the jx-pure bare-checkbox face paints it | — |
image | native <img> (unchanged — sanitized; the no-CLS unlock is a recorded followup) | — |
text / hardbreak / emoji / footnote bits / dl | native (the pure-text floor) | — |
kitchen sink
The default map, one pass: emphasis onto the text family, inline chips onto InlineCode, a task list as disabled native inputs, the GFM table onto the registry table (td[data-label] + per-column align), a fenced card onto code-card, the quote onto Blockquote, headings onto Heading, the rule onto Separator, links and bare-URL autolinks onto Link. Images: bitmap data URLs render; SVG data URLs and unsafe schemes omit the img entirely (alt preserved). Footnotes degrade to inert sup text.
the GFM vocabulary in one pass
Plain bold, italic, struck, inline chips, and a titled link — bare URLs autolink too: https://ui.jixoai.com
task lists
keyed prefix stays mounted while the tail grows
remount without a digest change (nothing does)
tables ride the registry Table
| node | maps to | alignment |
|---|---|---|
code_block | CodeCard | left |
table | Table under data-kind="table" | center |
| prose | the jx-pure native ladder | right |
// CodeCard repaints through its generation guard:
// drop to plain, re-highlight, no flash.
const fence = { closed: true, loading: false };
blockquotes keep the jx-pure rule — recursive blocks render inside.
Images: bitmap data URLs render; SVG data URLs and unsafe schemes omit the img entirely (alt preserved). Footnotes degrade inertly.1
sup text — no section semantics in v1.
<script lang="ts">
import Markdown from '@ui/markdown';
</script>
<!-- static face (the default): source parses FINAL — unclosed fences
settle, no cursor, nothing loads -->
<Markdown {source} />
<!-- streaming face: append-only source updates re-render only the
trailing block; the cursor rides the tail while it streams -->
<Markdown {source} streaming />
<!-- per-node-type overrides: TRUSTED application code — the same trust
level as any component you render by hand; overrides receive
{ node } raw and delegate children through the exported
MarkdownNode. The default map's security floor (html:false,
validateLink, image sanitize) covers the DEFAULT renderer. -->
<Markdown {source} components={{ link: DocLink }} />GitHub alerts
The default map's one new behavior: a [!NOTE|TIP|IMPORTANT|WARNING|CAUTION] marker on the quote's own first line renders a tonal Blockquote — label and status hue wired by the map (note→info, tip→success, important→primary, warning→warning, caution→error — statuses, never the destructive action hue). A half-typed marker simply does not match: the block renders the plain quote and keeps updating in place (L2 — the tail item never remounts on content mutation).
Note
GitHub alert markers on the quote's own first line render as tonal Blockquotes with the label and status hue wired by the map.
Tip
The five kinds — note, tip, important, warning, caution — map to the five status hues; caution is the error hue (statuses, never destructive).
Important
A half-typed marker simply does not match: the block renders the plain quote and keeps updating in place (the L2 in-place tail law).
Warning
Markers not on their own first line never trigger (the GitHub rule).
Caution
Mixed-case markers trigger —
> [!Note]is a note, GitHub behavior.
And a plain quote keeps the classic outline rule — no label, no hue, exactly the reading-content posture.
<!-- nothing to wire: the detector is part of the default map -->
<Markdown source={alertsDoc} />html equivalence
Markdown sources may mix HTML: the frozen tag table routes whitelisted tags onto the SAME components as their markdown spellings — <b>html bold</b> and **markdown bold** are one Strong, an html anchor is the Link, <code> is the InlineCode chip, <kbd> rides Kbd. Everything outside the table (span, div, script…) stays escaped literal text — the security floor lives render-side: zero {@html} anywhere, hrefs re-validate on both syntax paths.
HTML is markdown spelled differently — both spellings land in the same component: html bold and markdown bold are one Strong.
The frozen tag table: strong, em, i,
del, s, ins, u, mark,
H2O, x2, a code chip,
press ⌘K, and
an html anchor.
Everything outside the table stays escaped literal text: <span>span is not in the table</span>, and scripts never execute.
<!-- nothing to wire: equivalence is the default map -->
<Markdown source={equivalenceDoc} />details → accordion
The common <details>/<summary> pair in markdown rides the accordion — the W3C-first item that IS native details/summary (no div simulation, the same philosophy as every input here). Consecutive top-level details runs merge into ONE group (a pure parse-side transform; a growing group remounts once per semantic event, the link-reference-definition precedent); <details open> carries through; bodies parse their markdown; a summary-less details renders the default disclosure label.
Shipping
Consecutive top-level <details> runs merge into ONE
accordion group — the frame and seams a pile of bare disclosures lacks.
Bodies parse markdown: bold, chips and links.
Native semantics
The accordion IS native <details>/<summary> — W3C-first, the same
philosophy as every input in this registry. The open attribute carries
through: this item shipped open.
Unknown tags stay text
A <div> or <script> never becomes markup — the floor holds.
<!-- nothing to wire: the group merge is part of the default map -->
<Markdown source={accordionDoc} />typography presets
The prose density trio — one prop, one stack token: compact 13px/1.55 with an 8px block stack for dense chat panes, standard 14px/1.7 at 14px, relaxed 16px/1.75 at 20px (the Tailwind prose-base / GitHub body calibration). The block rhythm is a single --jx-md-stack law: adjacent siblings only, headings breathe at 1.75x, the first block hugs the top; the heading ladder is em-scaled so every preset keeps its hierarchy.
The block rhythm, one token
Every top-level block is one stack token apart — the first block never earns a top margin, and headings breathe at 1.75x above (the GitHub / Tailwind Typography calibration).
the prose scale is its own axis: 13/1.55 compact, 14/1.7 standard, 16/1.75 relaxed
checkboxes ride the text middle (the task-item law)
wrapped task text flows like any inline content
A blockquote keeps the face rule; nested prose inside keeps the face rhythm — only the root level rides the stack.
const stack = { compact: 8, standard: 14, relaxed: 20 }; // px between blocks
| preset | text | leading | stack |
|---|---|---|---|
| compact | 13px | 1.55 | 8px |
| standard | 14px | 1.70 | 14px |
| relaxed | 16px | 1.75 | 20px |
The heading ladder is em-scaled, so every preset keeps its hierarchy — relaxed renders the face’s exact numbers.
<Markdown source={doc} typography={typography} />streaming simulation
A live chunk feed: an interval appends a few characters to the source every 80ms — paragraphs grow in place, table rows settle as they close, and an open fence keeps a loading face with progressive highlighting until it closes. streaming follows the feed: pause drops to the final face (open constructs settle, the cursor leaves), resume re-enters streaming on the same extending source, reset recycles the parser (the non-append law). The cursor span is aria-hidden — the blink is decoration.
<script lang="ts">
import Markdown from '@ui/markdown';
const DOC = '## the full target document…';
const TICK = 80; // ms per chunk
const CHUNK = 4; // chars per chunk
let playing = $state(true);
let revealed = $state(0);
const source = $derived(DOC.slice(0, revealed));
const done = $derived(revealed >= DOC.length);
// streaming follows the feed: playing → true; pause AND completion
// both drop to the final face — the final parse settles open
// fences; resuming re-enters streaming on the same extending
// source (the false→true restart law, design §2.1)
$effect(() => {
if (!playing) return;
const timer = setInterval(() => {
if (revealed >= DOC.length) { playing = false; return; }
revealed = Math.min(revealed + CHUNK, DOC.length);
}, TICK);
return () => clearInterval(timer);
});
</script>
<Markdown {source} streaming={playing} />custom components
The components prop maps a node type to YOUR component: here link nodes route through DocLink — absolute http(s) hrefs open in a new tab with a trailing glyph, app routes keep default navigation — and children delegate back through the exported MarkdownNode. Overrides are trusted application code (the react-markdown model): they receive the node raw, and the default map's security floor does not cover what an override chooses to render.
one node type, your component
Every link node routes through the override — the registry site opens externally with a glyph, while an internal route keeps same-tab default navigation. All other types keep the default map: bold, chips, and the table below.
| node | renderer |
|---|---|
link | the DocLink override (this demo) |
| everything else | the default map, floor intact |
<script lang="ts">
import Markdown from '@ui/markdown';
import DocLink from './doc-link.svelte';
</script>
<!-- the seam is per node type; overrides receive { node } RAW (trusted
application code, design §3.3) and delegate children through the
exported MarkdownNode -->
<Markdown source={doc} components={{ link: DocLink }} />static document
The default face: no streaming prop, first parse final — release notes, READMEs, changelogs. An unclosed fence (the one at the end never closes) renders as settled code, never a loading state; task lists are static disabled inputs; the cursor never mounts.
release notes — the static face
streaming defaults to false: the first parse runs final semantics — nothing flickers, nothing loads, print stays clean.
settled constructs
an unclosed fence renders settled code, never a loading state (the fence below never closes)
task lists are static disabled inputs — presentation, not controls
shipped
followups tracked in the change
| axis | pinned |
|---|---|
| raw HTML | escaped literal text |
| unsafe URL schemes | demoted to plain text |
The pinned axes (html:false, typographer:false, breaks:false) are
the text-fidelity contract. This fence never closes — final
semantics still render it as settled code.<!-- the default face: no streaming prop, no cursor. The SAME
component renders chat streams and shipping docs. -->
<Markdown {source} />axes
Universal props
The eight-axis surface (explicit-props): size · shape · radius · density · color · theme · elevation · motion — each axis takes named steps, auto (inherit the ambient context; stamps nothing), an exact number (px · coefficient · dp · hue per axis), or query() for responsive/container-conditional values. FIRST-TIME contract, all no-own: the typography presets own the prose scale through their OWN tokens; the legacy density bridge keeps feeding nested chrome from the preset, and the root NEVER stamps a rung for the preset (the founding law, spec-pinned) — only an explicit lane does.
Markdown · universal props
Prose typography keeps its preset law; the axes ride additively on the root.
<Markdown density="auto">…</Markdown>api
API
Three props of the component's own plus the HTML div passthrough. The barrel also exports MarkdownNode — the override delegation seam — and parse.ts's adapter surface (createMarkdownParser, block keys/digests, the full node-type vocabulary).
Properties
| Property | Type | Default | Description |
|---|---|---|---|
| source * | string | — | Markdown source — a value-domain payload (the code-card precedent). Append-only updates while streaming re-render only the trailing block. |
| streaming | boolean | false | Opt-in streaming face: the tail cursor + tail-key semantics. false (the default) parses FINAL on first render — a static document, never a loading state. |
| typography | MarkdownTypography | — | |
| components | MarkdownComponents | — | Per-node-type renderer overrides — TRUSTED application code (the react-markdown model): overrides receive { node } raw and delegate children through the exported MarkdownNode. The security floor covers the DEFAULT map. |
| class | string | '' | Merged onto the jx-pure scoped root through cn(). |
| style | string | — | Inline style on the root div. |
| …rest | HTMLAttributes<HTMLDivElement> | — | Every other prop passes through to the root div untouched. |
| MarkdownNode (export) | Component<{ node, components? }> | — | The override delegation seam — an override renders the node’s children through the exported MarkdownNode (see the custom components demo). |
Universal props
| Property | Type | Default | Description |
|---|---|---|---|
| size | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' · ambient scope | the base scale — root font-size; parts size in em |
| shape | 'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto' | 'auto' · ambient scope | corner geometry (CSS corner-shape; §14 degrade table) |
| radius | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' · ambient scope | corner size; auto = the concentric broadcast (§3) |
| density | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' · ambient scope | spacing/leading scale over the kernel channels (§4) |
| color | 'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string | 'auto' · ambient scope | the hue axis of the fixed oklch primary system (§5) |
| theme | 'light' | 'dark' | 'system' | 'auto' | 'auto' · ambient scope | light/dark profile; system = the JS-mutable global (§6) |
| elevation | 'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number | 'auto' · ambient scope | official M3 levels over the surface ladder (§7) |
| motion | 'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number | 'auto' · ambient scope | intensity across the motion kernels (§8) |
the security floor (default map)
-
html: false— raw HTML from the source degrades to escaped literal text; nothing is ever parsed as markup - unsafe URL schemes (
javascript:and friends) demote to plain text at parse time — they never reach an href - image src survives
sanitizeImageSrcor the img is omitted entirely — bitmap data URLs render, SVG and other schemes never leak - zero
{@html}in the renderer — code markup lands as inert, escaped spans
the trust boundaries
-
componentsoverrides are trusted application code — they own what they render; the floor above covers the DEFAULT map - ambient markstream plugins (module-global registration) are a trusted process boundary — one dev warning states that BOTH the vocabulary and URL-security guarantees suspend while they are present
- the parser is a library dependency (
stream-markdown-parser); the renderer and the mapping vocabulary stay 100% first-party
See Also
The families the mapping vocabulary composes with.