feat: highlight fav words && split long paragraph

This commit is contained in:
Gabe
2025-10-19 00:19:47 +08:00
parent b6ff4aae6a
commit 2325155b1e
7 changed files with 402 additions and 7 deletions

View File

@@ -919,7 +919,7 @@ export const handleTranslate = async (
httpTimeout, httpTimeout,
}); });
if (!response) { if (!response) {
throw new Error("tranlate got empty response"); throw new Error("translate got empty response");
} }
const result = await parseTransRes(response, { const result = await parseTransRes(response, {
@@ -934,7 +934,7 @@ export const handleTranslate = async (
...apiSetting, ...apiSetting,
}); });
if (!result?.length) { if (!result?.length) {
throw new Error("tranlate got an unexpected result"); throw new Error("translate got an unexpected result");
} }
return result; return result;

View File

@@ -8,8 +8,13 @@ import {
MSG_TRANS_TOGGLE_STYLE, MSG_TRANS_TOGGLE_STYLE,
MSG_TRANS_PUTRULE, MSG_TRANS_PUTRULE,
APP_CONSTS, APP_CONSTS,
OPT_HIGHLIGHT_WORDS_DISABLE,
} from "./config"; } from "./config";
import { getFabWithDefault, getSettingWithDefault } from "./libs/storage"; import {
getFabWithDefault,
getSettingWithDefault,
getWordsWithDefault,
} from "./libs/storage";
import { Translator } from "./libs/translator"; import { Translator } from "./libs/translator";
import { isIframe, sendIframeMsg } from "./libs/iframe"; import { isIframe, sendIframeMsg } from "./libs/iframe";
import { touchTapListener } from "./libs/touch"; import { touchTapListener } from "./libs/touch";
@@ -209,7 +214,19 @@ export async function run(isUserscript = false) {
// 翻译网页 // 翻译网页
const rule = await matchRule(href, setting); const rule = await matchRule(href, setting);
const translator = new Translator(rule, setting, isUserscript); let favWords = [];
if (
rule.highlightWords &&
rule.highlightWords !== OPT_HIGHLIGHT_WORDS_DISABLE
) {
favWords = Object.keys(await getWordsWithDefault());
}
const translator = new Translator({
rule,
setting,
favWords,
isUserscript,
});
// 适配iframe // 适配iframe
if (isIframe) { if (isIframe) {

View File

@@ -719,6 +719,11 @@ export const I18N = {
en: `Terms Style`, en: `Terms Style`,
zh_TW: `專業術語樣式`, zh_TW: `專業術語樣式`,
}, },
highlight_style: {
zh: `词汇高亮样式`,
en: `Fav Words highlight style`,
zh_TW: `詞彙高亮樣式`,
},
selector_style_helper: { selector_style_helper: {
zh: `开启翻译时注入。`, zh: `开启翻译时注入。`,
en: `It is injected when translation is turned on.`, en: `It is injected when translation is turned on.`,
@@ -1669,6 +1674,52 @@ export const I18N = {
en: `Click to view [Custom Interface Example]`, en: `Click to view [Custom Interface Example]`,
zh_TW: `點選查看【自訂介面範例】`, zh_TW: `點選查看【自訂介面範例】`,
}, },
split_paragraph: {
zh: `切分长段落`,
en: `Split long paragraph`,
zh_TW: `切分長段落`,
},
split_length: {
zh: `切分长度 (0-10000)`,
en: `Segmentation length(0-10000)`,
zh_TW: `切分長度(0-10000)`,
},
highlight_words: {
zh: `高亮收藏词汇`,
en: `Highlight favorite words`,
zh_TW: `高亮收藏詞彙`,
},
split_disable: {
zh: `禁用`,
en: `Disable`,
zh_TW: `停用`,
},
split_textlength: {
zh: `按照长度切分`,
en: `Split by length`,
zh_TW: `依長度切分`,
},
split_punctuation: {
zh: `按照句子切分`,
en: `Split by sentence`,
zh_TW: `按照句子切分`,
},
highlight_disable: {
zh: `禁用`,
en: `Disable`,
zh_TW: `停用`,
},
highlight_beforetrans: {
zh: `翻译前高亮`,
en: `Highlight before translation`,
zh_TW: `翻譯前高亮`,
},
highlight_aftertrans: {
zh: `翻译后高亮`,
en: `Highlight after translation`,
zh_TW: `翻譯後高亮`,
},
}; };
export const i18n = (lang) => (key) => I18N[key]?.[lang] || ""; export const i18n = (lang) => (key) => I18N[key]?.[lang] || "";

View File

@@ -63,6 +63,24 @@ export const OPT_TIMING_ALL = [
OPT_TIMING_ALT, OPT_TIMING_ALT,
]; ];
export const OPT_SPLIT_PARAGRAPH_DISABLE = "split_disable";
export const OPT_SPLIT_PARAGRAPH_TEXTLENGTH = "split_textlength";
export const OPT_SPLIT_PARAGRAPH_PUNCTUATION = "split_punctuation";
export const OPT_SPLIT_PARAGRAPH_ALL = [
OPT_SPLIT_PARAGRAPH_DISABLE,
OPT_SPLIT_PARAGRAPH_PUNCTUATION,
OPT_SPLIT_PARAGRAPH_TEXTLENGTH,
];
export const OPT_HIGHLIGHT_WORDS_DISABLE = "highlight_disable";
export const OPT_HIGHLIGHT_WORDS_BEFORETRANS = "highlight_beforetrans";
export const OPT_HIGHLIGHT_WORDS_AFTERTRANS = "highlight_aftertrans";
export const OPT_HIGHLIGHT_WORDS_ALL = [
OPT_HIGHLIGHT_WORDS_DISABLE,
OPT_HIGHLIGHT_WORDS_BEFORETRANS,
OPT_HIGHLIGHT_WORDS_AFTERTRANS,
];
export const DEFAULT_DIY_STYLE = `color: #333; export const DEFAULT_DIY_STYLE = `color: #333;
background: linear-gradient( background: linear-gradient(
45deg, 45deg,
@@ -94,6 +112,7 @@ export const DEFAULT_RULE = {
bgColor: "", // 译文颜色 bgColor: "", // 译文颜色
textDiyStyle: "", // 自定义译文样式 textDiyStyle: "", // 自定义译文样式
termsStyle: "", // 专业术语样式 termsStyle: "", // 专业术语样式
highlightStyle: "", // 高亮词汇样式
selectStyle: "", // 选择器节点样式 selectStyle: "", // 选择器节点样式
parentStyle: "", // 选择器父节点样式 parentStyle: "", // 选择器父节点样式
grandStyle: "", // 选择器父节点样式 grandStyle: "", // 选择器父节点样式
@@ -116,6 +135,9 @@ export const DEFAULT_RULE = {
hasShadowroot: GLOBAL_KEY, // 是否包含shadowroot hasShadowroot: GLOBAL_KEY, // 是否包含shadowroot
rootsSelector: "", // 翻译范围选择器 rootsSelector: "", // 翻译范围选择器
ignoreSelector: "", // 不翻译的选择器 ignoreSelector: "", // 不翻译的选择器
splitParagraph: GLOBAL_KEY, // 切分段落
splitLength: 0, // 切分段落长度
highlightWords: GLOBAL_KEY, // 高亮词汇
}; };
// 全局规则 // 全局规则
@@ -133,6 +155,7 @@ export const GLOBLA_RULE = {
bgColor: "", // 译文颜色 bgColor: "", // 译文颜色
textDiyStyle: DEFAULT_DIY_STYLE, // 自定义译文样式 textDiyStyle: DEFAULT_DIY_STYLE, // 自定义译文样式
termsStyle: "font-weight: bold;", // 专业术语样式 termsStyle: "font-weight: bold;", // 专业术语样式
highlightStyle: "color: red;", // 高亮词汇样式
selectStyle: DEFAULT_SELECT_STYLE, // 选择器节点样式 selectStyle: DEFAULT_SELECT_STYLE, // 选择器节点样式
parentStyle: DEFAULT_SELECT_STYLE, // 选择器父节点样式 parentStyle: DEFAULT_SELECT_STYLE, // 选择器父节点样式
grandStyle: DEFAULT_SELECT_STYLE, // 选择器祖节点样式 grandStyle: DEFAULT_SELECT_STYLE, // 选择器祖节点样式
@@ -155,6 +178,9 @@ export const GLOBLA_RULE = {
hasShadowroot: "false", // 是否包含shadowroot hasShadowroot: "false", // 是否包含shadowroot
rootsSelector: "body", // 翻译范围选择器 rootsSelector: "body", // 翻译范围选择器
ignoreSelector: DEFAULT_IGNORE_SELECTOR, // 不翻译的选择器 ignoreSelector: DEFAULT_IGNORE_SELECTOR, // 不翻译的选择器
splitParagraph: OPT_SPLIT_PARAGRAPH_DISABLE, // 切分段落
splitLength: 100, // 切分段落长度
highlightWords: OPT_HIGHLIGHT_WORDS_DISABLE, // 高亮词汇
}; };
export const DEFAULT_RULES = [GLOBLA_RULE]; export const DEFAULT_RULES = [GLOBLA_RULE];

View File

@@ -7,6 +7,8 @@ import {
// OPT_TIMING_ALL, // OPT_TIMING_ALL,
DEFAULT_RULE, DEFAULT_RULE,
GLOBLA_RULE, GLOBLA_RULE,
OPT_SPLIT_PARAGRAPH_ALL,
OPT_HIGHLIGHT_WORDS_ALL,
} from "../config"; } from "../config";
import { loadOrFetchSubRules } from "./subRules"; import { loadOrFetchSubRules } from "./subRules";
import { getRulesWithDefault, setRules } from "./storage"; import { getRulesWithDefault, setRules } from "./storage";
@@ -53,6 +55,7 @@ export const matchRule = async (href, { injectRules, subrulesList }) => {
"terms", "terms",
"aiTerms", "aiTerms",
"termsStyle", "termsStyle",
"highlightStyle",
"selectStyle", "selectStyle",
"parentStyle", "parentStyle",
"grandStyle", "grandStyle",
@@ -82,12 +85,20 @@ export const matchRule = async (href, { injectRules, subrulesList }) => {
"transTitle", "transTitle",
// "detectRemote", // "detectRemote",
// "fixerFunc", // "fixerFunc",
"splitParagraph",
"highlightWords",
].forEach((key) => { ].forEach((key) => {
if (!rule[key] || rule[key] === GLOBAL_KEY) { if (!rule[key] || rule[key] === GLOBAL_KEY) {
rule[key] = globalRule[key]; rule[key] = globalRule[key];
} }
}); });
["splitLength"].forEach((key) => {
if (!rule[key]) {
rule[key] = globalRule[key];
}
});
// if (!rule.skipLangs || rule.skipLangs.length === 0) { // if (!rule.skipLangs || rule.skipLangs.length === 0) {
// rule.skipLangs = globalRule.skipLangs; // rule.skipLangs = globalRule.skipLangs;
// } // }
@@ -138,6 +149,7 @@ export const checkRules = (rules) => {
terms, terms,
aiTerms, aiTerms,
termsStyle, termsStyle,
highlightStyle,
selectStyle, selectStyle,
parentStyle, parentStyle,
grandStyle, grandStyle,
@@ -164,6 +176,9 @@ export const checkRules = (rules) => {
transStartHook, transStartHook,
transEndHook, transEndHook,
// transRemoveHook, // transRemoveHook,
splitParagraph,
splitLength,
highlightWords,
}) => ({ }) => ({
pattern: pattern.trim(), pattern: pattern.trim(),
selector: type(selector) === "string" ? selector : "", selector: type(selector) === "string" ? selector : "",
@@ -173,6 +188,7 @@ export const checkRules = (rules) => {
terms: type(terms) === "string" ? terms : "", terms: type(terms) === "string" ? terms : "",
aiTerms: type(aiTerms) === "string" ? aiTerms : "", aiTerms: type(aiTerms) === "string" ? aiTerms : "",
termsStyle: type(termsStyle) === "string" ? termsStyle : "", termsStyle: type(termsStyle) === "string" ? termsStyle : "",
highlightStyle: type(highlightStyle) === "string" ? highlightStyle : "",
selectStyle: type(selectStyle) === "string" ? selectStyle : "", selectStyle: type(selectStyle) === "string" ? selectStyle : "",
parentStyle: type(parentStyle) === "string" ? parentStyle : "", parentStyle: type(parentStyle) === "string" ? parentStyle : "",
grandStyle: type(grandStyle) === "string" ? grandStyle : "", grandStyle: type(grandStyle) === "string" ? grandStyle : "",
@@ -203,6 +219,15 @@ export const checkRules = (rules) => {
// transRemoveHook: // transRemoveHook:
// type(transRemoveHook) === "string" ? transRemoveHook : "", // type(transRemoveHook) === "string" ? transRemoveHook : "",
// fixerFunc: matchValue([GLOBAL_KEY, ...FIXER_ALL], fixerFunc), // fixerFunc: matchValue([GLOBAL_KEY, ...FIXER_ALL], fixerFunc),
splitParagraph: matchValue(
[GLOBAL_KEY, ...OPT_SPLIT_PARAGRAPH_ALL],
splitParagraph
),
splitLength: Number.isInteger(splitLength) ? splitLength : 0,
highlightWords: matchValue(
[GLOBAL_KEY, ...OPT_HIGHLIGHT_WORDS_ALL],
highlightWords
),
}) })
); );

View File

@@ -18,6 +18,11 @@ import {
MSG_TRANSBOX_TOGGLE, MSG_TRANSBOX_TOGGLE,
MSG_MOUSEHOVER_TOGGLE, MSG_MOUSEHOVER_TOGGLE,
MSG_TRANSINPUT_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"; } from "../config";
import interpreter from "./interpreter"; import interpreter from "./interpreter";
import { ShadowRootMonitor } from "./shadowroot"; import { ShadowRootMonitor } from "./shadowroot";
@@ -164,6 +169,8 @@ export class Translator {
warpper: `${APP_LCNAME}-wrapper notranslate`, warpper: `${APP_LCNAME}-wrapper notranslate`,
inner: `${APP_LCNAME}-inner`, inner: `${APP_LCNAME}-inner`,
term: `${APP_LCNAME}-term`, term: `${APP_LCNAME}-term`,
br: `${APP_LCNAME}-br`,
highlight: `${APP_LCNAME}-highlight`,
}; };
// 内置跳过翻译文本 // 内置跳过翻译文本
@@ -279,6 +286,7 @@ export class Translator {
#textClass = {}; // 译文样式class #textClass = {}; // 译文样式class
#textSheet = ""; // 译文样式字典 #textSheet = ""; // 译文样式字典
#apisMap = new Map(); // 用于接口快速查找 #apisMap = new Map(); // 用于接口快速查找
#favWords = []; // 收藏词汇
#isUserscript = false; #isUserscript = false;
#transboxManager = null; // 划词翻译 #transboxManager = null; // 划词翻译
@@ -289,6 +297,7 @@ export class Translator {
#viewNodes = new Set(); // 当前在可视范围内的单元 #viewNodes = new Set(); // 当前在可视范围内的单元
#processedNodes = new WeakMap(); // 已处理已执行翻译DOM操作的单元 #processedNodes = new WeakMap(); // 已处理已执行翻译DOM操作的单元
#rootNodes = new Set(); // 已监控的根节点 #rootNodes = new Set(); // 已监控的根节点
#skipMoNodes = new WeakSet(); // 忽略变化的节点
#removeKeydownHandler; // 快捷键清理函数 #removeKeydownHandler; // 快捷键清理函数
#hoveredNode = null; // 存储当前悬停的可翻译节点 #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.#setting = { ...Translator.DEFAULT_OPTIONS, ...setting };
this.#rule = { ...Translator.DEFAULT_RULE, ...rule }; this.#rule = { ...Translator.DEFAULT_RULE, ...rule };
this.#favWords = favWords;
this.#apisMap = new Map( this.#apisMap = new Map(
this.#setting.transApis.map((api) => [api.apiSlug, api]) this.#setting.transApis.map((api) => [api.apiSlug, api])
); );
@@ -585,6 +600,8 @@ export class Translator {
#createMutationObserver() { #createMutationObserver() {
return new MutationObserver((mutations) => { return new MutationObserver((mutations) => {
for (const mutation of mutations) { for (const mutation of mutations) {
if (this.#skipMoNodes.has(mutation.target)) return;
if ( if (
mutation.type === "characterData" && mutation.type === "characterData" &&
mutation.oldValue !== mutation.target.nodeValue mutation.oldValue !== mutation.target.nodeValue
@@ -599,6 +616,8 @@ export class Translator {
let nodes = new Set(); let nodes = new Set();
let hasText = false; let hasText = false;
mutation.addedNodes.forEach((node) => { mutation.addedNodes.forEach((node) => {
if (this.#skipMoNodes.has(node)) return;
if (/\S/.test(node.nodeValue)) { if (/\S/.test(node.nodeValue)) {
if (node.nodeType === Node.TEXT_NODE) { if (node.nodeType === Node.TEXT_NODE) {
hasText = true; hasText = true;
@@ -780,6 +799,10 @@ export class Translator {
// 开始/重新监控节点 // 开始/重新监控节点
#startObserveNode(node) { #startObserveNode(node) {
if (this.#rule.highlightWords === OPT_HIGHLIGHT_WORDS_BEFORETRANS) {
this.#highlightWordsDeeply(node);
}
if ( if (
!this.#observedNodes.has(node) && !this.#observedNodes.has(node) &&
this.#enabled && this.#enabled &&
@@ -861,7 +884,12 @@ export class Translator {
// 提前进行语言检测 // 提前进行语言检测
let deLang = ""; 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; const { langDetector, skipLangs = [] } = this.#setting;
if (fromLang === "auto") { if (fromLang === "auto") {
deLang = await tryDetectLang(node.textContent, langDetector); 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 = []; let nodeGroup = [];
[...node.childNodes].forEach((child) => { [...node.childNodes].forEach((child) => {
const shouldBreak = this.#shouldBreak(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) { #shouldBreak(node) {
if (!Translator.isElementOrFragment(node)) return false; if (!Translator.isElementOrFragment(node)) return false;
@@ -971,6 +1160,7 @@ export class Translator {
// detectRemote, // detectRemote,
// toLang, // toLang,
// skipLangs = [], // skipLangs = [],
highlightWords,
} = this.#rule; } = this.#rule;
const { const {
newlineLength, newlineLength,
@@ -1062,6 +1252,11 @@ export class Translator {
parentNode.parentElement.style.cssText += grandStyle; parentNode.parentElement.style.cssText += grandStyle;
} }
// 高亮词汇
if (highlightWords === OPT_HIGHLIGHT_WORDS_AFTERTRANS) {
nodes.forEach((node) => this.#highlightWordsDeeply(node));
}
// 翻译完成钩子函数 // 翻译完成钩子函数
if (transEndHook?.trim()) { if (transEndHook?.trim()) {
try { try {
@@ -1226,6 +1421,10 @@ export class Translator {
root root
.querySelectorAll(APP_LCNAME) .querySelectorAll(APP_LCNAME)
.forEach((el) => this.#removeTranslationElement(el)); .forEach((el) => this.#removeTranslationElement(el));
root
.querySelectorAll(Translator.KISS_CLASS.br)
.forEach((br) => br.remove());
} }
// 清理子节点译文dom // 清理子节点译文dom
@@ -1237,7 +1436,8 @@ export class Translator {
// 清理译文 // 清理译文
#removeTranslationElement(el) { #removeTranslationElement(el) {
this.#processedNodes.delete(el.parentElement); const parentElement = el.parentElement;
this.#processedNodes.delete(parentElement);
// 如果是仅显示译文模式,先恢复原文 // 如果是仅显示译文模式,先恢复原文
const { nodes, isHide } = this.#translationNodes.get(el) || {}; const { nodes, isHide } = this.#translationNodes.get(el) || {};
@@ -1247,6 +1447,11 @@ export class Translator {
this.#translationNodes.delete(el); this.#translationNodes.delete(el);
el.remove(); el.remove();
// 清除高亮
if (this.#rule.highlightWords === OPT_HIGHLIGHT_WORDS_AFTERTRANS) {
this.#removeHighlights(parentElement);
}
} }
// 恢复原文 // 恢复原文

View File

@@ -16,6 +16,10 @@ import {
URL_KISS_RULES_NEW_ISSUE, URL_KISS_RULES_NEW_ISSUE,
OPT_SYNCTYPE_WORKER, OPT_SYNCTYPE_WORKER,
DEFAULT_TRANS_TAG, DEFAULT_TRANS_TAG,
OPT_SPLIT_PARAGRAPH_DISABLE,
OPT_HIGHLIGHT_WORDS_DISABLE,
OPT_SPLIT_PARAGRAPH_ALL,
OPT_HIGHLIGHT_WORDS_ALL,
} from "../../config"; } from "../../config";
import { useState, useEffect, useMemo } from "react"; import { useState, useEffect, useMemo } from "react";
import { useI18n } from "../../hooks/I18n"; import { useI18n } from "../../hooks/I18n";
@@ -59,6 +63,7 @@ import AddIcon from "@mui/icons-material/Add";
import EditIcon from "@mui/icons-material/Edit"; import EditIcon from "@mui/icons-material/Edit";
import CancelIcon from "@mui/icons-material/Cancel"; import CancelIcon from "@mui/icons-material/Cancel";
import SaveIcon from "@mui/icons-material/Save"; import SaveIcon from "@mui/icons-material/Save";
import ValidationInput from "../../hooks/ValidationInput";
import { kissLog } from "../../libs/log"; import { kissLog } from "../../libs/log";
import { useApiList } from "../../hooks/Api"; import { useApiList } from "../../hooks/Api";
import ShowMoreButton from "./ShowMoreButton"; import ShowMoreButton from "./ShowMoreButton";
@@ -98,6 +103,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
terms = "", terms = "",
aiTerms = "", aiTerms = "",
termsStyle = "", termsStyle = "",
highlightStyle = "color: red;",
selectStyle = "", selectStyle = "",
parentStyle = "", parentStyle = "",
grandStyle = "", grandStyle = "",
@@ -124,6 +130,9 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
transStartHook = "", transStartHook = "",
transEndHook = "", transEndHook = "",
// transRemoveHook = "", // transRemoveHook = "",
splitParagraph = OPT_SPLIT_PARAGRAPH_DISABLE,
splitLength = 0,
highlightWords = OPT_HIGHLIGHT_WORDS_DISABLE,
} = formValues; } = formValues;
const isModified = useMemo(() => { const isModified = useMemo(() => {
@@ -423,6 +432,58 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
</TextField> </TextField>
</Grid> </Grid>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
size="small"
fullWidth
name="splitParagraph"
value={splitParagraph}
label={i18n("split_paragraph")}
disabled={disabled}
onChange={handleChange}
>
{GlobalItem}
{OPT_SPLIT_PARAGRAPH_ALL.map((item) => (
<MenuItem key={item} value={item}>
{i18n(item)}
</MenuItem>
))}
</TextField>
</Grid>
<Grid item xs={12} sm={12} md={6} lg={3}>
<ValidationInput
fullWidth
size="small"
label={i18n("split_length")}
type="number"
name="splitLength"
value={splitLength}
onChange={handleChange}
min={0}
max={1000}
/>
</Grid>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
size="small"
fullWidth
name="highlightWords"
value={highlightWords}
label={i18n("highlight_words")}
disabled={disabled}
onChange={handleChange}
>
{GlobalItem}
{OPT_HIGHLIGHT_WORDS_ALL.map((item) => (
<MenuItem key={item} value={item}>
{i18n(item)}
</MenuItem>
))}
</TextField>
</Grid>
<Grid item xs={12} sm={12} md={6} lg={3}> <Grid item xs={12} sm={12} md={6} lg={3}>
<TextField <TextField
select select
@@ -558,6 +619,16 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
maxRows={10} maxRows={10}
multiline multiline
/> />
<TextField
size="small"
label={i18n("highlight_style")}
name="highlightStyle"
value={highlightStyle}
disabled={disabled}
onChange={handleChange}
maxRows={10}
multiline
/>
<TextField <TextField
size="small" size="small"
label={i18n("selector_style")} label={i18n("selector_style")}