@cube-drone/marquee-parser 0.3.0 → 0.5.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/dist/inlines.js
CHANGED
|
@@ -5,12 +5,17 @@
|
|
|
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
|
import { isName, nameLen, parseAttrs, parseValue, utf8Len } from "./attrs.js";
|
|
9
14
|
import { MAX_TARGET_BYTES } from "./blocks.js";
|
|
10
15
|
export const MAX_INLINE_DEPTH = 16;
|
|
11
16
|
export const MAX_EMOJI_SLUG_BYTES = 64;
|
|
12
17
|
function rootFrame() {
|
|
13
|
-
return { openerRaw: "", name: "", attrs: {}, children: [], delims: [] };
|
|
18
|
+
return { openerRaw: "", name: "", attrs: {}, children: [], delims: [], openerCp: 0, openerCpEnd: 0 };
|
|
14
19
|
}
|
|
15
20
|
const ASCII_PUNCT = new Set("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~");
|
|
16
21
|
/** Grammar whitespace is ASCII only (SPEC.md, front-door normalization):
|
|
@@ -21,13 +26,20 @@ function isWs(c) {
|
|
|
21
26
|
function isSlugChar(c) {
|
|
22
27
|
return /^[a-z0-9_+-]$/.test(c);
|
|
23
28
|
}
|
|
24
|
-
export function parseInlines(text) {
|
|
25
|
-
return parseInlinesAt(text, 0);
|
|
29
|
+
export function parseInlines(text, P = null) {
|
|
30
|
+
return parseInlinesAt(text, 0, P);
|
|
31
|
+
}
|
|
32
|
+
/** Record a node's span from code-point range; returns the node. */
|
|
33
|
+
function mk(P, node, cpStart, cpEnd) {
|
|
34
|
+
if (P !== null) {
|
|
35
|
+
P.spans.set(node, { start: P.map[cpStart], end: P.map[cpEnd] });
|
|
36
|
+
}
|
|
37
|
+
return node;
|
|
26
38
|
}
|
|
27
39
|
/** `base` is the inline depth already spent by enclosing link text: spans,
|
|
28
40
|
* delimiters, and link nesting share the one <= 16 cap (a per-construct cap
|
|
29
41
|
* would let composition multiply past it). */
|
|
30
|
-
function parseInlinesAt(text, base) {
|
|
42
|
+
function parseInlinesAt(text, base, P) {
|
|
31
43
|
const chars = Array.from(text);
|
|
32
44
|
const frames = [rootFrame()];
|
|
33
45
|
let i = 0;
|
|
@@ -36,15 +48,15 @@ function parseInlinesAt(text, base) {
|
|
|
36
48
|
if (c === "\\") {
|
|
37
49
|
const n = chars[i + 1];
|
|
38
50
|
if (n === "\n") {
|
|
39
|
-
top(frames).children.push({ type: "hard_break" });
|
|
51
|
+
top(frames).children.push(mk(P, { type: "hard_break" }, i, i + 2));
|
|
40
52
|
i += 2;
|
|
41
53
|
}
|
|
42
54
|
else if (n !== undefined && ASCII_PUNCT.has(n)) {
|
|
43
|
-
pushStr(frames, n);
|
|
55
|
+
pushStr(frames, n, P, i, i + 2);
|
|
44
56
|
i += 2;
|
|
45
57
|
}
|
|
46
58
|
else {
|
|
47
|
-
pushStr(frames, "\\");
|
|
59
|
+
pushStr(frames, "\\", P, i, i + 1);
|
|
48
60
|
i += 1;
|
|
49
61
|
}
|
|
50
62
|
}
|
|
@@ -52,19 +64,19 @@ function parseInlinesAt(text, base) {
|
|
|
52
64
|
const n = runLen(chars, i, "`");
|
|
53
65
|
const k = findBacktickCloser(chars, i + n, n);
|
|
54
66
|
if (k !== null) {
|
|
55
|
-
top(frames).children.push({ type: "code_span", text: chars.slice(i + n, k).join("") });
|
|
67
|
+
top(frames).children.push(mk(P, { type: "code_span", text: chars.slice(i + n, k).join("") }, i, k + n));
|
|
56
68
|
i = k + n;
|
|
57
69
|
}
|
|
58
70
|
else {
|
|
59
|
-
pushStr(frames, "`".repeat(n));
|
|
71
|
+
pushStr(frames, "`".repeat(n), P, i, i + n);
|
|
60
72
|
i += n;
|
|
61
73
|
}
|
|
62
74
|
}
|
|
63
75
|
else if (c === "[") {
|
|
64
|
-
i = bracket(chars, i, false, frames, base);
|
|
76
|
+
i = bracket(chars, i, false, frames, base, P);
|
|
65
77
|
}
|
|
66
78
|
else if (c === "!" && chars[i + 1] === "[") {
|
|
67
|
-
i = bracket(chars, i + 1, true, frames, base);
|
|
79
|
+
i = bracket(chars, i + 1, true, frames, base, P);
|
|
68
80
|
}
|
|
69
81
|
else if (c === ":") {
|
|
70
82
|
let k = i + 1;
|
|
@@ -73,54 +85,54 @@ function parseInlinesAt(text, base) {
|
|
|
73
85
|
}
|
|
74
86
|
const slugLen = k - (i + 1);
|
|
75
87
|
if (slugLen >= 1 && slugLen <= MAX_EMOJI_SLUG_BYTES && chars[k] === ":") {
|
|
76
|
-
top(frames).children.push({ type: "emoji", slug: chars.slice(i + 1, k).join("") });
|
|
88
|
+
top(frames).children.push(mk(P, { type: "emoji", slug: chars.slice(i + 1, k).join("") }, i, k + 1));
|
|
77
89
|
i = k + 1;
|
|
78
90
|
}
|
|
79
91
|
else {
|
|
80
|
-
pushStr(frames, ":");
|
|
92
|
+
pushStr(frames, ":", P, i, i + 1);
|
|
81
93
|
i += 1;
|
|
82
94
|
}
|
|
83
95
|
}
|
|
84
96
|
else if (c === "*") {
|
|
85
97
|
const n = runLen(chars, i, "*");
|
|
86
98
|
if (n === 1) {
|
|
87
|
-
i = delimiter(chars, i, n, "em", "*", frames, base);
|
|
99
|
+
i = delimiter(chars, i, n, "em", "*", frames, base, P);
|
|
88
100
|
}
|
|
89
101
|
else if (n === 2) {
|
|
90
|
-
i = delimiter(chars, i, n, "strong", "**", frames, base);
|
|
102
|
+
i = delimiter(chars, i, n, "strong", "**", frames, base, P);
|
|
91
103
|
}
|
|
92
104
|
else {
|
|
93
|
-
pushStr(frames, "*".repeat(n));
|
|
105
|
+
pushStr(frames, "*".repeat(n), P, i, i + n);
|
|
94
106
|
i += n;
|
|
95
107
|
}
|
|
96
108
|
}
|
|
97
109
|
else if (c === "~") {
|
|
98
110
|
const n = runLen(chars, i, "~");
|
|
99
111
|
if (n === 2) {
|
|
100
|
-
i = delimiter(chars, i, n, "strike", "~~", frames, base);
|
|
112
|
+
i = delimiter(chars, i, n, "strike", "~~", frames, base, P);
|
|
101
113
|
}
|
|
102
114
|
else {
|
|
103
|
-
pushStr(frames, "~".repeat(n));
|
|
115
|
+
pushStr(frames, "~".repeat(n), P, i, i + n);
|
|
104
116
|
i += n;
|
|
105
117
|
}
|
|
106
118
|
}
|
|
107
119
|
else {
|
|
108
|
-
pushStr(frames, c);
|
|
120
|
+
pushStr(frames, c, P, i, i + 1);
|
|
109
121
|
i += 1;
|
|
110
122
|
}
|
|
111
123
|
}
|
|
112
124
|
// Container end: unclosed spans and delimiters revert to literal text.
|
|
113
125
|
while (frames.length > 1) {
|
|
114
126
|
const frame = frames.pop();
|
|
115
|
-
top(frames).children.push(...revertFrame(frame));
|
|
127
|
+
top(frames).children.push(...revertFrame(frame, P));
|
|
116
128
|
}
|
|
117
129
|
const root = frames.pop();
|
|
118
|
-
return normalize(revertDelims(root.children, root.delims));
|
|
130
|
+
return normalize(revertDelims(root.children, root.delims, P), P);
|
|
119
131
|
}
|
|
120
132
|
function top(frames) {
|
|
121
133
|
return frames[frames.length - 1];
|
|
122
134
|
}
|
|
123
|
-
function pushStr(frames, s) {
|
|
135
|
+
function pushStr(frames, s, P, cpStart, cpEnd) {
|
|
124
136
|
const frame = top(frames);
|
|
125
137
|
// An open delimiter sits (invisibly, until it closes) at its recorded
|
|
126
138
|
// index: text on its far side must not merge into text before it.
|
|
@@ -130,10 +142,16 @@ function pushStr(frames, s) {
|
|
|
130
142
|
const last = children[children.length - 1];
|
|
131
143
|
if (last.type === "text") {
|
|
132
144
|
last.value += s;
|
|
145
|
+
if (P !== null) {
|
|
146
|
+
const span = P.spans.get(last);
|
|
147
|
+
if (span !== undefined) {
|
|
148
|
+
span.end = P.map[cpEnd];
|
|
149
|
+
}
|
|
150
|
+
}
|
|
133
151
|
return;
|
|
134
152
|
}
|
|
135
153
|
}
|
|
136
|
-
children.push({ type: "text", value: s });
|
|
154
|
+
children.push(mk(P, { type: "text", value: s }, cpStart, cpEnd));
|
|
137
155
|
}
|
|
138
156
|
function runLen(chars, i, c) {
|
|
139
157
|
let n = 0;
|
|
@@ -165,7 +183,7 @@ function totalDepth(frames) {
|
|
|
165
183
|
}
|
|
166
184
|
return depth;
|
|
167
185
|
}
|
|
168
|
-
function delimiter(chars, i, n, kind, raw, frames, base) {
|
|
186
|
+
function delimiter(chars, i, n, kind, raw, frames, base, P) {
|
|
169
187
|
const canClose = i > 0 && !isWs(chars[i - 1]);
|
|
170
188
|
const next = chars[i + n];
|
|
171
189
|
const canOpen = next !== undefined && !isWs(next);
|
|
@@ -174,27 +192,27 @@ function delimiter(chars, i, n, kind, raw, frames, base) {
|
|
|
174
192
|
const innermost = frame.delims[frame.delims.length - 1];
|
|
175
193
|
if (canClose && innermost !== undefined && innermost.kind === kind) {
|
|
176
194
|
frame.delims.pop();
|
|
177
|
-
const inner = normalize(frame.children.splice(innermost.idx));
|
|
178
|
-
frame.children.push(kind === "em"
|
|
195
|
+
const inner = normalize(frame.children.splice(innermost.idx), P);
|
|
196
|
+
frame.children.push(mk(P, kind === "em"
|
|
179
197
|
? { type: "emphasis", children: inner }
|
|
180
198
|
: kind === "strong"
|
|
181
199
|
? { type: "strong", children: inner }
|
|
182
|
-
: { type: "strikethrough", children: inner });
|
|
200
|
+
: { type: "strikethrough", children: inner }, innermost.cp, i + n));
|
|
183
201
|
}
|
|
184
202
|
else if (canOpen && !deep) {
|
|
185
|
-
frame.delims.push({ kind, idx: frame.children.length, raw });
|
|
203
|
+
frame.delims.push({ kind, idx: frame.children.length, raw, cp: i });
|
|
186
204
|
}
|
|
187
205
|
else {
|
|
188
|
-
pushStr(frames, raw);
|
|
206
|
+
pushStr(frames, raw, P, i, i + n);
|
|
189
207
|
}
|
|
190
208
|
return i + n;
|
|
191
209
|
}
|
|
192
210
|
/** Handle a bracket construct starting at `chars[open]` (which is `[`).
|
|
193
211
|
* `embed` means a `!` sits just before it. Returns the new position. */
|
|
194
|
-
function bracket(chars, open, embed, frames, base) {
|
|
212
|
+
function bracket(chars, open, embed, frames, base, P) {
|
|
195
213
|
const bang = embed ? open - 1 : open;
|
|
196
214
|
const fallback = () => {
|
|
197
|
-
pushStr(frames, chars[bang]);
|
|
215
|
+
pushStr(frames, chars[bang], P, bang, bang + 1);
|
|
198
216
|
return bang + 1;
|
|
199
217
|
};
|
|
200
218
|
// Find the matching `]` (balanced, escape-aware).
|
|
@@ -230,9 +248,12 @@ function bracket(chars, open, embed, frames, base) {
|
|
|
230
248
|
const lexed = lexTarget(chars, k + 2);
|
|
231
249
|
if (lexed !== null) {
|
|
232
250
|
const depth = base + totalDepth(frames) + 1;
|
|
233
|
-
|
|
251
|
+
// The link text's own positions: the interior is a contiguous slice
|
|
252
|
+
// of this logical text, so its map is a slice of ours.
|
|
253
|
+
const subP = P === null ? null : { map: P.map.slice(open + 1, k + 1), spans: P.spans };
|
|
254
|
+
top(frames).children.push(mk(P, embed
|
|
234
255
|
? { type: "embed", target: lexed.target, alt: resolveEscapes(interior) }
|
|
235
|
-
: { type: "link", target: lexed.target, children: parseInlinesAt(interior, depth) });
|
|
256
|
+
: { type: "link", target: lexed.target, children: parseInlinesAt(interior, depth, subP) }, bang, lexed.end));
|
|
236
257
|
return lexed.end;
|
|
237
258
|
}
|
|
238
259
|
return fallback();
|
|
@@ -242,16 +263,16 @@ function bracket(chars, open, embed, frames, base) {
|
|
|
242
263
|
const name = interior.slice(1);
|
|
243
264
|
if (isName(name)) {
|
|
244
265
|
if (embed) {
|
|
245
|
-
pushStr(frames, "!"); // the ! belongs to links/embeds, not spans
|
|
266
|
+
pushStr(frames, "!", P, bang, bang + 1); // the ! belongs to links/embeds, not spans
|
|
246
267
|
}
|
|
247
268
|
if (frames.length > 1 && top(frames).name === name) {
|
|
248
269
|
const frame = frames.pop();
|
|
249
|
-
const children = normalize(revertDelims(frame.children, frame.delims));
|
|
250
|
-
top(frames).children.push({ type: "span", name: frame.name, attrs: frame.attrs, children });
|
|
270
|
+
const children = normalize(revertDelims(frame.children, frame.delims, P), P);
|
|
271
|
+
top(frames).children.push(mk(P, { type: "span", name: frame.name, attrs: frame.attrs, children }, frame.openerCp, k + 1));
|
|
251
272
|
}
|
|
252
273
|
else {
|
|
253
274
|
// Well-formed but mismatched or orphan: the characters back.
|
|
254
|
-
pushStr(frames, `[/${name}]
|
|
275
|
+
pushStr(frames, `[/${name}]`, P, open, k + 1);
|
|
255
276
|
}
|
|
256
277
|
return k + 1;
|
|
257
278
|
}
|
|
@@ -262,10 +283,10 @@ function bracket(chars, open, embed, frames, base) {
|
|
|
262
283
|
const opener = parseSpanOpener(interior);
|
|
263
284
|
if (opener !== null) {
|
|
264
285
|
if (embed) {
|
|
265
|
-
pushStr(frames, "!"); // the ! belongs to links/embeds, not spans
|
|
286
|
+
pushStr(frames, "!", P, bang, bang + 1); // the ! belongs to links/embeds, not spans
|
|
266
287
|
}
|
|
267
288
|
if (base + totalDepth(frames) >= MAX_INLINE_DEPTH) {
|
|
268
|
-
pushStr(frames, chars.slice(open, k + 1).join(""));
|
|
289
|
+
pushStr(frames, chars.slice(open, k + 1).join(""), P, open, k + 1);
|
|
269
290
|
}
|
|
270
291
|
else {
|
|
271
292
|
frames.push({
|
|
@@ -274,6 +295,8 @@ function bracket(chars, open, embed, frames, base) {
|
|
|
274
295
|
attrs: opener.attrs,
|
|
275
296
|
children: [],
|
|
276
297
|
delims: [],
|
|
298
|
+
openerCp: open,
|
|
299
|
+
openerCpEnd: k + 1,
|
|
277
300
|
});
|
|
278
301
|
}
|
|
279
302
|
return k + 1;
|
|
@@ -369,19 +392,22 @@ function resolveEscapes(s) {
|
|
|
369
392
|
}
|
|
370
393
|
/** An unclosed span reverts: its opener text, then its children, flattened
|
|
371
394
|
* into the parent (its own unmatched delimiters reverting first). */
|
|
372
|
-
function revertFrame(frame) {
|
|
373
|
-
const out = [
|
|
374
|
-
|
|
395
|
+
function revertFrame(frame, P) {
|
|
396
|
+
const out = [
|
|
397
|
+
mk(P, { type: "text", value: frame.openerRaw }, frame.openerCp, frame.openerCpEnd),
|
|
398
|
+
];
|
|
399
|
+
out.push(...revertDelims(frame.children, frame.delims, P));
|
|
375
400
|
return out;
|
|
376
401
|
}
|
|
377
|
-
function revertDelims(children, delims) {
|
|
402
|
+
function revertDelims(children, delims, P) {
|
|
378
403
|
for (let d = delims.length - 1; d >= 0; d -= 1) {
|
|
379
|
-
|
|
404
|
+
const delim = delims[d];
|
|
405
|
+
children.splice(delim.idx, 0, mk(P, { type: "text", value: delim.raw }, delim.cp, delim.cp + delim.raw.length));
|
|
380
406
|
}
|
|
381
407
|
return children;
|
|
382
408
|
}
|
|
383
409
|
/** Canonical text: adjacent literals merge, empty text nodes vanish. */
|
|
384
|
-
function normalize(children) {
|
|
410
|
+
function normalize(children, P) {
|
|
385
411
|
const out = [];
|
|
386
412
|
for (const node of children) {
|
|
387
413
|
if (node.type === "text") {
|
|
@@ -391,6 +417,13 @@ function normalize(children) {
|
|
|
391
417
|
const last = out[out.length - 1];
|
|
392
418
|
if (last !== undefined && last.type === "text") {
|
|
393
419
|
last.value += node.value;
|
|
420
|
+
if (P !== null) {
|
|
421
|
+
const lastSpan = P.spans.get(last);
|
|
422
|
+
const nodeSpan = P.spans.get(node);
|
|
423
|
+
if (lastSpan !== undefined && nodeSpan !== undefined) {
|
|
424
|
+
lastSpan.end = Math.max(lastSpan.end, nodeSpan.end);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
394
427
|
continue;
|
|
395
428
|
}
|
|
396
429
|
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cube-drone/marquee-parser",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Reference parser for the Marquee markup language",
|
|
5
5
|
"license": "MPL-2.0",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "git+https://github.com/cube-drone/marqueemarkup.git",
|
|
9
|
-
"directory": "ts/parser"
|
|
9
|
+
"directory": "ts/marquee-parser"
|
|
10
10
|
},
|
|
11
11
|
"type": "module",
|
|
12
12
|
"engines": {
|