{"version":3,"file":"streaming-markdown.d.ts","sourceRoot":"","sources":["../../src/components/streaming-markdown.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,KAAK,gBAAgB,EAAY,KAAK,aAAa,EAAE,MAAM,eAAe,CAAC;AAEpF;;;;;;;GAOG;AACH,qBAAa,iBAAkB,YAAW,SAAS;IAOjD,QAAQ,CAAC,QAAQ,EAAE,MAAM;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM;IACzB,QAAQ,CAAC,KAAK,EAAE,aAAa;IAC7B,QAAQ,CAAC,gBAAgB,CAAC;IAT3B,OAAO,CAAC,KAAK,CAAW;IACxB,OAAO,CAAC,SAAS,CAAM;IACvB,OAAO,CAAC,SAAS,CAAS;IAE1B,YACC,OAAO,EAAE,MAAM,EACN,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,aAAa,EACpB,gBAAgB,CAAC,8BAAkB,EAI5C;IAED,qDAAqD;IACrD,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAQ1B;IAED,sDAAsD;IACtD,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAO1B;IAED;;;OAGG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAMjC;IAED,UAAU,IAAI,MAAM,CAEnB;IAED,UAAU,IAAI,IAAI,CAEjB;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAE9B;CACD;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAgDnD","sourcesContent":["import type { Component } from \"../tui.js\";\nimport { type DefaultTextStyle, Markdown, type MarkdownTheme } from \"./markdown.js\";\n\n/**\n * Markdown renderer for chunked, in-flight assistant text.\n *\n * Wraps Markdown so partial content renders cleanly even while half-formed\n * tokens (open fences, unfinished bold, dangling link brackets) are still\n * arriving. Once the stream finalizes, swap in the canonical render via\n * `finalize()`.\n */\nexport class StreamingMarkdown implements Component {\n\tprivate inner: Markdown;\n\tprivate rawBuffer = \"\";\n\tprivate finalized = false;\n\n\tconstructor(\n\t\tinitial: string,\n\t\treadonly paddingX: number,\n\t\treadonly paddingY: number,\n\t\treadonly theme: MarkdownTheme,\n\t\treadonly defaultTextStyle?: DefaultTextStyle,\n\t) {\n\t\tthis.inner = new Markdown(balancePartial(initial), paddingX, paddingY, theme, defaultTextStyle);\n\t\tthis.rawBuffer = initial;\n\t}\n\n\t/** Append a streamed chunk and update the render. */\n\tappend(chunk: string): void {\n\t\tif (this.finalized) {\n\t\t\tthis.rawBuffer += chunk;\n\t\t\tthis.inner.setText(this.rawBuffer);\n\t\t\treturn;\n\t\t}\n\t\tthis.rawBuffer += chunk;\n\t\tthis.inner.setText(balancePartial(this.rawBuffer));\n\t}\n\n\t/** Replace the entire buffer (e.g. when retrying). */\n\tsetText(text: string): void {\n\t\tthis.rawBuffer = text;\n\t\tif (this.finalized) {\n\t\t\tthis.inner.setText(text);\n\t\t} else {\n\t\t\tthis.inner.setText(balancePartial(text));\n\t\t}\n\t}\n\n\t/**\n\t * Mark the stream complete and swap to the canonical full markdown render.\n\t * Subsequent appends (rare) keep the canonical render path.\n\t */\n\tfinalize(canonical?: string): void {\n\t\tthis.finalized = true;\n\t\tif (canonical !== undefined) {\n\t\t\tthis.rawBuffer = canonical;\n\t\t}\n\t\tthis.inner.setText(this.rawBuffer);\n\t}\n\n\tgetRawText(): string {\n\t\treturn this.rawBuffer;\n\t}\n\n\tinvalidate(): void {\n\t\tthis.inner.invalidate();\n\t}\n\n\trender(width: number): string[] {\n\t\treturn this.inner.render(width);\n\t}\n}\n\n/**\n * Balance partial markdown so the in-flight render doesn't trip over\n * unfinished syntax. Conservative: no semantic re-parsing, just close the\n * obvious cases (fences, runs of ** / * / ~~, dangling [ ).\n */\nexport function balancePartial(text: string): string {\n\tlet working = text;\n\n\t// Trim a trailing dangling link prefix like \"see [the docs\" or \"see [docs](\".\n\t// We only strip the unbalanced opening so the partial render shows the\n\t// preceding prose without an angry render of brackets.\n\tconst lastOpenBracket = working.lastIndexOf(\"[\");\n\tif (lastOpenBracket !== -1) {\n\t\tconst tail = working.slice(lastOpenBracket);\n\t\tconst closingBracket = tail.indexOf(\"]\");\n\t\tconst openParen = closingBracket !== -1 ? tail.indexOf(\"(\", closingBracket) : -1;\n\t\tconst closingParen = openParen !== -1 ? tail.indexOf(\")\", openParen) : -1;\n\t\tif (closingBracket === -1 || (openParen !== -1 && closingParen === -1)) {\n\t\t\tworking = working.slice(0, lastOpenBracket);\n\t\t}\n\t}\n\n\t// Close an open fenced code block. We count fence lines (``` at line start,\n\t// possibly with lang). Odd count → still inside a fence → append a closing\n\t// fence so the render terminates cleanly.\n\tconst lines = working.split(\"\\n\");\n\tconst fenceIndexes = lines.map((line, i) => (/^\\s{0,3}```/.test(line) ? i : -1)).filter((i) => i !== -1);\n\tif (fenceIndexes.length % 2 === 1) {\n\t\t// The last fence opener is still open (code is streaming in). Strip its\n\t\t// language so the renderer skips syntax highlighting for the in-flight\n\t\t// block: its text grows every frame, so highlighting can't be memoized\n\t\t// and would re-run the CPU-bound pass over an ever-larger buffer each\n\t\t// frame, stalling the single-threaded TUI loop. The block highlights once\n\t\t// the closing fence actually arrives (or on finalize).\n\t\tconst openerIndex = fenceIndexes[fenceIndexes.length - 1];\n\t\tlines[openerIndex] = lines[openerIndex].replace(/^(\\s{0,3}```).*$/, \"$1\");\n\t\tworking = lines.join(\"\\n\");\n\t\t// If the last fence opener is mid-line (no trailing newline), add one.\n\t\tif (!working.endsWith(\"\\n\")) working += \"\\n\";\n\t\tworking += \"```\\n\";\n\t}\n\n\t// Strip trailing odd-count strong markers ** that haven't been closed.\n\tif (countOutsideCode(working, \"**\") % 2 === 1) {\n\t\tworking = stripTrailingMarker(working, \"**\");\n\t}\n\t// Same for ~~ (strikethrough).\n\tif (countOutsideCode(working, \"~~\") % 2 === 1) {\n\t\tworking = stripTrailingMarker(working, \"~~\");\n\t}\n\t// Single * is ambiguous (lists, italics); leave it alone.\n\n\treturn working;\n}\n\nfunction countOutsideCode(text: string, marker: string): number {\n\t// Naive: count occurrences of `marker` outside backtick spans. Good enough\n\t// for the partial-balance use case — false positives only mean we leave a\n\t// run open, which the renderer already tolerates.\n\tlet count = 0;\n\tlet inCode = false;\n\tlet i = 0;\n\twhile (i < text.length) {\n\t\tconst ch = text[i];\n\t\tif (ch === \"`\") {\n\t\t\tinCode = !inCode;\n\t\t\ti += 1;\n\t\t\tcontinue;\n\t\t}\n\t\tif (!inCode && text.startsWith(marker, i)) {\n\t\t\tcount += 1;\n\t\t\ti += marker.length;\n\t\t\tcontinue;\n\t\t}\n\t\ti += 1;\n\t}\n\treturn count;\n}\n\nfunction stripTrailingMarker(text: string, marker: string): string {\n\tconst idx = text.lastIndexOf(marker);\n\tif (idx === -1) return text;\n\treturn text.slice(0, idx);\n}\n"]}