@cube-drone/marquee-parser 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -0
- package/dist/blocks.d.ts +10 -1
- package/dist/blocks.js +141 -35
- package/dist/index.d.ts +20 -0
- package/dist/index.js +28 -0
- package/dist/inlines.d.ts +8 -1
- package/dist/inlines.js +78 -45
- package/package.json +2 -2
- package/src/blocks.ts +196 -38
- package/src/index.ts +35 -1
- package/src/inlines.ts +117 -48
package/src/inlines.ts
CHANGED
|
@@ -5,20 +5,34 @@
|
|
|
5
5
|
//
|
|
6
6
|
// Works on code points (Array.from), mirroring Rust's Vec<char>, so the two
|
|
7
7
|
// implementations scan identical units.
|
|
8
|
+
//
|
|
9
|
+
// Positions (SPEC.md, "Source positions"): when a PosCtx rides along, every
|
|
10
|
+
// node gets a [start, end) span in the side-table - UTF-16 offsets into the
|
|
11
|
+
// normalized source, translated from code-point indices via `map`. The AST
|
|
12
|
+
// itself is untouched either way: spans live outside the wire contract.
|
|
8
13
|
|
|
9
14
|
import type { Attrs, Node } from "./ast.ts";
|
|
10
15
|
import { isName, nameLen, parseAttrs, parseValue, utf8Len } from "./attrs.ts";
|
|
11
|
-
import { MAX_TARGET_BYTES } from "./blocks.ts";
|
|
16
|
+
import { MAX_TARGET_BYTES, type Span } from "./blocks.ts";
|
|
12
17
|
|
|
13
18
|
export const MAX_INLINE_DEPTH = 16;
|
|
14
19
|
export const MAX_EMOJI_SLUG_BYTES = 64;
|
|
15
20
|
|
|
21
|
+
/** Positions context: map[cpIndex] = source UTF-16 offset of that code
|
|
22
|
+
* point in the logical text (one extra entry for the end). */
|
|
23
|
+
export interface PosCtx {
|
|
24
|
+
map: number[];
|
|
25
|
+
spans: WeakMap<Node, Span>;
|
|
26
|
+
}
|
|
27
|
+
|
|
16
28
|
type DelimKind = "em" | "strong" | "strike";
|
|
17
29
|
|
|
18
30
|
interface Delim {
|
|
19
31
|
kind: DelimKind;
|
|
20
32
|
idx: number;
|
|
21
33
|
raw: string;
|
|
34
|
+
/** Code-point index of the delimiter run in the logical text. */
|
|
35
|
+
cp: number;
|
|
22
36
|
}
|
|
23
37
|
|
|
24
38
|
interface Frame {
|
|
@@ -28,10 +42,13 @@ interface Frame {
|
|
|
28
42
|
attrs: Attrs;
|
|
29
43
|
children: Node[];
|
|
30
44
|
delims: Delim[];
|
|
45
|
+
/** Code-point range of the opener in the logical text. */
|
|
46
|
+
openerCp: number;
|
|
47
|
+
openerCpEnd: number;
|
|
31
48
|
}
|
|
32
49
|
|
|
33
50
|
function rootFrame(): Frame {
|
|
34
|
-
return { openerRaw: "", name: "", attrs: {}, children: [], delims: [] };
|
|
51
|
+
return { openerRaw: "", name: "", attrs: {}, children: [], delims: [], openerCp: 0, openerCpEnd: 0 };
|
|
35
52
|
}
|
|
36
53
|
|
|
37
54
|
const ASCII_PUNCT = new Set("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~");
|
|
@@ -46,14 +63,22 @@ function isSlugChar(c: string): boolean {
|
|
|
46
63
|
return /^[a-z0-9_+-]$/.test(c);
|
|
47
64
|
}
|
|
48
65
|
|
|
49
|
-
export function parseInlines(text: string): Node[] {
|
|
50
|
-
return parseInlinesAt(text, 0);
|
|
66
|
+
export function parseInlines(text: string, P: PosCtx | null = null): Node[] {
|
|
67
|
+
return parseInlinesAt(text, 0, P);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Record a node's span from code-point range; returns the node. */
|
|
71
|
+
function mk(P: PosCtx | null, node: Node, cpStart: number, cpEnd: number): Node {
|
|
72
|
+
if (P !== null) {
|
|
73
|
+
P.spans.set(node, { start: P.map[cpStart]!, end: P.map[cpEnd]! });
|
|
74
|
+
}
|
|
75
|
+
return node;
|
|
51
76
|
}
|
|
52
77
|
|
|
53
78
|
/** `base` is the inline depth already spent by enclosing link text: spans,
|
|
54
79
|
* delimiters, and link nesting share the one <= 16 cap (a per-construct cap
|
|
55
80
|
* would let composition multiply past it). */
|
|
56
|
-
function parseInlinesAt(text: string, base: number): Node[] {
|
|
81
|
+
function parseInlinesAt(text: string, base: number, P: PosCtx | null): Node[] {
|
|
57
82
|
const chars = Array.from(text);
|
|
58
83
|
const frames: Frame[] = [rootFrame()];
|
|
59
84
|
let i = 0;
|
|
@@ -63,29 +88,31 @@ function parseInlinesAt(text: string, base: number): Node[] {
|
|
|
63
88
|
if (c === "\\") {
|
|
64
89
|
const n = chars[i + 1];
|
|
65
90
|
if (n === "\n") {
|
|
66
|
-
top(frames).children.push({ type: "hard_break" });
|
|
91
|
+
top(frames).children.push(mk(P, { type: "hard_break" }, i, i + 2));
|
|
67
92
|
i += 2;
|
|
68
93
|
} else if (n !== undefined && ASCII_PUNCT.has(n)) {
|
|
69
|
-
pushStr(frames, n);
|
|
94
|
+
pushStr(frames, n, P, i, i + 2);
|
|
70
95
|
i += 2;
|
|
71
96
|
} else {
|
|
72
|
-
pushStr(frames, "\\");
|
|
97
|
+
pushStr(frames, "\\", P, i, i + 1);
|
|
73
98
|
i += 1;
|
|
74
99
|
}
|
|
75
100
|
} else if (c === "`") {
|
|
76
101
|
const n = runLen(chars, i, "`");
|
|
77
102
|
const k = findBacktickCloser(chars, i + n, n);
|
|
78
103
|
if (k !== null) {
|
|
79
|
-
top(frames).children.push(
|
|
104
|
+
top(frames).children.push(
|
|
105
|
+
mk(P, { type: "code_span", text: chars.slice(i + n, k).join("") }, i, k + n),
|
|
106
|
+
);
|
|
80
107
|
i = k + n;
|
|
81
108
|
} else {
|
|
82
|
-
pushStr(frames, "`".repeat(n));
|
|
109
|
+
pushStr(frames, "`".repeat(n), P, i, i + n);
|
|
83
110
|
i += n;
|
|
84
111
|
}
|
|
85
112
|
} else if (c === "[") {
|
|
86
|
-
i = bracket(chars, i, false, frames, base);
|
|
113
|
+
i = bracket(chars, i, false, frames, base, P);
|
|
87
114
|
} else if (c === "!" && chars[i + 1] === "[") {
|
|
88
|
-
i = bracket(chars, i + 1, true, frames, base);
|
|
115
|
+
i = bracket(chars, i + 1, true, frames, base, P);
|
|
89
116
|
} else if (c === ":") {
|
|
90
117
|
let k = i + 1;
|
|
91
118
|
while (k < chars.length && isSlugChar(chars[k]!)) {
|
|
@@ -93,32 +120,34 @@ function parseInlinesAt(text: string, base: number): Node[] {
|
|
|
93
120
|
}
|
|
94
121
|
const slugLen = k - (i + 1);
|
|
95
122
|
if (slugLen >= 1 && slugLen <= MAX_EMOJI_SLUG_BYTES && chars[k] === ":") {
|
|
96
|
-
top(frames).children.push(
|
|
123
|
+
top(frames).children.push(
|
|
124
|
+
mk(P, { type: "emoji", slug: chars.slice(i + 1, k).join("") }, i, k + 1),
|
|
125
|
+
);
|
|
97
126
|
i = k + 1;
|
|
98
127
|
} else {
|
|
99
|
-
pushStr(frames, ":");
|
|
128
|
+
pushStr(frames, ":", P, i, i + 1);
|
|
100
129
|
i += 1;
|
|
101
130
|
}
|
|
102
131
|
} else if (c === "*") {
|
|
103
132
|
const n = runLen(chars, i, "*");
|
|
104
133
|
if (n === 1) {
|
|
105
|
-
i = delimiter(chars, i, n, "em", "*", frames, base);
|
|
134
|
+
i = delimiter(chars, i, n, "em", "*", frames, base, P);
|
|
106
135
|
} else if (n === 2) {
|
|
107
|
-
i = delimiter(chars, i, n, "strong", "**", frames, base);
|
|
136
|
+
i = delimiter(chars, i, n, "strong", "**", frames, base, P);
|
|
108
137
|
} else {
|
|
109
|
-
pushStr(frames, "*".repeat(n));
|
|
138
|
+
pushStr(frames, "*".repeat(n), P, i, i + n);
|
|
110
139
|
i += n;
|
|
111
140
|
}
|
|
112
141
|
} else if (c === "~") {
|
|
113
142
|
const n = runLen(chars, i, "~");
|
|
114
143
|
if (n === 2) {
|
|
115
|
-
i = delimiter(chars, i, n, "strike", "~~", frames, base);
|
|
144
|
+
i = delimiter(chars, i, n, "strike", "~~", frames, base, P);
|
|
116
145
|
} else {
|
|
117
|
-
pushStr(frames, "~".repeat(n));
|
|
146
|
+
pushStr(frames, "~".repeat(n), P, i, i + n);
|
|
118
147
|
i += n;
|
|
119
148
|
}
|
|
120
149
|
} else {
|
|
121
|
-
pushStr(frames, c);
|
|
150
|
+
pushStr(frames, c, P, i, i + 1);
|
|
122
151
|
i += 1;
|
|
123
152
|
}
|
|
124
153
|
}
|
|
@@ -126,17 +155,17 @@ function parseInlinesAt(text: string, base: number): Node[] {
|
|
|
126
155
|
// Container end: unclosed spans and delimiters revert to literal text.
|
|
127
156
|
while (frames.length > 1) {
|
|
128
157
|
const frame = frames.pop()!;
|
|
129
|
-
top(frames).children.push(...revertFrame(frame));
|
|
158
|
+
top(frames).children.push(...revertFrame(frame, P));
|
|
130
159
|
}
|
|
131
160
|
const root = frames.pop()!;
|
|
132
|
-
return normalize(revertDelims(root.children, root.delims));
|
|
161
|
+
return normalize(revertDelims(root.children, root.delims, P), P);
|
|
133
162
|
}
|
|
134
163
|
|
|
135
164
|
function top(frames: Frame[]): Frame {
|
|
136
165
|
return frames[frames.length - 1]!;
|
|
137
166
|
}
|
|
138
167
|
|
|
139
|
-
function pushStr(frames: Frame[], s: string): void {
|
|
168
|
+
function pushStr(frames: Frame[], s: string, P: PosCtx | null, cpStart: number, cpEnd: number): void {
|
|
140
169
|
const frame = top(frames);
|
|
141
170
|
// An open delimiter sits (invisibly, until it closes) at its recorded
|
|
142
171
|
// index: text on its far side must not merge into text before it.
|
|
@@ -146,10 +175,16 @@ function pushStr(frames: Frame[], s: string): void {
|
|
|
146
175
|
const last = children[children.length - 1]!;
|
|
147
176
|
if (last.type === "text") {
|
|
148
177
|
last.value += s;
|
|
178
|
+
if (P !== null) {
|
|
179
|
+
const span = P.spans.get(last);
|
|
180
|
+
if (span !== undefined) {
|
|
181
|
+
span.end = P.map[cpEnd]!;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
149
184
|
return;
|
|
150
185
|
}
|
|
151
186
|
}
|
|
152
|
-
children.push({ type: "text", value: s });
|
|
187
|
+
children.push(mk(P, { type: "text", value: s }, cpStart, cpEnd));
|
|
153
188
|
}
|
|
154
189
|
|
|
155
190
|
function runLen(chars: string[], i: number, c: string): number {
|
|
@@ -192,6 +227,7 @@ function delimiter(
|
|
|
192
227
|
raw: string,
|
|
193
228
|
frames: Frame[],
|
|
194
229
|
base: number,
|
|
230
|
+
P: PosCtx | null,
|
|
195
231
|
): number {
|
|
196
232
|
const canClose = i > 0 && !isWs(chars[i - 1]!);
|
|
197
233
|
const next = chars[i + n];
|
|
@@ -201,18 +237,23 @@ function delimiter(
|
|
|
201
237
|
const innermost = frame.delims[frame.delims.length - 1];
|
|
202
238
|
if (canClose && innermost !== undefined && innermost.kind === kind) {
|
|
203
239
|
frame.delims.pop();
|
|
204
|
-
const inner = normalize(frame.children.splice(innermost.idx));
|
|
240
|
+
const inner = normalize(frame.children.splice(innermost.idx), P);
|
|
205
241
|
frame.children.push(
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
? { type: "
|
|
210
|
-
:
|
|
242
|
+
mk(
|
|
243
|
+
P,
|
|
244
|
+
kind === "em"
|
|
245
|
+
? { type: "emphasis", children: inner }
|
|
246
|
+
: kind === "strong"
|
|
247
|
+
? { type: "strong", children: inner }
|
|
248
|
+
: { type: "strikethrough", children: inner },
|
|
249
|
+
innermost.cp,
|
|
250
|
+
i + n,
|
|
251
|
+
),
|
|
211
252
|
);
|
|
212
253
|
} else if (canOpen && !deep) {
|
|
213
|
-
frame.delims.push({ kind, idx: frame.children.length, raw });
|
|
254
|
+
frame.delims.push({ kind, idx: frame.children.length, raw, cp: i });
|
|
214
255
|
} else {
|
|
215
|
-
pushStr(frames, raw);
|
|
256
|
+
pushStr(frames, raw, P, i, i + n);
|
|
216
257
|
}
|
|
217
258
|
return i + n;
|
|
218
259
|
}
|
|
@@ -225,10 +266,11 @@ function bracket(
|
|
|
225
266
|
embed: boolean,
|
|
226
267
|
frames: Frame[],
|
|
227
268
|
base: number,
|
|
269
|
+
P: PosCtx | null,
|
|
228
270
|
): number {
|
|
229
271
|
const bang = embed ? open - 1 : open;
|
|
230
272
|
const fallback = (): number => {
|
|
231
|
-
pushStr(frames, chars[bang]
|
|
273
|
+
pushStr(frames, chars[bang]!, P, bang, bang + 1);
|
|
232
274
|
return bang + 1;
|
|
233
275
|
};
|
|
234
276
|
|
|
@@ -264,10 +306,19 @@ function bracket(
|
|
|
264
306
|
const lexed = lexTarget(chars, k + 2);
|
|
265
307
|
if (lexed !== null) {
|
|
266
308
|
const depth = base + totalDepth(frames) + 1;
|
|
309
|
+
// The link text's own positions: the interior is a contiguous slice
|
|
310
|
+
// of this logical text, so its map is a slice of ours.
|
|
311
|
+
const subP: PosCtx | null =
|
|
312
|
+
P === null ? null : { map: P.map.slice(open + 1, k + 1), spans: P.spans };
|
|
267
313
|
top(frames).children.push(
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
314
|
+
mk(
|
|
315
|
+
P,
|
|
316
|
+
embed
|
|
317
|
+
? { type: "embed", target: lexed.target, alt: resolveEscapes(interior) }
|
|
318
|
+
: { type: "link", target: lexed.target, children: parseInlinesAt(interior, depth, subP) },
|
|
319
|
+
bang,
|
|
320
|
+
lexed.end,
|
|
321
|
+
),
|
|
271
322
|
);
|
|
272
323
|
return lexed.end;
|
|
273
324
|
}
|
|
@@ -279,15 +330,17 @@ function bracket(
|
|
|
279
330
|
const name = interior.slice(1);
|
|
280
331
|
if (isName(name)) {
|
|
281
332
|
if (embed) {
|
|
282
|
-
pushStr(frames, "!"); // the ! belongs to links/embeds, not spans
|
|
333
|
+
pushStr(frames, "!", P, bang, bang + 1); // the ! belongs to links/embeds, not spans
|
|
283
334
|
}
|
|
284
335
|
if (frames.length > 1 && top(frames).name === name) {
|
|
285
336
|
const frame = frames.pop()!;
|
|
286
|
-
const children = normalize(revertDelims(frame.children, frame.delims));
|
|
287
|
-
top(frames).children.push(
|
|
337
|
+
const children = normalize(revertDelims(frame.children, frame.delims, P), P);
|
|
338
|
+
top(frames).children.push(
|
|
339
|
+
mk(P, { type: "span", name: frame.name, attrs: frame.attrs, children }, frame.openerCp, k + 1),
|
|
340
|
+
);
|
|
288
341
|
} else {
|
|
289
342
|
// Well-formed but mismatched or orphan: the characters back.
|
|
290
|
-
pushStr(frames, `[/${name}]
|
|
343
|
+
pushStr(frames, `[/${name}]`, P, open, k + 1);
|
|
291
344
|
}
|
|
292
345
|
return k + 1;
|
|
293
346
|
}
|
|
@@ -299,10 +352,10 @@ function bracket(
|
|
|
299
352
|
const opener = parseSpanOpener(interior);
|
|
300
353
|
if (opener !== null) {
|
|
301
354
|
if (embed) {
|
|
302
|
-
pushStr(frames, "!"); // the ! belongs to links/embeds, not spans
|
|
355
|
+
pushStr(frames, "!", P, bang, bang + 1); // the ! belongs to links/embeds, not spans
|
|
303
356
|
}
|
|
304
357
|
if (base + totalDepth(frames) >= MAX_INLINE_DEPTH) {
|
|
305
|
-
pushStr(frames, chars.slice(open, k + 1).join(""));
|
|
358
|
+
pushStr(frames, chars.slice(open, k + 1).join(""), P, open, k + 1);
|
|
306
359
|
} else {
|
|
307
360
|
frames.push({
|
|
308
361
|
openerRaw: chars.slice(open, k + 1).join(""),
|
|
@@ -310,6 +363,8 @@ function bracket(
|
|
|
310
363
|
attrs: opener.attrs,
|
|
311
364
|
children: [],
|
|
312
365
|
delims: [],
|
|
366
|
+
openerCp: open,
|
|
367
|
+
openerCpEnd: k + 1,
|
|
313
368
|
});
|
|
314
369
|
}
|
|
315
370
|
return k + 1;
|
|
@@ -407,21 +462,28 @@ function resolveEscapes(s: string): string {
|
|
|
407
462
|
|
|
408
463
|
/** An unclosed span reverts: its opener text, then its children, flattened
|
|
409
464
|
* into the parent (its own unmatched delimiters reverting first). */
|
|
410
|
-
function revertFrame(frame: Frame): Node[] {
|
|
411
|
-
const out: Node[] = [
|
|
412
|
-
|
|
465
|
+
function revertFrame(frame: Frame, P: PosCtx | null): Node[] {
|
|
466
|
+
const out: Node[] = [
|
|
467
|
+
mk(P, { type: "text", value: frame.openerRaw }, frame.openerCp, frame.openerCpEnd),
|
|
468
|
+
];
|
|
469
|
+
out.push(...revertDelims(frame.children, frame.delims, P));
|
|
413
470
|
return out;
|
|
414
471
|
}
|
|
415
472
|
|
|
416
|
-
function revertDelims(children: Node[], delims: Delim[]): Node[] {
|
|
473
|
+
function revertDelims(children: Node[], delims: Delim[], P: PosCtx | null): Node[] {
|
|
417
474
|
for (let d = delims.length - 1; d >= 0; d -= 1) {
|
|
418
|
-
|
|
475
|
+
const delim = delims[d]!;
|
|
476
|
+
children.splice(
|
|
477
|
+
delim.idx,
|
|
478
|
+
0,
|
|
479
|
+
mk(P, { type: "text", value: delim.raw }, delim.cp, delim.cp + delim.raw.length),
|
|
480
|
+
);
|
|
419
481
|
}
|
|
420
482
|
return children;
|
|
421
483
|
}
|
|
422
484
|
|
|
423
485
|
/** Canonical text: adjacent literals merge, empty text nodes vanish. */
|
|
424
|
-
function normalize(children: Node[]): Node[] {
|
|
486
|
+
function normalize(children: Node[], P: PosCtx | null): Node[] {
|
|
425
487
|
const out: Node[] = [];
|
|
426
488
|
for (const node of children) {
|
|
427
489
|
if (node.type === "text") {
|
|
@@ -431,6 +493,13 @@ function normalize(children: Node[]): Node[] {
|
|
|
431
493
|
const last = out[out.length - 1];
|
|
432
494
|
if (last !== undefined && last.type === "text") {
|
|
433
495
|
last.value += node.value;
|
|
496
|
+
if (P !== null) {
|
|
497
|
+
const lastSpan = P.spans.get(last);
|
|
498
|
+
const nodeSpan = P.spans.get(node);
|
|
499
|
+
if (lastSpan !== undefined && nodeSpan !== undefined) {
|
|
500
|
+
lastSpan.end = Math.max(lastSpan.end, nodeSpan.end);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
434
503
|
continue;
|
|
435
504
|
}
|
|
436
505
|
}
|