diff --git a/src/applications/search/view/PhabricatorSearchResultView.php b/src/applications/search/view/PhabricatorSearchResultView.php --- a/src/applications/search/view/PhabricatorSearchResultView.php +++ b/src/applications/search/view/PhabricatorSearchResultView.php @@ -76,33 +76,92 @@ $link); } + + /** + * Find the words which are part of the query string, and bold them in a + * result string. This makes it easier for users to see why a result + * matched their query. + */ private function emboldenQuery($str) { - if (!$this->query) { + $query = $this->query->getParameter('query'); + + if (!strlen($query) || !strlen($str)) { return $str; } - $query = $this->query->getParameter('query'); + // This algorithm is safe but not especially fast, so don't bother if + // we're dealing with a lot of data. This mostly prevents silly/malicious + // queries from doing anything bad. + if (strlen($query) + strlen($str) > 2048) { + return $str; + } - $quoted_regexp = '/"([^"]*)"/'; - $matches = array(1 => array()); - preg_match_all($quoted_regexp, $query, $matches); - $quoted_queries = $matches[1]; - $query = preg_replace($quoted_regexp, '', $query); - - $query = preg_split('/\s+[+|]?/u', $query); - $query = array_filter($query); - $query = array_merge($query, $quoted_queries); - $str = phutil_escape_html($str); - foreach ($query as $word) { - $word = phutil_escape_html($word); - $word = preg_quote($word, '/'); - $word = preg_replace('/\\\\\*$/', '\w*', $word); - $str = preg_replace( - '/(?:^|\b)('.$word.')(?:\b|$)/i', - '\1', - $str); + // Keep track of which characters we're going to make bold. This is + // byte oriented, but we'll make sure we don't put a bold in the middle + // of a character later. + $bold = array_fill(0, strlen($str), false); + + // Split the query into words. + $parts = preg_split('/ +/', $query); + + // Find all occurrences of each word, and mark them to be emboldened. + foreach ($parts as $part) { + $part = trim($part); + $part = trim($part, '"+'); + if (!strlen($part)) { + continue; + } + + $matches = null; + $has_matches = preg_match_all( + '/(?:^|\b)('.preg_quote($part, '/').')/i', + $str, + $matches, + PREG_OFFSET_CAPTURE); + + if (!$has_matches) { + continue; + } + + // Flag the matching part of the range for boldening. + foreach ($matches[1] as $match) { + $offset = $match[1]; + for ($ii = 0; $ii < strlen($match[0]); $ii++) { + $bold[$offset + $ii] = true; + } + } } - return phutil_safe_html($str); + + // Split the string into ranges, applying bold styling as required. + $out = array(); + $buf = ''; + $pos = 0; + $is_bold = false; + foreach (phutil_utf8v($str) as $chr) { + if ($bold[$pos] != $is_bold) { + if (strlen($buf)) { + if ($is_bold) { + $out[] = phutil_tag('strong', array(), $buf); + } else { + $out[] = $buf; + } + $buf = ''; + } + $is_bold = !$is_bold; + } + $buf .= $chr; + $pos += strlen($chr); + } + + if (strlen($buf)) { + if ($is_bold) { + $out[] = phutil_tag('strong', array(), $buf); + } else { + $out[] = $buf; + } + } + + return $out; } }