initial
This commit is contained in:
+327
@@ -0,0 +1,327 @@
|
||||
(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);
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user