import React from 'react';
import PropTypes from 'prop-types';
import matchLinks from '../utils/matchLinks';
import memoize from 'lodash/memoize';
import cn from 'classnames';
import styles from './AdminCommentContent.css';
function escapeHTML(unsafe) {
return unsafe
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}
// generate a regulare expression that catches the `phrases`.
function generateRegExp(phrases) {
const inner = phrases
.map(phrase =>
phrase
.split(/\s+/)
.map(word => escapeRegExp(word))
.join('[\\s"?!.]+')
)
.join('|');
const pattern = `(^|[^\\w])(${inner})(?=[^\\w]|$)`;
try {
return new RegExp(pattern, 'iu');
} catch (_err) {
// IE does not support unicode support, so we'll create one without.
return new RegExp(pattern, 'i');
}
}
// Generate a regular expression detecting `suspectWords` and `bannedWords` phrases.
function getPhrasesRegexp(suspectWords, bannedWords) {
return generateRegExp([...suspectWords, ...bannedWords]);
}
// Memoized version as arguments rarely change.
const getPhrasesRegexpMemoized = memoize(getPhrasesRegexp);
function nl2br(body, keyPrefix) {
const tokens = body.split('\n').reduce((tokens, t, i) => {
if (i !== 0) {
tokens.push(
);
}
tokens.push(t);
return tokens;
}, []);
return tokens;
}
// markPhrases looks for `supsectWords` and `bannedWords` inside `body` and highlights them by returning
// an array of React Elements.
function markPhrases(body, suspectWords, bannedWords, keyPrefix) {
const regexp = getPhrasesRegexpMemoized(suspectWords, bannedWords);
const tokens = body.split(regexp);
return tokens.map(
(token, i) =>
i % 3 === 2 ? {token} : token
);
}
// markLinks looks for links inside `body` and highlights them by returning
// an array of React Elements.
function markLinks(body, keyPrefix) {
const matches = matchLinks(body);
const content = [];
let index = 0;
if (matches) {
matches.forEach((match, i) => {
content.push(body.substring(index, match.index));
content.push(
{match.text}
);
index = match.lastIndex;
});
}
content.push(body.substring(index));
return content;
}
// markPhrasesHTML looks for `supsectWords` and `bannedWords` inside `text` and highlights them by returning
// a HTML string.
function markPhrasesHTML(text, suspectWords, bannedWords) {
const regexp = getPhrasesRegexpMemoized(suspectWords, bannedWords);
const tokens = text.split(regexp);
if (tokens.length === 1) {
return text;
}
return tokens
.map(
(token, i) =>
i % 3 === 2 ? `${escapeHTML(token)}` : escapeHTML(token)
)
.join('');
}
// markHTMLNode manipulates the node by looking for #text nodes and adding markers
// for `supsectWords` and `bannedWords`.
function markHTMLNode(parentNode, suspectWords, bannedWords) {
parentNode.childNodes.forEach(node => {
if (node.nodeName === '#text') {
const newContent = markPhrasesHTML(
node.textContent,
suspectWords,
bannedWords
);
if (newContent !== node.textContent) {
const newNode = document.createElement('span');
newNode.innerHTML = newContent;
parentNode.replaceChild(newNode, node);
}
} else {
markHTMLNode(node, suspectWords, bannedWords);
}
});
}
// renderText performs all the marking of a text body and returns an array of React Elements.
function renderText(body, suspectWords, bannedWords) {
return nl2br(body).map((element, index) => {
// Skip br tags.
if (typeof element !== 'string') {
return element;
}
return markLinks(element, index).map((element, index) => {
// Keep highlighted links.
if (typeof element !== 'string') {
return element;
}
// Highlight suspect and banned phrase inside this part of text.
return markPhrases(element, suspectWords, bannedWords, index);
});
});
}
const commonPropTypes = {
className: PropTypes.string,
bannedWords: PropTypes.array.isRequired,
suspectWords: PropTypes.array.isRequired,
body: PropTypes.string.isRequired,
};
const AdminCommentContentText = ({
body,
className,
suspectWords,
bannedWords,
}) => {
return (