ssh git@grit.deadkey.org man

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