diff --git a/config-overrides.js b/config-overrides.js index b05308c..df4995d 100644 --- a/config-overrides.js +++ b/config-overrides.js @@ -32,7 +32,8 @@ const extWebpack = (config, env) => { options: paths.appSrc + "/options.js", background: paths.appSrc + "/background.js", content: paths.appSrc + "/content.js", - injector: paths.appSrc + "/injector.js", + "injector-subtitle": paths.appSrc + "/injector-subtitle.js", + "injector-shadowroot": paths.appSrc + "/injector-shadowroot.js", }; config.output.filename = "[name].js"; diff --git a/public/manifest.firefox.json b/public/manifest.firefox.json index 9e1b0a1..1746136 100644 --- a/public/manifest.firefox.json +++ b/public/manifest.firefox.json @@ -17,7 +17,8 @@ } ], "web_accessible_resources": [ - "injector.js" + "injector-subtitle.js", + "injector-shadowroot.js" ], "commands": { "_execute_browser_action": { diff --git a/public/manifest.json b/public/manifest.json index 863944c..9bb1100 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -19,8 +19,12 @@ ], "web_accessible_resources": [ { - "resources": ["injector.js"], + "resources": ["injector-subtitle.js"], "matches": ["https://www.youtube.com/*"] + }, + { + "resources": ["injector-shadowroot.js"], + "matches": [""] } ], "commands": { diff --git a/public/manifest.thunderbird.json b/public/manifest.thunderbird.json index f128f87..515213a 100644 --- a/public/manifest.thunderbird.json +++ b/public/manifest.thunderbird.json @@ -23,7 +23,8 @@ } ], "web_accessible_resources": [ - "injector.js" + "injector-subtitle.js", + "injector-shadowroot.js" ], "commands": { "_execute_browser_action": { diff --git a/src/injector-shadowroot.js b/src/injector-shadowroot.js new file mode 100644 index 0000000..586302d --- /dev/null +++ b/src/injector-shadowroot.js @@ -0,0 +1,3 @@ +import { shadowRootInjector } from "./injectors/shadowroot"; + +shadowRootInjector(); diff --git a/src/injector-subtitle.js b/src/injector-subtitle.js new file mode 100644 index 0000000..b5bd96b --- /dev/null +++ b/src/injector-subtitle.js @@ -0,0 +1,3 @@ +import { XMLHttpRequestInjector } from "./injectors/xmlhttp"; + +XMLHttpRequestInjector(); diff --git a/src/injector.js b/src/injector.js deleted file mode 100644 index b237a93..0000000 --- a/src/injector.js +++ /dev/null @@ -1,3 +0,0 @@ -import { XMLHttpRequestInjector } from "./subtitle/XMLHttpRequestInjector"; - -XMLHttpRequestInjector(); diff --git a/src/injectors/index.js b/src/injectors/index.js new file mode 100644 index 0000000..d0cac86 --- /dev/null +++ b/src/injectors/index.js @@ -0,0 +1,27 @@ +import { browser } from "../libs/browser"; +import { isExt } from "../libs/client"; +import { injectExternalJs, injectInlineJs } from "../libs/injector"; +import { shadowRootInjector } from "./shadowroot"; +import { XMLHttpRequestInjector } from "./xmlhttp"; + +export const INJECTOR = { + subtitle: "injector-subtitle.js", + shadowroot: "injector-shadowroot.js", +}; + +const injectorMap = { + [INJECTOR.subtitle]: XMLHttpRequestInjector, + [INJECTOR.shadowroot]: shadowRootInjector, +}; + +export function injectJs(name, id = "kiss-translator-inject-js") { + const injector = injectorMap[name]; + if (!injector) return; + + if (isExt) { + const src = browser.runtime.getURL(name); + injectExternalJs(src, id); + } else { + injectInlineJs(`(${injector})()`, id); + } +} diff --git a/src/injectors/shadowroot.js b/src/injectors/shadowroot.js new file mode 100644 index 0000000..7c5796d --- /dev/null +++ b/src/injectors/shadowroot.js @@ -0,0 +1,12 @@ +export const shadowRootInjector = () => { + try { + const orig = Element.prototype.attachShadow; + Element.prototype.attachShadow = function (...args) { + const root = orig.apply(this, args); + window.postMessage({ type: "KISS_SHADOW_ROOT_CREATED" }, "*"); + return root; + }; + } catch (err) { + console.log("shadowRootInjector", err); + } +}; diff --git a/src/injectors/xmlhttp.js b/src/injectors/xmlhttp.js new file mode 100644 index 0000000..65b6d15 --- /dev/null +++ b/src/injectors/xmlhttp.js @@ -0,0 +1,23 @@ +export const XMLHttpRequestInjector = () => { + try { + const originalOpen = XMLHttpRequest.prototype.open; + XMLHttpRequest.prototype.open = function (...args) { + const url = args[1]; + if (typeof url === "string" && url.includes("timedtext")) { + this.addEventListener("load", function () { + window.postMessage( + { + type: "KISS_XHR_DATA_YOUTUBE", + url: this.responseURL, + response: this.responseText, + }, + window.location.origin + ); + }); + } + return originalOpen.apply(this, args); + }; + } catch (err) { + console.log("XMLHttpRequestInjector", err); + } +}; diff --git a/src/libs/shadowRootMonitor.js b/src/libs/shadowRootMonitor.js deleted file mode 100644 index bed604b..0000000 --- a/src/libs/shadowRootMonitor.js +++ /dev/null @@ -1,56 +0,0 @@ -import { kissLog } from "./log"; - -/** - * @class ShadowRootMonitor - * @description 通过覆写 Element.prototype.attachShadow 来监控页面上所有新创建的 Shadow DOM - */ -export default class ShadowRootMonitor { - /** - * @param {function(ShadowRoot): void} callback - 当一个新的 shadowRoot 被创建时调用的回调函数。 - */ - constructor(callback) { - if (typeof callback !== "function") { - throw new Error("Callback must be a function."); - } - - this.callback = callback; - this.isMonitoring = false; - this.originalAttachShadow = Element.prototype.attachShadow; - } - - /** - * 开始监控 shadowRoot 的创建。 - */ - start() { - if (this.isMonitoring) { - return; - } - const monitorInstance = this; - - Element.prototype.attachShadow = function (...args) { - const shadowRoot = monitorInstance.originalAttachShadow.apply(this, args); - if (shadowRoot) { - try { - monitorInstance.callback(shadowRoot); - } catch (error) { - kissLog("Error in ShadowRootMonitor callback", error); - } - } - return shadowRoot; - }; - - this.isMonitoring = true; - } - - /** - * 停止监控,并恢复原始的 attachShadow 方法。 - */ - stop() { - if (!this.isMonitoring) { - return; - } - - Element.prototype.attachShadow = this.originalAttachShadow; - this.isMonitoring = false; - } -} diff --git a/src/libs/translator.js b/src/libs/translator.js index d2a5061..adebf80 100644 --- a/src/libs/translator.js +++ b/src/libs/translator.js @@ -17,7 +17,6 @@ import { OPT_SPLIT_PARAGRAPH_TEXTLENGTH, } from "../config"; import interpreter from "./interpreter"; -import ShadowRootMonitor from "./shadowRootMonitor"; import { clearFetchPool } from "./pool"; import { debounce, scheduleIdle, genEventName, truncateWords } from "./utils"; import { apiTranslate } from "../apis"; @@ -31,6 +30,7 @@ import { createLoadingSVG } from "./svg"; import { shortcutRegister } from "./shortcut"; import { tryDetectLang } from "./detect"; import { trustedTypesHelper } from "./trustedTypes"; +import { injectJs, INJECTOR } from "../injectors"; /** * @class Translator @@ -278,6 +278,7 @@ export class Translator { #rule; // 规则 #isInitialized = false; // 初始化状态 #isJsInjected = false; // 注入用户JS + #isShadowRootJsInjected = false; // #mouseHoverEnabled = false; // 鼠标悬停翻译 #enabled = false; // 全局默认状态 #runId = 0; // 用于中止过期的异步请求 @@ -305,11 +306,13 @@ export class Translator { #hoveredNode = null; // 存储当前悬停的可翻译节点 #boundMouseMoveHandler; // 鼠标事件 #boundKeyDownHandler; // 键盘事件 + #windowMessageHandler = null; + + #debouncedFindShadowRoot = null; #io; // IntersectionObserver #mo; // MutationObserver #dmm; // DebounceMouseMover - #srm; // ShadowRootMonitor #rescanQueue = new Set(); // “脏容器”队列 #isQueueProcessing = false; // 队列处理状态标志 @@ -368,12 +371,12 @@ export class Translator { this.#io = this.#createIntersectionObserver(); this.#mo = this.#createMutationObserver(); this.#dmm = this.#createDebounceMouseMover(); - this.#srm = this.#createShadowRootMonitor(); - // 监控shadowroot - if (this.#rule.hasShadowroot === "true") { - this.#srm.start(); - } + this.#windowMessageHandler = this.#handleWindowMessage.bind(this); + this.#debouncedFindShadowRoot = debounce( + this.#findAndObserveShadowRoot.bind(this), + 300 + ); // 鼠标悬停翻译 if (this.#setting.mouseHoverSetting.useMouseHover) { @@ -410,15 +413,41 @@ export class Translator { this.#startObserveRoot(root); }); - // 查找现有的所有shadowroot if (this.#rule.hasShadowroot === "true") { - try { - this.#findAllShadowRoots().forEach((shadowRoot) => { - this.#startObserveShadowRoot(shadowRoot); - }); - } catch (err) { - kissLog("findAllShadowRoots", err); - } + this.#attachShadowRootListener(); + this.#findAndObserveShadowRoot(); + } + } + + #handleWindowMessage(event) { + if (event.data?.type === "KISS_SHADOW_ROOT_CREATED") { + this.#debouncedFindShadowRoot(); + } + } + + #attachShadowRootListener() { + if (!this.#isShadowRootJsInjected) { + const id = "kiss-translator-inject-shadowroot-js"; + injectJs(INJECTOR.shadowroot, id); + + this.#isShadowRootJsInjected = true; + } + + window.addEventListener("message", this.#windowMessageHandler); + } + + #removeShadowRootListener() { + window.removeEventListener("message", this.#windowMessageHandler); + } + + // 查找现有的所有shadowroot + #findAndObserveShadowRoot() { + try { + this.#findAllShadowRoots().forEach((shadowRoot) => { + this.#startObserveShadowRoot(shadowRoot); + }); + } catch (err) { + kissLog("findAllShadowRoots", err); } } @@ -611,13 +640,6 @@ export class Translator { }, 100); } - // 创建shadowroot的回调 - #createShadowRootMonitor() { - return new ShadowRootMonitor((shadowRoot) => { - this.#startObserveShadowRoot(shadowRoot); - }); - } - // 跟踪鼠标下的可翻译节点 #handleMouseMove(event) { let targetNode = event.composedPath()[0]; @@ -1527,6 +1549,8 @@ export class Translator { // 停止监听,重置参数 #resetOptions() { + this.#removeShadowRootListener(); + this.#io.disconnect(); this.#mo.disconnect(); this.#viewNodes.clear(); @@ -1697,7 +1721,6 @@ export class Translator { stop() { this.disable(); this.#resetOptions(); - this.#srm.stop(); this.#disableMouseHover(); this.#removeInjector(); this.#isInitialized = false; diff --git a/src/libs/webfix.js b/src/libs/webfix.js deleted file mode 100644 index a686786..0000000 --- a/src/libs/webfix.js +++ /dev/null @@ -1,158 +0,0 @@ -/** - * 修复程序类型 - */ -export const FIXER_NONE = "-"; -export const FIXER_BR = "br"; -export const FIXER_BN = "bn"; -export const FIXER_BR_DIV = "brToDiv"; -export const FIXER_BN_DIV = "bnToDiv"; - -export const FIXER_ALL = [ - FIXER_NONE, - FIXER_BR, - FIXER_BN, - FIXER_BR_DIV, - FIXER_BN_DIV, -]; - -/** - * 修复过的标记 - */ -const fixedSign = "kiss-fixed"; - -/** - * 采用 `br` 换行网站的修复函数 - * 目标是将 `br` 替换成 `p` - * @param {*} node - * @returns - */ -function brFixer(node, tag = "p") { - if (node.hasAttribute(fixedSign)) { - return; - } - node.setAttribute(fixedSign, "true"); - - const gapTags = ["BR", "WBR"]; - const newlineTags = [ - "DIV", - "UL", - "OL", - "LI", - "H1", - "H2", - "H3", - "H4", - "H5", - "H6", - "P", - "HR", - "PRE", - "TABLE", - "BLOCKQUOTE", - ]; - - let html = ""; - node.childNodes.forEach(function (child, index) { - if (index === 0) { - html += `<${tag} class="kiss-p">`; - } - - if (gapTags.indexOf(child.nodeName) !== -1) { - html += `<${tag} class="kiss-p">`; - } else if (newlineTags.indexOf(child.nodeName) !== -1) { - html += `${child.outerHTML}<${tag} class="kiss-p">`; - } else if (child.outerHTML) { - html += child.outerHTML; - } else if (child.textContent) { - html += child.textContent; - } - - if (index === node.childNodes.length - 1) { - html += ``; - } - }); - node.innerHTML = html; -} - -function brDivFixer(node) { - return brFixer(node, "div"); -} - -/** - * 目标是将 `\n` 替换成 `p` - * @param {*} node - * @returns - */ -function bnFixer(node, tag = "p") { - if (node.hasAttribute(fixedSign)) { - return; - } - node.setAttribute(fixedSign, "true"); - node.innerHTML = node.innerHTML - .split("\n") - .map((item) => `<${tag} class="kiss-p">${item || " "}`) - .join(""); -} - -function bnDivFixer(node) { - return bnFixer(node, "div"); -} - -/** - * 查找、监听节点,并执行修复函数 - * @param {*} selector - * @param {*} fixer - * @param {*} rootSelector - */ -function run(selector, fixer, rootSelector) { - const mutaObserver = new MutationObserver(function (mutations) { - mutations.forEach(function (mutation) { - mutation.addedNodes.forEach(function (addNode) { - if (addNode && addNode.querySelectorAll) { - addNode.querySelectorAll(selector).forEach(function (node) { - fixer(node); - }); - } - }); - }); - }); - - let rootNodes = [document]; - if (rootSelector) { - rootNodes = document.querySelectorAll(rootSelector); - } - - rootNodes.forEach(function (rootNode) { - rootNode.querySelectorAll(selector).forEach(function (node) { - fixer(node); - }); - mutaObserver.observe(rootNode, { - childList: true, - subtree: true, - }); - }); -} - -/** - * 修复程序映射 - */ -const fixerMap = { - [FIXER_BR]: brFixer, - [FIXER_BN]: bnFixer, - [FIXER_BR_DIV]: brDivFixer, - [FIXER_BN_DIV]: bnDivFixer, -}; - -/** - * 执行fixer - * @param {*} param0 - */ -export function runFixer(selector, fixer = "-", rootSelector) { - try { - if (Object.keys(fixerMap).includes(fixer)) { - run(selector, fixerMap[fixer], rootSelector); - } - } catch (err) { - console.error(`[kiss-webfix run]: ${err.message}`); - } -} diff --git a/src/subtitle/XMLHttpRequestInjector.js b/src/subtitle/XMLHttpRequestInjector.js deleted file mode 100644 index 460c317..0000000 --- a/src/subtitle/XMLHttpRequestInjector.js +++ /dev/null @@ -1,19 +0,0 @@ -export const XMLHttpRequestInjector = () => { - const originalOpen = XMLHttpRequest.prototype.open; - XMLHttpRequest.prototype.open = function (...args) { - const url = args[1]; - if (typeof url === "string" && url.includes("timedtext")) { - this.addEventListener("load", function () { - window.postMessage( - { - type: "KISS_XHR_DATA_YOUTUBE", - url: this.responseURL, - response: this.responseText, - }, - window.location.origin - ); - }); - } - return originalOpen.apply(this, args); - }; -}; diff --git a/src/subtitle/subtitle.js b/src/subtitle/subtitle.js index 937726e..61147fe 100644 --- a/src/subtitle/subtitle.js +++ b/src/subtitle/subtitle.js @@ -1,18 +1,15 @@ import { YouTubeInitializer } from "./YouTubeCaptionProvider.js"; -import { browser } from "../libs/browser.js"; import { isMatch } from "../libs/utils.js"; import { DEFAULT_API_SETTING } from "../config/api.js"; import { DEFAULT_SUBTITLE_SETTING } from "../config/setting.js"; -import { injectExternalJs } from "../libs/injector.js"; import { logger } from "../libs/log.js"; -import { XMLHttpRequestInjector } from "./XMLHttpRequestInjector.js"; -import { injectInlineJs } from "../libs/injector.js"; +import { injectJs, INJECTOR } from "../injectors/index.js"; const providers = [ { pattern: "https://www.youtube.com", start: YouTubeInitializer }, ]; -export function runSubtitle({ href, setting, isUserscript }) { +export function runSubtitle({ href, setting }) { try { const subtitleSetting = setting.subtitleSetting || DEFAULT_SUBTITLE_SETTING; if (!subtitleSetting.enabled) { @@ -21,13 +18,8 @@ export function runSubtitle({ href, setting, isUserscript }) { const provider = providers.find((item) => isMatch(href, item.pattern)); if (provider) { - const id = "kiss-translator-xmlHttp-injector"; - if (isUserscript) { - injectInlineJs(`(${XMLHttpRequestInjector})()`, id); - } else { - const src = browser.runtime.getURL("injector.js"); - injectExternalJs(src, id); - } + const id = "kiss-translator-inject-subtitle-js"; + injectJs(INJECTOR.subtitle, id); const apiSetting = setting.transApis.find(