@helping-ai-workflow/md2doc 3.11.0 → 3.12.1

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.
@@ -1552,6 +1552,12 @@
1552
1552
  panels.say('已匯出 ' + exportName('svg'));
1553
1553
  }
1554
1554
 
1555
+ // PNG is rasterised at twice the SVG's own size: at 1x a 300-480px wide
1556
+ // diagram goes soft the moment it is pasted into a slide or shown on a
1557
+ // high-density screen (wave redesign spec section 5-7). The SVG is drawn
1558
+ // at the target size, so lines and text are re-rasterised, not upscaled.
1559
+ const PNG_SCALE = 2;
1560
+
1555
1561
  function exportPng() {
1556
1562
  const text = exportSvgText();
1557
1563
  if (text === '') { panels.say('預覽還沒畫出來,沒有東西可以匯出'); return; }
@@ -1568,14 +1574,14 @@
1568
1574
  const img = d.createElement('img');
1569
1575
  img.onload = function () {
1570
1576
  const cv = d.createElement('canvas');
1571
- cv.width = w; cv.height = h;
1577
+ cv.width = w * PNG_SCALE; cv.height = h * PNG_SCALE;
1572
1578
  const ctx2 = cv.getContext('2d');
1573
1579
  // The SVG's own background rect is white, but only over its own box;
1574
1580
  // painting white first means a PNG dropped into a dark-themed document
1575
1581
  // does not show that document's background through the margins.
1576
1582
  ctx2.fillStyle = '#ffffff';
1577
- ctx2.fillRect(0, 0, w, h);
1578
- ctx2.drawImage(img, 0, 0);
1583
+ ctx2.fillRect(0, 0, cv.width, cv.height);
1584
+ ctx2.drawImage(img, 0, 0, cv.width, cv.height);
1579
1585
  cv.toBlob(function (blob) {
1580
1586
  if (blob === null) { panels.say('PNG 轉檔失敗'); return; }
1581
1587
  downloadBlob(blob, exportName('png'));
package/lib/md2doc.js CHANGED
@@ -443,6 +443,8 @@ const READER_ICONS = {
443
443
  up: ICON_SVG_OPEN + '<path d="m18 15-6-6-6 6"/></svg>',
444
444
  down: ICON_SVG_OPEN + '<path d="m6 9 6 6 6-6"/></svg>',
445
445
  x: ICON_SVG_OPEN + '<path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>',
446
+ copy: ICON_SVG_OPEN + '<rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>',
447
+ check: ICON_SVG_OPEN + '<path d="M20 6 9 17l-5-5"/></svg>',
446
448
  };
447
449
 
448
450
  async function renderMarkdown(mdText, srcPath, opts = {}) {
@@ -1045,9 +1047,16 @@ ${itemsHtml}
1045
1047
  // The info string goes into an attribute: escape it (an info string such
1046
1048
  // as x" onmouseover=… must stay text). Ordinary language names are unchanged.
1047
1049
  const langAttr = escapeHtml(lang);
1048
- if (coloured !== null) return `<pre><code class="language-${langAttr} hljs">${coloured}</code></pre>\n`;
1049
- const escaped = code.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
1050
- return `<pre><code class="language-${langAttr}">${escaped}</code></pre>\n`;
1050
+ const pre = coloured !== null
1051
+ ? `<pre><code class="language-${langAttr} hljs">${coloured}</code></pre>`
1052
+ : `<pre><code class="language-${langAttr}">${code.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')}</code></pre>`;
1053
+ // v3.12.0: the reader's copy button. It sits on a wrapper beside the
1054
+ // <pre>, not inside it — <pre> scrolls sideways and would carry the
1055
+ // button off with the code. Icon only: a text label would be picked up
1056
+ // by search and by select-all. Never in edit mode, where it would land
1057
+ // inside the editable surface.
1058
+ if (opts.editMode) return pre + '\n';
1059
+ return `<div class="code-block">${pre}<button class="code-copy" type="button" aria-label="Copy code" title="Copy code">${READER_ICONS.copy}</button></div>\n`;
1051
1060
  };
1052
1061
 
1053
1062
  renderer.heading = function(token) {
@@ -2890,6 +2899,32 @@ const html = `<!DOCTYPE html>
2890
2899
  line-height: 1.45;
2891
2900
  }
2892
2901
  pre code { background: none; padding: 0; font-size: 0.875em; }
2902
+ /* v3.12.0: the copy button renderer.code puts beside each fenced block.
2903
+ The wrapper takes over the margin the pre used to have, so the spacing
2904
+ around a block does not move. The button hides at rest and shows on
2905
+ hover, on keyboard focus and after a press; a touch screen has no hover,
2906
+ so there it always shows. Colours are existing reader literals, so the
2907
+ theme post-pass carries them into dark mode. */
2908
+ .code-block { position: relative; margin: 1em 0; }
2909
+ .code-block > pre { margin: 0; }
2910
+ .code-copy {
2911
+ position: absolute; top: 8px; right: 8px;
2912
+ display: inline-flex; align-items: center; justify-content: center; gap: 6px;
2913
+ height: 30px; min-width: 30px; padding: 0 7px;
2914
+ border: 1px solid #d0d7de; border-radius: 6px;
2915
+ background: #ffffff; color: #57606a;
2916
+ font: inherit; font-size: 12px; line-height: 1; cursor: pointer;
2917
+ opacity: 0; transition: opacity 0.12s;
2918
+ }
2919
+ .code-block:hover .code-copy,
2920
+ .code-copy:focus-visible,
2921
+ .code-block.is-copied .code-copy,
2922
+ .code-block.is-manual .code-copy { opacity: 1; }
2923
+ .code-copy:hover { background: #eef2f6; color: #24292e; }
2924
+ .code-copy:focus-visible { outline: 2px solid #0969da; outline-offset: 2px; }
2925
+ .code-block.is-copied .code-copy { color: #0969da; }
2926
+ @media (hover: none) { .code-copy { opacity: 1; } }
2927
+ @media (prefers-reduced-motion: reduce) { .code-copy { transition: none; } }
2893
2928
  /* v3.10.0: fenced-code syntax colour, written at render time by
2894
2929
  lib/highlight.js (no script on the page). Colours are the syn-* roles. */
2895
2930
  .hljs-keyword, .hljs-selector-tag, .hljs-literal { color: var(--md-syn-keyword); }
@@ -3276,6 +3311,7 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
3276
3311
  .content table tbody td:first-child,
3277
3312
  .content table thead th:first-child { position: static; }
3278
3313
  .heading-anchor { display: none; }
3314
+ .code-copy { display: none; }
3279
3315
  pre { font-size: 9pt; }
3280
3316
  a[href]:after { content: none; }
3281
3317
  }
@@ -6010,6 +6046,55 @@ ${usesDrawioMultiPage ? ` // v3.4.0 batch2 Task 6: wired through a NAMED, reusa
6010
6046
  var shown = lightboxCanvas.firstElementChild;
6011
6047
  lightboxFitZoom = computeLightboxFit(!!(shown && shown.tagName.toLowerCase() === 'svg'));
6012
6048
  });
6049
+
6050
+ // v3.12.0: copy button on fenced code (renderer.code emits it; edit mode
6051
+ // does not). Delegated from document, so the edit page's wholesale
6052
+ // re-render of .content needs no rebind. The clipboard API first; then
6053
+ // execCommand over a selection of the code; and when both are refused the
6054
+ // code stays selected and the button says which keys to press.
6055
+ var CODE_COPY_RESET_MS = 2000;
6056
+ var CODE_COPY_DONE = '${READER_ICONS.check}<span>Copied</span>';
6057
+ var CODE_COPY_KEYS = /Mac|iPhone|iPad/.test(navigator.platform || '') ? 'Press Cmd+C' : 'Press Ctrl+C';
6058
+ function showCodeCopyResult(btn, wrap, ok) {
6059
+ if (btn.__md2docIcon === undefined) btn.__md2docIcon = btn.innerHTML;
6060
+ clearTimeout(btn.__md2docReset);
6061
+ wrap.classList.toggle('is-copied', ok);
6062
+ wrap.classList.toggle('is-manual', !ok);
6063
+ btn.innerHTML = ok ? CODE_COPY_DONE : '<span>' + CODE_COPY_KEYS + '</span>';
6064
+ btn.setAttribute('aria-label', ok ? 'Copied' : CODE_COPY_KEYS);
6065
+ btn.__md2docReset = setTimeout(function () {
6066
+ wrap.classList.remove('is-copied', 'is-manual');
6067
+ btn.innerHTML = btn.__md2docIcon;
6068
+ btn.setAttribute('aria-label', 'Copy code');
6069
+ }, CODE_COPY_RESET_MS);
6070
+ }
6071
+ function copyCodeBlock(btn) {
6072
+ var wrap = btn.closest('.code-block');
6073
+ var code = wrap && wrap.querySelector('pre > code');
6074
+ if (!code) return;
6075
+ function bySelection() {
6076
+ var range = document.createRange();
6077
+ range.selectNodeContents(code);
6078
+ var sel = window.getSelection();
6079
+ sel.removeAllRanges();
6080
+ sel.addRange(range);
6081
+ var ok = false;
6082
+ try { ok = document.execCommand('copy'); } catch (e) { ok = false; }
6083
+ if (ok) sel.removeAllRanges();
6084
+ showCodeCopyResult(btn, wrap, ok);
6085
+ }
6086
+ if (navigator.clipboard && navigator.clipboard.writeText) {
6087
+ navigator.clipboard.writeText(code.textContent).then(function () {
6088
+ showCodeCopyResult(btn, wrap, true);
6089
+ }, bySelection);
6090
+ } else {
6091
+ bySelection();
6092
+ }
6093
+ }
6094
+ document.addEventListener('click', function (event) {
6095
+ var btn = event.target.closest && event.target.closest('.code-block > .code-copy');
6096
+ if (btn) copyCodeBlock(btn);
6097
+ });
6013
6098
  })();
6014
6099
  </script>
6015
6100
  </body>
@@ -259,6 +259,15 @@
259
259
  return 'hsl(' + Math.round(x[0]) + ', ' + Math.round(Math.min(x[1], smax) * 100) + '%, ' + Math.round(l * 100) + '%)';
260
260
  }
261
261
  function lineColour(c) { return hsl(c)[1] > 0.25 ? tone(c, 0.68, 0.8) : INK; }
262
+ // WaveDrom draws every edge label (and point note) as g > rect + text inside wavearcs_N, the
263
+ // white rect masking the edge and waveform behind the words. Made transparent like other
264
+ // near-white fills, the line runs straight through the text in dark; it takes the page
265
+ // colour instead. Only this shape: graphviz/mermaid white fills still go transparent.
266
+ function isWaveLabelBacking(el) {
267
+ var next = el.nextElementSibling;
268
+ return /^rect$/i.test(el.tagName) && next !== null && /^text$/i.test(next.tagName) &&
269
+ el.closest('[id^="wavearcs"]') !== null;
270
+ }
262
271
  function recolourSvg(svg, dark) {
263
272
  var els = svg.querySelectorAll('polygon, ellipse, path, polyline, rect, line, circle, text, tspan, foreignObject *');
264
273
  var live = svg.hasAttribute('data-md2doc-live');
@@ -305,7 +314,7 @@
305
314
  } else {
306
315
  if (fill && fill[3] > 0) {
307
316
  var L = lum(fill);
308
- if (L > 0.85) ov += 'fill:transparent !important;';
317
+ if (L > 0.85) ov += isWaveLabelBacking(el) ? 'fill:var(--md-bg) !important;' : 'fill:transparent !important;';
309
318
  else if (L > 0.3) ov += 'fill:' + tone(fill, 0.26, 0.55) + ' !important;';
310
319
  else if (el.closest('g.edge, marker')) ov += 'fill:' + lineColour(fill) + ' !important;';
311
320
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helping-ai-workflow/md2doc",
3
- "version": "3.11.0",
3
+ "version": "3.12.1",
4
4
  "description": "Markdown → HTML / PDF renderer with WaveDrom, Mermaid, Graphviz, and draw.io support",
5
5
  "keywords": [
6
6
  "markdown",