@helping-ai-workflow/md2doc 3.7.0 → 3.9.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.
- package/README.md +5 -0
- package/THIRD-PARTY-NOTICES.md +21 -0
- package/bin/md2doc.js +19 -3
- package/lib/editor/client.js +3 -2
- package/lib/md2doc.js +531 -194
- package/lib/theme/runtime.js +268 -0
- package/lib/theme/tokens.js +140 -0
- package/package.json +6 -2
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
// md2doc reader theme toggle (v3.9.0). Inlined into reader HTML by
|
|
2
|
+
// lib/theme/tokens.js#applyReaderTheme — plain browser JS, not part of the
|
|
3
|
+
// lib/md2doc.js template literal. Must never contain a closing script tag.
|
|
4
|
+
// Spec: docs/superpowers/specs/2026-10-02-dark-mode-design.md
|
|
5
|
+
(function () {
|
|
6
|
+
'use strict';
|
|
7
|
+
var KEY = 'md2doc-theme';
|
|
8
|
+
var root = document.documentElement;
|
|
9
|
+
var btn = document.getElementById('md2doc-theme-toggle');
|
|
10
|
+
var dataEl = document.getElementById('md2doc-theme-data');
|
|
11
|
+
if (!btn || !dataEl) return;
|
|
12
|
+
btn.hidden = false;
|
|
13
|
+
var data = JSON.parse(dataEl.textContent);
|
|
14
|
+
var listeners = [];
|
|
15
|
+
function onTheme(fn) { listeners.push(fn); }
|
|
16
|
+
function notify(dark) { for (var i = 0; i < listeners.length; i++) listeners[i](dark); }
|
|
17
|
+
function isDark() { return root.getAttribute('data-md2doc-theme') === 'dark'; }
|
|
18
|
+
function save(dark) {
|
|
19
|
+
try {
|
|
20
|
+
if (dark) localStorage.setItem(KEY, 'dark'); else localStorage.removeItem(KEY);
|
|
21
|
+
} catch (e) { /* storage blocked: the choice lasts for this page only */ }
|
|
22
|
+
}
|
|
23
|
+
function paint() {
|
|
24
|
+
var dark = isDark();
|
|
25
|
+
var label = dark ? 'Dark theme (click for light)' : 'Light theme (click for dark)';
|
|
26
|
+
btn.textContent = dark ? '☾' : '☀';
|
|
27
|
+
btn.setAttribute('aria-label', label);
|
|
28
|
+
btn.title = label;
|
|
29
|
+
}
|
|
30
|
+
btn.addEventListener('click', function () {
|
|
31
|
+
var dark = !isDark();
|
|
32
|
+
if (dark) root.setAttribute('data-md2doc-theme', 'dark'); else root.removeAttribute('data-md2doc-theme');
|
|
33
|
+
save(dark);
|
|
34
|
+
paint();
|
|
35
|
+
notify(dark);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Phone (<= 1080px): the one button moves into the 44px top bar. A document
|
|
39
|
+
// with no headings has no bar, so the button keeps floating there.
|
|
40
|
+
var bar = document.getElementById('mobile-bar');
|
|
41
|
+
var phone = window.matchMedia('(max-width: 1080px)');
|
|
42
|
+
function place() {
|
|
43
|
+
if (bar && phone.matches) { if (btn.parentNode !== bar) bar.appendChild(btn); }
|
|
44
|
+
else if (btn.parentNode !== document.body) document.body.appendChild(btn);
|
|
45
|
+
}
|
|
46
|
+
if (phone.addEventListener) phone.addEventListener('change', place); else if (phone.addListener) phone.addListener(place);
|
|
47
|
+
place();
|
|
48
|
+
|
|
49
|
+
// Print is always light. The dark tokens are @media screen only; the
|
|
50
|
+
// listeners put diagrams back to their light form while printing.
|
|
51
|
+
var printedDark = false;
|
|
52
|
+
window.addEventListener('beforeprint', function () { printedDark = isDark(); if (printedDark) notify(false); });
|
|
53
|
+
window.addEventListener('afterprint', function () { if (printedDark) notify(true); printedDark = false; });
|
|
54
|
+
|
|
55
|
+
paint();
|
|
56
|
+
|
|
57
|
+
// ── mermaid: light and dark each drawn once, then swapped as markup ──
|
|
58
|
+
// The runtime takes over md2doc's own startOnLoad render (mermaid reads
|
|
59
|
+
// startOnLoad from its config at window load), draws light first so print
|
|
60
|
+
// and toggle-back always have it, and draws dark the first time it is needed.
|
|
61
|
+
var mermaids = [].slice.call(document.querySelectorAll('.content .mermaid'));
|
|
62
|
+
var hasMermaid = typeof window.mermaid !== 'undefined' && mermaids.length > 0;
|
|
63
|
+
var rendered = { light: null, dark: null };
|
|
64
|
+
var chain = Promise.resolve();
|
|
65
|
+
var inFlight = 0;
|
|
66
|
+
// Which render the hosts hold. The observer below must not recolour a light
|
|
67
|
+
// render that is shown on purpose (print while dark swaps light back in).
|
|
68
|
+
var mermaidShown = 'light';
|
|
69
|
+
function mermaidConfig(theme) {
|
|
70
|
+
return { startOnLoad: false, theme: 'base', themeVariables: theme === 'dark' ? data.mermaidDark : data.mermaidLight };
|
|
71
|
+
}
|
|
72
|
+
function swapMermaid(theme) {
|
|
73
|
+
mermaidShown = theme;
|
|
74
|
+
mermaids.forEach(function (el, i) { el.innerHTML = rendered[theme][i]; });
|
|
75
|
+
}
|
|
76
|
+
function cached(theme) {
|
|
77
|
+
var r = rendered[theme];
|
|
78
|
+
if (!r) return false;
|
|
79
|
+
for (var i = 0; i < mermaids.length; i++) if (typeof r[i] !== 'string') return false;
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
// Light: mermaid.run over the source-text hosts, which is what shows mermaid's
|
|
83
|
+
// own error box for a broken diagram. run() rejects after one, so the cache is
|
|
84
|
+
// stored either way; otherwise every toggle would redraw everything.
|
|
85
|
+
function drawLight() {
|
|
86
|
+
window.mermaid.initialize(mermaidConfig('light'));
|
|
87
|
+
mermaidShown = 'light';
|
|
88
|
+
mermaids.forEach(function (el) { el.removeAttribute('data-processed'); el.textContent = el.getAttribute('data-md2doc-src'); });
|
|
89
|
+
function store() { rendered.light = mermaids.map(function (el) { return el.innerHTML; }); }
|
|
90
|
+
return window.mermaid.run({ nodes: mermaids }).then(store, function (e) { store(); throw e; });
|
|
91
|
+
}
|
|
92
|
+
// Dark: no host is touched until its own svg is ready, so the page never
|
|
93
|
+
// collapses to source text (WebKit has no scroll anchoring and would lose the
|
|
94
|
+
// reading position). A broken diagram keeps its current markup.
|
|
95
|
+
function drawDark() {
|
|
96
|
+
window.mermaid.initialize(mermaidConfig('dark'));
|
|
97
|
+
mermaidShown = 'dark';
|
|
98
|
+
rendered.dark = [];
|
|
99
|
+
var p = Promise.resolve();
|
|
100
|
+
mermaids.forEach(function (el, i) {
|
|
101
|
+
p = p.then(function () {
|
|
102
|
+
return window.mermaid.render('md2doc-mermaid-dark-' + i, el.getAttribute('data-md2doc-src')).then(function (res) {
|
|
103
|
+
el.innerHTML = res.svg;
|
|
104
|
+
if (res.bindFunctions) res.bindFunctions(el);
|
|
105
|
+
var s = el.querySelector('svg');
|
|
106
|
+
if (s) { s.setAttribute('data-md2doc-recoloured', ''); s.setAttribute('data-md2doc-live', ''); }
|
|
107
|
+
}, function () {
|
|
108
|
+
var stray = document.getElementById('dmd2doc-mermaid-dark-' + i);
|
|
109
|
+
if (stray && stray.parentNode) stray.parentNode.removeChild(stray);
|
|
110
|
+
// The host keeps its light render (mermaid's error box); it was drawn light, so it needs the full recolour.
|
|
111
|
+
var kept = el.querySelector('svg');
|
|
112
|
+
if (kept && !kept.hasAttribute('data-md2doc-live')) recolourSvg(kept, isDark());
|
|
113
|
+
}).then(function () { rendered.dark[i] = el.innerHTML; });
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
return p.then(function () { window.mermaid.initialize(mermaidConfig('light')); });
|
|
117
|
+
}
|
|
118
|
+
function drawMermaid(theme) { return theme === 'dark' ? drawDark() : drawLight(); }
|
|
119
|
+
function showMermaid(dark) {
|
|
120
|
+
if (!hasMermaid) return;
|
|
121
|
+
var theme = dark ? 'dark' : 'light';
|
|
122
|
+
// Synchronous when nothing is drawing and the render is cached: print needs that.
|
|
123
|
+
// Known limit: if beforeprint fires while a dark draw is in flight, the light swap
|
|
124
|
+
// is queued behind it, so that print snapshot can catch the raw mermaid source.
|
|
125
|
+
if (!inFlight && cached(theme)) { swapMermaid(theme); return; }
|
|
126
|
+
inFlight++;
|
|
127
|
+
chain = chain.then(function () { return cached(theme) ? swapMermaid(theme) : drawMermaid(theme); })
|
|
128
|
+
.catch(function () { /* a failed render leaves mermaid's own error box */ })
|
|
129
|
+
.then(function () { inFlight--; });
|
|
130
|
+
}
|
|
131
|
+
if (hasMermaid) {
|
|
132
|
+
mermaids.forEach(function (el) { el.setAttribute('data-md2doc-src', el.textContent); });
|
|
133
|
+
window.mermaid.initialize(mermaidConfig('light'));
|
|
134
|
+
window.addEventListener('load', function () {
|
|
135
|
+
showMermaid(false);
|
|
136
|
+
if (isDark()) showMermaid(true);
|
|
137
|
+
});
|
|
138
|
+
onTheme(showMermaid);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── graphviz / WaveDrom / baked SVG: recolour by computed colour ──
|
|
142
|
+
// Rules: spec "Diagram recolour rules (D10)". Reading COMPUTED fill/stroke
|
|
143
|
+
// covers both graphviz presentation attributes and WaveDrom's own <style>.
|
|
144
|
+
var INK = '#c4c7cc';
|
|
145
|
+
var TEXT = '#e3e3e3';
|
|
146
|
+
// .mermaid svg is always included. A live dark render (data-md2doc-live, set by drawDark)
|
|
147
|
+
// keeps mermaid's own arrowheads, markers and edges: only its labels and node shapes
|
|
148
|
+
// carrying an author style are recoloured. A baked or light-drawn mermaid svg gets the full rules.
|
|
149
|
+
var RECOLOUR = '.content .graphviz svg, .content [id^="WaveDrom_Display_"] svg, .content .wavedrom-diagram svg, .content .mermaid svg';
|
|
150
|
+
var XHTML = 'http://www.w3.org/1999/xhtml';
|
|
151
|
+
// Saturation caps are the spec D10 values: 0.7 for text, 0.8 for lines, 0.55 for fills.
|
|
152
|
+
var probe = document.createElement('canvas').getContext('2d');
|
|
153
|
+
function rgba(c) {
|
|
154
|
+
if (!c || c === 'none' || c.indexOf('url(') === 0) return null;
|
|
155
|
+
var m = /rgba?\(([^)]+)\)/.exec(c);
|
|
156
|
+
if (!m) {
|
|
157
|
+
probe.fillStyle = '#000'; probe.fillStyle = c; c = probe.fillStyle;
|
|
158
|
+
if (c.charAt(0) === '#') return [parseInt(c.slice(1, 3), 16), parseInt(c.slice(3, 5), 16), parseInt(c.slice(5, 7), 16), 1];
|
|
159
|
+
m = /rgba?\(([^)]+)\)/.exec(c);
|
|
160
|
+
if (!m) return null;
|
|
161
|
+
}
|
|
162
|
+
var p = m[1].split(',').map(parseFloat);
|
|
163
|
+
return [p[0], p[1], p[2], p.length > 3 ? p[3] : 1];
|
|
164
|
+
}
|
|
165
|
+
function lum(c) {
|
|
166
|
+
function f(v) { v /= 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); }
|
|
167
|
+
return 0.2126 * f(c[0]) + 0.7152 * f(c[1]) + 0.0722 * f(c[2]);
|
|
168
|
+
}
|
|
169
|
+
function hsl(c) {
|
|
170
|
+
var r = c[0] / 255, g = c[1] / 255, b = c[2] / 255;
|
|
171
|
+
var mx = Math.max(r, g, b), mn = Math.min(r, g, b), d = mx - mn, l = (mx + mn) / 2, h = 0, s = 0;
|
|
172
|
+
if (d) {
|
|
173
|
+
s = l > 0.5 ? d / (2 - mx - mn) : d / (mx + mn);
|
|
174
|
+
h = mx === r ? (g - b) / d + (g < b ? 6 : 0) : mx === g ? (b - r) / d + 2 : (r - g) / d + 4;
|
|
175
|
+
h *= 60;
|
|
176
|
+
}
|
|
177
|
+
return [h, s, l];
|
|
178
|
+
}
|
|
179
|
+
function tone(c, l, smax) {
|
|
180
|
+
var x = hsl(c);
|
|
181
|
+
return 'hsl(' + Math.round(x[0]) + ', ' + Math.round(Math.min(x[1], smax) * 100) + '%, ' + Math.round(l * 100) + '%)';
|
|
182
|
+
}
|
|
183
|
+
function lineColour(c) { return hsl(c)[1] > 0.25 ? tone(c, 0.68, 0.8) : INK; }
|
|
184
|
+
function recolourSvg(svg, dark) {
|
|
185
|
+
var els = svg.querySelectorAll('polygon, ellipse, path, polyline, rect, line, circle, text, tspan, foreignObject *');
|
|
186
|
+
var live = svg.hasAttribute('data-md2doc-live');
|
|
187
|
+
for (var i = 0; i < els.length; i++) {
|
|
188
|
+
var el = els[i];
|
|
189
|
+
if (el.hasAttribute('data-md2doc-style')) {
|
|
190
|
+
var orig = el.getAttribute('data-md2doc-style');
|
|
191
|
+
if (orig) el.setAttribute('style', orig); else el.removeAttribute('style');
|
|
192
|
+
} else {
|
|
193
|
+
if (live && el.namespaceURI !== XHTML) {
|
|
194
|
+
var own = el.getAttribute('style') || '';
|
|
195
|
+
// Only what the author styled: node and subgraph shapes, linkStyle edges, text. Never "any inline style":
|
|
196
|
+
// mermaid writes its own fill and stroke inline (pie legend rects), and markers keep their theme colours.
|
|
197
|
+
if (el.closest('marker') || !/(^|;)\s*(fill|stroke)\s*:/.test(own) ||
|
|
198
|
+
!(el.closest('g.node, g.cluster') || el.matches('.flowchart-link') || /^(text|tspan)$/i.test(el.tagName))) continue;
|
|
199
|
+
}
|
|
200
|
+
el.setAttribute('data-md2doc-style', el.getAttribute('style') || '');
|
|
201
|
+
}
|
|
202
|
+
if (!dark) continue;
|
|
203
|
+
var cs = getComputedStyle(el);
|
|
204
|
+
var ov = '';
|
|
205
|
+
if (el.namespaceURI === XHTML) {
|
|
206
|
+
// Mermaid flowchart labels: HTML inside foreignObject, coloured by CSS.
|
|
207
|
+
var fg = rgba(cs.color), bgc = rgba(cs.backgroundColor);
|
|
208
|
+
if (fg && fg[3] > 0 && lum(fg) < 0.35) ov += 'color:' + (hsl(fg)[1] > 0.25 ? tone(fg, 0.78, 0.7) : TEXT) + ' !important;';
|
|
209
|
+
if (bgc && bgc[3] > 0) {
|
|
210
|
+
var BL = lum(bgc);
|
|
211
|
+
if (BL > 0.85) ov += 'background-color:transparent !important;';
|
|
212
|
+
else if (BL > 0.3) ov += 'background-color:' + tone(bgc, 0.26, 0.55) + ' !important;';
|
|
213
|
+
}
|
|
214
|
+
if (ov) {
|
|
215
|
+
var hb = el.getAttribute('data-md2doc-style');
|
|
216
|
+
el.setAttribute('style', (hb ? hb.replace(/;?\s*$/, ';') : '') + ov);
|
|
217
|
+
}
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
var fill = rgba(cs.fill), stroke = rgba(cs.stroke);
|
|
221
|
+
// Overrides are written once, as attribute text, never through el.style:
|
|
222
|
+
// setProperty leaves style="" on the element after the restore removes
|
|
223
|
+
// the attribute (Chromium re-serialises it), so light would not be
|
|
224
|
+
// byte-exact.
|
|
225
|
+
if (/^(text|tspan)$/i.test(el.tagName)) {
|
|
226
|
+
if (fill && fill[3] > 0 && lum(fill) < 0.35) ov += 'fill:' + (hsl(fill)[1] > 0.25 ? tone(fill, 0.78, 0.7) : TEXT) + ' !important;';
|
|
227
|
+
} else {
|
|
228
|
+
if (fill && fill[3] > 0) {
|
|
229
|
+
var L = lum(fill);
|
|
230
|
+
if (L > 0.85) ov += 'fill:transparent !important;';
|
|
231
|
+
else if (L > 0.3) ov += 'fill:' + tone(fill, 0.26, 0.55) + ' !important;';
|
|
232
|
+
else if (el.closest('g.edge, marker')) ov += 'fill:' + lineColour(fill) + ' !important;';
|
|
233
|
+
}
|
|
234
|
+
if (stroke && stroke[3] > 0 && lum(stroke) < 0.35) ov += 'stroke:' + lineColour(stroke) + ' !important;';
|
|
235
|
+
}
|
|
236
|
+
if (ov) {
|
|
237
|
+
var base = el.getAttribute('data-md2doc-style');
|
|
238
|
+
el.setAttribute('style', (base ? base.replace(/;?\s*$/, ';') : '') + ov);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (dark) svg.setAttribute('data-md2doc-recoloured', ''); else svg.removeAttribute('data-md2doc-recoloured');
|
|
242
|
+
}
|
|
243
|
+
function recolourAll(dark) {
|
|
244
|
+
var svgs = document.querySelectorAll(RECOLOUR);
|
|
245
|
+
for (var i = 0; i < svgs.length; i++) recolourSvg(svgs[i], dark);
|
|
246
|
+
}
|
|
247
|
+
onTheme(recolourAll);
|
|
248
|
+
// WaveDrom draws again at DOMContentLoaded, load, +250ms and +1000ms; watch
|
|
249
|
+
// for inserted svgs instead of copying that schedule (same reasoning as
|
|
250
|
+
// applyDiagramScale's observer in lib/md2doc.js).
|
|
251
|
+
var content = document.querySelector('.content');
|
|
252
|
+
if (content && typeof MutationObserver === 'function') {
|
|
253
|
+
new MutationObserver(function (records) {
|
|
254
|
+
if (!isDark()) return;
|
|
255
|
+
for (var r = 0; r < records.length; r++) {
|
|
256
|
+
var added = records[r].addedNodes;
|
|
257
|
+
for (var a = 0; a < added.length; a++) {
|
|
258
|
+
var node = added[a];
|
|
259
|
+
if (!node || node.nodeType !== 1) continue;
|
|
260
|
+
if (mermaidShown === 'light' && node.closest && node.closest('.mermaid')) continue;
|
|
261
|
+
var svgs = node.matches && node.matches(RECOLOUR) ? [node] : (node.querySelectorAll ? node.querySelectorAll(RECOLOUR) : []);
|
|
262
|
+
for (var k = 0; k < svgs.length; k++) recolourSvg(svgs[k], true);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}).observe(content, { childList: true, subtree: true });
|
|
266
|
+
}
|
|
267
|
+
if (isDark()) recolourAll(true);
|
|
268
|
+
})();
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
// v3.9.0 dark mode: the reader's colour table and the CSS that applies it.
|
|
5
|
+
// Spec: docs/superpowers/specs/2026-10-02-dark-mode-design.md
|
|
6
|
+
//
|
|
7
|
+
// applyReaderTheme() (added in Task 2) hands the reader <style> block to
|
|
8
|
+
// applyThemeTokens(). Every colour literal that has a role below becomes
|
|
9
|
+
// var(--md-<name>), and :root defines each var as that SAME literal, so light
|
|
10
|
+
// rendering is exactly what it was. Dark redefines the vars inside
|
|
11
|
+
// @media screen only, which is what keeps print light.
|
|
12
|
+
|
|
13
|
+
const THEME_TOKENS = [
|
|
14
|
+
{ name: 'bg', light: ['#ffffff', '#fff'], dark: '#1b1b1d' },
|
|
15
|
+
{ name: 'panel', light: ['#f3f5f7'], dark: '#232325' },
|
|
16
|
+
{ name: 'surface', light: ['#f6f8fa'], dark: '#2a2a2d' },
|
|
17
|
+
{ name: 'surface-2', light: ['#fafbfc'], dark: '#2a2a2d' },
|
|
18
|
+
{ name: 'fg', light: ['#24292e'], dark: '#e3e3e3' },
|
|
19
|
+
{ name: 'strong', light: ['#1f2328'], dark: '#f5f6f7' },
|
|
20
|
+
{ name: 'muted', light: ['#57606a'], dark: '#a3a6ab' },
|
|
21
|
+
{ name: 'muted-2', light: ['#59636e'], dark: '#a3a6ab' },
|
|
22
|
+
{ name: 'subtle', light: ['#6a737d'], dark: '#a3a6ab' },
|
|
23
|
+
{ name: 'rule', light: ['#e1e4e8'], dark: '#38393c' },
|
|
24
|
+
{ name: 'rule-2', light: ['#d8dee4'], dark: '#38393c' },
|
|
25
|
+
{ name: 'rule-3', light: ['#dfe2e5'], dark: '#38393c' },
|
|
26
|
+
{ name: 'rule-strong', light: ['#c9ced4'], dark: '#505257' },
|
|
27
|
+
{ name: 'border', light: ['#d0d7de'], dark: '#46484c' },
|
|
28
|
+
{ name: 'hover', light: ['#e3e8ee'], dark: '#303134' },
|
|
29
|
+
{ name: 'hover-2', light: ['#e8edf2'], dark: '#303134' },
|
|
30
|
+
{ name: 'hover-3', light: ['#eef2f6'], dark: '#303134' },
|
|
31
|
+
{ name: 'accent', light: ['#0969da'], dark: '#6ea8f5' },
|
|
32
|
+
{ name: 'active-fg', light: ['#0550ae'], dark: '#a5c9f8' },
|
|
33
|
+
{ name: 'active-fg-2', light: ['#0b57d0'], dark: '#a5c9f8' },
|
|
34
|
+
{ name: 'active-bg', light: ['#dbe6f3'], dark: '#24364f' },
|
|
35
|
+
{ name: 'hit-bg', light: ['#fff5c2'], dark: '#4d4417' },
|
|
36
|
+
{ name: 'mark-bg', light: ['#fde68a'], dark: '#6e5e14' },
|
|
37
|
+
{ name: 'code', light: ['#b93a0c'], dark: '#ff9f70' },
|
|
38
|
+
{ name: 'accent-soft', light: ['#93c5fd'], dark: '#6ea8f5' },
|
|
39
|
+
{ name: 'error', light: ['#b91c1c'], dark: '#ff8a8a' },
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
// Literals in reader rules that deliberately stay literal. Measured on real
|
|
43
|
+
// output (mac-tx-core design doc, 2026-10-02): '#000' is the TOC edge-fade
|
|
44
|
+
// mask (alpha only, never painted); '#dbeafe' is the draw.io sheet-bar
|
|
45
|
+
// pressed button, which sits inside the white draw.io plate in dark.
|
|
46
|
+
const KEEP_LITERALS = ['#000', '#dbeafe'];
|
|
47
|
+
|
|
48
|
+
// Content links: today they use the browser default, which stays in light.
|
|
49
|
+
const LINK_DARK = '#79b0f6';
|
|
50
|
+
|
|
51
|
+
// Rules whose literals stay literal: the editor's own UI (edit mode is light
|
|
52
|
+
// only) and the lightbox, which is a dark overlay already.
|
|
53
|
+
const SKIP_SELECTOR = /(^|[\s,>+~(])(html\.ed-|\.ed-|\.lightbox)/;
|
|
54
|
+
|
|
55
|
+
const MERMAID_LIGHT_VARS = {
|
|
56
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Arial, "Microsoft JhengHei", sans-serif',
|
|
57
|
+
primaryColor: '#eaf2fd', primaryBorderColor: '#0969da', primaryTextColor: '#1f2328', lineColor: '#57606a',
|
|
58
|
+
secondaryColor: '#f6f8fa', tertiaryColor: '#ffffff', edgeLabelBackground: '#ffffff',
|
|
59
|
+
};
|
|
60
|
+
const MERMAID_DARK_VARS = Object.assign({}, MERMAID_LIGHT_VARS, {
|
|
61
|
+
primaryColor: '#24364f', primaryBorderColor: '#6ea8f5', primaryTextColor: '#f5f6f7', lineColor: '#a3a6ab',
|
|
62
|
+
secondaryColor: '#2a2a2d', tertiaryColor: '#1b1b1d', edgeLabelBackground: '#1b1b1d',
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const LITERAL_TO_TOKEN = new Map();
|
|
66
|
+
for (const t of THEME_TOKENS) for (const l of t.light) LITERAL_TO_TOKEN.set(l, t.name);
|
|
67
|
+
|
|
68
|
+
// Line-based on purpose: the reader CSS is one declaration per line or one
|
|
69
|
+
// rule per line, and a line-based pass leaves comment prose (which names
|
|
70
|
+
// colours) alone. `selector` is the text before the last '{' seen.
|
|
71
|
+
function applyThemeTokens(css) {
|
|
72
|
+
let selector = '';
|
|
73
|
+
return css.split('\n').map((line) => {
|
|
74
|
+
const brace = line.indexOf('{');
|
|
75
|
+
if (brace !== -1) selector = line.slice(0, brace);
|
|
76
|
+
if (/^\s*(\/\*|\*)/.test(line)) return line;
|
|
77
|
+
if (SKIP_SELECTOR.test(selector)) return line;
|
|
78
|
+
return line.replace(/#[0-9a-fA-F]{3,6}\b/g, (m) => {
|
|
79
|
+
const name = LITERAL_TO_TOKEN.get(m.toLowerCase());
|
|
80
|
+
return name ? 'var(--md-' + name + ')' : m;
|
|
81
|
+
});
|
|
82
|
+
}).join('\n');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function themeCss() {
|
|
86
|
+
const D = ':root[data-md2doc-theme="dark"]';
|
|
87
|
+
const light = THEME_TOKENS.map((t) => '--md-' + t.name + ': ' + t.light[0] + ';').join(' ');
|
|
88
|
+
const dark = THEME_TOKENS.map((t) => '--md-' + t.name + ': ' + t.dark + ';').join(' ');
|
|
89
|
+
return [
|
|
90
|
+
' /* v3.9.0 dark mode (lib/theme/tokens.js). Light values are the literals they replace. */',
|
|
91
|
+
' :root { ' + light + ' }',
|
|
92
|
+
' @media screen {',
|
|
93
|
+
' ' + D + ' { ' + dark + ' color-scheme: dark; }',
|
|
94
|
+
' ' + D + ' .content a:not(.heading-anchor), ' + D + ' .content a:not(.heading-anchor):visited { color: ' + LINK_DARK + '; }',
|
|
95
|
+
' ' + D + ' .content img { background: #ffffff; border-radius: 4px; box-shadow: 0 0 0 10px #ffffff; }',
|
|
96
|
+
' ' + D + ' .content .drawio { background: #ffffff; border-radius: 6px; box-shadow: 0 0 0 10px #ffffff; }',
|
|
97
|
+
' ' + D + ' .lightbox-canvas > svg[data-md2doc-recoloured] { background: var(--md-bg); }',
|
|
98
|
+
' }',
|
|
99
|
+
' .md2doc-theme-toggle { position: fixed; right: 20px; bottom: 20px; z-index: 60; width: 40px; height: 40px;',
|
|
100
|
+
' border-radius: 50%; display: grid; place-items: center; padding: 0; font-size: 18px; line-height: 1;',
|
|
101
|
+
' cursor: pointer; border: 1px solid var(--md-border); background: var(--md-bg); color: var(--md-strong);',
|
|
102
|
+
' box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18); }',
|
|
103
|
+
' .md2doc-theme-toggle[hidden] { display: none; }',
|
|
104
|
+
' .md2doc-theme-toggle:hover { background: var(--md-hover); }',
|
|
105
|
+
' .md2doc-theme-toggle:focus-visible { outline: 2px solid var(--md-accent); outline-offset: 2px; }',
|
|
106
|
+
' .mobile-bar .md2doc-theme-toggle { position: static; margin-left: auto; flex: 0 0 auto; width: 36px; height: 36px;',
|
|
107
|
+
' box-shadow: none; font-size: 17px; }',
|
|
108
|
+
' @media print { .md2doc-theme-toggle { display: none !important; } }',
|
|
109
|
+
].join('\n');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
// Runs before any stylesheet, so a saved dark choice never flashes light.
|
|
114
|
+
const EARLY_SCRIPT = "<script>try{if(localStorage.getItem('md2doc-theme')==='dark')document.documentElement.setAttribute('data-md2doc-theme','dark');}catch(e){}</script>";
|
|
115
|
+
const TOGGLE_HTML = '<button type="button" class="md2doc-theme-toggle" hidden id="md2doc-theme-toggle" aria-label="Light theme (click for dark)" title="Light theme (click for dark)">☀</button>';
|
|
116
|
+
const RUNTIME_JS = fs.readFileSync(path.join(__dirname, 'runtime.js'), 'utf8');
|
|
117
|
+
|
|
118
|
+
// The reader <style> is the first one after </title>; KaTeX's own <style>
|
|
119
|
+
// comes after it, so "last </style> before </head>" is the wrong anchor here.
|
|
120
|
+
function applyReaderTheme(html) {
|
|
121
|
+
const titleEnd = html.indexOf('</title>');
|
|
122
|
+
const start = titleEnd < 0 ? -1 : html.indexOf('<style>', titleEnd);
|
|
123
|
+
const end = start < 0 ? -1 : html.indexOf('</style>', start);
|
|
124
|
+
if (end < 0) throw new Error('applyReaderTheme: reader <style> block not found');
|
|
125
|
+
let out = html.slice(0, start) + applyThemeTokens(html.slice(start, end)) + '\n' + themeCss() + '\n' + html.slice(end);
|
|
126
|
+
const head = out.indexOf('<head>\n');
|
|
127
|
+
if (head < 0) throw new Error('applyReaderTheme: <head> not found');
|
|
128
|
+
out = out.slice(0, head + 7) + EARLY_SCRIPT + '\n' + out.slice(head + 7);
|
|
129
|
+
// lastIndexOf: inlined diagram libraries carry '</body>' inside strings.
|
|
130
|
+
const bodyEnd = out.lastIndexOf('</body>');
|
|
131
|
+
if (bodyEnd < 0) throw new Error('applyReaderTheme: </body> not found');
|
|
132
|
+
const data = JSON.stringify({ mermaidLight: MERMAID_LIGHT_VARS, mermaidDark: MERMAID_DARK_VARS });
|
|
133
|
+
return out.slice(0, bodyEnd) + TOGGLE_HTML + '\n<script id="md2doc-theme-data" type="application/json">' + data +
|
|
134
|
+
'</script>\n<script>\n' + RUNTIME_JS + '\n</script>\n' + out.slice(bodyEnd);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = {
|
|
138
|
+
THEME_TOKENS, KEEP_LITERALS, LINK_DARK, MERMAID_LIGHT_VARS, MERMAID_DARK_VARS,
|
|
139
|
+
applyThemeTokens, themeCss, applyReaderTheme,
|
|
140
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@helping-ai-workflow/md2doc",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.9.0",
|
|
4
4
|
"description": "Markdown → HTML / PDF renderer with WaveDrom, Mermaid, Graphviz, and draw.io support",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"markdown",
|
|
@@ -45,7 +45,8 @@
|
|
|
45
45
|
},
|
|
46
46
|
"scripts": {
|
|
47
47
|
"preinstall": "node scripts/preinstall.js",
|
|
48
|
-
"test": "node test/md2doc.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/docsource.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"
|
|
48
|
+
"test": "node test/md2doc.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/docsource.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",
|
|
49
|
+
"test:browser": "node test/reader-click.test.js"
|
|
49
50
|
},
|
|
50
51
|
"repository": {
|
|
51
52
|
"type": "git",
|
|
@@ -57,5 +58,8 @@
|
|
|
57
58
|
"license": "MIT AND Apache-2.0",
|
|
58
59
|
"publishConfig": {
|
|
59
60
|
"access": "public"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"playwright": "1.63.0"
|
|
60
64
|
}
|
|
61
65
|
}
|