const SKIPPED_TAGS = { SCRIPT: true, STYLE: true } let replacements = [] let observer = null function isConfigured(realName, codename) { return realName && realName !== codename && !(realName.startsWith('[') && realName.endsWith(']')) } function replaceAsciiCodename(value, codename, realName) { const pattern = new RegExp('(^|[^A-Za-z0-9])' + codename + '(?=$|[^A-Za-z0-9])', 'g') return value.replace(pattern, function (match, prefix) { return prefix + realName }) } export function replaceSensitiveText(value) { if (typeof value !== 'string' || !value || !replacements.length) { return value } return replacements.reduce(function (result, item) { if (/^[A-Za-z0-9]+$/.test(item.codename)) { return replaceAsciiCodename(result, item.codename, item.real_name) } return result.split(item.codename).join(item.real_name) }, value) } function replaceTextNode(node) { const nextValue = replaceSensitiveText(node.nodeValue) if (nextValue !== node.nodeValue) { node.nodeValue = nextValue } } function replaceAttribute(element, attribute) { if (!element.hasAttribute(attribute)) { return } const value = element.getAttribute(attribute) const nextValue = replaceSensitiveText(value) if (nextValue !== value) { element.setAttribute(attribute, nextValue) } } function replaceAttributes(root) { const attributes = ['placeholder', 'title', 'aria-label'] // MutationObserver may report comment nodes or other non element nodes. if (root.nodeType !== 1 && root.nodeType !== 9 && root.nodeType !== 11) { return } if (root.nodeType === 1) { attributes.forEach(function (attribute) { replaceAttribute(root, attribute) }) } root.querySelectorAll('[placeholder], [title], [aria-label]').forEach(function (element) { attributes.forEach(function (attribute) { replaceAttribute(element, attribute) }) }) } function replaceDom(root) { if (!root || !replacements.length) { return } if (root.nodeType === 3) { replaceTextNode(root) return } replaceAttributes(root) const walker = document.createTreeWalker( root, NodeFilter.SHOW_TEXT, { acceptNode: function (node) { const parent = node.parentElement return parent && !SKIPPED_TAGS[parent.tagName] ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT } } ) let node = walker.nextNode() while (node) { replaceTextNode(node) node = walker.nextNode() } } function watchDom() { if (observer || !document.body) { return } observer = new MutationObserver(function (mutations) { mutations.forEach(function (mutation) { if (mutation.type === 'characterData') { replaceTextNode(mutation.target) return } mutation.addedNodes.forEach(function (node) { replaceDom(node) }) }) }) observer.observe(document.body, { childList: true, subtree: true, characterData: true }) } export async function installSensitiveWords() { try { const baseUrl = process.env.BASE_URL || '/' const response = await fetch(baseUrl + 'sensitive-words.json?ts=' + Date.now(), { cache: 'no-store' }) if (!response.ok) { return } const payload = await response.json() replacements = (payload.sensitive_words || []) .filter(function (item) { return item && item.codename && isConfigured(item.real_name, item.codename) }) .sort(function (left, right) { return right.codename.length - left.codename.length }) replaceDom(document.body) watchDom() } catch (error) { // The application still works with development codenames if the dictionary is unavailable. } }