Commit 516a684 h
Pat Ambrosio · 26-08-28 22:31
Redraw diagrams when the theme changes
Changes: 3 files
+111
-10
Changes (Side-by-Side)
share/jenny/layout/page.sh +42 -4
diagram_script() { |
| 116 |
// The page theme is a CSS custom property set, and mermaid needs |
116 |
// The page theme is a CSS custom property set, and mermaid needs |
| 117 |
// real values at init. Reading them from the document keeps one |
117 |
// real values at init. Reading them from the document keeps one |
| 118 |
// definition of the palette rather than a second copy here. |
118 |
// definition of the palette rather than a second copy here. |
| 119 |
var cs = getComputedStyle(document.documentElement); |
| |
| 120 |
var v = function(n, fallback) { |
119 |
var v = function(n, fallback) { |
| 121 |
var got = cs.getPropertyValue(n); |
| |
| |
120 |
// Read at call time, not once into a closure: the palette changes |
| |
121 |
// when the theme does, and a cached getComputedStyle would keep |
| |
122 |
// handing back the values from first paint. |
| |
123 |
var got = getComputedStyle(document.documentElement).getPropertyValue(n); |
| 122 |
return (got && got.trim()) || fallback; |
124 |
return (got && got.trim()) || fallback; |
| 123 |
}; |
125 |
}; |
| |
126 |
var SEL = '[class*="diagram-mermaid"]'; |
| |
127 |
// mermaid replaces the source text with the svg it renders, so the |
| |
128 |
// source has to be kept if the diagram is ever to be drawn again. |
| |
129 |
var sources = []; |
| |
130 |
document.querySelectorAll(SEL).forEach(function(el, i) { |
| |
131 |
el.setAttribute('data-diagram-index', i); |
| |
132 |
sources[i] = el.textContent; |
| |
133 |
}); |
| |
134 |
var configure = function() { |
| 124 |
mermaid.initialize({ |
135 |
mermaid.initialize({ |
| 125 |
// The renderer scans for its own class name, and the parser |
136 |
// The renderer scans for its own class name, and the parser |
| 126 |
// emits a neutral "diagram" class so the markup does not name |
137 |
// emits a neutral "diagram" class so the markup does not name |
diagram_script() { |
| 131 |
// names, so '.diagram-mermaid' never matched |
142 |
// names, so '.diagram-mermaid' never matched |
| 132 |
// 'diagram-mermaid-legend' and a legend block sat unrendered, |
143 |
// 'diagram-mermaid-legend' and a legend block sat unrendered, |
| 133 |
// showing its own source and overflowing the page by 3px. |
144 |
// showing its own source and overflowing the page by 3px. |
| 134 |
querySelector: '[class*="diagram-mermaid"]', |
| |
| |
145 |
querySelector: SEL, |
| 135 |
startOnLoad: false, |
146 |
startOnLoad: false, |
| 136 |
securityLevel: 'strict', |
147 |
securityLevel: 'strict', |
| 137 |
theme: 'base', |
148 |
theme: 'base', |
diagram_script() { |
| 144 |
fontFamily: 'Roboto, sans-serif' |
155 |
fontFamily: 'Roboto, sans-serif' |
| 145 |
} |
156 |
} |
| 146 |
}); |
157 |
}); |
| 147 |
mermaid.run({ querySelector: '[class*="diagram-mermaid"]' }); |
| |
| |
158 |
}; |
| |
159 |
var render = function() { |
| |
160 |
configure(); |
| |
161 |
mermaid.run({ querySelector: SEL }); |
| |
162 |
}; |
| |
163 |
render(); |
| |
164 |
|
| |
165 |
// mermaid resolves the palette into the svg it writes, so a diagram |
| |
166 |
// drawn under one theme keeps that theme's colours after a switch: |
| |
167 |
// dark node fills and pale label text sat on the light background, |
| |
168 |
// because only the CSS variables had changed and the svg had not. |
| |
169 |
// Restoring the source and drawing again is the only way to get the |
| |
170 |
// new palette into it -- there is nothing in the rendered svg to |
| |
171 |
// recolour selectively. |
| |
172 |
var rerender = function() { |
| |
173 |
document.querySelectorAll('[data-diagram-index]').forEach(function(el) { |
| |
174 |
var i = el.getAttribute('data-diagram-index'); |
| |
175 |
if (sources[i] === undefined) return; |
| |
176 |
el.removeAttribute('data-processed'); |
| |
177 |
el.textContent = sources[i]; |
| |
178 |
}); |
| |
179 |
render(); |
| |
180 |
}; |
| |
181 |
new MutationObserver(function(records) { |
| |
182 |
for (var i = 0; i < records.length; i++) { |
| |
183 |
if (records[i].attributeName === 'data-theme') { rerender(); return; } |
| |
184 |
} |
| |
185 |
}).observe(document.documentElement, { attributes: true }); |
| 148 |
})(); |
186 |
})(); |
| 149 |
</script> |
187 |
</script> |
| 150 |
<script> |
188 |
<script> |
share/jenny/layout/post.sh +42 -4
diagram_script() { |
| 32 |
// The page theme is a CSS custom property set, and mermaid needs |
32 |
// The page theme is a CSS custom property set, and mermaid needs |
| 33 |
// real values at init. Reading them from the document keeps one |
33 |
// real values at init. Reading them from the document keeps one |
| 34 |
// definition of the palette rather than a second copy here. |
34 |
// definition of the palette rather than a second copy here. |
| 35 |
var cs = getComputedStyle(document.documentElement); |
| |
| 36 |
var v = function(n, fallback) { |
35 |
var v = function(n, fallback) { |
| 37 |
var got = cs.getPropertyValue(n); |
| |
| |
36 |
// Read at call time, not once into a closure: the palette changes |
| |
37 |
// when the theme does, and a cached getComputedStyle would keep |
| |
38 |
// handing back the values from first paint. |
| |
39 |
var got = getComputedStyle(document.documentElement).getPropertyValue(n); |
| 38 |
return (got && got.trim()) || fallback; |
40 |
return (got && got.trim()) || fallback; |
| 39 |
}; |
41 |
}; |
| |
42 |
var SEL = '[class*="diagram-mermaid"]'; |
| |
43 |
// mermaid replaces the source text with the svg it renders, so the |
| |
44 |
// source has to be kept if the diagram is ever to be drawn again. |
| |
45 |
var sources = []; |
| |
46 |
document.querySelectorAll(SEL).forEach(function(el, i) { |
| |
47 |
el.setAttribute('data-diagram-index', i); |
| |
48 |
sources[i] = el.textContent; |
| |
49 |
}); |
| |
50 |
var configure = function() { |
| 40 |
mermaid.initialize({ |
51 |
mermaid.initialize({ |
| 41 |
// The renderer scans for its own class name, and the parser |
52 |
// The renderer scans for its own class name, and the parser |
| 42 |
// emits a neutral "diagram" class so the markup does not name |
53 |
// emits a neutral "diagram" class so the markup does not name |
diagram_script() { |
| 47 |
// names, so '.diagram-mermaid' never matched |
58 |
// names, so '.diagram-mermaid' never matched |
| 48 |
// 'diagram-mermaid-legend' and a legend block sat unrendered, |
59 |
// 'diagram-mermaid-legend' and a legend block sat unrendered, |
| 49 |
// showing its own source and overflowing the page by 3px. |
60 |
// showing its own source and overflowing the page by 3px. |
| 50 |
querySelector: '[class*="diagram-mermaid"]', |
| |
| |
61 |
querySelector: SEL, |
| 51 |
startOnLoad: false, |
62 |
startOnLoad: false, |
| 52 |
securityLevel: 'strict', |
63 |
securityLevel: 'strict', |
| 53 |
theme: 'base', |
64 |
theme: 'base', |
diagram_script() { |
| 60 |
fontFamily: 'Roboto, sans-serif' |
71 |
fontFamily: 'Roboto, sans-serif' |
| 61 |
} |
72 |
} |
| 62 |
}); |
73 |
}); |
| 63 |
mermaid.run({ querySelector: '[class*="diagram-mermaid"]' }); |
| |
| |
74 |
}; |
| |
75 |
var render = function() { |
| |
76 |
configure(); |
| |
77 |
mermaid.run({ querySelector: SEL }); |
| |
78 |
}; |
| |
79 |
render(); |
| |
80 |
|
| |
81 |
// mermaid resolves the palette into the svg it writes, so a diagram |
| |
82 |
// drawn under one theme keeps that theme's colours after a switch: |
| |
83 |
// dark node fills and pale label text sat on the light background, |
| |
84 |
// because only the CSS variables had changed and the svg had not. |
| |
85 |
// Restoring the source and drawing again is the only way to get the |
| |
86 |
// new palette into it -- there is nothing in the rendered svg to |
| |
87 |
// recolour selectively. |
| |
88 |
var rerender = function() { |
| |
89 |
document.querySelectorAll('[data-diagram-index]').forEach(function(el) { |
| |
90 |
var i = el.getAttribute('data-diagram-index'); |
| |
91 |
if (sources[i] === undefined) return; |
| |
92 |
el.removeAttribute('data-processed'); |
| |
93 |
el.textContent = sources[i]; |
| |
94 |
}); |
| |
95 |
render(); |
| |
96 |
}; |
| |
97 |
new MutationObserver(function(records) { |
| |
98 |
for (var i = 0; i < records.length; i++) { |
| |
99 |
if (records[i].attributeName === 'data-theme') { rerender(); return; } |
| |
100 |
} |
| |
101 |
}).observe(document.documentElement, { attributes: true }); |
| 64 |
})(); |
102 |
})(); |
| 65 |
</script> |
103 |
</script> |
| 66 |
<script> |
104 |
<script> |
test/regressions.sh +27 -2
it "the renderer matches both mermaid dialects" |
| 1315 |
# matched 'diagram-mermaid-legend'. A legend block sat unrendered, showing its |
1315 |
# matched 'diagram-mermaid-legend'. A legend block sat unrendered, showing its |
| 1316 |
# own source text and pushing the page 3px wider than the viewport -- found |
1316 |
# own source text and pushing the page 3px wider than the viewport -- found |
| 1317 |
# when a real document with a legend was published. |
1317 |
# when a real document with a legend was published. |
| 1318 |
assert_file_contains "$dg/.dist/chart.html" "querySelector: '[class*=\"diagram-mermaid\"]'" |
| |
| 1319 |
assert_file_not_contains "$dg/.dist/chart.html" "querySelector: '.diagram-mermaid'" |
| |
| |
1318 |
# The selector is now bound once to SEL and used for the first render, the |
| |
1319 |
# redraw after a theme change, and the modal, so the assertion follows it to |
| |
1320 |
# its definition. The attribute-substring form is what matters, not where the |
| |
1321 |
# literal sits. |
| |
1322 |
assert_file_contains "$dg/.dist/chart.html" "var SEL = '[class*=\"diagram-mermaid\"]';" |
| |
1323 |
assert_file_not_contains "$dg/.dist/chart.html" "var SEL = '.diagram-mermaid';" |
| 1320 |
|
1324 |
|
| 1321 |
it "the renderer is pointed at the class the parser emits" |
1325 |
it "the renderer is pointed at the class the parser emits" |
| 1322 |
# The markup uses a neutral "diagram" class rather than naming one renderer, |
1326 |
# The markup uses a neutral "diagram" class rather than naming one renderer, |
it "the body's repeat of the page title carries no rule" |
| 1393 |
assert_file_contains "$dg/.dist/chart.html" '.contents h1:not(:is(h1, h2) ~ *)::before { content: none; }' |
1397 |
assert_file_contains "$dg/.dist/chart.html" '.contents h1:not(:is(h1, h2) ~ *)::before { content: none; }' |
| 1394 |
assert_file_contains "$dg/.dist/post/charted.html" '.contents h1:not(:is(h1, h2) ~ *)::before { content: none; }' |
1398 |
assert_file_contains "$dg/.dist/post/charted.html" '.contents h1:not(:is(h1, h2) ~ *)::before { content: none; }' |
| 1395 |
|
1399 |
|
| |
1400 |
it "diagram sources are kept so the theme can be changed" |
| |
1401 |
# mermaid resolves the palette into the svg it writes and then replaces the |
| |
1402 |
# source text with that svg. A diagram drawn under one theme therefore keeps |
| |
1403 |
# that theme's colours after a switch -- dark node fills and pale labels on a |
| |
1404 |
# light page -- because only the CSS variables changed. There is nothing in the |
| |
1405 |
# rendered svg to recolour selectively, so the source is stashed first and the |
| |
1406 |
# diagram is drawn again. |
| |
1407 |
assert_file_contains "$dg/.dist/chart.html" "sources[i] = el.textContent;" |
| |
1408 |
assert_file_contains "$dg/.dist/post/charted.html" "sources[i] = el.textContent;" |
| |
1409 |
|
| |
1410 |
it "a theme change redraws the diagrams" |
| |
1411 |
# The observer watches data-theme on the root, which is what the theme script |
| |
1412 |
# sets. Verified in Chrome across four toggles: node fill tracked the theme |
| |
1413 |
# every time and all three diagrams survived. |
| |
1414 |
assert_file_contains "$dg/.dist/chart.html" "records[i].attributeName === 'data-theme'" |
| |
1415 |
|
| |
1416 |
it "the palette is read at render time, not once at load" |
| |
1417 |
# getComputedStyle cached into a closure would keep handing back the values |
| |
1418 |
# from first paint, so the redraw would repaint the diagram in the old colours. |
| |
1419 |
assert_file_contains "$dg/.dist/chart.html" "getComputedStyle(document.documentElement).getPropertyValue(n)" |
| |
1420 |
|
| 1396 |
it "an unrendered diagram degrades to readable source" |
1421 |
it "an unrendered diagram degrades to readable source" |
| 1397 |
# If the script is absent or blocked, the element still holds what the author |
1422 |
# If the script is absent or blocked, the element still holds what the author |
| 1398 |
# wrote. Shown as the code it is rather than as a paragraph with the newlines |
1423 |
# wrote. Shown as the code it is rather than as a paragraph with the newlines |
Raw
commit 516a6841936f0d6239ed2ed0fa9a3a1033afd965
Author: Pat Ambrosio <cp.ambrosio@gmail.com>
Date: Fri Aug 28 22:31:43 2026 +0800
Redraw diagrams when the theme changes
mermaid resolves the palette into the svg it writes and then replaces the
source text with that svg. A diagram drawn under one theme therefore kept
that theme's colours after a switch: dark node fills and near-white
labels sitting on a white page, because only the CSS variables had
changed and the rendered svg had not. There is nothing in the finished
svg to recolour selectively, so the fix is to draw it again.
Three parts. The source is stashed before the first render, since mermaid
destroys it. The palette is read at call time rather than cached into a
closure -- a cached getComputedStyle keeps handing back the values from
first paint, so the redraw would have repainted the old colours. A
MutationObserver on data-theme, which is what the theme script sets,
restores the sources and renders again.
Measured in Chrome over four consecutive toggles: node fill tracked the
theme every time, all three diagrams survived, and the full-screen modal
still opens and clones the svg in the current theme. The modal's handlers
sit on the container rather than the svg, so a redraw does not detach
them.
The selector is bound once to SEL now that three call sites share it. Its
value is unchanged; a test that pinned the literal at the querySelector
line follows it to the definition, and still refuses '.diagram-mermaid',
which is the form that once failed to match 'diagram-mermaid-legend'.
diff --git a/share/jenny/layout/page.sh b/share/jenny/layout/page.sh
index 1b5b78e..5530259 100755
--- a/share/jenny/layout/page.sh
+++ b/share/jenny/layout/page.sh
@@ -116,11 +116,22 @@ diagram_script() {
// The page theme is a CSS custom property set, and mermaid needs
// real values at init. Reading them from the document keeps one
// definition of the palette rather than a second copy here.
- var cs = getComputedStyle(document.documentElement);
var v = function(n, fallback) {
- var got = cs.getPropertyValue(n);
+ // Read at call time, not once into a closure: the palette changes
+ // when the theme does, and a cached getComputedStyle would keep
+ // handing back the values from first paint.
+ var got = getComputedStyle(document.documentElement).getPropertyValue(n);
return (got && got.trim()) || fallback;
};
+ var SEL = '[class*="diagram-mermaid"]';
+ // mermaid replaces the source text with the svg it renders, so the
+ // source has to be kept if the diagram is ever to be drawn again.
+ var sources = [];
+ document.querySelectorAll(SEL).forEach(function(el, i) {
+ el.setAttribute('data-diagram-index', i);
+ sources[i] = el.textContent;
+ });
+ var configure = function() {
mermaid.initialize({
// The renderer scans for its own class name, and the parser
// emits a neutral "diagram" class so the markup does not name
@@ -131,7 +142,7 @@ diagram_script() {
// names, so '.diagram-mermaid' never matched
// 'diagram-mermaid-legend' and a legend block sat unrendered,
// showing its own source and overflowing the page by 3px.
- querySelector: '[class*="diagram-mermaid"]',
+ querySelector: SEL,
startOnLoad: false,
securityLevel: 'strict',
theme: 'base',
@@ -144,7 +155,34 @@ diagram_script() {
fontFamily: 'Roboto, sans-serif'
}
});
- mermaid.run({ querySelector: '[class*="diagram-mermaid"]' });
+ };
+ var render = function() {
+ configure();
+ mermaid.run({ querySelector: SEL });
+ };
+ render();
+
+ // mermaid resolves the palette into the svg it writes, so a diagram
+ // drawn under one theme keeps that theme's colours after a switch:
+ // dark node fills and pale label text sat on the light background,
+ // because only the CSS variables had changed and the svg had not.
+ // Restoring the source and drawing again is the only way to get the
+ // new palette into it -- there is nothing in the rendered svg to
+ // recolour selectively.
+ var rerender = function() {
+ document.querySelectorAll('[data-diagram-index]').forEach(function(el) {
+ var i = el.getAttribute('data-diagram-index');
+ if (sources[i] === undefined) return;
+ el.removeAttribute('data-processed');
+ el.textContent = sources[i];
+ });
+ render();
+ };
+ new MutationObserver(function(records) {
+ for (var i = 0; i < records.length; i++) {
+ if (records[i].attributeName === 'data-theme') { rerender(); return; }
+ }
+ }).observe(document.documentElement, { attributes: true });
})();
</script>
<script>
diff --git a/share/jenny/layout/post.sh b/share/jenny/layout/post.sh
index ebbda04..e917096 100755
--- a/share/jenny/layout/post.sh
+++ b/share/jenny/layout/post.sh
@@ -32,11 +32,22 @@ diagram_script() {
// The page theme is a CSS custom property set, and mermaid needs
// real values at init. Reading them from the document keeps one
// definition of the palette rather than a second copy here.
- var cs = getComputedStyle(document.documentElement);
var v = function(n, fallback) {
- var got = cs.getPropertyValue(n);
+ // Read at call time, not once into a closure: the palette changes
+ // when the theme does, and a cached getComputedStyle would keep
+ // handing back the values from first paint.
+ var got = getComputedStyle(document.documentElement).getPropertyValue(n);
return (got && got.trim()) || fallback;
};
+ var SEL = '[class*="diagram-mermaid"]';
+ // mermaid replaces the source text with the svg it renders, so the
+ // source has to be kept if the diagram is ever to be drawn again.
+ var sources = [];
+ document.querySelectorAll(SEL).forEach(function(el, i) {
+ el.setAttribute('data-diagram-index', i);
+ sources[i] = el.textContent;
+ });
+ var configure = function() {
mermaid.initialize({
// The renderer scans for its own class name, and the parser
// emits a neutral "diagram" class so the markup does not name
@@ -47,7 +58,7 @@ diagram_script() {
// names, so '.diagram-mermaid' never matched
// 'diagram-mermaid-legend' and a legend block sat unrendered,
// showing its own source and overflowing the page by 3px.
- querySelector: '[class*="diagram-mermaid"]',
+ querySelector: SEL,
startOnLoad: false,
securityLevel: 'strict',
theme: 'base',
@@ -60,7 +71,34 @@ diagram_script() {
fontFamily: 'Roboto, sans-serif'
}
});
- mermaid.run({ querySelector: '[class*="diagram-mermaid"]' });
+ };
+ var render = function() {
+ configure();
+ mermaid.run({ querySelector: SEL });
+ };
+ render();
+
+ // mermaid resolves the palette into the svg it writes, so a diagram
+ // drawn under one theme keeps that theme's colours after a switch:
+ // dark node fills and pale label text sat on the light background,
+ // because only the CSS variables had changed and the svg had not.
+ // Restoring the source and drawing again is the only way to get the
+ // new palette into it -- there is nothing in the rendered svg to
+ // recolour selectively.
+ var rerender = function() {
+ document.querySelectorAll('[data-diagram-index]').forEach(function(el) {
+ var i = el.getAttribute('data-diagram-index');
+ if (sources[i] === undefined) return;
+ el.removeAttribute('data-processed');
+ el.textContent = sources[i];
+ });
+ render();
+ };
+ new MutationObserver(function(records) {
+ for (var i = 0; i < records.length; i++) {
+ if (records[i].attributeName === 'data-theme') { rerender(); return; }
+ }
+ }).observe(document.documentElement, { attributes: true });
})();
</script>
<script>
diff --git a/test/regressions.sh b/test/regressions.sh
index d929380..e520beb 100755
--- a/test/regressions.sh
+++ b/test/regressions.sh
@@ -1315,8 +1315,12 @@ it "the renderer matches both mermaid dialects"
# matched 'diagram-mermaid-legend'. A legend block sat unrendered, showing its
# own source text and pushing the page 3px wider than the viewport -- found
# when a real document with a legend was published.
-assert_file_contains "$dg/.dist/chart.html" "querySelector: '[class*=\"diagram-mermaid\"]'"
-assert_file_not_contains "$dg/.dist/chart.html" "querySelector: '.diagram-mermaid'"
+# The selector is now bound once to SEL and used for the first render, the
+# redraw after a theme change, and the modal, so the assertion follows it to
+# its definition. The attribute-substring form is what matters, not where the
+# literal sits.
+assert_file_contains "$dg/.dist/chart.html" "var SEL = '[class*=\"diagram-mermaid\"]';"
+assert_file_not_contains "$dg/.dist/chart.html" "var SEL = '.diagram-mermaid';"
it "the renderer is pointed at the class the parser emits"
# The markup uses a neutral "diagram" class rather than naming one renderer,
@@ -1393,6 +1397,27 @@ it "the body's repeat of the page title carries no rule"
assert_file_contains "$dg/.dist/chart.html" '.contents h1:not(:is(h1, h2) ~ *)::before { content: none; }'
assert_file_contains "$dg/.dist/post/charted.html" '.contents h1:not(:is(h1, h2) ~ *)::before { content: none; }'
+it "diagram sources are kept so the theme can be changed"
+# mermaid resolves the palette into the svg it writes and then replaces the
+# source text with that svg. A diagram drawn under one theme therefore keeps
+# that theme's colours after a switch -- dark node fills and pale labels on a
+# light page -- because only the CSS variables changed. There is nothing in the
+# rendered svg to recolour selectively, so the source is stashed first and the
+# diagram is drawn again.
+assert_file_contains "$dg/.dist/chart.html" "sources[i] = el.textContent;"
+assert_file_contains "$dg/.dist/post/charted.html" "sources[i] = el.textContent;"
+
+it "a theme change redraws the diagrams"
+# The observer watches data-theme on the root, which is what the theme script
+# sets. Verified in Chrome across four toggles: node fill tracked the theme
+# every time and all three diagrams survived.
+assert_file_contains "$dg/.dist/chart.html" "records[i].attributeName === 'data-theme'"
+
+it "the palette is read at render time, not once at load"
+# getComputedStyle cached into a closure would keep handing back the values
+# from first paint, so the redraw would repaint the diagram in the old colours.
+assert_file_contains "$dg/.dist/chart.html" "getComputedStyle(document.documentElement).getPropertyValue(n)"
+
it "an unrendered diagram degrades to readable source"
# If the script is absent or blocked, the element still holds what the author
# wrote. Shown as the code it is rather than as a paragraph with the newlines