1
0
mirror of https://github.com/paboyle/Grid.git synced 2025-06-13 12:47:05 +01:00
Files
Grid/assets/js/render-math-and-dump.js
2018-03-15 17:01:59 +00:00

286 lines
12 KiB
JavaScript

// PhantomJS setup
var webPage = require('webpage');
var page = webPage.create();
var system = require('system');
var waitForMath;
var noLongerExecWaitForMathJax = false;
// Usage
if (system.args.length < 2) {
console.log("Usage: phantomjs render-math-and-dump.js <filename>");
console.log("Renders <filename> and dumps the DOM to the console.");
phantom.exit();
};
// Open the page and on success set the window interval function
// Alternatives:
// page.open(encodeURI(system.args[1]), function(status) {
// page.open("file://" + encodeURI(system.args[1]), function(status) {
page.open(system.args[1], function(status) {
if (status === "success") {
// Execute the query every 20ms
waitForMath = window.setInterval(
waitForMathJax,
20
);
};
});
// This also works (to some extent) with web URLs.
// page.open(system.args[1], function(status){
// if (status === "success") {
// // In this setup a complete local MathJax installation
// // is present in MathJax/
// page.injectJs("MathJax/MathJax.js");
// page.injectJs("ConfigMathJax.js");
// // Execute the query every 20ms
// waitForMath = window.setInterval(
// waitForMathJax,
// 20
// );
// };
// });
// Removes all MathJax <script> elements from the DOM.
// This avoids a second processing in PrinceXML, which
// a) had nothing left to do (MathJax already).
// b) probably would fail, since PrinceXML at the moment still lacks MathJax support
var removeMathJaxScripts = function() {
// Does 'str' end with 'suffix'?
var strEndsWith = function(str, suffix) {
return str.indexOf(suffix, str.length - suffix.length) !== -1;
};
// Get and remove all MathJax related script elements
var MathJaxScriptFound = true;
var scripts = document.getElementsByTagName("script");
// Repeating this loop until no more matching scripts are found seems
// to be necessary because removing children from the DOM did not always
// happen immediately, some of them were still written to the output when
// dumping 'page.content'.
while (MathJaxScriptFound) {
MathJaxScriptFound = false;
for (var i = scripts.length - 1; i >= 0; i--) {
if (scripts[i].hasAttribute("id")) {
var id = scripts[i].getAttribute("id");
// Remove script if 'id' starts with 'MathJax-Element'
if (id.indexOf("MathJax-Element") === 0) {
MathJaxScriptFound = true;
scripts[i].parentNode.removeChild(scripts[i]);
};
} else if (scripts[i].hasAttribute("src")) {
var src = scripts[i].getAttribute("src");
// Remove script if 'src' ends on 'ConfigMathJax.js',
// 'MathJax.js' or 'jax.js'
if ((strEndsWith(src, "ConfigMathJax.js"))
|| (strEndsWith(src, "MathJax.js"))
|| (strEndsWith(src, "jax.js"))) {
MathJaxScriptFound = true;
scripts[i].parentNode.removeChild(scripts[i]);
};
};
};
scripts = document.getElementsByTagName("script");
};
// Remove div with ID 'MathJax_Font_Test'
var fontTest = document.getElementById("MathJax_Font_Test");
if (fontTest) {
fontTest.parentNode.removeChild(fontTest);
};
// Avoid empty HTML-Syntax elements (<meta>, <link>, <br>, <hr>, ...),
// this makes it possible to call PrinceXML in XML mode with the generated
// output from PhantomJS. In mode 'auto' this code can be disabled
// completely. Please also note that for example the resulting <br></br>
// is not compliant with HTML, so you should keep an eye on the output
// generated by PrinceXML. The same is probably true for <meta></meta>,
// <link></link> and the like.
elements = document.documentElement.getElementsByTagName("*");
for (var i = elements.length - 1; i >= 0; i--) {
if (elements[i].childNodes.length == 0) {
elements[i].appendChild(document.createTextNode(""));
};
};
};
// The problem with processing SVG formulas generated by MathJax in PrinceXML
// is the location of the SVG glyphs which are used by MathJax to construct
// formulas. MathJax creates an SVG path for every symbol/letter in a hidden
// div at the beginning of the document. Every glyph (actually an SVG path)
// has an ID. In every formula the glyphs are only referenced by this ID and
// *not* recreated every time the glyph is needed. This keeps the amount of
// markup added to the document small.
//
// Currently PrinceXML isn't able to use links to elements wich are defined
// in another SVG element (the so called SVG "islands" problem, mentioned for
// example here /forum/topic/1402/multiple-svg-regions-with-common-defs and
// here /forum/topic/2912/what-forms-does-prince-support-css-clip-path).
//
// This function collects all MathJax glyph definitions and copies them to any
// SVG formula element where they are used. The function does not copy *all*
// definitions to *all* formulas, only those needed for the current formula
// are copied.
//
// The function still has (at least) one "bug": By copying a glyph to more
// than one formula you'll end up with elements that have the same ID in the
// resulting document. For PrinceXML it doesn't seem to be a problem, but a
// validator will surely find it faulty.
var relocateMathJaxGlyphs = function () {
// Container holding all MathJax glyphs, declared here for later removal.
var mathJaxSVGGlyphContainer = null;
// Get an array of all MathJax SVG paths which form a glyph.
var getMathJaxGlyphs = function () {
// These glyphs are located inside an SVG 'defs' element and
// are later used/referenced to layout every formula.
// This code currently only checks for the presence of one
// glyph container (the first).
var glyphContainer = document.getElementById("MathJax_SVG_glyphs");
if (!glyphContainer) {
return [];
};
// Hold the container for later removal to avoid too many DOM queries.
var gcpn = glyphContainer.parentNode;
if (gcpn) {
mathJaxSVGGlyphContainer = gcpn;
};
// This could be changed to use '*' instead of 'path', but so far
// I haven't seen that MathJax uses something different than 'path'.
var paths = glyphContainer.getElementsByTagName("path");
var result = [];
for (var i = paths.length - 1; i >= 0; i--) {
var id = paths[i].getAttribute("id");
if (id) {
result.push({id:id, path:paths[i]});
};
};
return result;
};
// Get an array of all MathJax SVG display areas (formulas).
var getMathJaxSVGs = function () {
// SVG formulas are located in a span with class name 'MathJax_SVG'.
var svgContainers = document.getElementsByClassName("MathJax_SVG");
if (svgContainers.length === 0) {
return [];
};
var result = [];
for (var i = svgContainers.length - 1; i >= 0; i--) {
// Handle all embedded SVG containers (normally there should be
// only one, but it doesn't hurt to visit all).
var svgs = svgContainers[i].getElementsByTagName("svg");
for (var a = svgs.length - 1; a >= 0; a--) {
// Set missing SVG namespace
svgs[a].setAttribute("xmlns", "http://www.w3.org/2000/svg");
// At this stage we can already create the <defs> element (if
// not present). The element is supposed holds copies of the
// glyphs and later will be appended to *this* SVG element.
var defs;
var defsElements = svgs[a].getElementsByTagName("defs");
if (defsElements.length === 0) {
defs = document.createElement("defs");
} else {
defs = defsElements[0];
};
result.push({svg:svgs[a], defs:defs});
};
};
return result;
};
// Find the matching glyph path by id in 'glyphs' and return a clone
var getGlyphById = function (id, glyphs) {
for (var i = glyphs.length - 1; i >= 0; i--) {
// id and path are assumed to be always non-null
if (id === glyphs[i].id) {
return glyphs[i].path.cloneNode(true);
};
};
return null;
};
// Copy every SVG glyph referenced in the SVG element into the <defs>
// element (this was already created in getMathJaxSVGs, if not present).
var copySVGGlyphsToSVGDefsElement = function (svgObject, glyphs) {
// Get all nested <use> elements
var useElements = svgObject.svg.getElementsByTagName("use");
for (var i = useElements.length - 1; i >= 0; i--) {
// First lookup the xlink:href attribute. If the attribute is
// present verbatim in this form we'll take this one.
var id = useElements[i].getAttribute("xlink:href");
if (!id) {
// Next try the ordinary href attribute. Although PhantomJS
// later writes this as 'xlink:href' it seems that it only
// can be accessed using 'href'.
id = useElements[i].getAttribute("href");
};
// No suitable id found...
if ((!id) || (id.length < 2)) {
continue;
};
// Get the matching glyph as a clone from the list of all glyps
// and append it to the <defs> element.
var glyph = getGlyphById(id.slice(1), glyphs);
if (glyph) {
svgObject.defs.appendChild(glyph);
} else {
};
};
// Finally append the <defs> element to its parent SVG element
svgObject.svg.appendChild(svgObject.defs);
};
// This is the main loop which ...
// ... gets all glyphs ...
var mathJaxGlyphs = getMathJaxGlyphs();
if (mathJaxGlyphs.length > 0) {
// ... gets all formulas ...
var mathJaxSVGs = getMathJaxSVGs();
if (mathJaxSVGs.length > 0) {
for (var i = mathJaxSVGs.length - 1; i >= 0; i--) {
// ... and copies matching glyphs into every formula.
copySVGGlyphsToSVGDefsElement(mathJaxSVGs[i], mathJaxGlyphs);
};
};
};
// Finally we can remove the glyph container from the DOM
if (mathJaxSVGGlyphContainer) {
mathJaxSVGGlyphContainer
.parentNode
.removeChild(mathJaxSVGGlyphContainer);
};
};
// Query a variable named 'MathJaxFinished' in the document,
// see also ConfigMathJax.js.
var waitForMathJax = function () {
if (noLongerExecWaitForMathJax) {
return;
};
var mjFinished = page.evaluate(function () {
return MathJaxFinished;
});
if (mjFinished) {
// waitForMathJax was called at least twice (?), even after
// window.clearInterval(). This flag prevents it (see above).
noLongerExecWaitForMathJax = true;
window.clearInterval(waitForMathJax);
// Remove the scripts
page.evaluate(removeMathJaxScripts);
// Relocate/copy all SVG glyphs (paths)
page.evaluate(relocateMathJaxGlyphs);
// Dump the DOM to the console and exit.
console.log(page.content);
phantom.exit();
};
}