feat: highlight fav words && split long paragraph
This commit is contained in:
@@ -18,6 +18,11 @@ import {
|
||||
MSG_TRANSBOX_TOGGLE,
|
||||
MSG_MOUSEHOVER_TOGGLE,
|
||||
MSG_TRANSINPUT_TOGGLE,
|
||||
OPT_HIGHLIGHT_WORDS_BEFORETRANS,
|
||||
OPT_HIGHLIGHT_WORDS_AFTERTRANS,
|
||||
OPT_SPLIT_PARAGRAPH_PUNCTUATION,
|
||||
OPT_SPLIT_PARAGRAPH_DISABLE,
|
||||
OPT_SPLIT_PARAGRAPH_TEXTLENGTH,
|
||||
} from "../config";
|
||||
import interpreter from "./interpreter";
|
||||
import { ShadowRootMonitor } from "./shadowroot";
|
||||
@@ -164,6 +169,8 @@ export class Translator {
|
||||
warpper: `${APP_LCNAME}-wrapper notranslate`,
|
||||
inner: `${APP_LCNAME}-inner`,
|
||||
term: `${APP_LCNAME}-term`,
|
||||
br: `${APP_LCNAME}-br`,
|
||||
highlight: `${APP_LCNAME}-highlight`,
|
||||
};
|
||||
|
||||
// 内置跳过翻译文本
|
||||
@@ -279,6 +286,7 @@ export class Translator {
|
||||
#textClass = {}; // 译文样式class
|
||||
#textSheet = ""; // 译文样式字典
|
||||
#apisMap = new Map(); // 用于接口快速查找
|
||||
#favWords = []; // 收藏词汇
|
||||
|
||||
#isUserscript = false;
|
||||
#transboxManager = null; // 划词翻译
|
||||
@@ -289,6 +297,7 @@ export class Translator {
|
||||
#viewNodes = new Set(); // 当前在可视范围内的单元
|
||||
#processedNodes = new WeakMap(); // 已处理(已执行翻译DOM操作)的单元
|
||||
#rootNodes = new Set(); // 已监控的根节点
|
||||
#skipMoNodes = new WeakSet(); // 忽略变化的节点
|
||||
|
||||
#removeKeydownHandler; // 快捷键清理函数
|
||||
#hoveredNode = null; // 存储当前悬停的可翻译节点
|
||||
@@ -330,9 +339,15 @@ export class Translator {
|
||||
};
|
||||
}
|
||||
|
||||
constructor(rule = {}, setting = {}, isUserscript = false) {
|
||||
constructor({
|
||||
rule = {},
|
||||
setting = {},
|
||||
favWords = [],
|
||||
isUserscript = false,
|
||||
}) {
|
||||
this.#setting = { ...Translator.DEFAULT_OPTIONS, ...setting };
|
||||
this.#rule = { ...Translator.DEFAULT_RULE, ...rule };
|
||||
this.#favWords = favWords;
|
||||
this.#apisMap = new Map(
|
||||
this.#setting.transApis.map((api) => [api.apiSlug, api])
|
||||
);
|
||||
@@ -585,6 +600,8 @@ export class Translator {
|
||||
#createMutationObserver() {
|
||||
return new MutationObserver((mutations) => {
|
||||
for (const mutation of mutations) {
|
||||
if (this.#skipMoNodes.has(mutation.target)) return;
|
||||
|
||||
if (
|
||||
mutation.type === "characterData" &&
|
||||
mutation.oldValue !== mutation.target.nodeValue
|
||||
@@ -599,6 +616,8 @@ export class Translator {
|
||||
let nodes = new Set();
|
||||
let hasText = false;
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
if (this.#skipMoNodes.has(node)) return;
|
||||
|
||||
if (/\S/.test(node.nodeValue)) {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
hasText = true;
|
||||
@@ -780,6 +799,10 @@ export class Translator {
|
||||
|
||||
// 开始/重新监控节点
|
||||
#startObserveNode(node) {
|
||||
if (this.#rule.highlightWords === OPT_HIGHLIGHT_WORDS_BEFORETRANS) {
|
||||
this.#highlightWordsDeeply(node);
|
||||
}
|
||||
|
||||
if (
|
||||
!this.#observedNodes.has(node) &&
|
||||
this.#enabled &&
|
||||
@@ -861,7 +884,12 @@ export class Translator {
|
||||
|
||||
// 提前进行语言检测
|
||||
let deLang = "";
|
||||
const { fromLang = "auto", toLang } = this.#rule;
|
||||
const {
|
||||
fromLang = "auto",
|
||||
toLang,
|
||||
splitParagraph = OPT_SPLIT_PARAGRAPH_DISABLE,
|
||||
splitLength = 100,
|
||||
} = this.#rule;
|
||||
const { langDetector, skipLangs = [] } = this.#setting;
|
||||
if (fromLang === "auto") {
|
||||
deLang = await tryDetectLang(node.textContent, langDetector);
|
||||
@@ -876,6 +904,11 @@ export class Translator {
|
||||
}
|
||||
}
|
||||
|
||||
// 切分长段落
|
||||
if (splitParagraph !== OPT_SPLIT_PARAGRAPH_DISABLE) {
|
||||
this.#splitTextNodesBySentence(node, splitParagraph, splitLength);
|
||||
}
|
||||
|
||||
let nodeGroup = [];
|
||||
[...node.childNodes].forEach((child) => {
|
||||
const shouldBreak = this.#shouldBreak(child);
|
||||
@@ -895,6 +928,162 @@ export class Translator {
|
||||
}
|
||||
}
|
||||
|
||||
// 高亮词汇
|
||||
#highlightTextNode(textNode, wordRegex) {
|
||||
if (textNode.parentNode?.nodeName.toLowerCase() === "b") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!wordRegex.test(textNode.textContent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
wordRegex.lastIndex = 0;
|
||||
const fragments = textNode.textContent.split(wordRegex);
|
||||
const newNodes = [];
|
||||
|
||||
fragments.forEach((fragment, i) => {
|
||||
if (!fragment) return;
|
||||
|
||||
if (i % 2 === 1) {
|
||||
// 奇数索引是匹配到的关键词
|
||||
const bTag = document.createElement("b");
|
||||
bTag.className = Translator.KISS_CLASS.highlight;
|
||||
bTag.style.cssText = this.#rule.highlightStyle || "";
|
||||
bTag.textContent = fragment;
|
||||
this.#skipMoNodes.add(bTag);
|
||||
newNodes.push(bTag);
|
||||
} else {
|
||||
// 偶数索引是普通文本
|
||||
const newTextNode = document.createTextNode(fragment);
|
||||
this.#skipMoNodes.add(newTextNode);
|
||||
newNodes.push(newTextNode);
|
||||
}
|
||||
});
|
||||
|
||||
if (newNodes.length > 0) {
|
||||
textNode.replaceWith(...newNodes);
|
||||
}
|
||||
}
|
||||
|
||||
// 高亮词汇
|
||||
#highlightWordsDeeply(parentNode) {
|
||||
if (!parentNode || this.#favWords.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const escapedWords = this.#favWords.map(escapeRegex);
|
||||
const wordRegex = new RegExp(`\\b(${escapedWords.join("|")})\\b`, "gi");
|
||||
|
||||
if (parentNode.nodeType === Node.ELEMENT_NODE) {
|
||||
const walker = document.createTreeWalker(
|
||||
parentNode,
|
||||
NodeFilter.SHOW_TEXT,
|
||||
null,
|
||||
false
|
||||
);
|
||||
|
||||
const nodesToProcess = [];
|
||||
let node;
|
||||
while ((node = walker.nextNode())) {
|
||||
nodesToProcess.push(node);
|
||||
}
|
||||
|
||||
nodesToProcess.forEach((textNode) => {
|
||||
this.#highlightTextNode(textNode, wordRegex);
|
||||
});
|
||||
} else if (parentNode.nodeType === Node.TEXT_NODE) {
|
||||
this.#highlightTextNode(parentNode, wordRegex);
|
||||
}
|
||||
}
|
||||
|
||||
// 切分文本段落
|
||||
#splitTextNodesBySentence(parentNode, splitParagraph, splitLength) {
|
||||
const sentenceEndRegexForSplit = /[。!?]+|[.?!]+(?=\s+|$)/g;
|
||||
|
||||
[...parentNode.childNodes].forEach((node) => {
|
||||
if (node.nodeType !== Node.TEXT_NODE || node.textContent.trim() === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
const text = node.textContent;
|
||||
const parts = [];
|
||||
let lastIndex = 0;
|
||||
let match;
|
||||
|
||||
while ((match = sentenceEndRegexForSplit.exec(text)) !== null) {
|
||||
let realEndIndex = match.index + match[0].length;
|
||||
while (realEndIndex < text.length && /\s/.test(text[realEndIndex])) {
|
||||
realEndIndex++;
|
||||
}
|
||||
parts.push(text.substring(lastIndex, realEndIndex));
|
||||
lastIndex = realEndIndex;
|
||||
sentenceEndRegexForSplit.lastIndex = realEndIndex;
|
||||
}
|
||||
if (lastIndex < text.length) {
|
||||
parts.push(text.substring(lastIndex));
|
||||
}
|
||||
|
||||
const validParts = parts.filter((part) => part.trim().length > 0);
|
||||
if (validParts.length <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newNodes = validParts.map((part) => {
|
||||
const newNode = document.createTextNode(part);
|
||||
this.#skipMoNodes.add(newNode);
|
||||
return newNode;
|
||||
});
|
||||
|
||||
node.replaceWith(...newNodes);
|
||||
});
|
||||
|
||||
const sentenceEndRegexForTest = /(?:[。!??!]+|(?<!\d)\.)\s*$/;
|
||||
let textLength = 0;
|
||||
|
||||
[...parentNode.childNodes].forEach((node) => {
|
||||
textLength += node.textContent.length;
|
||||
|
||||
const isSentenceEnd = sentenceEndRegexForTest.test(node.textContent);
|
||||
if (!isSentenceEnd || node.nextSibling?.nodeName === "BR") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
splitParagraph === OPT_SPLIT_PARAGRAPH_PUNCTUATION ||
|
||||
(splitParagraph === OPT_SPLIT_PARAGRAPH_TEXTLENGTH &&
|
||||
textLength >= splitLength)
|
||||
) {
|
||||
textLength = 0;
|
||||
|
||||
const br = document.createElement("br");
|
||||
br.className = Translator.KISS_CLASS.br;
|
||||
this.#skipMoNodes.add(br);
|
||||
|
||||
node.after(br);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 清除高亮
|
||||
#removeHighlights(parentNode) {
|
||||
if (!parentNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const highlightedElements = parentNode.querySelectorAll(
|
||||
`.${Translator.KISS_CLASS.highlight}`
|
||||
);
|
||||
|
||||
highlightedElements.forEach((element) => {
|
||||
const textNode = document.createTextNode(element.textContent);
|
||||
element.replaceWith(textNode);
|
||||
});
|
||||
|
||||
parentNode.normalize();
|
||||
}
|
||||
|
||||
// 判断是否需要换行
|
||||
#shouldBreak(node) {
|
||||
if (!Translator.isElementOrFragment(node)) return false;
|
||||
@@ -971,6 +1160,7 @@ export class Translator {
|
||||
// detectRemote,
|
||||
// toLang,
|
||||
// skipLangs = [],
|
||||
highlightWords,
|
||||
} = this.#rule;
|
||||
const {
|
||||
newlineLength,
|
||||
@@ -1062,6 +1252,11 @@ export class Translator {
|
||||
parentNode.parentElement.style.cssText += grandStyle;
|
||||
}
|
||||
|
||||
// 高亮词汇
|
||||
if (highlightWords === OPT_HIGHLIGHT_WORDS_AFTERTRANS) {
|
||||
nodes.forEach((node) => this.#highlightWordsDeeply(node));
|
||||
}
|
||||
|
||||
// 翻译完成钩子函数
|
||||
if (transEndHook?.trim()) {
|
||||
try {
|
||||
@@ -1226,6 +1421,10 @@ export class Translator {
|
||||
root
|
||||
.querySelectorAll(APP_LCNAME)
|
||||
.forEach((el) => this.#removeTranslationElement(el));
|
||||
|
||||
root
|
||||
.querySelectorAll(Translator.KISS_CLASS.br)
|
||||
.forEach((br) => br.remove());
|
||||
}
|
||||
|
||||
// 清理子节点译文dom
|
||||
@@ -1237,7 +1436,8 @@ export class Translator {
|
||||
|
||||
// 清理译文
|
||||
#removeTranslationElement(el) {
|
||||
this.#processedNodes.delete(el.parentElement);
|
||||
const parentElement = el.parentElement;
|
||||
this.#processedNodes.delete(parentElement);
|
||||
|
||||
// 如果是仅显示译文模式,先恢复原文
|
||||
const { nodes, isHide } = this.#translationNodes.get(el) || {};
|
||||
@@ -1247,6 +1447,11 @@ export class Translator {
|
||||
|
||||
this.#translationNodes.delete(el);
|
||||
el.remove();
|
||||
|
||||
// 清除高亮
|
||||
if (this.#rule.highlightWords === OPT_HIGHLIGHT_WORDS_AFTERTRANS) {
|
||||
this.#removeHighlights(parentElement);
|
||||
}
|
||||
}
|
||||
|
||||
// 恢复原文
|
||||
|
||||
Reference in New Issue
Block a user