mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2025-12-09 16:05:07 +00:00
## About The Pull Request If we always define the fragment, the if after the loop will always pass. ## Why It's Good For The Game fixes #92468 <img width="653" height="223" alt="grafik" src="https://github.com/user-attachments/assets/7995b73d-f648-4cbd-b05e-c36f98cab47b" /> ## Changelog 🆑 fix: chat highlights /🆑
217 lines
5.5 KiB
TypeScript
217 lines
5.5 KiB
TypeScript
/**
|
|
* @file
|
|
* @copyright 2020 Aleksej Komarov
|
|
* @license MIT
|
|
*/
|
|
|
|
type NodeCreator = (text: string) => Node;
|
|
|
|
type ReplaceInTextNodeParams = {
|
|
node: Node;
|
|
regex: RegExp;
|
|
createNode: NodeCreator;
|
|
captureAdjust?: (str: string) => string;
|
|
};
|
|
|
|
/**
|
|
* Replaces text matching a regular expression with a custom node.
|
|
*/
|
|
function regexParseNode(params: ReplaceInTextNodeParams): {
|
|
nodes: Node[];
|
|
n: number;
|
|
} {
|
|
const { node, regex, createNode, captureAdjust } = params;
|
|
const text = node.textContent;
|
|
|
|
if (!text || !regex) {
|
|
return { nodes: [], n: 0 };
|
|
}
|
|
|
|
const nodes: Node[] = [];
|
|
const textLength = text.length;
|
|
let fragment: Node | undefined;
|
|
let count = 0;
|
|
let lastIndex = 0;
|
|
let match: RegExpExecArray | null;
|
|
let n = 0;
|
|
let new_node: Node;
|
|
|
|
while (true) {
|
|
match = regex.exec(text);
|
|
if (!match) break;
|
|
n += 1;
|
|
// Safety check to prevent permanent client crashing
|
|
if (++count > 9999) {
|
|
return { nodes: [], n: 0 };
|
|
}
|
|
// Lazy init fragment
|
|
if (!fragment) {
|
|
fragment = document.createDocumentFragment();
|
|
}
|
|
|
|
const matchText = captureAdjust ? captureAdjust(match[0]) : match[0];
|
|
const matchLength = matchText.length;
|
|
// If matchText is set to be a substring nested within the original
|
|
// matched text make sure to properly offset the index
|
|
const matchIndex = match.index + match[0].indexOf(matchText);
|
|
// Insert previous unmatched chunk
|
|
if (lastIndex < matchIndex) {
|
|
new_node = document.createTextNode(text.substring(lastIndex, matchIndex));
|
|
nodes.push(new_node);
|
|
fragment.appendChild(new_node);
|
|
}
|
|
lastIndex = matchIndex + matchLength;
|
|
// Create a wrapper node
|
|
new_node = createNode(matchText);
|
|
nodes.push(new_node);
|
|
fragment.appendChild(new_node);
|
|
}
|
|
if (fragment) {
|
|
// Insert the remaining unmatched chunk
|
|
if (lastIndex < textLength) {
|
|
new_node = document.createTextNode(text.substring(lastIndex, textLength));
|
|
nodes.push(new_node);
|
|
fragment.appendChild(new_node);
|
|
}
|
|
// Commit the fragment
|
|
node.parentNode?.replaceChild(fragment, node);
|
|
}
|
|
|
|
return {
|
|
nodes: nodes,
|
|
n: n,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Replace text of a node with custom nodes if they match
|
|
* a regex expression or are in a word list
|
|
*/
|
|
export const replaceInTextNode =
|
|
(regex: RegExp, words: string[] | null, createNode: NodeCreator) =>
|
|
(node: Node) => {
|
|
let nodes;
|
|
let result;
|
|
let n = 0;
|
|
|
|
if (regex) {
|
|
result = regexParseNode({
|
|
node: node,
|
|
regex: regex,
|
|
createNode: createNode,
|
|
});
|
|
nodes = result.nodes;
|
|
n += result.n;
|
|
}
|
|
|
|
if (words) {
|
|
let i = 0;
|
|
let wordRegexStr = '(';
|
|
for (const word of words) {
|
|
// Capture if the word is at the beginning, end, middle,
|
|
// or by itself in a message
|
|
wordRegexStr += `^${word}\\s\\W|\\s\\W${word}\\s\\W|\\s\\W${word}$|^${word}\\s\\W$`;
|
|
// Make sure the last character for the expression is NOT '|'
|
|
if (++i !== words.length) {
|
|
wordRegexStr += '|';
|
|
}
|
|
}
|
|
wordRegexStr += ')';
|
|
const wordRegex = new RegExp(wordRegexStr, 'gi');
|
|
if (regex && nodes) {
|
|
for (const a_node of nodes) {
|
|
result = regexParseNode({
|
|
node: a_node,
|
|
regex: wordRegex,
|
|
createNode: createNode,
|
|
captureAdjust: (str) => str.replace(/^\W|\W$/g, ''),
|
|
});
|
|
n += result.n;
|
|
}
|
|
} else {
|
|
result = regexParseNode({
|
|
node: node,
|
|
regex: wordRegex,
|
|
createNode: createNode,
|
|
captureAdjust: (str) => str.replace(/^\W|\W$/g, ''),
|
|
});
|
|
n += result.n;
|
|
}
|
|
}
|
|
return n;
|
|
};
|
|
|
|
// Highlight
|
|
// --------------------------------------------------------
|
|
|
|
/**
|
|
* Default highlight node.
|
|
*/
|
|
function createHighlightNode(text: string): Node {
|
|
const node = document.createElement('span');
|
|
node.setAttribute('style', 'background-color:#fd4;color:#000');
|
|
node.textContent = text;
|
|
return node;
|
|
}
|
|
|
|
/**
|
|
* Highlights the text in the node based on the provided regular expression.
|
|
*/
|
|
export function highlightNode(
|
|
/** Node which you want to process */
|
|
node: Node,
|
|
/** Regular expression to highlight */
|
|
regex: RegExp,
|
|
/** List of words to highlight */
|
|
words: string[],
|
|
createNode: NodeCreator = createHighlightNode,
|
|
): number {
|
|
if (!createNode) {
|
|
createNode = createHighlightNode;
|
|
}
|
|
let n = 0;
|
|
const childNodes = node.childNodes;
|
|
for (let i = 0; i < childNodes.length; i++) {
|
|
const node = childNodes[i];
|
|
// Is a text node
|
|
if (node.nodeType === 3) {
|
|
n += replaceInTextNode(regex, words, createNode)(node);
|
|
} else {
|
|
n += highlightNode(node, regex, words, createNode);
|
|
}
|
|
}
|
|
return n;
|
|
}
|
|
|
|
// Linkify
|
|
// --------------------------------------------------------
|
|
|
|
const URL_REGEX =
|
|
/(?:(?:https?:\/\/)|(?:www\.))(?:[^ ]*?\.[^ ]*?)+[-A-Za-z0-9+&@#/%?=~_|$!:,.;(){}]+/gi;
|
|
|
|
/**
|
|
* Highlights the text in the node based on the provided regular expression.
|
|
*/
|
|
export function linkifyNode(node: Node): number {
|
|
let n = 0;
|
|
const childNodes = node.childNodes;
|
|
for (let i = 0; i < childNodes.length; i++) {
|
|
const node = childNodes[i];
|
|
const tag = String(node.nodeName).toLowerCase();
|
|
// Is a text node
|
|
if (node.nodeType === 3) {
|
|
n += linkifyTextNode(node);
|
|
} else if (tag !== 'a') {
|
|
n += linkifyNode(node);
|
|
}
|
|
}
|
|
return n;
|
|
}
|
|
|
|
const linkifyTextNode = replaceInTextNode(URL_REGEX, null, (text) => {
|
|
const node = document.createElement('a');
|
|
node.href = text;
|
|
node.textContent = text;
|
|
return node;
|
|
});
|