Word Tooltip

Add custom tooltip to words based on a list of word and toolip text (configurable from the GM menu). The Shift+Win / Shift+Command / Shift+Super will highlight all words which have custom tooltip. To use, sites must be manually added via the script configuration.

// ==UserScript==
// @name         Word Tooltip
// @namespace    https://greasyfork.org/en/users/85671-jcunews
// @version      1.0.1
// @license      AGPLv3
// @author       jcunews
// @description  Add custom tooltip to words based on a list of word and toolip text (configurable from the GM menu). The Shift+Win / Shift+Command / Shift+Super will highlight all words which have custom tooltip. To use, sites must be manually added via the script configuration.
// @reference    https://www.reddit.com/r/software/comments/ouxp0l/need_chrome_extension_that_reports_text_based_on/
// @match        https://specific-site.com/*
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// ==/UserScript==

/*
Warning: this script affects performance, since it has to check each word in the web page text. It is highly recommended to enable it only on specific sites.
*/

((def, dat, drx, erx, obs, a) => {
  function processNode(node, i) {
    switch (node.nodeType) {
      case Node.ELEMENT_NODE:
        if (!["OPTION", "WORD"].includes(node.tagName)) {
          for (i = node.childNodes.length - 1; i >= 0; i--) processNode(node.childNodes[i]);
        }
        break;
      case Node.TEXT_NODE:
        processTextNode(node)
    }
  }
  function processTextNode(node, m, e) {
    drx.lastIndex = 0;
    if (m = drx.exec(node.data)) {
      if (m.index) { //middle
        node = node.splitText(m.index);
      } else { //start
        if (m[0].length < node.data.length) { //partial
          m = node.splitText(m[0].length);
        } else { //whole
          (e = document.createElement("WORD")).textContent = node.data;
          e.title = dat[m[0].toLowerCase()];
          node.replaceWith(e)
        } //else: already processed
      }
    }
  }
  function undo() {
    obs.disconnect();
    document.body.querySelectorAll("word").forEach((e, p) => {
      p = e.parentNode;
      e.replaceWith(e.firstChild);
      p.normalize();
    });
    obs.observe(document.body, {childList: true, subtree: true, characterData: true});
  }
  function parseData(dat, a, z) {
    a = [];
    try {
      dat = dat.split("\n").reduce((r, s, i, k, v) => {
        if ((s = s.trim()) && (s[0] !== "#")) {
          if ((i = s.indexOf("=")) >= 0) {
            k = s.substr(0, i).trim().toLowerCase();
            v = s.substr(i + 1).replace(/\\n/g, "\n");
            if (r[k] === undefined) {
              r[k] = v.trim();
              a.push(k.replace(erx, '\\$1'));
            }
          } else throw 1;
        }
        return r
      }, {});
      return [dat, new RegExp("\\b(?:" + a.join("|") + ")\\b", "gi")]
    } catch(z) {
      alert(`Invalid word list format.

Each line should contain the text word/phrase followed by
a '=' character, then followed by the tooltip word/phrase.

New lines in toolip can be specified as '\\n'.

If there are duplicate text words/phrases (case-insensitive),
only the last one will be effective.

Empty/blank line and lines which start with '#' are ignored.

e.g.

    something=description

    something else=some explanation
    #comments
    more something = line1\\nline2

`);
      return null
    }
  }
  def = `\
#Each line should contain the text word/phrase followed by
#a '=' character, then followed by the tooltip word/phrase.
#
#New lines in toolip can be specified as '\\n'.
#
#If there are duplicate text words/phrases (case-insensitive),
#only the last one will be effective.
#
#Empty/blank lines and lines which start with '#' are ignored.

#common
ftp=File Transfer Protocol
html = Hyper Text Markup Language
http = Hyper Text Transfer Protocol
https = Hyper Text Transfer Protocol (Secure)
gif = Graphic Interchange Format
iso = International Standard Organization
jpeg = Joint Photographic Experts Group
mpeg = Moving Picture Experts Group
png = Portable Network Graphics
text = Text\\n(duh...)
url = Uniform Resource Locator

#technical
ascii = American Standard Code for Information Interchange
avc = Advanced Video Coding
css = Cascading Style Sheet
dom = Document Object Model
json = JavaScript Object Notation
md5 = Message-Digest 5 algorithm
mime = Multipurpose Internet Mail Extensions
pgp = Pretty Good Privacy
sha = Secure Hash Algorithm
uri = Uniform Resource Identifier
utf = Unicode Transformation Format`;
  if ((dat = GM_getValue("wordList")) === undefined) GM_setValue("wordList", dat = def);
  erx = /([\\\/\'*+?|()\[\]{}.^$])/g;
  a = parseData(dat);
  dat = a[0];
  drx = a[1];
  if (document.head) {
    (a = document.createElement("STYLE")).innerHTML = '.wtshow word{background:#00d;color:#ff0}';
    document.documentElement.append(a)
    addEventListener("keydown", ev => {
      if ((ev.key === "OS") && ev.shiftKey) document.body.classList.add("wtshow")
    }, true);
    addEventListener("keyup", ev => {
      if (ev.key === "OS") document.body.classList.remove("wtshow")
    }, true);
    GM_registerMenuCommand("Edit Word List", (e) => {
      (e = document.createElement("DIV")).innerHTML = `<style>
#wtUjs{position:fixed;left:0;top:0;right:0;bottom:0;background:#0007;font:unset;font-family:sans-serif;cursor:pointer}
#wtUjsPop{transform:translateY(-50%);margin:40vh auto 0 auto;border:#007 solid .2em;padding:.5em;width:50vw;background:#fff;color:#000;cursor:auto}
#wtUjsTxt{box-sizing:border-box;width:100%;height:40vh;resize:none}
#wtUjsBtns{margin:1em 0 .5em 0;text-align:center}
#wtUjsBtns>button{margin:0 1em;width:5.2em}
</style>
<div id=wtUjsPop>
  <textarea id=wtUjsTxt></textarea>
  <div id=wtUjsBtns>
    <button id=wtUjsExp>Export...</button>
    <button id=wtUjsImp>Import...</button>
    <button id=wtUjsDef>Default</button>
    <button id=wtUjsRev>Revert</button>
    <button id=wtUjsOk>OK</button>
    <button id=wtUjsCancel>Cancel</button>
  </div>
</div>`;
      e.id = "wtUjs";
      e.onclick = (ev, e) => {
        switch (ev.target.id) {
          case "wtUjsExp":
            (e = document.createElement("A")).download = "wordList.txt";
            e.href = URL.createObjectURL(new Blob([navigator.platform === "Win32" ? wtUjsTxt.value.replace(/\n/g, "\r\n") : wtUjsTxt.value], {type: "text/plain"}));
            e.click();
            setTimeout(u => URL.revokeObjectURL(u), 10000, e.href);
            break;
          case "wtUjsImp":
            (e = document.createElement("INPUT")).type = "file";
            e.onchange = r => {
              r = new FileReader;
              r.onload = () => {
                if (parseData(r = r.result.replace(/\r\n/g, "\n"))) {
                  wtUjsTxt.value = r;
                  wtUjsTxt.focus();
                }
              };
              r.readAsText(e.files[0]);
            };
            e.click();
            break;
          case "wtUjsDef":
            wtUjsTxt.value = def;
            wtUjsTxt.focus();
            break;
          case "wtUjsRev":
            wtUjsTxt.value = GM_getValue("wordList");
            wtUjsTxt.focus();
            break;
          case "wtUjsOk":
            if (e = parseData(wtUjsTxt.value)) {
              dat = e[0];
              drx = e[1];
              GM_setValue("wordList", wtUjsTxt.value);
              undo();
              processNode(document.body);
              wtUjs.remove();
            }
            break;
          case "wtUjs":
          case "wtUjsCancel":
            wtUjs.remove()
        }
      };
      e.querySelector("#wtUjsTxt").value = GM_getValue("wordList");
      document.documentElement.append(e);
      wtUjsTxt.focus();
    });
    (obs = new MutationObserver(recs => recs.forEach(rec => {
      if (rec.type === "childList") {
        rec.addedNodes.forEach(processNode)
      } else processTextNode(rec.target);
    }))).observe(document.body, {childList: true, subtree: true, characterData: true});
    processNode(document.body);
  }
})();