TeX the World with Google Chart API addition.

By Ward Denker Last update Apr 19, 2011 — Installed 15,180 times.

There are 5 previous versions of this script.

// ==UserScript==
// @name           TeX the world
// @namespace      http://thewe.net/tex
// @description    TeX the world
// @include        *
// ==/UserScript==

// regexp for splitting a matched string into parts
var splitRegexp = /([\s\S]*?)\[;([\s\S]*?);\]([\s\S]*)/m;

// regexp for quickly testing if a text node contains our format
var matchRegexp = /\[;([\s\S]*?);\]/m;

// regexp for finding all TeX formulas in textContent
var matchRegexpGlobal = /\[;([\s\S]*?);\]/mg;

// signature to be typed when clicking ctrl-shift-;
var signature = '[To see formulas: http://thewe.net/tex]';

// create an image element for a given expression
function createTeXifiedImageElement(expr) {
    var imgElement = document.createElement('img');

    //imgElement.src = "http://thewe.net/tex/" + expr;

    var cacheServer = "";
    formulaCaching = getFormulaCachingPreference();

    //Ignore this if the user doesn't want caching to be on
    if(formulaCaching)
    {
       //If the user is requesting a reTeXify, they want a new uncached image.
       if(arguments.callee.caller.name.toString() == "reTeXifyImage")
          cacheServer = "";
       else
          cacheServer = ".nyud.net";
    }

    if(expr.length > 200)
       imgElement.src = "http://www.codecogs.com" + cacheServer + "/gif.latex?" + expr; // BS 20081227 - use Code Cogs better renderer
    else
       imgElement.src = "http://chart.apis.google.com" + cacheServer + "/chart?cht=tx&chl=" + escape(expr).replace(/\+/g,"%2B");  // If it's short, send to Google Chart API.

    imgElement.title = expr;
    imgElement.alt = "[;" + expr + ";]";
    imgElement.style.display = "inline";
    imgElement.addEventListener('dblclick', unTeXifyImage, false);
    return imgElement;
}

// TeXifies a range
function TeXifyRange(startNode, startNodeOpen, endNode, endNodeClose) {
    if (startNode == endNode) {
        var textNode = startNode;
        
        if (textNode.parentNode.nodeName == 'TEXTAREA')
            return;
        
        // infinite loop. it will break when there's nothing else matching the regexp.
        while (textNode.nodeValue.match(matchRegexp)) {
            textNodeSplit = splitRegexp.exec(textNode.nodeValue);
            
            // it matched. clear the text node and create the three
            // nodes before it - text, img, text (according to regexp match).
            // NOTE: the reason we clear the node value instead of using it for
            // the right part of the expression it to keep the cursor located
            // at the end of the node if the cursor is on it
            textNode.nodeValue = '';
            
            // create the text node that should be after the image and insert it
            var rightHalfNode = document.createTextNode(textNodeSplit[3]);
            textNode.parentNode.insertBefore(rightHalfNode, textNode);        

            // create the image and insert it
            var imgElement = createTeXifiedImageElement(textNodeSplit[2]);
            textNode.parentNode.insertBefore(imgElement, rightHalfNode);        

            // create the text node that should be before the image and insert it
            textNode.parentNode.insertBefore(document.createTextNode(textNodeSplit[1]), imgElement);
            
            // set the new textNode to be what was left. maybe there's more
            // to match (for example "[;A = B;] thus [;A \subseteq B;]")
            textNode = rightHalfNode;
        }    
    }
    else {
        // if the common ancestor is not the direct parent, stop immediately
        if (startNode.parentNode != endNode.parentNode)
            return;
    
        // verify that the only tags in the range are <br> and <wbr>
        var node;
        
        for (node = startNode; node != endNode; node = node.nextSibling)
          if (node.nodeType == 1 && node.tagName != 'BR' && node.tagName != 'WBR')
                break;
                
        if (node != endNode)
            return;

        // create the range and TeXify it                        
        var texRange = document.createRange();
        texRange.setStart(startNode, startNodeOpen);
        texRange.setEnd(endNode, endNodeClose);
		
        var tex = texRange.toString();
        tex = tex.substring(2, tex.length - 2);
        
        texRange.detach();
        
        var deleteRange = document.createRange();
        deleteRange.setStart(startNode, startNodeOpen);
        deleteRange.setEndBefore(endNode);
        
        deleteRange.deleteContents();
        deleteRange.insertNode(createTeXifiedImageElement(tex));
        
        var lastString = endNode.nodeValue.substring(endNodeClose);
        endNode.parentNode.insertBefore(document.createTextNode(lastString), endNode);
        endNode.nodeValue = '';
        
        deleteRange.detach();
    }
}

// translates a list of indexes in a node's textContent into node/offset
// pairs matching to the actual DOM tree
textContentData = function(baseNode, relevantIndexes, riIndex, index, result) {
    var relIndex = relevantIndexes[riIndex];
    
    // go through all children and check their textContent. in the meanwhile
    // remember which index we are currently in, and then recurse until finding
    // the actual node which contains the relevant index
    
    // implementation is based on the definition of textContent on
    // http://developer.mozilla.org/en/docs/DOM:element.textContent#Notes
    for (var child = baseNode.firstChild; child != null && riIndex < relevantIndexes.length; child = child.nextSibling) {
        if (child.nodeType == 7 || child.nodeType == 8)
            continue;
            
        var newIndex = index + child.textContent.length;
        
        while (newIndex > relIndex) {
            if (child.nodeType == 3 || child.nodeType == 4) {
                result.push({node: child, offset: relIndex - index});
                riIndex++;
            }
            else {
                result = textContentData(child, relevantIndexes, riIndex, index, result);
                riIndex = result.length;
            }

            if (riIndex >= relevantIndexes.length)
                break;

            relIndex = relevantIndexes[riIndex];
        }
        
        index = newIndex;
    }
    
    return result;
}

// TeXify a single window/frame
function TeXAFrame(win) {
    try {
        // register our keydown listener if this is a new frame
        if (!win.document.body.hasAttribute('_texified')) {
            win.document.addEventListener('keydown', onKeyDown, true);
            win.document.body.setAttribute('_texified', true);
        }
    
        var text = win.document.body.textContent;
        var relevantIndexes = [];
        
        // calculate the relevant indexes to search for afterwards
        // (the indexes of [; and ;])
        while ((theMatch = matchRegexpGlobal.exec(text)) != null) {
            relevantIndexes.push(theMatch.index);
            relevantIndexes.push(theMatch.index + theMatch[0].length - 1);
        }
        
        // get the mapping from these indexes to nodes/offset pairs
        var data = textContentData(win.document.body, relevantIndexes, 0, 0, []);
        
        for (var i = 0; i < data.length; i += 2)
            TeXifyRange(data[i].node, data[i].offset, data[i+1].node, data[i+1].offset + 1);
		
		 // BS 20081227
		// add listeners to images. these may have been lost during saves in Google Docs
		addunTeXifyImageEventListeners(win);
    }
    catch (e) { // security exception
    }
        
    // do the same for all frames in this window/frame
    for (var i = 0, n = win.frames.length; i < n; i++)
        TeXAFrame(win.frames[i]);
}


// TeXify a node that was previously unTeXified
function reTeXifyImage(event) {
    //var text = event.currentTarget.innerHTML;
    var text = event.currentTarget.textContent;  // BS 20081227 - fix bug where certain HTML escape chars are not handled correct
    var imgElement = createTeXifiedImageElement(text.substring(1, text.length - 1));
    event.currentTarget.parentNode.replaceChild(imgElement, event.currentTarget);    
}

// switch an image that was TeXified back to text.
// removes the ;s so that it doesn't get re-TeXified immediately
// double-click will re-TeXify is, and also adding the ;s back will do the job.
function unTeXifyImage(event) {
    var imgElement = event.currentTarget;
    
    var newSpanElement = document.createElement('span');
    newSpanElement.appendChild(document.createTextNode('[' + imgElement.title + ']'));
    newSpanElement.addEventListener('dblclick', reTeXifyImage, false);
    
    var newOuterSpanElement = document.createElement('span');
    newOuterSpanElement.appendChild(newSpanElement);

    imgElement.parentNode.replaceChild(newOuterSpanElement, imgElement);    
}

// simulate the user typing a string
function simulateTextTyping(target, str) {
    for (var i = 0; i < str.length; i++) {
        var keyEvent = document.createEvent("KeyboardEvent");
        keyEvent.initKeyEvent("keypress", true, true, null, false, false, false, false, 0, str.charCodeAt(i));
        target.dispatchEvent(keyEvent);
    }
}

// handle the keydown event -- if the key was ctrl-shift-; write our signature
function onKeyDown(event) {
    // ctrl-shift-;
    if (event.keyCode == 59 && (event.ctrlKey || event.metaKey) && event.shiftKey) { 
        // we use typing simulation in order to it to work not only in "regular" html components but also in 
        // high-tech javascript hacks like gmail and hotmail
        simulateTextTyping(event.target, signature);
    }
}

// TeXifies the world
function TeXTheWorld() {
    var startTime = new Date();
    TeXAFrame(window.top);
}

 // BS 20081227
// adds Event Listener to images
function addunTeXifyImageEventListeners(win) {
	
	var allDivs = win.document.evaluate(
		"//img[contains(@alt, '[;')]",
		win.document,
		null,
		XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
		null);
	
	for (var i = 0; i < allDivs.snapshotLength; i++)
		allDivs.snapshotItem(i).addEventListener('dblclick', unTeXifyImage, false);

}

if (self == top) { // run only in the top frame. we do our own frame parsing

   //************************************************************
   //This part only runs on Reddit
   if(document.location.href.indexOf('reddit.com') != -1)
   {
      //Set the script to run after the page is loaded so it can find elements
      setTimeout(loadmathbox,500);
   }
   //************************************************************

    TeXTheWorld(); // since we must handle dynamically created frames
    setInterval(TeXTheWorld, 3000);
}
      
// ********************************************************************
// Code added for Reddit /r/math users
// ********************************************************************



//Find any element which is tagged with the class name given.
function getElementsByClass(searchClass,node,tag)
{
   //Code for this function by Dustin Diaz
   //http://www.dustindiaz.com/getelementsbyclass/

   try
   {
      var classElements = new Array();
      if ( node == null )
         node = document;
      if ( tag == null )
         tag = '*';

      var els = node.getElementsByTagName(tag);
      var elsLen = els.length;
      var pattern = new RegExp('(^|\\s)'+searchClass+'(\\s|$)');

      for(i = 0, j = 0; i < elsLen; i++)
      {
         if(pattern.test(els[i].className))
         {
            classElements[j] = els[i];
            j++;
         }
      }

      return classElements;
   }
   catch(err)
   {
      alert("Error in getElementsByClass:\n\n" + err);
   }
}

//Adds a settings box to the sidebar for Reddit.
function loadmathbox()
{
   //Localize globals for performance
   var doc = document;
   var windoc = window.document;
   var pageFooter = getElementsByClass("footer rounded");

   //If the page footer has not yet loaded, refresh

   if(pageFooter.length == 0 && !windoc.body.hasAttribute('_loadsuccess'))
   {
      //Once the page has sufficiently loaded, don't run this script again (used like a global var)
      windoc.body.setAttribute('_loadsuccess',true);

      setTimeout(loadmathbox,500);
      return;
   }

   var pageSide = getElementsByClass("side");

   //The inbox does not have a sidebar.
   if(pageSide.length > 0)
   {
      var mathBox = doc.createElement("div");
      var mathBoxContent = doc.createElement("div");
      var mathBoxHeader = doc.createElement("H1");

      mathBoxHeader.innerHTML = "TexTheWorld";

      mathBox.setAttribute('class','sidecontentbox');
      mathBoxContent.setAttribute('class','content');
      mathBox.appendChild(mathBoxHeader);
      mathBox.appendChild(mathBoxContent);

      var disableCacheLink = doc.createElement("a");

      formulaCaching = getFormulaCachingPreference();

      if(formulaCaching)
         disableCacheLink.appendChild(doc.createTextNode("Turn off formula caching."));
      else
         disableCacheLink.appendChild(doc.createTextNode("Turn on formula caching."));

      //Create a closure to pass userName by value instead of by reference
      disableCacheLink.addEventListener("click", toggleFormulaCaching, false);

      //The mouse cursor won't become a "hand" unless there's something in the href
      disableCacheLink.setAttribute("href","javascript:void();");

      mathBoxContent.appendChild(disableCacheLink);
      mathBoxContent.appendChild(doc.createElement("br"));

      pageSide[0].appendChild(mathBox);
   }
}

//Turn on/off the formula caching feature (it causes problems with work firewalls for some users)
function toggleFormulaCaching()
{
   if(localStorage.getItem("formulaCaching"))
   {
      var formulaCaching = localStorage.getItem("formulaCaching");

      //Store it to local storage
      if(formulaCaching == "true")
         localStorage.setItem('formulaCaching', false);
      else
         localStorage.setItem('formulaCaching', true);
   }
   else
   {
      //It defaults to have caching on
      localStorage.setItem('formulaCaching', false);
   }

   //Reload the page without formula caching on.
   window.location.reload();
}


//Returns the user's preference for math formula caching
function getFormulaCachingPreference()
{
   if(localStorage.getItem("formulaCaching"))
   {
      formulaCaching = localStorage.getItem("formulaCaching");

      if(formulaCaching == "true")
         return true;
      else
         return false;
   }
   else
   {
      //Default to on.
      window.localStorage.setItem('formulaCaching', true);
      return true;
   }
}