(async function init() {
const domain = window.location.hostname;
const { disabledSites = [] } = await browser.storage.local.get('disabledSites');
if (disabledSites.includes(domain)) return;
let conversionCount = 0;
const pronounMap = { 'm': 'aym', 'll': 'ayll', 've': 'ayve', 'd': 'ayd' };
const acronymMap = { 'TIL': 'TAL', 'AITAH': 'AATAH' };
const pronounRegex = /\bI(?:['’](m|ll|ve|d))?\b/g;
const acronymRegex = /\b(TIL|AITAH)\b/g;
const skipTags = ['SCRIPT', 'STYLE', 'INPUT', 'TEXTAREA', 'IFRAME', 'CODE', 'PRE'];
function updateBadge() {
browser.runtime.sendMessage({ action: 'updateBadge', count: conversionCount });
}
function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
function isSentenceStart(text, offset) {
if (offset === 0) return true;
const before = text.slice(0, offset).trim();
if (before.length === 0) return true;
const lastChar = before.slice(-1);
return /[.!?\"\'“”‘’:]/.test(lastChar);
}
function startsUppercase(word) {
return !!word && word[0] === word[0].toUpperCase() && word[0] !== word[0].toLowerCase();
}
function isAllCaps(word) {
return !!word && /[A-Za-z]/.test(word) && word === word.toUpperCase();
}
function prevWord(text, offset) {
let i = offset - 1;
while (i >= 0 && !/[A-Za-z]/.test(text[i])) i--;
if (i < 0) return null;
const end = i + 1;
while (i >= 0 && /[A-Za-z'’]/.test(text[i])) i--;
return text.slice(i + 1, end);
}
function nextWord(text, offset) {
let i = offset;
while (i < text.length && !/[A-Za-z]/.test(text[i])) i++;
if (i >= text.length) return null;
const start = i;
while (i < text.length && /[A-Za-z'’]/.test(text[i])) i++;
return text.slice(start, i);
}
function isTitleContext(text, offset, matchLen) {
const prev = prevWord(text, offset);
if (!startsUppercase(prev)) return false;
const next = nextWord(text, offset + matchLen);
if (next === null) return true;
return startsUppercase(next);
}
function isAllCapsContext(text, offset, matchLen) {
const prev = prevWord(text, offset);
const next = nextWord(text, offset + matchLen);
if (prev === null && next === null) return false;
if (prev !== null && !isAllCaps(prev)) return false;
if (next !== null && !isAllCaps(next)) return false;
return true;
}
function transformText(text) {
let changed = false;
let result = text;
result = result.replace(acronymRegex, (match) => {
conversionCount++;
changed = true;
return acronymMap[match];
});
result = result.replace(pronounRegex, (match, suffix, offset, fullText) => {
if (offset > 0 && fullText[offset - 1] === '.') {
return match;
}
const nextCharIndex = offset + match.length;
if (nextCharIndex < fullText.length && (fullText[nextCharIndex] === '.' || fullText[nextCharIndex] === '-' || fullText[nextCharIndex] === '/')) {
return match;
}
conversionCount++;
changed = true;
const rep = suffix ? pronounMap[suffix] : 'ayh';
if (isAllCapsContext(fullText, offset, match.length)) return rep.toUpperCase();
const shouldCapitalize = isSentenceStart(fullText, offset) || isTitleContext(fullText, offset, match.length);
return shouldCapitalize ? capitalize(rep) : rep;
});
if (changed) updateBadge();
return result;
}
function walk(node) {
const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, {
acceptNode: (n) => skipTags.includes(n.parentElement?.tagName) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT
});
let textNode;
while (textNode = walker.nextNode()) {
const newVal = transformText(textNode.nodeValue);
if (newVal !== textNode.nodeValue) textNode.nodeValue = newVal;
}
}
walk(document.body);
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
const targetEl = mutation.target;
if (targetEl && targetEl.nodeType === 1) {
if (skipTags.includes(targetEl.tagName) || targetEl.isContentEditable) return;
if (targetEl.closest?.('[contenteditable="true"]')) return;
}
mutation.addedNodes.forEach(node => {
if (node.nodeType === 1) {
walk(node);
}
else if (node.nodeType === 3) {
const parent = node.parentElement || mutation.target;
if (parent && parent.nodeType === 1) {
if (skipTags.includes(parent.tagName)) return;
if (parent.isContentEditable || parent.closest?.('[contenteditable="true"]')) return;
}
const newVal = transformText(node.nodeValue);
if (newVal !== node.nodeValue) node.nodeValue = newVal;
}
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
})();