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.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴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);
  }
})();