@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.
@@ -423,5 +423,24 @@
423
423
  return serializeInline(rootEl).unsupported.length === 0;
424
424
  }
425
425
 
426
- return { serializeInline, canWysiwyg, escapeText, escapeBlockStarts };
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
  });
@@ -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
@@ -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 — degrading to literal '[ ]', which
498
- // is what the source said anyway — whenever the next block is not deeper,
499
- // i.e. when the item has no child to attach to. Merging it onto a SIBLING
500
- // would invent nesting that the document never had.
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;
@@ -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 ['quote', 'ordered-list', 'check', 'indent', 'outdent']) {
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
- /* The + button's small menu: 段落/標題/清單/表格/程式碼 — same dark
3617
- translucent pill language as .ed-handle-menu above. */
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: center; gap: 4px;
3673
- padding: 4px 6px; border-radius: 8px;
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: 28px; height: 26px; padding: 0 8px;
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: 12px; line-height: 1;
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 .ed-insert-menu, but
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 unit = start.closest('.ed-block') || start;
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 (unit !== start && node.querySelector) heading = node.querySelector(':scope > [data-reader-heading]');
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
- module.exports = { renderMarkdown };
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.14.1",
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": {