User:Remember the dot/Syntax highlighter.js

//Syntax highlighter with various advantages //See User:Remember the dot/Syntax highlighter for more information

(function {    "use strict";    //variables that are preserved between function calls    var textboxContainer;    var wpTextbox0;    var wpTextbox1;    var syntaxStyleElement;    var lastText;    var maxSpanNumber = -1; //the number of the last span available, used to tell if creating additional spans is necessary    var highlightSyntaxIfNeededIntervalID;    /* Define context-specific regexes, one for every common token that ends the       current context.       An attempt has been made to search for the most common syntaxes first, thus       maximizing performance. Syntaxes that begin with the same character are       searched for at the same time.       Wiki syntaxes from most common to least common:           internal link [http:// named external link]             {| table |}           http:// bare external link           =Heading= * unordered list # ordered list : indent ; small heading  pre  horizontal line italic bold three tildes username four tildes signature five tildes timestamp &entity; Flags: g for global search, m for make ^ match the beginning of each line and $ the end of each line */   var breakerRegexBase = "\\[(?:\\[|(?:https?:|ftp:)?//|mailto:)|\\{(?:\\{\\{?|\\|)|<(?:[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD-\\.0-9\u00B7\u0300-\u036F\u203F-\u203F-\u2040]*|!--[^]*?-->)|(?:https?://|ftp://|mailto:)[^\\s\"<>[\\]{-}]*[^\\s\",\\.:;<>[\\]{-}]|^(?:=|[*#:;]+|-{4,})|\\\\'\\\\'(?:\\\\')?|~{3,5}|&(?:[a-z]+|#(?:\\d+|[xX][0-9a-fA-F]+));"; function breakerRegexWithPrefix(prefix) {       //the stop token has to be at the beginning of the regex so that it takes precedence over substrings of itself. return new RegExp("(" + prefix + ")|" + breakerRegexBase, "gm"); }   var defaultBreakerRegex           = new RegExp(breakerRegexBase, "gm"); var wikilinkBreakerRegex         = breakerRegexWithPrefix("]][a-zA-Z]*"); var namedExternalLinkBreakerRegex = breakerRegexWithPrefix("]"); var parameterBreakerRegex        = breakerRegexWithPrefix("}}}"); var templateBreakerRegex         = breakerRegexWithPrefix("}}"); var tableBreakerRegex            = breakerRegexWithPrefix("\\|}"); var headingBreakerRegex          = breakerRegexWithPrefix("\n"); var boldBreakerRegex             = breakerRegexWithPrefix("\\\\'\\\\'\\\\'"); var italicBreakerRegex           = breakerRegexWithPrefix("\\\\'\\\\'"); var tagBreakerRegexCache         = {}; //browser workaround triggers var gecko  = ($.client.profile.layout == "gecko"); var presto = ($.client.profile.layout == "presto"); var webkit = ($.client.profile.layout == "webkit"); var trident = ($.client.profile.layout == "trident"); function highlightSyntax {       lastText = wpTextbox1.value; /* Backslashes and apostrophes are CSS-escaped at the beginning and all parsing regexes and functions are designed to match. On the other hand, newlines are not escaped until written so that in the regexes ^ and $ work for both newlines and the beginning or end of the string. */       var text = lastText.replace(/['\\]/g, "\\$&") + "\n"; //add a newline to fix scrolling and parsing issues var i = 0; //the location of the parser as it goes through wpTextbox1.value var css = ""; var spanNumber = 0; var lastColor; var before = true; //workaround for Opera //there are two problems here: // .scrollLeft is automatically scrolled beyond the value limit that http://www.w3.org/TR/cssom-view/#scroll-an-element specifies // will hide a character or two underneath the scrollbar instead of adding a scrollbar //this workaround forces wpTextbox0 to allow scrolling arbitrarily if (presto) {           text += new Array(wpTextbox1.scrollWidth).join(" "); }       //writes text into to-be-created span elements of wpTextbox0 using :before and :after pseudo-elements //both :before and :after are used because using two pseudo-elements per span is significantly faster than doubling the number of spans required function writeText(text, color) {           text = text.replace(/\n/g, "\\A "); //CSS-escape newlines. CSS ignores the space after the hex code of the escaped character if (color == lastColor) {               css += text; }           else {               //workaround for https://bugs.webkit.org/show_bug.cgi?id=17427 if (webkit && ((/[!"#%&)*+,-.:;=\?>\\\]\|\}~]$/.test(css) && /^[<\[\{]/.test(text)) || (css.substring(-1) == "-" && text.substring(0, 2) == "\\'")))               {                    text = "\u200B" + text; //insert a zero-width space                }                //whitespace is omitted in the hope of increasing performance                css += "'}#s" + spanNumber; //spans will be created with IDs s0 through sN                if (before)                {                    css += ":before{";                }                else                {                    css += ":after{";                    spanNumber++;                }                if (color)                {                    //"background-color" is 6 characters longer than "background" but the browser processes it faster                    css += "background-color:" + color + ";";                }                css += "content:'" + text; before = !before; lastColor = color; }       }        function highlightBlock(color, breakerRegex) {           breakerRegex.lastIndex = i;            var match; while (match = breakerRegex.exec(text)) {               if (match[1]) {                   //end token found writeText(text.substring(i, breakerRegex.lastIndex), color); i = breakerRegex.lastIndex; return; }               var lastBitOfTextInPreviousColor = text.substring(i, breakerRegex.lastIndex - match[0].length); if (lastBitOfTextInPreviousColor) //avoid calling writeText with text == "" to improve performance {                   writeText(lastBitOfTextInPreviousColor, color); }               i = breakerRegex.lastIndex; switch (match[0].charAt(0)) //cases in this switch should be arranged from most common to least common {                   case "[": if (match[0].charAt(1) == "[") {                           //wikilink writeText("[[", syntaxHighlighterConfig.wikilinkColor || color);                           highlightBlock(syntaxHighlighterConfig.wikilinkColor || color, wikilinkBreakerRegex);                        }                        else                        {                            //named external link                            writeText(match[0], syntaxHighlighterConfig.externalLinkColor || color);                            highlightBlock(syntaxHighlighterConfig.externalLinkColor || color, namedExternalLinkBreakerRegex);                        }                        break;                    case "{":                        if (match[0].charAt(1) == "{")                        {                            if (match[0].charAt(2) == "{")                            {                                //parameter                                writeText("{{{", syntaxHighlighterConfig.parameterColor || color);                                highlightBlock(syntaxHighlighterConfig.parameterColor || color, parameterBreakerRegex);                            }                            else                            {                                //template                                writeText("{{", syntaxHighlighterConfig.templateColor || color);                                highlightBlock(syntaxHighlighterConfig.templateColor || color, templateBreakerRegex);                            }                        }                        else //|                        {                            //table                            writeText("{|", syntaxHighlighterConfig.tableColor || color);                            highlightBlock(syntaxHighlighterConfig.tableColor || color, tableBreakerRegex);                        }                        break;                    case "<":                        if (match[0].charAt(1) == "!")                        {                            //comment tag                            writeText(match[0], syntaxHighlighterConfig.commentColor || color);                            break;                        }                        else                        {                            //some other kind of tag, search for its end                            //the search is made easier because XML attributes may not contain the character ">"                            var tagEnd = text.indexOf(">", i) + 1;                            if (tagEnd == 0)                            {                                //not a tag, just a "<" with some text after it                                writeText("<");                                i = i - match[0].length + 1;                                break;                            }                            if (text.charAt(tagEnd - 2) == "/")                            {                                //empty tag                                writeText(text.substring(i - match[0].length, tagEnd), syntaxHighlighterConfig.tagColor || color);                                i = tagEnd;                            }                            else                            {                                var tagName = match[0].substring(1);                                var stopAfter = "";                                //again, cases are ordered from most common to least common                                if (/nowiki|pre|math|syntaxhighlight|source|timeline|hiero/.test(tagName))                                {                                    //tag that can contain only plain text                                    var endIndex = text.indexOf(stopAfter, i);                                    if (endIndex == -1)                                    {                                        endIndex = text.length;                                    }                                    else                                    {                                        endIndex += stopAfter.length;                                    }                                    writeText(text.substring(i - match[0].length, endIndex), syntaxHighlighterConfig.tagColor || color);                                    i = endIndex;                                }                                else                                {                                    //ordinary tag                                    writeText(text.substring(i - match[0].length, tagEnd), syntaxHighlighterConfig.tagColor || color);                                    i = tagEnd;                                    if (!tagBreakerRegexCache[tagName])                                    {                                        tagBreakerRegexCache[tagName] = breakerRegexWithPrefix(stopAfter);                                    }                                    highlightBlock(syntaxHighlighterConfig.tagColor || color, tagBreakerRegexCache[tagName]);                                }                            }                        }                        break;                    case "h":                    case "f":                    case "m":                        //bare external link                        writeText(match[0], syntaxHighlighterConfig.externalLinkColor || color);                        break;                    case "=":                        if (/[^=]=+$/.test(text.substring(i, text.indexOf("\n", i)))) //the line begins and ends with an equals sign and has something else in the middle                        {                            //heading                            writeText("=", syntaxHighlighterConfig.headingColor || color);                            highlightBlock(syntaxHighlighterConfig.headingColor || color, headingBreakerRegex);                        }                        else                        {                            writeText("=", color); //move on, process this line as regular wikitext                        }                        break;                    case "*":                    case "#":                    case ":":                        //unordered list, ordered list, indent, small heading                        //just highlight the marker                        writeText(match[0], syntaxHighlighterConfig.listAndIndentColor || color);                        break;                    case ";":                        //small heading                        writeText(";", syntaxHighlighterConfig.headingColor || color);                        highlightBlock(syntaxHighlighterConfig.headingColor || color, headingBreakerRegex);                        break;                    case "-":                        //horizontal line                        writeText(match[0], syntaxHighlighterConfig.hrColor);                        break;                    case "\\":                        if (match[0].length == 6)                        {                            //bold                            writeText("\\'\\'\\'", syntaxHighlighterConfig.boldColor || color);                            highlightBlock(syntaxHighlighterConfig.boldColor || color, boldBreakerRegex);                        }                        else                        {                            //italic                            writeText("\\'\\'", syntaxHighlighterConfig.italicColor || color);                            highlightBlock(syntaxHighlighterConfig.italicColor || color, italicBreakerRegex);                        }                        break;                    case "~":                        //username, signature, timestamp                        writeText(match[0], syntaxHighlighterConfig.signatureColor || color);                        break;                    case "&":                        //entity                        writeText(match[0], syntaxHighlighterConfig.entityColor || color);                }                breakerRegex.lastIndex = i;            }        }        //start!        var startTime = Date.now;        highlightBlock("", defaultBreakerRegex);        //output the leftovers to make sure whitespace etc. matches        writeText(text.substring(i), "");        //do we have enough span elements to match the generated CSS?        while (maxSpanNumber < spanNumber)        {            maxSpanNumber++;            wpTextbox0.appendChild(document.createElement("span")).id = "s" + maxSpanNumber;        }        //finish CSS: move the extra '} from the beginning to the end        syntaxStyleElement.textContent = css.substring(2) + "'}";        //if highlighting took more than 100 milliseconds, disable it.        var endTime = Date.now;        /*if (typeof(runningTotal) == "undefined")        {            window.runningTotal = 0;            window.totalRuns = 0;        }        else        {            totalRuns++;            runningTotal += endTime - startTime;            document.title = runningTotal / totalRuns;        }//*/        if (endTime - startTime > 100)        {            clearInterval(highlightSyntaxIfNeededIntervalID);            syntaxStyleElement.textContent = "";            wpTextbox1.removeEventListener("input", highlightSyntax);            var errorMessage = {};            errorMessage["ca"] = "S'ha desactivat el remarcar de sintaxi en aquesta pàgina perquè tu ordinador es massa lent. El temps màxim permès per a remarcar és 100ms, i el teu ordinador ha trigat $1ms. Si utilitzes Chrome o Safari, això potser a causa de què el remarcador ha de treballar voltant WebKit bug 17427. Prova tancar algunes pestanyes i programes i fer clic en \"Mostra la previsualització\" o \"Mostra els canvis\". Si no funciona això, prova altre navegador web, i si això no funciona, prova un ordinador més ràpid.";            errorMessage["en"] = "Syntax highlighting on this page was disabled because your computer is too slow. The maximum allowed highlighting time is 100ms, and your computer took $1ms. If you are using Chrome or Safari, this could be because the syntax highlighter has to work around WebKit bug 17427. Try closing some tabs and programs and clicking \"Show preview\" or \"Show changes\". If that doesn't work, try a different web browser, and if that doesn't work, try a faster computer.";            errorMessage["es"] = "Se desactivó el resaltar de sintaxis en esta página porque tu ordenador es demasiado lento. El tiempo máximum permitido para resaltar es 100ms, y tu ordenador tardó $1ms. Si usas Chrome o Safari, esto puede ser a causa de que el resaltador tiene que trabajar alrededor de WebKit bug 17427. Prueba cerrar algunas pestañas y programas y hacer clic en \"Mostrar previsualización\" o \"Mostrar cambios\". Si no funciona esto, prueba otro navegador web, y si eso no funciona, prueba un ordenador más rápido.";            errorMessage["io"] = "Sintaxo-hailaitar en ca pagino esis nekapabligata pro ke tua ordinatro es tro lenta. La maxima permisata hailaitala tempo es 100ms, e tua ordinatro konsumis $1ms. Se tu uzas Chrome o Safari, co povas esar pro ke la sintaxo-hailaitero mustas laborar cirkum WebKit bug 17427. Probez klozar kelka tabi e programi e kliktar \"Previdar\" o \"Montrez chanji\". Se to ne funcionas, probez altra brauzero, e se to ne funcionas, probez plu rapida ordinatro.";            errorMessage = errorMessage[wgUserLanguage] || errorMessage[wgUserLanguage.substring(0, wgUserLanguage.indexOf("-"))] || errorMessage["en"];            wpTextbox1.style.backgroundColor = "";            wpTextbox1.style.position = "";            wpTextbox0.style.color = "red";            wpTextbox0.style.fontFamily = "";            wpTextbox0.style.fontWeight = "bold";            wpTextbox0.style.height = "";            wpTextbox0.appendChild(document.createRange.createContextualFragment(errorMessage.replace("$1", endTime - startTime)));        }    }    function syncScrollX    {        wpTextbox0.scrollLeft = wpTextbox1.scrollLeft;    }    function syncScrollY    {        wpTextbox0.scrollTop = wpTextbox1.scrollTop;    }    //this function runs once every 500ms to detect changes to wpTextbox1's text that the input event does not catch    //this happens when another script changes the text without knowing that the syntax highlighter needs to be informed    function highlightSyntaxIfNeeded    {        if (wpTextbox1.value != lastText)        {            highlightSyntax;        }        if (wpTextbox1.scrollLeft != wpTextbox0.scrollLeft)        {            syncScrollX;        }        if (wpTextbox1.scrollTop != wpTextbox0.scrollTop)        {            syncScrollY;        }    }    function setupSyntaxHighlighter    {        function configureColor(parameterName, hardcodedFallback)        {            if (syntaxHighlighterConfig[parameterName] == "normal")            {                syntaxHighlighterConfig[parameterName] = hardcodedFallback;            }            else if (syntaxHighlighterConfig[parameterName])            {                return;            }            else if (typeof(syntaxHighlighterConfig.defaultColor) != "undefined")            {                syntaxHighlighterConfig[parameterName] = syntaxHighlighterConfig.defaultColor;            }            else            {                syntaxHighlighterConfig[parameterName] = hardcodedFallback;            }        }        window.syntaxHighlighterConfig = window.syntaxHighlighterConfig || {};        configureColor("commentColor",       "#E6FFE6"); //green        configureColor("boldColor",          "#E5E5E5"); //gray        configureColor("entityColor",        "#E6FFE6"); //green        configureColor("externalLinkColor",  "#E6FFFF"); //cyan        configureColor("italicColor",        "#E5E5E5"); //gray        configureColor("headingColor",       "#E5E5E5"); //gray        configureColor("hrColor",            "#E5E5E5"); //gray        configureColor("listAndIndentColor", "#E6FFE6"); //green        configureColor("parameterColor",     "#FFCC66"); //orange        configureColor("signatureColor",     "#FFCC66"); //orange        configureColor("tagColor",           "#FFE6FF"); //pink        configureColor("tableColor",         "#FFFFCC"); //yellow        configureColor("templateColor",      "#FFFFCC"); //yellow        configureColor("wikilinkColor",      "#E6E6FF"); //blue        textboxContainer = document.createElement("div");        wpTextbox0 = document.createElement("div");        wpTextbox1 = document.getElementById("wpTextbox1");        syntaxStyleElement = document.createElement("style");        //the styling of the textbox and the background div must be kept very similar        wpTextbox0.style.backgroundColor = window.getComputedStyle(wpTextbox1).backgroundColor;        if (wpTextbox0.style.backgroundColor == "transparent")        {            //Opera and perhaps others return "transparent" instead of "white" if the background color is not specified            //http://www.w3.org/TR/1998/REC-CSS2-19980512/cascade.html#computed-value is ambiguous as to what should happen            wpTextbox0.style.backgroundColor = "white";        }        wpTextbox0.style.border          = "1px solid transparent";        wpTextbox0.style.boxSizing       = "border-box";        wpTextbox0.style.MozBoxSizing    = "border-box";        wpTextbox0.style.WebkitBoxSizing = "border-box";        wpTextbox0.style.color           = "transparent"; //makes it look just a little bit smoother        wpTextbox0.style.fontFamily      = window.getComputedStyle(wpTextbox1).fontFamily;        wpTextbox0.style.fontSize        = window.getComputedStyle(wpTextbox1).fontSize;        wpTextbox0.style.lineHeight      = "normal";        wpTextbox0.style.overflowX       = "auto";        wpTextbox0.style.overflowY       = "scroll";        wpTextbox0.style.whiteSpace      = "pre-wrap";        wpTextbox0.style.width           = "100%";        wpTextbox0.style.wordWrap        = "normal"; //see below        wpTextbox1.style.backgroundColor = "transparent";        wpTextbox1.style.border          = "1px inset gray";        wpTextbox1.style.boxSizing       = "border-box";        wpTextbox1.style.MozBoxSizing    = "border-box";        wpTextbox1.style.WebkitBoxSizing = "border-box";        wpTextbox1.style.lineHeight      = "normal";        wpTextbox1.style.margin          = 0; //firefox wants to put a 1px margin on the top and bottom of the textbox, which throws it out of alignment with wpTextbox0        wpTextbox1.style.overflowX       = "auto";        wpTextbox1.style.overflowY       = "scroll";        wpTextbox1.style.padding         = 0;        wpTextbox1.style.position        = "absolute";        wpTextbox1.style.resize          = "none";        wpTextbox1.style.left            = 0;        wpTextbox1.style.top             = 0;        wpTextbox1.style.width           = "100%";        wpTextbox1.style.wordWrap        = "normal"; //overall more visually appealing, and essential for Opera        wpTextbox0.style.height          = wpTextbox1.offsetHeight + "px";        if (gecko) //workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=157846        {            wpTextbox0.style.paddingLeft = "1px";            wpTextbox0.style.paddingRight = "1px";        }        else if (presto) //workaround for Opera        {            //wpTextbox0 must allow arbitrary scrolling on Opera (see above), so wpTextbox1 must also be given a horizontal scrollbar            //also, if overflowX is auto then when the window is resized the standard line breaking algorithm is not followed            wpTextbox1.style.overflowX = "scroll";        }        textboxContainer.style.position = "relative";        wpTextbox1.parentNode.insertBefore(textboxContainer, wpTextbox1);        textboxContainer.appendChild(wpTextbox1);        textboxContainer.appendChild(wpTextbox0);        //fix drop-downs in editing toolbar        $('.tool-select *').css({zIndex: 5});        document.head.appendChild(syntaxStyleElement);        wpTextbox1.addEventListener("input", highlightSyntax);        wpTextbox1.addEventListener("scroll", syncScrollX);        wpTextbox1.addEventListener("scroll", syncScrollY);        highlightSyntaxIfNeededIntervalID = setInterval(highlightSyntaxIfNeeded, 500);        highlightSyntax;    }    //enable the highlighter only when editing wikitext pages    //in the future a separate parser could be added for CSS and JS pages    //blacklist Internet Explorer, it's just too broken    if ((wgAction == "edit" || wgAction == "submit") && !((wgNamespaceNumber == 2 || wgNamespaceNumber == 8) && /\.(css|js)$/.test(wgTitle)) && !trident)    {        //the highlighter has to run after any other script (such as the editing toolbar) that reparents wpTextbox1        //Opera sometimes considers the page to have loaded before this script runs and sometimes after        if (document.readyState == "complete")        {            setupSyntaxHighlighter;        }        else        {            $(window).load(setupSyntaxHighlighter);        }    } });