feat: support subtitle translate for userscript

This commit is contained in:
Gabe
2025-10-15 21:41:09 +08:00
parent 5e67e15842
commit ecab4ab634
9 changed files with 89 additions and 70 deletions

View File

@@ -91,7 +91,6 @@ const userscriptWebpack = (config, env) => {
// @grant GM.getValue // @grant GM.getValue
// @grant GM.deleteValue // @grant GM.deleteValue
// @grant GM.info // @grant GM.info
// @grant GM.addElement
// @grant unsafeWindow // @grant unsafeWindow
// @connect translate.googleapis.com // @connect translate.googleapis.com
// @connect translate-pa.googleapis.com // @connect translate-pa.googleapis.com
@@ -132,7 +131,6 @@ const userscriptWebpack = (config, env) => {
config.entry = { config.entry = {
main: paths.appIndexJs, main: paths.appIndexJs,
options: paths.appSrc + "/options.js", options: paths.appSrc + "/options.js",
injector: paths.appSrc + "/injector.js",
"kiss-translator.user": paths.appSrc + "/userscript.js", "kiss-translator.user": paths.appSrc + "/userscript.js",
}; };

View File

@@ -20,6 +20,7 @@ import { trySyncAllSubRules } from "./libs/subRules";
import { isInBlacklist } from "./libs/blacklist"; import { isInBlacklist } from "./libs/blacklist";
import { runSubtitle } from "./subtitle/subtitle"; import { runSubtitle } from "./subtitle/subtitle";
import { logger } from "./libs/log"; import { logger } from "./libs/log";
import { injectInlineJs } from "./libs/injector";
/** /**
* 油猴脚本设置页面 * 油猴脚本设置页面
@@ -35,9 +36,10 @@ function runSettingPage() {
const ping = genEventName(); const ping = genEventName();
window.addEventListener(ping, handlePing); window.addEventListener(ping, handlePing);
// window.eval(`(${injectScript})("${ping}")`); // eslint-disable-line // window.eval(`(${injectScript})("${ping}")`); // eslint-disable-line
const script = document.createElement("script"); injectInlineJs(
script.textContent = `(${injectScript})("${ping}")`; `(${injectScript})("${ping}")`,
document.head.append(script); "kiss-translator-options-injector"
);
} }
} }

View File

@@ -1,19 +1,3 @@
(function () { import { XMLHttpRequestInjector } from "./subtitle/XMLHttpRequestInjector";
const originalOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (...args) { XMLHttpRequestInjector();
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);
};
})();

View File

@@ -1,28 +1,29 @@
// Function to inject inline JavaScript code import { trustedTypesHelper } from "./trustedTypes";
export const injectInlineJs = (code) => {
const el = document.createElement("script");
el.setAttribute("data-source", "kiss-inject injectInlineJs");
el.setAttribute("type", "text/javascript");
el.textContent = code;
document.body?.appendChild(el);
};
// Function to inject external JavaScript file // Function to inject inline JavaScript code
export const injectExternalJs = (src, id = "kiss-translator-injector") => { export const injectInlineJs = (code, id = "kiss-translator-inline-js") => {
if (document.getElementById(id)) { if (document.getElementById(id)) {
return; return;
} }
// const el = document.createElement("script"); const el = document.createElement("script");
// el.setAttribute("data-source", "kiss-inject injectExternalJs"); el.type = "text/javascript";
// el.setAttribute("type", "text/javascript"); el.id = id;
// el.setAttribute("src", src); el.textContent = trustedTypesHelper.createScript(code);
// el.setAttribute("id", id); (document.head || document.documentElement).appendChild(el);
// document.body?.appendChild(el); };
const script = document.createElement("script");
script.id = id; // Function to inject external JavaScript file
script.src = src; export const injectExternalJs = (src, id = "kiss-translator-external-js") => {
(document.head || document.documentElement).appendChild(script); if (document.getElementById(id)) {
return;
}
const el = document.createElement("script");
el.type = "text/javascript";
el.id = id;
el.src = trustedTypesHelper.createScriptURL(src);
(document.head || document.documentElement).appendChild(el);
}; };
// Function to inject internal CSS code // Function to inject internal CSS code

View File

@@ -37,6 +37,7 @@ import { browser } from "./browser";
import { isIframe, sendIframeMsg } from "./iframe"; import { isIframe, sendIframeMsg } from "./iframe";
import { TransboxManager } from "./tranbox"; import { TransboxManager } from "./tranbox";
import { InputTranslator } from "./inputTranslate"; import { InputTranslator } from "./inputTranslate";
import { trustedTypesHelper } from "./trustedTypes";
/** /**
* @class Translator * @class Translator
@@ -1025,7 +1026,7 @@ export class Translator {
translatedText, translatedText,
placeholderMap placeholderMap
); );
const trustedHTML = this.#createTrustedHTML(htmlString); const trustedHTML = trustedTypesHelper.createHTML(htmlString);
// const parser = new DOMParser(); // const parser = new DOMParser();
// const doc = parser.parseFromString(trustedHTML, "text/html"); // const doc = parser.parseFromString(trustedHTML, "text/html");
@@ -1076,19 +1077,6 @@ export class Translator {
} }
} }
#createTrustedHTML(html) {
if (window.trustedTypes && window.trustedTypes.createPolicy) {
const policy = window.trustedTypes.createPolicy(
"kiss-translator-policy#html",
{
createHTML: (input) => input,
}
);
return policy.createHTML(html);
}
return html;
}
// 处理节点转为翻译字符串 // 处理节点转为翻译字符串
#serializeForTranslation(nodes) { #serializeForTranslation(nodes) {
let replaceCounter = 0; // {{n}} let replaceCounter = 0; // {{n}}
@@ -1404,7 +1392,8 @@ export class Translator {
injectJs && sendBgMsg(MSG_INJECT_JS, injectJs); injectJs && sendBgMsg(MSG_INJECT_JS, injectJs);
injectCss && sendBgMsg(MSG_INJECT_CSS, injectCss); injectCss && sendBgMsg(MSG_INJECT_CSS, injectCss);
} else { } else {
injectJs && injectInlineJs(injectJs); injectJs &&
injectInlineJs(injectJs, "kiss-translator-userinit-injector");
injectCss && injectInternalCss(injectCss); injectCss && injectInternalCss(injectCss);
} }
} catch (err) { } catch (err) {

33
src/libs/trustedTypes.js Normal file
View File

@@ -0,0 +1,33 @@
export const trustedTypesHelper = (() => {
const POLICY_NAME = "kiss-translator-policy";
let policy = null;
if (globalThis.trustedTypes && globalThis.trustedTypes.createPolicy) {
try {
policy = globalThis.trustedTypes.createPolicy(POLICY_NAME, {
createHTML: (string) => string,
createScript: (string) => string,
createScriptURL: (string) => string,
});
} catch (err) {
if (err.message.includes("already exists")) {
policy = globalThis.trustedTypes.policies.get(POLICY_NAME);
} else {
console.error("cont create Trusted Types", err);
}
}
}
return {
createHTML: (htmlString) => {
return policy ? policy.createHTML(htmlString) : htmlString;
},
createScript: (scriptString) => {
return policy ? policy.createScript(scriptString) : scriptString;
},
createScriptURL: (urlString) => {
return policy ? policy.createScriptURL(urlString) : urlString;
},
isEnabled: () => policy !== null,
};
})();

View File

@@ -0,0 +1,19 @@
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);
};
};

View File

@@ -38,7 +38,6 @@ class YouTubeCaptionProvider {
initialize() { initialize() {
window.addEventListener("message", (event) => { window.addEventListener("message", (event) => {
if (event.source !== window) return;
if (event.data?.type === MSG_XHR_DATA_YOUTUBE) { if (event.data?.type === MSG_XHR_DATA_YOUTUBE) {
const { url, response } = event.data; const { url, response } = event.data;
if (url && response) { if (url && response) {

View File

@@ -5,6 +5,8 @@ import { DEFAULT_API_SETTING } from "../config/api.js";
import { DEFAULT_SUBTITLE_SETTING } from "../config/setting.js"; import { DEFAULT_SUBTITLE_SETTING } from "../config/setting.js";
import { injectExternalJs } from "../libs/injector.js"; import { injectExternalJs } from "../libs/injector.js";
import { logger } from "../libs/log.js"; import { logger } from "../libs/log.js";
import { XMLHttpRequestInjector } from "./XMLHttpRequestInjector.js";
import { injectInlineJs } from "../libs/injector.js";
const providers = [ const providers = [
{ pattern: "https://www.youtube.com", start: YouTubeInitializer }, { pattern: "https://www.youtube.com", start: YouTubeInitializer },
@@ -19,18 +21,10 @@ export function runSubtitle({ href, setting, isUserscript }) {
const provider = providers.find((item) => isMatch(href, item.pattern)); const provider = providers.find((item) => isMatch(href, item.pattern));
if (provider) { if (provider) {
const id = "kiss-translator-xmlHttp-injector";
if (isUserscript) { if (isUserscript) {
GM.addElement("script", { injectInlineJs(`(${XMLHttpRequestInjector})()`, id);
src: "https://github.com/fishjar/kiss-translator/blob/gh-pages/injector.js",
// src: "http://127.0.0.1:8000/injector.js",
type: "text/javascript",
}).onload = function () {
console.log(
"Script successfully injected and loaded via GM_addElement."
);
};
} else { } else {
const id = "kiss-translator-injector";
const src = browser.runtime.getURL("injector.js"); const src = browser.runtime.getURL("injector.js");
injectExternalJs(src, id); injectExternalJs(src, id);
} }