Files
kiss-translator/src/libs/translator.js

565 lines
14 KiB
JavaScript
Raw Normal View History

2023-08-05 15:32:51 +08:00
import { createRoot } from "react-dom/client";
import {
APP_LCNAME,
TRANS_MIN_LENGTH,
TRANS_MAX_LENGTH,
MSG_TRANS_CURRULE,
2023-08-21 16:06:21 +08:00
OPT_STYLE_DASHLINE,
OPT_STYLE_FUZZY,
2023-08-25 17:07:53 +08:00
SHADOW_KEY,
2023-09-06 18:00:18 +08:00
OPT_MOUSEKEY_DISABLE,
2023-09-06 23:44:01 +08:00
OPT_MOUSEKEY_MOUSEOVER,
2023-09-13 18:02:51 +08:00
DEFAULT_INPUT_RULE,
DEFAULT_TRANS_APIS,
2023-09-14 10:59:50 +08:00
DEFAULT_INPUT_SHORTCUT,
2023-09-14 14:45:22 +08:00
OPT_LANGS_LIST,
} from "../config";
2023-08-05 15:32:51 +08:00
import Content from "../views/Content";
2023-08-31 13:38:06 +08:00
import { updateFetchPool, clearFetchPool } from "./fetch";
2023-09-15 17:25:58 +08:00
import {
debounce,
genEventName,
removeEndchar,
matchInputStr,
sleep,
} from "./utils";
2023-09-13 18:02:51 +08:00
import { stepShortcutRegister } from "./shortcut";
import { apiTranslate } from "../apis";
import { tryDetectLang } from ".";
2023-09-15 17:25:58 +08:00
import { loadingSvg } from "./svg";
function isInputNode(node) {
return node.nodeName === "INPUT" || node.nodeName === "TEXTAREA";
}
function isEditAbleNode(node) {
return node.hasAttribute("contenteditable");
}
function selectContent(node) {
node.focus();
const range = document.createRange();
range.selectNodeContents(node);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
}
function pasteContentEvent(node, text) {
node.focus();
const data = new DataTransfer();
data.setData("text/plain", text);
const event = new ClipboardEvent("paste", { clipboardData: data });
document.dispatchEvent(event);
data.clearData();
}
function pasteContentCommand(node, text) {
node.focus();
document.execCommand("insertText", false, text);
}
function collapseToEnd(node) {
node.focus();
const selection = window.getSelection();
selection.collapseToEnd();
}
function getNodeText(node) {
if (isInputNode(node)) {
return node.value;
}
return node.innerText || node.textContent || "";
}
function addLoading(node, loadingId) {
const div = document.createElement("div");
div.id = loadingId;
div.innerHTML = loadingSvg;
div.style.cssText = `
width: ${node.offsetWidth}px;
height: ${node.offsetHeight}px;
line-height: ${node.offsetHeight}px;
position: absolute;
text-align: center;
left: ${node.offsetLeft}px;
top: ${node.offsetTop}px;
z-index: 2147483647;
`;
node.offsetParent?.appendChild(div);
}
function removeLoading(node, loadingId) {
const div = node.offsetParent.querySelector(`#${loadingId}`);
2023-09-15 17:25:58 +08:00
if (div) {
div.remove();
}
}
2023-08-23 17:53:46 +08:00
2023-08-05 15:32:51 +08:00
/**
* 翻译类
*/
export class Translator {
_rule = {};
2023-09-13 18:02:51 +08:00
_inputRule = {};
2023-08-30 18:05:37 +08:00
_setting = {};
_rootNodes = new Set();
_tranNodes = new Map();
2023-08-24 16:21:01 +08:00
_skipNodeNames = [
APP_LCNAME,
"style",
"svg",
"img",
"audio",
"video",
"textarea",
"input",
"button",
"select",
"option",
"head",
"script",
2023-08-25 17:07:53 +08:00
"iframe",
2023-08-24 16:21:01 +08:00
];
2023-09-02 14:14:27 +08:00
_eventName = genEventName();
2023-08-05 15:32:51 +08:00
2023-08-23 17:53:46 +08:00
// 显示
2023-08-05 15:32:51 +08:00
_interseObserver = new IntersectionObserver(
(intersections) => {
intersections.forEach((intersection) => {
if (intersection.isIntersecting) {
this._render(intersection.target);
this._interseObserver.unobserve(intersection.target);
}
});
},
{
threshold: 0.1,
}
);
2023-08-23 17:53:46 +08:00
// 变化
2023-08-05 15:32:51 +08:00
_mutaObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
2023-08-24 14:57:54 +08:00
if (
2023-08-24 16:21:01 +08:00
!this._skipNodeNames.includes(mutation.target.localName) &&
2023-08-24 14:57:54 +08:00
mutation.addedNodes.length > 0
) {
2023-08-25 17:07:53 +08:00
const nodes = Array.from(mutation.addedNodes).filter((node) => {
2023-08-24 16:21:01 +08:00
if (
this._skipNodeNames.includes(node.localName) ||
node.id === APP_LCNAME
) {
return false;
2023-08-24 14:57:54 +08:00
}
2023-08-24 16:21:01 +08:00
return true;
2023-08-24 14:57:54 +08:00
});
2023-08-25 17:07:53 +08:00
if (nodes.length > 0) {
// const rootNode = mutation.target.getRootNode();
// todo
2023-08-24 14:57:54 +08:00
this._reTranslate();
2023-08-05 15:32:51 +08:00
}
2023-08-24 14:57:54 +08:00
}
2023-08-05 15:32:51 +08:00
});
});
2023-08-24 16:21:01 +08:00
// 插入 shadowroot
2023-08-24 14:57:54 +08:00
_overrideAttachShadow = () => {
const _this = this;
const _attachShadow = HTMLElement.prototype.attachShadow;
HTMLElement.prototype.attachShadow = function () {
_this._reTranslate();
return _attachShadow.apply(this, arguments);
};
};
2023-08-30 18:05:37 +08:00
constructor(rule, setting) {
const { fetchInterval, fetchLimit } = setting;
2023-08-31 13:38:06 +08:00
updateFetchPool(fetchInterval, fetchLimit);
2023-08-24 14:57:54 +08:00
this._overrideAttachShadow();
2023-08-30 18:05:37 +08:00
this._setting = setting;
this._rule = rule;
2023-08-08 13:29:15 +08:00
if (rule.transOpen === "true") {
2023-08-05 15:32:51 +08:00
this._register();
}
2023-09-13 18:02:51 +08:00
2023-09-15 17:25:58 +08:00
this._inputRule = setting.inputRule || DEFAULT_INPUT_RULE;
2023-09-15 17:29:42 +08:00
if (this._inputRule.transOpen) {
2023-09-13 18:02:51 +08:00
this._registerInput();
}
2023-08-05 15:32:51 +08:00
}
2023-08-31 00:18:57 +08:00
get setting() {
return this._setting;
}
2023-09-02 14:14:27 +08:00
get eventName() {
return this._eventName;
}
2023-08-05 15:32:51 +08:00
get rule() {
// console.log("get rule", this._rule);
2023-08-05 15:32:51 +08:00
return this._rule;
}
set rule(rule) {
// console.log("set rule", rule);
this._rule = rule;
// 广播消息
2023-09-02 14:14:27 +08:00
const eventName = this._eventName;
window.dispatchEvent(
2023-09-02 14:14:27 +08:00
new CustomEvent(eventName, {
detail: {
action: MSG_TRANS_CURRULE,
args: rule,
},
})
);
}
2023-08-05 15:32:51 +08:00
updateRule = (obj) => {
this.rule = { ...this.rule, ...obj };
2023-08-05 15:32:51 +08:00
};
toggle = () => {
if (this.rule.transOpen === "true") {
this.rule = { ...this.rule, transOpen: "false" };
2023-08-05 15:32:51 +08:00
this._unRegister();
} else {
this.rule = { ...this.rule, transOpen: "true" };
2023-08-05 15:32:51 +08:00
this._register();
}
};
2023-08-21 16:06:21 +08:00
toggleStyle = () => {
const textStyle =
this.rule.textStyle === OPT_STYLE_FUZZY
? OPT_STYLE_DASHLINE
: OPT_STYLE_FUZZY;
this.rule = { ...this.rule, textStyle };
};
2023-08-25 22:48:11 +08:00
_querySelectorAll = (selector, node) => {
try {
2023-08-26 00:08:12 +08:00
return Array.from(node.querySelectorAll(selector));
2023-08-25 22:48:11 +08:00
} catch (err) {
console.log(`[querySelectorAll err]: ${selector}`);
}
return [];
2023-08-25 17:07:53 +08:00
};
2023-08-26 00:08:12 +08:00
_queryFilter = (selector, rootNode) => {
return this._querySelectorAll(selector, rootNode).filter(
(node) => this._queryFilter(selector, node).length === 0
);
};
_queryShadowNodes = (selector, rootNode) => {
this._rootNodes.add(rootNode);
this._queryFilter(selector, rootNode).forEach((item) => {
if (!this._tranNodes.has(item)) {
this._tranNodes.set(item, "");
}
});
Array.from(rootNode.querySelectorAll("*"))
.map((item) => item.shadowRoot)
.filter(Boolean)
.forEach((item) => {
this._queryShadowNodes(selector, item);
});
};
2023-08-24 14:57:54 +08:00
_queryNodes = (rootNode = document) => {
2023-08-25 17:07:53 +08:00
// const childRoots = Array.from(rootNode.querySelectorAll("*"))
// .map((item) => item.shadowRoot)
// .filter(Boolean);
// const childNodes = childRoots.map((item) => this._queryNodes(item));
// const nodes = Array.from(rootNode.querySelectorAll(this.rule.selector));
// return nodes.concat(childNodes).flat();
this._rootNodes.add(rootNode);
this._rule.selector
.split(";")
.map((item) => item.trim())
.filter(Boolean)
.forEach((selector) => {
if (selector.includes(SHADOW_KEY)) {
const [outSelector, inSelector] = selector
.split(SHADOW_KEY)
.map((item) => item.trim());
if (outSelector && inSelector) {
2023-08-25 22:48:11 +08:00
const outNodes = this._querySelectorAll(outSelector, rootNode);
2023-08-25 17:07:53 +08:00
outNodes.forEach((outNode) => {
if (outNode.shadowRoot) {
// this._rootNodes.add(outNode.shadowRoot);
// this._queryFilter(inSelector, outNode.shadowRoot).forEach(
// (item) => {
// if (!this._tranNodes.has(item)) {
// this._tranNodes.set(item, "");
// }
// }
// );
this._queryShadowNodes(inSelector, outNode.shadowRoot);
2023-08-25 17:07:53 +08:00
}
});
}
} else {
2023-08-26 00:08:12 +08:00
this._queryFilter(selector, rootNode).forEach((item) => {
2023-08-26 13:10:13 +08:00
if (!this._tranNodes.has(item)) {
this._tranNodes.set(item, "");
}
2023-08-25 17:07:53 +08:00
});
}
});
2023-08-24 14:57:54 +08:00
};
2023-08-05 15:32:51 +08:00
_register = () => {
2023-09-06 14:57:02 +08:00
if (this._rule.fromLang === this._rule.toLang) {
return;
}
2023-08-25 17:07:53 +08:00
// 搜索节点
this._queryNodes();
this._rootNodes.forEach((node) => {
// 监听节点变化;
this._mutaObserver.observe(node, {
childList: true,
subtree: true,
// characterData: true,
});
2023-08-05 15:32:51 +08:00
});
2023-08-26 13:10:13 +08:00
this._tranNodes.forEach((_, node) => {
2023-09-06 18:00:18 +08:00
if (
!this._setting.mouseKey ||
this._setting.mouseKey === OPT_MOUSEKEY_DISABLE
) {
// 监听节点显示
this._interseObserver.observe(node);
} else {
// 监听鼠标悬停
node.addEventListener("mouseover", this._handleMouseover);
}
2023-08-05 15:32:51 +08:00
});
2023-09-06 18:00:18 +08:00
};
2023-09-13 18:02:51 +08:00
_registerInput = () => {
2023-09-15 21:39:41 +08:00
const {
triggerShortcut: initTriggerShortcut,
2023-09-13 18:02:51 +08:00
translator,
fromLang,
2023-09-15 21:39:41 +08:00
toLang: initToLang,
triggerCount: initTriggerCount,
2023-09-15 20:44:01 +08:00
triggerTime,
2023-09-14 14:45:22 +08:00
transSign,
2023-09-13 18:02:51 +08:00
} = this._inputRule;
const apiSetting = (this._setting.transApis || DEFAULT_TRANS_APIS)[
translator
];
2023-10-11 10:27:51 +08:00
const { detectRemote } = this._setting;
2023-09-13 18:02:51 +08:00
2023-09-15 21:39:41 +08:00
let triggerShortcut = initTriggerShortcut;
let triggerCount = initTriggerCount;
2023-09-14 10:59:50 +08:00
if (triggerShortcut.length === 0) {
triggerShortcut = DEFAULT_INPUT_SHORTCUT;
triggerCount = 1;
}
2023-09-13 18:02:51 +08:00
stepShortcutRegister(
triggerShortcut,
2023-09-15 17:25:58 +08:00
async () => {
let node = document.activeElement;
if (!node) {
return;
}
while (node.shadowRoot) {
node = node.shadowRoot.activeElement;
}
if (!isInputNode(node) && !isEditAbleNode(node)) {
2023-09-15 17:25:58 +08:00
return;
}
2023-09-15 21:39:41 +08:00
let initText = getNodeText(node);
2023-09-15 17:25:58 +08:00
if (triggerShortcut.length === 1 && triggerShortcut[0].length === 1) {
2023-09-15 21:39:41 +08:00
// todo: remove multiple char
initText = removeEndchar(initText, triggerShortcut[0], triggerCount);
2023-09-15 17:25:58 +08:00
}
2023-09-15 21:39:41 +08:00
if (!initText.trim()) {
2023-09-15 17:25:58 +08:00
return;
}
2023-09-15 21:39:41 +08:00
let text = initText;
let toLang = initToLang;
2023-09-15 17:25:58 +08:00
if (transSign) {
const res = matchInputStr(text, transSign);
if (res) {
let lang = res[1];
if (lang === "zh" || lang === "cn") {
lang = "zh-CN";
} else if (lang === "tw" || lang === "hk") {
lang = "zh-TW";
}
if (lang && OPT_LANGS_LIST.includes(lang)) {
toLang = lang;
}
text = res[2];
2023-09-13 18:02:51 +08:00
}
2023-09-15 17:25:58 +08:00
}
// console.log("input -->", text);
2023-09-15 17:58:00 +08:00
const loadingId = "kiss-" + genEventName();
2023-09-15 17:25:58 +08:00
try {
addLoading(node, loadingId);
2023-10-11 10:27:51 +08:00
const deLang = await tryDetectLang(text, detectRemote);
2023-09-15 17:25:58 +08:00
if (deLang && toLang.includes(deLang)) {
2023-09-13 18:02:51 +08:00
return;
}
2023-09-15 17:25:58 +08:00
const [trText, isSame] = await apiTranslate({
translator,
text,
fromLang,
toLang,
apiSetting,
});
if (!trText || isSame) {
return;
2023-09-14 14:45:22 +08:00
}
2023-09-15 17:25:58 +08:00
if (isInputNode(node)) {
node.value = trText;
node.dispatchEvent(
new Event("input", { bubbles: true, cancelable: true })
);
return;
}
2023-09-13 18:02:51 +08:00
2023-09-15 17:25:58 +08:00
selectContent(node);
await sleep(200);
2023-09-13 18:02:51 +08:00
2023-09-15 17:25:58 +08:00
pasteContentEvent(node, trText);
await sleep(200);
2023-09-13 18:02:51 +08:00
2023-09-15 21:39:41 +08:00
// todo: use includes?
if (getNodeText(node).startsWith(initText)) {
2023-09-15 17:25:58 +08:00
pasteContentCommand(node, trText);
await sleep(100);
} else {
collapseToEnd(node);
2023-09-13 18:02:51 +08:00
}
2023-09-15 17:25:58 +08:00
} catch (err) {
console.log("[translate input]", err.message);
} finally {
removeLoading(node, loadingId);
2023-09-15 17:25:58 +08:00
}
2023-09-13 18:02:51 +08:00
},
2023-09-15 20:44:01 +08:00
triggerCount,
triggerTime
2023-09-13 18:02:51 +08:00
);
};
2023-09-06 18:00:18 +08:00
_handleMouseover = (e) => {
2023-09-06 23:35:09 +08:00
const key = this._setting.mouseKey.slice(3);
2023-09-06 23:44:01 +08:00
if (this._setting.mouseKey === OPT_MOUSEKEY_MOUSEOVER || e[key]) {
2023-09-06 18:00:18 +08:00
e.target.removeEventListener("mouseover", this._handleMouseover);
this._render(e.target);
}
};
2023-08-05 15:32:51 +08:00
_unRegister = () => {
// 解除节点变化监听
this._mutaObserver.disconnect();
2023-08-25 22:48:11 +08:00
// 解除节点显示监听
2023-09-06 18:00:18 +08:00
// this._interseObserver.disconnect();
2023-08-05 15:32:51 +08:00
2023-08-26 13:10:13 +08:00
this._tranNodes.forEach((_, node) => {
2023-09-06 18:00:18 +08:00
if (
!this._setting.mouseKey ||
this._setting.mouseKey === OPT_MOUSEKEY_DISABLE
) {
// 解除节点显示监听
this._interseObserver.unobserve(node);
} else {
// 移除鼠标悬停监听
node.removeEventListener("mouseover", this._handleMouseover);
}
// 移除已插入元素
2023-08-25 17:07:53 +08:00
node.querySelector(APP_LCNAME)?.remove();
2023-08-23 17:53:46 +08:00
});
2023-08-11 16:48:09 +08:00
2023-08-25 17:07:53 +08:00
// 清空节点集合
this._rootNodes.clear();
this._tranNodes.clear();
2023-08-11 16:48:09 +08:00
// 清空任务池
2023-08-31 13:38:06 +08:00
clearFetchPool();
2023-08-05 15:32:51 +08:00
};
2023-08-24 14:57:54 +08:00
_reTranslate = debounce(() => {
2023-08-25 17:07:53 +08:00
if (this._rule.transOpen === "true") {
this._register();
2023-08-24 14:57:54 +08:00
}
}, 500);
2023-08-05 15:32:51 +08:00
_render = (el) => {
2023-08-26 13:10:13 +08:00
let traEl = el.querySelector(APP_LCNAME);
// 已翻译
2023-08-26 13:10:13 +08:00
if (traEl) {
const preText = this._tranNodes.get(el);
const curText = el.innerText.trim();
// const traText = traEl.innerText.trim();
// todo
// 1. traText when loading
// 2. replace startsWith
if (curText.startsWith(preText)) {
return;
}
traEl.remove();
2023-08-05 15:32:51 +08:00
}
const q = el.innerText.trim();
2023-08-26 13:10:13 +08:00
this._tranNodes.set(el, q);
// 太长或太短
2023-08-30 18:05:37 +08:00
if (
!q ||
q.length < (this._setting.minLength ?? TRANS_MIN_LENGTH) ||
q.length > (this._setting.maxLength ?? TRANS_MAX_LENGTH)
) {
2023-08-05 15:32:51 +08:00
return;
}
2023-08-25 22:48:47 +08:00
// console.log("---> ", q);
2023-08-05 15:32:51 +08:00
2023-08-26 13:10:13 +08:00
traEl = document.createElement(APP_LCNAME);
traEl.style.visibility = "visible";
el.appendChild(traEl);
2023-08-11 16:48:09 +08:00
el.style.cssText +=
"-webkit-line-clamp: unset; max-height: none; height: auto;";
2023-08-24 14:57:54 +08:00
if (el.parentElement) {
el.parentElement.style.cssText +=
"-webkit-line-clamp: unset; max-height: none; height: auto;";
}
2023-08-05 15:32:51 +08:00
2023-08-26 13:10:13 +08:00
const root = createRoot(traEl);
2023-08-19 13:48:03 +08:00
root.render(<Content q={q} translator={this} />);
2023-08-05 15:32:51 +08:00
};
}