@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/blocks.ts
CHANGED
|
@@ -1,15 +1,31 @@
|
|
|
1
1
|
// Line-oriented block parser: a faithful port of rust/parser/src/blocks.rs.
|
|
2
2
|
// Where this file and that file disagree, one of them has a conformance bug.
|
|
3
|
+
//
|
|
4
|
+
// Positions (SPEC.md, "Source positions"): when a PosTracker rides along,
|
|
5
|
+
// every node gets a [start, end) span - UTF-16 offsets into the normalized
|
|
6
|
+
// source - recorded in the side-table. The AST itself never changes; the
|
|
7
|
+
// default path pays nothing.
|
|
3
8
|
|
|
4
9
|
import type { Node, Reason } from "./ast.ts";
|
|
5
10
|
import { isName, nameLen, parseAttrs, utf8Len } from "./attrs.ts";
|
|
6
|
-
import { parseInlines } from "./inlines.ts";
|
|
11
|
+
import { parseInlines, type PosCtx } from "./inlines.ts";
|
|
7
12
|
|
|
8
13
|
export const MAX_LIST_DEPTH = 16;
|
|
9
14
|
export const MAX_QUOTE_DEPTH = 16;
|
|
10
15
|
export const MAX_DIRECTIVE_DEPTH = 8;
|
|
11
16
|
export const MAX_TARGET_BYTES = 4096;
|
|
12
17
|
|
|
18
|
+
/** A node's source extent: [start, end) in UTF-16 code units over the
|
|
19
|
+
* front-door-normalized source (shebang line included in the count). */
|
|
20
|
+
export interface Span {
|
|
21
|
+
start: number;
|
|
22
|
+
end: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface PosTracker {
|
|
26
|
+
spans: WeakMap<Node, Span>;
|
|
27
|
+
}
|
|
28
|
+
|
|
13
29
|
interface Ctx {
|
|
14
30
|
dirDepth: number;
|
|
15
31
|
listDepth: number;
|
|
@@ -18,6 +34,9 @@ interface Ctx {
|
|
|
18
34
|
|
|
19
35
|
interface Cursor {
|
|
20
36
|
lines: string[];
|
|
37
|
+
/** Parallel to `lines`: source offset of each line's first character
|
|
38
|
+
* (post any container prefix-stripping). Null when positions are off. */
|
|
39
|
+
starts: number[] | null;
|
|
21
40
|
pos: number;
|
|
22
41
|
}
|
|
23
42
|
|
|
@@ -25,15 +44,68 @@ function peek(cur: Cursor): string | undefined {
|
|
|
25
44
|
return cur.lines[cur.pos];
|
|
26
45
|
}
|
|
27
46
|
|
|
28
|
-
export function parseBlocks(body: string): Node[] {
|
|
47
|
+
export function parseBlocks(body: string, T: PosTracker | null = null, baseOffset = 0): Node[] {
|
|
29
48
|
const lines = body.split("\n");
|
|
49
|
+
let starts: number[] | null = null;
|
|
50
|
+
if (T !== null) {
|
|
51
|
+
starts = [];
|
|
52
|
+
let off = baseOffset;
|
|
53
|
+
for (const line of lines) {
|
|
54
|
+
starts.push(off);
|
|
55
|
+
off += line.length + 1;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
30
58
|
// A final newline terminates the last line rather than creating an empty
|
|
31
59
|
// one (otherwise every trailing newline leaks a blank into fences).
|
|
32
60
|
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
33
61
|
lines.pop();
|
|
62
|
+
starts?.pop();
|
|
63
|
+
}
|
|
64
|
+
const cur: Cursor = { lines, starts, pos: 0 };
|
|
65
|
+
return parseContainer(cur, { dirDepth: 0, listDepth: 0, quoteDepth: 0 }, null, T);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function lineStartOf(cur: Cursor, i: number): number {
|
|
69
|
+
return cur.starts![i]!;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function lineEndOf(cur: Cursor, i: number): number {
|
|
73
|
+
return cur.starts![i]! + cur.lines[i]!.length;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function mark(T: PosTracker | null, cur: Cursor, node: Node, startLine: number, endLine: number): Node {
|
|
77
|
+
if (T !== null && cur.starts !== null) {
|
|
78
|
+
T.spans.set(node, { start: lineStartOf(cur, startLine), end: lineEndOf(cur, endLine) });
|
|
79
|
+
}
|
|
80
|
+
return node;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Build the logical inline text (segments joined by real source newlines)
|
|
84
|
+
* plus the code-point -> source-offset map the inline parser records from. */
|
|
85
|
+
function inlineCtx(
|
|
86
|
+
T: PosTracker | null,
|
|
87
|
+
segs: Array<{ text: string; start: number }>,
|
|
88
|
+
): { text: string; P: PosCtx | null } {
|
|
89
|
+
const text = segs.map((s) => s.text).join("\n");
|
|
90
|
+
if (T === null) {
|
|
91
|
+
return { text, P: null };
|
|
92
|
+
}
|
|
93
|
+
const map: number[] = [];
|
|
94
|
+
let end = 0;
|
|
95
|
+
for (let s = 0; s < segs.length; s += 1) {
|
|
96
|
+
if (s > 0) {
|
|
97
|
+
map.push(end); // the joining "\n" is the previous line's real newline
|
|
98
|
+
}
|
|
99
|
+
const seg = segs[s]!;
|
|
100
|
+
let off = seg.start;
|
|
101
|
+
for (const ch of seg.text) {
|
|
102
|
+
map.push(off);
|
|
103
|
+
off += ch.length;
|
|
104
|
+
}
|
|
105
|
+
end = off;
|
|
34
106
|
}
|
|
35
|
-
|
|
36
|
-
return
|
|
107
|
+
map.push(end);
|
|
108
|
+
return { text, P: { map, spans: T.spans } };
|
|
37
109
|
}
|
|
38
110
|
|
|
39
111
|
/**
|
|
@@ -41,7 +113,7 @@ export function parseBlocks(body: string): Node[] {
|
|
|
41
113
|
* directive whose body this is (null at document level and inside
|
|
42
114
|
* blockquotes / list items, which are fresh containers).
|
|
43
115
|
*/
|
|
44
|
-
function parseContainer(cur: Cursor, ctx: Ctx, openDir: string | null): Node[] {
|
|
116
|
+
function parseContainer(cur: Cursor, ctx: Ctx, openDir: string | null, T: PosTracker | null): Node[] {
|
|
45
117
|
const out: Node[] = [];
|
|
46
118
|
for (;;) {
|
|
47
119
|
const line = peek(cur);
|
|
@@ -55,6 +127,7 @@ function parseContainer(cur: Cursor, ctx: Ctx, openDir: string | null): Node[] {
|
|
|
55
127
|
|
|
56
128
|
// Directive open / close / error lines.
|
|
57
129
|
if (line.startsWith(":::")) {
|
|
130
|
+
const li = cur.pos;
|
|
58
131
|
cur.pos += 1;
|
|
59
132
|
const dir = classifyDirective(line);
|
|
60
133
|
if (dir.kind === "close") {
|
|
@@ -64,52 +137,76 @@ function parseContainer(cur: Cursor, ctx: Ctx, openDir: string | null): Node[] {
|
|
|
64
137
|
if (openDir !== null && dir.name === openDir) {
|
|
65
138
|
return out;
|
|
66
139
|
}
|
|
67
|
-
out.push(invalid(openDir !== null ? "mismatched_close" : "stray_close"));
|
|
140
|
+
out.push(mark(T, cur, invalid(openDir !== null ? "mismatched_close" : "stray_close"), li, li));
|
|
68
141
|
} else if (dir.kind === "open") {
|
|
69
142
|
if (ctx.dirDepth >= MAX_DIRECTIVE_DEPTH) {
|
|
70
|
-
out.push(invalid("depth_exceeded"));
|
|
143
|
+
out.push(mark(T, cur, invalid("depth_exceeded"), li, li));
|
|
71
144
|
continue;
|
|
72
145
|
}
|
|
73
146
|
const attrs = parseAttrs(dir.attrsSrc);
|
|
74
147
|
if (!attrs.ok) {
|
|
75
|
-
out.push(invalid(attrs.reason));
|
|
148
|
+
out.push(mark(T, cur, invalid(attrs.reason), li, li));
|
|
76
149
|
} else {
|
|
77
150
|
const children = dir.leaf
|
|
78
151
|
? []
|
|
79
|
-
: parseContainer(cur, { ...ctx, dirDepth: ctx.dirDepth + 1 }, dir.name);
|
|
80
|
-
|
|
152
|
+
: parseContainer(cur, { ...ctx, dirDepth: ctx.dirDepth + 1 }, dir.name, T);
|
|
153
|
+
// A container's close line (or EOF auto-close) has been consumed
|
|
154
|
+
// by the child parse: the span runs to the last consumed line.
|
|
155
|
+
const endLi = dir.leaf ? li : Math.max(li, cur.pos - 1);
|
|
156
|
+
out.push(
|
|
157
|
+
mark(T, cur, { type: "directive", name: dir.name, attrs: attrs.attrs, children }, li, endLi),
|
|
158
|
+
);
|
|
81
159
|
}
|
|
82
160
|
} else {
|
|
83
|
-
out.push(invalid(dir.reason));
|
|
161
|
+
out.push(mark(T, cur, invalid(dir.reason), li, li));
|
|
84
162
|
}
|
|
85
163
|
continue;
|
|
86
164
|
}
|
|
87
165
|
|
|
88
166
|
// Comment block: consecutive `%%` lines, raw content.
|
|
89
167
|
if (line.startsWith("%%")) {
|
|
168
|
+
const li = cur.pos;
|
|
90
169
|
const texts: string[] = [];
|
|
91
170
|
for (let l = peek(cur); l !== undefined && l.startsWith("%%"); l = peek(cur)) {
|
|
92
171
|
const rest = l.slice(2);
|
|
93
172
|
texts.push(rest.startsWith(" ") ? rest.slice(1) : rest);
|
|
94
173
|
cur.pos += 1;
|
|
95
174
|
}
|
|
96
|
-
out.push({ type: "comment", text: texts.join("\n") });
|
|
175
|
+
out.push(mark(T, cur, { type: "comment", text: texts.join("\n") }, li, cur.pos - 1));
|
|
97
176
|
continue;
|
|
98
177
|
}
|
|
99
178
|
|
|
100
179
|
const heading = headingLine(line);
|
|
101
180
|
if (heading !== null) {
|
|
181
|
+
const li = cur.pos;
|
|
102
182
|
cur.pos += 1;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
183
|
+
const content = trimSpaceTab(heading.content);
|
|
184
|
+
let P: PosCtx | null = null;
|
|
185
|
+
let text = content;
|
|
186
|
+
if (T !== null && cur.starts !== null) {
|
|
187
|
+
// Content offset: past the #s and their space, plus leading trim
|
|
188
|
+
// (all ASCII, so char counts are UTF-16 counts).
|
|
189
|
+
const lead = heading.content.length - heading.content.replace(/^[ \t]+/, "").length;
|
|
190
|
+
const start = lineStartOf(cur, li) + heading.level + 1 + lead;
|
|
191
|
+
const built = inlineCtx(T, [{ text: content, start }]);
|
|
192
|
+
text = built.text;
|
|
193
|
+
P = built.P;
|
|
194
|
+
}
|
|
195
|
+
out.push(
|
|
196
|
+
mark(
|
|
197
|
+
T,
|
|
198
|
+
cur,
|
|
199
|
+
{ type: "heading", level: heading.level, children: parseInlines(text, P) },
|
|
200
|
+
li,
|
|
201
|
+
li,
|
|
202
|
+
),
|
|
203
|
+
);
|
|
108
204
|
continue;
|
|
109
205
|
}
|
|
110
206
|
|
|
111
207
|
const fence = fenceOpen(line);
|
|
112
208
|
if (fence !== null) {
|
|
209
|
+
const li = cur.pos;
|
|
113
210
|
cur.pos += 1;
|
|
114
211
|
const content: string[] = [];
|
|
115
212
|
for (let l = peek(cur); l !== undefined; l = peek(cur)) {
|
|
@@ -121,43 +218,63 @@ function parseContainer(cur: Cursor, ctx: Ctx, openDir: string | null): Node[] {
|
|
|
121
218
|
}
|
|
122
219
|
const text = content.join("\n");
|
|
123
220
|
out.push(
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
221
|
+
mark(
|
|
222
|
+
T,
|
|
223
|
+
cur,
|
|
224
|
+
fence.info === null
|
|
225
|
+
? { type: "code_block", text }
|
|
226
|
+
: { type: "code_block", info: fence.info, text },
|
|
227
|
+
li,
|
|
228
|
+
cur.pos - 1,
|
|
229
|
+
),
|
|
127
230
|
);
|
|
128
231
|
continue;
|
|
129
232
|
}
|
|
130
233
|
|
|
131
234
|
if (line.startsWith(">") && ctx.quoteDepth < MAX_QUOTE_DEPTH) {
|
|
235
|
+
const li = cur.pos;
|
|
132
236
|
const innerLines: string[] = [];
|
|
237
|
+
const innerStarts: number[] | null = T !== null && cur.starts !== null ? [] : null;
|
|
133
238
|
for (let l = peek(cur); l !== undefined && l.startsWith(">"); l = peek(cur)) {
|
|
134
239
|
const rest = l.slice(1);
|
|
135
|
-
|
|
240
|
+
const stripped = rest.startsWith(" ") ? 2 : 1;
|
|
241
|
+
innerLines.push(l.slice(stripped));
|
|
242
|
+
innerStarts?.push(lineStartOf(cur, cur.pos) + stripped);
|
|
136
243
|
cur.pos += 1;
|
|
137
244
|
}
|
|
138
|
-
const sub: Cursor = { lines: innerLines, pos: 0 };
|
|
139
|
-
out.push(
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
245
|
+
const sub: Cursor = { lines: innerLines, starts: innerStarts, pos: 0 };
|
|
246
|
+
out.push(
|
|
247
|
+
mark(
|
|
248
|
+
T,
|
|
249
|
+
cur,
|
|
250
|
+
{
|
|
251
|
+
type: "blockquote",
|
|
252
|
+
children: parseContainer(sub, { ...ctx, quoteDepth: ctx.quoteDepth + 1 }, null, T),
|
|
253
|
+
},
|
|
254
|
+
li,
|
|
255
|
+
cur.pos - 1,
|
|
256
|
+
),
|
|
257
|
+
);
|
|
143
258
|
continue;
|
|
144
259
|
}
|
|
145
260
|
|
|
146
261
|
if (line.replace(/[ \t]+$/, "") === "---") {
|
|
262
|
+
const li = cur.pos;
|
|
147
263
|
cur.pos += 1;
|
|
148
|
-
out.push({ type: "thematic_break" });
|
|
264
|
+
out.push(mark(T, cur, { type: "thematic_break" }, li, li));
|
|
149
265
|
continue;
|
|
150
266
|
}
|
|
151
267
|
|
|
152
268
|
if (ctx.listDepth < MAX_LIST_DEPTH) {
|
|
153
269
|
const m = marker(line);
|
|
154
270
|
if (m !== null) {
|
|
155
|
-
out.push(parseList(cur, ctx, m));
|
|
271
|
+
out.push(parseList(cur, ctx, m, T));
|
|
156
272
|
continue;
|
|
157
273
|
}
|
|
158
274
|
}
|
|
159
275
|
|
|
160
276
|
// Paragraph: plain lines until blank / container end / block start.
|
|
277
|
+
const li = cur.pos;
|
|
161
278
|
const plines = [line];
|
|
162
279
|
cur.pos += 1;
|
|
163
280
|
for (let l = peek(cur); l !== undefined; l = peek(cur)) {
|
|
@@ -170,11 +287,19 @@ function parseContainer(cur: Cursor, ctx: Ctx, openDir: string | null): Node[] {
|
|
|
170
287
|
if (plines.length === 1) {
|
|
171
288
|
const target = turbolink(plines[0]!);
|
|
172
289
|
if (target !== null) {
|
|
173
|
-
out.push({ type: "turbolink", target });
|
|
290
|
+
out.push(mark(T, cur, { type: "turbolink", target }, li, li));
|
|
174
291
|
continue;
|
|
175
292
|
}
|
|
176
293
|
}
|
|
177
|
-
|
|
294
|
+
let P: PosCtx | null = null;
|
|
295
|
+
let text = plines.join("\n");
|
|
296
|
+
if (T !== null && cur.starts !== null) {
|
|
297
|
+
const segs = plines.map((t, idx) => ({ text: t, start: lineStartOf(cur, li + idx) }));
|
|
298
|
+
const built = inlineCtx(T, segs);
|
|
299
|
+
text = built.text;
|
|
300
|
+
P = built.P;
|
|
301
|
+
}
|
|
302
|
+
out.push(mark(T, cur, { type: "paragraph", children: parseInlines(text, P) }, li, cur.pos - 1));
|
|
178
303
|
}
|
|
179
304
|
return out;
|
|
180
305
|
}
|
|
@@ -262,12 +387,20 @@ function marker(line: string): Marker | null {
|
|
|
262
387
|
|
|
263
388
|
/** One list of one kind. A same-column marker of the other kind ends this
|
|
264
389
|
* list (the dispatcher immediately starts the next one). */
|
|
265
|
-
function parseList(cur: Cursor, ctx: Ctx, first: Marker): Node {
|
|
390
|
+
function parseList(cur: Cursor, ctx: Ctx, first: Marker, T: PosTracker | null): Node {
|
|
391
|
+
const tracking = T !== null && cur.starts !== null;
|
|
266
392
|
const ordered = first.ordered;
|
|
267
393
|
const items: Node[] = [];
|
|
394
|
+
const listLi = cur.pos;
|
|
395
|
+
let itemStart = tracking ? lineStartOf(cur, cur.pos) + first.indent : 0;
|
|
268
396
|
let buf: string[] = [peek(cur)!.slice(first.contentIdx)];
|
|
397
|
+
let bufStarts: number[] | null = tracking ? [lineStartOf(cur, cur.pos) + first.contentIdx] : null;
|
|
269
398
|
cur.pos += 1;
|
|
270
399
|
|
|
400
|
+
const finish = (): void => {
|
|
401
|
+
items.push(finishItem(buf, bufStarts, ctx, T, itemStart));
|
|
402
|
+
};
|
|
403
|
+
|
|
271
404
|
for (;;) {
|
|
272
405
|
const line = peek(cur);
|
|
273
406
|
if (line === undefined) {
|
|
@@ -294,6 +427,7 @@ function parseList(cur: Cursor, ctx: Ctx, first: Marker): Node {
|
|
|
294
427
|
break;
|
|
295
428
|
}
|
|
296
429
|
buf.push("");
|
|
430
|
+
bufStarts?.push(tracking ? lineStartOf(cur, cur.pos) : 0);
|
|
297
431
|
cur.pos += 1;
|
|
298
432
|
continue;
|
|
299
433
|
}
|
|
@@ -301,29 +435,53 @@ function parseList(cur: Cursor, ctx: Ctx, first: Marker): Node {
|
|
|
301
435
|
// Content (or a deeper marker) inside the current item; strip the
|
|
302
436
|
// content column. Off-grid extra spaces ride along (floor rule).
|
|
303
437
|
buf.push(line.slice(2));
|
|
438
|
+
bufStarts?.push(tracking ? lineStartOf(cur, cur.pos) + 2 : 0);
|
|
304
439
|
cur.pos += 1;
|
|
305
440
|
continue;
|
|
306
441
|
}
|
|
307
442
|
// Column 0 or 1 (floors to 0).
|
|
308
443
|
const m = marker(line);
|
|
309
444
|
if (m !== null && m.ordered === ordered) {
|
|
310
|
-
|
|
445
|
+
finish();
|
|
446
|
+
itemStart = tracking ? lineStartOf(cur, cur.pos) + m.indent : 0;
|
|
311
447
|
buf = [line.slice(m.contentIdx)];
|
|
448
|
+
bufStarts = tracking ? [lineStartOf(cur, cur.pos) + m.contentIdx] : null;
|
|
312
449
|
cur.pos += 1;
|
|
313
450
|
continue;
|
|
314
451
|
}
|
|
315
452
|
break; // column-0 block, or a kind switch
|
|
316
453
|
}
|
|
317
|
-
|
|
318
|
-
|
|
454
|
+
finish();
|
|
455
|
+
const list: Node = { type: "list", ordered, children: items };
|
|
456
|
+
if (tracking) {
|
|
457
|
+
T!.spans.set(list, {
|
|
458
|
+
start: lineStartOf(cur, listLi) + first.indent,
|
|
459
|
+
end: lineEndOf(cur, cur.pos - 1),
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
return list;
|
|
319
463
|
}
|
|
320
464
|
|
|
321
|
-
function finishItem(
|
|
322
|
-
|
|
323
|
-
|
|
465
|
+
function finishItem(
|
|
466
|
+
buf: string[],
|
|
467
|
+
bufStarts: number[] | null,
|
|
468
|
+
ctx: Ctx,
|
|
469
|
+
T: PosTracker | null,
|
|
470
|
+
itemStart: number,
|
|
471
|
+
): Node {
|
|
472
|
+
const sub: Cursor = { lines: buf, starts: bufStarts, pos: 0 };
|
|
473
|
+
const item: Node = {
|
|
324
474
|
type: "list_item",
|
|
325
|
-
children: parseContainer(sub, { ...ctx, listDepth: ctx.listDepth + 1 }, null),
|
|
475
|
+
children: parseContainer(sub, { ...ctx, listDepth: ctx.listDepth + 1 }, null, T),
|
|
326
476
|
};
|
|
477
|
+
if (T !== null && bufStarts !== null) {
|
|
478
|
+
let end = itemStart;
|
|
479
|
+
for (let i = 0; i < buf.length; i += 1) {
|
|
480
|
+
end = Math.max(end, bufStarts[i]! + buf[i]!.length);
|
|
481
|
+
}
|
|
482
|
+
T.spans.set(item, { start: itemStart, end });
|
|
483
|
+
}
|
|
484
|
+
return item;
|
|
327
485
|
}
|
|
328
486
|
|
|
329
487
|
type DirLine =
|
package/src/index.ts
CHANGED
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
// unknown dialect version (SPEC.md, "Conformance"), surfaced as a thrown
|
|
5
5
|
// error rather than a guessed parse.
|
|
6
6
|
|
|
7
|
-
import { parseBlocks } from "./blocks.ts";
|
|
7
|
+
import { parseBlocks, type PosTracker, type Span } from "./blocks.ts";
|
|
8
8
|
import type { Node } from "./ast.ts";
|
|
9
9
|
|
|
10
10
|
export type { Attrs, Node, Reason } from "./ast.ts";
|
|
11
|
+
export type { Span } from "./blocks.ts";
|
|
11
12
|
|
|
12
13
|
export class UnsupportedVersionError extends Error {
|
|
13
14
|
readonly version: number;
|
|
@@ -31,6 +32,39 @@ export function parse(input: string): Node {
|
|
|
31
32
|
return { type: "document", version, children: parseBlocks(body) };
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
/** parse() plus source positions - the editor-tooling extra, deliberately
|
|
36
|
+
* OUTSIDE the AST wire contract (SPEC.md, "Source positions"):
|
|
37
|
+
*
|
|
38
|
+
* - `doc` is byte-identical to what parse() returns: nodes carry no
|
|
39
|
+
* position fields, so serialization, vectors, and goldens are untouched.
|
|
40
|
+
* - `spans` maps each node object to its [start, end) source extent, in
|
|
41
|
+
* UTF-16 code units (what JavaScript strings and CodeMirror speak) over
|
|
42
|
+
* `source` - the front-door-NORMALIZED input (\r\n -> \n; the shebang
|
|
43
|
+
* line, when present, is included in the offset space).
|
|
44
|
+
* - Node-level extents are the guarantee (a span covers its opener through
|
|
45
|
+
* its closer); the interiors of canonicalized text (merged literals,
|
|
46
|
+
* resolved escapes) are covered but not subdivided.
|
|
47
|
+
*/
|
|
48
|
+
export function parseWithPositions(input: string): {
|
|
49
|
+
doc: Node;
|
|
50
|
+
spans: WeakMap<Node, Span>;
|
|
51
|
+
source: string;
|
|
52
|
+
} {
|
|
53
|
+
const normalized = input.replaceAll("\r\n", "\n").replaceAll("\r", "\n");
|
|
54
|
+
const [version, body] = stripVersion(normalized);
|
|
55
|
+
if (version !== 0) {
|
|
56
|
+
throw new UnsupportedVersionError(version);
|
|
57
|
+
}
|
|
58
|
+
const tracker: PosTracker = { spans: new WeakMap() };
|
|
59
|
+
const doc: Node = {
|
|
60
|
+
type: "document",
|
|
61
|
+
version,
|
|
62
|
+
children: parseBlocks(body, tracker, normalized.length - body.length),
|
|
63
|
+
};
|
|
64
|
+
tracker.spans.set(doc, { start: 0, end: normalized.length });
|
|
65
|
+
return { doc, spans: tracker.spans, source: normalized };
|
|
66
|
+
}
|
|
67
|
+
|
|
34
68
|
/** The in-band version declaration: line 1, exactly `#!marquee <integer>`.
|
|
35
69
|
* The default is version 0, forever. */
|
|
36
70
|
function stripVersion(s: string): [number, string] {
|