@helping-ai-workflow/md2doc 3.14.1 → 3.16.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/block-catalog.js +131 -0
- package/lib/editor/blockmap.js +3 -0
- package/lib/editor/client.js +3185 -633
- package/lib/editor/inline-md.js +20 -1
- package/lib/editor/lineops.js +6 -0
- package/lib/editor/list-md.js +11 -5
- package/lib/editor/server.js +40 -1
- package/lib/editor/toolbar-model.js +5 -3
- package/lib/md2doc.js +231 -28
- package/lib/task-items.js +48 -0
- package/package.json +2 -2
package/lib/editor/inline-md.js
CHANGED
|
@@ -423,5 +423,24 @@
|
|
|
423
423
|
return serializeInline(rootEl).unsupported.length === 0;
|
|
424
424
|
}
|
|
425
425
|
|
|
426
|
-
|
|
426
|
+
// v3.15.0 T5 (D15): what the URL field writes into an href. serializeAnchor
|
|
427
|
+
// puts the href into `[text](href)` unescaped, so a space or a parenthesis
|
|
428
|
+
// would end the destination early; those and angle brackets are
|
|
429
|
+
// percent-encoded. A bare host gets https://; anything that already names a
|
|
430
|
+
// scheme, an anchor or a path is kept.
|
|
431
|
+
function normalizeHref(raw) {
|
|
432
|
+
let h = String(raw == null ? '' : raw).trim();
|
|
433
|
+
if (h === '') return '';
|
|
434
|
+
if (!/^([a-z][a-z0-9+.-]*:|#|\/|\.\/|\.\.\/)/i.test(h)) h = 'https://' + h;
|
|
435
|
+
return h.replace(/ /g, '%20').replace(/\(/g, '%28').replace(/\)/g, '%29')
|
|
436
|
+
.replace(/</g, '%3C').replace(/>/g, '%3E');
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// A clipboard text that is exactly one http(s) URL (the address bar adds a
|
|
440
|
+
// trailing newline, some pages surrounding spaces).
|
|
441
|
+
function isPastedUrl(text) {
|
|
442
|
+
return /^https?:\/\/\S+$/.test(String(text == null ? '' : text).trim());
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return { serializeInline, canWysiwyg, escapeText, escapeBlockStarts, normalizeHref, isPastedUrl };
|
|
427
446
|
});
|
package/lib/editor/lineops.js
CHANGED
|
@@ -96,6 +96,12 @@
|
|
|
96
96
|
const span = { startLine: op.startLine, endLine: op.startLine + op.after.length - 1 };
|
|
97
97
|
return { lines: replaceLines(lines, span.startLine, span.endLine, op.before).lines, op };
|
|
98
98
|
};
|
|
99
|
+
// v3.15.0 T9 (D21): the op undo() would pop next, or null. Read, never
|
|
100
|
+
// popped: rollbackFailedRender() (lib/editor/client.js) undoes a failed
|
|
101
|
+
// commit only while that commit is still the newest op.
|
|
102
|
+
UndoStack.prototype.top = function () {
|
|
103
|
+
return this._done.length ? this._done[this._done.length - 1] : null;
|
|
104
|
+
};
|
|
99
105
|
// v3.4.0 batch3 Task 7 fix 1 (F3) — read and write the REDO tail.
|
|
100
106
|
//
|
|
101
107
|
// `push()` clears `_undone`, and that is correct for an ordinary edit: once
|
package/lib/editor/list-md.js
CHANGED
|
@@ -494,15 +494,21 @@
|
|
|
494
494
|
// gives the item a source line of its own (blockmap can then hand it a
|
|
495
495
|
// well-formed range). Only the checkbox forces the same-line form.
|
|
496
496
|
//
|
|
497
|
-
// The prefix is FLUSHED as its own line —
|
|
498
|
-
//
|
|
499
|
-
// i.e. when the item has no child to attach
|
|
500
|
-
// would invent nesting that the document
|
|
497
|
+
// The prefix is FLUSHED as its own line — `- [ ] `, its checkbox kept by
|
|
498
|
+
// the one space after it (v3.15.0 T3, R44; see flushPending()) — whenever
|
|
499
|
+
// the next block is not deeper, i.e. when the item has no child to attach
|
|
500
|
+
// to. Merging it onto a SIBLING would invent nesting that the document
|
|
501
|
+
// never had.
|
|
501
502
|
let pending = null; // { text, cols, indent, blockId, indentPrefix, marker }
|
|
502
503
|
|
|
504
|
+
// v3.15.0 T3 (R44): the flushed line keeps ONE space after its checkbox.
|
|
505
|
+
// `- [ ]` with nothing after it is a plain item whose text is "[ ]"
|
|
506
|
+
// (measured, marked) — the next commit of that item wrote `- \[ \]`, and
|
|
507
|
+
// typing into the empty to-do an Enter had made saved `- x\[ \]`. `- [ ] `
|
|
508
|
+
// is an empty to-do (the D5 skeleton), so that is what is written.
|
|
503
509
|
function flushPending() {
|
|
504
510
|
if (!pending) return;
|
|
505
|
-
lines.push(pending.text.replace(/[ \t]+$/, ''));
|
|
511
|
+
lines.push(pending.text.replace(/[ \t]+$/, '') + ' ');
|
|
506
512
|
lineMeta.push({ blockId: pending.blockId, indentPrefix: pending.indentPrefix,
|
|
507
513
|
marker: pending.marker });
|
|
508
514
|
pending = null;
|
package/lib/editor/server.js
CHANGED
|
@@ -4,7 +4,7 @@ const path = require('path');
|
|
|
4
4
|
const http = require('http');
|
|
5
5
|
const crypto = require('crypto');
|
|
6
6
|
const drawioLib = require('../drawio.js');
|
|
7
|
-
const { renderMarkdown } = require('../md2doc.js');
|
|
7
|
+
const { renderMarkdown, buildKatexStyleTag, diagramEnginePaths } = require('../md2doc.js');
|
|
8
8
|
|
|
9
9
|
const LINEOPS_SRC = fs.readFileSync(path.join(__dirname, 'lineops.js'), 'utf8');
|
|
10
10
|
const INLINE_MD_SRC = fs.readFileSync(path.join(__dirname, 'inline-md.js'), 'utf8');
|
|
@@ -25,6 +25,10 @@ const INDENT_CLAMP_SRC = fs.readFileSync(path.join(__dirname, 'indent-clamp.js')
|
|
|
25
25
|
// indent-clamp.js above — it only has to land before client.js reads
|
|
26
26
|
// window.md2docConvertMd.
|
|
27
27
|
const CONVERT_MD_SRC = fs.readFileSync(path.join(__dirname, 'convert-md.js'), 'utf8');
|
|
28
|
+
// v3.15.0 T1: the "/" block menu's catalogue (its items, filter and
|
|
29
|
+
// skeletons). Pure, no dependency on any other editor module; it only has to
|
|
30
|
+
// land before client.js reads window.md2docBlockCatalog.
|
|
31
|
+
const BLOCK_CATALOG_SRC = fs.readFileSync(path.join(__dirname, 'block-catalog.js'), 'utf8');
|
|
28
32
|
// v3.2.0: the pure string-diff module behind incremental DOM patching —
|
|
29
33
|
// stripBlockId()/patchmap() compare oldParts[i] against newParts[j] with only
|
|
30
34
|
// data-block-id normalized (see the global constraint on that). Same "no
|
|
@@ -190,6 +194,33 @@ function hostIsLoopback(hostHeader, port) {
|
|
|
190
194
|
return false;
|
|
191
195
|
}
|
|
192
196
|
|
|
197
|
+
// v3.15.0 T4 (D12, R9): the diagram engines an edit page loads on demand
|
|
198
|
+
// (client.js ensureDiagramEngines()) — the page inlines them only when the
|
|
199
|
+
// document used them at load. The paths are lib/md2doc.js's own resolved ones
|
|
200
|
+
// (B2 fix round 1, R61: one resolver, not two). Read per request (a few MB, a
|
|
201
|
+
// handful of requests per session at most).
|
|
202
|
+
function diagramLibBody(name) {
|
|
203
|
+
if (name === 'mermaid.js') {
|
|
204
|
+
const p = diagramEnginePaths.mermaid;
|
|
205
|
+
return p ? { type: 'application/javascript; charset=utf-8', body: fs.readFileSync(p, 'utf8') } : null;
|
|
206
|
+
}
|
|
207
|
+
if (name === 'wavedrom.js') {
|
|
208
|
+
// The skin first: the library reads the WaveSkin global it defines.
|
|
209
|
+
const skin = diagramEnginePaths.wavedromSkin;
|
|
210
|
+
const lib = diagramEnginePaths.wavedrom;
|
|
211
|
+
if (!skin || !lib) return null;
|
|
212
|
+
return { type: 'application/javascript; charset=utf-8',
|
|
213
|
+
body: fs.readFileSync(skin, 'utf8') + '\n' + fs.readFileSync(lib, 'utf8') };
|
|
214
|
+
}
|
|
215
|
+
if (name === 'katex.css') {
|
|
216
|
+
const tag = buildKatexStyleTag();
|
|
217
|
+
const open = tag.indexOf('>') + 1;
|
|
218
|
+
const close = tag.lastIndexOf('</style>');
|
|
219
|
+
return { type: 'text/css; charset=utf-8', body: tag.slice(open, close) };
|
|
220
|
+
}
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
193
224
|
function send(res, status, obj) {
|
|
194
225
|
const body = JSON.stringify(obj);
|
|
195
226
|
res.writeHead(status, { 'content-type': 'application/json' });
|
|
@@ -736,6 +767,7 @@ async function createEditorServer({
|
|
|
736
767
|
`<script>${HISTORY_SRC}</script>\n` +
|
|
737
768
|
`<script>${INDENT_CLAMP_SRC}</script>\n` +
|
|
738
769
|
`<script>${CONVERT_MD_SRC}</script>\n` +
|
|
770
|
+
`<script>${BLOCK_CATALOG_SRC}</script>\n` +
|
|
739
771
|
`<script>${PATCHMAP_SRC}</script>\n` +
|
|
740
772
|
`<script>${SELECTION_SRC}</script>\n` +
|
|
741
773
|
`<script>${CELL_RANGE_SRC}</script>\n` +
|
|
@@ -776,6 +808,13 @@ async function createEditorServer({
|
|
|
776
808
|
return res.end(out);
|
|
777
809
|
}
|
|
778
810
|
|
|
811
|
+
if (req.method === 'GET' && url.pathname.indexOf('/api/lib/') === 0) {
|
|
812
|
+
const lib = diagramLibBody(url.pathname.slice('/api/lib/'.length));
|
|
813
|
+
if (!lib) return send(res, 404, { error: 'unknown library' });
|
|
814
|
+
res.writeHead(200, { 'content-type': lib.type, 'cache-control': 'no-store' });
|
|
815
|
+
return res.end(lib.body);
|
|
816
|
+
}
|
|
817
|
+
|
|
779
818
|
if (req.method === 'POST' && url.pathname === '/api/render') {
|
|
780
819
|
const { fileId, content } = await readJson(req);
|
|
781
820
|
const file = absFiles[fileId];
|
|
@@ -37,7 +37,7 @@ const BUTTON_DEFS = [
|
|
|
37
37
|
{ id: 'italic', group: 'inline', label: 'I', icon: 'italic', title: '斜體', toggle: true },
|
|
38
38
|
{ id: 'strike', group: 'inline', label: 'S', icon: 'strikethrough', title: '刪除線', toggle: true },
|
|
39
39
|
{ id: 'inline-code', group: 'inline', label: '`', icon: 'code', title: '行內程式碼', toggle: true },
|
|
40
|
-
{ id: 'link', group: 'inline', label: '🔗', icon: 'link', title: '連結', toggle: true },
|
|
40
|
+
{ id: 'link', group: 'inline', label: '🔗', icon: 'link', title: '連結 (Ctrl+K)', toggle: true },
|
|
41
41
|
|
|
42
42
|
// indent
|
|
43
43
|
{ id: 'outdent', group: 'indent', label: '⇤', icon: 'indent-decrease', title: '減少縮排', toggle: false },
|
|
@@ -152,11 +152,13 @@ function deriveState(ctx) {
|
|
|
152
152
|
// that case).
|
|
153
153
|
|
|
154
154
|
// v3.14.0 T4 (D15): an EMPTY transient paragraph has no text to convert, so
|
|
155
|
-
// the conversions that wrap existing text (quote, numbered list, check) and
|
|
156
155
|
// the list-depth buttons stay off until it has some. ctx.transient is
|
|
157
156
|
// 'empty' | 'text' | null; the client derives it from the DOM.
|
|
157
|
+
// v3.15.0 T2 (D11): quote, numbered list and check are on again — on an
|
|
158
|
+
// empty transient they insert their empty block there (the "/" menu's
|
|
159
|
+
// skeletons), so only indent / outdent have nothing to act on.
|
|
158
160
|
if (c.transient === 'empty') {
|
|
159
|
-
for (const id of ['
|
|
161
|
+
for (const id of ['indent', 'outdent']) {
|
|
160
162
|
state[id].disabled = true;
|
|
161
163
|
}
|
|
162
164
|
}
|
package/lib/md2doc.js
CHANGED
|
@@ -190,6 +190,9 @@ let marked, Renderer;
|
|
|
190
190
|
let blockMathText = null;
|
|
191
191
|
try {
|
|
192
192
|
({ marked, Renderer } = require('marked'));
|
|
193
|
+
// v3.15.0 B2 fix round 1 (R56): the empty to-do token rule, shared with the
|
|
194
|
+
// editor's block map — see lib/task-items.js.
|
|
195
|
+
require('./task-items.js');
|
|
193
196
|
|
|
194
197
|
// Code-aware subscript / superscript as marked inline extensions.
|
|
195
198
|
// The old raw-text pre-pass (mdPre.replace(/~([^~]+)~/...)) ran BEFORE marked
|
|
@@ -464,6 +467,7 @@ const READER_ICONS = {
|
|
|
464
467
|
x: ICON_SVG_OPEN + '<path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>',
|
|
465
468
|
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>',
|
|
466
469
|
check: ICON_SVG_OPEN + '<path d="M20 6 9 17l-5-5"/></svg>',
|
|
470
|
+
camera: ICON_SVG_OPEN + '<path d="M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z"/><circle cx="12" cy="13" r="3"/></svg>',
|
|
467
471
|
};
|
|
468
472
|
|
|
469
473
|
async function renderMarkdown(mdText, srcPath, opts = {}) {
|
|
@@ -1489,7 +1493,8 @@ ${itemsHtml}
|
|
|
1489
1493
|
<div class="toc-header-actions">
|
|
1490
1494
|
<button id="toc-expand-all" type="button" aria-label="Expand all" title="Expand all">${READER_ICONS.unfold}</button>
|
|
1491
1495
|
<button id="toc-collapse-all" type="button" aria-label="Collapse all" title="Collapse all">${READER_ICONS.fold}</button>
|
|
1492
|
-
<button id="
|
|
1496
|
+
${opts.editMode ? '' : ` <button id="capture-mode-toggle" type="button" aria-label="Screenshot mode" title="Screenshot mode (Esc to exit)">${READER_ICONS.camera}</button>
|
|
1497
|
+
`} <button id="toc-collapse-toggle" type="button" aria-label="Collapse table of contents" title="Collapse / expand sidebar">${READER_ICONS.panel}</button>
|
|
1493
1498
|
</div>
|
|
1494
1499
|
<div class="toc-breadcrumb" data-toc-breadcrumb aria-label="Current location"></div>
|
|
1495
1500
|
</div>
|
|
@@ -2408,7 +2413,8 @@ const html = `<!DOCTYPE html>
|
|
|
2408
2413
|
TOC-list buttons (no list to act on) and shrink padding so the toggle
|
|
2409
2414
|
fits inside the rail instead of being clipped by overflow:hidden. */
|
|
2410
2415
|
body[data-toc-collapsed] #toc-expand-all,
|
|
2411
|
-
body[data-toc-collapsed] #toc-collapse-all
|
|
2416
|
+
body[data-toc-collapsed] #toc-collapse-all,
|
|
2417
|
+
body[data-toc-collapsed] #capture-mode-toggle {
|
|
2412
2418
|
display: none;
|
|
2413
2419
|
}
|
|
2414
2420
|
body[data-toc-collapsed] .toc {
|
|
@@ -2878,6 +2884,42 @@ const html = `<!DOCTYPE html>
|
|
|
2878
2884
|
min-width: 0;
|
|
2879
2885
|
flex: 1 1 auto;
|
|
2880
2886
|
}
|
|
2887
|
+
/* Screenshot mode (v3.16.0). A scroll-and-stitch full-page capture
|
|
2888
|
+
(FireShot and the like) glues one slice per screen, so whatever stays put
|
|
2889
|
+
while the page scrolls lands on every slice: hide all of it, and keep the
|
|
2890
|
+
column at the width it was read at (--md2doc-capture-w, measured on
|
|
2891
|
+
entry), centred, so its lines break as they did. */
|
|
2892
|
+
body[data-capture-mode] .reader-sidebar,
|
|
2893
|
+
body[data-capture-mode] .sidebar-splitter,
|
|
2894
|
+
body[data-capture-mode] .sidebar-scrim,
|
|
2895
|
+
body[data-capture-mode] .mobile-bar,
|
|
2896
|
+
body[data-capture-mode] .md2doc-theme-toggle {
|
|
2897
|
+
display: none;
|
|
2898
|
+
}
|
|
2899
|
+
body[data-capture-mode] .page-layout {
|
|
2900
|
+
display: block;
|
|
2901
|
+
}
|
|
2902
|
+
body[data-capture-mode] main.content {
|
|
2903
|
+
box-sizing: border-box;
|
|
2904
|
+
width: var(--md2doc-capture-w, auto);
|
|
2905
|
+
max-width: 100%;
|
|
2906
|
+
margin: 0 auto;
|
|
2907
|
+
}
|
|
2908
|
+
.capture-toast {
|
|
2909
|
+
position: fixed;
|
|
2910
|
+
top: 16px;
|
|
2911
|
+
left: 50%;
|
|
2912
|
+
transform: translateX(-50%);
|
|
2913
|
+
z-index: 300;
|
|
2914
|
+
padding: 8px 16px;
|
|
2915
|
+
border-radius: 999px;
|
|
2916
|
+
background: #24292e;
|
|
2917
|
+
color: #ffffff;
|
|
2918
|
+
font-size: 14px;
|
|
2919
|
+
line-height: 1.4;
|
|
2920
|
+
box-shadow: 0 4px 14px rgba(0,0,0,0.18);
|
|
2921
|
+
pointer-events: none;
|
|
2922
|
+
}
|
|
2881
2923
|
.content p,
|
|
2882
2924
|
.content li,
|
|
2883
2925
|
.content blockquote,
|
|
@@ -3582,6 +3624,41 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
|
|
|
3582
3624
|
.ed-handle-submenu { left: 100%; margin-left: 4px; max-height: calc(100vh - 8px); overflow-y: auto; }
|
|
3583
3625
|
.ed-handle-menu-btn:hover { background: var(--md-ed-hover); }
|
|
3584
3626
|
.ed-handle-menu-btn[hidden] { display: none; }
|
|
3627
|
+
/* v3.15.0 T1: the "/" block menu (decision D9, layout M1). Fixed, placed
|
|
3628
|
+
4px under the caret by the client runtime (above it when there is no
|
|
3629
|
+
room below) and closed on scroll. 280px wide, its list scrolls on its
|
|
3630
|
+
own under a 360px cap; group titles, 32px rows of a 16px icon, the label
|
|
3631
|
+
and a muted monospace hint on the right; a key-help footer. The row the
|
|
3632
|
+
keyboard cursor is on carries data-ed-slash-cursor. Same surface, ring
|
|
3633
|
+
and shadow roles as the other editor menus, so it follows the theme.
|
|
3634
|
+
(No backticks in this comment: it lives inside a JS template literal.) */
|
|
3635
|
+
.ed-slash-menu {
|
|
3636
|
+
position: fixed; z-index: 100; width: 280px; max-height: 360px;
|
|
3637
|
+
display: flex; flex-direction: column; box-sizing: border-box; overflow: hidden;
|
|
3638
|
+
border-radius: 8px; background: var(--md-ed-surface); color: var(--md-strong);
|
|
3639
|
+
box-shadow: 0 0 0 1px var(--md-ed-ring), 0 8px 24px var(--md-ed-shadow);
|
|
3640
|
+
font-size: 13px; line-height: 1.3;
|
|
3641
|
+
}
|
|
3642
|
+
.ed-slash-list { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 4px; }
|
|
3643
|
+
.ed-slash-group { padding: 8px 8px 4px; font-size: 11px; font-weight: 600; color: var(--md-muted); }
|
|
3644
|
+
.ed-slash-item {
|
|
3645
|
+
display: flex; align-items: center; gap: 8px; width: 100%; height: 32px;
|
|
3646
|
+
padding: 0 8px; box-sizing: border-box; margin: 0;
|
|
3647
|
+
border: 0; border-radius: 6px; background: transparent; color: inherit;
|
|
3648
|
+
font: inherit; text-align: left; cursor: pointer;
|
|
3649
|
+
}
|
|
3650
|
+
.ed-slash-item:hover, .ed-slash-item[data-ed-slash-cursor] { background: var(--md-ed-hover); }
|
|
3651
|
+
.ed-slash-item svg { flex: 0 0 auto; width: 16px; height: 16px; margin: 0; }
|
|
3652
|
+
.ed-slash-label { flex: 1 1 auto; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
3653
|
+
.ed-slash-hint {
|
|
3654
|
+
flex: 0 0 auto; color: var(--md-muted); font-size: 12px;
|
|
3655
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
3656
|
+
}
|
|
3657
|
+
.ed-slash-empty { padding: 8px; color: var(--md-muted); }
|
|
3658
|
+
.ed-slash-foot {
|
|
3659
|
+
flex: 0 0 auto; padding: 6px 10px; font-size: 11px; color: var(--md-muted);
|
|
3660
|
+
border-top: 1px solid var(--md-ed-ring);
|
|
3661
|
+
}
|
|
3585
3662
|
/* §10-gap fix: block-level INSERT — the + button sits NEXT TO the ⠿
|
|
3586
3663
|
handle in the left gutter, same node/visibility-toggle shape as
|
|
3587
3664
|
.ed-handle above (real per-block node, opacity-only reveal on hover).
|
|
@@ -3613,23 +3690,8 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
|
|
|
3613
3690
|
.ed-block:hover .ed-insert,
|
|
3614
3691
|
.ed-insert:focus { opacity: 1; }
|
|
3615
3692
|
.ed-insert:hover { background: var(--md-ed-hover); color: var(--md-strong); }
|
|
3616
|
-
/*
|
|
3617
|
-
|
|
3618
|
-
.ed-insert-menu {
|
|
3619
|
-
position: absolute; top: -4px; left: -4px; z-index: 6;
|
|
3620
|
-
display: flex; align-items: center; gap: 4px;
|
|
3621
|
-
padding: 4px 6px; border-radius: 8px;
|
|
3622
|
-
background: var(--md-ed-surface); color: var(--md-strong);
|
|
3623
|
-
box-shadow: 0 0 0 1px var(--md-ed-ring), 0 8px 24px var(--md-ed-shadow);
|
|
3624
|
-
}
|
|
3625
|
-
.ed-insert-menu-btn {
|
|
3626
|
-
min-width: 28px; height: 26px; padding: 0 8px;
|
|
3627
|
-
border: 1px solid transparent; border-radius: 6px;
|
|
3628
|
-
background: transparent; color: inherit;
|
|
3629
|
-
font: inherit; font-size: 12px; line-height: 1;
|
|
3630
|
-
white-space: nowrap; cursor: pointer;
|
|
3631
|
-
}
|
|
3632
|
-
.ed-insert-menu-btn:hover { background: var(--md-ed-hover); }
|
|
3693
|
+
/* v3.15.0 T2: the + button opens the "/" block menu (.ed-slash-menu
|
|
3694
|
+
above) on a new empty paragraph; its own small pill menu is gone. */
|
|
3633
3695
|
/* Task 5: hover-edge column/row insert bubbles — a SINGLETON pair of "+"
|
|
3634
3696
|
buttons (client runtime repositions them via getBoundingClientRect(),
|
|
3635
3697
|
never creates more than these two). Visual language deliberately
|
|
@@ -3648,6 +3710,32 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
|
|
|
3648
3710
|
}
|
|
3649
3711
|
.ed-tb-insert:hover { background: var(--md-accent); color: var(--md-ed-surface); }
|
|
3650
3712
|
.ed-tb-insert[hidden] { display: none; }
|
|
3713
|
+
/* v3.15.0 T8 (D20): the trailing add bars, a SINGLETON pair placed by the
|
|
3714
|
+
client runtime along the grid of the hovered table: .ed-tb-addrow under
|
|
3715
|
+
the last row (the grid's width; 22px tall, which the client lowers to
|
|
3716
|
+
the gap above the block under the table, R97), .ed-tb-addcol right of the
|
|
3717
|
+
last column (22px wide, the grid's height), each a centred plus on the
|
|
3718
|
+
hover tint. z-index 6 sits under the grips (7) and the bubbles (8), so a
|
|
3719
|
+
press where they overlap goes to those. Two rules, one per bar, because
|
|
3720
|
+
each is its own fixed overlay. */
|
|
3721
|
+
.ed-tb-addrow {
|
|
3722
|
+
position: fixed; z-index: 6;
|
|
3723
|
+
height: 22px; padding: 0; margin: 0; box-sizing: border-box;
|
|
3724
|
+
display: flex; align-items: center; justify-content: center;
|
|
3725
|
+
border: 0; border-radius: 4px;
|
|
3726
|
+
background: var(--md-ed-hover); color: var(--md-muted);
|
|
3727
|
+
font: inherit; font-size: 14px; line-height: 1; cursor: pointer;
|
|
3728
|
+
}
|
|
3729
|
+
.ed-tb-addcol {
|
|
3730
|
+
position: fixed; z-index: 6;
|
|
3731
|
+
width: 22px; padding: 0; margin: 0; box-sizing: border-box;
|
|
3732
|
+
display: flex; align-items: center; justify-content: center;
|
|
3733
|
+
border: 0; border-radius: 4px;
|
|
3734
|
+
background: var(--md-ed-hover); color: var(--md-muted);
|
|
3735
|
+
font: inherit; font-size: 14px; line-height: 1; cursor: pointer;
|
|
3736
|
+
}
|
|
3737
|
+
.ed-tb-addrow:hover, .ed-tb-addcol:hover { color: var(--md-accent); }
|
|
3738
|
+
.ed-tb-addrow[hidden], .ed-tb-addcol[hidden] { display: none; }
|
|
3651
3739
|
/* Task 6: table edge-click menus (delete/align) + row drag-reorder.
|
|
3652
3740
|
'.ed-te-hl' marks the currently-selected column's or row's CELLS
|
|
3653
3741
|
(th+td) — never the <tr>, which paints nothing the user can see once a
|
|
@@ -3667,23 +3755,39 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
|
|
|
3667
3755
|
.ed-cell-range { background: var(--md-ed-range) !important; }
|
|
3668
3756
|
.ed-cell-range-active { caret-color: transparent; }
|
|
3669
3757
|
.ed-cell-range-active ::selection { background: transparent; }
|
|
3758
|
+
/* v3.15.0 T7 (D18/D19): the edge menu is a vertical list in the look of
|
|
3759
|
+
the block menu above (.ed-handle-menu): 30px rows of a 16px icon and a
|
|
3760
|
+
label, an alignment row of three icon buttons (aria-pressed marks the
|
|
3761
|
+
current one), a separator, and the delete row in the error colour. The
|
|
3762
|
+
row the keyboard cursor is on carries data-ed-te-cursor. Placed by the
|
|
3763
|
+
client runtime under the column grip or beside the row grip, clamped to
|
|
3764
|
+
the table and the viewport. */
|
|
3670
3765
|
.ed-te-menu {
|
|
3671
3766
|
position: fixed; z-index: 12;
|
|
3672
|
-
display: flex; align-items:
|
|
3673
|
-
padding: 4px
|
|
3767
|
+
display: flex; flex-direction: column; align-items: stretch; gap: 2px;
|
|
3768
|
+
padding: 4px; border-radius: 8px;
|
|
3674
3769
|
background: var(--md-ed-surface); color: var(--md-strong);
|
|
3675
3770
|
box-shadow: 0 0 0 1px var(--md-ed-ring), 0 8px 24px var(--md-ed-shadow);
|
|
3676
3771
|
}
|
|
3677
3772
|
.ed-te-menu[hidden] { display: none; }
|
|
3678
3773
|
.ed-te-menu-btn {
|
|
3679
|
-
min-width:
|
|
3774
|
+
min-width: 132px; height: 30px; padding: 0 10px 0 8px;
|
|
3775
|
+
display: flex; align-items: center; gap: 8px;
|
|
3680
3776
|
border: 1px solid transparent; border-radius: 6px;
|
|
3681
3777
|
background: transparent; color: inherit;
|
|
3682
|
-
font: inherit; font-size:
|
|
3683
|
-
white-space: nowrap; cursor: pointer;
|
|
3778
|
+
font: inherit; font-size: 13px; line-height: 1;
|
|
3779
|
+
text-align: left; white-space: nowrap; cursor: pointer;
|
|
3684
3780
|
}
|
|
3781
|
+
.ed-te-menu-btn .ed-menu-icon { flex: 0 0 auto; width: 16px; height: 16px; margin: 0; }
|
|
3685
3782
|
.ed-te-menu-btn:hover { background: var(--md-ed-hover); }
|
|
3686
3783
|
.ed-te-menu-btn[hidden] { display: none; }
|
|
3784
|
+
.ed-te-menu-btn[data-ed-te-cursor] { background: var(--md-ed-hover); box-shadow: inset 0 0 0 1px var(--md-ed-ring); }
|
|
3785
|
+
.ed-te-menu-align { display: flex; gap: 2px; }
|
|
3786
|
+
.ed-te-menu-align[hidden] { display: none; }
|
|
3787
|
+
.ed-te-menu-align .ed-te-menu-btn { flex: 1 1 0; min-width: 0; padding: 0; justify-content: center; }
|
|
3788
|
+
.ed-te-menu-align .ed-te-menu-btn[aria-pressed="true"] { color: var(--md-accent); background: var(--md-ed-hover); }
|
|
3789
|
+
.ed-te-menu-sep { height: 1px; margin: 2px 4px; background: var(--md-border); }
|
|
3790
|
+
.ed-te-menu-delete { color: var(--md-ed-err); }
|
|
3687
3791
|
.ed-te-drop-indicator {
|
|
3688
3792
|
position: fixed; z-index: 8; height: 3px; border-radius: 2px;
|
|
3689
3793
|
background: var(--md-accent); pointer-events: none;
|
|
@@ -3817,6 +3921,48 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
|
|
|
3817
3921
|
.ed-seltb-btn:hover:not([aria-disabled="true"]) { color: var(--md-strong); }
|
|
3818
3922
|
.ed-seltb-sep { width: 1px; height: 18px; margin: 0 3px; background: var(--md-rule); align-self: center; }
|
|
3819
3923
|
.ed-ico { display: block; flex: none; }
|
|
3924
|
+
/* v3.15.0 T5: the URL field. A separate fixed element laid over the
|
|
3925
|
+
selection toolbar (which is hidden while it is up), never a child of it.
|
|
3926
|
+
Same surface, ring and shadow roles as the toolbar so it follows the
|
|
3927
|
+
theme; the input uses the field role. z-index 16 keeps it over the
|
|
3928
|
+
toolbar (15) and under the banner (999). */
|
|
3929
|
+
.ed-linkbar {
|
|
3930
|
+
position: fixed; z-index: 16;
|
|
3931
|
+
display: flex; align-items: center; gap: 6px; box-sizing: border-box;
|
|
3932
|
+
width: 300px; max-width: calc(100vw - 8px); padding: 4px 4px 4px 8px; border-radius: 8px;
|
|
3933
|
+
background: var(--md-ed-surface); color: var(--md-strong);
|
|
3934
|
+
box-shadow: 0 0 0 1px var(--md-ed-ring), 0 8px 24px var(--md-ed-shadow);
|
|
3935
|
+
}
|
|
3936
|
+
.ed-linkbar-icon { flex: none; display: inline-grid; place-items: center; color: var(--md-muted); }
|
|
3937
|
+
.ed-linkbar-input {
|
|
3938
|
+
flex: 1 1 auto; min-width: 0; height: 28px; box-sizing: border-box; padding: 0 8px;
|
|
3939
|
+
border: 1px solid var(--md-ed-ring); border-radius: 6px;
|
|
3940
|
+
background: var(--md-ed-field); color: inherit; font: inherit; font-size: 13px;
|
|
3941
|
+
}
|
|
3942
|
+
.ed-linkbar-input:focus { outline: none; border-color: var(--md-muted); }
|
|
3943
|
+
.ed-linkbar-apply {
|
|
3944
|
+
flex: none; height: 28px; padding: 0 10px; border: 0; border-radius: 6px;
|
|
3945
|
+
background: transparent; color: inherit; font: inherit; font-size: 13px; cursor: pointer;
|
|
3946
|
+
}
|
|
3947
|
+
.ed-linkbar-apply:hover { background: var(--md-ed-hover); }
|
|
3948
|
+
/* v3.15.0 T6: the link card, 6px under a link that holds the caret. Same
|
|
3949
|
+
surface, ring and shadow roles; the href is cut by an ellipsis. */
|
|
3950
|
+
.ed-linkcard {
|
|
3951
|
+
position: fixed; z-index: 15;
|
|
3952
|
+
display: flex; align-items: center; gap: 2px; box-sizing: border-box;
|
|
3953
|
+
max-width: calc(100vw - 8px); padding: 4px 4px 4px 10px; border-radius: 8px;
|
|
3954
|
+
background: var(--md-ed-surface); color: var(--md-strong); font-size: 13px;
|
|
3955
|
+
box-shadow: 0 0 0 1px var(--md-ed-ring), 0 8px 24px var(--md-ed-shadow);
|
|
3956
|
+
}
|
|
3957
|
+
.ed-linkcard-href {
|
|
3958
|
+
flex: 0 1 auto; min-width: 0; max-width: 320px; margin-right: 6px;
|
|
3959
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--md-muted);
|
|
3960
|
+
}
|
|
3961
|
+
.ed-linkcard-btn {
|
|
3962
|
+
flex: none; height: 26px; padding: 0 8px; border: 0; border-radius: 6px;
|
|
3963
|
+
background: transparent; color: inherit; font: inherit; font-size: 13px; cursor: pointer;
|
|
3964
|
+
}
|
|
3965
|
+
.ed-linkcard-btn:hover { background: var(--md-ed-hover); }
|
|
3820
3966
|
/* v3.1.0 §4: the always-visible editing toolbar. Its numeric language is
|
|
3821
3967
|
.lightbox-bar's (8px/12px padding, 6px gap, 13px type) over .ed-seltb's
|
|
3822
3968
|
dark pill, so the two floating surfaces read as one family.
|
|
@@ -4011,7 +4157,7 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
|
|
|
4011
4157
|
.ed-toolbar-status[data-state="failed"] { color: var(--md-ed-err); }
|
|
4012
4158
|
.ed-status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--md-ed-dirty); display: inline-block; }
|
|
4013
4159
|
.ed-toolbar-btn[data-ed-tb="save"][aria-pressed="true"] { color: var(--md-accent); background: var(--md-active-bg); }
|
|
4014
|
-
/* The H▾ dropdown: same singleton-panel idiom as
|
|
4160
|
+
/* The H▾ dropdown: same singleton-panel idiom as the ⠿ menu, but
|
|
4015
4161
|
anchored to a fixed bar rather than moved into a block, so it is
|
|
4016
4162
|
positioned in viewport coordinates by the client runtime.
|
|
4017
4163
|
max-height/overflow-y: same backstop role as .ed-handle-submenu above
|
|
@@ -4602,17 +4748,31 @@ ${mermaidInitTag}`)
|
|
|
4602
4748
|
// siblings are wrappers: the section ends at the next wrapper that is, or
|
|
4603
4749
|
// directly holds, a heading of the same or a shallower depth. In the reader
|
|
4604
4750
|
// the unit is the heading itself and the walk is the one it always was.
|
|
4751
|
+
// v3.15.0 R76: except for a heading nested in another block (a blockquote's
|
|
4752
|
+
// heading in the reader): the section index lists the text after that
|
|
4753
|
+
// block under the heading, so the unit is the top-level block that holds
|
|
4754
|
+
// it, and the walk (whose container is that block's parent) reaches the
|
|
4755
|
+
// text after the quote. Headings are still found only as top-level
|
|
4756
|
+
// siblings in the reader, so a quoted heading still ends no section.
|
|
4757
|
+
function topLevelBlockOf(node) {
|
|
4758
|
+
const root = node.closest('.content');
|
|
4759
|
+
if (!root) return node;
|
|
4760
|
+
let n = node;
|
|
4761
|
+
while (n.parentElement && n.parentElement !== root) n = n.parentElement;
|
|
4762
|
+
return n.parentElement === root ? n : node;
|
|
4763
|
+
}
|
|
4605
4764
|
function sectionBoundary(sectionId) {
|
|
4606
4765
|
const start = document.getElementById(sectionId);
|
|
4607
4766
|
if (!start) return null;
|
|
4608
4767
|
const startDepth = Number(start.getAttribute('data-reader-depth') || '1');
|
|
4609
|
-
const
|
|
4768
|
+
const wrapper = start.closest('.ed-block');
|
|
4769
|
+
const unit = wrapper || topLevelBlockOf(start);
|
|
4610
4770
|
let end = null;
|
|
4611
4771
|
let node = unit.nextElementSibling;
|
|
4612
4772
|
while (node) {
|
|
4613
4773
|
let heading = null;
|
|
4614
4774
|
if (node.matches && node.matches('[data-reader-heading]')) heading = node;
|
|
4615
|
-
else if (
|
|
4775
|
+
else if (wrapper && node.querySelector) heading = node.querySelector(':scope > [data-reader-heading]');
|
|
4616
4776
|
if (heading) {
|
|
4617
4777
|
const depth = Number(heading.getAttribute('data-reader-depth') || '1');
|
|
4618
4778
|
if (depth <= startDepth) {
|
|
@@ -4936,6 +5096,45 @@ ${mermaidInitTag}`)
|
|
|
4936
5096
|
setSidebarOpen(false);
|
|
4937
5097
|
}
|
|
4938
5098
|
});
|
|
5099
|
+
|
|
5100
|
+
// ── Screenshot mode (v3.16.0): only the document, for a full-page capture ──
|
|
5101
|
+
// The CSS beside '.capture-toast' hides everything that stays put while the
|
|
5102
|
+
// page scrolls. The column keeps the width measured here, so nothing reflows
|
|
5103
|
+
// and the reading position holds. Esc leaves; a reload never comes back in it.
|
|
5104
|
+
const captureBtn = document.getElementById('capture-mode-toggle');
|
|
5105
|
+
let captureToastTimer = 0;
|
|
5106
|
+
function setCaptureMode(on) {
|
|
5107
|
+
clearTimeout(captureToastTimer);
|
|
5108
|
+
const oldToast = document.querySelector('.capture-toast');
|
|
5109
|
+
if (oldToast) oldToast.remove();
|
|
5110
|
+
if (!on) {
|
|
5111
|
+
document.body.removeAttribute('data-capture-mode');
|
|
5112
|
+
document.body.style.removeProperty('--md2doc-capture-w');
|
|
5113
|
+
return;
|
|
5114
|
+
}
|
|
5115
|
+
setSidebarOpen(false);
|
|
5116
|
+
const w = document.querySelector('main.content').getBoundingClientRect().width;
|
|
5117
|
+
document.body.style.setProperty('--md2doc-capture-w', w + 'px');
|
|
5118
|
+
document.body.setAttribute('data-capture-mode', '');
|
|
5119
|
+
const toast = document.createElement('div');
|
|
5120
|
+
toast.className = 'capture-toast';
|
|
5121
|
+
toast.setAttribute('role', 'status');
|
|
5122
|
+
toast.textContent = 'Screenshot mode — press Esc or reload to exit';
|
|
5123
|
+
document.body.appendChild(toast);
|
|
5124
|
+
// Gone before the capture tool scrolls, or it would be on every slice too.
|
|
5125
|
+
captureToastTimer = setTimeout(() => toast.remove(), 2000);
|
|
5126
|
+
}
|
|
5127
|
+
if (captureBtn) {
|
|
5128
|
+
captureBtn.addEventListener('click', () => setCaptureMode(true));
|
|
5129
|
+
}
|
|
5130
|
+
// Registered ahead of the lightbox's own Esc handler, which closes the
|
|
5131
|
+
// diagram: while one is open, its Esc is the lightbox's and the mode stays.
|
|
5132
|
+
document.addEventListener('keydown', (event) => {
|
|
5133
|
+
if (lightboxIsOpen()) return;
|
|
5134
|
+
if (event.key === 'Escape' && document.body.hasAttribute('data-capture-mode')) {
|
|
5135
|
+
setCaptureMode(false);
|
|
5136
|
+
}
|
|
5137
|
+
});
|
|
4939
5138
|
document.addEventListener('click', (event) => {
|
|
4940
5139
|
if (!document.body.hasAttribute('data-sidebar-open')) return;
|
|
4941
5140
|
if (!event.target || !event.target.closest) return;
|
|
@@ -6381,7 +6580,11 @@ ${usesDrawioMultiPage ? ` // v3.4.0 batch2 Task 6: wired through a NAMED, reusa
|
|
|
6381
6580
|
drawioRefs: Array.from(drawioRefs, ([p, stamp]) => ({ path: p, stamp })),
|
|
6382
6581
|
};
|
|
6383
6582
|
}
|
|
6384
|
-
|
|
6583
|
+
// v3.15.0 B2 fix round 1 (R61): the resolved engine paths, so the editor
|
|
6584
|
+
// server's on-demand GET /api/lib/<name> serves exactly what this file inlines
|
|
6585
|
+
// instead of resolving them a second time.
|
|
6586
|
+
const diagramEnginePaths = { mermaid: localMermaidJs, wavedrom: localWaveDromJs, wavedromSkin: localWaveDromSkin };
|
|
6587
|
+
module.exports = { renderMarkdown, buildKatexStyleTag, diagramEnginePaths };
|
|
6385
6588
|
|
|
6386
6589
|
// Index of the first "{" in dot source that opens the graph body: not inside a
|
|
6387
6590
|
// double-quoted string, a // or /* */ comment, a line starting with # (dot
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// v3.15.0 B2 fix round 1 (R56): an EMPTY to-do item at the token level.
|
|
3
|
+
//
|
|
4
|
+
// marked reads `[ ]` / `[x]` as a checkbox only when a space follows it, and
|
|
5
|
+
// it trims an item's trailing whitespace first when the item is the last one
|
|
6
|
+
// of a NESTED list: `- a\n - [ ] \n` (end of file, or a paragraph next) lexes
|
|
7
|
+
// the child as a plain item whose text is "[ ]" (measured, marked 14.1.4).
|
|
8
|
+
// The editor writes exactly that shape for a new empty to-do (Enter at the end
|
|
9
|
+
// of a nested last to-do), and no byte form of an empty nested task survives
|
|
10
|
+
// marked without visible content. So the READING side decides: a list item
|
|
11
|
+
// whose whole text is `[ ]`, `[x]` or `[X]` and that marked did not already
|
|
12
|
+
// read as a task is an empty to-do, checked accordingly.
|
|
13
|
+
//
|
|
14
|
+
// One place for both readers: this module installs the rule on marked's own
|
|
15
|
+
// `list` tokenizer, so every marked.lexer() / marked.parse() in the process —
|
|
16
|
+
// the reader renderer (lib/md2doc.js) and the editor's block map
|
|
17
|
+
// (lib/editor/blockmap.js), which both require this file — sees the same
|
|
18
|
+
// token. An escaped box (`- \[ \]`, what the editor writes for that text in a
|
|
19
|
+
// plain item) is text and stays text. Other renderers (GitHub) still show a
|
|
20
|
+
// literal "[ ]" for such an item; that is what the file says.
|
|
21
|
+
const { marked } = require('marked');
|
|
22
|
+
|
|
23
|
+
const EMPTY_BOX = /^\[([ xX])\]$/;
|
|
24
|
+
|
|
25
|
+
function normalizeListToken(listToken) {
|
|
26
|
+
if (!listToken || !Array.isArray(listToken.items)) return listToken;
|
|
27
|
+
listToken.items.forEach((item) => {
|
|
28
|
+
if (!item || item.task) return;
|
|
29
|
+
const m = EMPTY_BOX.exec(String(item.text || '').trim());
|
|
30
|
+
if (!m) return;
|
|
31
|
+
item.task = true;
|
|
32
|
+
item.checked = m[1] !== ' ';
|
|
33
|
+
item.text = '';
|
|
34
|
+
item.tokens = [];
|
|
35
|
+
});
|
|
36
|
+
return listToken;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
marked.use({
|
|
40
|
+
tokenizer: {
|
|
41
|
+
list(src) {
|
|
42
|
+
const token = marked.Tokenizer.prototype.list.call(this, src);
|
|
43
|
+
return token ? normalizeListToken(token) : token;
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
module.exports = { normalizeListToken };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@helping-ai-workflow/md2doc",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.16.0",
|
|
4
4
|
"description": "Markdown → HTML / PDF renderer with WaveDrom, Mermaid, Graphviz, and draw.io support",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"markdown",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
},
|
|
47
47
|
"scripts": {
|
|
48
48
|
"preinstall": "node scripts/preinstall.js",
|
|
49
|
-
"test": "node test/md2doc.test.js && node test/highlight.test.js && node test/blockmap.test.js && node test/drawio.test.js && node test/images.test.js && node test/scroll-anchor.test.js && node test/lightbox.test.js && node test/lightbox-anno.test.js && node test/lightbox-anno-style.test.js && node test/reader-panels.test.js && node test/cli.test.js && node test/code-operator.test.js && node test/convert-md.test.js && node test/render-api.test.js && node test/lineops.test.js && node test/indent-clamp.test.js && node test/selection.test.js && node test/cell-range.test.js && node test/editmode-render.test.js && node test/editmode-wavedrom-reinit.test.js && node test/editor-server.test.js && node test/cli-edit.test.js && node test/open-viewer.test.js && node test/editor-client.test.js && node test/editor-client-runtime.test.js && node test/roundtrip.test.js && node test/byte-stability.test.js && node test/editor-server-throw.test.js && node test/editor-reader-rebind.test.js && node test/inline-md.test.js && node test/table-md.test.js && node test/table-anchor-recovery.test.js && node test/gate-compat.test.js && node test/history.test.js && node test/list-md.test.js && node test/table-width.test.js && node test/toolbar-model.test.js && node test/asset.test.js && node test/paste-md.test.js && node test/patchmap.test.js && node test/wave-codec.test.js && node test/wave-draw.test.js && node test/wave-geometry.test.js && node test/wave-store.test.js && node test/editor-journey.test.js && node test/detach-census.test.js && node test/editor-arrow-nav.test.js && node test/editor-cell-range.test.js && node test/reader-design.test.js && node test/theme.test.js && node test/release-check.test.js && node test/wave-overflow.test.js",
|
|
49
|
+
"test": "node test/md2doc.test.js && node test/highlight.test.js && node test/blockmap.test.js && node test/drawio.test.js && node test/images.test.js && node test/scroll-anchor.test.js && node test/lightbox.test.js && node test/lightbox-anno.test.js && node test/lightbox-anno-style.test.js && node test/reader-panels.test.js && node test/cli.test.js && node test/code-operator.test.js && node test/convert-md.test.js && node test/block-catalog.test.js && node test/render-api.test.js && node test/lineops.test.js && node test/indent-clamp.test.js && node test/selection.test.js && node test/cell-range.test.js && node test/editmode-render.test.js && node test/editmode-wavedrom-reinit.test.js && node test/editor-server.test.js && node test/cli-edit.test.js && node test/open-viewer.test.js && node test/editor-client.test.js && node test/editor-client-runtime.test.js && node test/roundtrip.test.js && node test/byte-stability.test.js && node test/editor-server-throw.test.js && node test/editor-reader-rebind.test.js && node test/inline-md.test.js && node test/table-md.test.js && node test/table-anchor-recovery.test.js && node test/gate-compat.test.js && node test/history.test.js && node test/list-md.test.js && node test/table-width.test.js && node test/toolbar-model.test.js && node test/asset.test.js && node test/paste-md.test.js && node test/patchmap.test.js && node test/wave-codec.test.js && node test/wave-draw.test.js && node test/wave-geometry.test.js && node test/wave-store.test.js && node test/editor-journey.test.js && node test/detach-census.test.js && node test/editor-arrow-nav.test.js && node test/editor-cell-range.test.js && node test/reader-design.test.js && node test/theme.test.js && node test/release-check.test.js && node test/wave-overflow.test.js",
|
|
50
50
|
"test:browser": "node test/reader-click.test.js && node test/editor-click.test.js && node test/wave-canvas.test.js"
|
|
51
51
|
},
|
|
52
52
|
"repository": {
|