// ==UserScript==
// @name Han Simplify
// @name:zh 汉字转换为简体字
// @description 将页面上的汉字转换为简体字,需要手动添加包含的网站以启用
// @namespace https://github.com/tiansh
// @version 1.7
// @resource t2s https://tiansh.github.io/reader/data/han/t2s.json
// @include *
// @exclude *
// @grant GM_getResourceURL
// @grant GM.getResourceUrl
// @run-at document-start
// @license MIT
// @supportURL https://github.com/tiansh/us-han-simplify/issues
// ==/UserScript==
/** @type {'t2s'|'s2t'} */
const RULE = 't2s';
/* global RULE */
/**
* @name RULE
* @type {'t2s'|'s2t'}
*/
/* eslint-env browser, greasemonkey */
; (async function () {
const fetchTable = async function (url) {
return (await fetch(url)).json();
};
const loadTable = async function () {
try {
return fetchTable(GM_getResourceURL(RULE));
} catch {
return fetchTable(await GM.getResourceUrl(RULE));
}
};
/** @type {{ [ch: string]: [string, number] }[]} */
const table = await loadTable();
const hasOwnProperty = Object.prototype.hasOwnProperty;
/** @param {string} text */
const translate = function (text) {
let output = '';
let state = 0;
for (let char of text) {
while (true) {
const current = table[state];
const hasMatch = hasOwnProperty.call(current, char);
if (!hasMatch && state === 0) {
output += char;
break;
}
if (hasMatch) {
const [adding, next] = current[char];
if (adding) output += adding;
state = next;
break;
}
const [adding, next] = current[''];
if (adding) output += adding;
state = next;
}
}
while (state !== 0) {
const current = table[state];
const [adding, next] = current[''];
if (adding) output += adding;
state = next;
}
return output;
};
// Do not characters marked as following languages
const skipLang = /^(?:ja|ko|vi)\b/i;
// Change lang attribute so correct fonts may be available
const fromLang = {
t2s: /^zh\b(?:(?!.*-Hans)-(?:TW|HK|MO)|.*-Hant|$)/i,
s2t: /^zh\b(?:(?!.*-Hant)-(?:CN|SG|MY)|.*-Hans|$)/i,
}[RULE];
// Overwrite language attribute with
const destLang = { t2s: 'zh-Hans', s2t: 'zh-Hant' }[RULE];
/** @type {WeakMap<Text|Attr, string>} */
const translated = new WeakMap();
/** @param {Text|Attr} node */
const translateNode = function (node) {
if (!node) return;
if (translated.has(node) && translated.get(node) === node.nodeValue) return;
if (/^\s*$/.test(node.nodeValue)) return;
const result = translate(node.nodeValue);
translated.set(node, result);
node.nodeValue = result;
};
/** @enum {number} */
const filterResult = {
TRANSLATE: 0, // Translate this node
SKIP_CHILD: 1, // Translate this node but not its children
SKIP_LANG: 2, // Do not translate nodes in certain language
SKIP_NODE: 3, // Do not translate this node and its children
};
/**
* @param {Document|Element|Text} node
* @param {filterResult} context
*/
const nodeFilter = function (node, context = null) {
// DOM Root
if (node instanceof Document) return filterResult.TRANSLATE;
// Inherit skip
if (context === filterResult.SKIP_NODE || context === filterResult.SKIP_CHILD) return filterResult.SKIP_NODE;
// Text Node
if (node instanceof Text) return context === filterResult.TRANSLATE ? filterResult.TRANSLATE : filterResult.SKIP_NODE;
// Skip other unknown nodes
if (!(node instanceof Element)) return filterResult.SKIP_NODE;
// Do not translate nodes which marked no translate
if (node.classList.contains('notranslate')) return filterResult.SKIP_NODE;
const translate = node.getAttribute('translate');
if (translate === 'no') return filterResult.SKIP_NODE;
// Do not translate content of certain type elements
const tagName = node.tagName;
let child = true;
if (['CODE', 'VAR'].includes(tagName) && translate !== 'yes') child = false;
else if (['SVG', 'MATH', 'SCRIPT', 'STYLE', 'TEXTAREA'].includes(tagName)) child = false;
else if (node.getAttribute('contenteditable') === 'true') child = false;
const lang = node.getAttribute('lang');
// If no language is specified
if (!lang) {
if (child) return context;
return context === filterResult.TRANSLATE ? filterResult.SKIP_CHILD : filterResult.SKIP_NODE;
}
// If text in languages that should be ignored
if (skipLang.test(lang)) {
if (child) return filterResult.SKIP_LANG;
else return filterResult.SKIP_NODE;
}
if (fromLang.test(lang)) {
node.setAttribute('ori-lang', node.getAttribute('lang'));
node.setAttribute('lang', destLang);
}
if (child) return filterResult.TRANSLATE;
else return filterResult.SKIP_CHILD;
};
/** @param {Text|Element} node */
const nodeFilterParents = function (node) {
const parents = [];
for (let p = node; p; p = p.parentNode) parents.push(p);
return parents.reverse().reduce((context, node) => nodeFilter(node, context), filterResult.TRANSLATE);
};
/**
* @param {Node} node
* @param {filterResult} context
*/
const translateTree = function translateTree(node, context) {
if (node instanceof Text) {
if (context === filterResult.TRANSLATE) translateNode(node);
} else if (node instanceof Element) {
const filter = nodeFilter(node, context);
if (filter === filterResult.SKIP_CHILD || filter === filterResult.TRANSLATE) {
const tagName = node.tagName, attrs = node.attributes;
if (['APPLET', 'AREA', 'IMG', 'INPUT'].includes(tagName)) translateNode(attrs.alt);
if (['INPUT', 'TEXTAREA'].includes(tagName)) translateNode(attrs.placeholder);
if (['A', 'AREA'].includes(tagName)) translateNode(attrs.download);
translateNode(attrs.title);
translateNode(attrs['aria-label']);
translateNode(attrs['aria-description']);
}
if (filter === filterResult.TRANSLATE || filter === filterResult.SKIP_LANG) {
[...node.childNodes].forEach(child => { translateTree(child, filter); });
}
} else if (node instanceof Document) {
[...node.childNodes].forEach(child => { translateTree(child, filterResult.TRANSLATE); });
}
};
/** @param {Text|Element} container */
const translateContainer = function (container) {
const filter = nodeFilterParents(container);
if (filter !== filterResult.SKIP_NODE) translateTree(container, filter);
};
const observer = new MutationObserver(function onMutate(records) {
const translateTargets = new Set();
records.forEach(record => {
if (record.type === 'childList') {
[...record.addedNodes].forEach(node => translateTargets.add(node));
} else {
translateTargets.add(record.target);
}
});
[...translateTargets].forEach(translateContainer);
});
observer.observe(document, { subtree: true, childList: true, characterData: true, attributes: true });
if (document.readyState === 'complete') {
translateContainer(document);
} else document.addEventListener('DOMContentLoaded', () => {
translateContainer(document);
}, { once: true });
}());