feat: highlight fav words && split long paragraph
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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] || "";
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|||||||
@@ -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
|
||||||
|
),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 恢复原文
|
// 恢复原文
|
||||||
|
|||||||
@@ -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")}
|
||||||
|
|||||||
Reference in New Issue
Block a user