From 477b4d3edd5f93928c6fdb807ba84697f177a3b9 Mon Sep 17 00:00:00 2001
From: hmhealey <harrisonmhealey@gmail.com>
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/utils/text_formatting.jsx')

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: `<span class='search-highlight'>${alias}</span>`,
-                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: `<span class='search-highlight'>${alias}</span>`,
+                    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

