@geml/geml 1.7.8 → 1.8.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/LICENSE +21 -21
- package/README.md +288 -285
- package/codemap/adapters/crg.mjs +120 -120
- package/codemap/adapters/joern.mjs +131 -131
- package/codemap/adapters/scip.mjs +658 -658
- package/codemap/browser-stub.mjs +34 -34
- package/codemap/build.mjs +629 -629
- package/codemap/cross-stack.mjs +303 -303
- package/codemap/detect.mjs +399 -399
- package/codemap/emit.mjs +510 -510
- package/codemap/entries.mjs +129 -129
- package/codemap/exclude.mjs +56 -56
- package/codemap/find.mjs +49 -49
- package/codemap/foldings.mjs +110 -110
- package/codemap/joern-export.sc +83 -83
- package/codemap/mcp-server.mjs +434 -431
- package/codemap/normalize.mjs +275 -275
- package/codemap/recipe-trust.mjs +103 -103
- package/codemap/refresh.mjs +313 -313
- package/codemap/render-all.mjs +90 -90
- package/codemap/serve.mjs +585 -585
- package/codemap/sfc-virtualize.mjs +367 -367
- package/codemap/verify.mjs +158 -158
- package/dist/attrs.d.ts +8 -0
- package/dist/attrs.js +24 -0
- package/dist/cli.js +190 -139
- package/dist/diagnostics.d.ts +1 -1
- package/dist/diagnostics.js +5 -1
- package/dist/geml.js +63 -25
- package/dist/inline.d.ts +7 -1
- package/dist/inline.js +203 -137
- package/dist/mcp.js +68 -26
- package/dist/render-html.js +35 -35
- package/dist/render.js +157 -157
- package/package.json +1 -1
- package/skill/SKILL.md +167 -167
- package/skill/references/authoring.geml +369 -365
package/dist/inline.js
CHANGED
|
@@ -72,41 +72,57 @@ function classifyDest(dest) {
|
|
|
72
72
|
return { doc: d };
|
|
73
73
|
return {};
|
|
74
74
|
}
|
|
75
|
+
function pairsOf(s) {
|
|
76
|
+
const br = new Int32Array(s.length).fill(-1);
|
|
77
|
+
const pa = new Int32Array(s.length).fill(-1);
|
|
78
|
+
const bs = [], ps = [];
|
|
79
|
+
for (let i = 0; i < s.length; i++) {
|
|
80
|
+
const c = s[i];
|
|
81
|
+
if (c === "[")
|
|
82
|
+
bs.push(i);
|
|
83
|
+
else if (c === "]") {
|
|
84
|
+
const j = bs.pop();
|
|
85
|
+
if (j !== undefined)
|
|
86
|
+
br[j] = i;
|
|
87
|
+
}
|
|
88
|
+
else if (c === "(")
|
|
89
|
+
ps.push(i);
|
|
90
|
+
else if (c === ")") {
|
|
91
|
+
const j = ps.pop();
|
|
92
|
+
if (j !== undefined)
|
|
93
|
+
pa[j] = i;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return { br, pa, off: 0 };
|
|
97
|
+
}
|
|
98
|
+
// A link label is a bracket-balanced span, so the map restricted to it IS the
|
|
99
|
+
// map of the substring — the maps are built ONCE for the whole inline and read
|
|
100
|
+
// through an offset by every nesting level, rather than rebuilt per level (which
|
|
101
|
+
// at the 100-deep cap would have multiplied a 1 MB line's map memory by 100).
|
|
102
|
+
// A partner falling outside the window means "unbalanced here", which is what
|
|
103
|
+
// the substring-local scan reported.
|
|
104
|
+
function pairEnd(m, p, s, i) {
|
|
105
|
+
const j = m[p.off + i];
|
|
106
|
+
if (j === undefined || j < 0)
|
|
107
|
+
return -1;
|
|
108
|
+
const end = j - p.off;
|
|
109
|
+
return end < s.length ? end : -1;
|
|
110
|
+
}
|
|
75
111
|
// Read a balanced `(...)` starting at s[i]==='('. Returns content and index
|
|
76
112
|
// just past the closing ')', or null if unbalanced.
|
|
77
|
-
function readParen(s, i) {
|
|
113
|
+
function readParen(s, i, p) {
|
|
78
114
|
if (s[i] !== "(")
|
|
79
115
|
return null;
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const c = s[j];
|
|
83
|
-
if (c === "(")
|
|
84
|
-
depth++;
|
|
85
|
-
else if (c === ")") {
|
|
86
|
-
depth--;
|
|
87
|
-
if (depth === 0)
|
|
88
|
-
return { content: s.slice(i + 1, j), end: j + 1 };
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
return null;
|
|
116
|
+
const j = pairEnd(p.pa, p, s, i);
|
|
117
|
+
return j < 0 ? null : { content: s.slice(i + 1, j), end: j + 1 };
|
|
92
118
|
}
|
|
93
119
|
// Read a balanced `[...]` starting at s[i]==='['. Returns content and index
|
|
94
120
|
// just past the closing ']', or null if unbalanced.
|
|
95
|
-
function readBracket(s, i) {
|
|
121
|
+
function readBracket(s, i, p) {
|
|
96
122
|
if (s[i] !== "[")
|
|
97
123
|
return null;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const c = s[j];
|
|
101
|
-
if (c === "[")
|
|
102
|
-
depth++;
|
|
103
|
-
else if (c === "]") {
|
|
104
|
-
depth--;
|
|
105
|
-
if (depth === 0)
|
|
106
|
-
return { content: s.slice(i + 1, j), end: j + 1 };
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
return null;
|
|
124
|
+
const j = pairEnd(p.br, p, s, i);
|
|
125
|
+
return j < 0 ? null : { content: s.slice(i + 1, j), end: j + 1 };
|
|
110
126
|
}
|
|
111
127
|
// Optional `{…}` attribute object immediately following a construct.
|
|
112
128
|
function readAttrs(s, i) {
|
|
@@ -120,13 +136,18 @@ function readAttrs(s, i) {
|
|
|
120
136
|
// Phase A: pull out high-priority atoms (escapes, code, math, media, links,
|
|
121
137
|
// auto-refs, footnotes, hard breaks). Everything else is left as text runs for
|
|
122
138
|
// phase B (emphasis). Children of links are fully re-parsed.
|
|
123
|
-
function scanAtoms(s, line, sink, depth
|
|
139
|
+
function scanAtoms(s, line, sink, depth, p) {
|
|
124
140
|
const out = [];
|
|
125
141
|
let buf = "";
|
|
126
142
|
const flush = () => { if (buf) {
|
|
127
143
|
out.push(buf);
|
|
128
144
|
buf = "";
|
|
129
145
|
} };
|
|
146
|
+
// Emit `node` as the atom occupying source span [start, end).
|
|
147
|
+
const atom = (node, start, end) => {
|
|
148
|
+
flush();
|
|
149
|
+
out.push({ node, first: s[start], last: s[end - 1] });
|
|
150
|
+
};
|
|
130
151
|
let i = 0;
|
|
131
152
|
while (i < s.length) {
|
|
132
153
|
const c = s[i];
|
|
@@ -134,16 +155,15 @@ function scanAtoms(s, line, sink, depth = 0) {
|
|
|
134
155
|
if (c === "\\") {
|
|
135
156
|
const next = s[i + 1];
|
|
136
157
|
if (next === undefined || next === "\n") { // line-final backslash
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
i
|
|
158
|
+
const end = i + (next === undefined ? 1 : 2);
|
|
159
|
+
atom({ type: "break" }, i, end);
|
|
160
|
+
i = end;
|
|
140
161
|
continue;
|
|
141
162
|
}
|
|
142
163
|
if (/[!-/:-@[-`{-~]/.test(next)) {
|
|
143
164
|
// ASCII punctuation -> literal, emitted as its own text atom so phase B
|
|
144
165
|
// (emphasis) cannot mistake an escaped `*`/`~` for a delimiter (§5.3(1)).
|
|
145
|
-
|
|
146
|
-
out.push({ type: "text", value: next });
|
|
166
|
+
atom({ type: "text", value: next }, i, i + 2);
|
|
147
167
|
i += 2;
|
|
148
168
|
continue;
|
|
149
169
|
}
|
|
@@ -159,8 +179,7 @@ function scanAtoms(s, line, sink, depth = 0) {
|
|
|
159
179
|
const fence = "`".repeat(n);
|
|
160
180
|
const close = s.indexOf(fence, i + n);
|
|
161
181
|
if (close >= 0) {
|
|
162
|
-
|
|
163
|
-
out.push({ type: "code", value: s.slice(i + n, close) });
|
|
182
|
+
atom({ type: "code", value: s.slice(i + n, close) }, i, close + n);
|
|
164
183
|
i = close + n;
|
|
165
184
|
continue;
|
|
166
185
|
}
|
|
@@ -172,8 +191,7 @@ function scanAtoms(s, line, sink, depth = 0) {
|
|
|
172
191
|
if (c === "$") {
|
|
173
192
|
const close = s.indexOf("$", i + 1);
|
|
174
193
|
if (close > i + 1) {
|
|
175
|
-
|
|
176
|
-
out.push({ type: "math", value: s.slice(i + 1, close) });
|
|
194
|
+
atom({ type: "math", value: s.slice(i + 1, close) }, i, close + 1);
|
|
177
195
|
i = close + 1;
|
|
178
196
|
continue;
|
|
179
197
|
}
|
|
@@ -187,15 +205,14 @@ function scanAtoms(s, line, sink, depth = 0) {
|
|
|
187
205
|
// `![[#x]](y)` would be claimed whole — the parenthesis run has to stay
|
|
188
206
|
// literal text, which is what this ordering pins.
|
|
189
207
|
if (c === "!" && s[i + 1] === "[" && s[i + 2] === "[") {
|
|
190
|
-
const inner = readBracket(s, i + 2); // the inner [...] after `![`
|
|
208
|
+
const inner = readBracket(s, i + 2, p); // the inner [...] after `![`
|
|
191
209
|
if (inner && s[inner.end] === "]") {
|
|
192
210
|
const { doc, anchor } = classifyDest(inner.content.trim());
|
|
193
211
|
if (anchor) {
|
|
194
|
-
flush();
|
|
195
212
|
const node = { type: "project", anchor };
|
|
196
213
|
if (doc)
|
|
197
214
|
node.doc = doc;
|
|
198
|
-
|
|
215
|
+
atom(node, i, inner.end + 1);
|
|
199
216
|
// Validated by the same §8 resolver as any reference; the target's TYPE
|
|
200
217
|
// is checked separately, since only inline content can be projected.
|
|
201
218
|
sink.refs.push({ kind: doc ? "cross" : "autoref", doc, anchor, line });
|
|
@@ -206,8 +223,8 @@ function scanAtoms(s, line, sink, depth = 0) {
|
|
|
206
223
|
}
|
|
207
224
|
}
|
|
208
225
|
if (c === "!" && s[i + 1] === "[") {
|
|
209
|
-
const label = readBracket(s, i + 1);
|
|
210
|
-
const paren = label ? readParen(s, label.end) : null;
|
|
226
|
+
const label = readBracket(s, i + 1, p);
|
|
227
|
+
const paren = label ? readParen(s, label.end, p) : null;
|
|
211
228
|
if (label && paren) {
|
|
212
229
|
const a = readAttrs(s, paren.end);
|
|
213
230
|
const attrObj = a ? a.attrs : { classes: [], attrs: {} };
|
|
@@ -232,24 +249,22 @@ function scanAtoms(s, line, sink, depth = 0) {
|
|
|
232
249
|
if (inf)
|
|
233
250
|
node.as = inf;
|
|
234
251
|
}
|
|
235
|
-
|
|
236
|
-
out.push(node);
|
|
252
|
+
atom(node, i, a ? a.end : paren.end);
|
|
237
253
|
i = a ? a.end : paren.end;
|
|
238
254
|
continue;
|
|
239
255
|
}
|
|
240
256
|
}
|
|
241
257
|
// §5.3(2): auto-reference [[#id]].
|
|
242
258
|
if (c === "[" && s[i + 1] === "[") {
|
|
243
|
-
const inner = readBracket(s, i + 1); // inner [...] after the first [
|
|
259
|
+
const inner = readBracket(s, i + 1, p); // inner [...] after the first [
|
|
244
260
|
if (inner && s[inner.end] === "]") {
|
|
245
261
|
const target = inner.content.trim();
|
|
246
262
|
const { doc, anchor } = classifyDest(target);
|
|
247
263
|
if (anchor) {
|
|
248
|
-
flush();
|
|
249
264
|
const node = { type: "autoref", anchor };
|
|
250
265
|
if (doc)
|
|
251
266
|
node.doc = doc;
|
|
252
|
-
|
|
267
|
+
atom(node, i, inner.end + 1);
|
|
253
268
|
sink.refs.push({ kind: doc ? "cross" : "autoref", doc, anchor, line });
|
|
254
269
|
i = inner.end + 1;
|
|
255
270
|
continue;
|
|
@@ -258,11 +273,10 @@ function scanAtoms(s, line, sink, depth = 0) {
|
|
|
258
273
|
}
|
|
259
274
|
// §5.3(2): footnote reference [^id].
|
|
260
275
|
if (c === "[" && s[i + 1] === "^") {
|
|
261
|
-
const br = readBracket(s, i);
|
|
276
|
+
const br = readBracket(s, i, p);
|
|
262
277
|
if (br && br.content.startsWith("^")) {
|
|
263
278
|
const ref = br.content.slice(1).trim();
|
|
264
|
-
|
|
265
|
-
out.push({ type: "footnote", ref });
|
|
279
|
+
atom({ type: "footnote", ref }, i, br.end);
|
|
266
280
|
sink.refs.push({ kind: "footnote", anchor: ref, line });
|
|
267
281
|
i = br.end;
|
|
268
282
|
continue;
|
|
@@ -270,15 +284,17 @@ function scanAtoms(s, line, sink, depth = 0) {
|
|
|
270
284
|
}
|
|
271
285
|
// §5.3(2): link [text](dest){…}.
|
|
272
286
|
if (c === "[") {
|
|
273
|
-
const label = readBracket(s, i);
|
|
274
|
-
const paren = label ? readParen(s, label.end) : null;
|
|
287
|
+
const label = readBracket(s, i, p);
|
|
288
|
+
const paren = label ? readParen(s, label.end, p) : null;
|
|
275
289
|
if (label && paren) {
|
|
276
290
|
const a = readAttrs(s, paren.end);
|
|
277
291
|
const attrObj = a ? a.attrs : { classes: [], attrs: {} };
|
|
278
292
|
const dest = classifyDest(paren.content);
|
|
279
293
|
const node = {
|
|
280
294
|
type: "link",
|
|
281
|
-
|
|
295
|
+
// The label window starts one character past this `[`, so the shared
|
|
296
|
+
// maps are read at that offset instead of being rebuilt for it.
|
|
297
|
+
children: parseInline(label.content, line, sink, depth + 1, { br: p.br, pa: p.pa, off: p.off + i + 1 }),
|
|
282
298
|
attrs: attrObj.attrs,
|
|
283
299
|
};
|
|
284
300
|
if (dest.href)
|
|
@@ -290,8 +306,7 @@ function scanAtoms(s, line, sink, depth = 0) {
|
|
|
290
306
|
if (dest.anchor || dest.doc) {
|
|
291
307
|
sink.refs.push({ kind: dest.doc ? "cross" : "internal", doc: dest.doc, anchor: dest.anchor, line });
|
|
292
308
|
}
|
|
293
|
-
|
|
294
|
-
out.push(node);
|
|
309
|
+
atom(node, i, a ? a.end : paren.end);
|
|
295
310
|
i = a ? a.end : paren.end;
|
|
296
311
|
continue;
|
|
297
312
|
}
|
|
@@ -302,19 +317,23 @@ function scanAtoms(s, line, sink, depth = 0) {
|
|
|
302
317
|
flush();
|
|
303
318
|
return out;
|
|
304
319
|
}
|
|
305
|
-
// Phase B: emphasis / strong / strikethrough
|
|
320
|
+
// Phase B: emphasis / strong / strikethrough over the whole inline sequence
|
|
321
|
+
// (§5.3, GEP-0007).
|
|
306
322
|
//
|
|
307
|
-
// A maximal run of `*` is an emphasis delimiter (one `*` ->
|
|
308
|
-
// strong, longer runs pair greedily); a maximal run of two or
|
|
309
|
-
// strikethrough delimiter (a lone `~` is literal). Whether a run
|
|
310
|
-
// and/or *close* is fixed by flanking: it must hug a non-space
|
|
311
|
-
// the side facing a punctuation character it must also have
|
|
312
|
-
// punctuation on the far side (the CommonMark left/right-flanking
|
|
313
|
-
// then paired by a single left-to-right stack scan with the
|
|
314
|
-
// nested and adjacent delimiters resolve to exactly one tree
|
|
315
|
-
// guesswork. Delimiters pair
|
|
316
|
-
// a code span,
|
|
317
|
-
//
|
|
323
|
+
// A maximal run of `*` in literal text is an emphasis delimiter (one `*` ->
|
|
324
|
+
// emphasis, two -> strong, longer runs pair greedily); a maximal run of two or
|
|
325
|
+
// more `~` is a strikethrough delimiter (a lone `~` is literal). Whether a run
|
|
326
|
+
// may *open* and/or *close* is fixed by flanking: it must hug a non-space
|
|
327
|
+
// character, and on the side facing a punctuation character it must also have
|
|
328
|
+
// whitespace or punctuation on the far side (the CommonMark left/right-flanking
|
|
329
|
+
// rule). Runs are then paired by a single left-to-right stack scan with the
|
|
330
|
+
// rule of three, so nested and adjacent delimiters resolve to exactly one tree
|
|
331
|
+
// — no leftmost-regex guesswork. Delimiters pair across phase-A atoms — a pair
|
|
332
|
+
// may wrap a code span, math, a link or image — but the atoms themselves are
|
|
333
|
+
// opaque: characters inside one are never delimiters, and at an atom boundary
|
|
334
|
+
// the flanking test reads the atom's edge source characters (AtomPart). A
|
|
335
|
+
// delimiter run never pairs across a block boundary, and any run left unpaired
|
|
336
|
+
// is literal text.
|
|
318
337
|
// Unicode punctuation, not just ASCII (§5.3). With an ASCII-only test, `“` and
|
|
319
338
|
// `,` count as ordinary letters, and a run hugged by CJK punctuation on the
|
|
320
339
|
// outside and ASCII punctuation on the inside stops flanking: `“*(foo)*”` loses
|
|
@@ -328,45 +347,68 @@ function flank(before, after) {
|
|
|
328
347
|
const bWS = isWS(before), aWS = isWS(after), bP = isPunct(before), aP = isPunct(after);
|
|
329
348
|
return { open: !aWS && (!aP || bWS || bP), close: !bWS && (!bP || aWS || aP) };
|
|
330
349
|
}
|
|
331
|
-
// Split
|
|
332
|
-
|
|
350
|
+
// Split the mixed phase-A sequence into a doubly-linked list of text,
|
|
351
|
+
// delimiter-run, and atom nodes. A delimiter run at the edge of a text part
|
|
352
|
+
// flanks against the neighboring part's edge character — an atom's recorded
|
|
353
|
+
// source edge, or the adjacent char of a neighboring text part — and against
|
|
354
|
+
// nothing (whitespace) at the ends of the sequence.
|
|
355
|
+
function tokenizeRuns(parts) {
|
|
333
356
|
let head = null, tail = null;
|
|
334
357
|
const push = (node) => { node.prev = tail; if (tail)
|
|
335
358
|
tail.next = node;
|
|
336
359
|
else
|
|
337
360
|
head = node; tail = node; };
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
361
|
+
// …and thread every delimiter onto the delimiter-only chain as it is pushed.
|
|
362
|
+
let dhead = null, dtail = null, dn = 0;
|
|
363
|
+
const pushDelim = (d) => {
|
|
364
|
+
d.idx = dn++;
|
|
365
|
+
d.dprev = dtail;
|
|
366
|
+
if (dtail)
|
|
367
|
+
dtail.dnext = d;
|
|
368
|
+
else
|
|
369
|
+
dhead = d;
|
|
370
|
+
dtail = d;
|
|
371
|
+
push(d);
|
|
372
|
+
};
|
|
373
|
+
for (let k = 0; k < parts.length; k++) {
|
|
374
|
+
const part = parts[k];
|
|
375
|
+
if (typeof part !== "string") {
|
|
376
|
+
push({ t: "atom", node: part.node, prev: null, next: null });
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
const s = part;
|
|
380
|
+
// A text part's neighbors are always atoms (or the sequence edge):
|
|
381
|
+
// scanAtoms flushes buffered text exactly when it emits an atom, so two
|
|
382
|
+
// text parts are never adjacent.
|
|
383
|
+
const before0 = k > 0 ? parts[k - 1].last : undefined;
|
|
384
|
+
const after0 = k + 1 < parts.length ? parts[k + 1].first : undefined;
|
|
385
|
+
let i = 0;
|
|
386
|
+
while (i < s.length) {
|
|
387
|
+
const c = s[i];
|
|
388
|
+
if (c === "*" || c === "~") {
|
|
389
|
+
let j = i;
|
|
390
|
+
while (s[j] === c)
|
|
391
|
+
j++;
|
|
392
|
+
const n = j - i;
|
|
393
|
+
if (c === "~" && n < 2)
|
|
394
|
+
push({ t: "text", v: "~", prev: null, next: null });
|
|
395
|
+
else {
|
|
396
|
+
const f = flank(i > 0 ? s[i - 1] : before0, j < s.length ? s[j] : after0);
|
|
397
|
+
pushDelim({ t: "delim", ch: c, n, open: f.open, close: f.close, idx: 0, dprev: null, dnext: null, prev: null, next: null });
|
|
398
|
+
}
|
|
399
|
+
i = j;
|
|
400
|
+
}
|
|
348
401
|
else {
|
|
349
|
-
|
|
350
|
-
|
|
402
|
+
let j = i;
|
|
403
|
+
while (j < s.length && s[j] !== "*" && s[j] !== "~")
|
|
404
|
+
j++;
|
|
405
|
+
push({ t: "text", v: s.slice(i, j), prev: null, next: null });
|
|
406
|
+
i = j;
|
|
351
407
|
}
|
|
352
|
-
i = j;
|
|
353
|
-
}
|
|
354
|
-
else {
|
|
355
|
-
let j = i;
|
|
356
|
-
while (j < s.length && s[j] !== "*" && s[j] !== "~")
|
|
357
|
-
j++;
|
|
358
|
-
push({ t: "text", v: s.slice(i, j), prev: null, next: null });
|
|
359
|
-
i = j;
|
|
360
408
|
}
|
|
361
409
|
}
|
|
362
|
-
return head;
|
|
410
|
+
return { head, first: dhead };
|
|
363
411
|
}
|
|
364
|
-
const nextDelim = (n) => { for (; n; n = n.next)
|
|
365
|
-
if (n.t === "delim")
|
|
366
|
-
return n; return null; };
|
|
367
|
-
const prevDelim = (n) => { for (; n; n = n.prev)
|
|
368
|
-
if (n.t === "delim")
|
|
369
|
-
return n; return null; };
|
|
370
412
|
// Rule of three: when either side can also play the other role, a combined
|
|
371
413
|
// length that is a multiple of three is only allowed if both lengths are.
|
|
372
414
|
function rule3(o, c) {
|
|
@@ -374,6 +416,18 @@ function rule3(o, c) {
|
|
|
374
416
|
return (o.n + c.n) % 3 !== 0 || (o.n % 3 === 0 && c.n % 3 === 0);
|
|
375
417
|
return true;
|
|
376
418
|
}
|
|
419
|
+
// Drop a delimiter from the delimiter-only chain. Called for a spent delimiter
|
|
420
|
+
// and for every delimiter that a new wrap swallows: the chain must hold exactly
|
|
421
|
+
// the delimiters still pairable AT THIS LEVEL, or an opener search would reach
|
|
422
|
+
// inside a finished span and splice its own list.
|
|
423
|
+
function unlinkDelim(d) {
|
|
424
|
+
if (d.dprev)
|
|
425
|
+
d.dprev.dnext = d.dnext;
|
|
426
|
+
if (d.dnext)
|
|
427
|
+
d.dnext.dprev = d.dprev;
|
|
428
|
+
d.dprev = null;
|
|
429
|
+
d.dnext = null;
|
|
430
|
+
}
|
|
377
431
|
function unlink(node, head) {
|
|
378
432
|
if (node.prev)
|
|
379
433
|
node.prev.next = node.next;
|
|
@@ -381,38 +435,58 @@ function unlink(node, head) {
|
|
|
381
435
|
head = node.next;
|
|
382
436
|
if (node.next)
|
|
383
437
|
node.next.prev = node.prev;
|
|
438
|
+
if (node.t === "delim")
|
|
439
|
+
unlinkDelim(node);
|
|
384
440
|
return head;
|
|
385
441
|
}
|
|
386
442
|
// The CommonMark emphasis algorithm over the delimiter list: scan closers left
|
|
387
443
|
// to right, pair each with the nearest eligible opener, wrap the span, and bound
|
|
388
444
|
// future searches with `bottom` so the scan stays linear and deterministic.
|
|
389
|
-
function processEmphasis(head) {
|
|
445
|
+
function processEmphasis(head, first) {
|
|
446
|
+
// The cut-off is a delimiter POSITION, not a node: the delimiter it names can
|
|
447
|
+
// be consumed and unlinked later on, and a stale node reference would then
|
|
448
|
+
// never be reached — the search would run to the head of the list every time,
|
|
449
|
+
// which is the O(n^2) blowup this map exists to prevent. -1 means "no bound
|
|
450
|
+
// yet" (the whole prefix is searchable), matching an unset entry.
|
|
390
451
|
const bottom = new Map();
|
|
391
|
-
let closer =
|
|
452
|
+
let closer = first;
|
|
392
453
|
while (closer) {
|
|
393
|
-
|
|
394
|
-
|
|
454
|
+
// A `~` run pairs TWO characters at a time, so one leftover character is no
|
|
455
|
+
// longer a delimiter — exactly as a lone `~` was never one (tokenizeRuns).
|
|
456
|
+
// Skipping it here is what keeps `n` from going negative: pairing a spent run
|
|
457
|
+
// again drove `n` past 0, and since the loop only advances on `n === 0` the
|
|
458
|
+
// same closer was re-paired forever, allocating a wrap each time (a hang on
|
|
459
|
+
// `~~~a~~~`) or reaching finalize with n = -1 (`"~".repeat(-1)` threw a
|
|
460
|
+
// RangeError on `~~~~a~~~`).
|
|
461
|
+
if (!closer.close || (closer.ch === "~" && closer.n < 2)) {
|
|
462
|
+
closer = closer.dnext;
|
|
395
463
|
continue;
|
|
396
464
|
}
|
|
397
465
|
const ch = closer.ch;
|
|
398
466
|
const key = `${ch}${closer.open ? 1 : 0}${closer.n % 3}`;
|
|
399
|
-
const stop = bottom.
|
|
400
|
-
let opener =
|
|
467
|
+
const stop = bottom.get(key) ?? -1;
|
|
468
|
+
let opener = closer.dprev;
|
|
401
469
|
let found = null;
|
|
402
|
-
while (opener && opener
|
|
403
|
-
|
|
470
|
+
while (opener && opener.idx > stop) {
|
|
471
|
+
// …and the same for the opener side: a `~` run down to one character can
|
|
472
|
+
// no longer open, so `use = 2` never takes more than a side has left.
|
|
473
|
+
if (opener.open && opener.ch === ch && (ch !== "~" || opener.n >= 2) && rule3(opener, closer)) {
|
|
404
474
|
found = opener;
|
|
405
475
|
break;
|
|
406
476
|
}
|
|
407
|
-
opener =
|
|
477
|
+
opener = opener.dprev;
|
|
408
478
|
}
|
|
409
479
|
if (found) {
|
|
410
480
|
const use = ch === "~" ? 2 : (found.n >= 2 && closer.n >= 2 ? 2 : 1);
|
|
411
481
|
const kind = ch === "~" ? "strike" : use === 2 ? "strong" : "emph";
|
|
412
|
-
// Gather and detach the nodes strictly between opener and closer.
|
|
482
|
+
// Gather and detach the nodes strictly between opener and closer. Any
|
|
483
|
+
// delimiter among them is now inside the new span — unpaired and literal —
|
|
484
|
+
// so it leaves the delimiter chain with them.
|
|
413
485
|
let kidsHead = null, kidsTail = null;
|
|
414
486
|
for (let p = found.next; p && p !== closer;) {
|
|
415
487
|
const q = p.next;
|
|
488
|
+
if (p.t === "delim")
|
|
489
|
+
unlinkDelim(p);
|
|
416
490
|
p.prev = kidsTail;
|
|
417
491
|
p.next = null;
|
|
418
492
|
if (kidsTail)
|
|
@@ -430,21 +504,25 @@ function processEmphasis(head) {
|
|
|
430
504
|
if (found.n === 0)
|
|
431
505
|
head = unlink(found, head);
|
|
432
506
|
if (closer.n === 0) {
|
|
433
|
-
const after = closer.
|
|
507
|
+
const after = closer.dnext;
|
|
434
508
|
head = unlink(closer, head);
|
|
435
|
-
closer =
|
|
509
|
+
closer = after;
|
|
436
510
|
}
|
|
437
511
|
// else: keep the same closer (it still has delimiter characters left).
|
|
438
512
|
}
|
|
439
513
|
else {
|
|
440
|
-
|
|
441
|
-
|
|
514
|
+
// Nothing before this closer can open for this key, so no later closer
|
|
515
|
+
// with the same key need look past the delimiter just before it either.
|
|
516
|
+
bottom.set(key, closer.dprev ? closer.dprev.idx : -1);
|
|
517
|
+
closer = closer.dnext;
|
|
442
518
|
}
|
|
443
519
|
}
|
|
444
520
|
return head;
|
|
445
521
|
}
|
|
446
522
|
// Linked list of (possibly nested) nodes -> Inline[]; unpaired delimiters and
|
|
447
|
-
// empty text vanish into literal text, with adjacent text runs merged.
|
|
523
|
+
// empty text vanish into literal text, with adjacent text runs merged. An
|
|
524
|
+
// escaped-punctuation atom is a text Inline, so it folds into its neighbors —
|
|
525
|
+
// the emitted sequence is canonical with no adjacent text nodes at any level.
|
|
448
526
|
function finalize(head) {
|
|
449
527
|
const out = [];
|
|
450
528
|
const pushText = (v) => {
|
|
@@ -459,29 +537,25 @@ function finalize(head) {
|
|
|
459
537
|
pushText(n.v);
|
|
460
538
|
else if (n.t === "delim")
|
|
461
539
|
pushText(n.ch.repeat(n.n));
|
|
540
|
+
else if (n.t === "atom") {
|
|
541
|
+
if (n.node.type === "text")
|
|
542
|
+
pushText(n.node.value);
|
|
543
|
+
else
|
|
544
|
+
out.push(n.node);
|
|
545
|
+
}
|
|
462
546
|
else
|
|
463
547
|
out.push({ type: n.kind, children: finalize(n.kids) });
|
|
464
548
|
}
|
|
465
549
|
return out;
|
|
466
550
|
}
|
|
467
|
-
function emphasize(
|
|
468
|
-
const head = tokenizeRuns(
|
|
469
|
-
return head ? finalize(processEmphasis(head)) : [];
|
|
470
|
-
}
|
|
471
|
-
// Coalesce adjacent literal text nodes (e.g. an escaped `*` atom sitting between
|
|
472
|
-
// two text runs) so the inline sequence is canonical.
|
|
473
|
-
function mergeText(ns) {
|
|
474
|
-
const out = [];
|
|
475
|
-
for (const n of ns) {
|
|
476
|
-
const last = out[out.length - 1];
|
|
477
|
-
if (n.type === "text" && last && last.type === "text")
|
|
478
|
-
last.value += n.value;
|
|
479
|
-
else
|
|
480
|
-
out.push(n);
|
|
481
|
-
}
|
|
482
|
-
return out;
|
|
551
|
+
function emphasize(parts) {
|
|
552
|
+
const { head, first } = tokenizeRuns(parts);
|
|
553
|
+
return head ? finalize(processEmphasis(head, first)) : [];
|
|
483
554
|
}
|
|
484
|
-
|
|
555
|
+
// `pairs` is internal: the bracket/paren partner maps of the WHOLE inline, with
|
|
556
|
+
// the offset of this call's window into them. Only the recursive link-label call
|
|
557
|
+
// supplies it; every external caller omits it and gets the maps built here.
|
|
558
|
+
export function parseInline(s, line, sink, depth = 0, pairs) {
|
|
485
559
|
if (depth > MAX_INLINE_NESTING) {
|
|
486
560
|
// Pathological nesting (thousands of nested link labels) would overflow the
|
|
487
561
|
// call stack (R2-7). Degrade the over-deep content to text — emphasis only,
|
|
@@ -489,15 +563,7 @@ export function parseInline(s, line, sink, depth = 0) {
|
|
|
489
563
|
const diags = sink.diags;
|
|
490
564
|
if (Array.isArray(diags) && !diags.some((d) => d.code === "inline-nesting-too-deep"))
|
|
491
565
|
diags.push({ severity: "error", code: "inline-nesting-too-deep", message: `inline nesting too deep (max ${MAX_INLINE_NESTING})`, line });
|
|
492
|
-
return
|
|
493
|
-
}
|
|
494
|
-
const atoms = scanAtoms(s, line, sink, depth);
|
|
495
|
-
const out = [];
|
|
496
|
-
for (const a of atoms) {
|
|
497
|
-
if (typeof a === "string")
|
|
498
|
-
out.push(...emphasize(a));
|
|
499
|
-
else
|
|
500
|
-
out.push(a);
|
|
566
|
+
return emphasize([s]);
|
|
501
567
|
}
|
|
502
|
-
return
|
|
568
|
+
return emphasize(scanAtoms(s, line, sink, depth, pairs ?? pairsOf(s)));
|
|
503
569
|
}
|