@rezi-ui/core 0.1.0-beta.1 → 0.1.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/abi.d.ts +1 -1
- package/dist/abi.js +1 -1
- package/dist/app/createApp.js +32 -2
- package/dist/app/createApp.js.map +1 -1
- package/dist/app/inspectorOverlayHelper.d.ts.map +1 -1
- package/dist/app/inspectorOverlayHelper.js +3 -0
- package/dist/app/inspectorOverlayHelper.js.map +1 -1
- package/dist/app/types.d.ts +6 -0
- package/dist/app/types.d.ts.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/renderer/renderToDrawlist/boxBorder.d.ts.map +1 -1
- package/dist/renderer/renderToDrawlist/boxBorder.js +5 -2
- package/dist/renderer/renderToDrawlist/boxBorder.js.map +1 -1
- package/dist/widgets/markdown/ast.d.ts +71 -0
- package/dist/widgets/markdown/ast.d.ts.map +1 -0
- package/dist/widgets/markdown/ast.js +10 -0
- package/dist/widgets/markdown/ast.js.map +1 -0
- package/dist/widgets/markdown/index.d.ts +25 -0
- package/dist/widgets/markdown/index.d.ts.map +1 -0
- package/dist/widgets/markdown/index.js +32 -0
- package/dist/widgets/markdown/index.js.map +1 -0
- package/dist/widgets/markdown/parse.d.ts +39 -0
- package/dist/widgets/markdown/parse.d.ts.map +1 -0
- package/dist/widgets/markdown/parse.js +791 -0
- package/dist/widgets/markdown/parse.js.map +1 -0
- package/dist/widgets/markdown/render.d.ts +33 -0
- package/dist/widgets/markdown/render.d.ts.map +1 -0
- package/dist/widgets/markdown/render.js +308 -0
- package/dist/widgets/markdown/render.js.map +1 -0
- package/dist/widgets/markdown/stream.d.ts +46 -0
- package/dist/widgets/markdown/stream.d.ts.map +1 -0
- package/dist/widgets/markdown/stream.js +124 -0
- package/dist/widgets/markdown/stream.js.map +1 -0
- package/dist/widgets/types/base.d.ts +11 -0
- package/dist/widgets/types/base.d.ts.map +1 -1
- package/dist/widgets/types.d.ts +1 -1
- package/dist/widgets/types.d.ts.map +1 -1
- package/dist/widgets/ui.d.ts +2 -0
- package/dist/widgets/ui.d.ts.map +1 -1
- package/dist/widgets/ui.js +2 -0
- package/dist/widgets/ui.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,791 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* packages/core/src/widgets/markdown/parse.ts — GFM-subset markdown parser.
|
|
3
|
+
*
|
|
4
|
+
* Why: ui.markdown() needs a dependency-free, deterministic parser that is
|
|
5
|
+
* safe on untrusted input (PR bodies, agent output). The grammar is a
|
|
6
|
+
* pragmatic GitHub-Flavored-Markdown subset:
|
|
7
|
+
*
|
|
8
|
+
* blocks: ATX headings, paragraphs, fenced code, indented code,
|
|
9
|
+
* blockquotes, ordered/unordered lists (nested), task items,
|
|
10
|
+
* thematic breaks, pipe tables
|
|
11
|
+
* inlines: **strong**, *em*, ~~del~~, `code`, [text](url), <autolinks>,
|
|
12
|
+
* bare http(s) URLs, hard breaks, backslash escapes, and basic
|
|
13
|
+
* HTML entities
|
|
14
|
+
*
|
|
15
|
+
* Intentional divergences from full GFM, kept simple on purpose:
|
|
16
|
+
* - no setext headings, reference links, images, footnotes, or raw HTML
|
|
17
|
+
* (HTML tags render as literal text)
|
|
18
|
+
* - no lazy paragraph continuation inside blockquotes or list items
|
|
19
|
+
* - the CommonMark emphasis algorithm is approximated with flanking checks
|
|
20
|
+
* - table delimiter rows must contain at least one `|`
|
|
21
|
+
*
|
|
22
|
+
* The parser never throws. Malformed constructs degrade to literal text,
|
|
23
|
+
* nesting depth is bounded, and inline scanning runs on a work budget so
|
|
24
|
+
* adversarial input (for example long runs of `*` or `[`) cannot trigger
|
|
25
|
+
* quadratic blowups.
|
|
26
|
+
*/
|
|
27
|
+
const MAX_INLINE_DEPTH = 16;
|
|
28
|
+
const MAX_BLOCK_DEPTH = 16;
|
|
29
|
+
/** Multiplier for the per-call inline scanning budget (see module docs). */
|
|
30
|
+
const INLINE_BUDGET_PER_CHAR = 64;
|
|
31
|
+
const ASCII_PUNCT = new Set("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~");
|
|
32
|
+
const ATX_RE = /^ {0,3}(#{1,6})(?:[ \t]+(.*?))?[ \t]*$/;
|
|
33
|
+
const HR_RE = /^ {0,3}(?:(?:\*[ \t]*){3,}|(?:-[ \t]*){3,}|(?:_[ \t]*){3,})$/;
|
|
34
|
+
const FENCE_OPEN_RE = /^( {0,3})(`{3,}|~{3,})[ \t]*(.*)$/;
|
|
35
|
+
const BLOCKQUOTE_RE = /^ {0,3}> ?(.*)$/;
|
|
36
|
+
const LIST_RE = /^( {0,3})(?:([-*+])|(\d{1,9})([.)]))( +)(.*)$/;
|
|
37
|
+
const TABLE_DELIM_RE = /^ {0,3}\|?[ \t]*:?-+:?[ \t]*(?:\|[ \t]*:?-+:?[ \t]*)*\|?[ \t]*$/;
|
|
38
|
+
const INDENTED_CODE_RE = /^(?: {4}|\t)/;
|
|
39
|
+
const TASK_ITEM_RE = /^\[( |x|X)\][ \t]+(.*)$/;
|
|
40
|
+
const NAMED_ENTITIES = Object.freeze({
|
|
41
|
+
amp: "&",
|
|
42
|
+
lt: "<",
|
|
43
|
+
gt: ">",
|
|
44
|
+
quot: '"',
|
|
45
|
+
apos: "'",
|
|
46
|
+
nbsp: " ",
|
|
47
|
+
});
|
|
48
|
+
function isWhitespace(ch) {
|
|
49
|
+
return ch === " " || ch === "\t" || ch === "\n";
|
|
50
|
+
}
|
|
51
|
+
function isWordChar(ch) {
|
|
52
|
+
return ch !== undefined && /[A-Za-z0-9]/.test(ch);
|
|
53
|
+
}
|
|
54
|
+
function leadingSpaces(line) {
|
|
55
|
+
let n = 0;
|
|
56
|
+
while (n < line.length && line[n] === " ")
|
|
57
|
+
n++;
|
|
58
|
+
return n;
|
|
59
|
+
}
|
|
60
|
+
function decodeEntities(text) {
|
|
61
|
+
if (!text.includes("&"))
|
|
62
|
+
return text;
|
|
63
|
+
return text.replace(/&(#[xX]?[0-9a-fA-F]{1,6}|[a-zA-Z]{2,6});/g, (match, body) => {
|
|
64
|
+
if (body.startsWith("#")) {
|
|
65
|
+
const hex = body[1] === "x" || body[1] === "X";
|
|
66
|
+
const digits = hex ? body.slice(2) : body.slice(1);
|
|
67
|
+
if (digits.length === 0)
|
|
68
|
+
return match;
|
|
69
|
+
if (!hex && !/^[0-9]+$/.test(digits))
|
|
70
|
+
return match;
|
|
71
|
+
const code = Number.parseInt(digits, hex ? 16 : 10);
|
|
72
|
+
if (!Number.isFinite(code) || code <= 0 || code > 0x10ffff)
|
|
73
|
+
return "�";
|
|
74
|
+
if (code >= 0xd800 && code <= 0xdfff)
|
|
75
|
+
return "�";
|
|
76
|
+
return String.fromCodePoint(code);
|
|
77
|
+
}
|
|
78
|
+
const named = NAMED_ENTITIES[body.toLowerCase()];
|
|
79
|
+
return named ?? match;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function pushText(out, text) {
|
|
83
|
+
if (text.length === 0)
|
|
84
|
+
return;
|
|
85
|
+
const last = out[out.length - 1];
|
|
86
|
+
if (last !== undefined && last.kind === "text") {
|
|
87
|
+
out[out.length - 1] = { kind: "text", text: last.text + text };
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
out.push({ kind: "text", text });
|
|
91
|
+
}
|
|
92
|
+
function scanRun(input, start, ch) {
|
|
93
|
+
let i = start;
|
|
94
|
+
while (i < input.length && input[i] === ch)
|
|
95
|
+
i++;
|
|
96
|
+
return i - start;
|
|
97
|
+
}
|
|
98
|
+
function findBacktickClose(input, from, runLen, ctx) {
|
|
99
|
+
let i = from;
|
|
100
|
+
while (i < input.length) {
|
|
101
|
+
if (ctx.budget-- <= 0)
|
|
102
|
+
return -1;
|
|
103
|
+
if (input[i] === "`") {
|
|
104
|
+
const len = scanRun(input, i, "`");
|
|
105
|
+
if (len === runLen)
|
|
106
|
+
return i;
|
|
107
|
+
i += len;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
i++;
|
|
111
|
+
}
|
|
112
|
+
return -1;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Finds a valid emphasis closing run at or after `from`. Closer validity is
|
|
116
|
+
* position-local (it does not depend on the opener), so a failed search is
|
|
117
|
+
* memoized per marker+need to keep adversarial inputs linear.
|
|
118
|
+
*/
|
|
119
|
+
function findEmphasisClose(input, from, marker, need, ctx) {
|
|
120
|
+
const memoKey = `${marker}${need}`;
|
|
121
|
+
const knownEmptyFrom = ctx.noCloserFrom.get(memoKey);
|
|
122
|
+
if (knownEmptyFrom !== undefined && from >= knownEmptyFrom)
|
|
123
|
+
return -1;
|
|
124
|
+
let i = from;
|
|
125
|
+
while (i < input.length) {
|
|
126
|
+
if (ctx.budget-- <= 0)
|
|
127
|
+
return -1;
|
|
128
|
+
const ch = input[i];
|
|
129
|
+
if (ch === "\\") {
|
|
130
|
+
i += 2;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (ch === "`") {
|
|
134
|
+
const run = scanRun(input, i, "`");
|
|
135
|
+
const close = findBacktickClose(input, i + run, run, ctx);
|
|
136
|
+
i = close >= 0 ? close + run : i + run;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (ch === marker) {
|
|
140
|
+
const run = scanRun(input, i, marker);
|
|
141
|
+
const prev = input[i - 1];
|
|
142
|
+
const next = input[i + run];
|
|
143
|
+
const prevOk = prev !== undefined && !isWhitespace(prev);
|
|
144
|
+
const nextOk = marker !== "_" || !isWordChar(next);
|
|
145
|
+
// Consume the LAST `need` markers of a longer closing run so leading
|
|
146
|
+
// extras stay inside the content (closes `**bold *nested***` cleanly).
|
|
147
|
+
if (run >= need && prevOk && nextOk && i > from)
|
|
148
|
+
return i + (run - need);
|
|
149
|
+
i += run;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
i++;
|
|
153
|
+
}
|
|
154
|
+
ctx.noCloserFrom.set(memoKey, from);
|
|
155
|
+
return -1;
|
|
156
|
+
}
|
|
157
|
+
function tryParseEmphasis(input, start, marker, depth, ctx) {
|
|
158
|
+
const run = scanRun(input, start, marker);
|
|
159
|
+
const after = input[start + run];
|
|
160
|
+
if (after === undefined || isWhitespace(after))
|
|
161
|
+
return null;
|
|
162
|
+
if (marker === "_" && isWordChar(input[start - 1]))
|
|
163
|
+
return null;
|
|
164
|
+
// Opening markers beyond the consumed delimiter re-emit as literal text
|
|
165
|
+
// (full CommonMark would nest them; this subset keeps them visible).
|
|
166
|
+
if (run >= 3) {
|
|
167
|
+
const close = findEmphasisClose(input, start + run, marker, 3, ctx);
|
|
168
|
+
if (close > start + run) {
|
|
169
|
+
const inner = parseInlines(input.slice(start + run, close), depth + 1, ctx);
|
|
170
|
+
const strong = { kind: "strong", children: inner };
|
|
171
|
+
return {
|
|
172
|
+
node: { kind: "em", children: [strong] },
|
|
173
|
+
end: close + 3,
|
|
174
|
+
prefix: marker.repeat(run - 3),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (run >= 2) {
|
|
179
|
+
const close = findEmphasisClose(input, start + run, marker, 2, ctx);
|
|
180
|
+
if (close > start + run) {
|
|
181
|
+
const inner = parseInlines(input.slice(start + run, close), depth + 1, ctx);
|
|
182
|
+
return {
|
|
183
|
+
node: { kind: "strong", children: inner },
|
|
184
|
+
end: close + 2,
|
|
185
|
+
prefix: marker.repeat(run - 2),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const close = findEmphasisClose(input, start + run, marker, 1, ctx);
|
|
190
|
+
if (close > start + run) {
|
|
191
|
+
const inner = parseInlines(input.slice(start + run, close), depth + 1, ctx);
|
|
192
|
+
return {
|
|
193
|
+
node: { kind: "em", children: inner },
|
|
194
|
+
end: close + 1,
|
|
195
|
+
prefix: marker.repeat(run - 1),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
function tryParseDel(input, start, depth, ctx) {
|
|
201
|
+
const contentStart = start + 2;
|
|
202
|
+
const after = input[contentStart];
|
|
203
|
+
if (after === undefined || isWhitespace(after))
|
|
204
|
+
return null;
|
|
205
|
+
let i = contentStart;
|
|
206
|
+
while (i < input.length) {
|
|
207
|
+
if (ctx.budget-- <= 0)
|
|
208
|
+
return null;
|
|
209
|
+
const ch = input[i];
|
|
210
|
+
if (ch === "\\") {
|
|
211
|
+
i += 2;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (ch === "`") {
|
|
215
|
+
const run = scanRun(input, i, "`");
|
|
216
|
+
const close = findBacktickClose(input, i + run, run, ctx);
|
|
217
|
+
i = close >= 0 ? close + run : i + run;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (ch === "~" && input[i + 1] === "~") {
|
|
221
|
+
const prev = input[i - 1];
|
|
222
|
+
if (prev !== undefined && !isWhitespace(prev) && i > contentStart) {
|
|
223
|
+
const inner = parseInlines(input.slice(contentStart, i), depth + 1, ctx);
|
|
224
|
+
return { node: { kind: "del", children: inner }, end: i + 2 };
|
|
225
|
+
}
|
|
226
|
+
i += 2;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
i++;
|
|
230
|
+
}
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
function tryParseLink(input, start, depth, ctx) {
|
|
234
|
+
let i = start + 1;
|
|
235
|
+
let bracketDepth = 1;
|
|
236
|
+
while (i < input.length) {
|
|
237
|
+
if (ctx.budget-- <= 0)
|
|
238
|
+
return null;
|
|
239
|
+
const ch = input[i];
|
|
240
|
+
if (ch === "\\") {
|
|
241
|
+
i += 2;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (ch === "[")
|
|
245
|
+
bracketDepth++;
|
|
246
|
+
else if (ch === "]") {
|
|
247
|
+
bracketDepth--;
|
|
248
|
+
if (bracketDepth === 0)
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
i++;
|
|
252
|
+
}
|
|
253
|
+
if (i >= input.length || bracketDepth !== 0)
|
|
254
|
+
return null;
|
|
255
|
+
const labelEnd = i;
|
|
256
|
+
if (input[labelEnd + 1] !== "(")
|
|
257
|
+
return null;
|
|
258
|
+
let j = labelEnd + 2;
|
|
259
|
+
let parenDepth = 1;
|
|
260
|
+
while (j < input.length) {
|
|
261
|
+
if (ctx.budget-- <= 0)
|
|
262
|
+
return null;
|
|
263
|
+
const ch = input[j];
|
|
264
|
+
if (ch === "\\") {
|
|
265
|
+
j += 2;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (ch === "(")
|
|
269
|
+
parenDepth++;
|
|
270
|
+
else if (ch === ")") {
|
|
271
|
+
parenDepth--;
|
|
272
|
+
if (parenDepth === 0)
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
j++;
|
|
276
|
+
}
|
|
277
|
+
if (j >= input.length || parenDepth !== 0)
|
|
278
|
+
return null;
|
|
279
|
+
let target = input.slice(labelEnd + 2, j).trim();
|
|
280
|
+
const titled = /^(\S+)[ \t]+["'][^"']*["']$/.exec(target);
|
|
281
|
+
const titledTarget = titled?.[1];
|
|
282
|
+
if (titledTarget !== undefined)
|
|
283
|
+
target = titledTarget;
|
|
284
|
+
if (target.startsWith("<") && target.endsWith(">") && target.length >= 2) {
|
|
285
|
+
target = target.slice(1, -1);
|
|
286
|
+
}
|
|
287
|
+
const label = input.slice(start + 1, labelEnd);
|
|
288
|
+
const children = label.length === 0
|
|
289
|
+
? [{ kind: "text", text: target }]
|
|
290
|
+
: parseInlines(label, depth + 1, ctx);
|
|
291
|
+
return { node: { kind: "link", href: target, children }, end: j + 1 };
|
|
292
|
+
}
|
|
293
|
+
function countChar(text, ch) {
|
|
294
|
+
let n = 0;
|
|
295
|
+
for (const c of text)
|
|
296
|
+
if (c === ch)
|
|
297
|
+
n++;
|
|
298
|
+
return n;
|
|
299
|
+
}
|
|
300
|
+
/** Strips trailing punctuation that is conventionally not part of a bare URL. */
|
|
301
|
+
function trimUrlTrailing(url) {
|
|
302
|
+
let out = url;
|
|
303
|
+
for (;;) {
|
|
304
|
+
const last = out[out.length - 1];
|
|
305
|
+
if (last === undefined)
|
|
306
|
+
break;
|
|
307
|
+
if (/[.,;:!?'"]/.test(last)) {
|
|
308
|
+
out = out.slice(0, -1);
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
if (last === ")" && countChar(out, ")") > countChar(out, "(")) {
|
|
312
|
+
out = out.slice(0, -1);
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
return out;
|
|
318
|
+
}
|
|
319
|
+
function tryParseBareUrl(input, start) {
|
|
320
|
+
if (!input.startsWith("http://", start) && !input.startsWith("https://", start))
|
|
321
|
+
return null;
|
|
322
|
+
if (isWordChar(input[start - 1]))
|
|
323
|
+
return null;
|
|
324
|
+
let end = start;
|
|
325
|
+
while (end < input.length) {
|
|
326
|
+
const ch = input[end];
|
|
327
|
+
if (ch === undefined || isWhitespace(ch) || ch === "<" || ch === ">")
|
|
328
|
+
break;
|
|
329
|
+
end++;
|
|
330
|
+
}
|
|
331
|
+
const url = trimUrlTrailing(input.slice(start, end));
|
|
332
|
+
if (!/^https?:\/\/\S+$/.test(url) || url.endsWith("//"))
|
|
333
|
+
return null;
|
|
334
|
+
return {
|
|
335
|
+
node: { kind: "link", href: url, children: [{ kind: "text", text: url }] },
|
|
336
|
+
end: start + url.length,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
function parseInlines(input, depth, ctx) {
|
|
340
|
+
const out = [];
|
|
341
|
+
if (depth > MAX_INLINE_DEPTH) {
|
|
342
|
+
pushText(out, decodeEntities(input));
|
|
343
|
+
return out;
|
|
344
|
+
}
|
|
345
|
+
let plain = "";
|
|
346
|
+
let i = 0;
|
|
347
|
+
const flush = () => {
|
|
348
|
+
if (plain.length > 0) {
|
|
349
|
+
pushText(out, decodeEntities(plain));
|
|
350
|
+
plain = "";
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
while (i < input.length) {
|
|
354
|
+
const ch = input[i];
|
|
355
|
+
if (ch === undefined)
|
|
356
|
+
break;
|
|
357
|
+
if (ctx.budget-- <= 0) {
|
|
358
|
+
plain += input.slice(i);
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
if (ch === "\\" && i + 1 < input.length) {
|
|
362
|
+
const next = input[i + 1];
|
|
363
|
+
if (next !== undefined && ASCII_PUNCT.has(next)) {
|
|
364
|
+
plain += next;
|
|
365
|
+
i += 2;
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (ch === "`") {
|
|
370
|
+
const run = scanRun(input, i, "`");
|
|
371
|
+
const close = findBacktickClose(input, i + run, run, ctx);
|
|
372
|
+
if (close >= 0) {
|
|
373
|
+
flush();
|
|
374
|
+
let content = input.slice(i + run, close).replace(/\n/g, " ");
|
|
375
|
+
if (content.length >= 2 &&
|
|
376
|
+
content.startsWith(" ") &&
|
|
377
|
+
content.endsWith(" ") &&
|
|
378
|
+
content.trim().length > 0) {
|
|
379
|
+
content = content.slice(1, -1);
|
|
380
|
+
}
|
|
381
|
+
out.push({ kind: "code", text: content });
|
|
382
|
+
i = close + run;
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
plain += input.slice(i, i + run);
|
|
386
|
+
i += run;
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
if (ch === "<") {
|
|
390
|
+
const m = /^<(https?:\/\/[^\s<>]+)>/.exec(input.slice(i));
|
|
391
|
+
const href = m?.[1];
|
|
392
|
+
if (m !== null && href !== undefined) {
|
|
393
|
+
flush();
|
|
394
|
+
out.push({ kind: "link", href, children: [{ kind: "text", text: href }] });
|
|
395
|
+
i += m[0].length;
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
if (ch === "[") {
|
|
400
|
+
const link = tryParseLink(input, i, depth, ctx);
|
|
401
|
+
if (link !== null) {
|
|
402
|
+
flush();
|
|
403
|
+
out.push(link.node);
|
|
404
|
+
i = link.end;
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (ch === "*" || ch === "_") {
|
|
409
|
+
const em = tryParseEmphasis(input, i, ch, depth, ctx);
|
|
410
|
+
if (em !== null) {
|
|
411
|
+
plain += em.prefix;
|
|
412
|
+
flush();
|
|
413
|
+
out.push(em.node);
|
|
414
|
+
i = em.end;
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
const run = scanRun(input, i, ch);
|
|
418
|
+
plain += input.slice(i, i + run);
|
|
419
|
+
i += run;
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
if (ch === "~" && input[i + 1] === "~") {
|
|
423
|
+
const del = tryParseDel(input, i, depth, ctx);
|
|
424
|
+
if (del !== null) {
|
|
425
|
+
flush();
|
|
426
|
+
out.push(del.node);
|
|
427
|
+
i = del.end;
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
plain += "~~";
|
|
431
|
+
i += 2;
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
if (ch === "h") {
|
|
435
|
+
const bare = tryParseBareUrl(input, i);
|
|
436
|
+
if (bare !== null) {
|
|
437
|
+
flush();
|
|
438
|
+
out.push(bare.node);
|
|
439
|
+
i = bare.end;
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
plain += ch;
|
|
444
|
+
i++;
|
|
445
|
+
}
|
|
446
|
+
flush();
|
|
447
|
+
return out;
|
|
448
|
+
}
|
|
449
|
+
function newInlineCtx(input) {
|
|
450
|
+
return { budget: input.length * INLINE_BUDGET_PER_CHAR + 1024, noCloserFrom: new Map() };
|
|
451
|
+
}
|
|
452
|
+
/** Parses one inline run with a fresh work budget. */
|
|
453
|
+
function parseInlineRun(input, depth) {
|
|
454
|
+
return parseInlines(input, depth, newInlineCtx(input));
|
|
455
|
+
}
|
|
456
|
+
function parseParagraphInlines(rawLines, depth) {
|
|
457
|
+
const out = [];
|
|
458
|
+
for (let idx = 0; idx < rawLines.length; idx++) {
|
|
459
|
+
const raw = rawLines[idx] ?? "";
|
|
460
|
+
const isLast = idx === rawLines.length - 1;
|
|
461
|
+
let content = raw.trim();
|
|
462
|
+
let hardBreak = false;
|
|
463
|
+
if (!isLast) {
|
|
464
|
+
if (/ {2,}$/.test(raw))
|
|
465
|
+
hardBreak = true;
|
|
466
|
+
else if (content.endsWith("\\") && !content.endsWith("\\\\")) {
|
|
467
|
+
hardBreak = true;
|
|
468
|
+
content = content.slice(0, -1).trimEnd();
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
for (const node of parseInlineRun(content, depth)) {
|
|
472
|
+
if (node.kind === "text")
|
|
473
|
+
pushText(out, node.text);
|
|
474
|
+
else
|
|
475
|
+
out.push(node);
|
|
476
|
+
}
|
|
477
|
+
if (!isLast) {
|
|
478
|
+
if (hardBreak)
|
|
479
|
+
out.push({ kind: "break" });
|
|
480
|
+
else
|
|
481
|
+
pushText(out, " ");
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return out;
|
|
485
|
+
}
|
|
486
|
+
function dedentUpTo(line, columns) {
|
|
487
|
+
let removed = 0;
|
|
488
|
+
while (removed < columns && line[removed] === " ")
|
|
489
|
+
removed++;
|
|
490
|
+
return line.slice(removed);
|
|
491
|
+
}
|
|
492
|
+
function splitTableCells(line) {
|
|
493
|
+
const trimmed = line.trim();
|
|
494
|
+
const cells = [];
|
|
495
|
+
let current = "";
|
|
496
|
+
let i = trimmed[0] === "|" ? 1 : 0;
|
|
497
|
+
while (i < trimmed.length) {
|
|
498
|
+
const ch = trimmed[i];
|
|
499
|
+
if (ch === "\\" && trimmed[i + 1] === "|") {
|
|
500
|
+
current += "|";
|
|
501
|
+
i += 2;
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
if (ch === "\\" && i + 1 < trimmed.length) {
|
|
505
|
+
current += ch;
|
|
506
|
+
current += trimmed[i + 1] ?? "";
|
|
507
|
+
i += 2;
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
if (ch === "|") {
|
|
511
|
+
cells.push(current.trim());
|
|
512
|
+
current = "";
|
|
513
|
+
i++;
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
current += ch ?? "";
|
|
517
|
+
i++;
|
|
518
|
+
}
|
|
519
|
+
if (current.trim().length > 0 || !trimmed.endsWith("|") || trimmed.length === 0) {
|
|
520
|
+
cells.push(current.trim());
|
|
521
|
+
}
|
|
522
|
+
return cells;
|
|
523
|
+
}
|
|
524
|
+
function parseTable(lines, start, depth) {
|
|
525
|
+
const header = lines[start] ?? "";
|
|
526
|
+
const delim = lines[start + 1] ?? "";
|
|
527
|
+
if (!header.includes("|"))
|
|
528
|
+
return null;
|
|
529
|
+
if (!delim.includes("|"))
|
|
530
|
+
return null;
|
|
531
|
+
if (!TABLE_DELIM_RE.test(delim))
|
|
532
|
+
return null;
|
|
533
|
+
const headCells = splitTableCells(header);
|
|
534
|
+
const align = splitTableCells(delim).map((cell) => {
|
|
535
|
+
const left = cell.startsWith(":");
|
|
536
|
+
const right = cell.endsWith(":");
|
|
537
|
+
if (left && right)
|
|
538
|
+
return "center";
|
|
539
|
+
if (right)
|
|
540
|
+
return "right";
|
|
541
|
+
return "left";
|
|
542
|
+
});
|
|
543
|
+
if (headCells.length !== align.length || headCells.length === 0)
|
|
544
|
+
return null;
|
|
545
|
+
const rows = [];
|
|
546
|
+
let i = start + 2;
|
|
547
|
+
while (i < lines.length) {
|
|
548
|
+
const line = lines[i] ?? "";
|
|
549
|
+
if (line.trim().length === 0 || !line.includes("|"))
|
|
550
|
+
break;
|
|
551
|
+
const cells = splitTableCells(line).slice(0, headCells.length);
|
|
552
|
+
while (cells.length < headCells.length)
|
|
553
|
+
cells.push("");
|
|
554
|
+
rows.push(cells.map((cell) => parseInlineRun(cell, depth + 1)));
|
|
555
|
+
i++;
|
|
556
|
+
}
|
|
557
|
+
return {
|
|
558
|
+
block: {
|
|
559
|
+
kind: "table",
|
|
560
|
+
align,
|
|
561
|
+
head: headCells.map((cell) => parseInlineRun(cell, depth + 1)),
|
|
562
|
+
rows,
|
|
563
|
+
},
|
|
564
|
+
next: i,
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
function parseList(lines, start, depth) {
|
|
568
|
+
const first = LIST_RE.exec(lines[start] ?? "");
|
|
569
|
+
if (first === null)
|
|
570
|
+
return null;
|
|
571
|
+
const ordered = first[3] !== undefined;
|
|
572
|
+
const firstNumber = Number.parseInt(first[3] ?? "1", 10);
|
|
573
|
+
const startNumber = ordered && Number.isFinite(firstNumber) ? firstNumber : 1;
|
|
574
|
+
const items = [];
|
|
575
|
+
let i = start;
|
|
576
|
+
while (i < lines.length) {
|
|
577
|
+
const m = LIST_RE.exec(lines[i] ?? "");
|
|
578
|
+
if (m === null)
|
|
579
|
+
break;
|
|
580
|
+
if ((m[3] !== undefined) !== ordered)
|
|
581
|
+
break;
|
|
582
|
+
const indent = (m[1] ?? "").length;
|
|
583
|
+
const markerLength = ordered ? (m[3] ?? "1").length + 1 : 1;
|
|
584
|
+
const gap = Math.min((m[5] ?? " ").length, 4);
|
|
585
|
+
const contentIndent = indent + markerLength + gap;
|
|
586
|
+
const itemLines = [m[6] ?? ""];
|
|
587
|
+
i++;
|
|
588
|
+
while (i < lines.length) {
|
|
589
|
+
const line = lines[i] ?? "";
|
|
590
|
+
if (line.trim().length === 0) {
|
|
591
|
+
let j = i + 1;
|
|
592
|
+
while (j < lines.length && (lines[j] ?? "").trim().length === 0)
|
|
593
|
+
j++;
|
|
594
|
+
const upcoming = lines[j];
|
|
595
|
+
if (upcoming !== undefined && leadingSpaces(upcoming) >= contentIndent) {
|
|
596
|
+
itemLines.push("");
|
|
597
|
+
i++;
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
break;
|
|
601
|
+
}
|
|
602
|
+
if (leadingSpaces(line) >= contentIndent) {
|
|
603
|
+
itemLines.push(line.slice(contentIndent));
|
|
604
|
+
i++;
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
break;
|
|
608
|
+
}
|
|
609
|
+
let checked = null;
|
|
610
|
+
const task = TASK_ITEM_RE.exec(itemLines[0] ?? "");
|
|
611
|
+
if (task !== null) {
|
|
612
|
+
checked = task[1] !== " ";
|
|
613
|
+
itemLines[0] = task[2] ?? "";
|
|
614
|
+
}
|
|
615
|
+
items.push({ checked, blocks: parseBlocks(itemLines, depth + 1) });
|
|
616
|
+
while (i < lines.length && (lines[i] ?? "").trim().length === 0)
|
|
617
|
+
i++;
|
|
618
|
+
}
|
|
619
|
+
if (items.length === 0)
|
|
620
|
+
return null;
|
|
621
|
+
return {
|
|
622
|
+
block: { kind: "list", ordered, start: startNumber, items },
|
|
623
|
+
next: i,
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
function isParagraphInterrupter(line) {
|
|
627
|
+
if (line.trim().length === 0)
|
|
628
|
+
return true;
|
|
629
|
+
if (ATX_RE.test(line))
|
|
630
|
+
return true;
|
|
631
|
+
if (FENCE_OPEN_RE.test(line))
|
|
632
|
+
return true;
|
|
633
|
+
if (HR_RE.test(line))
|
|
634
|
+
return true;
|
|
635
|
+
if (BLOCKQUOTE_RE.test(line))
|
|
636
|
+
return true;
|
|
637
|
+
if (LIST_RE.test(line))
|
|
638
|
+
return true;
|
|
639
|
+
return false;
|
|
640
|
+
}
|
|
641
|
+
function parseBlocks(lines, depth, starts) {
|
|
642
|
+
const blocks = [];
|
|
643
|
+
if (depth > MAX_BLOCK_DEPTH) {
|
|
644
|
+
const text = lines.join(" ").trim();
|
|
645
|
+
if (text.length > 0) {
|
|
646
|
+
starts?.push(0);
|
|
647
|
+
blocks.push({ kind: "paragraph", children: [{ kind: "text", text }] });
|
|
648
|
+
}
|
|
649
|
+
return blocks;
|
|
650
|
+
}
|
|
651
|
+
let i = 0;
|
|
652
|
+
while (i < lines.length) {
|
|
653
|
+
const blockStart = i;
|
|
654
|
+
const line = lines[i] ?? "";
|
|
655
|
+
if (line.trim().length === 0) {
|
|
656
|
+
i++;
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
const fence = FENCE_OPEN_RE.exec(line);
|
|
660
|
+
if (fence !== null) {
|
|
661
|
+
const fenceIndent = (fence[1] ?? "").length;
|
|
662
|
+
const fenceRun = fence[2] ?? "```";
|
|
663
|
+
const fenceChar = fenceRun.startsWith("~") ? "~" : "`";
|
|
664
|
+
const info = (fence[3] ?? "").trim();
|
|
665
|
+
if (fenceChar === "~" || !info.includes("`")) {
|
|
666
|
+
const language = (info.split(/\s+/)[0] ?? "").toLowerCase();
|
|
667
|
+
const closeRe = new RegExp(`^ {0,3}${fenceChar}{${fenceRun.length},}[ \\t]*$`);
|
|
668
|
+
const content = [];
|
|
669
|
+
i++;
|
|
670
|
+
while (i < lines.length && !closeRe.test(lines[i] ?? "")) {
|
|
671
|
+
content.push(dedentUpTo(lines[i] ?? "", fenceIndent));
|
|
672
|
+
i++;
|
|
673
|
+
}
|
|
674
|
+
if (i < lines.length)
|
|
675
|
+
i++;
|
|
676
|
+
starts?.push(blockStart);
|
|
677
|
+
blocks.push({ kind: "codeBlock", language, text: content.join("\n") });
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
const atx = ATX_RE.exec(line);
|
|
682
|
+
if (atx !== null) {
|
|
683
|
+
const level = Math.min(Math.max((atx[1] ?? "#").length, 1), 6);
|
|
684
|
+
const content = (atx[2] ?? "").replace(/[ \t]+#+$/, "").trim();
|
|
685
|
+
starts?.push(blockStart);
|
|
686
|
+
blocks.push({ kind: "heading", level, children: parseInlineRun(content, depth) });
|
|
687
|
+
i++;
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
if (HR_RE.test(line)) {
|
|
691
|
+
starts?.push(blockStart);
|
|
692
|
+
blocks.push({ kind: "hr" });
|
|
693
|
+
i++;
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
if (BLOCKQUOTE_RE.test(line)) {
|
|
697
|
+
const inner = [];
|
|
698
|
+
while (i < lines.length) {
|
|
699
|
+
const m = BLOCKQUOTE_RE.exec(lines[i] ?? "");
|
|
700
|
+
if (m === null)
|
|
701
|
+
break;
|
|
702
|
+
inner.push(m[1] ?? "");
|
|
703
|
+
i++;
|
|
704
|
+
}
|
|
705
|
+
starts?.push(blockStart);
|
|
706
|
+
blocks.push({ kind: "blockquote", children: parseBlocks(inner, depth + 1) });
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
709
|
+
const list = parseList(lines, i, depth);
|
|
710
|
+
if (list !== null) {
|
|
711
|
+
starts?.push(blockStart);
|
|
712
|
+
blocks.push(list.block);
|
|
713
|
+
i = list.next;
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
const table = parseTable(lines, i, depth);
|
|
717
|
+
if (table !== null) {
|
|
718
|
+
starts?.push(blockStart);
|
|
719
|
+
blocks.push(table.block);
|
|
720
|
+
i = table.next;
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
if (INDENTED_CODE_RE.test(line)) {
|
|
724
|
+
const content = [];
|
|
725
|
+
while (i < lines.length) {
|
|
726
|
+
const current = lines[i] ?? "";
|
|
727
|
+
if (INDENTED_CODE_RE.test(current)) {
|
|
728
|
+
content.push(current.replace(INDENTED_CODE_RE, ""));
|
|
729
|
+
i++;
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
if (current.trim().length === 0) {
|
|
733
|
+
let j = i + 1;
|
|
734
|
+
while (j < lines.length && (lines[j] ?? "").trim().length === 0)
|
|
735
|
+
j++;
|
|
736
|
+
const upcoming = lines[j];
|
|
737
|
+
if (upcoming !== undefined && INDENTED_CODE_RE.test(upcoming)) {
|
|
738
|
+
for (let k = i; k < j; k++)
|
|
739
|
+
content.push("");
|
|
740
|
+
i = j;
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
break;
|
|
745
|
+
}
|
|
746
|
+
starts?.push(blockStart);
|
|
747
|
+
blocks.push({ kind: "codeBlock", language: "", text: content.join("\n") });
|
|
748
|
+
continue;
|
|
749
|
+
}
|
|
750
|
+
const paragraph = [line];
|
|
751
|
+
i++;
|
|
752
|
+
while (i < lines.length && !isParagraphInterrupter(lines[i] ?? "")) {
|
|
753
|
+
paragraph.push(lines[i] ?? "");
|
|
754
|
+
i++;
|
|
755
|
+
}
|
|
756
|
+
starts?.push(blockStart);
|
|
757
|
+
blocks.push({ kind: "paragraph", children: parseParagraphInlines(paragraph, depth) });
|
|
758
|
+
}
|
|
759
|
+
return blocks;
|
|
760
|
+
}
|
|
761
|
+
function deepFreeze(value) {
|
|
762
|
+
if (value !== null && typeof value === "object" && !Object.isFrozen(value)) {
|
|
763
|
+
Object.freeze(value);
|
|
764
|
+
for (const key of Object.keys(value)) {
|
|
765
|
+
deepFreeze(value[key]);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
return value;
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Parses pre-normalized lines into deeply frozen top-level blocks. Internal
|
|
772
|
+
* surface shared by parseMarkdown() and the streaming parser; when `starts`
|
|
773
|
+
* is provided it receives the source line index of every top-level block.
|
|
774
|
+
*/
|
|
775
|
+
export function parseMarkdownLines(lines, starts) {
|
|
776
|
+
const blocks = parseBlocks(lines, 0, starts);
|
|
777
|
+
for (const block of blocks)
|
|
778
|
+
deepFreeze(block);
|
|
779
|
+
return blocks;
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Parses a GFM-subset markdown document. Never throws: malformed constructs
|
|
783
|
+
* degrade to literal text and the result is deeply frozen.
|
|
784
|
+
*/
|
|
785
|
+
export function parseMarkdown(source) {
|
|
786
|
+
const text = typeof source === "string" ? source : "";
|
|
787
|
+
const normalized = text.replace(/\r\n?/g, "\n").split("\u0000").join("\uFFFD");
|
|
788
|
+
const blocks = parseMarkdownLines(normalized.split("\n"));
|
|
789
|
+
return Object.freeze({ blocks: Object.freeze(blocks) });
|
|
790
|
+
}
|
|
791
|
+
//# sourceMappingURL=parse.js.map
|