mirror of
https://github.com/wassname/talk.git
synced 2026-09-10 12:43:11 +08:00
Implement AdminCommentContent for text and html
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
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, '"')
|
||||
.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(<br key={`${keyPrefix}_${i}`} />);
|
||||
}
|
||||
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 ? <mark key={`${keyPrefix}_${i}`}>{token}</mark> : 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(
|
||||
<a key={`${keyPrefix}_${i}`} href={match.url} target="_blank">
|
||||
{match.text}
|
||||
</a>
|
||||
);
|
||||
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 ? `<mark>${escapeHTML(token)}</mark>` : 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 (
|
||||
<div className={cn(className, styles.content)}>
|
||||
{renderText(body, suspectWords, bannedWords)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
AdminCommentContentText.propTypes = commonPropTypes;
|
||||
|
||||
const AdminCommentContentHTML = ({
|
||||
body,
|
||||
className,
|
||||
suspectWords,
|
||||
bannedWords,
|
||||
}) => {
|
||||
const node = document.createElement('div');
|
||||
node.innerHTML = body;
|
||||
markHTMLNode(node, suspectWords, bannedWords);
|
||||
return (
|
||||
<div
|
||||
className={cn(className, styles.content)}
|
||||
dangerouslySetInnerHTML={{ __html: node.innerHTML }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
AdminCommentContentHTML.propTypes = commonPropTypes;
|
||||
|
||||
const AdminCommentContent = ({
|
||||
className,
|
||||
body,
|
||||
suspectWords,
|
||||
bannedWords,
|
||||
html,
|
||||
}) => {
|
||||
const Component = html ? AdminCommentContentHTML : AdminCommentContentText;
|
||||
return (
|
||||
<Component
|
||||
className={className}
|
||||
body={body}
|
||||
suspectWords={suspectWords}
|
||||
bannedWords={bannedWords}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
AdminCommentContent.propTypes = {
|
||||
...commonPropTypes,
|
||||
html: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default AdminCommentContent;
|
||||
Reference in New Issue
Block a user