@localix-ai/flowdown 0.0.0-stage → 0.5.0
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/LICENSE +21 -0
- package/README.md +101 -2
- package/dist/browser/file-actions.js +80 -0
- package/dist/browser/file-actions.js.map +1 -0
- package/dist/core/block-facts.js +72 -0
- package/dist/core/block-facts.js.map +1 -0
- package/dist/core/blocks.d.ts +25 -0
- package/dist/core/blocks.js +463 -0
- package/dist/core/blocks.js.map +1 -0
- package/dist/core/bounded-cache.js +42 -0
- package/dist/core/bounded-cache.js.map +1 -0
- package/dist/core/dollars.d.ts +12 -0
- package/dist/core/dollars.js +88 -0
- package/dist/core/dollars.js.map +1 -0
- package/dist/core/fence-scan.js +53 -0
- package/dist/core/fence-scan.js.map +1 -0
- package/dist/core/file-tree-parse.d.ts +26 -0
- package/dist/core/file-tree-parse.js +51 -0
- package/dist/core/file-tree-parse.js.map +1 -0
- package/dist/core/forms-html.js +63 -0
- package/dist/core/forms-html.js.map +1 -0
- package/dist/core/forms.d.ts +28 -0
- package/dist/core/forms.js +23 -0
- package/dist/core/forms.js.map +1 -0
- package/dist/core/headings.d.ts +11 -0
- package/dist/core/headings.js +74 -0
- package/dist/core/headings.js.map +1 -0
- package/dist/core/heal.d.ts +11 -0
- package/dist/core/heal.js +220 -0
- package/dist/core/heal.js.map +1 -0
- package/dist/core/load-once.js +23 -0
- package/dist/core/load-once.js.map +1 -0
- package/dist/core/math-delimiters.js +33 -0
- package/dist/core/math-delimiters.js.map +1 -0
- package/dist/core/math-syntax.js +56 -0
- package/dist/core/math-syntax.js.map +1 -0
- package/dist/core/prose.d.ts +10 -0
- package/dist/core/prose.js +77 -0
- package/dist/core/prose.js.map +1 -0
- package/dist/core/remark-answer-blocks.d.ts +5 -0
- package/dist/core/remark-answer-blocks.js +172 -0
- package/dist/core/remark-answer-blocks.js.map +1 -0
- package/dist/core/remark-answer-containers.js +299 -0
- package/dist/core/remark-answer-containers.js.map +1 -0
- package/dist/core/sanitize-schema.d.ts +18 -0
- package/dist/core/sanitize-schema.js +62 -0
- package/dist/core/sanitize-schema.js.map +1 -0
- package/dist/core/syntax.d.ts +8 -0
- package/dist/core/syntax.js +32 -0
- package/dist/core/syntax.js.map +1 -0
- package/dist/dialect.d.ts +92 -0
- package/dist/dialect.js +176 -0
- package/dist/dialect.js.map +1 -0
- package/dist/document/fence.d.ts +25 -0
- package/dist/document/fence.js +101 -0
- package/dist/document/fence.js.map +1 -0
- package/dist/document/index.d.ts +46 -0
- package/dist/document/index.js +66 -0
- package/dist/document/index.js.map +1 -0
- package/dist/document/table.d.ts +11 -0
- package/dist/document/table.js +35 -0
- package/dist/document/table.js.map +1 -0
- package/dist/dom/index.d.ts +21 -0
- package/dist/dom/index.js +123 -0
- package/dist/dom/index.js.map +1 -0
- package/dist/dom/patch.js +104 -0
- package/dist/dom/patch.js.map +1 -0
- package/dist/element/index.d.ts +29 -0
- package/dist/element/index.js +64 -0
- package/dist/element/index.js.map +1 -0
- package/dist/engine/blocks.js +774 -0
- package/dist/engine/blocks.js.map +1 -0
- package/dist/engine/entities.js +83 -0
- package/dist/engine/entities.js.map +1 -0
- package/dist/engine/entity-core.js +9 -0
- package/dist/engine/entity-core.js.map +1 -0
- package/dist/engine/entity-table.js +9 -0
- package/dist/engine/entity-table.js.map +1 -0
- package/dist/engine/html.d.ts +7 -0
- package/dist/engine/html.js +207 -0
- package/dist/engine/html.js.map +1 -0
- package/dist/engine/index.d.ts +7 -0
- package/dist/engine/index.js +74 -0
- package/dist/engine/index.js.map +1 -0
- package/dist/engine/inlines.js +852 -0
- package/dist/engine/inlines.js.map +1 -0
- package/dist/engine/types.d.ts +109 -0
- package/dist/engine/url.d.ts +11 -0
- package/dist/engine/url.js +16 -0
- package/dist/engine/url.js.map +1 -0
- package/dist/engine/visit.d.ts +10 -0
- package/dist/engine/visit.js +29 -0
- package/dist/engine/visit.js.map +1 -0
- package/dist/highlight/highlight.d.ts +4 -0
- package/dist/highlight/highlight.js +144 -0
- package/dist/highlight/highlight.js.map +1 -0
- package/dist/highlight/index.d.ts +4 -0
- package/dist/highlight/index.js +18 -0
- package/dist/highlight/index.js.map +1 -0
- package/dist/html/index.d.ts +4 -0
- package/dist/html/index.js +544 -0
- package/dist/html/index.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +13 -0
- package/dist/math/index.d.ts +17 -0
- package/dist/math/index.js +20 -0
- package/dist/math/index.js.map +1 -0
- package/dist/math/typeset.d.ts +12 -0
- package/dist/math/typeset.js +74 -0
- package/dist/math/typeset.js.map +1 -0
- package/dist/mermaid/diagram.d.ts +8 -0
- package/dist/mermaid/diagram.js +114 -0
- package/dist/mermaid/diagram.js.map +1 -0
- package/dist/mermaid/drawing.js +139 -0
- package/dist/mermaid/drawing.js.map +1 -0
- package/dist/mermaid/icons.js +16 -0
- package/dist/mermaid/icons.js.map +1 -0
- package/dist/mermaid/index.d.ts +13 -0
- package/dist/mermaid/index.js +3 -0
- package/dist/mermaid/loader.d.ts +5 -0
- package/dist/mermaid/loader.js +18 -0
- package/dist/mermaid/loader.js.map +1 -0
- package/dist/mermaid/look.d.ts +16 -0
- package/dist/mermaid/look.js +167 -0
- package/dist/mermaid/look.js.map +1 -0
- package/dist/mermaid/normalize-svg.js +44 -0
- package/dist/mermaid/normalize-svg.js.map +1 -0
- package/dist/mermaid/renderer.d.ts +22 -0
- package/dist/mermaid/renderer.js +132 -0
- package/dist/mermaid/renderer.js.map +1 -0
- package/dist/mermaid/viewer.js +309 -0
- package/dist/mermaid/viewer.js.map +1 -0
- package/dist/prompt/index.d.ts +19 -0
- package/dist/prompt/index.js +54 -0
- package/dist/prompt/index.js.map +1 -0
- package/dist/react/callout.d.ts +9 -0
- package/dist/react/callout.js +12 -0
- package/dist/react/callout.js.map +1 -0
- package/dist/react/code-block.d.ts +18 -0
- package/dist/react/code-block.js +81 -0
- package/dist/react/code-block.js.map +1 -0
- package/dist/react/context.d.ts +18 -0
- package/dist/react/context.js +17 -0
- package/dist/react/context.js.map +1 -0
- package/dist/react/draw.d.ts +2 -0
- package/dist/react/draw.js +16 -0
- package/dist/react/draw.js.map +1 -0
- package/dist/react/file-tree.d.ts +16 -0
- package/dist/react/file-tree.js +69 -0
- package/dist/react/file-tree.js.map +1 -0
- package/dist/react/growing-code.js +36 -0
- package/dist/react/growing-code.js.map +1 -0
- package/dist/react/helpers.d.ts +6 -0
- package/dist/react/helpers.js +17 -0
- package/dist/react/helpers.js.map +1 -0
- package/dist/react/icons.js +23 -0
- package/dist/react/icons.js.map +1 -0
- package/dist/react/labels.js +9 -0
- package/dist/react/labels.js.map +1 -0
- package/dist/react/markdown.d.ts +78 -0
- package/dist/react/markdown.js +204 -0
- package/dist/react/markdown.js.map +1 -0
- package/dist/react/render.d.ts +8 -0
- package/dist/react/render.js +41 -0
- package/dist/react/render.js.map +1 -0
- package/dist/react/table-rows.js +66 -0
- package/dist/react/table-rows.js.map +1 -0
- package/dist/react/table.js +36 -0
- package/dist/react/table.js.map +1 -0
- package/dist/react/tabs.d.ts +25 -0
- package/dist/react/tabs.js +58 -0
- package/dist/react/tabs.js.map +1 -0
- package/dist/react/use-smooth-text.d.ts +6 -0
- package/dist/react/use-smooth-text.js +156 -0
- package/dist/react/use-smooth-text.js.map +1 -0
- package/dist/react/use-stage.d.ts +9 -0
- package/dist/react/use-stage.js +57 -0
- package/dist/react/use-stage.js.map +1 -0
- package/dist/remark.d.ts +20 -0
- package/dist/remark.js +10 -0
- package/dist/render/factory.d.ts +15 -0
- package/dist/render/labels.d.ts +41 -0
- package/dist/render/labels.js +43 -0
- package/dist/render/labels.js.map +1 -0
- package/dist/render/tree.js +312 -0
- package/dist/render/tree.js.map +1 -0
- package/dist/render/view.d.ts +55 -0
- package/dist/render/view.js +302 -0
- package/dist/render/view.js.map +1 -0
- package/dist/render/views.d.ts +16 -0
- package/dist/render/views.js +261 -0
- package/dist/render/views.js.map +1 -0
- package/dist/render/vnode.d.ts +21 -0
- package/dist/render/vnode.js +194 -0
- package/dist/render/vnode.js.map +1 -0
- package/dist/static/index.d.ts +28 -0
- package/dist/static/index.js +27 -0
- package/dist/static/index.js.map +1 -0
- package/dist/stream.d.ts +12 -0
- package/dist/stream.js +3 -0
- package/dist/styles.css +1351 -0
- package/dist/tui/code.d.ts +5 -0
- package/dist/tui/code.js +240 -0
- package/dist/tui/code.js.map +1 -0
- package/dist/tui/index.d.ts +54 -0
- package/dist/tui/index.js +99 -0
- package/dist/tui/index.js.map +1 -0
- package/dist/tui/math.js +242 -0
- package/dist/tui/math.js.map +1 -0
- package/dist/tui/printable.d.ts +12 -0
- package/dist/tui/printable.js +31 -0
- package/dist/tui/printable.js.map +1 -0
- package/dist/tui/render.d.ts +12 -0
- package/dist/tui/render.js +524 -0
- package/dist/tui/render.js.map +1 -0
- package/dist/tui/style.d.ts +35 -0
- package/dist/tui/style.js +77 -0
- package/dist/tui/style.js.map +1 -0
- package/dist/tui/table.js +73 -0
- package/dist/tui/table.js.map +1 -0
- package/dist/tui/width.d.ts +2 -0
- package/dist/tui/width.js +133 -0
- package/dist/tui/width.js.map +1 -0
- package/dist/tui/wrap.d.ts +8 -0
- package/dist/tui/wrap.js +190 -0
- package/dist/tui/wrap.js.map +1 -0
- package/package.json +127 -4
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { inlineMathClose, isLiteralDollar } from "./dollars.js";
|
|
2
|
+
import { opensInlineMath } from "./math-delimiters.js";
|
|
3
|
+
import { isEscaped, markdownProseWithoutCode } from "./prose.js";
|
|
4
|
+
// src/core/syntax.ts
|
|
5
|
+
/**
|
|
6
|
+
* Whether the prose holds a formula, so KaTeX is worth loading: a `$$…$$` pair, or a single-dollar
|
|
7
|
+
* pair by the parser's rule (`math-delimiters.ts`). Prices, shell variables and code do not count.
|
|
8
|
+
*/
|
|
9
|
+
function hasMathSyntax(markdown) {
|
|
10
|
+
if (!markdown.includes("$")) return false;
|
|
11
|
+
const text = markdownProseWithoutCode(markdown);
|
|
12
|
+
for (let index = text.indexOf("$"); index !== -1; index = text.indexOf("$", index + 1)) {
|
|
13
|
+
if (isEscaped(text, index)) continue;
|
|
14
|
+
if (text[index + 1] === "$") {
|
|
15
|
+
const closing = text.indexOf("$$", index + 2);
|
|
16
|
+
if (closing !== -1 && text.slice(index + 2, closing).trim()) return true;
|
|
17
|
+
index = closing === -1 ? index + 1 : closing + 1;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (text[index - 1] === "$" || isLiteralDollar(text, index)) continue;
|
|
21
|
+
if (opensInlineMath(text, index) && inlineMathClose(text, index) !== -1) return true;
|
|
22
|
+
}
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
/** Raw HTML only matters in prose. Tags shown inside fenced/indented/inline code
|
|
26
|
+
* stay literal, so they must not enable the full rehype-raw + sanitize pipeline. */
|
|
27
|
+
function hasHtmlSyntax(markdown) {
|
|
28
|
+
return /<[a-zA-Z][\w-]*[\s/>]/.test(markdownProseWithoutCode(markdown));
|
|
29
|
+
}
|
|
30
|
+
export { hasHtmlSyntax, hasMathSyntax };
|
|
31
|
+
|
|
32
|
+
//# sourceMappingURL=syntax.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"syntax.js","names":[],"sources":["../../src/core/syntax.ts"],"sourcesContent":["import { inlineMathClose, isLiteralDollar } from './dollars'\nimport { opensInlineMath } from './math-delimiters'\nimport { isEscaped, markdownProseWithoutCode } from './prose'\n\n/**\n * Whether the prose holds a formula, so KaTeX is worth loading: a `$$…$$` pair, or a single-dollar\n * pair by the parser's rule (`math-delimiters.ts`). Prices, shell variables and code do not count.\n */\nexport function hasMathSyntax(markdown: string): boolean {\n if (!markdown.includes('$')) return false\n const text = markdownProseWithoutCode(markdown)\n\n for (let index = text.indexOf('$'); index !== -1; index = text.indexOf('$', index + 1)) {\n if (isEscaped(text, index)) continue\n if (text[index + 1] === '$') {\n const closing = text.indexOf('$$', index + 2)\n if (closing !== -1 && text.slice(index + 2, closing).trim()) return true\n index = closing === -1 ? index + 1 : closing + 1\n continue\n }\n if (text[index - 1] === '$' || isLiteralDollar(text, index)) continue\n if (opensInlineMath(text, index) && inlineMathClose(text, index) !== -1) return true\n }\n\n return false\n}\n\n/** Raw HTML only matters in prose. Tags shown inside fenced/indented/inline code\n * stay literal, so they must not enable the full rehype-raw + sanitize pipeline. */\nexport function hasHtmlSyntax(markdown: string): boolean {\n return /<[a-zA-Z][\\w-]*[\\s/>]/.test(markdownProseWithoutCode(markdown))\n}\n"],"mappings":";;;;;;;;AAQA,SAAgB,cAAc,UAA2B;CACvD,IAAI,CAAC,SAAS,SAAS,GAAG,GAAG,OAAO;CACpC,MAAM,OAAO,yBAAyB,QAAQ;CAE9C,KAAK,IAAI,QAAQ,KAAK,QAAQ,GAAG,GAAG,UAAU,IAAI,QAAQ,KAAK,QAAQ,KAAK,QAAQ,CAAC,GAAG;EACtF,IAAI,UAAU,MAAM,KAAK,GAAG;EAC5B,IAAI,KAAK,QAAQ,OAAO,KAAK;GAC3B,MAAM,UAAU,KAAK,QAAQ,MAAM,QAAQ,CAAC;GAC5C,IAAI,YAAY,MAAM,KAAK,MAAM,QAAQ,GAAG,OAAO,CAAC,CAAC,KAAK,GAAG,OAAO;GACpE,QAAQ,YAAY,KAAK,QAAQ,IAAI,UAAU;GAC/C;EACF;EACA,IAAI,KAAK,QAAQ,OAAO,OAAO,gBAAgB,MAAM,KAAK,GAAG;EAC7D,IAAI,gBAAgB,MAAM,KAAK,KAAK,gBAAgB,MAAM,KAAK,MAAM,IAAI,OAAO;CAClF;CAEA,OAAO;AACT;;;AAIA,SAAgB,cAAc,UAA2B;CACvD,OAAO,wBAAwB,KAAK,yBAAyB,QAAQ,CAAC;AACxE"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/** Shared answer/document grammar; this leaf has no server or browser imports. */
|
|
2
|
+
/**
|
|
3
|
+
* Callout kinds, in GitHub's alert syntax (`> [!WARNING]`). GitHub's own set, not an invented
|
|
4
|
+
* one: the model already writes it, and an answer pasted into an issue or a README keeps its
|
|
5
|
+
* rendering there.
|
|
6
|
+
*/
|
|
7
|
+
export declare const CALLOUT_KINDS: readonly ['note', 'tip', 'important', 'warning', 'caution'];
|
|
8
|
+
export type CalloutKind = (typeof CALLOUT_KINDS)[number];
|
|
9
|
+
/** Fence language whose body renders as a diagram rather than as code. */
|
|
10
|
+
export declare const DIAGRAM_FENCE_LANGUAGE = "mermaid";
|
|
11
|
+
/**
|
|
12
|
+
* Diagram types worth naming to the model. Mermaid parses more; these are the ones that read
|
|
13
|
+
* well at chat width, so the prompt steers toward them instead of listing the whole grammar.
|
|
14
|
+
*/
|
|
15
|
+
export declare const DIAGRAM_KINDS: readonly ['flowchart', 'sequenceDiagram', 'stateDiagram-v2', 'erDiagram', 'gantt', 'pie', 'xychart-beta', 'classDiagram', 'mindmap', 'timeline', 'quadrantChart', 'sankey-beta', 'radar-beta', 'treemap-beta', 'gitGraph'];
|
|
16
|
+
/**
|
|
17
|
+
* A quote's closing source line: `— Author` (em dash) or `-- Author` for keyboards without one.
|
|
18
|
+
* Anchored at a line start, so a dash inside a sentence never turns into an attribution.
|
|
19
|
+
*/
|
|
20
|
+
export declare const ATTRIBUTION_PREFIX: RegExp;
|
|
21
|
+
export interface CalloutMarker {
|
|
22
|
+
kind: CalloutKind;
|
|
23
|
+
/** Text after the marker on its own line (`> [!TIP] Faster builds`), or '' for none. */
|
|
24
|
+
title: string;
|
|
25
|
+
/** Characters of the input the marker and its title line consumed, newline included. */
|
|
26
|
+
consumed: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Read a callout marker at the start of a quote's first line.
|
|
30
|
+
*
|
|
31
|
+
* An unknown kind (`[!FOO]`) is not a callout: it stays a quote with its text intact, which is
|
|
32
|
+
* also what GitHub does, so a typo degrades to readable text instead of a mislabelled box.
|
|
33
|
+
*/
|
|
34
|
+
export declare function parseCalloutMarker(text: string): CalloutMarker | null;
|
|
35
|
+
/**
|
|
36
|
+
* CONTAINER FORMS: a quote opened by a marker of ours, over ordinary Markdown, that the renderer
|
|
37
|
+
* lays out as something a plain list or heading cannot be.
|
|
38
|
+
*
|
|
39
|
+
* steps a list of stages read in order → a vertical timeline (dot, title, quiet "when", text)
|
|
40
|
+
* tabs `#### Label` sections → one panel per variant (an OS, a language), one shown at a time
|
|
41
|
+
* stats a list of `**value** label` items → a row of headline figures
|
|
42
|
+
* pros a list → the case for, with a check per point
|
|
43
|
+
* cons a list → the case against; right after `pros` the two sit side by side
|
|
44
|
+
* details the quote's own content, folded under its title (a log, a derivation, an aside the
|
|
45
|
+
* reader may skip) → a native `<details>`
|
|
46
|
+
* sources a list of links → the numbered references of a reply, each with its site
|
|
47
|
+
*
|
|
48
|
+
* Opt-in by marker, never guessed from shape: models open MANY list items with bold
|
|
49
|
+
* (`**Pros:** …`) and section with headings, and turning each of those into a widget would be
|
|
50
|
+
* wrong far more often than right. In the quote they stay plain Markdown — a quote holding a list
|
|
51
|
+
* or sections — so an answer still reads anywhere this dialect is not rendered.
|
|
52
|
+
*/
|
|
53
|
+
export declare const CONTAINER_KINDS: readonly ['steps', 'tabs', 'stats', 'pros', 'cons', 'details', 'sources'];
|
|
54
|
+
export type ContainerKind = (typeof CONTAINER_KINDS)[number];
|
|
55
|
+
/** The marker as the prompt writes it: `[!STEPS]`, `[!TABS]`, … */
|
|
56
|
+
export declare const containerMarker: (kind: ContainerKind) => string;
|
|
57
|
+
/**
|
|
58
|
+
* The prompt's example of each container form, kept here so the renderer's tests render the very
|
|
59
|
+
* text the model is shown — an example that drew as stray markers would teach the model a broken
|
|
60
|
+
* form. Each is written the way models write these forms: the marker alone as a one-line quote,
|
|
61
|
+
* the content unquoted after it (the renderer reads that by grammar, `remarkAnswerContainers`).
|
|
62
|
+
* Shown, not described: the markers are ours and absent from any training data, and a model that
|
|
63
|
+
* was only TOLD about a form kept writing the table or numbered list it writes by habit.
|
|
64
|
+
*/
|
|
65
|
+
export declare const ANSWER_FORM_EXAMPLES: {
|
|
66
|
+
readonly comparison: readonly [`> ${string}`, "#### PostgreSQL", `> ${string}`, "- concurrent writes", "", `> ${string}`, "- needs a server", "", "---"];
|
|
67
|
+
readonly verdict: readonly [`> ${string}`, "- typed APIs", "", `> ${string}`, "- a build step"];
|
|
68
|
+
readonly steps: readonly [`> ${string}`, "1. **First 24 hours**", " *Day 1*", " Rest and keep warm."];
|
|
69
|
+
readonly stats: readonly [`> ${string}`, "- **3×** faster installs", "- **−40%** memory"];
|
|
70
|
+
readonly details: readonly [`> ${string} Full log`, "> The long part, quoted."];
|
|
71
|
+
readonly sources: readonly [`> ${string}`, "- [Page title](https://example.com/page)"];
|
|
72
|
+
};
|
|
73
|
+
export interface ContainerMarker {
|
|
74
|
+
kind: ContainerKind;
|
|
75
|
+
/** Text after the marker on its own line (`> [!STEPS] Recovery`), or '' for none. */
|
|
76
|
+
title: string;
|
|
77
|
+
/** Characters of the input the marker and its title line consumed, newline included. */
|
|
78
|
+
consumed: number;
|
|
79
|
+
}
|
|
80
|
+
/** Read a container marker at a quote's start. An unknown kind is not one (nor a callout). */
|
|
81
|
+
export declare function parseContainerMarker(text: string): ContainerMarker | null;
|
|
82
|
+
/**
|
|
83
|
+
* Fence language whose body is a directory layout (`├──` / `└──` lines) drawn as a file tree.
|
|
84
|
+
* A fence the model already writes as `text`: naming it `tree` is the whole opt-in.
|
|
85
|
+
*/
|
|
86
|
+
export declare const TREE_FENCE_LANGUAGE = "tree";
|
|
87
|
+
/**
|
|
88
|
+
* The file a code fence names after its language: ` ```ts title="src/app.ts" ` (also `filename=`,
|
|
89
|
+
* `file=`), or the path alone (` ```ts src/app.ts `), the way an agent writing a file labels it.
|
|
90
|
+
* '' when the info string names none; any other word there (`{1,3}`, `showLineNumbers`) is not one.
|
|
91
|
+
*/
|
|
92
|
+
export declare function fenceTitle(meta: string | null | undefined): string;
|
package/dist/dialect.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// src/dialect.ts
|
|
2
|
+
/** Shared answer/document grammar; this leaf has no server or browser imports. */
|
|
3
|
+
/**
|
|
4
|
+
* Callout kinds, in GitHub's alert syntax (`> [!WARNING]`). GitHub's own set, not an invented
|
|
5
|
+
* one: the model already writes it, and an answer pasted into an issue or a README keeps its
|
|
6
|
+
* rendering there.
|
|
7
|
+
*/
|
|
8
|
+
const CALLOUT_KINDS = [
|
|
9
|
+
"note",
|
|
10
|
+
"tip",
|
|
11
|
+
"important",
|
|
12
|
+
"warning",
|
|
13
|
+
"caution"
|
|
14
|
+
];
|
|
15
|
+
/** Fence language whose body renders as a diagram rather than as code. */
|
|
16
|
+
const DIAGRAM_FENCE_LANGUAGE = "mermaid";
|
|
17
|
+
/**
|
|
18
|
+
* Diagram types worth naming to the model. Mermaid parses more; these are the ones that read
|
|
19
|
+
* well at chat width, so the prompt steers toward them instead of listing the whole grammar.
|
|
20
|
+
*/
|
|
21
|
+
const DIAGRAM_KINDS = [
|
|
22
|
+
"flowchart",
|
|
23
|
+
"sequenceDiagram",
|
|
24
|
+
"stateDiagram-v2",
|
|
25
|
+
"erDiagram",
|
|
26
|
+
"gantt",
|
|
27
|
+
"pie",
|
|
28
|
+
"xychart-beta",
|
|
29
|
+
"classDiagram",
|
|
30
|
+
"mindmap",
|
|
31
|
+
"timeline",
|
|
32
|
+
"quadrantChart",
|
|
33
|
+
"sankey-beta",
|
|
34
|
+
"radar-beta",
|
|
35
|
+
"treemap-beta",
|
|
36
|
+
"gitGraph"
|
|
37
|
+
];
|
|
38
|
+
/**
|
|
39
|
+
* A quote's closing source line: `— Author` (em dash) or `-- Author` for keyboards without one.
|
|
40
|
+
* Anchored at a line start, so a dash inside a sentence never turns into an attribution.
|
|
41
|
+
*/
|
|
42
|
+
const ATTRIBUTION_PREFIX = /^(?:—|--)[ \t]+/;
|
|
43
|
+
const CALLOUT_MARKER = /^\[!([A-Za-z]+)\][ \t]*/;
|
|
44
|
+
function isCalloutKind(value) {
|
|
45
|
+
return CALLOUT_KINDS.includes(value);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Read a callout marker at the start of a quote's first line.
|
|
49
|
+
*
|
|
50
|
+
* An unknown kind (`[!FOO]`) is not a callout: it stays a quote with its text intact, which is
|
|
51
|
+
* also what GitHub does, so a typo degrades to readable text instead of a mislabelled box.
|
|
52
|
+
*/
|
|
53
|
+
function parseCalloutMarker(text) {
|
|
54
|
+
const marker = readMarker(text);
|
|
55
|
+
return marker && isCalloutKind(marker.kind) ? {
|
|
56
|
+
...marker,
|
|
57
|
+
kind: marker.kind
|
|
58
|
+
} : null;
|
|
59
|
+
}
|
|
60
|
+
/** A `[!KIND] title` marker line of either form, before its kind is checked. */
|
|
61
|
+
function readMarker(text) {
|
|
62
|
+
const match = CALLOUT_MARKER.exec(text);
|
|
63
|
+
if (!match) return null;
|
|
64
|
+
const rest = text.slice(match[0].length);
|
|
65
|
+
const lineEnd = rest.indexOf("\n");
|
|
66
|
+
const title = (lineEnd === -1 ? rest : rest.slice(0, lineEnd)).trim();
|
|
67
|
+
const consumed = match[0].length + (lineEnd === -1 ? rest.length : lineEnd + 1);
|
|
68
|
+
return {
|
|
69
|
+
kind: match[1].toLowerCase(),
|
|
70
|
+
title,
|
|
71
|
+
consumed
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* CONTAINER FORMS: a quote opened by a marker of ours, over ordinary Markdown, that the renderer
|
|
76
|
+
* lays out as something a plain list or heading cannot be.
|
|
77
|
+
*
|
|
78
|
+
* steps a list of stages read in order → a vertical timeline (dot, title, quiet "when", text)
|
|
79
|
+
* tabs `#### Label` sections → one panel per variant (an OS, a language), one shown at a time
|
|
80
|
+
* stats a list of `**value** label` items → a row of headline figures
|
|
81
|
+
* pros a list → the case for, with a check per point
|
|
82
|
+
* cons a list → the case against; right after `pros` the two sit side by side
|
|
83
|
+
* details the quote's own content, folded under its title (a log, a derivation, an aside the
|
|
84
|
+
* reader may skip) → a native `<details>`
|
|
85
|
+
* sources a list of links → the numbered references of a reply, each with its site
|
|
86
|
+
*
|
|
87
|
+
* Opt-in by marker, never guessed from shape: models open MANY list items with bold
|
|
88
|
+
* (`**Pros:** …`) and section with headings, and turning each of those into a widget would be
|
|
89
|
+
* wrong far more often than right. In the quote they stay plain Markdown — a quote holding a list
|
|
90
|
+
* or sections — so an answer still reads anywhere this dialect is not rendered.
|
|
91
|
+
*/
|
|
92
|
+
const CONTAINER_KINDS = [
|
|
93
|
+
"steps",
|
|
94
|
+
"tabs",
|
|
95
|
+
"stats",
|
|
96
|
+
"pros",
|
|
97
|
+
"cons",
|
|
98
|
+
"details",
|
|
99
|
+
"sources"
|
|
100
|
+
];
|
|
101
|
+
/** The marker as the prompt writes it: `[!STEPS]`, `[!TABS]`, … */
|
|
102
|
+
const containerMarker = (kind) => `[!${kind.toUpperCase()}]`;
|
|
103
|
+
/**
|
|
104
|
+
* The prompt's example of each container form, kept here so the renderer's tests render the very
|
|
105
|
+
* text the model is shown — an example that drew as stray markers would teach the model a broken
|
|
106
|
+
* form. Each is written the way models write these forms: the marker alone as a one-line quote,
|
|
107
|
+
* the content unquoted after it (the renderer reads that by grammar, `remarkAnswerContainers`).
|
|
108
|
+
* Shown, not described: the markers are ours and absent from any training data, and a model that
|
|
109
|
+
* was only TOLD about a form kept writing the table or numbered list it writes by habit.
|
|
110
|
+
*/
|
|
111
|
+
const ANSWER_FORM_EXAMPLES = {
|
|
112
|
+
comparison: [
|
|
113
|
+
`> ${containerMarker("tabs")}`,
|
|
114
|
+
"#### PostgreSQL",
|
|
115
|
+
`> ${containerMarker("pros")}`,
|
|
116
|
+
"- concurrent writes",
|
|
117
|
+
"",
|
|
118
|
+
`> ${containerMarker("cons")}`,
|
|
119
|
+
"- needs a server",
|
|
120
|
+
"",
|
|
121
|
+
"---"
|
|
122
|
+
],
|
|
123
|
+
verdict: [
|
|
124
|
+
`> ${containerMarker("pros")}`,
|
|
125
|
+
"- typed APIs",
|
|
126
|
+
"",
|
|
127
|
+
`> ${containerMarker("cons")}`,
|
|
128
|
+
"- a build step"
|
|
129
|
+
],
|
|
130
|
+
steps: [
|
|
131
|
+
`> ${containerMarker("steps")}`,
|
|
132
|
+
"1. **First 24 hours**",
|
|
133
|
+
" *Day 1*",
|
|
134
|
+
" Rest and keep warm."
|
|
135
|
+
],
|
|
136
|
+
stats: [
|
|
137
|
+
`> ${containerMarker("stats")}`,
|
|
138
|
+
"- **3×** faster installs",
|
|
139
|
+
"- **−40%** memory"
|
|
140
|
+
],
|
|
141
|
+
details: [`> ${containerMarker("details")} Full log`, "> The long part, quoted."],
|
|
142
|
+
sources: [`> ${containerMarker("sources")}`, "- [Page title](https://example.com/page)"]
|
|
143
|
+
};
|
|
144
|
+
function isContainerKind(value) {
|
|
145
|
+
return CONTAINER_KINDS.includes(value);
|
|
146
|
+
}
|
|
147
|
+
/** Read a container marker at a quote's start. An unknown kind is not one (nor a callout). */
|
|
148
|
+
function parseContainerMarker(text) {
|
|
149
|
+
const marker = readMarker(text);
|
|
150
|
+
return marker && isContainerKind(marker.kind) ? {
|
|
151
|
+
...marker,
|
|
152
|
+
kind: marker.kind
|
|
153
|
+
} : null;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Fence language whose body is a directory layout (`├──` / `└──` lines) drawn as a file tree.
|
|
157
|
+
* A fence the model already writes as `text`: naming it `tree` is the whole opt-in.
|
|
158
|
+
*/
|
|
159
|
+
const TREE_FENCE_LANGUAGE = "tree";
|
|
160
|
+
const FENCE_TITLE = /(?:^|\s)(?:title|filename|file)=(?:"([^"]*)"|'([^']*)'|(\S+))/;
|
|
161
|
+
const FENCE_PATH = /^[\w@.\-/\\[\]()]*[\w)\]]\.[A-Za-z0-9]+$/;
|
|
162
|
+
/**
|
|
163
|
+
* The file a code fence names after its language: ` ```ts title="src/app.ts" ` (also `filename=`,
|
|
164
|
+
* `file=`), or the path alone (` ```ts src/app.ts `), the way an agent writing a file labels it.
|
|
165
|
+
* '' when the info string names none; any other word there (`{1,3}`, `showLineNumbers`) is not one.
|
|
166
|
+
*/
|
|
167
|
+
function fenceTitle(meta) {
|
|
168
|
+
if (!meta) return "";
|
|
169
|
+
const named = FENCE_TITLE.exec(meta);
|
|
170
|
+
if (named) return (named[1] ?? named[2] ?? named[3] ?? "").trim();
|
|
171
|
+
const bare = meta.trim();
|
|
172
|
+
return FENCE_PATH.test(bare) ? bare : "";
|
|
173
|
+
}
|
|
174
|
+
export { ANSWER_FORM_EXAMPLES, ATTRIBUTION_PREFIX, CALLOUT_KINDS, CONTAINER_KINDS, DIAGRAM_FENCE_LANGUAGE, DIAGRAM_KINDS, TREE_FENCE_LANGUAGE, containerMarker, fenceTitle, parseCalloutMarker, parseContainerMarker };
|
|
175
|
+
|
|
176
|
+
//# sourceMappingURL=dialect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dialect.js","names":[],"sources":["../src/dialect.ts"],"sourcesContent":["/** Shared answer/document grammar; this leaf has no server or browser imports. */\n\n/**\n * Callout kinds, in GitHub's alert syntax (`> [!WARNING]`). GitHub's own set, not an invented\n * one: the model already writes it, and an answer pasted into an issue or a README keeps its\n * rendering there.\n */\nexport const CALLOUT_KINDS = ['note', 'tip', 'important', 'warning', 'caution'] as const\nexport type CalloutKind = (typeof CALLOUT_KINDS)[number]\n\n/** Fence language whose body renders as a diagram rather than as code. */\nexport const DIAGRAM_FENCE_LANGUAGE = 'mermaid'\n\n/**\n * Diagram types worth naming to the model. Mermaid parses more; these are the ones that read\n * well at chat width, so the prompt steers toward them instead of listing the whole grammar.\n */\nexport const DIAGRAM_KINDS = [\n 'flowchart',\n 'sequenceDiagram',\n 'stateDiagram-v2',\n 'erDiagram',\n 'gantt',\n 'pie',\n 'xychart-beta',\n 'classDiagram',\n 'mindmap',\n 'timeline',\n 'quadrantChart',\n 'sankey-beta',\n 'radar-beta',\n 'treemap-beta',\n 'gitGraph',\n] as const\n\n/**\n * A quote's closing source line: `— Author` (em dash) or `-- Author` for keyboards without one.\n * Anchored at a line start, so a dash inside a sentence never turns into an attribution.\n */\nexport const ATTRIBUTION_PREFIX = /^(?:—|--)[ \\t]+/\n\nconst CALLOUT_MARKER = /^\\[!([A-Za-z]+)\\][ \\t]*/\n\nexport interface CalloutMarker {\n kind: CalloutKind\n /** Text after the marker on its own line (`> [!TIP] Faster builds`), or '' for none. */\n title: string\n /** Characters of the input the marker and its title line consumed, newline included. */\n consumed: number\n}\n\nfunction isCalloutKind(value: string): value is CalloutKind {\n return (CALLOUT_KINDS as readonly string[]).includes(value)\n}\n\n/**\n * Read a callout marker at the start of a quote's first line.\n *\n * An unknown kind (`[!FOO]`) is not a callout: it stays a quote with its text intact, which is\n * also what GitHub does, so a typo degrades to readable text instead of a mislabelled box.\n */\nexport function parseCalloutMarker(text: string): CalloutMarker | null {\n const marker = readMarker(text)\n return marker && isCalloutKind(marker.kind) ? { ...marker, kind: marker.kind } : null\n}\n\n/** A `[!KIND] title` marker line of either form, before its kind is checked. */\nfunction readMarker(text: string): { kind: string; title: string; consumed: number } | null {\n const match = CALLOUT_MARKER.exec(text)\n if (!match) return null\n const rest = text.slice(match[0].length)\n const lineEnd = rest.indexOf('\\n')\n const title = (lineEnd === -1 ? rest : rest.slice(0, lineEnd)).trim()\n const consumed = match[0].length + (lineEnd === -1 ? rest.length : lineEnd + 1)\n return { kind: match[1]!.toLowerCase(), title, consumed }\n}\n\n/**\n * CONTAINER FORMS: a quote opened by a marker of ours, over ordinary Markdown, that the renderer\n * lays out as something a plain list or heading cannot be.\n *\n * steps a list of stages read in order → a vertical timeline (dot, title, quiet \"when\", text)\n * tabs `#### Label` sections → one panel per variant (an OS, a language), one shown at a time\n * stats a list of `**value** label` items → a row of headline figures\n * pros a list → the case for, with a check per point\n * cons a list → the case against; right after `pros` the two sit side by side\n * details the quote's own content, folded under its title (a log, a derivation, an aside the\n * reader may skip) → a native `<details>`\n * sources a list of links → the numbered references of a reply, each with its site\n *\n * Opt-in by marker, never guessed from shape: models open MANY list items with bold\n * (`**Pros:** …`) and section with headings, and turning each of those into a widget would be\n * wrong far more often than right. In the quote they stay plain Markdown — a quote holding a list\n * or sections — so an answer still reads anywhere this dialect is not rendered.\n */\nexport const CONTAINER_KINDS = [\n 'steps',\n 'tabs',\n 'stats',\n 'pros',\n 'cons',\n 'details',\n 'sources',\n] as const\nexport type ContainerKind = (typeof CONTAINER_KINDS)[number]\n\n/** The marker as the prompt writes it: `[!STEPS]`, `[!TABS]`, … */\nexport const containerMarker = (kind: ContainerKind) => `[!${kind.toUpperCase()}]`\n\n/**\n * The prompt's example of each container form, kept here so the renderer's tests render the very\n * text the model is shown — an example that drew as stray markers would teach the model a broken\n * form. Each is written the way models write these forms: the marker alone as a one-line quote,\n * the content unquoted after it (the renderer reads that by grammar, `remarkAnswerContainers`).\n * Shown, not described: the markers are ours and absent from any training data, and a model that\n * was only TOLD about a form kept writing the table or numbered list it writes by habit.\n */\nexport const ANSWER_FORM_EXAMPLES = {\n comparison: [\n `> ${containerMarker('tabs')}`,\n '#### PostgreSQL',\n `> ${containerMarker('pros')}`,\n '- concurrent writes',\n '',\n `> ${containerMarker('cons')}`,\n '- needs a server',\n '',\n '---',\n ],\n verdict: [\n `> ${containerMarker('pros')}`,\n '- typed APIs',\n '',\n `> ${containerMarker('cons')}`,\n '- a build step',\n ],\n steps: [\n `> ${containerMarker('steps')}`,\n '1. **First 24 hours**',\n ' *Day 1*',\n ' Rest and keep warm.',\n ],\n stats: [`> ${containerMarker('stats')}`, '- **3×** faster installs', '- **−40%** memory'],\n details: [`> ${containerMarker('details')} Full log`, '> The long part, quoted.'],\n sources: [`> ${containerMarker('sources')}`, '- [Page title](https://example.com/page)'],\n} as const satisfies Record<string, readonly string[]>\n\nfunction isContainerKind(value: string): value is ContainerKind {\n return (CONTAINER_KINDS as readonly string[]).includes(value)\n}\n\nexport interface ContainerMarker {\n kind: ContainerKind\n /** Text after the marker on its own line (`> [!STEPS] Recovery`), or '' for none. */\n title: string\n /** Characters of the input the marker and its title line consumed, newline included. */\n consumed: number\n}\n\n/** Read a container marker at a quote's start. An unknown kind is not one (nor a callout). */\nexport function parseContainerMarker(text: string): ContainerMarker | null {\n const marker = readMarker(text)\n return marker && isContainerKind(marker.kind) ? { ...marker, kind: marker.kind } : null\n}\n\n/**\n * Fence language whose body is a directory layout (`├──` / `└──` lines) drawn as a file tree.\n * A fence the model already writes as `text`: naming it `tree` is the whole opt-in.\n */\nexport const TREE_FENCE_LANGUAGE = 'tree'\n\nconst FENCE_TITLE = /(?:^|\\s)(?:title|filename|file)=(?:\"([^\"]*)\"|'([^']*)'|(\\S+))/\nconst FENCE_PATH = /^[\\w@.\\-/\\\\[\\]()]*[\\w)\\]]\\.[A-Za-z0-9]+$/\n\n/**\n * The file a code fence names after its language: ` ```ts title=\"src/app.ts\" ` (also `filename=`,\n * `file=`), or the path alone (` ```ts src/app.ts `), the way an agent writing a file labels it.\n * '' when the info string names none; any other word there (`{1,3}`, `showLineNumbers`) is not one.\n */\nexport function fenceTitle(meta: string | null | undefined): string {\n if (!meta) return ''\n const named = FENCE_TITLE.exec(meta)\n if (named) return (named[1] ?? named[2] ?? named[3] ?? '').trim()\n const bare = meta.trim()\n return FENCE_PATH.test(bare) ? bare : ''\n}\n"],"mappings":";;;;;;;AAOA,MAAa,gBAAgB;CAAC;CAAQ;CAAO;CAAa;CAAW;AAAS;;AAI9E,MAAa,yBAAyB;;;;;AAMtC,MAAa,gBAAgB;CAC3B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;;;;AAMA,MAAa,qBAAqB;AAElC,MAAM,iBAAiB;AAUvB,SAAS,cAAc,OAAqC;CAC1D,OAAQ,cAAoC,SAAS,KAAK;AAC5D;;;;;;;AAQA,SAAgB,mBAAmB,MAAoC;CACrE,MAAM,SAAS,WAAW,IAAI;CAC9B,OAAO,UAAU,cAAc,OAAO,IAAI,IAAI;EAAE,GAAG;EAAQ,MAAM,OAAO;CAAK,IAAI;AACnF;;AAGA,SAAS,WAAW,MAAwE;CAC1F,MAAM,QAAQ,eAAe,KAAK,IAAI;CACtC,IAAI,CAAC,OAAO,OAAO;CACnB,MAAM,OAAO,KAAK,MAAM,MAAM,EAAE,CAAC,MAAM;CACvC,MAAM,UAAU,KAAK,QAAQ,IAAI;CACjC,MAAM,SAAS,YAAY,KAAK,OAAO,KAAK,MAAM,GAAG,OAAO,EAAA,CAAG,KAAK;CACpE,MAAM,WAAW,MAAM,EAAE,CAAC,UAAU,YAAY,KAAK,KAAK,SAAS,UAAU;CAC7E,OAAO;EAAE,MAAM,MAAM,EAAE,CAAE,YAAY;EAAG;EAAO;CAAS;AAC1D;;;;;;;;;;;;;;;;;;;AAoBA,MAAa,kBAAkB;CAC7B;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;AAIA,MAAa,mBAAmB,SAAwB,KAAK,KAAK,YAAY,EAAE;;;;;;;;;AAUhF,MAAa,uBAAuB;CAClC,YAAY;EACV,KAAK,gBAAgB,MAAM;EAC3B;EACA,KAAK,gBAAgB,MAAM;EAC3B;EACA;EACA,KAAK,gBAAgB,MAAM;EAC3B;EACA;EACA;CACF;CACA,SAAS;EACP,KAAK,gBAAgB,MAAM;EAC3B;EACA;EACA,KAAK,gBAAgB,MAAM;EAC3B;CACF;CACA,OAAO;EACL,KAAK,gBAAgB,OAAO;EAC5B;EACA;EACA;CACF;CACA,OAAO;EAAC,KAAK,gBAAgB,OAAO;EAAK;EAA4B;CAAmB;CACxF,SAAS,CAAC,KAAK,gBAAgB,SAAS,EAAE,YAAY,0BAA0B;CAChF,SAAS,CAAC,KAAK,gBAAgB,SAAS,KAAK,0CAA0C;AACzF;AAEA,SAAS,gBAAgB,OAAuC;CAC9D,OAAQ,gBAAsC,SAAS,KAAK;AAC9D;;AAWA,SAAgB,qBAAqB,MAAsC;CACzE,MAAM,SAAS,WAAW,IAAI;CAC9B,OAAO,UAAU,gBAAgB,OAAO,IAAI,IAAI;EAAE,GAAG;EAAQ,MAAM,OAAO;CAAK,IAAI;AACrF;;;;;AAMA,MAAa,sBAAsB;AAEnC,MAAM,cAAc;AACpB,MAAM,aAAa;;;;;;AAOnB,SAAgB,WAAW,MAAyC;CAClE,IAAI,CAAC,MAAM,OAAO;CAClB,MAAM,QAAQ,YAAY,KAAK,IAAI;CACnC,IAAI,OAAO,QAAQ,MAAM,MAAM,MAAM,MAAM,MAAM,MAAM,GAAA,CAAI,KAAK;CAChE,MAAM,OAAO,KAAK,KAAK;CACvB,OAAO,WAAW,KAAK,IAAI,IAAI,OAAO;AACxC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The fence read last time: its body up to `clean` is complete lines, none of which closes it. A
|
|
3
|
+
* fence that grows is read once per token, and the next read starts where this one stopped.
|
|
4
|
+
*/
|
|
5
|
+
export interface FenceMemory {
|
|
6
|
+
read: {
|
|
7
|
+
fence: string;
|
|
8
|
+
body: string;
|
|
9
|
+
clean: number;
|
|
10
|
+
} | null;
|
|
11
|
+
}
|
|
12
|
+
/** A place for it. A block that draws a growing fence keeps its own; this one is the default. */
|
|
13
|
+
export declare const createFenceMemory: () => FenceMemory;
|
|
14
|
+
/** An open fence: its language, the file it names, and the code written so far. */
|
|
15
|
+
export interface GrowingFence {
|
|
16
|
+
language: string | undefined;
|
|
17
|
+
title: string;
|
|
18
|
+
body: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The language and code of a block that is one open fence, closed by the healer; else null.
|
|
22
|
+
* `closer` is the healer's line when it is kept beside `source` (a newline, unless the source ends
|
|
23
|
+
* in one, and the markers): the block is read as the two joined, without joining them.
|
|
24
|
+
*/
|
|
25
|
+
export declare function growingFence(source: string, closer?: string, memory?: FenceMemory): GrowingFence | null;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { fenceTitle } from "../dialect.js";
|
|
2
|
+
// src/document/fence.ts
|
|
3
|
+
/**
|
|
4
|
+
* A block that is one code fence still being written, read without parsing the block: what a
|
|
5
|
+
* renderer needs to draw a long fence in pieces, a token costing only its last lines.
|
|
6
|
+
*/
|
|
7
|
+
const OPENING = /^(`{3,}|~{3,})([^\n`]*)\n/;
|
|
8
|
+
/** Whether the line of `text` from `from` to `to` closes a fence: `length` markers or more, alone. */
|
|
9
|
+
function closesFence(text, from, to, marker, length, indent) {
|
|
10
|
+
let at = from;
|
|
11
|
+
while (at < to && at - from < indent && text[at] === " ") at += 1;
|
|
12
|
+
const run = at;
|
|
13
|
+
while (at < to && text[at] === marker) at += 1;
|
|
14
|
+
if (at - run < length) return false;
|
|
15
|
+
while (at < to && (text[at] === " " || text[at] === " ")) at += 1;
|
|
16
|
+
return at === to;
|
|
17
|
+
}
|
|
18
|
+
/** A place for it. A block that draws a growing fence keeps its own; this one is the default. */
|
|
19
|
+
const createFenceMemory = () => ({ read: null });
|
|
20
|
+
const shared = createFenceMemory();
|
|
21
|
+
/** `rest` without its last line, when that line closes the fence: the healer's is not code. */
|
|
22
|
+
function unclosed(rest, marker, length) {
|
|
23
|
+
const closing = rest.lastIndexOf("\n");
|
|
24
|
+
return closesFence(rest, closing + 1, rest.length, marker, length, 0) ? rest.slice(0, Math.max(closing, 0)) : rest;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The language and code of a block that is one open fence, closed by the healer; else null.
|
|
28
|
+
* `closer` is the healer's line when it is kept beside `source` (a newline, unless the source ends
|
|
29
|
+
* in one, and the markers): the block is read as the two joined, without joining them.
|
|
30
|
+
*/
|
|
31
|
+
function growingFence(source, closer = "", memory = shared) {
|
|
32
|
+
if (closer && !source.includes("\n")) return growingFence(source + closer, "", memory);
|
|
33
|
+
const opening = OPENING.exec(source);
|
|
34
|
+
if (!opening) return null;
|
|
35
|
+
const fence = opening[1] ?? "```";
|
|
36
|
+
const marker = fence[0] ?? "`";
|
|
37
|
+
const rest = source.slice(opening[0].length);
|
|
38
|
+
const markers = closer.trimStart();
|
|
39
|
+
let body;
|
|
40
|
+
if (!closer) body = unclosed(rest, marker, fence.length);
|
|
41
|
+
else if (markers[0] !== marker || markers.length < fence.length) body = unclosed(rest + closer, marker, fence.length);
|
|
42
|
+
else body = closer !== markers || !rest ? rest : rest.slice(0, -1);
|
|
43
|
+
const read = memory.read;
|
|
44
|
+
let at = read !== null && read.fence === fence && body.length >= read.clean && body.startsWith(read.body.slice(0, read.clean)) ? read.clean : 0;
|
|
45
|
+
memory.read = null;
|
|
46
|
+
for (let end = body.indexOf("\n", at); end !== -1; end = body.indexOf("\n", at)) {
|
|
47
|
+
if (closesFence(body, at, end, marker, fence.length, 3)) return null;
|
|
48
|
+
at = end + 1;
|
|
49
|
+
}
|
|
50
|
+
if (closesFence(body, at, body.length, marker, fence.length, 3)) return null;
|
|
51
|
+
memory.read = {
|
|
52
|
+
fence,
|
|
53
|
+
body,
|
|
54
|
+
clean: at
|
|
55
|
+
};
|
|
56
|
+
const info = opening[2]?.trim() ?? "";
|
|
57
|
+
const language = info.split(/\s+/)[0] || void 0;
|
|
58
|
+
return {
|
|
59
|
+
language,
|
|
60
|
+
title: fenceTitle(info.slice(language?.length ?? 0)),
|
|
61
|
+
body
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/** How many lines a piece of a growing fence holds. */
|
|
65
|
+
const LINES = 40;
|
|
66
|
+
/**
|
|
67
|
+
* Cuts a body that grows at its end into its pieces. It remembers where the complete pieces end,
|
|
68
|
+
* so a token costs the last piece and not a count of every line above it.
|
|
69
|
+
*/
|
|
70
|
+
function createCutter() {
|
|
71
|
+
let text = "";
|
|
72
|
+
/** Where each complete piece ends in `text`. */
|
|
73
|
+
let ends = [];
|
|
74
|
+
let run = 0;
|
|
75
|
+
return (body) => {
|
|
76
|
+
const done = ends.at(-1) ?? 0;
|
|
77
|
+
if (done > 0 && !(body.length >= done && body.startsWith(text.slice(0, done)))) {
|
|
78
|
+
ends = [];
|
|
79
|
+
run += 1;
|
|
80
|
+
}
|
|
81
|
+
let start = ends.at(-1) ?? 0;
|
|
82
|
+
let line = 0;
|
|
83
|
+
for (let at = body.indexOf("\n", start); at !== -1; at = body.indexOf("\n", at + 1)) {
|
|
84
|
+
line += 1;
|
|
85
|
+
if (line % LINES === 0) {
|
|
86
|
+
start = at + 1;
|
|
87
|
+
ends.push(start);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
text = body;
|
|
91
|
+
const parts = ends.map((end, index) => body.slice(ends[index - 1] ?? 0, end));
|
|
92
|
+
parts.push(body.slice(start));
|
|
93
|
+
return {
|
|
94
|
+
parts,
|
|
95
|
+
run
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
export { createCutter, createFenceMemory, growingFence };
|
|
100
|
+
|
|
101
|
+
//# sourceMappingURL=fence.js.map
|
|
@@ -0,0 +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 &&\n read.fence === fence &&\n body.length >= read.clean &&\n body.startsWith(read.body.slice(0, read.clean))\n ? read.clean\n : 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 && !(body.length >= done && body.startsWith(text.slice(0, 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":";;;;;;AAMA,MAAM,UAAU;;AAGhB,SAAS,YACP,MACA,MACA,IACA,QACA,QACA,QACS;CACT,IAAI,KAAK;CACT,OAAO,KAAK,MAAM,KAAK,OAAO,UAAU,KAAK,QAAQ,KAAK,MAAM;CAChE,MAAM,MAAM;CACZ,OAAO,KAAK,MAAM,KAAK,QAAQ,QAAQ,MAAM;CAC7C,IAAI,KAAK,MAAM,QAAQ,OAAO;CAC9B,OAAO,KAAK,OAAO,KAAK,QAAQ,OAAO,KAAK,QAAQ,MAAO,MAAM;CACjE,OAAO,OAAO;AAChB;;AAWA,MAAa,2BAAwC,EAAE,MAAM,KAAK;AAElE,MAAM,SAAS,kBAAkB;;AAGjC,SAAS,SAAS,MAAc,QAAgB,QAAwB;CACtE,MAAM,UAAU,KAAK,YAAY,IAAI;CACrC,OAAO,YAAY,MAAM,UAAU,GAAG,KAAK,QAAQ,QAAQ,QAAQ,CAAC,IAChE,KAAK,MAAM,GAAG,KAAK,IAAI,SAAS,CAAC,CAAC,IAClC;AACN;;;;;;AAcA,SAAgB,aACd,QACA,SAAS,IACT,SAAsB,QACD;CAErB,IAAI,UAAU,CAAC,OAAO,SAAS,IAAI,GAAG,OAAO,aAAa,SAAS,QAAQ,IAAI,MAAM;CACrF,MAAM,UAAU,QAAQ,KAAK,MAAM;CACnC,IAAI,CAAC,SAAS,OAAO;CACrB,MAAM,QAAQ,QAAQ,MAAM;CAC5B,MAAM,SAAS,MAAM,MAAM;CAC3B,MAAM,OAAO,OAAO,MAAM,QAAQ,EAAE,CAAC,MAAM;CAC3C,MAAM,UAAU,OAAO,UAAU;CACjC,IAAI;CACJ,IAAI,CAAC,QAAQ,OAAO,SAAS,MAAM,QAAQ,MAAM,MAAM;MAClD,IAAI,QAAQ,OAAO,UAAU,QAAQ,SAAS,MAAM,QAEvD,OAAO,SAAS,OAAO,QAAQ,QAAQ,MAAM,MAAM;MAEhD,OAAO,WAAW,WAAW,CAAC,OAAO,OAAO,KAAK,MAAM,GAAG,EAAE;CAEjE,MAAM,OAAO,OAAO;CACpB,IAAI,KACF,SAAS,QACT,KAAK,UAAU,SACf,KAAK,UAAU,KAAK,SACpB,KAAK,WAAW,KAAK,KAAK,MAAM,GAAG,KAAK,KAAK,CAAC,IAC1C,KAAK,QACL;CACN,OAAO,OAAO;CACd,KAAK,IAAI,MAAM,KAAK,QAAQ,MAAM,EAAE,GAAG,QAAQ,IAAI,MAAM,KAAK,QAAQ,MAAM,EAAE,GAAG;EAC/E,IAAI,YAAY,MAAM,IAAI,KAAK,QAAQ,MAAM,QAAQ,CAAC,GAAG,OAAO;EAChE,KAAK,MAAM;CACb;CAEA,IAAI,YAAY,MAAM,IAAI,KAAK,QAAQ,QAAQ,MAAM,QAAQ,CAAC,GAAG,OAAO;CACxE,OAAO,OAAO;EAAE;EAAO;EAAM,OAAO;CAAG;CACvC,MAAM,OAAO,QAAQ,EAAE,EAAE,KAAK,KAAK;CACnC,MAAM,WAAW,KAAK,MAAM,KAAK,CAAC,CAAC,MAAM,KAAA;CACzC,OAAO;EAAE;EAAU,OAAO,WAAW,KAAK,MAAM,UAAU,UAAU,CAAC,CAAC;EAAG;CAAK;AAChF;;AAGA,MAAM,QAAQ;;;;;AAgBd,SAAgB,eAAsC;CACpD,IAAI,OAAO;;CAEX,IAAI,OAAiB,CAAC;CACtB,IAAI,MAAM;CACV,QAAQ,SAAS;EACf,MAAM,OAAO,KAAK,GAAG,EAAE,KAAK;EAC5B,IAAI,OAAO,KAAK,EAAE,KAAK,UAAU,QAAQ,KAAK,WAAW,KAAK,MAAM,GAAG,IAAI,CAAC,IAAI;GAC9E,OAAO,CAAC;GACR,OAAO;EACT;EACA,IAAI,QAAQ,KAAK,GAAG,EAAE,KAAK;EAC3B,IAAI,OAAO;EACX,KAAK,IAAI,KAAK,KAAK,QAAQ,MAAM,KAAK,GAAG,OAAO,IAAI,KAAK,KAAK,QAAQ,MAAM,KAAK,CAAC,GAAG;GACnF,QAAQ;GACR,IAAI,OAAO,UAAU,GAAG;IACtB,QAAQ,KAAK;IACb,KAAK,KAAK,KAAK;GACjB;EACF;EACA,OAAO;EAEP,MAAM,QAAQ,KAAK,KAAK,KAAK,UAAU,KAAK,MAAM,KAAK,QAAQ,MAAM,GAAG,GAAG,CAAC;EAC5E,MAAM,KAAK,KAAK,MAAM,KAAK,CAAC;EAC5B,OAAO;GAAE;GAAO;EAAI;CACtB;AACF"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { MarkdownBlock, isFenceBlock, splitBlocks } from "../core/blocks.js";
|
|
2
|
+
import { FenceMemory, GrowingFence, createFenceMemory, growingFence } from "./fence.js";
|
|
3
|
+
import { TableRows, tableRows } from "./table.js";
|
|
4
|
+
import { flowdownDialect } from "../core/remark-answer-blocks.js";
|
|
5
|
+
import { Form, FormName, formOf } from "../core/forms.js";
|
|
6
|
+
export interface UpdateOptions {
|
|
7
|
+
/** The text is a reply still arriving: its unfinished end is completed. */
|
|
8
|
+
streaming?: boolean;
|
|
9
|
+
/** The renderer typesets `$…$`: a literal dollar (`$5`) is escaped so it is not read as one. */
|
|
10
|
+
math?: boolean;
|
|
11
|
+
/** Work out an id for every heading, numbered across the document (`setup`, `setup-1`). */
|
|
12
|
+
headingIds?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface DocumentState {
|
|
15
|
+
/**
|
|
16
|
+
* The document's top-level blocks, in order. A block that reads the same as at the last update
|
|
17
|
+
* is the same object: compare by identity to know what to draw again.
|
|
18
|
+
*/
|
|
19
|
+
blocks: readonly MarkdownBlock[];
|
|
20
|
+
/**
|
|
21
|
+
* The line that closes the code fence the text ends inside, or ''. It belongs after the last
|
|
22
|
+
* block's source and is kept beside it, not joined: a long fence is then not copied on every
|
|
23
|
+
* token. A renderer reads the last block as `source + closer`.
|
|
24
|
+
*/
|
|
25
|
+
closer: string;
|
|
26
|
+
/** Whether the text ends inside a code fence that is still being written. */
|
|
27
|
+
growing: boolean;
|
|
28
|
+
/** Each block's heading ids, by where the heading starts in the document; null when not asked for. */
|
|
29
|
+
headingIds: ReadonlyArray<ReadonlyMap<number, string>> | null;
|
|
30
|
+
}
|
|
31
|
+
export interface MarkdownDocument {
|
|
32
|
+
/** The document for `text`. The same text and options give back the same state object. */
|
|
33
|
+
update(text: string, options?: UpdateOptions): DocumentState;
|
|
34
|
+
}
|
|
35
|
+
/** Whether any block has a formula: a renderer that loads its typesetter on demand asks this. */
|
|
36
|
+
export declare const usesMath: (blocks: readonly MarkdownBlock[]) => boolean;
|
|
37
|
+
/** Whether any block has raw HTML in its prose. */
|
|
38
|
+
export declare const usesHtml: (blocks: readonly MarkdownBlock[]) => boolean;
|
|
39
|
+
/** Whether any block names a character reference outside the common ones (`↛`). */
|
|
40
|
+
export declare const usesRareEntity: (blocks: readonly MarkdownBlock[]) => boolean;
|
|
41
|
+
/**
|
|
42
|
+
* One document that is drawn more than once: a reply while it streams, a text being edited. It
|
|
43
|
+
* remembers the blocks it cut, so an update costs the end of the text, not the text.
|
|
44
|
+
*/
|
|
45
|
+
export declare function createDocument(): MarkdownDocument;
|
|
46
|
+
export { type FenceMemory, type Form, type FormName, type GrowingFence, type MarkdownBlock, type TableRows, createFenceMemory, flowdownDialect, formOf, growingFence, isFenceBlock, splitBlocks, tableRows };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { blockHasHtml, blockHasMath, blockHasRareEntity, escapedBlock, headingIdsOfBlocks } from "../core/block-facts.js";
|
|
2
|
+
import { createBlockSplitter, isFenceBlock, normalizeLineEndings, splitBlocks } from "../core/blocks.js";
|
|
3
|
+
import { createFenceScanner } from "../core/fence-scan.js";
|
|
4
|
+
import { closePartialProse } from "../core/heal.js";
|
|
5
|
+
import { createFenceMemory, growingFence } from "./fence.js";
|
|
6
|
+
import { tableRows } from "./table.js";
|
|
7
|
+
import { flowdownDialect } from "../core/remark-answer-blocks.js";
|
|
8
|
+
import { formOf } from "../core/forms.js";
|
|
9
|
+
// src/document/index.ts
|
|
10
|
+
/**
|
|
11
|
+
* `@localix-ai/flowdown/document`: a Markdown text as the blocks a renderer draws, kept up while it grows.
|
|
12
|
+
*
|
|
13
|
+
* This is the part of streaming that belongs to no renderer. A reply arrives a few characters at a
|
|
14
|
+
* time; each update the text is completed where it stops mid-construct, cut into its top-level
|
|
15
|
+
* blocks, and the blocks that did not change are handed back as the very same objects. A renderer
|
|
16
|
+
* for any target (React, the DOM, a terminal) then draws a block once and, on the next token,
|
|
17
|
+
* only the last one again:
|
|
18
|
+
*
|
|
19
|
+
* const document = createDocument()
|
|
20
|
+
* for await (const text of reply) {
|
|
21
|
+
* const { blocks } = document.update(text, { streaming: true })
|
|
22
|
+
* // draw the blocks that are new objects; the rest are already on the page
|
|
23
|
+
* }
|
|
24
|
+
* document.update(text) // the finished reply: nothing is completed any more
|
|
25
|
+
*/
|
|
26
|
+
/** Whether any block has a formula: a renderer that loads its typesetter on demand asks this. */
|
|
27
|
+
const usesMath = (blocks) => blocks.some(blockHasMath);
|
|
28
|
+
/** Whether any block has raw HTML in its prose. */
|
|
29
|
+
const usesHtml = (blocks) => blocks.some(blockHasHtml);
|
|
30
|
+
/** Whether any block names a character reference outside the common ones (`↛`). */
|
|
31
|
+
const usesRareEntity = (blocks) => blocks.some(blockHasRareEntity);
|
|
32
|
+
/**
|
|
33
|
+
* One document that is drawn more than once: a reply while it streams, a text being edited. It
|
|
34
|
+
* remembers the blocks it cut, so an update costs the end of the text, not the text.
|
|
35
|
+
*/
|
|
36
|
+
function createDocument() {
|
|
37
|
+
const openFence = createFenceScanner();
|
|
38
|
+
const splitter = createBlockSplitter();
|
|
39
|
+
let last = null;
|
|
40
|
+
return { update(input, { streaming = false, math = false, headingIds = false } = {}) {
|
|
41
|
+
const text = normalizeLineEndings(input);
|
|
42
|
+
if (last !== null && last.text === text && last.streaming === streaming && last.math === math && last.headingIds === headingIds) return last.state;
|
|
43
|
+
const fence = streaming ? openFence(text) : null;
|
|
44
|
+
const closer = fence ? `${text.endsWith("\n") ? "" : "\n"}${fence.marker}` : "";
|
|
45
|
+
const healed = streaming && !fence ? closePartialProse(text) : text;
|
|
46
|
+
const cut = splitter.split(healed);
|
|
47
|
+
const blocks = math ? cut.map(escapedBlock) : cut;
|
|
48
|
+
const state = {
|
|
49
|
+
blocks,
|
|
50
|
+
closer,
|
|
51
|
+
growing: fence !== null,
|
|
52
|
+
headingIds: headingIds ? headingIdsOfBlocks(blocks) : null
|
|
53
|
+
};
|
|
54
|
+
last = {
|
|
55
|
+
text,
|
|
56
|
+
streaming,
|
|
57
|
+
math,
|
|
58
|
+
headingIds,
|
|
59
|
+
state
|
|
60
|
+
};
|
|
61
|
+
return state;
|
|
62
|
+
} };
|
|
63
|
+
}
|
|
64
|
+
export { createDocument, createFenceMemory, flowdownDialect, formOf, growingFence, isFenceBlock, splitBlocks, tableRows, usesHtml, usesMath, usesRareEntity };
|
|
65
|
+
|
|
66
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/document/index.ts"],"sourcesContent":["/**\n * `@localix-ai/flowdown/document`: a Markdown text as the blocks a renderer draws, kept up while it grows.\n *\n * This is the part of streaming that belongs to no renderer. A reply arrives a few characters at a\n * time; each update the text is completed where it stops mid-construct, cut into its top-level\n * blocks, and the blocks that did not change are handed back as the very same objects. A renderer\n * for any target (React, the DOM, a terminal) then draws a block once and, on the next token,\n * only the last one again:\n *\n * const document = createDocument()\n * for await (const text of reply) {\n * const { blocks } = document.update(text, { streaming: true })\n * // draw the blocks that are new objects; the rest are already on the page\n * }\n * document.update(text) // the finished reply: nothing is completed any more\n */\nimport {\n blockHasHtml,\n blockHasMath,\n blockHasRareEntity,\n escapedBlock,\n headingIdsOfBlocks,\n} from '../core/block-facts'\nimport { createBlockSplitter, normalizeLineEndings, type MarkdownBlock } from '../core/blocks'\nimport { createFenceScanner } from '../core/fence-scan'\nimport { closePartialProse } from '../core/heal'\n\nexport { splitBlocks, isFenceBlock, type MarkdownBlock } from '../core/blocks'\nexport { growingFence, createFenceMemory, type FenceMemory, type GrowingFence } from './fence'\nexport { tableRows, type TableRows } from './table'\n/**\n * The dialect for `parse` of `@localix-ai/flowdown/engine`, and what it marked a node as: a target of your\n * own passes `extensions: [flowdownDialect]` and switches on `formOf(node)?.form`.\n */\nexport { flowdownDialect } from '../core/remark-answer-blocks'\nexport { formOf, type Form, type FormName } from '../core/forms'\n\nexport interface UpdateOptions {\n /** The text is a reply still arriving: its unfinished end is completed. */\n streaming?: boolean\n /** The renderer typesets `$…$`: a literal dollar (`$5`) is escaped so it is not read as one. */\n math?: boolean\n /** Work out an id for every heading, numbered across the document (`setup`, `setup-1`). */\n headingIds?: boolean\n}\n\nexport interface DocumentState {\n /**\n * The document's top-level blocks, in order. A block that reads the same as at the last update\n * is the same object: compare by identity to know what to draw again.\n */\n blocks: readonly MarkdownBlock[]\n /**\n * The line that closes the code fence the text ends inside, or ''. It belongs after the last\n * block's source and is kept beside it, not joined: a long fence is then not copied on every\n * token. A renderer reads the last block as `source + closer`.\n */\n closer: string\n /** Whether the text ends inside a code fence that is still being written. */\n growing: boolean\n /** Each block's heading ids, by where the heading starts in the document; null when not asked for. */\n headingIds: ReadonlyArray<ReadonlyMap<number, string>> | null\n}\n\nexport interface MarkdownDocument {\n /** The document for `text`. The same text and options give back the same state object. */\n update(text: string, options?: UpdateOptions): DocumentState\n}\n\n/** Whether any block has a formula: a renderer that loads its typesetter on demand asks this. */\nexport const usesMath = (blocks: readonly MarkdownBlock[]): boolean => blocks.some(blockHasMath)\n\n/** Whether any block has raw HTML in its prose. */\nexport const usesHtml = (blocks: readonly MarkdownBlock[]): boolean => blocks.some(blockHasHtml)\n\n/** Whether any block names a character reference outside the common ones (`↛`). */\nexport const usesRareEntity = (blocks: readonly MarkdownBlock[]): boolean =>\n blocks.some(blockHasRareEntity)\n\n/**\n * One document that is drawn more than once: a reply while it streams, a text being edited. It\n * remembers the blocks it cut, so an update costs the end of the text, not the text.\n */\nexport function createDocument(): MarkdownDocument {\n // The scan and the split are this document's own: another reply streaming beside it does not\n // make either start over.\n const openFence = createFenceScanner()\n const splitter = createBlockSplitter()\n let last: {\n text: string\n streaming: boolean\n math: boolean\n headingIds: boolean\n state: DocumentState\n } | null = null\n\n return {\n update(input, { streaming = false, math = false, headingIds = false } = {}) {\n // `\\r\\n` from a Windows file or a server that sends it reads as `\\n` everywhere below.\n const text = normalizeLineEndings(input)\n if (\n last !== null &&\n last.text === text &&\n last.streaming === streaming &&\n last.math === math &&\n last.headingIds === headingIds\n )\n return last.state\n // Inside a fence left open, the reply is read as it is and the line that closes the fence\n // goes to the last block beside its text: joined here, every token would copy the reply.\n const fence = streaming ? openFence(text) : null\n const closer = fence ? `${text.endsWith('\\n') ? '' : '\\n'}${fence.marker}` : ''\n const healed = streaming && !fence ? closePartialProse(text) : text\n // Blocks that did not change come back as the same objects, and everything worked out\n // from a block is kept beside it.\n const cut = splitter.split(healed)\n // Literal dollars are escaped only where `$` can mean math: elsewhere positions stay exact.\n const blocks = math ? cut.map(escapedBlock) : cut\n const state: DocumentState = {\n blocks,\n closer,\n growing: fence !== null,\n headingIds: headingIds ? headingIdsOfBlocks(blocks) : null,\n }\n last = { text, streaming, math, headingIds, state }\n return state\n },\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAsEA,MAAa,YAAY,WAA8C,OAAO,KAAK,YAAY;;AAG/F,MAAa,YAAY,WAA8C,OAAO,KAAK,YAAY;;AAG/F,MAAa,kBAAkB,WAC7B,OAAO,KAAK,kBAAkB;;;;;AAMhC,SAAgB,iBAAmC;CAGjD,MAAM,YAAY,mBAAmB;CACrC,MAAM,WAAW,oBAAoB;CACrC,IAAI,OAMO;CAEX,OAAO,EACL,OAAO,OAAO,EAAE,YAAY,OAAO,OAAO,OAAO,aAAa,UAAU,CAAC,GAAG;EAE1E,MAAM,OAAO,qBAAqB,KAAK;EACvC,IACE,SAAS,QACT,KAAK,SAAS,QACd,KAAK,cAAc,aACnB,KAAK,SAAS,QACd,KAAK,eAAe,YAEpB,OAAO,KAAK;EAGd,MAAM,QAAQ,YAAY,UAAU,IAAI,IAAI;EAC5C,MAAM,SAAS,QAAQ,GAAG,KAAK,SAAS,IAAI,IAAI,KAAK,OAAO,MAAM,WAAW;EAC7E,MAAM,SAAS,aAAa,CAAC,QAAQ,kBAAkB,IAAI,IAAI;EAG/D,MAAM,MAAM,SAAS,MAAM,MAAM;EAEjC,MAAM,SAAS,OAAO,IAAI,IAAI,YAAY,IAAI;EAC9C,MAAM,QAAuB;GAC3B;GACA;GACA,SAAS,UAAU;GACnB,YAAY,aAAa,mBAAmB,MAAM,IAAI;EACxD;EACA,OAAO;GAAE;GAAM;GAAW;GAAM;GAAY;EAAM;EAClD,OAAO;CACT,EACF;AACF"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** A table's header (its two lines, each with its line break) and its body rows. */
|
|
2
|
+
export interface TableRows {
|
|
3
|
+
head: string;
|
|
4
|
+
rows: string[];
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* The header (its two lines) and the body rows of a block that is one table and nothing else:
|
|
8
|
+
* every line opens with a pipe, so none of them can be anything but a row. Null for any other
|
|
9
|
+
* block, and for a table this cannot read with certainty; those are drawn by one parse, as ever.
|
|
10
|
+
*/
|
|
11
|
+
export declare function tableRows(source: string): TableRows | null;
|