feat: add shadowroot injector
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
}
|
||||
],
|
||||
"web_accessible_resources": [
|
||||
"injector.js"
|
||||
"injector-subtitle.js",
|
||||
"injector-shadowroot.js"
|
||||
],
|
||||
"commands": {
|
||||
"_execute_browser_action": {
|
||||
|
||||
@@ -19,8 +19,12 @@
|
||||
],
|
||||
"web_accessible_resources": [
|
||||
{
|
||||
"resources": ["injector.js"],
|
||||
"resources": ["injector-subtitle.js"],
|
||||
"matches": ["https://www.youtube.com/*"]
|
||||
},
|
||||
{
|
||||
"resources": ["injector-shadowroot.js"],
|
||||
"matches": ["<all_urls>"]
|
||||
}
|
||||
],
|
||||
"commands": {
|
||||
|
||||
@@ -23,7 +23,8 @@
|
||||
}
|
||||
],
|
||||
"web_accessible_resources": [
|
||||
"injector.js"
|
||||
"injector-subtitle.js",
|
||||
"injector-shadowroot.js"
|
||||
],
|
||||
"commands": {
|
||||
"_execute_browser_action": {
|
||||
|
||||
3
src/injector-shadowroot.js
Normal file
3
src/injector-shadowroot.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import { shadowRootInjector } from "./injectors/shadowroot";
|
||||
|
||||
shadowRootInjector();
|
||||
3
src/injector-subtitle.js
Normal file
3
src/injector-subtitle.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import { XMLHttpRequestInjector } from "./injectors/xmlhttp";
|
||||
|
||||
XMLHttpRequestInjector();
|
||||
@@ -1,3 +0,0 @@
|
||||
import { XMLHttpRequestInjector } from "./subtitle/XMLHttpRequestInjector";
|
||||
|
||||
XMLHttpRequestInjector();
|
||||
27
src/injectors/index.js
Normal file
27
src/injectors/index.js
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
12
src/injectors/shadowroot.js
Normal file
12
src/injectors/shadowroot.js
Normal file
@@ -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);
|
||||
}
|
||||
};
|
||||
23
src/injectors/xmlhttp.js
Normal file
23
src/injectors/xmlhttp.js
Normal file
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,8 +413,35 @@ export class Translator {
|
||||
this.#startObserveRoot(root);
|
||||
});
|
||||
|
||||
// 查找现有的所有shadowroot
|
||||
if (this.#rule.hasShadowroot === "true") {
|
||||
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);
|
||||
@@ -420,7 +450,6 @@ export class Translator {
|
||||
kissLog("findAllShadowRoots", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#createPlaceholderRegex() {
|
||||
const escapedStart = Translator.escapeRegex(
|
||||
@@ -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;
|
||||
|
||||
@@ -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}><${tag} class="kiss-p">`;
|
||||
} else if (newlineTags.indexOf(child.nodeName) !== -1) {
|
||||
html += `</${tag}>${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 += `</${tag}>`;
|
||||
}
|
||||
});
|
||||
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 || " "}</${tag}>`)
|
||||
.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}`);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
};
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user