Decorate comment text to add @username links and scratch-domain links

This commit is contained in:
Paul Kaplan 2018-10-24 11:25:11 -04:00
parent ecb497b30f
commit 9066686c2b
3 changed files with 98 additions and 30 deletions

View file

@ -5,26 +5,71 @@ const reactStringReplace = require('react-string-replace');
* Helper method that replaces @mentions and #hashtags in plain text * Helper method that replaces @mentions and #hashtags in plain text
* *
* @param {string} text string to convert * @param {string} text string to convert
* @return {string} string with links for @mentions and #hashtags * @param {?object} opts options object of boolean flags, defaults to all true
* @property {boolean} opts.hashtag If #hashtags should be converted to search links
* @property {boolean} opts.usernames If @usernames should be converted to /users/username links
* @property {boolean} opts.scratchLinks If scratch-domain links should be converted to <a> links
* @return {Array} Array with strings and react components for links
*/ */
module.exports = text => { module.exports = (text, opts) => {
let replacedText; opts = opts || {
usernames: true,
hashtags: true,
scratchLinks: true
};
let replacedText = [text];
// Match @-mentions (username is alphanumeric, underscore and dash) // Match @-mentions (username is alphanumeric, underscore and dash)
replacedText = reactStringReplace(text, /@([\w-]+)/g, (match, i) => ( if (opts.usernames) {
replacedText = reactStringReplace(replacedText, /@([\w-]+)/g, (match, i) => (
<a <a
href={`/users/${match}`} href={`/users/${match}`}
key={match + i} key={match + i}
>@{match}</a> >@{match}</a>
)); ));
}
// Match hashtags // Match hashtags
if (opts.hashtags) {
replacedText = reactStringReplace(replacedText, /(#[\w-]+)/g, (match, i) => ( replacedText = reactStringReplace(replacedText, /(#[\w-]+)/g, (match, i) => (
<a <a
href={`/search/projects?q=${match}`} href={`/search/projects?q=${match}`}
key={match + i} key={match + i}
>{match}</a> >{match}</a>
)); ));
}
// Match scratch links
/*
Ported from the python...
"Oh boy a giant regex!" Said nobody ever.
(^|\s)(https?://(?:[\w-]+\.)*scratch\.mit\.edu(?:/(?:\S*[\w:/#[\]@\$&\'()*+=])?)?(?![^?!,:;\w\s]\S))
(^|\s)
Only begin capturing after a space, or at the beginning of a word
https?
URLs beginning with http or https
://(?:[\w-]+\.)*scratch\.mit\.edu
allow *.scratch.mit.edu urls
(?:/...)?
optionally followed by a slash
(?:\S*[\w:/#[\]@\$&\'()*+=])?
optionally that slash is followed by anything that's not a space, until
that string is followed by URL-valid characters that aren't punctuation
(?![^?!,:;\w\s]\S))
Don't capture if this string is embedded in another string (e.g., the
beginning of a non-scratch URL), but allow punctuation
*/
if (opts.scratchLinks) {
// eslint-disable-next-line max-len
const linkRegexp = /((?:^|\s)https?:\/\/(?:[\w-]+\.)*(?:scratch\.mit\.edu|scratch-wiki\.info)(?:\/(?:\S*[\w:/#[\]@$&'()*+=])?)?(?![^?!,:;\w\s]\S))/g;
replacedText = reactStringReplace(replacedText, linkRegexp, (match, i) => (
<a
href={match}
key={match + i}
>{match}</a>
));
}
return replacedText; return replacedText;
}; };

View file

@ -11,6 +11,7 @@ const FormattedMessage = require('react-intl').FormattedMessage;
const ComposeComment = require('./compose-comment.jsx'); const ComposeComment = require('./compose-comment.jsx');
const DeleteCommentModal = require('../../../components/modal/comments/delete-comment.jsx'); const DeleteCommentModal = require('../../../components/modal/comments/delete-comment.jsx');
const ReportCommentModal = require('../../../components/modal/comments/report-comment.jsx'); const ReportCommentModal = require('../../../components/modal/comments/report-comment.jsx');
const decorateText = require('../../../lib/decorate-text.jsx');
require('./comment.scss'); require('./comment.scss');
@ -101,6 +102,16 @@ class Comment extends React.Component {
const visible = visibility === 'visible'; const visible = visibility === 'visible';
let commentText = content;
if (replyUsername) {
commentText = `@${replyUsername} ${commentText}`;
}
commentText = decorateText(commentText, {
scratchLinks: true,
usernames: true,
hashtags: false
});
return ( return (
<div <div
className="flex-row comment" className="flex-row comment"
@ -167,13 +178,17 @@ class Comment extends React.Component {
*/} */}
<span className="comment-content"> <span className="comment-content">
{replyUsername && ( {commentText.map(fragment => {
<a href={`/users/${replyUsername}`}>@{replyUsername}&nbsp;</a> if (typeof fragment === 'string') {
)} return (
<EmojiText <EmojiText
as="span" as="span"
text={content} text={fragment}
/> />
);
}
return fragment;
})}
</span> </span>
<FlexRow className="comment-bottom-row"> <FlexRow className="comment-bottom-row">
<span className="comment-time"> <span className="comment-time">

View file

@ -238,7 +238,11 @@ const PreviewPresentation = ({
value={projectInfo.instructions} value={projectInfo.instructions}
/> : /> :
<div className="project-description"> <div className="project-description">
{decorateText(projectInfo.instructions)} {decorateText(projectInfo.instructions, {
usernames: true,
hashtags: true,
scratchLinks: false
})}
</div> </div>
} }
</FlexRow> </FlexRow>
@ -270,7 +274,11 @@ const PreviewPresentation = ({
value={projectInfo.description} value={projectInfo.description}
/> : /> :
<div className="project-description last"> <div className="project-description last">
{decorateText(projectInfo.description)} {decorateText(projectInfo.description, {
usernames: true,
hashtags: true,
scratchLinks: false
})}
</div> </div>
} }
</FlexRow> </FlexRow>