// ==UserScript==
// @name           Type Reset
// @namespace      tag:wojcikm4@msu.edu,2007-10:scripts/public
// @description    Various functions for operating on text on a page
// ==/UserScript==


function TypeChange(action)
{
   /***
   Helper functions.
   ***/

   /* An iterator for the nodes that calls a callback on each character */
   function processNodeChars(visitor,nodes)
   {
      /* Iterate over text nodes */
      for (var t = 0; t < nodes.snapshotLength; t++)
      {
         /* Get the text as an array */
         var node = nodes.snapshotItem(t);
         if (!node.data || !node.data.length) continue;
         var text = node.data.split("");

         /* Map it using the visitor */
         var newtext = text.map(visitor);

         /* Change the text node to the new text */
         node.data = newtext.join("");
      }
   }

   /* An iterator for the nodes that calls a callback on each node */
   function processNodes(visitor,nodes)
   {
      /* Iterate over text nodes */
      for (var t = 0; t < nodes.snapshotLength; t++)
      {
         /* Get the text */
         var node = nodes.snapshotItem(t);
         if (!node.data || !node.data.length) continue;
         visitor(node);
      }
   }

   /* Get all nodes after our div, matching the specified pattern */
   function getNodes(path)
   {

      /* Get XPath list of text nodes outside our special div */
      var nodes = document.evaluate
      (
         "//div[@id='visrhetctl']/following::" + path
       , document
       , null
       , XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE
       , null
      );

      GM_log("found " + nodes.snapshotLength + " nodes");
      return nodes;
   }

   /* Create or find an output area */
   function outputArea()
   {
      var oa = document.getElementById("visrhetout");
      if (! oa)
      {
         var vrctr = document.getElementById("visrhetctr");
         if (! vrctr) return null;
         oa = document.createElement("div");
         oa.id = "visrhetout";
         vrctr.appendChild(oa);
      }
      else
      {
         /* Clear it out */
         oa.innerHTML = "";
         oa.style.display = "";
      }
      return oa;
   }



   /***
   Define our various subfunctions.
   ***/

   /* Run all text through Rot-13 */
   function doRot13()
   {
      /* Helpers for performing Rot-13 (assumes contiguous ranges!) */
      var LowerBase = "a".charCodeAt(0),
          UpperBase = "A".charCodeAt(0);

      /* Map it to its rot-13 equivalent */
      processNodeChars
      (
         function(char)
         {
            /* Get character code */
            var val = char.charCodeAt(0);

            if (char.match(/[a-z]/))
            {
               /* Convert to the rotated lowercase letter */
               return String.fromCharCode
               (
                  ((val - LowerBase + 13) % 26) + LowerBase
               );
            }
            else if (char.match(/[A-Z]/))
            {
               /* Convert to the rotated uppercase letter */
               return String.fromCharCode
               (
                  ((val - UpperBase + 13) % 26) + UpperBase
               );
            }
            else
            {
               /* No change */
               return char;
            }
         }
       , getNodes("text()")
      );
   };

   /* Convert all text into P's and q's */
   function doPandQ()
   {
      /* Map capital letters to P and lowercase to q, numerals to 8 */
      processNodeChars
      (
         function(char)
         {
            if (char.match(/[A-Z]/))
               return "P";
            else if (char.match(/[a-z]/))
               return "q";
            else if (char.match(/[0-9]/))
               return "8";
            else
               return char;
         }
       , getNodes("text()")
      );
   };

   /***
   For each letter, substitute one of the same case with a similar shape
   (considering ascenders and descenders, etc). This is based on typical
   shapes, and won't correspond as well in typefaces with unusual shapes.
   It also only handles English letters.

   I could make this reversible, or better have it store the state and
   include a set of reverse mappings (the transformation is information-
   preserving), but it's just as easy to refresh the page.
   ***/
   function doSubst()
   {
      var LowerBase = "a".charCodeAt(0),
          UpperBase = "A".charCodeAt(0),
          DigitBase = "0".charCodeAt(0);
      var UpperMap = "VSGPKYQNTLBJWMCFOXZIDUHRAE",
          LowerMap = "ohabcdjklgtinueqyszfvxmwpz",
          DigitMap = "8750924136";

      processNodeChars
      (
         function(char)
         {
            var val = char.charCodeAt(0);

            if (char.match(/[a-z]/))
            {
               return LowerMap.charAt(val - LowerBase);
            }
            else if (char.match(/[A-Z]/))
            {
               return UpperMap.charAt(val - UpperBase);
            }
            else if (char.match(/[0-9]/))
            {
               return DigitMap.charAt(val - DigitBase);
            }
            else
            {
               /* No change */
               return char;
            }
         }
       , getNodes("text()")
      );
   }

   /* Set all fonts to the given family */
   function doFontFamily(family,nodes)
   {
      processNodes
      (
         function(node)
         {
            var style = getComputedStyle(node.parentNode,"");
            if (style && style.fontFamily)
               node.parentNode.style.fontFamily = family;
         }
       , nodes? nodes : getNodes("text()")
      );
   };

   /* Build a list of the fonts in the document in the output area */
   function doFontList(nodes)
   {
      var outDiv = outputArea();
      if (! outDiv)
      {
         alert("Fail: output area unavailable");
         return;
      }
      outDiv.style.backgroundColor = "white";

      var fontList = [];

      function addFontIfNew(font)
      {
         var c;
         if (font == "") return;
         for (c = 0; c < fontList.length; c++)
            if (fontList[c] == font) break;
         if (c == fontList.length)
            fontList.push(font);
      };

      processNodes
      (
         function(node)
         {
            var style = getComputedStyle(node.parentNode, "");
            if (style && style.fontFamily) addFontIfNew(style.fontFamily);
         }
       , nodes? nodes : getNodes("text()")
      );

      var c;
      while (c = fontList.shift())
      {
         var cEntry = document.createElement("p");
         cEntry.innerHTML = c;
         cEntry.style.fontFamily = c;
         outDiv.appendChild(cEntry);
      }

      var closer = document.createElement("p");
      closer.innerHTML =
         "<a href='#' "
       + "onclick=\""
       + "   var oa = document.getElementById('visrhetout');"
       + "   oa.style.display='none';"
       + "\">"
       + "Close this!"
       + "</a>";
      outDiv.appendChild(closer);
   };


   /***
   Process the button click by executing the appropriate subfunction.
   ***/

   switch (action)
   {
      case "Rot13": doRot13(); break;
      case "PandQ": doPandQ(); break;
      case "Subst": doSubst(); break;

      case "FontList": doFontList(null); break;

      case "Serif":    doFontFamily("serif", null);      break;
      case "SanSerif": doFontFamily("sans-serif", null); break;

      case "SetFamilyAll":
         var family = RetrieveText("Family");
         GM_log("SetFamilyAll: Family is " + family);
         doFontFamily(family, null);
         break;
      case "SetFamilyH":
         var family = RetrieveText("Family");
         GM_log("SetFamilyH invoked with " + family);
         /* Do h1, h2, etc */
         for (var lvl=1; lvl <= 6; lvl++)
            doFontFamily(family, getNodes("h" + lvl + "//text()"));
         /* Do some other header-like things */
         var headerContainers =
         [
            "dt", "caption", "label"
         ];
         for (var idx = 0; idx < headerContainers.length; idx++)
            doFontFamily(family, getNodes(headerContainers[idx] + "//text()"));
         break;
      case "SetFamilyM":
         var family = RetrieveText("Family");
         GM_log("SetFamilyM invoked");
         var textContainers =
         [
            "div", "p", "address", "a", "q", "dd", "li", "sub", "sup", "span",
            "td"
         ];
         for (var idx = 0; idx < textContainers.length; idx++)
            doFontFamily(family, getNodes(textContainers[idx] + "//text()"));
         break;
   }

   return false;
}

function RetrieveText(name)
{
   if (typeof window.typereset == "undefined")
   {
      GM_log("No typereset attribute ");
      return "";
   }
   else if (typeof window.typereset[name] == "undefined")
   {
      GM_log("No typereset attribute named " + name);
      return "";
   }

   return window.typereset[name];
}


/* Create the div and the controls */
function AddControls()
{
   var visrhetDiv = document.getElementById("visrhetctl");
   if (! visrhetDiv)
   {
      visrhetDiv = document.createElement("div");
      visrhetDiv.id = "visrhetctl";
      visrhetDiv.width = "100%";
      visrhetDiv.style.background = "white";
      visrhetDiv.style.color = "black";
      visrhetDiv.style.borderBottomStyle = "solid";
      visrhetDiv.style.borderBottomWidth = "1px";
      visrhetDiv.style.marginBottom = "1px";
      visrhetDiv.style.paddingBottom = "2px";

      /***
      Setting overflow establishes a new block-formatting context, so that
      the page proper will start below the floated contents of this div.
      ***/
      visrhetDiv.style.overflow = "auto";

      /* Create the containing div with the show/hide control */
      vrctrDiv = document.createElement("div");
      vrctrDiv.id = "visrhetctr";
      vrctrDiv.style.background = "white";
      vrctrDiv.style.color = "black";
      vrctrDiv.innerHTML =
         "<a href=\"#\" "
       + "onClick=\""
       + "  var visrhetDiv = document.getElementById('visrhetctl');"
       + "  if (visrhetDiv.style.display == 'none')"
       + "     visrhetDiv.style.display = '';"
       + "  else"
       + "     visrhetDiv.style.display = 'none';"
       + "\">"
       + "Visual Rhetoric Controls"
       + "</a>";

      /* Initially the controls are hidden */
      visrhetDiv.style.display = "none";

      /***
      Add the inner container to the outer container, and the outer container
      to the page.
      ***/
      vrctrDiv.appendChild(visrhetDiv);
      document.body.insertBefore(vrctrDiv, document.body.firstChild);
   }

   var typeresetDiv = document.createElement("div");
   typeresetDiv.id = "typereset";

   typeresetDiv.innerHTML =
      '<p style="font-size: large; font-weight: bold; font-family: sansserif;">'
    + '   Type Reset'
    + '</p>'
    + '<p style="font-size: small; font-family: serif;">'
    + '   <a href="mailto:wojcikm4@msu.edu">Michael Wojcik</a>,'
    + '   Rhetoric &amp; Writing, Michigan State University'
    + '</p>'
    + '<button id="Rot13">Rot-13</button>'
    + '<button id="PandQ">P\'s and q\'s</button>'
    + '<button id="Subst">Scramble</button>'
    + '<button id="FontList">List Fonts</button>'
    + '<button id="Serif">Serif</button>'
    + '<button id="SanSerif">Sans-Serif</button>'
    + '<p style="font-family: sans-serif; font-size: medium;">Set '
    + '   <button id="SetFamilyAll">all fonts</button>'
    + ' / <button id="SetFamilyH">headers</button>'
    + ' / <button id="SetFamilyM">main text</button>'
    + ' to: <input id="Family" type="text" /></p>'
    ;

   typeresetDiv.style.width = "45%";
   typeresetDiv.style.cssFloat = "left";
   typeresetDiv.style.clear = "none";
   typeresetDiv.style.paddingTop =
   typeresetDiv.style.paddingLeft =
   typeresetDiv.style.paddingRight =
   typeresetDiv.style.paddingBottom = "1px";

   typeresetDiv.style.background = "white";
   typeresetDiv.style.color = "black";

   visrhetDiv.appendChild(typeresetDiv);

   /* Set the buttons' onclick attributes to invoke the handler */
   function SetButtonAction(name)
   {
      document.getElementById(name).addEventListener
      (
         "click"
       , function(){TypeChange(name)}
       , true
      );
   }

   SetButtonAction("Rot13");
   SetButtonAction("PandQ");
   SetButtonAction("Subst");
   SetButtonAction("FontList");
   SetButtonAction("Serif");
   SetButtonAction("SanSerif");
   SetButtonAction("SetFamilyAll");
   SetButtonAction("SetFamilyH");
   SetButtonAction("SetFamilyM");

   /* Capture the contents of text fields */
   function SaveText(name, value)
   {
      GM_log("Setting window.typereset's " + name + " attribute to " + value);
      if (typeof window.typereset == "undefined")
         window.typereset = new Object();
      window.typereset[name] = value;
   }

   function CaptureTextField(name)
   {
      var elem = document.getElementById(name);
      if (! elem) return;
      elem.addEventListener
      (
         "change"
       , function(){SaveText(name, elem.value)}
       , true
      );
   }

   CaptureTextField("Family");
}

window.addEventListener("load", AddControls, true);

