@helping-ai-workflow/md2doc 3.17.6 → 3.17.7

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.
@@ -154,6 +154,13 @@
154
154
  return inner.indexOf(']') === -1;
155
155
  }
156
156
 
157
+ // The test serializeAnchor() uses to write `[[body]]` (v3.17.7: shared
158
+ // with serializeInline()'s read-back check).
159
+ function isCitationNode(node) {
160
+ const href = typeof node.getAttribute === 'function' ? node.getAttribute('href') : null;
161
+ const text = node.textContent;
162
+ return !!href && href.charAt(0) === '#' && /^\[.*\]$/.test(text) && isCitationEligible(node, text);
163
+ }
157
164
  function serializeAnchor(node, unsupported, opts) {
158
165
  const href = node.getAttribute('href');
159
166
  // A raw HTML anchor (`<a id="ref-x"></a>`, NO href attribute) is not a
@@ -391,10 +398,16 @@
391
398
  if (typeof node.getAttribute !== 'function') return false;
392
399
  return (' ' + (node.getAttribute('class') || '') + ' ').indexOf(' search-hit ') !== -1;
393
400
  }
394
- function walkChildren(nodes, unsupported, opts) {
401
+ // `ctx` (v3.17.7) says how a STRONG / EM nested inside another STRONG / EM
402
+ // is spelled; see serializeInline(). Absent means the plain `*` spelling.
403
+ // `touching` (v3.17.7) is the mark tag that ends right before these
404
+ // nodes, for a transparent wrapper whose first child may need the
405
+ // same-tag guard against it.
406
+ function walkChildren(nodes, unsupported, opts, ctx, touching) {
407
+ const c = ctx || PLAIN;
395
408
  let out = '';
396
409
  let firstSegment = true;
397
- let lastMarkTag = null;
410
+ let lastMarkTag = touching || null;
398
411
  for (let i = 0; i < nodes.length; i++) {
399
412
  const node = nodes[i];
400
413
  if (node.nodeType === 3) {
@@ -409,7 +422,7 @@
409
422
  if (name === 'DIV') {
410
423
  let added = '';
411
424
  if (!firstSegment) added += '<br>';
412
- const inner = walkChildren(node.childNodes, unsupported, opts);
425
+ const inner = walkChildren(node.childNodes, unsupported, opts, c);
413
426
  added += inner.md;
414
427
  out = appendPiece(out, added);
415
428
  if (added.length) lastMarkTag = inner.md.length ? inner.lastMarkTag : null;
@@ -419,7 +432,7 @@
419
432
  if (name === 'MARK' && markIsSearchHit(node)) {
420
433
  // v3.14.0 T9: the reader's search paints its hits as this element and
421
434
  // it is transient, so it serializes as what it wraps.
422
- const inner = walkChildren(node.childNodes, unsupported, opts);
435
+ const inner = walkChildren(node.childNodes, unsupported, opts, c, lastMarkTag);
423
436
  out = appendPiece(out, inner.md);
424
437
  if (inner.md.length) lastMarkTag = inner.lastMarkTag;
425
438
  firstSegment = false;
@@ -429,7 +442,7 @@
429
442
  if (spanHasAttributes(node)) {
430
443
  unsupported.push(name);
431
444
  } else {
432
- const inner = walkChildren(node.childNodes, unsupported, opts);
445
+ const inner = walkChildren(node.childNodes, unsupported, opts, c, lastMarkTag);
433
446
  out = appendPiece(out, inner.md);
434
447
  if (inner.md.length) lastMarkTag = inner.lastMarkTag;
435
448
  }
@@ -438,12 +451,12 @@
438
451
  }
439
452
  if (name === 'STRONG' || name === 'B' || name === 'EM' || name === 'I') {
440
453
  const tag = (name === 'STRONG' || name === 'B') ? 'STRONG' : 'EM';
441
- const delim = tag === 'STRONG' ? '**' : '*';
442
- const e = splitEdges(walkChildren(node.childNodes, unsupported, opts).md);
454
+ const e = splitEdges(walkChildren(node.childNodes, unsupported, opts, c.inner).md);
443
455
  if (e.core) {
444
456
  // a hoisted leading space already separates two same-tag runs
445
457
  const guard = !e.lead && lastMarkTag === tag ? '<!-- -->' : '';
446
- out = appendPiece(out, e.lead + guard + delim + e.core + delim + e.trail);
458
+ const delim = c.delims[tag];
459
+ out = appendPiece(out, e.lead + guard + c.before + delim + e.core + delim + c.after + e.trail);
447
460
  lastMarkTag = e.trail ? null : tag;
448
461
  } else if (e.lead || e.trail) {
449
462
  out = appendPiece(out, e.lead + e.trail);
@@ -458,9 +471,15 @@
458
471
  // accepted on input (some contenteditable/paste paths produce it)
459
472
  // but the toolbar/serializer always speak DEL, matching what marked
460
473
  // itself emits.
461
- const e = splitEdges(walkChildren(node.childNodes, unsupported, opts).md);
462
- out = appendPiece(out, e.core ? e.lead + '~~' + e.core + '~~' + e.trail : e.lead + e.trail);
463
- if (e.core || e.lead || e.trail) lastMarkTag = null;
474
+ //
475
+ // v3.17.7 N2: two strikethroughs side by side get the same guard as
476
+ // two bolds. `~~a~~~~b~~` reads as ONE del holding `a~~~~b` (measured
477
+ // on 71fedaa: strike `two` right after `~~one~~` wrote exactly that).
478
+ const e = splitEdges(walkChildren(node.childNodes, unsupported, opts, c).md);
479
+ const guard = e.core && !e.lead && lastMarkTag === 'DEL' ? '<!-- -->' : '';
480
+ out = appendPiece(out, e.core ? e.lead + guard + '~~' + e.core + '~~' + e.trail : e.lead + e.trail);
481
+ if (e.core) lastMarkTag = e.trail ? null : 'DEL';
482
+ else if (e.lead || e.trail) lastMarkTag = null;
464
483
  firstSegment = false;
465
484
  continue;
466
485
  }
@@ -475,14 +494,18 @@
475
494
  // backtick, literal `<u>` typed as plain text is already escaped by
476
495
  // the `<` -> `&lt;` rule above, so it can never collide with a real
477
496
  // toolbar-made underline.
478
- out = appendPiece(out, '<u>' + walkChildren(node.childNodes, unsupported, opts).md + '</u>');
497
+ out = appendPiece(out, '<u>' + walkChildren(node.childNodes, unsupported, opts, c).md + '</u>');
479
498
  lastMarkTag = null;
480
499
  firstSegment = false;
481
500
  continue;
482
501
  }
483
502
  if (name === 'CODE') {
484
- out = appendPiece(out, serializeCode(node, opts));
485
- lastMarkTag = null;
503
+ // v3.17.7 N3: two code spans side by side are one span to a reader
504
+ // (`` `one``two` `` is the code "one``two"), so they get a guard too.
505
+ // serializeCode() never writes an empty span, so this always fires
506
+ // between two real ones.
507
+ out = appendPiece(out, (lastMarkTag === 'CODE' ? '<!-- -->' : '') + serializeCode(node, opts));
508
+ lastMarkTag = 'CODE';
486
509
  firstSegment = false;
487
510
  continue;
488
511
  }
@@ -536,13 +559,227 @@
536
559
  return { md: out, lastMarkTag };
537
560
  }
538
561
 
562
+ // ── v3.17.7 N1: check the bytes with the reader's own parser ─────────────
563
+ //
564
+ // A STRONG / EM nested inside another STRONG / EM is written `*` / `**`
565
+ // like any other, and most of the time marked reads that back. Not always:
566
+ // marked's emphasis scan (emStrong in marked 14) walks from an opening run
567
+ // to its closer counting the `*` runs in between, and a run with word
568
+ // characters on both sides is a "mid" run it may skip under the rule of 3.
569
+ // Whether a nested mark survives therefore depends on the characters
570
+ // around its runs. Measured on 71fedaa, each from a toolbar gesture:
571
+ // `*`c`**i**tal*` -> the em is lost, its stars show
572
+ // `*~~a**a**~~*` -> `~~` is invisible to that scan
573
+ // `前面「引號<!-- -->*」**粗**後面,中文*。`
574
+ // while `*a**i**tal*` and `***ital* bold**` read fine. The rules are
575
+ // marked's, so marked decides: serializeInline() writes the bytes as
576
+ // before, parses them, and compares the marks on every character with the
577
+ // DOM it was given. Only when they differ does it try other spellings for
578
+ // the nested marks, in this order, keeping the first that reads back:
579
+ // 1. `__` / `_` for every STRONG / EM inside another (those runs are
580
+ // not `*`, so the outer scan does not count them);
581
+ // 2. the same with `<!-- -->` on both sides of each such mark (a run
582
+ // next to `<` is no longer flanked by a word character), then each of
583
+ // those guards dropped again if the bytes still read back without it.
584
+ // If neither reads back the original bytes are kept: what this release
585
+ // wrote before. Bytes that already read back are never touched, so no
586
+ // file written by an earlier release changes by being opened and saved.
587
+ // A fuzz over random strong / em / del / code trees (inline-md.test.js;
588
+ // 7725 of 20000 read back wrong on 71fedaa) needed nothing beyond step 2.
589
+ //
590
+ // A repaired spelling never keeps `<!--` at the start of a line: marked
591
+ // reads a line-start `<!--` as an HTML block and ends the paragraph there
592
+ // (v3.17.5 C1), and parseInline() cannot see that. It does not have to:
593
+ // the only guards step 2 adds sit before an opening run or after a
594
+ // closing one, and after a newline the character before an opening run
595
+ // is whitespace, which already does what the guard's `>` does (no
596
+ // right-flanking, opening allowed). So a step-2 guard at a line start is
597
+ // dropped before anything is parsed, whether or not the drop pass below
598
+ // runs (re-review: with the pass skipped past 16 guards, one survived
599
+ // after a hard break and the reader cut the paragraph there). The walk
600
+ // itself never writes one (needsFlankSeparator() sees `\n` as a non-word
601
+ // character).
602
+ //
603
+ // The parser is a private `new Marked()` with gfm on and nothing else, in
604
+ // node and in the editor page alike (server.js injects marked's UMD build
605
+ // before this file). The reader's own instance carries the md2doc
606
+ // renderer, whose text hook writes into the render's search index, and
607
+ // its sub / sup / KaTeX extensions only see `~` / `^` / `$`, which this
608
+ // comparison is not about. No parser (a page without marked) means no
609
+ // check: the bytes are written as before.
610
+ const GUARD = '<!-- -->';
611
+ const STAR = { STRONG: '**', EM: '*' };
612
+ const UNDERSCORE = { STRONG: '__', EM: '_' };
613
+ // PLAIN.inner is PLAIN: the spelling every release before this one used.
614
+ const PLAIN = { delims: STAR, before: '', after: '', inner: null };
615
+ PLAIN.inner = PLAIN;
616
+ function spelling(before) {
617
+ const nested = { delims: UNDERSCORE, before, after: before, inner: null };
618
+ nested.inner = nested;
619
+ return { delims: STAR, before: '', after: '', inner: nested };
620
+ }
621
+ const NESTED_UNDERSCORE = spelling('');
622
+ // One character stands in for each step-2 guard until it is decided, so
623
+ // only those guards are ever dropped, never one the walk wrote itself. It
624
+ // must be punctuation to needsFlankSeparator(), as the `<` it stands for
625
+ // is: with a word-like stand-in the walk skips a guard the real bytes need
626
+ // (measured: `ital**<!-- -->_…` lost the `<!-- -->` before its `**`).
627
+ // U+2E3B THREE-EM DASH (\p{Pd}); a block whose text holds one skips step 2.
628
+ const PENDING = '\u2E3B';
629
+ const NESTED_GUARDED = spelling(PENDING);
630
+
631
+ let inlineParser; // undefined: not looked up yet; null: none available
632
+ function getInlineParser() {
633
+ if (inlineParser !== undefined) return inlineParser;
634
+ inlineParser = null;
635
+ try {
636
+ let lib = null;
637
+ if (typeof module === 'object' && module.exports && typeof require === 'function') lib = require('marked');
638
+ else if (typeof self !== 'undefined' && self.marked) lib = self.marked;
639
+ if (lib && typeof lib.Marked === 'function') {
640
+ const m = new lib.Marked({ gfm: true, breaks: false });
641
+ inlineParser = (s) => m.parseInline(s);
642
+ }
643
+ } catch (e) {
644
+ inlineParser = null;
645
+ }
646
+ return inlineParser;
647
+ }
648
+ // Tests swap the parser: a function, null to switch the check off, or
649
+ // nothing to go back to the lookup above.
650
+ function setInlineParser(fn) {
651
+ inlineParser = typeof fn === 'function' ? fn : (fn === null ? null : undefined);
652
+ }
653
+
654
+ // One entry per non-space character: the character and the sorted set of
655
+ // S(trong) / E(m) / D(el) around it. A code span is one entry holding its
656
+ // text without whitespace (a reader strips one space at each end, and
657
+ // spacing is not what this check is about). Whitespace is left out
658
+ // (splitEdges() moves it across a delimiter on purpose), and so are line
659
+ // breaks, link targets and <u>. A citation is written `[[body]]`, which
660
+ // this private parser (no md2doc extension) reads as that literal text,
661
+ // so its DOM side counts the outer brackets too: without them the two
662
+ // sides never agreed and any block holding a citation was never repaired
663
+ // (final review).
664
+ const MARK_LETTER = { STRONG: 'S', B: 'S', EM: 'E', I: 'E', DEL: 'D', S: 'D' };
665
+ const SPACE = /\s/;
666
+ function domMarks(nodes, marks, out) {
667
+ for (let i = 0; i < nodes.length; i++) {
668
+ const node = nodes[i];
669
+ if (node.nodeType === 3) {
670
+ for (const ch of node.textContent) if (!SPACE.test(ch)) out.push(ch + '[' + marks + ']');
671
+ continue;
672
+ }
673
+ if (node.nodeType !== 1) continue;
674
+ if (node.nodeName === 'CODE') { out.push('`' + node.textContent.replace(/\s+/g, '') + '[' + marks + ']'); continue; }
675
+ if (node.nodeName === 'A' && isCitationNode(node)) {
676
+ for (const ch of '[' + node.textContent + ']') if (!SPACE.test(ch)) out.push(ch + '[' + marks + ']');
677
+ continue;
678
+ }
679
+ const k = MARK_LETTER[node.nodeName];
680
+ const inner = k && marks.indexOf(k) === -1 ? (marks + k).split('').sort().join('') : marks;
681
+ domMarks(node.childNodes, inner, out);
682
+ }
683
+ return out;
684
+ }
685
+ const HTML_TAG_MARK = { strong: 'S', em: 'E', del: 'D' };
686
+ const HTML_ENTITY = { amp: '&', lt: '<', gt: '>', quot: '"', '#39': "'" };
687
+ function htmlMarks(html) {
688
+ const out = [];
689
+ const open = [];
690
+ let code = null;
691
+ const re = /<!--[\s\S]*?-->|<(\/?)([a-zA-Z][a-zA-Z0-9]*)\b[^>]*>|&(amp|lt|gt|quot|#39);|([^<&]+|[<&])/g;
692
+ const current = () => open.slice().sort().filter((k, i, a) => a.indexOf(k) === i).join('');
693
+ let m;
694
+ while ((m = re.exec(html))) {
695
+ if (m[0].startsWith('<!--')) continue;
696
+ if (m[2]) {
697
+ const tag = m[2].toLowerCase();
698
+ if (tag === 'code') {
699
+ if (m[1]) { out.push('`' + (code || '').replace(/\s+/g, '') + '[' + current() + ']'); code = null; } else code = '';
700
+ continue;
701
+ }
702
+ const k = HTML_TAG_MARK[tag];
703
+ if (!k) continue;
704
+ if (m[1]) { const at = open.lastIndexOf(k); if (at !== -1) open.splice(at, 1); } else open.push(k);
705
+ continue;
706
+ }
707
+ const t = m[3] ? HTML_ENTITY[m[3]] : m[4];
708
+ if (code !== null) { code += t; continue; }
709
+ for (const ch of t) if (!SPACE.test(ch)) out.push(ch + '[' + current() + ']');
710
+ }
711
+ return out;
712
+ }
713
+
539
714
  // `opts` is optional; its one field, `codePipe`, is described at
540
715
  // serializeCode(). Every caller but table-md.js passes nothing.
541
716
  function serializeInline(rootEl, opts) {
542
717
  const unsupported = [];
543
718
  const md = walkChildren(rootEl.childNodes, unsupported, opts).md;
719
+ // The spellings below only differ where a STRONG / EM sits inside another
720
+ // one, so a block without that is written as walked, unparsed: no cost
721
+ // on the per-keystroke shortcut check or for callers that only want
722
+ // `unsupported`.
723
+ if (unsupported.length || !hasNestedStar(rootEl.childNodes, false)) return { md, unsupported };
724
+ const parse = getInlineParser();
725
+ if (!parse) return { md, unsupported };
726
+ const want = domMarks(rootEl.childNodes, '', []).join('|');
727
+ // table-md.js writes a code `|` as its placeholder; the cell shows `|`
728
+ // (GFM drops the `\` of `\|` before the cell is parsed inline).
729
+ const pipe = opts && opts.codePipe;
730
+ const readsBack = (s) => {
731
+ try {
732
+ return htmlMarks(parse(pipe ? s.split(pipe).join('|') : s)).join('|') === want;
733
+ } catch (e) {
734
+ return false;
735
+ }
736
+ };
737
+ if (readsBack(md)) return { md, unsupported };
738
+ const one = walkChildren(rootEl.childNodes, [], opts, NESTED_UNDERSCORE).md;
739
+ if (readsBack(one)) return { md: one, unsupported };
740
+ if (domText(rootEl.childNodes).indexOf(PENDING) === -1) {
741
+ const parts = walkChildren(rootEl.childNodes, [], opts, NESTED_GUARDED).md.split(PENDING);
742
+ // guard k sits right after parts[k]; none is kept at a line start
743
+ let before = '';
744
+ const keep = parts.slice(1).map((_, k) => {
745
+ before += parts[k];
746
+ return !/(^|\n)[ \t]*$/.test(before);
747
+ });
748
+ const build = () => parts.reduce((acc, part, i) =>
749
+ (i ? acc + (keep[i - 1] ? GUARD : '') : '') + part, '');
750
+ if (readsBack(build())) {
751
+ // Each drop is one more parse of the whole block. Past a handful of
752
+ // guards keep them all: correct bytes, a few extra `<!-- -->`
753
+ // (final review measured 400 repaired marks at 3.4 s otherwise).
754
+ for (let i = 0; i < keep.length && keep.length <= MAX_GUARDS_TO_DROP; i++) {
755
+ if (!keep[i]) continue;
756
+ keep[i] = false;
757
+ if (!readsBack(build())) keep[i] = true;
758
+ }
759
+ return { md: build(), unsupported };
760
+ }
761
+ }
544
762
  return { md, unsupported };
545
763
  }
764
+ const MAX_GUARDS_TO_DROP = 16;
765
+ const STAR_TAGS = { STRONG: 1, B: 1, EM: 1, I: 1 };
766
+ // Whether a STRONG / EM sits inside another one, through any wrapper the
767
+ // walk spells with the context it was given (not CODE, not a link label).
768
+ function hasNestedStar(nodes, inStar) {
769
+ for (let i = 0; i < nodes.length; i++) {
770
+ const node = nodes[i];
771
+ if (node.nodeType !== 1 || node.nodeName === 'CODE' || node.nodeName === 'A') continue;
772
+ const star = STAR_TAGS[node.nodeName] === 1;
773
+ if (star && inStar) return true;
774
+ if (hasNestedStar(node.childNodes, inStar || star)) return true;
775
+ }
776
+ return false;
777
+ }
778
+ function domText(nodes) {
779
+ let s = '';
780
+ for (let i = 0; i < nodes.length; i++) s += nodes[i].textContent || '';
781
+ return s;
782
+ }
546
783
 
547
784
  function canWysiwyg(rootEl) {
548
785
  return serializeInline(rootEl).unsupported.length === 0;
@@ -706,5 +943,5 @@
706
943
  return out;
707
944
  }
708
945
 
709
- return { serializeInline, canWysiwyg, escapeText, escapeBlockStarts, normalizeHref, isPastedUrl, parseImageParagraph, rewriteImage, setNamedReferenceDecoder };
946
+ return { serializeInline, canWysiwyg, escapeText, escapeBlockStarts, normalizeHref, isPastedUrl, parseImageParagraph, rewriteImage, setNamedReferenceDecoder, setInlineParser };
710
947
  });
@@ -215,8 +215,47 @@ function isHeadingRow(tr) {
215
215
  // against the header. Escape any `|` that isn't already an escaped `\|`
216
216
  // (the latter can arrive when a nested table's own already-escaped cell
217
217
  // content is flattened into its parent cell, see cellMarkdown below).
218
+ //
219
+ // v3.17.7 F2: "already escaped" means an ODD run of backslashes before the
220
+ // `|`, which is how marked's splitCells() decides it (the parity rule
221
+ // v3.17.6 wrote down in table-md.js). The old test was one backslash, any
222
+ // run: text `a \| b` (turndown writes its backslash as `\\`) came out
223
+ // `a \\| b`, an even run, and the cell was cut in two (measured on 71fedaa).
218
224
  function escapePipes(s) {
219
- return String(s).replace(/(?<!\\)\|/g, '\\|');
225
+ return String(s).replace(/(\\*)\|/g, function (m, run) {
226
+ return run.length % 2 ? m : run + '\\|';
227
+ });
228
+ }
229
+
230
+ // v3.17.7 F2: code whose text has an odd backslash run before a `|` has no
231
+ // spelling in a GFM table cell. The backslash that keeps the pipe in the
232
+ // cell is the one GFM deletes before the code span is read, so the code
233
+ // shows one backslash fewer (or, with one backslash more, the run turns even
234
+ // and the pipe ends the cell); table-md.js refuses the same code (v3.17.6).
235
+ // Paste cannot refuse, so such a span is written as text with EVERY ASCII
236
+ // punctuation character backslash-escaped (CommonMark allows that for all
237
+ // of them): then nothing in it is read as markup -- not `<!--`, not an
238
+ // entity, not md2doc's `^sup^` / `~sub~` / `$math$`. turndown's own
239
+ // escape() leaves those alone (final review: `a<!-- \| -->` lost its text,
240
+ // `^2^` became superscript). The `|` comes out `\|`, an odd run, which
241
+ // escapePipes() then leaves as it is. Every character arrives; only the
242
+ // code style does not.
243
+ var ODD_BACKSLASH_PIPE = /(?:^|[^\\])(?:\\\\)*\\\|/;
244
+ function addUnspellableCellCodeRule(service) {
245
+ service.addRule('unspellableCellCode', {
246
+ filter: function (node) {
247
+ if (node.nodeName !== 'CODE') return false;
248
+ if (node.parentNode && node.parentNode.nodeName === 'PRE') return false;
249
+ if (!ODD_BACKSLASH_PIPE.test(node.textContent || '')) return false;
250
+ for (var p = node.parentNode; p; p = p.parentNode) {
251
+ if (p.nodeName === 'TD' || p.nodeName === 'TH') return true;
252
+ }
253
+ return false;
254
+ },
255
+ replacement: function (content, node) {
256
+ return (node.textContent || '').replace(/[!-\/:-@\[-`{-~]/g, '\\$&');
257
+ }
258
+ });
220
259
  }
221
260
 
222
261
  // Only the ROWS THIS TABLE OWNS -- direct <tr> children, and <tr> children
@@ -345,6 +384,7 @@ function htmlToMarkdown(html) {
345
384
  addStrikethroughRule(service);
346
385
  addFlankingEmphasisRules(service);
347
386
  addTableRules(service);
387
+ addUnspellableCellCodeRule(service);
348
388
  return service.turndown(html == null ? '' : String(html));
349
389
  }
350
390
 
@@ -8,6 +8,12 @@ const { renderMarkdown, buildKatexStyleTag, diagramEnginePaths } = require('../m
8
8
  const { createDotPreview } = require('./dot-preview.js');
9
9
 
10
10
  const LINEOPS_SRC = fs.readFileSync(path.join(__dirname, 'lineops.js'), 'utf8');
11
+ // v3.17.7: marked's browser build (the package's exported `marked.min.js`,
12
+ // UMD, defines the global `marked`), so inline-md.js can parse the bytes it
13
+ // is about to save and check that the reader shows the marks the editor held
14
+ // (serializeInline()). Injected before inline-md.js; inline-md.js looks the
15
+ // global up at its first serialize, so the order is for reading, not load.
16
+ const MARKED_SRC = fs.readFileSync(require.resolve('marked/marked.min.js'), 'utf8');
11
17
  const INLINE_MD_SRC = fs.readFileSync(path.join(__dirname, 'inline-md.js'), 'utf8');
12
18
  const TABLE_MD_SRC = fs.readFileSync(path.join(__dirname, 'table-md.js'), 'utf8');
13
19
  // Task 4 (Phase 3): list-md.js's browser factory reads `root.md2docInlineMd`
@@ -776,6 +782,7 @@ async function createEditorServer({
776
782
  const inject =
777
783
  `<script>window.__ED__ = ${payload.replace(/</g, '\\u003c')}</script>\n` +
778
784
  `<script>${LINEOPS_SRC}</script>\n` +
785
+ `<script>${MARKED_SRC}</script>\n` +
779
786
  `<script>${INLINE_MD_SRC}</script>\n` +
780
787
  `<script>${TABLE_MD_SRC}</script>\n` +
781
788
  `<script>${LIST_MD_SRC}</script>\n` +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helping-ai-workflow/md2doc",
3
- "version": "3.17.6",
3
+ "version": "3.17.7",
4
4
  "description": "Markdown → HTML / PDF renderer with WaveDrom, Mermaid, Graphviz, and draw.io support",
5
5
  "keywords": [
6
6
  "markdown",