introduction #
mdz is a strict markdown dialect built for streaming, Svelte authoring, docs websites, and untrusted content. The same grammar serves multiple use cases:
- streaming output from LLMs and other live sources
- authoring with Svelte components, with an optional build-time preprocessor that compiles static usage to plain Svelte markup
- rendering TSDoc/JSDoc comments on docs websites with domain-specific behavior like backticked identifiers linkifying to API docs
- dynamic content from untrusted users, with granular control over enabled behaviors
mdz's predictable grammar means streaming never re-parses: LLM output renders incrementally,
and the infrequent corrections -- like an unclosed ** or a link resolving late --
are bounded, explicit opcodes. The streaming design is derived from @pngwn's ideas in this Bluesky thread (pngwn.at), and other parts of the mdz design are inspired by
pngwn's MDsveX.
mdz takes after CommonMark and GFM, diverging where their rules fight streaming or carry avoidable complexity and ambiguity.
It ships:
- Mdz and MdzStream: Svelte 5 components to render regular and streaming content
- MdzStreamParser: an incremental parser that emits append-only rendering opcodes as chunks arrive, never re-parsing
- mdz_parse: a synchronous parser producing an MdzNode tree
- svelte_preprocess_mdz: a preprocessor that compiles static mdz content to plain Svelte markup at build time
AI disclosure: almost everything here was generated by machine agents except for the designs and intros. mdz is the result of 5+ years of prototyping ideas.
For more see the docs ahead and the repo.
Principles #
- Streaming is a property of the grammar, not just the parser — every ambiguity resolves within a bounded hold or an explicit, local opcode; constructs that break this (setext headings, reference links, the loose/tight list distinction) are excluded rather than worked around.
- One syntax per feature —
**bold**,_italic_,~~strike~~, no alternate spellings. - Chunking doesn't change the result — the streamed tree matches the one-shot parse (documented adversarial cases aside), and the parser never re-parses.
- Predictable over permissive — false negatives over false positives:
__init__, intraword underscores, and stray*stay literal. - Structure is the parser's job; presentation is the renderer's — whitespace style, syntax highlighting, and rich components inject at the rendering seam.
- Nothing renders by default except mdz — HTML elements and Svelte components must be registered; unregistered tags render as visible placeholders.
Install #
npm i -D @fuzdev/mdz Svelte, SvelteKit, and @fuzdev/fuz_util are peer dependencies (fuz_util is used only
by the build-time preprocessor).
Usage #
Render static content:
import Mdz from '@fuzdev/mdz/Mdz.svelte'; <Mdz content="Some **bold** and `code` and a [link](/docs)." /> Parse to a node tree:
import {mdz_parse} from '@fuzdev/mdz/mdz.js';
const nodes = mdz_parse('# Heading\n\nSome **bold** text.'); The usage docs walk through the full dialect with interactive examples, and the formal grammar is the normative syntax reference.
Streaming #
Feed chunks to MdzStreamParser as they arrive (e.g. from an LLM) and render the emitted opcodes with MdzStream — the final tree is identical to what mdz_parse produces for the same input, regardless of chunking. See the streaming docs for the live demo, usage, and the opcode design.
Rendering seam #
By default, inline `code` renders as plain <code> and fenced
code blocks render as plain <pre><code>. Inject richer renderers via MdzRoot — the prop contracts match fuz_ui's DocsLink (auto-linked API identifiers) and fuz_code's Code (syntax highlighting):
<MdzRoot code={DocsLink} codeblock={Code}>
<Mdz content={content} />
</MdzRoot>Preprocessor #
svelte_preprocess_mdz compiles static <Mdz content="…"> usages to pre-rendered markup at build time, eliminating
runtime parsing for known-static content — see the svelte_preprocess_mdz docs
for setup and options.
usage #
This section has each feature's syntax with live rendered examples. The formal grammar is the normative syntax reference, and the introduction covers what mdz is for and the principles behind it.
mdz was created to author content with Svelte components
and to render TSDoc/JSDoc comments on docs websites — hence the domain-specific behavior:
linkified `backtick-wrapped` declarations and modules, auto-detected URLs prefixed with https://, /, ./, and ../, and registered
Svelte components in content.
import Mdz from '@fuzdev/mdz/Mdz.svelte'; <Mdz content="Some **bold** and `code` and a [link](/docs)." /> Some bold and code and a link.
Playground #
Bold and italic and strikethrough text.
Inline links to identifiers using backticks: mdz_parse, Mdz
A heading
A paragraph with links: fuz homepage, ./grammar
const y = 1336;Streaming #
mdz renders content incrementally as it arrives (e.g. from an LLM), with no re-parsing. See the streaming docs for the live demo, the opcode design, and the three rendering paths.
Basic formatting #
Supports bold, italic, and strikethrough:
<Mdz content="**Bold** and _italic_ and ~~strikethrough~~ text." /> Bold and italic and strikethrough text.
All inline formatting can nest:
<Mdz content="**~~_All_ three~~ combi**_ned_" /> All three combined
Inline code auto-linking #
Backtick code automatically links to identifiers and modules:
To parse markdown directly, use `mdz_parse` from module `mdz.ts`. Non-identifiers become plain code elements:
This `identifier` does not exist. This identifier does not exist.
Links #
mdz supports four kinds of links:
- standard markdown link syntax
- external URLs starting with
https://orhttp:// - absolute paths starting with
/ - relative paths starting with
./or../
[Fuz API docs](https://fuz.dev/docs/api) and https://fuz.dev/docs/api and /docs/api Relative paths are resolved against the base context (set via MdzRoot) when provided, producing correct absolute paths. Without base, they use raw hrefs (the browser resolves them against the current URL):
See ./grammar and ../streaming and ../usage for relative paths. See ./grammar and ../streaming and ../usage for relative paths.
Line breaks and paragraphs #
Single newlines are soft breaks, like standard markdown — they render as spaces by default:
First line.
Second line.
Third line. First line. Second line. Third line.
Double newlines create paragraph breaks:
First paragraph.
Second paragraph.
Soft break in second paragraph. First paragraph.
Second paragraph. Soft break in second paragraph.
Three or more newlines are the same as two — one paragraph break:
First paragraph.
Second paragraph separated by an extra newline. First paragraph.
Second paragraph separated by an extra newline.
To force a line break within a paragraph, use an explicit <br /> (it's an
HTML element, so it must be registered):
First line.<br />Second line. First line.
Second line.
To instead render every newline as a line break — handy for chat-style user input — see the whitespace prop in the whitespace section.
Headings #
Use 1-6 hashes followed by a space:
#### h4 ~~with~~ _italic_ h4 with italic
Must start at column 0 and have a space after hashes. No blank lines are required around headings. Headings can include inline formatting.
Lists #
Unordered items use -, ordered items use 1. — each followed by a space.
A marker at column 0 starts a list; indenting nests:
- first item
- second **item**
- nested item
- third item - first item
- second item
- nested item
- third item
Ordered lists render GFM-style: the first item's number sets start and the
browser numbers the rest, so the 1./1./1. reordering idiom
works — but authored numbers are preserved in the AST:
3. starts at three
1. authored numbers are preserved
2. but render in order - starts at three
- authored numbers are preserved
- but render in order
Blank lines between items don't end the list (loose LLM-style lists keep their structure), items render tight, and a column-0 non-marker line always ends the list — there is no lazy continuation. Items can contain paragraphs, nested lists, code blocks, and blockquotes on their own indented lines. See the formal grammar for the precise rules.
Blockquotes #
Prefix each line with > and a space — a quote's content is a mini-document,
so headings, lists, code blocks, and deeper quotes all work inside. A bare > line breaks paragraphs within the quote, and a blank line ends it:
> A quote with **formatting** and `code`.
> Same paragraph, soft break.
>
> New paragraph via bare `>`.
>> Nested quote. A quote with formatting and
code. Same paragraph, soft break.New paragraph via bare
>.Nested quote.
The space is required (>a is literal text) and there is no lazy continuation —
every quoted line carries the prefix.
Code blocks #
mdz uses fuz_code for syntax highlighting. Use three or more backticks with optional language hint:
```ts
const z: number = 43;
``` const z: number = 43;Must start at column 0; the closing fence needs at least as many backticks as the opening fence. Empty code blocks are valid. No blank lines are required around code blocks.
Horizontal rules #
Use exactly three hyphens (---) at the start of a line to create a horizontal
rule. No blank lines are required around it. mdz has no setext headings, so --- after a paragraph is always an HR:
Section one.
---
Section two. Section one.
Section two.
Whitespace #
The parser preserves whitespace in text nodes exactly as authored — single newlines stay
literal \n characters and no <br> nodes are created. How
that whitespace renders is the consumer's choice via the whitespace prop, which sets white-space on the wrapper element. By default no style is applied,
so whitespace collapses like standard markdown:
<Mdz content=" see
how
whitespace
renders " /> see how whitespace renders
With whitespace="pre-line", every newline renders as a line break while spaces
still collapse — useful for chat-style user input where pressing Enter should mean a new
line:
<Mdz content=" see
how
whitespace
renders " whitespace="pre-line" /> see how whitespace renders
With whitespace="pre-wrap", spaces, tabs, and newlines are all rendered
faithfully:
<Mdz content=" see
how
whitespace
renders " whitespace="pre-wrap" /> see how whitespace renders
HTML elements #
mdz supports an opt-in set of HTML elements for semantic markup and styling.
<aside>This is _italicized <code>code</code>_ inside an `aside`.</aside> <marquee>use it or lose it</marquee> Elements must be registered:
<MdzRoot elements={new Map([['code', true], ['aside', true], ['marquee', true], ['br', true]])}>
<Mdz content="<aside>text</aside>" />
</MdzRoot> Unregistered elements render as <tag-name /> placeholders for security.
Svelte components #
mdz supports a minimal subset of Svelte component syntax — tags with children, no props yet. Components are distinguished from HTML elements by their uppercase first letter:
<Alert>This is an `Alert` with _italicized <code>code</code>_ inside.</Alert> Alert with italicized code inside.Components must be registered:
<MdzRoot components={new Map([['Alert', Alert]])}>
<Mdz content="<Alert>warning</Alert>" />
</MdzRoot> Unregistered components render as <ComponentName /> placeholders.
Advanced usage #
For more control, use mdz_parse directly with MdzNodeView:
import {mdz_parse} from '@fuzdev/mdz/mdz.js';
import MdzNodeView from '@fuzdev/mdz/MdzNodeView.svelte';
const nodes = mdz_parse(content); <div class="custom">
{#each nodes as node}
<MdzNodeView {node} />
{/each}
</div> You own the container, so you control presentation — for example apply white-space:pre-line to render newlines as line breaks, or white-space:pre to avoid wrapping.
Compatibility with other markdowns #
mdz takes after CommonMark and GFM — their behavior is the baseline wherever it fits streaming. But mdz is a dialect, not a subset: it deliberately supports one syntax per feature and drops constructs whose ambiguity costs more than they're worth. The highlights:
| Feature | CommonMark/GFM | mdz |
|---|---|---|
| bold | **text** or __text__ | **text** only |
| italic | *text* or _text_ | _text_ only — single * is literal |
| strikethrough | ~~text~~, and ~text~ on github.com | ~~text~~ only — a single ~ is always literal, so ~/dev/paths never strike |
| doubled delimiters | __text__ bold, ~~text~~ strike | ~~text~~ strikes; __text__ stays literal — underscore runs
never pair, so __init__ is safe |
| setext headings | text + --- = h2 | none — --- is always an HR |
| indented code | 4-space indent | none — fenced only |
| lazy continuation | yes — dedented/unprefixed lines continue lists and quotes | none — list continuation must be indented past the marker; every quote line carries its prefix |
| reference links | [text][ref] | none — inline [text](url) only |
| hard breaks | trailing double-space or \ | <br /> (registered element) |
| relative path links | not auto-linked | ./path and ../path auto-link |
Block elements (headings, HR, codeblocks, lists, blockquotes) can interrupt paragraphs without blank lines, while inline formatting prefers false negatives over false positives. Every smaller divergence — list nesting and empty items, blockquote prefixes, tag scoping, link parsing — is enumerated in the full divergence table on the formal grammar page.
More docs #
- streaming — live demo, rendering paths, opcode design, and the streaming model
- svelte_preprocess_mdz — compiles static content to plain Svelte markup at build time
- grammar — the formal grammar (the normative syntax reference) and the full CommonMark/GFM divergence table
- fixtures — renders every test fixture live with its parsed JSON
streaming #
Use the Mdz component (see usage) when you have complete
content upfront. For content that arrives incrementally (e.g. from an LLM), use MdzStreamParser with MdzStreamState and MdzStream. The
parser emits opcodes as rendering instructions — never re-parsing — and the state applies
them as fine-grained Svelte mutations. The streaming design is derived from @pngwn's ideas in this Bluesky thread (pngwn.at), which originated the approach mdz implements:
restrict the dialect so streaming is tractable, render optimistically and correct when
wrong, minimize work by never re-parsing, and emit serializable target-agnostic opcodes
instead of a tree.
Demo #
Click the stream button below — each character is fed one at a time to show how constructs build incrementally:
(press to begin)
opcodes (0)
import {MdzStreamParser} from '@fuzdev/mdz/mdz_stream_parser.js';
import {MdzStreamState} from '@fuzdev/mdz/mdz_stream_state.svelte.js';
const parser = new MdzStreamParser();
const stream = new MdzStreamState();
// feed chunks as they arrive
parser.feed(chunk);
stream.apply_batch(parser.take_opcodes());
// when done
parser.finish();
stream.apply_batch(parser.take_opcodes()); <MdzStream {stream} />Three rendering paths #
mdz ships three ways to turn .mdz text into a rendered tree. Pick based on whether content is static, available all-at-once, or streaming.
Path 1: Mdz component (default)
For inline use in a Svelte template, with content known up front:
<Mdz content="**bold** text" />Internally calls mdz_parse and renders via MdzNodeView. Best for: documentation pages, alerts, tooltips — anything where you have the full string before render. With /docs/svelte_preprocess_mdz this also compiles away at build time for static strings.
Path 2: mdz_parse + MdzNodeView (one-shot, manual)
For control over wrapper markup, custom whitespace handling, or non-default CSS:
import {mdz_parse} from '@fuzdev/mdz/mdz.js';
import MdzNodeView from '@fuzdev/mdz/MdzNodeView.svelte';
const nodes = mdz_parse(content);<div class="custom white-space:pre">
{#each nodes as node}
<MdzNodeView {node} />
{/each}
</div>Same input, same tree as path 1, but you own the surrounding container. mdz_parse is the canonical reference parser — fixture tests pin its output as the source of truth.
Path 3: MdzStreamParser + MdzStreamState + MdzStream
For content that arrives in chunks:
import {MdzStreamParser} from '@fuzdev/mdz/mdz_stream_parser.js';
import {MdzStreamState} from '@fuzdev/mdz/mdz_stream_state.svelte.js';
const parser = new MdzStreamParser();
const stream = new MdzStreamState();
// feed chunks as they arrive
parser.feed(chunk);
stream.apply_batch(parser.take_opcodes());
// when done
parser.finish();
stream.apply_batch(parser.take_opcodes());<MdzStream {stream} />MdzStreamParser emits opcodes — small, serializable rendering instructions — as bytes arrive. MdzStreamState applies them to a reactive Svelte 5 tree. MdzStream walks that tree and produces DOM. Each layer is replaceable; opcodes are target-agnostic.
Picking a path #
The split is by input regime. The sync parser (paths 1–2) owns random-access input — content you already have as a complete string. The streaming parser (path 3) owns append-only input — content that arrives over time. They implement one grammar; parity tests bind them, with the sync parser as the normative reference.
Use path 1 when the content is fixed at write time or arrives from a synchronous source. The svelte_preprocess_mdz preprocessor can collapse the call to a static render.
Use path 2 when you need custom wrapping markup but still parse all-at-once.
Use path 3 when chunks arrive over time. The output tree is identical to path 1/2 for the same final input, outside the documented adversarial cases (see below).
Opcode design #
A streaming parser can't backtrack — it must emit something coherent for every byte it consumes. mdz handles this with optimistic opens and explicit reverts.
When the parser sees ** it doesn't know yet whether it'll form bold or end up as literal **. It emits open Bold immediately. If a closing ** arrives, it emits close Bold and the speculation succeeded. If a paragraph break or EOF interrupts first, it emits revert Bold — the consumer drops the wrapper, re-parents the children to the grandparent, and prepends the literal ** delimiter as text.
The opcode types are:
open— open a container (Paragraph, Bold, Italic, Link, Heading, List, ListItem, Codeblock, etc.)close— close the previously opened container, with deferred metadata resolved at close time (heading id, link reference). May carrydiscard: truefor whitespace-only paragraphs the consumer should drop.text— create a leaf Text or Code nodeappend_text— extend the last text node (avoids one node per character during plain runs)trim_text— drop trailing characters from a text node (used for trailing-newline trim at block close)void— create a self-contained leaf (Hr)revert— undo an optimistic inline open (block structure is never speculative)wrap— retroactively wrap an existing text node in a Link (auto-links only — may also split trailing punctuation; see below)
The full type definition is in mdz_opcodes.ts.
Why wrap exists #
wrap existsAuto-detected URLs (https://..., /path, ./relative) are the one case where neither optimistic-open nor hold-until-terminator gives a good streaming feel.
If the parser opens a Link optimistically on every leading h, every word starting with h flashes blue before reverting. If it instead holds all bytes until a terminator, a 40-character URL creates a 40-character pause in the rendered output — readers see a stutter.
The wrap opcode resolves both problems. The URL streams as ordinary visible text. When the terminator finally arrives, wrap retroactively re-parents that text node inside a Link. The text content never changes — only its parent changes. Readers see prose flowing naturally, then a single moment where the URL upgrades from prose to clickable link. No flash, no pause.
wrap also handles trailing punctuation trim. For https://fuz.dev., the . is not part of the URL. wrap carries trim_end and trim_id fields that split the text node — the URL portion goes inside the Link, the trailing punctuation becomes a sibling Text node after it.
Determinism and chunk boundaries #
The opcode sequence is not deterministic across different chunk sizes. The same input fed as one chunk versus many produces different intermediate text/append_text splits and different optimistic/revert sequences along the way.
The final rendered tree is deterministic. Bold, italic, and strikethrough open optimistically when no closer is visible yet, and four mechanisms keep the chunked result identical to the one-shot parse:
- Greedy rejection carries over. One-shot parsing rejects an italic opener whose first closer candidate fails its word boundary. When that candidate only arrives in a later chunk, the streaming parser reverts the already-open container (
revert_failed_close) — sothe _user_id fieldnever stays italic, no matter how it's chunked. (Bold and strikethrough have no boundary checks to fail — their doubled delimiters pair anywhere.) - A one-character hold at the delimiter. A
*or~that is the last buffered character waits for the next character to learn whether it doubles into a delimiter; a potential italic closer at the buffer end waits because its word boundary depends on what follows. - Failed closers can re-open. After a failed-closer revert, the same delimiter is re-tried as a fresh opener, matching where the one-shot parse continues.
- EOF gating. At
finish()the buffer is complete, so open decisions stop speculating: bold, italic, and strikethrough open only when their confirmed closer is already in the final buffer, and an inline-code candidate without one degrades to literal text. Content held back during streaming (e.g. behind an undecided backtick) still parses like the one-shot parse when the stream ends.
Four residual divergence classes remain:
- Italic-bounded code spans. An inline-code candidate held across chunks can decide text-vs-code bounded by a wrongly-optimistic italic — one that opened before its failed closer was visible, where the one-shot parse greedy-rejects it and scans unbounded. This needs an
_-bearing code span chunked so the italic opens before the span's closing backtick arrives; italic is the only wedge, since it's the only delimiter whose one-shot form rejects on a failed first closer. - Unclosed optimistic code spans. An inline-code candidate that opened optimistically and never closes consumes its tail as raw code text, so formatting inside it never forms once EOF flattens it back to text — the parser never re-parses.
- EOF-flat links and tags.
finish()doesn't open links or tags, so link/tag syntax held into EOF parses flat. - Block interrupts across optimistic inlines. A column-0 block line (heading, HR, fence, list marker, quote prefix) interrupts the paragraph even when an optimistic inline spans it — the one-shot parse, knowing the closer exists, swallows the line as inline text instead; streaming can't know, and interrupting matches the one-shot parse whenever no closer ever arrives.
Chunked-equals-one-shot is asserted across chunk sizes in src/test/mdz_parser_parity.test.ts, including failed-closer and delimiter-run inputs.
Append-only invariant #
Once emitted, opcodes are never mutated or removed. This means:
- A consumer can persist the opcode stream and replay it later.
- A network protocol can carry opcodes from parser to renderer.
- The renderer never re-parses.
The revert and trim_text opcodes look retroactive but aren't — they're new opcodes the consumer interprets as "drop these nodes" or "shorten this string". The stream itself only grows.
Stated precisely, the invariant is no implicit retroactivity. Corrections to already-emitted output are allowed — but they must be bounded, local, and expressed in the stream itself (revert, wrap, trim_text), so the set of things that can ever visually change is enumerable and testable. Re-parsing is the unbounded, implicit form of correction — "anything in this region may now differ, go figure out what" — which pushes diffing onto every consumer and excludes write-once targets. That is what mdz bans, and it's why the dialect is restricted: every construct must be decidable within a bounded hold or correctable with a local opcode.
This is the core of pngwn's opcode insight: because no opcode is ever mutated or removed, the stream itself is the incremental interface — no tree to produce, no diffing to minimize work, and any target (a Svelte tree, HTML, native views) can consume it.
Consumers #
mdz ships two opcode consumers:
mdz_opcodes_to_nodes(opcodes)— replays an opcode array into the sameMdzNode[]tree that mdz_parse produces. Used by tests to assert parity. Useful when you want the static tree shape but already have opcodes (e.g. cached from an earlier stream).- MdzStreamState — applies opcodes to a reactive Svelte 5 tree of MdzStreamNode instances. Each node's
content,children, and metadata fields are$state, so Svelte updates only what changed. MdzStream renders the tree.
The two consumers' outputs are structurally equivalent (parity tests assert this). MdzStreamState is built for fine-grained reactivity and keeps per-id node identity for granular updates, so it skips a couple of tidies (adjacent-Text merging, single-tag paragraph unwrap) that mdz_opcodes_to_nodes applies at tree-build time.
Limitations #
- Residual divergences under adversarial input, as documented above — italic-bounded and unclosed code spans under adversarial chunking, link/tag syntax held into EOF, and column-0 block lines interrupting paragraphs spanned by an optimistic inline.
- Opcode stream order varies with chunking, but the final tree does not.
- No partial-revert — once a container closes, it's committed. Mid-render edits aren't supported.
- Single-pass — backtracking would defeat the streaming guarantee. Ambiguous syntax (e.g. unclosed
[) renders as visible text via revert rather than re-parsing.
See also #
- MdzStreamParser — the parser class
- MdzStreamState — reactive consumer
- mdz_opcodes_to_nodes — tree consumer
- MdzOpcode — opcode type union
- /docs/usage/grammar — formal grammar for the dialect
svelte_preprocess_mdz #
svelte_preprocess_mdz is a Svelte preprocessor that compiles static mdz content to Svelte markup at build time. Instead of parsing mdz at runtime and rendering dynamically, the preprocessor replaces the Mdz component with MdzPrecompiled containing pre-rendered children.
Setup #
Add the preprocessor, svelte_preprocess_mdz.ts, to svelte.config.js:
import {svelte_preprocess_mdz} from '@fuzdev/mdz/svelte_preprocess_mdz.js';
export default {
preprocess: [
svelte_preprocess_mdz({
components: {Alert: '$lib/Alert.svelte'},
elements: {aside: true, details: true},
}),
// ...other preprocessors
],
}; The preprocessor should run before other preprocessors like vitePreprocess() so it can parse the original Svelte source. The input to svelte_preprocess_mdz is SveltePreprocessMdzOptions.
Code and codeblock components #
The preprocessor mirrors the runtime rendering seam (see introduction): code_component_import sets the component for inline code (receives reference) and codeblock_component_import the component for code
blocks (receives lang and content). When unset, output is plain <code> and <pre><code>, matching the runtime
default. Set them to the same components you inject at runtime and precompiled output stays
identical:
svelte_preprocess_mdz({
code_component_import: '@fuzdev/fuz_ui/DocsLink.svelte',
codeblock_component_import: '@fuzdev/fuz_code/Code.svelte',
})How it works #
The preprocessor transforms static content at build time:
<!-- Before -->
<Mdz content="**bold** and `some_fn`" />
<!-- After -->
<MdzPrecompiled><p><strong>bold</strong> and <DocsLink reference={'some_fn'} /></p></MdzPrecompiled> For ternary expressions with static branches, it generates Svelte control flow:
<!-- Before -->
<Mdz content={show ? '**a**' : '**b**'} />
<!-- After -->
<MdzPrecompiled>{#if show}<p><strong>a</strong></p>{:else}<p><strong>b</strong></p>{/if}</MdzPrecompiled> The preprocessor also manages imports automatically:
- adds imports required by the rendered content (e.g., DocsLink,
Code,resolve, configured components) - removes the Mdz import when all usages are transformed
- removes dead
constbindings consumed only by transformed content
What gets transformed #
The preprocessor handles these static content patterns:
- string attributes:
content="**bold**" - JS string expressions:
content={'**bold**'} - template literals without interpolation:
content={`**bold**`} - const variable references:
const msg = '**bold**';content={msg} - ternary chains:
content={show ? '**a**' : '**b**'} - nested ternaries:
content={a ? 'x' : b ? 'y' : 'z'}
Relative paths and the base attribute #
Content with relative auto-links (./grammar, ../streaming) needs to
know its base path to resolve those at compile time. Add a static base attribute
to the Mdz tag:
<Mdz base="/docs/usage/" content="see ./grammar and ../streaming" /> The preprocessor reads base, resolves relative paths to absolute via mdz_resolve_relative_path(), and emits the resolved href values. base must be a static string literal — dynamic expressions cause the call to fall back
to runtime rendering.
Without base, relative paths are kept as raw hrefs and the browser resolves them
against the current URL at click time. This is a preprocessor-only attribute; at runtime Mdz accepts a base prop with the same meaning.
Skip conditions #
The preprocessor falls back to runtime rendering when:
- the file is excluded via
exclude - no matching import source is found for Mdz
- the import is
import type(not a runtime import) - MdzPrecompiled is already imported from a different source
- the
contentprop is dynamic (variable, function call,$state,$derived) - spread attributes are present (
{...props}) - content references unconfigured components or elements
- a ternary branch has dynamic content or unconfigured tags
api #
Browse the full api docs.
library #
npm i -D @fuzdev/mdz
fixtures #
Browse the 412 test fixtures.