aboutsummaryrefslogtreecommitdiffhomepage
path: root/application/formatter/BookmarkDefaultFormatter.php
diff options
context:
space:
mode:
authorArthurHoaro <arthur@hoa.ro>2020-10-12 11:35:55 +0200
committerArthurHoaro <arthur@hoa.ro>2020-10-16 20:31:12 +0200
commit4e3875c0ce7f3b17e3d358dc5ecb1f8bed64546b (patch)
tree4deb157f03ce7d5402dbfeb65743951d97e527cf /application/formatter/BookmarkDefaultFormatter.php
parent64cac2562661c55f679dba5a7c308e7764f430b5 (diff)
downloadShaarli-4e3875c0ce7f3b17e3d358dc5ecb1f8bed64546b.tar.gz
Shaarli-4e3875c0ce7f3b17e3d358dc5ecb1f8bed64546b.tar.zst
Shaarli-4e3875c0ce7f3b17e3d358dc5ecb1f8bed64546b.zip
Feature: highlight fulltext search results
How it works: 1. when a fulltext search is made, Shaarli looks for the first occurence position of every term matching the search. No change here, but we store these positions in an array, in Bookmark's additionalContent. 2. when formatting bookmarks (through BookmarkFormatter implementation): 1. first we insert specific tokens at every search result positions 2. we format the content (escape HTML, apply markdown, etc.) 3. as a last step, we replace our token with displayable span elements Cons: this tightens coupling between search filters and formatters Pros: it was absolutely necessary not to perform the search twice. this solution has close to no impact on performances. Fixes #205
Diffstat (limited to 'application/formatter/BookmarkDefaultFormatter.php')
-rw-r--r--application/formatter/BookmarkDefaultFormatter.php132
1 files changed, 127 insertions, 5 deletions
diff --git a/application/formatter/BookmarkDefaultFormatter.php b/application/formatter/BookmarkDefaultFormatter.php
index 9d4a0fa0..d58a5e39 100644
--- a/application/formatter/BookmarkDefaultFormatter.php
+++ b/application/formatter/BookmarkDefaultFormatter.php
@@ -12,10 +12,13 @@ namespace Shaarli\Formatter;
12 */ 12 */
13class BookmarkDefaultFormatter extends BookmarkFormatter 13class BookmarkDefaultFormatter extends BookmarkFormatter
14{ 14{
15 const SEARCH_HIGHLIGHT_OPEN = '|@@HIGHLIGHT';
16 const SEARCH_HIGHLIGHT_CLOSE = 'HIGHLIGHT@@|';
17
15 /** 18 /**
16 * @inheritdoc 19 * @inheritdoc
17 */ 20 */
18 public function formatTitle($bookmark) 21 protected function formatTitle($bookmark)
19 { 22 {
20 return escape($bookmark->getTitle()); 23 return escape($bookmark->getTitle());
21 } 24 }
@@ -23,10 +26,28 @@ class BookmarkDefaultFormatter extends BookmarkFormatter
23 /** 26 /**
24 * @inheritdoc 27 * @inheritdoc
25 */ 28 */
26 public function formatDescription($bookmark) 29 protected function formatTitleHtml($bookmark)
30 {
31 $title = $this->tokenizeSearchHighlightField(
32 $bookmark->getTitle() ?? '',
33 $bookmark->getAdditionalContentEntry('search_highlight')['title'] ?? []
34 );
35
36 return $this->replaceTokens(escape($title));
37 }
38
39 /**
40 * @inheritdoc
41 */
42 protected function formatDescription($bookmark)
27 { 43 {
28 $indexUrl = ! empty($this->contextData['index_url']) ? $this->contextData['index_url'] : ''; 44 $indexUrl = ! empty($this->contextData['index_url']) ? $this->contextData['index_url'] : '';
29 return format_description(escape($bookmark->getDescription()), $indexUrl); 45 $description = $this->tokenizeSearchHighlightField(
46 $bookmark->getDescription() ?? '',
47 $bookmark->getAdditionalContentEntry('search_highlight')['description'] ?? []
48 );
49
50 return $this->replaceTokens(format_description(escape($description), $indexUrl));
30 } 51 }
31 52
32 /** 53 /**
@@ -40,7 +61,27 @@ class BookmarkDefaultFormatter extends BookmarkFormatter
40 /** 61 /**
41 * @inheritdoc 62 * @inheritdoc
42 */ 63 */
43 public function formatTagString($bookmark) 64 protected function formatTagListHtml($bookmark)
65 {
66 if (empty($bookmark->getAdditionalContentEntry('search_highlight')['tags'])) {
67 return $this->formatTagList($bookmark);
68 }
69
70 $tags = $this->tokenizeSearchHighlightField(
71 $bookmark->getTagsString(),
72 $bookmark->getAdditionalContentEntry('search_highlight')['tags']
73 );
74 $tags = $this->filterTagList(explode(' ', $tags));
75 $tags = escape($tags);
76 $tags = $this->replaceTokensArray($tags);
77
78 return $tags;
79 }
80
81 /**
82 * @inheritdoc
83 */
84 protected function formatTagString($bookmark)
44 { 85 {
45 return implode(' ', $this->formatTagList($bookmark)); 86 return implode(' ', $this->formatTagList($bookmark));
46 } 87 }
@@ -48,7 +89,7 @@ class BookmarkDefaultFormatter extends BookmarkFormatter
48 /** 89 /**
49 * @inheritdoc 90 * @inheritdoc
50 */ 91 */
51 public function formatUrl($bookmark) 92 protected function formatUrl($bookmark)
52 { 93 {
53 if ($bookmark->isNote() && isset($this->contextData['index_url'])) { 94 if ($bookmark->isNote() && isset($this->contextData['index_url'])) {
54 return rtrim($this->contextData['index_url'], '/') . '/' . escape(ltrim($bookmark->getUrl(), '/')); 95 return rtrim($this->contextData['index_url'], '/') . '/' . escape(ltrim($bookmark->getUrl(), '/'));
@@ -80,8 +121,89 @@ class BookmarkDefaultFormatter extends BookmarkFormatter
80 /** 121 /**
81 * @inheritdoc 122 * @inheritdoc
82 */ 123 */
124 protected function formatUrlHtml($bookmark)
125 {
126 $url = $this->tokenizeSearchHighlightField(
127 $bookmark->getUrl() ?? '',
128 $bookmark->getAdditionalContentEntry('search_highlight')['url'] ?? []
129 );
130
131 return $this->replaceTokens(escape($url));
132 }
133
134 /**
135 * @inheritdoc
136 */
83 protected function formatThumbnail($bookmark) 137 protected function formatThumbnail($bookmark)
84 { 138 {
85 return escape($bookmark->getThumbnail()); 139 return escape($bookmark->getThumbnail());
86 } 140 }
141
142 /**
143 * Insert search highlight token in provided field content based on a list of search result positions
144 *
145 * @param string $fieldContent
146 * @param array|null $positions List of of search results with 'start' and 'end' positions.
147 *
148 * @return string Updated $fieldContent.
149 */
150 protected function tokenizeSearchHighlightField(string $fieldContent, ?array $positions): string
151 {
152 if (empty($positions)) {
153 return $fieldContent;
154 }
155
156 $insertedTokens = 0;
157 $tokenLength = strlen(static::SEARCH_HIGHLIGHT_OPEN);
158 foreach ($positions as $position) {
159 $position = [
160 'start' => $position['start'] + ($insertedTokens * $tokenLength),
161 'end' => $position['end'] + ($insertedTokens * $tokenLength),
162 ];
163
164 $content = mb_substr($fieldContent, 0, $position['start']);
165 $content .= static::SEARCH_HIGHLIGHT_OPEN;
166 $content .= mb_substr($fieldContent, $position['start'], $position['end'] - $position['start']);
167 $content .= static::SEARCH_HIGHLIGHT_CLOSE;
168 $content .= mb_substr($fieldContent, $position['end']);
169
170 $fieldContent = $content;
171
172 $insertedTokens += 2;
173 }
174
175 return $fieldContent;
176 }
177
178 /**
179 * Replace search highlight tokens with HTML highlighted span.
180 *
181 * @param string $fieldContent
182 *
183 * @return string updated content.
184 */
185 protected function replaceTokens(string $fieldContent): string
186 {
187 return str_replace(
188 [static::SEARCH_HIGHLIGHT_OPEN, static::SEARCH_HIGHLIGHT_CLOSE],
189 ['<span class="search-highlight">', '</span>'],
190 $fieldContent
191 );
192 }
193
194 /**
195 * Apply replaceTokens to an array of content strings.
196 *
197 * @param string[] $fieldContents
198 *
199 * @return array
200 */
201 protected function replaceTokensArray(array $fieldContents): array
202 {
203 foreach ($fieldContents as &$entry) {
204 $entry = $this->replaceTokens($entry);
205 }
206
207 return $fieldContents;
208 }
87} 209}