diff options
author | ArthurHoaro <arthur@hoa.ro> | 2020-10-16 20:40:49 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-10-16 20:40:49 +0200 |
commit | 6866ed766f917f37bc7e1276779dece63d0f2835 (patch) | |
tree | 89b085ac6c4cd7608526f855f452d1a797dbb333 | |
parent | 64cac2562661c55f679dba5a7c308e7764f430b5 (diff) | |
parent | f1a148ab92c061ac129b5b2976de02d45b6a71e7 (diff) | |
download | Shaarli-6866ed766f917f37bc7e1276779dece63d0f2835.tar.gz Shaarli-6866ed766f917f37bc7e1276779dece63d0f2835.tar.zst Shaarli-6866ed766f917f37bc7e1276779dece63d0f2835.zip |
Merge pull request #1588 from ArthurHoaro/feature/search-highlight
-rw-r--r-- | application/bookmark/Bookmark.php | 46 | ||||
-rw-r--r-- | application/bookmark/BookmarkFilter.php | 111 | ||||
-rw-r--r-- | application/formatter/BookmarkDefaultFormatter.php | 132 | ||||
-rw-r--r-- | application/formatter/BookmarkFormatter.php | 79 | ||||
-rw-r--r-- | application/formatter/BookmarkMarkdownFormatter.php | 6 | ||||
-rw-r--r-- | assets/default/scss/shaarli.scss | 4 | ||||
-rw-r--r-- | tests/api/controllers/links/GetLinksTest.php | 2 | ||||
-rw-r--r-- | tests/bookmark/BookmarkFileServiceTest.php | 16 | ||||
-rw-r--r-- | tests/bookmark/BookmarkFilterTest.php | 40 | ||||
-rw-r--r-- | tests/formatter/BookmarkDefaultFormatterTest.php | 115 | ||||
-rw-r--r-- | tests/legacy/LegacyLinkDBTest.php | 12 | ||||
-rw-r--r-- | tests/utils/ReferenceLinkDB.php | 2 | ||||
-rw-r--r-- | tpl/default/linklist.html | 6 |
13 files changed, 533 insertions, 38 deletions
diff --git a/application/bookmark/Bookmark.php b/application/bookmark/Bookmark.php index fa45d2fc..ea565d1f 100644 --- a/application/bookmark/Bookmark.php +++ b/application/bookmark/Bookmark.php | |||
@@ -54,6 +54,9 @@ class Bookmark | |||
54 | /** @var bool True if the bookmark can only be seen while logged in */ | 54 | /** @var bool True if the bookmark can only be seen while logged in */ |
55 | protected $private; | 55 | protected $private; |
56 | 56 | ||
57 | /** @var mixed[] Available to store any additional content for a bookmark. Currently used for search highlight. */ | ||
58 | protected $additionalContent = []; | ||
59 | |||
57 | /** | 60 | /** |
58 | * Initialize a link from array data. Especially useful to create a Bookmark from former link storage format. | 61 | * Initialize a link from array data. Especially useful to create a Bookmark from former link storage format. |
59 | * | 62 | * |
@@ -95,6 +98,8 @@ class Bookmark | |||
95 | * - the URL with the permalink | 98 | * - the URL with the permalink |
96 | * - the title with the URL | 99 | * - the title with the URL |
97 | * | 100 | * |
101 | * Also make sure that we do not save search highlights in the datastore. | ||
102 | * | ||
98 | * @throws InvalidBookmarkException | 103 | * @throws InvalidBookmarkException |
99 | */ | 104 | */ |
100 | public function validate(): void | 105 | public function validate(): void |
@@ -112,6 +117,9 @@ class Bookmark | |||
112 | if (empty($this->title)) { | 117 | if (empty($this->title)) { |
113 | $this->title = $this->url; | 118 | $this->title = $this->url; |
114 | } | 119 | } |
120 | if (array_key_exists('search_highlight', $this->additionalContent)) { | ||
121 | unset($this->additionalContent['search_highlight']); | ||
122 | } | ||
115 | } | 123 | } |
116 | 124 | ||
117 | /** | 125 | /** |
@@ -436,6 +444,44 @@ class Bookmark | |||
436 | } | 444 | } |
437 | 445 | ||
438 | /** | 446 | /** |
447 | * Get entire additionalContent array. | ||
448 | * | ||
449 | * @return mixed[] | ||
450 | */ | ||
451 | public function getAdditionalContent(): array | ||
452 | { | ||
453 | return $this->additionalContent; | ||
454 | } | ||
455 | |||
456 | /** | ||
457 | * Set a single entry in additionalContent, by key. | ||
458 | * | ||
459 | * @param string $key | ||
460 | * @param mixed|null $value Any type of value can be set. | ||
461 | * | ||
462 | * @return $this | ||
463 | */ | ||
464 | public function addAdditionalContentEntry(string $key, $value): self | ||
465 | { | ||
466 | $this->additionalContent[$key] = $value; | ||
467 | |||
468 | return $this; | ||
469 | } | ||
470 | |||
471 | /** | ||
472 | * Get a single entry in additionalContent, by key. | ||
473 | * | ||
474 | * @param string $key | ||
475 | * @param mixed|null $default | ||
476 | * | ||
477 | * @return mixed|null can be any type or even null. | ||
478 | */ | ||
479 | public function getAdditionalContentEntry(string $key, $default = null) | ||
480 | { | ||
481 | return array_key_exists($key, $this->additionalContent) ? $this->additionalContent[$key] : $default; | ||
482 | } | ||
483 | |||
484 | /** | ||
439 | * Rename a tag in tags list. | 485 | * Rename a tag in tags list. |
440 | * | 486 | * |
441 | * @param string $fromTag | 487 | * @param string $fromTag |
diff --git a/application/bookmark/BookmarkFilter.php b/application/bookmark/BookmarkFilter.php index 4232f114..c79386ea 100644 --- a/application/bookmark/BookmarkFilter.php +++ b/application/bookmark/BookmarkFilter.php | |||
@@ -201,7 +201,7 @@ class BookmarkFilter | |||
201 | return $this->noFilter($visibility); | 201 | return $this->noFilter($visibility); |
202 | } | 202 | } |
203 | 203 | ||
204 | $filtered = array(); | 204 | $filtered = []; |
205 | $search = mb_convert_case(html_entity_decode($searchterms), MB_CASE_LOWER, 'UTF-8'); | 205 | $search = mb_convert_case(html_entity_decode($searchterms), MB_CASE_LOWER, 'UTF-8'); |
206 | $exactRegex = '/"([^"]+)"/'; | 206 | $exactRegex = '/"([^"]+)"/'; |
207 | // Retrieve exact search terms. | 207 | // Retrieve exact search terms. |
@@ -213,8 +213,8 @@ class BookmarkFilter | |||
213 | $explodedSearchAnd = array_values(array_filter($explodedSearchAnd)); | 213 | $explodedSearchAnd = array_values(array_filter($explodedSearchAnd)); |
214 | 214 | ||
215 | // Filter excluding terms and update andSearch. | 215 | // Filter excluding terms and update andSearch. |
216 | $excludeSearch = array(); | 216 | $excludeSearch = []; |
217 | $andSearch = array(); | 217 | $andSearch = []; |
218 | foreach ($explodedSearchAnd as $needle) { | 218 | foreach ($explodedSearchAnd as $needle) { |
219 | if ($needle[0] == '-' && strlen($needle) > 1) { | 219 | if ($needle[0] == '-' && strlen($needle) > 1) { |
220 | $excludeSearch[] = substr($needle, 1); | 220 | $excludeSearch[] = substr($needle, 1); |
@@ -234,33 +234,38 @@ class BookmarkFilter | |||
234 | } | 234 | } |
235 | } | 235 | } |
236 | 236 | ||
237 | // Concatenate link fields to search across fields. | 237 | $lengths = []; |
238 | // Adds a '\' separator for exact search terms. | 238 | $content = $this->buildFullTextSearchableLink($link, $lengths); |
239 | $content = mb_convert_case($link->getTitle(), MB_CASE_LOWER, 'UTF-8') .'\\'; | ||
240 | $content .= mb_convert_case($link->getDescription(), MB_CASE_LOWER, 'UTF-8') .'\\'; | ||
241 | $content .= mb_convert_case($link->getUrl(), MB_CASE_LOWER, 'UTF-8') .'\\'; | ||
242 | $content .= mb_convert_case($link->getTagsString(), MB_CASE_LOWER, 'UTF-8') .'\\'; | ||
243 | 239 | ||
244 | // Be optimistic | 240 | // Be optimistic |
245 | $found = true; | 241 | $found = true; |
242 | $foundPositions = []; | ||
246 | 243 | ||
247 | // First, we look for exact term search | 244 | // First, we look for exact term search |
248 | for ($i = 0; $i < count($exactSearch) && $found; $i++) { | 245 | // Then iterate over keywords, if keyword is not found, |
249 | $found = strpos($content, $exactSearch[$i]) !== false; | ||
250 | } | ||
251 | |||
252 | // Iterate over keywords, if keyword is not found, | ||
253 | // no need to check for the others. We want all or nothing. | 246 | // no need to check for the others. We want all or nothing. |
254 | for ($i = 0; $i < count($andSearch) && $found; $i++) { | 247 | foreach ([$exactSearch, $andSearch] as $search) { |
255 | $found = strpos($content, $andSearch[$i]) !== false; | 248 | for ($i = 0; $i < count($search) && $found !== false; $i++) { |
249 | $found = mb_strpos($content, $search[$i]); | ||
250 | if ($found === false) { | ||
251 | break; | ||
252 | } | ||
253 | |||
254 | $foundPositions[] = ['start' => $found, 'end' => $found + mb_strlen($search[$i])]; | ||
255 | } | ||
256 | } | 256 | } |
257 | 257 | ||
258 | // Exclude terms. | 258 | // Exclude terms. |
259 | for ($i = 0; $i < count($excludeSearch) && $found; $i++) { | 259 | for ($i = 0; $i < count($excludeSearch) && $found !== false; $i++) { |
260 | $found = strpos($content, $excludeSearch[$i]) === false; | 260 | $found = strpos($content, $excludeSearch[$i]) === false; |
261 | } | 261 | } |
262 | 262 | ||
263 | if ($found) { | 263 | if ($found !== false) { |
264 | $link->addAdditionalContentEntry( | ||
265 | 'search_highlight', | ||
266 | $this->postProcessFoundPositions($lengths, $foundPositions) | ||
267 | ); | ||
268 | |||
264 | $filtered[$id] = $link; | 269 | $filtered[$id] = $link; |
265 | } | 270 | } |
266 | } | 271 | } |
@@ -477,4 +482,74 @@ class BookmarkFilter | |||
477 | 482 | ||
478 | return preg_split('/\s+/', $tagsOut, -1, PREG_SPLIT_NO_EMPTY); | 483 | return preg_split('/\s+/', $tagsOut, -1, PREG_SPLIT_NO_EMPTY); |
479 | } | 484 | } |
485 | |||
486 | /** | ||
487 | * This method finalize the content of the foundPositions array, | ||
488 | * by associated all search results to their associated bookmark field, | ||
489 | * making sure that there is no overlapping results, etc. | ||
490 | * | ||
491 | * @param array $fieldLengths Start and end positions of every bookmark fields in the aggregated bookmark content. | ||
492 | * @param array $foundPositions Positions where the search results were found in the aggregated content. | ||
493 | * | ||
494 | * @return array Updated $foundPositions, by bookmark field. | ||
495 | */ | ||
496 | protected function postProcessFoundPositions(array $fieldLengths, array $foundPositions): array | ||
497 | { | ||
498 | // Sort results by starting position ASC. | ||
499 | usort($foundPositions, function (array $entryA, array $entryB): int { | ||
500 | return $entryA['start'] > $entryB['start'] ? 1 : -1; | ||
501 | }); | ||
502 | |||
503 | $out = []; | ||
504 | $currentMax = -1; | ||
505 | foreach ($foundPositions as $foundPosition) { | ||
506 | // we do not allow overlapping highlights | ||
507 | if ($foundPosition['start'] < $currentMax) { | ||
508 | continue; | ||
509 | } | ||
510 | |||
511 | $currentMax = $foundPosition['end']; | ||
512 | foreach ($fieldLengths as $part => $length) { | ||
513 | if ($foundPosition['start'] < $length['start'] || $foundPosition['start'] > $length['end']) { | ||
514 | continue; | ||
515 | } | ||
516 | |||
517 | $out[$part][] = [ | ||
518 | 'start' => $foundPosition['start'] - $length['start'], | ||
519 | 'end' => $foundPosition['end'] - $length['start'], | ||
520 | ]; | ||
521 | break; | ||
522 | } | ||
523 | } | ||
524 | |||
525 | return $out; | ||
526 | } | ||
527 | |||
528 | /** | ||
529 | * Concatenate link fields to search across fields. Adds a '\' separator for exact search terms. | ||
530 | * Also populate $length array with starting and ending positions of every bookmark field | ||
531 | * inside concatenated content. | ||
532 | * | ||
533 | * @param Bookmark $link | ||
534 | * @param array $lengths (by reference) | ||
535 | * | ||
536 | * @return string Lowercase concatenated fields content. | ||
537 | */ | ||
538 | protected function buildFullTextSearchableLink(Bookmark $link, array &$lengths): string | ||
539 | { | ||
540 | $content = mb_convert_case($link->getTitle(), MB_CASE_LOWER, 'UTF-8') .'\\'; | ||
541 | $content .= mb_convert_case($link->getDescription(), MB_CASE_LOWER, 'UTF-8') .'\\'; | ||
542 | $content .= mb_convert_case($link->getUrl(), MB_CASE_LOWER, 'UTF-8') .'\\'; | ||
543 | $content .= mb_convert_case($link->getTagsString(), MB_CASE_LOWER, 'UTF-8') .'\\'; | ||
544 | |||
545 | $lengths['title'] = ['start' => 0, 'end' => mb_strlen($link->getTitle())]; | ||
546 | $nextField = $lengths['title']['end'] + 1; | ||
547 | $lengths['description'] = ['start' => $nextField, 'end' => $nextField + mb_strlen($link->getDescription())]; | ||
548 | $nextField = $lengths['description']['end'] + 1; | ||
549 | $lengths['url'] = ['start' => $nextField, 'end' => $nextField + mb_strlen($link->getUrl())]; | ||
550 | $nextField = $lengths['url']['end'] + 1; | ||
551 | $lengths['tags'] = ['start' => $nextField, 'end' => $nextField + mb_strlen($link->getTagsString())]; | ||
552 | |||
553 | return $content; | ||
554 | } | ||
480 | } | 555 | } |
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 | */ |
13 | class BookmarkDefaultFormatter extends BookmarkFormatter | 13 | class 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 | } |
diff --git a/application/formatter/BookmarkFormatter.php b/application/formatter/BookmarkFormatter.php index 0042dafe..e1b7f705 100644 --- a/application/formatter/BookmarkFormatter.php +++ b/application/formatter/BookmarkFormatter.php | |||
@@ -2,7 +2,7 @@ | |||
2 | 2 | ||
3 | namespace Shaarli\Formatter; | 3 | namespace Shaarli\Formatter; |
4 | 4 | ||
5 | use DateTime; | 5 | use DateTimeInterface; |
6 | use Shaarli\Bookmark\Bookmark; | 6 | use Shaarli\Bookmark\Bookmark; |
7 | use Shaarli\Config\ConfigManager; | 7 | use Shaarli\Config\ConfigManager; |
8 | 8 | ||
@@ -11,6 +11,29 @@ use Shaarli\Config\ConfigManager; | |||
11 | * | 11 | * |
12 | * Abstract class processing all bookmark attributes through methods designed to be overridden. | 12 | * Abstract class processing all bookmark attributes through methods designed to be overridden. |
13 | * | 13 | * |
14 | * List of available formatted fields: | ||
15 | * - id ID | ||
16 | * - shorturl Unique identifier, used in permalinks | ||
17 | * - url URL, can be altered in some way, e.g. passing through an HTTP reverse proxy | ||
18 | * - real_url (legacy) same as `url` | ||
19 | * - url_html URL to be displayed in HTML content (it can contain HTML tags) | ||
20 | * - title Title | ||
21 | * - title_html Title to be displayed in HTML content (it can contain HTML tags) | ||
22 | * - description Description content. It most likely contains HTML tags | ||
23 | * - thumbnail Thumbnail: path to local cache file, false if there is none, null if hasn't been retrieved | ||
24 | * - taglist List of tags (array) | ||
25 | * - taglist_urlencoded List of tags (array) URL encoded: it must be used to create a link to a URL containing a tag | ||
26 | * - taglist_html List of tags (array) to be displayed in HTML content (it can contain HTML tags) | ||
27 | * - tags Tags separated by a single whitespace | ||
28 | * - tags_urlencoded Tags separated by a single whitespace, URL encoded: must be used to create a link | ||
29 | * - sticky Is sticky (bool) | ||
30 | * - private Is private (bool) | ||
31 | * - class Additional CSS class | ||
32 | * - created Creation DateTime | ||
33 | * - updated Last edit DateTime | ||
34 | * - timestamp Creation timestamp | ||
35 | * - updated_timestamp Last edit timestamp | ||
36 | * | ||
14 | * @package Shaarli\Formatter | 37 | * @package Shaarli\Formatter |
15 | */ | 38 | */ |
16 | abstract class BookmarkFormatter | 39 | abstract class BookmarkFormatter |
@@ -55,13 +78,16 @@ abstract class BookmarkFormatter | |||
55 | $out['shorturl'] = $this->formatShortUrl($bookmark); | 78 | $out['shorturl'] = $this->formatShortUrl($bookmark); |
56 | $out['url'] = $this->formatUrl($bookmark); | 79 | $out['url'] = $this->formatUrl($bookmark); |
57 | $out['real_url'] = $this->formatRealUrl($bookmark); | 80 | $out['real_url'] = $this->formatRealUrl($bookmark); |
81 | $out['url_html'] = $this->formatUrlHtml($bookmark); | ||
58 | $out['title'] = $this->formatTitle($bookmark); | 82 | $out['title'] = $this->formatTitle($bookmark); |
83 | $out['title_html'] = $this->formatTitleHtml($bookmark); | ||
59 | $out['description'] = $this->formatDescription($bookmark); | 84 | $out['description'] = $this->formatDescription($bookmark); |
60 | $out['thumbnail'] = $this->formatThumbnail($bookmark); | 85 | $out['thumbnail'] = $this->formatThumbnail($bookmark); |
61 | $out['urlencoded_taglist'] = $this->formatUrlEncodedTagList($bookmark); | ||
62 | $out['taglist'] = $this->formatTagList($bookmark); | 86 | $out['taglist'] = $this->formatTagList($bookmark); |
63 | $out['urlencoded_tags'] = $this->formatUrlEncodedTagString($bookmark); | 87 | $out['taglist_urlencoded'] = $this->formatTagListUrlEncoded($bookmark); |
88 | $out['taglist_html'] = $this->formatTagListHtml($bookmark); | ||
64 | $out['tags'] = $this->formatTagString($bookmark); | 89 | $out['tags'] = $this->formatTagString($bookmark); |
90 | $out['tags_urlencoded'] = $this->formatTagStringUrlEncoded($bookmark); | ||
65 | $out['sticky'] = $bookmark->isSticky(); | 91 | $out['sticky'] = $bookmark->isSticky(); |
66 | $out['private'] = $bookmark->isPrivate(); | 92 | $out['private'] = $bookmark->isPrivate(); |
67 | $out['class'] = $this->formatClass($bookmark); | 93 | $out['class'] = $this->formatClass($bookmark); |
@@ -69,6 +95,7 @@ abstract class BookmarkFormatter | |||
69 | $out['updated'] = $this->formatUpdated($bookmark); | 95 | $out['updated'] = $this->formatUpdated($bookmark); |
70 | $out['timestamp'] = $this->formatCreatedTimestamp($bookmark); | 96 | $out['timestamp'] = $this->formatCreatedTimestamp($bookmark); |
71 | $out['updated_timestamp'] = $this->formatUpdatedTimestamp($bookmark); | 97 | $out['updated_timestamp'] = $this->formatUpdatedTimestamp($bookmark); |
98 | |||
72 | return $out; | 99 | return $out; |
73 | } | 100 | } |
74 | 101 | ||
@@ -136,6 +163,18 @@ abstract class BookmarkFormatter | |||
136 | } | 163 | } |
137 | 164 | ||
138 | /** | 165 | /** |
166 | * Format Url Html: to be displayed in HTML content, it can contains HTML tags. | ||
167 | * | ||
168 | * @param Bookmark $bookmark instance | ||
169 | * | ||
170 | * @return string formatted Url HTML | ||
171 | */ | ||
172 | protected function formatUrlHtml($bookmark) | ||
173 | { | ||
174 | return $this->formatUrl($bookmark); | ||
175 | } | ||
176 | |||
177 | /** | ||
139 | * Format Title | 178 | * Format Title |
140 | * | 179 | * |
141 | * @param Bookmark $bookmark instance | 180 | * @param Bookmark $bookmark instance |
@@ -148,6 +187,18 @@ abstract class BookmarkFormatter | |||
148 | } | 187 | } |
149 | 188 | ||
150 | /** | 189 | /** |
190 | * Format Title HTML: to be displayed in HTML content, it can contains HTML tags. | ||
191 | * | ||
192 | * @param Bookmark $bookmark instance | ||
193 | * | ||
194 | * @return string formatted Title | ||
195 | */ | ||
196 | protected function formatTitleHtml($bookmark) | ||
197 | { | ||
198 | return $bookmark->getTitle(); | ||
199 | } | ||
200 | |||
201 | /** | ||
151 | * Format Description | 202 | * Format Description |
152 | * | 203 | * |
153 | * @param Bookmark $bookmark instance | 204 | * @param Bookmark $bookmark instance |
@@ -190,12 +241,24 @@ abstract class BookmarkFormatter | |||
190 | * | 241 | * |
191 | * @return array formatted Tags | 242 | * @return array formatted Tags |
192 | */ | 243 | */ |
193 | protected function formatUrlEncodedTagList($bookmark) | 244 | protected function formatTagListUrlEncoded($bookmark) |
194 | { | 245 | { |
195 | return array_map('urlencode', $this->filterTagList($bookmark->getTags())); | 246 | return array_map('urlencode', $this->filterTagList($bookmark->getTags())); |
196 | } | 247 | } |
197 | 248 | ||
198 | /** | 249 | /** |
250 | * Format Tags HTML: to be displayed in HTML content, it can contains HTML tags. | ||
251 | * | ||
252 | * @param Bookmark $bookmark instance | ||
253 | * | ||
254 | * @return array formatted Tags | ||
255 | */ | ||
256 | protected function formatTagListHtml($bookmark) | ||
257 | { | ||
258 | return $this->formatTagList($bookmark); | ||
259 | } | ||
260 | |||
261 | /** | ||
199 | * Format TagString | 262 | * Format TagString |
200 | * | 263 | * |
201 | * @param Bookmark $bookmark instance | 264 | * @param Bookmark $bookmark instance |
@@ -214,9 +277,9 @@ abstract class BookmarkFormatter | |||
214 | * | 277 | * |
215 | * @return string formatted TagString | 278 | * @return string formatted TagString |
216 | */ | 279 | */ |
217 | protected function formatUrlEncodedTagString($bookmark) | 280 | protected function formatTagStringUrlEncoded($bookmark) |
218 | { | 281 | { |
219 | return implode(' ', $this->formatUrlEncodedTagList($bookmark)); | 282 | return implode(' ', $this->formatTagListUrlEncoded($bookmark)); |
220 | } | 283 | } |
221 | 284 | ||
222 | /** | 285 | /** |
@@ -237,7 +300,7 @@ abstract class BookmarkFormatter | |||
237 | * | 300 | * |
238 | * @param Bookmark $bookmark instance | 301 | * @param Bookmark $bookmark instance |
239 | * | 302 | * |
240 | * @return DateTime instance | 303 | * @return DateTimeInterface instance |
241 | */ | 304 | */ |
242 | protected function formatCreated(Bookmark $bookmark) | 305 | protected function formatCreated(Bookmark $bookmark) |
243 | { | 306 | { |
@@ -249,7 +312,7 @@ abstract class BookmarkFormatter | |||
249 | * | 312 | * |
250 | * @param Bookmark $bookmark instance | 313 | * @param Bookmark $bookmark instance |
251 | * | 314 | * |
252 | * @return DateTime instance | 315 | * @return DateTimeInterface instance |
253 | */ | 316 | */ |
254 | protected function formatUpdated(Bookmark $bookmark) | 317 | protected function formatUpdated(Bookmark $bookmark) |
255 | { | 318 | { |
diff --git a/application/formatter/BookmarkMarkdownFormatter.php b/application/formatter/BookmarkMarkdownFormatter.php index 5d244d4c..f7714be9 100644 --- a/application/formatter/BookmarkMarkdownFormatter.php +++ b/application/formatter/BookmarkMarkdownFormatter.php | |||
@@ -56,7 +56,10 @@ class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter | |||
56 | return parent::formatDescription($bookmark); | 56 | return parent::formatDescription($bookmark); |
57 | } | 57 | } |
58 | 58 | ||
59 | $processedDescription = $bookmark->getDescription(); | 59 | $processedDescription = $this->tokenizeSearchHighlightField( |
60 | $bookmark->getDescription() ?? '', | ||
61 | $bookmark->getAdditionalContentEntry('search_highlight')['description'] ?? [] | ||
62 | ); | ||
60 | $processedDescription = $this->filterProtocols($processedDescription); | 63 | $processedDescription = $this->filterProtocols($processedDescription); |
61 | $processedDescription = $this->formatHashTags($processedDescription); | 64 | $processedDescription = $this->formatHashTags($processedDescription); |
62 | $processedDescription = $this->reverseEscapedHtml($processedDescription); | 65 | $processedDescription = $this->reverseEscapedHtml($processedDescription); |
@@ -65,6 +68,7 @@ class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter | |||
65 | ->setBreaksEnabled(true) | 68 | ->setBreaksEnabled(true) |
66 | ->text($processedDescription); | 69 | ->text($processedDescription); |
67 | $processedDescription = $this->sanitizeHtml($processedDescription); | 70 | $processedDescription = $this->sanitizeHtml($processedDescription); |
71 | $processedDescription = $this->replaceTokens($processedDescription); | ||
68 | 72 | ||
69 | if (!empty($processedDescription)) { | 73 | if (!empty($processedDescription)) { |
70 | $processedDescription = '<div class="markdown">'. $processedDescription . '</div>'; | 74 | $processedDescription = '<div class="markdown">'. $processedDescription . '</div>'; |
diff --git a/assets/default/scss/shaarli.scss b/assets/default/scss/shaarli.scss index a528adb0..2f49bbd2 100644 --- a/assets/default/scss/shaarli.scss +++ b/assets/default/scss/shaarli.scss | |||
@@ -671,6 +671,10 @@ body, | |||
671 | content: ''; | 671 | content: ''; |
672 | } | 672 | } |
673 | } | 673 | } |
674 | |||
675 | .search-highlight { | ||
676 | background-color: yellow; | ||
677 | } | ||
674 | } | 678 | } |
675 | 679 | ||
676 | .linklist-item-buttons { | 680 | .linklist-item-buttons { |
diff --git a/tests/api/controllers/links/GetLinksTest.php b/tests/api/controllers/links/GetLinksTest.php index 0f5073b4..b1c46ee2 100644 --- a/tests/api/controllers/links/GetLinksTest.php +++ b/tests/api/controllers/links/GetLinksTest.php | |||
@@ -398,7 +398,7 @@ class GetLinksTest extends \Shaarli\TestCase | |||
398 | $response = $this->controller->getLinks($request, new Response()); | 398 | $response = $this->controller->getLinks($request, new Response()); |
399 | $this->assertEquals(200, $response->getStatusCode()); | 399 | $this->assertEquals(200, $response->getStatusCode()); |
400 | $data = json_decode((string) $response->getBody(), true); | 400 | $data = json_decode((string) $response->getBody(), true); |
401 | $this->assertEquals(4, count($data)); | 401 | $this->assertEquals(5, count($data)); |
402 | $this->assertEquals(6, $data[0]['id']); | 402 | $this->assertEquals(6, $data[0]['id']); |
403 | 403 | ||
404 | // wildcard: placeholder at the middle | 404 | // wildcard: placeholder at the middle |
diff --git a/tests/bookmark/BookmarkFileServiceTest.php b/tests/bookmark/BookmarkFileServiceTest.php index 42485c99..daafd250 100644 --- a/tests/bookmark/BookmarkFileServiceTest.php +++ b/tests/bookmark/BookmarkFileServiceTest.php | |||
@@ -748,6 +748,10 @@ class BookmarkFileServiceTest extends TestCase | |||
748 | // They need to be grouped with the first case found - order by date DESC: `sTuff`. | 748 | // They need to be grouped with the first case found - order by date DESC: `sTuff`. |
749 | 'sTuff' => 2, | 749 | 'sTuff' => 2, |
750 | 'ut' => 1, | 750 | 'ut' => 1, |
751 | 'assurance' => 1, | ||
752 | 'coding-style' => 1, | ||
753 | 'quality' => 1, | ||
754 | 'standards' => 1, | ||
751 | ], | 755 | ], |
752 | $this->publicLinkDB->bookmarksCountPerTag() | 756 | $this->publicLinkDB->bookmarksCountPerTag() |
753 | ); | 757 | ); |
@@ -776,6 +780,10 @@ class BookmarkFileServiceTest extends TestCase | |||
776 | 'tag3' => 1, | 780 | 'tag3' => 1, |
777 | 'tag4' => 1, | 781 | 'tag4' => 1, |
778 | 'ut' => 1, | 782 | 'ut' => 1, |
783 | 'assurance' => 1, | ||
784 | 'coding-style' => 1, | ||
785 | 'quality' => 1, | ||
786 | 'standards' => 1, | ||
779 | ], | 787 | ], |
780 | $this->privateLinkDB->bookmarksCountPerTag() | 788 | $this->privateLinkDB->bookmarksCountPerTag() |
781 | ); | 789 | ); |
@@ -918,6 +926,10 @@ class BookmarkFileServiceTest extends TestCase | |||
918 | 'tag4' => 1, | 926 | 'tag4' => 1, |
919 | 'ut' => 1, | 927 | 'ut' => 1, |
920 | 'w3c' => 1, | 928 | 'w3c' => 1, |
929 | 'assurance' => 1, | ||
930 | 'coding-style' => 1, | ||
931 | 'quality' => 1, | ||
932 | 'standards' => 1, | ||
921 | ]; | 933 | ]; |
922 | $tags = $this->privateLinkDB->bookmarksCountPerTag(); | 934 | $tags = $this->privateLinkDB->bookmarksCountPerTag(); |
923 | 935 | ||
@@ -1016,6 +1028,10 @@ class BookmarkFileServiceTest extends TestCase | |||
1016 | 'stallman' => 1, | 1028 | 'stallman' => 1, |
1017 | 'ut' => 1, | 1029 | 'ut' => 1, |
1018 | 'w3c' => 1, | 1030 | 'w3c' => 1, |
1031 | 'assurance' => 1, | ||
1032 | 'coding-style' => 1, | ||
1033 | 'quality' => 1, | ||
1034 | 'standards' => 1, | ||
1019 | ]; | 1035 | ]; |
1020 | $bookmark = new Bookmark(); | 1036 | $bookmark = new Bookmark(); |
1021 | $bookmark->setTags(['newTagToCount', BookmarkMarkdownFormatter::NO_MD_TAG]); | 1037 | $bookmark->setTags(['newTagToCount', BookmarkMarkdownFormatter::NO_MD_TAG]); |
diff --git a/tests/bookmark/BookmarkFilterTest.php b/tests/bookmark/BookmarkFilterTest.php index 644abbc8..574d8e3f 100644 --- a/tests/bookmark/BookmarkFilterTest.php +++ b/tests/bookmark/BookmarkFilterTest.php | |||
@@ -2,7 +2,6 @@ | |||
2 | 2 | ||
3 | namespace Shaarli\Bookmark; | 3 | namespace Shaarli\Bookmark; |
4 | 4 | ||
5 | use Exception; | ||
6 | use malkusch\lock\mutex\NoMutex; | 5 | use malkusch\lock\mutex\NoMutex; |
7 | use ReferenceLinkDB; | 6 | use ReferenceLinkDB; |
8 | use Shaarli\Config\ConfigManager; | 7 | use Shaarli\Config\ConfigManager; |
@@ -525,4 +524,43 @@ class BookmarkFilterTest extends TestCase | |||
525 | )) | 524 | )) |
526 | ); | 525 | ); |
527 | } | 526 | } |
527 | |||
528 | /** | ||
529 | * Test search result highlights in every field of bookmark reference #9. | ||
530 | */ | ||
531 | public function testFullTextSearchHighlight(): void | ||
532 | { | ||
533 | $bookmarks = self::$linkFilter->filter( | ||
534 | BookmarkFilter::$FILTER_TEXT, | ||
535 | '"psr-2" coding guide http fig "psr-2/" "This guide" basic standard. coding-style quality assurance' | ||
536 | ); | ||
537 | |||
538 | static::assertCount(1, $bookmarks); | ||
539 | static::assertArrayHasKey(9, $bookmarks); | ||
540 | |||
541 | $bookmark = $bookmarks[9]; | ||
542 | $expectedHighlights = [ | ||
543 | 'title' => [ | ||
544 | ['start' => 0, 'end' => 5], // "psr-2" | ||
545 | ['start' => 7, 'end' => 13], // coding | ||
546 | ['start' => 20, 'end' => 25], // guide | ||
547 | ], | ||
548 | 'description' => [ | ||
549 | ['start' => 0, 'end' => 10], // "This guide" | ||
550 | ['start' => 45, 'end' => 50], // basic | ||
551 | ['start' => 58, 'end' => 67], // standard. | ||
552 | ], | ||
553 | 'url' => [ | ||
554 | ['start' => 0, 'end' => 4], // http | ||
555 | ['start' => 15, 'end' => 18], // fig | ||
556 | ['start' => 27, 'end' => 33], // "psr-2/" | ||
557 | ], | ||
558 | 'tags' => [ | ||
559 | ['start' => 0, 'end' => 12], // coding-style | ||
560 | ['start' => 23, 'end' => 30], // quality | ||
561 | ['start' => 31, 'end' => 40], // assurance | ||
562 | ], | ||
563 | ]; | ||
564 | static::assertSame($expectedHighlights, $bookmark->getAdditionalContentEntry('search_highlight')); | ||
565 | } | ||
528 | } | 566 | } |
diff --git a/tests/formatter/BookmarkDefaultFormatterTest.php b/tests/formatter/BookmarkDefaultFormatterTest.php index 9534436e..3fc6f8dc 100644 --- a/tests/formatter/BookmarkDefaultFormatterTest.php +++ b/tests/formatter/BookmarkDefaultFormatterTest.php | |||
@@ -174,4 +174,119 @@ class BookmarkDefaultFormatterTest extends TestCase | |||
174 | $this->assertSame($tags, $link['taglist']); | 174 | $this->assertSame($tags, $link['taglist']); |
175 | $this->assertSame(implode(' ', $tags), $link['tags']); | 175 | $this->assertSame(implode(' ', $tags), $link['tags']); |
176 | } | 176 | } |
177 | |||
178 | /** | ||
179 | * Test formatTitleHtml with search result highlight. | ||
180 | */ | ||
181 | public function testFormatTitleHtmlWithSearchHighlight(): void | ||
182 | { | ||
183 | $this->formatter = new BookmarkDefaultFormatter($this->conf, false); | ||
184 | |||
185 | $bookmark = new Bookmark(); | ||
186 | $bookmark->setTitle('PSR-2: Coding Style Guide'); | ||
187 | $bookmark->addAdditionalContentEntry( | ||
188 | 'search_highlight', | ||
189 | ['title' => [ | ||
190 | ['start' => 0, 'end' => 5], // "psr-2" | ||
191 | ['start' => 7, 'end' => 13], // coding | ||
192 | ['start' => 20, 'end' => 25], // guide | ||
193 | ]] | ||
194 | ); | ||
195 | |||
196 | $link = $this->formatter->format($bookmark); | ||
197 | |||
198 | $this->assertSame( | ||
199 | '<span class="search-highlight">PSR-2</span>: ' . | ||
200 | '<span class="search-highlight">Coding</span> Style ' . | ||
201 | '<span class="search-highlight">Guide</span>', | ||
202 | $link['title_html'] | ||
203 | ); | ||
204 | } | ||
205 | |||
206 | /** | ||
207 | * Test formatDescription with search result highlight. | ||
208 | */ | ||
209 | public function testFormatDescriptionWithSearchHighlight(): void | ||
210 | { | ||
211 | $this->formatter = new BookmarkDefaultFormatter($this->conf, false); | ||
212 | |||
213 | $bookmark = new Bookmark(); | ||
214 | $bookmark->setDescription('This guide extends and expands on PSR-1, the basic coding standard.'); | ||
215 | $bookmark->addAdditionalContentEntry( | ||
216 | 'search_highlight', | ||
217 | ['description' => [ | ||
218 | ['start' => 0, 'end' => 10], // "This guide" | ||
219 | ['start' => 45, 'end' => 50], // basic | ||
220 | ['start' => 58, 'end' => 67], // standard. | ||
221 | ]] | ||
222 | ); | ||
223 | |||
224 | $link = $this->formatter->format($bookmark); | ||
225 | |||
226 | $this->assertSame( | ||
227 | '<span class="search-highlight">This guide</span> extends and expands on PSR-1, the ' . | ||
228 | '<span class="search-highlight">basic</span> coding ' . | ||
229 | '<span class="search-highlight">standard.</span>', | ||
230 | $link['description'] | ||
231 | ); | ||
232 | } | ||
233 | |||
234 | /** | ||
235 | * Test formatUrlHtml with search result highlight. | ||
236 | */ | ||
237 | public function testFormatUrlHtmlWithSearchHighlight(): void | ||
238 | { | ||
239 | $this->formatter = new BookmarkDefaultFormatter($this->conf, false); | ||
240 | |||
241 | $bookmark = new Bookmark(); | ||
242 | $bookmark->setUrl('http://www.php-fig.org/psr/psr-2/'); | ||
243 | $bookmark->addAdditionalContentEntry( | ||
244 | 'search_highlight', | ||
245 | ['url' => [ | ||
246 | ['start' => 0, 'end' => 4], // http | ||
247 | ['start' => 15, 'end' => 18], // fig | ||
248 | ['start' => 27, 'end' => 33], // "psr-2/" | ||
249 | ]] | ||
250 | ); | ||
251 | |||
252 | $link = $this->formatter->format($bookmark); | ||
253 | |||
254 | $this->assertSame( | ||
255 | '<span class="search-highlight">http</span>://www.php-' . | ||
256 | '<span class="search-highlight">fig</span>.org/psr/' . | ||
257 | '<span class="search-highlight">psr-2/</span>', | ||
258 | $link['url_html'] | ||
259 | ); | ||
260 | } | ||
261 | |||
262 | /** | ||
263 | * Test formatTagListHtml with search result highlight. | ||
264 | */ | ||
265 | public function testFormatTagListHtmlWithSearchHighlight(): void | ||
266 | { | ||
267 | $this->formatter = new BookmarkDefaultFormatter($this->conf, false); | ||
268 | |||
269 | $bookmark = new Bookmark(); | ||
270 | $bookmark->setTagsString('coding-style standards quality assurance'); | ||
271 | $bookmark->addAdditionalContentEntry( | ||
272 | 'search_highlight', | ||
273 | ['tags' => [ | ||
274 | ['start' => 0, 'end' => 12], // coding-style | ||
275 | ['start' => 23, 'end' => 30], // quality | ||
276 | ['start' => 31, 'end' => 40], // assurance | ||
277 | ],] | ||
278 | ); | ||
279 | |||
280 | $link = $this->formatter->format($bookmark); | ||
281 | |||
282 | $this->assertSame( | ||
283 | [ | ||
284 | '<span class="search-highlight">coding-style</span>', | ||
285 | 'standards', | ||
286 | '<span class="search-highlight">quality</span>', | ||
287 | '<span class="search-highlight">assurance</span>', | ||
288 | ], | ||
289 | $link['taglist_html'] | ||
290 | ); | ||
291 | } | ||
177 | } | 292 | } |
diff --git a/tests/legacy/LegacyLinkDBTest.php b/tests/legacy/LegacyLinkDBTest.php index df2cad62..5c3fd425 100644 --- a/tests/legacy/LegacyLinkDBTest.php +++ b/tests/legacy/LegacyLinkDBTest.php | |||
@@ -296,6 +296,10 @@ class LegacyLinkDBTest extends \Shaarli\TestCase | |||
296 | // They need to be grouped with the first case found - order by date DESC: `sTuff`. | 296 | // They need to be grouped with the first case found - order by date DESC: `sTuff`. |
297 | 'sTuff' => 2, | 297 | 'sTuff' => 2, |
298 | 'ut' => 1, | 298 | 'ut' => 1, |
299 | 'assurance' => 1, | ||
300 | 'coding-style' => 1, | ||
301 | 'quality' => 1, | ||
302 | 'standards' => 1, | ||
299 | ), | 303 | ), |
300 | self::$publicLinkDB->linksCountPerTag() | 304 | self::$publicLinkDB->linksCountPerTag() |
301 | ); | 305 | ); |
@@ -324,6 +328,10 @@ class LegacyLinkDBTest extends \Shaarli\TestCase | |||
324 | 'tag3' => 1, | 328 | 'tag3' => 1, |
325 | 'tag4' => 1, | 329 | 'tag4' => 1, |
326 | 'ut' => 1, | 330 | 'ut' => 1, |
331 | 'assurance' => 1, | ||
332 | 'coding-style' => 1, | ||
333 | 'quality' => 1, | ||
334 | 'standards' => 1, | ||
327 | ), | 335 | ), |
328 | self::$privateLinkDB->linksCountPerTag() | 336 | self::$privateLinkDB->linksCountPerTag() |
329 | ); | 337 | ); |
@@ -544,6 +552,10 @@ class LegacyLinkDBTest extends \Shaarli\TestCase | |||
544 | 'tag4' => 1, | 552 | 'tag4' => 1, |
545 | 'ut' => 1, | 553 | 'ut' => 1, |
546 | 'w3c' => 1, | 554 | 'w3c' => 1, |
555 | 'assurance' => 1, | ||
556 | 'coding-style' => 1, | ||
557 | 'quality' => 1, | ||
558 | 'standards' => 1, | ||
547 | ]; | 559 | ]; |
548 | $tags = self::$privateLinkDB->linksCountPerTag(); | 560 | $tags = self::$privateLinkDB->linksCountPerTag(); |
549 | 561 | ||
diff --git a/tests/utils/ReferenceLinkDB.php b/tests/utils/ReferenceLinkDB.php index fc3cb109..1f53dc3c 100644 --- a/tests/utils/ReferenceLinkDB.php +++ b/tests/utils/ReferenceLinkDB.php | |||
@@ -82,7 +82,7 @@ class ReferenceLinkDB | |||
82 | 'This guide extends and expands on PSR-1, the basic coding standard.', | 82 | 'This guide extends and expands on PSR-1, the basic coding standard.', |
83 | 0, | 83 | 0, |
84 | DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20121206_152312'), | 84 | DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20121206_152312'), |
85 | '' | 85 | 'coding-style standards quality assurance' |
86 | ); | 86 | ); |
87 | 87 | ||
88 | $this->addLink( | 88 | $this->addLink( |
diff --git a/tpl/default/linklist.html b/tpl/default/linklist.html index e1fb54dd..beab0eac 100644 --- a/tpl/default/linklist.html +++ b/tpl/default/linklist.html | |||
@@ -165,7 +165,7 @@ | |||
165 | <i class="fa fa-sticky-note" aria-hidden="true"></i> | 165 | <i class="fa fa-sticky-note" aria-hidden="true"></i> |
166 | {/if} | 166 | {/if} |
167 | 167 | ||
168 | <span class="linklist-link">{$value.title}</span> | 168 | <span class="linklist-link">{$value.title_html}</span> |
169 | </a> | 169 | </a> |
170 | </h2> | 170 | </h2> |
171 | </div> | 171 | </div> |
@@ -183,7 +183,7 @@ | |||
183 | {$tag_counter=count($value.taglist)} | 183 | {$tag_counter=count($value.taglist)} |
184 | {loop="value.taglist"} | 184 | {loop="value.taglist"} |
185 | <span class="label label-tag" title="{$strAddTag}"> | 185 | <span class="label label-tag" title="{$strAddTag}"> |
186 | <a href="{$base_path}/add-tag/{$value1.urlencoded_taglist.$key2}">{$value}</a> | 186 | <a href="{$base_path}/add-tag/{$value1.taglist_urlencoded.$key2}">{$value1.taglist_html.$key2}</a> |
187 | </span> | 187 | </span> |
188 | {if="$tag_counter - 1 != $counter"}·{/if} | 188 | {if="$tag_counter - 1 != $counter"}·{/if} |
189 | {/loop} | 189 | {/loop} |
@@ -251,7 +251,7 @@ | |||
251 | {ignore}do not add space or line break between these div - Firefox issue{/ignore} | 251 | {ignore}do not add space or line break between these div - Firefox issue{/ignore} |
252 | class="linklist-item-infos-url pure-u-lg-5-12 pure-u-1"> | 252 | class="linklist-item-infos-url pure-u-lg-5-12 pure-u-1"> |
253 | <a href="{$value.real_url}" aria-label="{$value.title}" title="{$value.title}"> | 253 | <a href="{$value.real_url}" aria-label="{$value.title}" title="{$value.title}"> |
254 | <i class="fa fa-link" aria-hidden="true"></i> {$value.url} | 254 | <i class="fa fa-link" aria-hidden="true"></i> {$value.url_html} |
255 | </a> | 255 | </a> |
256 | <div class="linklist-item-buttons pure-u-0 pure-u-lg-visible"> | 256 | <div class="linklist-item-buttons pure-u-0 pure-u-lg-visible"> |
257 | <a href="#" aria-label="{$strFold}" title="{$strFold}" class="fold-button"><i class="fa fa-chevron-up" aria-hidden="true"></i></a> | 257 | <a href="#" aria-label="{$strFold}" title="{$strFold}" class="fold-button"><i class="fa fa-chevron-up" aria-hidden="true"></i></a> |