From 9066686c2bb613a4ad071d73af0d7cd20a2e1495 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Wed, 24 Oct 2018 11:25:11 -0400 Subject: [PATCH] Decorate comment text to add @username links and scratch-domain links --- src/lib/decorate-text.jsx | 87 ++++++++++++++++++++------- src/views/preview/comment/comment.jsx | 29 ++++++--- src/views/preview/presentation.jsx | 12 +++- 3 files changed, 98 insertions(+), 30 deletions(-) diff --git a/src/lib/decorate-text.jsx b/src/lib/decorate-text.jsx index 4bd2583c9..8d92bad75 100644 --- a/src/lib/decorate-text.jsx +++ b/src/lib/decorate-text.jsx @@ -3,28 +3,73 @@ const reactStringReplace = require('react-string-replace'); /** * Helper method that replaces @mentions and #hashtags in plain text - * - * @param {string} text string to convert - * @return {string} string with links for @mentions and #hashtags + * + * @param {string} text string to convert + * @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 links + * @return {Array} Array with strings and react components for links */ -module.exports = text => { - let replacedText; - +module.exports = (text, opts) => { + opts = opts || { + usernames: true, + hashtags: true, + scratchLinks: true + }; + + let replacedText = [text]; + // Match @-mentions (username is alphanumeric, underscore and dash) - replacedText = reactStringReplace(text, /@([\w-]+)/g, (match, i) => ( - @{match} - )); - - // Match hashtags - replacedText = reactStringReplace(replacedText, /(#[\w-]+)/g, (match, i) => ( - {match} - )); - + if (opts.usernames) { + replacedText = reactStringReplace(replacedText, /@([\w-]+)/g, (match, i) => ( + @{match} + )); + } + + // Match hashtags + if (opts.hashtags) { + replacedText = reactStringReplace(replacedText, /(#[\w-]+)/g, (match, i) => ( + {match} + )); + } + + // 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) => ( + {match} + )); + } + return replacedText; }; diff --git a/src/views/preview/comment/comment.jsx b/src/views/preview/comment/comment.jsx index c6785e99d..1e7a70d1b 100644 --- a/src/views/preview/comment/comment.jsx +++ b/src/views/preview/comment/comment.jsx @@ -11,6 +11,7 @@ const FormattedMessage = require('react-intl').FormattedMessage; const ComposeComment = require('./compose-comment.jsx'); const DeleteCommentModal = require('../../../components/modal/comments/delete-comment.jsx'); const ReportCommentModal = require('../../../components/modal/comments/report-comment.jsx'); +const decorateText = require('../../../lib/decorate-text.jsx'); require('./comment.scss'); @@ -101,6 +102,16 @@ class Comment extends React.Component { const visible = visibility === 'visible'; + let commentText = content; + if (replyUsername) { + commentText = `@${replyUsername} ${commentText}`; + } + commentText = decorateText(commentText, { + scratchLinks: true, + usernames: true, + hashtags: false + }); + return (
- {replyUsername && ( - @{replyUsername}  - )} - + {commentText.map(fragment => { + if (typeof fragment === 'string') { + return ( + + ); + } + return fragment; + })} diff --git a/src/views/preview/presentation.jsx b/src/views/preview/presentation.jsx index de421d13e..4d67968ee 100644 --- a/src/views/preview/presentation.jsx +++ b/src/views/preview/presentation.jsx @@ -238,7 +238,11 @@ const PreviewPresentation = ({ value={projectInfo.instructions} /> :
- {decorateText(projectInfo.instructions)} + {decorateText(projectInfo.instructions, { + usernames: true, + hashtags: true, + scratchLinks: false + })}
}
@@ -270,7 +274,11 @@ const PreviewPresentation = ({ value={projectInfo.description} /> :
- {decorateText(projectInfo.description)} + {decorateText(projectInfo.description, { + usernames: true, + hashtags: true, + scratchLinks: false + })}
}