Files
glagolify/content.js
T

346 lines
8.7 KiB
JavaScript

(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);
});
})();