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