@helping-ai-workflow/md2doc 3.16.0 → 3.17.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
@@ -1993,6 +1993,48 @@ ${edUlMarkerCss}${edOlCounterCss}
1993
1993
  color: var(--md-strong); cursor: pointer; box-shadow: 0 1px 3px var(--md-ed-shadow);
1994
1994
  }
1995
1995
  .ed-wave-edit-btn:hover { background: var(--md-ed-surface); border-color: var(--md-border); }
1996
+ .ed-diagram-edit-btn {
1997
+ position: fixed; z-index: 12; padding: 3px 10px; font-size: 12px;
1998
+ display: inline-flex; align-items: center; gap: 4px;
1999
+ border: 1px solid var(--md-border); border-radius: 4px; background: var(--md-ed-surface);
2000
+ color: var(--md-strong); cursor: pointer; box-shadow: 0 1px 3px var(--md-ed-shadow);
2001
+ }
2002
+ .ed-diagram-edit-btn[hidden] { display: none; }
2003
+ .ed-diagram-edit-btn:hover { background: var(--md-ed-surface); border-color: var(--md-border); }
2004
+ .ed-diagram-edit-btn svg { flex: 0 0 auto; width: 14px; height: 14px; margin: 0; }
2005
+ /* v3.17.0 T5: the diagram panel (openDiagramPanel() in the client runtime).
2006
+ It replaces the block's content in place, inside the block, like the raw
2007
+ box: a 2px accent frame, the header (language, the muted hint, the done
2008
+ button), the source (monospace on the field colour, 50vh at most, then it
2009
+ scrolls), the preview (60vh at most, scrolls both ways) and one message
2010
+ line. A preview kept after an error, or after an answer with nothing to
2011
+ draw, is dimmed (.is-stale). No backticks in this comment: it lives
2012
+ inside a JS template literal. */
2013
+ .ed-diagram-panel {
2014
+ border: 2px solid var(--md-accent); border-radius: 6px; padding: 8px;
2015
+ background: var(--md-ed-surface); color: var(--md-strong);
2016
+ }
2017
+ .ed-diagram-head { display: flex; align-items: center; gap: 8px; margin: 0 0 6px; font-size: 13px; }
2018
+ .ed-diagram-lang { font-weight: 600; }
2019
+ .ed-diagram-hint { flex: 1 1 auto; min-width: 0; color: var(--md-muted); font-size: 12px; }
2020
+ .ed-diagram-done {
2021
+ flex: 0 0 auto; font: inherit; font-size: 13px; padding: 2px 12px; cursor: pointer;
2022
+ border: 1px solid var(--md-border); border-radius: 6px;
2023
+ background: var(--md-ed-surface); color: var(--md-strong);
2024
+ }
2025
+ .ed-diagram-done:hover { background: var(--md-ed-hover); }
2026
+ .ed-diagram-src {
2027
+ display: block; width: 100%; box-sizing: border-box; min-height: 3em; max-height: 50vh; overflow-y: auto;
2028
+ padding: 6px; resize: vertical; tab-size: 2;
2029
+ font-family: "Cascadia Mono", Consolas, ui-monospace, monospace; font-size: 13px;
2030
+ border: 1px solid var(--md-border); border-radius: 6px;
2031
+ background: var(--md-ed-field); color: var(--md-strong);
2032
+ }
2033
+ .ed-diagram-preview { margin-top: 8px; max-height: 60vh; overflow: auto; outline: none; }
2034
+ .ed-diagram-preview svg { display: block; max-width: none; }
2035
+ .ed-diagram-preview.is-stale { opacity: 0.35; }
2036
+ .ed-diagram-error { margin-top: 6px; font-size: 12px; color: var(--md-ed-err); }
2037
+ .ed-diagram-error[data-kind="empty"] { color: var(--md-muted); }
1996
2038
  .ed-wave-overlay {
1997
2039
  position: fixed; inset: 0; z-index: 998; background: rgba(0,0,0,.45);
1998
2040
  display: flex; align-items: center; justify-content: center;
@@ -2905,6 +2947,34 @@ const html = `<!DOCTYPE html>
2905
2947
  max-width: 100%;
2906
2948
  margin: 0 auto;
2907
2949
  }
2950
+ /* v3.16.1: a capture tool hides every sticky element after its first slice,
2951
+ so a sticky first column came out blank from the second screen down. The
2952
+ runtime also widens each box that scrolls sideways (fitWideBoxes); the
2953
+ rules below are its last resort, for a box still wider than the screen. */
2954
+ body[data-capture-mode] .content table tbody td:first-child,
2955
+ body[data-capture-mode] .content table thead th:first-child {
2956
+ position: static;
2957
+ }
2958
+ body[data-capture-mode] .content table.capture-wrap th:not(.cell-narrow),
2959
+ body[data-capture-mode] .content table.capture-wrap td:not(.cell-narrow),
2960
+ body[data-capture-mode] .content table.capture-wrap td:not(.cell-narrow) code {
2961
+ white-space: normal;
2962
+ overflow-wrap: break-word;
2963
+ min-width: 0;
2964
+ }
2965
+ body[data-capture-mode] .content table.capture-wrap th,
2966
+ body[data-capture-mode] .content table.capture-wrap td {
2967
+ padding: 0.4em 0.6em;
2968
+ }
2969
+ body[data-capture-mode] .content pre.capture-wrap,
2970
+ body[data-capture-mode] .content pre.capture-wrap code {
2971
+ white-space: pre-wrap;
2972
+ overflow-wrap: anywhere;
2973
+ }
2974
+ body[data-capture-mode] .content .diagram-scroll.capture-wrap > svg {
2975
+ max-width: 100% !important;
2976
+ height: auto;
2977
+ }
2908
2978
  .capture-toast {
2909
2979
  position: fixed;
2910
2980
  top: 16px;
@@ -3492,6 +3562,17 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
3492
3562
  outline: 2px solid var(--md-accent); outline-offset: 2px;
3493
3563
  caret-color: var(--md-accent);
3494
3564
  }
3565
+ /* v3.17.0 T2: a fenced code block edited in place. The pre itself is the
3566
+ editing host (editCodeInPlace() in the client runtime) and takes the
3567
+ same ring as an armed surface, drawn outside its own padding. Code never
3568
+ wraps, also where an engine's contenteditable default would wrap it, and
3569
+ an empty body keeps one line of height to click and type into. */
3570
+ pre[data-ed-code-edit] {
3571
+ white-space: pre; overflow-wrap: normal; word-wrap: normal; line-break: auto;
3572
+ cursor: text; caret-color: var(--md-accent);
3573
+ }
3574
+ pre[data-ed-code-edit]:focus { outline: 2px solid var(--md-accent); outline-offset: 2px; }
3575
+ pre[data-ed-code-edit] > code { display: block; min-height: 1lh; }
3495
3576
  /* v3.14.0 T4: the empty-paragraph hint. syncEmptyMark() in the client sets
3496
3577
  data-ed-empty on a paragraph surface with no text; a real paragraph shows
3497
3578
  the hint while focused, the transient (an empty document's, or one Enter
@@ -3659,6 +3740,53 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
3659
3740
  flex: 0 0 auto; padding: 6px 10px; font-size: 11px; color: var(--md-muted);
3660
3741
  border-top: 1px solid var(--md-ed-ring);
3661
3742
  }
3743
+ /* v3.17.0 T3: the language chip and the copy chip of a fenced code block.
3744
+ Both live in one singleton on document.body (not inside .content: the
3745
+ client reads .content back as its own render), fixed at the top-right of
3746
+ the block the pointer is over or that is being edited. 24px tall, the
3747
+ editor surface with a 1px ring and a 6px radius; the copy chip shows the
3748
+ same check and Copied feedback as the reader button. The language menu
3749
+ is the slash menu look: 220px wide, 320px tall at most, its list scrolls
3750
+ on its own. No backticks in this comment: it lives inside a JS template
3751
+ literal. */
3752
+ .ed-code-chips {
3753
+ position: fixed; z-index: 12; display: flex; align-items: center; gap: 4px;
3754
+ }
3755
+ .ed-code-chips[hidden] { display: none; }
3756
+ .ed-code-chip {
3757
+ display: inline-flex; align-items: center; justify-content: center; gap: 4px;
3758
+ height: 24px; min-width: 24px; padding: 0 6px; margin: 0; box-sizing: border-box;
3759
+ border: 0; border-radius: 6px; background: var(--md-ed-surface); color: var(--md-muted);
3760
+ box-shadow: 0 0 0 1px var(--md-ed-ring);
3761
+ font: inherit; font-size: 12px; line-height: 1; cursor: pointer;
3762
+ }
3763
+ .ed-code-chip:hover, .ed-code-chip[aria-expanded="true"] { background: var(--md-ed-hover); color: var(--md-strong); }
3764
+ .ed-code-chip svg { flex: 0 0 auto; width: 14px; height: 14px; margin: 0; }
3765
+ .ed-code-copy.is-copied { color: var(--md-accent); }
3766
+ .ed-code-langmenu {
3767
+ position: fixed; z-index: 100; width: 220px; max-height: 320px;
3768
+ display: flex; flex-direction: column; box-sizing: border-box; overflow: hidden;
3769
+ border-radius: 8px; background: var(--md-ed-surface); color: var(--md-strong);
3770
+ box-shadow: 0 0 0 1px var(--md-ed-ring), 0 8px 24px var(--md-ed-shadow);
3771
+ font-size: 13px; line-height: 1.3;
3772
+ }
3773
+ .ed-code-langfilter {
3774
+ flex: 0 0 auto; margin: 4px 4px 0; padding: 0 8px; height: 28px; box-sizing: border-box;
3775
+ border: 0; border-radius: 6px; background: var(--md-ed-hover); color: inherit;
3776
+ font: inherit; outline: none;
3777
+ }
3778
+ .ed-code-langfilter::placeholder { color: var(--md-muted); }
3779
+ .ed-code-langlist { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 4px; }
3780
+ .ed-code-langitem {
3781
+ display: flex; align-items: center; gap: 8px; width: 100%; height: 32px;
3782
+ padding: 0 8px; box-sizing: border-box; margin: 0;
3783
+ border: 0; border-radius: 6px; background: transparent; color: inherit;
3784
+ font: inherit; text-align: left; cursor: pointer;
3785
+ }
3786
+ .ed-code-langitem:hover, .ed-code-langitem[data-ed-lang-cursor] { background: var(--md-ed-hover); }
3787
+ .ed-code-langitem span { flex: 1 1 auto; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
3788
+ .ed-code-langitem svg { flex: 0 0 auto; width: 14px; height: 14px; margin: 0; }
3789
+ .ed-code-langempty { padding: 8px; color: var(--md-muted); }
3662
3790
  /* §10-gap fix: block-level INSERT — the + button sits NEXT TO the ⠿
3663
3791
  handle in the left gutter, same node/visibility-toggle shape as
3664
3792
  .ed-handle above (real per-block node, opacity-only reveal on hover).
@@ -3963,6 +4091,32 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
3963
4091
  background: transparent; color: inherit; font: inherit; font-size: 13px; cursor: pointer;
3964
4092
  }
3965
4093
  .ed-linkcard-btn:hover { background: var(--md-ed-hover); }
4094
+ /* v3.17.0 T7: the image toolbar, 8px inside the top-left of the image the
4095
+ pointer is over (a fixed singleton on body, like the link card; the same
4096
+ surface, ring and shadow roles). 刪除 is the error colour. In its alt-text
4097
+ mode the buttons give way to one input. */
4098
+ .ed-imgtb {
4099
+ position: fixed; z-index: 15;
4100
+ display: flex; align-items: center; gap: 2px; box-sizing: border-box;
4101
+ max-width: calc(100vw - 8px); padding: 3px; border-radius: 8px;
4102
+ background: var(--md-ed-surface); color: var(--md-strong); font-size: 13px;
4103
+ box-shadow: 0 0 0 1px var(--md-ed-ring), 0 8px 24px var(--md-ed-shadow);
4104
+ }
4105
+ .ed-imgtb[hidden] { display: none; }
4106
+ .ed-imgtb-btns { display: flex; align-items: center; gap: 2px; }
4107
+ .ed-imgtb-btns[hidden], .ed-imgtb-input[hidden] { display: none; }
4108
+ .ed-imgtb-btn {
4109
+ flex: none; height: 26px; padding: 0 8px; border: 0; border-radius: 6px;
4110
+ background: transparent; color: inherit; font: inherit; font-size: 13px; cursor: pointer;
4111
+ }
4112
+ .ed-imgtb-btn:hover { background: var(--md-ed-hover); }
4113
+ .ed-imgtb-delete { color: var(--md-ed-err); }
4114
+ .ed-imgtb-input {
4115
+ flex: 1 1 auto; width: 260px; max-width: calc(100vw - 24px); min-width: 0; height: 28px;
4116
+ box-sizing: border-box; padding: 0 8px; border: 1px solid var(--md-ed-ring); border-radius: 6px;
4117
+ background: var(--md-ed-field); color: inherit; font: inherit; font-size: 13px;
4118
+ }
4119
+ .ed-imgtb-input:focus { outline: none; border-color: var(--md-muted); }
3966
4120
  /* v3.1.0 §4: the always-visible editing toolbar. Its numeric language is
3967
4121
  .lightbox-bar's (8px/12px padding, 6px gap, 13px type) over .ed-seltb's
3968
4122
  dark pill, so the two floating surfaces read as one family.
@@ -5103,11 +5257,49 @@ ${mermaidInitTag}`)
5103
5257
  // and the reading position holds. Esc leaves; a reload never comes back in it.
5104
5258
  const captureBtn = document.getElementById('capture-mode-toggle');
5105
5259
  let captureToastTimer = 0;
5260
+ // A capture tool only sees the part of a sideways-scrolling box that is in
5261
+ // view, so each one (a wide table, a long code line, a wide diagram) is
5262
+ // widened to its full width, centred on the screen, at most the screen less
5263
+ // a 16px gutter each side; the prose keeps its reading width. One still too
5264
+ // wide wraps (.capture-wrap), and a table still too wide after that gets
5265
+ // smaller type. Each box's own style attribute is kept and put back on exit.
5266
+ let captureFitted = [];
5267
+ function fitWideBoxes() {
5268
+ const vw = document.documentElement.clientWidth;
5269
+ const boxes = document.querySelectorAll('main.content table, main.content pre, main.content .diagram-scroll');
5270
+ for (const el of boxes) {
5271
+ if (el.scrollWidth <= el.clientWidth + 1) continue;
5272
+ captureFitted.push({ el: el, style: el.getAttribute('style') });
5273
+ const w = Math.min(el.scrollWidth, vw - 32);
5274
+ el.style.boxSizing = 'border-box';
5275
+ el.style.width = w + 'px';
5276
+ el.style.maxWidth = 'none';
5277
+ const margin = parseFloat(getComputedStyle(el).marginLeft) || 0;
5278
+ el.style.marginLeft = (margin + (vw - w) / 2 - el.getBoundingClientRect().left) + 'px';
5279
+ if (el.scrollWidth > el.clientWidth + 1) el.classList.add('capture-wrap');
5280
+ // Many columns on a narrow screen can stay too wide even wrapped: shrink
5281
+ // the table's type (the wrap rule's cell padding is in em) until it fits.
5282
+ let size = parseFloat(getComputedStyle(el).fontSize);
5283
+ for (let i = 0; i < 8 && el.tagName === 'TABLE' && el.scrollWidth > el.clientWidth + 1; i++) {
5284
+ size = size * el.clientWidth / el.scrollWidth * 0.98;
5285
+ el.style.fontSize = size + 'px';
5286
+ }
5287
+ }
5288
+ }
5289
+ function unfitWideBoxes() {
5290
+ for (const f of captureFitted) {
5291
+ f.el.classList.remove('capture-wrap');
5292
+ if (f.style === null) f.el.removeAttribute('style');
5293
+ else f.el.setAttribute('style', f.style);
5294
+ }
5295
+ captureFitted = [];
5296
+ }
5106
5297
  function setCaptureMode(on) {
5107
5298
  clearTimeout(captureToastTimer);
5108
5299
  const oldToast = document.querySelector('.capture-toast');
5109
5300
  if (oldToast) oldToast.remove();
5110
5301
  if (!on) {
5302
+ unfitWideBoxes();
5111
5303
  document.body.removeAttribute('data-capture-mode');
5112
5304
  document.body.style.removeProperty('--md2doc-capture-w');
5113
5305
  return;
@@ -5116,6 +5308,8 @@ ${mermaidInitTag}`)
5116
5308
  const w = document.querySelector('main.content').getBoundingClientRect().width;
5117
5309
  document.body.style.setProperty('--md2doc-capture-w', w + 'px');
5118
5310
  document.body.setAttribute('data-capture-mode', '');
5311
+ unfitWideBoxes();
5312
+ fitWideBoxes();
5119
5313
  const toast = document.createElement('div');
5120
5314
  toast.className = 'capture-toast';
5121
5315
  toast.setAttribute('role', 'status');
@@ -6584,7 +6778,7 @@ ${usesDrawioMultiPage ? ` // v3.4.0 batch2 Task 6: wired through a NAMED, reusa
6584
6778
  // server's on-demand GET /api/lib/<name> serves exactly what this file inlines
6585
6779
  // instead of resolving them a second time.
6586
6780
  const diagramEnginePaths = { mermaid: localMermaidJs, wavedrom: localWaveDromJs, wavedromSkin: localWaveDromSkin };
6587
- module.exports = { renderMarkdown, buildKatexStyleTag, diagramEnginePaths };
6781
+ module.exports = { renderMarkdown, buildKatexStyleTag, diagramEnginePaths, renderDotSvg };
6588
6782
 
6589
6783
  // Index of the first "{" in dot source that opens the graph body: not inside a
6590
6784
  // double-quoted string, a // or /* */ comment, a line starting with # (dot
@@ -6622,59 +6816,83 @@ function firstDotBrace(src) {
6622
6816
  return -1;
6623
6817
  }
6624
6818
 
6819
+ // One dot path for the bake and the editor preview. The loaded Graphviz
6820
+ // instance is cached at module scope (the library does not memoise load()).
6821
+ let graphvizLoading = null;
6822
+ function loadGraphviz() {
6823
+ if (!graphvizLoading) {
6824
+ const { Graphviz } = require('@hpcc-js/wasm-graphviz');
6825
+ graphvizLoading = Graphviz.load();
6826
+ // A failed load must not poison the cache: the next call tries again.
6827
+ graphvizLoading.catch(() => { graphvizLoading = null; });
6828
+ }
6829
+ return graphvizLoading;
6830
+ }
6831
+ async function renderDotSvg(dotSrc) {
6832
+ const gv = await loadGraphviz();
6833
+ // Graphviz measures label widths with the font it lays out in, and with
6834
+ // no fontname that is Times, whose metrics are narrower than the sans
6835
+ // the SVG is drawn in -- so labels overflowed their boxes. Declare
6836
+ // Helvetica BEFORE layout, right after the first brace; a fontname the
6837
+ // author writes later in the source still wins. Exactly "Helvetica":
6838
+ // a "Helvetica,sans-Serif" family list falls back to Times widths.
6839
+ const at = firstDotBrace(dotSrc);
6840
+ const laidOut = at < 0 ? dotSrc : dotSrc.slice(0, at + 1) +
6841
+ ' graph [fontname="Helvetica"]; node [fontname="Helvetica"]; edge [fontname="Helvetica"]; ' + dotSrc.slice(at + 1);
6842
+ let laid;
6843
+ try {
6844
+ laid = gv.dot(laidOut);
6845
+ } catch (injectErr) {
6846
+ // Injection must never turn a valid graph into an error: lay the
6847
+ // original source out as the author wrote it.
6848
+ if (laidOut === dotSrc) throw injectErr;
6849
+ laid = gv.dot(dotSrc);
6850
+ }
6851
+ return laid
6852
+ .replace(/<\?xml[^>]*\?>/g, '')
6853
+ .replace(/<!DOCTYPE[\s\S]*?>/g, '')
6854
+ // drop graphviz's "Generated by graphviz" banner comment so the div and <svg> are adjacent
6855
+ .replace(/<!--[\s\S]*?-->/g, '')
6856
+ // v3.8.0: graphviz falls back to Times when no fontname is set. The
6857
+ // injection above makes Helvetica the layout font, so this rewrite
6858
+ // now only touches an author's explicit Times-Roman, or a figure
6859
+ // whose injection fell back to the original source; other explicit
6860
+ // fontnames are untouched.
6861
+ .replace(/font-family="Times,serif"/g, 'font-family="Helvetica,sans-Serif"')
6862
+ .trim();
6863
+ }
6864
+
6625
6865
  // Render every deferred dot/graphviz placeholder to inline SVG using the
6626
6866
  // in-process WASM engine. Loads the WASM module only when at least one dot
6627
6867
  // block exists, so text-only docs pay nothing.
6628
6868
  async function bakeGraphviz(htmlStr) {
6629
6869
  if (!GRAPHVIZ_PLACEHOLDER.probe.test(htmlStr)) return htmlStr;
6630
- let Graphviz;
6631
6870
  try {
6632
- ({ Graphviz } = require('@hpcc-js/wasm-graphviz'));
6871
+ require('@hpcc-js/wasm-graphviz');
6633
6872
  } catch (e) {
6634
6873
  console.error('[ERROR] @hpcc-js/wasm-graphviz not installed — run `npm install`:', e.message);
6635
6874
  return htmlStr;
6636
6875
  }
6637
- const gv = await Graphviz.load();
6638
- return htmlStr.replace(GRAPHVIZ_PLACEHOLDER.re, (m, b64) => {
6639
- const dotSrc = Buffer.from(b64, 'base64').toString('utf8');
6876
+ // A WASM load failure throws, as it always did; only a per-block render
6877
+ // failure degrades that block to pre/code.
6878
+ await loadGraphviz();
6879
+ const jobs = [];
6880
+ htmlStr.replace(GRAPHVIZ_PLACEHOLDER.re, (m, b64) => {
6881
+ jobs.push(Buffer.from(b64, 'base64').toString('utf8'));
6882
+ return m;
6883
+ });
6884
+ const out = [];
6885
+ for (const dotSrc of jobs) {
6640
6886
  try {
6641
- // Graphviz measures label widths with the font it lays out in, and with
6642
- // no fontname that is Times, whose metrics are narrower than the sans
6643
- // the SVG is drawn in -- so labels overflowed their boxes. Declare
6644
- // Helvetica BEFORE layout, right after the first brace; a fontname the
6645
- // author writes later in the source still wins. Exactly "Helvetica":
6646
- // a "Helvetica,sans-Serif" family list falls back to Times widths.
6647
- const at = firstDotBrace(dotSrc);
6648
- const laidOut = at < 0 ? dotSrc : dotSrc.slice(0, at + 1) +
6649
- ' graph [fontname="Helvetica"]; node [fontname="Helvetica"]; edge [fontname="Helvetica"]; ' + dotSrc.slice(at + 1);
6650
- let laid;
6651
- try {
6652
- laid = gv.dot(laidOut);
6653
- } catch (injectErr) {
6654
- // Injection must never turn a valid graph into an error: lay the
6655
- // original source out as the author wrote it.
6656
- if (laidOut === dotSrc) throw injectErr;
6657
- laid = gv.dot(dotSrc);
6658
- }
6659
- const svg = laid
6660
- .replace(/<\?xml[^>]*\?>/g, '')
6661
- .replace(/<!DOCTYPE[\s\S]*?>/g, '')
6662
- // drop graphviz's "Generated by graphviz" banner comment so the div and <svg> are adjacent
6663
- .replace(/<!--[\s\S]*?-->/g, '')
6664
- // v3.8.0: graphviz falls back to Times when no fontname is set. The
6665
- // injection above makes Helvetica the layout font, so this rewrite
6666
- // now only touches an author's explicit Times-Roman, or a figure
6667
- // whose injection fell back to the original source; other explicit
6668
- // fontnames are untouched.
6669
- .replace(/font-family="Times,serif"/g, 'font-family="Helvetica,sans-Serif"')
6670
- .trim();
6671
- return `<div class="graphviz">${svg}</div>`;
6887
+ out.push(`<div class="graphviz">${await renderDotSvg(dotSrc)}</div>`);
6672
6888
  } catch (e) {
6673
6889
  console.error('[WARN] graphviz render failed:', e.message);
6674
6890
  const escaped = dotSrc.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
6675
- return `<pre><code class="language-dot">${escaped}</code></pre>\n`;
6891
+ out.push(`<pre><code class="language-dot">${escaped}</code></pre>\n`);
6676
6892
  }
6677
- });
6893
+ }
6894
+ let i = 0;
6895
+ return htmlStr.replace(GRAPHVIZ_PLACEHOLDER.re, () => out[i++]);
6678
6896
  }
6679
6897
 
6680
6898
  function launchBrowser(puppeteer) {
@@ -22,7 +22,9 @@
22
22
  if (at !== -1) listeners.splice(at, 1);
23
23
  };
24
24
  }
25
- function notify(dark) { for (var i = 0; i < listeners.length; i++) listeners[i](dark); }
25
+ // Over a copy: a listener may unsubscribe itself (or another) while it
26
+ // runs, and a splice under the loop would skip the next one.
27
+ function notify(dark) { var ls = listeners.slice(); for (var i = 0; i < ls.length; i++) ls[i](dark); }
26
28
  function isDark() { return root.getAttribute('data-md2doc-theme') === 'dark'; }
27
29
  function save(dark) {
28
30
  try {
@@ -228,6 +230,35 @@
228
230
  return true;
229
231
  };
230
232
 
233
+ // v3.17.0 (D13): one off-page render for the editor's diagram panel preview.
234
+ // It waits its turn on the same chain, because mermaid has ONE global config
235
+ // and a render beside a light / dark draw could be drawn with the other's
236
+ // theme. Its turn initialises the theme on screen at that moment, renders
237
+ // under a fresh id, and puts the light config back after (the invariant
238
+ // every draw here keeps). Resolves the svg text; rejects with mermaid's
239
+ // error, its stray #d<id> element removed. Counted in inFlight like the
240
+ // draws, so a toggle to a cached theme waits behind it too.
241
+ var previewSeq = 0;
242
+ function renderMermaid(src) {
243
+ if (typeof window.mermaid === 'undefined') return Promise.reject(new Error('mermaid is not loaded'));
244
+ inFlight++;
245
+ var run = chain.then(function () {
246
+ var id = 'md2doc-mermaid-preview-' + (++previewSeq);
247
+ window.mermaid.initialize(mermaidConfig(isDark() ? 'dark' : 'light'));
248
+ return Promise.resolve().then(function () { return window.mermaid.render(id, String(src)); }).then(function (res) {
249
+ window.mermaid.initialize(mermaidConfig('light'));
250
+ return res.svg;
251
+ }, function (e) {
252
+ var stray = document.getElementById('d' + id);
253
+ if (stray && stray.parentNode) stray.parentNode.removeChild(stray);
254
+ window.mermaid.initialize(mermaidConfig('light'));
255
+ throw e;
256
+ });
257
+ });
258
+ chain = run.catch(function () { /* the caller shows the error */ }).then(function () { inFlight--; });
259
+ return run;
260
+ }
261
+
231
262
  // ── graphviz / WaveDrom / baked SVG: recolour by computed colour ──
232
263
  // Rules: spec "Diagram recolour rules (D10)". Reading COMPUTED fill/stroke
233
264
  // covers both graphviz presentation attributes and WaveDrom's own <style>.
@@ -617,6 +648,6 @@
617
648
  // Public surface for pages that draw their own SVG outside .content (the wave editor canvas).
618
649
  // `__listenerCount` is a test hook (test/editor-click.test.js): the wave
619
650
  // editor subscribes once per open and must unsubscribe on close.
620
- window.md2docTheme = { isDark: isDark, onTheme: onTheme, recolourSvg: recolourSvg,
651
+ window.md2docTheme = { isDark: isDark, onTheme: onTheme, recolourSvg: recolourSvg, renderMermaid: renderMermaid,
621
652
  __listenerCount: function () { return listeners.length; } };
622
653
  })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helping-ai-workflow/md2doc",
3
- "version": "3.16.0",
3
+ "version": "3.17.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/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",
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/code-edit.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/theme-runtime.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": {