@localix-ai/flowdown 0.5.0 → 0.5.1
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 +8 -4
- package/dist/core/blocks.js +3 -2
- package/dist/core/blocks.js.map +1 -1
- package/dist/core/continues.js +15 -0
- package/dist/core/continues.js.map +1 -0
- package/dist/core/fence-scan.js +10 -1
- package/dist/core/fence-scan.js.map +1 -1
- package/dist/core/file-tree-parse.js +1 -1
- package/dist/core/file-tree-parse.js.map +1 -1
- package/dist/core/heal.js +1 -1
- package/dist/core/heal.js.map +1 -1
- package/dist/document/fence.js +3 -2
- package/dist/document/fence.js.map +1 -1
- package/dist/dom/index.js +25 -7
- package/dist/dom/index.js.map +1 -1
- package/dist/element/index.d.ts +6 -0
- package/dist/element/index.js.map +1 -1
- package/dist/engine/blocks.js +11 -0
- package/dist/engine/blocks.js.map +1 -1
- package/dist/engine/inlines.js +40 -5
- package/dist/engine/inlines.js.map +1 -1
- package/dist/react/code-block.js +15 -2
- package/dist/react/code-block.js.map +1 -1
- package/dist/react/draw.js +1 -0
- package/dist/react/draw.js.map +1 -1
- package/dist/react/markdown.js +4 -2
- package/dist/react/markdown.js.map +1 -1
- package/dist/react/scrolls.js +62 -0
- package/dist/react/scrolls.js.map +1 -0
- package/dist/react/table.js +6 -20
- package/dist/react/table.js.map +1 -1
- package/dist/react/use-smooth-text.js +2 -1
- package/dist/react/use-smooth-text.js.map +1 -1
- package/dist/render/labels.d.ts +5 -0
- package/dist/render/labels.js +3 -0
- package/dist/render/labels.js.map +1 -1
- package/dist/render/tree.js +2 -0
- package/dist/render/tree.js.map +1 -1
- package/dist/render/view.d.ts +4 -1
- package/dist/render/view.js +6 -2
- package/dist/render/view.js.map +1 -1
- package/dist/render/views.js +2 -2
- package/dist/render/views.js.map +1 -1
- package/dist/styles.css +12 -1
- package/dist/tui/wrap.js +5 -4
- package/dist/tui/wrap.js.map +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
8
|
<strong>Ultrafast, lightweight streaming Markdown for React and AI replies</strong><br>
|
|
9
|
-
Its own parser · 0 dependencies ·
|
|
9
|
+
Its own parser · 0 dependencies · 32 KB gzipped · 17× faster on a long stream
|
|
10
10
|
</p>
|
|
11
11
|
|
|
12
12
|
<p align="center">
|
|
@@ -53,16 +53,20 @@ export function Reply({ text, done }: { text: string; done: boolean }) {
|
|
|
53
53
|
|
|
54
54
|
## Why
|
|
55
55
|
|
|
56
|
-
- **Fast.** A 100k-character reply streams in 0.
|
|
57
|
-
Streamdown 6.
|
|
56
|
+
- **Fast.** A 100k-character reply streams in 0.14 s. The next fastest renderer takes 2.4 s and
|
|
57
|
+
Streamdown 6.5 s.
|
|
58
58
|
- **Linear.** A reply four times longer costs about three times more, not sixteen, and no update
|
|
59
59
|
takes more than 5 ms.
|
|
60
60
|
- **Repairs a reply mid-stream.** Unfinished `**bold`, half-typed links and open code fences never
|
|
61
61
|
show their raw syntax.
|
|
62
62
|
- **Complete.** Its parser passes every example of CommonMark and of GitHub's extensions.
|
|
63
|
-
- **Light.** One package with no dependencies,
|
|
63
|
+
- **Light.** One package with no dependencies, 32 KB gzipped, styled without Tailwind.
|
|
64
64
|
- **Safe.** Raw HTML from a reply is sanitized, and links are held to safe protocols.
|
|
65
65
|
|
|
66
|
+
<p align="center">
|
|
67
|
+
<a href="https://flowdown.dev/docs/comparison"><img alt="flowdown beside five other React Markdown renderers: time to stream six kinds of reply, download size, packages installed, and the cost of a four times longer reply" src="https://raw.githubusercontent.com/rsaryev/flowdown/main/bench/results/charts/compare.png" width="878"></a>
|
|
68
|
+
</p>
|
|
69
|
+
|
|
66
70
|
Every figure, its method and where another renderer suits better are in
|
|
67
71
|
[the comparison](https://flowdown.dev/docs/comparison).
|
|
68
72
|
|
package/dist/core/blocks.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { parseContainerMarker } from "../dialect.js";
|
|
2
|
+
import { continues } from "./continues.js";
|
|
2
3
|
// src/core/blocks.ts
|
|
3
4
|
/**
|
|
4
5
|
* Splitting a document into top-level blocks that can be rendered, and remembered, one by one.
|
|
@@ -423,7 +424,7 @@ function createBlockSplitter() {
|
|
|
423
424
|
let kept = blocks.slice(0, Math.max(0, blocks.length - REOPENED));
|
|
424
425
|
let last = kept.at(-1);
|
|
425
426
|
let from = last ? last.offset + last.source.length : 0;
|
|
426
|
-
if (from > 0 && !(markdown
|
|
427
|
+
if (from > 0 && !continues(markdown, text, from)) {
|
|
427
428
|
kept = [];
|
|
428
429
|
last = void 0;
|
|
429
430
|
from = 0;
|
|
@@ -431,7 +432,7 @@ function createBlockSplitter() {
|
|
|
431
432
|
let mark = null;
|
|
432
433
|
if (progress !== null && progress.from === from) for (let index = progress.marks.length - 1; index >= 0 && !mark; index -= 1) {
|
|
433
434
|
const tried = progress.marks[index];
|
|
434
|
-
if (markdown
|
|
435
|
+
if (continues(markdown, text, tried.at, from)) mark = tried;
|
|
435
436
|
}
|
|
436
437
|
lexed += markdown.length - (mark ? mark.at : from);
|
|
437
438
|
const cut = pieces(markdown, from, mark ? progress : null, mark);
|
package/dist/core/blocks.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"blocks.js","names":[],"sources":["../../src/core/blocks.ts"],"sourcesContent":["/**\n * Splitting a document into top-level blocks that can be rendered, and remembered, one by one.\n *\n * A streaming reply grows at its end, and re-rendering the whole of it on every token is work\n * proportional to its length, repeated per token. Cut at its top-level blocks instead, every block\n * but the last is text that no longer changes: a renderer that memoizes each block by its source\n * parses and reconciles only the tail.\n *\n * The cut must never change the result, so the text is cut only where one parse of the whole\n * document is certain to start a new top-level block, and a block is merged with its neighbours\n * wherever that parse would read them together:\n * - the dialect's forms: a marker-only quote (`> [!STEPS]`) governs the list after it, `[!TABS]`\n * the run of sections after it, and a `[!PROS]` right before a `[!CONS]` is one pair;\n * - an image alone in its paragraph and an emphasis-only line under it, its caption;\n * - an HTML block whose tag is closed further down;\n * - a `$$` display formula with blank lines inside;\n * - a document with footnote or link reference definitions, which resolve across blocks, is\n * not split at all.\n *\n * The boundaries are found by a scan of the lines (`step`), not by a Markdown lexer: it follows\n * only what decides where a top-level block can end (a fence, a list, an HTML block that blank\n * lines do not end), and where it cannot tell, it stops cutting for the rest of the document.\n * Merging is always safe, a wrong cut never is. The tests render a corpus both ways and require\n * the same markup, at every prefix.\n */\nimport { parseContainerMarker, type ContainerKind } from '../dialect'\n\nexport interface MarkdownBlock {\n /** The block's own source. */\n source: string\n /** Where it starts in the document: characters before it, and its 1-based line. */\n offset: number\n line: number\n}\n\nconst FENCE = /^ {0,3}(`{3,}|~{3,})(.*)$/\nconst INNER_FENCE = /^[ \\t]*(`{3,}|~{3,})(.*)$/\nconst ITEM = /^( {0,3})([-+*]|\\d{1,9}[.)])(.*)$/\n/** A list item that may interrupt a paragraph: a bullet, or the number 1, with text after it. */\nconst INTERRUPTING_ITEM = /^ {0,3}(?:[-+*]|1[.)]) +\\S/\nconst RULE = /^ {0,3}(?:(?:-[ \\t]*){3,}|(?:_[ \\t]*){3,}|(?:\\*[ \\t]*){3,})$/\nconst HEADING = /^ {0,3}(#{1,6})(?:[ \\t]|$)/\nconst UNDERLINE = /\\n {0,3}(?:=+|-+)[ \\t]*(?:\\n|$)/\nconst QUOTE = /^ {0,3}>[ \\t]?(.*)$/\n/** HTML blocks that a blank line does not end: how each opens, and what closes it. */\nconst LONG_HTML: ReadonlyArray<readonly [RegExp, RegExp]> = [\n [/^ {0,3}<(?:pre|script|style|textarea)(?=[\\s>]|$)/i, /<\\/(?:pre|script|style|textarea)>/i],\n [/^ {0,3}<!--/, /-->/],\n [/^ {0,3}<\\?/, /\\?>/],\n [/^ {0,3}<!\\[CDATA\\[/, /\\]\\]>/],\n [/^ {0,3}<![A-Za-z]/, />/],\n]\nconst HTML_START = /^ {0,3}<[A-Za-z/!?]/\nconst HTML_LINE = /^ {0,3}<[A-Za-z]/m\nconst HTML_LINES = /^ {0,3}<([A-Za-z][\\w:-]*)(?=[\\s>/])/gm\nconst FOOTNOTE_DEFINITION = /\\[\\^[^\\]\\s]{1,200}\\]:/\nconst LINK_DEFINITION = /^ {0,3}\\[[^\\]]+\\]:/\nconst OPEN_LABEL = /^ {0,3}\\[[^\\]]*$/\nconst IMAGE_ONLY = /^!\\[[^\\]]*\\]\\([^)]*\\)$/\nconst DOUBLE_DOLLAR = /(?<!\\\\)\\$\\$/g\nconst VOID_TAGS = new Set(['area', 'br', 'col', 'embed', 'hr', 'img', 'input', 'source', 'wbr'])\n\nconst whole = (markdown: string): MarkdownBlock[] => [{ source: markdown, offset: 0, line: 1 }]\n\n/** What the scan knows after a line: only what decides where a top-level block can end. */\ninterface Scan {\n /** The opening run of the top-level fence the text is inside. */\n fence: string | null\n /** What closes the HTML block the text is inside, one that blank lines do not end. */\n html: RegExp | null\n /** The content column of the open top-level list item; 0 outside a list. */\n list: number\n /** The list may already be over: a line left of the item that was a lazy continuation or not. */\n unsure: boolean\n /** The lines since the last blank one may be an HTML block, where a fence line is not a fence. */\n markup: boolean\n /** The next line starts a block: the last one was blank or closed a fence, or there was none. */\n fresh: boolean\n /** An odd number of `$$` so far in this block: a display formula may be open. */\n odd: boolean\n /** A `[label` that has not closed yet: it may turn out to be a link definition. */\n label: boolean\n /**\n * The last line with text was indented four or more: code, or the rest of a paragraph. The\n * parser still has indented code open when it reads the next line, and reads it as one that\n * interrupts: `2. two` or an empty `-` is then text, not a list. The line after is never cut.\n */\n indented: boolean\n /** The last line with text closed a top-level fence: whatever comes next is a new block. */\n closed: boolean\n /** Text follows a closed fence in the same piece: that piece is not only a fence. */\n mixed: boolean\n /** The opening run of a fence inside the open list item. */\n inner: string | null\n /** A line with text has been read: blank lines at the very start belong to the first block. */\n seen: boolean\n /** The scan cannot tell what the next lines are: no more cuts. */\n stuck: boolean\n /** A definition that resolves across blocks: the document stays one block. */\n whole: boolean\n}\n\nconst start = (): Scan => ({\n fence: null,\n html: null,\n list: 0,\n unsure: false,\n markup: false,\n fresh: true,\n odd: false,\n label: false,\n closed: false,\n indented: false,\n mixed: false,\n inner: null,\n seen: false,\n stuck: false,\n whole: false,\n})\n\n/** The column a line's text starts at, tabs to the next multiple of four; -1 for a blank line. */\nfunction indentOf(line: string): number {\n let column = 0\n for (let index = 0; index < line.length; index += 1) {\n const code = line.charCodeAt(index)\n if (code === 32) column += 1\n else if (code === 9) column += 4 - (column % 4)\n else return column\n }\n return -1\n}\n\n/**\n * The content column of a list item's line, 0 when the line is not one, -1 when it is one whose\n * column a tab after the marker leaves unknown.\n */\nfunction itemColumn(line: string): number {\n const item = ITEM.exec(line)\n if (!item) return 0\n const base = item[1]!.length + item[2]!.length\n const rest = item[3]!\n if (rest === '') return base + 1\n if (rest[0] === '\\t') return -1\n if (rest[0] !== ' ') return 0\n const text = rest.trimStart()\n const spaces = rest.length - text.length\n if (text === '') return base + 1\n if (rest.slice(0, spaces).includes('\\t')) return -1\n return base + (spaces > 4 ? 1 : spaces)\n}\n\n/**\n * One line through the scan. True when the line starts a new top-level block for certain: after a\n * blank line outside every open construct, or as the fence line of a top-level fence.\n */\nfunction step(scan: Scan, line: string): boolean {\n const seen = scan.seen\n const closed = scan.closed\n if (!seen && line.trim() !== '') scan.seen = true\n const cut = advance(scan, line)\n // A line with text right after a fence that did not start a block: the scan was stuck on it.\n if (closed && !scan.closed && !cut) scan.mixed = true\n return cut && seen\n}\n\nfunction advance(scan: Scan, line: string): boolean {\n if (scan.stuck || scan.whole) return false\n if (scan.fence) {\n const closing = FENCE.exec(line)\n if (\n closing &&\n closing[1]![0] === scan.fence[0] &&\n closing[1]!.length >= scan.fence.length &&\n closing[2]!.trim() === ''\n ) {\n scan.fence = null\n scan.fresh = true\n scan.closed = true\n }\n return false\n }\n if (scan.html) {\n if (scan.html.test(line)) scan.html = null\n return false\n }\n const indent = indentOf(line)\n if (indent === -1) {\n scan.fresh = true\n scan.markup = false\n scan.label = false\n return false\n }\n const fresh = scan.fresh\n const closed = scan.closed\n scan.fresh = false\n scan.closed = false\n\n if (FOOTNOTE_DEFINITION.test(line) || LINK_DEFINITION.test(line)) return definition(scan)\n if (scan.label) {\n const close = line.indexOf(']')\n if (close !== -1) {\n if (line[close + 1] === ':') return definition(scan)\n scan.label = false\n }\n } else if (OPEN_LABEL.test(line)) scan.label = true\n\n const odd = scan.odd\n if (line.includes('$$') && (line.match(DOUBLE_DOLLAR)?.length ?? 0) % 2 === 1) scan.odd = !odd\n\n const fence = FENCE.exec(line)\n // A backtick fence has no backtick in its info string: ```` ```a``` ```` is inline code.\n const opensFence = fence !== null && !(fence[1]![0] === '`' && fence[2]!.includes('`'))\n // Inside a formula or an HTML block a fence line is text, and which of the two it is here\n // cannot be told from the lines alone.\n if (opensFence && (odd || scan.markup)) return stick(scan)\n\n if (scan.list > 0) {\n if (indent >= scan.list) {\n // In the item, if the list is still open; a top-level fence if it is not.\n if (opensFence && scan.unsure) return stick(scan)\n const inner = INNER_FENCE.exec(line)\n if (scan.inner) {\n if (\n inner &&\n inner[1]![0] === scan.inner[0] &&\n inner[1]!.length >= scan.inner.length &&\n inner[2]!.trim() === ''\n )\n scan.inner = null\n } else if (inner && !(inner[1]![0] === '`' && inner[2]!.includes('`'))) scan.inner = inner[1]!\n return false\n }\n // A fence in the item that a line left of the item ends: where its last blank lines go\n // depends on what follows, so nothing is cut from here on.\n if (scan.inner) return stick(scan)\n const column = RULE.test(line) ? 0 : itemColumn(line)\n if (column === -1) return stick(scan)\n if (column > 0) {\n // The next item of the list (or of a new one right after it).\n scan.list = column\n if (fresh) scan.unsure = false\n return false\n }\n if (\n fresh ||\n opensFence ||\n HEADING.test(line) ||\n RULE.test(line) ||\n QUOTE.test(line) ||\n LONG_HTML.some(([opens]) => opens.test(line))\n ) {\n // Left of the item after a blank line, or a block that cannot be a lazy continuation: the\n // list is over, and the line is read as a top-level one below.\n scan.list = 0\n scan.unsure = false\n } else {\n // A lazy continuation of the item's paragraph, or, if the item did not end in a paragraph,\n // the first line after the list.\n scan.unsure = true\n if (HTML_START.test(line)) scan.markup = true\n return false\n }\n }\n\n // Indented four or more after a blank line: indented code, which goes on over blank lines.\n // Right after a fence, any line starts a block.\n const indented = scan.indented\n scan.indented = indent >= 4\n const cut = closed || (fresh && indent < 4 && !odd && !indented)\n for (const [opens, closes] of LONG_HTML) {\n if (!opens.test(line)) continue\n if (!closes.test(line.replace(opens, ''))) scan.html = closes\n return cut\n }\n if (opensFence) {\n scan.fence = fence![1]!\n // A fence starts a block wherever it is written, and is one whatever follows it.\n return !odd\n }\n const column = indent < 4 && !RULE.test(line) ? itemColumn(line) : 0\n if (column === -1) return stick(scan)\n // After a line indented four, which was code or the rest of a paragraph, only the scan of the\n // lines before it could say which a list item that cannot interrupt is.\n if (column > 0 && indented && !INTERRUPTING_ITEM.test(line)) return stick(scan)\n if (column > 0) {\n scan.list = column\n scan.unsure = scan.markup || (!fresh && !INTERRUPTING_ITEM.test(line))\n return cut\n }\n if (HTML_START.test(line)) scan.markup = true\n return cut\n}\n\nfunction stick(scan: Scan): false {\n scan.stuck = true\n return false\n}\n\nfunction definition(scan: Scan): false {\n scan.whole = true\n return false\n}\n\n/** The line of `text` that starts at `at`, without its newline. */\nfunction lineAt(text: string, at: number): string {\n const end = text.indexOf('\\n', at)\n return text.slice(at, end === -1 ? text.length : end)\n}\n\n/** Whether `raw` has a line that opens an HTML tag still open at its end. */\nfunction htmlOpen(raw: string): boolean {\n // A comment left open takes whatever follows it.\n if (raw.includes('<!--') && raw.split('<!--').length > raw.split('-->').length) return true\n if (!HTML_LINE.test(raw)) return false\n const tried = new Set<string>()\n for (const opener of raw.matchAll(HTML_LINES)) {\n const tag = opener[1]!.toLowerCase()\n if (tried.has(tag) || VOID_TAGS.has(tag)) continue\n tried.add(tag)\n const rest = raw.slice(opener.index)\n const openers = rest.match(new RegExp(`<${tag}(?=[\\\\s>/])[^>]*>`, 'gi')) ?? []\n const opened = openers.filter((found) => !found.endsWith('/>')).length\n const closed = rest.match(new RegExp(`</${tag}\\\\s*>`, 'gi'))?.length ?? 0\n if (opened > closed) return true\n }\n return false\n}\n\n/**\n * The pieces the scan cut (`starts`, offsets into `text`) joined into the blocks that must render\n * together: the offsets where a block starts. Read by the same grammar as the remark pass.\n */\nfunction group(text: string, starts: readonly number[]): number[] {\n const count = starts.length\n const endOf = (piece: number) => (piece + 1 < count ? starts[piece + 1]! : text.length)\n const first = (piece: number) => (piece < count ? lineAt(text, starts[piece]!) : '')\n const kindOf = (line: string): ContainerKind | null => {\n const quoted = QUOTE.exec(line)\n return quoted ? (parseContainerMarker(quoted[1]!.trimStart())?.kind ?? null) : null\n }\n const isItem = (line: string) => !RULE.test(line) && itemColumn(line) !== 0\n /** The piece after the tabs whose first heading is in `holder`, on `line` or underlined. */\n const tabsEnd = (holder: number, line: string, fallback: number): number => {\n if (holder >= count) return fallback\n const opening = HEADING.exec(line)\n // A heading underlined with `===` has a depth this scan does not read: it ends at a `---`.\n if (!opening && !UNDERLINE.test(text.slice(starts[holder]!, endOf(holder)))) return fallback\n const depth = opening ? opening[1]!.length : 1\n let end = holder + 1\n for (; end < count; end += 1) {\n const next = first(end)\n if (RULE.test(next)) break\n const heading = HEADING.exec(next)\n if (heading && heading[1]!.length < depth) break\n }\n return end\n }\n /**\n * The form a piece holds a marker line for, and the piece after everything it governs. A list\n * form takes the one list right after it; tabs take the sections from the heading right after\n * them up to a shallower heading, a `---` or the end. The marker is a quote of its own, on any\n * line of the piece: on the last, what it governs is the next piece. A marker whose quote goes\n * on (`> [!PROS]\\n> - a`) holds its own content. Of several markers, the one that reaches\n * furthest decides.\n */\n const form = (piece: number): { kind: ContainerKind; end: number } | null => {\n if (piece >= count) return null\n const body = text.slice(starts[piece]!, endOf(piece)).trimEnd()\n if (!body.includes('[!')) return null\n const lines = body.split('\\n')\n let found: { kind: ContainerKind; end: number } | null = null\n for (let index = 0; index < lines.length; index += 1) {\n const kind = kindOf(lines[index]!)\n // The marker opens its quote: the line above it is not of the same quote.\n if (!kind || (index > 0 && QUOTE.test(lines[index - 1]!))) continue\n const next = lines[index + 1]\n let end = piece + 1\n if (next === undefined) {\n if (kind === 'tabs') end = tabsEnd(piece + 1, first(piece + 1), piece + 1)\n else if (isItem(first(piece + 1))) end = piece + 2\n } else if (kind === 'tabs' && !QUOTE.test(next)) end = tabsEnd(piece, next, piece + 1)\n const reach: number = found ? found.end : 0\n found = { kind, end: Math.max(end, reach) }\n }\n return found\n }\n\n const blocks: number[] = []\n let piece = 0\n while (piece < count) {\n const from = piece\n const found = form(piece)\n piece = found ? found.end : piece + 1\n\n // A pros right before a cons (or the reverse) is set as one pair.\n if (found && (found.kind === 'pros' || found.kind === 'cons')) {\n const other = form(piece)\n if (other && other.kind === (found.kind === 'pros' ? 'cons' : 'pros')) piece = other.end\n }\n\n // An image alone in its paragraph takes the emphasis line under it as its caption. Merging is\n // always safe (it is what one parse does anyway), so the test here can be loose.\n if (\n piece === from + 1 &&\n piece < count &&\n IMAGE_ONLY.test(text.slice(starts[from]!, endOf(from)).trim()) &&\n /^[*_]/.test(first(piece).trimStart())\n )\n piece += 1\n\n // An HTML block whose tag closes further down.\n while (piece < count && htmlOpen(text.slice(starts[from]!, starts[piece]!))) piece += 1\n\n blocks.push(starts[from]!)\n }\n return blocks\n}\n\n/**\n * Line endings as the scan reads them: `\\r\\n` and `\\r` become `\\n`. Offsets of the blocks are into\n * the normalized text, which is what the renderer draws from.\n */\nexport const normalizeLineEndings = (markdown: string): string =>\n markdown.includes('\\r') ? markdown.replace(/\\r\\n?/g, '\\n') : markdown\n\n/** Blocks that are one fenced code block and nothing else: no prose, HTML, math or heading. */\nconst fenced = new WeakSet<MarkdownBlock>()\n\n/** Whether the splitter cut this block as one fenced code block. */\nexport const isFenceBlock = (block: MarkdownBlock): boolean => fenced.has(block)\n\n/** The scan after a complete line: the next text can go on from there. */\ninterface Mark {\n /** The offset after that line. */\n at: number\n scan: Scan\n /** How many pieces had been cut by then. */\n count: number\n}\n\n/** Where a scan can be resumed. */\ninterface Progress {\n /** Where the scan started: the end of the blocks kept as final. */\n from: number\n /** The pieces cut up to the last mark, `from` first. */\n starts: number[]\n /**\n * The scan after each of the last complete lines, oldest first. Several, because a healed\n * stream rewrites its end: the fence it closes or the row it drops is a line that the next\n * update does not have, and the scan then goes on from the line before.\n */\n marks: Mark[]\n}\n\nconst MARKS = 3\n\n/**\n * The pieces of `text` from `from` on: the offsets where the scan cut, or null when a definition\n * keeps the document whole. `progress` and `mark` are where an earlier scan of the same start\n * can go on from.\n */\nfunction pieces(\n text: string,\n from: number,\n progress: Progress | null,\n mark: Mark | null,\n): { starts: number[]; progress: Progress; mixed: boolean } | null {\n const scan = mark ? { ...mark.scan } : start()\n const starts = progress && mark ? progress.starts.slice(0, mark.count) : [from]\n const marks = progress && mark ? progress.marks.filter((kept) => kept.at <= mark.at) : []\n let at = mark ? mark.at : from\n // Marks are left after the last complete lines only.\n let marked = text.length\n for (let back = 0; back < MARKS && marked > at; back += 1)\n marked = text.lastIndexOf('\\n', marked - 1)\n for (let end = text.indexOf('\\n', at); end !== -1; end = text.indexOf('\\n', at)) {\n if (step(scan, text.slice(at, end)) && at > from) starts.push(at)\n at = end + 1\n if (end >= marked) marks.push({ at, scan: { ...scan }, count: starts.length })\n }\n const next: Progress = { from, starts: [...starts], marks: marks.slice(-MARKS) }\n // The last line is still being written: read, but not remembered.\n if (at < text.length && step(scan, text.slice(at)) && at > from) starts.push(at)\n return scan.whole ? null : { starts, progress: next, mixed: scan.mixed }\n}\n\n/** The blocks of `text` that the pieces at `cuts` make, the first on line `line`. */\nfunction build(\n text: string,\n cuts: readonly number[],\n line: number,\n mixed: boolean,\n): MarkdownBlock[] {\n const starts = group(text, cuts)\n const blocks: MarkdownBlock[] = []\n let current = line\n let piece = 0\n for (let index = 0; index < starts.length; index += 1) {\n const offset = starts[index]!\n const end = starts[index + 1] ?? text.length\n const source = text.slice(offset, end)\n const block = { source, offset, line: current }\n while (cuts[piece]! < offset) piece += 1\n // One piece that opens with a fence line is that fence and the blank lines after it: the scan\n // cuts before a top-level fence and after the line that closes it.\n // (When the scan got stuck after a fence, the last piece holds more than that fence.)\n if ((cuts[piece + 1] ?? text.length) === end && !(mixed && end === text.length)) {\n const opening = FENCE.exec(lineAt(text, offset))\n if (opening && !(opening[1]![0] === '`' && opening[2]!.includes('`'))) fenced.add(block)\n }\n blocks.push(block)\n // The last block's lines place nothing: a fence that grows is not counted on every token.\n if (index + 1 === starts.length) break\n for (let at = source.indexOf('\\n'); at !== -1; at = source.indexOf('\\n', at + 1)) current += 1\n }\n return blocks\n}\n\n/** The top-level blocks of a document, cut where one parse of the whole would not join them. */\nexport function splitBlocks(input: string): MarkdownBlock[] {\n const markdown = normalizeLineEndings(input)\n if (!markdown) return whole(markdown)\n const cut = pieces(markdown, 0, null, null)\n return cut ? build(markdown, cut.starts, 1, cut.mixed) : whole(markdown)\n}\n\n/**\n * How many blocks at the end are read again. Text appended to a document can change only the\n * group it is appended to, and that group can join the one before it (a `[!CONS]` arriving\n * under a `[!PROS]`); every group above those two is final.\n */\nconst REOPENED = 2\n\nexport interface BlockSplitter {\n /** The same blocks as `splitBlocks(markdown)`; the ones that did not change are the same objects. */\n split(markdown: string): MarkdownBlock[]\n /** Characters scanned so far: the work the splitter has done. */\n readonly lexed: number\n}\n\n/**\n * `splitBlocks` for a document that grows at its end, as a streaming reply does. It remembers the\n * blocks it cut last time and where its scan stopped: when the new text still starts with all of\n * them but the last two, it keeps those, as the very same objects, and scans only the lines that\n * are new. Any other text (a reply that was regenerated, or edited above its end) is split afresh.\n * Each update then costs the new characters, not the reply: a stream is linear in its length.\n */\nexport function createBlockSplitter(): BlockSplitter {\n let text = ''\n let blocks: MarkdownBlock[] = []\n let progress: Progress | null = null\n let lexed = 0\n\n return {\n get lexed() {\n return lexed\n },\n split(input) {\n const markdown = normalizeLineEndings(input)\n if (!markdown) {\n text = ''\n blocks = []\n progress = null\n return whole(markdown)\n }\n let kept = blocks.slice(0, Math.max(0, blocks.length - REOPENED))\n let last = kept.at(-1)\n let from = last ? last.offset + last.source.length : 0\n if (from > 0 && !(markdown.length >= from && markdown.startsWith(text.slice(0, from)))) {\n kept = []\n last = undefined\n from = 0\n }\n // The scan goes on from the last line it read that is still there, when it started at the\n // same place.\n let mark: Mark | null = null\n if (progress !== null && progress.from === from) {\n for (let index = progress.marks.length - 1; index >= 0 && !mark; index -= 1) {\n const tried = progress.marks[index]!\n if (markdown.length >= tried.at && markdown.startsWith(text.slice(from, tried.at), from))\n mark = tried\n }\n }\n lexed += markdown.length - (mark ? mark.at : from)\n const cut = pieces(markdown, from, mark ? progress : null, mark)\n text = markdown\n if (!cut) {\n // A definition arrived: what was cut before it is one block with it now.\n blocks = []\n progress = null\n return whole(markdown)\n }\n progress = cut.progress\n let line = 1\n if (last) {\n line = last.line\n for (let at = last.source.indexOf('\\n'); at !== -1; at = last.source.indexOf('\\n', at + 1))\n line += 1\n }\n const rest = build(markdown, cut.starts, line, cut.mixed)\n // A block that reads the same as last time is the same object, with what was worked out\n // from it.\n for (let index = 0; index < rest.length; index += 1) {\n const before = blocks[kept.length + index]\n const after = rest[index]!\n if (before && before.offset === after.offset && before.source === after.source) {\n rest[index] = before\n }\n }\n blocks = [...kept, ...rest]\n return blocks\n },\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCA,MAAM,QAAQ;AACd,MAAM,cAAc;AACpB,MAAM,OAAO;;AAEb,MAAM,oBAAoB;AAC1B,MAAM,OAAO;AACb,MAAM,UAAU;AAChB,MAAM,YAAY;AAClB,MAAM,QAAQ;;AAEd,MAAM,YAAsD;CAC1D,CAAC,qDAAqD,oCAAoC;CAC1F,CAAC,eAAe,KAAK;CACrB,CAAC,cAAc,KAAK;CACpB,CAAC,sBAAsB,OAAO;CAC9B,CAAC,qBAAqB,GAAG;AAC3B;AACA,MAAM,aAAa;AACnB,MAAM,YAAY;AAClB,MAAM,aAAa;AACnB,MAAM,sBAAsB;AAC5B,MAAM,kBAAkB;AACxB,MAAM,aAAa;AACnB,MAAM,aAAa;AACnB,MAAM,gBAAgB;AACtB,MAAM,4BAAY,IAAI,IAAI;CAAC;CAAQ;CAAM;CAAO;CAAS;CAAM;CAAO;CAAS;CAAU;AAAK,CAAC;AAE/F,MAAM,SAAS,aAAsC,CAAC;CAAE,QAAQ;CAAU,QAAQ;CAAG,MAAM;AAAE,CAAC;AAwC9F,MAAM,eAAqB;CACzB,OAAO;CACP,MAAM;CACN,MAAM;CACN,QAAQ;CACR,QAAQ;CACR,OAAO;CACP,KAAK;CACL,OAAO;CACP,QAAQ;CACR,UAAU;CACV,OAAO;CACP,OAAO;CACP,MAAM;CACN,OAAO;CACP,OAAO;AACT;;AAGA,SAAS,SAAS,MAAsB;CACtC,IAAI,SAAS;CACb,KAAK,IAAI,QAAQ,GAAG,QAAQ,KAAK,QAAQ,SAAS,GAAG;EACnD,MAAM,OAAO,KAAK,WAAW,KAAK;EAClC,IAAI,SAAS,IAAI,UAAU;OACtB,IAAI,SAAS,GAAG,UAAU,IAAK,SAAS;OACxC,OAAO;CACd;CACA,OAAO;AACT;;;;;AAMA,SAAS,WAAW,MAAsB;CACxC,MAAM,OAAO,KAAK,KAAK,IAAI;CAC3B,IAAI,CAAC,MAAM,OAAO;CAClB,MAAM,OAAO,KAAK,EAAE,CAAE,SAAS,KAAK,EAAE,CAAE;CACxC,MAAM,OAAO,KAAK;CAClB,IAAI,SAAS,IAAI,OAAO,OAAO;CAC/B,IAAI,KAAK,OAAO,KAAM,OAAO;CAC7B,IAAI,KAAK,OAAO,KAAK,OAAO;CAC5B,MAAM,OAAO,KAAK,UAAU;CAC5B,MAAM,SAAS,KAAK,SAAS,KAAK;CAClC,IAAI,SAAS,IAAI,OAAO,OAAO;CAC/B,IAAI,KAAK,MAAM,GAAG,MAAM,CAAC,CAAC,SAAS,GAAI,GAAG,OAAO;CACjD,OAAO,QAAQ,SAAS,IAAI,IAAI;AAClC;;;;;AAMA,SAAS,KAAK,MAAY,MAAuB;CAC/C,MAAM,OAAO,KAAK;CAClB,MAAM,SAAS,KAAK;CACpB,IAAI,CAAC,QAAQ,KAAK,KAAK,MAAM,IAAI,KAAK,OAAO;CAC7C,MAAM,MAAM,QAAQ,MAAM,IAAI;CAE9B,IAAI,UAAU,CAAC,KAAK,UAAU,CAAC,KAAK,KAAK,QAAQ;CACjD,OAAO,OAAO;AAChB;AAEA,SAAS,QAAQ,MAAY,MAAuB;CAClD,IAAI,KAAK,SAAS,KAAK,OAAO,OAAO;CACrC,IAAI,KAAK,OAAO;EACd,MAAM,UAAU,MAAM,KAAK,IAAI;EAC/B,IACE,WACA,QAAQ,EAAE,CAAE,OAAO,KAAK,MAAM,MAC9B,QAAQ,EAAE,CAAE,UAAU,KAAK,MAAM,UACjC,QAAQ,EAAE,CAAE,KAAK,MAAM,IACvB;GACA,KAAK,QAAQ;GACb,KAAK,QAAQ;GACb,KAAK,SAAS;EAChB;EACA,OAAO;CACT;CACA,IAAI,KAAK,MAAM;EACb,IAAI,KAAK,KAAK,KAAK,IAAI,GAAG,KAAK,OAAO;EACtC,OAAO;CACT;CACA,MAAM,SAAS,SAAS,IAAI;CAC5B,IAAI,WAAW,IAAI;EACjB,KAAK,QAAQ;EACb,KAAK,SAAS;EACd,KAAK,QAAQ;EACb,OAAO;CACT;CACA,MAAM,QAAQ,KAAK;CACnB,MAAM,SAAS,KAAK;CACpB,KAAK,QAAQ;CACb,KAAK,SAAS;CAEd,IAAI,oBAAoB,KAAK,IAAI,KAAK,gBAAgB,KAAK,IAAI,GAAG,OAAO,WAAW,IAAI;CACxF,IAAI,KAAK,OAAO;EACd,MAAM,QAAQ,KAAK,QAAQ,GAAG;EAC9B,IAAI,UAAU,IAAI;GAChB,IAAI,KAAK,QAAQ,OAAO,KAAK,OAAO,WAAW,IAAI;GACnD,KAAK,QAAQ;EACf;CACF,OAAO,IAAI,WAAW,KAAK,IAAI,GAAG,KAAK,QAAQ;CAE/C,MAAM,MAAM,KAAK;CACjB,IAAI,KAAK,SAAS,IAAI,MAAM,KAAK,MAAM,aAAa,CAAC,EAAE,UAAU,KAAK,MAAM,GAAG,KAAK,MAAM,CAAC;CAE3F,MAAM,QAAQ,MAAM,KAAK,IAAI;CAE7B,MAAM,aAAa,UAAU,QAAQ,EAAE,MAAM,EAAE,CAAE,OAAO,OAAO,MAAM,EAAE,CAAE,SAAS,GAAG;CAGrF,IAAI,eAAe,OAAO,KAAK,SAAS,OAAO,MAAM,IAAI;CAEzD,IAAI,KAAK,OAAO,GAAG;EACjB,IAAI,UAAU,KAAK,MAAM;GAEvB,IAAI,cAAc,KAAK,QAAQ,OAAO,MAAM,IAAI;GAChD,MAAM,QAAQ,YAAY,KAAK,IAAI;GACnC,IAAI,KAAK,OAEL;QAAA,SACA,MAAM,EAAE,CAAE,OAAO,KAAK,MAAM,MAC5B,MAAM,EAAE,CAAE,UAAU,KAAK,MAAM,UAC/B,MAAM,EAAE,CAAE,KAAK,MAAM,IAErB,KAAK,QAAQ;GAAA,OACV,IAAI,SAAS,EAAE,MAAM,EAAE,CAAE,OAAO,OAAO,MAAM,EAAE,CAAE,SAAS,GAAG,IAAI,KAAK,QAAQ,MAAM;GAC3F,OAAO;EACT;EAGA,IAAI,KAAK,OAAO,OAAO,MAAM,IAAI;EACjC,MAAM,SAAS,KAAK,KAAK,IAAI,IAAI,IAAI,WAAW,IAAI;EACpD,IAAI,WAAW,IAAI,OAAO,MAAM,IAAI;EACpC,IAAI,SAAS,GAAG;GAEd,KAAK,OAAO;GACZ,IAAI,OAAO,KAAK,SAAS;GACzB,OAAO;EACT;EACA,IACE,SACA,cACA,QAAQ,KAAK,IAAI,KACjB,KAAK,KAAK,IAAI,KACd,MAAM,KAAK,IAAI,KACf,UAAU,MAAM,CAAC,WAAW,MAAM,KAAK,IAAI,CAAC,GAC5C;GAGA,KAAK,OAAO;GACZ,KAAK,SAAS;EAChB,OAAO;GAGL,KAAK,SAAS;GACd,IAAI,WAAW,KAAK,IAAI,GAAG,KAAK,SAAS;GACzC,OAAO;EACT;CACF;CAIA,MAAM,WAAW,KAAK;CACtB,KAAK,WAAW,UAAU;CAC1B,MAAM,MAAM,UAAW,SAAS,SAAS,KAAK,CAAC,OAAO,CAAC;CACvD,KAAK,MAAM,CAAC,OAAO,WAAW,WAAW;EACvC,IAAI,CAAC,MAAM,KAAK,IAAI,GAAG;EACvB,IAAI,CAAC,OAAO,KAAK,KAAK,QAAQ,OAAO,EAAE,CAAC,GAAG,KAAK,OAAO;EACvD,OAAO;CACT;CACA,IAAI,YAAY;EACd,KAAK,QAAQ,MAAO;EAEpB,OAAO,CAAC;CACV;CACA,MAAM,SAAS,SAAS,KAAK,CAAC,KAAK,KAAK,IAAI,IAAI,WAAW,IAAI,IAAI;CACnE,IAAI,WAAW,IAAI,OAAO,MAAM,IAAI;CAGpC,IAAI,SAAS,KAAK,YAAY,CAAC,kBAAkB,KAAK,IAAI,GAAG,OAAO,MAAM,IAAI;CAC9E,IAAI,SAAS,GAAG;EACd,KAAK,OAAO;EACZ,KAAK,SAAS,KAAK,UAAW,CAAC,SAAS,CAAC,kBAAkB,KAAK,IAAI;EACpE,OAAO;CACT;CACA,IAAI,WAAW,KAAK,IAAI,GAAG,KAAK,SAAS;CACzC,OAAO;AACT;AAEA,SAAS,MAAM,MAAmB;CAChC,KAAK,QAAQ;CACb,OAAO;AACT;AAEA,SAAS,WAAW,MAAmB;CACrC,KAAK,QAAQ;CACb,OAAO;AACT;;AAGA,SAAS,OAAO,MAAc,IAAoB;CAChD,MAAM,MAAM,KAAK,QAAQ,MAAM,EAAE;CACjC,OAAO,KAAK,MAAM,IAAI,QAAQ,KAAK,KAAK,SAAS,GAAG;AACtD;;AAGA,SAAS,SAAS,KAAsB;CAEtC,IAAI,IAAI,SAAS,MAAM,KAAK,IAAI,MAAM,MAAM,CAAC,CAAC,SAAS,IAAI,MAAM,KAAK,CAAC,CAAC,QAAQ,OAAO;CACvF,IAAI,CAAC,UAAU,KAAK,GAAG,GAAG,OAAO;CACjC,MAAM,wBAAQ,IAAI,IAAY;CAC9B,KAAK,MAAM,UAAU,IAAI,SAAS,UAAU,GAAG;EAC7C,MAAM,MAAM,OAAO,EAAE,CAAE,YAAY;EACnC,IAAI,MAAM,IAAI,GAAG,KAAK,UAAU,IAAI,GAAG,GAAG;EAC1C,MAAM,IAAI,GAAG;EACb,MAAM,OAAO,IAAI,MAAM,OAAO,KAAK;EAInC,KAHgB,KAAK,MAAM,IAAI,OAAO,IAAI,IAAI,oBAAoB,IAAI,CAAC,KAAK,CAAC,EAAA,CACtD,QAAQ,UAAU,CAAC,MAAM,SAAS,IAAI,CAAC,CAAC,CAAC,UACjD,KAAK,MAAM,IAAI,OAAO,KAAK,IAAI,QAAQ,IAAI,CAAC,CAAC,EAAE,UAAU,IACnD,OAAO;CAC9B;CACA,OAAO;AACT;;;;;AAMA,SAAS,MAAM,MAAc,QAAqC;CAChE,MAAM,QAAQ,OAAO;CACrB,MAAM,SAAS,UAAmB,QAAQ,IAAI,QAAQ,OAAO,QAAQ,KAAM,KAAK;CAChF,MAAM,SAAS,UAAmB,QAAQ,QAAQ,OAAO,MAAM,OAAO,MAAO,IAAI;CACjF,MAAM,UAAU,SAAuC;EACrD,MAAM,SAAS,MAAM,KAAK,IAAI;EAC9B,OAAO,SAAU,qBAAqB,OAAO,EAAE,CAAE,UAAU,CAAC,CAAC,EAAE,QAAQ,OAAQ;CACjF;CACA,MAAM,UAAU,SAAiB,CAAC,KAAK,KAAK,IAAI,KAAK,WAAW,IAAI,MAAM;;CAE1E,MAAM,WAAW,QAAgB,MAAc,aAA6B;EAC1E,IAAI,UAAU,OAAO,OAAO;EAC5B,MAAM,UAAU,QAAQ,KAAK,IAAI;EAEjC,IAAI,CAAC,WAAW,CAAC,UAAU,KAAK,KAAK,MAAM,OAAO,SAAU,MAAM,MAAM,CAAC,CAAC,GAAG,OAAO;EACpF,MAAM,QAAQ,UAAU,QAAQ,EAAE,CAAE,SAAS;EAC7C,IAAI,MAAM,SAAS;EACnB,OAAO,MAAM,OAAO,OAAO,GAAG;GAC5B,MAAM,OAAO,MAAM,GAAG;GACtB,IAAI,KAAK,KAAK,IAAI,GAAG;GACrB,MAAM,UAAU,QAAQ,KAAK,IAAI;GACjC,IAAI,WAAW,QAAQ,EAAE,CAAE,SAAS,OAAO;EAC7C;EACA,OAAO;CACT;;;;;;;;;CASA,MAAM,QAAQ,UAA+D;EAC3E,IAAI,SAAS,OAAO,OAAO;EAC3B,MAAM,OAAO,KAAK,MAAM,OAAO,QAAS,MAAM,KAAK,CAAC,CAAC,CAAC,QAAQ;EAC9D,IAAI,CAAC,KAAK,SAAS,IAAI,GAAG,OAAO;EACjC,MAAM,QAAQ,KAAK,MAAM,IAAI;EAC7B,IAAI,QAAqD;EACzD,KAAK,IAAI,QAAQ,GAAG,QAAQ,MAAM,QAAQ,SAAS,GAAG;GACpD,MAAM,OAAO,OAAO,MAAM,MAAO;GAEjC,IAAI,CAAC,QAAS,QAAQ,KAAK,MAAM,KAAK,MAAM,QAAQ,EAAG,GAAI;GAC3D,MAAM,OAAO,MAAM,QAAQ;GAC3B,IAAI,MAAM,QAAQ;GAClB,IAAI,SAAS,KAAA,GAAW;IACtB,IAAI,SAAS,QAAQ,MAAM,QAAQ,QAAQ,GAAG,MAAM,QAAQ,CAAC,GAAG,QAAQ,CAAC;SACpE,IAAI,OAAO,MAAM,QAAQ,CAAC,CAAC,GAAG,MAAM,QAAQ;GACnD,OAAO,IAAI,SAAS,UAAU,CAAC,MAAM,KAAK,IAAI,GAAG,MAAM,QAAQ,OAAO,MAAM,QAAQ,CAAC;GACrF,MAAM,QAAgB,QAAQ,MAAM,MAAM;GAC1C,QAAQ;IAAE;IAAM,KAAK,KAAK,IAAI,KAAK,KAAK;GAAE;EAC5C;EACA,OAAO;CACT;CAEA,MAAM,SAAmB,CAAC;CAC1B,IAAI,QAAQ;CACZ,OAAO,QAAQ,OAAO;EACpB,MAAM,OAAO;EACb,MAAM,QAAQ,KAAK,KAAK;EACxB,QAAQ,QAAQ,MAAM,MAAM,QAAQ;EAGpC,IAAI,UAAU,MAAM,SAAS,UAAU,MAAM,SAAS,SAAS;GAC7D,MAAM,QAAQ,KAAK,KAAK;GACxB,IAAI,SAAS,MAAM,UAAU,MAAM,SAAS,SAAS,SAAS,SAAS,QAAQ,MAAM;EACvF;EAIA,IACE,UAAU,OAAO,KACjB,QAAQ,SACR,WAAW,KAAK,KAAK,MAAM,OAAO,OAAQ,MAAM,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,KAC7D,QAAQ,KAAK,MAAM,KAAK,CAAC,CAAC,UAAU,CAAC,GAErC,SAAS;EAGX,OAAO,QAAQ,SAAS,SAAS,KAAK,MAAM,OAAO,OAAQ,OAAO,MAAO,CAAC,GAAG,SAAS;EAEtF,OAAO,KAAK,OAAO,KAAM;CAC3B;CACA,OAAO;AACT;;;;;AAMA,MAAa,wBAAwB,aACnC,SAAS,SAAS,IAAI,IAAI,SAAS,QAAQ,UAAU,IAAI,IAAI;;AAG/D,MAAM,yBAAS,IAAI,QAAuB;;AAG1C,MAAa,gBAAgB,UAAkC,OAAO,IAAI,KAAK;AAyB/E,MAAM,QAAQ;;;;;;AAOd,SAAS,OACP,MACA,MACA,UACA,MACiE;CACjE,MAAM,OAAO,OAAO,EAAE,GAAG,KAAK,KAAK,IAAI,MAAM;CAC7C,MAAM,SAAS,YAAY,OAAO,SAAS,OAAO,MAAM,GAAG,KAAK,KAAK,IAAI,CAAC,IAAI;CAC9E,MAAM,QAAQ,YAAY,OAAO,SAAS,MAAM,QAAQ,SAAS,KAAK,MAAM,KAAK,EAAE,IAAI,CAAC;CACxF,IAAI,KAAK,OAAO,KAAK,KAAK;CAE1B,IAAI,SAAS,KAAK;CAClB,KAAK,IAAI,OAAO,GAAG,OAAO,SAAS,SAAS,IAAI,QAAQ,GACtD,SAAS,KAAK,YAAY,MAAM,SAAS,CAAC;CAC5C,KAAK,IAAI,MAAM,KAAK,QAAQ,MAAM,EAAE,GAAG,QAAQ,IAAI,MAAM,KAAK,QAAQ,MAAM,EAAE,GAAG;EAC/E,IAAI,KAAK,MAAM,KAAK,MAAM,IAAI,GAAG,CAAC,KAAK,KAAK,MAAM,OAAO,KAAK,EAAE;EAChE,KAAK,MAAM;EACX,IAAI,OAAO,QAAQ,MAAM,KAAK;GAAE;GAAI,MAAM,EAAE,GAAG,KAAK;GAAG,OAAO,OAAO;EAAO,CAAC;CAC/E;CACA,MAAM,OAAiB;EAAE;EAAM,QAAQ,CAAC,GAAG,MAAM;EAAG,OAAO,MAAM,MAAM,EAAM;CAAE;CAE/E,IAAI,KAAK,KAAK,UAAU,KAAK,MAAM,KAAK,MAAM,EAAE,CAAC,KAAK,KAAK,MAAM,OAAO,KAAK,EAAE;CAC/E,OAAO,KAAK,QAAQ,OAAO;EAAE;EAAQ,UAAU;EAAM,OAAO,KAAK;CAAM;AACzE;;AAGA,SAAS,MACP,MACA,MACA,MACA,OACiB;CACjB,MAAM,SAAS,MAAM,MAAM,IAAI;CAC/B,MAAM,SAA0B,CAAC;CACjC,IAAI,UAAU;CACd,IAAI,QAAQ;CACZ,KAAK,IAAI,QAAQ,GAAG,QAAQ,OAAO,QAAQ,SAAS,GAAG;EACrD,MAAM,SAAS,OAAO;EACtB,MAAM,MAAM,OAAO,QAAQ,MAAM,KAAK;EACtC,MAAM,SAAS,KAAK,MAAM,QAAQ,GAAG;EACrC,MAAM,QAAQ;GAAE;GAAQ;GAAQ,MAAM;EAAQ;EAC9C,OAAO,KAAK,SAAU,QAAQ,SAAS;EAIvC,KAAK,KAAK,QAAQ,MAAM,KAAK,YAAY,OAAO,EAAE,SAAS,QAAQ,KAAK,SAAS;GAC/E,MAAM,UAAU,MAAM,KAAK,OAAO,MAAM,MAAM,CAAC;GAC/C,IAAI,WAAW,EAAE,QAAQ,EAAE,CAAE,OAAO,OAAO,QAAQ,EAAE,CAAE,SAAS,GAAG,IAAI,OAAO,IAAI,KAAK;EACzF;EACA,OAAO,KAAK,KAAK;EAEjB,IAAI,QAAQ,MAAM,OAAO,QAAQ;EACjC,KAAK,IAAI,KAAK,OAAO,QAAQ,IAAI,GAAG,OAAO,IAAI,KAAK,OAAO,QAAQ,MAAM,KAAK,CAAC,GAAG,WAAW;CAC/F;CACA,OAAO;AACT;;AAGA,SAAgB,YAAY,OAAgC;CAC1D,MAAM,WAAW,qBAAqB,KAAK;CAC3C,IAAI,CAAC,UAAU,OAAO,MAAM,QAAQ;CACpC,MAAM,MAAM,OAAO,UAAU,GAAG,MAAM,IAAI;CAC1C,OAAO,MAAM,MAAM,UAAU,IAAI,QAAQ,GAAG,IAAI,KAAK,IAAI,MAAM,QAAQ;AACzE;;;;;;AAOA,MAAM,WAAW;;;;;;;;AAgBjB,SAAgB,sBAAqC;CACnD,IAAI,OAAO;CACX,IAAI,SAA0B,CAAC;CAC/B,IAAI,WAA4B;CAChC,IAAI,QAAQ;CAEZ,OAAO;EACL,IAAI,QAAQ;GACV,OAAO;EACT;EACA,MAAM,OAAO;GACX,MAAM,WAAW,qBAAqB,KAAK;GAC3C,IAAI,CAAC,UAAU;IACb,OAAO;IACP,SAAS,CAAC;IACV,WAAW;IACX,OAAO,MAAM,QAAQ;GACvB;GACA,IAAI,OAAO,OAAO,MAAM,GAAG,KAAK,IAAI,GAAG,OAAO,SAAS,QAAQ,CAAC;GAChE,IAAI,OAAO,KAAK,GAAG,EAAE;GACrB,IAAI,OAAO,OAAO,KAAK,SAAS,KAAK,OAAO,SAAS;GACrD,IAAI,OAAO,KAAK,EAAE,SAAS,UAAU,QAAQ,SAAS,WAAW,KAAK,MAAM,GAAG,IAAI,CAAC,IAAI;IACtF,OAAO,CAAC;IACR,OAAO,KAAA;IACP,OAAO;GACT;GAGA,IAAI,OAAoB;GACxB,IAAI,aAAa,QAAQ,SAAS,SAAS,MACzC,KAAK,IAAI,QAAQ,SAAS,MAAM,SAAS,GAAG,SAAS,KAAK,CAAC,MAAM,SAAS,GAAG;IAC3E,MAAM,QAAQ,SAAS,MAAM;IAC7B,IAAI,SAAS,UAAU,MAAM,MAAM,SAAS,WAAW,KAAK,MAAM,MAAM,MAAM,EAAE,GAAG,IAAI,GACrF,OAAO;GACX;GAEF,SAAS,SAAS,UAAU,OAAO,KAAK,KAAK;GAC7C,MAAM,MAAM,OAAO,UAAU,MAAM,OAAO,WAAW,MAAM,IAAI;GAC/D,OAAO;GACP,IAAI,CAAC,KAAK;IAER,SAAS,CAAC;IACV,WAAW;IACX,OAAO,MAAM,QAAQ;GACvB;GACA,WAAW,IAAI;GACf,IAAI,OAAO;GACX,IAAI,MAAM;IACR,OAAO,KAAK;IACZ,KAAK,IAAI,KAAK,KAAK,OAAO,QAAQ,IAAI,GAAG,OAAO,IAAI,KAAK,KAAK,OAAO,QAAQ,MAAM,KAAK,CAAC,GACvF,QAAQ;GACZ;GACA,MAAM,OAAO,MAAM,UAAU,IAAI,QAAQ,MAAM,IAAI,KAAK;GAGxD,KAAK,IAAI,QAAQ,GAAG,QAAQ,KAAK,QAAQ,SAAS,GAAG;IACnD,MAAM,SAAS,OAAO,KAAK,SAAS;IACpC,MAAM,QAAQ,KAAK;IACnB,IAAI,UAAU,OAAO,WAAW,MAAM,UAAU,OAAO,WAAW,MAAM,QACtE,KAAK,SAAS;GAElB;GACA,SAAS,CAAC,GAAG,MAAM,GAAG,IAAI;GAC1B,OAAO;EACT;CACF;AACF"}
|
|
1
|
+
{"version":3,"file":"blocks.js","names":[],"sources":["../../src/core/blocks.ts"],"sourcesContent":["/**\n * Splitting a document into top-level blocks that can be rendered, and remembered, one by one.\n *\n * A streaming reply grows at its end, and re-rendering the whole of it on every token is work\n * proportional to its length, repeated per token. Cut at its top-level blocks instead, every block\n * but the last is text that no longer changes: a renderer that memoizes each block by its source\n * parses and reconciles only the tail.\n *\n * The cut must never change the result, so the text is cut only where one parse of the whole\n * document is certain to start a new top-level block, and a block is merged with its neighbours\n * wherever that parse would read them together:\n * - the dialect's forms: a marker-only quote (`> [!STEPS]`) governs the list after it, `[!TABS]`\n * the run of sections after it, and a `[!PROS]` right before a `[!CONS]` is one pair;\n * - an image alone in its paragraph and an emphasis-only line under it, its caption;\n * - an HTML block whose tag is closed further down;\n * - a `$$` display formula with blank lines inside;\n * - a document with footnote or link reference definitions, which resolve across blocks, is\n * not split at all.\n *\n * The boundaries are found by a scan of the lines (`step`), not by a Markdown lexer: it follows\n * only what decides where a top-level block can end (a fence, a list, an HTML block that blank\n * lines do not end), and where it cannot tell, it stops cutting for the rest of the document.\n * Merging is always safe, a wrong cut never is. The tests render a corpus both ways and require\n * the same markup, at every prefix.\n */\nimport { parseContainerMarker, type ContainerKind } from '../dialect'\nimport { continues } from './continues'\n\nexport interface MarkdownBlock {\n /** The block's own source. */\n source: string\n /** Where it starts in the document: characters before it, and its 1-based line. */\n offset: number\n line: number\n}\n\nconst FENCE = /^ {0,3}(`{3,}|~{3,})(.*)$/\nconst INNER_FENCE = /^[ \\t]*(`{3,}|~{3,})(.*)$/\nconst ITEM = /^( {0,3})([-+*]|\\d{1,9}[.)])(.*)$/\n/** A list item that may interrupt a paragraph: a bullet, or the number 1, with text after it. */\nconst INTERRUPTING_ITEM = /^ {0,3}(?:[-+*]|1[.)]) +\\S/\nconst RULE = /^ {0,3}(?:(?:-[ \\t]*){3,}|(?:_[ \\t]*){3,}|(?:\\*[ \\t]*){3,})$/\nconst HEADING = /^ {0,3}(#{1,6})(?:[ \\t]|$)/\nconst UNDERLINE = /\\n {0,3}(?:=+|-+)[ \\t]*(?:\\n|$)/\nconst QUOTE = /^ {0,3}>[ \\t]?(.*)$/\n/** HTML blocks that a blank line does not end: how each opens, and what closes it. */\nconst LONG_HTML: ReadonlyArray<readonly [RegExp, RegExp]> = [\n [/^ {0,3}<(?:pre|script|style|textarea)(?=[\\s>]|$)/i, /<\\/(?:pre|script|style|textarea)>/i],\n [/^ {0,3}<!--/, /-->/],\n [/^ {0,3}<\\?/, /\\?>/],\n [/^ {0,3}<!\\[CDATA\\[/, /\\]\\]>/],\n [/^ {0,3}<![A-Za-z]/, />/],\n]\nconst HTML_START = /^ {0,3}<[A-Za-z/!?]/\nconst HTML_LINE = /^ {0,3}<[A-Za-z]/m\nconst HTML_LINES = /^ {0,3}<([A-Za-z][\\w:-]*)(?=[\\s>/])/gm\nconst FOOTNOTE_DEFINITION = /\\[\\^[^\\]\\s]{1,200}\\]:/\nconst LINK_DEFINITION = /^ {0,3}\\[[^\\]]+\\]:/\nconst OPEN_LABEL = /^ {0,3}\\[[^\\]]*$/\nconst IMAGE_ONLY = /^!\\[[^\\]]*\\]\\([^)]*\\)$/\nconst DOUBLE_DOLLAR = /(?<!\\\\)\\$\\$/g\nconst VOID_TAGS = new Set(['area', 'br', 'col', 'embed', 'hr', 'img', 'input', 'source', 'wbr'])\n\nconst whole = (markdown: string): MarkdownBlock[] => [{ source: markdown, offset: 0, line: 1 }]\n\n/** What the scan knows after a line: only what decides where a top-level block can end. */\ninterface Scan {\n /** The opening run of the top-level fence the text is inside. */\n fence: string | null\n /** What closes the HTML block the text is inside, one that blank lines do not end. */\n html: RegExp | null\n /** The content column of the open top-level list item; 0 outside a list. */\n list: number\n /** The list may already be over: a line left of the item that was a lazy continuation or not. */\n unsure: boolean\n /** The lines since the last blank one may be an HTML block, where a fence line is not a fence. */\n markup: boolean\n /** The next line starts a block: the last one was blank or closed a fence, or there was none. */\n fresh: boolean\n /** An odd number of `$$` so far in this block: a display formula may be open. */\n odd: boolean\n /** A `[label` that has not closed yet: it may turn out to be a link definition. */\n label: boolean\n /**\n * The last line with text was indented four or more: code, or the rest of a paragraph. The\n * parser still has indented code open when it reads the next line, and reads it as one that\n * interrupts: `2. two` or an empty `-` is then text, not a list. The line after is never cut.\n */\n indented: boolean\n /** The last line with text closed a top-level fence: whatever comes next is a new block. */\n closed: boolean\n /** Text follows a closed fence in the same piece: that piece is not only a fence. */\n mixed: boolean\n /** The opening run of a fence inside the open list item. */\n inner: string | null\n /** A line with text has been read: blank lines at the very start belong to the first block. */\n seen: boolean\n /** The scan cannot tell what the next lines are: no more cuts. */\n stuck: boolean\n /** A definition that resolves across blocks: the document stays one block. */\n whole: boolean\n}\n\nconst start = (): Scan => ({\n fence: null,\n html: null,\n list: 0,\n unsure: false,\n markup: false,\n fresh: true,\n odd: false,\n label: false,\n closed: false,\n indented: false,\n mixed: false,\n inner: null,\n seen: false,\n stuck: false,\n whole: false,\n})\n\n/** The column a line's text starts at, tabs to the next multiple of four; -1 for a blank line. */\nfunction indentOf(line: string): number {\n let column = 0\n for (let index = 0; index < line.length; index += 1) {\n const code = line.charCodeAt(index)\n if (code === 32) column += 1\n else if (code === 9) column += 4 - (column % 4)\n else return column\n }\n return -1\n}\n\n/**\n * The content column of a list item's line, 0 when the line is not one, -1 when it is one whose\n * column a tab after the marker leaves unknown.\n */\nfunction itemColumn(line: string): number {\n const item = ITEM.exec(line)\n if (!item) return 0\n const base = item[1]!.length + item[2]!.length\n const rest = item[3]!\n if (rest === '') return base + 1\n if (rest[0] === '\\t') return -1\n if (rest[0] !== ' ') return 0\n const text = rest.trimStart()\n const spaces = rest.length - text.length\n if (text === '') return base + 1\n if (rest.slice(0, spaces).includes('\\t')) return -1\n return base + (spaces > 4 ? 1 : spaces)\n}\n\n/**\n * One line through the scan. True when the line starts a new top-level block for certain: after a\n * blank line outside every open construct, or as the fence line of a top-level fence.\n */\nfunction step(scan: Scan, line: string): boolean {\n const seen = scan.seen\n const closed = scan.closed\n if (!seen && line.trim() !== '') scan.seen = true\n const cut = advance(scan, line)\n // A line with text right after a fence that did not start a block: the scan was stuck on it.\n if (closed && !scan.closed && !cut) scan.mixed = true\n return cut && seen\n}\n\nfunction advance(scan: Scan, line: string): boolean {\n if (scan.stuck || scan.whole) return false\n if (scan.fence) {\n const closing = FENCE.exec(line)\n if (\n closing &&\n closing[1]![0] === scan.fence[0] &&\n closing[1]!.length >= scan.fence.length &&\n closing[2]!.trim() === ''\n ) {\n scan.fence = null\n scan.fresh = true\n scan.closed = true\n }\n return false\n }\n if (scan.html) {\n if (scan.html.test(line)) scan.html = null\n return false\n }\n const indent = indentOf(line)\n if (indent === -1) {\n scan.fresh = true\n scan.markup = false\n scan.label = false\n return false\n }\n const fresh = scan.fresh\n const closed = scan.closed\n scan.fresh = false\n scan.closed = false\n\n if (FOOTNOTE_DEFINITION.test(line) || LINK_DEFINITION.test(line)) return definition(scan)\n if (scan.label) {\n const close = line.indexOf(']')\n if (close !== -1) {\n if (line[close + 1] === ':') return definition(scan)\n scan.label = false\n }\n } else if (OPEN_LABEL.test(line)) scan.label = true\n\n const odd = scan.odd\n if (line.includes('$$') && (line.match(DOUBLE_DOLLAR)?.length ?? 0) % 2 === 1) scan.odd = !odd\n\n const fence = FENCE.exec(line)\n // A backtick fence has no backtick in its info string: ```` ```a``` ```` is inline code.\n const opensFence = fence !== null && !(fence[1]![0] === '`' && fence[2]!.includes('`'))\n // Inside a formula or an HTML block a fence line is text, and which of the two it is here\n // cannot be told from the lines alone.\n if (opensFence && (odd || scan.markup)) return stick(scan)\n\n if (scan.list > 0) {\n if (indent >= scan.list) {\n // In the item, if the list is still open; a top-level fence if it is not.\n if (opensFence && scan.unsure) return stick(scan)\n const inner = INNER_FENCE.exec(line)\n if (scan.inner) {\n if (\n inner &&\n inner[1]![0] === scan.inner[0] &&\n inner[1]!.length >= scan.inner.length &&\n inner[2]!.trim() === ''\n )\n scan.inner = null\n } else if (inner && !(inner[1]![0] === '`' && inner[2]!.includes('`'))) scan.inner = inner[1]!\n return false\n }\n // A fence in the item that a line left of the item ends: where its last blank lines go\n // depends on what follows, so nothing is cut from here on.\n if (scan.inner) return stick(scan)\n const column = RULE.test(line) ? 0 : itemColumn(line)\n if (column === -1) return stick(scan)\n if (column > 0) {\n // The next item of the list (or of a new one right after it).\n scan.list = column\n if (fresh) scan.unsure = false\n return false\n }\n if (\n fresh ||\n opensFence ||\n HEADING.test(line) ||\n RULE.test(line) ||\n QUOTE.test(line) ||\n LONG_HTML.some(([opens]) => opens.test(line))\n ) {\n // Left of the item after a blank line, or a block that cannot be a lazy continuation: the\n // list is over, and the line is read as a top-level one below.\n scan.list = 0\n scan.unsure = false\n } else {\n // A lazy continuation of the item's paragraph, or, if the item did not end in a paragraph,\n // the first line after the list.\n scan.unsure = true\n if (HTML_START.test(line)) scan.markup = true\n return false\n }\n }\n\n // Indented four or more after a blank line: indented code, which goes on over blank lines.\n // Right after a fence, any line starts a block.\n const indented = scan.indented\n scan.indented = indent >= 4\n const cut = closed || (fresh && indent < 4 && !odd && !indented)\n for (const [opens, closes] of LONG_HTML) {\n if (!opens.test(line)) continue\n if (!closes.test(line.replace(opens, ''))) scan.html = closes\n return cut\n }\n if (opensFence) {\n scan.fence = fence![1]!\n // A fence starts a block wherever it is written, and is one whatever follows it.\n return !odd\n }\n const column = indent < 4 && !RULE.test(line) ? itemColumn(line) : 0\n if (column === -1) return stick(scan)\n // After a line indented four, which was code or the rest of a paragraph, only the scan of the\n // lines before it could say which a list item that cannot interrupt is.\n if (column > 0 && indented && !INTERRUPTING_ITEM.test(line)) return stick(scan)\n if (column > 0) {\n scan.list = column\n scan.unsure = scan.markup || (!fresh && !INTERRUPTING_ITEM.test(line))\n return cut\n }\n if (HTML_START.test(line)) scan.markup = true\n return cut\n}\n\nfunction stick(scan: Scan): false {\n scan.stuck = true\n return false\n}\n\nfunction definition(scan: Scan): false {\n scan.whole = true\n return false\n}\n\n/** The line of `text` that starts at `at`, without its newline. */\nfunction lineAt(text: string, at: number): string {\n const end = text.indexOf('\\n', at)\n return text.slice(at, end === -1 ? text.length : end)\n}\n\n/** Whether `raw` has a line that opens an HTML tag still open at its end. */\nfunction htmlOpen(raw: string): boolean {\n // A comment left open takes whatever follows it.\n if (raw.includes('<!--') && raw.split('<!--').length > raw.split('-->').length) return true\n if (!HTML_LINE.test(raw)) return false\n const tried = new Set<string>()\n for (const opener of raw.matchAll(HTML_LINES)) {\n const tag = opener[1]!.toLowerCase()\n if (tried.has(tag) || VOID_TAGS.has(tag)) continue\n tried.add(tag)\n const rest = raw.slice(opener.index)\n const openers = rest.match(new RegExp(`<${tag}(?=[\\\\s>/])[^>]*>`, 'gi')) ?? []\n const opened = openers.filter((found) => !found.endsWith('/>')).length\n const closed = rest.match(new RegExp(`</${tag}\\\\s*>`, 'gi'))?.length ?? 0\n if (opened > closed) return true\n }\n return false\n}\n\n/**\n * The pieces the scan cut (`starts`, offsets into `text`) joined into the blocks that must render\n * together: the offsets where a block starts. Read by the same grammar as the remark pass.\n */\nfunction group(text: string, starts: readonly number[]): number[] {\n const count = starts.length\n const endOf = (piece: number) => (piece + 1 < count ? starts[piece + 1]! : text.length)\n const first = (piece: number) => (piece < count ? lineAt(text, starts[piece]!) : '')\n const kindOf = (line: string): ContainerKind | null => {\n const quoted = QUOTE.exec(line)\n return quoted ? (parseContainerMarker(quoted[1]!.trimStart())?.kind ?? null) : null\n }\n const isItem = (line: string) => !RULE.test(line) && itemColumn(line) !== 0\n /** The piece after the tabs whose first heading is in `holder`, on `line` or underlined. */\n const tabsEnd = (holder: number, line: string, fallback: number): number => {\n if (holder >= count) return fallback\n const opening = HEADING.exec(line)\n // A heading underlined with `===` has a depth this scan does not read: it ends at a `---`.\n if (!opening && !UNDERLINE.test(text.slice(starts[holder]!, endOf(holder)))) return fallback\n const depth = opening ? opening[1]!.length : 1\n let end = holder + 1\n for (; end < count; end += 1) {\n const next = first(end)\n if (RULE.test(next)) break\n const heading = HEADING.exec(next)\n if (heading && heading[1]!.length < depth) break\n }\n return end\n }\n /**\n * The form a piece holds a marker line for, and the piece after everything it governs. A list\n * form takes the one list right after it; tabs take the sections from the heading right after\n * them up to a shallower heading, a `---` or the end. The marker is a quote of its own, on any\n * line of the piece: on the last, what it governs is the next piece. A marker whose quote goes\n * on (`> [!PROS]\\n> - a`) holds its own content. Of several markers, the one that reaches\n * furthest decides.\n */\n const form = (piece: number): { kind: ContainerKind; end: number } | null => {\n if (piece >= count) return null\n const body = text.slice(starts[piece]!, endOf(piece)).trimEnd()\n if (!body.includes('[!')) return null\n const lines = body.split('\\n')\n let found: { kind: ContainerKind; end: number } | null = null\n for (let index = 0; index < lines.length; index += 1) {\n const kind = kindOf(lines[index]!)\n // The marker opens its quote: the line above it is not of the same quote.\n if (!kind || (index > 0 && QUOTE.test(lines[index - 1]!))) continue\n const next = lines[index + 1]\n let end = piece + 1\n if (next === undefined) {\n if (kind === 'tabs') end = tabsEnd(piece + 1, first(piece + 1), piece + 1)\n else if (isItem(first(piece + 1))) end = piece + 2\n } else if (kind === 'tabs' && !QUOTE.test(next)) end = tabsEnd(piece, next, piece + 1)\n const reach: number = found ? found.end : 0\n found = { kind, end: Math.max(end, reach) }\n }\n return found\n }\n\n const blocks: number[] = []\n let piece = 0\n while (piece < count) {\n const from = piece\n const found = form(piece)\n piece = found ? found.end : piece + 1\n\n // A pros right before a cons (or the reverse) is set as one pair.\n if (found && (found.kind === 'pros' || found.kind === 'cons')) {\n const other = form(piece)\n if (other && other.kind === (found.kind === 'pros' ? 'cons' : 'pros')) piece = other.end\n }\n\n // An image alone in its paragraph takes the emphasis line under it as its caption. Merging is\n // always safe (it is what one parse does anyway), so the test here can be loose.\n if (\n piece === from + 1 &&\n piece < count &&\n IMAGE_ONLY.test(text.slice(starts[from]!, endOf(from)).trim()) &&\n /^[*_]/.test(first(piece).trimStart())\n )\n piece += 1\n\n // An HTML block whose tag closes further down.\n while (piece < count && htmlOpen(text.slice(starts[from]!, starts[piece]!))) piece += 1\n\n blocks.push(starts[from]!)\n }\n return blocks\n}\n\n/**\n * Line endings as the scan reads them: `\\r\\n` and `\\r` become `\\n`. Offsets of the blocks are into\n * the normalized text, which is what the renderer draws from.\n */\nexport const normalizeLineEndings = (markdown: string): string =>\n markdown.includes('\\r') ? markdown.replace(/\\r\\n?/g, '\\n') : markdown\n\n/** Blocks that are one fenced code block and nothing else: no prose, HTML, math or heading. */\nconst fenced = new WeakSet<MarkdownBlock>()\n\n/** Whether the splitter cut this block as one fenced code block. */\nexport const isFenceBlock = (block: MarkdownBlock): boolean => fenced.has(block)\n\n/** The scan after a complete line: the next text can go on from there. */\ninterface Mark {\n /** The offset after that line. */\n at: number\n scan: Scan\n /** How many pieces had been cut by then. */\n count: number\n}\n\n/** Where a scan can be resumed. */\ninterface Progress {\n /** Where the scan started: the end of the blocks kept as final. */\n from: number\n /** The pieces cut up to the last mark, `from` first. */\n starts: number[]\n /**\n * The scan after each of the last complete lines, oldest first. Several, because a healed\n * stream rewrites its end: the fence it closes or the row it drops is a line that the next\n * update does not have, and the scan then goes on from the line before.\n */\n marks: Mark[]\n}\n\nconst MARKS = 3\n\n/**\n * The pieces of `text` from `from` on: the offsets where the scan cut, or null when a definition\n * keeps the document whole. `progress` and `mark` are where an earlier scan of the same start\n * can go on from.\n */\nfunction pieces(\n text: string,\n from: number,\n progress: Progress | null,\n mark: Mark | null,\n): { starts: number[]; progress: Progress; mixed: boolean } | null {\n const scan = mark ? { ...mark.scan } : start()\n const starts = progress && mark ? progress.starts.slice(0, mark.count) : [from]\n const marks = progress && mark ? progress.marks.filter((kept) => kept.at <= mark.at) : []\n let at = mark ? mark.at : from\n // Marks are left after the last complete lines only.\n let marked = text.length\n for (let back = 0; back < MARKS && marked > at; back += 1)\n marked = text.lastIndexOf('\\n', marked - 1)\n for (let end = text.indexOf('\\n', at); end !== -1; end = text.indexOf('\\n', at)) {\n if (step(scan, text.slice(at, end)) && at > from) starts.push(at)\n at = end + 1\n if (end >= marked) marks.push({ at, scan: { ...scan }, count: starts.length })\n }\n const next: Progress = { from, starts: [...starts], marks: marks.slice(-MARKS) }\n // The last line is still being written: read, but not remembered.\n if (at < text.length && step(scan, text.slice(at)) && at > from) starts.push(at)\n return scan.whole ? null : { starts, progress: next, mixed: scan.mixed }\n}\n\n/** The blocks of `text` that the pieces at `cuts` make, the first on line `line`. */\nfunction build(\n text: string,\n cuts: readonly number[],\n line: number,\n mixed: boolean,\n): MarkdownBlock[] {\n const starts = group(text, cuts)\n const blocks: MarkdownBlock[] = []\n let current = line\n let piece = 0\n for (let index = 0; index < starts.length; index += 1) {\n const offset = starts[index]!\n const end = starts[index + 1] ?? text.length\n const source = text.slice(offset, end)\n const block = { source, offset, line: current }\n while (cuts[piece]! < offset) piece += 1\n // One piece that opens with a fence line is that fence and the blank lines after it: the scan\n // cuts before a top-level fence and after the line that closes it.\n // (When the scan got stuck after a fence, the last piece holds more than that fence.)\n if ((cuts[piece + 1] ?? text.length) === end && !(mixed && end === text.length)) {\n const opening = FENCE.exec(lineAt(text, offset))\n if (opening && !(opening[1]![0] === '`' && opening[2]!.includes('`'))) fenced.add(block)\n }\n blocks.push(block)\n // The last block's lines place nothing: a fence that grows is not counted on every token.\n if (index + 1 === starts.length) break\n for (let at = source.indexOf('\\n'); at !== -1; at = source.indexOf('\\n', at + 1)) current += 1\n }\n return blocks\n}\n\n/** The top-level blocks of a document, cut where one parse of the whole would not join them. */\nexport function splitBlocks(input: string): MarkdownBlock[] {\n const markdown = normalizeLineEndings(input)\n if (!markdown) return whole(markdown)\n const cut = pieces(markdown, 0, null, null)\n return cut ? build(markdown, cut.starts, 1, cut.mixed) : whole(markdown)\n}\n\n/**\n * How many blocks at the end are read again. Text appended to a document can change only the\n * group it is appended to, and that group can join the one before it (a `[!CONS]` arriving\n * under a `[!PROS]`); every group above those two is final.\n */\nconst REOPENED = 2\n\nexport interface BlockSplitter {\n /** The same blocks as `splitBlocks(markdown)`; the ones that did not change are the same objects. */\n split(markdown: string): MarkdownBlock[]\n /** Characters scanned so far: the work the splitter has done. */\n readonly lexed: number\n}\n\n/**\n * `splitBlocks` for a document that grows at its end, as a streaming reply does. It remembers the\n * blocks it cut last time and where its scan stopped: when the new text still starts with all of\n * them but the last two, it keeps those, as the very same objects, and scans only the lines that\n * are new. Any other text (a reply that was regenerated, or edited above its end) is split afresh.\n * Each update then costs the new characters, not the reply: a stream is linear in its length.\n */\nexport function createBlockSplitter(): BlockSplitter {\n let text = ''\n let blocks: MarkdownBlock[] = []\n let progress: Progress | null = null\n let lexed = 0\n\n return {\n get lexed() {\n return lexed\n },\n split(input) {\n const markdown = normalizeLineEndings(input)\n if (!markdown) {\n text = ''\n blocks = []\n progress = null\n return whole(markdown)\n }\n let kept = blocks.slice(0, Math.max(0, blocks.length - REOPENED))\n let last = kept.at(-1)\n let from = last ? last.offset + last.source.length : 0\n if (from > 0 && !continues(markdown, text, from)) {\n kept = []\n last = undefined\n from = 0\n }\n // The scan goes on from the last line it read that is still there, when it started at the\n // same place.\n let mark: Mark | null = null\n if (progress !== null && progress.from === from) {\n for (let index = progress.marks.length - 1; index >= 0 && !mark; index -= 1) {\n const tried = progress.marks[index]!\n if (continues(markdown, text, tried.at, from)) mark = tried\n }\n }\n lexed += markdown.length - (mark ? mark.at : from)\n const cut = pieces(markdown, from, mark ? progress : null, mark)\n text = markdown\n if (!cut) {\n // A definition arrived: what was cut before it is one block with it now.\n blocks = []\n progress = null\n return whole(markdown)\n }\n progress = cut.progress\n let line = 1\n if (last) {\n line = last.line\n for (let at = last.source.indexOf('\\n'); at !== -1; at = last.source.indexOf('\\n', at + 1))\n line += 1\n }\n const rest = build(markdown, cut.starts, line, cut.mixed)\n // A block that reads the same as last time is the same object, with what was worked out\n // from it.\n for (let index = 0; index < rest.length; index += 1) {\n const before = blocks[kept.length + index]\n const after = rest[index]!\n if (before && before.offset === after.offset && before.source === after.source) {\n rest[index] = before\n }\n }\n blocks = [...kept, ...rest]\n return blocks\n },\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// src/core/continues.ts
|
|
2
|
+
/**
|
|
3
|
+
* Whether `next` holds what `previous` held from `from` up to `upTo`: the question every part that
|
|
4
|
+
* remembers a growing text asks on each token, before it goes on from where it stopped.
|
|
5
|
+
*
|
|
6
|
+
* It is asked with `===` on two slices, not with `startsWith`. The two say the same, but V8
|
|
7
|
+
* (Chrome, Edge, Node) compares the slices as memory and walks `startsWith` a character at a
|
|
8
|
+
* time: on a 35 KB reply that is one microsecond against seventy, on every token.
|
|
9
|
+
*
|
|
10
|
+
* `upTo` is a place inside `previous`; past its end the answer is no, and the caller starts over.
|
|
11
|
+
*/
|
|
12
|
+
const continues = (next, previous, upTo, from = 0) => next.length >= upTo && next.slice(from, upTo) === previous.slice(from, upTo);
|
|
13
|
+
export { continues };
|
|
14
|
+
|
|
15
|
+
//# sourceMappingURL=continues.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"continues.js","names":[],"sources":["../../src/core/continues.ts"],"sourcesContent":["/**\n * Whether `next` holds what `previous` held from `from` up to `upTo`: the question every part that\n * remembers a growing text asks on each token, before it goes on from where it stopped.\n *\n * It is asked with `===` on two slices, not with `startsWith`. The two say the same, but V8\n * (Chrome, Edge, Node) compares the slices as memory and walks `startsWith` a character at a\n * time: on a 35 KB reply that is one microsecond against seventy, on every token.\n *\n * `upTo` is a place inside `previous`; past its end the answer is no, and the caller starts over.\n */\nexport const continues = (next: string, previous: string, upTo: number, from = 0): boolean =>\n next.length >= upTo && next.slice(from, upTo) === previous.slice(from, upTo)\n"],"mappings":";;;;;;;;;;;AAUA,MAAa,aAAa,MAAc,UAAkB,MAAc,OAAO,MAC7E,KAAK,UAAU,QAAQ,KAAK,MAAM,MAAM,IAAI,MAAM,SAAS,MAAM,MAAM,IAAI"}
|
package/dist/core/fence-scan.js
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
+
import { continues } from "./continues.js";
|
|
1
2
|
// src/core/fence-scan.ts
|
|
3
|
+
/**
|
|
4
|
+
* Incremental fenced-code scan for streaming markdown.
|
|
5
|
+
*
|
|
6
|
+
* A streaming reply is re-stabilized on every token, and every call has to know whether the text
|
|
7
|
+
* ends inside an open fence. Rescanning the whole reply each time is quadratic in its length, so
|
|
8
|
+
* the scan state after the last complete line is remembered and resumed when the next text extends
|
|
9
|
+
* it. Any other text simply rescans from the start.
|
|
10
|
+
*/
|
|
2
11
|
const FENCE_LINE_RE = /^\s{0,3}(`{3,}|~{3,})(.*)$/;
|
|
3
12
|
/** Complete lines scanned so far; tests read it to prove the scan resumes instead of restarting. */
|
|
4
13
|
const fenceScanStats = { lines: 0 };
|
|
@@ -27,7 +36,7 @@ function createFenceScanner() {
|
|
|
27
36
|
};
|
|
28
37
|
return (text) => {
|
|
29
38
|
let { prefix, open } = cached;
|
|
30
|
-
if (!text.
|
|
39
|
+
if (!continues(text, prefix, prefix.length)) {
|
|
31
40
|
prefix = "";
|
|
32
41
|
open = null;
|
|
33
42
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fence-scan.js","names":[],"sources":["../../src/core/fence-scan.ts"],"sourcesContent":["/**\n * Incremental fenced-code scan for streaming markdown.\n *\n * A streaming reply is re-stabilized on every token, and every call has to know whether the text\n * ends inside an open fence. Rescanning the whole reply each time is quadratic in its length, so\n * the scan state after the last complete line is remembered and resumed when the next text extends\n * it. Any other text simply rescans from the start.\n */\n\nexport interface OpenFence {\n marker: string\n bodyStart: number\n}\n\nconst FENCE_LINE_RE = /^\\s{0,3}(`{3,}|~{3,})(.*)$/\n\ninterface ScanState {\n /** Text through the end of the last complete line (ends with `\\n`, or is empty). */\n prefix: string\n open: OpenFence | null\n}\n\n/** Complete lines scanned so far; tests read it to prove the scan resumes instead of restarting. */\nexport const fenceScanStats = { lines: 0 }\n\n/** The fence state after one line, given the state before it. */\nfunction step(\n open: OpenFence | null,\n line: string,\n lineEnd: number,\n textLength: number,\n): OpenFence | null {\n const m = FENCE_LINE_RE.exec(line)\n if (!m) return open\n if (open === null) {\n // A backtick fence has no backtick in its info string: ```` ```a``` ```` is inline code.\n if (m[1]![0] === '`' && m[2]!.includes('`')) return null\n return { marker: m[1]!, bodyStart: Math.min(lineEnd, textLength) }\n }\n const closes =\n m[1]![0] === open.marker[0] && m[1]!.length >= open.marker.length && m[2]!.trim() === ''\n return closes ? null : open\n}\n\n/**\n * The fence the text is currently inside — its marker (e.g. \"```\") and where its body starts —\n * or null when all fenced code blocks are closed. A closing fence must use the same character,\n * be at least as long as the opener, and carry no info string.\n */\nexport type FenceScanner = (text: string) => OpenFence | null\n\n/**\n * A scan that remembers one growing text. Each rendered document has its own: two replies that\n * stream at once would otherwise take turns forgetting each other and scan from the start on\n * every token.\n */\nexport function createFenceScanner(): FenceScanner {\n let cached: ScanState = { prefix: '', open: null }\n\n return (text) => {\n let { prefix, open } = cached\n if (!text.
|
|
1
|
+
{"version":3,"file":"fence-scan.js","names":[],"sources":["../../src/core/fence-scan.ts"],"sourcesContent":["/**\n * Incremental fenced-code scan for streaming markdown.\n *\n * A streaming reply is re-stabilized on every token, and every call has to know whether the text\n * ends inside an open fence. Rescanning the whole reply each time is quadratic in its length, so\n * the scan state after the last complete line is remembered and resumed when the next text extends\n * it. Any other text simply rescans from the start.\n */\nimport { continues } from './continues'\n\nexport interface OpenFence {\n marker: string\n bodyStart: number\n}\n\nconst FENCE_LINE_RE = /^\\s{0,3}(`{3,}|~{3,})(.*)$/\n\ninterface ScanState {\n /** Text through the end of the last complete line (ends with `\\n`, or is empty). */\n prefix: string\n open: OpenFence | null\n}\n\n/** Complete lines scanned so far; tests read it to prove the scan resumes instead of restarting. */\nexport const fenceScanStats = { lines: 0 }\n\n/** The fence state after one line, given the state before it. */\nfunction step(\n open: OpenFence | null,\n line: string,\n lineEnd: number,\n textLength: number,\n): OpenFence | null {\n const m = FENCE_LINE_RE.exec(line)\n if (!m) return open\n if (open === null) {\n // A backtick fence has no backtick in its info string: ```` ```a``` ```` is inline code.\n if (m[1]![0] === '`' && m[2]!.includes('`')) return null\n return { marker: m[1]!, bodyStart: Math.min(lineEnd, textLength) }\n }\n const closes =\n m[1]![0] === open.marker[0] && m[1]!.length >= open.marker.length && m[2]!.trim() === ''\n return closes ? null : open\n}\n\n/**\n * The fence the text is currently inside — its marker (e.g. \"```\") and where its body starts —\n * or null when all fenced code blocks are closed. A closing fence must use the same character,\n * be at least as long as the opener, and carry no info string.\n */\nexport type FenceScanner = (text: string) => OpenFence | null\n\n/**\n * A scan that remembers one growing text. Each rendered document has its own: two replies that\n * stream at once would otherwise take turns forgetting each other and scan from the start on\n * every token.\n */\nexport function createFenceScanner(): FenceScanner {\n let cached: ScanState = { prefix: '', open: null }\n\n return (text) => {\n let { prefix, open } = cached\n if (!continues(text, prefix, prefix.length)) {\n prefix = ''\n open = null\n }\n let offset = prefix.length\n const lastNewline = text.lastIndexOf('\\n')\n while (offset <= lastNewline) {\n const nl = text.indexOf('\\n', offset)\n open = step(open, text.slice(offset, nl), nl + 1, text.length)\n fenceScanStats.lines += 1\n offset = nl + 1\n }\n if (offset !== prefix.length) cached = { prefix: text.slice(0, offset), open }\n // The unterminated last line is evaluated but never committed: it may still change.\n return offset < text.length || lastNewline === -1\n ? step(open, text.slice(offset), text.length + 1, text.length)\n : open\n }\n}\n\n/** The scan for a caller that follows one text at a time: `@localix-ai/flowdown/stream`'s functions. */\nexport const openFence: FenceScanner = createFenceScanner()\n"],"mappings":";;;;;;;;;;AAeA,MAAM,gBAAgB;;AAStB,MAAa,iBAAiB,EAAE,OAAO,EAAE;;AAGzC,SAAS,KACP,MACA,MACA,SACA,YACkB;CAClB,MAAM,IAAI,cAAc,KAAK,IAAI;CACjC,IAAI,CAAC,GAAG,OAAO;CACf,IAAI,SAAS,MAAM;EAEjB,IAAI,EAAE,EAAE,CAAE,OAAO,OAAO,EAAE,EAAE,CAAE,SAAS,GAAG,GAAG,OAAO;EACpD,OAAO;GAAE,QAAQ,EAAE;GAAK,WAAW,KAAK,IAAI,SAAS,UAAU;EAAE;CACnE;CAGA,OADE,EAAE,EAAE,CAAE,OAAO,KAAK,OAAO,MAAM,EAAE,EAAE,CAAE,UAAU,KAAK,OAAO,UAAU,EAAE,EAAE,CAAE,KAAK,MAAM,KACxE,OAAO;AACzB;;;;;;AAcA,SAAgB,qBAAmC;CACjD,IAAI,SAAoB;EAAE,QAAQ;EAAI,MAAM;CAAK;CAEjD,QAAQ,SAAS;EACf,IAAI,EAAE,QAAQ,SAAS;EACvB,IAAI,CAAC,UAAU,MAAM,QAAQ,OAAO,MAAM,GAAG;GAC3C,SAAS;GACT,OAAO;EACT;EACA,IAAI,SAAS,OAAO;EACpB,MAAM,cAAc,KAAK,YAAY,IAAI;EACzC,OAAO,UAAU,aAAa;GAC5B,MAAM,KAAK,KAAK,QAAQ,MAAM,MAAM;GACpC,OAAO,KAAK,MAAM,KAAK,MAAM,QAAQ,EAAE,GAAG,KAAK,GAAG,KAAK,MAAM;GAC7D,eAAe,SAAS;GACxB,SAAS,KAAK;EAChB;EACA,IAAI,WAAW,OAAO,QAAQ,SAAS;GAAE,QAAQ,KAAK,MAAM,GAAG,MAAM;GAAG;EAAK;EAE7E,OAAO,SAAS,KAAK,UAAU,gBAAgB,KAC3C,KAAK,MAAM,KAAK,MAAM,MAAM,GAAG,KAAK,SAAS,GAAG,KAAK,MAAM,IAC3D;CACN;AACF;;AAGA,MAAa,YAA0B,mBAAmB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-tree-parse.js","names":[],"sources":["../../src/core/file-tree-parse.ts"],"sourcesContent":["/**\n * Reading a ```tree fence: the directory layout models already draw with box characters, as rows\n * a renderer can lay out (`FileTree.tsx`).\n *\n * src/\n * ├── app.ts # entry point\n * └── features/\n * └── chat/\n *\n * Depth is the number of 4-column guides (`│ ` or four spaces) before a branch (`├── ` /\n * `└── `); a line with no branch is a root. A row is a FOLDER when its name ends with `/` or the\n * next row is deeper. A comment — after `#`, or after a run of two or more spaces — is set apart\n * from the name. A line the grammar does not fit is kept as a plain row at its depth, so a tree\n * still reads when a model draws it slightly off.\n */\nexport interface TreeRow {\n depth: number\n name: string\n comment: string\n folder: boolean\n /** The row's place in the tree, `src/app/App.tsx` — a stable key for focus and collapse. */\n path: string\n /** The nearest shallower row above it; null at the top level. */\n parent: number | null\n}\n\nconst GUIDE = /^(?:│ {3}|\\| {3}| {4})/\nconst BRANCH = /^(?:├──|└──|\\|--|`--|\\+--)\\s?/\n\nfunction readLine(line: string): Pick<TreeRow, 'depth' | 'name' | 'comment'> | null {\n let rest = line.
|
|
1
|
+
{"version":3,"file":"file-tree-parse.js","names":[],"sources":["../../src/core/file-tree-parse.ts"],"sourcesContent":["/**\n * Reading a ```tree fence: the directory layout models already draw with box characters, as rows\n * a renderer can lay out (`FileTree.tsx`).\n *\n * src/\n * ├── app.ts # entry point\n * └── features/\n * └── chat/\n *\n * Depth is the number of 4-column guides (`│ ` or four spaces) before a branch (`├── ` /\n * `└── `); a line with no branch is a root. A row is a FOLDER when its name ends with `/` or the\n * next row is deeper. A comment — after `#`, or after a run of two or more spaces — is set apart\n * from the name. A line the grammar does not fit is kept as a plain row at its depth, so a tree\n * still reads when a model draws it slightly off.\n */\nexport interface TreeRow {\n depth: number\n name: string\n comment: string\n folder: boolean\n /** The row's place in the tree, `src/app/App.tsx` — a stable key for focus and collapse. */\n path: string\n /** The nearest shallower row above it; null at the top level. */\n parent: number | null\n}\n\nconst GUIDE = /^(?:│ {3}|\\| {3}| {4})/\nconst BRANCH = /^(?:├──|└──|\\|--|`--|\\+--)\\s?/\n\nfunction readLine(line: string): Pick<TreeRow, 'depth' | 'name' | 'comment'> | null {\n let rest = line.trimEnd()\n if (!rest.trim()) return null\n let depth = 0\n while (GUIDE.test(rest)) {\n rest = rest.replace(GUIDE, '')\n depth++\n }\n const branch = BRANCH.exec(rest)\n if (branch) {\n rest = rest.slice(branch[0].length)\n depth++\n } else rest = rest.trimStart()\n const commentAt = rest.search(/\\s+#|\\s{2,}/)\n const name = (commentAt === -1 ? rest : rest.slice(0, commentAt)).trim()\n const comment =\n commentAt === -1\n ? ''\n : rest\n .slice(commentAt)\n .replace(/^\\s*#?\\s*/, '')\n .trim()\n return { depth, name, comment }\n}\n\nexport function parseFileTree(source: string): TreeRow[] {\n const rows = source.split('\\n').flatMap((line) => {\n const row = readLine(line)\n return row ? [row] : []\n })\n const parsed: TreeRow[] = []\n const plain: string[] = []\n rows.forEach((row, index) => {\n let parent: number | null = null\n for (let above = index - 1; above >= 0; above--)\n if (rows[above]!.depth < row.depth) {\n parent = above\n break\n }\n plain.push(`${parent === null ? '' : `${plain[parent]!.replace(/\\/$/, '')}/`}${row.name}`)\n parsed.push({\n ...row,\n folder: row.name.endsWith('/') || (rows[index + 1]?.depth ?? 0) > row.depth,\n // Two rows may share a path when a model repeats a name; the index keeps the key unique.\n path: `${plain[index]}#${index}`,\n parent,\n })\n })\n return parsed\n}\n"],"mappings":";AA0BA,MAAM,QAAQ;AACd,MAAM,SAAS;AAEf,SAAS,SAAS,MAAkE;CAClF,IAAI,OAAO,KAAK,QAAQ;CACxB,IAAI,CAAC,KAAK,KAAK,GAAG,OAAO;CACzB,IAAI,QAAQ;CACZ,OAAO,MAAM,KAAK,IAAI,GAAG;EACvB,OAAO,KAAK,QAAQ,OAAO,EAAE;EAC7B;CACF;CACA,MAAM,SAAS,OAAO,KAAK,IAAI;CAC/B,IAAI,QAAQ;EACV,OAAO,KAAK,MAAM,OAAO,EAAE,CAAC,MAAM;EAClC;CACF,OAAO,OAAO,KAAK,UAAU;CAC7B,MAAM,YAAY,KAAK,OAAO,aAAa;CAC3C,MAAM,QAAQ,cAAc,KAAK,OAAO,KAAK,MAAM,GAAG,SAAS,EAAA,CAAG,KAAK;CACvE,MAAM,UACJ,cAAc,KACV,KACA,KACG,MAAM,SAAS,CAAC,CAChB,QAAQ,aAAa,EAAE,CAAC,CACxB,KAAK;CACd,OAAO;EAAE;EAAO;EAAM;CAAQ;AAChC;AAEA,SAAgB,cAAc,QAA2B;CACvD,MAAM,OAAO,OAAO,MAAM,IAAI,CAAC,CAAC,SAAS,SAAS;EAChD,MAAM,MAAM,SAAS,IAAI;EACzB,OAAO,MAAM,CAAC,GAAG,IAAI,CAAC;CACxB,CAAC;CACD,MAAM,SAAoB,CAAC;CAC3B,MAAM,QAAkB,CAAC;CACzB,KAAK,SAAS,KAAK,UAAU;EAC3B,IAAI,SAAwB;EAC5B,KAAK,IAAI,QAAQ,QAAQ,GAAG,SAAS,GAAG,SACtC,IAAI,KAAK,MAAM,CAAE,QAAQ,IAAI,OAAO;GAClC,SAAS;GACT;EACF;EACF,MAAM,KAAK,GAAG,WAAW,OAAO,KAAK,GAAG,MAAM,OAAO,CAAE,QAAQ,OAAO,EAAE,EAAE,KAAK,IAAI,MAAM;EACzF,OAAO,KAAK;GACV,GAAG;GACH,QAAQ,IAAI,KAAK,SAAS,GAAG,MAAM,KAAK,QAAQ,EAAE,EAAE,SAAS,KAAK,IAAI;GAEtE,MAAM,GAAG,MAAM,OAAO,GAAG;GACzB;EACF,CAAC;CACH,CAAC;CACD,OAAO;AACT"}
|
package/dist/core/heal.js
CHANGED
|
@@ -176,7 +176,7 @@ function dropTrailingPartialCalloutMarker(text) {
|
|
|
176
176
|
* nothing follows it but blank space; once the list starts, the form renders.
|
|
177
177
|
*/
|
|
178
178
|
function dropTrailingBareFormMarker(text) {
|
|
179
|
-
const trimmed = text.
|
|
179
|
+
const trimmed = text.trimEnd();
|
|
180
180
|
const nl = trimmed.lastIndexOf("\n");
|
|
181
181
|
const quoted = /^\s{0,3}>[ \t]?(.*)$/.exec(trimmed.slice(nl + 1));
|
|
182
182
|
if (!quoted || !parseContainerMarker(quoted[1].trim())) return text;
|
package/dist/core/heal.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"heal.js","names":[],"sources":["../../src/core/heal.ts"],"sourcesContent":["/**\n * Streaming-tail markdown stabilization.\n *\n * While a reply streams, the accumulated markdown frequently ends mid-construct:\n * an unterminated ``` fence renders the partial code as raw text, a dangling\n * `**` or backtick shows literal markers, a half-typed `[link](url` shows raw\n * syntax — and each of these \"pops\" into its formatted form a few frames later\n * once the model closes it. `closePartialMarkdown` conservatively completes\n * (or hides) those trailing constructs so the streamed view stays visually\n * stable. It is applied ONLY while streaming; the final text renders verbatim.\n */\nimport { parseContainerMarker } from '../dialect'\nimport { inlineMathClose, isLiteralDollar } from './dollars'\nimport { openFence } from './fence-scan'\nimport { opensInlineMath } from './math-delimiters'\nimport { isEscaped } from './prose'\n\n/** Whether a stabilized fence is the one incomplete body still being written. */\nexport function isGrowingFence(growing: string | null, source: string): boolean {\n return growing !== null && growing.trimEnd() === source.trimEnd()\n}\n\n/**\n * The body of the fence a streaming text ends inside, or null when every fence is closed.\n *\n * Only the LAST fence can be unterminated, so this is how a renderer tells the one block that is\n * still being written from the finished blocks above it — a diagram above the tail is complete\n * and can be drawn now, instead of waiting for the whole reply to settle.\n */\nexport function openFenceBody(text: string): string | null {\n const fence = openFence(text)\n return fence ? text.slice(fence.bodyStart) : null\n}\n\n/** A trailing half-written image (` is hidden — a\n * broken-image flash is worse than a beat of nothing. */\nfunction dropTrailingPartialImage(tail: string): string {\n return tail.replace(/!\\[[^\\]]*(\\]\\([^)]*)?$/, '')\n}\n\n/** A trailing `[text](partial-url` gets its `)` so the anchor text renders as a\n * link immediately instead of flashing raw syntax until the URL completes. */\nfunction closeTrailingLink(tail: string): string {\n return /(^|[^!])\\[[^\\]]*\\]\\([^)]*$/.test(tail) ? `${tail})` : tail\n}\n\n/**\n * Code spans and CLOSED formulas blanked to spaces, positions kept. Emphasis has no meaning\n * inside either, and the balancers below would otherwise read `$a*b$` or `$x_i$` as an open\n * emphasis and append a stray `*` or `_` to the streaming tail.\n */\nfunction maskInert(tail: string): string {\n const blank = (span: string) => ' '.repeat(span.length)\n return tail.replace(/`[^`]*`/g, blank).replace(/\\$\\$[\\s\\S]*?\\$\\$|\\$[^$\\n]+\\$/g, blank)\n}\n\n/**\n * A formula still being typed is withheld from its opening `$` until the closing one arrives.\n * Math is typeset while a reply streams, so a half-typed `$\\frac{a}{` would otherwise print as\n * raw LaTeX and then jump into a formula. Dollars are paired by the renderer's own rule\n * (`literalDollars`, `math-delimiters.ts`), so the two never disagree about one `$`: a price, a\n * shell variable or a `$` with a space after it (`5$ and`) opens nothing and hides nothing.\n *\n * Only the first `$` left open is withheld, and only while no `$` follows it: a `$` that came and\n * did not close it (the shell line `$(seq 1 12); do printf \"%s\" $i`) shows these dollars are text.\n */\nfunction withholdPartialMath(tail: string): string {\n const code = tail.replace(/`[^`]*`/g, (span) => ' '.repeat(span.length))\n const displays: number[] = []\n for (let index = code.indexOf('$$'); index !== -1; index = code.indexOf('$$', index + 2)) {\n if (!isEscaped(code, index)) displays.push(index)\n }\n if (displays.length % 2 === 1) return tail.slice(0, displays.at(-1)).trimEnd()\n\n const inline = code.replace(/\\$\\$[\\s\\S]*?\\$\\$/g, (span) => ' '.repeat(span.length))\n for (let index = inline.indexOf('$'); index !== -1; index = inline.indexOf('$', index + 1)) {\n if (isEscaped(inline, index) || isLiteralDollar(inline, index)) continue\n // The last character: the one after it, which says whether it opens, is not written yet.\n const last = index === inline.length - 1\n if (!last && !opensInlineMath(inline, index)) continue\n const close = last ? -1 : inlineMathClose(inline, index)\n if (close !== -1) {\n index = close\n continue\n }\n return inline.includes('$', index + 1) ? tail : tail.slice(0, index).trimEnd()\n }\n return tail\n}\n\n/** Balance inline code: an odd number of backticks means an open span. A bare\n * trailing opener is stripped (closing it would render an empty span as\n * literal ``` `` ```); an opener with content gets closed. */\nfunction balanceInlineCode(tail: string): string {\n const count = (tail.replace(/\\\\`/g, '').match(/`/g) ?? []).length\n if (count % 2 === 0) return tail\n return tail.endsWith('`') ? tail.slice(0, -1) : `${tail}\\``\n}\n\n/** Balance a two-char emphasis delimiter (`**` / `~~`). Occurrences inside\n * inline code spans are ignored. A bare trailing opener is stripped (closing\n * an empty pair would render four literal characters); one with content after\n * it gets closed. */\nfunction balanceDoubleDelimiter(tail: string, delim: string): string {\n const stripped = maskInert(tail)\n let count = 0\n for (let i = 0; (i = stripped.indexOf(delim, i)) !== -1; i += delim.length) count += 1\n if (count % 2 === 0) return tail\n return tail.endsWith(delim) ? tail.slice(0, -delim.length) : `${tail}${delim}`\n}\n\nconst WORD_RE = /\\w/\n\n/**\n * Balance a single-char emphasis delimiter (`*` or `_`) — the most common\n * mid-stream flash after `**`: `*italic` shows the literal `*` until the model\n * closes it, then pops into italics. We append the missing closer so it renders\n * as emphasis immediately.\n *\n * The danger is false positives — a wrongly appended `*`/`_` italicises text the\n * author never marked up. So we count ONLY \"emphasis-capable\" lone delimiters:\n * - runs of length ≥ 2 are skipped (they belong to `**`/`__`/`~~` handling);\n * - inside inline code is masked out;\n * - `*` counts only when it touches a word char on one side — excludes globs\n * (`*.ts`), multiplication (`a * b`) and bullets (`* item`), where the `*` is\n * flanked by whitespace/punctuation;\n * - `_` additionally requires the word char to be on the OPENING side only —\n * so intraword underscores (`file_name`, `__init__`, `snake_case`) never\n * count, matching CommonMark's \"no intraword `_` emphasis\" rule.\n * An odd active count ⇒ a trailing opener is dangling: append one closer (or\n * strip a bare trailing opener, mirroring the double-delimiter rule).\n */\nfunction balanceSingleEmphasis(tail: string, char: '*' | '_'): string {\n const masked = maskInert(tail)\n let active = 0\n let i = 0\n while (i < masked.length) {\n if (masked[i] !== char) {\n i += 1\n continue\n }\n let j = i\n while (j < masked.length && masked[j] === char) j += 1\n if (j - i === 1) {\n const prevWord = WORD_RE.test(masked[i - 1] ?? '')\n const nextWord = WORD_RE.test(masked[j] ?? '')\n const counts =\n char === '*' ? prevWord || nextWord : (!prevWord && nextWord) || (prevWord && !nextWord)\n if (counts) active += 1\n }\n i = j\n }\n if (active % 2 === 0) return tail\n return tail.endsWith(char) ? tail.slice(0, -1) : `${tail}${char}`\n}\n\nconst ROW_START = /^\\s{0,3}\\|/\n/** The row of dashes under a table's header: `|---|:--:|`, its outer pipes optional. */\nconst DELIMITER_ROW = /^\\s*\\|?\\s*:?-{3,}:?\\s*(?:\\|\\s*:?-{3,}:?\\s*)*\\|?\\s*$/\n\n/** Hide only a partial row of an already established GFM table. A line starting\n * with `|` outside a table may be ordinary prose and must remain visible. */\nfunction dropTrailingPartialTableRow(text: string): string {\n if (text.endsWith('\\n')) return text\n const nl = text.lastIndexOf('\\n')\n if (nl === -1 || !ROW_START.test(text.slice(nl + 1))) return text\n\n // Up the lines above, one at a time: the rows of the table, to the row of dashes under its\n // header. The reply is not cut into its lines; a long one would be on every token of a row.\n for (let end = nl; end > 0;) {\n const start = text.lastIndexOf('\\n', end - 1) + 1\n // The first line of the text has no header above it.\n if (start === 0) break\n const line = text.slice(start, end)\n if (DELIMITER_ROW.test(line)) {\n const above = start === 1 ? 0 : text.lastIndexOf('\\n', start - 2) + 1\n return ROW_START.test(text.slice(above, start - 1)) ? text.slice(0, nl) : text\n }\n if (!ROW_START.test(line)) break\n end = start - 1\n }\n return text\n}\n\n/**\n * A quote line that is still typing its callout marker (`> [`, `> [!`, `> [!WARN`) is hidden\n * until the `]` arrives: it would otherwise print as a grey quote reading \"[!WARN\" and a beat\n * later turn into a note. A complete marker is kept — it already renders as the empty note.\n */\nfunction dropTrailingPartialCalloutMarker(text: string): string {\n const nl = text.lastIndexOf('\\n')\n const lastLine = text.slice(nl + 1)\n if (!/^\\s{0,3}>[ \\t]?\\[(?:!\\w*)?$/.test(lastLine)) return text\n return nl === -1 ? '' : text.slice(0, nl)\n}\n\n/**\n * A form's marker that arrived before its content (`> [!STEPS]` and nothing yet) is hidden: alone,\n * it is a quote reading \"[!STEPS]\", and the steps replace it a token later. Hidden only while\n * nothing follows it but blank space; once the list starts, the form renders.\n */\nfunction dropTrailingBareFormMarker(text: string): string {\n const trimmed = text.replace(/\\s+$/, '')\n const nl = trimmed.lastIndexOf('\\n')\n const quoted = /^\\s{0,3}>[ \\t]?(.*)$/.exec(trimmed.slice(nl + 1))\n if (!quoted || !parseContainerMarker(quoted[1]!.trim())) return text\n return nl === -1 ? '' : trimmed.slice(0, nl + 1)\n}\n\nexport function closePartialMarkdown(text: string): string {\n if (!text) return text\n\n // Inside an unterminated fence: close it and skip inline fixes — everything\n // after the opener is code, where emphasis/link syntax has no meaning.\n const fence = openFence(text)\n if (fence) return `${text}${text.endsWith('\\n') ? '' : '\\n'}${fence.marker}`\n return closePartialProse(text)\n}\n\n/**\n * `closePartialMarkdown` for a text known to end outside a fenced code block: the renderer asks\n * its own scanner about the fence, and comes here with the answer.\n */\nexport function closePartialProse(text: string): string {\n if (!text) return text\n\n // A half-typed table row is its own kind of flash — hide it. The active edit\n // is that row, so there's nothing else to stabilize this frame.\n const detabled = dropTrailingPartialTableRow(text)\n if (detabled !== text) return detabled\n // Same for a callout marker still being typed on its quote line.\n const unmarked = dropTrailingPartialCalloutMarker(text)\n if (unmarked !== text) return unmarked\n const bare = dropTrailingBareFormMarker(text)\n if (bare !== text) return bare\n\n // Inline constructs cannot span a blank line, so only the last paragraph can\n // be dangling — operate on it alone and keep the rest byte-identical.\n const splitAt = text.lastIndexOf('\\n\\n')\n const head = splitAt === -1 ? '' : text.slice(0, splitAt + 2)\n let tail = splitAt === -1 ? text : text.slice(splitAt + 2)\n if (!tail) return text\n\n tail = dropTrailingPartialImage(tail)\n tail = withholdPartialMath(tail)\n tail = closeTrailingLink(tail)\n tail = balanceInlineCode(tail)\n tail = balanceDoubleDelimiter(tail, '**')\n tail = balanceDoubleDelimiter(tail, '~~')\n tail = balanceSingleEmphasis(tail, '*')\n tail = balanceSingleEmphasis(tail, '_')\n // Nothing to complete: the very text, so a long reply is not copied on every token.\n if (head.length + tail.length === text.length && text.endsWith(tail)) return text\n return head + tail\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"heal.js","names":[],"sources":["../../src/core/heal.ts"],"sourcesContent":["/**\n * Streaming-tail markdown stabilization.\n *\n * While a reply streams, the accumulated markdown frequently ends mid-construct:\n * an unterminated ``` fence renders the partial code as raw text, a dangling\n * `**` or backtick shows literal markers, a half-typed `[link](url` shows raw\n * syntax — and each of these \"pops\" into its formatted form a few frames later\n * once the model closes it. `closePartialMarkdown` conservatively completes\n * (or hides) those trailing constructs so the streamed view stays visually\n * stable. It is applied ONLY while streaming; the final text renders verbatim.\n */\nimport { parseContainerMarker } from '../dialect'\nimport { inlineMathClose, isLiteralDollar } from './dollars'\nimport { openFence } from './fence-scan'\nimport { opensInlineMath } from './math-delimiters'\nimport { isEscaped } from './prose'\n\n/** Whether a stabilized fence is the one incomplete body still being written. */\nexport function isGrowingFence(growing: string | null, source: string): boolean {\n return growing !== null && growing.trimEnd() === source.trimEnd()\n}\n\n/**\n * The body of the fence a streaming text ends inside, or null when every fence is closed.\n *\n * Only the LAST fence can be unterminated, so this is how a renderer tells the one block that is\n * still being written from the finished blocks above it — a diagram above the tail is complete\n * and can be drawn now, instead of waiting for the whole reply to settle.\n */\nexport function openFenceBody(text: string): string | null {\n const fence = openFence(text)\n return fence ? text.slice(fence.bodyStart) : null\n}\n\n/** A trailing half-written image (` is hidden — a\n * broken-image flash is worse than a beat of nothing. */\nfunction dropTrailingPartialImage(tail: string): string {\n return tail.replace(/!\\[[^\\]]*(\\]\\([^)]*)?$/, '')\n}\n\n/** A trailing `[text](partial-url` gets its `)` so the anchor text renders as a\n * link immediately instead of flashing raw syntax until the URL completes. */\nfunction closeTrailingLink(tail: string): string {\n return /(^|[^!])\\[[^\\]]*\\]\\([^)]*$/.test(tail) ? `${tail})` : tail\n}\n\n/**\n * Code spans and CLOSED formulas blanked to spaces, positions kept. Emphasis has no meaning\n * inside either, and the balancers below would otherwise read `$a*b$` or `$x_i$` as an open\n * emphasis and append a stray `*` or `_` to the streaming tail.\n */\nfunction maskInert(tail: string): string {\n const blank = (span: string) => ' '.repeat(span.length)\n return tail.replace(/`[^`]*`/g, blank).replace(/\\$\\$[\\s\\S]*?\\$\\$|\\$[^$\\n]+\\$/g, blank)\n}\n\n/**\n * A formula still being typed is withheld from its opening `$` until the closing one arrives.\n * Math is typeset while a reply streams, so a half-typed `$\\frac{a}{` would otherwise print as\n * raw LaTeX and then jump into a formula. Dollars are paired by the renderer's own rule\n * (`literalDollars`, `math-delimiters.ts`), so the two never disagree about one `$`: a price, a\n * shell variable or a `$` with a space after it (`5$ and`) opens nothing and hides nothing.\n *\n * Only the first `$` left open is withheld, and only while no `$` follows it: a `$` that came and\n * did not close it (the shell line `$(seq 1 12); do printf \"%s\" $i`) shows these dollars are text.\n */\nfunction withholdPartialMath(tail: string): string {\n const code = tail.replace(/`[^`]*`/g, (span) => ' '.repeat(span.length))\n const displays: number[] = []\n for (let index = code.indexOf('$$'); index !== -1; index = code.indexOf('$$', index + 2)) {\n if (!isEscaped(code, index)) displays.push(index)\n }\n if (displays.length % 2 === 1) return tail.slice(0, displays.at(-1)).trimEnd()\n\n const inline = code.replace(/\\$\\$[\\s\\S]*?\\$\\$/g, (span) => ' '.repeat(span.length))\n for (let index = inline.indexOf('$'); index !== -1; index = inline.indexOf('$', index + 1)) {\n if (isEscaped(inline, index) || isLiteralDollar(inline, index)) continue\n // The last character: the one after it, which says whether it opens, is not written yet.\n const last = index === inline.length - 1\n if (!last && !opensInlineMath(inline, index)) continue\n const close = last ? -1 : inlineMathClose(inline, index)\n if (close !== -1) {\n index = close\n continue\n }\n return inline.includes('$', index + 1) ? tail : tail.slice(0, index).trimEnd()\n }\n return tail\n}\n\n/** Balance inline code: an odd number of backticks means an open span. A bare\n * trailing opener is stripped (closing it would render an empty span as\n * literal ``` `` ```); an opener with content gets closed. */\nfunction balanceInlineCode(tail: string): string {\n const count = (tail.replace(/\\\\`/g, '').match(/`/g) ?? []).length\n if (count % 2 === 0) return tail\n return tail.endsWith('`') ? tail.slice(0, -1) : `${tail}\\``\n}\n\n/** Balance a two-char emphasis delimiter (`**` / `~~`). Occurrences inside\n * inline code spans are ignored. A bare trailing opener is stripped (closing\n * an empty pair would render four literal characters); one with content after\n * it gets closed. */\nfunction balanceDoubleDelimiter(tail: string, delim: string): string {\n const stripped = maskInert(tail)\n let count = 0\n for (let i = 0; (i = stripped.indexOf(delim, i)) !== -1; i += delim.length) count += 1\n if (count % 2 === 0) return tail\n return tail.endsWith(delim) ? tail.slice(0, -delim.length) : `${tail}${delim}`\n}\n\nconst WORD_RE = /\\w/\n\n/**\n * Balance a single-char emphasis delimiter (`*` or `_`) — the most common\n * mid-stream flash after `**`: `*italic` shows the literal `*` until the model\n * closes it, then pops into italics. We append the missing closer so it renders\n * as emphasis immediately.\n *\n * The danger is false positives — a wrongly appended `*`/`_` italicises text the\n * author never marked up. So we count ONLY \"emphasis-capable\" lone delimiters:\n * - runs of length ≥ 2 are skipped (they belong to `**`/`__`/`~~` handling);\n * - inside inline code is masked out;\n * - `*` counts only when it touches a word char on one side — excludes globs\n * (`*.ts`), multiplication (`a * b`) and bullets (`* item`), where the `*` is\n * flanked by whitespace/punctuation;\n * - `_` additionally requires the word char to be on the OPENING side only —\n * so intraword underscores (`file_name`, `__init__`, `snake_case`) never\n * count, matching CommonMark's \"no intraword `_` emphasis\" rule.\n * An odd active count ⇒ a trailing opener is dangling: append one closer (or\n * strip a bare trailing opener, mirroring the double-delimiter rule).\n */\nfunction balanceSingleEmphasis(tail: string, char: '*' | '_'): string {\n const masked = maskInert(tail)\n let active = 0\n let i = 0\n while (i < masked.length) {\n if (masked[i] !== char) {\n i += 1\n continue\n }\n let j = i\n while (j < masked.length && masked[j] === char) j += 1\n if (j - i === 1) {\n const prevWord = WORD_RE.test(masked[i - 1] ?? '')\n const nextWord = WORD_RE.test(masked[j] ?? '')\n const counts =\n char === '*' ? prevWord || nextWord : (!prevWord && nextWord) || (prevWord && !nextWord)\n if (counts) active += 1\n }\n i = j\n }\n if (active % 2 === 0) return tail\n return tail.endsWith(char) ? tail.slice(0, -1) : `${tail}${char}`\n}\n\nconst ROW_START = /^\\s{0,3}\\|/\n/** The row of dashes under a table's header: `|---|:--:|`, its outer pipes optional. */\nconst DELIMITER_ROW = /^\\s*\\|?\\s*:?-{3,}:?\\s*(?:\\|\\s*:?-{3,}:?\\s*)*\\|?\\s*$/\n\n/** Hide only a partial row of an already established GFM table. A line starting\n * with `|` outside a table may be ordinary prose and must remain visible. */\nfunction dropTrailingPartialTableRow(text: string): string {\n if (text.endsWith('\\n')) return text\n const nl = text.lastIndexOf('\\n')\n if (nl === -1 || !ROW_START.test(text.slice(nl + 1))) return text\n\n // Up the lines above, one at a time: the rows of the table, to the row of dashes under its\n // header. The reply is not cut into its lines; a long one would be on every token of a row.\n for (let end = nl; end > 0;) {\n const start = text.lastIndexOf('\\n', end - 1) + 1\n // The first line of the text has no header above it.\n if (start === 0) break\n const line = text.slice(start, end)\n if (DELIMITER_ROW.test(line)) {\n const above = start === 1 ? 0 : text.lastIndexOf('\\n', start - 2) + 1\n return ROW_START.test(text.slice(above, start - 1)) ? text.slice(0, nl) : text\n }\n if (!ROW_START.test(line)) break\n end = start - 1\n }\n return text\n}\n\n/**\n * A quote line that is still typing its callout marker (`> [`, `> [!`, `> [!WARN`) is hidden\n * until the `]` arrives: it would otherwise print as a grey quote reading \"[!WARN\" and a beat\n * later turn into a note. A complete marker is kept — it already renders as the empty note.\n */\nfunction dropTrailingPartialCalloutMarker(text: string): string {\n const nl = text.lastIndexOf('\\n')\n const lastLine = text.slice(nl + 1)\n if (!/^\\s{0,3}>[ \\t]?\\[(?:!\\w*)?$/.test(lastLine)) return text\n return nl === -1 ? '' : text.slice(0, nl)\n}\n\n/**\n * A form's marker that arrived before its content (`> [!STEPS]` and nothing yet) is hidden: alone,\n * it is a quote reading \"[!STEPS]\", and the steps replace it a token later. Hidden only while\n * nothing follows it but blank space; once the list starts, the form renders.\n */\nfunction dropTrailingBareFormMarker(text: string): string {\n // `trimEnd`, not `/\\s+$/`: the pattern is tried at every run of blank space in the text.\n const trimmed = text.trimEnd()\n const nl = trimmed.lastIndexOf('\\n')\n const quoted = /^\\s{0,3}>[ \\t]?(.*)$/.exec(trimmed.slice(nl + 1))\n if (!quoted || !parseContainerMarker(quoted[1]!.trim())) return text\n return nl === -1 ? '' : trimmed.slice(0, nl + 1)\n}\n\nexport function closePartialMarkdown(text: string): string {\n if (!text) return text\n\n // Inside an unterminated fence: close it and skip inline fixes — everything\n // after the opener is code, where emphasis/link syntax has no meaning.\n const fence = openFence(text)\n if (fence) return `${text}${text.endsWith('\\n') ? '' : '\\n'}${fence.marker}`\n return closePartialProse(text)\n}\n\n/**\n * `closePartialMarkdown` for a text known to end outside a fenced code block: the renderer asks\n * its own scanner about the fence, and comes here with the answer.\n */\nexport function closePartialProse(text: string): string {\n if (!text) return text\n\n // A half-typed table row is its own kind of flash — hide it. The active edit\n // is that row, so there's nothing else to stabilize this frame.\n const detabled = dropTrailingPartialTableRow(text)\n if (detabled !== text) return detabled\n // Same for a callout marker still being typed on its quote line.\n const unmarked = dropTrailingPartialCalloutMarker(text)\n if (unmarked !== text) return unmarked\n const bare = dropTrailingBareFormMarker(text)\n if (bare !== text) return bare\n\n // Inline constructs cannot span a blank line, so only the last paragraph can\n // be dangling — operate on it alone and keep the rest byte-identical.\n const splitAt = text.lastIndexOf('\\n\\n')\n const head = splitAt === -1 ? '' : text.slice(0, splitAt + 2)\n let tail = splitAt === -1 ? text : text.slice(splitAt + 2)\n if (!tail) return text\n\n tail = dropTrailingPartialImage(tail)\n tail = withholdPartialMath(tail)\n tail = closeTrailingLink(tail)\n tail = balanceInlineCode(tail)\n tail = balanceDoubleDelimiter(tail, '**')\n tail = balanceDoubleDelimiter(tail, '~~')\n tail = balanceSingleEmphasis(tail, '*')\n tail = balanceSingleEmphasis(tail, '_')\n // Nothing to complete: the very text, so a long reply is not copied on every token.\n if (head.length + tail.length === text.length && text.endsWith(tail)) return text\n return head + tail\n}\n"],"mappings":"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"}
|
package/dist/document/fence.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { fenceTitle } from "../dialect.js";
|
|
2
|
+
import { continues } from "../core/continues.js";
|
|
2
3
|
// src/document/fence.ts
|
|
3
4
|
/**
|
|
4
5
|
* A block that is one code fence still being written, read without parsing the block: what a
|
|
@@ -41,7 +42,7 @@ function growingFence(source, closer = "", memory = shared) {
|
|
|
41
42
|
else if (markers[0] !== marker || markers.length < fence.length) body = unclosed(rest + closer, marker, fence.length);
|
|
42
43
|
else body = closer !== markers || !rest ? rest : rest.slice(0, -1);
|
|
43
44
|
const read = memory.read;
|
|
44
|
-
let at = read !== null && read.fence === fence && body
|
|
45
|
+
let at = read !== null && read.fence === fence && continues(body, read.body, read.clean) ? read.clean : 0;
|
|
45
46
|
memory.read = null;
|
|
46
47
|
for (let end = body.indexOf("\n", at); end !== -1; end = body.indexOf("\n", at)) {
|
|
47
48
|
if (closesFence(body, at, end, marker, fence.length, 3)) return null;
|
|
@@ -74,7 +75,7 @@ function createCutter() {
|
|
|
74
75
|
let run = 0;
|
|
75
76
|
return (body) => {
|
|
76
77
|
const done = ends.at(-1) ?? 0;
|
|
77
|
-
if (done > 0 && !(body
|
|
78
|
+
if (done > 0 && !continues(body, text, done)) {
|
|
78
79
|
ends = [];
|
|
79
80
|
run += 1;
|
|
80
81
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fence.js","names":[],"sources":["../../src/document/fence.ts"],"sourcesContent":["/**\n * A block that is one code fence still being written, read without parsing the block: what a\n * renderer needs to draw a long fence in pieces, a token costing only its last lines.\n */\nimport { fenceTitle } from '../dialect'\n\nconst OPENING = /^(`{3,}|~{3,})([^\\n`]*)\\n/\n\n/** Whether the line of `text` from `from` to `to` closes a fence: `length` markers or more, alone. */\nfunction closesFence(\n text: string,\n from: number,\n to: number,\n marker: string,\n length: number,\n indent: number,\n): boolean {\n let at = from\n while (at < to && at - from < indent && text[at] === ' ') at += 1\n const run = at\n while (at < to && text[at] === marker) at += 1\n if (at - run < length) return false\n while (at < to && (text[at] === ' ' || text[at] === '\\t')) at += 1\n return at === to\n}\n\n/**\n * The fence read last time: its body up to `clean` is complete lines, none of which closes it. A\n * fence that grows is read once per token, and the next read starts where this one stopped.\n */\nexport interface FenceMemory {\n read: { fence: string; body: string; clean: number } | null\n}\n\n/** A place for it. A block that draws a growing fence keeps its own; this one is the default. */\nexport const createFenceMemory = (): FenceMemory => ({ read: null })\n\nconst shared = createFenceMemory()\n\n/** `rest` without its last line, when that line closes the fence: the healer's is not code. */\nfunction unclosed(rest: string, marker: string, length: number): string {\n const closing = rest.lastIndexOf('\\n')\n return closesFence(rest, closing + 1, rest.length, marker, length, 0)\n ? rest.slice(0, Math.max(closing, 0))\n : rest\n}\n\n/** An open fence: its language, the file it names, and the code written so far. */\nexport interface GrowingFence {\n language: string | undefined\n title: string\n body: string\n}\n\n/**\n * The language and code of a block that is one open fence, closed by the healer; else null.\n * `closer` is the healer's line when it is kept beside `source` (a newline, unless the source ends\n * in one, and the markers): the block is read as the two joined, without joining them.\n */\nexport function growingFence(\n source: string,\n closer = '',\n memory: FenceMemory = shared,\n): GrowingFence | null {\n // The opening line is still being written: short, and read joined.\n if (closer && !source.includes('\\n')) return growingFence(source + closer, '', memory)\n const opening = OPENING.exec(source)\n if (!opening) return null\n const fence = opening[1] ?? '```'\n const marker = fence[0] ?? '`'\n const rest = source.slice(opening[0].length)\n const markers = closer.trimStart()\n let body: string\n if (!closer) body = unclosed(rest, marker, fence.length)\n else if (markers[0] !== marker || markers.length < fence.length)\n // Not a line that closes this fence: it is read with the text, as any other line.\n body = unclosed(rest + closer, marker, fence.length)\n // It closes the fence: the code is what is above it, without the newline that ends that.\n else body = closer !== markers || !rest ? rest : rest.slice(0, -1)\n // A fence closed earlier in the block means the block is not just this fence.\n const read = memory.read\n let at =\n read !== null
|
|
1
|
+
{"version":3,"file":"fence.js","names":[],"sources":["../../src/document/fence.ts"],"sourcesContent":["/**\n * A block that is one code fence still being written, read without parsing the block: what a\n * renderer needs to draw a long fence in pieces, a token costing only its last lines.\n */\nimport { fenceTitle } from '../dialect'\nimport { continues } from '../core/continues'\n\nconst OPENING = /^(`{3,}|~{3,})([^\\n`]*)\\n/\n\n/** Whether the line of `text` from `from` to `to` closes a fence: `length` markers or more, alone. */\nfunction closesFence(\n text: string,\n from: number,\n to: number,\n marker: string,\n length: number,\n indent: number,\n): boolean {\n let at = from\n while (at < to && at - from < indent && text[at] === ' ') at += 1\n const run = at\n while (at < to && text[at] === marker) at += 1\n if (at - run < length) return false\n while (at < to && (text[at] === ' ' || text[at] === '\\t')) at += 1\n return at === to\n}\n\n/**\n * The fence read last time: its body up to `clean` is complete lines, none of which closes it. A\n * fence that grows is read once per token, and the next read starts where this one stopped.\n */\nexport interface FenceMemory {\n read: { fence: string; body: string; clean: number } | null\n}\n\n/** A place for it. A block that draws a growing fence keeps its own; this one is the default. */\nexport const createFenceMemory = (): FenceMemory => ({ read: null })\n\nconst shared = createFenceMemory()\n\n/** `rest` without its last line, when that line closes the fence: the healer's is not code. */\nfunction unclosed(rest: string, marker: string, length: number): string {\n const closing = rest.lastIndexOf('\\n')\n return closesFence(rest, closing + 1, rest.length, marker, length, 0)\n ? rest.slice(0, Math.max(closing, 0))\n : rest\n}\n\n/** An open fence: its language, the file it names, and the code written so far. */\nexport interface GrowingFence {\n language: string | undefined\n title: string\n body: string\n}\n\n/**\n * The language and code of a block that is one open fence, closed by the healer; else null.\n * `closer` is the healer's line when it is kept beside `source` (a newline, unless the source ends\n * in one, and the markers): the block is read as the two joined, without joining them.\n */\nexport function growingFence(\n source: string,\n closer = '',\n memory: FenceMemory = shared,\n): GrowingFence | null {\n // The opening line is still being written: short, and read joined.\n if (closer && !source.includes('\\n')) return growingFence(source + closer, '', memory)\n const opening = OPENING.exec(source)\n if (!opening) return null\n const fence = opening[1] ?? '```'\n const marker = fence[0] ?? '`'\n const rest = source.slice(opening[0].length)\n const markers = closer.trimStart()\n let body: string\n if (!closer) body = unclosed(rest, marker, fence.length)\n else if (markers[0] !== marker || markers.length < fence.length)\n // Not a line that closes this fence: it is read with the text, as any other line.\n body = unclosed(rest + closer, marker, fence.length)\n // It closes the fence: the code is what is above it, without the newline that ends that.\n else body = closer !== markers || !rest ? rest : rest.slice(0, -1)\n // A fence closed earlier in the block means the block is not just this fence.\n const read = memory.read\n let at =\n read !== null && read.fence === fence && continues(body, read.body, read.clean) ? read.clean : 0\n memory.read = null\n for (let end = body.indexOf('\\n', at); end !== -1; end = body.indexOf('\\n', at)) {\n if (closesFence(body, at, end, marker, fence.length, 3)) return null\n at = end + 1\n }\n // The last line is still being written: read, but not remembered.\n if (closesFence(body, at, body.length, marker, fence.length, 3)) return null\n memory.read = { fence, body, clean: at }\n const info = opening[2]?.trim() ?? ''\n const language = info.split(/\\s+/)[0] || undefined\n return { language, title: fenceTitle(info.slice(language?.length ?? 0)), body }\n}\n\n/** How many lines a piece of a growing fence holds. */\nconst LINES = 40\n\n/** The pieces of a body: their text, each ending in its newline but the last. */\nexport interface Cut {\n parts: string[]\n /**\n * Changes when the body is not the last one grown: pieces of the same run that are complete\n * are the same text, and are not compared.\n */\n run: number\n}\n\n/**\n * Cuts a body that grows at its end into its pieces. It remembers where the complete pieces end,\n * so a token costs the last piece and not a count of every line above it.\n */\nexport function createCutter(): (body: string) => Cut {\n let text = ''\n /** Where each complete piece ends in `text`. */\n let ends: number[] = []\n let run = 0\n return (body) => {\n const done = ends.at(-1) ?? 0\n if (done > 0 && !continues(body, text, done)) {\n ends = []\n run += 1\n }\n let start = ends.at(-1) ?? 0\n let line = 0\n for (let at = body.indexOf('\\n', start); at !== -1; at = body.indexOf('\\n', at + 1)) {\n line += 1\n if (line % LINES === 0) {\n start = at + 1\n ends.push(start)\n }\n }\n text = body\n // Cut from this body each time, so no piece keeps an earlier body alive.\n const parts = ends.map((end, index) => body.slice(ends[index - 1] ?? 0, end))\n parts.push(body.slice(start))\n return { parts, run }\n }\n}\n"],"mappings":"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"}
|
package/dist/dom/index.js
CHANGED
|
@@ -23,10 +23,16 @@ import { defaultUrlTransform } from "../engine/url.js";
|
|
|
23
23
|
*/
|
|
24
24
|
/** Documents mounted so far: each gets its own ids for its tabs. */
|
|
25
25
|
let mounts = 0;
|
|
26
|
-
/**
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
/**
|
|
27
|
+
* A table's box, or a code block's `<pre>`, scrolls sideways when what it holds is wider: then the
|
|
28
|
+
* keyboard must reach it.
|
|
29
|
+
*/
|
|
30
|
+
function measure(box, labels) {
|
|
31
|
+
const scrolls = box.scrollWidth > box.clientWidth + 1;
|
|
32
|
+
if (scrolls === box.hasAttribute("tabindex")) return;
|
|
33
|
+
const label = box.localName === "pre" ? labels.scrollableCode : labels.scrollableTable;
|
|
34
|
+
if (scrolls) {
|
|
35
|
+
box.setAttribute("role", "group");
|
|
30
36
|
box.setAttribute("aria-label", label);
|
|
31
37
|
box.setAttribute("tabindex", "0");
|
|
32
38
|
} else {
|
|
@@ -48,10 +54,12 @@ function mount(target, options = {}) {
|
|
|
48
54
|
/** The table boxes on the page, and the ones made since the last drawing. */
|
|
49
55
|
const boxes = /* @__PURE__ */ new WeakSet();
|
|
50
56
|
let fresh = [];
|
|
57
|
+
/** Whether a measuring of the last code block is already waiting for the next frame. */
|
|
58
|
+
let asked = false;
|
|
51
59
|
const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver((entries) => {
|
|
52
60
|
for (const entry of entries) {
|
|
53
|
-
const box = entry.target.closest(".fd-table");
|
|
54
|
-
if (box) measure(box, labels
|
|
61
|
+
const box = entry.target.closest(".fd-table, pre");
|
|
62
|
+
if (box) measure(box, labels);
|
|
55
63
|
}
|
|
56
64
|
});
|
|
57
65
|
const view = createView({
|
|
@@ -90,7 +98,7 @@ function mount(target, options = {}) {
|
|
|
90
98
|
drawing = false;
|
|
91
99
|
}
|
|
92
100
|
for (const box of fresh) {
|
|
93
|
-
measure(box, labels
|
|
101
|
+
measure(box, labels);
|
|
94
102
|
if (observer) {
|
|
95
103
|
observer.observe(box);
|
|
96
104
|
const table = box.querySelector("table");
|
|
@@ -98,6 +106,16 @@ function mount(target, options = {}) {
|
|
|
98
106
|
}
|
|
99
107
|
}
|
|
100
108
|
fresh = [];
|
|
109
|
+
if (!asked && root !== null && typeof requestAnimationFrame === "function") {
|
|
110
|
+
asked = true;
|
|
111
|
+
requestAnimationFrame(() => {
|
|
112
|
+
asked = false;
|
|
113
|
+
if (gone || root === null) return;
|
|
114
|
+
const blocks = root.getElementsByTagName("pre");
|
|
115
|
+
const last = blocks[blocks.length - 1];
|
|
116
|
+
if (last && boxes.has(last)) measure(last, labels);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
101
119
|
}
|
|
102
120
|
draw();
|
|
103
121
|
return {
|
package/dist/dom/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/dom/index.ts"],"sourcesContent":["/**\n * `@localix-ai/flowdown/dom`: Markdown on a page with no framework.\n *\n * import { mount } from '@localix-ai/flowdown/dom'\n * import '@localix-ai/flowdown/styles.css'\n *\n * const reply = mount(document.querySelector('#reply'))\n * for await (const text of stream) reply.update(text, { streaming: true })\n * reply.update(text)\n *\n * The elements are the ones `<Markdown>` draws, and the page is kept up to date the same way: a\n * block that did not change is not touched, so a token costs the last block. Tabs switch, a file\n * tree folds, a code block copies and saves, a wide table scrolls: all with the keyboard too.\n *\n * It is also what a binding for a framework is made of: mount into the element the framework\n * gives you, call `update` when the text changes, `destroy` when the element goes.\n */\nimport { codeFileName, copyText, downloadFile } from '../browser/file-actions'\nimport { mergeLabels } from '../render/labels'\nimport { createView, type ViewOptions } from '../render/view'\nimport type { DrawnElement } from '../render/vnode'\nimport { create, patch } from './patch'\n\nexport type { ViewComponents } from '../render/view'\nexport type { VComponent, VElement, VNode } from '../render/vnode'\nexport { DEFAULT_LABELS, type LabelOverrides, type MarkdownLabels } from '../render/labels'\nexport type { Highlighter } from '../render/views'\nexport { defaultUrlTransform, type UrlTransform } from '../engine/url'\nexport type { Extension, Node } from '../engine/types'\n\nexport type MountOptions = Omit<ViewOptions, 'host' | 'idPrefix'>\n\nexport interface UpdateOptions {\n /** The text is a reply still arriving: its unfinished end is completed, and a caret follows it. */\n streaming?: boolean\n}\n\nexport interface Mounted {\n /** The document's root: `<div class=\"flowdown\">`, the last child of what it was mounted in. */\n readonly element: HTMLElement\n /** Shows `markdown`. Call it on every token: what did not change is not drawn again. */\n update(markdown: string, options?: UpdateOptions): void\n /** Takes the document off the page and stops everything it started. */\n destroy(): void\n}\n\n/** Documents mounted so far: each gets its own ids for its tabs. */\nlet mounts = 0\n\n
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/dom/index.ts"],"sourcesContent":["/**\n * `@localix-ai/flowdown/dom`: Markdown on a page with no framework.\n *\n * import { mount } from '@localix-ai/flowdown/dom'\n * import '@localix-ai/flowdown/styles.css'\n *\n * const reply = mount(document.querySelector('#reply'))\n * for await (const text of stream) reply.update(text, { streaming: true })\n * reply.update(text)\n *\n * The elements are the ones `<Markdown>` draws, and the page is kept up to date the same way: a\n * block that did not change is not touched, so a token costs the last block. Tabs switch, a file\n * tree folds, a code block copies and saves, a wide table scrolls: all with the keyboard too.\n *\n * It is also what a binding for a framework is made of: mount into the element the framework\n * gives you, call `update` when the text changes, `destroy` when the element goes.\n */\nimport { codeFileName, copyText, downloadFile } from '../browser/file-actions'\nimport { mergeLabels, type MarkdownLabels } from '../render/labels'\nimport { createView, type ViewOptions } from '../render/view'\nimport type { DrawnElement } from '../render/vnode'\nimport { create, patch } from './patch'\n\nexport type { ViewComponents } from '../render/view'\nexport type { VComponent, VElement, VNode } from '../render/vnode'\nexport { DEFAULT_LABELS, type LabelOverrides, type MarkdownLabels } from '../render/labels'\nexport type { Highlighter } from '../render/views'\nexport { defaultUrlTransform, type UrlTransform } from '../engine/url'\nexport type { Extension, Node } from '../engine/types'\n\nexport type MountOptions = Omit<ViewOptions, 'host' | 'idPrefix'>\n\nexport interface UpdateOptions {\n /** The text is a reply still arriving: its unfinished end is completed, and a caret follows it. */\n streaming?: boolean\n}\n\nexport interface Mounted {\n /** The document's root: `<div class=\"flowdown\">`, the last child of what it was mounted in. */\n readonly element: HTMLElement\n /** Shows `markdown`. Call it on every token: what did not change is not drawn again. */\n update(markdown: string, options?: UpdateOptions): void\n /** Takes the document off the page and stops everything it started. */\n destroy(): void\n}\n\n/** Documents mounted so far: each gets its own ids for its tabs. */\nlet mounts = 0\n\n/**\n * A table's box, or a code block's `<pre>`, scrolls sideways when what it holds is wider: then the\n * keyboard must reach it.\n */\nfunction measure(box: Element, labels: MarkdownLabels): void {\n // One pixel of slack: a subpixel content width rounds up past the box it fits in.\n const scrolls = box.scrollWidth > box.clientWidth + 1\n if (scrolls === box.hasAttribute('tabindex')) return\n const label = box.localName === 'pre' ? labels.scrollableCode : labels.scrollableTable\n if (scrolls) {\n box.setAttribute('role', 'group')\n box.setAttribute('aria-label', label)\n box.setAttribute('tabindex', '0')\n } else {\n box.removeAttribute('role')\n box.removeAttribute('aria-label')\n box.removeAttribute('tabindex')\n }\n}\n\nexport function mount(target: Element, options: MountOptions = {}): Mounted {\n const document = target.ownerDocument\n const labels = mergeLabels(options.labels)\n let text = ''\n let streaming = false\n let shown: DrawnElement | null = null\n let root: HTMLElement | null = null\n let drawing = false\n let again = false\n let gone = false\n\n /** The table boxes on the page, and the ones made since the last drawing. */\n const boxes = new WeakSet<Element>()\n let fresh: Element[] = []\n /** Whether a measuring of the last code block is already waiting for the next frame. */\n let asked = false\n const observer =\n typeof ResizeObserver === 'undefined'\n ? null\n : new ResizeObserver((entries) => {\n for (const entry of entries) {\n const box = entry.target.closest('.fd-table, pre')\n if (box) measure(box, labels)\n }\n })\n\n const view = createView({\n ...options,\n idPrefix: `fd-${(mounts += 1)}-`,\n host: {\n redraw: () => draw(),\n copy: copyText,\n download: (code, title, language) => downloadFile(code, codeFileName(title, language)),\n table: (box) => {\n const element = box as Element\n if (boxes.has(element)) return\n boxes.add(element)\n fresh.push(element)\n },\n },\n })\n\n function draw(): void {\n if (gone) return\n // A listener that fires while the page is being changed asks for one more drawing after it.\n if (drawing) {\n again = true\n return\n }\n drawing = true\n try {\n do {\n again = false\n const next = view.render(text, streaming)\n if (root === null || shown === null) {\n root = create(next, document) as HTMLElement\n target.appendChild(root)\n } else {\n root = patch(root, shown, next, document) as HTMLElement\n }\n shown = next\n } while (again)\n } finally {\n drawing = false\n }\n // Measured once it is on the page, and again whenever it or its table changes size: a reply\n // that streams a row in, a window made narrower.\n for (const box of fresh) {\n measure(box, labels)\n if (observer) {\n observer.observe(box)\n const table = box.querySelector('table')\n if (table) observer.observe(table)\n }\n }\n fresh = []\n // A line of code getting longer resizes nothing an observer watches. The block being written\n // is the last one: it is measured once a frame, when the page is laid out anyway.\n if (!asked && root !== null && typeof requestAnimationFrame === 'function') {\n asked = true\n requestAnimationFrame(() => {\n asked = false\n if (gone || root === null) return\n const blocks = root.getElementsByTagName('pre')\n const last = blocks[blocks.length - 1]\n if (last && boxes.has(last)) measure(last, labels)\n })\n }\n }\n\n draw()\n\n return {\n get element() {\n return root!\n },\n update(markdown, { streaming: live = false } = {}) {\n text = markdown\n streaming = live\n draw()\n },\n destroy() {\n if (gone) return\n gone = true\n observer?.disconnect()\n view.dispose()\n root?.remove()\n },\n }\n}\n"],"mappings":"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"}
|