Hitokoto / static /embed.js
vomebook's picture
feat: add explicit CCRD embedded reader bridge
af228e3
Raw History Blame Contribute Delete
5.52 kB
"use strict";
// Explicit CCRD pilot bridge. No URL text import, automatic synthesis or wildcard
// postMessage. Parent identity and exact iframe Window are both mandatory.
(() => {
if (!embeddedReader) return;
const PARENT = "https://vomebook-search.hf.space";
let channel = "", imported = null, replacement = null, modal = false;
const title = document.createElement("p");
title.className = "note embed-source";
document.querySelector("main").prepend(title);
const confirmation = document.createElement("div");
confirmation.className = "actions";
confirmation.hidden = true;
const replace = document.createElement("button"), keep = document.createElement("button");
replace.type = keep.type = "button";
replace.id = "embed-replace"; keep.id = "embed-keep";
replace.textContent = "停止原朗读并替换文字"; keep.textContent = "保留原朗读";
confirmation.append(replace, keep);
title.after(confirmation);
function send(type, extra = {}) {
if (channel) window.parent.postMessage({ type, channel, ...extra }, PARENT);
}
function validText(text, max, allowEmpty = false) {
if (typeof text !== "string" || (!allowEmpty && !text.trim())) return false;
let count = 0;
for (const _ of text) if (++count > max) return false;
return true;
}
function focusable() {
return Array.from(document.querySelectorAll("button, a[href], input, select, textarea, audio[controls], [tabindex]:not([tabindex='-1'])"))
.filter(node => !node.disabled && !node.hidden && node.getClientRects().length);
}
function load(data) {
replacement = null;
confirmation.hidden = true;
stop({ message: "已载入 CCRD 正文;确认文字后点击合成才向语音 API 提交。" });
imported = { text: data.text, context_before: data.context_before, context_after: data.context_after };
bookmark = null;
$("#remember").checked = false;
$("#speech-text").value = data.text;
$("#reader-view").hidden = true;
$("#reader-source").replaceChildren();
$("#position").textContent = "";
$("#metrics").textContent = "";
$("#reading-progress").value = 0;
title.textContent = "CCRD · " + data.title;
controls();
send("ccrd-reader-loaded", { characters: Array.from(data.text).length });
}
replace.addEventListener("click", () => { if (replacement) load(replacement); });
keep.addEventListener("click", () => { replacement = null; confirmation.hidden = true; send("ccrd-reader-rejected"); });
window.HitokotoEmbed = {
context(plan, text) {
if (!imported || text !== imported.text || !plan.length) return;
plan[0].context_before = imported.context_before;
plan[plan.length - 1].context_after = imported.context_after;
},
};
window.addEventListener("message", (event) => {
const data = event.data;
if (event.origin !== PARENT || event.source !== window.parent || !data ||
typeof data.channel !== "string" || !/^[a-f0-9]{48}$/.test(data.channel)) return;
if (data.type === "ccrd-reader-init" && data.version === 1) {
if (channel && channel !== data.channel) return;
channel = data.channel;
modal = data.modal === true;
send("ccrd-reader-ready", { version: 1, max_characters: ReaderPlan.maxText });
return;
}
if (!channel || data.channel !== channel) return;
if (data.type === "ccrd-reader-import") {
if (!validText(data.text, ReaderPlan.maxText) || !validText(data.title, 200, true) ||
!validText(data.context_before, 32, true) || !validText(data.context_after, 32, true)) {
send("ccrd-reader-rejected"); return;
}
const payload = { text: data.text, title: data.title, context_before: data.context_before, context_after: data.context_after };
if (session) {
// Native confirm() is restricted in cross-origin iframes in some
// browsers. Keep this explicit user decision inside the visible UI.
replacement = payload;
confirmation.hidden = false;
send("ccrd-reader-pending");
return;
}
load(payload);
} else if (data.type === "ccrd-reader-theme" && typeof data.dark === "boolean") {
document.documentElement.style.colorScheme = data.dark ? "dark" : "light";
} else if (data.type === "ccrd-reader-layout" && typeof data.modal === "boolean") {
modal = data.modal;
} else if (data.type === "ccrd-reader-focus" && modal && typeof data.last === "boolean") {
const nodes = focusable();
(data.last ? nodes[nodes.length - 1] : nodes[0])?.focus();
} else if (data.type === "ccrd-reader-stop") {
replacement = null;
confirmation.hidden = true;
stop({ message: "已停止,当前页面内可从进度继续" });
send("ccrd-reader-stopped");
}
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape" && event.target.tagName !== "SELECT") {
event.preventDefault(); send("ccrd-reader-collapse");
}
if (modal && event.key === "Tab") {
const nodes = focusable();
if ((!event.shiftKey && event.target === nodes[nodes.length - 1]) || (event.shiftKey && event.target === nodes[0])) {
event.preventDefault(); send("ccrd-reader-focus-out", { reverse: event.shiftKey });
}
}
});
$("#speech-text").value = "";
$("#remember").checked = false;
title.textContent = "CCRD 朗读试点:请从搜索站载入选文或预览正文。仅在当前页面保留进度,不自动保存正文。";
})();