@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,88 @@
|
|
|
1
|
+
import { closesInlineMath, opensInlineMath } from "./math-delimiters.js";
|
|
2
|
+
import { isEscaped, markdownProseWithoutCode } from "./prose.js";
|
|
3
|
+
// src/core/dollars.ts
|
|
4
|
+
/**
|
|
5
|
+
* Which `$` in a reply is a dollar sign rather than a math delimiter — one rule for the renderer
|
|
6
|
+
* and for the streaming stabilizer.
|
|
7
|
+
*
|
|
8
|
+
* remark-math pairs ANY two single dollars in a paragraph, so the moment a reply held one real
|
|
9
|
+
* formula, "costs $5 and $10" was typeset as the formula `5 and`, and "$HOME/bin and $PATH" became
|
|
10
|
+
* italic math that also swallowed the backticks after it. The sniffer that decides whether to load
|
|
11
|
+
* KaTeX already knew better; the parser did not. So the literal dollars are escaped (`\$`) before
|
|
12
|
+
* the parser sees them, which CommonMark renders as a plain `$` — and the rule is kept here, once,
|
|
13
|
+
* because the stabilizer must make the same call about the dollar a streaming tail ends on.
|
|
14
|
+
*/
|
|
15
|
+
/** A currency amount: digits, an optional magnitude, then a non-math character (`/` for a price per
|
|
16
|
+
* unit, `$5/GB`) or the end. */
|
|
17
|
+
const CURRENCY = /^\d[\d.,]*(?:[kKmMbB])?(?=[\s.,;:!?)\]'"`–—/-]|$)/;
|
|
18
|
+
/** A shell or environment variable: an upper-case name, ended by a path or list separator. */
|
|
19
|
+
const VARIABLE = /^[A-Z_][A-Z0-9_]+(?=[/:.,;)\]'"`]|$)/;
|
|
20
|
+
/** A longer upper-case name also ends at a space (`$PATH before`); `$AB = CD$` stays math. */
|
|
21
|
+
const LONG_VARIABLE = /^[A-Z_][A-Z0-9_]{2,}(?=\s)/;
|
|
22
|
+
/**
|
|
23
|
+
* Whether the `$` at `index` is a dollar sign by its own look: a price or a shell variable. A `$`
|
|
24
|
+
* that pairs with no other (`$ x`, the shell line `$(seq 1 12) … $i`) is text as well, but that
|
|
25
|
+
* takes the whole paragraph to tell: `literalDollars` asks both.
|
|
26
|
+
*/
|
|
27
|
+
function isLiteralDollar(text, index) {
|
|
28
|
+
if (text[index] !== "$" || isEscaped(text, index)) return false;
|
|
29
|
+
if (text[index - 1] === "$" || text[index + 1] === "$") return false;
|
|
30
|
+
const after = text.slice(index + 1, index + 48);
|
|
31
|
+
return CURRENCY.test(after) || VARIABLE.test(after) || LONG_VARIABLE.test(after);
|
|
32
|
+
}
|
|
33
|
+
/** A single `$`, neither escaped nor part of a `$$` run. */
|
|
34
|
+
const isSingleDollar = (text, index) => text[index] === "$" && text[index - 1] !== "$" && text[index + 1] !== "$" && !isEscaped(text, index);
|
|
35
|
+
/** Where the paragraph holding `index` ends: inline math never crosses a blank line. */
|
|
36
|
+
function paragraphEnd(text, index) {
|
|
37
|
+
const blank = /\n[ \t]*\n/g;
|
|
38
|
+
blank.lastIndex = index;
|
|
39
|
+
return blank.exec(text)?.index ?? text.length;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The single `$` that closes the formula opened at `open`, or -1 when none in its paragraph does.
|
|
43
|
+
* A dollar sign (`$5`) is skipped: it is text, so it closes nothing.
|
|
44
|
+
*/
|
|
45
|
+
function inlineMathClose(text, open) {
|
|
46
|
+
const end = paragraphEnd(text, open);
|
|
47
|
+
for (let at = text.indexOf("$", open + 1); at !== -1 && at < end; at = text.indexOf("$", at + 1)) if (isSingleDollar(text, at) && !isLiteralDollar(text, at) && closesInlineMath(text, at)) return at;
|
|
48
|
+
return -1;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Every single `$` in prose that is text rather than a delimiter, read left to right the way the
|
|
52
|
+
* parser reads them: a price or a variable, a `$` that cannot open (`$ x`), and one that opens but
|
|
53
|
+
* finds no closing `$` in its paragraph. A `$` that closes a formula is not in the list.
|
|
54
|
+
*/
|
|
55
|
+
function literalDollars(prose) {
|
|
56
|
+
const literal = [];
|
|
57
|
+
for (let index = prose.indexOf("$"); index !== -1; index = prose.indexOf("$", index + 1)) {
|
|
58
|
+
if (prose[index + 1] === "$") {
|
|
59
|
+
while (prose[index + 1] === "$") index += 1;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (!isSingleDollar(prose, index)) continue;
|
|
63
|
+
const close = isLiteralDollar(prose, index) || !opensInlineMath(prose, index) ? -1 : inlineMathClose(prose, index);
|
|
64
|
+
if (close === -1) literal.push(index);
|
|
65
|
+
else index = close;
|
|
66
|
+
}
|
|
67
|
+
return literal;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The source with every dollar in prose that is not a delimiter escaped (`\$`), so a parser
|
|
71
|
+
* that pairs any two dollars (remark-math) reads the same formulas `<Markdown>` does. Code is left
|
|
72
|
+
* byte for byte.
|
|
73
|
+
*/
|
|
74
|
+
function escapeLiteralDollars(markdown) {
|
|
75
|
+
if (!markdown.includes("$")) return markdown;
|
|
76
|
+
const literal = literalDollars(markdownProseWithoutCode(markdown));
|
|
77
|
+
if (literal.length === 0) return markdown;
|
|
78
|
+
let escaped = "";
|
|
79
|
+
let copied = 0;
|
|
80
|
+
for (const index of literal) {
|
|
81
|
+
escaped += `${markdown.slice(copied, index)}\\`;
|
|
82
|
+
copied = index;
|
|
83
|
+
}
|
|
84
|
+
return escaped + markdown.slice(copied);
|
|
85
|
+
}
|
|
86
|
+
export { escapeLiteralDollars, inlineMathClose, isLiteralDollar, literalDollars };
|
|
87
|
+
|
|
88
|
+
//# sourceMappingURL=dollars.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dollars.js","names":[],"sources":["../../src/core/dollars.ts"],"sourcesContent":["import { closesInlineMath, opensInlineMath } from './math-delimiters'\nimport { isEscaped, markdownProseWithoutCode } from './prose'\n\n/**\n * Which `$` in a reply is a dollar sign rather than a math delimiter — one rule for the renderer\n * and for the streaming stabilizer.\n *\n * remark-math pairs ANY two single dollars in a paragraph, so the moment a reply held one real\n * formula, \"costs $5 and $10\" was typeset as the formula `5 and`, and \"$HOME/bin and $PATH\" became\n * italic math that also swallowed the backticks after it. The sniffer that decides whether to load\n * KaTeX already knew better; the parser did not. So the literal dollars are escaped (`\\$`) before\n * the parser sees them, which CommonMark renders as a plain `$` — and the rule is kept here, once,\n * because the stabilizer must make the same call about the dollar a streaming tail ends on.\n */\n\n/** A currency amount: digits, an optional magnitude, then a non-math character (`/` for a price per\n * unit, `$5/GB`) or the end. */\nconst CURRENCY = /^\\d[\\d.,]*(?:[kKmMbB])?(?=[\\s.,;:!?)\\]'\"`–—/-]|$)/\n/** A shell or environment variable: an upper-case name, ended by a path or list separator. */\nconst VARIABLE = /^[A-Z_][A-Z0-9_]+(?=[/:.,;)\\]'\"`]|$)/\n/** A longer upper-case name also ends at a space (`$PATH before`); `$AB = CD$` stays math. */\nconst LONG_VARIABLE = /^[A-Z_][A-Z0-9_]{2,}(?=\\s)/\n\n/**\n * Whether the `$` at `index` is a dollar sign by its own look: a price or a shell variable. A `$`\n * that pairs with no other (`$ x`, the shell line `$(seq 1 12) … $i`) is text as well, but that\n * takes the whole paragraph to tell: `literalDollars` asks both.\n */\nexport function isLiteralDollar(text: string, index: number): boolean {\n if (text[index] !== '$' || isEscaped(text, index)) return false\n // `$$` is display math, never a dollar sign.\n if (text[index - 1] === '$' || text[index + 1] === '$') return false\n const after = text.slice(index + 1, index + 48)\n return CURRENCY.test(after) || VARIABLE.test(after) || LONG_VARIABLE.test(after)\n}\n\n/** A single `$`, neither escaped nor part of a `$$` run. */\nconst isSingleDollar = (text: string, index: number) =>\n text[index] === '$' &&\n text[index - 1] !== '$' &&\n text[index + 1] !== '$' &&\n !isEscaped(text, index)\n\n/** Where the paragraph holding `index` ends: inline math never crosses a blank line. */\nfunction paragraphEnd(text: string, index: number): number {\n const blank = /\\n[ \\t]*\\n/g\n blank.lastIndex = index\n return blank.exec(text)?.index ?? text.length\n}\n\n/**\n * The single `$` that closes the formula opened at `open`, or -1 when none in its paragraph does.\n * A dollar sign (`$5`) is skipped: it is text, so it closes nothing.\n */\nexport function inlineMathClose(text: string, open: number): number {\n const end = paragraphEnd(text, open)\n for (\n let at = text.indexOf('$', open + 1);\n at !== -1 && at < end;\n at = text.indexOf('$', at + 1)\n ) {\n if (isSingleDollar(text, at) && !isLiteralDollar(text, at) && closesInlineMath(text, at))\n return at\n }\n return -1\n}\n\n/**\n * Every single `$` in prose that is text rather than a delimiter, read left to right the way the\n * parser reads them: a price or a variable, a `$` that cannot open (`$ x`), and one that opens but\n * finds no closing `$` in its paragraph. A `$` that closes a formula is not in the list.\n */\nexport function literalDollars(prose: string): number[] {\n const literal: number[] = []\n for (let index = prose.indexOf('$'); index !== -1; index = prose.indexOf('$', index + 1)) {\n if (prose[index + 1] === '$') {\n // A `$$` run is display math, or the parser's to pair by length: skip the whole run.\n while (prose[index + 1] === '$') index += 1\n continue\n }\n if (!isSingleDollar(prose, index)) continue\n const close =\n isLiteralDollar(prose, index) || !opensInlineMath(prose, index)\n ? -1\n : inlineMathClose(prose, index)\n if (close === -1) literal.push(index)\n else index = close\n }\n return literal\n}\n\n/**\n * The source with every dollar in prose that is not a delimiter escaped (`\\$`), so a parser\n * that pairs any two dollars (remark-math) reads the same formulas `<Markdown>` does. Code is left\n * byte for byte.\n */\nexport function escapeLiteralDollars(markdown: string): string {\n if (!markdown.includes('$')) return markdown\n const literal = literalDollars(markdownProseWithoutCode(markdown))\n if (literal.length === 0) return markdown\n let escaped = ''\n let copied = 0\n for (const index of literal) {\n escaped += `${markdown.slice(copied, index)}\\\\`\n copied = index\n }\n return escaped + markdown.slice(copied)\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAiBA,MAAM,WAAW;;AAEjB,MAAM,WAAW;;AAEjB,MAAM,gBAAgB;;;;;;AAOtB,SAAgB,gBAAgB,MAAc,OAAwB;CACpE,IAAI,KAAK,WAAW,OAAO,UAAU,MAAM,KAAK,GAAG,OAAO;CAE1D,IAAI,KAAK,QAAQ,OAAO,OAAO,KAAK,QAAQ,OAAO,KAAK,OAAO;CAC/D,MAAM,QAAQ,KAAK,MAAM,QAAQ,GAAG,QAAQ,EAAE;CAC9C,OAAO,SAAS,KAAK,KAAK,KAAK,SAAS,KAAK,KAAK,KAAK,cAAc,KAAK,KAAK;AACjF;;AAGA,MAAM,kBAAkB,MAAc,UACpC,KAAK,WAAW,OAChB,KAAK,QAAQ,OAAO,OACpB,KAAK,QAAQ,OAAO,OACpB,CAAC,UAAU,MAAM,KAAK;;AAGxB,SAAS,aAAa,MAAc,OAAuB;CACzD,MAAM,QAAQ;CACd,MAAM,YAAY;CAClB,OAAO,MAAM,KAAK,IAAI,CAAC,EAAE,SAAS,KAAK;AACzC;;;;;AAMA,SAAgB,gBAAgB,MAAc,MAAsB;CAClE,MAAM,MAAM,aAAa,MAAM,IAAI;CACnC,KACE,IAAI,KAAK,KAAK,QAAQ,KAAK,OAAO,CAAC,GACnC,OAAO,MAAM,KAAK,KAClB,KAAK,KAAK,QAAQ,KAAK,KAAK,CAAC,GAE7B,IAAI,eAAe,MAAM,EAAE,KAAK,CAAC,gBAAgB,MAAM,EAAE,KAAK,iBAAiB,MAAM,EAAE,GACrF,OAAO;CAEX,OAAO;AACT;;;;;;AAOA,SAAgB,eAAe,OAAyB;CACtD,MAAM,UAAoB,CAAC;CAC3B,KAAK,IAAI,QAAQ,MAAM,QAAQ,GAAG,GAAG,UAAU,IAAI,QAAQ,MAAM,QAAQ,KAAK,QAAQ,CAAC,GAAG;EACxF,IAAI,MAAM,QAAQ,OAAO,KAAK;GAE5B,OAAO,MAAM,QAAQ,OAAO,KAAK,SAAS;GAC1C;EACF;EACA,IAAI,CAAC,eAAe,OAAO,KAAK,GAAG;EACnC,MAAM,QACJ,gBAAgB,OAAO,KAAK,KAAK,CAAC,gBAAgB,OAAO,KAAK,IAC1D,KACA,gBAAgB,OAAO,KAAK;EAClC,IAAI,UAAU,IAAI,QAAQ,KAAK,KAAK;OAC/B,QAAQ;CACf;CACA,OAAO;AACT;;;;;;AAOA,SAAgB,qBAAqB,UAA0B;CAC7D,IAAI,CAAC,SAAS,SAAS,GAAG,GAAG,OAAO;CACpC,MAAM,UAAU,eAAe,yBAAyB,QAAQ,CAAC;CACjE,IAAI,QAAQ,WAAW,GAAG,OAAO;CACjC,IAAI,UAAU;CACd,IAAI,SAAS;CACb,KAAK,MAAM,SAAS,SAAS;EAC3B,WAAW,GAAG,SAAS,MAAM,QAAQ,KAAK,EAAE;EAC5C,SAAS;CACX;CACA,OAAO,UAAU,SAAS,MAAM,MAAM;AACxC"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// src/core/fence-scan.ts
|
|
2
|
+
const FENCE_LINE_RE = /^\s{0,3}(`{3,}|~{3,})(.*)$/;
|
|
3
|
+
/** Complete lines scanned so far; tests read it to prove the scan resumes instead of restarting. */
|
|
4
|
+
const fenceScanStats = { lines: 0 };
|
|
5
|
+
/** The fence state after one line, given the state before it. */
|
|
6
|
+
function step(open, line, lineEnd, textLength) {
|
|
7
|
+
const m = FENCE_LINE_RE.exec(line);
|
|
8
|
+
if (!m) return open;
|
|
9
|
+
if (open === null) {
|
|
10
|
+
if (m[1][0] === "`" && m[2].includes("`")) return null;
|
|
11
|
+
return {
|
|
12
|
+
marker: m[1],
|
|
13
|
+
bodyStart: Math.min(lineEnd, textLength)
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
return m[1][0] === open.marker[0] && m[1].length >= open.marker.length && m[2].trim() === "" ? null : open;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A scan that remembers one growing text. Each rendered document has its own: two replies that
|
|
20
|
+
* stream at once would otherwise take turns forgetting each other and scan from the start on
|
|
21
|
+
* every token.
|
|
22
|
+
*/
|
|
23
|
+
function createFenceScanner() {
|
|
24
|
+
let cached = {
|
|
25
|
+
prefix: "",
|
|
26
|
+
open: null
|
|
27
|
+
};
|
|
28
|
+
return (text) => {
|
|
29
|
+
let { prefix, open } = cached;
|
|
30
|
+
if (!text.startsWith(prefix)) {
|
|
31
|
+
prefix = "";
|
|
32
|
+
open = null;
|
|
33
|
+
}
|
|
34
|
+
let offset = prefix.length;
|
|
35
|
+
const lastNewline = text.lastIndexOf("\n");
|
|
36
|
+
while (offset <= lastNewline) {
|
|
37
|
+
const nl = text.indexOf("\n", offset);
|
|
38
|
+
open = step(open, text.slice(offset, nl), nl + 1, text.length);
|
|
39
|
+
fenceScanStats.lines += 1;
|
|
40
|
+
offset = nl + 1;
|
|
41
|
+
}
|
|
42
|
+
if (offset !== prefix.length) cached = {
|
|
43
|
+
prefix: text.slice(0, offset),
|
|
44
|
+
open
|
|
45
|
+
};
|
|
46
|
+
return offset < text.length || lastNewline === -1 ? step(open, text.slice(offset), text.length + 1, text.length) : open;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** The scan for a caller that follows one text at a time: `@localix-ai/flowdown/stream`'s functions. */
|
|
50
|
+
const openFence = createFenceScanner();
|
|
51
|
+
export { createFenceScanner, fenceScanStats, openFence };
|
|
52
|
+
|
|
53
|
+
//# sourceMappingURL=fence-scan.js.map
|
|
@@ -0,0 +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.startsWith(prefix)) {\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":";AAcA,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,KAAK,WAAW,MAAM,GAAG;GAC5B,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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading a ```tree fence: the directory layout models already draw with box characters, as rows
|
|
3
|
+
* a renderer can lay out (`FileTree.tsx`).
|
|
4
|
+
*
|
|
5
|
+
* src/
|
|
6
|
+
* ├── app.ts # entry point
|
|
7
|
+
* └── features/
|
|
8
|
+
* └── chat/
|
|
9
|
+
*
|
|
10
|
+
* Depth is the number of 4-column guides (`│ ` or four spaces) before a branch (`├── ` /
|
|
11
|
+
* `└── `); a line with no branch is a root. A row is a FOLDER when its name ends with `/` or the
|
|
12
|
+
* next row is deeper. A comment — after `#`, or after a run of two or more spaces — is set apart
|
|
13
|
+
* from the name. A line the grammar does not fit is kept as a plain row at its depth, so a tree
|
|
14
|
+
* still reads when a model draws it slightly off.
|
|
15
|
+
*/
|
|
16
|
+
export interface TreeRow {
|
|
17
|
+
depth: number;
|
|
18
|
+
name: string;
|
|
19
|
+
comment: string;
|
|
20
|
+
folder: boolean;
|
|
21
|
+
/** The row's place in the tree, `src/app/App.tsx` — a stable key for focus and collapse. */
|
|
22
|
+
path: string;
|
|
23
|
+
/** The nearest shallower row above it; null at the top level. */
|
|
24
|
+
parent: number | null;
|
|
25
|
+
}
|
|
26
|
+
export declare function parseFileTree(source: string): TreeRow[];
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// src/core/file-tree-parse.ts
|
|
2
|
+
const GUIDE = /^(?:│ {3}|\| {3}| {4})/;
|
|
3
|
+
const BRANCH = /^(?:├──|└──|\|--|`--|\+--)\s?/;
|
|
4
|
+
function readLine(line) {
|
|
5
|
+
let rest = line.replace(/\s+$/, "");
|
|
6
|
+
if (!rest.trim()) return null;
|
|
7
|
+
let depth = 0;
|
|
8
|
+
while (GUIDE.test(rest)) {
|
|
9
|
+
rest = rest.replace(GUIDE, "");
|
|
10
|
+
depth++;
|
|
11
|
+
}
|
|
12
|
+
const branch = BRANCH.exec(rest);
|
|
13
|
+
if (branch) {
|
|
14
|
+
rest = rest.slice(branch[0].length);
|
|
15
|
+
depth++;
|
|
16
|
+
} else rest = rest.trimStart();
|
|
17
|
+
const commentAt = rest.search(/\s+#|\s{2,}/);
|
|
18
|
+
const name = (commentAt === -1 ? rest : rest.slice(0, commentAt)).trim();
|
|
19
|
+
const comment = commentAt === -1 ? "" : rest.slice(commentAt).replace(/^\s*#?\s*/, "").trim();
|
|
20
|
+
return {
|
|
21
|
+
depth,
|
|
22
|
+
name,
|
|
23
|
+
comment
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function parseFileTree(source) {
|
|
27
|
+
const rows = source.split("\n").flatMap((line) => {
|
|
28
|
+
const row = readLine(line);
|
|
29
|
+
return row ? [row] : [];
|
|
30
|
+
});
|
|
31
|
+
const parsed = [];
|
|
32
|
+
const plain = [];
|
|
33
|
+
rows.forEach((row, index) => {
|
|
34
|
+
let parent = null;
|
|
35
|
+
for (let above = index - 1; above >= 0; above--) if (rows[above].depth < row.depth) {
|
|
36
|
+
parent = above;
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
plain.push(`${parent === null ? "" : `${plain[parent].replace(/\/$/, "")}/`}${row.name}`);
|
|
40
|
+
parsed.push({
|
|
41
|
+
...row,
|
|
42
|
+
folder: row.name.endsWith("/") || (rows[index + 1]?.depth ?? 0) > row.depth,
|
|
43
|
+
path: `${plain[index]}#${index}`,
|
|
44
|
+
parent
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
return parsed;
|
|
48
|
+
}
|
|
49
|
+
export { parseFileTree };
|
|
50
|
+
|
|
51
|
+
//# sourceMappingURL=file-tree-parse.js.map
|
|
@@ -0,0 +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.replace(/\\s+$/, '')\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,QAAQ,EAAE;CAClC,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"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// src/core/forms-html.ts
|
|
2
|
+
/** The element a form becomes; a form not here keeps the element its node already is. */
|
|
3
|
+
const TAGS = {
|
|
4
|
+
"quote-source": "footer",
|
|
5
|
+
figure: "figure",
|
|
6
|
+
caption: "figcaption",
|
|
7
|
+
"section-number": "span",
|
|
8
|
+
form: "div",
|
|
9
|
+
"step-head": "div",
|
|
10
|
+
"step-title": "span",
|
|
11
|
+
"step-meta": "span",
|
|
12
|
+
"step-body": "div",
|
|
13
|
+
stat: "div",
|
|
14
|
+
"stat-value": "span",
|
|
15
|
+
"stat-label": "span",
|
|
16
|
+
source: "div",
|
|
17
|
+
"source-title": "span",
|
|
18
|
+
"source-host": "span",
|
|
19
|
+
"source-note": "span",
|
|
20
|
+
tabs: "answer-tabs",
|
|
21
|
+
tab: "answer-tab",
|
|
22
|
+
verdict: "aside",
|
|
23
|
+
verdicts: "div",
|
|
24
|
+
details: "details",
|
|
25
|
+
summary: "summary"
|
|
26
|
+
};
|
|
27
|
+
/** Forms that are told apart by their element or an attribute, not by a class. */
|
|
28
|
+
const UNCLASSED = /* @__PURE__ */ new Set([
|
|
29
|
+
"callout",
|
|
30
|
+
"code",
|
|
31
|
+
"caption",
|
|
32
|
+
"tabs",
|
|
33
|
+
"tab",
|
|
34
|
+
"summary"
|
|
35
|
+
]);
|
|
36
|
+
const lowerToHtml = (form, data) => {
|
|
37
|
+
const name = form.form;
|
|
38
|
+
if (name === "callout") return { hProperties: {
|
|
39
|
+
dataCallout: form.kind,
|
|
40
|
+
...form.title ? { dataCalloutTitle: form.title } : {}
|
|
41
|
+
} };
|
|
42
|
+
if (name === "code") return { hProperties: {
|
|
43
|
+
...data?.hProperties,
|
|
44
|
+
dataTitle: form.title
|
|
45
|
+
} };
|
|
46
|
+
const tag = TAGS[name];
|
|
47
|
+
const properties = {};
|
|
48
|
+
if (!UNCLASSED.has(name)) properties.className = [`fd-${name}`];
|
|
49
|
+
if (name === "tab") properties.dataLabel = form.label;
|
|
50
|
+
if (name === "verdict") properties.dataVerdict = form.kind;
|
|
51
|
+
let has = false;
|
|
52
|
+
for (const _ in properties) {
|
|
53
|
+
has = true;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
...tag === void 0 ? null : { hName: tag },
|
|
58
|
+
...has ? { hProperties: properties } : null
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
export { lowerToHtml };
|
|
62
|
+
|
|
63
|
+
//# sourceMappingURL=forms-html.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"forms-html.js","names":[],"sources":["../../src/core/forms-html.ts"],"sourcesContent":["/**\n * The dialect's forms as HTML: the element each becomes and the class the stylesheet draws it by,\n * in the two fields remark and the engine's renderer both read (`hName`, `hProperties`).\n */\nimport type { FormName, Lowering } from './forms'\n\n/** The element a form becomes; a form not here keeps the element its node already is. */\nconst TAGS: Partial<Record<FormName, string>> = {\n 'quote-source': 'footer',\n figure: 'figure',\n caption: 'figcaption',\n 'section-number': 'span',\n form: 'div',\n 'step-head': 'div',\n 'step-title': 'span',\n 'step-meta': 'span',\n 'step-body': 'div',\n stat: 'div',\n 'stat-value': 'span',\n 'stat-label': 'span',\n source: 'div',\n 'source-title': 'span',\n 'source-host': 'span',\n 'source-note': 'span',\n tabs: 'answer-tabs',\n tab: 'answer-tab',\n verdict: 'aside',\n verdicts: 'div',\n details: 'details',\n summary: 'summary',\n}\n\n/** Forms that are told apart by their element or an attribute, not by a class. */\nconst UNCLASSED = new Set<FormName>(['callout', 'code', 'caption', 'tabs', 'tab', 'summary'])\n\nexport const lowerToHtml: Lowering = (form, data) => {\n const name = form.form\n if (name === 'callout')\n return {\n hProperties: {\n dataCallout: form.kind,\n ...(form.title ? { dataCalloutTitle: form.title } : {}),\n },\n }\n // A fence's file rides on its <code>, beside what a stage before this one put there.\n if (name === 'code')\n return {\n hProperties: { ...(data?.hProperties as Record<string, unknown>), dataTitle: form.title },\n }\n const tag = TAGS[name]\n const properties: Record<string, unknown> = {}\n if (!UNCLASSED.has(name)) properties.className = [`fd-${name}`]\n if (name === 'tab') properties.dataLabel = form.label\n if (name === 'verdict') properties.dataVerdict = form.kind\n let has = false\n for (const _ in properties) {\n has = true\n break\n }\n return {\n ...(tag === undefined ? null : { hName: tag }),\n ...(has ? { hProperties: properties } : null),\n }\n}\n"],"mappings":";;AAOA,MAAM,OAA0C;CAC9C,gBAAgB;CAChB,QAAQ;CACR,SAAS;CACT,kBAAkB;CAClB,MAAM;CACN,aAAa;CACb,cAAc;CACd,aAAa;CACb,aAAa;CACb,MAAM;CACN,cAAc;CACd,cAAc;CACd,QAAQ;CACR,gBAAgB;CAChB,eAAe;CACf,eAAe;CACf,MAAM;CACN,KAAK;CACL,SAAS;CACT,UAAU;CACV,SAAS;CACT,SAAS;AACX;;AAGA,MAAM,4BAAY,IAAI,IAAc;CAAC;CAAW;CAAQ;CAAW;CAAQ;CAAO;AAAS,CAAC;AAE5F,MAAa,eAAyB,MAAM,SAAS;CACnD,MAAM,OAAO,KAAK;CAClB,IAAI,SAAS,WACX,OAAO,EACL,aAAa;EACX,aAAa,KAAK;EAClB,GAAI,KAAK,QAAQ,EAAE,kBAAkB,KAAK,MAAM,IAAI,CAAC;CACvD,EACF;CAEF,IAAI,SAAS,QACX,OAAO,EACL,aAAa;EAAE,GAAI,MAAM;EAAyC,WAAW,KAAK;CAAM,EAC1F;CACF,MAAM,MAAM,KAAK;CACjB,MAAM,aAAsC,CAAC;CAC7C,IAAI,CAAC,UAAU,IAAI,IAAI,GAAG,WAAW,YAAY,CAAC,MAAM,MAAM;CAC9D,IAAI,SAAS,OAAO,WAAW,YAAY,KAAK;CAChD,IAAI,SAAS,WAAW,WAAW,cAAc,KAAK;CACtD,IAAI,MAAM;CACV,KAAK,MAAM,KAAK,YAAY;EAC1B,MAAM;EACN;CACF;CACA,OAAO;EACL,GAAI,QAAQ,KAAA,IAAY,OAAO,EAAE,OAAO,IAAI;EAC5C,GAAI,MAAM,EAAE,aAAa,WAAW,IAAI;CAC1C;AACF"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the dialect says a node IS, apart from how any target draws it.
|
|
3
|
+
*
|
|
4
|
+
* A pass of the dialect marks nodes with a {@link Form} (`data.flowdown`): this list is a timeline,
|
|
5
|
+
* this quote a warning, this paragraph the head of a step. A target reads the mark and draws the
|
|
6
|
+
* node its own way: a terminal prints a callout as a coloured bar, a page as an `<aside>`. What a
|
|
7
|
+
* form is in HTML is one table (`forms-html.ts`), handed to the pass by whoever wants HTML: the
|
|
8
|
+
* pass itself names no element and no class.
|
|
9
|
+
*
|
|
10
|
+
* A target that does not know a form draws the node as the Markdown it still is (a list, a quote,
|
|
11
|
+
* a paragraph), so a form added later reads as text everywhere before any target learns it.
|
|
12
|
+
*/
|
|
13
|
+
import { CalloutKind } from "../dialect.js";
|
|
14
|
+
/** Every mark the dialect makes. A name is its own contract: targets switch on it. */
|
|
15
|
+
export type FormName = 'callout' | 'quote-source' | 'figure' | 'caption' | 'section-number' | 'code' | 'form' | 'steps' | 'steps-title' | 'step-head' | 'step-title' | 'step-meta' | 'step-body' | 'stats' | 'stats-title' | 'stat' | 'stat-value' | 'stat-label' | 'sources' | 'sources-title' | 'source' | 'source-title' | 'source-host' | 'source-note' | 'tabs' | 'tabs-title' | 'tab' | 'verdict' | 'verdict-title' | 'verdict-list' | 'verdicts' | 'details' | 'summary';
|
|
16
|
+
export interface Form {
|
|
17
|
+
form: FormName;
|
|
18
|
+
/** A callout's kind (`warning`), or which side a verdict argues (`pros`, `cons`). */
|
|
19
|
+
kind?: CalloutKind | 'pros' | 'cons';
|
|
20
|
+
/** The title the author wrote: on a callout, and the file a code fence names. */
|
|
21
|
+
title?: string;
|
|
22
|
+
/** A tab's label. */
|
|
23
|
+
label?: string;
|
|
24
|
+
}
|
|
25
|
+
/** The form a node was marked with, if any. */
|
|
26
|
+
export declare const formOf: (node: {
|
|
27
|
+
data?: unknown;
|
|
28
|
+
} | undefined) => Form | undefined;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// src/core/forms.ts
|
|
2
|
+
let lowering = null;
|
|
3
|
+
/** Runs a pass with `lower` applied to every node it marks. A pass is synchronous. */
|
|
4
|
+
function lowered(lower, pass) {
|
|
5
|
+
const before = lowering;
|
|
6
|
+
lowering = lower;
|
|
7
|
+
try {
|
|
8
|
+
return pass();
|
|
9
|
+
} finally {
|
|
10
|
+
lowering = before;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** A node's data with `form` marked on it. Without `data`, the mark is all the node carries. */
|
|
14
|
+
const marked = (form, data) => ({
|
|
15
|
+
...data,
|
|
16
|
+
flowdown: form,
|
|
17
|
+
...lowering?.(form, data)
|
|
18
|
+
});
|
|
19
|
+
/** The form a node was marked with, if any. */
|
|
20
|
+
const formOf = (node) => (node?.data)?.flowdown;
|
|
21
|
+
export { formOf, lowered, marked };
|
|
22
|
+
|
|
23
|
+
//# sourceMappingURL=forms.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"forms.js","names":[],"sources":["../../src/core/forms.ts"],"sourcesContent":["/**\n * What the dialect says a node IS, apart from how any target draws it.\n *\n * A pass of the dialect marks nodes with a {@link Form} (`data.flowdown`): this list is a timeline,\n * this quote a warning, this paragraph the head of a step. A target reads the mark and draws the\n * node its own way: a terminal prints a callout as a coloured bar, a page as an `<aside>`. What a\n * form is in HTML is one table (`forms-html.ts`), handed to the pass by whoever wants HTML: the\n * pass itself names no element and no class.\n *\n * A target that does not know a form draws the node as the Markdown it still is (a list, a quote,\n * a paragraph), so a form added later reads as text everywhere before any target learns it.\n */\nimport type { CalloutKind } from '../dialect'\n\n/** Every mark the dialect makes. A name is its own contract: targets switch on it. */\nexport type FormName =\n | 'callout'\n | 'quote-source'\n | 'figure'\n | 'caption'\n | 'section-number'\n | 'code'\n | 'form'\n | 'steps'\n | 'steps-title'\n | 'step-head'\n | 'step-title'\n | 'step-meta'\n | 'step-body'\n | 'stats'\n | 'stats-title'\n | 'stat'\n | 'stat-value'\n | 'stat-label'\n | 'sources'\n | 'sources-title'\n | 'source'\n | 'source-title'\n | 'source-host'\n | 'source-note'\n | 'tabs'\n | 'tabs-title'\n | 'tab'\n | 'verdict'\n | 'verdict-title'\n | 'verdict-list'\n | 'verdicts'\n | 'details'\n | 'summary'\n\nexport interface Form {\n form: FormName\n /** A callout's kind (`warning`), or which side a verdict argues (`pros`, `cons`). */\n kind?: CalloutKind | 'pros' | 'cons'\n /** The title the author wrote: on a callout, and the file a code fence names. */\n title?: string\n /** A tab's label. */\n label?: string\n}\n\n/** A node's fields besides its own: what a pass has written on it. */\nexport type NodeData = Record<string, unknown> & { flowdown?: Form }\n\n/**\n * What a form is in one target's own terms, as fields to put on the node beside the mark. `data`\n * is what the node carried before, for a lowering that adds to it.\n */\nexport type Lowering = (form: Form, data: NodeData | undefined) => Record<string, unknown>\n\nlet lowering: Lowering | null = null\n\n/** Runs a pass with `lower` applied to every node it marks. A pass is synchronous. */\nexport function lowered<T>(lower: Lowering | null, pass: () => T): T {\n const before = lowering\n lowering = lower\n try {\n return pass()\n } finally {\n lowering = before\n }\n}\n\n/** A node's data with `form` marked on it. Without `data`, the mark is all the node carries. */\nexport const marked = <D extends object>(form: Form, data?: D): D & { flowdown: Form } =>\n ({ ...data, flowdown: form, ...lowering?.(form, data as NodeData | undefined) }) as D & {\n flowdown: Form\n }\n\n/** The form a node was marked with, if any. */\nexport const formOf = (node: { data?: unknown } | undefined): Form | undefined =>\n (node?.data as NodeData | undefined)?.flowdown\n"],"mappings":";AAqEA,IAAI,WAA4B;;AAGhC,SAAgB,QAAW,OAAwB,MAAkB;CACnE,MAAM,SAAS;CACf,WAAW;CACX,IAAI;EACF,OAAO,KAAK;CACd,UAAU;EACR,WAAW;CACb;AACF;;AAGA,MAAa,UAA4B,MAAY,UAClD;CAAE,GAAG;CAAM,UAAU;CAAM,GAAG,WAAW,MAAM,IAA4B;AAAE;;AAKhF,MAAa,UAAU,UACpB,MAAM,KAAA,EAA+B"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Tree } from "../engine/types.js";
|
|
2
|
+
export interface MarkdownHeading {
|
|
3
|
+
depth: number;
|
|
4
|
+
text: string;
|
|
5
|
+
id: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function extractMarkdownHeadings(content: string): MarkdownHeading[];
|
|
8
|
+
/** remark: give every heading of an mdast tree its id (`data.hProperties.id`). */
|
|
9
|
+
export declare function remarkHeadingIds(): (tree: Tree) => void;
|
|
10
|
+
/** Every heading's id, keyed by where the heading starts in `content`. */
|
|
11
|
+
export declare function headingIdsByOffset(content: string): Map<number, string>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { parse } from "../engine/index.js";
|
|
2
|
+
import { toText, visit } from "../engine/visit.js";
|
|
3
|
+
// src/core/headings.ts
|
|
4
|
+
function slugifyHeading(text) {
|
|
5
|
+
return text.toLowerCase().replace(/[`*_~]/g, "").replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-+|-+$/g, "");
|
|
6
|
+
}
|
|
7
|
+
/** One pass owns both the outline and the rendered ids, repeated headings included. */
|
|
8
|
+
function assignHeadings(tree) {
|
|
9
|
+
const headings = [];
|
|
10
|
+
const used = /* @__PURE__ */ new Set();
|
|
11
|
+
visit(tree, (node) => {
|
|
12
|
+
if (node.type !== "heading") return;
|
|
13
|
+
const text = toText(node);
|
|
14
|
+
const base = slugifyHeading(text) || "section";
|
|
15
|
+
let id = base;
|
|
16
|
+
let suffix = 1;
|
|
17
|
+
while (used.has(id)) id = `${base}-${suffix++}`;
|
|
18
|
+
used.add(id);
|
|
19
|
+
node.data = {
|
|
20
|
+
...node.data,
|
|
21
|
+
hProperties: {
|
|
22
|
+
...node.data?.hProperties,
|
|
23
|
+
id
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
headings.push({
|
|
27
|
+
depth: node.depth ?? 1,
|
|
28
|
+
text,
|
|
29
|
+
id
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
return headings;
|
|
33
|
+
}
|
|
34
|
+
function extractMarkdownHeadings(content) {
|
|
35
|
+
return assignHeadings(parse(content));
|
|
36
|
+
}
|
|
37
|
+
/** remark: give every heading of an mdast tree its id (`data.hProperties.id`). */
|
|
38
|
+
function remarkHeadingIds() {
|
|
39
|
+
return (tree) => {
|
|
40
|
+
assignHeadings(tree);
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Every heading in `content` with where it starts and the text its id is made from, before
|
|
45
|
+
* repeated ids are numbered: the numbering is the document's, done over all of its blocks.
|
|
46
|
+
*/
|
|
47
|
+
function headingBases(content) {
|
|
48
|
+
const bases = [];
|
|
49
|
+
visit(parse(content), (node) => {
|
|
50
|
+
const offset = node.position?.start.offset;
|
|
51
|
+
if (node.type === "heading" && offset !== void 0) bases.push({
|
|
52
|
+
offset,
|
|
53
|
+
base: slugifyHeading(toText(node)) || "section"
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
return bases;
|
|
57
|
+
}
|
|
58
|
+
/** Every heading's id, keyed by where the heading starts in `content`. */
|
|
59
|
+
function headingIdsByOffset(content) {
|
|
60
|
+
const tree = parse(content);
|
|
61
|
+
const ids = /* @__PURE__ */ new Map();
|
|
62
|
+
const headings = assignHeadings(tree);
|
|
63
|
+
let at = 0;
|
|
64
|
+
visit(tree, (node) => {
|
|
65
|
+
if (node.type !== "heading") return;
|
|
66
|
+
const offset = node.position?.start.offset;
|
|
67
|
+
const id = headings[at++]?.id;
|
|
68
|
+
if (offset !== void 0 && id !== void 0) ids.set(offset, id);
|
|
69
|
+
});
|
|
70
|
+
return ids;
|
|
71
|
+
}
|
|
72
|
+
export { extractMarkdownHeadings, headingBases, headingIdsByOffset, remarkHeadingIds };
|
|
73
|
+
|
|
74
|
+
//# sourceMappingURL=headings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"headings.js","names":[],"sources":["../../src/core/headings.ts"],"sourcesContent":["import { parse } from '../engine'\nimport type { Node, Tree } from '../engine/types'\nimport { toText, visit } from '../engine/visit'\n\nexport interface MarkdownHeading {\n depth: number\n text: string\n id: string\n}\n\nfunction slugifyHeading(text: string): string {\n return text\n .toLowerCase()\n .replace(/[`*_~]/g, '')\n .replace(/[^\\p{L}\\p{N}]+/gu, '-')\n .replace(/^-+|-+$/g, '')\n}\n\n/** One pass owns both the outline and the rendered ids, repeated headings included. */\nfunction assignHeadings(tree: Node): MarkdownHeading[] {\n const headings: MarkdownHeading[] = []\n const used = new Set<string>()\n visit(tree, (node) => {\n if (node.type !== 'heading') return\n const text = toText(node)\n const base = slugifyHeading(text) || 'section'\n let id = base\n let suffix = 1\n while (used.has(id)) id = `${base}-${suffix++}`\n used.add(id)\n node.data = { ...node.data, hProperties: { ...(node.data?.hProperties as object), id } }\n headings.push({ depth: node.depth ?? 1, text, id })\n })\n return headings\n}\n\nexport function extractMarkdownHeadings(content: string): MarkdownHeading[] {\n return assignHeadings(parse(content))\n}\n\n/** remark: give every heading of an mdast tree its id (`data.hProperties.id`). */\nexport function remarkHeadingIds() {\n return (tree: Tree): void => {\n assignHeadings(tree as Node)\n }\n}\n\n/**\n * Every heading in `content` with where it starts and the text its id is made from, before\n * repeated ids are numbered: the numbering is the document's, done over all of its blocks.\n */\nexport function headingBases(content: string): Array<{ offset: number; base: string }> {\n const bases: Array<{ offset: number; base: string }> = []\n visit(parse(content), (node) => {\n const offset = node.position?.start.offset\n if (node.type === 'heading' && offset !== undefined)\n bases.push({ offset, base: slugifyHeading(toText(node)) || 'section' })\n })\n return bases\n}\n\n/** Every heading's id, keyed by where the heading starts in `content`. */\nexport function headingIdsByOffset(content: string): Map<number, string> {\n const tree = parse(content)\n const ids = new Map<number, string>()\n const headings = assignHeadings(tree)\n let at = 0\n visit(tree, (node) => {\n if (node.type !== 'heading') return\n const offset = node.position?.start.offset\n const id = headings[at++]?.id\n if (offset !== undefined && id !== undefined) ids.set(offset, id)\n })\n return ids\n}\n"],"mappings":";;;AAUA,SAAS,eAAe,MAAsB;CAC5C,OAAO,KACJ,YAAY,CAAC,CACb,QAAQ,WAAW,EAAE,CAAC,CACtB,QAAQ,oBAAoB,GAAG,CAAC,CAChC,QAAQ,YAAY,EAAE;AAC3B;;AAGA,SAAS,eAAe,MAA+B;CACrD,MAAM,WAA8B,CAAC;CACrC,MAAM,uBAAO,IAAI,IAAY;CAC7B,MAAM,OAAO,SAAS;EACpB,IAAI,KAAK,SAAS,WAAW;EAC7B,MAAM,OAAO,OAAO,IAAI;EACxB,MAAM,OAAO,eAAe,IAAI,KAAK;EACrC,IAAI,KAAK;EACT,IAAI,SAAS;EACb,OAAO,KAAK,IAAI,EAAE,GAAG,KAAK,GAAG,KAAK,GAAG;EACrC,KAAK,IAAI,EAAE;EACX,KAAK,OAAO;GAAE,GAAG,KAAK;GAAM,aAAa;IAAE,GAAI,KAAK,MAAM;IAAwB;GAAG;EAAE;EACvF,SAAS,KAAK;GAAE,OAAO,KAAK,SAAS;GAAG;GAAM;EAAG,CAAC;CACpD,CAAC;CACD,OAAO;AACT;AAEA,SAAgB,wBAAwB,SAAoC;CAC1E,OAAO,eAAe,MAAM,OAAO,CAAC;AACtC;;AAGA,SAAgB,mBAAmB;CACjC,QAAQ,SAAqB;EAC3B,eAAe,IAAY;CAC7B;AACF;;;;;AAMA,SAAgB,aAAa,SAA0D;CACrF,MAAM,QAAiD,CAAC;CACxD,MAAM,MAAM,OAAO,IAAI,SAAS;EAC9B,MAAM,SAAS,KAAK,UAAU,MAAM;EACpC,IAAI,KAAK,SAAS,aAAa,WAAW,KAAA,GACxC,MAAM,KAAK;GAAE;GAAQ,MAAM,eAAe,OAAO,IAAI,CAAC,KAAK;EAAU,CAAC;CAC1E,CAAC;CACD,OAAO;AACT;;AAGA,SAAgB,mBAAmB,SAAsC;CACvE,MAAM,OAAO,MAAM,OAAO;CAC1B,MAAM,sBAAM,IAAI,IAAoB;CACpC,MAAM,WAAW,eAAe,IAAI;CACpC,IAAI,KAAK;CACT,MAAM,OAAO,SAAS;EACpB,IAAI,KAAK,SAAS,WAAW;EAC7B,MAAM,SAAS,KAAK,UAAU,MAAM;EACpC,MAAM,KAAK,SAAS,KAAK,EAAE;EAC3B,IAAI,WAAW,KAAA,KAAa,OAAO,KAAA,GAAW,IAAI,IAAI,QAAQ,EAAE;CAClE,CAAC;CACD,OAAO;AACT"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Whether a stabilized fence is the one incomplete body still being written. */
|
|
2
|
+
export declare function isGrowingFence(growing: string | null, source: string): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* The body of the fence a streaming text ends inside, or null when every fence is closed.
|
|
5
|
+
*
|
|
6
|
+
* Only the LAST fence can be unterminated, so this is how a renderer tells the one block that is
|
|
7
|
+
* still being written from the finished blocks above it — a diagram above the tail is complete
|
|
8
|
+
* and can be drawn now, instead of waiting for the whole reply to settle.
|
|
9
|
+
*/
|
|
10
|
+
export declare function openFenceBody(text: string): string | null;
|
|
11
|
+
export declare function closePartialMarkdown(text: string): string;
|