defuss-markdown 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +159 -0
- package/dist/index.d.ts +68 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +477 -0
- package/package.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aron Homberg
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# defuss-markdown
|
|
2
|
+
|
|
3
|
+
Incremental Markdown rendering for Defuss, using Incremark as the parser core.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add defuss-markdown
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { updateWithMarkdown } from "defuss-markdown";
|
|
15
|
+
|
|
16
|
+
await updateWithMarkdown(containerOrRef, markdownStream);
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`containerOrRef` can be:
|
|
20
|
+
|
|
21
|
+
- a DOM `Element`
|
|
22
|
+
- a Defuss ref exposing `render(jsx)`
|
|
23
|
+
- a Defuss ref exposing `update(jsx)`
|
|
24
|
+
- a ref-like object with `current: Element`
|
|
25
|
+
|
|
26
|
+
`markdownStream` can be:
|
|
27
|
+
|
|
28
|
+
- `string`
|
|
29
|
+
- `Promise<string>`
|
|
30
|
+
- `AsyncIterable<string>`
|
|
31
|
+
- `AsyncIterator<string>`
|
|
32
|
+
|
|
33
|
+
## What it does
|
|
34
|
+
|
|
35
|
+
- streams chunks into Incremark
|
|
36
|
+
- tracks incremental block updates
|
|
37
|
+
- maps Markdown AST nodes to Defuss-compatible JSX/VNodes
|
|
38
|
+
- renders keyed blocks so Defuss can morph incrementally
|
|
39
|
+
|
|
40
|
+
## API
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
async function updateWithMarkdown(
|
|
44
|
+
target: Element | Ref,
|
|
45
|
+
input: string | Promise<string> | AsyncIterable<string> | AsyncIterator<string>,
|
|
46
|
+
options?: UpdateWithMarkdownOptions,
|
|
47
|
+
): Promise<void>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Options
|
|
51
|
+
|
|
52
|
+
| Option | Type | Description |
|
|
53
|
+
|--------|------|-------------|
|
|
54
|
+
| `parser` | `ParserLike` | Inject your own parser instance |
|
|
55
|
+
| `createParser` | `() => ParserLike \| Promise<ParserLike>` | Inject a parser factory (sync or async) |
|
|
56
|
+
| `parserOptions` | `Record<string, unknown>` | Passed to the default Incremark parser constructor/factory |
|
|
57
|
+
| `render` | `(jsx, target) => void` | Custom render function (skips dynamic `defuss` import) |
|
|
58
|
+
| `allowDangerousHtml` | `boolean` | Allow raw HTML nodes to use `dangerouslySetInnerHTML` (default: `false`) |
|
|
59
|
+
| `blockWrapper` | `(block, rendered) => VNodeChild` | Wrap each rendered block with custom JSX |
|
|
60
|
+
| `nodeRenderer` | `(node, context) => VNodeChild \| FALL_THROUGH` | Override or extend node rendering per-node |
|
|
61
|
+
|
|
62
|
+
### `FALL_THROUGH`
|
|
63
|
+
|
|
64
|
+
A sentinel symbol exported alongside `updateWithMarkdown`. Return it from a custom `nodeRenderer` to fall back to the built-in renderer for that node:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import { updateWithMarkdown, FALL_THROUGH } from "defuss-markdown";
|
|
68
|
+
|
|
69
|
+
await updateWithMarkdown(target, markdown, {
|
|
70
|
+
nodeRenderer: (node) => {
|
|
71
|
+
if (node.type === "paragraph") {
|
|
72
|
+
// Custom paragraph rendering
|
|
73
|
+
return { type: "div", attributes: { class: "custom-p" }, children: [] };
|
|
74
|
+
}
|
|
75
|
+
return FALL_THROUGH; // default rendering for everything else
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Supported Markdown AST Nodes
|
|
81
|
+
|
|
82
|
+
The following node types are mapped to Defuss VNodes:
|
|
83
|
+
|
|
84
|
+
| AST Node | HTML Output |
|
|
85
|
+
|----------|-------------|
|
|
86
|
+
| `paragraph` | `<p>` |
|
|
87
|
+
| `heading` (depth 1-6) | `<h1>`-`<h6>` |
|
|
88
|
+
| `text` | text node |
|
|
89
|
+
| `strong` | `<strong>` |
|
|
90
|
+
| `emphasis` | `<em>` |
|
|
91
|
+
| `delete` | `<del>` |
|
|
92
|
+
| `inlineCode` | `<code>` |
|
|
93
|
+
| `code` | `<pre><code>` (with optional `language-*` class) |
|
|
94
|
+
| `blockquote` | `<blockquote>` |
|
|
95
|
+
| `list` | `<ul>` or `<ol>` (with `start` attr) |
|
|
96
|
+
| `listItem` | `<li>` (with `<input type="checkbox">` when `checked` is boolean) |
|
|
97
|
+
| `link` | `<a>` |
|
|
98
|
+
| `image` | `<img>` |
|
|
99
|
+
| `break` | `<br>` |
|
|
100
|
+
| `thematicBreak` | `<hr>` |
|
|
101
|
+
| `table` | `<table>` with `<thead>`/`<tbody>`, cell alignment via `style` |
|
|
102
|
+
| `html` | `<pre>` (safe) or `<div dangerouslySetInnerHTML>` (when allowed) |
|
|
103
|
+
| `math` | `<pre class="language-math">` |
|
|
104
|
+
| `inlineMath` | `<code class="language-math">` |
|
|
105
|
+
| `footnoteReference` | `<sup><a href="#fn-{id}">` |
|
|
106
|
+
| `footnoteDefinition` | `<div id="fn-{id}" class="footnote-definition">` |
|
|
107
|
+
| `yaml` | `<pre>` |
|
|
108
|
+
| `definition` | _(omitted)_ |
|
|
109
|
+
| `root` | children rendered directly |
|
|
110
|
+
| unknown | `<div>` or `<span>` with `data-md-node` attr |
|
|
111
|
+
|
|
112
|
+
### Parser Interface (`ParserLike`)
|
|
113
|
+
|
|
114
|
+
Any object satisfying `ParserLike` can be injected. The library reads blocks through multiple fallback paths:
|
|
115
|
+
|
|
116
|
+
1. `parser.getBlocks()` method
|
|
117
|
+
2. `parser.getCompletedBlocks()` + `parser.pendingBlock` / `parser.pendingBlocks`
|
|
118
|
+
3. `parser.blocks` property
|
|
119
|
+
4. `parser.completedBlocks` + `parser.pendingBlocks` / `parser.pendingBlock`
|
|
120
|
+
5. Incremental update objects returned from `append()` with `completed` / `pending` / `updated` / `removed` keys
|
|
121
|
+
|
|
122
|
+
The parser must expose at least one of `append(chunk)` or `render(content)`.
|
|
123
|
+
|
|
124
|
+
### Streaming Example
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
import { updateWithMarkdown } from "defuss-markdown";
|
|
128
|
+
|
|
129
|
+
const stream = async function* () {
|
|
130
|
+
yield "# Hello\n\n";
|
|
131
|
+
yield "This is **streamed** markdown.\n";
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
await updateWithMarkdown(containerEl, stream(), {
|
|
135
|
+
render: (jsx, target) => myRender(jsx, target),
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Notes
|
|
140
|
+
|
|
141
|
+
By default the package tries to resolve either:
|
|
142
|
+
|
|
143
|
+
- `createIncremarkParser(...)` from `@incremark/core`
|
|
144
|
+
- `new IncremarkParser(...)` from `@incremark/core`
|
|
145
|
+
|
|
146
|
+
and then uses Defuss `render(...)` for Element targets.
|
|
147
|
+
|
|
148
|
+
For fully controlled environments, inject both the parser and render function through `options`.
|
|
149
|
+
|
|
150
|
+
## Development
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
bun install
|
|
154
|
+
bun test
|
|
155
|
+
bun run test:coverage # with v8 coverage report (text + html + lcov)
|
|
156
|
+
bun run build
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Coverage reports are generated in the `coverage/` directory.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { VNodeChild, Ref } from 'defuss';
|
|
2
|
+
|
|
3
|
+
type DefussPrimitive = string | number | boolean | null | undefined;
|
|
4
|
+
interface MarkdownBlockLike {
|
|
5
|
+
id: string;
|
|
6
|
+
node: MarkdownAstNode;
|
|
7
|
+
status?: "pending" | "completed" | string;
|
|
8
|
+
}
|
|
9
|
+
interface MarkdownAstNode {
|
|
10
|
+
type: string;
|
|
11
|
+
children?: MarkdownAstNode[];
|
|
12
|
+
value?: string;
|
|
13
|
+
depth?: number;
|
|
14
|
+
ordered?: boolean;
|
|
15
|
+
start?: number;
|
|
16
|
+
spread?: boolean;
|
|
17
|
+
checked?: boolean | null;
|
|
18
|
+
lang?: string | null;
|
|
19
|
+
meta?: string | null;
|
|
20
|
+
url?: string;
|
|
21
|
+
alt?: string;
|
|
22
|
+
title?: string | null;
|
|
23
|
+
align?: Array<"left" | "right" | "center" | null>;
|
|
24
|
+
identifier?: string;
|
|
25
|
+
label?: string | null;
|
|
26
|
+
referenceType?: string;
|
|
27
|
+
name?: string;
|
|
28
|
+
attributes?: Array<{
|
|
29
|
+
type?: string;
|
|
30
|
+
name?: string;
|
|
31
|
+
value?: string;
|
|
32
|
+
}>;
|
|
33
|
+
data?: Record<string, unknown>;
|
|
34
|
+
[key: string]: unknown;
|
|
35
|
+
}
|
|
36
|
+
interface ParserLike {
|
|
37
|
+
append?: (chunk: string) => unknown;
|
|
38
|
+
finalize?: () => unknown;
|
|
39
|
+
render?: (content: string) => unknown;
|
|
40
|
+
reset?: () => void;
|
|
41
|
+
blocks?: MarkdownBlockLike[];
|
|
42
|
+
completedBlocks?: MarkdownBlockLike[];
|
|
43
|
+
pendingBlocks?: MarkdownBlockLike[];
|
|
44
|
+
pendingBlock?: MarkdownBlockLike | null;
|
|
45
|
+
getBlocks?: () => MarkdownBlockLike[];
|
|
46
|
+
getCompletedBlocks?: () => MarkdownBlockLike[];
|
|
47
|
+
[key: string]: unknown;
|
|
48
|
+
}
|
|
49
|
+
interface UpdateWithMarkdownOptions {
|
|
50
|
+
parser?: ParserLike;
|
|
51
|
+
createParser?: () => ParserLike | Promise<ParserLike>;
|
|
52
|
+
parserOptions?: Record<string, unknown>;
|
|
53
|
+
render?: (jsx: VNodeChild, target: Element) => unknown | Promise<unknown>;
|
|
54
|
+
allowDangerousHtml?: boolean;
|
|
55
|
+
blockWrapper?: (block: MarkdownBlockLike, rendered: VNodeChild) => VNodeChild;
|
|
56
|
+
nodeRenderer?: (node: MarkdownAstNode, context: RenderContext) => VNodeChild | typeof FALL_THROUGH;
|
|
57
|
+
}
|
|
58
|
+
declare const FALL_THROUGH: unique symbol;
|
|
59
|
+
interface RenderContext {
|
|
60
|
+
allowDangerousHtml: boolean;
|
|
61
|
+
nodeRenderer?: UpdateWithMarkdownOptions["nodeRenderer"];
|
|
62
|
+
cellTag?: "td" | "th";
|
|
63
|
+
}
|
|
64
|
+
declare function updateWithMarkdown(target: Element | Ref, input: string | Promise<string> | AsyncIterable<string> | AsyncIterator<string>, options?: UpdateWithMarkdownOptions): Promise<void>;
|
|
65
|
+
|
|
66
|
+
export { FALL_THROUGH, updateWithMarkdown };
|
|
67
|
+
export type { DefussPrimitive, MarkdownAstNode, MarkdownBlockLike, ParserLike, UpdateWithMarkdownOptions };
|
|
68
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sources":["../../src/index.ts"],"mappings":"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","names":[]}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
const FALL_THROUGH = Symbol("defuss-markdown:fallback-renderer");
|
|
2
|
+
const DEFAULT_PARSER_OPTIONS = Object.freeze({
|
|
3
|
+
gfm: true,
|
|
4
|
+
math: true,
|
|
5
|
+
htmlTree: true,
|
|
6
|
+
containers: true
|
|
7
|
+
});
|
|
8
|
+
async function updateWithMarkdown(target, input, options = {}) {
|
|
9
|
+
const parser = await getParser(options);
|
|
10
|
+
const state = createBlockState();
|
|
11
|
+
if (isAsyncIterable(input) || isAsyncIterator(input)) {
|
|
12
|
+
for await (const chunk of toAsyncIterable(input)) {
|
|
13
|
+
const update = appendChunk(parser, chunk);
|
|
14
|
+
hydrateStateFromParserOrUpdate(parser, update, state);
|
|
15
|
+
await renderIntoTarget(target, snapshotBlocks(state), options);
|
|
16
|
+
}
|
|
17
|
+
const finalizeUpdate2 = parser.finalize?.();
|
|
18
|
+
hydrateStateFromParserOrUpdate(parser, finalizeUpdate2, state);
|
|
19
|
+
await renderIntoTarget(target, snapshotBlocks(state), options);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const content = await input;
|
|
23
|
+
appendChunk(parser, content);
|
|
24
|
+
const finalizeUpdate = parser.finalize?.();
|
|
25
|
+
hydrateStateFromParserOrUpdate(parser, finalizeUpdate, state);
|
|
26
|
+
await renderIntoTarget(target, snapshotBlocks(state), options);
|
|
27
|
+
}
|
|
28
|
+
async function getParser(options) {
|
|
29
|
+
if (options.parser) {
|
|
30
|
+
return options.parser;
|
|
31
|
+
}
|
|
32
|
+
if (options.createParser) {
|
|
33
|
+
return await options.createParser();
|
|
34
|
+
}
|
|
35
|
+
const incremark = await dynamicModuleImport("@incremark/core");
|
|
36
|
+
const parserOptions = { ...DEFAULT_PARSER_OPTIONS, ...options.parserOptions ?? {} };
|
|
37
|
+
if (typeof incremark?.createIncremarkParser === "function") {
|
|
38
|
+
return incremark.createIncremarkParser(parserOptions);
|
|
39
|
+
}
|
|
40
|
+
if (typeof incremark?.IncremarkParser === "function") {
|
|
41
|
+
return new incremark.IncremarkParser(parserOptions);
|
|
42
|
+
}
|
|
43
|
+
if (typeof incremark?.default === "function") {
|
|
44
|
+
try {
|
|
45
|
+
return new incremark.default(parserOptions);
|
|
46
|
+
} catch {
|
|
47
|
+
return incremark.default(parserOptions);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
throw new Error(
|
|
51
|
+
"Could not construct an Incremark parser. Provide options.parser or options.createParser, or install @incremark/core with either createIncremarkParser() or IncremarkParser exported."
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
function appendChunk(parser, chunk) {
|
|
55
|
+
if (typeof parser.append === "function") {
|
|
56
|
+
return parser.append(chunk);
|
|
57
|
+
}
|
|
58
|
+
if (typeof parser.render === "function") {
|
|
59
|
+
return parser.render(chunk);
|
|
60
|
+
}
|
|
61
|
+
throw new Error("The provided parser does not expose append() or render().");
|
|
62
|
+
}
|
|
63
|
+
async function renderIntoTarget(target, blocks, options) {
|
|
64
|
+
const jsx = renderBlocks(blocks, options);
|
|
65
|
+
if (isRefLike(target)) {
|
|
66
|
+
const refRenderer = typeof target.render === "function" ? target.render : typeof target.update === "function" ? target.update : null;
|
|
67
|
+
if (refRenderer) {
|
|
68
|
+
await refRenderer.call(target, jsx);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (target.current instanceof Element) {
|
|
72
|
+
await renderIntoElement(target.current, jsx, options);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
throw new Error("Ref target has neither render()/update() nor a current Element.");
|
|
76
|
+
}
|
|
77
|
+
await renderIntoElement(target, jsx, options);
|
|
78
|
+
}
|
|
79
|
+
async function renderIntoElement(target, jsx, options) {
|
|
80
|
+
if (options.render) {
|
|
81
|
+
await options.render(jsx, target);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const defuss = await dynamicModuleImport("defuss");
|
|
85
|
+
const render = defuss?.render ?? defuss?.default?.render ?? defuss?.default ?? null;
|
|
86
|
+
if (typeof render !== "function") {
|
|
87
|
+
throw new Error(
|
|
88
|
+
"Could not resolve defuss render(). Provide options.render, or install defuss exporting render()."
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
await render(jsx, target);
|
|
92
|
+
}
|
|
93
|
+
function renderBlocks(blocks, options) {
|
|
94
|
+
const context = {
|
|
95
|
+
allowDangerousHtml: Boolean(options.allowDangerousHtml),
|
|
96
|
+
nodeRenderer: options.nodeRenderer
|
|
97
|
+
};
|
|
98
|
+
return blocks.map((block) => {
|
|
99
|
+
const rendered = applyBlockKey(
|
|
100
|
+
block.id,
|
|
101
|
+
renderNode(block.node, context),
|
|
102
|
+
block
|
|
103
|
+
);
|
|
104
|
+
if (typeof options.blockWrapper === "function") {
|
|
105
|
+
return options.blockWrapper(block, rendered);
|
|
106
|
+
}
|
|
107
|
+
return rendered;
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
function renderNode(node, context) {
|
|
111
|
+
const custom = context.nodeRenderer?.(node, context);
|
|
112
|
+
if (custom !== void 0 && custom !== FALL_THROUGH) {
|
|
113
|
+
return custom;
|
|
114
|
+
}
|
|
115
|
+
switch (node.type) {
|
|
116
|
+
case "root":
|
|
117
|
+
return renderChildren(node.children, context);
|
|
118
|
+
case "paragraph":
|
|
119
|
+
return h("p", {}, renderChildren(node.children, context));
|
|
120
|
+
case "heading": {
|
|
121
|
+
const depth = clampHeadingDepth(node.depth);
|
|
122
|
+
return h(`h${depth}`, {}, renderChildren(node.children, context));
|
|
123
|
+
}
|
|
124
|
+
case "text":
|
|
125
|
+
return node.value ?? "";
|
|
126
|
+
case "strong":
|
|
127
|
+
return h("strong", {}, renderChildren(node.children, context));
|
|
128
|
+
case "emphasis":
|
|
129
|
+
return h("em", {}, renderChildren(node.children, context));
|
|
130
|
+
case "delete":
|
|
131
|
+
return h("del", {}, renderChildren(node.children, context));
|
|
132
|
+
case "inlineCode":
|
|
133
|
+
return h("code", {}, node.value ?? "");
|
|
134
|
+
case "code": {
|
|
135
|
+
const className = node.lang ? [`language-${String(node.lang)}`] : [];
|
|
136
|
+
const codeAttrs = className.length > 0 ? { class: className } : {};
|
|
137
|
+
return h("pre", {}, h("code", codeAttrs, node.value ?? ""));
|
|
138
|
+
}
|
|
139
|
+
case "blockquote":
|
|
140
|
+
return h("blockquote", {}, renderChildren(node.children, context));
|
|
141
|
+
case "list": {
|
|
142
|
+
const tag = node.ordered ? "ol" : "ul";
|
|
143
|
+
const attrs = {};
|
|
144
|
+
if (node.ordered && typeof node.start === "number" && node.start !== 1) {
|
|
145
|
+
attrs.start = node.start;
|
|
146
|
+
}
|
|
147
|
+
return h(tag, attrs, renderChildren(node.children, context));
|
|
148
|
+
}
|
|
149
|
+
case "listItem": {
|
|
150
|
+
const children = [...normalizeChildren(renderChildren(node.children, context))];
|
|
151
|
+
if (typeof node.checked === "boolean") {
|
|
152
|
+
children.unshift(
|
|
153
|
+
h("input", {
|
|
154
|
+
type: "checkbox",
|
|
155
|
+
checked: node.checked,
|
|
156
|
+
disabled: true
|
|
157
|
+
}),
|
|
158
|
+
" "
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
return h("li", {}, children);
|
|
162
|
+
}
|
|
163
|
+
case "link": {
|
|
164
|
+
const attrs = { href: node.url ?? "" };
|
|
165
|
+
if (node.title) {
|
|
166
|
+
attrs.title = node.title;
|
|
167
|
+
}
|
|
168
|
+
return h("a", attrs, renderChildren(node.children, context));
|
|
169
|
+
}
|
|
170
|
+
case "image": {
|
|
171
|
+
const attrs = {
|
|
172
|
+
src: node.url ?? "",
|
|
173
|
+
alt: node.alt ?? ""
|
|
174
|
+
};
|
|
175
|
+
if (node.title) {
|
|
176
|
+
attrs.title = node.title;
|
|
177
|
+
}
|
|
178
|
+
return h("img", attrs);
|
|
179
|
+
}
|
|
180
|
+
case "break":
|
|
181
|
+
return h("br", {});
|
|
182
|
+
case "thematicBreak":
|
|
183
|
+
return h("hr", {});
|
|
184
|
+
case "table":
|
|
185
|
+
return renderTable(node, context);
|
|
186
|
+
case "tableRow":
|
|
187
|
+
return h("tr", {}, renderTableRow(node, context.cellTag ?? "td", context));
|
|
188
|
+
case "tableCell":
|
|
189
|
+
return h(context.cellTag ?? "td", {}, renderChildren(node.children, context));
|
|
190
|
+
case "html": {
|
|
191
|
+
const html = String(node.value ?? "");
|
|
192
|
+
if (!context.allowDangerousHtml) {
|
|
193
|
+
return h("pre", {}, html);
|
|
194
|
+
}
|
|
195
|
+
return h("div", {
|
|
196
|
+
dangerouslySetInnerHTML: { __html: html }
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
case "math":
|
|
200
|
+
return h("pre", { class: ["language-math"] }, node.value ?? "");
|
|
201
|
+
case "inlineMath":
|
|
202
|
+
return h("code", { class: ["language-math"] }, node.value ?? "");
|
|
203
|
+
case "footnoteReference": {
|
|
204
|
+
const label = String(node.identifier ?? node.label ?? "fn");
|
|
205
|
+
return h(
|
|
206
|
+
"sup",
|
|
207
|
+
{},
|
|
208
|
+
h("a", { href: `#fn-${label}` }, `[${label}]`)
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
case "footnoteDefinition": {
|
|
212
|
+
const label = String(node.identifier ?? node.label ?? "fn");
|
|
213
|
+
return h(
|
|
214
|
+
"div",
|
|
215
|
+
{ id: `fn-${label}`, class: ["footnote-definition"] },
|
|
216
|
+
renderChildren(node.children, context)
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
case "yaml":
|
|
220
|
+
return h("pre", {}, node.value ?? "");
|
|
221
|
+
case "definition":
|
|
222
|
+
return null;
|
|
223
|
+
default:
|
|
224
|
+
return renderUnknownNode(node, context);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
function renderUnknownNode(node, context) {
|
|
228
|
+
if (Array.isArray(node.children) && node.children.length > 0) {
|
|
229
|
+
return h(
|
|
230
|
+
inferWrapperTag(node.type),
|
|
231
|
+
{ "data-md-node": node.type },
|
|
232
|
+
renderChildren(node.children, context)
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
if (typeof node.value === "string") {
|
|
236
|
+
return h(inferWrapperTag(node.type), { "data-md-node": node.type }, node.value);
|
|
237
|
+
}
|
|
238
|
+
return h(inferWrapperTag(node.type), { "data-md-node": node.type });
|
|
239
|
+
}
|
|
240
|
+
function renderTable(node, context) {
|
|
241
|
+
const rows = Array.isArray(node.children) ? node.children : [];
|
|
242
|
+
if (rows.length === 0) {
|
|
243
|
+
return h("table", {});
|
|
244
|
+
}
|
|
245
|
+
const [headerRow, ...bodyRows] = rows;
|
|
246
|
+
const alignments = Array.isArray(node.align) ? node.align : [];
|
|
247
|
+
return h(
|
|
248
|
+
"table",
|
|
249
|
+
{},
|
|
250
|
+
h(
|
|
251
|
+
"thead",
|
|
252
|
+
{},
|
|
253
|
+
renderTableRow(headerRow, "th", { ...context, cellTag: "th" }, alignments)
|
|
254
|
+
),
|
|
255
|
+
bodyRows.length > 0 ? h(
|
|
256
|
+
"tbody",
|
|
257
|
+
{},
|
|
258
|
+
bodyRows.map(
|
|
259
|
+
(row) => renderTableRow(row, "td", { ...context, cellTag: "td" }, alignments)
|
|
260
|
+
)
|
|
261
|
+
) : null
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
function renderTableRow(row, cellTag, context, alignments = []) {
|
|
265
|
+
const cells = Array.isArray(row.children) ? row.children : [];
|
|
266
|
+
return h(
|
|
267
|
+
"tr",
|
|
268
|
+
{},
|
|
269
|
+
cells.map((cell, index) => {
|
|
270
|
+
const align = alignments[index];
|
|
271
|
+
const attrs = {};
|
|
272
|
+
if (align) {
|
|
273
|
+
attrs.style = { textAlign: align };
|
|
274
|
+
}
|
|
275
|
+
return h(
|
|
276
|
+
cellTag,
|
|
277
|
+
attrs,
|
|
278
|
+
renderChildren(cell.children, { ...context, cellTag })
|
|
279
|
+
);
|
|
280
|
+
})
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
function renderChildren(children, context) {
|
|
284
|
+
if (!Array.isArray(children) || children.length === 0) {
|
|
285
|
+
return [];
|
|
286
|
+
}
|
|
287
|
+
return children.map((child) => renderNode(child, context));
|
|
288
|
+
}
|
|
289
|
+
function applyBlockKey(blockId, rendered, block) {
|
|
290
|
+
if (isVNode(rendered)) {
|
|
291
|
+
rendered.attributes = {
|
|
292
|
+
...rendered.attributes ?? {},
|
|
293
|
+
key: blockId,
|
|
294
|
+
"data-block-id": blockId,
|
|
295
|
+
"data-block-status": block.status ?? "pending"
|
|
296
|
+
};
|
|
297
|
+
return rendered;
|
|
298
|
+
}
|
|
299
|
+
return h(
|
|
300
|
+
"div",
|
|
301
|
+
{
|
|
302
|
+
key: blockId,
|
|
303
|
+
"data-block-id": blockId,
|
|
304
|
+
"data-block-status": block.status ?? "pending"
|
|
305
|
+
},
|
|
306
|
+
rendered
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
function createBlockState() {
|
|
310
|
+
return {
|
|
311
|
+
order: [],
|
|
312
|
+
map: /* @__PURE__ */ new Map()
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
function hydrateStateFromParserOrUpdate(parser, update, state) {
|
|
316
|
+
const directSnapshot = extractSnapshotBlocksFromUpdate(update);
|
|
317
|
+
if (directSnapshot) {
|
|
318
|
+
replaceState(state, directSnapshot);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const parserSnapshot = extractBlocksFromParser(parser);
|
|
322
|
+
if (parserSnapshot) {
|
|
323
|
+
replaceState(state, parserSnapshot);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
applyIncrementalCollections(state, update);
|
|
327
|
+
}
|
|
328
|
+
function extractSnapshotBlocksFromUpdate(update) {
|
|
329
|
+
if (!update || typeof update !== "object") {
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
const value = update;
|
|
333
|
+
const direct = pickBlockArray(value.blocks);
|
|
334
|
+
return direct.length > 0 ? direct : null;
|
|
335
|
+
}
|
|
336
|
+
function extractBlocksFromParser(parser) {
|
|
337
|
+
const methodBlocks = pickBlockArray(parser.getBlocks?.());
|
|
338
|
+
if (methodBlocks.length > 0) {
|
|
339
|
+
return methodBlocks;
|
|
340
|
+
}
|
|
341
|
+
const methodMerged = mergeCompletedAndPending(
|
|
342
|
+
pickBlockArray(parser.getCompletedBlocks?.()),
|
|
343
|
+
pickBlockArray(parser.pendingBlocks ?? parser.pendingBlock)
|
|
344
|
+
);
|
|
345
|
+
if (methodMerged.length > 0) {
|
|
346
|
+
return methodMerged;
|
|
347
|
+
}
|
|
348
|
+
const propBlocks = pickBlockArray(parser.blocks);
|
|
349
|
+
if (propBlocks.length > 0) {
|
|
350
|
+
return propBlocks;
|
|
351
|
+
}
|
|
352
|
+
const propMerged = mergeCompletedAndPending(
|
|
353
|
+
pickBlockArray(parser.completedBlocks),
|
|
354
|
+
pickBlockArray(parser.pendingBlocks ?? parser.pendingBlock)
|
|
355
|
+
);
|
|
356
|
+
return propMerged.length > 0 ? propMerged : null;
|
|
357
|
+
}
|
|
358
|
+
function applyIncrementalCollections(state, update) {
|
|
359
|
+
if (!update || typeof update !== "object") {
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
const value = update;
|
|
363
|
+
for (const key of ["completed", "updated", "pending"]) {
|
|
364
|
+
for (const block of pickBlockArray(value[key])) {
|
|
365
|
+
upsertBlock(state, block);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
for (const block of pickBlockArray(value.removed)) {
|
|
369
|
+
removeBlock(state, block.id);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
function replaceState(state, blocks) {
|
|
373
|
+
state.order = [];
|
|
374
|
+
state.map.clear();
|
|
375
|
+
for (const block of blocks) {
|
|
376
|
+
upsertBlock(state, block);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
function upsertBlock(state, block) {
|
|
380
|
+
if (!state.map.has(block.id)) {
|
|
381
|
+
state.order.push(block.id);
|
|
382
|
+
}
|
|
383
|
+
state.map.set(block.id, block);
|
|
384
|
+
}
|
|
385
|
+
function removeBlock(state, id) {
|
|
386
|
+
state.map.delete(id);
|
|
387
|
+
state.order = state.order.filter((entry) => entry !== id);
|
|
388
|
+
}
|
|
389
|
+
function snapshotBlocks(state) {
|
|
390
|
+
return state.order.map((id) => state.map.get(id)).filter((block) => Boolean(block));
|
|
391
|
+
}
|
|
392
|
+
function mergeCompletedAndPending(completed, pending) {
|
|
393
|
+
return [...completed, ...pending];
|
|
394
|
+
}
|
|
395
|
+
function pickBlockArray(value) {
|
|
396
|
+
if (Array.isArray(value)) {
|
|
397
|
+
return value.filter(isBlockLike);
|
|
398
|
+
}
|
|
399
|
+
if (isBlockLike(value)) {
|
|
400
|
+
return [value];
|
|
401
|
+
}
|
|
402
|
+
return [];
|
|
403
|
+
}
|
|
404
|
+
function isBlockLike(value) {
|
|
405
|
+
return Boolean(
|
|
406
|
+
value && typeof value === "object" && typeof value.id === "string" && value.node && typeof value.node === "object"
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
function h(type, attributes = {}, ...children) {
|
|
410
|
+
return {
|
|
411
|
+
type,
|
|
412
|
+
attributes,
|
|
413
|
+
children: normalizeChildren(children)
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
function normalizeChildren(children) {
|
|
417
|
+
const normalized = [];
|
|
418
|
+
for (const child of children) {
|
|
419
|
+
if (Array.isArray(child)) {
|
|
420
|
+
normalized.push(...normalizeChildren(child));
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
if (child === null || child === void 0 || typeof child === "boolean") {
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
normalized.push(child);
|
|
427
|
+
}
|
|
428
|
+
return normalized;
|
|
429
|
+
}
|
|
430
|
+
function isVNode(value) {
|
|
431
|
+
return Boolean(
|
|
432
|
+
value && typeof value === "object" && typeof value.type === "string"
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
function inferWrapperTag(type) {
|
|
436
|
+
if (type.includes("block") || type.includes("container") || type.includes("quote") || type.includes("definition")) {
|
|
437
|
+
return "div";
|
|
438
|
+
}
|
|
439
|
+
return "span";
|
|
440
|
+
}
|
|
441
|
+
function clampHeadingDepth(depth) {
|
|
442
|
+
if (typeof depth !== "number" || Number.isNaN(depth)) {
|
|
443
|
+
return 1;
|
|
444
|
+
}
|
|
445
|
+
if (depth < 1) return 1;
|
|
446
|
+
if (depth > 6) return 6;
|
|
447
|
+
return depth;
|
|
448
|
+
}
|
|
449
|
+
function isRefLike(value) {
|
|
450
|
+
return Boolean(value && typeof value === "object" && !(value instanceof Element));
|
|
451
|
+
}
|
|
452
|
+
function isAsyncIterable(value) {
|
|
453
|
+
return Boolean(value && typeof value[Symbol.asyncIterator] === "function");
|
|
454
|
+
}
|
|
455
|
+
function isAsyncIterator(value) {
|
|
456
|
+
return Boolean(
|
|
457
|
+
value && typeof value === "object" && typeof value.next === "function"
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
async function* toAsyncIterable(input) {
|
|
461
|
+
if (isAsyncIterable(input)) {
|
|
462
|
+
yield* input;
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
while (true) {
|
|
466
|
+
const result = await input.next();
|
|
467
|
+
if (result.done) {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
yield result.value;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
async function dynamicModuleImport(specifier) {
|
|
474
|
+
return Function("specifier", "return import(specifier)")(specifier);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
export { FALL_THROUGH, updateWithMarkdown };
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "defuss-markdown",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Incremental Markdown -> Defuss JSX bridge built on Incremark core.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"markdown",
|
|
8
|
+
"defuss",
|
|
9
|
+
"incremark",
|
|
10
|
+
"streaming",
|
|
11
|
+
"jsx"
|
|
12
|
+
],
|
|
13
|
+
"repository": {
|
|
14
|
+
"url": "git+https://github.com/kyr0/defuss.git",
|
|
15
|
+
"type": "git"
|
|
16
|
+
},
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"author": {
|
|
19
|
+
"name": "Aron Homberg",
|
|
20
|
+
"email": "info@aron-homberg.de",
|
|
21
|
+
"url": "https://aron-homberg.de"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18"
|
|
25
|
+
},
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"import": "./dist/index.js"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"main": "./dist/index.js",
|
|
34
|
+
"sideEffects": false,
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"README.md",
|
|
38
|
+
"LICENSE"
|
|
39
|
+
],
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"@incremark/core": "^1.0.2",
|
|
42
|
+
"defuss": "^3.4.5"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"clean": "rm -rf dist coverage",
|
|
46
|
+
"test": "vitest run",
|
|
47
|
+
"test:coverage": "vitest run --coverage",
|
|
48
|
+
"build": "npm run test && pkgroll",
|
|
49
|
+
"prepublishOnly": "npm run build"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/node": "^25.5.2",
|
|
53
|
+
"pkgroll": "^2.27.0",
|
|
54
|
+
"typescript": "^6.0.2",
|
|
55
|
+
"vitest": "^4.1.2"
|
|
56
|
+
}
|
|
57
|
+
}
|