@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/README.md
CHANGED
|
@@ -15,6 +15,27 @@ const doc = parse("# hello *world*\n");
|
|
|
15
15
|
version (`UnsupportedVersionError`). Malformed constructs become `invalid_directive` nodes
|
|
16
16
|
with spec'd reasons; confusing inline input degrades to literal text. Nothing is ever eaten.
|
|
17
17
|
|
|
18
|
+
**For editor tooling** there's `parseWithPositions(source)`, returning
|
|
19
|
+
`{ doc, spans, source }`: the same AST byte-for-byte (positions live in a `WeakMap`
|
|
20
|
+
side-table, never on the nodes, so serialization and the wire contract are untouched), with
|
|
21
|
+
each node mapped to its `[start, end)` extent in **UTF-16 code units** over the *normalized*
|
|
22
|
+
source (`\r\n` → `\n`; the returned `source` is the string offsets refer to — hand it, not
|
|
23
|
+
your raw input, to anything consuming the spans). A span covers its construct
|
|
24
|
+
opener-through-closer, markers included; the interiors of canonicalized text (merged
|
|
25
|
+
literals, resolved escapes) are covered but not subdivided. Built for CodeMirror
|
|
26
|
+
decorations; deliberately outside the cross-implementation conformance corpus (see SPEC.md,
|
|
27
|
+
"Source positions").
|
|
28
|
+
|
|
29
|
+
Three sharp edges, all consequences of "a span is a *source* extent":
|
|
30
|
+
|
|
31
|
+
- `slice(span) !== node.value` wherever the source is denser than the text — `a \*b\* c`
|
|
32
|
+
slices nine characters for a seven-character text node.
|
|
33
|
+
- A construct that crosses a line inside a container **contains that container's prefix**:
|
|
34
|
+
in `> *a\n> b*`, the emphasis span slices `*a\n> b*`, `> ` and all. Range *marks* are fine;
|
|
35
|
+
a *replace* decoration over such a range would swallow the quote marker, so split
|
|
36
|
+
multi-line ranges per line before replacing.
|
|
37
|
+
- Blank lines between blocks belong to no node: coverage has gaps, by design.
|
|
38
|
+
|
|
18
39
|
You probably want [`@cube-drone/marquee-markup`](https://www.npmjs.com/package/@cube-drone/marquee-markup)
|
|
19
40
|
(batteries included: parse + render + CLI) unless you're building a renderer or tool of your
|
|
20
41
|
own — in which case the AST contract is specified in
|
package/dist/blocks.d.ts
CHANGED
|
@@ -3,4 +3,13 @@ export declare const MAX_LIST_DEPTH = 16;
|
|
|
3
3
|
export declare const MAX_QUOTE_DEPTH = 16;
|
|
4
4
|
export declare const MAX_DIRECTIVE_DEPTH = 8;
|
|
5
5
|
export declare const MAX_TARGET_BYTES = 4096;
|
|
6
|
-
|
|
6
|
+
/** A node's source extent: [start, end) in UTF-16 code units over the
|
|
7
|
+
* front-door-normalized source (shebang line included in the count). */
|
|
8
|
+
export interface Span {
|
|
9
|
+
start: number;
|
|
10
|
+
end: number;
|
|
11
|
+
}
|
|
12
|
+
export interface PosTracker {
|
|
13
|
+
spans: WeakMap<Node, Span>;
|
|
14
|
+
}
|
|
15
|
+
export declare function parseBlocks(body: string, T?: PosTracker | null, baseOffset?: number): Node[];
|
package/dist/blocks.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
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
|
import { isName, nameLen, parseAttrs, utf8Len } from "./attrs.js";
|
|
4
9
|
import { parseInlines } from "./inlines.js";
|
|
5
10
|
export const MAX_LIST_DEPTH = 16;
|
|
@@ -9,22 +14,68 @@ export const MAX_TARGET_BYTES = 4096;
|
|
|
9
14
|
function peek(cur) {
|
|
10
15
|
return cur.lines[cur.pos];
|
|
11
16
|
}
|
|
12
|
-
export function parseBlocks(body) {
|
|
17
|
+
export function parseBlocks(body, T = null, baseOffset = 0) {
|
|
13
18
|
const lines = body.split("\n");
|
|
19
|
+
let starts = null;
|
|
20
|
+
if (T !== null) {
|
|
21
|
+
starts = [];
|
|
22
|
+
let off = baseOffset;
|
|
23
|
+
for (const line of lines) {
|
|
24
|
+
starts.push(off);
|
|
25
|
+
off += line.length + 1;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
14
28
|
// A final newline terminates the last line rather than creating an empty
|
|
15
29
|
// one (otherwise every trailing newline leaks a blank into fences).
|
|
16
30
|
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
17
31
|
lines.pop();
|
|
32
|
+
starts?.pop();
|
|
18
33
|
}
|
|
19
|
-
const cur = { lines, pos: 0 };
|
|
20
|
-
return parseContainer(cur, { dirDepth: 0, listDepth: 0, quoteDepth: 0 }, null);
|
|
34
|
+
const cur = { lines, starts, pos: 0 };
|
|
35
|
+
return parseContainer(cur, { dirDepth: 0, listDepth: 0, quoteDepth: 0 }, null, T);
|
|
36
|
+
}
|
|
37
|
+
function lineStartOf(cur, i) {
|
|
38
|
+
return cur.starts[i];
|
|
39
|
+
}
|
|
40
|
+
function lineEndOf(cur, i) {
|
|
41
|
+
return cur.starts[i] + cur.lines[i].length;
|
|
42
|
+
}
|
|
43
|
+
function mark(T, cur, node, startLine, endLine) {
|
|
44
|
+
if (T !== null && cur.starts !== null) {
|
|
45
|
+
T.spans.set(node, { start: lineStartOf(cur, startLine), end: lineEndOf(cur, endLine) });
|
|
46
|
+
}
|
|
47
|
+
return node;
|
|
48
|
+
}
|
|
49
|
+
/** Build the logical inline text (segments joined by real source newlines)
|
|
50
|
+
* plus the code-point -> source-offset map the inline parser records from. */
|
|
51
|
+
function inlineCtx(T, segs) {
|
|
52
|
+
const text = segs.map((s) => s.text).join("\n");
|
|
53
|
+
if (T === null) {
|
|
54
|
+
return { text, P: null };
|
|
55
|
+
}
|
|
56
|
+
const map = [];
|
|
57
|
+
let end = 0;
|
|
58
|
+
for (let s = 0; s < segs.length; s += 1) {
|
|
59
|
+
if (s > 0) {
|
|
60
|
+
map.push(end); // the joining "\n" is the previous line's real newline
|
|
61
|
+
}
|
|
62
|
+
const seg = segs[s];
|
|
63
|
+
let off = seg.start;
|
|
64
|
+
for (const ch of seg.text) {
|
|
65
|
+
map.push(off);
|
|
66
|
+
off += ch.length;
|
|
67
|
+
}
|
|
68
|
+
end = off;
|
|
69
|
+
}
|
|
70
|
+
map.push(end);
|
|
71
|
+
return { text, P: { map, spans: T.spans } };
|
|
21
72
|
}
|
|
22
73
|
/**
|
|
23
74
|
* Parse blocks until the container ends. `openDir` is the name of the
|
|
24
75
|
* directive whose body this is (null at document level and inside
|
|
25
76
|
* blockquotes / list items, which are fresh containers).
|
|
26
77
|
*/
|
|
27
|
-
function parseContainer(cur, ctx, openDir) {
|
|
78
|
+
function parseContainer(cur, ctx, openDir, T) {
|
|
28
79
|
const out = [];
|
|
29
80
|
for (;;) {
|
|
30
81
|
const line = peek(cur);
|
|
@@ -37,6 +88,7 @@ function parseContainer(cur, ctx, openDir) {
|
|
|
37
88
|
}
|
|
38
89
|
// Directive open / close / error lines.
|
|
39
90
|
if (line.startsWith(":::")) {
|
|
91
|
+
const li = cur.pos;
|
|
40
92
|
cur.pos += 1;
|
|
41
93
|
const dir = classifyDirective(line);
|
|
42
94
|
if (dir.kind === "close") {
|
|
@@ -46,52 +98,66 @@ function parseContainer(cur, ctx, openDir) {
|
|
|
46
98
|
if (openDir !== null && dir.name === openDir) {
|
|
47
99
|
return out;
|
|
48
100
|
}
|
|
49
|
-
out.push(invalid(openDir !== null ? "mismatched_close" : "stray_close"));
|
|
101
|
+
out.push(mark(T, cur, invalid(openDir !== null ? "mismatched_close" : "stray_close"), li, li));
|
|
50
102
|
}
|
|
51
103
|
else if (dir.kind === "open") {
|
|
52
104
|
if (ctx.dirDepth >= MAX_DIRECTIVE_DEPTH) {
|
|
53
|
-
out.push(invalid("depth_exceeded"));
|
|
105
|
+
out.push(mark(T, cur, invalid("depth_exceeded"), li, li));
|
|
54
106
|
continue;
|
|
55
107
|
}
|
|
56
108
|
const attrs = parseAttrs(dir.attrsSrc);
|
|
57
109
|
if (!attrs.ok) {
|
|
58
|
-
out.push(invalid(attrs.reason));
|
|
110
|
+
out.push(mark(T, cur, invalid(attrs.reason), li, li));
|
|
59
111
|
}
|
|
60
112
|
else {
|
|
61
113
|
const children = dir.leaf
|
|
62
114
|
? []
|
|
63
|
-
: parseContainer(cur, { ...ctx, dirDepth: ctx.dirDepth + 1 }, dir.name);
|
|
64
|
-
|
|
115
|
+
: parseContainer(cur, { ...ctx, dirDepth: ctx.dirDepth + 1 }, dir.name, T);
|
|
116
|
+
// A container's close line (or EOF auto-close) has been consumed
|
|
117
|
+
// by the child parse: the span runs to the last consumed line.
|
|
118
|
+
const endLi = dir.leaf ? li : Math.max(li, cur.pos - 1);
|
|
119
|
+
out.push(mark(T, cur, { type: "directive", name: dir.name, attrs: attrs.attrs, children }, li, endLi));
|
|
65
120
|
}
|
|
66
121
|
}
|
|
67
122
|
else {
|
|
68
|
-
out.push(invalid(dir.reason));
|
|
123
|
+
out.push(mark(T, cur, invalid(dir.reason), li, li));
|
|
69
124
|
}
|
|
70
125
|
continue;
|
|
71
126
|
}
|
|
72
127
|
// Comment block: consecutive `%%` lines, raw content.
|
|
73
128
|
if (line.startsWith("%%")) {
|
|
129
|
+
const li = cur.pos;
|
|
74
130
|
const texts = [];
|
|
75
131
|
for (let l = peek(cur); l !== undefined && l.startsWith("%%"); l = peek(cur)) {
|
|
76
132
|
const rest = l.slice(2);
|
|
77
133
|
texts.push(rest.startsWith(" ") ? rest.slice(1) : rest);
|
|
78
134
|
cur.pos += 1;
|
|
79
135
|
}
|
|
80
|
-
out.push({ type: "comment", text: texts.join("\n") });
|
|
136
|
+
out.push(mark(T, cur, { type: "comment", text: texts.join("\n") }, li, cur.pos - 1));
|
|
81
137
|
continue;
|
|
82
138
|
}
|
|
83
139
|
const heading = headingLine(line);
|
|
84
140
|
if (heading !== null) {
|
|
141
|
+
const li = cur.pos;
|
|
85
142
|
cur.pos += 1;
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
143
|
+
const content = trimSpaceTab(heading.content);
|
|
144
|
+
let P = null;
|
|
145
|
+
let text = content;
|
|
146
|
+
if (T !== null && cur.starts !== null) {
|
|
147
|
+
// Content offset: past the #s and their space, plus leading trim
|
|
148
|
+
// (all ASCII, so char counts are UTF-16 counts).
|
|
149
|
+
const lead = heading.content.length - heading.content.replace(/^[ \t]+/, "").length;
|
|
150
|
+
const start = lineStartOf(cur, li) + heading.level + 1 + lead;
|
|
151
|
+
const built = inlineCtx(T, [{ text: content, start }]);
|
|
152
|
+
text = built.text;
|
|
153
|
+
P = built.P;
|
|
154
|
+
}
|
|
155
|
+
out.push(mark(T, cur, { type: "heading", level: heading.level, children: parseInlines(text, P) }, li, li));
|
|
91
156
|
continue;
|
|
92
157
|
}
|
|
93
158
|
const fence = fenceOpen(line);
|
|
94
159
|
if (fence !== null) {
|
|
160
|
+
const li = cur.pos;
|
|
95
161
|
cur.pos += 1;
|
|
96
162
|
const content = [];
|
|
97
163
|
for (let l = peek(cur); l !== undefined; l = peek(cur)) {
|
|
@@ -102,38 +168,44 @@ function parseContainer(cur, ctx, openDir) {
|
|
|
102
168
|
content.push(l);
|
|
103
169
|
}
|
|
104
170
|
const text = content.join("\n");
|
|
105
|
-
out.push(fence.info === null
|
|
171
|
+
out.push(mark(T, cur, fence.info === null
|
|
106
172
|
? { type: "code_block", text }
|
|
107
|
-
: { type: "code_block", info: fence.info, text });
|
|
173
|
+
: { type: "code_block", info: fence.info, text }, li, cur.pos - 1));
|
|
108
174
|
continue;
|
|
109
175
|
}
|
|
110
176
|
if (line.startsWith(">") && ctx.quoteDepth < MAX_QUOTE_DEPTH) {
|
|
177
|
+
const li = cur.pos;
|
|
111
178
|
const innerLines = [];
|
|
179
|
+
const innerStarts = T !== null && cur.starts !== null ? [] : null;
|
|
112
180
|
for (let l = peek(cur); l !== undefined && l.startsWith(">"); l = peek(cur)) {
|
|
113
181
|
const rest = l.slice(1);
|
|
114
|
-
|
|
182
|
+
const stripped = rest.startsWith(" ") ? 2 : 1;
|
|
183
|
+
innerLines.push(l.slice(stripped));
|
|
184
|
+
innerStarts?.push(lineStartOf(cur, cur.pos) + stripped);
|
|
115
185
|
cur.pos += 1;
|
|
116
186
|
}
|
|
117
|
-
const sub = { lines: innerLines, pos: 0 };
|
|
118
|
-
out.push({
|
|
187
|
+
const sub = { lines: innerLines, starts: innerStarts, pos: 0 };
|
|
188
|
+
out.push(mark(T, cur, {
|
|
119
189
|
type: "blockquote",
|
|
120
|
-
children: parseContainer(sub, { ...ctx, quoteDepth: ctx.quoteDepth + 1 }, null),
|
|
121
|
-
});
|
|
190
|
+
children: parseContainer(sub, { ...ctx, quoteDepth: ctx.quoteDepth + 1 }, null, T),
|
|
191
|
+
}, li, cur.pos - 1));
|
|
122
192
|
continue;
|
|
123
193
|
}
|
|
124
194
|
if (line.replace(/[ \t]+$/, "") === "---") {
|
|
195
|
+
const li = cur.pos;
|
|
125
196
|
cur.pos += 1;
|
|
126
|
-
out.push({ type: "thematic_break" });
|
|
197
|
+
out.push(mark(T, cur, { type: "thematic_break" }, li, li));
|
|
127
198
|
continue;
|
|
128
199
|
}
|
|
129
200
|
if (ctx.listDepth < MAX_LIST_DEPTH) {
|
|
130
201
|
const m = marker(line);
|
|
131
202
|
if (m !== null) {
|
|
132
|
-
out.push(parseList(cur, ctx, m));
|
|
203
|
+
out.push(parseList(cur, ctx, m, T));
|
|
133
204
|
continue;
|
|
134
205
|
}
|
|
135
206
|
}
|
|
136
207
|
// Paragraph: plain lines until blank / container end / block start.
|
|
208
|
+
const li = cur.pos;
|
|
137
209
|
const plines = [line];
|
|
138
210
|
cur.pos += 1;
|
|
139
211
|
for (let l = peek(cur); l !== undefined; l = peek(cur)) {
|
|
@@ -146,11 +218,19 @@ function parseContainer(cur, ctx, openDir) {
|
|
|
146
218
|
if (plines.length === 1) {
|
|
147
219
|
const target = turbolink(plines[0]);
|
|
148
220
|
if (target !== null) {
|
|
149
|
-
out.push({ type: "turbolink", target });
|
|
221
|
+
out.push(mark(T, cur, { type: "turbolink", target }, li, li));
|
|
150
222
|
continue;
|
|
151
223
|
}
|
|
152
224
|
}
|
|
153
|
-
|
|
225
|
+
let P = null;
|
|
226
|
+
let text = plines.join("\n");
|
|
227
|
+
if (T !== null && cur.starts !== null) {
|
|
228
|
+
const segs = plines.map((t, idx) => ({ text: t, start: lineStartOf(cur, li + idx) }));
|
|
229
|
+
const built = inlineCtx(T, segs);
|
|
230
|
+
text = built.text;
|
|
231
|
+
P = built.P;
|
|
232
|
+
}
|
|
233
|
+
out.push(mark(T, cur, { type: "paragraph", children: parseInlines(text, P) }, li, cur.pos - 1));
|
|
154
234
|
}
|
|
155
235
|
return out;
|
|
156
236
|
}
|
|
@@ -220,11 +300,18 @@ function marker(line) {
|
|
|
220
300
|
}
|
|
221
301
|
/** One list of one kind. A same-column marker of the other kind ends this
|
|
222
302
|
* list (the dispatcher immediately starts the next one). */
|
|
223
|
-
function parseList(cur, ctx, first) {
|
|
303
|
+
function parseList(cur, ctx, first, T) {
|
|
304
|
+
const tracking = T !== null && cur.starts !== null;
|
|
224
305
|
const ordered = first.ordered;
|
|
225
306
|
const items = [];
|
|
307
|
+
const listLi = cur.pos;
|
|
308
|
+
let itemStart = tracking ? lineStartOf(cur, cur.pos) + first.indent : 0;
|
|
226
309
|
let buf = [peek(cur).slice(first.contentIdx)];
|
|
310
|
+
let bufStarts = tracking ? [lineStartOf(cur, cur.pos) + first.contentIdx] : null;
|
|
227
311
|
cur.pos += 1;
|
|
312
|
+
const finish = () => {
|
|
313
|
+
items.push(finishItem(buf, bufStarts, ctx, T, itemStart));
|
|
314
|
+
};
|
|
228
315
|
for (;;) {
|
|
229
316
|
const line = peek(cur);
|
|
230
317
|
if (line === undefined) {
|
|
@@ -252,6 +339,7 @@ function parseList(cur, ctx, first) {
|
|
|
252
339
|
break;
|
|
253
340
|
}
|
|
254
341
|
buf.push("");
|
|
342
|
+
bufStarts?.push(tracking ? lineStartOf(cur, cur.pos) : 0);
|
|
255
343
|
cur.pos += 1;
|
|
256
344
|
continue;
|
|
257
345
|
}
|
|
@@ -259,28 +347,46 @@ function parseList(cur, ctx, first) {
|
|
|
259
347
|
// Content (or a deeper marker) inside the current item; strip the
|
|
260
348
|
// content column. Off-grid extra spaces ride along (floor rule).
|
|
261
349
|
buf.push(line.slice(2));
|
|
350
|
+
bufStarts?.push(tracking ? lineStartOf(cur, cur.pos) + 2 : 0);
|
|
262
351
|
cur.pos += 1;
|
|
263
352
|
continue;
|
|
264
353
|
}
|
|
265
354
|
// Column 0 or 1 (floors to 0).
|
|
266
355
|
const m = marker(line);
|
|
267
356
|
if (m !== null && m.ordered === ordered) {
|
|
268
|
-
|
|
357
|
+
finish();
|
|
358
|
+
itemStart = tracking ? lineStartOf(cur, cur.pos) + m.indent : 0;
|
|
269
359
|
buf = [line.slice(m.contentIdx)];
|
|
360
|
+
bufStarts = tracking ? [lineStartOf(cur, cur.pos) + m.contentIdx] : null;
|
|
270
361
|
cur.pos += 1;
|
|
271
362
|
continue;
|
|
272
363
|
}
|
|
273
364
|
break; // column-0 block, or a kind switch
|
|
274
365
|
}
|
|
275
|
-
|
|
276
|
-
|
|
366
|
+
finish();
|
|
367
|
+
const list = { type: "list", ordered, children: items };
|
|
368
|
+
if (tracking) {
|
|
369
|
+
T.spans.set(list, {
|
|
370
|
+
start: lineStartOf(cur, listLi) + first.indent,
|
|
371
|
+
end: lineEndOf(cur, cur.pos - 1),
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
return list;
|
|
277
375
|
}
|
|
278
|
-
function finishItem(buf, ctx) {
|
|
279
|
-
const sub = { lines: buf, pos: 0 };
|
|
280
|
-
|
|
376
|
+
function finishItem(buf, bufStarts, ctx, T, itemStart) {
|
|
377
|
+
const sub = { lines: buf, starts: bufStarts, pos: 0 };
|
|
378
|
+
const item = {
|
|
281
379
|
type: "list_item",
|
|
282
|
-
children: parseContainer(sub, { ...ctx, listDepth: ctx.listDepth + 1 }, null),
|
|
380
|
+
children: parseContainer(sub, { ...ctx, listDepth: ctx.listDepth + 1 }, null, T),
|
|
283
381
|
};
|
|
382
|
+
if (T !== null && bufStarts !== null) {
|
|
383
|
+
let end = itemStart;
|
|
384
|
+
for (let i = 0; i < buf.length; i += 1) {
|
|
385
|
+
end = Math.max(end, bufStarts[i] + buf[i].length);
|
|
386
|
+
}
|
|
387
|
+
T.spans.set(item, { start: itemStart, end });
|
|
388
|
+
}
|
|
389
|
+
return item;
|
|
284
390
|
}
|
|
285
391
|
function classifyDirective(line) {
|
|
286
392
|
const rest = line.slice(3).replace(/[ \t]+$/, "");
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,27 @@
|
|
|
1
|
+
import { type Span } from "./blocks.ts";
|
|
1
2
|
import type { Node } from "./ast.ts";
|
|
2
3
|
export type { Attrs, Node, Reason } from "./ast.ts";
|
|
4
|
+
export type { Span } from "./blocks.ts";
|
|
3
5
|
export declare class UnsupportedVersionError extends Error {
|
|
4
6
|
readonly version: number;
|
|
5
7
|
constructor(version: number);
|
|
6
8
|
}
|
|
7
9
|
export declare function parse(input: string): Node;
|
|
10
|
+
/** parse() plus source positions - the editor-tooling extra, deliberately
|
|
11
|
+
* OUTSIDE the AST wire contract (SPEC.md, "Source positions"):
|
|
12
|
+
*
|
|
13
|
+
* - `doc` is byte-identical to what parse() returns: nodes carry no
|
|
14
|
+
* position fields, so serialization, vectors, and goldens are untouched.
|
|
15
|
+
* - `spans` maps each node object to its [start, end) source extent, in
|
|
16
|
+
* UTF-16 code units (what JavaScript strings and CodeMirror speak) over
|
|
17
|
+
* `source` - the front-door-NORMALIZED input (\r\n -> \n; the shebang
|
|
18
|
+
* line, when present, is included in the offset space).
|
|
19
|
+
* - Node-level extents are the guarantee (a span covers its opener through
|
|
20
|
+
* its closer); the interiors of canonicalized text (merged literals,
|
|
21
|
+
* resolved escapes) are covered but not subdivided.
|
|
22
|
+
*/
|
|
23
|
+
export declare function parseWithPositions(input: string): {
|
|
24
|
+
doc: Node;
|
|
25
|
+
spans: WeakMap<Node, Span>;
|
|
26
|
+
source: string;
|
|
27
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -22,6 +22,34 @@ export function parse(input) {
|
|
|
22
22
|
}
|
|
23
23
|
return { type: "document", version, children: parseBlocks(body) };
|
|
24
24
|
}
|
|
25
|
+
/** parse() plus source positions - the editor-tooling extra, deliberately
|
|
26
|
+
* OUTSIDE the AST wire contract (SPEC.md, "Source positions"):
|
|
27
|
+
*
|
|
28
|
+
* - `doc` is byte-identical to what parse() returns: nodes carry no
|
|
29
|
+
* position fields, so serialization, vectors, and goldens are untouched.
|
|
30
|
+
* - `spans` maps each node object to its [start, end) source extent, in
|
|
31
|
+
* UTF-16 code units (what JavaScript strings and CodeMirror speak) over
|
|
32
|
+
* `source` - the front-door-NORMALIZED input (\r\n -> \n; the shebang
|
|
33
|
+
* line, when present, is included in the offset space).
|
|
34
|
+
* - Node-level extents are the guarantee (a span covers its opener through
|
|
35
|
+
* its closer); the interiors of canonicalized text (merged literals,
|
|
36
|
+
* resolved escapes) are covered but not subdivided.
|
|
37
|
+
*/
|
|
38
|
+
export function parseWithPositions(input) {
|
|
39
|
+
const normalized = input.replaceAll("\r\n", "\n").replaceAll("\r", "\n");
|
|
40
|
+
const [version, body] = stripVersion(normalized);
|
|
41
|
+
if (version !== 0) {
|
|
42
|
+
throw new UnsupportedVersionError(version);
|
|
43
|
+
}
|
|
44
|
+
const tracker = { spans: new WeakMap() };
|
|
45
|
+
const doc = {
|
|
46
|
+
type: "document",
|
|
47
|
+
version,
|
|
48
|
+
children: parseBlocks(body, tracker, normalized.length - body.length),
|
|
49
|
+
};
|
|
50
|
+
tracker.spans.set(doc, { start: 0, end: normalized.length });
|
|
51
|
+
return { doc, spans: tracker.spans, source: normalized };
|
|
52
|
+
}
|
|
25
53
|
/** The in-band version declaration: line 1, exactly `#!marquee <integer>`.
|
|
26
54
|
* The default is version 0, forever. */
|
|
27
55
|
function stripVersion(s) {
|
package/dist/inlines.d.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import type { Node } from "./ast.ts";
|
|
2
|
+
import { type Span } from "./blocks.ts";
|
|
2
3
|
export declare const MAX_INLINE_DEPTH = 16;
|
|
3
4
|
export declare const MAX_EMOJI_SLUG_BYTES = 64;
|
|
4
|
-
|
|
5
|
+
/** Positions context: map[cpIndex] = source UTF-16 offset of that code
|
|
6
|
+
* point in the logical text (one extra entry for the end). */
|
|
7
|
+
export interface PosCtx {
|
|
8
|
+
map: number[];
|
|
9
|
+
spans: WeakMap<Node, Span>;
|
|
10
|
+
}
|
|
11
|
+
export declare function parseInlines(text: string, P?: PosCtx | null): Node[];
|