@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.
- package/bin/md2doc.js +6 -0
- package/lib/editor/client.js +1015 -194
- package/lib/editor/inline-md.js +45 -1
- package/lib/editor/list-md.js +8 -1
- package/lib/editor/server.js +5 -0
- package/lib/editor/wave-canvas.js +301 -0
- package/lib/editor/wave-codec.js +417 -11
- package/lib/editor/wave-draw.js +209 -1202
- package/lib/editor/wave-geometry.js +145 -10
- package/lib/editor/wave-panels.js +907 -918
- package/lib/editor/wave-store.js +486 -91
- package/lib/editor/wave-ui.js +3588 -2232
- package/lib/md2doc.js +765 -534
- package/lib/theme/runtime.js +296 -29
- package/lib/theme/tokens.js +3 -0
- package/lib/wave-overflow.js +107 -0
- package/package.json +3 -3
- package/scripts/release-check.js +60 -0
package/lib/theme/runtime.js
CHANGED
|
@@ -12,7 +12,16 @@
|
|
|
12
12
|
btn.hidden = false;
|
|
13
13
|
var data = JSON.parse(dataEl.textContent);
|
|
14
14
|
var listeners = [];
|
|
15
|
-
|
|
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
|
|
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 (
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
if (fill
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
})();
|
package/lib/theme/tokens.js
CHANGED
|
@@ -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.
|
|
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/
|
|
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)));
|