@helping-ai-workflow/md2doc 3.14.1 → 3.15.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 +151 -26
- 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
|
|
@@ -3582,6 +3585,41 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
|
|
|
3582
3585
|
.ed-handle-submenu { left: 100%; margin-left: 4px; max-height: calc(100vh - 8px); overflow-y: auto; }
|
|
3583
3586
|
.ed-handle-menu-btn:hover { background: var(--md-ed-hover); }
|
|
3584
3587
|
.ed-handle-menu-btn[hidden] { display: none; }
|
|
3588
|
+
/* v3.15.0 T1: the "/" block menu (decision D9, layout M1). Fixed, placed
|
|
3589
|
+
4px under the caret by the client runtime (above it when there is no
|
|
3590
|
+
room below) and closed on scroll. 280px wide, its list scrolls on its
|
|
3591
|
+
own under a 360px cap; group titles, 32px rows of a 16px icon, the label
|
|
3592
|
+
and a muted monospace hint on the right; a key-help footer. The row the
|
|
3593
|
+
keyboard cursor is on carries data-ed-slash-cursor. Same surface, ring
|
|
3594
|
+
and shadow roles as the other editor menus, so it follows the theme.
|
|
3595
|
+
(No backticks in this comment: it lives inside a JS template literal.) */
|
|
3596
|
+
.ed-slash-menu {
|
|
3597
|
+
position: fixed; z-index: 100; width: 280px; max-height: 360px;
|
|
3598
|
+
display: flex; flex-direction: column; box-sizing: border-box; overflow: hidden;
|
|
3599
|
+
border-radius: 8px; background: var(--md-ed-surface); color: var(--md-strong);
|
|
3600
|
+
box-shadow: 0 0 0 1px var(--md-ed-ring), 0 8px 24px var(--md-ed-shadow);
|
|
3601
|
+
font-size: 13px; line-height: 1.3;
|
|
3602
|
+
}
|
|
3603
|
+
.ed-slash-list { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 4px; }
|
|
3604
|
+
.ed-slash-group { padding: 8px 8px 4px; font-size: 11px; font-weight: 600; color: var(--md-muted); }
|
|
3605
|
+
.ed-slash-item {
|
|
3606
|
+
display: flex; align-items: center; gap: 8px; width: 100%; height: 32px;
|
|
3607
|
+
padding: 0 8px; box-sizing: border-box; margin: 0;
|
|
3608
|
+
border: 0; border-radius: 6px; background: transparent; color: inherit;
|
|
3609
|
+
font: inherit; text-align: left; cursor: pointer;
|
|
3610
|
+
}
|
|
3611
|
+
.ed-slash-item:hover, .ed-slash-item[data-ed-slash-cursor] { background: var(--md-ed-hover); }
|
|
3612
|
+
.ed-slash-item svg { flex: 0 0 auto; width: 16px; height: 16px; margin: 0; }
|
|
3613
|
+
.ed-slash-label { flex: 1 1 auto; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
3614
|
+
.ed-slash-hint {
|
|
3615
|
+
flex: 0 0 auto; color: var(--md-muted); font-size: 12px;
|
|
3616
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
3617
|
+
}
|
|
3618
|
+
.ed-slash-empty { padding: 8px; color: var(--md-muted); }
|
|
3619
|
+
.ed-slash-foot {
|
|
3620
|
+
flex: 0 0 auto; padding: 6px 10px; font-size: 11px; color: var(--md-muted);
|
|
3621
|
+
border-top: 1px solid var(--md-ed-ring);
|
|
3622
|
+
}
|
|
3585
3623
|
/* §10-gap fix: block-level INSERT — the + button sits NEXT TO the ⠿
|
|
3586
3624
|
handle in the left gutter, same node/visibility-toggle shape as
|
|
3587
3625
|
.ed-handle above (real per-block node, opacity-only reveal on hover).
|
|
@@ -3613,23 +3651,8 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
|
|
|
3613
3651
|
.ed-block:hover .ed-insert,
|
|
3614
3652
|
.ed-insert:focus { opacity: 1; }
|
|
3615
3653
|
.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); }
|
|
3654
|
+
/* v3.15.0 T2: the + button opens the "/" block menu (.ed-slash-menu
|
|
3655
|
+
above) on a new empty paragraph; its own small pill menu is gone. */
|
|
3633
3656
|
/* Task 5: hover-edge column/row insert bubbles — a SINGLETON pair of "+"
|
|
3634
3657
|
buttons (client runtime repositions them via getBoundingClientRect(),
|
|
3635
3658
|
never creates more than these two). Visual language deliberately
|
|
@@ -3648,6 +3671,32 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
|
|
|
3648
3671
|
}
|
|
3649
3672
|
.ed-tb-insert:hover { background: var(--md-accent); color: var(--md-ed-surface); }
|
|
3650
3673
|
.ed-tb-insert[hidden] { display: none; }
|
|
3674
|
+
/* v3.15.0 T8 (D20): the trailing add bars, a SINGLETON pair placed by the
|
|
3675
|
+
client runtime along the grid of the hovered table: .ed-tb-addrow under
|
|
3676
|
+
the last row (the grid's width; 22px tall, which the client lowers to
|
|
3677
|
+
the gap above the block under the table, R97), .ed-tb-addcol right of the
|
|
3678
|
+
last column (22px wide, the grid's height), each a centred plus on the
|
|
3679
|
+
hover tint. z-index 6 sits under the grips (7) and the bubbles (8), so a
|
|
3680
|
+
press where they overlap goes to those. Two rules, one per bar, because
|
|
3681
|
+
each is its own fixed overlay. */
|
|
3682
|
+
.ed-tb-addrow {
|
|
3683
|
+
position: fixed; z-index: 6;
|
|
3684
|
+
height: 22px; padding: 0; margin: 0; box-sizing: border-box;
|
|
3685
|
+
display: flex; align-items: center; justify-content: center;
|
|
3686
|
+
border: 0; border-radius: 4px;
|
|
3687
|
+
background: var(--md-ed-hover); color: var(--md-muted);
|
|
3688
|
+
font: inherit; font-size: 14px; line-height: 1; cursor: pointer;
|
|
3689
|
+
}
|
|
3690
|
+
.ed-tb-addcol {
|
|
3691
|
+
position: fixed; z-index: 6;
|
|
3692
|
+
width: 22px; padding: 0; margin: 0; box-sizing: border-box;
|
|
3693
|
+
display: flex; align-items: center; justify-content: center;
|
|
3694
|
+
border: 0; border-radius: 4px;
|
|
3695
|
+
background: var(--md-ed-hover); color: var(--md-muted);
|
|
3696
|
+
font: inherit; font-size: 14px; line-height: 1; cursor: pointer;
|
|
3697
|
+
}
|
|
3698
|
+
.ed-tb-addrow:hover, .ed-tb-addcol:hover { color: var(--md-accent); }
|
|
3699
|
+
.ed-tb-addrow[hidden], .ed-tb-addcol[hidden] { display: none; }
|
|
3651
3700
|
/* Task 6: table edge-click menus (delete/align) + row drag-reorder.
|
|
3652
3701
|
'.ed-te-hl' marks the currently-selected column's or row's CELLS
|
|
3653
3702
|
(th+td) — never the <tr>, which paints nothing the user can see once a
|
|
@@ -3667,23 +3716,39 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
|
|
|
3667
3716
|
.ed-cell-range { background: var(--md-ed-range) !important; }
|
|
3668
3717
|
.ed-cell-range-active { caret-color: transparent; }
|
|
3669
3718
|
.ed-cell-range-active ::selection { background: transparent; }
|
|
3719
|
+
/* v3.15.0 T7 (D18/D19): the edge menu is a vertical list in the look of
|
|
3720
|
+
the block menu above (.ed-handle-menu): 30px rows of a 16px icon and a
|
|
3721
|
+
label, an alignment row of three icon buttons (aria-pressed marks the
|
|
3722
|
+
current one), a separator, and the delete row in the error colour. The
|
|
3723
|
+
row the keyboard cursor is on carries data-ed-te-cursor. Placed by the
|
|
3724
|
+
client runtime under the column grip or beside the row grip, clamped to
|
|
3725
|
+
the table and the viewport. */
|
|
3670
3726
|
.ed-te-menu {
|
|
3671
3727
|
position: fixed; z-index: 12;
|
|
3672
|
-
display: flex; align-items:
|
|
3673
|
-
padding: 4px
|
|
3728
|
+
display: flex; flex-direction: column; align-items: stretch; gap: 2px;
|
|
3729
|
+
padding: 4px; border-radius: 8px;
|
|
3674
3730
|
background: var(--md-ed-surface); color: var(--md-strong);
|
|
3675
3731
|
box-shadow: 0 0 0 1px var(--md-ed-ring), 0 8px 24px var(--md-ed-shadow);
|
|
3676
3732
|
}
|
|
3677
3733
|
.ed-te-menu[hidden] { display: none; }
|
|
3678
3734
|
.ed-te-menu-btn {
|
|
3679
|
-
min-width:
|
|
3735
|
+
min-width: 132px; height: 30px; padding: 0 10px 0 8px;
|
|
3736
|
+
display: flex; align-items: center; gap: 8px;
|
|
3680
3737
|
border: 1px solid transparent; border-radius: 6px;
|
|
3681
3738
|
background: transparent; color: inherit;
|
|
3682
|
-
font: inherit; font-size:
|
|
3683
|
-
white-space: nowrap; cursor: pointer;
|
|
3739
|
+
font: inherit; font-size: 13px; line-height: 1;
|
|
3740
|
+
text-align: left; white-space: nowrap; cursor: pointer;
|
|
3684
3741
|
}
|
|
3742
|
+
.ed-te-menu-btn .ed-menu-icon { flex: 0 0 auto; width: 16px; height: 16px; margin: 0; }
|
|
3685
3743
|
.ed-te-menu-btn:hover { background: var(--md-ed-hover); }
|
|
3686
3744
|
.ed-te-menu-btn[hidden] { display: none; }
|
|
3745
|
+
.ed-te-menu-btn[data-ed-te-cursor] { background: var(--md-ed-hover); box-shadow: inset 0 0 0 1px var(--md-ed-ring); }
|
|
3746
|
+
.ed-te-menu-align { display: flex; gap: 2px; }
|
|
3747
|
+
.ed-te-menu-align[hidden] { display: none; }
|
|
3748
|
+
.ed-te-menu-align .ed-te-menu-btn { flex: 1 1 0; min-width: 0; padding: 0; justify-content: center; }
|
|
3749
|
+
.ed-te-menu-align .ed-te-menu-btn[aria-pressed="true"] { color: var(--md-accent); background: var(--md-ed-hover); }
|
|
3750
|
+
.ed-te-menu-sep { height: 1px; margin: 2px 4px; background: var(--md-border); }
|
|
3751
|
+
.ed-te-menu-delete { color: var(--md-ed-err); }
|
|
3687
3752
|
.ed-te-drop-indicator {
|
|
3688
3753
|
position: fixed; z-index: 8; height: 3px; border-radius: 2px;
|
|
3689
3754
|
background: var(--md-accent); pointer-events: none;
|
|
@@ -3817,6 +3882,48 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
|
|
|
3817
3882
|
.ed-seltb-btn:hover:not([aria-disabled="true"]) { color: var(--md-strong); }
|
|
3818
3883
|
.ed-seltb-sep { width: 1px; height: 18px; margin: 0 3px; background: var(--md-rule); align-self: center; }
|
|
3819
3884
|
.ed-ico { display: block; flex: none; }
|
|
3885
|
+
/* v3.15.0 T5: the URL field. A separate fixed element laid over the
|
|
3886
|
+
selection toolbar (which is hidden while it is up), never a child of it.
|
|
3887
|
+
Same surface, ring and shadow roles as the toolbar so it follows the
|
|
3888
|
+
theme; the input uses the field role. z-index 16 keeps it over the
|
|
3889
|
+
toolbar (15) and under the banner (999). */
|
|
3890
|
+
.ed-linkbar {
|
|
3891
|
+
position: fixed; z-index: 16;
|
|
3892
|
+
display: flex; align-items: center; gap: 6px; box-sizing: border-box;
|
|
3893
|
+
width: 300px; max-width: calc(100vw - 8px); padding: 4px 4px 4px 8px; border-radius: 8px;
|
|
3894
|
+
background: var(--md-ed-surface); color: var(--md-strong);
|
|
3895
|
+
box-shadow: 0 0 0 1px var(--md-ed-ring), 0 8px 24px var(--md-ed-shadow);
|
|
3896
|
+
}
|
|
3897
|
+
.ed-linkbar-icon { flex: none; display: inline-grid; place-items: center; color: var(--md-muted); }
|
|
3898
|
+
.ed-linkbar-input {
|
|
3899
|
+
flex: 1 1 auto; min-width: 0; height: 28px; box-sizing: border-box; padding: 0 8px;
|
|
3900
|
+
border: 1px solid var(--md-ed-ring); border-radius: 6px;
|
|
3901
|
+
background: var(--md-ed-field); color: inherit; font: inherit; font-size: 13px;
|
|
3902
|
+
}
|
|
3903
|
+
.ed-linkbar-input:focus { outline: none; border-color: var(--md-muted); }
|
|
3904
|
+
.ed-linkbar-apply {
|
|
3905
|
+
flex: none; height: 28px; padding: 0 10px; border: 0; border-radius: 6px;
|
|
3906
|
+
background: transparent; color: inherit; font: inherit; font-size: 13px; cursor: pointer;
|
|
3907
|
+
}
|
|
3908
|
+
.ed-linkbar-apply:hover { background: var(--md-ed-hover); }
|
|
3909
|
+
/* v3.15.0 T6: the link card, 6px under a link that holds the caret. Same
|
|
3910
|
+
surface, ring and shadow roles; the href is cut by an ellipsis. */
|
|
3911
|
+
.ed-linkcard {
|
|
3912
|
+
position: fixed; z-index: 15;
|
|
3913
|
+
display: flex; align-items: center; gap: 2px; box-sizing: border-box;
|
|
3914
|
+
max-width: calc(100vw - 8px); padding: 4px 4px 4px 10px; border-radius: 8px;
|
|
3915
|
+
background: var(--md-ed-surface); color: var(--md-strong); font-size: 13px;
|
|
3916
|
+
box-shadow: 0 0 0 1px var(--md-ed-ring), 0 8px 24px var(--md-ed-shadow);
|
|
3917
|
+
}
|
|
3918
|
+
.ed-linkcard-href {
|
|
3919
|
+
flex: 0 1 auto; min-width: 0; max-width: 320px; margin-right: 6px;
|
|
3920
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--md-muted);
|
|
3921
|
+
}
|
|
3922
|
+
.ed-linkcard-btn {
|
|
3923
|
+
flex: none; height: 26px; padding: 0 8px; border: 0; border-radius: 6px;
|
|
3924
|
+
background: transparent; color: inherit; font: inherit; font-size: 13px; cursor: pointer;
|
|
3925
|
+
}
|
|
3926
|
+
.ed-linkcard-btn:hover { background: var(--md-ed-hover); }
|
|
3820
3927
|
/* v3.1.0 §4: the always-visible editing toolbar. Its numeric language is
|
|
3821
3928
|
.lightbox-bar's (8px/12px padding, 6px gap, 13px type) over .ed-seltb's
|
|
3822
3929
|
dark pill, so the two floating surfaces read as one family.
|
|
@@ -4011,7 +4118,7 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
|
|
|
4011
4118
|
.ed-toolbar-status[data-state="failed"] { color: var(--md-ed-err); }
|
|
4012
4119
|
.ed-status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--md-ed-dirty); display: inline-block; }
|
|
4013
4120
|
.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
|
|
4121
|
+
/* The H▾ dropdown: same singleton-panel idiom as the ⠿ menu, but
|
|
4015
4122
|
anchored to a fixed bar rather than moved into a block, so it is
|
|
4016
4123
|
positioned in viewport coordinates by the client runtime.
|
|
4017
4124
|
max-height/overflow-y: same backstop role as .ed-handle-submenu above
|
|
@@ -4602,17 +4709,31 @@ ${mermaidInitTag}`)
|
|
|
4602
4709
|
// siblings are wrappers: the section ends at the next wrapper that is, or
|
|
4603
4710
|
// directly holds, a heading of the same or a shallower depth. In the reader
|
|
4604
4711
|
// the unit is the heading itself and the walk is the one it always was.
|
|
4712
|
+
// v3.15.0 R76: except for a heading nested in another block (a blockquote's
|
|
4713
|
+
// heading in the reader): the section index lists the text after that
|
|
4714
|
+
// block under the heading, so the unit is the top-level block that holds
|
|
4715
|
+
// it, and the walk (whose container is that block's parent) reaches the
|
|
4716
|
+
// text after the quote. Headings are still found only as top-level
|
|
4717
|
+
// siblings in the reader, so a quoted heading still ends no section.
|
|
4718
|
+
function topLevelBlockOf(node) {
|
|
4719
|
+
const root = node.closest('.content');
|
|
4720
|
+
if (!root) return node;
|
|
4721
|
+
let n = node;
|
|
4722
|
+
while (n.parentElement && n.parentElement !== root) n = n.parentElement;
|
|
4723
|
+
return n.parentElement === root ? n : node;
|
|
4724
|
+
}
|
|
4605
4725
|
function sectionBoundary(sectionId) {
|
|
4606
4726
|
const start = document.getElementById(sectionId);
|
|
4607
4727
|
if (!start) return null;
|
|
4608
4728
|
const startDepth = Number(start.getAttribute('data-reader-depth') || '1');
|
|
4609
|
-
const
|
|
4729
|
+
const wrapper = start.closest('.ed-block');
|
|
4730
|
+
const unit = wrapper || topLevelBlockOf(start);
|
|
4610
4731
|
let end = null;
|
|
4611
4732
|
let node = unit.nextElementSibling;
|
|
4612
4733
|
while (node) {
|
|
4613
4734
|
let heading = null;
|
|
4614
4735
|
if (node.matches && node.matches('[data-reader-heading]')) heading = node;
|
|
4615
|
-
else if (
|
|
4736
|
+
else if (wrapper && node.querySelector) heading = node.querySelector(':scope > [data-reader-heading]');
|
|
4616
4737
|
if (heading) {
|
|
4617
4738
|
const depth = Number(heading.getAttribute('data-reader-depth') || '1');
|
|
4618
4739
|
if (depth <= startDepth) {
|
|
@@ -6381,7 +6502,11 @@ ${usesDrawioMultiPage ? ` // v3.4.0 batch2 Task 6: wired through a NAMED, reusa
|
|
|
6381
6502
|
drawioRefs: Array.from(drawioRefs, ([p, stamp]) => ({ path: p, stamp })),
|
|
6382
6503
|
};
|
|
6383
6504
|
}
|
|
6384
|
-
|
|
6505
|
+
// v3.15.0 B2 fix round 1 (R61): the resolved engine paths, so the editor
|
|
6506
|
+
// server's on-demand GET /api/lib/<name> serves exactly what this file inlines
|
|
6507
|
+
// instead of resolving them a second time.
|
|
6508
|
+
const diagramEnginePaths = { mermaid: localMermaidJs, wavedrom: localWaveDromJs, wavedromSkin: localWaveDromSkin };
|
|
6509
|
+
module.exports = { renderMarkdown, buildKatexStyleTag, diagramEnginePaths };
|
|
6385
6510
|
|
|
6386
6511
|
// Index of the first "{" in dot source that opens the graph body: not inside a
|
|
6387
6512
|
// 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.15.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": {
|