@helping-ai-workflow/md2doc 3.13.0 → 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.
@@ -38,6 +38,9 @@
38
38
  }
39
39
  btn.addEventListener('click', function () {
40
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 */ } }
41
44
  if (dark) root.setAttribute('data-md2doc-theme', 'dark'); else root.removeAttribute('data-md2doc-theme');
42
45
  save(dark);
43
46
  paint();
@@ -277,10 +280,208 @@
277
280
  return /^rect$/i.test(el.tagName) && next !== null && /^text$/i.test(next.tagName) &&
278
281
  el.closest('[id^="wavearcs"]') !== null;
279
282
  }
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
+ }
280
480
  function recolourSvg(svg, dark, opts) {
281
481
  var backing = (opts && opts.backing) || 'var(--md-bg)';
282
482
  var els = svg.querySelectorAll('polygon, ellipse, path, polyline, rect, line, circle, text, tspan, foreignObject *');
283
483
  var live = svg.hasAttribute('data-md2doc-live');
484
+ var texts = [];
284
485
  for (var i = 0; i < els.length; i++) {
285
486
  var el = els[i];
286
487
  if (el.hasAttribute('data-md2doc-style')) {
@@ -297,43 +498,94 @@
297
498
  el.setAttribute('data-md2doc-style', el.getAttribute('style') || '');
298
499
  }
299
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; }
300
504
  var cs = getComputedStyle(el);
301
505
  var ov = '';
302
- if (el.namespaceURI === XHTML) {
303
- // Mermaid flowchart labels: HTML inside foreignObject, coloured by CSS.
304
- var fg = rgba(cs.color), bgc = rgba(cs.backgroundColor);
305
- if (fg && fg[3] > 0 && lum(fg) < 0.35) ov += 'color:' + (hsl(fg)[1] > 0.25 ? tone(fg, 0.78, 0.7) : TEXT) + ' !important;';
306
- if (bgc && bgc[3] > 0) {
307
- var BL = lum(bgc);
308
- if (BL > 0.85) ov += 'background-color:transparent !important;';
309
- else if (BL > 0.3) ov += 'background-color:' + tone(bgc, 0.26, 0.55) + ' !important;';
310
- }
311
- if (ov) {
312
- var hb = el.getAttribute('data-md2doc-style');
313
- el.setAttribute('style', (hb ? hb.replace(/;?\s*$/, ';') : '') + ov);
314
- }
315
- continue;
316
- }
317
506
  var fill = rgba(cs.fill), stroke = rgba(cs.stroke);
318
507
  // Overrides are written once, as attribute text, never through el.style:
319
508
  // setProperty leaves style="" on the element after the restore removes
320
509
  // the attribute (Chromium re-serialises it), so light would not be
321
510
  // byte-exact.
322
- if (/^(text|tspan)$/i.test(el.tagName)) {
323
- if (fill && fill[3] > 0 && lum(fill) < 0.35) ov += 'fill:' + (hsl(fill)[1] > 0.25 ? tone(fill, 0.78, 0.7) : TEXT) + ' !important;';
324
- } else {
325
- if (fill && fill[3] > 0) {
326
- var L = lum(fill);
327
- if (L > 0.85) ov += isWaveLabelBacking(el) ? 'fill:' + backing + ' !important;' : 'fill:transparent !important;';
328
- else if (L > 0.3) ov += 'fill:' + tone(fill, 0.26, 0.55) + ' !important;';
329
- 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); }
330
552
  }
331
- if (stroke && stroke[3] > 0 && lum(stroke) < 0.35) ov += 'stroke:' + lineColour(stroke) + ' !important;';
332
553
  }
333
- if (ov) {
334
- var base = el.getAttribute('data-md2doc-style');
335
- 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
+ }
336
587
  }
588
+ restyle(t, tov);
337
589
  }
338
590
  if (dark) svg.setAttribute('data-md2doc-recoloured', ''); else svg.removeAttribute('data-md2doc-recoloured');
339
591
  }
@@ -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;',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helping-ai-workflow/md2doc",
3
- "version": "3.13.0",
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",