This replaces MathJax inline markup in an <svg>
element with SVG-rendered MathJax.
See test0.html and test1.html for usage examples.
/* | |
* SVG_MathJax | |
* | |
* Copyright 2014 Jason M. Sachs | |
* Based loosely on an approach outlined by Martin Clark | |
* in http://stackoverflow.com/a/21923030/44330 | |
* | |
* Licensed under the Apache License, Version 2.0 (the "License"); | |
* you may not use this file except in compliance with the License. | |
* You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
* See the License for the specific language governing permissions and | |
* limitations under the License. | |
* | |
* Modifications: John H. Cole, 2024 | |
* - Updated to work with MathJax v3. | |
* - keep the entire SVG, not just the g nodes. | |
* - Reworked scale, width, and height calculations. | |
* - Added MutationObserver to detect when SVGs are added to the DOM. | |
* - Implemented polling to ensure all SVGs are injected before calling MathJax. | |
* - Some refactoring to improve readability. | |
* | |
* https://gist.github.com/jhcole/0fcefdc1c0a931dcc27407074bfc87b2 | |
* | |
*/ | |
(function() { | |
// Apply a function to elements of an array x | |
function forEach(x, f) { | |
var n = x.length; | |
for (var i = 0; i < n; ++i) { | |
f(x[i]); | |
} | |
} | |
// Find all the SVG text elements that are delimited by | |
// \( \) or $ $ MathJax delimiters | |
// (with optional whitespace before/after) | |
function findSVGMathJax(f, context) { | |
const re = /^\s*([LlRrCc]?)(\\\(.*\\\)|\$.*\$)\s*$/; | |
context = context || document; | |
context.querySelectorAll('svg').forEach(svg => { | |
svg.querySelectorAll('text').forEach(text => { | |
const match = text.textContent.match(re); | |
if (match) { | |
const justification = match[1]; | |
const mathmarkup = match[2].replace(/^\$(.*)\$$/,'\\($1\\)'); | |
f(text, justification, mathmarkup); | |
} | |
}); | |
}); | |
} | |
function _install(options) { | |
const items = []; | |
// Move the raw MathJax items to a temporary element | |
MathJax.startup.promise.then(() => { | |
const mathbucket = document.createElement('div'); | |
mathbucket.setAttribute('id', 'mathjax_svg_bucket'); | |
document.body.appendChild(mathbucket); | |
findSVGMathJax(function(text, justification, mathmarkup) { | |
const div = document.createElement('div'); | |
mathbucket.appendChild(div); | |
div.appendChild(document.createTextNode(mathmarkup)); | |
items.push({text: text, div: div, align: justification}); | |
}); | |
MathJax.typesetPromise().then(() => { | |
forEach(items, function(item) { | |
const svgdest = item.text; | |
const x0 = +item.text.getAttribute('x'); | |
const y0 = +item.text.getAttribute('y'); | |
const svgmath = item.div.getElementsByClassName('MathJax')[0].getElementsByTagName('svg')[0]; | |
const justification = item.align; | |
let fontsize = svgdest.getAttribute('font-size'); | |
// If the font-size attribute is not found, look for it in the style attribute | |
if (!fontsize) { | |
fontsize = window.getComputedStyle(svgdest).fontSize; | |
} | |
const scale = options.scale*parseFloat(fontsize); | |
let x1; | |
const svgmathinfo = { | |
width: svgmath.getBoundingClientRect().width, | |
height: svgmath.getBoundingClientRect().height | |
}; | |
switch (justification.toUpperCase()) { | |
case 'L': x1 = 0; break; | |
case 'R': x1 = -svgmathinfo.width * scale; break; | |
case 'C': // default to center | |
default: x1 = -svgmathinfo.width * 0.5 * scale; break; | |
} | |
const y1 = -svgmathinfo.height * scale; | |
svgmath.setAttribute('transform', 'translate(' + x0 + ' ' + y0 + ')' | |
+' translate(' + x1 + ' ' + y1 + ')' | |
+' scale(' + scale + ')' | |
); | |
if (options.escape_clip) { | |
svgdest.parentNode.removeAttribute('clip-path'); | |
} | |
svgdest.parentNode.replaceChild(svgmath, svgdest); | |
}); | |
// Remove the temporary items | |
mathbucket.parentNode.removeChild(mathbucket); | |
}).catch(err => { | |
console.error('MathJax processing error:', err); | |
}); | |
}); | |
} | |
class F { | |
constructor() { | |
this.scale = 0.09; | |
this.escape_clip = false; | |
} | |
install() { | |
_install(this); | |
} | |
} | |
window.Svg_MathJax = new F(); | |
// Create a MutationObserver to detect when SVGs are added to the DOM | |
var observer = new MutationObserver(function(mutations) { | |
// Check if all SVGs are injected | |
console.log('DOM Mutated: Checking for pending SVG injection'); | |
if (document.querySelectorAll("img.svg-injectable").length === 0) { | |
observer.disconnect(); | |
console.log('Finished injecting SVGs, applying MathJax'); | |
window.Svg_MathJax.install(); | |
} | |
}); | |
// Ensure the DOM is fully loaded before setting up the observer | |
document.addEventListener('DOMContentLoaded', function() { | |
if (document.querySelectorAll("img.svg-injectable").length === 0) { | |
console.log('No pending SVGs to inject, applying MathJax'); | |
window.Svg_MathJax.install(); | |
} else { | |
// Start observing the document for added nodes | |
console.log('Starting observer'); | |
observer.observe(document.body, { childList: true, subtree: true }); | |
} | |
}); | |
})(); |
<!DOCTYPE html> | |
<html> | |
<head> | |
<script> | |
window.MathJax = { | |
options: { | |
ignoreHtmlClass: 'tex2jax_ignore', | |
} | |
}; | |
</script> | |
<script defer="defer" src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js"></script> | |
<script defer="defer" src="svg_mathjax.js"></script> | |
</head> | |
<body> | |
<div style="font-size: 12pt;"> | |
<p> | |
Outside an SVG, MathJax converts math automatically (e.g., \( a^2 + b^2 = c^2 \)). | |
Inside an SVG, MathJax also converts math automatically, but the positioning is | |
off. To address this, the "tex2jax_ignore" class is added to the SVGs parent div | |
to prevent MathJax from processing the math within it. Then the "svg_mathjax.js" | |
script: | |
</p> | |
<ol> | |
<li>locates every math text in an SVG</li> | |
<li>moves them to a temporary div,</li> | |
<li>lets MathJax convert them to SVGs,</li> | |
<li>calculates positioning adjustments,</li> | |
<li>and inserts the generated SVGs in place of their text elements.</li> | |
</ol> | |
</div> | |
<div class="tex2jax_ignore"> | |
<svg width="300" height="100" version="1.1" xmlns="http://www.w3.org/2000/svg"> | |
<rect width="100%" height="100%" fill="grey" /> | |
<text x="150" y="30" font-size="12pt">\( a^2 + b^2 = c^2 \)</text> | |
<text x="150" y="60" font-size="12pt">L\( a^2 + b^2 = c^2 \)</text> | |
<text x="150" y="90" font-size="12pt">R\( a^2 + b^2 = c^2 \)</text> | |
</svg> | |
</div> | |
</body> | |
</html> |
<!DOCTYPE html> | |
<html> | |
<head> | |
<script> | |
window.MathJax = { | |
options: { | |
ignoreHtmlClass: 'tex2jax_ignore', | |
} | |
}; | |
</script> | |
<script defer="defer" src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js"></script> | |
<script defer="defer" src="svg_mathjax.js"></script> | |
</head> | |
<body> | |
<div class="tex2jax_ignore"> | |
<svg xmlns="http://www.w3.org/2000/svg" xmlns:inkspace="http://www.inkscape.org/namespaces/inkscape" xmlns:xlink="http://www.w3.org/1999/xlink" width="800" height="600"> | |
<defs id="defs_block"> | |
<filter height="1.504" id="filter_blur" inkspace:collect="always" width="1.1575" x="-0.07875" y="-0.252"> | |
<feGaussianBlur id="feGaussianBlur3780" inkspace:collect="always" stdDeviation="4.2" /> | |
</filter> | |
</defs> | |
<title>blockdiag</title> | |
<rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="67" y="46" /> | |
<rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="259" y="46" /> | |
<rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="451" y="46" /> | |
<rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="451" y="126" /> | |
<rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="643" y="126" /> | |
<rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="64" y="40" /> | |
<text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="100" y="65">plain text</text> | |
<rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="256" y="40" /> | |
<text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="320" y="65">\( e^{-sT} \)</text> | |
<rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="448" y="40" /> | |
<text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="512" y="70">R\( \frac{3}{s^2+1} \)</text> | |
<text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="470" y="95">aligned at the right</text> | |
<rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="448" y="120" /> | |
<text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="512" y="150">L\( H(s) \)</text> | |
<text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="470" y="175">aligned at the left</text> | |
<rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="640" y="120" /> | |
<text fill="rgb(0,0,0)" font-family="sansserif" font-size="11" font-style="normal" font-weight="normal" x="704" y="150">$ K_p + \left( 1 + {1 \over T s} \right) $</text> | |
<path d="M 192 60 L 248 60" fill="none" stroke="rgb(0,0,0)" /> | |
<polygon fill="rgb(0,0,0)" points="255,60 248,56 248,64 255,60" stroke="rgb(0,0,0)" /> | |
<path d="M 384 60 L 440 60" fill="none" stroke="rgb(0,0,0)" /> | |
<polygon fill="rgb(0,0,0)" points="447,60 440,56 440,64 447,60" stroke="rgb(0,0,0)" /> | |
<path d="M 384 60 L 416 60" fill="none" stroke="rgb(0,0,0)" /> | |
<path d="M 416 60 L 416 140" fill="none" stroke="rgb(0,0,0)" /> | |
<path d="M 416 140 L 440 140" fill="none" stroke="rgb(0,0,0)" /> | |
<polygon fill="rgb(0,0,0)" points="447,140 440,136 440,144 447,140" stroke="rgb(0,0,0)" /> | |
<path d="M 576 140 L 632 140" fill="none" stroke="rgb(0,0,0)" /> | |
<polygon fill="rgb(0,0,0)" points="639,140 632,136 632,144 639,140" stroke="rgb(0,0,0)" /> | |
<path d="M 768 140 L 784 140" fill="none" stroke="rgb(0,0,0)" /> | |
<path d="M 784 140 L 784 25" fill="none" stroke="rgb(0,0,0)" /> | |
<path d="M 320 25 L 784 25" fill="none" stroke="rgb(0,0,0)" /> | |
<path d="M 320 25 L 320 32" fill="none" stroke="rgb(0,0,0)" /> | |
<polygon fill="rgb(0,0,0)" points="320,39 316,32 324,32 320,39" stroke="rgb(0,0,0)" /> | |
</svg> | |
</div> | |
</body> | |
</html> |