Greasy Fork

Text Highlighter 2015

Automatically highlight user-defined text with Seek function (2015-09-20)

目前为 2015-10-11 提交的版本。查看 最新版本

// ==UserScript==
// @name          Text Highlighter 2015
// @author        erosman and Jefferson "jscher2000" Scher
// @namespace     JeffersonScher
// @version       2.0.1
// @description   Automatically highlight user-defined text with Seek function (2015-09-20)
// @include       https://greasyfork.org/*
// @grant         GM_registerMenuCommand
// @grant         GM_setValue
// @grant         GM_getValue
// @grant         GM_getResourceURL
// @copyright     Copyright 2015 Jefferson Scher. Portions created by erosman.
// @license       BSD 3-clause
// @resource      mycon http://www.jeffersonscher.com/gm/src/gfrk-TH15-ver201.png
// ==/UserScript==
var script_about = "https://greasyfork.org/en/scripts/3719-text-highlighter-dynamic";

/* --------- Note ---------
  TO INCLUDE SITES (only Greasy Fork is initially included):

  Go to Add-ons - User Scripts (Ctrl+Shift+a/Cmd+Shift+a on Firefox Windows/Mac)
  Click on the Script's Option
  Under User Settings Tab, Add Included/Excluded Pages that you want the script to run on
  Click OK

  Note from erosman: If you find that another script clashes with this script, set Text Highlighter to Execute first.
  Go to Add-ons - User Scripts ('Ctrl+ Shift + a' on Firefox)
  Right Click on the Script
  On the context menu click: Execute first

  On Add-ons - User Scripts, you can also Click on the Execution Order (top Right) and
  change the execution order so that Text Highlighter runs before those scripts that clashes with it.

  --------- History ---------
  http://userscripts-mirror.org/scripts/show/292083
  http://userscripts-mirror.org/topics/187122.html
*/

(function() { // anonymous function wrapper, used for error checking & limiting scope
  'use strict';
  
  if (window.self !== window.top) { return; } // end execution if in a frame

  // sample keyword+style object to get started
  var hlobjDefault = {
    "set100" : {
      keywords : "scripts|script",
      type: "string",
      textcolor : "rgb(0,0,0)",
      backcolor : "rgb(255,255,128)",
      fontweight : "inherit",
      custom : "",
      enabled : "true",
      visible : "true",
      updated : ""
    },
    "set099" : {
      keywords : "site",
      type: "word",
      textcolor : "rgb(0,0,0)",
      backcolor : "rgb(255,192,255)",
      fontweight : "inherit",
      custom : "",
      enabled : "true",
      visible : "true",
      updated : ""
    },
    "set098" : {
      keywords : "^September \\d{1,2}",
      type: "regex",
      textcolor : "rgb(0,0,0)",
      backcolor : "rgb(192,255,255)",
      fontweight : "inherit",
      custom : "",
      enabled : "true",
      visible : "true",
      updated : ""
    }
  };
  var kwhieditstyle = ["rgb(0,0,255)","rgb(255,255,0)","inherit",""];

  // read pref storage: keyword-style sets
  var hljson = GM_getValue("kwstyles");
  if (!hljson || hljson.length == 0){
    var hlobj = hlobjDefault;
    // check for legacy preferences
    var kwold = GM_getValue("keywords");
    if (kwold) if(kwold.length > 0) {
      hlobj.set100.keywords = kwold.split(',').join('|');
    }
    var hlold = GM_getValue("highlightStyle");
    if (hlold) if(hlold.length > 0) {
      // really should try to parse this, but for now...
      hlobj.set100.custom = hlold;
    }
    // save starting values
    hljson = JSON.stringify(hlobj);
    GM_setValue("kwstyles",hljson);
  } else {
    var hlobj = JSON.parse(hljson);
  }
  // global keys array
  var hlkeys = Object.keys(hlobj);

  // read/set other prefs
  var hlbtnvis = GM_getValue("hlbtnvis");
  if (!hlbtnvis){
    hlbtnvis = "on";
    GM_setValue("hlbtnvis",hlbtnvis);
  }
  var hlprecode = GM_getValue("hlprecode");
  if (!hlprecode){
    hlprecode = true;
    GM_setValue("hlprecode",hlprecode);
  }
  var hlnextset = GM_getValue("hlnextset");
  if (!hlnextset){
    hlnextset = 101;
    GM_setValue("hlnextset",hlnextset);
  }
  
  // Inject CSS
  function insertCSS(setkeys){
    for (var j = 0; j < setkeys.length; ++j){
      var hlset = setkeys[j];
      if (hlobj[hlset].visible == "true"){
        var rule = "."+hlset+"{display:inline!important;";
        if (hlobj[hlset].textcolor.length > 0) rule += "color:"+hlobj[hlset].textcolor+";";
        if (hlobj[hlset].backcolor.length > 0) rule += "background-color:"+hlobj[hlset].backcolor+";";
        if (hlobj[hlset].fontweight.length > 0) rule += "font-weight:"+hlobj[hlset].fontweight+";";
        if (hlobj[hlset].custom.length > 0) rule += hlobj[hlset].custom+";";
        rule += "}";
        var setrule = document.querySelector('style[hlset="' + hlset +'"]');
        if (!setrule){
          var s = document.createElement("style");
          s.type = "text/css";
          s.setAttribute("hlset", hlset);
          s.appendChild(document.createTextNode(rule));
          document.body.appendChild(s);
        } else {
          setrule.innerHTML = rule;
        }
      }
    }
  }
  insertCSS(hlkeys);

  // Main workhorse routine
  function THmo_doHighlight(el,subset){
    if (subset) var keyset = subset;
    else var keyset = hlkeys;
    for (var j = 0; j < keyset.length; ++j) {
      var hlset = keyset[j];
      if (hlobj[hlset].visible == "true" && hlobj[hlset].enabled == "true"){
        var hlkeywords = hlobj[hlset].keywords;
        if (hlkeywords.length > 0) {
          if (hlobj[hlset].type != "regex"){
            var rQuantifiers = /[-\/\\^$*+?.()[\]{}]/g;
            hlkeywords = hlkeywords.replace(rQuantifiers, '\\$&');
            if (hlobj[hlset].type == "word"){
              hlkeywords = "\\b" + hlkeywords.replace(/\|/g, "\\b|\\b") + "\\b";
            }
          }
          //console.log("hlset:"+hlset+"\nhlkeywords:"+hlkeywords);
          var pat = new RegExp('(' + hlkeywords + ')', 'gi');
          var span = document.createElement('thdfrag');
          span.setAttribute("thdcontain","true");
          // getting all text nodes with a few exceptions
          if (hlprecode){
            var snapElements = document.evaluate(
                './/text()[normalize-space() != "" ' +
                'and not(ancestor::style) ' +
                'and not(ancestor::script) ' +
                'and not(ancestor::textarea) ' +
                'and not(ancestor::div[@id="thdtopbar"]) ' +
                'and not(ancestor::div[@id="kwhiedit"]) ' +
                'and not(parent::thdfrag[@txhidy15])]',
                el, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
          } else {
            var snapElements = document.evaluate(
                './/text()[normalize-space() != "" ' +
                'and not(ancestor::style) ' +
                'and not(ancestor::script) ' +
                'and not(ancestor::textarea) ' +
                'and not(ancestor::pre) ' +
                'and not(ancestor::code) ' +
                'and not(ancestor::div[@id="thdtopbar"]) ' +
                'and not(ancestor::div[@id="kwhiedit"]) ' +
                'and not(parent::thdfrag[@txhidy15])]',
                el, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
          }

          if (!snapElements.snapshotItem(0)) { break; }

          for (var i = 0, len = snapElements.snapshotLength; i < len; i++) {
            var node = snapElements.snapshotItem(i);
            // check if it contains the keywords
            if (pat.test(node.nodeValue)) {
              // create an element, replace the text node with an element
              var sp = span.cloneNode(true);
              sp.innerHTML = node.nodeValue.replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(pat, '<thdfrag class="THmo '+hlset+'" txhidy15="'+hlset+'">$1</thdfrag>');
              node.parentNode.replaceChild(sp, node);
              // try to un-nest containers
              if (sp.parentNode.hasAttribute("thdcontain")) sp.outerHTML = sp.innerHTML;
            }
          }
        }
      }
    }
  }
  // first run
  THmo_doHighlight(document.body,null);
  
  // Add MutationObserver to catch content added dynamically
  var THmo_MutOb = (window.MutationObserver) ? window.MutationObserver : window.WebKitMutationObserver;
  if (THmo_MutOb){
    var THmo_chgMon = new THmo_MutOb(function(mutationSet){
      mutationSet.forEach(function(mutation){
        for (var i=0; i<mutation.addedNodes.length; i++){
          if (mutation.addedNodes[i].nodeType == 1){
            THmo_doHighlight(mutation.addedNodes[i],null);
          }
        }
      });
    });
    // attach chgMon to document.body
    var opts = {childList: true, subtree: true};
    THmo_chgMon.observe(document.body, opts);
  }

  // Set up top highlight/seek bar
  var kwhibar = document.createElement("div");
  kwhibar.id = "thdtopbar";
  if (hlbtnvis == "on") var btnchk = " checked=\"checked\"";
  if (hlprecode) var btnprecode = " checked=\"checked\"";
  else var btnchk = "";
  kwhibar.innerHTML = "<form id=\"thdtopform\" onsubmit=\"return false\"><p id=\"thdtopbarhome\"><a href=\"" + script_about + "\" target=\"_blank\" title=\"Go to script install page\">JS</a></p>" +
    "<div id=\"thdtopcurrent\"><p id=\"thdtopkeywords\" title=\"Click to View, Edit, Seek, or Add Keywords\">Click here to manage keyword/highlight sets</p>" +
    "<div id=\"thdtopdrop\" style=\"display:none;\"><div id=\"thdtable\"><table cellspacing=\"0\"><tbody id=\"kwhitbod\"></tbody></table></div><p><button id=\"btnkwhiadd\">Add New Set</button>" +
    "<span style=\"float:right\"><button id=\"btnkwhiexport\">Export Sets</button> [?Import?] <button class=\"btnkwhiclose\" onclick=\"document.getElementById('thdtopdrop').style.display='none';return false;\">X</button></span></p></div></div>" +
    "<div id=\"thdtopfindbuttons\"><button title=\"First match\" thdaction=\"f\"><b>l</b>&#x25c0;</button> <button title=\"Previous match\" thdaction=\"p\">&#x25c0;</button> <span id=\"thdseekdesc\">Seek</span> <button title=\"Next match\" thdaction=\"n\">&#x25b6;</button> <button title=\"Last match\" thdaction=\"l\">&#x25b6;<b>l</b></button><div id=\"thdseekfail\"></div></div>" +
    "<div id=\"thdtopoptions\"><div>Options</div><ul><li><label title=\"Float a button in the upper right corner of the document to quickly access this panel\"><input type=\"checkbox\" id=\"chkhbtn\"" + btnchk +
    "> Show H button</label></li><li><label title=\"Highlight matches in &lt;pre&gt; and &lt;code&gt; tags\"><input type=\"checkbox\" id=\"chkprecode\"" + btnprecode +
    "> Match in pre/code</label></li></ul></div>" +
    "<button class=\"btnkwhiclose\" onclick=\"document.getElementById('thdtopbar').style.display='none';document.getElementById('thdtopspacer').style.display='none';return false;\" style=\"float:right\">X</button></form>" +
    "<style type=\"text/css\">#thdtopbar{position:fixed;top:0;left:0;height:26px;width:100%;padding:0;color:#024;background:#ddd;font-family:sans-serif;font-size:16px;line-height:16px;border-bottom:1px solid #024;z-index:2500;display:none} " +
    "#thdtopbar,#thdtopbar *{box-sizing:content-box;} #thdtopform{display:block;position:relative;float:left;width:100%;margin:0;border:none;} " +
    "#thdtopbarhome,#thdtopcurrent,#thdtopfindbuttons,#thdtopoptions{float:left;top:0;left:0;margin:0;padding:5px 8px 4px;border-right:1px solid #fff;font-size:16px;} " +
    "#thdtopbarhome{width:22px;text-align:center;overflow:hidden;} #thdtopbarhome a{display:block;} #thdtopbarhome img{display:block;border:none;border-radius:3px;padding:3px;margin:-3px 0 -4px 0;background-color:#fff} " +
    "#thdtopfindbuttons{padding-bottom:1px;position:relative} #thdtopfindbuttons button{margin:-5px 0 -2px 0;width:36px;height:22px;color:#024;background:#f0f0f0;border:1px solid #024;border-radius:4px;padding:1px 3px;} " +
    "#thdtopfindbuttons button:hover{background:#ffa;} #thdseekdesc{cursor:pointer} #thdtopkeywords{margin:0;width:500px;cursor:pointer;} " +
    "#thdseekfail{display:none;position:absolute;top:30px;left:15px;z-index:2001;width:200px;color:#f8f8f8;background:#b00;border-radius:6px;text-align:center;font-size:12px;padding:3px}" +
    "#thdtopkeywords span{display:inline-block;width:100%;overflow:hidden;text-overflow:ellipsis;} #thdtable{max-height:600px;overflow-y:auto;overflow-x:hidden} " +
    "#thdtopdrop{position:absolute;top:26px;left:38px;width:500px;margin:0 -1px 0 -1px;padding:0 8px 8px 8px;background:#ddd;border:1px solid #024;border-top:none;border-radius:0 0 6px 6px;} " +
    "#thdtopdrop table{width:100%;background:#fff;border-top:1px solid #000;border-left:1px solid #000;table-layout:fixed} " +
    "#thdtopdrop td{padding:4px 4px; vertical-align:top;border-right:1px solid #000;border-bottom:1px solid #000;} #thdtopdrop td div{word-wrap:break-word} #thdtopdrop p{margin-top:8px;margin-bottom:0;} " +
    "#thdtopoptions{position:relative;width:160px;height:26px;padding:0 8px;} #thdtopoptions > div{padding:5px 0 4px;} " +
    "#thdtopoptions ul{position:absolute;top:26px;left:0;width:160px;margin:0 -1px 0 -1px;padding:0 8px 8px 8px;background:#ddd;border:1px solid #024;border-top:none;border-radius:0 0 6px 6px;list-style:none;} " +
    "#thdtopoptions li{width:100%;float:left;padding:2px 0;} #thdtopoptions ul{display:none;} #thdtopoptions:hover ul{display: block;border:1px solid #024;border-top:none;} #thdtopoptions li:hover{background:#eee;}" +
    ".btnkwhiclose{float:right;font-size:11px;margin-top:2px;} .thdtype{color:#ccc;float:right;font-size:12px;padding-top:8px;} #thdtopbar label{font-weight:normal;display:inline;margin:0}</style>";
  document.body.appendChild(kwhibar);
  // Attach event handlers
  document.getElementById("thdtopkeywords").addEventListener("click",thddroptoggle,false);
  document.getElementById("kwhitbod").addEventListener("click",kwhiformevent,false);
  document.getElementById("btnkwhiadd").addEventListener("click",kwhinewset,false);
  document.getElementById("btnkwhiexport").addEventListener("click",kwhiexport,false);
  document.getElementById("thdtopfindbuttons").addEventListener("click",thdseek,false);
  document.getElementById("chkhbtn").addEventListener("click",kwhihbtn,false);
  document.getElementById("chkprecode").addEventListener("click",kwhiprecode,false);
  // Add spacer at top of body
  var divsp = document.createElement("div");
  divsp.id = "thdtopspacer";
  divsp.setAttribute("style","clear:both;display:none");
  divsp.style.height = parseInt(27 - parseInt(window.getComputedStyle(document.body,null).getPropertyValue("margin-top"))) + "px";
  document.body.insertBefore(divsp, document.body.childNodes[0]);
  // Switch JS text to icon
  var JSBTN = document.createElement("img");
  JSBTN.src = GM_getResourceURL("mycon");
  document.querySelector("#thdtopbar a").textContent = "";
  document.querySelector("#thdtopbar a").appendChild(JSBTN);
  // Add menu item
  GM_registerMenuCommand("Show Text Highlighter Bar - View, Edit, Add Keywords and Styles", editKW);
  // Inject H button
  if (hlbtnvis == "off") var hbtndisp = ' style="display:none"';
  else hbtndisp = '';
  var dNew = document.createElement("div");
  dNew.innerHTML = '<button id="btnshowkwhi"' + hbtndisp + '>H</button><style type="text/css">#btnshowkwhi{position:fixed;top:4px;right:4px;opacity:0.2;' +
    'color:#000;background-color:#ffa;font-weight:bold;font-size:12px;border:1px solid #ccc;border-radius:4px;padding:2px 3px;z-index:1999;min-width:22px;min-height:22px}' +
    '#btnshowkwhi:hover{opacity:0.8}@media print{#btnshowkwhi{display:none;}}</style>';
  document.body.appendChild(dNew);
  document.getElementById("btnshowkwhi").addEventListener("click",editKW,false);
  
  function editKW(e){
    refreshSetList();
    // show form
    document.getElementById("thdtopbar").style.display = "block";
    document.getElementById("thdtopspacer").style.display = "block";
  }
  function thdDropSetList(e){
    refreshSetList();
    document.getElementById("thdtopdrop").style.display = "block";
  }
  function thddroptoggle(e){
    if (document.getElementById("thdtopdrop").style.display == "none") thdDropSetList();
    else document.getElementById("thdtopdrop").style.display = "none";
  }
  function refreshSetList(e){
    // clear old rows from form
    document.getElementById("kwhitbod").innerHTML = "";
    // populate data - hlobj is global
    for (var j = 0; j < hlkeys.length; ++j){
      var hlset = hlkeys[j];
      if (hlobj[hlset].visible == "true"){
        if (hlobj[hlset].enabled == "true") var strchk = ' checked=\"checked\"';
        else var strchk = '';
        var newrow = document.createElement("tr");
        var thdtypenote = '';
        newrow.setAttribute("kwhiset", hlset);
        if(hlobj[hlset].type != "string"){
          thdtypenote = '<span class="thdtype">' + hlobj[hlset].type + '</span>';
        } 
        if (j == 0){
          newrow.innerHTML = '<td style=\"width:286px\"><div class=\"' + hlset + '\">' + hlobj[hlset].keywords + '</div>' + thdtypenote + '</td>' +
            '<td style=\"width:195px\"><button kwhiset=\"' + hlset + '\" title=\"Bring matches into view\">Seek</button> ' +
            '<button kwhiset=\"' + hlset + '\">Edit</button> <label><input type=\"checkbox\" kwhiset=\"' + hlset + 
            '\"' + strchk + '"> Enabled </label></td>';
        } else {
          newrow.innerHTML = '<td><div class=\"' + hlset + '\">' + hlobj[hlset].keywords + '</div>' + thdtypenote + '</td>' +
            '<td><button kwhiset=\"' + hlset + '\" title=\"Bring matches into view\">Seek</button> ' +
            '<button kwhiset=\"' + hlset + '\">Edit</button> <label><input type=\"checkbox\" kwhiset=\"' + hlset + 
            '\"' + strchk + '"> Enabled </label></td>';
        }
        document.getElementById("kwhitbod").appendChild(newrow);
      }
    }
  }
  
  function kwhiformevent(e){
    if (e.target.nodeName == "INPUT"){ // Enabled checkbox
      var hlsetnum = e.target.getAttribute("kwhiset");
      kwhienabledisable(hlsetnum, e.target.checked);
    } 
    if (e.target.nodeName == "BUTTON"){ // Call up edit form or find bar
      var hlset = e.target.getAttribute('kwhiset');
      if (e.target.textContent == "Edit"){
        // set set number attribute
        document.querySelector('#kwhiedit tr').setAttribute('kwhiset', hlset);
        // set class for keywords
        document.querySelector('#kwhiedit td:nth-of-type(1) p:nth-of-type(1)').className = hlset;
        // enter placeholder text & type
        document.querySelector('#kwhiedit td:nth-of-type(1) p:nth-of-type(1)').textContent = hlobj[hlset].keywords;
        document.getElementById("kwhipattype").selectedIndex = 0;
        if (hlobj[hlset].type == "word") document.getElementById("kwhipattype").selectedIndex = 1;
        if (hlobj[hlset].type == "regex") document.getElementById("kwhipattype").selectedIndex = 2;
        // set style editing to default and override with set rules
        kwhieditstyle = ["rgb(0,0,255)","rgb(255,255,0)","inherit",""]; // defaults
        if (hlobj[hlset].textcolor.length > 0) kwhieditstyle[0] = hlobj[hlset].textcolor;
        if (hlobj[hlset].backcolor.length > 0) kwhieditstyle[1] = hlobj[hlset].backcolor;
        if (hlobj[hlset].fontweight.length > 0) kwhieditstyle[2] = hlobj[hlset].fontweight;
        if (hlobj[hlset].custom.length > 0) kwhieditstyle[3] = hlobj[hlset].custom;
        kwhiShowEditForm();
      }
      if (e.target.textContent == "Seek"){
        // Populate current seek set to #thdtopkeywords
        var divDataTD = e.target.parentNode.previousElementSibling;
        document.getElementById("thdtopkeywords").innerHTML = "<i>Seeking:</i> " + divDataTD.firstChild.outerHTML;
        // Store set to seek in #thdtopfindbuttons
        document.getElementById("thdtopfindbuttons").setAttribute("thdseek", hlset);
        // Close Keyword Sets form
        document.getElementById('thdtopdrop').style.display='none';
        // Send click event to the "seek first" button
        document.getElementById('thdtopfindbuttons').children[0].click();
      }
    }
  }
  function kwhienabledisable(hlsetnum,enable){
    if (enable == false) {
      // Update object and persist to GM storage
      hlobj[hlsetnum].enabled = "false";
      hljson = JSON.stringify(hlobj);
      GM_setValue("kwstyles",hljson);
      // Unhighlight
      unhighlight(hlsetnum);
      // Clear seek info from bar if this set is there
      var seekset = document.getElementById("thdtopfindbuttons").getAttribute("thdseek");
      if (seekset){
        if(seekset.indexOf("|") > -1) seekset = seekset.split("|")[0];
        if (hlsetnum == seekset){
          document.getElementById("thdtopfindbuttons").setAttribute("thdseek","");
          document.getElementById("thdseekdesc").textContent = "Seek";
          document.getElementById("thdtopkeywords").innerHTML = "Click here to manage keyword/highlight sets";
        }
      } 
    } else {
      // Update object and persist to GM storage
      hlobj[hlsetnum].enabled = "true";
      hljson = JSON.stringify(hlobj);
      GM_setValue("kwstyles",hljson);
      // Highlight
      THmo_doHighlight(document.body,[hlsetnum]);
    }
  }
  function kwhinewset(e,kwtext){ // call up new set form
    // set set number attribute
    document.querySelector('#kwhiedit tr').setAttribute('kwhiset', 'new');
    // clear class for keywords
    document.querySelector('#kwhiedit td:nth-of-type(1) p:nth-of-type(1)').className = "";
    // enter placeholder text & default type
    if (kwtext) document.querySelector('#kwhiedit td:nth-of-type(1) p:nth-of-type(1)').textContent = kwtext;
    else document.querySelector('#kwhiedit td:nth-of-type(1) p:nth-of-type(1)').textContent = "larry|moe|curly";
    document.getElementById("kwhipattype").selectedIndex = 0;
    // set style editing to defaults
    kwhieditstyle = ["rgb(0,0,255)","rgb(255,255,0)","inherit",""];
    kwhiShowEditForm();
  }
  function kwhiShowEditForm(){
    var rule = "#stylecontrols>p>span{";
    if (kwhieditstyle[0].length > 0) rule += "color:"+kwhieditstyle[0]+";";
    if (kwhieditstyle[1].length > 0) rule += "background-color:"+kwhieditstyle[1]+";";
    if (kwhieditstyle[2].length > 0) rule += "font-weight:"+kwhieditstyle[2]+";";
    if (kwhieditstyle[3].length > 0) rule += kwhieditstyle[3]+";";
    document.getElementById("kwhiedittemp").innerHTML = rule + "}";
    populateRGB("txt",kwhieditstyle[0]);
    populateRGB("bkg",kwhieditstyle[1]);
    document.getElementById("fwsel").value = kwhieditstyle[2];
    document.getElementById("kwhicustom").value = kwhieditstyle[3];
    // show form
    document.getElementById("kwhiedit").style.display = "block";
  }
  function kwhiexport(e){
    prompt("JSON data\nPress Ctrl+c or right-click to copy\n ", JSON.stringify(hlobj));
  }
  function kwhihbtn(e){
    if (e.target.checked == false){
      hlbtnvis = "off";
      GM_setValue("hlbtnvis",hlbtnvis);
      document.getElementById("btnshowkwhi").style.display = "none";
    } else {
      hlbtnvis = "on";
      GM_setValue("hlbtnvis",hlbtnvis);
      document.getElementById("btnshowkwhi").style.display = "";
    }
  }
  function kwhiprecode(e){
    if (e.target.checked == false){
      // Update var, persist the preference, unhighlight, rehighlight
      hlprecode = false;
      GM_setValue("hlprecode",hlprecode);
      unhighlight(null);
      THmo_doHighlight(document.body);
    } else {
      // Update var, persist the preference, rehighlight
      hlprecode = true;
      GM_setValue("hlprecode",hlprecode);
      THmo_doHighlight(document.body);
    }
  }
  function thdseek(e){
    if (e.target.nodeName == "DIV") return; // ignore background clicks
    var seekset = e.currentTarget.getAttribute("thdseek");
    if (!seekset){ // user needs to select a set to seek in
      thdDropSetList();
    } else {
      var seekparams = seekset.split("|");
      var seekmatches = document.querySelectorAll('thdfrag[txhidy15="'+seekparams[0]+'"]');
      // Update or add total size of set; FIGURE OUT LATER: what if this changed??
      seekparams[1] = seekmatches.length;
      if (seekmatches.length > 0){
        if (e.target.nodeName == "THDFRAG"){ // re-scroll to the current reference
          thdshow(seekmatches[parseInt(seekparams[2])]);
        } else { // BUTTON
          var seekaction = e.target.getAttribute("thdaction");
          if (!seekaction) seekaction = "f";
          if (seekparams.length == 3){ // User has seeked in this set
            switch (seekaction){
              case "f":
                seekparams[2] = 0;
                var rtn = thdshow(seekmatches[parseInt(seekparams[2])]);
                if (rtn == false) seekagain("n");
                break;
              case "p":
                if (parseInt(seekparams[2]) > 0) {
                  seekparams[2] = parseInt(seekparams[2]) - 1;
                  var rtn = thdshow(seekmatches[parseInt(seekparams[2])]);
                  if (rtn == false){
                    if (parseInt(seekparams[2]) > 0) seekagain("p");
                    else seekfailnotc("No previous match visible");
                  } 
                } else {
                  seekfailnotc("Already reached first match");
                }
                break;
              case "n":
                if (parseInt(seekparams[2]) < (seekmatches.length-1)) {
                  seekparams[2] = parseInt(seekparams[2]) + 1;
                  var rtn = thdshow(seekmatches[parseInt(seekparams[2])]);
                  if (rtn == false){
                    if (parseInt(seekparams[2]) < (seekmatches.length-1)) seekagain("n");
                    else seekfailnotc("No later match visible");
                  } 
                } else {
                  seekparams[2] = (seekmatches.length-1); // in case it's too high, fix that here
                  seekfailnotc("Already reached last match");
                }
                break;
              case "l":
                seekparams[2] = (seekmatches.length-1);
                var rtn = thdshow(seekmatches[parseInt(seekparams[2])]);
                if (rtn == false) seekagain("p");
                break;
            }
          } else {
            seekparams[2] = 0;
            thdshow(seekmatches[parseInt(seekparams[2])]);
          }
          document.getElementById("thdtopfindbuttons").setAttribute("thdseek", seekparams.join("|"));
          document.getElementById("thdseekdesc").textContent = (parseInt(seekparams[2])+1) + " of " + seekparams[1];
        }
      } else {
        document.getElementById("thdseekdesc").textContent = "0 of 0";
      }
    }
  }
  function thdshow(elt){ // this could be much prettier with animation!
    elt.scrollIntoView();
    var rect = elt.getClientRects()[0];
    if (rect){ // scroll down if behind the control bar
      if (rect.top < 27) window.scroll(0, window.scrollY-27);
      return true;
    } else { // match is not visible
      return false;
    }
  }
  function seekagain(dir){
    switch (dir){
      case "p":
        seekfailnotc("Hidden, trying previous match...");
        window.setTimeout(function(){document.querySelector('button[thdaction="p"]').click();},250);
        break;
      case "n":
        seekfailnotc("Hidden, trying next match...");
        window.setTimeout(function(){document.querySelector('button[thdaction="n"]').click();},250);
        break;
    }
  }
  var evttimer;
  function seekfailnotc(txt){
    var sfdiv = document.getElementById("thdseekfail");
    sfdiv.textContent = txt;
    sfdiv.style.display = "block";
    if (evttimer) window.clearTimeout(evttimer);
    evttimer = window.setTimeout(function(){document.getElementById("thdseekfail").style.display="none";}, 800);
  }
  function unhighlight(setnum){
    if (setnum) var tgts = document.querySelectorAll('thdfrag[txhidy15="' + setnum + '"]');
    else var tgts = document.querySelectorAll('thdfrag[txhidy15]'); // remove ALL
    for (var i=0; i<tgts.length; i++){
      // Check for co-extant parent(s) to remove potentially stranded <span>s
      var parnode = tgts[i].parentNode, parpar = parnode.parentNode, tgtspan;
      if (parnode.hasAttribute("thdcontain") && parnode.innerHTML == tgts[i].outerHTML){
        parnode.outerHTML = tgts[i].textContent.replace(/</g, '&lt;').replace(/>/g, '&gt;');
        tgtspan = parpar;
      } else {
        tgts[i].outerHTML = tgts[i].textContent.replace(/</g, '&lt;').replace(/>/g, '&gt;');
        tgtspan = parnode;
      }
      tgtspan.normalize();
      if (tgtspan.hasAttribute("thdcontain")){
        parnode = tgtspan.parentNode;
        if (parnode){
          if (parnode.hasAttribute("thdcontain") && parnode.innerHTML == tgtspan.outerHTML && tgtspan.querySelectorAll('thdfrag[txhidy15]').length == 0){
            parnode.outerHTML = tgtspan.innerHTML;
          } else if (parnode.innerHTML == tgtspan.outerHTML && tgtspan.querySelectorAll('thdfrag[txhidy15]').length == 0) {
            parnode.innerHTML = tgtspan.innerHTML;
          }
        }
      }
    }
  }
  
  // Set up add/edit form
  var kwhied = document.createElement("div");
  kwhied.id = "kwhiedit";
  kwhied.innerHTML = "<form onsubmit=\"return false;\"><p style=\"margin-top:0\"><b>Edit/Add Keywords/Highlighting</b>" +
    "<button class=\"btnkwhiclose\" onclick=\"document.getElementById('kwhiedit').style.display='none'; return false;\">X</button>" +
    "</p><p>List longer forms of a word first to match both in full. Example: \"children|child\" will highlight both, but \"child|children\" " +
    "will only highlight child, it won't expand the selection to children.</p>" +
    "<table cellspacing=\"0\" style=\"table-layout:fixed\"><tbody><tr kwhiset=\"new\"><td style=\"width:45%\">" +
    "<p contenteditable=\"true\" style=\"border:1px dotted #000;word-wrap:break-word;display:block!important\" class=\"\">placeholder</p>" +
    "<p style=\"margin-top:2em\">Match type: <select id=\"kwhipattype\"><option value=\"string\" selected>Anywhere in a word</option>" +
    "<option value=\"word\">\"Whole\" words only</option><option value=\"regex\">Regular Expression (advanced)</option></select></p></td>" +
    "<td style=\"width:55%\" id=\"stylecontrols\"><p><span>Text color:</span> R:<input id=\"txtr\" type=\"number\" min=\"0\" max=\"255\" step=\"1\" " +
    "style=\"width:4em\" value=\"0\"> G:<input id=\"txtg\" type=\"number\" min=\"0\" max=\"255\" step=\"1\" " +
    "style=\"width:4em\" value=\"0\"> B:<input id=\"txtb\" type=\"number\" min=\"0\" max=\"255\" step=\"1\" " +
    "style=\"width:4em\" value=\"0\"> <button id=\"btntxtreset\">Reset</button></p><p><span>Background:</span> R:<input id=\"bkgr\" " +
    "type=\"number\" min=\"0\" max=\"255\" step=\"1\" style=\"width:4em\" value=\"255\"> G:<input id=\"bkgg\" " +
    "type=\"number\" min=\"0\" max=\"255\" step=\"1\" style=\"width:4em\" value=\"255\"> B:<input id=\"bkgb\" " +
    "type=\"number\" min=\"0\" max=\"255\" step=\"1\" style=\"width:4em\" value=\"128\"> <button id=\"btnbkgreset\">Reset</button>" +
    "</p><p><span>Font-weight:</span> <select id=\"fwsel\"><option value=\"inherit\" selected>inherit</option>" +
    "<option value=\"bold\"><b>bold</b></option><option value=\"normal\">not bold</option></select></p><p><span>Custom:</span> <input type=\"text\" " +
    "id=\"kwhicustom\" style=\"width:55%\"> <button id=\"kwhicustomapply\">Apply</button></p></td></tr></tbody></table>" +
    "<p><button id=\"btnkwhisave\">Save Changes</button> <button id=\"btnkwhicancel\">Discard Changes</button> <button id=\"btnkwhiremove\">Hide Set</button></p></form><style type=\"text/css\">" +
    "#kwhiedit{position:fixed;top:1px;left:150px;width:800px;height:400px;border:1px solid #000;border-radius:6px;padding:1em;color:#000;" +
    "background:#fafafa;z-index:2501;display:none} #kwhiedit table{width:100%;background:#fff;border-top:1px solid #000;" +
    "border-left:1px solid #000;} #kwhiedit td{padding:0 16px; vertical-align:top;border-right:1px solid #000;border-bottom:1px solid #000;}" +
    "#stylecontrols>p>span{display:inline-block;width:6.5em;}</style><style type=\"text/css\" id=\"kwhiedittemp\"></style></div>";
  document.body.appendChild(kwhied);
  // Attach event handlers
  document.getElementById("btnkwhisave").addEventListener("click",kwhisavechg,false);
  document.getElementById("btnkwhicancel").addEventListener("click",kwhicancel,false);
  document.getElementById("btnkwhiremove").addEventListener("click",kwhiremove,false);
  document.getElementById("stylecontrols").addEventListener("input", updatestyle, false);
  document.getElementById("btntxtreset").addEventListener("click",kwhicolorreset,false);
  document.getElementById("btnbkgreset").addEventListener("click",kwhicolorreset,false);
  document.getElementById("fwsel").addEventListener("change",kwhifwchg,false);
  document.getElementById("kwhicustomapply").addEventListener("click",kwhicustom,false);

  function kwhisavechg(e){
    // Update object, regenerate CSS if applicable, apply to document
    var hlset = document.querySelector('#kwhiedit td:nth-of-type(1) p:nth-of-type(1)').className;
    var kwtext = document.querySelector('#kwhiedit td:nth-of-type(1) p:nth-of-type(1)').textContent;
    if (hlset == ""){ 
      // create a new set number
      var hlset = "set" + hlnextset;
      hlnextset += 1;
      GM_setValue("hlnextset",hlnextset);
      // add the set
      if (document.getElementById("kwhipattype").value == "regex") kwtext = kwtext.replace(/\\/g, "\\");
      hlobj[hlset] = {
        keywords : kwtext,
        type: document.getElementById("kwhipattype").value,
        textcolor : kwhieditstyle[0],
        backcolor : kwhieditstyle[1],
        fontweight : kwhieditstyle[2],
        custom : kwhieditstyle[3],
        enabled : "true",
        visible : "true",
        updated : ""
      }
      // Update the global key array
      hlkeys = Object.keys(hlobj);
    } else {
      hlobj[hlset].type = document.getElementById("kwhipattype").value;
      // Save keyword changes after user confirmation
      if (kwtext != hlobj[hlset].keywords){
        if (confirm("Save updated keywords (and other changes)?")){
          if (hlobj[hlset].type != "regex") hlobj[hlset].keywords = kwtext;
          else hlobj[hlset].keywords = kwtext.replace(/\\/g, "\\");
        } else return;
      }
      // Save style changes without confirmation
      hlobj[hlset].textcolor = kwhieditstyle[0];
      hlobj[hlset].backcolor = kwhieditstyle[1];
      hlobj[hlset].fontweight = kwhieditstyle[2];
      hlobj[hlset].custom = kwhieditstyle[3];
      // Set updated date/time
      hlobj[hlset].updated = (new Date()).toJSON();
    }
    // Persist the object
    hljson = JSON.stringify(hlobj);
    GM_setValue("kwstyles",hljson);
    // Update CSS rule and parent form
    insertCSS([hlset]);
    refreshSetList();
    // Unhighlight, re-highlight, close dialog
    unhighlight(hlset);
    THmo_doHighlight(document.body,[hlset])
    document.getElementById('kwhiedit').style.display='none';
  }
  function kwhicancel(e){
    // Close dialog (fields will be refresh if it is opened again)
    document.getElementById('kwhiedit').style.display='none';
  }
  function kwhiremove(e){
    var hlset = document.querySelector('#kwhiedit td:nth-of-type(1) p:nth-of-type(1)').className;
    if (hlset == ""){
      alert("This set has not been saved and therefore does not need to be hidden, you can just close the dialog to discard it.");
    } else {
      if (confirm("Are you sure you want to hide this set instead of editing it to your own liking?")){
        hlobj[hlset].visible = "false";
        hlobj[hlset].updated = (new Date()).toJSON();
        // Persist the object
        hljson = JSON.stringify(hlobj);
        GM_setValue("kwstyles",hljson);
        // Update set list, remove highlighting, close form
        refreshSetList();
        unhighlight(hlset);
        document.getElementById('kwhiedit').style.display='none';
      }
    }
  }
  function kwhicolorreset(e){
    // what set is this?
    var set = document.querySelector('#kwhiedit tr').getAttribute('kwhiset');
    // check which button, reset the RGB
    if (e.target.id == "btntxtreset"){
      if (set == "new"){
        kwhieditstyle[0] = "rgb(0,0,255)";
      } else {
        kwhieditstyle[0] = hlobj[set].textcolor;
      }
      populateRGB("txt",kwhieditstyle[0]);
      setdivstyle(["txt"]);
    }
    if (e.target.id == "btnbkgreset"){
      if (set == "new"){
        kwhieditstyle[1] = "rgb(255,255,0)";        
      } else {
        kwhieditstyle[1] = hlobj[set].backcolor;
      }
      populateRGB("bkg",kwhieditstyle[1]);
      setdivstyle(["bkg"]);
    }
    e.target.blur();
  }
  function populateRGB(prop,stylestring){
    var rgbvals = stylestring.substr(stylestring.indexOf("(")+1);
    rgbvals = rgbvals.substr(0,rgbvals.length-1).split(",");
    document.getElementById(prop+"r").value = parseInt(rgbvals[0]);
    document.getElementById(prop+"g").value = parseInt(rgbvals[1]);
    document.getElementById(prop+"b").value = parseInt(rgbvals[2]);
  }
  function updatestyle(e){
    // validate value and apply change
    if (e.target.id.indexOf("txt") == 0 || e.target.id.indexOf("bkg") == 0){
      if (isNaN(e.target.value)){
        alert("Please only use values between 0 and 255");
        return;
      }
      if (parseInt(e.target.value) != e.target.value){
        e.target.value = parseInt(e.target.value);
      }
      if (e.target.value < 0){
        e.target.value = 0;
      }
      if (e.target.value > 255){
        e.target.value = 255;
      }
      if (e.target.id.indexOf("txt") == 0) setdivstyle(["txt"]);
      if (e.target.id.indexOf("bkg") == 0) setdivstyle(["bkg"]);
    } else {
      if (e.target.id == "kwhicustom") return;
      console.log("updatestyle on "+e.target.id);
    }
  }
  function setdivstyle(props){
    for (var i=0; i<props.length; i++){
      switch (props[i]){
        case "txt":
          kwhieditstyle[0] = "rgb(" + document.getElementById("txtr").value + "," +
            document.getElementById("txtg").value + "," + document.getElementById("txtb").value + ")";
          break;
        case "bkg":
          kwhieditstyle[1] = "rgb(" + document.getElementById("bkgr").value + "," +
            document.getElementById("bkgg").value + "," + document.getElementById("bkgb").value + ")";
          break;
        default:
          console.log("default?");
      }
    }
    var rule = "#stylecontrols>p>span{";
    if (kwhieditstyle[0].length > 0) rule += "color:"+kwhieditstyle[0]+";";
    if (kwhieditstyle[1].length > 0) rule += "background-color:"+kwhieditstyle[1]+";";
    if (kwhieditstyle[2].length > 0) rule += "font-weight:"+kwhieditstyle[2]+";";
    if (kwhieditstyle[3].length > 0) rule += kwhieditstyle[3]+";";
    document.getElementById("kwhiedittemp").innerHTML = rule + "}";
  }
  function kwhifwchg(e){
    kwhieditstyle[2] = e.target.value;
    setdivstyle([]);
  }
  function kwhicustom(e){
    kwhieditstyle[3] = document.getElementById("kwhicustom").value;
    setdivstyle([]);
  }
  
  // Context menu options -- do not replace any existing menu!
  if (!document.body.hasAttribute("contextmenu") && "contextMenu" in document.documentElement){
    var cmenu = document.createElement("menu");
    cmenu.id = "THDcontext";
    cmenu.setAttribute("type", "context");
    cmenu.innerHTML = '<menu label="Text Highlighter">' +
      '<menuitem id="THDshowbar" label="Show bar"></menuitem>' +
      '<menuitem id="THDenableset" label="Enable matching set"></menuitem>' +
      '<menuitem id="THDdisableset" label="Disable this set"></menuitem>' +
      '<menuitem id="THDnewset" label="Add new set"></menuitem>' +
      '</menu>';
    document.body.appendChild(cmenu);
    document.getElementById("THDshowbar").addEventListener("click",editKW,false);
    document.getElementById("THDenableset").addEventListener("click",cmenuEnable,false);
    document.getElementById("THDdisableset").addEventListener("click",cmenuDisable,false);
    document.getElementById("THDnewset").addEventListener("click",cmenuNewset,false);
    // attach menu and create event for filtering
    document.body.setAttribute("contextmenu", "THDcontext");
    document.body.addEventListener("contextmenu",cmenuFilter,false);
  }
  function cmenuFilter(e){
    document.getElementById("THDenableset").setAttribute("disabled","disabled");
    document.getElementById("THDenableset").setAttribute("THDtext","");
    document.getElementById("THDdisableset").setAttribute("disabled","disabled");
    document.getElementById("THDdisableset").setAttribute("THDset","");
    var s = window.getSelection();
    if (s.isCollapsed) document.getElementById("THDnewset").setAttribute("THDtext","");
    else document.getElementById("THDnewset").setAttribute("THDtext",s.getRangeAt(0).toString().trim());
    if (e.target.hasAttribute('txhidy15')){
      document.getElementById("THDdisableset").removeAttribute("disabled");
      document.getElementById("THDdisableset").setAttribute("THDset",e.target.getAttribute('txhidy15'));
    } else {
      document.getElementById("THDdisableset").setAttribute("disabled","disabled");
      if (!s.isCollapsed){
        document.getElementById("THDenableset").removeAttribute("disabled");
        document.getElementById("THDenableset").setAttribute("THDtext",s.getRangeAt(0).toString().trim());
      }
    }
  }
  function cmenuEnable(e){
    var kw = e.target.getAttribute("THDtext").toLowerCase();
    var toggled = false;
    for (var j = 0; j < hlkeys.length; ++j){
      var hlset = hlkeys[j];
      var kwlist = "|" + hlobj[hlset].keywords.toLowerCase() + "|";
      if(kwlist.indexOf("|" + kw + "|") > -1){
        if (hlobj[hlset].enabled == "true") break; // already enabled
        kwhienabledisable(hlset,true);
        refreshSetList();
        toggled = true;
        break;
      }
    }
    if (toggled == false){
      if (document.getElementById("thdtopbar").style.display != "block") editKW();
      if (document.getElementById("thdtopdrop").style.display != "block") thdDropSetList();
    }
  }
  function cmenuDisable(e){
    kwhienabledisable(e.target.getAttribute("THDset"),false);
    refreshSetList();
  }
  function cmenuNewset(e){
    //TODO - if there's a selection, get it into the form
    kwhinewset(e,e.target.getAttribute("THDtext"));
  }

  // TESTING ONLY
  function flushData(){
    GM_setValue("kwstyles", "");
  }
  GM_registerMenuCommand("TEST ONLY - flush keyword sets for Text Highlighter", flushData);

})(); // end of anonymous function