From 4e3875c0ce7f3b17e3d358dc5ecb1f8bed64546b Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Mon, 12 Oct 2020 11:35:55 +0200 Subject: 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 --- application/formatter/BookmarkDefaultFormatter.php | 132 ++++++++++++++++++++- 1 file changed, 127 insertions(+), 5 deletions(-) (limited to 'application/formatter/BookmarkDefaultFormatter.php') 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; */ class BookmarkDefaultFormatter extends BookmarkFormatter { + const SEARCH_HIGHLIGHT_OPEN = '|@@HIGHLIGHT'; + const SEARCH_HIGHLIGHT_CLOSE = 'HIGHLIGHT@@|'; + /** * @inheritdoc */ - public function formatTitle($bookmark) + protected function formatTitle($bookmark) { return escape($bookmark->getTitle()); } @@ -23,10 +26,28 @@ class BookmarkDefaultFormatter extends BookmarkFormatter /** * @inheritdoc */ - public function formatDescription($bookmark) + protected function formatTitleHtml($bookmark) + { + $title = $this->tokenizeSearchHighlightField( + $bookmark->getTitle() ?? '', + $bookmark->getAdditionalContentEntry('search_highlight')['title'] ?? [] + ); + + return $this->replaceTokens(escape($title)); + } + + /** + * @inheritdoc + */ + protected function formatDescription($bookmark) { $indexUrl = ! empty($this->contextData['index_url']) ? $this->contextData['index_url'] : ''; - return format_description(escape($bookmark->getDescription()), $indexUrl); + $description = $this->tokenizeSearchHighlightField( + $bookmark->getDescription() ?? '', + $bookmark->getAdditionalContentEntry('search_highlight')['description'] ?? [] + ); + + return $this->replaceTokens(format_description(escape($description), $indexUrl)); } /** @@ -40,7 +61,27 @@ class BookmarkDefaultFormatter extends BookmarkFormatter /** * @inheritdoc */ - public function formatTagString($bookmark) + protected function formatTagListHtml($bookmark) + { + if (empty($bookmark->getAdditionalContentEntry('search_highlight')['tags'])) { + return $this->formatTagList($bookmark); + } + + $tags = $this->tokenizeSearchHighlightField( + $bookmark->getTagsString(), + $bookmark->getAdditionalContentEntry('search_highlight')['tags'] + ); + $tags = $this->filterTagList(explode(' ', $tags)); + $tags = escape($tags); + $tags = $this->replaceTokensArray($tags); + + return $tags; + } + + /** + * @inheritdoc + */ + protected function formatTagString($bookmark) { return implode(' ', $this->formatTagList($bookmark)); } @@ -48,7 +89,7 @@ class BookmarkDefaultFormatter extends BookmarkFormatter /** * @inheritdoc */ - public function formatUrl($bookmark) + protected function formatUrl($bookmark) { if ($bookmark->isNote() && isset($this->contextData['index_url'])) { return rtrim($this->contextData['index_url'], '/') . '/' . escape(ltrim($bookmark->getUrl(), '/')); @@ -77,6 +118,19 @@ class BookmarkDefaultFormatter extends BookmarkFormatter return escape($bookmark->getUrl()); } + /** + * @inheritdoc + */ + protected function formatUrlHtml($bookmark) + { + $url = $this->tokenizeSearchHighlightField( + $bookmark->getUrl() ?? '', + $bookmark->getAdditionalContentEntry('search_highlight')['url'] ?? [] + ); + + return $this->replaceTokens(escape($url)); + } + /** * @inheritdoc */ @@ -84,4 +138,72 @@ class BookmarkDefaultFormatter extends BookmarkFormatter { return escape($bookmark->getThumbnail()); } + + /** + * Insert search highlight token in provided field content based on a list of search result positions + * + * @param string $fieldContent + * @param array|null $positions List of of search results with 'start' and 'end' positions. + * + * @return string Updated $fieldContent. + */ + protected function tokenizeSearchHighlightField(string $fieldContent, ?array $positions): string + { + if (empty($positions)) { + return $fieldContent; + } + + $insertedTokens = 0; + $tokenLength = strlen(static::SEARCH_HIGHLIGHT_OPEN); + foreach ($positions as $position) { + $position = [ + 'start' => $position['start'] + ($insertedTokens * $tokenLength), + 'end' => $position['end'] + ($insertedTokens * $tokenLength), + ]; + + $content = mb_substr($fieldContent, 0, $position['start']); + $content .= static::SEARCH_HIGHLIGHT_OPEN; + $content .= mb_substr($fieldContent, $position['start'], $position['end'] - $position['start']); + $content .= static::SEARCH_HIGHLIGHT_CLOSE; + $content .= mb_substr($fieldContent, $position['end']); + + $fieldContent = $content; + + $insertedTokens += 2; + } + + return $fieldContent; + } + + /** + * Replace search highlight tokens with HTML highlighted span. + * + * @param string $fieldContent + * + * @return string updated content. + */ + protected function replaceTokens(string $fieldContent): string + { + return str_replace( + [static::SEARCH_HIGHLIGHT_OPEN, static::SEARCH_HIGHLIGHT_CLOSE], + ['', ''], + $fieldContent + ); + } + + /** + * Apply replaceTokens to an array of content strings. + * + * @param string[] $fieldContents + * + * @return array + */ + protected function replaceTokensArray(array $fieldContents): array + { + foreach ($fieldContents as &$entry) { + $entry = $this->replaceTokens($entry); + } + + return $fieldContents; + } } -- cgit v1.2.3 From 740b32b520e6b1723512c6f9b78cef6575b1725b Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Tue, 3 Nov 2020 12:38:38 +0100 Subject: Default formatter: add a setting to disable auto-linkification + update documentation + single parameter for both URL and hashtags Fixes #1094 --- application/formatter/BookmarkDefaultFormatter.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'application/formatter/BookmarkDefaultFormatter.php') diff --git a/application/formatter/BookmarkDefaultFormatter.php b/application/formatter/BookmarkDefaultFormatter.php index d58a5e39..149a3eb9 100644 --- a/application/formatter/BookmarkDefaultFormatter.php +++ b/application/formatter/BookmarkDefaultFormatter.php @@ -46,8 +46,13 @@ class BookmarkDefaultFormatter extends BookmarkFormatter $bookmark->getDescription() ?? '', $bookmark->getAdditionalContentEntry('search_highlight')['description'] ?? [] ); + $description = format_description( + escape($description), + $indexUrl, + $this->conf->get('formatter_settings.autolink', true) + ); - return $this->replaceTokens(format_description(escape($description), $indexUrl)); + return $this->replaceTokens($description); } /** -- cgit v1.2.3 From b3bd8c3e8d367975980043e772f7cd78b7f96bc6 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Thu, 22 Oct 2020 16:21:03 +0200 Subject: Feature: support any tag separator So it allows to have multiple words tags. Breaking change: commas ',' are no longer a default separator. Fixes #594 --- application/formatter/BookmarkDefaultFormatter.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'application/formatter/BookmarkDefaultFormatter.php') diff --git a/application/formatter/BookmarkDefaultFormatter.php b/application/formatter/BookmarkDefaultFormatter.php index 149a3eb9..51bea0f1 100644 --- a/application/formatter/BookmarkDefaultFormatter.php +++ b/application/formatter/BookmarkDefaultFormatter.php @@ -68,15 +68,16 @@ class BookmarkDefaultFormatter extends BookmarkFormatter */ protected function formatTagListHtml($bookmark) { + $tagsSeparator = $this->conf->get('general.tags_separator', ' '); if (empty($bookmark->getAdditionalContentEntry('search_highlight')['tags'])) { return $this->formatTagList($bookmark); } $tags = $this->tokenizeSearchHighlightField( - $bookmark->getTagsString(), + $bookmark->getTagsString($tagsSeparator), $bookmark->getAdditionalContentEntry('search_highlight')['tags'] ); - $tags = $this->filterTagList(explode(' ', $tags)); + $tags = $this->filterTagList(tags_str2array($tags, $tagsSeparator)); $tags = escape($tags); $tags = $this->replaceTokensArray($tags); @@ -88,7 +89,7 @@ class BookmarkDefaultFormatter extends BookmarkFormatter */ protected function formatTagString($bookmark) { - return implode(' ', $this->formatTagList($bookmark)); + return implode($this->conf->get('general.tags_separator'), $this->formatTagList($bookmark)); } /** -- cgit v1.2.3 From b99e00f7cd5f7e2090f44cd97bfb426db55340c2 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sun, 8 Nov 2020 15:02:45 +0100 Subject: Manually fix remaining PHPCS errors --- application/formatter/BookmarkDefaultFormatter.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'application/formatter/BookmarkDefaultFormatter.php') diff --git a/application/formatter/BookmarkDefaultFormatter.php b/application/formatter/BookmarkDefaultFormatter.php index 51bea0f1..7e0afafc 100644 --- a/application/formatter/BookmarkDefaultFormatter.php +++ b/application/formatter/BookmarkDefaultFormatter.php @@ -12,8 +12,8 @@ namespace Shaarli\Formatter; */ class BookmarkDefaultFormatter extends BookmarkFormatter { - const SEARCH_HIGHLIGHT_OPEN = '|@@HIGHLIGHT'; - const SEARCH_HIGHLIGHT_CLOSE = 'HIGHLIGHT@@|'; + protected const SEARCH_HIGHLIGHT_OPEN = '|@@HIGHLIGHT'; + protected const SEARCH_HIGHLIGHT_CLOSE = 'HIGHLIGHT@@|'; /** * @inheritdoc -- cgit v1.2.3