postext 0.3.5 → 0.3.7

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 (147) hide show
  1. package/dist/__tests__/exports.test.js +1 -0
  2. package/dist/__tests__/exports.test.js.map +1 -1
  3. package/dist/canvas-backend/blockRender.d.ts +3 -0
  4. package/dist/canvas-backend/blockRender.d.ts.map +1 -0
  5. package/dist/canvas-backend/blockRender.js +176 -0
  6. package/dist/canvas-backend/blockRender.js.map +1 -0
  7. package/dist/canvas-backend/decorations.d.ts +6 -0
  8. package/dist/canvas-backend/decorations.d.ts.map +1 -0
  9. package/dist/canvas-backend/decorations.js +106 -0
  10. package/dist/canvas-backend/decorations.js.map +1 -0
  11. package/dist/{canvas-backend.d.ts → canvas-backend/index.d.ts} +2 -2
  12. package/dist/canvas-backend/index.d.ts.map +1 -0
  13. package/dist/canvas-backend/index.js +66 -0
  14. package/dist/canvas-backend/index.js.map +1 -0
  15. package/dist/index.d.ts +2 -1
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +1 -1
  18. package/dist/index.js.map +1 -1
  19. package/dist/knuthPlass/breakpoints.d.ts +6 -0
  20. package/dist/knuthPlass/breakpoints.d.ts.map +1 -0
  21. package/dist/knuthPlass/breakpoints.js +284 -0
  22. package/dist/knuthPlass/breakpoints.js.map +1 -0
  23. package/dist/knuthPlass/constants.d.ts +8 -0
  24. package/dist/knuthPlass/constants.d.ts.map +1 -0
  25. package/dist/knuthPlass/constants.js +8 -0
  26. package/dist/knuthPlass/constants.js.map +1 -0
  27. package/dist/knuthPlass/index.d.ts +12 -0
  28. package/dist/knuthPlass/index.d.ts.map +1 -0
  29. package/dist/knuthPlass/index.js +11 -0
  30. package/dist/knuthPlass/index.js.map +1 -0
  31. package/dist/knuthPlass/pretextAdapter.d.ts +12 -0
  32. package/dist/knuthPlass/pretextAdapter.d.ts.map +1 -0
  33. package/dist/knuthPlass/pretextAdapter.js +178 -0
  34. package/dist/knuthPlass/pretextAdapter.js.map +1 -0
  35. package/dist/knuthPlass/richAdapter.d.ts +26 -0
  36. package/dist/knuthPlass/richAdapter.d.ts.map +1 -0
  37. package/dist/knuthPlass/richAdapter.js +189 -0
  38. package/dist/knuthPlass/richAdapter.js.map +1 -0
  39. package/dist/{knuthPlass.d.ts → knuthPlass/types.d.ts} +1 -31
  40. package/dist/knuthPlass/types.d.ts.map +1 -0
  41. package/dist/knuthPlass/types.js +5 -0
  42. package/dist/knuthPlass/types.js.map +1 -0
  43. package/dist/knuthPlass/utils.d.ts +2 -0
  44. package/dist/knuthPlass/utils.d.ts.map +1 -0
  45. package/dist/knuthPlass/utils.js +4 -0
  46. package/dist/knuthPlass/utils.js.map +1 -0
  47. package/dist/math/rasterCache.d.ts.map +1 -1
  48. package/dist/math/rasterCache.js +29 -6
  49. package/dist/math/rasterCache.js.map +1 -1
  50. package/dist/measure/cache.d.ts +5 -0
  51. package/dist/measure/cache.d.ts.map +1 -0
  52. package/dist/measure/cache.js +38 -0
  53. package/dist/measure/cache.js.map +1 -0
  54. package/dist/measure/canvas.d.ts +8 -0
  55. package/dist/measure/canvas.d.ts.map +1 -0
  56. package/dist/measure/canvas.js +30 -0
  57. package/dist/measure/canvas.js.map +1 -0
  58. package/dist/measure/font.d.ts +19 -0
  59. package/dist/measure/font.d.ts.map +1 -0
  60. package/dist/measure/font.js +34 -0
  61. package/dist/measure/font.js.map +1 -0
  62. package/dist/measure/index.d.ts +7 -0
  63. package/dist/measure/index.d.ts.map +1 -0
  64. package/dist/measure/index.js +6 -0
  65. package/dist/measure/index.js.map +1 -0
  66. package/dist/measure/plain.d.ts +13 -0
  67. package/dist/measure/plain.d.ts.map +1 -0
  68. package/dist/measure/plain.js +167 -0
  69. package/dist/measure/plain.js.map +1 -0
  70. package/dist/measure/rich.d.ts +25 -0
  71. package/dist/measure/rich.d.ts.map +1 -0
  72. package/dist/measure/rich.js +246 -0
  73. package/dist/measure/rich.js.map +1 -0
  74. package/dist/measure/types.d.ts +28 -0
  75. package/dist/measure/types.d.ts.map +1 -0
  76. package/dist/measure/types.js +2 -0
  77. package/dist/measure/types.js.map +1 -0
  78. package/dist/parse/blockParser.d.ts +28 -0
  79. package/dist/parse/blockParser.d.ts.map +1 -0
  80. package/dist/parse/blockParser.js +302 -0
  81. package/dist/parse/blockParser.js.map +1 -0
  82. package/dist/parse/index.d.ts +4 -0
  83. package/dist/parse/index.d.ts.map +1 -0
  84. package/dist/parse/index.js +3 -0
  85. package/dist/parse/index.js.map +1 -0
  86. package/dist/parse/inlineFormatting.d.ts +12 -0
  87. package/dist/parse/inlineFormatting.d.ts.map +1 -0
  88. package/dist/parse/inlineFormatting.js +90 -0
  89. package/dist/parse/inlineFormatting.js.map +1 -0
  90. package/dist/parse/inlineMath.d.ts +29 -0
  91. package/dist/parse/inlineMath.d.ts.map +1 -0
  92. package/dist/parse/inlineMath.js +141 -0
  93. package/dist/parse/inlineMath.js.map +1 -0
  94. package/dist/parse/sourceMapping.d.ts +12 -0
  95. package/dist/parse/sourceMapping.d.ts.map +1 -0
  96. package/dist/parse/sourceMapping.js +130 -0
  97. package/dist/parse/sourceMapping.js.map +1 -0
  98. package/dist/{parse.d.ts → parse/types.d.ts} +2 -25
  99. package/dist/parse/types.d.ts.map +1 -0
  100. package/dist/parse/types.js +2 -0
  101. package/dist/parse/types.js.map +1 -0
  102. package/dist/pipeline/build.d.ts +13 -1
  103. package/dist/pipeline/build.d.ts.map +1 -1
  104. package/dist/pipeline/build.js +43 -277
  105. package/dist/pipeline/build.js.map +1 -1
  106. package/dist/pipeline/buildBlockKind.d.ts +33 -0
  107. package/dist/pipeline/buildBlockKind.d.ts.map +1 -0
  108. package/dist/pipeline/buildBlockKind.js +82 -0
  109. package/dist/pipeline/buildBlockKind.js.map +1 -0
  110. package/dist/pipeline/buildHelpers.d.ts +47 -0
  111. package/dist/pipeline/buildHelpers.d.ts.map +1 -0
  112. package/dist/pipeline/buildHelpers.js +169 -0
  113. package/dist/pipeline/buildHelpers.js.map +1 -0
  114. package/dist/pipeline/buildMeasurement.d.ts +27 -0
  115. package/dist/pipeline/buildMeasurement.d.ts.map +1 -0
  116. package/dist/pipeline/buildMeasurement.js +49 -0
  117. package/dist/pipeline/buildMeasurement.js.map +1 -0
  118. package/dist/pipeline/index.d.ts +2 -1
  119. package/dist/pipeline/index.d.ts.map +1 -1
  120. package/dist/pipeline/index.js +1 -1
  121. package/dist/pipeline/index.js.map +1 -1
  122. package/dist/worker/client.d.ts +21 -0
  123. package/dist/worker/client.d.ts.map +1 -0
  124. package/dist/worker/client.js +98 -0
  125. package/dist/worker/client.js.map +1 -0
  126. package/dist/worker/layout.worker.d.ts +2 -0
  127. package/dist/worker/layout.worker.d.ts.map +1 -0
  128. package/dist/worker/layout.worker.js +121 -0
  129. package/dist/worker/layout.worker.js.map +1 -0
  130. package/dist/worker/protocol.d.ts +43 -0
  131. package/dist/worker/protocol.d.ts.map +1 -0
  132. package/dist/worker/protocol.js +2 -0
  133. package/dist/worker/protocol.js.map +1 -0
  134. package/package.json +9 -1
  135. package/dist/canvas-backend.d.ts.map +0 -1
  136. package/dist/canvas-backend.js +0 -370
  137. package/dist/canvas-backend.js.map +0 -1
  138. package/dist/knuthPlass.d.ts.map +0 -1
  139. package/dist/knuthPlass.js +0 -665
  140. package/dist/knuthPlass.js.map +0 -1
  141. package/dist/measure.d.ts +0 -61
  142. package/dist/measure.d.ts.map +0 -1
  143. package/dist/measure.js +0 -512
  144. package/dist/measure.js.map +0 -1
  145. package/dist/parse.d.ts.map +0 -1
  146. package/dist/parse.js +0 -653
  147. package/dist/parse.js.map +0 -1
package/dist/parse.js DELETED
@@ -1,653 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // Minimal block-level markdown tokenizer
3
- // ---------------------------------------------------------------------------
4
- const HEADING_RE = /^(#{1,6})\s+(.+)$/;
5
- const TASK_ITEM_RE = /^(\s*)([-*+])\s+\[([ xX])\]\s+(.*)$/;
6
- const ORDERED_LIST_ITEM_RE = /^(\s*)(\d+)([.)])\s+(.*)$/;
7
- const LIST_ITEM_RE = /^(\s*)([-*+])\s+(.*)$/;
8
- /** Object Replacement Character — atomic plain-text placeholder for a math
9
- * span. One code unit per formula so `sourceMap` stays 1-to-1. */
10
- export const MATH_PLACEHOLDER = '\uFFFC';
11
- /** One-line `$$ ... $$` display math (the whole line is the formula). */
12
- const BLOCK_MATH_SINGLE_RE = /^\s*\$\$([\s\S]+?)\$\$\s*$/;
13
- /** Standalone `$$` marker (opening or closing a multi-line display block). */
14
- const BLOCK_MATH_FENCE_RE = /^\s*\$\$\s*$/;
15
- /** Extract inline `$...$` math spans from a line's text.
16
- * - Returns a cleaned text where each match is replaced by `MATH_PLACEHOLDER`.
17
- * - Returns the list of math metadata aligned with the order of placeholders.
18
- * - Detects unclosed `$` (odd number of unescaped `$` in the text).
19
- *
20
- * `absOffsets[i]` is the absolute source offset of `text[i]` in the original
21
- * markdown; used to emit accurate `sourceStart`/`sourceEnd` on each math
22
- * span. When the caller cannot provide an exact map (e.g. text has been
23
- * reassembled from multiple source lines) it can pass `null` and math
24
- * offsets will default to the block's source range.
25
- */
26
- function extractInlineMath(text, absOffsets, fallbackStart, fallbackEnd) {
27
- const maths = [];
28
- const issues = [];
29
- let out = '';
30
- let i = 0;
31
- const absAt = (idx) => {
32
- if (absOffsets && idx < absOffsets.length)
33
- return absOffsets[idx];
34
- if (absOffsets && absOffsets.length > 0)
35
- return absOffsets[absOffsets.length - 1] + (idx - (absOffsets.length - 1));
36
- return fallbackStart + idx;
37
- };
38
- while (i < text.length) {
39
- const ch = text[i];
40
- if (ch === '\\' && text[i + 1] === '$') {
41
- // Escaped dollar sign — keep as literal `$` in output.
42
- out += '$';
43
- i += 2;
44
- continue;
45
- }
46
- if (ch !== '$') {
47
- out += ch;
48
- i++;
49
- continue;
50
- }
51
- // Found a '$'. Skip display-math `$$` sequences — those are handled at
52
- // block level, so inside a paragraph we treat `$$` as literal.
53
- if (text[i + 1] === '$') {
54
- out += '$$';
55
- i += 2;
56
- continue;
57
- }
58
- // Scan for the matching closing `$`, honouring `\$` escapes.
59
- let j = i + 1;
60
- let foundClose = -1;
61
- while (j < text.length) {
62
- if (text[j] === '\\' && text[j + 1] === '$') {
63
- j += 2;
64
- continue;
65
- }
66
- if (text[j] === '$') {
67
- foundClose = j;
68
- break;
69
- }
70
- // Bail on newline — inline math must be on a single line.
71
- if (text[j] === '\n')
72
- break;
73
- j++;
74
- }
75
- if (foundClose < 0) {
76
- issues.push({
77
- kind: 'unclosedMath',
78
- delimiter: '$',
79
- sourceStart: absAt(i),
80
- sourceEnd: fallbackEnd,
81
- tex: text.slice(i + 1),
82
- });
83
- out += text.slice(i);
84
- break;
85
- }
86
- const tex = text.slice(i + 1, foundClose).replace(/\\\$/g, '$');
87
- maths.push({ tex, sourceStart: absAt(i), sourceEnd: absAt(foundClose) + 1 });
88
- out += MATH_PLACEHOLDER;
89
- i = foundClose + 1;
90
- }
91
- return { cleaned: out, maths, issues };
92
- }
93
- /** Walk an InlineSpan list and attach MathMeta to each `\uFFFC` occurrence
94
- * in order. Splits plain-text spans around the placeholder so the math
95
- * span is its own entry (carrying the ambient bold/italic). */
96
- function injectMathSpans(spans, maths) {
97
- if (maths.length === 0)
98
- return spans;
99
- const out = [];
100
- let idx = 0;
101
- for (const span of spans) {
102
- if (span.text.indexOf(MATH_PLACEHOLDER) < 0) {
103
- out.push(span);
104
- continue;
105
- }
106
- let buf = '';
107
- for (const ch of span.text) {
108
- if (ch === MATH_PLACEHOLDER) {
109
- if (buf.length > 0) {
110
- out.push({ text: buf, bold: span.bold, italic: span.italic });
111
- buf = '';
112
- }
113
- const meta = maths[idx++];
114
- if (meta) {
115
- out.push({
116
- text: MATH_PLACEHOLDER,
117
- bold: span.bold,
118
- italic: span.italic,
119
- math: meta,
120
- });
121
- }
122
- }
123
- else {
124
- buf += ch;
125
- }
126
- }
127
- if (buf.length > 0) {
128
- out.push({ text: buf, bold: span.bold, italic: span.italic });
129
- }
130
- }
131
- return out;
132
- }
133
- /** Overwrite `sourceMap[i]` entries corresponding to math placeholders with
134
- * the math span's absolute sourceStart. Keeps click-to-focus accurate for
135
- * formulas even though the placeholder char has no direct source match. */
136
- function fixMathSourceMap(text, spans, sourceMap) {
137
- if (sourceMap.length === 0)
138
- return;
139
- // Walk spans in order, tracking plain-text position. Each math span
140
- // contributes exactly one placeholder char whose sourceMap entry we
141
- // overwrite with its span's sourceStart.
142
- let p = 0;
143
- for (const span of spans) {
144
- if (span.math && p < sourceMap.length) {
145
- sourceMap[p] = span.math.sourceStart;
146
- }
147
- p += span.text.length;
148
- if (p > text.length)
149
- break;
150
- }
151
- }
152
- /**
153
- * Strip inline markdown formatting for plain-text extraction.
154
- * Handles bold, italic, code, links, and images.
155
- */
156
- function stripInlineFormatting(text) {
157
- return text
158
- .replace(/!\[.*?\]\(.*?\)/g, '') // images
159
- .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // links
160
- .replace(/\*\*(.+?)\*\*/g, '$1') // bold
161
- .replace(/__(.+?)__/g, '$1') // bold alt
162
- .replace(/\*(.+?)\*/g, '$1') // italic
163
- .replace(/_(.+?)_/g, '$1') // italic alt
164
- .replace(/`(.+?)`/g, '$1') // inline code
165
- .trim();
166
- }
167
- /**
168
- * Strip non-emphasis inline formatting (images, links, code) but keep bold/italic markers.
169
- */
170
- function stripNonEmphasisFormatting(text) {
171
- return text
172
- .replace(/!\[.*?\]\(.*?\)/g, '') // images
173
- .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // links
174
- .replace(/`(.+?)`/g, '$1'); // inline code
175
- }
176
- /**
177
- * Split a text region into italic/non-italic spans, carrying an ambient bold flag.
178
- */
179
- function splitItalicSpans(text, bold, forcedItalic, out) {
180
- if (forcedItalic) {
181
- if (text.length > 0)
182
- out.push({ text, bold, italic: true });
183
- return;
184
- }
185
- const italicRe = /\*(.+?)\*|_(.+?)_/g;
186
- let last = 0;
187
- let m;
188
- while ((m = italicRe.exec(text)) !== null) {
189
- if (m.index > last) {
190
- const before = text.slice(last, m.index);
191
- if (before.length > 0)
192
- out.push({ text: before, bold, italic: false });
193
- }
194
- const inner = m[1] ?? m[2];
195
- if (inner.length > 0)
196
- out.push({ text: inner, bold, italic: true });
197
- last = m.index + m[0].length;
198
- }
199
- if (last < text.length) {
200
- const rest = text.slice(last);
201
- if (rest.length > 0)
202
- out.push({ text: rest, bold, italic: false });
203
- }
204
- }
205
- /**
206
- * Parse inline formatting to produce spans with bold and italic flags.
207
- * Recognizes ***bold italic***, **bold**, *italic* (and underscore equivalents).
208
- */
209
- function parseInlineFormatting(text) {
210
- const cleaned = stripNonEmphasisFormatting(text);
211
- const spans = [];
212
- // Triple markers (bold+italic) first, then double (bold) — longest first.
213
- const boldRe = /\*\*\*(.+?)\*\*\*|___(.+?)___|\*\*(.+?)\*\*|__(.+?)__/g;
214
- let lastIndex = 0;
215
- let match;
216
- while ((match = boldRe.exec(cleaned)) !== null) {
217
- if (match.index > lastIndex) {
218
- splitItalicSpans(cleaned.slice(lastIndex, match.index), false, false, spans);
219
- }
220
- const triple = match[1] ?? match[2];
221
- if (triple !== undefined) {
222
- splitItalicSpans(triple, true, true, spans);
223
- }
224
- else {
225
- const inner = match[3] ?? match[4];
226
- splitItalicSpans(inner, true, false, spans);
227
- }
228
- lastIndex = match.index + match[0].length;
229
- }
230
- if (lastIndex < cleaned.length) {
231
- splitItalicSpans(cleaned.slice(lastIndex), false, false, spans);
232
- }
233
- if (spans.length === 0) {
234
- const trimmed = cleaned.trim();
235
- if (trimmed.length > 0) {
236
- splitItalicSpans(trimmed, false, false, spans);
237
- }
238
- }
239
- return spans;
240
- }
241
- /**
242
- * Build a per-character map from plain text to absolute source offsets.
243
- * Greedy matches each plain char against the raw source (delimited by
244
- * [blockSrcStart, blockSrcEnd)), skipping markdown markers and treating
245
- * newlines/tabs as spaces for paragraph line joins.
246
- */
247
- function computeSourceMap(markdown, blockSrcStart, blockSrcEnd, plainText) {
248
- const map = new Array(plainText.length);
249
- let r = blockSrcStart;
250
- for (let p = 0; p < plainText.length; p++) {
251
- const ch = plainText[p];
252
- // Math placeholder: the plain char represents `$...$` in the markdown.
253
- // Advance to the opening `$`, map to it, then skip past the closing `$`
254
- // so subsequent plain chars can keep aligning with the source.
255
- if (ch === MATH_PLACEHOLDER) {
256
- while (r < blockSrcEnd && markdown[r] !== '$')
257
- r++;
258
- if (r >= blockSrcEnd) {
259
- map[p] = blockSrcEnd;
260
- continue;
261
- }
262
- map[p] = r;
263
- let j = r + 1;
264
- while (j < blockSrcEnd) {
265
- if (markdown[j] === '\\' && markdown[j + 1] === '$') {
266
- j += 2;
267
- continue;
268
- }
269
- if (markdown[j] === '$') {
270
- j++;
271
- break;
272
- }
273
- if (markdown[j] === '\n')
274
- break;
275
- j++;
276
- }
277
- r = j;
278
- continue;
279
- }
280
- const isSpace = ch === ' ';
281
- while (r < blockSrcEnd) {
282
- const rc = markdown[r];
283
- if (rc === ch)
284
- break;
285
- if (isSpace && (rc === '\n' || rc === '\t'))
286
- break;
287
- r++;
288
- }
289
- if (r >= blockSrcEnd) {
290
- map[p] = blockSrcEnd;
291
- }
292
- else {
293
- map[p] = r;
294
- r++;
295
- }
296
- }
297
- return map;
298
- }
299
- const COLLAPSIBLE_WS_RE = /[ \t\n\r\f]/;
300
- /**
301
- * Collapse runs of `[ \t\n\r\f]` to a single space and strip leading/trailing
302
- * whitespace across spans. Mirrors pretext's `normalizeWhitespaceNormal` so
303
- * that `spans` and the block's plain text stay aligned with what the layout
304
- * engine actually renders — otherwise cursor/selection mapping drifts by one
305
- * character per collapsed whitespace character.
306
- */
307
- function normalizeWhitespaceInSpans(spans) {
308
- const out = [];
309
- let inSpace = true; // start true to strip leading whitespace
310
- for (const span of spans) {
311
- let result = '';
312
- for (const ch of span.text) {
313
- if (COLLAPSIBLE_WS_RE.test(ch)) {
314
- if (!inSpace) {
315
- result += ' ';
316
- inSpace = true;
317
- }
318
- }
319
- else {
320
- result += ch;
321
- inSpace = false;
322
- }
323
- }
324
- out.push({ ...span, text: result });
325
- }
326
- // Strip trailing space from the last span that contributed content
327
- for (let i = out.length - 1; i >= 0; i--) {
328
- const text = out[i].text;
329
- if (text.length === 0)
330
- continue;
331
- if (text.endsWith(' ')) {
332
- out[i] = { ...out[i], text: text.slice(0, -1) };
333
- }
334
- break;
335
- }
336
- return out.filter((s) => s.text.length > 0);
337
- }
338
- /**
339
- * Build normalized text, spans, and sourceMap for a block. The plain text is
340
- * whitespace-normalized to match pretext's internal normalization so that the
341
- * per-character `sourceMap` aligns with rendered line segments.
342
- */
343
- function buildBlockMapping(markdown, blockSrcStart, blockSrcEnd, rawSpans) {
344
- const rawText = rawSpans.map((s) => s.text).join('');
345
- const rawSourceMap = computeSourceMap(markdown, blockSrcStart, blockSrcEnd, rawText);
346
- const spans = normalizeWhitespaceInSpans(rawSpans);
347
- const text = spans.map((s) => s.text).join('');
348
- // Walk the raw text building the same normalization, and project each kept
349
- // normalized character onto the rawSourceMap to obtain the source offset.
350
- const sourceMap = [];
351
- let inSpace = true;
352
- for (let i = 0; i < rawText.length; i++) {
353
- const ch = rawText[i];
354
- if (COLLAPSIBLE_WS_RE.test(ch)) {
355
- if (!inSpace) {
356
- sourceMap.push(rawSourceMap[i] ?? blockSrcEnd);
357
- inSpace = true;
358
- }
359
- }
360
- else {
361
- sourceMap.push(rawSourceMap[i] ?? blockSrcEnd);
362
- inSpace = false;
363
- }
364
- }
365
- if (sourceMap.length > text.length)
366
- sourceMap.length = text.length;
367
- return { text, spans, sourceMap };
368
- }
369
- // Single-slot memo used by parseMarkdownMemo. The returned array and its
370
- // ContentBlocks are treated as read-only by the rest of the pipeline.
371
- let _parseMemoInput = null;
372
- let _parseMemoResult = null;
373
- /**
374
- * Memoized wrapper around parseMarkdown: returns the cached result when the
375
- * input string is byte-for-byte identical to the previous call. This avoids
376
- * reparsing the whole document on each keystroke when upstream recomputes
377
- * only because a sibling state changed.
378
- */
379
- export function parseMarkdownMemo(markdown) {
380
- if (_parseMemoResult !== null && _parseMemoInput === markdown) {
381
- return _parseMemoResult.blocks;
382
- }
383
- const result = parseMarkdownWithIssues(markdown);
384
- _parseMemoInput = markdown;
385
- _parseMemoResult = result;
386
- return result.blocks;
387
- }
388
- export function parseMarkdownWithIssuesMemo(markdown) {
389
- if (_parseMemoResult !== null && _parseMemoInput === markdown) {
390
- return _parseMemoResult;
391
- }
392
- const result = parseMarkdownWithIssues(markdown);
393
- _parseMemoInput = markdown;
394
- _parseMemoResult = result;
395
- return result;
396
- }
397
- export function parseMarkdown(markdown) {
398
- return parseMarkdownWithIssues(markdown).blocks;
399
- }
400
- /**
401
- * Merge consecutive blockquote lines into a single block, and consecutive
402
- * non-blank, non-special lines into paragraphs.
403
- */
404
- export function parseMarkdownWithIssues(markdown) {
405
- const blocks = [];
406
- const issues = [];
407
- const rawLines = markdown.split('\n');
408
- // Precompute starting offset of each raw line in the original markdown.
409
- // lineOffsets[i] = offset of first char of rawLines[i]. Line terminator '\n'
410
- // contributes exactly 1 character between consecutive lines.
411
- const lineOffsets = new Array(rawLines.length);
412
- {
413
- let cur = 0;
414
- for (let k = 0; k < rawLines.length; k++) {
415
- lineOffsets[k] = cur;
416
- cur += rawLines[k].length + 1; // +1 for the '\n' that was split away
417
- }
418
- }
419
- const lineEndOffset = (k) => lineOffsets[k] + rawLines[k].length;
420
- let i = 0;
421
- while (i < rawLines.length) {
422
- const line = rawLines[i];
423
- const trimmed = line.trim();
424
- // Skip blank lines
425
- if (trimmed === '') {
426
- i++;
427
- continue;
428
- }
429
- // Display math — single-line `$$ ... $$`
430
- const singleMath = line.match(BLOCK_MATH_SINGLE_RE);
431
- if (singleMath) {
432
- const srcStart = lineOffsets[i];
433
- const srcEnd = lineEndOffset(i);
434
- blocks.push({
435
- type: 'mathDisplay',
436
- text: '',
437
- spans: [],
438
- tex: singleMath[1],
439
- sourceStart: srcStart,
440
- sourceEnd: srcEnd,
441
- sourceMap: [],
442
- });
443
- i++;
444
- continue;
445
- }
446
- // Display math — multi-line `$$` ... `$$` (opening fence on its own line).
447
- if (BLOCK_MATH_FENCE_RE.test(line)) {
448
- const startIdx = i;
449
- i++;
450
- const texLines = [];
451
- let closed = false;
452
- while (i < rawLines.length) {
453
- if (BLOCK_MATH_FENCE_RE.test(rawLines[i])) {
454
- closed = true;
455
- break;
456
- }
457
- texLines.push(rawLines[i]);
458
- i++;
459
- }
460
- const srcStart = lineOffsets[startIdx];
461
- const srcEnd = closed ? lineEndOffset(i) : lineEndOffset(i - 1);
462
- if (!closed) {
463
- issues.push({
464
- kind: 'unclosedMathBlock',
465
- delimiter: '$$',
466
- sourceStart: srcStart,
467
- sourceEnd: srcEnd,
468
- tex: texLines.join('\n'),
469
- });
470
- }
471
- blocks.push({
472
- type: 'mathDisplay',
473
- text: '',
474
- spans: [],
475
- tex: texLines.join('\n'),
476
- sourceStart: srcStart,
477
- sourceEnd: srcEnd,
478
- sourceMap: [],
479
- });
480
- if (closed)
481
- i++; // consume the closing fence
482
- continue;
483
- }
484
- // Heading
485
- const headingMatch = trimmed.match(HEADING_RE);
486
- if (headingMatch) {
487
- const headingRawContent = headingMatch[2];
488
- const srcStart = lineOffsets[i];
489
- const srcEnd = lineEndOffset(i);
490
- // Absolute offset of the first content char (after the `# `).
491
- const prefixLen = line.indexOf(headingRawContent);
492
- const contentAbsStart = srcStart + (prefixLen >= 0 ? prefixLen : 0);
493
- const { cleaned, maths, issues: mathIssues } = extractInlineMath(headingRawContent, null, contentAbsStart, srcEnd);
494
- issues.push(...mathIssues);
495
- const rawText = stripInlineFormatting(cleaned);
496
- const rawSpans = injectMathSpans([{ text: rawText, bold: false, italic: false }], maths);
497
- const mapping = buildBlockMapping(markdown, srcStart, srcEnd, rawSpans);
498
- fixMathSourceMap(mapping.text, mapping.spans, mapping.sourceMap);
499
- blocks.push({
500
- type: 'heading',
501
- text: mapping.text,
502
- spans: mapping.spans.length > 0 ? mapping.spans : [{ text: '', bold: false, italic: false }],
503
- level: headingMatch[1].length,
504
- sourceStart: srcStart,
505
- sourceEnd: srcEnd,
506
- sourceMap: mapping.sourceMap,
507
- });
508
- i++;
509
- continue;
510
- }
511
- // List items — unordered (`-`, `*`, `+`), ordered (`1.`, `1)`) and
512
- // GFM task (`- [ ]`, `- [x]`). Consume consecutive list lines of any kind
513
- // (nesting and mixed kinds are allowed); the pipeline segments ordered
514
- // runs by kind and depth for numbering.
515
- const isListStart = TASK_ITEM_RE.test(line) || ORDERED_LIST_ITEM_RE.test(line) || LIST_ITEM_RE.test(line);
516
- if (isListStart) {
517
- while (i < rawLines.length) {
518
- const curRaw = rawLines[i];
519
- const taskMatch = curRaw.match(TASK_ITEM_RE);
520
- const orderedMatch = !taskMatch ? curRaw.match(ORDERED_LIST_ITEM_RE) : null;
521
- const unorderedMatch = !taskMatch && !orderedMatch ? curRaw.match(LIST_ITEM_RE) : null;
522
- const anyMatch = taskMatch ?? orderedMatch ?? unorderedMatch;
523
- if (anyMatch) {
524
- const leading = anyMatch[1].length;
525
- const depth = Math.max(1, Math.min(5, Math.floor(leading / 2) + 1));
526
- const srcStart = lineOffsets[i];
527
- const srcEnd = lineEndOffset(i);
528
- let listKind;
529
- let itemText;
530
- let markerLength;
531
- let startNumber;
532
- let checked;
533
- if (taskMatch) {
534
- listKind = 'task';
535
- checked = taskMatch[3] !== ' ';
536
- itemText = taskMatch[4];
537
- // leading spaces + bullet (1) + space (1) + `[x]` (3) + space (1)
538
- markerLength = taskMatch[2].length + 1 + 3 + 1;
539
- }
540
- else if (orderedMatch) {
541
- listKind = 'ordered';
542
- startNumber = parseInt(orderedMatch[2], 10);
543
- itemText = orderedMatch[4];
544
- // number digits + separator (1) + space (1)
545
- markerLength = orderedMatch[2].length + 1 + 1;
546
- }
547
- else {
548
- listKind = 'unordered';
549
- itemText = unorderedMatch[3];
550
- markerLength = unorderedMatch[2].length + 1;
551
- }
552
- const contentOffset = leading + markerLength;
553
- const itemSrcStart = srcStart + contentOffset;
554
- const mathExtract = extractInlineMath(itemText, null, itemSrcStart, srcEnd);
555
- issues.push(...mathExtract.issues);
556
- const rawSpans = injectMathSpans(parseInlineFormatting(mathExtract.cleaned), mathExtract.maths);
557
- const mapping = buildBlockMapping(markdown, itemSrcStart, srcEnd, rawSpans);
558
- fixMathSourceMap(mapping.text, mapping.spans, mapping.sourceMap);
559
- const block = {
560
- type: 'listItem',
561
- text: mapping.text,
562
- spans: mapping.spans.length > 0 ? mapping.spans : [{ text: '', bold: false, italic: false }],
563
- depth,
564
- listKind,
565
- sourceStart: srcStart,
566
- sourceEnd: srcEnd,
567
- sourceMap: mapping.sourceMap,
568
- };
569
- if (startNumber !== undefined)
570
- block.startNumber = startNumber;
571
- if (checked !== undefined)
572
- block.checked = checked;
573
- blocks.push(block);
574
- i++;
575
- continue;
576
- }
577
- // Blank line: allow a single blank followed by another list item.
578
- if (curRaw.trim() === '' &&
579
- i + 1 < rawLines.length &&
580
- (TASK_ITEM_RE.test(rawLines[i + 1]) ||
581
- ORDERED_LIST_ITEM_RE.test(rawLines[i + 1]) ||
582
- LIST_ITEM_RE.test(rawLines[i + 1]))) {
583
- i++;
584
- continue;
585
- }
586
- break;
587
- }
588
- continue;
589
- }
590
- // Blockquote — collect consecutive > lines
591
- if (trimmed.startsWith('>')) {
592
- const quoteLines = [];
593
- const startIdx = i;
594
- let lastIdx = i;
595
- while (i < rawLines.length) {
596
- const ql = rawLines[i].trim();
597
- if (!ql.startsWith('>'))
598
- break;
599
- quoteLines.push(ql.replace(/^>\s?/, ''));
600
- lastIdx = i;
601
- i++;
602
- }
603
- const srcStart = lineOffsets[startIdx];
604
- const srcEnd = lineEndOffset(lastIdx);
605
- const mathExtract = extractInlineMath(quoteLines.join(' '), null, srcStart, srcEnd);
606
- issues.push(...mathExtract.issues);
607
- const rawSpans = injectMathSpans(parseInlineFormatting(mathExtract.cleaned), mathExtract.maths);
608
- const mapping = buildBlockMapping(markdown, srcStart, srcEnd, rawSpans);
609
- fixMathSourceMap(mapping.text, mapping.spans, mapping.sourceMap);
610
- blocks.push({
611
- type: 'blockquote',
612
- text: mapping.text,
613
- spans: mapping.spans,
614
- sourceStart: srcStart,
615
- sourceEnd: srcEnd,
616
- sourceMap: mapping.sourceMap,
617
- });
618
- continue;
619
- }
620
- // Paragraph — collect consecutive non-blank, non-special lines
621
- const paraLines = [];
622
- const startIdx = i;
623
- let lastIdx = i;
624
- while (i < rawLines.length) {
625
- const rawLine = rawLines[i];
626
- const pl = rawLine.trim();
627
- if (pl === '' || pl.match(HEADING_RE) || pl.startsWith('>') || rawLine.match(LIST_ITEM_RE))
628
- break;
629
- paraLines.push(pl);
630
- lastIdx = i;
631
- i++;
632
- }
633
- if (paraLines.length > 0) {
634
- const srcStart = lineOffsets[startIdx];
635
- const srcEnd = lineEndOffset(lastIdx);
636
- const mathExtract = extractInlineMath(paraLines.join(' '), null, srcStart, srcEnd);
637
- issues.push(...mathExtract.issues);
638
- const rawSpans = injectMathSpans(parseInlineFormatting(mathExtract.cleaned), mathExtract.maths);
639
- const mapping = buildBlockMapping(markdown, srcStart, srcEnd, rawSpans);
640
- fixMathSourceMap(mapping.text, mapping.spans, mapping.sourceMap);
641
- blocks.push({
642
- type: 'paragraph',
643
- text: mapping.text,
644
- spans: mapping.spans,
645
- sourceStart: srcStart,
646
- sourceEnd: srcEnd,
647
- sourceMap: mapping.sourceMap,
648
- });
649
- }
650
- }
651
- return { blocks, issues };
652
- }
653
- //# sourceMappingURL=parse.js.map