@helping-ai-workflow/md2doc 3.0.1 → 3.2.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/md2doc.js CHANGED
@@ -313,7 +313,7 @@ function renderEditModeList(listToken, blocks, biRef, out) {
313
313
  item.tokens.filter((tk) => tk.type === 'list')
314
314
  .forEach((ct) => renderEditModeList(ct, blocks, biRef, parts));
315
315
  });
316
- return parts.join('\n');
316
+ return parts;
317
317
  }
318
318
 
319
319
  async function renderMarkdown(mdText, srcPath, opts = {}) {
@@ -487,6 +487,10 @@ async function renderMarkdown(mdText, srcPath, opts = {}) {
487
487
  }
488
488
 
489
489
  let bodyHtml;
490
+ // v3.2.0: the per-block edit-mode parts (1:1 with `blocks`), already baked.
491
+ // Stays null outside editMode — the reader/PDF path has no per-block DOM to
492
+ // patch, only the whole-document `bodyHtml`.
493
+ let parts = null;
490
494
  let tocHtml = '';
491
495
  let serializedSections = '[]';
492
496
  // NOTE: intentionally no try/catch here. A throw from marked/katex parsing
@@ -893,7 +897,7 @@ ${itemsHtml}
893
897
  // exactly what the whole-document parse would emit for that token,
894
898
  // using the same renderer/options set above.
895
899
  const tokens = marked.lexer(mdPre);
896
- const parts = [];
900
+ parts = [];
897
901
  // `v` is the block cursor; `prevLi`/`types` are the §3.8 run state the
898
902
  // flat list renderer above threads through its own recursion (see
899
903
  // liRunStartsHere()).
@@ -901,7 +905,7 @@ ${itemsHtml}
901
905
  for (const t of tokens) {
902
906
  if (t.type === 'space') continue;
903
907
  if (t.type === 'list') {
904
- parts.push(renderEditModeList(t, blocks, biRef));
908
+ parts.push(...renderEditModeList(t, blocks, biRef));
905
909
  continue;
906
910
  }
907
911
  resetLiRunState(biRef); // §3.8 rule (c): a non-li block ends every run
@@ -920,6 +924,14 @@ ${itemsHtml}
920
924
  throw new Error('edit-mode render consumed ' + biRef.v +
921
925
  ' blocks but the block map has ' + blocks.length);
922
926
  }
927
+ // v3.2.0: bake EACH part, then join — not join-then-bake. The patch path
928
+ // inserts parts[i] straight into the DOM and the client has no graphviz
929
+ // renderer (__md2docInitDiagrams handles only mermaid and WaveDrom), so
930
+ // an unbaked part lands as an empty <div class="graphviz">. bakeGraphviz
931
+ // short-circuits on parts with no data-graphviz-src, and each placeholder
932
+ // falls entirely inside one part, so this is byte-identical to baking the
933
+ // joined string.
934
+ parts = await Promise.all(parts.map(bakeGraphviz));
923
935
  bodyHtml = parts.join('\n');
924
936
  } else {
925
937
  bodyHtml = marked.parse(mdPre);
@@ -1328,6 +1340,48 @@ ${itemsHtml}
1328
1340
  content: ""; position: absolute;
1329
1341
  left: calc(-1 * (var(--ed-gutter-w) + var(--ed-gutter-shift))); top: 0;
1330
1342
  width: calc(var(--ed-gutter-w) + var(--ed-gutter-shift)); height: 100%;
1343
+ }
1344
+ /* v3.1.0 §4: room for the fixed toolbar (.ed-toolbar, further up this
1345
+ stylesheet). The bar is position:fixed — NOT sticky — because
1346
+ lib/editor/client.js mounts it on document.body: rerenderAll() replaces
1347
+ .content's innerHTML wholesale, so a toolbar living inside the content
1348
+ column would be destroyed by the first commit with nothing to rebuild
1349
+ it. Fixed chrome takes no layout space, so the page has to make its
1350
+ own: this is the only place that offset is written. The mobile rule at
1351
+ max-width:1080px above already reserves 60px for .sidebar-toggle; the
1352
+ bar sits above it, so the two offsets ADD there.
1353
+ .reader-sidebar is sticky at top: 24px in the base stylesheet — pushed
1354
+ down by the same amount here so the outline does not slide under the
1355
+ bar, with its height reduced to match so it still ends at the viewport
1356
+ bottom. */
1357
+ .page-layout { padding-top: var(--ed-toolbar-h); }
1358
+ .reader-sidebar {
1359
+ top: calc(24px + var(--ed-toolbar-h));
1360
+ height: calc(100vh - 48px - var(--ed-toolbar-h));
1361
+ }
1362
+ @media (max-width: 1080px) {
1363
+ .page-layout { padding-top: calc(60px + var(--ed-toolbar-h)); }
1364
+ .reader-sidebar { top: 0; height: 100vh; padding-top: calc(16px + var(--ed-toolbar-h)); }
1365
+ }
1366
+ /* The 大綱 button's DESKTOP half, on an attribute of its OWN.
1367
+ ⚠ It deliberately does NOT reuse body[data-sidebar-open], and the reason
1368
+ was measured rather than reasoned: the rule
1369
+ 'body[data-sidebar-open] .sidebar-scrim { display: block; }' sits at BASE scope in this
1370
+ stylesheet, not inside the max-width:1080px drawer block. Giving that
1371
+ same attribute a second, desktop meaning therefore also paints
1372
+ .sidebar-scrim (position:fixed; inset:0; z-index:98; rgba(0,0,0,.35))
1373
+ across the whole viewport — the desktop page went grey and
1374
+ click-through-dead, and the scrim's own click handler (which calls
1375
+ setSidebarOpen(false)) meant the next click anywhere silently undid it.
1376
+ A private attribute, written by toggleOutlineSidebar() in
1377
+ lib/editor/client.js, leaves data-sidebar-open to the mobile drawer
1378
+ alone and lets #sidebar-toggle's aria-expanded stay truthful.
1379
+ The attribute names the non-default state ("hidden") because on desktop
1380
+ the outline is a sticky column that is SHOWN by default; the mobile
1381
+ drawer keeps its own "open" attribute for the same reason in reverse. */
1382
+ @media (min-width: 1081px) {
1383
+ body[data-ed-outline-hidden] .reader-sidebar,
1384
+ body[data-ed-outline-hidden] .sidebar-splitter { display: none; }
1331
1385
  }`;
1332
1386
 
1333
1387
  const html = `<!DOCTYPE html>
@@ -2185,6 +2239,17 @@ ${itemsHtml}
2185
2239
  }
2186
2240
  .ed-block:hover .ed-handle,
2187
2241
  .ed-handle:focus { opacity: 1; }
2242
+ /* v3.0.2: a table's row grip is a document.body child at z-index 7 that
2243
+ straddles the table's left border, and a table block's ⠿ sits at the
2244
+ header row's own Y — so a pointer travelling left toward the ⠿ crosses
2245
+ the grip, elementFromPoint stops returning the block, and :hover above
2246
+ goes false mid-approach. The grip cannot move (it must be a body child
2247
+ to stay put while the table scrolls), so lib/editor/client.js holds the
2248
+ gutter lit with this class for the duration instead. It does NOT
2249
+ restore :hover, so .ed-block:hover's own outline still blinks there —
2250
+ a recorded, unfixed residue. */
2251
+ .ed-block.ed-keep-lit .ed-handle,
2252
+ .ed-block.ed-keep-lit .ed-insert { opacity: 1; }
2188
2253
  .ed-handle:hover { background: rgba(0, 0, 0, 0.08); }
2189
2254
  /* The ⠿ handle's menu: 轉換成 › / 建立副本 / 刪除 / MD 原始碼 (spec §3.7),
2190
2255
  each led by a 16px currentColor icon (v2.12.0; drawn in
@@ -2469,6 +2534,110 @@ ${itemsHtml}
2469
2534
  .ed-seltb-i { font-style: italic; }
2470
2535
  .ed-seltb-s { text-decoration: line-through; }
2471
2536
  .ed-seltb-u { text-decoration: underline; }
2537
+ /* v3.1.0 §4: the always-visible editing toolbar. Its numeric language is
2538
+ .lightbox-bar's (8px/12px padding, 6px gap, 13px type) over .ed-seltb's
2539
+ dark pill, so the two floating surfaces read as one family.
2540
+ position:fixed, NOT sticky: lib/editor/client.js mounts this on
2541
+ document.body because rerenderAll() replaces .content's innerHTML
2542
+ wholesale and would silently destroy a toolbar mounted inside it.
2543
+ Deliberately does NOT read any --ed-gutter-* token — those describe the
2544
+ ⠿/+ pair's geometry and this is a new, unrelated scale (the height is
2545
+ its own token because the edit-mode layout chunk at the end of this
2546
+ stylesheet has to reserve exactly that much room for it).
2547
+ z-index sits above the mobile drawer (99) and .sidebar-toggle (100) but
2548
+ below .ed-conflict (999): a save conflict must always be reachable. */
2549
+ :root { --ed-toolbar-h: 44px; }
2550
+ .ed-toolbar {
2551
+ position: fixed; top: 0; left: 0; right: 0; z-index: 101;
2552
+ box-sizing: border-box; height: var(--ed-toolbar-h);
2553
+ display: flex; align-items: center; justify-content: center;
2554
+ flex-wrap: nowrap; overflow-x: auto; overflow-y: hidden;
2555
+ gap: 6px; padding: 8px 12px;
2556
+ background: rgba(16, 18, 21, 0.92); color: #e6edf3;
2557
+ font-size: 13px;
2558
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
2559
+ }
2560
+ .ed-toolbar-sep {
2561
+ flex: 0 0 auto; width: 1px; height: 18px; margin: 0 2px;
2562
+ background: rgba(255, 255, 255, 0.25);
2563
+ }
2564
+ .ed-toolbar-btn {
2565
+ flex: 0 0 auto;
2566
+ min-width: 28px; height: 26px; padding: 0 8px;
2567
+ border: 1px solid rgba(255, 255, 255, 0.25); border-radius: 6px;
2568
+ background: rgba(255, 255, 255, 0.08); color: inherit;
2569
+ font: inherit; font-size: 13px; line-height: 1;
2570
+ white-space: nowrap; cursor: pointer;
2571
+ }
2572
+ .ed-toolbar-btn:hover:not(:disabled) { background: rgba(255, 255, 255, 0.18); }
2573
+ .ed-toolbar-btn:disabled { opacity: 0.38; cursor: default; }
2574
+ .ed-toolbar-btn[aria-pressed="true"] {
2575
+ background: #e6edf3; color: #101215; border-color: #e6edf3;
2576
+ }
2577
+ .ed-toolbar-b { font-weight: bold; }
2578
+ .ed-toolbar-i { font-style: italic; }
2579
+ .ed-toolbar-s { text-decoration: line-through; }
2580
+ /* H1 fix: the brief's literal margin-left: auto (a flex-flow item) was
2581
+ measured to break .ed-toolbar's justify-content: center — an auto
2582
+ margin on any flex item absorbs ALL positive free space before
2583
+ justify-content sees any, so the ~22-button group snapped flush left on
2584
+ every viewport wide enough not to overflow. position: fixed removes
2585
+ the slot from the flex layout entirely (buttons measured byte-identical
2586
+ to a build with the slot absent, at both a 1400px and a 420px viewport),
2587
+ and — unlike position: absolute against .ed-toolbar's own
2588
+ position: fixed box — it anchors to the browser viewport, not to
2589
+ .ed-toolbar's scrolled content, so it does not drift as the button
2590
+ row's overflow-x: auto scrolls (measured identical rect at scrollLeft
2591
+ 0 / mid / max, where the absolute alternative moved by the exact scroll
2592
+ delta). References no gutter-alignment custom property.
2593
+ H2 fix: pointer-events: none below is LOAD-BEARING while this slot
2594
+ ships empty, not decorative. At narrow viewports .ed-toolbar's own
2595
+ overflow-x: auto lets the button row scroll to a rest position where
2596
+ this fixed 7rem box's footprint physically covers three real buttons
2597
+ (image / outline / preview, measured at 420px width, scrollLeft at its
2598
+ max 221) — document.elementFromPoint() over any of them resolves to
2599
+ this span, not the button underneath, and a real click on it never
2600
+ reaches the button's listener. Harmless today only because the span
2601
+ carries no content to interact with. WHOEVER WIRES UP TWO-WAY SYNC
2602
+ (§3) AND PUTS REAL CONTENT OR CONTROLS HERE MUST remove or scope this
2603
+ rule AND independently solve the overlap (e.g. reflow the slot in-flow
2604
+ at narrow widths, or shrink/hide it below some width) — simply
2605
+ deleting this line without doing that reopens the swallowed-click
2606
+ regression, this time on content someone can actually see and try to
2607
+ click. */
2608
+ .ed-toolbar-status { position: fixed; top: 0; right: 12px;
2609
+ height: var(--ed-toolbar-h); line-height: var(--ed-toolbar-h);
2610
+ min-width: 7rem; text-align: right; font-size: 12px; opacity: .8;
2611
+ pointer-events: none; }
2612
+ /* The H▾ dropdown: same singleton-panel idiom as .ed-insert-menu, but
2613
+ anchored to a fixed bar rather than moved into a block, so it is
2614
+ positioned in viewport coordinates by the client runtime. */
2615
+ .ed-toolbar-menu {
2616
+ position: fixed; z-index: 102;
2617
+ display: flex; flex-direction: column; gap: 2px;
2618
+ padding: 4px; border-radius: 8px;
2619
+ background: rgba(16, 18, 21, 0.96); color: #e6edf3;
2620
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35);
2621
+ }
2622
+ .ed-toolbar-menu[hidden] { display: none; }
2623
+ .ed-toolbar-menu-btn {
2624
+ min-width: 120px; padding: 4px 10px; text-align: left;
2625
+ border: none; border-radius: 4px; background: transparent; color: inherit;
2626
+ font: inherit; font-size: 13px; line-height: 1.4; cursor: pointer;
2627
+ }
2628
+ .ed-toolbar-menu-btn:hover { background: rgba(255, 255, 255, 0.18); }
2629
+ /* 追加 4: the whole-document source escape hatch. Replaces the rendered
2630
+ content column for the duration of source mode; the document's own
2631
+ lines[] is untouched until the mode is left (see docsource.js). */
2632
+ .ed-source {
2633
+ display: block; width: 100%; box-sizing: border-box;
2634
+ min-height: 70vh; padding: 12px;
2635
+ font-family: monospace; font-size: 13px; line-height: 1.6;
2636
+ border: 1px solid #d0d7de; border-radius: 8px; resize: vertical;
2637
+ white-space: pre; overflow-wrap: normal;
2638
+ }
2639
+ body[data-ed-mode="preview"] .ed-handle,
2640
+ body[data-ed-mode="preview"] .ed-insert { display: none !important; }
2472
2641
  .ed-editing { position: relative; }
2473
2642
  .ed-raw {
2474
2643
  display: block; width: 100%; font-family: monospace; font-size: 13px;
@@ -4121,8 +4290,11 @@ ${mermaidInitTag}`)
4121
4290
  </html>`;
4122
4291
 
4123
4292
  const bakedHtml = await bakeGraphviz(html);
4293
+ // editMode's bodyHtml is already baked per-part above; re-baking here is a
4294
+ // short-circuit no-op (bakeGraphviz bails out when there's no
4295
+ // data-graphviz-src left). Non-editMode bodyHtml is baked here as before.
4124
4296
  const bakedBody = await bakeGraphviz(bodyHtml);
4125
- return { html: bakedHtml, bodyHtml: bakedBody, blocks };
4297
+ return { html: bakedHtml, bodyHtml: bakedBody, parts, blocks };
4126
4298
  }
4127
4299
  module.exports = { renderMarkdown };
4128
4300
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helping-ai-workflow/md2doc",
3
- "version": "3.0.1",
3
+ "version": "3.2.0",
4
4
  "description": "Markdown → HTML / PDF renderer with WaveDrom, Mermaid, and Graphviz support",
5
5
  "keywords": [
6
6
  "markdown",
@@ -32,11 +32,12 @@
32
32
  "marked-katex-extension": "^5.1.10",
33
33
  "mermaid": "11.15.0",
34
34
  "puppeteer": "^24.15.0",
35
+ "turndown": "^7.2.4",
35
36
  "wavedrom": "3.5.0"
36
37
  },
37
38
  "scripts": {
38
39
  "preinstall": "node scripts/preinstall.js",
39
- "test": "node test/md2doc.test.js && node test/blockmap.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/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/gate-compat.test.js && node test/history.test.js && node test/list-md.test.js && node test/table-width.test.js"
40
+ "test": "node test/md2doc.test.js && node test/blockmap.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/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/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/docsource.test.js && node test/asset.test.js && node test/paste-md.test.js && node test/patchmap.test.js"
40
41
  },
41
42
  "repository": {
42
43
  "type": "git",