@localix-ai/flowdown 0.0.0-stage → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +105 -2
  3. package/dist/browser/file-actions.js +80 -0
  4. package/dist/browser/file-actions.js.map +1 -0
  5. package/dist/core/block-facts.js +72 -0
  6. package/dist/core/block-facts.js.map +1 -0
  7. package/dist/core/blocks.d.ts +25 -0
  8. package/dist/core/blocks.js +464 -0
  9. package/dist/core/blocks.js.map +1 -0
  10. package/dist/core/bounded-cache.js +42 -0
  11. package/dist/core/bounded-cache.js.map +1 -0
  12. package/dist/core/continues.js +15 -0
  13. package/dist/core/continues.js.map +1 -0
  14. package/dist/core/dollars.d.ts +12 -0
  15. package/dist/core/dollars.js +88 -0
  16. package/dist/core/dollars.js.map +1 -0
  17. package/dist/core/fence-scan.js +62 -0
  18. package/dist/core/fence-scan.js.map +1 -0
  19. package/dist/core/file-tree-parse.d.ts +26 -0
  20. package/dist/core/file-tree-parse.js +51 -0
  21. package/dist/core/file-tree-parse.js.map +1 -0
  22. package/dist/core/forms-html.js +63 -0
  23. package/dist/core/forms-html.js.map +1 -0
  24. package/dist/core/forms.d.ts +28 -0
  25. package/dist/core/forms.js +23 -0
  26. package/dist/core/forms.js.map +1 -0
  27. package/dist/core/headings.d.ts +11 -0
  28. package/dist/core/headings.js +74 -0
  29. package/dist/core/headings.js.map +1 -0
  30. package/dist/core/heal.d.ts +11 -0
  31. package/dist/core/heal.js +220 -0
  32. package/dist/core/heal.js.map +1 -0
  33. package/dist/core/load-once.js +23 -0
  34. package/dist/core/load-once.js.map +1 -0
  35. package/dist/core/math-delimiters.js +33 -0
  36. package/dist/core/math-delimiters.js.map +1 -0
  37. package/dist/core/math-syntax.js +56 -0
  38. package/dist/core/math-syntax.js.map +1 -0
  39. package/dist/core/prose.d.ts +10 -0
  40. package/dist/core/prose.js +77 -0
  41. package/dist/core/prose.js.map +1 -0
  42. package/dist/core/remark-answer-blocks.d.ts +5 -0
  43. package/dist/core/remark-answer-blocks.js +172 -0
  44. package/dist/core/remark-answer-blocks.js.map +1 -0
  45. package/dist/core/remark-answer-containers.js +299 -0
  46. package/dist/core/remark-answer-containers.js.map +1 -0
  47. package/dist/core/sanitize-schema.d.ts +18 -0
  48. package/dist/core/sanitize-schema.js +62 -0
  49. package/dist/core/sanitize-schema.js.map +1 -0
  50. package/dist/core/syntax.d.ts +8 -0
  51. package/dist/core/syntax.js +32 -0
  52. package/dist/core/syntax.js.map +1 -0
  53. package/dist/dialect.d.ts +92 -0
  54. package/dist/dialect.js +176 -0
  55. package/dist/dialect.js.map +1 -0
  56. package/dist/document/fence.d.ts +25 -0
  57. package/dist/document/fence.js +102 -0
  58. package/dist/document/fence.js.map +1 -0
  59. package/dist/document/index.d.ts +46 -0
  60. package/dist/document/index.js +66 -0
  61. package/dist/document/index.js.map +1 -0
  62. package/dist/document/table.d.ts +11 -0
  63. package/dist/document/table.js +35 -0
  64. package/dist/document/table.js.map +1 -0
  65. package/dist/dom/index.d.ts +21 -0
  66. package/dist/dom/index.js +141 -0
  67. package/dist/dom/index.js.map +1 -0
  68. package/dist/dom/patch.js +104 -0
  69. package/dist/dom/patch.js.map +1 -0
  70. package/dist/element/index.d.ts +35 -0
  71. package/dist/element/index.js +64 -0
  72. package/dist/element/index.js.map +1 -0
  73. package/dist/engine/blocks.js +785 -0
  74. package/dist/engine/blocks.js.map +1 -0
  75. package/dist/engine/entities.js +83 -0
  76. package/dist/engine/entities.js.map +1 -0
  77. package/dist/engine/entity-core.js +9 -0
  78. package/dist/engine/entity-core.js.map +1 -0
  79. package/dist/engine/entity-table.js +9 -0
  80. package/dist/engine/entity-table.js.map +1 -0
  81. package/dist/engine/html.d.ts +7 -0
  82. package/dist/engine/html.js +207 -0
  83. package/dist/engine/html.js.map +1 -0
  84. package/dist/engine/index.d.ts +7 -0
  85. package/dist/engine/index.js +74 -0
  86. package/dist/engine/index.js.map +1 -0
  87. package/dist/engine/inlines.js +887 -0
  88. package/dist/engine/inlines.js.map +1 -0
  89. package/dist/engine/types.d.ts +109 -0
  90. package/dist/engine/url.d.ts +11 -0
  91. package/dist/engine/url.js +16 -0
  92. package/dist/engine/url.js.map +1 -0
  93. package/dist/engine/visit.d.ts +10 -0
  94. package/dist/engine/visit.js +29 -0
  95. package/dist/engine/visit.js.map +1 -0
  96. package/dist/highlight/highlight.d.ts +4 -0
  97. package/dist/highlight/highlight.js +144 -0
  98. package/dist/highlight/highlight.js.map +1 -0
  99. package/dist/highlight/index.d.ts +4 -0
  100. package/dist/highlight/index.js +18 -0
  101. package/dist/highlight/index.js.map +1 -0
  102. package/dist/html/index.d.ts +4 -0
  103. package/dist/html/index.js +544 -0
  104. package/dist/html/index.js.map +1 -0
  105. package/dist/index.d.ts +27 -0
  106. package/dist/index.js +13 -0
  107. package/dist/math/index.d.ts +17 -0
  108. package/dist/math/index.js +20 -0
  109. package/dist/math/index.js.map +1 -0
  110. package/dist/math/typeset.d.ts +12 -0
  111. package/dist/math/typeset.js +74 -0
  112. package/dist/math/typeset.js.map +1 -0
  113. package/dist/mermaid/diagram.d.ts +8 -0
  114. package/dist/mermaid/diagram.js +114 -0
  115. package/dist/mermaid/diagram.js.map +1 -0
  116. package/dist/mermaid/drawing.js +139 -0
  117. package/dist/mermaid/drawing.js.map +1 -0
  118. package/dist/mermaid/icons.js +16 -0
  119. package/dist/mermaid/icons.js.map +1 -0
  120. package/dist/mermaid/index.d.ts +13 -0
  121. package/dist/mermaid/index.js +3 -0
  122. package/dist/mermaid/loader.d.ts +5 -0
  123. package/dist/mermaid/loader.js +18 -0
  124. package/dist/mermaid/loader.js.map +1 -0
  125. package/dist/mermaid/look.d.ts +16 -0
  126. package/dist/mermaid/look.js +167 -0
  127. package/dist/mermaid/look.js.map +1 -0
  128. package/dist/mermaid/normalize-svg.js +44 -0
  129. package/dist/mermaid/normalize-svg.js.map +1 -0
  130. package/dist/mermaid/renderer.d.ts +22 -0
  131. package/dist/mermaid/renderer.js +132 -0
  132. package/dist/mermaid/renderer.js.map +1 -0
  133. package/dist/mermaid/viewer.js +309 -0
  134. package/dist/mermaid/viewer.js.map +1 -0
  135. package/dist/prompt/index.d.ts +19 -0
  136. package/dist/prompt/index.js +54 -0
  137. package/dist/prompt/index.js.map +1 -0
  138. package/dist/react/callout.d.ts +9 -0
  139. package/dist/react/callout.js +12 -0
  140. package/dist/react/callout.js.map +1 -0
  141. package/dist/react/code-block.d.ts +18 -0
  142. package/dist/react/code-block.js +94 -0
  143. package/dist/react/code-block.js.map +1 -0
  144. package/dist/react/context.d.ts +18 -0
  145. package/dist/react/context.js +17 -0
  146. package/dist/react/context.js.map +1 -0
  147. package/dist/react/draw.d.ts +2 -0
  148. package/dist/react/draw.js +17 -0
  149. package/dist/react/draw.js.map +1 -0
  150. package/dist/react/file-tree.d.ts +16 -0
  151. package/dist/react/file-tree.js +69 -0
  152. package/dist/react/file-tree.js.map +1 -0
  153. package/dist/react/growing-code.js +36 -0
  154. package/dist/react/growing-code.js.map +1 -0
  155. package/dist/react/helpers.d.ts +6 -0
  156. package/dist/react/helpers.js +17 -0
  157. package/dist/react/helpers.js.map +1 -0
  158. package/dist/react/icons.js +23 -0
  159. package/dist/react/icons.js.map +1 -0
  160. package/dist/react/labels.js +9 -0
  161. package/dist/react/labels.js.map +1 -0
  162. package/dist/react/markdown.d.ts +78 -0
  163. package/dist/react/markdown.js +206 -0
  164. package/dist/react/markdown.js.map +1 -0
  165. package/dist/react/render.d.ts +8 -0
  166. package/dist/react/render.js +41 -0
  167. package/dist/react/render.js.map +1 -0
  168. package/dist/react/scrolls.js +62 -0
  169. package/dist/react/scrolls.js.map +1 -0
  170. package/dist/react/table-rows.js +66 -0
  171. package/dist/react/table-rows.js.map +1 -0
  172. package/dist/react/table.js +22 -0
  173. package/dist/react/table.js.map +1 -0
  174. package/dist/react/tabs.d.ts +25 -0
  175. package/dist/react/tabs.js +58 -0
  176. package/dist/react/tabs.js.map +1 -0
  177. package/dist/react/use-smooth-text.d.ts +6 -0
  178. package/dist/react/use-smooth-text.js +157 -0
  179. package/dist/react/use-smooth-text.js.map +1 -0
  180. package/dist/react/use-stage.d.ts +9 -0
  181. package/dist/react/use-stage.js +57 -0
  182. package/dist/react/use-stage.js.map +1 -0
  183. package/dist/remark.d.ts +20 -0
  184. package/dist/remark.js +10 -0
  185. package/dist/render/factory.d.ts +15 -0
  186. package/dist/render/labels.d.ts +46 -0
  187. package/dist/render/labels.js +46 -0
  188. package/dist/render/labels.js.map +1 -0
  189. package/dist/render/tree.js +314 -0
  190. package/dist/render/tree.js.map +1 -0
  191. package/dist/render/view.d.ts +58 -0
  192. package/dist/render/view.js +306 -0
  193. package/dist/render/view.js.map +1 -0
  194. package/dist/render/views.d.ts +16 -0
  195. package/dist/render/views.js +261 -0
  196. package/dist/render/views.js.map +1 -0
  197. package/dist/render/vnode.d.ts +21 -0
  198. package/dist/render/vnode.js +194 -0
  199. package/dist/render/vnode.js.map +1 -0
  200. package/dist/static/index.d.ts +28 -0
  201. package/dist/static/index.js +27 -0
  202. package/dist/static/index.js.map +1 -0
  203. package/dist/stream.d.ts +12 -0
  204. package/dist/stream.js +3 -0
  205. package/dist/styles.css +1362 -0
  206. package/dist/tui/code.d.ts +5 -0
  207. package/dist/tui/code.js +240 -0
  208. package/dist/tui/code.js.map +1 -0
  209. package/dist/tui/index.d.ts +54 -0
  210. package/dist/tui/index.js +99 -0
  211. package/dist/tui/index.js.map +1 -0
  212. package/dist/tui/math.js +242 -0
  213. package/dist/tui/math.js.map +1 -0
  214. package/dist/tui/printable.d.ts +12 -0
  215. package/dist/tui/printable.js +31 -0
  216. package/dist/tui/printable.js.map +1 -0
  217. package/dist/tui/render.d.ts +12 -0
  218. package/dist/tui/render.js +524 -0
  219. package/dist/tui/render.js.map +1 -0
  220. package/dist/tui/style.d.ts +35 -0
  221. package/dist/tui/style.js +77 -0
  222. package/dist/tui/style.js.map +1 -0
  223. package/dist/tui/table.js +73 -0
  224. package/dist/tui/table.js.map +1 -0
  225. package/dist/tui/width.d.ts +2 -0
  226. package/dist/tui/width.js +133 -0
  227. package/dist/tui/width.js.map +1 -0
  228. package/dist/tui/wrap.d.ts +8 -0
  229. package/dist/tui/wrap.js +191 -0
  230. package/dist/tui/wrap.js.map +1 -0
  231. package/package.json +128 -4
@@ -0,0 +1,220 @@
1
+ import { parseContainerMarker } from "../dialect.js";
2
+ import { inlineMathClose, isLiteralDollar } from "./dollars.js";
3
+ import { openFence } from "./fence-scan.js";
4
+ import { opensInlineMath } from "./math-delimiters.js";
5
+ import { isEscaped } from "./prose.js";
6
+ // src/core/heal.ts
7
+ /**
8
+ * Streaming-tail markdown stabilization.
9
+ *
10
+ * While a reply streams, the accumulated markdown frequently ends mid-construct:
11
+ * an unterminated ``` fence renders the partial code as raw text, a dangling
12
+ * `**` or backtick shows literal markers, a half-typed `[link](url` shows raw
13
+ * syntax — and each of these "pops" into its formatted form a few frames later
14
+ * once the model closes it. `closePartialMarkdown` conservatively completes
15
+ * (or hides) those trailing constructs so the streamed view stays visually
16
+ * stable. It is applied ONLY while streaming; the final text renders verbatim.
17
+ */
18
+ /** Whether a stabilized fence is the one incomplete body still being written. */
19
+ function isGrowingFence(growing, source) {
20
+ return growing !== null && growing.trimEnd() === source.trimEnd();
21
+ }
22
+ /**
23
+ * The body of the fence a streaming text ends inside, or null when every fence is closed.
24
+ *
25
+ * Only the LAST fence can be unterminated, so this is how a renderer tells the one block that is
26
+ * still being written from the finished blocks above it — a diagram above the tail is complete
27
+ * and can be drawn now, instead of waiting for the whole reply to settle.
28
+ */
29
+ function openFenceBody(text) {
30
+ const fence = openFence(text);
31
+ return fence ? text.slice(fence.bodyStart) : null;
32
+ }
33
+ /** A trailing half-written image (`![alt](partial-url` or `![alt`) is hidden — a
34
+ * broken-image flash is worse than a beat of nothing. */
35
+ function dropTrailingPartialImage(tail) {
36
+ return tail.replace(/!\[[^\]]*(\]\([^)]*)?$/, "");
37
+ }
38
+ /** A trailing `[text](partial-url` gets its `)` so the anchor text renders as a
39
+ * link immediately instead of flashing raw syntax until the URL completes. */
40
+ function closeTrailingLink(tail) {
41
+ return /(^|[^!])\[[^\]]*\]\([^)]*$/.test(tail) ? `${tail})` : tail;
42
+ }
43
+ /**
44
+ * Code spans and CLOSED formulas blanked to spaces, positions kept. Emphasis has no meaning
45
+ * inside either, and the balancers below would otherwise read `$a*b$` or `$x_i$` as an open
46
+ * emphasis and append a stray `*` or `_` to the streaming tail.
47
+ */
48
+ function maskInert(tail) {
49
+ const blank = (span) => " ".repeat(span.length);
50
+ return tail.replace(/`[^`]*`/g, blank).replace(/\$\$[\s\S]*?\$\$|\$[^$\n]+\$/g, blank);
51
+ }
52
+ /**
53
+ * A formula still being typed is withheld from its opening `$` until the closing one arrives.
54
+ * Math is typeset while a reply streams, so a half-typed `$\frac{a}{` would otherwise print as
55
+ * raw LaTeX and then jump into a formula. Dollars are paired by the renderer's own rule
56
+ * (`literalDollars`, `math-delimiters.ts`), so the two never disagree about one `$`: a price, a
57
+ * shell variable or a `$` with a space after it (`5$ and`) opens nothing and hides nothing.
58
+ *
59
+ * Only the first `$` left open is withheld, and only while no `$` follows it: a `$` that came and
60
+ * did not close it (the shell line `$(seq 1 12); do printf "%s" $i`) shows these dollars are text.
61
+ */
62
+ function withholdPartialMath(tail) {
63
+ const code = tail.replace(/`[^`]*`/g, (span) => " ".repeat(span.length));
64
+ const displays = [];
65
+ for (let index = code.indexOf("$$"); index !== -1; index = code.indexOf("$$", index + 2)) if (!isEscaped(code, index)) displays.push(index);
66
+ if (displays.length % 2 === 1) return tail.slice(0, displays.at(-1)).trimEnd();
67
+ const inline = code.replace(/\$\$[\s\S]*?\$\$/g, (span) => " ".repeat(span.length));
68
+ for (let index = inline.indexOf("$"); index !== -1; index = inline.indexOf("$", index + 1)) {
69
+ if (isEscaped(inline, index) || isLiteralDollar(inline, index)) continue;
70
+ const last = index === inline.length - 1;
71
+ if (!last && !opensInlineMath(inline, index)) continue;
72
+ const close = last ? -1 : inlineMathClose(inline, index);
73
+ if (close !== -1) {
74
+ index = close;
75
+ continue;
76
+ }
77
+ return inline.includes("$", index + 1) ? tail : tail.slice(0, index).trimEnd();
78
+ }
79
+ return tail;
80
+ }
81
+ /** Balance inline code: an odd number of backticks means an open span. A bare
82
+ * trailing opener is stripped (closing it would render an empty span as
83
+ * literal ``` `` ```); an opener with content gets closed. */
84
+ function balanceInlineCode(tail) {
85
+ if ((tail.replace(/\\`/g, "").match(/`/g) ?? []).length % 2 === 0) return tail;
86
+ return tail.endsWith("`") ? tail.slice(0, -1) : `${tail}\``;
87
+ }
88
+ /** Balance a two-char emphasis delimiter (`**` / `~~`). Occurrences inside
89
+ * inline code spans are ignored. A bare trailing opener is stripped (closing
90
+ * an empty pair would render four literal characters); one with content after
91
+ * it gets closed. */
92
+ function balanceDoubleDelimiter(tail, delim) {
93
+ const stripped = maskInert(tail);
94
+ let count = 0;
95
+ for (let i = 0; (i = stripped.indexOf(delim, i)) !== -1; i += delim.length) count += 1;
96
+ if (count % 2 === 0) return tail;
97
+ return tail.endsWith(delim) ? tail.slice(0, -delim.length) : `${tail}${delim}`;
98
+ }
99
+ const WORD_RE = /\w/;
100
+ /**
101
+ * Balance a single-char emphasis delimiter (`*` or `_`) — the most common
102
+ * mid-stream flash after `**`: `*italic` shows the literal `*` until the model
103
+ * closes it, then pops into italics. We append the missing closer so it renders
104
+ * as emphasis immediately.
105
+ *
106
+ * The danger is false positives — a wrongly appended `*`/`_` italicises text the
107
+ * author never marked up. So we count ONLY "emphasis-capable" lone delimiters:
108
+ * - runs of length ≥ 2 are skipped (they belong to `**`/`__`/`~~` handling);
109
+ * - inside inline code is masked out;
110
+ * - `*` counts only when it touches a word char on one side — excludes globs
111
+ * (`*.ts`), multiplication (`a * b`) and bullets (`* item`), where the `*` is
112
+ * flanked by whitespace/punctuation;
113
+ * - `_` additionally requires the word char to be on the OPENING side only —
114
+ * so intraword underscores (`file_name`, `__init__`, `snake_case`) never
115
+ * count, matching CommonMark's "no intraword `_` emphasis" rule.
116
+ * An odd active count ⇒ a trailing opener is dangling: append one closer (or
117
+ * strip a bare trailing opener, mirroring the double-delimiter rule).
118
+ */
119
+ function balanceSingleEmphasis(tail, char) {
120
+ const masked = maskInert(tail);
121
+ let active = 0;
122
+ let i = 0;
123
+ while (i < masked.length) {
124
+ if (masked[i] !== char) {
125
+ i += 1;
126
+ continue;
127
+ }
128
+ let j = i;
129
+ while (j < masked.length && masked[j] === char) j += 1;
130
+ if (j - i === 1) {
131
+ const prevWord = WORD_RE.test(masked[i - 1] ?? "");
132
+ const nextWord = WORD_RE.test(masked[j] ?? "");
133
+ if (char === "*" ? prevWord || nextWord : !prevWord && nextWord || prevWord && !nextWord) active += 1;
134
+ }
135
+ i = j;
136
+ }
137
+ if (active % 2 === 0) return tail;
138
+ return tail.endsWith(char) ? tail.slice(0, -1) : `${tail}${char}`;
139
+ }
140
+ const ROW_START = /^\s{0,3}\|/;
141
+ /** The row of dashes under a table's header: `|---|:--:|`, its outer pipes optional. */
142
+ const DELIMITER_ROW = /^\s*\|?\s*:?-{3,}:?\s*(?:\|\s*:?-{3,}:?\s*)*\|?\s*$/;
143
+ /** Hide only a partial row of an already established GFM table. A line starting
144
+ * with `|` outside a table may be ordinary prose and must remain visible. */
145
+ function dropTrailingPartialTableRow(text) {
146
+ if (text.endsWith("\n")) return text;
147
+ const nl = text.lastIndexOf("\n");
148
+ if (nl === -1 || !ROW_START.test(text.slice(nl + 1))) return text;
149
+ for (let end = nl; end > 0;) {
150
+ const start = text.lastIndexOf("\n", end - 1) + 1;
151
+ if (start === 0) break;
152
+ const line = text.slice(start, end);
153
+ if (DELIMITER_ROW.test(line)) {
154
+ const above = start === 1 ? 0 : text.lastIndexOf("\n", start - 2) + 1;
155
+ return ROW_START.test(text.slice(above, start - 1)) ? text.slice(0, nl) : text;
156
+ }
157
+ if (!ROW_START.test(line)) break;
158
+ end = start - 1;
159
+ }
160
+ return text;
161
+ }
162
+ /**
163
+ * A quote line that is still typing its callout marker (`> [`, `> [!`, `> [!WARN`) is hidden
164
+ * until the `]` arrives: it would otherwise print as a grey quote reading "[!WARN" and a beat
165
+ * later turn into a note. A complete marker is kept — it already renders as the empty note.
166
+ */
167
+ function dropTrailingPartialCalloutMarker(text) {
168
+ const nl = text.lastIndexOf("\n");
169
+ const lastLine = text.slice(nl + 1);
170
+ if (!/^\s{0,3}>[ \t]?\[(?:!\w*)?$/.test(lastLine)) return text;
171
+ return nl === -1 ? "" : text.slice(0, nl);
172
+ }
173
+ /**
174
+ * A form's marker that arrived before its content (`> [!STEPS]` and nothing yet) is hidden: alone,
175
+ * it is a quote reading "[!STEPS]", and the steps replace it a token later. Hidden only while
176
+ * nothing follows it but blank space; once the list starts, the form renders.
177
+ */
178
+ function dropTrailingBareFormMarker(text) {
179
+ const trimmed = text.trimEnd();
180
+ const nl = trimmed.lastIndexOf("\n");
181
+ const quoted = /^\s{0,3}>[ \t]?(.*)$/.exec(trimmed.slice(nl + 1));
182
+ if (!quoted || !parseContainerMarker(quoted[1].trim())) return text;
183
+ return nl === -1 ? "" : trimmed.slice(0, nl + 1);
184
+ }
185
+ function closePartialMarkdown(text) {
186
+ if (!text) return text;
187
+ const fence = openFence(text);
188
+ if (fence) return `${text}${text.endsWith("\n") ? "" : "\n"}${fence.marker}`;
189
+ return closePartialProse(text);
190
+ }
191
+ /**
192
+ * `closePartialMarkdown` for a text known to end outside a fenced code block: the renderer asks
193
+ * its own scanner about the fence, and comes here with the answer.
194
+ */
195
+ function closePartialProse(text) {
196
+ if (!text) return text;
197
+ const detabled = dropTrailingPartialTableRow(text);
198
+ if (detabled !== text) return detabled;
199
+ const unmarked = dropTrailingPartialCalloutMarker(text);
200
+ if (unmarked !== text) return unmarked;
201
+ const bare = dropTrailingBareFormMarker(text);
202
+ if (bare !== text) return bare;
203
+ const splitAt = text.lastIndexOf("\n\n");
204
+ const head = splitAt === -1 ? "" : text.slice(0, splitAt + 2);
205
+ let tail = splitAt === -1 ? text : text.slice(splitAt + 2);
206
+ if (!tail) return text;
207
+ tail = dropTrailingPartialImage(tail);
208
+ tail = withholdPartialMath(tail);
209
+ tail = closeTrailingLink(tail);
210
+ tail = balanceInlineCode(tail);
211
+ tail = balanceDoubleDelimiter(tail, "**");
212
+ tail = balanceDoubleDelimiter(tail, "~~");
213
+ tail = balanceSingleEmphasis(tail, "*");
214
+ tail = balanceSingleEmphasis(tail, "_");
215
+ if (head.length + tail.length === text.length && text.endsWith(tail)) return text;
216
+ return head + tail;
217
+ }
218
+ export { closePartialMarkdown, closePartialProse, isGrowingFence, openFenceBody };
219
+
220
+ //# sourceMappingURL=heal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"heal.js","names":[],"sources":["../../src/core/heal.ts"],"sourcesContent":["/**\n * Streaming-tail markdown stabilization.\n *\n * While a reply streams, the accumulated markdown frequently ends mid-construct:\n * an unterminated ``` fence renders the partial code as raw text, a dangling\n * `**` or backtick shows literal markers, a half-typed `[link](url` shows raw\n * syntax — and each of these \"pops\" into its formatted form a few frames later\n * once the model closes it. `closePartialMarkdown` conservatively completes\n * (or hides) those trailing constructs so the streamed view stays visually\n * stable. It is applied ONLY while streaming; the final text renders verbatim.\n */\nimport { parseContainerMarker } from '../dialect'\nimport { inlineMathClose, isLiteralDollar } from './dollars'\nimport { openFence } from './fence-scan'\nimport { opensInlineMath } from './math-delimiters'\nimport { isEscaped } from './prose'\n\n/** Whether a stabilized fence is the one incomplete body still being written. */\nexport function isGrowingFence(growing: string | null, source: string): boolean {\n return growing !== null && growing.trimEnd() === source.trimEnd()\n}\n\n/**\n * The body of the fence a streaming text ends inside, or null when every fence is closed.\n *\n * Only the LAST fence can be unterminated, so this is how a renderer tells the one block that is\n * still being written from the finished blocks above it — a diagram above the tail is complete\n * and can be drawn now, instead of waiting for the whole reply to settle.\n */\nexport function openFenceBody(text: string): string | null {\n const fence = openFence(text)\n return fence ? text.slice(fence.bodyStart) : null\n}\n\n/** A trailing half-written image (`![alt](partial-url` or `![alt`) is hidden — a\n * broken-image flash is worse than a beat of nothing. */\nfunction dropTrailingPartialImage(tail: string): string {\n return tail.replace(/!\\[[^\\]]*(\\]\\([^)]*)?$/, '')\n}\n\n/** A trailing `[text](partial-url` gets its `)` so the anchor text renders as a\n * link immediately instead of flashing raw syntax until the URL completes. */\nfunction closeTrailingLink(tail: string): string {\n return /(^|[^!])\\[[^\\]]*\\]\\([^)]*$/.test(tail) ? `${tail})` : tail\n}\n\n/**\n * Code spans and CLOSED formulas blanked to spaces, positions kept. Emphasis has no meaning\n * inside either, and the balancers below would otherwise read `$a*b$` or `$x_i$` as an open\n * emphasis and append a stray `*` or `_` to the streaming tail.\n */\nfunction maskInert(tail: string): string {\n const blank = (span: string) => ' '.repeat(span.length)\n return tail.replace(/`[^`]*`/g, blank).replace(/\\$\\$[\\s\\S]*?\\$\\$|\\$[^$\\n]+\\$/g, blank)\n}\n\n/**\n * A formula still being typed is withheld from its opening `$` until the closing one arrives.\n * Math is typeset while a reply streams, so a half-typed `$\\frac{a}{` would otherwise print as\n * raw LaTeX and then jump into a formula. Dollars are paired by the renderer's own rule\n * (`literalDollars`, `math-delimiters.ts`), so the two never disagree about one `$`: a price, a\n * shell variable or a `$` with a space after it (`5$ and`) opens nothing and hides nothing.\n *\n * Only the first `$` left open is withheld, and only while no `$` follows it: a `$` that came and\n * did not close it (the shell line `$(seq 1 12); do printf \"%s\" $i`) shows these dollars are text.\n */\nfunction withholdPartialMath(tail: string): string {\n const code = tail.replace(/`[^`]*`/g, (span) => ' '.repeat(span.length))\n const displays: number[] = []\n for (let index = code.indexOf('$$'); index !== -1; index = code.indexOf('$$', index + 2)) {\n if (!isEscaped(code, index)) displays.push(index)\n }\n if (displays.length % 2 === 1) return tail.slice(0, displays.at(-1)).trimEnd()\n\n const inline = code.replace(/\\$\\$[\\s\\S]*?\\$\\$/g, (span) => ' '.repeat(span.length))\n for (let index = inline.indexOf('$'); index !== -1; index = inline.indexOf('$', index + 1)) {\n if (isEscaped(inline, index) || isLiteralDollar(inline, index)) continue\n // The last character: the one after it, which says whether it opens, is not written yet.\n const last = index === inline.length - 1\n if (!last && !opensInlineMath(inline, index)) continue\n const close = last ? -1 : inlineMathClose(inline, index)\n if (close !== -1) {\n index = close\n continue\n }\n return inline.includes('$', index + 1) ? tail : tail.slice(0, index).trimEnd()\n }\n return tail\n}\n\n/** Balance inline code: an odd number of backticks means an open span. A bare\n * trailing opener is stripped (closing it would render an empty span as\n * literal ``` `` ```); an opener with content gets closed. */\nfunction balanceInlineCode(tail: string): string {\n const count = (tail.replace(/\\\\`/g, '').match(/`/g) ?? []).length\n if (count % 2 === 0) return tail\n return tail.endsWith('`') ? tail.slice(0, -1) : `${tail}\\``\n}\n\n/** Balance a two-char emphasis delimiter (`**` / `~~`). Occurrences inside\n * inline code spans are ignored. A bare trailing opener is stripped (closing\n * an empty pair would render four literal characters); one with content after\n * it gets closed. */\nfunction balanceDoubleDelimiter(tail: string, delim: string): string {\n const stripped = maskInert(tail)\n let count = 0\n for (let i = 0; (i = stripped.indexOf(delim, i)) !== -1; i += delim.length) count += 1\n if (count % 2 === 0) return tail\n return tail.endsWith(delim) ? tail.slice(0, -delim.length) : `${tail}${delim}`\n}\n\nconst WORD_RE = /\\w/\n\n/**\n * Balance a single-char emphasis delimiter (`*` or `_`) — the most common\n * mid-stream flash after `**`: `*italic` shows the literal `*` until the model\n * closes it, then pops into italics. We append the missing closer so it renders\n * as emphasis immediately.\n *\n * The danger is false positives — a wrongly appended `*`/`_` italicises text the\n * author never marked up. So we count ONLY \"emphasis-capable\" lone delimiters:\n * - runs of length ≥ 2 are skipped (they belong to `**`/`__`/`~~` handling);\n * - inside inline code is masked out;\n * - `*` counts only when it touches a word char on one side — excludes globs\n * (`*.ts`), multiplication (`a * b`) and bullets (`* item`), where the `*` is\n * flanked by whitespace/punctuation;\n * - `_` additionally requires the word char to be on the OPENING side only —\n * so intraword underscores (`file_name`, `__init__`, `snake_case`) never\n * count, matching CommonMark's \"no intraword `_` emphasis\" rule.\n * An odd active count ⇒ a trailing opener is dangling: append one closer (or\n * strip a bare trailing opener, mirroring the double-delimiter rule).\n */\nfunction balanceSingleEmphasis(tail: string, char: '*' | '_'): string {\n const masked = maskInert(tail)\n let active = 0\n let i = 0\n while (i < masked.length) {\n if (masked[i] !== char) {\n i += 1\n continue\n }\n let j = i\n while (j < masked.length && masked[j] === char) j += 1\n if (j - i === 1) {\n const prevWord = WORD_RE.test(masked[i - 1] ?? '')\n const nextWord = WORD_RE.test(masked[j] ?? '')\n const counts =\n char === '*' ? prevWord || nextWord : (!prevWord && nextWord) || (prevWord && !nextWord)\n if (counts) active += 1\n }\n i = j\n }\n if (active % 2 === 0) return tail\n return tail.endsWith(char) ? tail.slice(0, -1) : `${tail}${char}`\n}\n\nconst ROW_START = /^\\s{0,3}\\|/\n/** The row of dashes under a table's header: `|---|:--:|`, its outer pipes optional. */\nconst DELIMITER_ROW = /^\\s*\\|?\\s*:?-{3,}:?\\s*(?:\\|\\s*:?-{3,}:?\\s*)*\\|?\\s*$/\n\n/** Hide only a partial row of an already established GFM table. A line starting\n * with `|` outside a table may be ordinary prose and must remain visible. */\nfunction dropTrailingPartialTableRow(text: string): string {\n if (text.endsWith('\\n')) return text\n const nl = text.lastIndexOf('\\n')\n if (nl === -1 || !ROW_START.test(text.slice(nl + 1))) return text\n\n // Up the lines above, one at a time: the rows of the table, to the row of dashes under its\n // header. The reply is not cut into its lines; a long one would be on every token of a row.\n for (let end = nl; end > 0;) {\n const start = text.lastIndexOf('\\n', end - 1) + 1\n // The first line of the text has no header above it.\n if (start === 0) break\n const line = text.slice(start, end)\n if (DELIMITER_ROW.test(line)) {\n const above = start === 1 ? 0 : text.lastIndexOf('\\n', start - 2) + 1\n return ROW_START.test(text.slice(above, start - 1)) ? text.slice(0, nl) : text\n }\n if (!ROW_START.test(line)) break\n end = start - 1\n }\n return text\n}\n\n/**\n * A quote line that is still typing its callout marker (`> [`, `> [!`, `> [!WARN`) is hidden\n * until the `]` arrives: it would otherwise print as a grey quote reading \"[!WARN\" and a beat\n * later turn into a note. A complete marker is kept — it already renders as the empty note.\n */\nfunction dropTrailingPartialCalloutMarker(text: string): string {\n const nl = text.lastIndexOf('\\n')\n const lastLine = text.slice(nl + 1)\n if (!/^\\s{0,3}>[ \\t]?\\[(?:!\\w*)?$/.test(lastLine)) return text\n return nl === -1 ? '' : text.slice(0, nl)\n}\n\n/**\n * A form's marker that arrived before its content (`> [!STEPS]` and nothing yet) is hidden: alone,\n * it is a quote reading \"[!STEPS]\", and the steps replace it a token later. Hidden only while\n * nothing follows it but blank space; once the list starts, the form renders.\n */\nfunction dropTrailingBareFormMarker(text: string): string {\n // `trimEnd`, not `/\\s+$/`: the pattern is tried at every run of blank space in the text.\n const trimmed = text.trimEnd()\n const nl = trimmed.lastIndexOf('\\n')\n const quoted = /^\\s{0,3}>[ \\t]?(.*)$/.exec(trimmed.slice(nl + 1))\n if (!quoted || !parseContainerMarker(quoted[1]!.trim())) return text\n return nl === -1 ? '' : trimmed.slice(0, nl + 1)\n}\n\nexport function closePartialMarkdown(text: string): string {\n if (!text) return text\n\n // Inside an unterminated fence: close it and skip inline fixes — everything\n // after the opener is code, where emphasis/link syntax has no meaning.\n const fence = openFence(text)\n if (fence) return `${text}${text.endsWith('\\n') ? '' : '\\n'}${fence.marker}`\n return closePartialProse(text)\n}\n\n/**\n * `closePartialMarkdown` for a text known to end outside a fenced code block: the renderer asks\n * its own scanner about the fence, and comes here with the answer.\n */\nexport function closePartialProse(text: string): string {\n if (!text) return text\n\n // A half-typed table row is its own kind of flash — hide it. The active edit\n // is that row, so there's nothing else to stabilize this frame.\n const detabled = dropTrailingPartialTableRow(text)\n if (detabled !== text) return detabled\n // Same for a callout marker still being typed on its quote line.\n const unmarked = dropTrailingPartialCalloutMarker(text)\n if (unmarked !== text) return unmarked\n const bare = dropTrailingBareFormMarker(text)\n if (bare !== text) return bare\n\n // Inline constructs cannot span a blank line, so only the last paragraph can\n // be dangling — operate on it alone and keep the rest byte-identical.\n const splitAt = text.lastIndexOf('\\n\\n')\n const head = splitAt === -1 ? '' : text.slice(0, splitAt + 2)\n let tail = splitAt === -1 ? text : text.slice(splitAt + 2)\n if (!tail) return text\n\n tail = dropTrailingPartialImage(tail)\n tail = withholdPartialMath(tail)\n tail = closeTrailingLink(tail)\n tail = balanceInlineCode(tail)\n tail = balanceDoubleDelimiter(tail, '**')\n tail = balanceDoubleDelimiter(tail, '~~')\n tail = balanceSingleEmphasis(tail, '*')\n tail = balanceSingleEmphasis(tail, '_')\n // Nothing to complete: the very text, so a long reply is not copied on every token.\n if (head.length + tail.length === text.length && text.endsWith(tail)) return text\n return head + tail\n}\n"],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ // src/core/load-once.ts
2
+ /**
3
+ * A lazy loader that runs once and remembers its success, but not its failure.
4
+ *
5
+ * The heavy parts (the math typesetter, the diagram engine) are fetched on first use. A loader
6
+ * that cached its rejection as faithfully as its result left the feature dead for the rest of the
7
+ * page after one failed chunk fetch (an app updated under an open tab, a dev server re-optimizing a
8
+ * dependency). A failure here clears the slot, so the next caller retries, while concurrent
9
+ * callers still share one request.
10
+ */
11
+ function loadOnce(load) {
12
+ let pending = null;
13
+ return () => {
14
+ pending ??= load().catch((error) => {
15
+ pending = null;
16
+ throw error;
17
+ });
18
+ return pending;
19
+ };
20
+ }
21
+ export { loadOnce };
22
+
23
+ //# sourceMappingURL=load-once.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load-once.js","names":[],"sources":["../../src/core/load-once.ts"],"sourcesContent":["/**\n * A lazy loader that runs once and remembers its success, but not its failure.\n *\n * The heavy parts (the math typesetter, the diagram engine) are fetched on first use. A loader\n * that cached its rejection as faithfully as its result left the feature dead for the rest of the\n * page after one failed chunk fetch (an app updated under an open tab, a dev server re-optimizing a\n * dependency). A failure here clears the slot, so the next caller retries, while concurrent\n * callers still share one request.\n */\nexport function loadOnce<T>(load: () => Promise<T>): () => Promise<T> {\n let pending: Promise<T> | null = null\n return () => {\n pending ??= load().catch((error: unknown) => {\n pending = null\n throw error\n })\n return pending\n }\n}\n"],"mappings":";;;;;;;;;;AASA,SAAgB,SAAY,MAA0C;CACpE,IAAI,UAA6B;CACjC,aAAa;EACX,YAAY,KAAK,CAAC,CAAC,OAAO,UAAmB;GAC3C,UAAU;GACV,MAAM;EACR,CAAC;EACD,OAAO;CACT;AACF"}
@@ -0,0 +1,33 @@
1
+ // src/core/math-delimiters.ts
2
+ /**
3
+ * When one `$` is a delimiter of inline math: pandoc's rule, which GitHub follows. The parser, the
4
+ * KaTeX sniffer, the literal-dollar escaper and the streaming stabilizer all ask here, so they
5
+ * agree about every `$`.
6
+ *
7
+ * - The opening `$` has something other than a space right after it.
8
+ * - The closing `$` has something other than a space right before it, and no digit right after.
9
+ * An escaped `\$` closes nothing: it is a dollar sign inside the formula.
10
+ *
11
+ * So `$x$` is math, and `$ x $`, `$a$5` and a shell line's `$(seq 1 12) … $i` are not.
12
+ */
13
+ const isSpace = (char) => char === " " || char === " " || char === "\n" || char === "\r";
14
+ const isDigit = (char) => char !== void 0 && char >= "0" && char <= "9";
15
+ /** Whether a single `$` at `index` can open a formula. At the end of the text it cannot. */
16
+ function opensInlineMath(text, index) {
17
+ const next = text[index + 1];
18
+ return next !== void 0 && next !== "$" && !isSpace(next);
19
+ }
20
+ /**
21
+ * Whether a single `$` at `index` can close a formula. At the end of the text it can: a stream
22
+ * that has not yet written the next character is read as if it ends there.
23
+ */
24
+ function closesInlineMath(text, index) {
25
+ const before = text[index - 1];
26
+ if (before === void 0 || isSpace(before) || before === "$" || isDigit(text[index + 1])) return false;
27
+ let slashes = 0;
28
+ while (text[index - 1 - slashes] === "\\") slashes += 1;
29
+ return slashes % 2 === 0;
30
+ }
31
+ export { closesInlineMath, opensInlineMath };
32
+
33
+ //# sourceMappingURL=math-delimiters.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"math-delimiters.js","names":[],"sources":["../../src/core/math-delimiters.ts"],"sourcesContent":["/**\n * When one `$` is a delimiter of inline math: pandoc's rule, which GitHub follows. The parser, the\n * KaTeX sniffer, the literal-dollar escaper and the streaming stabilizer all ask here, so they\n * agree about every `$`.\n *\n * - The opening `$` has something other than a space right after it.\n * - The closing `$` has something other than a space right before it, and no digit right after.\n * An escaped `\\$` closes nothing: it is a dollar sign inside the formula.\n *\n * So `$x$` is math, and `$ x $`, `$a$5` and a shell line's `$(seq 1 12) … $i` are not.\n */\n\nconst isSpace = (char: string | undefined) =>\n char === ' ' || char === '\\t' || char === '\\n' || char === '\\r'\nconst isDigit = (char: string | undefined) => char !== undefined && char >= '0' && char <= '9'\n\n/** Whether a single `$` at `index` can open a formula. At the end of the text it cannot. */\nexport function opensInlineMath(text: string, index: number): boolean {\n const next = text[index + 1]\n return next !== undefined && next !== '$' && !isSpace(next)\n}\n\n/**\n * Whether a single `$` at `index` can close a formula. At the end of the text it can: a stream\n * that has not yet written the next character is read as if it ends there.\n */\nexport function closesInlineMath(text: string, index: number): boolean {\n const before = text[index - 1]\n if (before === undefined || isSpace(before) || before === '$' || isDigit(text[index + 1]))\n return false\n let slashes = 0\n while (text[index - 1 - slashes] === '\\\\') slashes += 1\n return slashes % 2 === 0\n}\n"],"mappings":";;;;;;;;;;;;AAYA,MAAM,WAAW,SACf,SAAS,OAAO,SAAS,OAAQ,SAAS,QAAQ,SAAS;AAC7D,MAAM,WAAW,SAA6B,SAAS,KAAA,KAAa,QAAQ,OAAO,QAAQ;;AAG3F,SAAgB,gBAAgB,MAAc,OAAwB;CACpE,MAAM,OAAO,KAAK,QAAQ;CAC1B,OAAO,SAAS,KAAA,KAAa,SAAS,OAAO,CAAC,QAAQ,IAAI;AAC5D;;;;;AAMA,SAAgB,iBAAiB,MAAc,OAAwB;CACrE,MAAM,SAAS,KAAK,QAAQ;CAC5B,IAAI,WAAW,KAAA,KAAa,QAAQ,MAAM,KAAK,WAAW,OAAO,QAAQ,KAAK,QAAQ,EAAE,GACtF,OAAO;CACT,IAAI,UAAU;CACd,OAAO,KAAK,QAAQ,IAAI,aAAa,MAAM,WAAW;CACtD,OAAO,UAAU,MAAM;AACzB"}
@@ -0,0 +1,56 @@
1
+ import { visit } from "../engine/visit.js";
2
+ import { closesInlineMath, opensInlineMath } from "./math-delimiters.js";
3
+ // src/core/math-syntax.ts
4
+ const DOLLAR = 36;
5
+ /**
6
+ * `$x$`, read the way a code span is: a run of dollars opens it, the next run of the same length
7
+ * closes it, and nothing between them is Markdown. A single dollar also follows pandoc's rule
8
+ * (`math-delimiters.ts`): `$ x $`, `$a$5` and a shell line's `$(seq 1 12) … $i` are text.
9
+ */
10
+ function inlineMath(state) {
11
+ const { text, pos } = state;
12
+ let open = pos;
13
+ while (text.charCodeAt(open) === DOLLAR) open++;
14
+ const size = open - pos;
15
+ if (size === 1 && !opensInlineMath(text, pos)) return false;
16
+ for (let at = text.indexOf("$", open); at !== -1; at = text.indexOf("$", at)) {
17
+ let end = at;
18
+ while (text.charCodeAt(end) === DOLLAR) end++;
19
+ if (end - at === size && (size > 1 || closesInlineMath(text, at))) {
20
+ let value = text.slice(open, at).replace(/\n/g, " ");
21
+ if (value.length > 2 && value.startsWith(" ") && value.endsWith(" ") && value.trim() !== "") value = value.slice(1, -1);
22
+ state.push({
23
+ type: "inlineMath",
24
+ value
25
+ });
26
+ state.pos = end;
27
+ return true;
28
+ }
29
+ at = end;
30
+ }
31
+ return false;
32
+ }
33
+ /** The inline rule of a formula. */
34
+ const MATH_INLINE = [{
35
+ chars: "$",
36
+ parse: inlineMath
37
+ }];
38
+ /** A formula of a tree, with GitHub's third way to write a display one made one: a `math` fence. */
39
+ function eachFormula(root, each) {
40
+ visit(root, (node) => {
41
+ if (node.type === "code" && node.lang === "math") node.type = "math";
42
+ if (node.type === "inlineMath" || node.type === "math") each(node, node.type === "math");
43
+ });
44
+ }
45
+ /** Formulas read and left as their LaTeX: for a target that draws them its own way. */
46
+ const mathSyntax = {
47
+ name: "math",
48
+ mathFences: true,
49
+ inline: MATH_INLINE,
50
+ transform(root) {
51
+ eachFormula(root, () => {});
52
+ }
53
+ };
54
+ export { MATH_INLINE, eachFormula, mathSyntax };
55
+
56
+ //# sourceMappingURL=math-syntax.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"math-syntax.js","names":[],"sources":["../../src/core/math-syntax.ts"],"sourcesContent":["/**\n * How a formula is written: `$…$`, `$$…$$` and a ```` ```math ```` fence, read into `inlineMath`\n * and `math` nodes that hold the LaTeX as written. What a formula is drawn as is the target's:\n * `@localix-ai/flowdown/math` typesets it with KaTeX for a page, a terminal writes it in Unicode.\n */\nimport type { Extension, InlineRule, InlineState, Node } from '../engine/types'\nimport { visit } from '../engine/visit'\nimport { closesInlineMath, opensInlineMath } from './math-delimiters'\n\nconst DOLLAR = 36\n\n/**\n * `$x$`, read the way a code span is: a run of dollars opens it, the next run of the same length\n * closes it, and nothing between them is Markdown. A single dollar also follows pandoc's rule\n * (`math-delimiters.ts`): `$ x $`, `$a$5` and a shell line's `$(seq 1 12) … $i` are text.\n */\nfunction inlineMath(state: InlineState): boolean {\n const { text, pos } = state\n let open = pos\n while (text.charCodeAt(open) === DOLLAR) open++\n const size = open - pos\n if (size === 1 && !opensInlineMath(text, pos)) return false\n for (let at = text.indexOf('$', open); at !== -1; at = text.indexOf('$', at)) {\n let end = at\n while (text.charCodeAt(end) === DOLLAR) end++\n if (end - at === size && (size > 1 || closesInlineMath(text, at))) {\n let value = text.slice(open, at).replace(/\\n/g, ' ')\n // One space of padding inside `$$ … $$` is not part of the formula.\n if (value.length > 2 && value.startsWith(' ') && value.endsWith(' ') && value.trim() !== '')\n value = value.slice(1, -1)\n state.push({ type: 'inlineMath', value })\n state.pos = end\n return true\n }\n at = end\n }\n return false\n}\n\n/** The inline rule of a formula. */\nexport const MATH_INLINE: InlineRule[] = [{ chars: '$', parse: inlineMath }]\n\n/** A formula of a tree, with GitHub's third way to write a display one made one: a `math` fence. */\nexport function eachFormula(root: Node, each: (node: Node, display: boolean) => void): void {\n visit(root, (node) => {\n if (node.type === 'code' && node.lang === 'math') node.type = 'math'\n if (node.type === 'inlineMath' || node.type === 'math') each(node, node.type === 'math')\n })\n}\n\n/** Formulas read and left as their LaTeX: for a target that draws them its own way. */\nexport const mathSyntax: Extension = {\n name: 'math',\n mathFences: true,\n inline: MATH_INLINE,\n transform(root) {\n eachFormula(root, () => {})\n },\n}\n"],"mappings":";;;AASA,MAAM,SAAS;;;;;;AAOf,SAAS,WAAW,OAA6B;CAC/C,MAAM,EAAE,MAAM,QAAQ;CACtB,IAAI,OAAO;CACX,OAAO,KAAK,WAAW,IAAI,MAAM,QAAQ;CACzC,MAAM,OAAO,OAAO;CACpB,IAAI,SAAS,KAAK,CAAC,gBAAgB,MAAM,GAAG,GAAG,OAAO;CACtD,KAAK,IAAI,KAAK,KAAK,QAAQ,KAAK,IAAI,GAAG,OAAO,IAAI,KAAK,KAAK,QAAQ,KAAK,EAAE,GAAG;EAC5E,IAAI,MAAM;EACV,OAAO,KAAK,WAAW,GAAG,MAAM,QAAQ;EACxC,IAAI,MAAM,OAAO,SAAS,OAAO,KAAK,iBAAiB,MAAM,EAAE,IAAI;GACjE,IAAI,QAAQ,KAAK,MAAM,MAAM,EAAE,CAAC,CAAC,QAAQ,OAAO,GAAG;GAEnD,IAAI,MAAM,SAAS,KAAK,MAAM,WAAW,GAAG,KAAK,MAAM,SAAS,GAAG,KAAK,MAAM,KAAK,MAAM,IACvF,QAAQ,MAAM,MAAM,GAAG,EAAE;GAC3B,MAAM,KAAK;IAAE,MAAM;IAAc;GAAM,CAAC;GACxC,MAAM,MAAM;GACZ,OAAO;EACT;EACA,KAAK;CACP;CACA,OAAO;AACT;;AAGA,MAAa,cAA4B,CAAC;CAAE,OAAO;CAAK,OAAO;AAAW,CAAC;;AAG3E,SAAgB,YAAY,MAAY,MAAoD;CAC1F,MAAM,OAAO,SAAS;EACpB,IAAI,KAAK,SAAS,UAAU,KAAK,SAAS,QAAQ,KAAK,OAAO;EAC9D,IAAI,KAAK,SAAS,gBAAgB,KAAK,SAAS,QAAQ,KAAK,MAAM,KAAK,SAAS,MAAM;CACzF,CAAC;AACH;;AAGA,MAAa,aAAwB;CACnC,MAAM;CACN,YAAY;CACZ,QAAQ;CACR,UAAU,MAAM;EACd,YAAY,YAAY,CAAC,CAAC;CAC5B;AACF"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Which characters of a markdown source are prose — the text outside fenced, indented and inline
3
+ * code — as one mask the math sniffer, the HTML sniffer and the literal-dollar escaper all read.
4
+ *
5
+ * The mask keeps the source's exact length and line breaks: code characters become spaces, so an
6
+ * index into the mask is an index into the source. The escaper depends on that — it inserts a
7
+ * backslash at a position found in the mask — and a mask that dropped code lines (as the first
8
+ * version here did) would shift every position after the first code block.
9
+ */
10
+ export declare function markdownProseWithoutCode(markdown: string): string;
@@ -0,0 +1,77 @@
1
+ // src/core/prose.ts
2
+ /**
3
+ * Which characters of a markdown source are prose — the text outside fenced, indented and inline
4
+ * code — as one mask the math sniffer, the HTML sniffer and the literal-dollar escaper all read.
5
+ *
6
+ * The mask keeps the source's exact length and line breaks: code characters become spaces, so an
7
+ * index into the mask is an index into the source. The escaper depends on that — it inserts a
8
+ * backslash at a position found in the mask — and a mask that dropped code lines (as the first
9
+ * version here did) would shift every position after the first code block.
10
+ */
11
+ function markdownProseWithoutCode(markdown) {
12
+ if (!CODE_HINT.test(markdown)) return markdown;
13
+ const prose = [];
14
+ let fence = null;
15
+ const blank = (line) => " ".repeat(line.length);
16
+ for (const line of markdown.split("\n")) {
17
+ const marker = FENCE_MARKER.exec(line);
18
+ if (fence) {
19
+ if (marker && marker[1][0] === fence.character && marker[1].length >= fence.length && marker[2].trim() === "") fence = null;
20
+ prose.push(blank(line));
21
+ continue;
22
+ }
23
+ if (marker && !(marker[1][0] === "`" && marker[2].includes("`"))) {
24
+ fence = {
25
+ character: marker[1][0],
26
+ length: marker[1].length
27
+ };
28
+ prose.push(blank(line));
29
+ continue;
30
+ }
31
+ prose.push(INDENTED.test(line) ? blank(line) : line);
32
+ }
33
+ const source = prose.join("\n");
34
+ let visible = "";
35
+ let copied = 0;
36
+ for (let index = source.indexOf("`"); index !== -1; index = source.indexOf("`", index)) {
37
+ if (isEscaped(source, index)) {
38
+ index += 1;
39
+ continue;
40
+ }
41
+ let delimiterLength = 1;
42
+ while (source[index + delimiterLength] === "`") delimiterLength += 1;
43
+ let closing = source.indexOf("`", index + delimiterLength);
44
+ while (closing !== -1) {
45
+ if (isEscaped(source, closing)) {
46
+ closing = source.indexOf("`", closing + 1);
47
+ continue;
48
+ }
49
+ let closingLength = 1;
50
+ while (source[closing + closingLength] === "`") closingLength += 1;
51
+ if (closingLength === delimiterLength) break;
52
+ closing = source.indexOf("`", closing + closingLength);
53
+ }
54
+ if (closing === -1) {
55
+ index += delimiterLength;
56
+ continue;
57
+ }
58
+ const end = closing + delimiterLength;
59
+ visible += source.slice(copied, index) + source.slice(index, end).replace(NOT_NEWLINE, " ");
60
+ copied = end;
61
+ index = end;
62
+ }
63
+ return copied === 0 ? source : visible + source.slice(copied);
64
+ }
65
+ const CODE_HINT = /`|~~~|^(?: {4}|\t)/m;
66
+ const FENCE_MARKER = /^ {0,3}(`{3,}|~{3,})(.*)$/;
67
+ const INDENTED = /^(?: {4}|\t)/;
68
+ const NOT_NEWLINE = /[^\n]/g;
69
+ /** Whether the character at `index` is backslash-escaped (an odd run of `\` before it). */
70
+ function isEscaped(text, index) {
71
+ let slashCount = 0;
72
+ for (let cursor = index - 1; cursor >= 0 && text[cursor] === "\\"; cursor -= 1) slashCount += 1;
73
+ return slashCount % 2 === 1;
74
+ }
75
+ export { isEscaped, markdownProseWithoutCode };
76
+
77
+ //# sourceMappingURL=prose.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prose.js","names":[],"sources":["../../src/core/prose.ts"],"sourcesContent":["/**\n * Which characters of a markdown source are prose — the text outside fenced, indented and inline\n * code — as one mask the math sniffer, the HTML sniffer and the literal-dollar escaper all read.\n *\n * The mask keeps the source's exact length and line breaks: code characters become spaces, so an\n * index into the mask is an index into the source. The escaper depends on that — it inserts a\n * backslash at a position found in the mask — and a mask that dropped code lines (as the first\n * version here did) would shift every position after the first code block.\n */\nexport function markdownProseWithoutCode(markdown: string): string {\n // Nothing here can be code: no backtick or tilde run, no line indented four.\n if (!CODE_HINT.test(markdown)) return markdown\n const prose: string[] = []\n let fence: { character: string; length: number } | null = null\n const blank = (line: string) => ' '.repeat(line.length)\n\n for (const line of markdown.split('\\n')) {\n const marker = FENCE_MARKER.exec(line)\n if (fence) {\n if (\n marker &&\n marker[1]![0] === fence.character &&\n marker[1]!.length >= fence.length &&\n marker[2]!.trim() === ''\n ) {\n fence = null\n }\n prose.push(blank(line))\n continue\n }\n // A backtick fence has no backtick in its info string: ```` ```a``` ```` is inline code.\n if (marker && !(marker[1]![0] === '`' && marker[2]!.includes('`'))) {\n fence = { character: marker[1]![0]!, length: marker[1]!.length }\n prose.push(blank(line))\n continue\n }\n\n // Four-space/tab-indented CommonMark code is as inert as a fenced block.\n prose.push(INDENTED.test(line) ? blank(line) : line)\n }\n\n // Inline code spans may cross line breaks. Blank only complete spans; an\n // unmatched backtick stays prose, matching CommonMark's literal treatment.\n const source = prose.join('\\n')\n let visible = ''\n let copied = 0\n for (let index = source.indexOf('`'); index !== -1; index = source.indexOf('`', index)) {\n if (isEscaped(source, index)) {\n index += 1\n continue\n }\n let delimiterLength = 1\n while (source[index + delimiterLength] === '`') delimiterLength += 1\n let closing = source.indexOf('`', index + delimiterLength)\n while (closing !== -1) {\n if (isEscaped(source, closing)) {\n closing = source.indexOf('`', closing + 1)\n continue\n }\n let closingLength = 1\n while (source[closing + closingLength] === '`') closingLength += 1\n if (closingLength === delimiterLength) break\n closing = source.indexOf('`', closing + closingLength)\n }\n if (closing === -1) {\n index += delimiterLength\n continue\n }\n const end = closing + delimiterLength\n visible += source.slice(copied, index) + source.slice(index, end).replace(NOT_NEWLINE, ' ')\n copied = end\n index = end\n }\n return copied === 0 ? source : visible + source.slice(copied)\n}\n\nconst CODE_HINT = /`|~~~|^(?: {4}|\\t)/m\nconst FENCE_MARKER = /^ {0,3}(`{3,}|~{3,})(.*)$/\nconst INDENTED = /^(?: {4}|\\t)/\nconst NOT_NEWLINE = /[^\\n]/g\n\n/** Whether the character at `index` is backslash-escaped (an odd run of `\\` before it). */\nexport function isEscaped(text: string, index: number): boolean {\n let slashCount = 0\n for (let cursor = index - 1; cursor >= 0 && text[cursor] === '\\\\'; cursor -= 1) {\n slashCount += 1\n }\n return slashCount % 2 === 1\n}\n"],"mappings":";;;;;;;;;;AASA,SAAgB,yBAAyB,UAA0B;CAEjE,IAAI,CAAC,UAAU,KAAK,QAAQ,GAAG,OAAO;CACtC,MAAM,QAAkB,CAAC;CACzB,IAAI,QAAsD;CAC1D,MAAM,SAAS,SAAiB,IAAI,OAAO,KAAK,MAAM;CAEtD,KAAK,MAAM,QAAQ,SAAS,MAAM,IAAI,GAAG;EACvC,MAAM,SAAS,aAAa,KAAK,IAAI;EACrC,IAAI,OAAO;GACT,IACE,UACA,OAAO,EAAE,CAAE,OAAO,MAAM,aACxB,OAAO,EAAE,CAAE,UAAU,MAAM,UAC3B,OAAO,EAAE,CAAE,KAAK,MAAM,IAEtB,QAAQ;GAEV,MAAM,KAAK,MAAM,IAAI,CAAC;GACtB;EACF;EAEA,IAAI,UAAU,EAAE,OAAO,EAAE,CAAE,OAAO,OAAO,OAAO,EAAE,CAAE,SAAS,GAAG,IAAI;GAClE,QAAQ;IAAE,WAAW,OAAO,EAAE,CAAE;IAAK,QAAQ,OAAO,EAAE,CAAE;GAAO;GAC/D,MAAM,KAAK,MAAM,IAAI,CAAC;GACtB;EACF;EAGA,MAAM,KAAK,SAAS,KAAK,IAAI,IAAI,MAAM,IAAI,IAAI,IAAI;CACrD;CAIA,MAAM,SAAS,MAAM,KAAK,IAAI;CAC9B,IAAI,UAAU;CACd,IAAI,SAAS;CACb,KAAK,IAAI,QAAQ,OAAO,QAAQ,GAAG,GAAG,UAAU,IAAI,QAAQ,OAAO,QAAQ,KAAK,KAAK,GAAG;EACtF,IAAI,UAAU,QAAQ,KAAK,GAAG;GAC5B,SAAS;GACT;EACF;EACA,IAAI,kBAAkB;EACtB,OAAO,OAAO,QAAQ,qBAAqB,KAAK,mBAAmB;EACnE,IAAI,UAAU,OAAO,QAAQ,KAAK,QAAQ,eAAe;EACzD,OAAO,YAAY,IAAI;GACrB,IAAI,UAAU,QAAQ,OAAO,GAAG;IAC9B,UAAU,OAAO,QAAQ,KAAK,UAAU,CAAC;IACzC;GACF;GACA,IAAI,gBAAgB;GACpB,OAAO,OAAO,UAAU,mBAAmB,KAAK,iBAAiB;GACjE,IAAI,kBAAkB,iBAAiB;GACvC,UAAU,OAAO,QAAQ,KAAK,UAAU,aAAa;EACvD;EACA,IAAI,YAAY,IAAI;GAClB,SAAS;GACT;EACF;EACA,MAAM,MAAM,UAAU;EACtB,WAAW,OAAO,MAAM,QAAQ,KAAK,IAAI,OAAO,MAAM,OAAO,GAAG,CAAC,CAAC,QAAQ,aAAa,GAAG;EAC1F,SAAS;EACT,QAAQ;CACV;CACA,OAAO,WAAW,IAAI,SAAS,UAAU,OAAO,MAAM,MAAM;AAC9D;AAEA,MAAM,YAAY;AAClB,MAAM,eAAe;AACrB,MAAM,WAAW;AACjB,MAAM,cAAc;;AAGpB,SAAgB,UAAU,MAAc,OAAwB;CAC9D,IAAI,aAAa;CACjB,KAAK,IAAI,SAAS,QAAQ,GAAG,UAAU,KAAK,KAAK,YAAY,MAAM,UAAU,GAC3E,cAAc;CAEhB,OAAO,aAAa,MAAM;AAC5B"}
@@ -0,0 +1,5 @@
1
+ import { Extension, Tree } from "../engine/types.js";
2
+ /** The dialect as a remark plugin: its forms as the elements and classes `styles.css` draws. */
3
+ export declare const remarkAnswerBlocks: () => (tree: Tree) => void;
4
+ /** The dialect as an extension of the engine: what `<Markdown>` reads every document with. */
5
+ export declare const flowdownDialect: Extension;