@copilotkit/markdown 2.0.0-rc.19 → 2.0.0-rc.21
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/README.md +24 -7
- package/dist/dom/index.cjs +255 -0
- package/dist/dom/index.cjs.map +1 -0
- package/dist/dom/index.d.cts +93 -0
- package/dist/dom/index.d.cts.map +1 -0
- package/dist/dom/index.d.mts +93 -0
- package/dist/dom/index.d.mts.map +1 -0
- package/dist/dom/index.mjs +254 -0
- package/dist/dom/index.mjs.map +1 -0
- package/dist/index.cjs +522 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +271 -4
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +271 -4
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +512 -12
- package/dist/index.mjs.map +1 -1
- package/dist/react/index.cjs +66 -458
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +13 -22
- package/dist/react/index.d.cts.map +1 -1
- package/dist/react/index.d.mts +13 -22
- package/dist/react/index.d.mts.map +1 -1
- package/dist/react/index.mjs +67 -459
- package/dist/react/index.mjs.map +1 -1
- package/dist/react-native/index.cjs.map +1 -1
- package/dist/react-native/index.d.cts.map +1 -1
- package/dist/react-native/index.d.mts.map +1 -1
- package/dist/react-native/index.mjs.map +1 -1
- package/dist/vue/index.cjs +86 -199
- package/dist/vue/index.cjs.map +1 -1
- package/dist/vue/index.d.cts +46 -16
- package/dist/vue/index.d.cts.map +1 -1
- package/dist/vue/index.d.mts +46 -16
- package/dist/vue/index.d.mts.map +1 -1
- package/dist/vue/index.mjs +88 -201
- package/dist/vue/index.mjs.map +1 -1
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -1,25 +1,42 @@
|
|
|
1
1
|
# @copilotkit/markdown
|
|
2
2
|
|
|
3
|
-
A zero-dependency streaming Markdown parser with framework renderers for CopilotKit.
|
|
3
|
+
A zero-dependency streaming Markdown parser with framework renderers for CopilotKit.
|
|
4
|
+
|
|
5
|
+
## How it is layered
|
|
6
|
+
|
|
7
|
+
1. **Parser** (root entry, no framework). `createStreamingMarkdownParserState`, `parseStreamingMarkdownChunk` and `finalizeStreamingMarkdown` turn growing Markdown text into an immutable AST. Unchanged nodes keep their identity between chunks.
|
|
8
|
+
2. **Render tree** (root entry, no framework). `updateStreamingMarkdownParserSession` re-parses only appended text. `buildStreamingMarkdownRenderTree` turns parser state into elements, sanitized attributes, click actions, text segments for the per-token fade-in, and the caret position. `getStreamingMarkdownStyles` returns the stylesheet every renderer injects.
|
|
9
|
+
3. **Backends** (one per entry point). Each one only maps the render tree to its framework:
|
|
10
|
+
- `@copilotkit/markdown/react` — React elements
|
|
11
|
+
- `@copilotkit/markdown/vue` — Vue VNodes
|
|
12
|
+
- `@copilotkit/markdown/dom` — real DOM nodes, with keyed patching (used by Angular, and by anything without a framework)
|
|
13
|
+
- `@copilotkit/markdown/react-native` — React Native views (renders the AST directly)
|
|
14
|
+
|
|
15
|
+
Markup and URL sanitization live in the render tree, so a fix there applies to every web renderer.
|
|
4
16
|
|
|
5
17
|
## Usage
|
|
6
18
|
|
|
7
19
|
```ts
|
|
8
|
-
// Streaming Markdown parser (no framework dependency)
|
|
9
20
|
import {
|
|
10
21
|
createStreamingMarkdownParserState,
|
|
11
22
|
parseStreamingMarkdownChunk,
|
|
12
23
|
finalizeStreamingMarkdown,
|
|
13
24
|
} from "@copilotkit/markdown";
|
|
14
25
|
|
|
15
|
-
// React renderer
|
|
16
26
|
import { StreamingMarkdownRenderer } from "@copilotkit/markdown/react";
|
|
17
|
-
|
|
18
|
-
// Vue renderer
|
|
19
27
|
import { StreamingMarkdownRenderer as VueStreamingMarkdownRenderer } from "@copilotkit/markdown/vue";
|
|
20
|
-
|
|
21
|
-
// React Native renderer
|
|
22
28
|
import { StreamingMarkdownRenderer as NativeStreamingMarkdownRenderer } from "@copilotkit/markdown/react-native";
|
|
29
|
+
|
|
30
|
+
import { createStreamingMarkdownDomRenderer } from "@copilotkit/markdown/dom";
|
|
31
|
+
|
|
32
|
+
const renderer = createStreamingMarkdownDomRenderer(element);
|
|
33
|
+
renderer.render({ content: "**Hello**", caret: true });
|
|
34
|
+
renderer.render({ content: "**Hello** world", isComplete: true });
|
|
35
|
+
renderer.destroy();
|
|
23
36
|
```
|
|
24
37
|
|
|
38
|
+
## Features
|
|
39
|
+
|
|
40
|
+
GFM paragraphs, headings, lists, task lists, block quotes, fenced code, tables, strikethrough, autolinks, and footnote-style citations. Raw HTML is shown as text, never parsed. Set the `enableMath` parser option to get `math` nodes for `$…$` and `$$…$$`; render them with a `math` node renderer (for example with KaTeX).
|
|
41
|
+
|
|
25
42
|
> **Note:** This package is under active development. APIs will be documented once stabilized.
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
let _copilotkit_markdown = require("@copilotkit/markdown");
|
|
3
|
+
|
|
4
|
+
//#region src/dom/streaming-markdown-dom-renderer.ts
|
|
5
|
+
function toKebabCase(property) {
|
|
6
|
+
return property.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
|
|
7
|
+
}
|
|
8
|
+
const CARET_STYLE = Object.entries(_copilotkit_markdown.STREAMING_MARKDOWN_CARET_STYLE).map(([property, value]) => `${toKebabCase(property)}:${value}`).join(";");
|
|
9
|
+
function caretElement(key) {
|
|
10
|
+
return {
|
|
11
|
+
kind: "element",
|
|
12
|
+
key,
|
|
13
|
+
tag: "span",
|
|
14
|
+
attrs: {
|
|
15
|
+
..._copilotkit_markdown.STREAMING_MARKDOWN_CARET_ATTRS,
|
|
16
|
+
style: CARET_STYLE
|
|
17
|
+
},
|
|
18
|
+
children: []
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function prefixed(prefix, children) {
|
|
22
|
+
return children.map((child) => ({
|
|
23
|
+
...child,
|
|
24
|
+
key: `${prefix}:${child.key}`
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
function toVChildren(children, context, slot) {
|
|
28
|
+
return children.flatMap((child) => {
|
|
29
|
+
switch (child.kind) {
|
|
30
|
+
case "node": return toVNode(child, context);
|
|
31
|
+
case "element": {
|
|
32
|
+
const element = {
|
|
33
|
+
kind: "element",
|
|
34
|
+
key: child.key,
|
|
35
|
+
tag: child.tag,
|
|
36
|
+
attrs: child.attrs,
|
|
37
|
+
children: toVChildren(child.children, context, slot)
|
|
38
|
+
};
|
|
39
|
+
return child.action ? [{
|
|
40
|
+
...element,
|
|
41
|
+
action: child.action
|
|
42
|
+
}] : [element];
|
|
43
|
+
}
|
|
44
|
+
case "text": return [{
|
|
45
|
+
kind: "text",
|
|
46
|
+
key: child.key,
|
|
47
|
+
text: child.text
|
|
48
|
+
}];
|
|
49
|
+
case "caret": return [caretElement(child.key)];
|
|
50
|
+
case "slot": return slot;
|
|
51
|
+
default: return child;
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
function renderOverride(renderNode, renderer, context) {
|
|
56
|
+
const cached = context.overrideCache.get(renderNode.node);
|
|
57
|
+
if (cached?.renderer === renderer && !renderNode.containsCaret) return cached.dom;
|
|
58
|
+
const doc = context.document;
|
|
59
|
+
const materialize = (entries) => entries.map((entry) => createDom(entry, doc));
|
|
60
|
+
const children = renderNode.children ? materialize(toVChildren(renderNode.children, context, [])) : null;
|
|
61
|
+
const defaultNodes = materialize(toVChildren(renderNode.output, context, renderNode.children ? toVChildren(renderNode.children, context, []) : []));
|
|
62
|
+
const defaultNode = defaultNodes.length === 1 ? defaultNodes[0] : wrapInFragment(defaultNodes, doc);
|
|
63
|
+
const dom = renderer({
|
|
64
|
+
node: renderNode.node,
|
|
65
|
+
children,
|
|
66
|
+
defaultNode,
|
|
67
|
+
citation: renderNode.citation,
|
|
68
|
+
document: doc
|
|
69
|
+
});
|
|
70
|
+
context.overrideCache.set(renderNode.node, {
|
|
71
|
+
renderer,
|
|
72
|
+
dom
|
|
73
|
+
});
|
|
74
|
+
return dom;
|
|
75
|
+
}
|
|
76
|
+
function wrapInFragment(nodes, doc) {
|
|
77
|
+
if (nodes.length === 0) return null;
|
|
78
|
+
const fragment = doc.createDocumentFragment();
|
|
79
|
+
for (const node of nodes) fragment.appendChild(node);
|
|
80
|
+
return fragment;
|
|
81
|
+
}
|
|
82
|
+
function toVNode(renderNode, context) {
|
|
83
|
+
const renderer = context.nodeRenderers?.[renderNode.rendererKey] ?? context.nodeRenderers?.node;
|
|
84
|
+
if (renderer) {
|
|
85
|
+
const dom = renderOverride(renderNode, renderer, context);
|
|
86
|
+
return dom ? [{
|
|
87
|
+
kind: "raw",
|
|
88
|
+
key: renderNode.key,
|
|
89
|
+
dom
|
|
90
|
+
}] : [];
|
|
91
|
+
}
|
|
92
|
+
const slot = renderNode.children ? toVChildren(renderNode.children, context, []) : [];
|
|
93
|
+
return prefixed(renderNode.key, toVChildren(renderNode.output, context, slot));
|
|
94
|
+
}
|
|
95
|
+
function setAttributes(element, next, previous) {
|
|
96
|
+
const previousNames = Object.keys(previous);
|
|
97
|
+
for (const name of previousNames) if (!(name in next)) element.removeAttribute(name);
|
|
98
|
+
const entries = Object.entries(next);
|
|
99
|
+
for (const [name, value] of entries) if (previous[name] !== value) element.setAttribute(name, value === true ? "" : value);
|
|
100
|
+
}
|
|
101
|
+
function createDom(child, doc) {
|
|
102
|
+
switch (child.kind) {
|
|
103
|
+
case "raw": return child.dom;
|
|
104
|
+
case "text":
|
|
105
|
+
child.dom = doc.createTextNode(child.text);
|
|
106
|
+
return child.dom;
|
|
107
|
+
case "element": {
|
|
108
|
+
const element = doc.createElement(child.tag);
|
|
109
|
+
setAttributes(element, child.attrs, {});
|
|
110
|
+
for (const entry of child.children) element.appendChild(createDom(entry, doc));
|
|
111
|
+
child.dom = element;
|
|
112
|
+
return element;
|
|
113
|
+
}
|
|
114
|
+
default: return child;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/** Updates `previous`'s DOM to match `next` and returns it, or `null` if incompatible. */
|
|
118
|
+
function patchDom(previous, next, doc) {
|
|
119
|
+
if (previous.kind === "text" && next.kind === "text" && previous.dom) {
|
|
120
|
+
if (previous.text !== next.text) previous.dom.data = next.text;
|
|
121
|
+
next.dom = previous.dom;
|
|
122
|
+
return next.dom;
|
|
123
|
+
}
|
|
124
|
+
if (previous.kind === "raw" && next.kind === "raw") return previous.dom === next.dom ? next.dom : null;
|
|
125
|
+
if (!(previous.kind === "element" && next.kind === "element" && previous.tag === next.tag && previous.dom !== void 0)) return null;
|
|
126
|
+
const element = previous.dom;
|
|
127
|
+
if (!element) return null;
|
|
128
|
+
setAttributes(element, next.attrs, previous.attrs);
|
|
129
|
+
patchChildren(element, previous.children, next.children, doc);
|
|
130
|
+
next.dom = element;
|
|
131
|
+
return element;
|
|
132
|
+
}
|
|
133
|
+
function patchChildren(parent, previous, next, doc) {
|
|
134
|
+
const unused = new Map(previous.map((child) => [child.key, child]));
|
|
135
|
+
let cursor = parent.firstChild;
|
|
136
|
+
for (const child of next) {
|
|
137
|
+
const match = unused.get(child.key);
|
|
138
|
+
const reused = match ? patchDom(match, child, doc) : null;
|
|
139
|
+
if (match && reused) unused.delete(child.key);
|
|
140
|
+
const dom = reused ?? createDom(child, doc);
|
|
141
|
+
if (dom === cursor) cursor = cursor.nextSibling;
|
|
142
|
+
else parent.insertBefore(dom, cursor);
|
|
143
|
+
}
|
|
144
|
+
const leftovers = Array.from(unused.values());
|
|
145
|
+
for (const child of leftovers) {
|
|
146
|
+
const dom = child.dom;
|
|
147
|
+
if (dom && dom.parentNode === parent) parent.removeChild(dom);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function removeAllChildren(parent) {
|
|
151
|
+
while (parent.firstChild) parent.removeChild(parent.firstChild);
|
|
152
|
+
}
|
|
153
|
+
function collectActions(children, actions) {
|
|
154
|
+
for (const child of children) {
|
|
155
|
+
if (child.kind !== "element" || !child.dom) continue;
|
|
156
|
+
if (child.action) actions.set(child.dom, child.action);
|
|
157
|
+
collectActions(child.children, actions);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Creates a framework-free streaming markdown renderer that draws into
|
|
162
|
+
* `container`. It parses incrementally, renders the same render tree as the
|
|
163
|
+
* React and Vue renderers (same markup, URL sanitization, caret, citations,
|
|
164
|
+
* and per-token fade-in), and patches the DOM by key so text that is already
|
|
165
|
+
* on screen keeps its nodes and does not animate again.
|
|
166
|
+
*
|
|
167
|
+
* Creates nodes with `container.ownerDocument`, so it works with server DOMs
|
|
168
|
+
* that have no global `document`.
|
|
169
|
+
*
|
|
170
|
+
* @param container - Element the renderer owns. Its children are replaced.
|
|
171
|
+
* @returns `render(props)` to draw the latest props, and `destroy()` to remove
|
|
172
|
+
* the output and the click listener.
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* ```ts
|
|
176
|
+
* const renderer = createStreamingMarkdownDomRenderer(element);
|
|
177
|
+
* renderer.render({ content: "**Hi**", caret: true });
|
|
178
|
+
* renderer.render({ content: "**Hi** there", isComplete: true });
|
|
179
|
+
* renderer.destroy();
|
|
180
|
+
* ```
|
|
181
|
+
* @public
|
|
182
|
+
*/
|
|
183
|
+
function createStreamingMarkdownDomRenderer(container) {
|
|
184
|
+
const doc = container.ownerDocument;
|
|
185
|
+
let session = null;
|
|
186
|
+
let props = { content: "" };
|
|
187
|
+
let styleElement = null;
|
|
188
|
+
let rootElement = null;
|
|
189
|
+
let previousChildren = [];
|
|
190
|
+
let actions = /* @__PURE__ */ new WeakMap();
|
|
191
|
+
const overrideCache = /* @__PURE__ */ new WeakMap();
|
|
192
|
+
const onClick = (event) => {
|
|
193
|
+
let target = event.target instanceof Node ? event.target : null;
|
|
194
|
+
while (target && target !== container) {
|
|
195
|
+
const action = actions.get(target);
|
|
196
|
+
if (action?.type === "link") {
|
|
197
|
+
props.onLinkClick?.(event, action.url, action.node);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (action?.type === "citation") {
|
|
201
|
+
props.onCitationClick?.(event, action.citation, action.node);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
target = target.parentNode;
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
container.addEventListener("click", onClick);
|
|
208
|
+
function render(nextProps) {
|
|
209
|
+
props = nextProps;
|
|
210
|
+
session = (0, _copilotkit_markdown.updateStreamingMarkdownParserSession)(session, nextProps.content, nextProps.options, nextProps.isComplete ?? false);
|
|
211
|
+
const state = session.parserState;
|
|
212
|
+
const tree = (0, _copilotkit_markdown.buildStreamingMarkdownRenderTree)(state, {
|
|
213
|
+
caret: nextProps.caret === true,
|
|
214
|
+
nodeClassNames: nextProps.nodeClassNames
|
|
215
|
+
});
|
|
216
|
+
if (!styleElement || !rootElement || rootElement.parentNode !== container) {
|
|
217
|
+
styleElement = doc.createElement("style");
|
|
218
|
+
rootElement = doc.createElement("div");
|
|
219
|
+
rootElement.setAttribute("data-streaming-markdown-root", "");
|
|
220
|
+
previousChildren = [];
|
|
221
|
+
removeAllChildren(container);
|
|
222
|
+
container.appendChild(styleElement);
|
|
223
|
+
container.appendChild(rootElement);
|
|
224
|
+
}
|
|
225
|
+
const styles = (0, _copilotkit_markdown.getStreamingMarkdownStyles)();
|
|
226
|
+
if (styleElement.textContent !== styles) styleElement.textContent = styles;
|
|
227
|
+
const rootClass = nextProps.className ? `${_copilotkit_markdown.STREAMING_MARKDOWN_ROOT_CLASS} ${nextProps.className}` : _copilotkit_markdown.STREAMING_MARKDOWN_ROOT_CLASS;
|
|
228
|
+
if (rootElement.className !== rootClass) rootElement.className = rootClass;
|
|
229
|
+
const nextChildren = tree.root ? toVNode(tree.root, {
|
|
230
|
+
document: doc,
|
|
231
|
+
nodeRenderers: nextProps.nodeRenderers,
|
|
232
|
+
overrideCache
|
|
233
|
+
}) : [];
|
|
234
|
+
patchChildren(rootElement, previousChildren, nextChildren, doc);
|
|
235
|
+
previousChildren = nextChildren;
|
|
236
|
+
actions = /* @__PURE__ */ new WeakMap();
|
|
237
|
+
collectActions(nextChildren, actions);
|
|
238
|
+
}
|
|
239
|
+
function destroy() {
|
|
240
|
+
container.removeEventListener("click", onClick);
|
|
241
|
+
removeAllChildren(container);
|
|
242
|
+
session = null;
|
|
243
|
+
styleElement = null;
|
|
244
|
+
rootElement = null;
|
|
245
|
+
previousChildren = [];
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
render,
|
|
249
|
+
destroy
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
//#endregion
|
|
254
|
+
exports.createStreamingMarkdownDomRenderer = createStreamingMarkdownDomRenderer;
|
|
255
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["STREAMING_MARKDOWN_CARET_STYLE","STREAMING_MARKDOWN_CARET_ATTRS","STREAMING_MARKDOWN_ROOT_CLASS"],"sources":["../../src/dom/streaming-markdown-dom-renderer.ts"],"sourcesContent":["import {\n STREAMING_MARKDOWN_CARET_ATTRS,\n STREAMING_MARKDOWN_CARET_STYLE,\n STREAMING_MARKDOWN_ROOT_CLASS,\n buildStreamingMarkdownRenderTree,\n getStreamingMarkdownStyles,\n updateStreamingMarkdownParserSession,\n} from \"@copilotkit/markdown\";\nimport type {\n StreamingMarkdownAstNode,\n StreamingMarkdownCitationRenderData,\n StreamingMarkdownNodeClassNames,\n StreamingMarkdownNodeRendererKey,\n StreamingMarkdownParserOptions,\n StreamingMarkdownParserSession,\n StreamingMarkdownRenderAction,\n StreamingMarkdownRenderChild,\n StreamingMarkdownRenderNode,\n} from \"@copilotkit/markdown\";\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * Props supplied to a custom DOM node renderer.\n *\n * @public\n */\nexport interface StreamingMarkdownDomNodeRendererProps {\n /** The immutable AST node being rendered. */\n node: StreamingMarkdownAstNode;\n /** Freshly rendered child content for container nodes; `null` for leaves. */\n children: Node[] | null;\n /** Freshly rendered default element for this node. */\n defaultNode: Node | null;\n /** Citation metadata when rendering a citation node. */\n citation?: StreamingMarkdownCitationRenderData;\n /** Document to create nodes with (the container's owner document). */\n document: Document;\n}\n\n/**\n * Custom renderer for one node type. Return the node to insert, or `null`\n * to render nothing.\n *\n * @public\n */\nexport type StreamingMarkdownDomNodeRenderer = (\n props: StreamingMarkdownDomNodeRendererProps,\n) => Node | null;\n\n/**\n * Custom DOM renderers keyed by camelCase node names (`node` is a catch-all).\n *\n * @public\n */\nexport type StreamingMarkdownDomNodeRenderers = Partial<\n Record<StreamingMarkdownNodeRendererKey, StreamingMarkdownDomNodeRenderer>\n>;\n\n/**\n * Inputs for one render. Pass the full set every time, like component props.\n *\n * @public\n */\nexport interface StreamingMarkdownDomRenderProps {\n /** Full markdown source that grows over time. */\n content: string;\n /** `true` once the source is final; hides the caret. */\n isComplete?: boolean;\n /** Parser option overrides. */\n options?: Partial<StreamingMarkdownParserOptions>;\n /** Render the default streaming caret inside the deepest open node. */\n caret?: boolean;\n /** Extra class on the root element. */\n className?: string;\n /** Called when a link or autolink is clicked. */\n onLinkClick?: (\n event: MouseEvent,\n url: string,\n node: StreamingMarkdownAstNode,\n ) => void;\n /** Called when a citation reference with a URL is clicked. */\n onCitationClick?: (\n event: MouseEvent,\n citation: StreamingMarkdownCitationRenderData,\n node: StreamingMarkdownAstNode,\n ) => void;\n /** Custom renderers keyed by node type. */\n nodeRenderers?: StreamingMarkdownDomNodeRenderers;\n /** Extra classes for the default element of a node type. */\n nodeClassNames?: StreamingMarkdownNodeClassNames;\n}\n\n// ---------------------------------------------------------------------------\n// Virtual nodes\n// ---------------------------------------------------------------------------\n\ntype VElement = {\n kind: \"element\";\n key: string;\n tag: string;\n attrs: Readonly<Record<string, string | true>>;\n action?: StreamingMarkdownRenderAction;\n children: VChild[];\n dom?: Element;\n};\ntype VText = { kind: \"text\"; key: string; text: string; dom?: Text };\ntype VRaw = { kind: \"raw\"; key: string; dom: Node };\ntype VChild = VElement | VText | VRaw;\n\ntype BuildContext = {\n document: Document;\n nodeRenderers: StreamingMarkdownDomNodeRenderers | undefined;\n overrideCache: WeakMap<\n StreamingMarkdownAstNode,\n { renderer: StreamingMarkdownDomNodeRenderer; dom: Node | null }\n >;\n};\n\nfunction toKebabCase(property: string) {\n return property.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);\n}\n\nconst CARET_STYLE = Object.entries(STREAMING_MARKDOWN_CARET_STYLE)\n .map(([property, value]) => `${toKebabCase(property)}:${value}`)\n .join(\";\");\n\nfunction caretElement(key: string): VElement {\n return {\n kind: \"element\",\n key,\n tag: \"span\",\n attrs: { ...STREAMING_MARKDOWN_CARET_ATTRS, style: CARET_STYLE },\n children: [],\n };\n}\n\nfunction prefixed(prefix: string, children: VChild[]) {\n return children.map((child) => ({ ...child, key: `${prefix}:${child.key}` }));\n}\n\nfunction toVChildren(\n children: StreamingMarkdownRenderChild[],\n context: BuildContext,\n slot: VChild[],\n): VChild[] {\n return children.flatMap((child) => {\n switch (child.kind) {\n case \"node\":\n return toVNode(child, context);\n case \"element\": {\n const element: VElement = {\n kind: \"element\",\n key: child.key,\n tag: child.tag,\n attrs: child.attrs,\n children: toVChildren(child.children, context, slot),\n };\n return child.action\n ? [{ ...element, action: child.action }]\n : [element];\n }\n case \"text\":\n return [{ kind: \"text\", key: child.key, text: child.text }];\n case \"caret\":\n return [caretElement(child.key)];\n case \"slot\":\n return slot;\n default: {\n const unreachable: never = child;\n return unreachable;\n }\n }\n });\n}\n\nfunction renderOverride(\n renderNode: StreamingMarkdownRenderNode,\n renderer: StreamingMarkdownDomNodeRenderer,\n context: BuildContext,\n) {\n // AST nodes keep their identity while unchanged, so an override's output can\n // be reused unless the caret (which is not part of the node) moves inside it.\n const cached = context.overrideCache.get(renderNode.node);\n if (cached?.renderer === renderer && !renderNode.containsCaret) {\n return cached.dom;\n }\n\n const doc = context.document;\n const materialize = (entries: VChild[]) =>\n entries.map((entry) => createDom(entry, doc));\n const children = renderNode.children\n ? materialize(toVChildren(renderNode.children, context, []))\n : null;\n const defaultNodes = materialize(\n toVChildren(\n renderNode.output,\n context,\n renderNode.children ? toVChildren(renderNode.children, context, []) : [],\n ),\n );\n const defaultNode =\n defaultNodes.length === 1\n ? defaultNodes[0]\n : wrapInFragment(defaultNodes, doc);\n\n const dom = renderer({\n node: renderNode.node,\n children,\n defaultNode,\n citation: renderNode.citation,\n document: doc,\n });\n context.overrideCache.set(renderNode.node, { renderer, dom });\n return dom;\n}\n\nfunction wrapInFragment(nodes: Node[], doc: Document) {\n if (nodes.length === 0) return null;\n const fragment = doc.createDocumentFragment();\n for (const node of nodes) fragment.appendChild(node);\n return fragment;\n}\n\nfunction toVNode(\n renderNode: StreamingMarkdownRenderNode,\n context: BuildContext,\n): VChild[] {\n const renderer =\n context.nodeRenderers?.[renderNode.rendererKey] ??\n context.nodeRenderers?.node;\n if (renderer) {\n const dom = renderOverride(renderNode, renderer, context);\n return dom ? [{ kind: \"raw\", key: renderNode.key, dom }] : [];\n }\n\n const slot = renderNode.children\n ? toVChildren(renderNode.children, context, [])\n : [];\n return prefixed(\n renderNode.key,\n toVChildren(renderNode.output, context, slot),\n );\n}\n\n// ---------------------------------------------------------------------------\n// DOM creation and keyed patching\n// ---------------------------------------------------------------------------\n\nfunction setAttributes(\n element: Element,\n next: Readonly<Record<string, string | true>>,\n previous: Readonly<Record<string, string | true>>,\n) {\n const previousNames = Object.keys(previous);\n for (const name of previousNames) {\n if (!(name in next)) element.removeAttribute(name);\n }\n const entries = Object.entries(next);\n for (const [name, value] of entries) {\n if (previous[name] !== value) {\n element.setAttribute(name, value === true ? \"\" : value);\n }\n }\n}\n\nfunction createDom(child: VChild, doc: Document): Node {\n switch (child.kind) {\n case \"raw\":\n return child.dom;\n case \"text\": {\n child.dom = doc.createTextNode(child.text);\n return child.dom;\n }\n case \"element\": {\n const element = doc.createElement(child.tag);\n setAttributes(element, child.attrs, {});\n for (const entry of child.children) {\n element.appendChild(createDom(entry, doc));\n }\n child.dom = element;\n return element;\n }\n default: {\n const unreachable: never = child;\n return unreachable;\n }\n }\n}\n\n/** Updates `previous`'s DOM to match `next` and returns it, or `null` if incompatible. */\nfunction patchDom(previous: VChild, next: VChild, doc: Document): Node | null {\n if (previous.kind === \"text\" && next.kind === \"text\" && previous.dom) {\n if (previous.text !== next.text) previous.dom.data = next.text;\n next.dom = previous.dom;\n return next.dom;\n }\n if (previous.kind === \"raw\" && next.kind === \"raw\") {\n return previous.dom === next.dom ? next.dom : null;\n }\n const sameElement =\n previous.kind === \"element\" &&\n next.kind === \"element\" &&\n previous.tag === next.tag &&\n previous.dom !== undefined;\n if (!sameElement) {\n return null;\n }\n const element = previous.dom;\n if (!element) return null;\n setAttributes(element, next.attrs, previous.attrs);\n patchChildren(element, previous.children, next.children, doc);\n next.dom = element;\n return element;\n}\n\nfunction patchChildren(\n parent: Node,\n previous: VChild[],\n next: VChild[],\n doc: Document,\n) {\n const unused = new Map(previous.map((child) => [child.key, child]));\n let cursor: Node | null = parent.firstChild;\n\n for (const child of next) {\n const match = unused.get(child.key);\n const reused = match ? patchDom(match, child, doc) : null;\n if (match && reused) unused.delete(child.key);\n const dom = reused ?? createDom(child, doc);\n\n if (dom === cursor) {\n cursor = cursor.nextSibling;\n } else {\n parent.insertBefore(dom, cursor);\n }\n }\n\n const leftovers = Array.from(unused.values());\n for (const child of leftovers) {\n const dom = child.dom;\n if (dom && dom.parentNode === parent) parent.removeChild(dom);\n }\n}\n\n// Server DOMs (domino) lack replaceChildren(), so clear by hand.\nfunction removeAllChildren(parent: Node) {\n while (parent.firstChild) parent.removeChild(parent.firstChild);\n}\n\nfunction collectActions(\n children: VChild[],\n actions: WeakMap<Node, StreamingMarkdownRenderAction>,\n) {\n for (const child of children) {\n if (child.kind !== \"element\" || !child.dom) continue;\n if (child.action) actions.set(child.dom, child.action);\n collectActions(child.children, actions);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Renderer\n// ---------------------------------------------------------------------------\n\n/**\n * Creates a framework-free streaming markdown renderer that draws into\n * `container`. It parses incrementally, renders the same render tree as the\n * React and Vue renderers (same markup, URL sanitization, caret, citations,\n * and per-token fade-in), and patches the DOM by key so text that is already\n * on screen keeps its nodes and does not animate again.\n *\n * Creates nodes with `container.ownerDocument`, so it works with server DOMs\n * that have no global `document`.\n *\n * @param container - Element the renderer owns. Its children are replaced.\n * @returns `render(props)` to draw the latest props, and `destroy()` to remove\n * the output and the click listener.\n *\n * @example\n * ```ts\n * const renderer = createStreamingMarkdownDomRenderer(element);\n * renderer.render({ content: \"**Hi**\", caret: true });\n * renderer.render({ content: \"**Hi** there\", isComplete: true });\n * renderer.destroy();\n * ```\n * @public\n */\nexport function createStreamingMarkdownDomRenderer(container: HTMLElement) {\n const doc = container.ownerDocument;\n let session: StreamingMarkdownParserSession | null = null;\n let props: StreamingMarkdownDomRenderProps = { content: \"\" };\n let styleElement: HTMLStyleElement | null = null;\n let rootElement: HTMLDivElement | null = null;\n let previousChildren: VChild[] = [];\n let actions = new WeakMap<Node, StreamingMarkdownRenderAction>();\n const overrideCache: BuildContext[\"overrideCache\"] = new WeakMap();\n\n // One delegated listener; actions are looked up per click so callbacks and\n // links always reflect the latest render.\n const onClick = (event: MouseEvent) => {\n let target = event.target instanceof Node ? event.target : null;\n while (target && target !== container) {\n const action = actions.get(target);\n if (action?.type === \"link\") {\n props.onLinkClick?.(event, action.url, action.node);\n return;\n }\n if (action?.type === \"citation\") {\n props.onCitationClick?.(event, action.citation, action.node);\n return;\n }\n target = target.parentNode;\n }\n };\n container.addEventListener(\"click\", onClick);\n\n function render(nextProps: StreamingMarkdownDomRenderProps) {\n props = nextProps;\n session = updateStreamingMarkdownParserSession(\n session,\n nextProps.content,\n nextProps.options,\n nextProps.isComplete ?? false,\n );\n const state = session.parserState;\n const tree = buildStreamingMarkdownRenderTree(state, {\n caret: nextProps.caret === true,\n nodeClassNames: nextProps.nodeClassNames,\n });\n\n if (!styleElement || !rootElement || rootElement.parentNode !== container) {\n styleElement = doc.createElement(\"style\");\n rootElement = doc.createElement(\"div\");\n rootElement.setAttribute(\"data-streaming-markdown-root\", \"\");\n previousChildren = [];\n removeAllChildren(container);\n container.appendChild(styleElement);\n container.appendChild(rootElement);\n }\n const styles = getStreamingMarkdownStyles();\n if (styleElement.textContent !== styles) styleElement.textContent = styles;\n const rootClass = nextProps.className\n ? `${STREAMING_MARKDOWN_ROOT_CLASS} ${nextProps.className}`\n : STREAMING_MARKDOWN_ROOT_CLASS;\n if (rootElement.className !== rootClass) rootElement.className = rootClass;\n\n const nextChildren = tree.root\n ? toVNode(tree.root, {\n document: doc,\n nodeRenderers: nextProps.nodeRenderers,\n overrideCache,\n })\n : [];\n patchChildren(rootElement, previousChildren, nextChildren, doc);\n previousChildren = nextChildren;\n actions = new WeakMap();\n collectActions(nextChildren, actions);\n }\n\n function destroy() {\n container.removeEventListener(\"click\", onClick);\n removeAllChildren(container);\n session = null;\n styleElement = null;\n rootElement = null;\n previousChildren = [];\n }\n\n return { render, destroy };\n}\n\n/**\n * Renderer returned by {@link createStreamingMarkdownDomRenderer}.\n * @public\n */\nexport type StreamingMarkdownDomRenderer = ReturnType<\n typeof createStreamingMarkdownDomRenderer\n>;\n"],"mappings":"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"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { StreamingMarkdownAstNode, StreamingMarkdownCitationRenderData, StreamingMarkdownNodeClassNames, StreamingMarkdownNodeRendererKey, StreamingMarkdownParserOptions } from "@copilotkit/markdown";
|
|
2
|
+
|
|
3
|
+
//#region src/dom/streaming-markdown-dom-renderer.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Props supplied to a custom DOM node renderer.
|
|
6
|
+
*
|
|
7
|
+
* @public
|
|
8
|
+
*/
|
|
9
|
+
interface StreamingMarkdownDomNodeRendererProps {
|
|
10
|
+
/** The immutable AST node being rendered. */
|
|
11
|
+
node: StreamingMarkdownAstNode;
|
|
12
|
+
/** Freshly rendered child content for container nodes; `null` for leaves. */
|
|
13
|
+
children: Node[] | null;
|
|
14
|
+
/** Freshly rendered default element for this node. */
|
|
15
|
+
defaultNode: Node | null;
|
|
16
|
+
/** Citation metadata when rendering a citation node. */
|
|
17
|
+
citation?: StreamingMarkdownCitationRenderData;
|
|
18
|
+
/** Document to create nodes with (the container's owner document). */
|
|
19
|
+
document: Document;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Custom renderer for one node type. Return the node to insert, or `null`
|
|
23
|
+
* to render nothing.
|
|
24
|
+
*
|
|
25
|
+
* @public
|
|
26
|
+
*/
|
|
27
|
+
type StreamingMarkdownDomNodeRenderer = (props: StreamingMarkdownDomNodeRendererProps) => Node | null;
|
|
28
|
+
/**
|
|
29
|
+
* Custom DOM renderers keyed by camelCase node names (`node` is a catch-all).
|
|
30
|
+
*
|
|
31
|
+
* @public
|
|
32
|
+
*/
|
|
33
|
+
type StreamingMarkdownDomNodeRenderers = Partial<Record<StreamingMarkdownNodeRendererKey, StreamingMarkdownDomNodeRenderer>>;
|
|
34
|
+
/**
|
|
35
|
+
* Inputs for one render. Pass the full set every time, like component props.
|
|
36
|
+
*
|
|
37
|
+
* @public
|
|
38
|
+
*/
|
|
39
|
+
interface StreamingMarkdownDomRenderProps {
|
|
40
|
+
/** Full markdown source that grows over time. */
|
|
41
|
+
content: string;
|
|
42
|
+
/** `true` once the source is final; hides the caret. */
|
|
43
|
+
isComplete?: boolean;
|
|
44
|
+
/** Parser option overrides. */
|
|
45
|
+
options?: Partial<StreamingMarkdownParserOptions>;
|
|
46
|
+
/** Render the default streaming caret inside the deepest open node. */
|
|
47
|
+
caret?: boolean;
|
|
48
|
+
/** Extra class on the root element. */
|
|
49
|
+
className?: string;
|
|
50
|
+
/** Called when a link or autolink is clicked. */
|
|
51
|
+
onLinkClick?: (event: MouseEvent, url: string, node: StreamingMarkdownAstNode) => void;
|
|
52
|
+
/** Called when a citation reference with a URL is clicked. */
|
|
53
|
+
onCitationClick?: (event: MouseEvent, citation: StreamingMarkdownCitationRenderData, node: StreamingMarkdownAstNode) => void;
|
|
54
|
+
/** Custom renderers keyed by node type. */
|
|
55
|
+
nodeRenderers?: StreamingMarkdownDomNodeRenderers;
|
|
56
|
+
/** Extra classes for the default element of a node type. */
|
|
57
|
+
nodeClassNames?: StreamingMarkdownNodeClassNames;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Creates a framework-free streaming markdown renderer that draws into
|
|
61
|
+
* `container`. It parses incrementally, renders the same render tree as the
|
|
62
|
+
* React and Vue renderers (same markup, URL sanitization, caret, citations,
|
|
63
|
+
* and per-token fade-in), and patches the DOM by key so text that is already
|
|
64
|
+
* on screen keeps its nodes and does not animate again.
|
|
65
|
+
*
|
|
66
|
+
* Creates nodes with `container.ownerDocument`, so it works with server DOMs
|
|
67
|
+
* that have no global `document`.
|
|
68
|
+
*
|
|
69
|
+
* @param container - Element the renderer owns. Its children are replaced.
|
|
70
|
+
* @returns `render(props)` to draw the latest props, and `destroy()` to remove
|
|
71
|
+
* the output and the click listener.
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```ts
|
|
75
|
+
* const renderer = createStreamingMarkdownDomRenderer(element);
|
|
76
|
+
* renderer.render({ content: "**Hi**", caret: true });
|
|
77
|
+
* renderer.render({ content: "**Hi** there", isComplete: true });
|
|
78
|
+
* renderer.destroy();
|
|
79
|
+
* ```
|
|
80
|
+
* @public
|
|
81
|
+
*/
|
|
82
|
+
declare function createStreamingMarkdownDomRenderer(container: HTMLElement): {
|
|
83
|
+
render: (nextProps: StreamingMarkdownDomRenderProps) => void;
|
|
84
|
+
destroy: () => void;
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Renderer returned by {@link createStreamingMarkdownDomRenderer}.
|
|
88
|
+
* @public
|
|
89
|
+
*/
|
|
90
|
+
type StreamingMarkdownDomRenderer = ReturnType<typeof createStreamingMarkdownDomRenderer>;
|
|
91
|
+
//#endregion
|
|
92
|
+
export { type StreamingMarkdownDomNodeRenderer, type StreamingMarkdownDomNodeRendererProps, type StreamingMarkdownDomNodeRenderers, type StreamingMarkdownDomRenderProps, type StreamingMarkdownDomRenderer, createStreamingMarkdownDomRenderer };
|
|
93
|
+
//# sourceMappingURL=index.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../../src/dom/streaming-markdown-dom-renderer.ts"],"mappings":";;;;;AA6BA;;;UAAiB,qCAAA;EAIL;EAFV,IAAA,EAAM,wBAAA;EAMK;EAJX,QAAA,EAAU,IAAA;EAMQ;EAJlB,WAAA,EAAa,IAAA;EAJb;EAMA,QAAA,GAAW,mCAAA;EAJX;EAMA,QAAA,EAAU,QAAA;AAAA;;;;;;;KASA,gCAAA,IACV,KAAA,EAAO,qCAAA,KACJ,IAAA;AAFL;;;;;AAAA,KASY,iCAAA,GAAoC,OAAA,CAC9C,MAAA,CAAO,gCAAA,EAAkC,gCAAA;;;;AAD3C;;UASiB,+BAAA;EARR;EAUP,OAAA;EAVA;EAYA,UAAA;EAbqD;EAerD,OAAA,GAAU,OAAA,CAAQ,8BAAA;EAf4B;EAiB9C,KAAA;EAhBO;EAkBP,SAAA;EAlByE;EAoBzE,WAAA,IACE,KAAA,EAAO,UAAA,EACP,GAAA,UACA,IAAA,EAAM,wBAAA;EAfO;EAkBf,eAAA,IACE,KAAA,EAAO,UAAA,EACP,QAAA,EAAU,mCAAA,EACV,IAAA,EAAM,wBAAA;;EAGR,aAAA,GAAgB,iCAAA;EAlBN;EAoBV,cAAA,GAAiB,+BAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;;iBA0SH,kCAAA,CAAmC,SAAA,EAAW,WAAA;sBA6BjC,+BAAA;;;;;;;KA2DjB,4BAAA,GAA+B,UAAA,QAClC,kCAAA"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { StreamingMarkdownAstNode, StreamingMarkdownCitationRenderData, StreamingMarkdownNodeClassNames, StreamingMarkdownNodeRendererKey, StreamingMarkdownParserOptions } from "@copilotkit/markdown";
|
|
2
|
+
|
|
3
|
+
//#region src/dom/streaming-markdown-dom-renderer.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Props supplied to a custom DOM node renderer.
|
|
6
|
+
*
|
|
7
|
+
* @public
|
|
8
|
+
*/
|
|
9
|
+
interface StreamingMarkdownDomNodeRendererProps {
|
|
10
|
+
/** The immutable AST node being rendered. */
|
|
11
|
+
node: StreamingMarkdownAstNode;
|
|
12
|
+
/** Freshly rendered child content for container nodes; `null` for leaves. */
|
|
13
|
+
children: Node[] | null;
|
|
14
|
+
/** Freshly rendered default element for this node. */
|
|
15
|
+
defaultNode: Node | null;
|
|
16
|
+
/** Citation metadata when rendering a citation node. */
|
|
17
|
+
citation?: StreamingMarkdownCitationRenderData;
|
|
18
|
+
/** Document to create nodes with (the container's owner document). */
|
|
19
|
+
document: Document;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Custom renderer for one node type. Return the node to insert, or `null`
|
|
23
|
+
* to render nothing.
|
|
24
|
+
*
|
|
25
|
+
* @public
|
|
26
|
+
*/
|
|
27
|
+
type StreamingMarkdownDomNodeRenderer = (props: StreamingMarkdownDomNodeRendererProps) => Node | null;
|
|
28
|
+
/**
|
|
29
|
+
* Custom DOM renderers keyed by camelCase node names (`node` is a catch-all).
|
|
30
|
+
*
|
|
31
|
+
* @public
|
|
32
|
+
*/
|
|
33
|
+
type StreamingMarkdownDomNodeRenderers = Partial<Record<StreamingMarkdownNodeRendererKey, StreamingMarkdownDomNodeRenderer>>;
|
|
34
|
+
/**
|
|
35
|
+
* Inputs for one render. Pass the full set every time, like component props.
|
|
36
|
+
*
|
|
37
|
+
* @public
|
|
38
|
+
*/
|
|
39
|
+
interface StreamingMarkdownDomRenderProps {
|
|
40
|
+
/** Full markdown source that grows over time. */
|
|
41
|
+
content: string;
|
|
42
|
+
/** `true` once the source is final; hides the caret. */
|
|
43
|
+
isComplete?: boolean;
|
|
44
|
+
/** Parser option overrides. */
|
|
45
|
+
options?: Partial<StreamingMarkdownParserOptions>;
|
|
46
|
+
/** Render the default streaming caret inside the deepest open node. */
|
|
47
|
+
caret?: boolean;
|
|
48
|
+
/** Extra class on the root element. */
|
|
49
|
+
className?: string;
|
|
50
|
+
/** Called when a link or autolink is clicked. */
|
|
51
|
+
onLinkClick?: (event: MouseEvent, url: string, node: StreamingMarkdownAstNode) => void;
|
|
52
|
+
/** Called when a citation reference with a URL is clicked. */
|
|
53
|
+
onCitationClick?: (event: MouseEvent, citation: StreamingMarkdownCitationRenderData, node: StreamingMarkdownAstNode) => void;
|
|
54
|
+
/** Custom renderers keyed by node type. */
|
|
55
|
+
nodeRenderers?: StreamingMarkdownDomNodeRenderers;
|
|
56
|
+
/** Extra classes for the default element of a node type. */
|
|
57
|
+
nodeClassNames?: StreamingMarkdownNodeClassNames;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Creates a framework-free streaming markdown renderer that draws into
|
|
61
|
+
* `container`. It parses incrementally, renders the same render tree as the
|
|
62
|
+
* React and Vue renderers (same markup, URL sanitization, caret, citations,
|
|
63
|
+
* and per-token fade-in), and patches the DOM by key so text that is already
|
|
64
|
+
* on screen keeps its nodes and does not animate again.
|
|
65
|
+
*
|
|
66
|
+
* Creates nodes with `container.ownerDocument`, so it works with server DOMs
|
|
67
|
+
* that have no global `document`.
|
|
68
|
+
*
|
|
69
|
+
* @param container - Element the renderer owns. Its children are replaced.
|
|
70
|
+
* @returns `render(props)` to draw the latest props, and `destroy()` to remove
|
|
71
|
+
* the output and the click listener.
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```ts
|
|
75
|
+
* const renderer = createStreamingMarkdownDomRenderer(element);
|
|
76
|
+
* renderer.render({ content: "**Hi**", caret: true });
|
|
77
|
+
* renderer.render({ content: "**Hi** there", isComplete: true });
|
|
78
|
+
* renderer.destroy();
|
|
79
|
+
* ```
|
|
80
|
+
* @public
|
|
81
|
+
*/
|
|
82
|
+
declare function createStreamingMarkdownDomRenderer(container: HTMLElement): {
|
|
83
|
+
render: (nextProps: StreamingMarkdownDomRenderProps) => void;
|
|
84
|
+
destroy: () => void;
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Renderer returned by {@link createStreamingMarkdownDomRenderer}.
|
|
88
|
+
* @public
|
|
89
|
+
*/
|
|
90
|
+
type StreamingMarkdownDomRenderer = ReturnType<typeof createStreamingMarkdownDomRenderer>;
|
|
91
|
+
//#endregion
|
|
92
|
+
export { type StreamingMarkdownDomNodeRenderer, type StreamingMarkdownDomNodeRendererProps, type StreamingMarkdownDomNodeRenderers, type StreamingMarkdownDomRenderProps, type StreamingMarkdownDomRenderer, createStreamingMarkdownDomRenderer };
|
|
93
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/dom/streaming-markdown-dom-renderer.ts"],"mappings":";;;;;AA6BA;;;UAAiB,qCAAA;EAIL;EAFV,IAAA,EAAM,wBAAA;EAMK;EAJX,QAAA,EAAU,IAAA;EAMQ;EAJlB,WAAA,EAAa,IAAA;EAJb;EAMA,QAAA,GAAW,mCAAA;EAJX;EAMA,QAAA,EAAU,QAAA;AAAA;;;;;;;KASA,gCAAA,IACV,KAAA,EAAO,qCAAA,KACJ,IAAA;AAFL;;;;;AAAA,KASY,iCAAA,GAAoC,OAAA,CAC9C,MAAA,CAAO,gCAAA,EAAkC,gCAAA;;;;AAD3C;;UASiB,+BAAA;EARR;EAUP,OAAA;EAVA;EAYA,UAAA;EAbqD;EAerD,OAAA,GAAU,OAAA,CAAQ,8BAAA;EAf4B;EAiB9C,KAAA;EAhBO;EAkBP,SAAA;EAlByE;EAoBzE,WAAA,IACE,KAAA,EAAO,UAAA,EACP,GAAA,UACA,IAAA,EAAM,wBAAA;EAfO;EAkBf,eAAA,IACE,KAAA,EAAO,UAAA,EACP,QAAA,EAAU,mCAAA,EACV,IAAA,EAAM,wBAAA;;EAGR,aAAA,GAAgB,iCAAA;EAlBN;EAoBV,cAAA,GAAiB,+BAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;;iBA0SH,kCAAA,CAAmC,SAAA,EAAW,WAAA;sBA6BjC,+BAAA;;;;;;;KA2DjB,4BAAA,GAA+B,UAAA,QAClC,kCAAA"}
|