StackExchange MathJax editing code

February 12, 2016 ยท View on GitHub

// The MIT License (MIT) // // Copyright (c) 2016 Stack Exchange // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // // =============================================== "use strict";

StackExchange.mathjaxEditing = (function () { var ready = false; // true after initial typeset is complete var pending = false; // true when MathJax has been requested var preview = null; // the preview container var inline = "$"; // the inline math delimiter var blocks, start, end, last, braces; // used in searching for math var math; // stores math until markdone is done var HUB = MathJax.Hub;

//
//  Runs after initial typeset
// 
HUB.Queue(function () {
    ready = true;
    HUB.processUpdateTime = 50; // reduce update time so that we can cancel easier
    HUB.Config({
        "HTML-CSS": {
            EqnChunk: 10,
            EqnChunkFactor: 1
        },
        // reduce chunk for more frequent updates
        SVG: {
            EqnChunk: 10,
            EqnChunkFactor: 1
        }
    });
});

//
//  The pattern for math delimiters and special symbols
//    needed for searching for math in the page.
//
var SPLIT = /(\$\$?|\\(?:begin|end)\{[a-z]*\*?\}|\\[\\{}$]|[{}]|(?:\n\s*)+|@@\d+@@)/i;

//
//  The math is in blocks i through j, so 
//    collect it into one block and clear the others.
//  Replace &, <, and > by named entities.
//  For IE, put <br> at the ends of comments since IE removes \n.
//  Clear the current math positions and store the index of the
//    math, then push the math string onto the storage array.
//
function processMath(i, j, preProcess) {
    var block = blocks.slice(i, j + 1).join("").replace(/&/g, "&amp;") // use HTML entity for &
    .replace(/</g, "&lt;") // use HTML entity for <
    .replace(/>/g, "&gt;") // use HTML entity for >
    ;
    if (HUB.Browser.isMSIE) {
        block = block.replace(/(%[^\n]*)\n/g, "\$1<br/>\n")
    }
    while (j > i) {
        blocks[j] = "";
        j--;
    }
    blocks[i] = "@@" + math.length + "@@";
    if (preProcess)
        block = preProcess(block);
    math.push(block);
    start = end = last = null;
}


var capturingStringSplit;
if ("aba".split(/(b)/).length === 3) {
    capturingStringSplit = function (str, regex) { return str.split(regex) }
}
else { // IE8
    capturingStringSplit = function (str, regex) {
        var result = [], match;
        if (!regex.global) {
            var source = regex.toString(),
                flags = "";
            source = source.replace(/^\/(.*)\/([im]*)$/, function (wholematch, re, fl) { flags = fl; return re; });
            regex = new RegExp(source, flags + "g");
        }
        regex.lastIndex = 0;
        var lastPos = 0;
        while (match = regex.exec(str))
        {
            result.push(str.substring(lastPos, match.index));
            result.push.apply(result, match.slice(1));
            lastPos = match.index + match[0].length;
        }
        result.push(str.substring(lastPos));
        return result;
    }
}


//
//  Break up the text into its component parts and search
//    through them for math delimiters, braces, linebreaks, etc.
//  Math delimiters must match and braces must balance.
//  Don't allow math to pass through a double linebreak
//    (which will be a paragraph).
//
function removeMath(text) {
    start = end = last = null; // for tracking math delimiters
    math = []; // stores math strings for latter
    
    // Except for extreme edge cases, this should catch precisely those pieces of the markdown
    // source that will later be turned into code spans. While MathJax will not TeXify code spans,
    // we still have to consider them at this point; the following issue has happened several times:
    //
    //     `$foo` and `$bar` are varibales.  -->  <code>$foo ` and `$bar</code> are variables.

    var hasCodeSpans = /`/.test(text),
        deTilde;
    if (hasCodeSpans) {
        text = text.replace(/~/g, "~T").replace(/(^|[^\\`])(`+)(?!`)([^\n]*?[^`\n])\2(?!`)/gm, function (wholematch) {
            return wholematch.replace(/\$/g, "~D");
        });
        deTilde = function (text) { return text.replace(/~([TD])/g, function (wholematch, character) { return { T: "~", D: "$" }[character]; }) };
    } else {
        deTilde = function (text) { return text; };
    }
    
    
    blocks = capturingStringSplit(text.replace(/\r\n?/g, "\n"), SPLIT);
    
    for (var i = 1, m = blocks.length; i < m; i += 2) {
        var block = blocks[i];
        if (block.charAt(0) === "@") {
            //
            //  Things that look like our math markers will get
            //  stored and then retrieved along with the math.
            //
            blocks[i] = "@@" + math.length + "@@";
            math.push(block);
        }
        else if (start) {
            //
            //  If we are in math, look for the end delimiter,
            //    but don't go past double line breaks, and
            //    and balance braces within the math.
            //
            if (block === end) {
                if (braces) {
                    last = i
                }
                else {
                    processMath(start, i, deTilde)
                }
            }
            else if (block.match(/\n.*\n/)) {
                if (last) {
                    i = last;
                    processMath(start, i, deTilde)
                }
                start = end = last = null;
                braces = 0;
            }
            else if (block === "{") {
                braces++
            }
            else if (block === "}" && braces) {
                braces--
            }
        }
        else {
            //
            //  Look for math start delimiters and when
            //    found, set up the end delimiter.
            //
            if (block === inline || block === "$$") {
                start = i;
                end = block;
                braces = 0;
            }
            else if (block.substr(1, 5) === "begin") {
                start = i;
                end = "\\end" + block.substr(6);
                braces = 0;
            }
        }
    }
    if (last) {
        processMath(start, last, deTilde)
    }
    return deTilde(blocks.join(""));
}

//
//  Put back the math strings that were saved,
//    and clear the math array (no need to keep it around).
//  
function replaceMath(text) {
    text = text.replace(/@@(\d+)@@/g, function (match, n) {
        return math[n]
    });
    math = null;
    return text;
}

//
//  This is run to restart MathJax after it has finished
//    the previous run (that may have been canceled)
//
function RestartMJ() {
    pending = false;
    HUB.cancelTypeset = false; // won't need to do this in the future
    HUB.Queue(["Typeset", HUB, preview]);
}

//
//  When the preview changes, cancel MathJax and restart,
//    if we haven't done that already.
//
function UpdateMJ() {
    if (!pending && ready) {
        pending = true;
        HUB.Cancel();
        HUB.Queue(RestartMJ);
    }
}

//
//  Save the preview ID and the inline math delimiter.
//  Create a converter for the editor and register a preConversion hook
//   to handle escaping the math.
//  Create a preview refresh hook to handle starting MathJax.
//
function prepareWmdForMathJax(editorObject, wmdId, delimiters) {
    preview = document.getElementById("wmd-preview" + wmdId);
    inline = delimiters[0][0];

    var converterObject = editorObject.getConverter();
    converterObject.hooks.chain("preConversion", removeMath);
    converterObject.hooks.chain("preSafe", replaceMath);
    editorObject.hooks.chain("onPreviewRefresh", UpdateMJ);
}

return {
    prepareWmdForMathJax: prepareWmdForMathJax
}

})();

// // Set up MathJax to allow canceling of typesetting, if it // doesn't already have that. // (function () { var HUB = MathJax.Hub;

if (!HUB.Cancel) {

    HUB.cancelTypeset = false;
    var CANCELMESSAGE = "MathJax Canceled";

    HUB.Register.StartupHook("HTML-CSS Jax Config", function () {
        var HTMLCSS = MathJax.OutputJax["HTML-CSS"],
            TRANSLATE = HTMLCSS.Translate;
        HTMLCSS.Augment({
            Translate: function (script, state) {
                if (HUB.cancelTypeset || state.cancelled) {
                    throw Error(CANCELMESSAGE)
                }
                return TRANSLATE.call(HTMLCSS, script, state);
            }
        });
    });

    HUB.Register.StartupHook("SVG Jax Config", function () {
        var SVG = MathJax.OutputJax["SVG"],
            TRANSLATE = SVG.Translate;
        SVG.Augment({
            Translate: function (script, state) {
                if (HUB.cancelTypeset || state.cancelled) {
                    throw Error(CANCELMESSAGE)
                }
                return TRANSLATE.call(SVG, script, state);
            }
        });
    });

    HUB.Register.StartupHook("TeX Jax Config", function () {
        var TEX = MathJax.InputJax.TeX,
            TRANSLATE = TEX.Translate;
        TEX.Augment({
            Translate: function (script, state) {
                if (HUB.cancelTypeset || state.cancelled) {
                    throw Error(CANCELMESSAGE)
                }
                return TRANSLATE.call(TEX, script, state);
            }
        });
    });

    var PROCESSERROR = HUB.processError;
    HUB.processError = function (error, state, type) {
        if (error.message !== CANCELMESSAGE) {
            return PROCESSERROR.call(HUB, error, state, type)
        }
        MathJax.Message.Clear(0, 0);
        state.jaxIDs = [];
        state.jax = {};
        state.scripts = [];
        state.i = state.j = 0;
        state.cancelled = true;
        return null;
    };

    HUB.Cancel = function () {
        this.cancelTypeset = true;
    };
}

})();