From 477b4d3edd5f93928c6fdb807ba84697f177a3b9 Mon Sep 17 00:00:00 2001 From: hmhealey Date: Thu, 12 Nov 2015 16:53:32 -0500 Subject: Improved highlighting of search flags to handle quotes and flags --- web/react/utils/text_formatting.jsx | 104 +++++++++++++++++++++++++++++------- 1 file changed, 86 insertions(+), 18 deletions(-) (limited to 'web/react') diff --git a/web/react/utils/text_formatting.jsx b/web/react/utils/text_formatting.jsx index 705d85cf6..7f1d7175d 100644 --- a/web/react/utils/text_formatting.jsx +++ b/web/react/utils/text_formatting.jsx @@ -259,30 +259,73 @@ function autolinkHashtags(text, tokens) { return output.replace(/(^|\W)(#[a-zA-Z][a-zA-Z0-9.\-_]*)\b/g, replaceHashtagWithToken); } -function highlightSearchTerm(text, tokens, searchTerm) { - let output = text; +const puncStart = /^[.,()&$!\[\]{}':;\\]+/; +const puncEnd = /[.,()&$#!\[\]{}':;\\]+$/; - var newTokens = new Map(); - for (const [alias, token] of tokens) { - if (token.originalText.indexOf(searchTerm.replace(/\*$/, '')) > -1) { - const index = tokens.size + newTokens.size; - const newAlias = `MM_SEARCHTERM${index}`; +function parseSearchTerms(searchTerm) { + let terms = []; - newTokens.set(newAlias, { - value: `${alias}`, - originalText: token.originalText - }); + let termString = searchTerm; - output = output.replace(alias, newAlias); + while (termString) { + let captured; + + // check for a quoted string + captured = (/^"(.*?)"/).exec(termString); + if (captured) { + termString = termString.substring(captured[0].length); + terms.push(captured[1]); + continue; + } + + // check for a search flag (and don't add it to terms) + captured = (/^(?:in|from|channel): ?\S+/).exec(termString); + if (captured) { + termString = termString.substring(captured[0].length); + continue; + } + + // capture any plain text up until the next quote or search flag + captured = (/^.+?(?=\bin|\bfrom|\bchannel|"|$)/).exec(termString); + if (captured) { + termString = termString.substring(captured[0].length); + + // break the text up into words based on how the server splits them in SqlPostStore.SearchPosts and then discard empty terms + terms.push(...captured[0].split(/[ <>+\-\(\)\~\@]/).filter((term) => !!term)); + continue; } + + // we should never reach this point since at least one of the regexes should match something in the remaining text + throw new Error('Infinite loop in search term parsing: ' + termString); } - // the new tokens are stashed in a separate map since we can't add objects to a map during iteration - for (const newToken of newTokens) { - tokens.set(newToken[0], newToken[1]); + // remove punctuation from each term + terms = terms.map((term) => term.replace(puncStart, '').replace(puncEnd, '')); + + return terms; +} + +function convertSearchTermToRegex(term) { + let pattern; + if (term.endsWith('*')) { + pattern = '\\b' + escapeRegex(term.substring(0, term.length - 1)); + } else { + pattern = '\\b' + escapeRegex(term) + '\\b'; } - function replaceSearchTermWithToken(fullMatch, prefix, word) { + return new RegExp(pattern, 'gi'); +} + +function highlightSearchTerm(text, tokens, searchTerm) { + const terms = parseSearchTerms(searchTerm); + + if (terms.length === 0) { + return text; + } + + let output = text; + + function replaceSearchTermWithToken(word) { const index = tokens.size; const alias = `MM_SEARCHTERM${index}`; @@ -291,10 +334,35 @@ function highlightSearchTerm(text, tokens, searchTerm) { originalText: word }); - return prefix + alias; + return alias; } - return output.replace(new RegExp(`()(${escapeRegex(searchTerm)})`, 'gi'), replaceSearchTermWithToken); + for (const term of terms) { + // highlight existing tokens matching search terms + var newTokens = new Map(); + for (const [alias, token] of tokens) { + if (token.originalText === term.replace(/\*$/, '')) { + const index = tokens.size + newTokens.size; + const newAlias = `MM_SEARCHTERM${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 (const newToken of newTokens) { + tokens.set(newToken[0], newToken[1]); + } + + output = output.replace(convertSearchTermToRegex(term), replaceSearchTermWithToken); + } + + return output; } function replaceTokens(text, tokens) { -- cgit v1.2.3-1-g7c22