@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3final class PhabricatorSearchResultView extends AphrontView {
4
5 private $handle;
6 private $object;
7 private $tokens;
8
9 public function setHandle(PhabricatorObjectHandle $handle) {
10 $this->handle = $handle;
11 return $this;
12 }
13
14 /**
15 * @param array<PhabricatorFulltextToken> $tokens
16 * @return $this
17 */
18 public function setTokens(array $tokens) {
19 assert_instances_of($tokens, PhabricatorFulltextToken::class);
20 $this->tokens = $tokens;
21 return $this;
22 }
23
24 public function setObject($object) {
25 $this->object = $object;
26 return $this;
27 }
28
29 /**
30 * Render a search result item
31 *
32 * @return PHUIObjectItemView|null
33 */
34 public function render() {
35 $handle = $this->handle;
36 if (!$handle->isComplete()) {
37 return null;
38 }
39
40 require_celerity_resource('phabricator-search-results-css');
41
42 $type_name = nonempty($handle->getTypeName(), pht('Document'));
43
44 $raw_title = $handle->getFullName();
45 $title = $this->emboldenQuery($raw_title);
46
47 $item = id(new PHUIObjectItemView())
48 ->setHeader($title)
49 ->setTitleText($raw_title)
50 ->setHref($handle->getURI())
51 ->setImageURI($handle->getImageURI())
52 ->addAttribute($type_name);
53
54 if ($handle->getStatus() == PhabricatorObjectHandle::STATUS_CLOSED) {
55 $item->setDisabled(true);
56 $item->addAttribute(pht('Closed'));
57 }
58
59 return $item;
60 }
61
62
63 /**
64 * Find the words which are part of the query string, and bold them in a
65 * result string. This makes it easier for users to see why a result
66 * matched their query.
67 */
68 private function emboldenQuery($str) {
69 $tokens = $this->tokens;
70
71 if (!$tokens) {
72 return $str;
73 }
74
75 if (count($tokens) > 16) {
76 return $str;
77 }
78
79 if (!strlen($str)) {
80 return $str;
81 }
82
83 if (strlen($str) > 2048) {
84 return $str;
85 }
86
87 $patterns = array();
88 foreach ($tokens as $token) {
89 $raw_token = $token->getToken();
90 $operator = $raw_token->getOperator();
91
92 $value = $raw_token->getValue();
93
94 switch ($operator) {
95 case PhutilSearchQueryCompiler::OPERATOR_SUBSTRING:
96 case PhutilSearchQueryCompiler::OPERATOR_EXACT:
97 $patterns[] = '(('.preg_quote($value).'))ui';
98 break;
99 case PhutilSearchQueryCompiler::OPERATOR_AND:
100 $patterns[] = '((?<=\W|^)('.preg_quote($value).')(?=\W|\z))ui';
101 break;
102 default:
103 // Don't highlight anything else, particularly "NOT".
104 break;
105 }
106 }
107
108 // Find all matches for all query terms in the document title, then reduce
109 // them to a map from offsets to highlighted sequence lengths. If two terms
110 // match at the same position, we choose the longer one.
111 $all_matches = array();
112 foreach ($patterns as $pattern) {
113 $matches = null;
114 $ok = preg_match_all(
115 $pattern,
116 $str,
117 $matches,
118 PREG_OFFSET_CAPTURE);
119 if (!$ok) {
120 continue;
121 }
122
123 foreach ($matches[1] as $match) {
124 $match_text = $match[0];
125 $match_offset = $match[1];
126
127 if (!isset($all_matches[$match_offset])) {
128 $all_matches[$match_offset] = 0;
129 }
130
131 $all_matches[$match_offset] = max(
132 $all_matches[$match_offset],
133 strlen($match_text));
134 }
135 }
136
137 // Go through the string one display glyph at a time. If a glyph starts
138 // on a highlighted byte position, turn on highlighting for the number
139 // of matching bytes. If a query searches for "e" and the document contains
140 // an "e" followed by a bunch of combining marks, this will correctly
141 // highlight the entire glyph.
142 $parts = array();
143 $highlight = 0;
144 $offset = 0;
145 foreach (phutil_utf8v_combined($str) as $character) {
146 $length = strlen($character);
147
148 if (isset($all_matches[$offset])) {
149 $highlight = $all_matches[$offset];
150 }
151
152 if ($highlight > 0) {
153 $is_highlighted = true;
154 $highlight -= $length;
155 } else {
156 $is_highlighted = false;
157 }
158
159 $parts[] = array(
160 'text' => $character,
161 'highlighted' => $is_highlighted,
162 );
163
164 $offset += $length;
165 }
166
167 // Combine all the sequences together so we aren't emitting a tag around
168 // every individual character.
169 $last = null;
170 foreach ($parts as $key => $part) {
171 if ($last !== null) {
172 if ($part['highlighted'] == $parts[$last]['highlighted']) {
173 $parts[$last]['text'] .= $part['text'];
174 unset($parts[$key]);
175 continue;
176 }
177 }
178
179 $last = $key;
180 }
181
182 // Finally, add tags.
183 $result = array();
184 foreach ($parts as $part) {
185 if ($part['highlighted']) {
186 $result[] = phutil_tag('strong', array(), $part['text']);
187 } else {
188 $result[] = $part['text'];
189 }
190 }
191
192 return $result;
193 }
194
195}