(function () { "use strict"; const EXCLUDED_ANCESTORS = "head, script, style, noscript, template, textarea, select, option"; const FONT_FAMILIES = Object.freeze({ round: '"Glagolify Noto Sans Glagolitic"', angular: '"Glagolify Shafarik Angular"' }); let fontFamily = FONT_FAMILIES.round; const trackedText = new Map(); const styledElements = new Map(); const observers = new Map(); let ownTextMutationCounts = new WeakMap(); let ownStyleMutationCounts = new WeakMap(); let enabled = false; function isEditable(element) { return ( element.ownerDocument.designMode === "on" || element.isContentEditable ); } function isEligible(node) { const parent = node.parentElement; return Boolean( parent && !parent.closest(EXCLUDED_ANCESTORS) && !isEditable(parent) ); } function observedRootFor(node) { const root = node.getRootNode(); return observers.has(root) ? root : null; } function markOwnMutation(counts, node) { if (!observedRootFor(node)) { return; } counts.set(node, (counts.get(node) || 0) + 1); } function consumeOwnMutation(counts, node) { const count = counts.get(node) || 0; if (count === 0) { return false; } if (count === 1) { counts.delete(node); } else { counts.set(node, count - 1); } return true; } function appliedFontFamily(computedFamily) { return computedFamily ? `${fontFamily}, ${computedFamily}` : `${fontFamily}, sans-serif`; } function setFontStyle(style) { const nextFontFamily = FONT_FAMILIES[style]; if (!nextFontFamily || nextFontFamily === fontFamily) { return; } fontFamily = nextFontFamily; for (const [element, record] of styledElements) { writeAppliedFont(element, record, record.pageComputedFamily); } } function writeAppliedFont(element, record, computedFamily) { const appliedValue = appliedFontFamily(computedFamily); record.appliedValue = appliedValue; if ( element.style.getPropertyValue("font-family") === appliedValue && element.style.getPropertyPriority("font-family") === "important" ) { return; } markOwnMutation(ownStyleMutationCounts, element); element.style.setProperty("font-family", appliedValue, "important"); } function applyFont(element) { if (styledElements.has(element)) { return; } const computedFamily = getComputedStyle(element).fontFamily; const record = { pageValue: element.style.getPropertyValue("font-family"), pagePriority: element.style.getPropertyPriority("font-family"), pageComputedFamily: computedFamily, appliedValue: "" }; styledElements.set(element, record); writeAppliedFont(element, record, computedFamily); } function refreshPageFont(element) { const record = styledElements.get(element); if (!record) { return; } const currentValue = element.style.getPropertyValue("font-family"); const currentPriority = element.style.getPropertyPriority("font-family"); if (currentValue === record.appliedValue && currentPriority === "important") { return; } record.pageValue = currentValue; record.pagePriority = currentPriority; record.pageComputedFamily = getComputedStyle(element).fontFamily; writeAppliedFont(element, record, record.pageComputedFamily); } function restoreFont(element, record) { const currentValue = element.style.getPropertyValue("font-family"); const currentPriority = element.style.getPropertyPriority("font-family"); if (currentValue !== record.appliedValue || currentPriority !== "important") { return; } markOwnMutation(ownStyleMutationCounts, element); if (record.pageValue) { element.style.setProperty("font-family", record.pageValue, record.pagePriority); } else { element.style.removeProperty("font-family"); } } function restoreText(node) { const record = trackedText.get(node); if (!record) { return; } if (node.data === record.transformed) { markOwnMutation(ownTextMutationCounts, node); node.data = record.original; } trackedText.delete(node); } function transformText(node) { if (!isEligible(node)) { restoreText(node); return; } const previous = trackedText.get(node); if (previous && node.data === previous.transformed) { return; } const original = node.data; const transformed = Glagolify.transliterate(original); if (transformed === original) { trackedText.delete(node); return; } trackedText.set(node, { original, transformed }); applyFont(node.parentElement); markOwnMutation(ownTextMutationCounts, node); node.data = transformed; } function visitText(root, callback) { if (root.nodeType === Node.TEXT_NODE) { callback(root); return; } if ( root.nodeType !== Node.ELEMENT_NODE && root.nodeType !== Node.DOCUMENT_NODE && root.nodeType !== Node.DOCUMENT_FRAGMENT_NODE ) { return; } const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT); let node; while ((node = walker.nextNode())) { callback(node); } } function reconcile(root) { visitText(root, transformText); } function discoverOpenShadowRoots(root) { if ( root.nodeType === Node.ELEMENT_NODE && root.shadowRoot && root.shadowRoot.mode === "open" ) { observeRoot(root.shadowRoot); } if (typeof root.querySelectorAll !== "function") { return; } for (const element of root.querySelectorAll("*")) { if (element.shadowRoot && element.shadowRoot.mode === "open") { observeRoot(element.shadowRoot); } } } function cleanupDisconnected() { for (const node of trackedText.keys()) { if (!node.isConnected) { restoreText(node); } } for (const [element, record] of styledElements) { if (!element.isConnected) { restoreFont(element, record); styledElements.delete(element); } } for (const [root, observer] of observers) { if (root !== document && !root.host.isConnected) { observer.disconnect(); observers.delete(root); } } } function handleMutations(records) { for (const record of records) { if (record.type === "characterData") { if (!consumeOwnMutation(ownTextMutationCounts, record.target)) { reconcile(record.target); } continue; } if (record.type === "attributes") { if (record.attributeName === "style") { if (!consumeOwnMutation(ownStyleMutationCounts, record.target)) { refreshPageFont(record.target); } } else if (record.attributeName === "contenteditable") { reconcile(record.target); } continue; } for (const node of record.addedNodes) { reconcile(node); discoverOpenShadowRoots(node); } } cleanupDisconnected(); } function observeRoot(root) { if (observers.has(root)) { return; } reconcile(root); const observer = new MutationObserver(handleMutations); observer.observe(root, { subtree: true, childList: true, characterData: true, attributes: true, attributeFilter: ["contenteditable", "style"] }); observers.set(root, observer); discoverOpenShadowRoots(root); } function enable() { if (enabled) { return; } enabled = true; observeRoot(document); } function disable() { if (!enabled) { return; } enabled = false; for (const observer of observers.values()) { observer.disconnect(); } observers.clear(); for (const node of trackedText.keys()) { restoreText(node); } for (const [element, record] of styledElements) { restoreFont(element, record); } styledElements.clear(); ownTextMutationCounts = new WeakMap(); ownStyleMutationCounts = new WeakMap(); } function setEnabled(target, style) { setFontStyle(style); if (target) { enable(); } else { disable(); } return { enabled }; } browser.runtime.onMessage.addListener((message) => { if (!message || message.type !== "glagolify:set-enabled" || typeof message.enabled !== "boolean") { return undefined; } return Promise.resolve(setEnabled(message.enabled, message.fontStyle)); }); browser.runtime.sendMessage({ type: "glagolify:document-ready" }) .then((response) => { setEnabled(Boolean(response && response.enabled), response && response.fontStyle); }) .catch(() => { setEnabled(false); }); })();