@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,464 @@
1
+ import { parseContainerMarker } from "../dialect.js";
2
+ import { continues } from "./continues.js";
3
+ // src/core/blocks.ts
4
+ /**
5
+ * Splitting a document into top-level blocks that can be rendered, and remembered, one by one.
6
+ *
7
+ * A streaming reply grows at its end, and re-rendering the whole of it on every token is work
8
+ * proportional to its length, repeated per token. Cut at its top-level blocks instead, every block
9
+ * but the last is text that no longer changes: a renderer that memoizes each block by its source
10
+ * parses and reconciles only the tail.
11
+ *
12
+ * The cut must never change the result, so the text is cut only where one parse of the whole
13
+ * document is certain to start a new top-level block, and a block is merged with its neighbours
14
+ * wherever that parse would read them together:
15
+ * - the dialect's forms: a marker-only quote (`> [!STEPS]`) governs the list after it, `[!TABS]`
16
+ * the run of sections after it, and a `[!PROS]` right before a `[!CONS]` is one pair;
17
+ * - an image alone in its paragraph and an emphasis-only line under it, its caption;
18
+ * - an HTML block whose tag is closed further down;
19
+ * - a `$$` display formula with blank lines inside;
20
+ * - a document with footnote or link reference definitions, which resolve across blocks, is
21
+ * not split at all.
22
+ *
23
+ * The boundaries are found by a scan of the lines (`step`), not by a Markdown lexer: it follows
24
+ * only what decides where a top-level block can end (a fence, a list, an HTML block that blank
25
+ * lines do not end), and where it cannot tell, it stops cutting for the rest of the document.
26
+ * Merging is always safe, a wrong cut never is. The tests render a corpus both ways and require
27
+ * the same markup, at every prefix.
28
+ */
29
+ const FENCE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
30
+ const INNER_FENCE = /^[ \t]*(`{3,}|~{3,})(.*)$/;
31
+ const ITEM = /^( {0,3})([-+*]|\d{1,9}[.)])(.*)$/;
32
+ /** A list item that may interrupt a paragraph: a bullet, or the number 1, with text after it. */
33
+ const INTERRUPTING_ITEM = /^ {0,3}(?:[-+*]|1[.)]) +\S/;
34
+ const RULE = /^ {0,3}(?:(?:-[ \t]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})$/;
35
+ const HEADING = /^ {0,3}(#{1,6})(?:[ \t]|$)/;
36
+ const UNDERLINE = /\n {0,3}(?:=+|-+)[ \t]*(?:\n|$)/;
37
+ const QUOTE = /^ {0,3}>[ \t]?(.*)$/;
38
+ /** HTML blocks that a blank line does not end: how each opens, and what closes it. */
39
+ const LONG_HTML = [
40
+ [/^ {0,3}<(?:pre|script|style|textarea)(?=[\s>]|$)/i, /<\/(?:pre|script|style|textarea)>/i],
41
+ [/^ {0,3}<!--/, /-->/],
42
+ [/^ {0,3}<\?/, /\?>/],
43
+ [/^ {0,3}<!\[CDATA\[/, /\]\]>/],
44
+ [/^ {0,3}<![A-Za-z]/, />/]
45
+ ];
46
+ const HTML_START = /^ {0,3}<[A-Za-z/!?]/;
47
+ const HTML_LINE = /^ {0,3}<[A-Za-z]/m;
48
+ const HTML_LINES = /^ {0,3}<([A-Za-z][\w:-]*)(?=[\s>/])/gm;
49
+ const FOOTNOTE_DEFINITION = /\[\^[^\]\s]{1,200}\]:/;
50
+ const LINK_DEFINITION = /^ {0,3}\[[^\]]+\]:/;
51
+ const OPEN_LABEL = /^ {0,3}\[[^\]]*$/;
52
+ const IMAGE_ONLY = /^!\[[^\]]*\]\([^)]*\)$/;
53
+ const DOUBLE_DOLLAR = /(?<!\\)\$\$/g;
54
+ const VOID_TAGS = /* @__PURE__ */ new Set([
55
+ "area",
56
+ "br",
57
+ "col",
58
+ "embed",
59
+ "hr",
60
+ "img",
61
+ "input",
62
+ "source",
63
+ "wbr"
64
+ ]);
65
+ const whole = (markdown) => [{
66
+ source: markdown,
67
+ offset: 0,
68
+ line: 1
69
+ }];
70
+ const start = () => ({
71
+ fence: null,
72
+ html: null,
73
+ list: 0,
74
+ unsure: false,
75
+ markup: false,
76
+ fresh: true,
77
+ odd: false,
78
+ label: false,
79
+ closed: false,
80
+ indented: false,
81
+ mixed: false,
82
+ inner: null,
83
+ seen: false,
84
+ stuck: false,
85
+ whole: false
86
+ });
87
+ /** The column a line's text starts at, tabs to the next multiple of four; -1 for a blank line. */
88
+ function indentOf(line) {
89
+ let column = 0;
90
+ for (let index = 0; index < line.length; index += 1) {
91
+ const code = line.charCodeAt(index);
92
+ if (code === 32) column += 1;
93
+ else if (code === 9) column += 4 - column % 4;
94
+ else return column;
95
+ }
96
+ return -1;
97
+ }
98
+ /**
99
+ * The content column of a list item's line, 0 when the line is not one, -1 when it is one whose
100
+ * column a tab after the marker leaves unknown.
101
+ */
102
+ function itemColumn(line) {
103
+ const item = ITEM.exec(line);
104
+ if (!item) return 0;
105
+ const base = item[1].length + item[2].length;
106
+ const rest = item[3];
107
+ if (rest === "") return base + 1;
108
+ if (rest[0] === " ") return -1;
109
+ if (rest[0] !== " ") return 0;
110
+ const text = rest.trimStart();
111
+ const spaces = rest.length - text.length;
112
+ if (text === "") return base + 1;
113
+ if (rest.slice(0, spaces).includes(" ")) return -1;
114
+ return base + (spaces > 4 ? 1 : spaces);
115
+ }
116
+ /**
117
+ * One line through the scan. True when the line starts a new top-level block for certain: after a
118
+ * blank line outside every open construct, or as the fence line of a top-level fence.
119
+ */
120
+ function step(scan, line) {
121
+ const seen = scan.seen;
122
+ const closed = scan.closed;
123
+ if (!seen && line.trim() !== "") scan.seen = true;
124
+ const cut = advance(scan, line);
125
+ if (closed && !scan.closed && !cut) scan.mixed = true;
126
+ return cut && seen;
127
+ }
128
+ function advance(scan, line) {
129
+ if (scan.stuck || scan.whole) return false;
130
+ if (scan.fence) {
131
+ const closing = FENCE.exec(line);
132
+ if (closing && closing[1][0] === scan.fence[0] && closing[1].length >= scan.fence.length && closing[2].trim() === "") {
133
+ scan.fence = null;
134
+ scan.fresh = true;
135
+ scan.closed = true;
136
+ }
137
+ return false;
138
+ }
139
+ if (scan.html) {
140
+ if (scan.html.test(line)) scan.html = null;
141
+ return false;
142
+ }
143
+ const indent = indentOf(line);
144
+ if (indent === -1) {
145
+ scan.fresh = true;
146
+ scan.markup = false;
147
+ scan.label = false;
148
+ return false;
149
+ }
150
+ const fresh = scan.fresh;
151
+ const closed = scan.closed;
152
+ scan.fresh = false;
153
+ scan.closed = false;
154
+ if (FOOTNOTE_DEFINITION.test(line) || LINK_DEFINITION.test(line)) return definition(scan);
155
+ if (scan.label) {
156
+ const close = line.indexOf("]");
157
+ if (close !== -1) {
158
+ if (line[close + 1] === ":") return definition(scan);
159
+ scan.label = false;
160
+ }
161
+ } else if (OPEN_LABEL.test(line)) scan.label = true;
162
+ const odd = scan.odd;
163
+ if (line.includes("$$") && (line.match(DOUBLE_DOLLAR)?.length ?? 0) % 2 === 1) scan.odd = !odd;
164
+ const fence = FENCE.exec(line);
165
+ const opensFence = fence !== null && !(fence[1][0] === "`" && fence[2].includes("`"));
166
+ if (opensFence && (odd || scan.markup)) return stick(scan);
167
+ if (scan.list > 0) {
168
+ if (indent >= scan.list) {
169
+ if (opensFence && scan.unsure) return stick(scan);
170
+ const inner = INNER_FENCE.exec(line);
171
+ if (scan.inner) {
172
+ if (inner && inner[1][0] === scan.inner[0] && inner[1].length >= scan.inner.length && inner[2].trim() === "") scan.inner = null;
173
+ } else if (inner && !(inner[1][0] === "`" && inner[2].includes("`"))) scan.inner = inner[1];
174
+ return false;
175
+ }
176
+ if (scan.inner) return stick(scan);
177
+ const column = RULE.test(line) ? 0 : itemColumn(line);
178
+ if (column === -1) return stick(scan);
179
+ if (column > 0) {
180
+ scan.list = column;
181
+ if (fresh) scan.unsure = false;
182
+ return false;
183
+ }
184
+ if (fresh || opensFence || HEADING.test(line) || RULE.test(line) || QUOTE.test(line) || LONG_HTML.some(([opens]) => opens.test(line))) {
185
+ scan.list = 0;
186
+ scan.unsure = false;
187
+ } else {
188
+ scan.unsure = true;
189
+ if (HTML_START.test(line)) scan.markup = true;
190
+ return false;
191
+ }
192
+ }
193
+ const indented = scan.indented;
194
+ scan.indented = indent >= 4;
195
+ const cut = closed || fresh && indent < 4 && !odd && !indented;
196
+ for (const [opens, closes] of LONG_HTML) {
197
+ if (!opens.test(line)) continue;
198
+ if (!closes.test(line.replace(opens, ""))) scan.html = closes;
199
+ return cut;
200
+ }
201
+ if (opensFence) {
202
+ scan.fence = fence[1];
203
+ return !odd;
204
+ }
205
+ const column = indent < 4 && !RULE.test(line) ? itemColumn(line) : 0;
206
+ if (column === -1) return stick(scan);
207
+ if (column > 0 && indented && !INTERRUPTING_ITEM.test(line)) return stick(scan);
208
+ if (column > 0) {
209
+ scan.list = column;
210
+ scan.unsure = scan.markup || !fresh && !INTERRUPTING_ITEM.test(line);
211
+ return cut;
212
+ }
213
+ if (HTML_START.test(line)) scan.markup = true;
214
+ return cut;
215
+ }
216
+ function stick(scan) {
217
+ scan.stuck = true;
218
+ return false;
219
+ }
220
+ function definition(scan) {
221
+ scan.whole = true;
222
+ return false;
223
+ }
224
+ /** The line of `text` that starts at `at`, without its newline. */
225
+ function lineAt(text, at) {
226
+ const end = text.indexOf("\n", at);
227
+ return text.slice(at, end === -1 ? text.length : end);
228
+ }
229
+ /** Whether `raw` has a line that opens an HTML tag still open at its end. */
230
+ function htmlOpen(raw) {
231
+ if (raw.includes("<!--") && raw.split("<!--").length > raw.split("-->").length) return true;
232
+ if (!HTML_LINE.test(raw)) return false;
233
+ const tried = /* @__PURE__ */ new Set();
234
+ for (const opener of raw.matchAll(HTML_LINES)) {
235
+ const tag = opener[1].toLowerCase();
236
+ if (tried.has(tag) || VOID_TAGS.has(tag)) continue;
237
+ tried.add(tag);
238
+ const rest = raw.slice(opener.index);
239
+ if ((rest.match(new RegExp(`<${tag}(?=[\\s>/])[^>]*>`, "gi")) ?? []).filter((found) => !found.endsWith("/>")).length > (rest.match(new RegExp(`</${tag}\\s*>`, "gi"))?.length ?? 0)) return true;
240
+ }
241
+ return false;
242
+ }
243
+ /**
244
+ * The pieces the scan cut (`starts`, offsets into `text`) joined into the blocks that must render
245
+ * together: the offsets where a block starts. Read by the same grammar as the remark pass.
246
+ */
247
+ function group(text, starts) {
248
+ const count = starts.length;
249
+ const endOf = (piece) => piece + 1 < count ? starts[piece + 1] : text.length;
250
+ const first = (piece) => piece < count ? lineAt(text, starts[piece]) : "";
251
+ const kindOf = (line) => {
252
+ const quoted = QUOTE.exec(line);
253
+ return quoted ? parseContainerMarker(quoted[1].trimStart())?.kind ?? null : null;
254
+ };
255
+ const isItem = (line) => !RULE.test(line) && itemColumn(line) !== 0;
256
+ /** The piece after the tabs whose first heading is in `holder`, on `line` or underlined. */
257
+ const tabsEnd = (holder, line, fallback) => {
258
+ if (holder >= count) return fallback;
259
+ const opening = HEADING.exec(line);
260
+ if (!opening && !UNDERLINE.test(text.slice(starts[holder], endOf(holder)))) return fallback;
261
+ const depth = opening ? opening[1].length : 1;
262
+ let end = holder + 1;
263
+ for (; end < count; end += 1) {
264
+ const next = first(end);
265
+ if (RULE.test(next)) break;
266
+ const heading = HEADING.exec(next);
267
+ if (heading && heading[1].length < depth) break;
268
+ }
269
+ return end;
270
+ };
271
+ /**
272
+ * The form a piece holds a marker line for, and the piece after everything it governs. A list
273
+ * form takes the one list right after it; tabs take the sections from the heading right after
274
+ * them up to a shallower heading, a `---` or the end. The marker is a quote of its own, on any
275
+ * line of the piece: on the last, what it governs is the next piece. A marker whose quote goes
276
+ * on (`> [!PROS]\n> - a`) holds its own content. Of several markers, the one that reaches
277
+ * furthest decides.
278
+ */
279
+ const form = (piece) => {
280
+ if (piece >= count) return null;
281
+ const body = text.slice(starts[piece], endOf(piece)).trimEnd();
282
+ if (!body.includes("[!")) return null;
283
+ const lines = body.split("\n");
284
+ let found = null;
285
+ for (let index = 0; index < lines.length; index += 1) {
286
+ const kind = kindOf(lines[index]);
287
+ if (!kind || index > 0 && QUOTE.test(lines[index - 1])) continue;
288
+ const next = lines[index + 1];
289
+ let end = piece + 1;
290
+ if (next === void 0) {
291
+ if (kind === "tabs") end = tabsEnd(piece + 1, first(piece + 1), piece + 1);
292
+ else if (isItem(first(piece + 1))) end = piece + 2;
293
+ } else if (kind === "tabs" && !QUOTE.test(next)) end = tabsEnd(piece, next, piece + 1);
294
+ const reach = found ? found.end : 0;
295
+ found = {
296
+ kind,
297
+ end: Math.max(end, reach)
298
+ };
299
+ }
300
+ return found;
301
+ };
302
+ const blocks = [];
303
+ let piece = 0;
304
+ while (piece < count) {
305
+ const from = piece;
306
+ const found = form(piece);
307
+ piece = found ? found.end : piece + 1;
308
+ if (found && (found.kind === "pros" || found.kind === "cons")) {
309
+ const other = form(piece);
310
+ if (other && other.kind === (found.kind === "pros" ? "cons" : "pros")) piece = other.end;
311
+ }
312
+ if (piece === from + 1 && piece < count && IMAGE_ONLY.test(text.slice(starts[from], endOf(from)).trim()) && /^[*_]/.test(first(piece).trimStart())) piece += 1;
313
+ while (piece < count && htmlOpen(text.slice(starts[from], starts[piece]))) piece += 1;
314
+ blocks.push(starts[from]);
315
+ }
316
+ return blocks;
317
+ }
318
+ /**
319
+ * Line endings as the scan reads them: `\r\n` and `\r` become `\n`. Offsets of the blocks are into
320
+ * the normalized text, which is what the renderer draws from.
321
+ */
322
+ const normalizeLineEndings = (markdown) => markdown.includes("\r") ? markdown.replace(/\r\n?/g, "\n") : markdown;
323
+ /** Blocks that are one fenced code block and nothing else: no prose, HTML, math or heading. */
324
+ const fenced = /* @__PURE__ */ new WeakSet();
325
+ /** Whether the splitter cut this block as one fenced code block. */
326
+ const isFenceBlock = (block) => fenced.has(block);
327
+ const MARKS = 3;
328
+ /**
329
+ * The pieces of `text` from `from` on: the offsets where the scan cut, or null when a definition
330
+ * keeps the document whole. `progress` and `mark` are where an earlier scan of the same start
331
+ * can go on from.
332
+ */
333
+ function pieces(text, from, progress, mark) {
334
+ const scan = mark ? { ...mark.scan } : start();
335
+ const starts = progress && mark ? progress.starts.slice(0, mark.count) : [from];
336
+ const marks = progress && mark ? progress.marks.filter((kept) => kept.at <= mark.at) : [];
337
+ let at = mark ? mark.at : from;
338
+ let marked = text.length;
339
+ for (let back = 0; back < MARKS && marked > at; back += 1) marked = text.lastIndexOf("\n", marked - 1);
340
+ for (let end = text.indexOf("\n", at); end !== -1; end = text.indexOf("\n", at)) {
341
+ if (step(scan, text.slice(at, end)) && at > from) starts.push(at);
342
+ at = end + 1;
343
+ if (end >= marked) marks.push({
344
+ at,
345
+ scan: { ...scan },
346
+ count: starts.length
347
+ });
348
+ }
349
+ const next = {
350
+ from,
351
+ starts: [...starts],
352
+ marks: marks.slice(-3)
353
+ };
354
+ if (at < text.length && step(scan, text.slice(at)) && at > from) starts.push(at);
355
+ return scan.whole ? null : {
356
+ starts,
357
+ progress: next,
358
+ mixed: scan.mixed
359
+ };
360
+ }
361
+ /** The blocks of `text` that the pieces at `cuts` make, the first on line `line`. */
362
+ function build(text, cuts, line, mixed) {
363
+ const starts = group(text, cuts);
364
+ const blocks = [];
365
+ let current = line;
366
+ let piece = 0;
367
+ for (let index = 0; index < starts.length; index += 1) {
368
+ const offset = starts[index];
369
+ const end = starts[index + 1] ?? text.length;
370
+ const source = text.slice(offset, end);
371
+ const block = {
372
+ source,
373
+ offset,
374
+ line: current
375
+ };
376
+ while (cuts[piece] < offset) piece += 1;
377
+ if ((cuts[piece + 1] ?? text.length) === end && !(mixed && end === text.length)) {
378
+ const opening = FENCE.exec(lineAt(text, offset));
379
+ if (opening && !(opening[1][0] === "`" && opening[2].includes("`"))) fenced.add(block);
380
+ }
381
+ blocks.push(block);
382
+ if (index + 1 === starts.length) break;
383
+ for (let at = source.indexOf("\n"); at !== -1; at = source.indexOf("\n", at + 1)) current += 1;
384
+ }
385
+ return blocks;
386
+ }
387
+ /** The top-level blocks of a document, cut where one parse of the whole would not join them. */
388
+ function splitBlocks(input) {
389
+ const markdown = normalizeLineEndings(input);
390
+ if (!markdown) return whole(markdown);
391
+ const cut = pieces(markdown, 0, null, null);
392
+ return cut ? build(markdown, cut.starts, 1, cut.mixed) : whole(markdown);
393
+ }
394
+ /**
395
+ * How many blocks at the end are read again. Text appended to a document can change only the
396
+ * group it is appended to, and that group can join the one before it (a `[!CONS]` arriving
397
+ * under a `[!PROS]`); every group above those two is final.
398
+ */
399
+ const REOPENED = 2;
400
+ /**
401
+ * `splitBlocks` for a document that grows at its end, as a streaming reply does. It remembers the
402
+ * blocks it cut last time and where its scan stopped: when the new text still starts with all of
403
+ * them but the last two, it keeps those, as the very same objects, and scans only the lines that
404
+ * are new. Any other text (a reply that was regenerated, or edited above its end) is split afresh.
405
+ * Each update then costs the new characters, not the reply: a stream is linear in its length.
406
+ */
407
+ function createBlockSplitter() {
408
+ let text = "";
409
+ let blocks = [];
410
+ let progress = null;
411
+ let lexed = 0;
412
+ return {
413
+ get lexed() {
414
+ return lexed;
415
+ },
416
+ split(input) {
417
+ const markdown = normalizeLineEndings(input);
418
+ if (!markdown) {
419
+ text = "";
420
+ blocks = [];
421
+ progress = null;
422
+ return whole(markdown);
423
+ }
424
+ let kept = blocks.slice(0, Math.max(0, blocks.length - REOPENED));
425
+ let last = kept.at(-1);
426
+ let from = last ? last.offset + last.source.length : 0;
427
+ if (from > 0 && !continues(markdown, text, from)) {
428
+ kept = [];
429
+ last = void 0;
430
+ from = 0;
431
+ }
432
+ let mark = null;
433
+ if (progress !== null && progress.from === from) for (let index = progress.marks.length - 1; index >= 0 && !mark; index -= 1) {
434
+ const tried = progress.marks[index];
435
+ if (continues(markdown, text, tried.at, from)) mark = tried;
436
+ }
437
+ lexed += markdown.length - (mark ? mark.at : from);
438
+ const cut = pieces(markdown, from, mark ? progress : null, mark);
439
+ text = markdown;
440
+ if (!cut) {
441
+ blocks = [];
442
+ progress = null;
443
+ return whole(markdown);
444
+ }
445
+ progress = cut.progress;
446
+ let line = 1;
447
+ if (last) {
448
+ line = last.line;
449
+ for (let at = last.source.indexOf("\n"); at !== -1; at = last.source.indexOf("\n", at + 1)) line += 1;
450
+ }
451
+ const rest = build(markdown, cut.starts, line, cut.mixed);
452
+ for (let index = 0; index < rest.length; index += 1) {
453
+ const before = blocks[kept.length + index];
454
+ const after = rest[index];
455
+ if (before && before.offset === after.offset && before.source === after.source) rest[index] = before;
456
+ }
457
+ blocks = [...kept, ...rest];
458
+ return blocks;
459
+ }
460
+ };
461
+ }
462
+ export { createBlockSplitter, isFenceBlock, normalizeLineEndings, splitBlocks };
463
+
464
+ //# sourceMappingURL=blocks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blocks.js","names":[],"sources":["../../src/core/blocks.ts"],"sourcesContent":["/**\n * Splitting a document into top-level blocks that can be rendered, and remembered, one by one.\n *\n * A streaming reply grows at its end, and re-rendering the whole of it on every token is work\n * proportional to its length, repeated per token. Cut at its top-level blocks instead, every block\n * but the last is text that no longer changes: a renderer that memoizes each block by its source\n * parses and reconciles only the tail.\n *\n * The cut must never change the result, so the text is cut only where one parse of the whole\n * document is certain to start a new top-level block, and a block is merged with its neighbours\n * wherever that parse would read them together:\n * - the dialect's forms: a marker-only quote (`> [!STEPS]`) governs the list after it, `[!TABS]`\n * the run of sections after it, and a `[!PROS]` right before a `[!CONS]` is one pair;\n * - an image alone in its paragraph and an emphasis-only line under it, its caption;\n * - an HTML block whose tag is closed further down;\n * - a `$$` display formula with blank lines inside;\n * - a document with footnote or link reference definitions, which resolve across blocks, is\n * not split at all.\n *\n * The boundaries are found by a scan of the lines (`step`), not by a Markdown lexer: it follows\n * only what decides where a top-level block can end (a fence, a list, an HTML block that blank\n * lines do not end), and where it cannot tell, it stops cutting for the rest of the document.\n * Merging is always safe, a wrong cut never is. The tests render a corpus both ways and require\n * the same markup, at every prefix.\n */\nimport { parseContainerMarker, type ContainerKind } from '../dialect'\nimport { continues } from './continues'\n\nexport interface MarkdownBlock {\n /** The block's own source. */\n source: string\n /** Where it starts in the document: characters before it, and its 1-based line. */\n offset: number\n line: number\n}\n\nconst FENCE = /^ {0,3}(`{3,}|~{3,})(.*)$/\nconst INNER_FENCE = /^[ \\t]*(`{3,}|~{3,})(.*)$/\nconst ITEM = /^( {0,3})([-+*]|\\d{1,9}[.)])(.*)$/\n/** A list item that may interrupt a paragraph: a bullet, or the number 1, with text after it. */\nconst INTERRUPTING_ITEM = /^ {0,3}(?:[-+*]|1[.)]) +\\S/\nconst RULE = /^ {0,3}(?:(?:-[ \\t]*){3,}|(?:_[ \\t]*){3,}|(?:\\*[ \\t]*){3,})$/\nconst HEADING = /^ {0,3}(#{1,6})(?:[ \\t]|$)/\nconst UNDERLINE = /\\n {0,3}(?:=+|-+)[ \\t]*(?:\\n|$)/\nconst QUOTE = /^ {0,3}>[ \\t]?(.*)$/\n/** HTML blocks that a blank line does not end: how each opens, and what closes it. */\nconst LONG_HTML: ReadonlyArray<readonly [RegExp, RegExp]> = [\n [/^ {0,3}<(?:pre|script|style|textarea)(?=[\\s>]|$)/i, /<\\/(?:pre|script|style|textarea)>/i],\n [/^ {0,3}<!--/, /-->/],\n [/^ {0,3}<\\?/, /\\?>/],\n [/^ {0,3}<!\\[CDATA\\[/, /\\]\\]>/],\n [/^ {0,3}<![A-Za-z]/, />/],\n]\nconst HTML_START = /^ {0,3}<[A-Za-z/!?]/\nconst HTML_LINE = /^ {0,3}<[A-Za-z]/m\nconst HTML_LINES = /^ {0,3}<([A-Za-z][\\w:-]*)(?=[\\s>/])/gm\nconst FOOTNOTE_DEFINITION = /\\[\\^[^\\]\\s]{1,200}\\]:/\nconst LINK_DEFINITION = /^ {0,3}\\[[^\\]]+\\]:/\nconst OPEN_LABEL = /^ {0,3}\\[[^\\]]*$/\nconst IMAGE_ONLY = /^!\\[[^\\]]*\\]\\([^)]*\\)$/\nconst DOUBLE_DOLLAR = /(?<!\\\\)\\$\\$/g\nconst VOID_TAGS = new Set(['area', 'br', 'col', 'embed', 'hr', 'img', 'input', 'source', 'wbr'])\n\nconst whole = (markdown: string): MarkdownBlock[] => [{ source: markdown, offset: 0, line: 1 }]\n\n/** What the scan knows after a line: only what decides where a top-level block can end. */\ninterface Scan {\n /** The opening run of the top-level fence the text is inside. */\n fence: string | null\n /** What closes the HTML block the text is inside, one that blank lines do not end. */\n html: RegExp | null\n /** The content column of the open top-level list item; 0 outside a list. */\n list: number\n /** The list may already be over: a line left of the item that was a lazy continuation or not. */\n unsure: boolean\n /** The lines since the last blank one may be an HTML block, where a fence line is not a fence. */\n markup: boolean\n /** The next line starts a block: the last one was blank or closed a fence, or there was none. */\n fresh: boolean\n /** An odd number of `$$` so far in this block: a display formula may be open. */\n odd: boolean\n /** A `[label` that has not closed yet: it may turn out to be a link definition. */\n label: boolean\n /**\n * The last line with text was indented four or more: code, or the rest of a paragraph. The\n * parser still has indented code open when it reads the next line, and reads it as one that\n * interrupts: `2. two` or an empty `-` is then text, not a list. The line after is never cut.\n */\n indented: boolean\n /** The last line with text closed a top-level fence: whatever comes next is a new block. */\n closed: boolean\n /** Text follows a closed fence in the same piece: that piece is not only a fence. */\n mixed: boolean\n /** The opening run of a fence inside the open list item. */\n inner: string | null\n /** A line with text has been read: blank lines at the very start belong to the first block. */\n seen: boolean\n /** The scan cannot tell what the next lines are: no more cuts. */\n stuck: boolean\n /** A definition that resolves across blocks: the document stays one block. */\n whole: boolean\n}\n\nconst start = (): Scan => ({\n fence: null,\n html: null,\n list: 0,\n unsure: false,\n markup: false,\n fresh: true,\n odd: false,\n label: false,\n closed: false,\n indented: false,\n mixed: false,\n inner: null,\n seen: false,\n stuck: false,\n whole: false,\n})\n\n/** The column a line's text starts at, tabs to the next multiple of four; -1 for a blank line. */\nfunction indentOf(line: string): number {\n let column = 0\n for (let index = 0; index < line.length; index += 1) {\n const code = line.charCodeAt(index)\n if (code === 32) column += 1\n else if (code === 9) column += 4 - (column % 4)\n else return column\n }\n return -1\n}\n\n/**\n * The content column of a list item's line, 0 when the line is not one, -1 when it is one whose\n * column a tab after the marker leaves unknown.\n */\nfunction itemColumn(line: string): number {\n const item = ITEM.exec(line)\n if (!item) return 0\n const base = item[1]!.length + item[2]!.length\n const rest = item[3]!\n if (rest === '') return base + 1\n if (rest[0] === '\\t') return -1\n if (rest[0] !== ' ') return 0\n const text = rest.trimStart()\n const spaces = rest.length - text.length\n if (text === '') return base + 1\n if (rest.slice(0, spaces).includes('\\t')) return -1\n return base + (spaces > 4 ? 1 : spaces)\n}\n\n/**\n * One line through the scan. True when the line starts a new top-level block for certain: after a\n * blank line outside every open construct, or as the fence line of a top-level fence.\n */\nfunction step(scan: Scan, line: string): boolean {\n const seen = scan.seen\n const closed = scan.closed\n if (!seen && line.trim() !== '') scan.seen = true\n const cut = advance(scan, line)\n // A line with text right after a fence that did not start a block: the scan was stuck on it.\n if (closed && !scan.closed && !cut) scan.mixed = true\n return cut && seen\n}\n\nfunction advance(scan: Scan, line: string): boolean {\n if (scan.stuck || scan.whole) return false\n if (scan.fence) {\n const closing = FENCE.exec(line)\n if (\n closing &&\n closing[1]![0] === scan.fence[0] &&\n closing[1]!.length >= scan.fence.length &&\n closing[2]!.trim() === ''\n ) {\n scan.fence = null\n scan.fresh = true\n scan.closed = true\n }\n return false\n }\n if (scan.html) {\n if (scan.html.test(line)) scan.html = null\n return false\n }\n const indent = indentOf(line)\n if (indent === -1) {\n scan.fresh = true\n scan.markup = false\n scan.label = false\n return false\n }\n const fresh = scan.fresh\n const closed = scan.closed\n scan.fresh = false\n scan.closed = false\n\n if (FOOTNOTE_DEFINITION.test(line) || LINK_DEFINITION.test(line)) return definition(scan)\n if (scan.label) {\n const close = line.indexOf(']')\n if (close !== -1) {\n if (line[close + 1] === ':') return definition(scan)\n scan.label = false\n }\n } else if (OPEN_LABEL.test(line)) scan.label = true\n\n const odd = scan.odd\n if (line.includes('$$') && (line.match(DOUBLE_DOLLAR)?.length ?? 0) % 2 === 1) scan.odd = !odd\n\n const fence = FENCE.exec(line)\n // A backtick fence has no backtick in its info string: ```` ```a``` ```` is inline code.\n const opensFence = fence !== null && !(fence[1]![0] === '`' && fence[2]!.includes('`'))\n // Inside a formula or an HTML block a fence line is text, and which of the two it is here\n // cannot be told from the lines alone.\n if (opensFence && (odd || scan.markup)) return stick(scan)\n\n if (scan.list > 0) {\n if (indent >= scan.list) {\n // In the item, if the list is still open; a top-level fence if it is not.\n if (opensFence && scan.unsure) return stick(scan)\n const inner = INNER_FENCE.exec(line)\n if (scan.inner) {\n if (\n inner &&\n inner[1]![0] === scan.inner[0] &&\n inner[1]!.length >= scan.inner.length &&\n inner[2]!.trim() === ''\n )\n scan.inner = null\n } else if (inner && !(inner[1]![0] === '`' && inner[2]!.includes('`'))) scan.inner = inner[1]!\n return false\n }\n // A fence in the item that a line left of the item ends: where its last blank lines go\n // depends on what follows, so nothing is cut from here on.\n if (scan.inner) return stick(scan)\n const column = RULE.test(line) ? 0 : itemColumn(line)\n if (column === -1) return stick(scan)\n if (column > 0) {\n // The next item of the list (or of a new one right after it).\n scan.list = column\n if (fresh) scan.unsure = false\n return false\n }\n if (\n fresh ||\n opensFence ||\n HEADING.test(line) ||\n RULE.test(line) ||\n QUOTE.test(line) ||\n LONG_HTML.some(([opens]) => opens.test(line))\n ) {\n // Left of the item after a blank line, or a block that cannot be a lazy continuation: the\n // list is over, and the line is read as a top-level one below.\n scan.list = 0\n scan.unsure = false\n } else {\n // A lazy continuation of the item's paragraph, or, if the item did not end in a paragraph,\n // the first line after the list.\n scan.unsure = true\n if (HTML_START.test(line)) scan.markup = true\n return false\n }\n }\n\n // Indented four or more after a blank line: indented code, which goes on over blank lines.\n // Right after a fence, any line starts a block.\n const indented = scan.indented\n scan.indented = indent >= 4\n const cut = closed || (fresh && indent < 4 && !odd && !indented)\n for (const [opens, closes] of LONG_HTML) {\n if (!opens.test(line)) continue\n if (!closes.test(line.replace(opens, ''))) scan.html = closes\n return cut\n }\n if (opensFence) {\n scan.fence = fence![1]!\n // A fence starts a block wherever it is written, and is one whatever follows it.\n return !odd\n }\n const column = indent < 4 && !RULE.test(line) ? itemColumn(line) : 0\n if (column === -1) return stick(scan)\n // After a line indented four, which was code or the rest of a paragraph, only the scan of the\n // lines before it could say which a list item that cannot interrupt is.\n if (column > 0 && indented && !INTERRUPTING_ITEM.test(line)) return stick(scan)\n if (column > 0) {\n scan.list = column\n scan.unsure = scan.markup || (!fresh && !INTERRUPTING_ITEM.test(line))\n return cut\n }\n if (HTML_START.test(line)) scan.markup = true\n return cut\n}\n\nfunction stick(scan: Scan): false {\n scan.stuck = true\n return false\n}\n\nfunction definition(scan: Scan): false {\n scan.whole = true\n return false\n}\n\n/** The line of `text` that starts at `at`, without its newline. */\nfunction lineAt(text: string, at: number): string {\n const end = text.indexOf('\\n', at)\n return text.slice(at, end === -1 ? text.length : end)\n}\n\n/** Whether `raw` has a line that opens an HTML tag still open at its end. */\nfunction htmlOpen(raw: string): boolean {\n // A comment left open takes whatever follows it.\n if (raw.includes('<!--') && raw.split('<!--').length > raw.split('-->').length) return true\n if (!HTML_LINE.test(raw)) return false\n const tried = new Set<string>()\n for (const opener of raw.matchAll(HTML_LINES)) {\n const tag = opener[1]!.toLowerCase()\n if (tried.has(tag) || VOID_TAGS.has(tag)) continue\n tried.add(tag)\n const rest = raw.slice(opener.index)\n const openers = rest.match(new RegExp(`<${tag}(?=[\\\\s>/])[^>]*>`, 'gi')) ?? []\n const opened = openers.filter((found) => !found.endsWith('/>')).length\n const closed = rest.match(new RegExp(`</${tag}\\\\s*>`, 'gi'))?.length ?? 0\n if (opened > closed) return true\n }\n return false\n}\n\n/**\n * The pieces the scan cut (`starts`, offsets into `text`) joined into the blocks that must render\n * together: the offsets where a block starts. Read by the same grammar as the remark pass.\n */\nfunction group(text: string, starts: readonly number[]): number[] {\n const count = starts.length\n const endOf = (piece: number) => (piece + 1 < count ? starts[piece + 1]! : text.length)\n const first = (piece: number) => (piece < count ? lineAt(text, starts[piece]!) : '')\n const kindOf = (line: string): ContainerKind | null => {\n const quoted = QUOTE.exec(line)\n return quoted ? (parseContainerMarker(quoted[1]!.trimStart())?.kind ?? null) : null\n }\n const isItem = (line: string) => !RULE.test(line) && itemColumn(line) !== 0\n /** The piece after the tabs whose first heading is in `holder`, on `line` or underlined. */\n const tabsEnd = (holder: number, line: string, fallback: number): number => {\n if (holder >= count) return fallback\n const opening = HEADING.exec(line)\n // A heading underlined with `===` has a depth this scan does not read: it ends at a `---`.\n if (!opening && !UNDERLINE.test(text.slice(starts[holder]!, endOf(holder)))) return fallback\n const depth = opening ? opening[1]!.length : 1\n let end = holder + 1\n for (; end < count; end += 1) {\n const next = first(end)\n if (RULE.test(next)) break\n const heading = HEADING.exec(next)\n if (heading && heading[1]!.length < depth) break\n }\n return end\n }\n /**\n * The form a piece holds a marker line for, and the piece after everything it governs. A list\n * form takes the one list right after it; tabs take the sections from the heading right after\n * them up to a shallower heading, a `---` or the end. The marker is a quote of its own, on any\n * line of the piece: on the last, what it governs is the next piece. A marker whose quote goes\n * on (`> [!PROS]\\n> - a`) holds its own content. Of several markers, the one that reaches\n * furthest decides.\n */\n const form = (piece: number): { kind: ContainerKind; end: number } | null => {\n if (piece >= count) return null\n const body = text.slice(starts[piece]!, endOf(piece)).trimEnd()\n if (!body.includes('[!')) return null\n const lines = body.split('\\n')\n let found: { kind: ContainerKind; end: number } | null = null\n for (let index = 0; index < lines.length; index += 1) {\n const kind = kindOf(lines[index]!)\n // The marker opens its quote: the line above it is not of the same quote.\n if (!kind || (index > 0 && QUOTE.test(lines[index - 1]!))) continue\n const next = lines[index + 1]\n let end = piece + 1\n if (next === undefined) {\n if (kind === 'tabs') end = tabsEnd(piece + 1, first(piece + 1), piece + 1)\n else if (isItem(first(piece + 1))) end = piece + 2\n } else if (kind === 'tabs' && !QUOTE.test(next)) end = tabsEnd(piece, next, piece + 1)\n const reach: number = found ? found.end : 0\n found = { kind, end: Math.max(end, reach) }\n }\n return found\n }\n\n const blocks: number[] = []\n let piece = 0\n while (piece < count) {\n const from = piece\n const found = form(piece)\n piece = found ? found.end : piece + 1\n\n // A pros right before a cons (or the reverse) is set as one pair.\n if (found && (found.kind === 'pros' || found.kind === 'cons')) {\n const other = form(piece)\n if (other && other.kind === (found.kind === 'pros' ? 'cons' : 'pros')) piece = other.end\n }\n\n // An image alone in its paragraph takes the emphasis line under it as its caption. Merging is\n // always safe (it is what one parse does anyway), so the test here can be loose.\n if (\n piece === from + 1 &&\n piece < count &&\n IMAGE_ONLY.test(text.slice(starts[from]!, endOf(from)).trim()) &&\n /^[*_]/.test(first(piece).trimStart())\n )\n piece += 1\n\n // An HTML block whose tag closes further down.\n while (piece < count && htmlOpen(text.slice(starts[from]!, starts[piece]!))) piece += 1\n\n blocks.push(starts[from]!)\n }\n return blocks\n}\n\n/**\n * Line endings as the scan reads them: `\\r\\n` and `\\r` become `\\n`. Offsets of the blocks are into\n * the normalized text, which is what the renderer draws from.\n */\nexport const normalizeLineEndings = (markdown: string): string =>\n markdown.includes('\\r') ? markdown.replace(/\\r\\n?/g, '\\n') : markdown\n\n/** Blocks that are one fenced code block and nothing else: no prose, HTML, math or heading. */\nconst fenced = new WeakSet<MarkdownBlock>()\n\n/** Whether the splitter cut this block as one fenced code block. */\nexport const isFenceBlock = (block: MarkdownBlock): boolean => fenced.has(block)\n\n/** The scan after a complete line: the next text can go on from there. */\ninterface Mark {\n /** The offset after that line. */\n at: number\n scan: Scan\n /** How many pieces had been cut by then. */\n count: number\n}\n\n/** Where a scan can be resumed. */\ninterface Progress {\n /** Where the scan started: the end of the blocks kept as final. */\n from: number\n /** The pieces cut up to the last mark, `from` first. */\n starts: number[]\n /**\n * The scan after each of the last complete lines, oldest first. Several, because a healed\n * stream rewrites its end: the fence it closes or the row it drops is a line that the next\n * update does not have, and the scan then goes on from the line before.\n */\n marks: Mark[]\n}\n\nconst MARKS = 3\n\n/**\n * The pieces of `text` from `from` on: the offsets where the scan cut, or null when a definition\n * keeps the document whole. `progress` and `mark` are where an earlier scan of the same start\n * can go on from.\n */\nfunction pieces(\n text: string,\n from: number,\n progress: Progress | null,\n mark: Mark | null,\n): { starts: number[]; progress: Progress; mixed: boolean } | null {\n const scan = mark ? { ...mark.scan } : start()\n const starts = progress && mark ? progress.starts.slice(0, mark.count) : [from]\n const marks = progress && mark ? progress.marks.filter((kept) => kept.at <= mark.at) : []\n let at = mark ? mark.at : from\n // Marks are left after the last complete lines only.\n let marked = text.length\n for (let back = 0; back < MARKS && marked > at; back += 1)\n marked = text.lastIndexOf('\\n', marked - 1)\n for (let end = text.indexOf('\\n', at); end !== -1; end = text.indexOf('\\n', at)) {\n if (step(scan, text.slice(at, end)) && at > from) starts.push(at)\n at = end + 1\n if (end >= marked) marks.push({ at, scan: { ...scan }, count: starts.length })\n }\n const next: Progress = { from, starts: [...starts], marks: marks.slice(-MARKS) }\n // The last line is still being written: read, but not remembered.\n if (at < text.length && step(scan, text.slice(at)) && at > from) starts.push(at)\n return scan.whole ? null : { starts, progress: next, mixed: scan.mixed }\n}\n\n/** The blocks of `text` that the pieces at `cuts` make, the first on line `line`. */\nfunction build(\n text: string,\n cuts: readonly number[],\n line: number,\n mixed: boolean,\n): MarkdownBlock[] {\n const starts = group(text, cuts)\n const blocks: MarkdownBlock[] = []\n let current = line\n let piece = 0\n for (let index = 0; index < starts.length; index += 1) {\n const offset = starts[index]!\n const end = starts[index + 1] ?? text.length\n const source = text.slice(offset, end)\n const block = { source, offset, line: current }\n while (cuts[piece]! < offset) piece += 1\n // One piece that opens with a fence line is that fence and the blank lines after it: the scan\n // cuts before a top-level fence and after the line that closes it.\n // (When the scan got stuck after a fence, the last piece holds more than that fence.)\n if ((cuts[piece + 1] ?? text.length) === end && !(mixed && end === text.length)) {\n const opening = FENCE.exec(lineAt(text, offset))\n if (opening && !(opening[1]![0] === '`' && opening[2]!.includes('`'))) fenced.add(block)\n }\n blocks.push(block)\n // The last block's lines place nothing: a fence that grows is not counted on every token.\n if (index + 1 === starts.length) break\n for (let at = source.indexOf('\\n'); at !== -1; at = source.indexOf('\\n', at + 1)) current += 1\n }\n return blocks\n}\n\n/** The top-level blocks of a document, cut where one parse of the whole would not join them. */\nexport function splitBlocks(input: string): MarkdownBlock[] {\n const markdown = normalizeLineEndings(input)\n if (!markdown) return whole(markdown)\n const cut = pieces(markdown, 0, null, null)\n return cut ? build(markdown, cut.starts, 1, cut.mixed) : whole(markdown)\n}\n\n/**\n * How many blocks at the end are read again. Text appended to a document can change only the\n * group it is appended to, and that group can join the one before it (a `[!CONS]` arriving\n * under a `[!PROS]`); every group above those two is final.\n */\nconst REOPENED = 2\n\nexport interface BlockSplitter {\n /** The same blocks as `splitBlocks(markdown)`; the ones that did not change are the same objects. */\n split(markdown: string): MarkdownBlock[]\n /** Characters scanned so far: the work the splitter has done. */\n readonly lexed: number\n}\n\n/**\n * `splitBlocks` for a document that grows at its end, as a streaming reply does. It remembers the\n * blocks it cut last time and where its scan stopped: when the new text still starts with all of\n * them but the last two, it keeps those, as the very same objects, and scans only the lines that\n * are new. Any other text (a reply that was regenerated, or edited above its end) is split afresh.\n * Each update then costs the new characters, not the reply: a stream is linear in its length.\n */\nexport function createBlockSplitter(): BlockSplitter {\n let text = ''\n let blocks: MarkdownBlock[] = []\n let progress: Progress | null = null\n let lexed = 0\n\n return {\n get lexed() {\n return lexed\n },\n split(input) {\n const markdown = normalizeLineEndings(input)\n if (!markdown) {\n text = ''\n blocks = []\n progress = null\n return whole(markdown)\n }\n let kept = blocks.slice(0, Math.max(0, blocks.length - REOPENED))\n let last = kept.at(-1)\n let from = last ? last.offset + last.source.length : 0\n if (from > 0 && !continues(markdown, text, from)) {\n kept = []\n last = undefined\n from = 0\n }\n // The scan goes on from the last line it read that is still there, when it started at the\n // same place.\n let mark: Mark | null = null\n if (progress !== null && progress.from === from) {\n for (let index = progress.marks.length - 1; index >= 0 && !mark; index -= 1) {\n const tried = progress.marks[index]!\n if (continues(markdown, text, tried.at, from)) mark = tried\n }\n }\n lexed += markdown.length - (mark ? mark.at : from)\n const cut = pieces(markdown, from, mark ? progress : null, mark)\n text = markdown\n if (!cut) {\n // A definition arrived: what was cut before it is one block with it now.\n blocks = []\n progress = null\n return whole(markdown)\n }\n progress = cut.progress\n let line = 1\n if (last) {\n line = last.line\n for (let at = last.source.indexOf('\\n'); at !== -1; at = last.source.indexOf('\\n', at + 1))\n line += 1\n }\n const rest = build(markdown, cut.starts, line, cut.mixed)\n // A block that reads the same as last time is the same object, with what was worked out\n // from it.\n for (let index = 0; index < rest.length; index += 1) {\n const before = blocks[kept.length + index]\n const after = rest[index]!\n if (before && before.offset === after.offset && before.source === after.source) {\n rest[index] = before\n }\n }\n blocks = [...kept, ...rest]\n return blocks\n },\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ // src/core/bounded-cache.ts
2
+ function createBoundedCache({ maxEntries, maxWeight = Number.POSITIVE_INFINITY, weigh = () => 0 }) {
3
+ const entries = /* @__PURE__ */ new Map();
4
+ let total = 0;
5
+ const remove = (key) => {
6
+ const entry = entries.get(key);
7
+ if (!entry) return;
8
+ total -= entry.weight;
9
+ entries.delete(key);
10
+ };
11
+ return {
12
+ get(key) {
13
+ const entry = entries.get(key);
14
+ if (!entry) return void 0;
15
+ entries.delete(key);
16
+ entries.set(key, entry);
17
+ return entry.value;
18
+ },
19
+ set(key, value) {
20
+ remove(key);
21
+ const weight = weigh(value, key);
22
+ if (weight > maxWeight) return false;
23
+ entries.set(key, {
24
+ value,
25
+ weight
26
+ });
27
+ total += weight;
28
+ while (entries.size > maxEntries || total > maxWeight) {
29
+ const oldest = entries.keys().next();
30
+ if (oldest.done) break;
31
+ remove(oldest.value);
32
+ }
33
+ return entries.has(key);
34
+ },
35
+ get size() {
36
+ return entries.size;
37
+ }
38
+ };
39
+ }
40
+ export { createBoundedCache };
41
+
42
+ //# sourceMappingURL=bounded-cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bounded-cache.js","names":[],"sources":["../../src/core/bounded-cache.ts"],"sourcesContent":["/**\n * A least-recently-used cache bounded by entry count and by total weight.\n *\n * Typeset formulas and drawn diagrams are kept for the life of the page, so a long chat does not\n * pay KaTeX or Mermaid again when a message scrolls back into view. Both are bounded by weight as\n * well as count: one big entity diagram is hundreds of kilobytes of SVG, so a count alone would\n * not bound the heap.\n *\n * `Map` insertion order is the recency queue: a hit is re-inserted at the tail, and eviction takes\n * from the head. A value heavier than the whole budget is not stored, so it cannot flush every\n * small entry on its way in.\n */\nexport interface BoundedCache<K, V> {\n get(key: K): V | undefined\n set(key: K, value: V): boolean\n readonly size: number\n}\n\nexport interface BoundedCacheOptions<K, V> {\n maxEntries: number\n maxWeight?: number\n weigh?: (value: V, key: K) => number\n}\n\nexport function createBoundedCache<K, V>({\n maxEntries,\n maxWeight = Number.POSITIVE_INFINITY,\n weigh = () => 0,\n}: BoundedCacheOptions<K, V>): BoundedCache<K, V> {\n const entries = new Map<K, { value: V; weight: number }>()\n let total = 0\n\n const remove = (key: K) => {\n const entry = entries.get(key)\n if (!entry) return\n total -= entry.weight\n entries.delete(key)\n }\n\n return {\n get(key) {\n const entry = entries.get(key)\n if (!entry) return undefined\n entries.delete(key)\n entries.set(key, entry)\n return entry.value\n },\n set(key, value) {\n remove(key)\n const weight = weigh(value, key)\n if (weight > maxWeight) return false\n entries.set(key, { value, weight })\n total += weight\n // oxlint-disable-next-line no-unmodified-loop-condition -- `remove` lowers `total`.\n while (entries.size > maxEntries || total > maxWeight) {\n const oldest = entries.keys().next()\n if (oldest.done) break\n remove(oldest.value)\n }\n return entries.has(key)\n },\n get size() {\n return entries.size\n },\n }\n}\n"],"mappings":";AAwBA,SAAgB,mBAAyB,EACvC,YACA,YAAY,OAAO,mBACnB,cAAc,KACkC;CAChD,MAAM,0BAAU,IAAI,IAAqC;CACzD,IAAI,QAAQ;CAEZ,MAAM,UAAU,QAAW;EACzB,MAAM,QAAQ,QAAQ,IAAI,GAAG;EAC7B,IAAI,CAAC,OAAO;EACZ,SAAS,MAAM;EACf,QAAQ,OAAO,GAAG;CACpB;CAEA,OAAO;EACL,IAAI,KAAK;GACP,MAAM,QAAQ,QAAQ,IAAI,GAAG;GAC7B,IAAI,CAAC,OAAO,OAAO,KAAA;GACnB,QAAQ,OAAO,GAAG;GAClB,QAAQ,IAAI,KAAK,KAAK;GACtB,OAAO,MAAM;EACf;EACA,IAAI,KAAK,OAAO;GACd,OAAO,GAAG;GACV,MAAM,SAAS,MAAM,OAAO,GAAG;GAC/B,IAAI,SAAS,WAAW,OAAO;GAC/B,QAAQ,IAAI,KAAK;IAAE;IAAO;GAAO,CAAC;GAClC,SAAS;GAET,OAAO,QAAQ,OAAO,cAAc,QAAQ,WAAW;IACrD,MAAM,SAAS,QAAQ,KAAK,CAAC,CAAC,KAAK;IACnC,IAAI,OAAO,MAAM;IACjB,OAAO,OAAO,KAAK;GACrB;GACA,OAAO,QAAQ,IAAI,GAAG;EACxB;EACA,IAAI,OAAO;GACT,OAAO,QAAQ;EACjB;CACF;AACF"}
@@ -0,0 +1,15 @@
1
+ // src/core/continues.ts
2
+ /**
3
+ * Whether `next` holds what `previous` held from `from` up to `upTo`: the question every part that
4
+ * remembers a growing text asks on each token, before it goes on from where it stopped.
5
+ *
6
+ * It is asked with `===` on two slices, not with `startsWith`. The two say the same, but V8
7
+ * (Chrome, Edge, Node) compares the slices as memory and walks `startsWith` a character at a
8
+ * time: on a 35 KB reply that is one microsecond against seventy, on every token.
9
+ *
10
+ * `upTo` is a place inside `previous`; past its end the answer is no, and the caller starts over.
11
+ */
12
+ const continues = (next, previous, upTo, from = 0) => next.length >= upTo && next.slice(from, upTo) === previous.slice(from, upTo);
13
+ export { continues };
14
+
15
+ //# sourceMappingURL=continues.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"continues.js","names":[],"sources":["../../src/core/continues.ts"],"sourcesContent":["/**\n * Whether `next` holds what `previous` held from `from` up to `upTo`: the question every part that\n * remembers a growing text asks on each token, before it goes on from where it stopped.\n *\n * It is asked with `===` on two slices, not with `startsWith`. The two say the same, but V8\n * (Chrome, Edge, Node) compares the slices as memory and walks `startsWith` a character at a\n * time: on a 35 KB reply that is one microsecond against seventy, on every token.\n *\n * `upTo` is a place inside `previous`; past its end the answer is no, and the caller starts over.\n */\nexport const continues = (next: string, previous: string, upTo: number, from = 0): boolean =>\n next.length >= upTo && next.slice(from, upTo) === previous.slice(from, upTo)\n"],"mappings":";;;;;;;;;;;AAUA,MAAa,aAAa,MAAc,UAAkB,MAAc,OAAO,MAC7E,KAAK,UAAU,QAAQ,KAAK,MAAM,MAAM,IAAI,MAAM,SAAS,MAAM,MAAM,IAAI"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Whether the `$` at `index` is a dollar sign by its own look: a price or a shell variable. A `$`
3
+ * that pairs with no other (`$ x`, the shell line `$(seq 1 12) … $i`) is text as well, but that
4
+ * takes the whole paragraph to tell: `literalDollars` asks both.
5
+ */
6
+ export declare function isLiteralDollar(text: string, index: number): boolean;
7
+ /**
8
+ * The source with every dollar in prose that is not a delimiter escaped (`\$`), so a parser
9
+ * that pairs any two dollars (remark-math) reads the same formulas `<Markdown>` does. Code is left
10
+ * byte for byte.
11
+ */
12
+ export declare function escapeLiteralDollars(markdown: string): string;