@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.
- package/lib/editor/inline-md.js +252 -15
- package/lib/editor/paste-md.js +41 -1
- package/lib/editor/server.js +7 -0
- package/package.json +1 -1
package/lib/editor/inline-md.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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 `<` -> `<` 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
|
-
|
|
485
|
-
|
|
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
|
});
|
package/lib/editor/paste-md.js
CHANGED
|
@@ -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(/(
|
|
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
|
|
package/lib/editor/server.js
CHANGED
|
@@ -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` +
|