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.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

// ==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);
  }
})();