@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 CHANGED
@@ -1,25 +1,42 @@
1
1
  # @copilotkit/markdown
2
2
 
3
- A zero-dependency streaming Markdown parser with framework renderers for CopilotKit. The root entry point exposes the framework-agnostic parser. The `/react`, `/vue`, and `/react-native` entry points each expose a `StreamingMarkdownRenderer` for incrementally streamed Markdown text.
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"}