@helping-ai-workflow/md2doc 3.10.0 → 3.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/editor/client.js +769 -178
- package/lib/editor/convert-md.js +18 -11
- package/lib/editor/inline-md.js +8 -0
- package/lib/editor/list-md.js +26 -1
- package/lib/md2doc.js +93 -4
- package/package.json +1 -1
package/lib/editor/convert-md.js
CHANGED
|
@@ -91,19 +91,24 @@ function stripMarker(sourceLines, blockKind) {
|
|
|
91
91
|
if (blockKind === 'li') {
|
|
92
92
|
const m = src[0].match(LI_MARKER);
|
|
93
93
|
if (!m) return { content: [], ok: false };
|
|
94
|
-
// A MULTI-LINE li refuses. The previous code flattened continuations with
|
|
95
|
-
// replace(/^\s+/, ''), which is exactly the guess the code branch below
|
|
96
|
-
// refuses to make for an indented code block -- and it silently destroys
|
|
97
|
-
// whatever structure the continuation carried (a nested fence, an indented
|
|
98
|
-
// sub-block). Today the \u00a74.1 gate refuses a multi-line li as an operation
|
|
99
|
-
// target before this is ever reached, so this is defence in depth; dead
|
|
100
|
-
// code in this repo has a history of later gaining a caller, and it must
|
|
101
|
-
// not be a mine when it does.
|
|
102
|
-
if (src.length > 1) return { content: [], ok: false };
|
|
103
94
|
let first = src[0].slice(m[0].length);
|
|
104
95
|
const box = first.match(TASK_BOX);
|
|
105
96
|
if (box) first = first.slice(box[0].length);
|
|
106
|
-
|
|
97
|
+
// v3.11: a MULTI-LINE li (a hard-wrapped item, or a hard break) converts
|
|
98
|
+
// too. Its continuation lines belong to the item's own paragraph — any
|
|
99
|
+
// nested block would have made the run unsupported and refused before
|
|
100
|
+
// this — so each loses exactly the item's CONTENT column (the marker's
|
|
101
|
+
// width: anything deeper keeps its relative indent) or, on a lazy line
|
|
102
|
+
// indented less than that, all of its leading whitespace. That is not the
|
|
103
|
+
// old blanket replace(/^\s+/, '') the code branch below refuses to guess
|
|
104
|
+
// with: an over-indented line keeps its extra columns.
|
|
105
|
+
const col = m[0].length;
|
|
106
|
+
const rest = src.slice(1).map((l) => {
|
|
107
|
+
let k = 0;
|
|
108
|
+
while (k < col && l.charAt(k) === ' ') k++;
|
|
109
|
+
return k === col ? l.slice(col) : l.replace(/^\s+/, '');
|
|
110
|
+
});
|
|
111
|
+
return { content: [first].concat(rest), ok: true };
|
|
107
112
|
}
|
|
108
113
|
|
|
109
114
|
if (blockKind === 'heading') {
|
|
@@ -160,7 +165,9 @@ function emitAs(content, target, opts) {
|
|
|
160
165
|
// A heading is one line by definition. Joining is the only lossless move
|
|
161
166
|
// available; splitting would make one gesture produce two blocks.
|
|
162
167
|
const hashes = '#'.repeat(Number(target.slice(1)));
|
|
163
|
-
|
|
168
|
+
// A multi-line body (a converted hard-wrapped item) becomes one heading
|
|
169
|
+
// line; trailing hard-break spaces would otherwise land mid-heading.
|
|
170
|
+
const text = body.length > 1 ? body.map((l) => l.trim()).join(' ') : body.join(' ');
|
|
164
171
|
// An EMPTY heading emits no trailing space, or converting '##' to a
|
|
165
172
|
// heading of the same level would rewrite the line to '## '.
|
|
166
173
|
return [text === '' ? prefix + hashes : prefix + hashes + ' ' + text];
|
package/lib/editor/inline-md.js
CHANGED
|
@@ -106,6 +106,14 @@
|
|
|
106
106
|
|
|
107
107
|
function serializeAnchor(node, unsupported) {
|
|
108
108
|
const href = node.getAttribute('href');
|
|
109
|
+
// A raw HTML anchor (`<a id="ref-x"></a>`, NO href attribute) is not a
|
|
110
|
+
// markdown link: emitting '[label]()' for it erased the anchor on the
|
|
111
|
+
// first edit of its block. Unsupported, so the block keeps its source.
|
|
112
|
+
// An empty href ('' — from '[x]()') is still a link.
|
|
113
|
+
if (href === null) {
|
|
114
|
+
unsupported.push('A');
|
|
115
|
+
return '';
|
|
116
|
+
}
|
|
109
117
|
const text = node.textContent;
|
|
110
118
|
// citation: <a href="#slug">[body]</a> -> [[body]]
|
|
111
119
|
if (href && href.charAt(0) === '#' && /^\[.*\]$/.test(text)) {
|
package/lib/editor/list-md.js
CHANGED
|
@@ -450,6 +450,15 @@
|
|
|
450
450
|
// replace from rewriting lines the user never looked at.
|
|
451
451
|
function serializeBlocks(blockEls, opts) {
|
|
452
452
|
const carryOver = (opts && opts.carryOver) || null;
|
|
453
|
+
// v3.11 (batch-2 §6): `opts.blankBefore(blockEl, prevItemEl)` — true when a
|
|
454
|
+
// blank line separates this item from the item emitted before it. The
|
|
455
|
+
// CALLER decides (client.js reads it off the file: a loose list keeps the
|
|
456
|
+
// blank lines it had, and a new neighbour takes the spacing of the list it
|
|
457
|
+
// joins). The blank line gets a lineMeta entry with blockId null and
|
|
458
|
+
// `blank: true`, so
|
|
459
|
+
// `lineMeta.length === md.split('\n').length` still holds.
|
|
460
|
+
const blankBefore = (opts && typeof opts.blankBefore === 'function') ? opts.blankBefore : null;
|
|
461
|
+
let prevItemEl = null;
|
|
453
462
|
const unsupported = [];
|
|
454
463
|
const unsupportedByLi = [];
|
|
455
464
|
const multiLineBlockIds = [];
|
|
@@ -586,8 +595,16 @@
|
|
|
586
595
|
// keep. (isBlankBlockText() stays as-is for the block's own direct
|
|
587
596
|
// children, where the template's inter-element whitespace can legally
|
|
588
597
|
// be a single space with no newline.)
|
|
589
|
-
|
|
598
|
+
let inlineKids = [];
|
|
590
599
|
allChildNodes(textEl).forEach((c) => { if (!isBlankText(c)) inlineKids.push(c); });
|
|
600
|
+
// v3.11 (batch-2 §6): a LOOSE item's surface is one <p> — its content
|
|
601
|
+
// is that paragraph's, and the blank lines that made the list loose
|
|
602
|
+
// are restated between items (opts.blankBefore below), not carried by
|
|
603
|
+
// the <p>. Two or more <p> (a multi-paragraph item) still reach the
|
|
604
|
+
// inline serializer whole and stay unsupported.
|
|
605
|
+
if (inlineKids.length === 1 && inlineKids[0].nodeType === 1 && inlineKids[0].nodeName === 'P') {
|
|
606
|
+
inlineKids = allChildNodes(inlineKids[0]);
|
|
607
|
+
}
|
|
591
608
|
const res = inlineMd.serializeInline({ childNodes: inlineKids });
|
|
592
609
|
itemMd = res.md;
|
|
593
610
|
res.unsupported.forEach((u) => innerUnsupported.push(u));
|
|
@@ -709,6 +726,14 @@
|
|
|
709
726
|
// Round 6: join an open task prefix (above) to this line, dropping the
|
|
710
727
|
// columns it already wrote; then, if THIS item is itself a content-free
|
|
711
728
|
// task item, become the prefix instead of emitting.
|
|
729
|
+
// A line joined onto an open task prefix is the same physical line, so
|
|
730
|
+
// it never takes a blank in front of it.
|
|
731
|
+
const blankHere = !pending && lines.length > 0 && !!blankBefore && !!blankBefore(blockEl, prevItemEl);
|
|
732
|
+
prevItemEl = blockEl;
|
|
733
|
+
if (blankHere) {
|
|
734
|
+
lines.push('');
|
|
735
|
+
lineMeta.push({ blockId: null, indentPrefix: '', marker: '', blank: true });
|
|
736
|
+
}
|
|
712
737
|
let head = indentPrefix + marker;
|
|
713
738
|
// `indentPrefix` in lineMeta is the text PHYSICALLY standing before this
|
|
714
739
|
// line's own marker. Normally that is the accumulated width; on a line a
|
package/lib/md2doc.js
CHANGED
|
@@ -360,10 +360,14 @@ function renderEditModeList(listToken, blocks, biRef, out) {
|
|
|
360
360
|
const runStart = liRunStartsHere(biRef, b) ? ' data-run-start="1"' : '';
|
|
361
361
|
const listStartAttr = listStart ? ' data-list-start="1"' : '';
|
|
362
362
|
listStart = false;
|
|
363
|
+
// v3.11 (batch-2 §6): marked's own looseness for this list token, read
|
|
364
|
+
// by the lexer off the source (a blank line between items). The editor
|
|
365
|
+
// gives a NEW neighbour in this list the same spacing.
|
|
366
|
+
const looseAttr = listToken.loose ? ' data-list-loose="1"' : '';
|
|
363
367
|
parts.push(
|
|
364
368
|
`<div class="ed-block" data-block-id="${b.id}" data-block-type="li"` +
|
|
365
369
|
` data-list-type="${b.listType}" data-task="${b.task ? 1 : 0}" data-indent="${b.indent}"` +
|
|
366
|
-
runStart + listStartAttr + ` style="--ed-indent:${b.indent}">` +
|
|
370
|
+
runStart + listStartAttr + looseAttr + ` style="--ed-indent:${b.indent}">` +
|
|
367
371
|
`<span class="ed-li-marker" aria-hidden="true"></span>` + check +
|
|
368
372
|
`<div class="ed-li-text">${inner}</div>` +
|
|
369
373
|
`</div>`
|
|
@@ -439,6 +443,8 @@ const READER_ICONS = {
|
|
|
439
443
|
up: ICON_SVG_OPEN + '<path d="m18 15-6-6-6 6"/></svg>',
|
|
440
444
|
down: ICON_SVG_OPEN + '<path d="m6 9 6 6 6-6"/></svg>',
|
|
441
445
|
x: ICON_SVG_OPEN + '<path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>',
|
|
446
|
+
copy: ICON_SVG_OPEN + '<rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>',
|
|
447
|
+
check: ICON_SVG_OPEN + '<path d="M20 6 9 17l-5-5"/></svg>',
|
|
442
448
|
};
|
|
443
449
|
|
|
444
450
|
async function renderMarkdown(mdText, srcPath, opts = {}) {
|
|
@@ -1041,9 +1047,16 @@ ${itemsHtml}
|
|
|
1041
1047
|
// The info string goes into an attribute: escape it (an info string such
|
|
1042
1048
|
// as x" onmouseover=… must stay text). Ordinary language names are unchanged.
|
|
1043
1049
|
const langAttr = escapeHtml(lang);
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1050
|
+
const pre = coloured !== null
|
|
1051
|
+
? `<pre><code class="language-${langAttr} hljs">${coloured}</code></pre>`
|
|
1052
|
+
: `<pre><code class="language-${langAttr}">${code.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')}</code></pre>`;
|
|
1053
|
+
// v3.12.0: the reader's copy button. It sits on a wrapper beside the
|
|
1054
|
+
// <pre>, not inside it — <pre> scrolls sideways and would carry the
|
|
1055
|
+
// button off with the code. Icon only: a text label would be picked up
|
|
1056
|
+
// by search and by select-all. Never in edit mode, where it would land
|
|
1057
|
+
// inside the editable surface.
|
|
1058
|
+
if (opts.editMode) return pre + '\n';
|
|
1059
|
+
return `<div class="code-block">${pre}<button class="code-copy" type="button" aria-label="Copy code" title="Copy code">${READER_ICONS.copy}</button></div>\n`;
|
|
1047
1060
|
};
|
|
1048
1061
|
|
|
1049
1062
|
renderer.heading = function(token) {
|
|
@@ -2886,6 +2899,32 @@ const html = `<!DOCTYPE html>
|
|
|
2886
2899
|
line-height: 1.45;
|
|
2887
2900
|
}
|
|
2888
2901
|
pre code { background: none; padding: 0; font-size: 0.875em; }
|
|
2902
|
+
/* v3.12.0: the copy button renderer.code puts beside each fenced block.
|
|
2903
|
+
The wrapper takes over the margin the pre used to have, so the spacing
|
|
2904
|
+
around a block does not move. The button hides at rest and shows on
|
|
2905
|
+
hover, on keyboard focus and after a press; a touch screen has no hover,
|
|
2906
|
+
so there it always shows. Colours are existing reader literals, so the
|
|
2907
|
+
theme post-pass carries them into dark mode. */
|
|
2908
|
+
.code-block { position: relative; margin: 1em 0; }
|
|
2909
|
+
.code-block > pre { margin: 0; }
|
|
2910
|
+
.code-copy {
|
|
2911
|
+
position: absolute; top: 8px; right: 8px;
|
|
2912
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 6px;
|
|
2913
|
+
height: 30px; min-width: 30px; padding: 0 7px;
|
|
2914
|
+
border: 1px solid #d0d7de; border-radius: 6px;
|
|
2915
|
+
background: #ffffff; color: #57606a;
|
|
2916
|
+
font: inherit; font-size: 12px; line-height: 1; cursor: pointer;
|
|
2917
|
+
opacity: 0; transition: opacity 0.12s;
|
|
2918
|
+
}
|
|
2919
|
+
.code-block:hover .code-copy,
|
|
2920
|
+
.code-copy:focus-visible,
|
|
2921
|
+
.code-block.is-copied .code-copy,
|
|
2922
|
+
.code-block.is-manual .code-copy { opacity: 1; }
|
|
2923
|
+
.code-copy:hover { background: #eef2f6; color: #24292e; }
|
|
2924
|
+
.code-copy:focus-visible { outline: 2px solid #0969da; outline-offset: 2px; }
|
|
2925
|
+
.code-block.is-copied .code-copy { color: #0969da; }
|
|
2926
|
+
@media (hover: none) { .code-copy { opacity: 1; } }
|
|
2927
|
+
@media (prefers-reduced-motion: reduce) { .code-copy { transition: none; } }
|
|
2889
2928
|
/* v3.10.0: fenced-code syntax colour, written at render time by
|
|
2890
2929
|
lib/highlight.js (no script on the page). Colours are the syn-* roles. */
|
|
2891
2930
|
.hljs-keyword, .hljs-selector-tag, .hljs-literal { color: var(--md-syn-keyword); }
|
|
@@ -3272,6 +3311,7 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
|
|
|
3272
3311
|
.content table tbody td:first-child,
|
|
3273
3312
|
.content table thead th:first-child { position: static; }
|
|
3274
3313
|
.heading-anchor { display: none; }
|
|
3314
|
+
.code-copy { display: none; }
|
|
3275
3315
|
pre { font-size: 9pt; }
|
|
3276
3316
|
a[href]:after { content: none; }
|
|
3277
3317
|
}
|
|
@@ -6006,6 +6046,55 @@ ${usesDrawioMultiPage ? ` // v3.4.0 batch2 Task 6: wired through a NAMED, reusa
|
|
|
6006
6046
|
var shown = lightboxCanvas.firstElementChild;
|
|
6007
6047
|
lightboxFitZoom = computeLightboxFit(!!(shown && shown.tagName.toLowerCase() === 'svg'));
|
|
6008
6048
|
});
|
|
6049
|
+
|
|
6050
|
+
// v3.12.0: copy button on fenced code (renderer.code emits it; edit mode
|
|
6051
|
+
// does not). Delegated from document, so the edit page's wholesale
|
|
6052
|
+
// re-render of .content needs no rebind. The clipboard API first; then
|
|
6053
|
+
// execCommand over a selection of the code; and when both are refused the
|
|
6054
|
+
// code stays selected and the button says which keys to press.
|
|
6055
|
+
var CODE_COPY_RESET_MS = 2000;
|
|
6056
|
+
var CODE_COPY_DONE = '${READER_ICONS.check}<span>Copied</span>';
|
|
6057
|
+
var CODE_COPY_KEYS = /Mac|iPhone|iPad/.test(navigator.platform || '') ? 'Press Cmd+C' : 'Press Ctrl+C';
|
|
6058
|
+
function showCodeCopyResult(btn, wrap, ok) {
|
|
6059
|
+
if (btn.__md2docIcon === undefined) btn.__md2docIcon = btn.innerHTML;
|
|
6060
|
+
clearTimeout(btn.__md2docReset);
|
|
6061
|
+
wrap.classList.toggle('is-copied', ok);
|
|
6062
|
+
wrap.classList.toggle('is-manual', !ok);
|
|
6063
|
+
btn.innerHTML = ok ? CODE_COPY_DONE : '<span>' + CODE_COPY_KEYS + '</span>';
|
|
6064
|
+
btn.setAttribute('aria-label', ok ? 'Copied' : CODE_COPY_KEYS);
|
|
6065
|
+
btn.__md2docReset = setTimeout(function () {
|
|
6066
|
+
wrap.classList.remove('is-copied', 'is-manual');
|
|
6067
|
+
btn.innerHTML = btn.__md2docIcon;
|
|
6068
|
+
btn.setAttribute('aria-label', 'Copy code');
|
|
6069
|
+
}, CODE_COPY_RESET_MS);
|
|
6070
|
+
}
|
|
6071
|
+
function copyCodeBlock(btn) {
|
|
6072
|
+
var wrap = btn.closest('.code-block');
|
|
6073
|
+
var code = wrap && wrap.querySelector('pre > code');
|
|
6074
|
+
if (!code) return;
|
|
6075
|
+
function bySelection() {
|
|
6076
|
+
var range = document.createRange();
|
|
6077
|
+
range.selectNodeContents(code);
|
|
6078
|
+
var sel = window.getSelection();
|
|
6079
|
+
sel.removeAllRanges();
|
|
6080
|
+
sel.addRange(range);
|
|
6081
|
+
var ok = false;
|
|
6082
|
+
try { ok = document.execCommand('copy'); } catch (e) { ok = false; }
|
|
6083
|
+
if (ok) sel.removeAllRanges();
|
|
6084
|
+
showCodeCopyResult(btn, wrap, ok);
|
|
6085
|
+
}
|
|
6086
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
6087
|
+
navigator.clipboard.writeText(code.textContent).then(function () {
|
|
6088
|
+
showCodeCopyResult(btn, wrap, true);
|
|
6089
|
+
}, bySelection);
|
|
6090
|
+
} else {
|
|
6091
|
+
bySelection();
|
|
6092
|
+
}
|
|
6093
|
+
}
|
|
6094
|
+
document.addEventListener('click', function (event) {
|
|
6095
|
+
var btn = event.target.closest && event.target.closest('.code-block > .code-copy');
|
|
6096
|
+
if (btn) copyCodeBlock(btn);
|
|
6097
|
+
});
|
|
6009
6098
|
})();
|
|
6010
6099
|
</script>
|
|
6011
6100
|
</body>
|