@helping-ai-workflow/md2doc 3.12.1 → 3.13.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.
@@ -12,7 +12,16 @@
12
12
  btn.hidden = false;
13
13
  var data = JSON.parse(dataEl.textContent);
14
14
  var listeners = [];
15
- function onTheme(fn) { listeners.push(fn); }
15
+ // Returns the unsubscribe: a page that draws and discards its own svgs (the
16
+ // wave editor's canvas, once per open) must not leave a listener behind
17
+ // holding what it drew.
18
+ function onTheme(fn) {
19
+ listeners.push(fn);
20
+ return function () {
21
+ var at = listeners.indexOf(fn);
22
+ if (at !== -1) listeners.splice(at, 1);
23
+ };
24
+ }
16
25
  function notify(dark) { for (var i = 0; i < listeners.length; i++) listeners[i](dark); }
17
26
  function isDark() { return root.getAttribute('data-md2doc-theme') === 'dark'; }
18
27
  function save(dark) {
@@ -29,6 +38,9 @@
29
38
  }
30
39
  btn.addEventListener('click', function () {
31
40
  var dark = !isDark();
41
+ // Measured while the light layout is still clean: see prepareGeometry. A
42
+ // failure there only costs that speed-up; the toggle must still work.
43
+ if (dark) { try { prepareGeometry(); } catch (e) { /* recolourSvg measures lazily instead */ } }
32
44
  if (dark) root.setAttribute('data-md2doc-theme', 'dark'); else root.removeAttribute('data-md2doc-theme');
33
45
  save(dark);
34
46
  paint();
@@ -268,9 +280,208 @@
268
280
  return /^rect$/i.test(el.tagName) && next !== null && /^text$/i.test(next.tagName) &&
269
281
  el.closest('[id^="wavearcs"]') !== null;
270
282
  }
271
- function recolourSvg(svg, dark) {
283
+ function contrast(a, b) {
284
+ var x = lum(a), y = lum(b);
285
+ return (Math.max(x, y) + 0.05) / (Math.min(x, y) + 0.05);
286
+ }
287
+ // ── Where a text sits (R7 option A) ──
288
+ // Geometry is kept apart from colour: a text's STACK is every shape before it in
289
+ // document order (= paint order) whose fill AREA covers the text's centre, bottom
290
+ // to top, whatever its fill. That is the same in light and dark, so the toggle
291
+ // measures it BEFORE the theme attribute flips, while the page layout is still
292
+ // clean (prepareGeometry), and keeps it per svg; a read after the flip would force
293
+ // a synchronous layout of the whole restyled page. Colour is applied afterwards:
294
+ // the stack's FINAL fills are composited (fill alpha x fill-opacity x opacity of
295
+ // the shape and its ancestors) over the page, so a semi-transparent cell counts as
296
+ // the colour it really shows.
297
+ var PAINTED = 'polygon, ellipse, path, rect, circle';
298
+ var NOT_PAINTED = 'defs, marker, clipPath, mask, pattern, symbol, foreignObject';
299
+ var geoCache = typeof WeakMap === 'function' ? new WeakMap() : null;
300
+ // A mermaid XHTML label is measured by its foreignObject.
301
+ function geoOf(el) { return el.namespaceURI === XHTML ? el.closest('foreignObject') : el; }
302
+ function centreOf(g) {
303
+ if (!g || typeof g.getBBox !== 'function') return null;
304
+ var b, m;
305
+ try { b = g.getBBox(); m = g.getCTM(); } catch (e) { return null; }
306
+ if (!m || (!b.width && !b.height)) return null;
307
+ return new DOMPoint(b.x + b.width / 2, b.y + b.height / 2).matrixTransform(m);
308
+ }
309
+ function geomShapes(svg) {
310
+ var all = svg.querySelectorAll(PAINTED), list = [];
311
+ for (var i = 0; i < all.length; i++) {
312
+ var s = all[i], b, m;
313
+ if (s.closest(NOT_PAINTED) || typeof s.isPointInFill !== 'function') continue;
314
+ try { b = s.getBBox(); m = s.getCTM(); } catch (e) { continue; }
315
+ if (!m) continue;
316
+ // Its box in viewport units, for a cheap reject before isPointInFill.
317
+ var xs = [], ys = [], cs = [[b.x, b.y], [b.x + b.width, b.y], [b.x, b.y + b.height], [b.x + b.width, b.y + b.height]];
318
+ for (var c = 0; c < 4; c++) { var q = new DOMPoint(cs[c][0], cs[c][1]).matrixTransform(m); xs.push(q.x); ys.push(q.y); }
319
+ list.push({ el: s, m: m, vp: s.nearestViewportElement,
320
+ box: [Math.min.apply(null, xs), Math.min.apply(null, ys), Math.max.apply(null, xs), Math.max.apply(null, ys)] });
321
+ }
322
+ return list;
323
+ }
324
+ // null when the element has no geometry (detached, display:none).
325
+ function stackAt(g, shapes) {
326
+ var p = centreOf(g);
327
+ if (!p) return null;
328
+ var vp = g.nearestViewportElement, stack = [];
329
+ for (var i = 0; i < shapes.length; i++) {
330
+ var s = shapes[i];
331
+ if (s.vp !== vp || p.x < s.box[0] || p.x > s.box[2] || p.y < s.box[1] || p.y > s.box[3]) continue;
332
+ if (!(s.el.compareDocumentPosition(g) & 4)) break; // document order: every later shape follows g too
333
+ if (s.el.isPointInFill(p.matrixTransform(s.m.inverse()))) stack.push(s.el);
334
+ }
335
+ return stack;
336
+ }
337
+ // A mermaid svg is rebuilt from markup on every theme swap (new elements, same
338
+ // drawing), so its stacks are ALSO kept per host and per render kind (light / live
339
+ // dark) as indices: the n-th text or foreignObject over the m-th shapes. A swapped-in
340
+ // svg with the same counts adopts them without reading geometry.
341
+ var GEO_EL = 'text, tspan, foreignObject';
342
+ var hostGeo = typeof WeakMap === 'function' ? new WeakMap() : null;
343
+ function hostEntry(svg, make) {
344
+ var host = hostGeo && svg.closest('.mermaid');
345
+ if (!host) return null;
346
+ var kind = svg.hasAttribute('data-md2doc-live') ? 'live' : 'light';
347
+ var all = hostGeo.get(host) || {};
348
+ var nT = svg.querySelectorAll(GEO_EL).length, nS = svg.querySelectorAll(PAINTED).length, e = all[kind];
349
+ if (!e || e.nT !== nT || e.nS !== nS) {
350
+ if (!make) return null;
351
+ e = all[kind] = { nT: nT, nS: nS, map: {} };
352
+ hostGeo.set(host, all);
353
+ }
354
+ return e;
355
+ }
356
+ function adopt(svg) {
357
+ var e = geoCache && hostEntry(svg, false);
358
+ if (!e) return;
359
+ var keep = geoCache.get(svg) || new Map(), gl = svg.querySelectorAll(GEO_EL), sl = svg.querySelectorAll(PAINTED);
360
+ for (var t in e.map) {
361
+ var g = gl[+t];
362
+ if (g && !keep.has(g)) keep.set(g, e.map[t].map(function (n) { return sl[n]; }));
363
+ }
364
+ if (keep.size) geoCache.set(svg, keep);
365
+ }
366
+ // An element with no geometry (empty label, display:none) is remembered for the
367
+ // current toggle only (geoGen): it may be shown before the next one.
368
+ var noGeo = typeof WeakMap === 'function' ? new WeakMap() : null, geoGen = 0;
369
+ // Measures the given geometry elements of one svg and remembers each stack.
370
+ function measure(svg, gs) {
371
+ var shapes = null, keep = geoCache ? geoCache.get(svg) || new Map() : new Map(), out = new Map();
372
+ for (var i = 0; i < gs.length; i++) {
373
+ var g = gs[i];
374
+ if (!g || out.has(g)) continue;
375
+ if (shapes === null) shapes = geomShapes(svg);
376
+ var st = stackAt(g, shapes);
377
+ out.set(g, st);
378
+ if (st) keep.set(g, st); else if (noGeo) noGeo.set(g, geoGen);
379
+ }
380
+ if (geoCache && keep.size) geoCache.set(svg, keep);
381
+ var e = keep.size ? hostEntry(svg, true) : null;
382
+ if (e) {
383
+ var gl = [].slice.call(svg.querySelectorAll(GEO_EL)), sl = [].slice.call(svg.querySelectorAll(PAINTED));
384
+ out.forEach(function (st, g) {
385
+ if (st) e.map[gl.indexOf(g)] = st.map(function (s) { return sl.indexOf(s); });
386
+ });
387
+ }
388
+ return out;
389
+ }
390
+ // Called by the toggle before it flips to dark. A light-drawn mermaid svg is
391
+ // measured too (it is recoloured until the live dark render arrives) unless that
392
+ // dark render is cached: then the swap replaces it before anything is recoloured.
393
+ function prepareGeometry() {
394
+ if (!geoCache) return;
395
+ geoGen++;
396
+ var swapsNow = hasMermaid && !inFlight && cached('dark');
397
+ var svgs = document.querySelectorAll(RECOLOUR);
398
+ for (var i = 0; i < svgs.length; i++) {
399
+ var svg = svgs[i], gs = [];
400
+ if (swapsNow && svg.closest('.mermaid')) continue;
401
+ adopt(svg);
402
+ var known = geoCache.get(svg), ts = svg.querySelectorAll('text, tspan, foreignObject *');
403
+ for (var k = 0; k < ts.length; k++) {
404
+ var g = geoOf(ts[k]);
405
+ // Every text, not only the dark ones: an inherited colour can turn dark (or
406
+ // light) with the page once the theme flips.
407
+ if (g && !(known && known.has(g)) && !(noGeo && noGeo.get(g) === geoGen)) gs.push(g);
408
+ }
409
+ if (gs.length) measure(svg, gs);
410
+ }
411
+ }
412
+ function num(v) { var n = parseFloat(v); return isNaN(n) ? 1 : n; }
413
+ // A shape's final paint as [r, g, b, alpha], or null when it shows nothing.
414
+ function paintOf(s, svg, memo) {
415
+ if (memo.has(s)) return memo.get(s);
416
+ var cs = getComputedStyle(s), f = rgba(cs.fill), out = null;
417
+ if (f) {
418
+ var a = f[3] * num(cs.fillOpacity) * num(cs.opacity);
419
+ for (var p = s.parentNode; a > 0 && p && p !== svg && p.nodeType === 1; p = p.parentNode) a *= num(getComputedStyle(p).opacity);
420
+ if (a > 0) out = [f[0], f[1], f[2], Math.min(a, 1)];
421
+ }
422
+ memo.set(s, out);
423
+ return out;
424
+ }
425
+ // The colour under a text: its stack composited over the page; null when nothing
426
+ // in the stack paints (the text then keeps the old rule).
427
+ function composite(stack, page, svg, memo) {
428
+ var c = null;
429
+ for (var i = 0; i < stack.length; i++) {
430
+ var p = paintOf(stack[i], svg, memo);
431
+ if (!p) continue;
432
+ if (!c) c = page.slice(0, 3);
433
+ c = [p[0] * p[3] + c[0] * (1 - p[3]), p[1] * p[3] + c[1] * (1 - p[3]), p[2] * p[3] + c[2] * (1 - p[3])];
434
+ }
435
+ return c ? [c[0], c[1], c[2], 1] : null;
436
+ }
437
+ // No geometry at recolour time (a diagram hidden while the theme flips): the text
438
+ // could sit on any shape of its own node or cluster, so it is judged against all
439
+ // of them: each shape's paint composited over the page, or the page where a shape
440
+ // paints nothing (a table cell without a bgcolor). Text in no node keeps the old rule.
441
+ function nodeFallback(el, page, svg, memo) {
442
+ var n = el.closest('g.node, g.cluster');
443
+ if (!n) return null;
444
+ var pg = [page[0], page[1], page[2], 1], all = n.querySelectorAll(PAINTED), out = [], seen = {};
445
+ for (var i = 0; i < all.length; i++) {
446
+ if (all[i].closest('foreignObject')) continue;
447
+ var c = composite([all[i]], page, svg, memo) || pg;
448
+ if (!seen[c.join()]) { seen[c.join()] = 1; out.push(c); }
449
+ }
450
+ return out.length ? out : [pg];
451
+ }
452
+ // R7 option A: a dark text keeps its author colour unless the rule's replacement
453
+ // (a toned colour for saturated text) or #e3e3e3 reads better on what is under it.
454
+ // `unders` is the colour under it (one entry) or, with no geometry, every colour it
455
+ // might sit on: each option is scored by its WORST contrast over them, the best
456
+ // score wins, ties keep the author's then the toned colour. null = keep the
457
+ // author's colour. Over no shape (unders null): the old rule (the replacement).
458
+ // Shapes are never changed here.
459
+ function textFor(own, unders) {
460
+ var cand = hsl(own)[1] > 0.25 ? tone(own, 0.78, 0.7) : TEXT;
461
+ if (!unders) return cand;
462
+ var opts = cand === TEXT ? [null, TEXT] : [null, cand, TEXT], best = null, bestC = -1;
463
+ for (var i = 0; i < opts.length; i++) {
464
+ var col = opts[i] === null ? own : rgba(opts[i]), worst = Infinity;
465
+ for (var u = 0; u < unders.length; u++) worst = Math.min(worst, contrast(col, unders[u]));
466
+ if (worst > bestC) { best = opts[i]; bestC = worst; }
467
+ }
468
+ return best;
469
+ }
470
+ function decide(own, unders, memo) {
471
+ var key = own.join() + '|' + (unders ? unders.map(function (u) { return u.join(); }).join(';') : '');
472
+ if (!(key in memo)) memo[key] = textFor(own, unders);
473
+ return memo[key];
474
+ }
475
+ function restyle(el, ov) {
476
+ if (!ov) return;
477
+ var base = el.getAttribute('data-md2doc-style');
478
+ el.setAttribute('style', (base ? base.replace(/;?\s*$/, ';') : '') + ov);
479
+ }
480
+ function recolourSvg(svg, dark, opts) {
481
+ var backing = (opts && opts.backing) || 'var(--md-bg)';
272
482
  var els = svg.querySelectorAll('polygon, ellipse, path, polyline, rect, line, circle, text, tspan, foreignObject *');
273
483
  var live = svg.hasAttribute('data-md2doc-live');
484
+ var texts = [];
274
485
  for (var i = 0; i < els.length; i++) {
275
486
  var el = els[i];
276
487
  if (el.hasAttribute('data-md2doc-style')) {
@@ -287,43 +498,94 @@
287
498
  el.setAttribute('data-md2doc-style', el.getAttribute('style') || '');
288
499
  }
289
500
  if (!dark) continue;
501
+ // Texts and mermaid's XHTML labels wait for the second pass, so each one
502
+ // is paired with the FINAL fill of its own shape whatever the document order.
503
+ if (el.namespaceURI === XHTML || /^(text|tspan)$/i.test(el.tagName)) { texts.push(el); continue; }
290
504
  var cs = getComputedStyle(el);
291
505
  var ov = '';
292
- if (el.namespaceURI === XHTML) {
293
- // Mermaid flowchart labels: HTML inside foreignObject, coloured by CSS.
294
- var fg = rgba(cs.color), bgc = rgba(cs.backgroundColor);
295
- if (fg && fg[3] > 0 && lum(fg) < 0.35) ov += 'color:' + (hsl(fg)[1] > 0.25 ? tone(fg, 0.78, 0.7) : TEXT) + ' !important;';
296
- if (bgc && bgc[3] > 0) {
297
- var BL = lum(bgc);
298
- if (BL > 0.85) ov += 'background-color:transparent !important;';
299
- else if (BL > 0.3) ov += 'background-color:' + tone(bgc, 0.26, 0.55) + ' !important;';
300
- }
301
- if (ov) {
302
- var hb = el.getAttribute('data-md2doc-style');
303
- el.setAttribute('style', (hb ? hb.replace(/;?\s*$/, ';') : '') + ov);
304
- }
305
- continue;
306
- }
307
506
  var fill = rgba(cs.fill), stroke = rgba(cs.stroke);
308
507
  // Overrides are written once, as attribute text, never through el.style:
309
508
  // setProperty leaves style="" on the element after the restore removes
310
509
  // the attribute (Chromium re-serialises it), so light would not be
311
510
  // byte-exact.
312
- if (/^(text|tspan)$/i.test(el.tagName)) {
313
- if (fill && fill[3] > 0 && lum(fill) < 0.35) ov += 'fill:' + (hsl(fill)[1] > 0.25 ? tone(fill, 0.78, 0.7) : TEXT) + ' !important;';
314
- } else {
315
- if (fill && fill[3] > 0) {
316
- var L = lum(fill);
317
- if (L > 0.85) ov += isWaveLabelBacking(el) ? 'fill:var(--md-bg) !important;' : 'fill:transparent !important;';
318
- else if (L > 0.3) ov += 'fill:' + tone(fill, 0.26, 0.55) + ' !important;';
319
- else if (el.closest('g.edge, marker')) ov += 'fill:' + lineColour(fill) + ' !important;';
511
+ if (fill && fill[3] > 0) {
512
+ var L = lum(fill);
513
+ if (L > 0.85) ov += isWaveLabelBacking(el) ? 'fill:' + backing + ' !important;' : 'fill:transparent !important;';
514
+ else if (L > 0.3) ov += 'fill:' + tone(fill, 0.26, 0.55) + ' !important;';
515
+ else if (el.closest('g.edge, marker')) ov += 'fill:' + lineColour(fill) + ' !important;';
516
+ }
517
+ if (stroke && stroke[3] > 0 && lum(stroke) < 0.35) ov += 'stroke:' + lineColour(stroke) + ' !important;';
518
+ restyle(el, ov);
519
+ }
520
+ // Only a dark text has a choice to make. It uses the stack measured before the
521
+ // toggle flipped the theme when there is one. Otherwise geometry is read only
522
+ // when the choice can differ between what the text might sit on (each opaque
523
+ // paint, or none; any translucent paint always measures): reading it forces a
524
+ // synchronous layout, which a freshly drawn svg (the wave editor redraws on every
525
+ // edit) has not had yet. An svg text's own fill is read here once and reused
526
+ // below: the writes below would otherwise force a style recalc before every read.
527
+ var memo = {}, paints = new Map(), unders = [], owns = [], need = [], fills = null, translucent = false;
528
+ if (dark) adopt(svg);
529
+ var known = geoCache && geoCache.get(svg), page = null;
530
+ function pageColour() {
531
+ if (page) return page;
532
+ var bk = opts && opts.backing;
533
+ page = (bk && /^rgba?\(/.test(bk) ? rgba(bk) : null) || rgba(getComputedStyle(document.body).backgroundColor) || [255, 255, 255, 1];
534
+ return page;
535
+ }
536
+ for (var k = 0; k < texts.length; k++) {
537
+ var kc = owns[k] = rgba(texts[k].namespaceURI === XHTML ? getComputedStyle(texts[k]).color : getComputedStyle(texts[k]).fill);
538
+ if (!kc || kc[3] === 0 || lum(kc) >= 0.35) continue;
539
+ var st = known && known.get(geoOf(texts[k]));
540
+ if (st && st.every(function (s) { return svg.contains(s); })) { var cu = composite(st, pageColour(), svg, paints); unders[k] = cu ? [cu] : null; continue; }
541
+ var kg = geoOf(texts[k]);
542
+ if (noGeo && kg && noGeo.get(kg) === geoGen) { unders[k] = nodeFallback(texts[k], pageColour(), svg, paints); continue; }
543
+ if (fills === null) {
544
+ fills = [];
545
+ var all = svg.querySelectorAll(PAINTED), seen = {};
546
+ for (var q = 0; q < all.length; q++) {
547
+ if (all[q].closest(NOT_PAINTED)) continue;
548
+ var pq = paintOf(all[q], svg, paints);
549
+ if (!pq) continue;
550
+ if (pq[3] < 1) translucent = true;
551
+ else if (!seen[pq.join()]) { seen[pq.join()] = 1; fills.push(pq); }
320
552
  }
321
- if (stroke && stroke[3] > 0 && lum(stroke) < 0.35) ov += 'stroke:' + lineColour(stroke) + ' !important;';
322
553
  }
323
- if (ov) {
324
- var base = el.getAttribute('data-md2doc-style');
325
- el.setAttribute('style', (base ? base.replace(/;?\s*$/, ';') : '') + ov);
554
+ var first = decide(kc, null, memo), differs = translucent;
555
+ for (var w = 0; w < fills.length && !differs; w++) differs = decide(kc, [fills[w]], memo) !== first;
556
+ if (differs) need.push(k);
557
+ }
558
+ if (need.length) {
559
+ var got = measure(svg, need.map(function (n) { return geoOf(texts[n]); }));
560
+ for (var r = 0; r < need.length; r++) {
561
+ var ns = got.get(geoOf(texts[need[r]]));
562
+ var nc = ns ? composite(ns, pageColour(), svg, paints) : null;
563
+ unders[need[r]] = ns ? (nc ? [nc] : null) : nodeFallback(texts[need[r]], pageColour(), svg, paints);
564
+ }
565
+ }
566
+ for (var j = 0; j < texts.length; j++) {
567
+ var t = texts[j], tov = '';
568
+ if (t.namespaceURI === XHTML) {
569
+ // Mermaid flowchart labels: HTML inside foreignObject, coloured by CSS.
570
+ // Read again: a label inherits the colour its parent was just given.
571
+ var tcs = getComputedStyle(t), fg = rgba(tcs.color), bgc = rgba(tcs.backgroundColor);
572
+ if (fg && fg[3] > 0 && lum(fg) < 0.35) {
573
+ var fc = decide(fg, unders[j], memo);
574
+ if (fc) tov += 'color:' + fc + ' !important;';
575
+ }
576
+ if (bgc && bgc[3] > 0) {
577
+ var BL = lum(bgc);
578
+ if (BL > 0.85) tov += 'background-color:transparent !important;';
579
+ else if (BL > 0.3) tov += 'background-color:' + tone(bgc, 0.26, 0.55) + ' !important;';
580
+ }
581
+ } else {
582
+ var tf = owns[j];
583
+ if (tf && tf[3] > 0 && lum(tf) < 0.35) {
584
+ var tc = decide(tf, unders[j], memo);
585
+ if (tc) tov += 'fill:' + tc + ' !important;';
586
+ }
326
587
  }
588
+ restyle(t, tov);
327
589
  }
328
590
  if (dark) svg.setAttribute('data-md2doc-recoloured', ''); else svg.removeAttribute('data-md2doc-recoloured');
329
591
  }
@@ -352,4 +614,9 @@
352
614
  }).observe(content, { childList: true, subtree: true });
353
615
  }
354
616
  if (isDark()) recolourAll(true);
617
+ // Public surface for pages that draw their own SVG outside .content (the wave editor canvas).
618
+ // `__listenerCount` is a test hook (test/editor-click.test.js): the wave
619
+ // editor subscribes once per open and must unsubscribe on close.
620
+ window.md2docTheme = { isDark: isDark, onTheme: onTheme, recolourSvg: recolourSvg,
621
+ __listenerCount: function () { return listeners.length; } };
355
622
  })();
@@ -163,6 +163,9 @@ function themeCss() {
163
163
  ' ' + D + ' .content a:not(.heading-anchor), ' + D + ' .content a:not(.heading-anchor):visited { color: ' + LINK_DARK + '; }',
164
164
  ' ' + D + ' .content img { background: #ffffff; border-radius: 4px; box-shadow: 0 0 0 10px #ffffff; }',
165
165
  ' ' + D + ' .content .drawio { background: #ffffff; border-radius: 6px; box-shadow: 0 0 0 10px #ffffff; }',
166
+ // R7 甲: a Markdown-native inline svg (wrapped in .inline-svg by the renderer) gets the
167
+ // same white plate; its ink stays the light text colour because the plate is always white.
168
+ ' ' + D + ' .content .inline-svg > svg { background: #ffffff; color: #1f2328; border-radius: 4px; box-shadow: 0 0 0 10px #ffffff; }',
166
169
  ' ' + D + ' .lightbox-canvas > svg[data-md2doc-recoloured] { background: var(--md-bg); }',
167
170
  ' }',
168
171
  ' .md2doc-theme-toggle { position: fixed; right: 20px; bottom: 20px; z-index: 60; width: 40px; height: 40px;',
@@ -0,0 +1,107 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * wave-overflow — estimate how wide a WaveDrom data label is and find the ones
5
+ * wider than the segment they sit in.
6
+ *
7
+ * UMD: node `module.exports`. The edit page loads it through lib/editor/server.js
8
+ * `waveModuleTag`, whose CommonJS shim supplies `module`, so in the browser it
9
+ * lands at `window.__md2docWave['wave-overflow.js']` (wave-ui.js receives it as
10
+ * `opts.waveOverflow`). Segments come from wave-codec (`expandWave`,
11
+ * `dataSlotOf`), never from a second parser of the wave string.
12
+ *
13
+ * Widths are the standard Helvetica AFM advances (1/1000 em) for ASCII 32-126;
14
+ * anything else counts one em. The reader draws data labels in Helvetica at
15
+ * 14.6667px, which measured 107px for twelve F and 149px for
16
+ * "D2 (client FCS last 4B)"; this table reproduces both within 2px.
17
+ */
18
+ (function (root, factory) {
19
+ if (typeof module === 'object' && module.exports) {
20
+ module.exports = factory(require('./editor/wave-codec.js'));
21
+ } else {
22
+ // Only a bare <script> with no shim reaches this branch; nothing ships it
23
+ // that way today.
24
+ root.md2docWaveOverflow = factory(root.__md2docWave['wave-codec.js']);
25
+ }
26
+ }(typeof self !== 'undefined' ? self : this, function (codec) {
27
+ // Index 0 is code 32 (space).
28
+ const AFM = [
29
+ 278, 278, 355, 556, 556, 889, 667, 191, 333, 333, 389, 584, 278, 333, 278, 278,
30
+ 556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 278, 278, 584, 584, 584, 556,
31
+ 1015, 667, 667, 722, 722, 667, 611, 778, 722, 278, 500, 667, 556, 833, 722, 778,
32
+ 667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611, 278, 278, 278, 469, 556,
33
+ 333, 556, 556, 500, 556, 556, 278, 556, 556, 222, 222, 500, 222, 833, 556, 556,
34
+ 556, 556, 333, 500, 278, 556, 500, 722, 500, 500, 500, 334, 260, 334, 584,
35
+ ];
36
+ const DEFAULT_FONT_PX = 14.6667;
37
+ const CYCLE_UNITS = 40;
38
+ const PAD = 6; // breathing room kept on top of the text
39
+
40
+ function estimateTextWidth(text, fontPx) {
41
+ const px = typeof fontPx === 'number' ? fontPx : DEFAULT_FONT_PX;
42
+ let em = 0;
43
+ for (const ch of String(text)) {
44
+ const c = ch.codePointAt(0);
45
+ em += (c >= 32 && c <= 126 ? AFM[c - 32] : 1000) / 1000;
46
+ }
47
+ return em * px;
48
+ }
49
+
50
+ // WaveDrom's own rule (parse-config.js): rounded, <=0 -> 1, capped at 100.
51
+ function hscaleOf(doc) {
52
+ const h = doc && doc.config && doc.config.hscale;
53
+ if (!h) return 1;
54
+ const n = Math.round(h > 0 ? Math.round(h) : 1);
55
+ return n > 0 ? Math.min(n, 100) : 1;
56
+ }
57
+
58
+ function lanesOf(list, out) {
59
+ for (const item of list) {
60
+ if (Array.isArray(item)) lanesOf(item, out);
61
+ else if (item && typeof item === 'object') out.push(item);
62
+ }
63
+ return out;
64
+ }
65
+
66
+ function dataList(lane) {
67
+ if (Array.isArray(lane.data)) return lane.data.map(String);
68
+ if (typeof lane.data === 'string') return lane.data.split(/\s+/).filter(Boolean);
69
+ return [];
70
+ }
71
+
72
+ function overflowOf(doc) {
73
+ const out = [];
74
+ if (!doc || !Array.isArray(doc.signal)) return out;
75
+ const hscale = hscaleOf(doc);
76
+ lanesOf(doc.signal, []).forEach(function (lane, laneIndex) {
77
+ if (typeof lane.wave !== 'string') return;
78
+ const cells = codec.expandWave(lane.wave);
79
+ const data = dataList(lane);
80
+ const p = Number(lane.period);
81
+ const period = p > 0 ? p : 1;
82
+ for (let i = 0; i < cells.length; i++) {
83
+ const slot = codec.dataSlotOf(lane, i);
84
+ if (slot === null || slot >= data.length) continue;
85
+ let n = 1;
86
+ while (i + n < cells.length && cells[i + n].held) n++;
87
+ const label = data[slot];
88
+ const need = estimateTextWidth(label) + PAD;
89
+ const unit = n * CYCLE_UNITS * period;
90
+ if (need <= unit * hscale) continue;
91
+ out.push({
92
+ laneIndex: laneIndex, name: typeof lane.name === 'string' ? lane.name : '',
93
+ slot: slot, label: label, cells: n, needScale: Math.ceil(need / unit),
94
+ });
95
+ }
96
+ });
97
+ return out;
98
+ }
99
+
100
+ function suggestScale(list, current) {
101
+ let k = 0;
102
+ for (const o of list || []) if (o.needScale > k) k = o.needScale;
103
+ return k || (typeof current === 'number' ? current : 1);
104
+ }
105
+
106
+ return { estimateTextWidth: estimateTextWidth, overflowOf: overflowOf, suggestScale: suggestScale };
107
+ }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helping-ai-workflow/md2doc",
3
- "version": "3.12.1",
3
+ "version": "3.13.1",
4
4
  "description": "Markdown → HTML / PDF renderer with WaveDrom, Mermaid, Graphviz, and draw.io support",
5
5
  "keywords": [
6
6
  "markdown",
@@ -46,8 +46,8 @@
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/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/wave-rail-clearance.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",
50
- "test:browser": "node test/reader-click.test.js && node test/editor-click.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/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",
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": {
53
53
  "type": "git",
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // Release gate for .github/workflows/publish.yml.
5
+ //
6
+ // The test suites run once, on the pull request. Branch protection on `main`
7
+ // requires those checks to be green and the branch to be up to date before a
8
+ // merge, so every commit on main is a tree that already passed them; running
9
+ // `npm test` again on the tag only repeated a 40-minute job (v3.12.1: four
10
+ // `npm test` runs for one release). What the tag job still has to prove is that
11
+ // the release metadata agrees:
12
+ // - the tag is vX.Y.Z and equals package.json's version;
13
+ // - the top `## vX.Y.Z — YYYY-MM-DD` heading of CHANGELOG.md is that version;
14
+ // - (CLI, --on-main) the tagged commit is reachable from origin/main.
15
+ //
16
+ // Usage: node scripts/release-check.js <tag> [--on-main]
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const { execFileSync } = require('child_process');
21
+
22
+ const HEADING = /^## v(\d+\.\d+\.\d+) — \d{4}-\d{2}-\d{2}\s*$/m;
23
+
24
+ function checkRelease({ tag, pkgVersion, changelog }) {
25
+ const errors = [];
26
+ if (!/^v\d+\.\d+\.\d+$/.test(tag)) {
27
+ errors.push(`tag "${tag}" is not of the form vX.Y.Z`);
28
+ } else if (tag !== 'v' + pkgVersion) {
29
+ errors.push(`tag ${tag} does not match package.json version ${pkgVersion}`);
30
+ }
31
+ const m = HEADING.exec(changelog);
32
+ if (m === null) {
33
+ errors.push('CHANGELOG.md has no "## vX.Y.Z — YYYY-MM-DD" heading');
34
+ } else if ('v' + m[1] !== tag) {
35
+ errors.push(`CHANGELOG.md top section is v${m[1]}, not ${tag}`);
36
+ }
37
+ return errors;
38
+ }
39
+
40
+ function main(argv) {
41
+ const tag = argv[0];
42
+ if (!tag) { console.error('usage: node scripts/release-check.js <tag> [--on-main]'); return 2; }
43
+ const root = path.join(__dirname, '..');
44
+ const pkgVersion = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).version;
45
+ const changelog = fs.readFileSync(path.join(root, 'CHANGELOG.md'), 'utf8');
46
+ const errors = checkRelease({ tag, pkgVersion, changelog });
47
+ if (argv.includes('--on-main')) {
48
+ try {
49
+ execFileSync('git', ['merge-base', '--is-ancestor', 'HEAD', 'origin/main'], { cwd: root, stdio: 'ignore' });
50
+ } catch (e) {
51
+ errors.push(`the tagged commit is not on origin/main (tag only merge commits on main)`);
52
+ }
53
+ }
54
+ for (const e of errors) console.error('release-check: ' + e);
55
+ if (errors.length === 0) console.log(`release-check: ${tag} ok (package.json ${pkgVersion}, CHANGELOG top section ${tag})`);
56
+ return errors.length === 0 ? 0 : 1;
57
+ }
58
+
59
+ module.exports = { checkRelease };
60
+ if (require.main === module) process.exit(main(process.argv.slice(2)));