@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/editor/asset.js +235 -0
- package/lib/editor/client.js +2017 -85
- package/lib/editor/docsource.js +105 -0
- package/lib/editor/paste-md.js +275 -0
- package/lib/editor/patchmap.js +83 -0
- package/lib/editor/server.js +195 -3
- package/lib/editor/table-md.js +66 -34
- package/lib/editor/toolbar-model.js +160 -0
- package/lib/md2doc.js +176 -4
- package/package.json +3 -2
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
|
|
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
|
-
|
|
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
|
|
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",
|