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.
|
}
|
}
|