defuss-markdown
v1.0.1
Published
Incremental Markdown -> Defuss JSX bridge built on Incremark core.
Maintainers
Readme
defuss-markdown
Incremental Markdown rendering for Defuss, using Incremark as the parser core. It also supports custom JSX / component rendering.
Install
bun add defuss-markdownUsage
import { updateWithMarkdown } from "defuss-markdown";
await updateWithMarkdown(containerOrRef, markdownStream);containerOrRef can be:
- a DOM
Element - a Defuss ref exposing
render(jsx) - a Defuss ref exposing
update(jsx) - a ref-like object with
current: Element
markdownStream can be:
stringPromise<string>AsyncIterable<string>AsyncIterator<string>
What it does
- streams chunks into Incremark
- tracks incremental block updates
- maps Markdown AST nodes to Defuss-compatible JSX/VNodes
- renders keyed blocks so Defuss can morph incrementally
API
async function updateWithMarkdown(
target: Element | Ref,
input: string | Promise<string> | AsyncIterable<string> | AsyncIterator<string>,
options?: UpdateWithMarkdownOptions,
): Promise<void>Options
| Option | Type | Description |
|--------|------|-------------|
| parser | ParserLike | Inject your own parser instance |
| createParser | () => ParserLike \| Promise<ParserLike> | Inject a parser factory (sync or async) |
| parserOptions | Record<string, unknown> | Passed to the default Incremark parser constructor/factory |
| render | (jsx, target) => void | Custom render function (skips dynamic defuss import) |
| allowDangerousHtml | boolean | Allow raw HTML nodes to use dangerouslySetInnerHTML (default: false) |
| blockWrapper | (block, rendered) => VNodeChild | Wrap each rendered block with custom JSX |
| nodeRenderer | (node, context) => VNodeChild \| FALL_THROUGH | Override or extend node rendering per-node |
FALL_THROUGH
A sentinel symbol exported alongside updateWithMarkdown. Return it from a custom nodeRenderer to fall back to the built-in renderer for that node.
The second argument context exposes renderChildren(node.children, context) and renderNode(node, context) so you can recursively render a node's children inside your custom wrapper:
import { updateWithMarkdown, FALL_THROUGH } from "defuss-markdown";
await updateWithMarkdown(target, markdown, {
nodeRenderer: (node, context) => {
if (node.type === "paragraph") {
// Wrap paragraphs in a custom <div>, but still render their children
return <div class="custom-p">{...context.renderChildren(node.children, context)}</div>;
}
return FALL_THROUGH; // default rendering for everything else
},
});Supported Markdown AST Nodes
The following node types are mapped to Defuss VNodes:
| AST Node | HTML Output |
|----------|-------------|
| paragraph | <p> |
| heading (depth 1-6) | <h1>-<h6> |
| text | text node |
| strong | <strong> |
| emphasis | <em> |
| delete | <del> |
| inlineCode | <code> |
| code | <pre><code> (with optional language-* class) |
| blockquote | <blockquote> |
| list | <ul> or <ol> (with start attr) |
| listItem | <li> (with <input type="checkbox"> when checked is boolean) |
| link | <a> |
| image | <img> |
| break | <br> |
| thematicBreak | <hr> |
| table | <table> with <thead>/<tbody>, cell alignment via style |
| html | <pre> (safe) or <div dangerouslySetInnerHTML> (when allowed) |
| math | <pre class="language-math"> |
| inlineMath | <code class="language-math"> |
| footnoteReference | <sup><a href="#fn-{id}"> |
| footnoteDefinition | <div id="fn-{id}" class="footnote-definition"> |
| yaml | <pre> |
| definition | (omitted) |
| root | children rendered directly |
| unknown | <div> or <span> with data-md-node attr |
Parser Interface (ParserLike)
Any object satisfying ParserLike can be injected. The library reads blocks through multiple fallback paths:
parser.getBlocks()methodparser.getCompletedBlocks()+parser.pendingBlock/parser.pendingBlocksparser.blockspropertyparser.completedBlocks+parser.pendingBlocks/parser.pendingBlock- Incremental update objects returned from
append()withcompleted/pending/updated/removedkeys
The parser must expose at least one of append(chunk) or render(content).
Streaming Example
import { updateWithMarkdown } from "defuss-markdown";
const stream = async function* () {
yield "# Hello\n\n";
yield "This is **streamed** markdown.\n";
};
await updateWithMarkdown(containerEl, stream(), {
render: (jsx, target) => myRender(jsx, target),
});Notes
By default the package tries to resolve either:
createIncremarkParser(...)from@incremark/corenew IncremarkParser(...)from@incremark/core
and then uses Defuss render(...) for Element targets.
For fully controlled environments, inject both the parser and render function through options.
Development
bun install
bun test
bun run test:coverage # with v8 coverage report (text + html + lcov)
bun run buildCoverage reports are generated in the coverage/ directory.
