From 2d2939576cce38b3f4517d243d51dd6998eda42f Mon Sep 17 00:00:00 2001 From: hmhealey Date: Wed, 9 Sep 2015 14:59:10 -0400 Subject: Added skeleton for new text formatting code which will replace textToJsx --- web/react/utils/text_formatting.jsx | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 web/react/utils/text_formatting.jsx diff --git a/web/react/utils/text_formatting.jsx b/web/react/utils/text_formatting.jsx new file mode 100644 index 000000000..2e1416d1d --- /dev/null +++ b/web/react/utils/text_formatting.jsx @@ -0,0 +1,36 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +const Constants = require('../utils/constants.jsx'); +const UserStore = require('../stores/user_store.jsx'); + +export function formatText(text, options = {}) { + let output = sanitize(text); + + // TODO autolink @mentions + // TODO highlight mentions of self + // TODO autolink urls + // TODO highlight search terms + // TODO autolink hashtags + + // TODO leave space for markdown + + if (options.singleline) { + output = output.replace('\n', ' '); + } else { + output = output.replace('\n', '
'); + } + + return output; +} + +export function sanitize(text) { + let output = text; + + // normal string.replace only does a single occurrance so use a regex instead + output = output.replace(/&/g, '&'); + output = output.replace(//g, '>'); + + return output; +} -- cgit v1.2.3-1-g7c22 From 91f258c725bd749fc44b177e131e61c936e7a88b Mon Sep 17 00:00:00 2001 From: hmhealey Date: Thu, 10 Sep 2015 11:05:31 -0400 Subject: Added handling for @mentions to the new text formatting --- web/react/utils/text_formatting.jsx | 63 +++++++++++++++++++++++++++++++------ 1 file changed, 54 insertions(+), 9 deletions(-) diff --git a/web/react/utils/text_formatting.jsx b/web/react/utils/text_formatting.jsx index 2e1416d1d..111155c3e 100644 --- a/web/react/utils/text_formatting.jsx +++ b/web/react/utils/text_formatting.jsx @@ -1,12 +1,21 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -const Constants = require('../utils/constants.jsx'); +const Constants = require('./constants.jsx'); const UserStore = require('../stores/user_store.jsx'); export function formatText(text, options = {}) { let output = sanitize(text); + let atMentions; + [output, atMentions] = stripAtMentions(output); + + output = reinsertAtMentions(output, atMentions); + + output = replaceNewlines(output, options.singleline); + + return output; + // TODO autolink @mentions // TODO highlight mentions of self // TODO autolink urls @@ -14,14 +23,6 @@ export function formatText(text, options = {}) { // TODO autolink hashtags // TODO leave space for markdown - - if (options.singleline) { - output = output.replace('\n', ' '); - } else { - output = output.replace('\n', '
'); - } - - return output; } export function sanitize(text) { @@ -34,3 +35,47 @@ export function sanitize(text) { return output; } + +function stripAtMentions(text) { + let output = text; + let atMentions = new Map(); + + function stripAtMention(fullMatch, prefix, mentionText, username) { + if (Constants.SPECIAL_MENTIONS.indexOf(username) !== -1 || UserStore.getProfileByUsername(username)) { + const index = atMentions.size; + const alias = `ATMENTION${index}`; + + atMentions.set(alias, {mentionText: mentionText, username: username}); + + return prefix + alias; + } else { + return fullMatch; + } + } + + output = output.replace(/(^|\s)(@([a-z0-9.\-_]+))/g, stripAtMention); + + return [output, atMentions]; +} +window.stripAtMentions = stripAtMentions; + +function reinsertAtMentions(text, atMentions) { + let output = text; + + function reinsertAtMention(replacement, alias) { + output = output.replace(alias, `${replacement.mentionText}`); + } + + atMentions.forEach(reinsertAtMention); + + return output; +} +window.reinsertAtMentions = reinsertAtMentions; + +function replaceNewlines(text, singleline) { + if (!singleline) { + return text.replace(/\n/g, '
'); + } else { + return text.replace(/\n/g, ' '); + } +} -- cgit v1.2.3-1-g7c22 From 56312f8f53e210b299076c9d420fab2fb59502bb Mon Sep 17 00:00:00 2001 From: hmhealey Date: Thu, 10 Sep 2015 11:06:08 -0400 Subject: Changed one instance of textToJsx to use new text formatting for testing purposes --- web/react/components/post_body.jsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx index d9b8f20ce..e08936923 100644 --- a/web/react/components/post_body.jsx +++ b/web/react/components/post_body.jsx @@ -5,6 +5,7 @@ const FileAttachmentList = require('./file_attachment_list.jsx'); const UserStore = require('../stores/user_store.jsx'); const Utils = require('../utils/utils.jsx'); const Constants = require('../utils/constants.jsx'); +const TextFormatting = require('../utils/text_formatting.jsx'); const twemoji = require('twemoji'); export default class PostBody extends React.Component { @@ -12,6 +13,7 @@ export default class PostBody extends React.Component { super(props); this.parseEmojis = this.parseEmojis.bind(this); + this.handleClick = this.handleClick.bind(this); const linkData = Utils.extractLinks(this.props.post.message); this.state = {links: linkData.links, message: linkData.text}; @@ -29,6 +31,12 @@ export default class PostBody extends React.Component { const linkData = Utils.extractLinks(nextProps.post.message); this.setState({links: linkData.links, message: linkData.text}); } + handleClick(e) { + let mentionAttribute = e.target.getAttributeNode('data-mention'); + if (mentionAttribute) { + Utils.searchForTerm(mentionAttribute.value); + } + } render() { const post = this.props.post; const filenames = this.props.post.filenames; @@ -135,7 +143,7 @@ export default class PostBody extends React.Component { key={`${post.id}_message`} className={postClass} > - {loading}{inner} + {loading}

{fileAttachmentHolder} {embed} -- cgit v1.2.3-1-g7c22 From 5d28ffa4447d3eb616536620df6678944918d3dd Mon Sep 17 00:00:00 2001 From: hmhealey Date: Thu, 10 Sep 2015 14:56:11 -0400 Subject: Added highlighting when the current user is mentioned to the new text formatting --- web/react/utils/text_formatting.jsx | 86 +++++++++++++++++++++++++++++-------- 1 file changed, 69 insertions(+), 17 deletions(-) diff --git a/web/react/utils/text_formatting.jsx b/web/react/utils/text_formatting.jsx index 111155c3e..90ff7f41f 100644 --- a/web/react/utils/text_formatting.jsx +++ b/web/react/utils/text_formatting.jsx @@ -6,18 +6,19 @@ const UserStore = require('../stores/user_store.jsx'); export function formatText(text, options = {}) { let output = sanitize(text); + let tokens = new Map(); - let atMentions; - [output, atMentions] = stripAtMentions(output); + // TODO strip urls first - output = reinsertAtMentions(output, atMentions); + output = stripAtMentions(output, tokens); + output = stripSelfMentions(output, tokens); + + output = replaceTokens(output, tokens); output = replaceNewlines(output, options.singleline); return output; - // TODO autolink @mentions - // TODO highlight mentions of self // TODO autolink urls // TODO highlight search terms // TODO autolink hashtags @@ -36,16 +37,18 @@ export function sanitize(text) { return output; } -function stripAtMentions(text) { +function stripAtMentions(text, tokens) { let output = text; - let atMentions = new Map(); - function stripAtMention(fullMatch, prefix, mentionText, username) { + function stripAtMention(fullMatch, prefix, mention, username) { if (Constants.SPECIAL_MENTIONS.indexOf(username) !== -1 || UserStore.getProfileByUsername(username)) { - const index = atMentions.size; + const index = tokens.size; const alias = `ATMENTION${index}`; - atMentions.set(alias, {mentionText: mentionText, username: username}); + tokens.set(alias, { + value: `${mention}`, + originalText: mention + }); return prefix + alias; } else { @@ -53,24 +56,73 @@ function stripAtMentions(text) { } } - output = output.replace(/(^|\s)(@([a-z0-9.\-_]+))/g, stripAtMention); + output = output.replace(/(^|\s)(@([a-z0-9.\-_]+))/gi, stripAtMention); - return [output, atMentions]; + return output; } window.stripAtMentions = stripAtMentions; -function reinsertAtMentions(text, atMentions) { +function stripSelfMentions(text, tokens) { let output = text; - function reinsertAtMention(replacement, alias) { - output = output.replace(alias, `${replacement.mentionText}`); + let mentionKeys = UserStore.getCurrentMentionKeys(); + + // look for any existing tokens which are self mentions and should be highlighted + var newTokens = new Map(); + for (let [alias, token] of tokens) { + if (mentionKeys.indexOf(token.originalText) !== -1) { + const index = newTokens.size; + const newAlias = `SELFMENTION${index}`; + + newTokens.set(newAlias, { + value: `${alias}`, + originalText: token.originalText + }); + + output = output.replace(alias, newAlias); + } + } + + // the new tokens are stashed in a separate map since we can't add objects to a map during iteration + for (let newToken of newTokens) { + tokens.set(newToken[0], newToken[1]); + } + + // look for self mentions in the text + function stripSelfMention(fullMatch, prefix, mention) { + const index = tokens.size; + const alias = `SELFMENTION${index}`; + + tokens.set(alias, { + value: `${mention}`, + originalText: mention + }); + + return prefix + alias; } - atMentions.forEach(reinsertAtMention); + for (let mention of UserStore.getCurrentMentionKeys()) { + output = output.replace(new RegExp(`(^|\\W)(${mention})\\b`, 'gi'), stripSelfMention); + } + + return output; +} + +function replaceTokens(text, tokens) { + let output = text; + + // iterate backwards through the map so that we do replacement in the opposite order that we added tokens + const aliases = [...tokens.keys()]; + for (let i = aliases.length - 1; i >= 0; i--) { + const alias = aliases[i]; + const token = tokens.get(alias); + console.log('replacing ' + alias + ' with ' + token.value); + output = output.replace(alias, token.value); + } return output; } -window.reinsertAtMentions = reinsertAtMentions; +window.replaceTokens = replaceTokens; function replaceNewlines(text, singleline) { if (!singleline) { -- cgit v1.2.3-1-g7c22 From 3ab2814af39df15d66e3fa149b5024692b3c6310 Mon Sep 17 00:00:00 2001 From: hmhealey Date: Thu, 10 Sep 2015 15:19:37 -0400 Subject: Added autolinking of urls to the new text formatting --- web/react/utils/text_formatting.jsx | 39 +++++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/web/react/utils/text_formatting.jsx b/web/react/utils/text_formatting.jsx index 90ff7f41f..db1568bab 100644 --- a/web/react/utils/text_formatting.jsx +++ b/web/react/utils/text_formatting.jsx @@ -1,6 +1,7 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. +const Autolinker = require('autolinker'); const Constants = require('./constants.jsx'); const UserStore = require('../stores/user_store.jsx'); @@ -8,8 +9,7 @@ export function formatText(text, options = {}) { let output = sanitize(text); let tokens = new Map(); - // TODO strip urls first - + output = stripLinks(output, tokens); output = stripAtMentions(output, tokens); output = stripSelfMentions(output, tokens); @@ -19,7 +19,6 @@ export function formatText(text, options = {}) { return output; - // TODO autolink urls // TODO highlight search terms // TODO autolink hashtags @@ -37,6 +36,38 @@ export function sanitize(text) { return output; } +function stripLinks(text, tokens) { + function stripLink(autolinker, match) { + let text = match.getMatchedText(); + let url = text; + if (!url.startsWith('http')) { + url = `http://${text}`; + } + + const index = tokens.size; + const alias = `LINK${index}`; + + tokens.set(alias, { + value: `${text}`, + originalText: text + }); + + return alias; + } + + // we can't just use a static autolinker because we need to set replaceFn + const autolinker = new Autolinker({ + urls: true, + email: false, + phone: false, + twitter: false, + hashtag: false, + replaceFn: stripLink + }); + + return autolinker.link(text); +} + function stripAtMentions(text, tokens) { let output = text; @@ -47,7 +78,7 @@ function stripAtMentions(text, tokens) { tokens.set(alias, { value: `${mention}`, - originalText: mention + oreplaceLinkriginalText: mention }); return prefix + alias; -- cgit v1.2.3-1-g7c22 From e1797c7d55d8bbb23e315d14377640a1b1673802 Mon Sep 17 00:00:00 2001 From: hmhealey Date: Thu, 10 Sep 2015 18:18:23 -0400 Subject: Added autolinking of hashtags to the new text formatting --- web/react/utils/text_formatting.jsx | 54 ++++++++++++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/web/react/utils/text_formatting.jsx b/web/react/utils/text_formatting.jsx index db1568bab..930a6bbfb 100644 --- a/web/react/utils/text_formatting.jsx +++ b/web/react/utils/text_formatting.jsx @@ -12,6 +12,7 @@ export function formatText(text, options = {}) { output = stripLinks(output, tokens); output = stripAtMentions(output, tokens); output = stripSelfMentions(output, tokens); + output = stripHashtags(output, tokens); output = replaceTokens(output, tokens); @@ -20,7 +21,6 @@ export function formatText(text, options = {}) { return output; // TODO highlight search terms - // TODO autolink hashtags // TODO leave space for markdown } @@ -58,7 +58,7 @@ function stripLinks(text, tokens) { // we can't just use a static autolinker because we need to set replaceFn const autolinker = new Autolinker({ urls: true, - email: false, + email: true, phone: false, twitter: false, hashtag: false, @@ -72,13 +72,14 @@ function stripAtMentions(text, tokens) { let output = text; function stripAtMention(fullMatch, prefix, mention, username) { - if (Constants.SPECIAL_MENTIONS.indexOf(username) !== -1 || UserStore.getProfileByUsername(username)) { + const usernameLower = username.toLowerCase(); + if (Constants.SPECIAL_MENTIONS.indexOf(usernameLower) !== -1 || UserStore.getProfileByUsername(usernameLower)) { const index = tokens.size; const alias = `ATMENTION${index}`; tokens.set(alias, { - value: `${mention}`, - oreplaceLinkriginalText: mention + value: `${mention}`, + originalText: mention }); return prefix + alias; @@ -87,7 +88,7 @@ function stripAtMentions(text, tokens) { } } - output = output.replace(/(^|\s)(@([a-z0-9.\-_]+))/gi, stripAtMention); + output = output.replace(/(^|\s)(@([a-z0-9.\-_]*[a-z0-9]))/gi, stripAtMention); return output; } @@ -139,6 +140,47 @@ function stripSelfMentions(text, tokens) { return output; } +function stripHashtags(text, tokens) { + let output = text; + + var newTokens = new Map(); + for (let [alias, token] of tokens) { + if (token.originalText.startsWith('#')) { + const index = newTokens.size; + const newAlias = `HASHTAG${index}`; + + newTokens.set(newAlias, { + value: `${token.originalText}`, + originalText: token.originalText + }); + + output = output.replace(alias, newAlias); + } + } + + // the new tokens are stashed in a separate map since we can't add objects to a map during iteration + for (let newToken of newTokens) { + tokens.set(newToken[0], newToken[1]); + } + + // look for hashtags in the text + function stripHashtag(fullMatch, prefix, hashtag) { + const index = tokens.size; + const alias = `HASHTAG${index}`; + + tokens.set(alias, { + value: `${hashtag}`, + originalText: hashtag + }); + + return prefix + alias; + } + + output = output.replace(/(^|\W)(#[a-zA-Z0-9.\-_]+)\b/g, stripHashtag); + + return output; +} + function replaceTokens(text, tokens) { let output = text; -- cgit v1.2.3-1-g7c22 From 214e48835a55be9ca1800740fd229b030d3c24a8 Mon Sep 17 00:00:00 2001 From: hmhealey Date: Fri, 11 Sep 2015 11:21:34 -0400 Subject: Added highlighting of search terms to the new text formatting --- web/react/utils/text_formatting.jsx | 47 ++++++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/web/react/utils/text_formatting.jsx b/web/react/utils/text_formatting.jsx index 930a6bbfb..48db78abc 100644 --- a/web/react/utils/text_formatting.jsx +++ b/web/react/utils/text_formatting.jsx @@ -13,16 +13,15 @@ export function formatText(text, options = {}) { output = stripAtMentions(output, tokens); output = stripSelfMentions(output, tokens); output = stripHashtags(output, tokens); + output = highlightSearchTerm(output, tokens, options.searchTerm); output = replaceTokens(output, tokens); output = replaceNewlines(output, options.singleline); - return output; - - // TODO highlight search terms + // TODO markdown - // TODO leave space for markdown + return output; } export function sanitize(text) { @@ -176,9 +175,45 @@ function stripHashtags(text, tokens) { return prefix + alias; } - output = output.replace(/(^|\W)(#[a-zA-Z0-9.\-_]+)\b/g, stripHashtag); + return output.replace(/(^|\W)(#[a-zA-Z0-9.\-_]+)\b/g, stripHashtag); +} + +function highlightSearchTerm(text, tokens, searchTerm) { + let output = text; - return output; + var newTokens = new Map(); + for (let [alias, token] of tokens) { + if (token.originalText === searchTerm) { + const index = newTokens.size; + const newAlias = `SEARCH_TERM${index}`; + + newTokens.set(newAlias, { + value: `${alias}`, + originalText: token.originalText + }); + + output = output.replace(alias, newAlias); + } + } + + // the new tokens are stashed in a separate map since we can't add objects to a map during iteration + for (let newToken of newTokens) { + tokens.set(newToken[0], newToken[1]); + } + + function replaceSearchTerm(fullMatch, prefix, word) { + const index = tokens.size; + const alias = `SEARCH_TERM${index}`; + + tokens.set(alias, { + value: `${word}`, + originalText: word + }); + + return prefix + alias; + } + + return output.replace(new RegExp(`(^|\\W)(${searchTerm})\b`, 'gi'), replaceSearchTerm); } function replaceTokens(text, tokens) { -- cgit v1.2.3-1-g7c22 From 3e3b52554a49b8c2d9c242061bfc82be2948b645 Mon Sep 17 00:00:00 2001 From: hmhealey Date: Mon, 14 Sep 2015 11:45:40 -0400 Subject: Cleaned up TextFormatting and added a default click handler for hashtags and @mentions --- web/react/utils/text_formatting.jsx | 121 ++++++++++++++++++++++-------------- 1 file changed, 74 insertions(+), 47 deletions(-) diff --git a/web/react/utils/text_formatting.jsx b/web/react/utils/text_formatting.jsx index 48db78abc..2f93841f0 100644 --- a/web/react/utils/text_formatting.jsx +++ b/web/react/utils/text_formatting.jsx @@ -4,51 +4,68 @@ const Autolinker = require('autolinker'); const Constants = require('./constants.jsx'); const UserStore = require('../stores/user_store.jsx'); - +const Utils = require('./utils.jsx'); + +// Performs formatting of user posts including highlighting mentions and search terms and converting urls, hashtags, and +// @mentions to links by taking a user's message and returning a string of formatted html. Also takes a number of options +// as part of the second parameter: +// - searchTerm - If specified, this word is highlighted in the resulting html. Defaults to nothing. +// - mentionHighlight - Specifies whether or not to highlight mentions of the current user. Defaults to true. +// - singleline - Specifies whether or not to remove newlines. Defaults to false. export function formatText(text, options = {}) { - let output = sanitize(text); - let tokens = new Map(); + let output = sanitizeHtml(text); + const tokens = new Map(); + + // replace important words and phrases with tokens + output = autolinkUrls(output, tokens); + output = autolinkAtMentions(output, tokens); + output = autolinkHashtags(output, tokens); - output = stripLinks(output, tokens); - output = stripAtMentions(output, tokens); - output = stripSelfMentions(output, tokens); - output = stripHashtags(output, tokens); - output = highlightSearchTerm(output, tokens, options.searchTerm); + if (options.searchTerm) { + output = highlightSearchTerm(output, tokens, options.searchTerm); + } + if (!('mentionHighlight' in options) || options.mentionHighlight) { + output = highlightCurrentMentions(output, tokens); + } + + // reinsert tokens with formatted versions of the important words and phrases output = replaceTokens(output, tokens); + // replace newlines with html line breaks output = replaceNewlines(output, options.singleline); - // TODO markdown - return output; } -export function sanitize(text) { +export function sanitizeHtml(text) { let output = text; // normal string.replace only does a single occurrance so use a regex instead output = output.replace(/&/g, '&'); output = output.replace(//g, '>'); + output = output.replace(/'/g, '''); + output = output.replace(/"/g, '"'); return output; } -function stripLinks(text, tokens) { - function stripLink(autolinker, match) { - let text = match.getMatchedText(); - let url = text; +function autolinkUrls(text, tokens) { + function replaceUrlWithToken(autolinker, match) { + const linkText = match.getMatchedText(); + let url = linkText; + if (!url.startsWith('http')) { - url = `http://${text}`; + url = `http://${linkText}`; } const index = tokens.size; const alias = `LINK${index}`; tokens.set(alias, { - value: `${text}`, - originalText: text + value: `${linkText}`, + originalText: linkText }); return alias; @@ -61,16 +78,16 @@ function stripLinks(text, tokens) { phone: false, twitter: false, hashtag: false, - replaceFn: stripLink + replaceFn: replaceUrlWithToken }); return autolinker.link(text); } -function stripAtMentions(text, tokens) { +function autolinkAtMentions(text, tokens) { let output = text; - function stripAtMention(fullMatch, prefix, mention, username) { + function replaceAtMentionWithToken(fullMatch, prefix, mention, username) { const usernameLower = username.toLowerCase(); if (Constants.SPECIAL_MENTIONS.indexOf(usernameLower) !== -1 || UserStore.getProfileByUsername(usernameLower)) { const index = tokens.size; @@ -82,25 +99,24 @@ function stripAtMentions(text, tokens) { }); return prefix + alias; - } else { - return fullMatch; } + + return fullMatch; } - output = output.replace(/(^|\s)(@([a-z0-9.\-_]*[a-z0-9]))/gi, stripAtMention); + output = output.replace(/(^|\s)(@([a-z0-9.\-_]*[a-z0-9]))/gi, replaceAtMentionWithToken); return output; } -window.stripAtMentions = stripAtMentions; -function stripSelfMentions(text, tokens) { +function highlightCurrentMentions(text, tokens) { let output = text; - let mentionKeys = UserStore.getCurrentMentionKeys(); + const mentionKeys = UserStore.getCurrentMentionKeys(); // look for any existing tokens which are self mentions and should be highlighted var newTokens = new Map(); - for (let [alias, token] of tokens) { + for (const [alias, token] of tokens) { if (mentionKeys.indexOf(token.originalText) !== -1) { const index = newTokens.size; const newAlias = `SELFMENTION${index}`; @@ -115,12 +131,12 @@ function stripSelfMentions(text, tokens) { } // the new tokens are stashed in a separate map since we can't add objects to a map during iteration - for (let newToken of newTokens) { + for (const newToken of newTokens) { tokens.set(newToken[0], newToken[1]); } // look for self mentions in the text - function stripSelfMention(fullMatch, prefix, mention) { + function replaceCurrentMentionWithToken(fullMatch, prefix, mention) { const index = tokens.size; const alias = `SELFMENTION${index}`; @@ -132,24 +148,24 @@ function stripSelfMentions(text, tokens) { return prefix + alias; } - for (let mention of UserStore.getCurrentMentionKeys()) { - output = output.replace(new RegExp(`(^|\\W)(${mention})\\b`, 'gi'), stripSelfMention); + for (const mention of UserStore.getCurrentMentionKeys()) { + output = output.replace(new RegExp(`(^|\\W)(${mention})\\b`, 'gi'), replaceCurrentMentionWithToken); } return output; } -function stripHashtags(text, tokens) { +function autolinkHashtags(text, tokens) { let output = text; var newTokens = new Map(); - for (let [alias, token] of tokens) { + for (const [alias, token] of tokens) { if (token.originalText.startsWith('#')) { const index = newTokens.size; const newAlias = `HASHTAG${index}`; newTokens.set(newAlias, { - value: `${token.originalText}`, + value: `${token.originalText}`, originalText: token.originalText }); @@ -158,31 +174,31 @@ function stripHashtags(text, tokens) { } // the new tokens are stashed in a separate map since we can't add objects to a map during iteration - for (let newToken of newTokens) { + for (const newToken of newTokens) { tokens.set(newToken[0], newToken[1]); } // look for hashtags in the text - function stripHashtag(fullMatch, prefix, hashtag) { + function replaceHashtagWithToken(fullMatch, prefix, hashtag) { const index = tokens.size; const alias = `HASHTAG${index}`; tokens.set(alias, { - value: `${hashtag}`, + value: `${hashtag}`, originalText: hashtag }); return prefix + alias; } - return output.replace(/(^|\W)(#[a-zA-Z0-9.\-_]+)\b/g, stripHashtag); + return output.replace(/(^|\W)(#[a-zA-Z0-9.\-_]+)\b/g, replaceHashtagWithToken); } function highlightSearchTerm(text, tokens, searchTerm) { let output = text; var newTokens = new Map(); - for (let [alias, token] of tokens) { + for (const [alias, token] of tokens) { if (token.originalText === searchTerm) { const index = newTokens.size; const newAlias = `SEARCH_TERM${index}`; @@ -197,11 +213,11 @@ function highlightSearchTerm(text, tokens, searchTerm) { } // the new tokens are stashed in a separate map since we can't add objects to a map during iteration - for (let newToken of newTokens) { + for (const newToken of newTokens) { tokens.set(newToken[0], newToken[1]); } - function replaceSearchTerm(fullMatch, prefix, word) { + function replaceSearchTermWithToken(fullMatch, prefix, word) { const index = tokens.size; const alias = `SEARCH_TERM${index}`; @@ -213,7 +229,7 @@ function highlightSearchTerm(text, tokens, searchTerm) { return prefix + alias; } - return output.replace(new RegExp(`(^|\\W)(${searchTerm})\b`, 'gi'), replaceSearchTerm); + return output.replace(new RegExp(`(^|\\W)(${searchTerm})\\b`, 'gi'), replaceSearchTermWithToken); } function replaceTokens(text, tokens) { @@ -224,18 +240,29 @@ function replaceTokens(text, tokens) { for (let i = aliases.length - 1; i >= 0; i--) { const alias = aliases[i]; const token = tokens.get(alias); - console.log('replacing ' + alias + ' with ' + token.value); output = output.replace(alias, token.value); } return output; } -window.replaceTokens = replaceTokens; function replaceNewlines(text, singleline) { if (!singleline) { return text.replace(/\n/g, '
'); - } else { - return text.replace(/\n/g, ' '); + } + + return text.replace(/\n/g, ' '); +} + +// A click handler that can be used with the results of TextFormatting.formatText to add default functionality +// to clicked hashtags and @mentions. +export function handleClick(e) { + const mentionAttribute = e.target.getAttributeNode('data-mention'); + const hashtagAttribute = e.target.getAttributeNode('data-hashtag'); + + if (mentionAttribute) { + Utils.searchForTerm(mentionAttribute.value); + } else if (hashtagAttribute) { + Utils.searchForTerm(hashtagAttribute.value); } } -- cgit v1.2.3-1-g7c22 From 15aa853da5f6be8a95970d73a700afc6b626c572 Mon Sep 17 00:00:00 2001 From: hmhealey Date: Mon, 14 Sep 2015 11:48:07 -0400 Subject: Removed all calls to textToJsx and replaced them with calls to TextFormatting --- web/react/components/channel_header.jsx | 8 ++++---- web/react/components/message_wrapper.jsx | 10 +++------- web/react/components/navbar.jsx | 2 +- web/react/components/post_body.jsx | 14 +++++--------- web/react/components/rhs_comment.jsx | 10 ++++++++-- web/react/components/rhs_root_post.jsx | 7 +++++-- web/react/components/search_results_item.jsx | 16 +++++++++++++--- 7 files changed, 39 insertions(+), 28 deletions(-) diff --git a/web/react/components/channel_header.jsx b/web/react/components/channel_header.jsx index db23a5831..0dbbc20d4 100644 --- a/web/react/components/channel_header.jsx +++ b/web/react/components/channel_header.jsx @@ -8,6 +8,7 @@ const SocketStore = require('../stores/socket_store.jsx'); const NavbarSearchBox = require('./search_bar.jsx'); const AsyncClient = require('../utils/async_client.jsx'); const Client = require('../utils/client.jsx'); +const TextFormatting = require('../utils/text_formatting.jsx'); const Utils = require('../utils/utils.jsx'); const MessageWrapper = require('./message_wrapper.jsx'); const PopoverListMembers = require('./popover_list_members.jsx'); @@ -107,7 +108,6 @@ export default class ChannelHeader extends React.Component { } const channel = this.state.channel; - const description = Utils.textToJsx(channel.description, {singleline: true, noMentionHighlight: true}); const popoverContent = React.renderToString(); let channelTitle = channel.display_name; const currentId = UserStore.getCurrentId(); @@ -326,9 +326,9 @@ export default class ChannelHeader extends React.Component { data-toggle='popover' data-content={popoverContent} className='description' - > - {description} - + onClick={TextFormatting.handleClick} + dangerouslySetInnerHTML={{__html: TextFormatting.formatText(channel.description, {singleline: true, mentionHighlight: false})}} + /> diff --git a/web/react/components/message_wrapper.jsx b/web/react/components/message_wrapper.jsx index bce305853..5adf4f228 100644 --- a/web/react/components/message_wrapper.jsx +++ b/web/react/components/message_wrapper.jsx @@ -1,7 +1,7 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -var Utils = require('../utils/utils.jsx'); +var TextFormatting = require('../utils/text_formatting.jsx'); export default class MessageWrapper extends React.Component { constructor(props) { @@ -10,10 +10,7 @@ export default class MessageWrapper extends React.Component { } render() { if (this.props.message) { - var inner = Utils.textToJsx(this.props.message, this.props.options); - return ( -
{inner}
- ); + return
; } return
; @@ -21,8 +18,7 @@ export default class MessageWrapper extends React.Component { } MessageWrapper.defaultProps = { - message: null, - options: null + message: '' }; MessageWrapper.propTypes = { message: React.PropTypes.string, diff --git a/web/react/components/navbar.jsx b/web/react/components/navbar.jsx index 2258bf2b3..cae9f12e4 100644 --- a/web/react/components/navbar.jsx +++ b/web/react/components/navbar.jsx @@ -332,7 +332,7 @@ export default class Navbar extends React.Component { popoverContent = React.renderToString( ); isAdmin = this.state.member.roles.indexOf('admin') > -1; diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx index e08936923..df4ed3d57 100644 --- a/web/react/components/post_body.jsx +++ b/web/react/components/post_body.jsx @@ -13,7 +13,6 @@ export default class PostBody extends React.Component { super(props); this.parseEmojis = this.parseEmojis.bind(this); - this.handleClick = this.handleClick.bind(this); const linkData = Utils.extractLinks(this.props.post.message); this.state = {links: linkData.links, message: linkData.text}; @@ -31,17 +30,10 @@ export default class PostBody extends React.Component { const linkData = Utils.extractLinks(nextProps.post.message); this.setState({links: linkData.links, message: linkData.text}); } - handleClick(e) { - let mentionAttribute = e.target.getAttributeNode('data-mention'); - if (mentionAttribute) { - Utils.searchForTerm(mentionAttribute.value); - } - } render() { const post = this.props.post; const filenames = this.props.post.filenames; const parentPost = this.props.parentPost; - const inner = Utils.textToJsx(this.state.message); let comment = ''; let postClass = ''; @@ -143,7 +135,11 @@ export default class PostBody extends React.Component { key={`${post.id}_message`} className={postClass} > - {loading} + {loading} +

{fileAttachmentHolder} {embed} diff --git a/web/react/components/rhs_comment.jsx b/web/react/components/rhs_comment.jsx index f1a90102c..ed136c01f 100644 --- a/web/react/components/rhs_comment.jsx +++ b/web/react/components/rhs_comment.jsx @@ -12,6 +12,7 @@ var FileAttachmentList = require('./file_attachment_list.jsx'); var Client = require('../utils/client.jsx'); var AsyncClient = require('../utils/async_client.jsx'); var ActionTypes = Constants.ActionTypes; +var TextFormatting = require('../utils/text_formatting.jsx'); var twemoji = require('twemoji'); export default class RhsComment extends React.Component { @@ -84,7 +85,6 @@ export default class RhsComment extends React.Component { type = 'Comment'; } - var message = Utils.textToJsx(post.message); var timestamp = UserStore.getCurrentUser().update_at; var loading; @@ -202,7 +202,13 @@ export default class RhsComment extends React.Component {
-

{loading}{message}

+

+ {loading} + +

{fileAttachment}
diff --git a/web/react/components/rhs_root_post.jsx b/web/react/components/rhs_root_post.jsx index 83b57b955..85755a85c 100644 --- a/web/react/components/rhs_root_post.jsx +++ b/web/react/components/rhs_root_post.jsx @@ -4,6 +4,7 @@ var ChannelStore = require('../stores/channel_store.jsx'); var UserProfile = require('./user_profile.jsx'); var UserStore = require('../stores/user_store.jsx'); +var TextFormatting = require('../utils/text_formatting.jsx'); var utils = require('../utils/utils.jsx'); var FileAttachmentList = require('./file_attachment_list.jsx'); var twemoji = require('twemoji'); @@ -35,7 +36,6 @@ export default class RhsRootPost extends React.Component { } render() { var post = this.props.post; - var message = utils.textToJsx(post.message); var isOwner = UserStore.getCurrentId() === post.user_id; var timestamp = UserStore.getProfile(post.user_id).update_at; var channel = ChannelStore.get(post.channel_id); @@ -140,7 +140,10 @@ export default class RhsRootPost extends React.Component {
-

{message}

+

{fileAttachment}

diff --git a/web/react/components/search_results_item.jsx b/web/react/components/search_results_item.jsx index aa56f1174..0e951f5c6 100644 --- a/web/react/components/search_results_item.jsx +++ b/web/react/components/search_results_item.jsx @@ -10,6 +10,7 @@ var client = require('../utils/client.jsx'); var AsyncClient = require('../utils/async_client.jsx'); var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); var Constants = require('../utils/constants.jsx'); +var TextFormatting = require('../utils/text_formatting.jsx'); var ActionTypes = Constants.ActionTypes; export default class SearchResultsItem extends React.Component { @@ -56,7 +57,6 @@ export default class SearchResultsItem extends React.Component { } render() { - var message = utils.textToJsx(this.props.post.message, {searchTerm: this.props.term, noMentionHighlight: !this.props.isMentionSearch}); var channelName = ''; var channel = ChannelStore.get(this.props.post.channel_id); var timestamp = UserStore.getCurrentUser().update_at; @@ -68,6 +68,11 @@ export default class SearchResultsItem extends React.Component { } } + const formattingOptions = { + searchTerm: this.props.term, + mentionHighlight: this.props.isMentionSearch + }; + return (
-
{message}
+
+ +
); @@ -102,4 +112,4 @@ SearchResultsItem.propTypes = { post: React.PropTypes.object, isMentionSearch: React.PropTypes.bool, term: React.PropTypes.string -}; \ No newline at end of file +}; -- cgit v1.2.3-1-g7c22 From d8779efc9978a915b8f9b055af0c2850bee57b14 Mon Sep 17 00:00:00 2001 From: hmhealey Date: Mon, 14 Sep 2015 11:53:39 -0400 Subject: Removed textToJsx! --- web/react/utils/utils.jsx | 199 ---------------------------------------------- 1 file changed, 199 deletions(-) diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index 71cd1d344..abab04b0b 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -434,205 +434,6 @@ export function searchForTerm(term) { }); } -var puncStartRegex = /^((?![@#])\W)+/g; -var puncEndRegex = /(\W)+$/g; - -export function textToJsx(textin, options) { - var text = textin; - if (options && options.singleline) { - var repRegex = new RegExp('\n', 'g'); //eslint-disable-line no-control-regex - text = text.replace(repRegex, ' '); - } - - var searchTerm = ''; - if (options && options.searchTerm) { - searchTerm = options.searchTerm.toLowerCase(); - } - - var mentionClass = 'mention-highlight'; - if (options && options.noMentionHighlight) { - mentionClass = ''; - } - - var inner = []; - - // Function specific regex - var hashRegex = /^href="#[^']+"|(^#[A-Za-z]+[A-Za-z0-9_\-]*[A-Za-z0-9])$/g; - - var implicitKeywords = UserStore.getCurrentMentionKeys(); - - var lines = text.split('\n'); - for (let i = 0; i < lines.length; i++) { - var line = lines[i]; - var words = line.split(' '); - var highlightSearchClass = ''; - for (let z = 0; z < words.length; z++) { - var word = words[z]; - var trimWord = word.replace(puncStartRegex, '').replace(puncEndRegex, '').trim(); - var mentionRegex = /^(?:@)([a-z0-9_]+)$/gi; // looks loop invariant but a weird JS bug needs it to be redefined here - var explicitMention = mentionRegex.exec(trimWord); - - if (searchTerm !== '') { - let searchWords = searchTerm.split(' '); - for (let idx in searchWords) { - if ({}.hasOwnProperty.call(searchWords, idx)) { - let searchWord = searchWords[idx]; - if (searchWord === word.toLowerCase() || searchWord === trimWord.toLowerCase()) { - highlightSearchClass = ' search-highlight'; - break; - } else if (searchWord.charAt(searchWord.length - 1) === '*') { - let searchWordPrefix = searchWord.slice(0, -1); - if (trimWord.toLowerCase().indexOf(searchWordPrefix) > -1 || word.toLowerCase().indexOf(searchWordPrefix) > -1) { - highlightSearchClass = ' search-highlight'; - break; - } - } - } - } - } - - if (explicitMention && - (UserStore.getProfileByUsername(explicitMention[1]) || - Constants.SPECIAL_MENTIONS.indexOf(explicitMention[1]) !== -1)) { - let name = explicitMention[1]; - - // do both a non-case sensitive and case senstive check - let mClass = ''; - if (implicitKeywords.indexOf('@' + name.toLowerCase()) !== -1 || implicitKeywords.indexOf('@' + name) !== -1) { - mClass = mentionClass; - } - - let suffix = word.match(puncEndRegex); - let prefix = word.match(puncStartRegex); - - if (searchTerm === name) { - highlightSearchClass = ' search-highlight'; - } - - inner.push( - - {prefix} - searchForTerm(name)} //eslint-disable-line no-loop-func - > - @{name} - - {suffix} - {' '} - - ); - } else if (testUrlMatch(word).length) { - let match = testUrlMatch(word)[0]; - let link = match.link; - - let prefix = word.substring(0, word.indexOf(match.text)); - let suffix = word.substring(word.indexOf(match.text) + match.text.length); - - inner.push( - - {prefix} - - {match.text} - - {suffix} - {' '} - - ); - } else if (trimWord.match(hashRegex)) { - let suffix = word.match(puncEndRegex); - let prefix = word.match(puncStartRegex); - let mClass = ''; - if (implicitKeywords.indexOf(trimWord) !== -1 || implicitKeywords.indexOf(trimWord.toLowerCase()) !== -1) { - mClass = mentionClass; - } - - if (searchTerm === trimWord.substring(1).toLowerCase() || searchTerm === trimWord.toLowerCase()) { - highlightSearchClass = ' search-highlight'; - } - - inner.push( - - {prefix} - - {trimWord} - - {suffix} - {' '} - - ); - } else if (implicitKeywords.indexOf(trimWord) !== -1 || implicitKeywords.indexOf(trimWord.toLowerCase()) !== -1) { - let suffix = word.match(puncEndRegex); - let prefix = word.match(puncStartRegex); - - if (trimWord.charAt(0) === '@') { - if (searchTerm === trimWord.substring(1).toLowerCase()) { - highlightSearchClass = ' search-highlight'; - } - inner.push( - - {prefix} - - {trimWord} - - {suffix} - {' '} - - ); - } else { - inner.push( - - {prefix} - - {replaceHtmlEntities(trimWord)} - - {suffix} - {' '} - - ); - } - } else if (word === '') { - - // if word is empty dont include a span - - } else { - inner.push( - - - {replaceHtmlEntities(word)} - - {' '} - - ); - } - highlightSearchClass = ''; - } - if (i !== lines.length - 1) { - inner.push( -
- ); - } - } - - return inner; -} - export function getFileType(extin) { var ext = extin.toLowerCase(); if (Constants.IMAGE_TYPES.indexOf(ext) > -1) { -- cgit v1.2.3-1-g7c22 From 4c9ae22b6207c477b92737f4e79901c7366a4792 Mon Sep 17 00:00:00 2001 From: hmhealey Date: Mon, 14 Sep 2015 15:08:13 -0400 Subject: Renamed text formatting tokens so that there should be significantly less chance of having conflicting tokens --- web/react/utils/text_formatting.jsx | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/web/react/utils/text_formatting.jsx b/web/react/utils/text_formatting.jsx index 2f93841f0..2c67d7a46 100644 --- a/web/react/utils/text_formatting.jsx +++ b/web/react/utils/text_formatting.jsx @@ -61,7 +61,7 @@ function autolinkUrls(text, tokens) { } const index = tokens.size; - const alias = `LINK${index}`; + const alias = `__MM_LINK${index}__`; tokens.set(alias, { value: `${linkText}`, @@ -91,7 +91,7 @@ function autolinkAtMentions(text, tokens) { const usernameLower = username.toLowerCase(); if (Constants.SPECIAL_MENTIONS.indexOf(usernameLower) !== -1 || UserStore.getProfileByUsername(usernameLower)) { const index = tokens.size; - const alias = `ATMENTION${index}`; + const alias = `__MM_ATMENTION${index}__`; tokens.set(alias, { value: `${mention}`, @@ -118,8 +118,8 @@ function highlightCurrentMentions(text, tokens) { var newTokens = new Map(); for (const [alias, token] of tokens) { if (mentionKeys.indexOf(token.originalText) !== -1) { - const index = newTokens.size; - const newAlias = `SELFMENTION${index}`; + const index = tokens.size + newTokens.size; + const newAlias = `__MM_SELFMENTION${index}__`; newTokens.set(newAlias, { value: `${alias}`, @@ -138,7 +138,7 @@ function highlightCurrentMentions(text, tokens) { // look for self mentions in the text function replaceCurrentMentionWithToken(fullMatch, prefix, mention) { const index = tokens.size; - const alias = `SELFMENTION${index}`; + const alias = `__MM_SELFMENTION${index}__`; tokens.set(alias, { value: `${mention}`, @@ -161,8 +161,8 @@ function autolinkHashtags(text, tokens) { var newTokens = new Map(); for (const [alias, token] of tokens) { if (token.originalText.startsWith('#')) { - const index = newTokens.size; - const newAlias = `HASHTAG${index}`; + const index = tokens.size + newTokens.size; + const newAlias = `__MM_HASHTAG${index}__`; newTokens.set(newAlias, { value: `${token.originalText}`, @@ -181,7 +181,7 @@ function autolinkHashtags(text, tokens) { // look for hashtags in the text function replaceHashtagWithToken(fullMatch, prefix, hashtag) { const index = tokens.size; - const alias = `HASHTAG${index}`; + const alias = `__MM_HASHTAG${index}__`; tokens.set(alias, { value: `${hashtag}`, @@ -200,8 +200,8 @@ function highlightSearchTerm(text, tokens, searchTerm) { var newTokens = new Map(); for (const [alias, token] of tokens) { if (token.originalText === searchTerm) { - const index = newTokens.size; - const newAlias = `SEARCH_TERM${index}`; + const index = tokens.size + newTokens.size; + const newAlias = `__MM_SEARCHTERM${index}__`; newTokens.set(newAlias, { value: `${alias}`, @@ -219,7 +219,7 @@ function highlightSearchTerm(text, tokens, searchTerm) { function replaceSearchTermWithToken(fullMatch, prefix, word) { const index = tokens.size; - const alias = `SEARCH_TERM${index}`; + const alias = `__MM_SEARCHTERM${index}__`; tokens.set(alias, { value: `${word}`, -- cgit v1.2.3-1-g7c22