// SPDX-License-Identifier: GPL-3.0-or-later
import "./external-links.js";
import "./render/glossary.js";
const BLOCKS = new Set(["BR", "DIV", "LI", "OL", "P", "TABLE", "TBODY", "TD", "TFOOT", "TH", "THEAD", "TR", "UL"]);
function plainText(node) {
if (node.nodeType === 3) return node.textContent;
if (node.getAttribute?.("aria-hidden") === "true") return "";
return [...node.childNodes].map(plainText).join("") + (BLOCKS.has(node.nodeName) ? "\n" : "");
}
function imageSize(image, value) {
const units = value.sizeUnits === "em" ? "em" : "px";
for (const dimension of ["width", "height"]) {
if (Number.isFinite(value[dimension]) && value[dimension] > 0) image.setAttribute(dimension, String(value[dimension]));
const preferred = value[dimension === "width" ? "preferredWidth" : "preferredHeight"];
if (Number.isFinite(preferred) && preferred > 0) image.style[dimension] = `${preferred}${units}`;
}
if (image.style.width && !image.style.height) image.style.height = "auto";
else if (image.style.height && !image.style.width) image.style.width = "auto";
}
export function createAnkiDefinitionRenderer(document, request, filenameFor) {
const inert = document.implementation.createHTMLDocument("");
const groups = new Map();
for (const glossary of request.term.glossaries) {
if (!groups.has(glossary.dictionary)) groups.set(glossary.dictionary, []);
groups.get(glossary.dictionary).push(glossary);
}
const media = new Map(request.dictionaryMedia.map(item => [JSON.stringify([item.dictionary, item.path]), item.filename]));
const dictionaryAlias = dictionary => Object.hasOwn(request.dictionaryAliases, dictionary)
? request.dictionaryAliases[dictionary] : dictionary;
const escape = text => { const span = inert.createElement("span"); span.textContent = text; return span.innerHTML; };
function appendImage(doc, parent, value, { dictionary, path }, pending) {
const image = doc.createElement("img");
image.className = "gloss-sc-img";
pending.push(Promise.resolve(filenameFor ? filenameFor(dictionary, path) : media.get(JSON.stringify([dictionary, path])))
.then(filename => { if (filename) image.setAttribute("src", filename); }));
image.alt = typeof value.title === "string" ? value.title : "Dictionary image";
image.style.maxWidth = "100%";
image.style.objectFit = "contain";
imageSize(image, value);
parent.append(image);
}
function content(glossary, pending) {
const body = inert.createElement("div");
body.className = "gsm-hoshidicts-glossary-content";
body.dataset.hoshidictsDictionary = glossary.dictionary;
globalThis.HDGlossary.appendTextOnlyGlossary(inert, body, glossary.glossary, {
dictionary: glossary.dictionary, appendImage: pending ? (...args) => appendImage(...args, pending) : () => {},
});
return body;
}
function plainDefinition(selected, noDictionary) {
const lines = [];
for (const [dictionary, glossaries] of selected) {
if (!noDictionary) lines.push(`(${escape(dictionaryAlias(dictionary))})`);
for (const glossary of glossaries) {
lines.push(...plainText(content(glossary)).split(/\r?\n|\r/u).map(line => line.trim()).filter(Boolean).map(escape));
}
}
return lines.join("
");
}
function entry(glossary, brief, noDictionary, pending) {
const wrapper = inert.createElement("div");
const labels = brief ? [] : [glossary.definitionTags, glossary.termTags,
noDictionary ? "" : dictionaryAlias(glossary.dictionary)].filter(Boolean);
if (labels.length) {
const meta = inert.createElement("i");
meta.className = "yomitan-glossary-meta";
meta.textContent = `(${labels.join(", ")})`;
wrapper.append(meta, " ");
}
wrapper.append(content(glossary, pending));
return wrapper;
}
function appendStyles(root, selected) {
const names = new Set(selected.map(([name]) => name));
const styles = request.dictionaryStyles.filter(style => names.has(style.dictionary));
if (!styles.length) return;
const applied = globalThis.HDGlossary.applyDictionaryStyles(document, root, request.generation, styles);
// Escape a serialized closing style tag's slash without corrupting CSS
// strings or pre-existing selector escapes.
for (const style of applied) style.textContent = style.textContent.replace(/<\/style/giu, value => String.raw`<\/${value.slice(2)}`);
}
function appendDetails(root) {
const details = [];
if (request.term.rules) details.push(`Rules: ${escape(request.term.rules)}`);
if (request.trace.length) details.push(`Deinflection: ${request.trace.map(step => escape(step.name)).join(" > ")}`);
if (!details.length) return;
const small = inert.createElement("small");
small.className = "yomitan-glossary-details";
small.innerHTML = details.join("
");
root.append(small);
}
return async ({ dictionary, firstOnly = false, brief = false, noDictionary = false, plain = false }) => {
let selected = [...groups].filter(([name]) => dictionary === undefined || name === dictionary);
if (firstOnly) selected = selected.slice(0, 1);
if (!selected.length) return "";
if (plain) return plainDefinition(selected, noDictionary);
const pending = [];
const root = inert.createElement("div");
root.className = "yomitan-glossary";
root.style.cssText = "text-align: left; contain: layout paint style; isolation: isolate;";
const list = inert.createElement("ol");
for (const [name, glossaries] of selected) {
const page = inert.createElement("li");
page.dataset.dictionary = name;
if (glossaries.length === 1) page.append(entry(glossaries[0], brief, noDictionary, pending));
else {
const senses = inert.createElement("ul");
for (const glossary of glossaries) { const sense = inert.createElement("li"); sense.append(entry(glossary, brief, noDictionary, pending)); senses.append(sense); }
page.append(senses);
}
list.append(page);
}
root.append(list);
appendStyles(root, selected);
if (!brief) appendDetails(root);
await Promise.all(pending);
return root.outerHTML;
};
}