@helping-ai-workflow/md2doc 2.6.1 → 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 +662 -3
  2. package/package.json +2 -2
package/lib/md2doc.js CHANGED
@@ -1364,6 +1364,59 @@ const html = `<!DOCTYPE html>
1364
1364
  cursor: pointer;
1365
1365
  }
1366
1366
  .lightbox-bar button:hover { background: rgba(255, 255, 255, 0.18); }
1367
+ .lightbox-bar button.is-active {
1368
+ background: rgba(147, 197, 253, 0.35);
1369
+ box-shadow: inset 0 0 0 1px #93c5fd;
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
+ }
1399
+ .lightbox-sep {
1400
+ width: 1px;
1401
+ align-self: stretch;
1402
+ margin: 4px 2px;
1403
+ background: rgba(255, 255, 255, 0.25);
1404
+ }
1405
+ /* Annotation overlay: absolute twin of the artwork, same viewBox, so shapes
1406
+ live in image coordinates and ride every zoom for free. Must undo the
1407
+ .lightbox-canvas > * block/white-background defaults. */
1408
+ .lightbox-canvas > .lightbox-anno {
1409
+ position: absolute;
1410
+ inset: 0;
1411
+ width: 100%;
1412
+ height: 100%;
1413
+ background: transparent;
1414
+ pointer-events: none;
1415
+ }
1416
+ .lightbox-stage[data-anno-cursor="draw"],
1417
+ .lightbox-stage[data-anno-cursor="draw"] .lightbox-anno { cursor: crosshair; }
1418
+ .lightbox-anno g[data-anno-id] { cursor: move; }
1419
+ .lightbox-anno [data-anno-handle] { cursor: nwse-resize; }
1367
1420
  .lightbox-zoom-value {
1368
1421
  min-width: 56px;
1369
1422
  text-align: center;
@@ -1384,7 +1437,7 @@ const html = `<!DOCTYPE html>
1384
1437
  overscroll-behavior: contain;
1385
1438
  }
1386
1439
  .lightbox-stage[data-panning] { cursor: grabbing; }
1387
- .lightbox-canvas { margin: 0 auto; }
1440
+ .lightbox-canvas { margin: 0 auto; position: relative; }
1388
1441
  /* Sized in px by the runtime; the child follows, so the scroll extent grows
1389
1442
  with the zoom. A CSS transform would scale the pixels and leave the scroll
1390
1443
  area at the original size, stranding the edges out of reach. */
@@ -2158,6 +2211,29 @@ ${mermaidInitTag}` : ''}
2158
2211
  var lightboxNaturalH = 1;
2159
2212
  var lightboxReturnScroll = 0;
2160
2213
 
2214
+ // Annotation layer state. Shapes are keyed per source node in memory only:
2215
+ // close/reopen keeps them, reload starts clean (they are reading-session
2216
+ // scratch, and localStorage keys would go stale when the doc regenerates).
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];
2220
+ var annoStore = (typeof WeakMap === 'function') ? new WeakMap() : null;
2221
+ var annoShapes = [];
2222
+ var annoSvg = null;
2223
+ var annoMode = null; // null | 'f' | 'e' | 'r' | 'l' | 'a' | 'm'
2224
+ var annoSelected = null;
2225
+ var annoDraft = null;
2226
+ var annoUndoStack = [];
2227
+ var annoRedoStack = [];
2228
+ var annoStrokeW = 3;
2229
+ var annoColor = ANNO_COLORS[0];
2230
+ var annoWidthScale = 1;
2231
+ var annoToolButtons = {};
2232
+ var annoStyleButtons = [];
2233
+ var annoSourceNode = null;
2234
+ var annoCloneEl = null;
2235
+ var annoBaseSrc = null;
2236
+
2161
2237
  function lightboxIsOpen() {
2162
2238
  return !!lightboxEl && !lightboxEl.hidden;
2163
2239
  }
@@ -2209,6 +2285,518 @@ ${mermaidInitTag}` : ''}
2209
2285
  setLightboxZoom(lightboxZoom * factor, anchorX, anchorY);
2210
2286
  }
2211
2287
 
2288
+ // ── Lightbox annotation layer ─────────────────────────────────────────────
2289
+ function annoGeomOf(shape) {
2290
+ if (shape.type === 'path') return { pts: shape.pts.map(function (p) { return [p[0], p[1]]; }) };
2291
+ var geom = {};
2292
+ Object.keys(shape).forEach(function (k) { if (k !== 'type') geom[k] = shape[k]; });
2293
+ return geom;
2294
+ }
2295
+
2296
+ function annoSetGeom(shape, geom) {
2297
+ if (shape.type === 'path') shape.pts = geom.pts.map(function (p) { return [p[0], p[1]]; });
2298
+ else Object.keys(geom).forEach(function (k) { shape[k] = geom[k]; });
2299
+ }
2300
+
2301
+ function annoBBox(shape) {
2302
+ if (shape.type === 'rect') return { x: shape.x, y: shape.y, w: shape.w, h: shape.h };
2303
+ if (shape.type === 'ellipse') return { x: shape.cx - shape.rx, y: shape.cy - shape.ry, w: shape.rx * 2, h: shape.ry * 2 };
2304
+ if (shape.type === 'line' || shape.type === 'arrow') {
2305
+ return {
2306
+ x: Math.min(shape.x1, shape.x2), y: Math.min(shape.y1, shape.y2),
2307
+ w: Math.abs(shape.x2 - shape.x1), h: Math.abs(shape.y2 - shape.y1),
2308
+ };
2309
+ }
2310
+ var xs = shape.pts.map(function (p) { return p[0]; });
2311
+ var ys = shape.pts.map(function (p) { return p[1]; });
2312
+ var x = Math.min.apply(null, xs);
2313
+ var y = Math.min.apply(null, ys);
2314
+ return { x: x, y: y, w: Math.max.apply(null, xs) - x, h: Math.max.apply(null, ys) - y };
2315
+ }
2316
+
2317
+ function annoApplyBBox(shape, from, to) {
2318
+ var sx = from.w > 0.01 ? to.w / from.w : 1;
2319
+ var sy = from.h > 0.01 ? to.h / from.h : 1;
2320
+ if (shape.type === 'rect') {
2321
+ shape.x = to.x; shape.y = to.y; shape.w = to.w; shape.h = to.h;
2322
+ } else if (shape.type === 'ellipse') {
2323
+ shape.cx = to.x + to.w / 2; shape.cy = to.y + to.h / 2;
2324
+ shape.rx = to.w / 2; shape.ry = to.h / 2;
2325
+ } else if (shape.type === 'line' || shape.type === 'arrow') {
2326
+ var x1 = to.x + (shape.x1 - from.x) * sx;
2327
+ var y1 = to.y + (shape.y1 - from.y) * sy;
2328
+ var x2 = to.x + (shape.x2 - from.x) * sx;
2329
+ var y2 = to.y + (shape.y2 - from.y) * sy;
2330
+ shape.x1 = x1; shape.y1 = y1; shape.x2 = x2; shape.y2 = y2;
2331
+ } else {
2332
+ shape.pts = shape.pts.map(function (p) {
2333
+ return [to.x + (p[0] - from.x) * sx, to.y + (p[1] - from.y) * sy];
2334
+ });
2335
+ }
2336
+ }
2337
+
2338
+ function annoPathD(pts) {
2339
+ return pts.map(function (p, i) {
2340
+ return (i === 0 ? 'M' : 'L') + p[0].toFixed(1) + ' ' + p[1].toFixed(1);
2341
+ }).join(' ');
2342
+ }
2343
+
2344
+ function annoShapeEl(shape) {
2345
+ var tag = (shape.type === 'line' || shape.type === 'arrow') ? 'line' : shape.type;
2346
+ var el = document.createElementNS(ANNO_NS, tag);
2347
+ var color = shape.color || ANNO_COLORS[0];
2348
+ if (shape.type === 'line' || shape.type === 'arrow') {
2349
+ el.setAttribute('x1', shape.x1.toFixed(1)); el.setAttribute('y1', shape.y1.toFixed(1));
2350
+ el.setAttribute('x2', shape.x2.toFixed(1)); el.setAttribute('y2', shape.y2.toFixed(1));
2351
+ el.setAttribute('stroke-linecap', 'round');
2352
+ if (shape.type === 'arrow') el.setAttribute('marker-end', 'url(#anno-arrow-' + color.slice(1) + ')');
2353
+ } else if (shape.type === 'rect') {
2354
+ el.setAttribute('x', shape.x.toFixed(1)); el.setAttribute('y', shape.y.toFixed(1));
2355
+ el.setAttribute('width', Math.max(0.1, shape.w).toFixed(1));
2356
+ el.setAttribute('height', Math.max(0.1, shape.h).toFixed(1));
2357
+ el.setAttribute('fill', 'transparent');
2358
+ } else if (shape.type === 'ellipse') {
2359
+ el.setAttribute('cx', shape.cx.toFixed(1)); el.setAttribute('cy', shape.cy.toFixed(1));
2360
+ el.setAttribute('rx', Math.max(0.1, shape.rx).toFixed(1));
2361
+ el.setAttribute('ry', Math.max(0.1, shape.ry).toFixed(1));
2362
+ el.setAttribute('fill', 'transparent');
2363
+ } else {
2364
+ el.setAttribute('d', annoPathD(shape.pts));
2365
+ el.setAttribute('fill', 'none');
2366
+ el.setAttribute('stroke-linejoin', 'round');
2367
+ el.setAttribute('stroke-linecap', 'round');
2368
+ }
2369
+ el.setAttribute('stroke', color);
2370
+ el.setAttribute('stroke-width', (annoStrokeW * (shape.ws || 1)).toFixed(1));
2371
+ return el;
2372
+ }
2373
+
2374
+ function annoEnsureDefs(svg) {
2375
+ var defs = document.createElementNS(ANNO_NS, '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);
2393
+ }
2394
+
2395
+ function annoRedraw() {
2396
+ if (!annoSvg) return;
2397
+ while (annoSvg.firstChild) annoSvg.removeChild(annoSvg.firstChild);
2398
+ annoEnsureDefs(annoSvg);
2399
+ annoShapes.forEach(function (shape, index) {
2400
+ var g = document.createElementNS(ANNO_NS, 'g');
2401
+ g.setAttribute('data-anno-id', String(index));
2402
+ g.appendChild(annoShapeEl(shape));
2403
+ if (shape.type === 'path' || shape.type === 'line' || shape.type === 'arrow') {
2404
+ // A 3px stroke is an unclickable target — give strokes a fat invisible twin.
2405
+ var hit = document.createElementNS(ANNO_NS, shape.type === 'path' ? 'path' : 'line');
2406
+ if (shape.type === 'path') {
2407
+ hit.setAttribute('d', annoPathD(shape.pts));
2408
+ } else {
2409
+ hit.setAttribute('x1', shape.x1); hit.setAttribute('y1', shape.y1);
2410
+ hit.setAttribute('x2', shape.x2); hit.setAttribute('y2', shape.y2);
2411
+ }
2412
+ hit.setAttribute('fill', 'none');
2413
+ hit.setAttribute('stroke', 'transparent');
2414
+ hit.setAttribute('stroke-width', String(annoStrokeW * 5));
2415
+ hit.setAttribute('data-anno-hit', '');
2416
+ g.appendChild(hit);
2417
+ }
2418
+ annoSvg.appendChild(g);
2419
+ });
2420
+ if (annoSelected && annoShapes.indexOf(annoSelected) !== -1) {
2421
+ var box = annoBBox(annoSelected);
2422
+ var ui = document.createElementNS(ANNO_NS, 'rect');
2423
+ ui.setAttribute('data-anno-ui', '');
2424
+ ui.setAttribute('x', box.x); ui.setAttribute('y', box.y);
2425
+ ui.setAttribute('width', Math.max(0.1, box.w)); ui.setAttribute('height', Math.max(0.1, box.h));
2426
+ ui.setAttribute('fill', 'none');
2427
+ ui.setAttribute('stroke', '#3b82f6');
2428
+ ui.setAttribute('stroke-width', String(Math.max(1, annoStrokeW / 2)));
2429
+ ui.setAttribute('stroke-dasharray', (annoStrokeW * 2) + ' ' + annoStrokeW);
2430
+ annoSvg.appendChild(ui);
2431
+ var hs = annoStrokeW * 3;
2432
+ [[box.x, box.y], [box.x + box.w, box.y], [box.x, box.y + box.h], [box.x + box.w, box.y + box.h]]
2433
+ .forEach(function (corner, i) {
2434
+ var h = document.createElementNS(ANNO_NS, 'rect');
2435
+ h.setAttribute('data-anno-ui', '');
2436
+ h.setAttribute('data-anno-handle', String(i));
2437
+ h.setAttribute('x', corner[0] - hs / 2); h.setAttribute('y', corner[1] - hs / 2);
2438
+ h.setAttribute('width', hs); h.setAttribute('height', hs);
2439
+ h.setAttribute('fill', '#3b82f6');
2440
+ annoSvg.appendChild(h);
2441
+ });
2442
+ }
2443
+ }
2444
+
2445
+ function annoPushOp(op) {
2446
+ annoUndoStack.push(op);
2447
+ annoRedoStack.length = 0;
2448
+ annoBake();
2449
+ }
2450
+
2451
+ function annoApplyOp(op, reverse) {
2452
+ if (op.kind === 'add') {
2453
+ if (reverse) annoShapes.splice(annoShapes.indexOf(op.shape), 1);
2454
+ else annoShapes.splice(Math.min(op.index, annoShapes.length), 0, op.shape);
2455
+ } else if (op.kind === 'del') {
2456
+ if (reverse) annoShapes.splice(Math.min(op.index, annoShapes.length), 0, op.shape);
2457
+ else annoShapes.splice(annoShapes.indexOf(op.shape), 1);
2458
+ } else if (op.kind === 'geom') {
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;
2464
+ } else if (op.kind === 'clear') {
2465
+ if (reverse) op.shapes.forEach(function (s) { annoShapes.push(s); });
2466
+ else annoShapes.length = 0;
2467
+ }
2468
+ }
2469
+
2470
+ function annoUndo() {
2471
+ var op = annoUndoStack.pop();
2472
+ if (!op) return;
2473
+ annoApplyOp(op, true);
2474
+ annoRedoStack.push(op);
2475
+ annoSelected = null;
2476
+ annoRedraw();
2477
+ annoBake();
2478
+ }
2479
+
2480
+ function annoRedo() {
2481
+ var op = annoRedoStack.pop();
2482
+ if (!op) return;
2483
+ annoApplyOp(op, false);
2484
+ annoUndoStack.push(op);
2485
+ annoSelected = null;
2486
+ annoRedraw();
2487
+ annoBake();
2488
+ }
2489
+
2490
+ function annoClearAll() {
2491
+ if (!annoShapes.length) return;
2492
+ annoPushOp({ kind: 'clear', shapes: annoShapes.slice() });
2493
+ annoShapes.length = 0;
2494
+ annoSelected = null;
2495
+ annoRedraw();
2496
+ }
2497
+
2498
+ function annoDeleteSelected() {
2499
+ if (!annoSelected) return;
2500
+ var index = annoShapes.indexOf(annoSelected);
2501
+ if (index === -1) return;
2502
+ annoPushOp({ kind: 'del', shape: annoSelected, index: index });
2503
+ annoShapes.splice(index, 1);
2504
+ annoSelected = null;
2505
+ annoRedraw();
2506
+ }
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
+
2631
+ function setAnnoMode(mode) {
2632
+ annoMode = (annoMode === mode) ? null : mode;
2633
+ if (annoMode !== 'm') annoSelected = null;
2634
+ Object.keys(annoToolButtons).forEach(function (key) {
2635
+ annoToolButtons[key].classList.toggle('is-active', key === annoMode);
2636
+ });
2637
+ if (annoSvg) annoSvg.style.pointerEvents = annoMode ? 'auto' : 'none';
2638
+ if (lightboxStage) {
2639
+ if (annoMode === 'f' || annoMode === 'e' || annoMode === 'r') {
2640
+ lightboxStage.setAttribute('data-anno-cursor', 'draw');
2641
+ } else {
2642
+ lightboxStage.removeAttribute('data-anno-cursor');
2643
+ }
2644
+ }
2645
+ annoRedraw();
2646
+ }
2647
+
2648
+ function annoPoint(event) {
2649
+ var rect = annoSvg.getBoundingClientRect();
2650
+ var scale = rect.width > 0 ? lightboxNaturalW / rect.width : 1;
2651
+ return { x: (event.clientX - rect.left) * scale, y: (event.clientY - rect.top) * scale };
2652
+ }
2653
+
2654
+ function annoCancelDraft() {
2655
+ if (!annoDraft) return;
2656
+ if (annoDraft.el && annoDraft.el.parentNode) annoDraft.el.parentNode.removeChild(annoDraft.el);
2657
+ annoDraft = null;
2658
+ }
2659
+
2660
+ function annoPointerDown(event) {
2661
+ if (!annoMode || event.button !== 0) return;
2662
+ event.preventDefault();
2663
+ event.stopPropagation();
2664
+ if (annoSvg.setPointerCapture) {
2665
+ try { annoSvg.setPointerCapture(event.pointerId); } catch (e) { /* detached */ }
2666
+ }
2667
+ var pt = annoPoint(event);
2668
+ if (annoMode === 'm') {
2669
+ var handle = event.target.closest ? event.target.closest('[data-anno-handle]') : null;
2670
+ var group = event.target.closest ? event.target.closest('g[data-anno-id]') : null;
2671
+ if (handle && annoSelected) {
2672
+ var corner = Number(handle.getAttribute('data-anno-handle'));
2673
+ var box = annoBBox(annoSelected);
2674
+ annoDraft = {
2675
+ kind: 'resize', shape: annoSelected, corner: corner,
2676
+ startBox: box, before: annoGeomOf(annoSelected), moved: false,
2677
+ anchor: {
2678
+ x: (corner === 0 || corner === 2) ? box.x + box.w : box.x,
2679
+ y: (corner === 0 || corner === 1) ? box.y + box.h : box.y,
2680
+ },
2681
+ };
2682
+ } else if (group) {
2683
+ annoSelected = annoShapes[Number(group.getAttribute('data-anno-id'))] || null;
2684
+ annoDraft = annoSelected ? {
2685
+ kind: 'move', shape: annoSelected, start: pt,
2686
+ startBox: annoBBox(annoSelected), before: annoGeomOf(annoSelected), moved: false,
2687
+ } : null;
2688
+ annoRedraw();
2689
+ } else {
2690
+ annoSelected = null;
2691
+ annoRedraw();
2692
+ }
2693
+ return;
2694
+ }
2695
+ var shape;
2696
+ if (annoMode === 'f') shape = { type: 'path', pts: [[pt.x, pt.y]] };
2697
+ else if (annoMode === 'e') shape = { type: 'ellipse', cx: pt.x, cy: pt.y, rx: 0, ry: 0 };
2698
+ else if (annoMode === 'l') shape = { type: 'line', x1: pt.x, y1: pt.y, x2: pt.x, y2: pt.y };
2699
+ else if (annoMode === 'a') shape = { type: 'arrow', x1: pt.x, y1: pt.y, x2: pt.x, y2: pt.y };
2700
+ else shape = { type: 'rect', x: pt.x, y: pt.y, w: 0, h: 0 };
2701
+ shape.color = annoColor;
2702
+ shape.ws = annoWidthScale;
2703
+ annoDraft = { kind: 'draw', shape: shape, start: pt, el: annoShapeEl(shape), clientDist: 0, lastClient: [event.clientX, event.clientY] };
2704
+ annoSvg.appendChild(annoDraft.el);
2705
+ }
2706
+
2707
+ function annoUpdateDraftEl() {
2708
+ var d = annoDraft;
2709
+ var fresh = annoShapeEl(d.shape);
2710
+ d.el.parentNode.replaceChild(fresh, d.el);
2711
+ d.el = fresh;
2712
+ }
2713
+
2714
+ function annoPointerMove(event) {
2715
+ if (!annoDraft) return;
2716
+ var pt = annoPoint(event);
2717
+ var d = annoDraft;
2718
+ if (d.kind === 'draw') {
2719
+ d.clientDist += Math.abs(event.clientX - d.lastClient[0]) + Math.abs(event.clientY - d.lastClient[1]);
2720
+ d.lastClient = [event.clientX, event.clientY];
2721
+ if (d.shape.type === 'path') {
2722
+ d.shape.pts.push([pt.x, pt.y]);
2723
+ } else if (d.shape.type === 'line' || d.shape.type === 'arrow') {
2724
+ d.shape.x2 = pt.x; d.shape.y2 = pt.y;
2725
+ } else if (d.shape.type === 'ellipse') {
2726
+ d.shape.cx = (d.start.x + pt.x) / 2; d.shape.cy = (d.start.y + pt.y) / 2;
2727
+ d.shape.rx = Math.abs(pt.x - d.start.x) / 2; d.shape.ry = Math.abs(pt.y - d.start.y) / 2;
2728
+ } else {
2729
+ d.shape.x = Math.min(d.start.x, pt.x); d.shape.y = Math.min(d.start.y, pt.y);
2730
+ d.shape.w = Math.abs(pt.x - d.start.x); d.shape.h = Math.abs(pt.y - d.start.y);
2731
+ }
2732
+ annoUpdateDraftEl();
2733
+ return;
2734
+ }
2735
+ d.moved = true;
2736
+ if (d.kind === 'move') {
2737
+ var box = d.startBox;
2738
+ annoApplyBBox(d.shape, annoBBox(d.shape), {
2739
+ x: box.x + (pt.x - d.start.x), y: box.y + (pt.y - d.start.y), w: box.w, h: box.h,
2740
+ });
2741
+ } else {
2742
+ var ax = d.anchor.x;
2743
+ var ay = d.anchor.y;
2744
+ annoApplyBBox(d.shape, annoBBox(d.shape), {
2745
+ x: Math.min(ax, pt.x), y: Math.min(ay, pt.y),
2746
+ w: Math.max(1, Math.abs(pt.x - ax)), h: Math.max(1, Math.abs(pt.y - ay)),
2747
+ });
2748
+ }
2749
+ annoRedraw();
2750
+ }
2751
+
2752
+ function annoPointerUp() {
2753
+ if (!annoDraft) return;
2754
+ var d = annoDraft;
2755
+ annoDraft = null;
2756
+ if (d.kind === 'draw') {
2757
+ if (d.el.parentNode) d.el.parentNode.removeChild(d.el);
2758
+ var tooSmall = d.clientDist < 4 ||
2759
+ (d.shape.type === 'path' && d.shape.pts.length < 2);
2760
+ if (!tooSmall) {
2761
+ annoPushOp({ kind: 'add', shape: d.shape, index: annoShapes.length });
2762
+ annoShapes.push(d.shape);
2763
+ }
2764
+ annoRedraw();
2765
+ return;
2766
+ }
2767
+ if (d.moved) {
2768
+ annoPushOp({ kind: 'geom', shape: d.shape, before: d.before, after: annoGeomOf(d.shape) });
2769
+ }
2770
+ annoRedraw();
2771
+ }
2772
+
2773
+ function annoSetup(sourceNode) {
2774
+ annoSourceNode = sourceNode;
2775
+ annoCloneEl = lightboxCanvas.firstElementChild;
2776
+ annoBaseSrc = (annoCloneEl && annoCloneEl.tagName === 'IMG') ? annoCloneEl.src : null;
2777
+ annoShapes = [];
2778
+ if (annoStore) {
2779
+ annoShapes = annoStore.get(sourceNode);
2780
+ if (!annoShapes) { annoShapes = []; annoStore.set(sourceNode, annoShapes); }
2781
+ }
2782
+ annoUndoStack = [];
2783
+ annoRedoStack = [];
2784
+ annoSelected = null;
2785
+ annoCancelDraft();
2786
+ annoStrokeW = Math.max(3, Math.round(Math.max(lightboxNaturalW, lightboxNaturalH) / 300));
2787
+ annoSvg = document.createElementNS(ANNO_NS, 'svg');
2788
+ annoSvg.setAttribute('class', 'lightbox-anno');
2789
+ annoSvg.setAttribute('viewBox', '0 0 ' + lightboxNaturalW + ' ' + lightboxNaturalH);
2790
+ annoSvg.addEventListener('pointerdown', annoPointerDown);
2791
+ annoSvg.addEventListener('pointermove', annoPointerMove);
2792
+ annoSvg.addEventListener('pointerup', annoPointerUp);
2793
+ annoSvg.addEventListener('pointercancel', annoPointerUp);
2794
+ lightboxCanvas.appendChild(annoSvg);
2795
+ annoMode = null;
2796
+ setAnnoMode(null);
2797
+ if (annoShapes.length) annoBake();
2798
+ }
2799
+
2212
2800
  function buildLightbox() {
2213
2801
  if (lightboxEl) return;
2214
2802
  lightboxEl = document.createElement('div');
@@ -2221,7 +2809,7 @@ ${mermaidInitTag}` : ''}
2221
2809
  bar.className = 'lightbox-bar';
2222
2810
  var hint = document.createElement('span');
2223
2811
  hint.className = 'lightbox-hint';
2224
- hint.textContent = 'scroll to pan · shift+scroll sideways · ctrl+scroll to zoom · drag to pan · esc to close';
2812
+ hint.textContent = 'ctrl+scroll zoom · drag pan · f/e/r/l/a draw · m select · del remove · ctrl+z undo · esc close';
2225
2813
  lightboxZoomValue = document.createElement('span');
2226
2814
  lightboxZoomValue.className = 'lightbox-zoom-value';
2227
2815
  lightboxZoomValue.setAttribute('data-lightbox-zoom-value', '');
@@ -2231,6 +2819,44 @@ ${mermaidInitTag}` : ''}
2231
2819
  bar.appendChild(lightboxButton('+', 'Zoom in', function () { zoomLightboxBy(LIGHTBOX_STEP); }));
2232
2820
  bar.appendChild(lightboxButton('Fit', 'Fit to window', function () { setLightboxZoom(lightboxFitZoom); }));
2233
2821
  bar.appendChild(lightboxButton('1:1', 'Actual size', function () { setLightboxZoom(1); }));
2822
+ var sep = document.createElement('span');
2823
+ sep.className = 'lightbox-sep';
2824
+ bar.appendChild(sep);
2825
+ [
2826
+ ['f', '✎', 'Freehand (f)'],
2827
+ ['e', '○', 'Ellipse (e)'],
2828
+ ['r', '▭', 'Rectangle (r)'],
2829
+ ['l', '╱', 'Line (l)'],
2830
+ ['a', '↗', 'Arrow (a)'],
2831
+ ['m', '✥', 'Select / move (m)'],
2832
+ ].forEach(function (tool) {
2833
+ var button = lightboxButton(tool[1], tool[2], function () { setAnnoMode(tool[0]); });
2834
+ button.setAttribute('data-anno-tool', tool[0]);
2835
+ annoToolButtons[tool[0]] = button;
2836
+ bar.appendChild(button);
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);
2857
+ var clearButton = lightboxButton('Clear', 'Clear all annotations', annoClearAll);
2858
+ clearButton.setAttribute('data-anno-clear', '');
2859
+ bar.appendChild(clearButton);
2234
2860
  bar.appendChild(lightboxButton('✕', 'Close', closeLightbox));
2235
2861
 
2236
2862
  lightboxStage = document.createElement('div');
@@ -2261,6 +2887,8 @@ ${mermaidInitTag}` : ''}
2261
2887
 
2262
2888
  lightboxStage.addEventListener('mousedown', function (event) {
2263
2889
  if (event.button !== 0) return;
2890
+ // With a tool armed the overlay owns pointer events over the artwork.
2891
+ if (annoMode && event.target && event.target.closest && event.target.closest('.lightbox-anno')) return;
2264
2892
  panning = true;
2265
2893
  panDistance = 0;
2266
2894
  panX = event.clientX;
@@ -2291,6 +2919,7 @@ ${mermaidInitTag}` : ''}
2291
2919
  });
2292
2920
  lightboxStage.addEventListener('dblclick', function (event) {
2293
2921
  event.preventDefault();
2922
+ if (annoMode && event.target && event.target.closest && event.target.closest('.lightbox-anno')) return;
2294
2923
  setLightboxZoom(Math.abs(lightboxZoom - 1) < 0.001 ? lightboxFitZoom : 1);
2295
2924
  });
2296
2925
  }
@@ -2331,6 +2960,7 @@ ${mermaidInitTag}` : ''}
2331
2960
  lightboxCanvas.removeChild(lightboxCanvas.firstChild);
2332
2961
  }
2333
2962
  lightboxCanvas.appendChild(clone);
2963
+ annoSetup(node);
2334
2964
 
2335
2965
  lightboxReturnScroll = window.scrollY;
2336
2966
  suppressAnchorCapture = true;
@@ -2347,6 +2977,14 @@ ${mermaidInitTag}` : ''}
2347
2977
  if (!lightboxIsOpen()) return;
2348
2978
  lightboxEl.hidden = true;
2349
2979
  document.body.removeAttribute('data-lightbox-open');
2980
+ annoCancelDraft();
2981
+ annoSyncInline();
2982
+ annoSourceNode = null;
2983
+ annoCloneEl = null;
2984
+ annoBaseSrc = null;
2985
+ annoSvg = null;
2986
+ annoMode = null;
2987
+ setAnnoMode(null);
2350
2988
  while (lightboxCanvas.firstChild) {
2351
2989
  lightboxCanvas.removeChild(lightboxCanvas.firstChild);
2352
2990
  }
@@ -2372,7 +3010,28 @@ ${mermaidInitTag}` : ''}
2372
3010
 
2373
3011
  document.addEventListener('keydown', function (event) {
2374
3012
  if (!lightboxIsOpen()) return;
2375
- if (event.key === 'Escape') { event.preventDefault(); closeLightbox(); return; }
3013
+ var key = event.key.toLowerCase();
3014
+ if ((event.ctrlKey || event.metaKey) && key === 'z' && !event.shiftKey) {
3015
+ event.preventDefault(); annoUndo(); return;
3016
+ }
3017
+ if ((event.ctrlKey || event.metaKey) && (key === 'y' || (key === 'z' && event.shiftKey))) {
3018
+ event.preventDefault(); annoRedo(); return;
3019
+ }
3020
+ if (event.ctrlKey || event.metaKey || event.altKey) return;
3021
+ if (event.key === 'Escape') {
3022
+ event.preventDefault();
3023
+ // Layered: cancel an in-progress draw, then drop the selection, then close.
3024
+ if (annoDraft) { annoCancelDraft(); annoRedraw(); return; }
3025
+ if (annoSelected) { annoSelected = null; annoRedraw(); return; }
3026
+ closeLightbox();
3027
+ return;
3028
+ }
3029
+ if (key === 'f' || key === 'e' || key === 'r' || key === 'l' || key === 'a' || key === 'm') {
3030
+ event.preventDefault(); setAnnoMode(key); return;
3031
+ }
3032
+ if (event.key === 'Delete' || event.key === 'Backspace') {
3033
+ event.preventDefault(); annoDeleteSelected(); return;
3034
+ }
2376
3035
  if (event.key === '+' || event.key === '=') { event.preventDefault(); zoomLightboxBy(LIGHTBOX_STEP); return; }
2377
3036
  if (event.key === '-' || event.key === '_') { event.preventDefault(); zoomLightboxBy(1 / LIGHTBOX_STEP); return; }
2378
3037
  if (event.key === '0') { event.preventDefault(); setLightboxZoom(lightboxFitZoom); return; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helping-ai-workflow/md2doc",
3
- "version": "2.6.1",
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/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",