@helping-ai-workflow/md2doc 2.7.0 → 2.8.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.
Files changed (2) hide show
  1. package/lib/md2doc.js +219 -19
  2. package/package.json +2 -2
package/lib/md2doc.js CHANGED
@@ -1368,6 +1368,34 @@ const html = `<!DOCTYPE html>
1368
1368
  background: rgba(147, 197, 253, 0.35);
1369
1369
  box-shadow: inset 0 0 0 1px #93c5fd;
1370
1370
  }
1371
+ .lightbox-swatch {
1372
+ width: 16px;
1373
+ height: 16px;
1374
+ min-width: 16px;
1375
+ padding: 0;
1376
+ border-radius: 50%;
1377
+ border: 2px solid rgba(255, 255, 255, 0.4);
1378
+ cursor: pointer;
1379
+ align-self: center;
1380
+ }
1381
+ .lightbox-swatch.is-active {
1382
+ border-color: #ffffff;
1383
+ box-shadow: 0 0 0 2px #93c5fd;
1384
+ }
1385
+ /* Inline annotation overlay left behind after closing the lightbox:
1386
+ same-viewBox svg over the source figure, in-memory only. */
1387
+ .anno-inline-wrap {
1388
+ position: relative;
1389
+ display: inline-block;
1390
+ max-width: 100%;
1391
+ }
1392
+ .anno-inline {
1393
+ position: absolute;
1394
+ inset: 0;
1395
+ width: 100%;
1396
+ height: 100%;
1397
+ pointer-events: none;
1398
+ }
1371
1399
  .lightbox-sep {
1372
1400
  width: 1px;
1373
1401
  align-self: stretch;
@@ -2187,16 +2215,24 @@ ${mermaidInitTag}` : ''}
2187
2215
  // close/reopen keeps them, reload starts clean (they are reading-session
2188
2216
  // scratch, and localStorage keys would go stale when the doc regenerates).
2189
2217
  var ANNO_NS = 'http://www.w3.org/2000/svg';
2218
+ var ANNO_COLORS = ['#ef4444', '#3b82f6', '#22c55e', '#f59e0b', '#111827'];
2219
+ var ANNO_WIDTHS = [0.6, 1, 1.8];
2190
2220
  var annoStore = (typeof WeakMap === 'function') ? new WeakMap() : null;
2191
2221
  var annoShapes = [];
2192
2222
  var annoSvg = null;
2193
- var annoMode = null; // null | 'f' | 'e' | 'r' | 'm'
2223
+ var annoMode = null; // null | 'f' | 'e' | 'r' | 'l' | 'a' | 'm'
2194
2224
  var annoSelected = null;
2195
2225
  var annoDraft = null;
2196
2226
  var annoUndoStack = [];
2197
2227
  var annoRedoStack = [];
2198
2228
  var annoStrokeW = 3;
2229
+ var annoColor = ANNO_COLORS[0];
2230
+ var annoWidthScale = 1;
2199
2231
  var annoToolButtons = {};
2232
+ var annoStyleButtons = [];
2233
+ var annoSourceNode = null;
2234
+ var annoCloneEl = null;
2235
+ var annoBaseSrc = null;
2200
2236
 
2201
2237
  function lightboxIsOpen() {
2202
2238
  return !!lightboxEl && !lightboxEl.hidden;
@@ -2308,11 +2344,12 @@ ${mermaidInitTag}` : ''}
2308
2344
  function annoShapeEl(shape) {
2309
2345
  var tag = (shape.type === 'line' || shape.type === 'arrow') ? 'line' : shape.type;
2310
2346
  var el = document.createElementNS(ANNO_NS, tag);
2347
+ var color = shape.color || ANNO_COLORS[0];
2311
2348
  if (shape.type === 'line' || shape.type === 'arrow') {
2312
2349
  el.setAttribute('x1', shape.x1.toFixed(1)); el.setAttribute('y1', shape.y1.toFixed(1));
2313
2350
  el.setAttribute('x2', shape.x2.toFixed(1)); el.setAttribute('y2', shape.y2.toFixed(1));
2314
2351
  el.setAttribute('stroke-linecap', 'round');
2315
- if (shape.type === 'arrow') el.setAttribute('marker-end', 'url(#anno-arrow-head)');
2352
+ if (shape.type === 'arrow') el.setAttribute('marker-end', 'url(#anno-arrow-' + color.slice(1) + ')');
2316
2353
  } else if (shape.type === 'rect') {
2317
2354
  el.setAttribute('x', shape.x.toFixed(1)); el.setAttribute('y', shape.y.toFixed(1));
2318
2355
  el.setAttribute('width', Math.max(0.1, shape.w).toFixed(1));
@@ -2329,32 +2366,36 @@ ${mermaidInitTag}` : ''}
2329
2366
  el.setAttribute('stroke-linejoin', 'round');
2330
2367
  el.setAttribute('stroke-linecap', 'round');
2331
2368
  }
2332
- el.setAttribute('stroke', '#ef4444');
2333
- el.setAttribute('stroke-width', String(annoStrokeW));
2369
+ el.setAttribute('stroke', color);
2370
+ el.setAttribute('stroke-width', (annoStrokeW * (shape.ws || 1)).toFixed(1));
2334
2371
  return el;
2335
2372
  }
2336
2373
 
2337
- function annoEnsureDefs() {
2374
+ function annoEnsureDefs(svg) {
2338
2375
  var defs = document.createElementNS(ANNO_NS, 'defs');
2339
- var marker = document.createElementNS(ANNO_NS, 'marker');
2340
- marker.setAttribute('id', 'anno-arrow-head');
2341
- marker.setAttribute('markerWidth', '8');
2342
- marker.setAttribute('markerHeight', '8');
2343
- marker.setAttribute('refX', '6.4');
2344
- marker.setAttribute('refY', '3');
2345
- marker.setAttribute('orient', 'auto');
2346
- var tip = document.createElementNS(ANNO_NS, 'path');
2347
- tip.setAttribute('d', 'M0 0 L7 3 L0 6 Z');
2348
- tip.setAttribute('fill', '#ef4444');
2349
- marker.appendChild(tip);
2350
- defs.appendChild(marker);
2351
- annoSvg.appendChild(defs);
2376
+ ANNO_COLORS.forEach(function (color) {
2377
+ var marker = document.createElementNS(ANNO_NS, 'marker');
2378
+ // Same id + content in every overlay svg, so document-wide url(#) lookups
2379
+ // always resolve to an identical marker.
2380
+ marker.setAttribute('id', 'anno-arrow-' + color.slice(1));
2381
+ marker.setAttribute('markerWidth', '8');
2382
+ marker.setAttribute('markerHeight', '8');
2383
+ marker.setAttribute('refX', '6.4');
2384
+ marker.setAttribute('refY', '3');
2385
+ marker.setAttribute('orient', 'auto');
2386
+ var tip = document.createElementNS(ANNO_NS, 'path');
2387
+ tip.setAttribute('d', 'M0 0 L7 3 L0 6 Z');
2388
+ tip.setAttribute('fill', color);
2389
+ marker.appendChild(tip);
2390
+ defs.appendChild(marker);
2391
+ });
2392
+ svg.appendChild(defs);
2352
2393
  }
2353
2394
 
2354
2395
  function annoRedraw() {
2355
2396
  if (!annoSvg) return;
2356
2397
  while (annoSvg.firstChild) annoSvg.removeChild(annoSvg.firstChild);
2357
- annoEnsureDefs();
2398
+ annoEnsureDefs(annoSvg);
2358
2399
  annoShapes.forEach(function (shape, index) {
2359
2400
  var g = document.createElementNS(ANNO_NS, 'g');
2360
2401
  g.setAttribute('data-anno-id', String(index));
@@ -2404,6 +2445,7 @@ ${mermaidInitTag}` : ''}
2404
2445
  function annoPushOp(op) {
2405
2446
  annoUndoStack.push(op);
2406
2447
  annoRedoStack.length = 0;
2448
+ annoBake();
2407
2449
  }
2408
2450
 
2409
2451
  function annoApplyOp(op, reverse) {
@@ -2415,6 +2457,10 @@ ${mermaidInitTag}` : ''}
2415
2457
  else annoShapes.splice(annoShapes.indexOf(op.shape), 1);
2416
2458
  } else if (op.kind === 'geom') {
2417
2459
  annoSetGeom(op.shape, reverse ? op.before : op.after);
2460
+ } else if (op.kind === 'style') {
2461
+ var style = reverse ? op.before : op.after;
2462
+ op.shape.color = style.color;
2463
+ op.shape.ws = style.ws;
2418
2464
  } else if (op.kind === 'clear') {
2419
2465
  if (reverse) op.shapes.forEach(function (s) { annoShapes.push(s); });
2420
2466
  else annoShapes.length = 0;
@@ -2428,6 +2474,7 @@ ${mermaidInitTag}` : ''}
2428
2474
  annoRedoStack.push(op);
2429
2475
  annoSelected = null;
2430
2476
  annoRedraw();
2477
+ annoBake();
2431
2478
  }
2432
2479
 
2433
2480
  function annoRedo() {
@@ -2437,6 +2484,7 @@ ${mermaidInitTag}` : ''}
2437
2484
  annoUndoStack.push(op);
2438
2485
  annoSelected = null;
2439
2486
  annoRedraw();
2487
+ annoBake();
2440
2488
  }
2441
2489
 
2442
2490
  function annoClearAll() {
@@ -2457,6 +2505,129 @@ ${mermaidInitTag}` : ''}
2457
2505
  annoRedraw();
2458
2506
  }
2459
2507
 
2508
+ function setAnnoStyle(patch) {
2509
+ if (patch.color) annoColor = patch.color;
2510
+ if (patch.ws) annoWidthScale = patch.ws;
2511
+ annoStyleButtons.forEach(function (entry) {
2512
+ entry.button.classList.toggle('is-active',
2513
+ entry.kind === 'color' ? entry.value === annoColor : entry.value === annoWidthScale);
2514
+ });
2515
+ // With a selection, the pickers restyle it (undoably); otherwise they only
2516
+ // set the style for the next shape.
2517
+ if (annoSelected) {
2518
+ var beforeStyle = { color: annoSelected.color, ws: annoSelected.ws };
2519
+ annoSelected.color = patch.color || annoSelected.color;
2520
+ annoSelected.ws = patch.ws || annoSelected.ws;
2521
+ annoPushOp({
2522
+ kind: 'style', shape: annoSelected,
2523
+ before: beforeStyle,
2524
+ after: { color: annoSelected.color, ws: annoSelected.ws },
2525
+ });
2526
+ annoRedraw();
2527
+ }
2528
+ }
2529
+
2530
+ // Re-bake the shown raster clone (image + shapes composited to a PNG data
2531
+ // URI) so a native right-click "Copy image" carries the annotations.
2532
+ function annoShapesSvgMarkup() {
2533
+ var svg = document.createElementNS(ANNO_NS, 'svg');
2534
+ svg.setAttribute('xmlns', ANNO_NS);
2535
+ svg.setAttribute('viewBox', '0 0 ' + lightboxNaturalW + ' ' + lightboxNaturalH);
2536
+ svg.setAttribute('width', lightboxNaturalW);
2537
+ svg.setAttribute('height', lightboxNaturalH);
2538
+ annoEnsureDefs(svg);
2539
+ annoShapes.forEach(function (shape) { svg.appendChild(annoShapeEl(shape)); });
2540
+ return new XMLSerializer().serializeToString(svg);
2541
+ }
2542
+
2543
+ function annoComposite(drawBase, scale, done) {
2544
+ var canvas = document.createElement('canvas');
2545
+ canvas.width = lightboxNaturalW * scale;
2546
+ canvas.height = lightboxNaturalH * scale;
2547
+ var ctx = canvas.getContext('2d');
2548
+ drawBase(ctx, canvas, function () {
2549
+ if (!annoShapes.length) { done(canvas); return; }
2550
+ var overlay = new Image();
2551
+ overlay.onload = function () {
2552
+ ctx.drawImage(overlay, 0, 0, canvas.width, canvas.height);
2553
+ done(canvas);
2554
+ };
2555
+ overlay.onerror = function () { done(canvas); };
2556
+ overlay.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(annoShapesSvgMarkup());
2557
+ });
2558
+ }
2559
+
2560
+ function annoBake() {
2561
+ if (!annoCloneEl || annoCloneEl.tagName !== 'IMG' || !annoBaseSrc) return;
2562
+ var expected = annoCloneEl;
2563
+ annoComposite(function (ctx, canvas, next) {
2564
+ var base = new Image();
2565
+ base.onload = function () { ctx.drawImage(base, 0, 0, canvas.width, canvas.height); next(); };
2566
+ base.onerror = function () { next(); };
2567
+ base.src = annoBaseSrc;
2568
+ }, 1, function (canvas) {
2569
+ if (annoCloneEl !== expected) return; // lightbox moved on to another image
2570
+ try { annoCloneEl.src = canvas.toDataURL('image/png'); }
2571
+ catch (e) { /* tainted canvas — keep the plain image */ }
2572
+ });
2573
+ }
2574
+
2575
+ function annoCopyImage() {
2576
+ var clone = annoCloneEl;
2577
+ if (!clone) return;
2578
+ var isImg = clone.tagName === 'IMG';
2579
+ annoComposite(function (ctx, canvas, next) {
2580
+ ctx.fillStyle = '#ffffff';
2581
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
2582
+ var base = new Image();
2583
+ base.onload = function () { ctx.drawImage(base, 0, 0, canvas.width, canvas.height); next(); };
2584
+ base.onerror = function () { next(); };
2585
+ base.src = isImg ? (annoBaseSrc || clone.src)
2586
+ : 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(new XMLSerializer().serializeToString(clone));
2587
+ }, isImg ? 1 : 2, function (canvas) {
2588
+ try {
2589
+ canvas.toBlob(function (blob) {
2590
+ if (!blob || typeof ClipboardItem === 'undefined' || !navigator.clipboard || !navigator.clipboard.write) return;
2591
+ navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]).catch(function () {});
2592
+ }, 'image/png');
2593
+ } catch (e) { /* clipboard unavailable */ }
2594
+ });
2595
+ }
2596
+
2597
+ // After Esc the drawings stay visible on the inline figure: a same-viewBox
2598
+ // overlay svg over the source element. In-memory only — gone on reload.
2599
+ function annoSyncInline() {
2600
+ var node = annoSourceNode;
2601
+ if (!node) return;
2602
+ var visual = lightboxSourceOf(node);
2603
+ if (!visual || !visual.parentNode) return;
2604
+ var wrap = visual.closest ? visual.closest('.anno-inline-wrap') : null;
2605
+ var shapes = annoShapes;
2606
+ if (!shapes.length) {
2607
+ if (wrap) {
2608
+ var old = wrap.querySelector('svg.anno-inline');
2609
+ if (old) wrap.removeChild(old);
2610
+ }
2611
+ return;
2612
+ }
2613
+ if (!wrap) {
2614
+ wrap = document.createElement('span');
2615
+ wrap.className = 'anno-inline-wrap';
2616
+ visual.parentNode.insertBefore(wrap, visual);
2617
+ wrap.appendChild(visual);
2618
+ }
2619
+ var overlay = wrap.querySelector('svg.anno-inline');
2620
+ if (!overlay) {
2621
+ overlay = document.createElementNS(ANNO_NS, 'svg');
2622
+ overlay.setAttribute('class', 'anno-inline');
2623
+ wrap.appendChild(overlay);
2624
+ }
2625
+ overlay.setAttribute('viewBox', '0 0 ' + lightboxNaturalW + ' ' + lightboxNaturalH);
2626
+ while (overlay.firstChild) overlay.removeChild(overlay.firstChild);
2627
+ annoEnsureDefs(overlay);
2628
+ shapes.forEach(function (shape) { overlay.appendChild(annoShapeEl(shape)); });
2629
+ }
2630
+
2460
2631
  function setAnnoMode(mode) {
2461
2632
  annoMode = (annoMode === mode) ? null : mode;
2462
2633
  if (annoMode !== 'm') annoSelected = null;
@@ -2527,6 +2698,8 @@ ${mermaidInitTag}` : ''}
2527
2698
  else if (annoMode === 'l') shape = { type: 'line', x1: pt.x, y1: pt.y, x2: pt.x, y2: pt.y };
2528
2699
  else if (annoMode === 'a') shape = { type: 'arrow', x1: pt.x, y1: pt.y, x2: pt.x, y2: pt.y };
2529
2700
  else shape = { type: 'rect', x: pt.x, y: pt.y, w: 0, h: 0 };
2701
+ shape.color = annoColor;
2702
+ shape.ws = annoWidthScale;
2530
2703
  annoDraft = { kind: 'draw', shape: shape, start: pt, el: annoShapeEl(shape), clientDist: 0, lastClient: [event.clientX, event.clientY] };
2531
2704
  annoSvg.appendChild(annoDraft.el);
2532
2705
  }
@@ -2598,6 +2771,9 @@ ${mermaidInitTag}` : ''}
2598
2771
  }
2599
2772
 
2600
2773
  function annoSetup(sourceNode) {
2774
+ annoSourceNode = sourceNode;
2775
+ annoCloneEl = lightboxCanvas.firstElementChild;
2776
+ annoBaseSrc = (annoCloneEl && annoCloneEl.tagName === 'IMG') ? annoCloneEl.src : null;
2601
2777
  annoShapes = [];
2602
2778
  if (annoStore) {
2603
2779
  annoShapes = annoStore.get(sourceNode);
@@ -2618,6 +2794,7 @@ ${mermaidInitTag}` : ''}
2618
2794
  lightboxCanvas.appendChild(annoSvg);
2619
2795
  annoMode = null;
2620
2796
  setAnnoMode(null);
2797
+ if (annoShapes.length) annoBake();
2621
2798
  }
2622
2799
 
2623
2800
  function buildLightbox() {
@@ -2658,6 +2835,25 @@ ${mermaidInitTag}` : ''}
2658
2835
  annoToolButtons[tool[0]] = button;
2659
2836
  bar.appendChild(button);
2660
2837
  });
2838
+ ANNO_COLORS.forEach(function (color) {
2839
+ var swatch = lightboxButton('', 'Stroke color ' + color, function () { setAnnoStyle({ color: color }); });
2840
+ swatch.className = 'lightbox-swatch';
2841
+ swatch.style.background = color;
2842
+ swatch.setAttribute('data-anno-color', color);
2843
+ if (color === annoColor) swatch.classList.add('is-active');
2844
+ annoStyleButtons.push({ kind: 'color', value: color, button: swatch });
2845
+ bar.appendChild(swatch);
2846
+ });
2847
+ [['S', ANNO_WIDTHS[0]], ['M', ANNO_WIDTHS[1]], ['L', ANNO_WIDTHS[2]]].forEach(function (width) {
2848
+ var button = lightboxButton(width[0], 'Stroke width ' + width[0], function () { setAnnoStyle({ ws: width[1] }); });
2849
+ button.setAttribute('data-anno-width', String(width[1]));
2850
+ if (width[1] === annoWidthScale) button.classList.add('is-active');
2851
+ annoStyleButtons.push({ kind: 'width', value: width[1], button: button });
2852
+ bar.appendChild(button);
2853
+ });
2854
+ var copyButton = lightboxButton('⧉', 'Copy image with annotations', annoCopyImage);
2855
+ copyButton.setAttribute('data-anno-copy', '');
2856
+ bar.appendChild(copyButton);
2661
2857
  var clearButton = lightboxButton('Clear', 'Clear all annotations', annoClearAll);
2662
2858
  clearButton.setAttribute('data-anno-clear', '');
2663
2859
  bar.appendChild(clearButton);
@@ -2782,6 +2978,10 @@ ${mermaidInitTag}` : ''}
2782
2978
  lightboxEl.hidden = true;
2783
2979
  document.body.removeAttribute('data-lightbox-open');
2784
2980
  annoCancelDraft();
2981
+ annoSyncInline();
2982
+ annoSourceNode = null;
2983
+ annoCloneEl = null;
2984
+ annoBaseSrc = null;
2785
2985
  annoSvg = null;
2786
2986
  annoMode = null;
2787
2987
  setAnnoMode(null);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helping-ai-workflow/md2doc",
3
- "version": "2.7.0",
3
+ "version": "2.8.0",
4
4
  "description": "Markdown → HTML / PDF renderer with WaveDrom, Mermaid, and Graphviz support",
5
5
  "keywords": [
6
6
  "markdown",
@@ -36,7 +36,7 @@
36
36
  },
37
37
  "scripts": {
38
38
  "preinstall": "node scripts/preinstall.js",
39
- "test": "node test/md2doc.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/reader-panels.test.js && node test/cli.test.js && node test/code-operator.test.js"
39
+ "test": "node test/md2doc.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"
40
40
  },
41
41
  "repository": {
42
42
  "type": "git",