328 lines
8.0 KiB
JavaScript
328 lines
8.0 KiB
JavaScript
(function () {
|
|
"use strict";
|
|
|
|
const EXCLUDED_ANCESTORS = "head, script, style, noscript, template, textarea, select, option";
|
|
const FONT_FAMILY = '"Glagolify Noto Sans Glagolitic"';
|
|
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
|
|
? `${FONT_FAMILY}, ${computedFamily}`
|
|
: `${FONT_FAMILY}, sans-serif`;
|
|
}
|
|
|
|
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 record = {
|
|
pageValue: element.style.getPropertyValue("font-family"),
|
|
pagePriority: element.style.getPropertyPriority("font-family"),
|
|
appliedValue: ""
|
|
};
|
|
const computedFamily = getComputedStyle(element).fontFamily;
|
|
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;
|
|
writeAppliedFont(element, record, getComputedStyle(element).fontFamily);
|
|
}
|
|
|
|
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) {
|
|
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));
|
|
});
|
|
|
|
browser.runtime.sendMessage({ type: "glagolify:document-ready" })
|
|
.then((response) => {
|
|
setEnabled(Boolean(response && response.enabled));
|
|
})
|
|
.catch(() => {
|
|
setEnabled(false);
|
|
});
|
|
})();
|