Page MenuHomec4science

PhabricatorSearchResultView.php
No OneTemporary

File Metadata

Created
Thu, May 2, 16:34

PhabricatorSearchResultView.php

<?php
final class PhabricatorSearchResultView extends AphrontView {
private $handle;
private $object;
private $tokens;
public function setHandle(PhabricatorObjectHandle $handle) {
$this->handle = $handle;
return $this;
}
public function setTokens(array $tokens) {
assert_instances_of($tokens, 'PhabricatorFulltextToken');
$this->tokens = $tokens;
return $this;
}
public function setObject($object) {
$this->object = $object;
return $this;
}
public function render() {
$handle = $this->handle;
if (!$handle->isComplete()) {
return;
}
require_celerity_resource('phabricator-search-results-css');
$type_name = nonempty($handle->getTypeName(), pht('Document'));
$raw_title = $handle->getFullName();
$title = $this->emboldenQuery($raw_title);
$item = id(new PHUIObjectItemView())
->setHeader($title)
->setTitleText($raw_title)
->setHref($handle->getURI())
->setImageURI($handle->getImageURI())
->addAttribute($type_name);
if ($handle->getStatus() == PhabricatorObjectHandle::STATUS_CLOSED) {
$item->setDisabled(true);
$item->addAttribute(pht('Closed'));
}
return $item;
}
/**
* 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) {
$tokens = $this->tokens;
if (!$tokens) {
return $str;
}
if (count($tokens) > 16) {
return $str;
}
if (!strlen($str)) {
return $str;
}
if (strlen($str) > 2048) {
return $str;
}
$patterns = array();
foreach ($tokens as $token) {
$raw_token = $token->getToken();
$operator = $raw_token->getOperator();
$value = $raw_token->getValue();
switch ($operator) {
case PhutilSearchQueryCompiler::OPERATOR_SUBSTRING:
case PhutilSearchQueryCompiler::OPERATOR_EXACT:
$patterns[] = '(('.preg_quote($value).'))ui';
break;
case PhutilSearchQueryCompiler::OPERATOR_AND:
$patterns[] = '((?<=\W|^)('.preg_quote($value).')(?=\W|\z))ui';
break;
default:
// Don't highlight anything else, particularly "NOT".
break;
}
}
// Find all matches for all query terms in the document title, then reduce
// them to a map from offsets to highlighted sequence lengths. If two terms
// match at the same position, we choose the longer one.
$all_matches = array();
foreach ($patterns as $pattern) {
$matches = null;
$ok = preg_match_all(
$pattern,
$str,
$matches,
PREG_OFFSET_CAPTURE);
if (!$ok) {
continue;
}
foreach ($matches[1] as $match) {
$match_text = $match[0];
$match_offset = $match[1];
if (!isset($all_matches[$match_offset])) {
$all_matches[$match_offset] = 0;
}
$all_matches[$match_offset] = max(
$all_matches[$match_offset],
strlen($match_text));
}
}
// Go through the string one display glyph at a time. If a glyph starts
// on a highlighted byte position, turn on highlighting for the number
// of matching bytes. If a query searches for "e" and the document contains
// an "e" followed by a bunch of combining marks, this will correctly
// highlight the entire glyph.
$parts = array();
$highlight = 0;
$offset = 0;
foreach (phutil_utf8v_combined($str) as $character) {
$length = strlen($character);
if (isset($all_matches[$offset])) {
$highlight = $all_matches[$offset];
}
if ($highlight > 0) {
$is_highlighted = true;
$highlight -= $length;
} else {
$is_highlighted = false;
}
$parts[] = array(
'text' => $character,
'highlighted' => $is_highlighted,
);
$offset += $length;
}
// Combine all the sequences together so we aren't emitting a tag around
// every individual character.
$last = null;
foreach ($parts as $key => $part) {
if ($last !== null) {
if ($part['highlighted'] == $parts[$last]['highlighted']) {
$parts[$last]['text'] .= $part['text'];
unset($parts[$key]);
continue;
}
}
$last = $key;
}
// Finally, add tags.
$result = array();
foreach ($parts as $part) {
if ($part['highlighted']) {
$result[] = phutil_tag('strong', array(), $part['text']);
} else {
$result[] = $part['text'];
}
}
return $result;
}
}

Event Timeline