@helping-ai-workflow/md2doc 3.17.8 → 3.17.9
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/lib/editor/table-md.js +105 -20
- package/lib/md2doc.js +130 -20
- package/package.json +1 -1
package/lib/editor/table-md.js
CHANGED
|
@@ -279,6 +279,9 @@
|
|
|
279
279
|
const uniform = widths.map(() => true);
|
|
280
280
|
let sepFields = null;
|
|
281
281
|
for (let r = 1; r < rows.length; r++) {
|
|
282
|
+
// A lazy body line (no '|' at all, v3.17.9 D7) has no fields and so no
|
|
283
|
+
// widths: it neither voids nor sets the table's width memory.
|
|
284
|
+
if (r >= 2 && rows[r].indexOf('|') === -1) continue;
|
|
282
285
|
const fields = splitFields(rows[r]);
|
|
283
286
|
// A ragged original is not a width source — bail rather than guess.
|
|
284
287
|
if (!fields || fields.length !== widths.length) return null;
|
|
@@ -361,8 +364,29 @@
|
|
|
361
364
|
// an untouched cell's comments, e.g. the `<!-- [[source, Cl.x]] -->`
|
|
362
365
|
// citation anchors in paperwork's hwe1 tables, which the canonical
|
|
363
366
|
// spelling would drop.
|
|
367
|
+
//
|
|
368
|
+
// v3.17.9 D7: a STRAY end tag (no start tag of that name earlier in the
|
|
369
|
+
// cell) is dropped too, for the same reason: the browser's parser ignores
|
|
370
|
+
// it, so the DOM never has it (`</content>` under a table). Kept, so the
|
|
371
|
+
// cell reads by its bytes as before: `</p>` and `</br>`, which the parser
|
|
372
|
+
// turns into an empty paragraph and a line break, and the table-structure
|
|
373
|
+
// names (some close the cell, some are ignored; keeping a tag only means
|
|
374
|
+
// the cell is reused when byte-equal, so the list errs on the safe side).
|
|
375
|
+
const STRAY_END_KEPT = /^(?:p|br|table|caption|colgroup|col|tbody|thead|tfoot|tr|td|th|template)$/;
|
|
376
|
+
function dropStrayEndTags(parts) {
|
|
377
|
+
const open = Object.create(null);
|
|
378
|
+
for (let i = 1; i < parts.length; i += 2) {
|
|
379
|
+
const m = /^<(\/?)([A-Za-z][A-Za-z0-9-]*)/.exec(parts[i]);
|
|
380
|
+
if (!m) continue;
|
|
381
|
+
const name = m[2].toLowerCase();
|
|
382
|
+
if (!m[1]) { open[name] = (open[name] || 0) + 1; continue; }
|
|
383
|
+
if (open[name]) { open[name] -= 1; continue; }
|
|
384
|
+
if (!STRAY_END_KEPT.test(name)) parts[i] = '';
|
|
385
|
+
}
|
|
386
|
+
return parts;
|
|
387
|
+
}
|
|
364
388
|
function normalizeCellHtml(html) {
|
|
365
|
-
return html.replace(/<!--[\s\S]*?-->/g, '').split(/(<[^>]*>)/).map((part, i) => (i % 2 ? part : part
|
|
389
|
+
return dropStrayEndTags(html.replace(/<!--[\s\S]*?-->/g, '').split(/(<[^>]*>)/)).map((part, i) => (i % 2 ? part : part
|
|
366
390
|
.replace(/&(?:#([0-9]+)|#[xX]([0-9a-fA-F]+)|([A-Za-z][A-Za-z0-9]*));/g, decodeRef)
|
|
367
391
|
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'))).join('');
|
|
368
392
|
}
|
|
@@ -410,6 +434,15 @@
|
|
|
410
434
|
// `line` is the source line trimmed. Unlike columnWidthsOf(), a ragged row
|
|
411
435
|
// does not void the table: reuse is decided per cell. null when there is
|
|
412
436
|
// no original or its header line is not a row.
|
|
437
|
+
//
|
|
438
|
+
// v3.17.9 D7: a body line with no '|' at all is still a row to marked (GFM:
|
|
439
|
+
// any non-blank line continues the table), e.g. a stray `</content>` right
|
|
440
|
+
// under a table. marked's splitCells() reads it as the whole line in the
|
|
441
|
+
// first cell and the rest empty, and so does this (`lazy: true`). Before
|
|
442
|
+
// this it was never paired, and an edit anywhere in the table rewrote it
|
|
443
|
+
// as `| | |`: the browser drops a stray end tag, so its cells are empty
|
|
444
|
+
// in the DOM. A line holding a '|' but not starting with one stays
|
|
445
|
+
// unreadable here, as before.
|
|
413
446
|
function originalRowsOf(originalMd) {
|
|
414
447
|
if (typeof originalMd !== 'string' || originalMd === '') return null;
|
|
415
448
|
const rows = originalMd.split('\n').filter((l) => l.trim() !== '');
|
|
@@ -417,7 +450,13 @@
|
|
|
417
450
|
const row = (l) => { const f = splitFields(l); return f ? { fields: f, line: l.trim() } : null; };
|
|
418
451
|
const header = row(rows[0]);
|
|
419
452
|
if (!header) return null;
|
|
420
|
-
|
|
453
|
+
const lazy = (l) => {
|
|
454
|
+
if (l.indexOf('|') !== -1) return null;
|
|
455
|
+
const fields = header.fields.map(() => '');
|
|
456
|
+
fields[0] = l.trim();
|
|
457
|
+
return { fields, line: l.trim(), lazy: true };
|
|
458
|
+
};
|
|
459
|
+
return { header, body: rows.slice(2).map((l) => row(l) || lazy(l)) };
|
|
421
460
|
}
|
|
422
461
|
|
|
423
462
|
// Pairs each current body row with an original body row (an index into
|
|
@@ -432,14 +471,16 @@
|
|
|
432
471
|
// Linear in rows (final review M1: the scans were quadratic, 3000 rows
|
|
433
472
|
// that all differ took 3 s): each row is hashed on its mapped cells'
|
|
434
473
|
// readingKey(), so "matches" is one Map lookup.
|
|
474
|
+
// A lazy original (v3.17.9 D7) has no key through the column map: its
|
|
475
|
+
// cells are positions, not named columns, so it is matched by position
|
|
476
|
+
// (see `pos` below), in the file's order like every other row.
|
|
435
477
|
function pairBodyRows(curRows, origBody, colMap, keyOf) {
|
|
436
478
|
const mapped = [];
|
|
437
479
|
colMap.forEach((k, i) => { if (k !== undefined) mapped.push(i); });
|
|
438
480
|
const pair = new Array(curRows.length).fill(-1);
|
|
439
|
-
if (mapped.length === 0) return pair;
|
|
440
481
|
const SEP = '\u0000';
|
|
441
482
|
const origKey = origBody.map((o) => {
|
|
442
|
-
if (!o) return null;
|
|
483
|
+
if (!o || o.lazy || mapped.length === 0) return null;
|
|
443
484
|
const parts = [];
|
|
444
485
|
for (let x = 0; x < mapped.length; x++) {
|
|
445
486
|
const f = o.fields[colMap[mapped[x]]];
|
|
@@ -459,26 +500,61 @@
|
|
|
459
500
|
});
|
|
460
501
|
const used = new Array(origBody.length).fill(false);
|
|
461
502
|
const anchor = new Array(curRows.length).fill(false);
|
|
503
|
+
// A table with a lazy original (v3.17.9 D7) is first matched position by
|
|
504
|
+
// position over every cell: `pos` holds each original's positional key,
|
|
505
|
+
// lazy or not, and a row takes the first unused original at or after j
|
|
506
|
+
// whose every cell reads the same, before any match through the column
|
|
507
|
+
// map. Through the map a renamed header leaves few cells (or none) to
|
|
508
|
+
// match on, so an ordinary original whose mapped cells happen to match
|
|
509
|
+
// (all empty, say) could take the row a lazy line is in. Kept in the
|
|
510
|
+
// file's order (re-review round 3 C1): paired before everything else, a
|
|
511
|
+
// lazy line took the first row reading like it, and an ordinary row that
|
|
512
|
+
// read the same as a lazy line below it swapped lines with it on a no-op.
|
|
513
|
+
let pos = null;
|
|
514
|
+
let curPos = null;
|
|
515
|
+
const lazyAt = origBody.findIndex((o) => o && o.lazy);
|
|
516
|
+
if (lazyAt !== -1) {
|
|
517
|
+
const n = origBody[lazyAt].fields.length;
|
|
518
|
+
const posKey = (get) => Array.from({ length: n }, (_, i) => keyOf(get(i))).join(SEP);
|
|
519
|
+
pos = new Map();
|
|
520
|
+
origBody.forEach((o, q) => {
|
|
521
|
+
if (!o) return;
|
|
522
|
+
const k = posKey((i) => (o.fields[i] === undefined ? '' : o.fields[i].trim()));
|
|
523
|
+
let e = pos.get(k);
|
|
524
|
+
if (!e) { e = { list: [], p1: 0, p2: 0 }; pos.set(k, e); }
|
|
525
|
+
e.list.push(q);
|
|
526
|
+
});
|
|
527
|
+
curPos = curRows.map((row) => posKey((i) => (row[i] === undefined ? '' : row[i])));
|
|
528
|
+
}
|
|
462
529
|
// (1) forward greedy: the first unused match at or after j. Every original
|
|
463
530
|
// used in this step lies before j, so a cursor that skips indices < j
|
|
464
531
|
// finds it, and it only moves forward.
|
|
532
|
+
const firstFrom = (e) => {
|
|
533
|
+
if (!e) return -1;
|
|
534
|
+
while (e.p1 < e.list.length && e.list[e.p1] < j) e.p1++;
|
|
535
|
+
return e.p1 < e.list.length ? e.p1 : -1;
|
|
536
|
+
};
|
|
465
537
|
let j = 0;
|
|
466
538
|
for (let c = 0; c < curRows.length; c++) {
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
if (
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
539
|
+
let e = pos ? pos.get(curPos[c]) : undefined;
|
|
540
|
+
let at = firstFrom(e);
|
|
541
|
+
if (at === -1) { e = byKey.get(curKey[c]); at = firstFrom(e); }
|
|
542
|
+
if (at === -1) continue;
|
|
543
|
+
const q = e.list[at];
|
|
544
|
+
e.p1 = at + 1;
|
|
545
|
+
pair[c] = q; used[q] = true; anchor[c] = true; j = q + 1;
|
|
474
546
|
}
|
|
475
547
|
// (2) a leftover row that matches an unused original anywhere (moved).
|
|
548
|
+
const firstUnused = (e) => {
|
|
549
|
+
if (!e) return -1;
|
|
550
|
+
while (e.p2 < e.list.length && used[e.list[e.p2]]) e.p2++;
|
|
551
|
+
return e.p2 < e.list.length ? e.list[e.p2++] : -1;
|
|
552
|
+
};
|
|
476
553
|
for (let c = 0; c < curRows.length; c++) {
|
|
477
554
|
if (pair[c] !== -1) continue;
|
|
478
|
-
|
|
479
|
-
if (
|
|
480
|
-
|
|
481
|
-
if (e.p2 < e.list.length) { const q = e.list[e.p2++]; pair[c] = q; used[q] = true; }
|
|
555
|
+
let q = pos ? firstUnused(pos.get(curPos[c])) : -1;
|
|
556
|
+
if (q === -1) q = firstUnused(byKey.get(curKey[c]));
|
|
557
|
+
if (q !== -1) { pair[c] = q; used[q] = true; }
|
|
482
558
|
}
|
|
483
559
|
// (3) the rows still left between two anchors take the unused originals
|
|
484
560
|
// between the same anchors, in order.
|
|
@@ -787,9 +863,15 @@
|
|
|
787
863
|
headerContents = h.contents;
|
|
788
864
|
headerWhole = h.whole;
|
|
789
865
|
const pair = pairBodyRows(bodyContentRows, src.body, colIdx, (x) => readingKey(parse, x));
|
|
866
|
+
// A lazy line's cells are positions in marked's split, not columns with
|
|
867
|
+
// a name, and its text is often in the file only (D7): a renamed header
|
|
868
|
+
// must not unmatch it. So it is mapped by position; every cell is still
|
|
869
|
+
// gated by readsSame(), so an edited cell is never overwritten.
|
|
870
|
+
const byPosition = headerContents.map((_, i) => (i < nOrigCols ? i : undefined));
|
|
790
871
|
bodyContentRows = bodyContentRows.map((canon, r) => {
|
|
791
872
|
const orig = pair[r] === -1 ? null : src.body[pair[r]];
|
|
792
|
-
const
|
|
873
|
+
const map = orig && orig.lazy ? byPosition : colIdx;
|
|
874
|
+
const b = reuseRow(parse, canon, bodyCellMd[r].map((x) => x.bad), orig, map, nOrigCols);
|
|
793
875
|
bodyWhole[r] = b.whole;
|
|
794
876
|
bodyOrig[r] = orig;
|
|
795
877
|
return b.contents;
|
|
@@ -810,7 +892,9 @@
|
|
|
810
892
|
// comparing a number against `undefined` is always false, so this
|
|
811
893
|
// loop leaves it alone and padField()'s `|| 0` fallback below
|
|
812
894
|
// gives it independent per-row minimal form instead.
|
|
813
|
-
|
|
895
|
+
// A lazy line that is written as itself (D7) is no column's content.
|
|
896
|
+
const allRows = [headerContents].concat(bodyContentRows.filter((_, r) =>
|
|
897
|
+
!(bodyWhole[r] && bodyOrig[r] && bodyOrig[r].lazy)));
|
|
814
898
|
for (let i = 0; i < widths.length; i++) {
|
|
815
899
|
let needed = widths[i];
|
|
816
900
|
allRows.forEach((row) => {
|
|
@@ -825,9 +909,10 @@
|
|
|
825
909
|
|
|
826
910
|
// F1: a row whose cells were all reused, in the original columns, is the
|
|
827
911
|
// original line itself (trimmed), padding and all, unless a column it
|
|
828
|
-
// holds was widened past that line's own field.
|
|
829
|
-
|
|
830
|
-
|
|
912
|
+
// holds was widened past that line's own field. A lazy line (D7) has no
|
|
913
|
+
// fields to pad, so it is always its own line.
|
|
914
|
+
const asWritten = (whole, orig) => whole && (orig.lazy || orig.fields.every((f, i) =>
|
|
915
|
+
!widths || widths[i] === undefined || widths[i] <= f.length));
|
|
831
916
|
const lines = [];
|
|
832
917
|
lines.push(asWritten(headerWhole, src && src.header) ? src.header.line
|
|
833
918
|
: serializeRow(headerContents, widths));
|
package/lib/md2doc.js
CHANGED
|
@@ -233,6 +233,117 @@ try {
|
|
|
233
233
|
],
|
|
234
234
|
});
|
|
235
235
|
|
|
236
|
+
// v3.17.9 D8: `[[ref-id, §sub]]` → a clickable citation, as an inline
|
|
237
|
+
// extension for the same reason as sub / sup above. It was a raw-text
|
|
238
|
+
// pre-pass over the whole file (mdPre), which rewrote it inside code spans,
|
|
239
|
+
// fenced code, HTML comments and tag attributes too: the code then showed
|
|
240
|
+
// the anchor's HTML as text, and the editor wrote that HTML back into the
|
|
241
|
+
// file on the first edit of the block. `text` is the anchor's visible text,
|
|
242
|
+
// escaped as the old html + text tokens were, so heading ids and the
|
|
243
|
+
// search index read the same strings as before. The body is literal: no
|
|
244
|
+
// bold / code / escapes inside a citation any more (the pre-pass left them
|
|
245
|
+
// to marked), which is what the editor writes back (isCitationEligible).
|
|
246
|
+
const escCiteAttr = (s) => String(s)
|
|
247
|
+
.replace(/&/g, '&')
|
|
248
|
+
.replace(/</g, '<')
|
|
249
|
+
.replace(/>/g, '>')
|
|
250
|
+
.replace(/"/g, '"');
|
|
251
|
+
marked.use({
|
|
252
|
+
extensions: [{
|
|
253
|
+
name: 'citation',
|
|
254
|
+
level: 'inline',
|
|
255
|
+
// No start(): marked's inline text rule already stops before every
|
|
256
|
+
// `[`, and this tokenizer is tried there. A start() that searched the
|
|
257
|
+
// rest of the paragraph for `[[` ran once per text token (re-review
|
|
258
|
+
// round 3 C3: quadratic, a 32000-character paragraph of `[a] ` took 0.2 s).
|
|
259
|
+
tokenizer(src) {
|
|
260
|
+
const m = /^\[\[([^\]\n]+)\]\]/.exec(src);
|
|
261
|
+
if (!m) return undefined;
|
|
262
|
+
const body = m[1].trim();
|
|
263
|
+
return { type: 'citation', raw: m[0], body, slug: body.split(',', 1)[0].trim(),
|
|
264
|
+
text: '[' + escCiteAttr(body) + ']' };
|
|
265
|
+
},
|
|
266
|
+
renderer(token) { return `<a href="#${escCiteAttr(token.slug)}">${token.text}</a>`; },
|
|
267
|
+
}],
|
|
268
|
+
});
|
|
269
|
+
// A citation inside a link label or an image alt: marked's label allows one
|
|
270
|
+
// nested bracket level and `[[...]]` is two, so `[see [[ref]]](#sec)` fell
|
|
271
|
+
// apart into text (the old pre-pass had removed one level first). Each
|
|
272
|
+
// citation in the label is masked as `\u0000[body]\u0000` (same length, one
|
|
273
|
+
// level), marked's own link tokenizer reads the masked text, and the link's
|
|
274
|
+
// raw / label come back from the real source: a link's label is re-lexed, so
|
|
275
|
+
// its citations render; an image's alt is the citations' visible text.
|
|
276
|
+
// A `[[` after an odd backslash run is escaped text, not a citation (the
|
|
277
|
+
// inline lexer reads `\[` as a literal bracket), so it is left unmasked.
|
|
278
|
+
// The mask is the regex /(\\*)\[\[([^\]\n]+)\]\]/g scanned by hand, in
|
|
279
|
+
// one pass (re-review round 3 C3): the tokenizer runs at every `[` of a
|
|
280
|
+
// paragraph, and the regex let each unclosed `[[` scan to the end of its
|
|
281
|
+
// line, so one 8000-character line of `[[a ` took 10 s. `stop` is the
|
|
282
|
+
// first `]` or newline at or after a body's start; it only moves forward.
|
|
283
|
+
const maskCitations = (s) => {
|
|
284
|
+
let out = '';
|
|
285
|
+
let from = 0;
|
|
286
|
+
let stop = -1;
|
|
287
|
+
for (let p = s.indexOf('[['); p !== -1; p = s.indexOf('[[', p)) {
|
|
288
|
+
if (stop < p + 2) {
|
|
289
|
+
stop = p + 2;
|
|
290
|
+
while (stop < s.length && s[stop] !== ']' && s[stop] !== '\n') stop++;
|
|
291
|
+
}
|
|
292
|
+
if (stop === p + 2 || s[stop] !== ']' || s[stop + 1] !== ']') { p++; continue; }
|
|
293
|
+
let bs = 0;
|
|
294
|
+
while (p - bs - 1 >= from && s[p - bs - 1] === '\\') bs++;
|
|
295
|
+
out += bs % 2 ? s.slice(from, stop + 2) : s.slice(from, p) + '\u0000[' + s.slice(p + 2, stop) + ']\u0000';
|
|
296
|
+
from = p = stop + 2;
|
|
297
|
+
}
|
|
298
|
+
return from ? out + s.slice(from) : s;
|
|
299
|
+
};
|
|
300
|
+
// Where the label of a link (or alt of an image) starting at `src` ends:
|
|
301
|
+
// the index of its closing `]`, or -1. marked's label allows escapes, code
|
|
302
|
+
// spans (at its own level only: inside a nested bracket a backtick is a
|
|
303
|
+
// plain character, re-review round 4) and one nested bracket level, and a
|
|
304
|
+
// masked citation is one level,
|
|
305
|
+
// so deeper than three raw levels is no label of marked's. Bounded so that
|
|
306
|
+
// a call costs about the distance to the next brackets: the tokenizer runs
|
|
307
|
+
// at the start of every token of a paragraph (re-review round 3 C3).
|
|
308
|
+
const labelEnd = (src) => {
|
|
309
|
+
const start = src[0] === '!' ? 1 : 0;
|
|
310
|
+
if (src[start] !== '[') return -1;
|
|
311
|
+
let depth = 0;
|
|
312
|
+
for (let i = start; i < src.length; i++) {
|
|
313
|
+
const ch = src[i];
|
|
314
|
+
if (ch === '\\') i++;
|
|
315
|
+
else if (ch === '`' && depth === 1) { const k = src.indexOf('`', i + 1); if (k !== -1) i = k; }
|
|
316
|
+
else if (ch === '[') { if (++depth > 3) return -1; }
|
|
317
|
+
else if (ch === ']') { if (--depth === 0) return i; }
|
|
318
|
+
}
|
|
319
|
+
return -1;
|
|
320
|
+
};
|
|
321
|
+
const defaultLink = marked.Tokenizer.prototype.link;
|
|
322
|
+
marked.use({ tokenizer: { link(src) {
|
|
323
|
+
// only an inline link or image whose label holds a `[[` is read here; a
|
|
324
|
+
// citation in its href or title stays literal (unmasked), as it is in a
|
|
325
|
+
// link whose label cites nothing (re-review round 3 C4)
|
|
326
|
+
const e = labelEnd(src);
|
|
327
|
+
if (e === -1 || src[e + 1] !== '(' || src.lastIndexOf('[[', e) === -1) return false;
|
|
328
|
+
const masked = maskCitations(src.slice(0, e + 1)) + src.slice(e + 1);
|
|
329
|
+
const tok = defaultLink.call(this, masked);
|
|
330
|
+
if (!tok || tok.raw.indexOf('\u0000') === -1) return false;
|
|
331
|
+
// a mask byte past the label means the scan disagreed with marked's
|
|
332
|
+
// label; leave the link to marked rather than leak the byte
|
|
333
|
+
if ((tok.href && tok.href.indexOf('\u0000') !== -1) || (tok.title && tok.title.indexOf('\u0000') !== -1)) return false;
|
|
334
|
+
tok.raw = src.slice(0, tok.raw.length);
|
|
335
|
+
if (tok.type === 'image') { tok.text = tok.text.replace(/\u0000/g, ''); return tok; }
|
|
336
|
+
// The label is lexed with its escapes in place (marked unescapes `\[`
|
|
337
|
+
// first, which would turn an escaped `\[[x]]` back into a citation).
|
|
338
|
+
const cap = this.rules.inline.link.exec(masked);
|
|
339
|
+
const label = src.substr(1, cap[1].length);
|
|
340
|
+
this.lexer.state.inLink = true;
|
|
341
|
+
tok.text = label.replace(/\\([\[\]])/g, '$1');
|
|
342
|
+
tok.tokens = this.lexer.inlineTokens(label);
|
|
343
|
+
this.lexer.state.inLink = false;
|
|
344
|
+
return tok;
|
|
345
|
+
} } });
|
|
346
|
+
|
|
236
347
|
// $$…$$ (display) and $…$ (inline) math via KaTeX. marked tokenizes code
|
|
237
348
|
// spans/fences first, so $ inside code stays literal; the extension's
|
|
238
349
|
// default no-space-adjacency rules keep prose currency ($5 to $10) unrendered.
|
|
@@ -887,6 +998,14 @@ ${itemsHtml}
|
|
|
887
998
|
if (item.type === 'codespan') {
|
|
888
999
|
return unescapeHtml(item.text || '');
|
|
889
1000
|
}
|
|
1001
|
+
// v3.17.9 D8: what the old [[...]] text pre-pass left here, an html
|
|
1002
|
+
// token's markup plus the text, so classifyColumns() (it measures the
|
|
1003
|
+
// markup: a citation column often reads as prose) and heading ids
|
|
1004
|
+
// (stripHtmlTags() removes it) see the strings they always did. A
|
|
1005
|
+
// patch release does not re-lay-out every table that cites.
|
|
1006
|
+
if (item.type === 'citation') {
|
|
1007
|
+
return '<a href="#' + item.slug + '">[' + item.body + ']</a>';
|
|
1008
|
+
}
|
|
890
1009
|
if (item.tokens) {
|
|
891
1010
|
return flattenTokenText(item.tokens);
|
|
892
1011
|
}
|
|
@@ -1361,35 +1480,26 @@ ${itemsHtml}
|
|
|
1361
1480
|
// concurrently, unlike the one-shot CLI).
|
|
1362
1481
|
marked.setOptions({ gfm: true, breaks: false, renderer });
|
|
1363
1482
|
|
|
1364
|
-
//
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
.replace(/>/g, '>')
|
|
1369
|
-
.replace(/"/g, '"');
|
|
1370
|
-
const mdPre = md
|
|
1371
|
-
.replace(/\[\[([^\]\n]+)\]\]/g, (_, inner) => { // [[ref-id, §sub]] → clickable citation
|
|
1372
|
-
const body = inner.trim();
|
|
1373
|
-
const slug = body.split(',', 1)[0].trim();
|
|
1374
|
-
return `<a href="#${escAttr(slug)}">[${escAttr(body)}]</a>`;
|
|
1375
|
-
});
|
|
1483
|
+
// `[[ref-id, §sub]]` citations are the `citation` inline extension
|
|
1484
|
+
// installed at require time (v3.17.9 D8); there is no text pre-pass, so
|
|
1485
|
+
// marked reads the file's own bytes and the block map's lines are the
|
|
1486
|
+
// file's lines.
|
|
1376
1487
|
|
|
1377
1488
|
// A $$ block's TeX reaches the index through the blockKatex hook at the
|
|
1378
1489
|
// top of the file, set only while this render's synchronous parse runs.
|
|
1379
1490
|
if (opts.editMode) {
|
|
1380
|
-
// Ranges are computed on
|
|
1381
|
-
//
|
|
1382
|
-
//
|
|
1383
|
-
// (guarded by the '[[...]] must not shift lines' test).
|
|
1491
|
+
// Ranges are computed on the file's own text (no pre-pass since
|
|
1492
|
+
// v3.17.9), so they are the lines the client holds (still guarded by
|
|
1493
|
+
// the '[[...]] must not shift lines' test).
|
|
1384
1494
|
const { buildBlockMap } = require('./editor/blockmap.js');
|
|
1385
|
-
blocks = buildBlockMap(
|
|
1495
|
+
blocks = buildBlockMap(md).blocks;
|
|
1386
1496
|
// marked.lexer() inline-lexes as part of block lexing (verified against
|
|
1387
1497
|
// marked 14.1.4: a paragraph token's `.tokens` already holds resolved
|
|
1388
1498
|
// inline tokens such as `strong`/`link`, not raw text) — so
|
|
1389
1499
|
// marked.parser([t]) on one already-lexed top-level token reproduces
|
|
1390
1500
|
// exactly what the whole-document parse would emit for that token,
|
|
1391
1501
|
// using the same renderer/options set above.
|
|
1392
|
-
const tokens = marked.lexer(
|
|
1502
|
+
const tokens = marked.lexer(md);
|
|
1393
1503
|
parts = [];
|
|
1394
1504
|
// `v` is the block cursor; `prevLi`/`types` are the §3.8 run state the
|
|
1395
1505
|
// flat list renderer above threads through its own recursion (see
|
|
@@ -1465,7 +1575,7 @@ ${itemsHtml}
|
|
|
1465
1575
|
} else {
|
|
1466
1576
|
try {
|
|
1467
1577
|
blockMathText = appendSectionText;
|
|
1468
|
-
bodyHtml = marked.parse(
|
|
1578
|
+
bodyHtml = marked.parse(md);
|
|
1469
1579
|
} finally {
|
|
1470
1580
|
blockMathText = null;
|
|
1471
1581
|
}
|
|
@@ -6790,7 +6900,7 @@ ${usesDrawioMultiPage ? ` // v3.4.0 batch2 Task 6: wired through a NAMED, reusa
|
|
|
6790
6900
|
// v3.4.0 §4: same short-circuit story for drawio. editMode already baked
|
|
6791
6901
|
// every part above, so both calls below are no-ops there (no browser is
|
|
6792
6902
|
// opened) — this is only load-bearing for the non-editMode
|
|
6793
|
-
// `bodyHtml = marked.parse(
|
|
6903
|
+
// `bodyHtml = marked.parse(md)` path, which never went through the
|
|
6794
6904
|
// per-part loop. `html` and `bodyHtml` share the same placeholders (html
|
|
6795
6905
|
// embeds bodyHtml), so both bakes share one lazily-opened browser instead
|
|
6796
6906
|
// of paying for two.
|