@copilotkit/markdown 2.0.0-rc.20 → 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/dist/index.mjs CHANGED
@@ -5,7 +5,8 @@
5
5
  const DEFAULT_OPTIONS = {
6
6
  segmenter: true,
7
7
  enableTables: true,
8
- enableAutolinks: true
8
+ enableAutolinks: true,
9
+ enableMath: false
9
10
  };
10
11
  /**
11
12
  * Merges user-provided parser options with defaults.
@@ -17,7 +18,8 @@ function normalizeOptions(options) {
17
18
  return {
18
19
  segmenter: options?.segmenter ?? DEFAULT_OPTIONS.segmenter,
19
20
  enableTables: options?.enableTables ?? DEFAULT_OPTIONS.enableTables,
20
- enableAutolinks: options?.enableAutolinks ?? DEFAULT_OPTIONS.enableAutolinks
21
+ enableAutolinks: options?.enableAutolinks ?? DEFAULT_OPTIONS.enableAutolinks,
22
+ enableMath: options?.enableMath ?? DEFAULT_OPTIONS.enableMath
21
23
  };
22
24
  }
23
25
  /**
@@ -663,6 +665,13 @@ function parseInline(text, absoluteStart, path, context) {
663
665
  i = code.next;
664
666
  continue;
665
667
  }
668
+ const math = context.options.enableMath ? parseMathInline(text, i, absoluteStart, path, nodes.length) : null;
669
+ if (math) {
670
+ flushTextBuffer();
671
+ nodes.push(math.node);
672
+ i = math.next;
673
+ continue;
674
+ }
666
675
  const strong = parseDelimitedInline(text, i, "**", "strong", absoluteStart, path, nodes.length, context, citations, warnings, hasWarnedSegmenterUnavailable) ?? parseDelimitedInline(text, i, "__", "strong", absoluteStart, path, nodes.length, context, citations, warnings, hasWarnedSegmenterUnavailable);
667
676
  if (strong) {
668
677
  flushTextBuffer();
@@ -1014,6 +1023,32 @@ function parseInlineCode(text, at, start, path, index) {
1014
1023
  next: close + len
1015
1024
  };
1016
1025
  }
1026
+ const DISPLAY_MATH = /^\$\$([\s\S]+?)\$\$/;
1027
+ const INLINE_MATH = /^\$(?!\s)([^$\n]*?\S)\$(?!\d)/;
1028
+ function parseMathInline(text, at, start, path, index) {
1029
+ if (text[at] !== "$") return null;
1030
+ const rest = text.slice(at);
1031
+ const display = DISPLAY_MATH.exec(rest);
1032
+ const match = display ?? INLINE_MATH.exec(rest);
1033
+ if (!match) return null;
1034
+ return {
1035
+ node: {
1036
+ path: `${path}.${index}`,
1037
+ type: "math",
1038
+ range: {
1039
+ start: start + at,
1040
+ end: start + at + match[0].length
1041
+ },
1042
+ closed: true,
1043
+ props: {
1044
+ text: match[1],
1045
+ display: display !== null
1046
+ },
1047
+ children: []
1048
+ },
1049
+ next: at + match[0].length
1050
+ };
1051
+ }
1017
1052
  function parseDelimitedInline(text, at, delimiter, type, start, path, index, context, citations, warnings, hasWarnedSegmenterUnavailable) {
1018
1053
  if (!text.startsWith(delimiter, at)) return null;
1019
1054
  const close = text.indexOf(delimiter, at + delimiter.length);
@@ -1070,6 +1105,11 @@ function parseDelimitedInline(text, at, delimiter, type, start, path, index, con
1070
1105
  //#endregion
1071
1106
  //#region src/parser/block-parser.ts
1072
1107
  /**
1108
+ * GFM task-list marker at the start of a list item: `[ ]`, `[x]` or `[X]`,
1109
+ * followed by whitespace and then content.
1110
+ */
1111
+ const TASK_MARKER = /^\[([ xX])\][ \t]+(?=\S)/;
1112
+ /**
1073
1113
  * Parses a contiguous range of source lines into block-level draft nodes.
1074
1114
  *
1075
1115
  * @param lines - Line-split source with offsets.
@@ -1342,9 +1382,10 @@ function parseList(lines, from, to, path, context, ordered) {
1342
1382
  const marker = matchListItem(lines[i].text);
1343
1383
  if (!marker || marker.ordered !== ordered) break;
1344
1384
  if (ordered && listStartNumber === null) listStartNumber = marker.start;
1385
+ const task = TASK_MARKER.exec(marker.content);
1345
1386
  const contentLines = [{
1346
1387
  ...lines[i],
1347
- text: marker.content
1388
+ text: task ? marker.content.slice(task[0].length) : marker.content
1348
1389
  }];
1349
1390
  const contentIndent = marker.contentIndent;
1350
1391
  i += 1;
@@ -1399,7 +1440,7 @@ function parseList(lines, from, to, path, context, ordered) {
1399
1440
  end: contentLines[contentLines.length - 1].end
1400
1441
  },
1401
1442
  closed: context.isComplete || contentLines[contentLines.length - 1].hasNewline,
1402
- props: {},
1443
+ props: task ? { checked: task[1] !== " " } : {},
1403
1444
  children: [paragraph]
1404
1445
  });
1405
1446
  citations = parsedInline.citations;
@@ -1674,11 +1715,19 @@ function opToAstNode(op, id, parentId, children) {
1674
1715
  tight: op.props["tight"],
1675
1716
  children
1676
1717
  };
1677
- case "list-item": return {
1678
- ...base,
1679
- type: "list-item",
1680
- children
1681
- };
1718
+ case "list-item": {
1719
+ const checked = op.props["checked"];
1720
+ return checked === void 0 ? {
1721
+ ...base,
1722
+ type: "list-item",
1723
+ children
1724
+ } : {
1725
+ ...base,
1726
+ type: "list-item",
1727
+ checked,
1728
+ children
1729
+ };
1730
+ }
1682
1731
  case "code-block": return {
1683
1732
  ...base,
1684
1733
  type: "code-block",
@@ -1762,6 +1811,12 @@ function opToAstNode(op, id, parentId, children) {
1762
1811
  url: op.props["url"],
1763
1812
  text: op.props["text"]
1764
1813
  };
1814
+ case "math": return {
1815
+ ...base,
1816
+ type: "math",
1817
+ text: op.props["text"],
1818
+ display: op.props["display"]
1819
+ };
1765
1820
  case "citation": return {
1766
1821
  ...base,
1767
1822
  type: "citation",
@@ -2000,12 +2055,14 @@ function isProduction() {
2000
2055
  *
2001
2056
  * No-op in production and after the first warning. Only the built-in default
2002
2057
  * renderers call this — a custom `markdownRenderer` never triggers it.
2058
+ * Pass `{ rendersMath: true }` from a default renderer that typesets math, so
2059
+ * only code blocks are flagged.
2003
2060
  *
2004
2061
  * @internal
2005
2062
  */
2006
- function warnUnsupportedRichSyntaxOnce(content) {
2063
+ function warnUnsupportedRichSyntaxOnce(content, { rendersMath = false } = {}) {
2007
2064
  if (warned || !content || isProduction()) return;
2008
- const hasMath = BLOCK_MATH.test(content) || BRACKET_MATH.test(content);
2065
+ const hasMath = !rendersMath && (BLOCK_MATH.test(content) || BRACKET_MATH.test(content));
2009
2066
  const hasHighlightableCode = HIGHLIGHTABLE_CODE.test(content);
2010
2067
  if (!hasMath && !hasHighlightableCode) return;
2011
2068
  warned = true;
@@ -2014,5 +2071,448 @@ function warnUnsupportedRichSyntaxOnce(content) {
2014
2071
  }
2015
2072
 
2016
2073
  //#endregion
2017
- export { createStreamingMarkdownParserState, finalizeStreamingMarkdown, parseStreamingMarkdownChunk, warnUnsupportedRichSyntaxOnce };
2074
+ //#region src/render/parser-session.ts
2075
+ function getSegmenterKey(segmenter) {
2076
+ if (segmenter === true || segmenter === false) return String(segmenter);
2077
+ return `object:${segmenter.locale ?? ""}:${segmenter.granularity ?? "word"}`;
2078
+ }
2079
+ function getOptionsKey(options) {
2080
+ return `${getSegmenterKey(options.segmenter)}|tables:${String(options.enableTables)}|autolinks:${String(options.enableAutolinks)}|math:${String(options.enableMath)}`;
2081
+ }
2082
+ function parseFullText(text, options, isCompleteInput) {
2083
+ const initialState = createStreamingMarkdownParserState(options);
2084
+ const parsedState = text.length > 0 ? parseStreamingMarkdownChunk(initialState, text) : initialState;
2085
+ return isCompleteInput ? finalizeStreamingMarkdown(parsedState) : parsedState;
2086
+ }
2087
+ /**
2088
+ * Returns the parser session for `text`, reusing `previous` when it can.
2089
+ *
2090
+ * - Same inputs: returns `previous` unchanged.
2091
+ * - Text grew by a suffix: parses only the suffix, so unchanged AST nodes keep
2092
+ * their identity (renderers rely on this to keep DOM and animations stable).
2093
+ * - Text was replaced or options changed: parses from scratch.
2094
+ * - `isComplete` toggled: finalizes (or re-opens) the current state.
2095
+ *
2096
+ * @param previous - The session from the last update, or `null` on first use.
2097
+ * @param text - Full markdown source, which usually grows over time.
2098
+ * @param options - Parser option overrides.
2099
+ * @param isCompleteInput - `true` once the source is final.
2100
+ * @public
2101
+ */
2102
+ function updateStreamingMarkdownParserSession(previous, text, options, isCompleteInput) {
2103
+ const normalized = normalizeOptions(options);
2104
+ const optionsKey = getOptionsKey(normalized);
2105
+ if (previous === null || previous.optionsKey !== optionsKey) return {
2106
+ parserState: parseFullText(text, normalized, isCompleteInput),
2107
+ text,
2108
+ optionsKey,
2109
+ isCompleteInput
2110
+ };
2111
+ const textChanged = text !== previous.text;
2112
+ const completionChanged = previous.isCompleteInput !== isCompleteInput;
2113
+ if (!textChanged && !completionChanged) return previous;
2114
+ if (!textChanged) {
2115
+ const parserState = isCompleteInput ? finalizeStreamingMarkdown(previous.parserState) : parseStreamingMarkdownChunk(previous.parserState, "");
2116
+ return {
2117
+ ...previous,
2118
+ parserState,
2119
+ isCompleteInput
2120
+ };
2121
+ }
2122
+ const suffix = text.startsWith(previous.text) ? text.slice(previous.text.length) : null;
2123
+ const parsed = suffix === null ? parseFullText(text, normalized, false) : parseStreamingMarkdownChunk(previous.parserState, suffix);
2124
+ return {
2125
+ parserState: isCompleteInput ? finalizeStreamingMarkdown(parsed) : parsed,
2126
+ text,
2127
+ optionsKey,
2128
+ isCompleteInput
2129
+ };
2130
+ }
2131
+
2132
+ //#endregion
2133
+ //#region src/render/render-tree.ts
2134
+ /**
2135
+ * Class on the root element every renderer emits.
2136
+ * @public
2137
+ */
2138
+ const STREAMING_MARKDOWN_ROOT_CLASS = "cpk-streaming-markdown-root";
2139
+ /**
2140
+ * Node classes CopilotKit's built-in chat markdown renderers (React, Vue,
2141
+ * Angular) apply, so all three emit the same themed markup.
2142
+ * @public
2143
+ */
2144
+ const CHAT_MARKDOWN_NODE_CLASS_NAMES = { codeBlock: "cpk-markdown-code-block" };
2145
+ const SEGMENT_CLASS = "cpk-streaming-markdown-segment";
2146
+ const CITATION_CLASS = "cpk-streaming-markdown-citation";
2147
+ const CITATION_LABEL_CLASS = "cpk-streaming-markdown-citation-label";
2148
+ const WORD_JOINER = "⁠";
2149
+ /**
2150
+ * Attributes of the default caret element.
2151
+ * @public
2152
+ */
2153
+ const STREAMING_MARKDOWN_CARET_ATTRS = {
2154
+ "aria-hidden": "true",
2155
+ class: "cpk-streaming-markdown-caret",
2156
+ "data-streaming-markdown-caret": "true"
2157
+ };
2158
+ /**
2159
+ * Inline style of the default caret element (camelCase CSS properties).
2160
+ * @public
2161
+ */
2162
+ const STREAMING_MARKDOWN_CARET_STYLE = {
2163
+ display: "inline-block",
2164
+ width: "0.48em",
2165
+ height: "0.48em",
2166
+ marginInlineStart: "0.08em",
2167
+ verticalAlign: "-0.08em",
2168
+ borderRadius: "999px",
2169
+ backgroundColor: "currentColor",
2170
+ opacity: "0.55"
2171
+ };
2172
+ const CITATION_STYLES = `
2173
+ .${STREAMING_MARKDOWN_ROOT_CLASS} .${CITATION_CLASS} {
2174
+ vertical-align: baseline;
2175
+ }
2176
+
2177
+ .${STREAMING_MARKDOWN_ROOT_CLASS} .${CITATION_LABEL_CLASS} {
2178
+ display: inline-flex;
2179
+ align-items: center;
2180
+ justify-content: center;
2181
+ inline-size: 1.4em;
2182
+ block-size: 1.4em;
2183
+ border-radius: 999px;
2184
+ border: 1px solid hsl(0 0% 50% / 0.35);
2185
+ background-color: hsl(0 0% 50% / 0.16);
2186
+ color: inherit;
2187
+ font-size: 0.7em;
2188
+ line-height: 1;
2189
+ font-variant-numeric: tabular-nums;
2190
+ text-decoration: none;
2191
+ transform: translateY(-0.15em);
2192
+ }
2193
+ `;
2194
+ const SEGMENT_FADE_IN_STYLES = `
2195
+ .${STREAMING_MARKDOWN_ROOT_CLASS} .${SEGMENT_CLASS} {
2196
+ opacity: 1;
2197
+ transition: opacity 400ms ease-out;
2198
+ @starting-style {
2199
+ opacity: 0;
2200
+ }
2201
+ }
2202
+ `;
2203
+ const SEGMENT_FALLBACK_STYLES = `
2204
+ .${STREAMING_MARKDOWN_ROOT_CLASS} .${SEGMENT_CLASS} {
2205
+ opacity: 1;
2206
+ transition: opacity 400ms ease-out;
2207
+ }
2208
+ `;
2209
+ /**
2210
+ * Stylesheet every renderer injects next to its root: the per-token fade-in
2211
+ * for text segments and the citation pill. Read at render time, so it is safe
2212
+ * to call during server rendering.
2213
+ *
2214
+ * @public
2215
+ */
2216
+ function getStreamingMarkdownStyles() {
2217
+ return (typeof navigator !== "undefined" && /jsdom/i.test(navigator.userAgent) ? SEGMENT_FALLBACK_STYLES : SEGMENT_FADE_IN_STYLES) + CITATION_STYLES;
2218
+ }
2219
+ const SAFE_HREF = /^(https?:|mailto:|tel:|#|\/|\.\/|\.\.\/)/i;
2220
+ const SAFE_IMG_SRC = /^(https?:|data:image\/(?!svg)|\/|\.\/|\.\.\/)/i;
2221
+ /**
2222
+ * Returns `href` when its scheme is allowed for links, else `undefined`.
2223
+ * @public
2224
+ */
2225
+ function sanitizeStreamingMarkdownHref(href) {
2226
+ if (!href) return void 0;
2227
+ return SAFE_HREF.test(href.trim()) ? href : void 0;
2228
+ }
2229
+ /**
2230
+ * Returns `src` when its scheme is allowed for images, else `undefined`.
2231
+ * @public
2232
+ */
2233
+ function sanitizeStreamingMarkdownImageSrc(src) {
2234
+ if (!src) return void 0;
2235
+ return SAFE_IMG_SRC.test(src.trim()) ? src : void 0;
2236
+ }
2237
+ const RENDERER_KEY_BY_NODE_TYPE = {
2238
+ document: "document",
2239
+ paragraph: "paragraph",
2240
+ heading: "heading",
2241
+ blockquote: "blockquote",
2242
+ list: "list",
2243
+ "list-item": "listItem",
2244
+ "code-block": "codeBlock",
2245
+ table: "table",
2246
+ "table-row": "tableRow",
2247
+ "table-cell": "tableCell",
2248
+ "thematic-break": "thematicBreak",
2249
+ text: "text",
2250
+ em: "em",
2251
+ strong: "strong",
2252
+ strikethrough: "strikethrough",
2253
+ "inline-code": "inlineCode",
2254
+ "soft-break": "softBreak",
2255
+ "hard-break": "hardBreak",
2256
+ image: "image",
2257
+ link: "link",
2258
+ autolink: "autolink",
2259
+ citation: "citation",
2260
+ math: "math"
2261
+ };
2262
+ const SLOT = {
2263
+ kind: "slot",
2264
+ key: "slot"
2265
+ };
2266
+ function indexNodes(state) {
2267
+ return new Map(state.nodes.map((node) => [node.id, node]));
2268
+ }
2269
+ function findOpenNode(stack, nodeById) {
2270
+ for (let index = stack.length - 1; index >= 0; index -= 1) {
2271
+ const candidate = nodeById.get(stack[index]);
2272
+ if (candidate && candidate.type !== "document") return candidate;
2273
+ }
2274
+ return null;
2275
+ }
2276
+ /**
2277
+ * The deepest open (still streaming) node other than the document, or `null`.
2278
+ * This is where the caret goes.
2279
+ *
2280
+ * @public
2281
+ */
2282
+ function findStreamingMarkdownOpenNode(state) {
2283
+ return findOpenNode(state.stack, indexNodes(state));
2284
+ }
2285
+ /**
2286
+ * Citation number and definition for a citation node.
2287
+ *
2288
+ * @public
2289
+ */
2290
+ function getStreamingMarkdownCitationData(node, citations) {
2291
+ return {
2292
+ id: node.idRef,
2293
+ number: node.number ?? citations.numbers[node.idRef] ?? node.idRef,
2294
+ definition: citations.definitions[node.idRef]
2295
+ };
2296
+ }
2297
+ function nodeAttrs(node, context, extra = {}) {
2298
+ const attrs = {};
2299
+ const className = context.nodeClassNames?.[RENDERER_KEY_BY_NODE_TYPE[node.type]];
2300
+ if (className) attrs.class = className;
2301
+ const entries = Object.entries(extra);
2302
+ for (const [name, value] of entries) if (value !== void 0) attrs[name] = value;
2303
+ attrs["data-streaming-markdown-node"] = node.type;
2304
+ attrs["data-node-open"] = String(!node.closed);
2305
+ return attrs;
2306
+ }
2307
+ function element(key, tag, attrs, children, action) {
2308
+ return action ? {
2309
+ kind: "element",
2310
+ key,
2311
+ tag,
2312
+ attrs,
2313
+ children,
2314
+ action
2315
+ } : {
2316
+ kind: "element",
2317
+ key,
2318
+ tag,
2319
+ attrs,
2320
+ children
2321
+ };
2322
+ }
2323
+ function text(key, value) {
2324
+ return {
2325
+ kind: "text",
2326
+ key,
2327
+ text: value
2328
+ };
2329
+ }
2330
+ function caretFor(nodeId, context) {
2331
+ return context.caretTargetNodeId === nodeId ? [{
2332
+ kind: "caret",
2333
+ key: `caret-${nodeId}`
2334
+ }] : [];
2335
+ }
2336
+ function renderNode(node, context, children, output, citation) {
2337
+ const base = {
2338
+ kind: "node",
2339
+ key: String(node.id),
2340
+ node,
2341
+ rendererKey: RENDERER_KEY_BY_NODE_TYPE[node.type],
2342
+ children,
2343
+ output,
2344
+ containsCaret: context.caretPath.has(node.id)
2345
+ };
2346
+ return citation ? {
2347
+ ...base,
2348
+ citation
2349
+ } : base;
2350
+ }
2351
+ function containerChildren(node, context) {
2352
+ return [...node.children.flatMap((childId) => {
2353
+ const child = context.nodeById.get(childId);
2354
+ return child ? [buildNode(child, context)] : [];
2355
+ }), ...caretFor(node.id, context)];
2356
+ }
2357
+ /** A container whose default output is one element wrapping its children. */
2358
+ function wrapped(node, context, tag, extra = {}, action) {
2359
+ const attrs = nodeAttrs(node, context, extra);
2360
+ return renderNode(node, context, containerChildren(node, context), [element(String(node.id), tag, attrs, [SLOT], action)]);
2361
+ }
2362
+ function textSegments(node) {
2363
+ if (node.text.length === 0) return [];
2364
+ if (node.segments.length === 0) return [element(`segment-${node.id}-full`, "span", {
2365
+ class: SEGMENT_CLASS,
2366
+ "data-streaming-markdown-segment-kind": "full",
2367
+ "data-streaming-markdown-whitespace": "false"
2368
+ }, [text("text", node.text)])];
2369
+ return node.segments.map((segment) => element(`segment-${node.id}-${segment.start}-${segment.kind}`, "span", {
2370
+ class: SEGMENT_CLASS,
2371
+ "data-streaming-markdown-segment-kind": segment.kind,
2372
+ "data-streaming-markdown-whitespace": String(segment.isWhitespace)
2373
+ }, [text("text", segment.noBreakBefore ? `${WORD_JOINER}${segment.text}` : segment.text)]));
2374
+ }
2375
+ function taskCheckbox(node, context) {
2376
+ const parent = node.parentId == null ? void 0 : context.nodeById.get(node.parentId);
2377
+ if (!(parent?.type === "list-item" && parent.checked !== void 0 && parent.children[0] === node.id)) return [];
2378
+ const attrs = {
2379
+ type: "checkbox",
2380
+ disabled: true,
2381
+ "data-streaming-markdown-task": "true"
2382
+ };
2383
+ if (parent.checked) attrs.checked = true;
2384
+ return [element(`task-${parent.id}`, "input", attrs, []), text("task-space", " ")];
2385
+ }
2386
+ function buildCitation(node, context) {
2387
+ const citation = getStreamingMarkdownCitationData(node, context.citations);
2388
+ const label = [text("label", String(citation.number))];
2389
+ const href = sanitizeStreamingMarkdownHref(citation.definition?.url);
2390
+ const inner = href ? element("label", "a", {
2391
+ href,
2392
+ target: "_blank",
2393
+ rel: "noopener noreferrer",
2394
+ role: "doc-noteref",
2395
+ class: CITATION_LABEL_CLASS
2396
+ }, label, {
2397
+ type: "citation",
2398
+ citation,
2399
+ node
2400
+ }) : element("label", "span", {
2401
+ role: "doc-noteref",
2402
+ class: CITATION_LABEL_CLASS
2403
+ }, label);
2404
+ const attrs = nodeAttrs(node, context);
2405
+ const supClass = attrs.class ? `${CITATION_CLASS} ${attrs.class}` : CITATION_CLASS;
2406
+ return renderNode(node, context, null, [element(String(node.id), "sup", {
2407
+ ...attrs,
2408
+ class: supClass
2409
+ }, [inner])], citation);
2410
+ }
2411
+ function buildNode(node, context) {
2412
+ const key = String(node.id);
2413
+ switch (node.type) {
2414
+ case "document": return renderNode(node, context, containerChildren(node, context), [SLOT]);
2415
+ case "paragraph": {
2416
+ const attrs = nodeAttrs(node, context);
2417
+ return renderNode(node, context, containerChildren(node, context), [element(key, "p", attrs, [...taskCheckbox(node, context), SLOT])]);
2418
+ }
2419
+ case "heading": return wrapped(node, context, `h${node.level}`);
2420
+ case "blockquote": return wrapped(node, context, "blockquote");
2421
+ case "list": return wrapped(node, context, node.ordered ? "ol" : "ul", {
2422
+ start: node.ordered && node.start != null ? String(node.start) : void 0,
2423
+ "data-list-tight": String(node.tight)
2424
+ });
2425
+ case "list-item": return wrapped(node, context, "li");
2426
+ case "table": {
2427
+ const attrs = nodeAttrs(node, context);
2428
+ return renderNode(node, context, containerChildren(node, context), [element(key, "table", attrs, [element("tbody", "tbody", {}, [SLOT])])]);
2429
+ }
2430
+ case "table-row": return wrapped(node, context, "tr");
2431
+ case "table-cell": {
2432
+ const parent = node.parentId == null ? void 0 : context.nodeById.get(node.parentId);
2433
+ return wrapped(node, context, parent?.type === "table-row" && parent.isHeader ? "th" : "td");
2434
+ }
2435
+ case "em": return wrapped(node, context, "em");
2436
+ case "strong": return wrapped(node, context, "strong");
2437
+ case "strikethrough": return wrapped(node, context, "s");
2438
+ case "link": {
2439
+ const href = sanitizeStreamingMarkdownHref(node.url);
2440
+ return wrapped(node, context, "a", {
2441
+ href,
2442
+ target: "_blank",
2443
+ rel: "noopener noreferrer",
2444
+ title: node.title
2445
+ }, href ? {
2446
+ type: "link",
2447
+ url: href,
2448
+ node
2449
+ } : void 0);
2450
+ }
2451
+ case "code-block": return renderNode(node, context, null, [element(key, "pre", nodeAttrs(node, context), [element("code", "code", node.info ? { "data-code-info": node.info } : {}, [text("text", node.text), ...caretFor(node.id, context)])])]);
2452
+ case "thematic-break": return renderNode(node, context, null, [element(key, "hr", nodeAttrs(node, context), [])]);
2453
+ case "hard-break": return renderNode(node, context, null, [element(key, "br", nodeAttrs(node, context), [])]);
2454
+ case "text": return renderNode(node, context, textSegments(node), [SLOT]);
2455
+ case "soft-break": return renderNode(node, context, [text("text", "\n")], [SLOT]);
2456
+ case "inline-code": return renderNode(node, context, [text("text", node.text)], [element(key, "code", nodeAttrs(node, context), [SLOT])]);
2457
+ case "image": return renderNode(node, context, null, [element(key, "img", nodeAttrs(node, context, {
2458
+ src: sanitizeStreamingMarkdownImageSrc(node.url),
2459
+ alt: node.alt,
2460
+ title: node.title
2461
+ }), [])]);
2462
+ case "autolink": {
2463
+ const href = sanitizeStreamingMarkdownHref(node.url);
2464
+ return renderNode(node, context, [text("text", node.text)], [element(key, "a", nodeAttrs(node, context, {
2465
+ href,
2466
+ target: "_blank",
2467
+ rel: "noopener noreferrer"
2468
+ }), [SLOT], href ? {
2469
+ type: "link",
2470
+ url: href,
2471
+ node
2472
+ } : void 0)]);
2473
+ }
2474
+ case "math": return renderNode(node, context, [text("text", node.text)], [element(key, "span", nodeAttrs(node, context, { "data-math-display": String(node.display) }), [SLOT])]);
2475
+ case "citation": return buildCitation(node, context);
2476
+ default: return node;
2477
+ }
2478
+ }
2479
+ /**
2480
+ * Turns parser state into a framework-neutral render tree: which elements to
2481
+ * create, their sanitized attributes, click actions, text segments for the
2482
+ * per-token animation, and where the caret goes. The React, Vue, and DOM
2483
+ * renderers all render this tree, so markup and sanitization live here only.
2484
+ *
2485
+ * @param state - Parser state from the streaming markdown parser.
2486
+ * @param options - Whether to place a caret, and optional per-node classes.
2487
+ * @returns The root render node (`null` when the state has no root) and the
2488
+ * open node the caret targets.
2489
+ * @public
2490
+ */
2491
+ function buildStreamingMarkdownRenderTree(state, options) {
2492
+ const nodeById = indexNodes(state);
2493
+ const openNode = findOpenNode(state.stack, nodeById);
2494
+ const caretTargetNodeId = options.caret && !state.isComplete && openNode ? openNode.id : null;
2495
+ const caretPath = /* @__PURE__ */ new Set();
2496
+ let cursor = caretTargetNodeId == null ? void 0 : nodeById.get(caretTargetNodeId);
2497
+ while (cursor) {
2498
+ caretPath.add(cursor.id);
2499
+ cursor = cursor.parentId == null ? void 0 : nodeById.get(cursor.parentId);
2500
+ }
2501
+ const context = {
2502
+ nodeById,
2503
+ citations: state.citations,
2504
+ caretTargetNodeId,
2505
+ caretPath,
2506
+ nodeClassNames: options.nodeClassNames
2507
+ };
2508
+ const rootNode = state.rootId == null ? void 0 : nodeById.get(state.rootId);
2509
+ return {
2510
+ root: rootNode ? buildNode(rootNode, context) : null,
2511
+ openNode,
2512
+ isComplete: state.isComplete
2513
+ };
2514
+ }
2515
+
2516
+ //#endregion
2517
+ export { CHAT_MARKDOWN_NODE_CLASS_NAMES, STREAMING_MARKDOWN_CARET_ATTRS, STREAMING_MARKDOWN_CARET_STYLE, STREAMING_MARKDOWN_ROOT_CLASS, buildStreamingMarkdownRenderTree, createStreamingMarkdownParserState, finalizeStreamingMarkdown, findStreamingMarkdownOpenNode, getStreamingMarkdownCitationData, getStreamingMarkdownStyles, parseStreamingMarkdownChunk, sanitizeStreamingMarkdownHref, sanitizeStreamingMarkdownImageSrc, updateStreamingMarkdownParserSession, warnUnsupportedRichSyntaxOnce };
2018
2518
  //# sourceMappingURL=index.mjs.map