aboutsummaryrefslogtreecommitdiffhomepage
path: root/application/bookmark
diff options
context:
space:
mode:
authorArthurHoaro <arthur@hoa.ro>2020-10-22 16:21:03 +0200
committerArthurHoaro <arthur@hoa.ro>2020-11-05 17:54:42 +0100
commitb3bd8c3e8d367975980043e772f7cd78b7f96bc6 (patch)
treeec79899ea564c093d8b0578f3e614881a4ea7c3d /application/bookmark
parent48df9f45b8c4b2995c1e04146071628668531b37 (diff)
downloadShaarli-b3bd8c3e8d367975980043e772f7cd78b7f96bc6.tar.gz
Shaarli-b3bd8c3e8d367975980043e772f7cd78b7f96bc6.tar.zst
Shaarli-b3bd8c3e8d367975980043e772f7cd78b7f96bc6.zip
Feature: support any tag separator
So it allows to have multiple words tags. Breaking change: commas ',' are no longer a default separator. Fixes #594
Diffstat (limited to 'application/bookmark')
-rw-r--r--application/bookmark/Bookmark.php39
-rw-r--r--application/bookmark/BookmarkFileService.php2
-rw-r--r--application/bookmark/BookmarkFilter.php47
-rw-r--r--application/bookmark/LinkUtils.php46
4 files changed, 99 insertions, 35 deletions
diff --git a/application/bookmark/Bookmark.php b/application/bookmark/Bookmark.php
index 4810c5e6..8aaeb9d8 100644
--- a/application/bookmark/Bookmark.php
+++ b/application/bookmark/Bookmark.php
@@ -60,11 +60,13 @@ class Bookmark
60 /** 60 /**
61 * 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.
62 * 62 *
63 * @param array $data 63 * @param array $data
64 * @param string $tagsSeparator Tags separator loaded from the config file.
65 * This is a context data, and it should *never* be stored in the Bookmark object.
64 * 66 *
65 * @return $this 67 * @return $this
66 */ 68 */
67 public function fromArray(array $data): Bookmark 69 public function fromArray(array $data, string $tagsSeparator = ' '): Bookmark
68 { 70 {
69 $this->id = $data['id'] ?? null; 71 $this->id = $data['id'] ?? null;
70 $this->shortUrl = $data['shorturl'] ?? null; 72 $this->shortUrl = $data['shorturl'] ?? null;
@@ -77,7 +79,7 @@ class Bookmark
77 if (is_array($data['tags'])) { 79 if (is_array($data['tags'])) {
78 $this->tags = $data['tags']; 80 $this->tags = $data['tags'];
79 } else { 81 } else {
80 $this->tags = preg_split('/\s+/', $data['tags'] ?? '', -1, PREG_SPLIT_NO_EMPTY); 82 $this->tags = tags_str2array($data['tags'] ?? '', $tagsSeparator);
81 } 83 }
82 if (! empty($data['updated'])) { 84 if (! empty($data['updated'])) {
83 $this->updated = $data['updated']; 85 $this->updated = $data['updated'];
@@ -348,7 +350,12 @@ class Bookmark
348 */ 350 */
349 public function setTags(?array $tags): Bookmark 351 public function setTags(?array $tags): Bookmark
350 { 352 {
351 $this->setTagsString(implode(' ', $tags ?? [])); 353 $this->tags = array_map(
354 function (string $tag): string {
355 return $tag[0] === '-' ? substr($tag, 1) : $tag;
356 },
357 tags_filter($tags, ' ')
358 );
352 359
353 return $this; 360 return $this;
354 } 361 }
@@ -420,11 +427,13 @@ class Bookmark
420 } 427 }
421 428
422 /** 429 /**
423 * @return string Bookmark's tags as a string, separated by a space 430 * @param string $separator Tags separator loaded from the config file.
431 *
432 * @return string Bookmark's tags as a string, separated by a separator
424 */ 433 */
425 public function getTagsString(): string 434 public function getTagsString(string $separator = ' '): string
426 { 435 {
427 return implode(' ', $this->getTags()); 436 return tags_array2str($this->getTags(), $separator);
428 } 437 }
429 438
430 /** 439 /**
@@ -444,19 +453,13 @@ class Bookmark
444 * - trailing dash in tags will be removed 453 * - trailing dash in tags will be removed
445 * 454 *
446 * @param string|null $tags 455 * @param string|null $tags
456 * @param string $separator Tags separator loaded from the config file.
447 * 457 *
448 * @return $this 458 * @return $this
449 */ 459 */
450 public function setTagsString(?string $tags): Bookmark 460 public function setTagsString(?string $tags, string $separator = ' '): Bookmark
451 { 461 {
452 // Remove first '-' char in tags. 462 $this->setTags(tags_str2array($tags, $separator));
453 $tags = preg_replace('/(^| )\-/', '$1', $tags ?? '');
454 // Explode all tags separted by spaces or commas
455 $tags = preg_split('/[\s,]+/', $tags);
456 // Remove eventual empty values
457 $tags = array_values(array_filter($tags));
458
459 $this->tags = $tags;
460 463
461 return $this; 464 return $this;
462 } 465 }
@@ -507,7 +510,7 @@ class Bookmark
507 */ 510 */
508 public function renameTag(string $fromTag, string $toTag): void 511 public function renameTag(string $fromTag, string $toTag): void
509 { 512 {
510 if (($pos = array_search($fromTag, $this->tags)) !== false) { 513 if (($pos = array_search($fromTag, $this->tags ?? [])) !== false) {
511 $this->tags[$pos] = trim($toTag); 514 $this->tags[$pos] = trim($toTag);
512 } 515 }
513 } 516 }
@@ -519,7 +522,7 @@ class Bookmark
519 */ 522 */
520 public function deleteTag(string $tag): void 523 public function deleteTag(string $tag): void
521 { 524 {
522 if (($pos = array_search($tag, $this->tags)) !== false) { 525 if (($pos = array_search($tag, $this->tags ?? [])) !== false) {
523 unset($this->tags[$pos]); 526 unset($this->tags[$pos]);
524 $this->tags = array_values($this->tags); 527 $this->tags = array_values($this->tags);
525 } 528 }
diff --git a/application/bookmark/BookmarkFileService.php b/application/bookmark/BookmarkFileService.php
index 3ea98a45..85efeea6 100644
--- a/application/bookmark/BookmarkFileService.php
+++ b/application/bookmark/BookmarkFileService.php
@@ -91,7 +91,7 @@ class BookmarkFileService implements BookmarkServiceInterface
91 } 91 }
92 } 92 }
93 93
94 $this->bookmarkFilter = new BookmarkFilter($this->bookmarks); 94 $this->bookmarkFilter = new BookmarkFilter($this->bookmarks, $this->conf);
95 } 95 }
96 96
97 /** 97 /**
diff --git a/application/bookmark/BookmarkFilter.php b/application/bookmark/BookmarkFilter.php
index c79386ea..5d8733dc 100644
--- a/application/bookmark/BookmarkFilter.php
+++ b/application/bookmark/BookmarkFilter.php
@@ -6,6 +6,7 @@ namespace Shaarli\Bookmark;
6 6
7use Exception; 7use Exception;
8use Shaarli\Bookmark\Exception\BookmarkNotFoundException; 8use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
9use Shaarli\Config\ConfigManager;
9 10
10/** 11/**
11 * Class LinkFilter. 12 * Class LinkFilter.
@@ -58,12 +59,16 @@ class BookmarkFilter
58 */ 59 */
59 private $bookmarks; 60 private $bookmarks;
60 61
62 /** @var ConfigManager */
63 protected $conf;
64
61 /** 65 /**
62 * @param Bookmark[] $bookmarks initialization. 66 * @param Bookmark[] $bookmarks initialization.
63 */ 67 */
64 public function __construct($bookmarks) 68 public function __construct($bookmarks, ConfigManager $conf)
65 { 69 {
66 $this->bookmarks = $bookmarks; 70 $this->bookmarks = $bookmarks;
71 $this->conf = $conf;
67 } 72 }
68 73
69 /** 74 /**
@@ -107,10 +112,14 @@ class BookmarkFilter
107 $filtered = $this->bookmarks; 112 $filtered = $this->bookmarks;
108 } 113 }
109 if (!empty($request[0])) { 114 if (!empty($request[0])) {
110 $filtered = (new BookmarkFilter($filtered))->filterTags($request[0], $casesensitive, $visibility); 115 $filtered = (new BookmarkFilter($filtered, $this->conf))
116 ->filterTags($request[0], $casesensitive, $visibility)
117 ;
111 } 118 }
112 if (!empty($request[1])) { 119 if (!empty($request[1])) {
113 $filtered = (new BookmarkFilter($filtered))->filterFulltext($request[1], $visibility); 120 $filtered = (new BookmarkFilter($filtered, $this->conf))
121 ->filterFulltext($request[1], $visibility)
122 ;
114 } 123 }
115 return $filtered; 124 return $filtered;
116 case self::$FILTER_TEXT: 125 case self::$FILTER_TEXT:
@@ -280,8 +289,9 @@ class BookmarkFilter
280 * 289 *
281 * @return string generated regex fragment 290 * @return string generated regex fragment
282 */ 291 */
283 private static function tag2regex(string $tag): string 292 protected function tag2regex(string $tag): string
284 { 293 {
294 $tagsSeparator = $this->conf->get('general.tags_separator', ' ');
285 $len = strlen($tag); 295 $len = strlen($tag);
286 if (!$len || $tag === "-" || $tag === "*") { 296 if (!$len || $tag === "-" || $tag === "*") {
287 // nothing to search, return empty regex 297 // nothing to search, return empty regex
@@ -295,12 +305,13 @@ class BookmarkFilter
295 $i = 0; // start at first character 305 $i = 0; // start at first character
296 $regex = '(?='; // use positive lookahead 306 $regex = '(?='; // use positive lookahead
297 } 307 }
298 $regex .= '.*(?:^| )'; // before tag may only be a space or the beginning 308 // before tag may only be the separator or the beginning
309 $regex .= '.*(?:^|' . $tagsSeparator . ')';
299 // iterate over string, separating it into placeholder and content 310 // iterate over string, separating it into placeholder and content
300 for (; $i < $len; $i++) { 311 for (; $i < $len; $i++) {
301 if ($tag[$i] === '*') { 312 if ($tag[$i] === '*') {
302 // placeholder found 313 // placeholder found
303 $regex .= '[^ ]*?'; 314 $regex .= '[^' . $tagsSeparator . ']*?';
304 } else { 315 } else {
305 // regular characters 316 // regular characters
306 $offset = strpos($tag, '*', $i); 317 $offset = strpos($tag, '*', $i);
@@ -316,7 +327,8 @@ class BookmarkFilter
316 $i = $offset; 327 $i = $offset;
317 } 328 }
318 } 329 }
319 $regex .= '(?:$| ))'; // after the tag may only be a space or the end 330 // after the tag may only be the separator or the end
331 $regex .= '(?:$|' . $tagsSeparator . '))';
320 return $regex; 332 return $regex;
321 } 333 }
322 334
@@ -334,14 +346,15 @@ class BookmarkFilter
334 */ 346 */
335 public function filterTags($tags, bool $casesensitive = false, string $visibility = 'all') 347 public function filterTags($tags, bool $casesensitive = false, string $visibility = 'all')
336 { 348 {
349 $tagsSeparator = $this->conf->get('general.tags_separator', ' ');
337 // get single tags (we may get passed an array, even though the docs say different) 350 // get single tags (we may get passed an array, even though the docs say different)
338 $inputTags = $tags; 351 $inputTags = $tags;
339 if (!is_array($tags)) { 352 if (!is_array($tags)) {
340 // we got an input string, split tags 353 // we got an input string, split tags
341 $inputTags = preg_split('/(?:\s+)|,/', $inputTags, -1, PREG_SPLIT_NO_EMPTY); 354 $inputTags = tags_str2array($inputTags, $tagsSeparator);
342 } 355 }
343 356
344 if (!count($inputTags)) { 357 if (count($inputTags) === 0) {
345 // no input tags 358 // no input tags
346 return $this->noFilter($visibility); 359 return $this->noFilter($visibility);
347 } 360 }
@@ -358,7 +371,7 @@ class BookmarkFilter
358 } 371 }
359 372
360 // build regex from all tags 373 // build regex from all tags
361 $re = '/^' . implode(array_map("self::tag2regex", $inputTags)) . '.*$/'; 374 $re = '/^' . implode(array_map([$this, 'tag2regex'], $inputTags)) . '.*$/';
362 if (!$casesensitive) { 375 if (!$casesensitive) {
363 // make regex case insensitive 376 // make regex case insensitive
364 $re .= 'i'; 377 $re .= 'i';
@@ -378,7 +391,8 @@ class BookmarkFilter
378 continue; 391 continue;
379 } 392 }
380 } 393 }
381 $search = $link->getTagsString(); // build search string, start with tags of current link 394 // build search string, start with tags of current link
395 $search = $link->getTagsString($tagsSeparator);
382 if (strlen(trim($link->getDescription())) && strpos($link->getDescription(), '#') !== false) { 396 if (strlen(trim($link->getDescription())) && strpos($link->getDescription(), '#') !== false) {
383 // description given and at least one possible tag found 397 // description given and at least one possible tag found
384 $descTags = array(); 398 $descTags = array();
@@ -390,9 +404,9 @@ class BookmarkFilter
390 ); 404 );
391 if (count($descTags[1])) { 405 if (count($descTags[1])) {
392 // there were some tags in the description, add them to the search string 406 // there were some tags in the description, add them to the search string
393 $search .= ' ' . implode(' ', $descTags[1]); 407 $search .= $tagsSeparator . tags_array2str($descTags[1], $tagsSeparator);
394 } 408 }
395 }; 409 }
396 // match regular expression with search string 410 // match regular expression with search string
397 if (!preg_match($re, $search)) { 411 if (!preg_match($re, $search)) {
398 // this entry does _not_ match our regex 412 // this entry does _not_ match our regex
@@ -422,7 +436,7 @@ class BookmarkFilter
422 } 436 }
423 } 437 }
424 438
425 if (empty(trim($link->getTagsString()))) { 439 if (empty($link->getTags())) {
426 $filtered[$key] = $link; 440 $filtered[$key] = $link;
427 } 441 }
428 } 442 }
@@ -537,10 +551,11 @@ class BookmarkFilter
537 */ 551 */
538 protected function buildFullTextSearchableLink(Bookmark $link, array &$lengths): string 552 protected function buildFullTextSearchableLink(Bookmark $link, array &$lengths): string
539 { 553 {
554 $tagString = $link->getTagsString($this->conf->get('general.tags_separator', ' '));
540 $content = mb_convert_case($link->getTitle(), MB_CASE_LOWER, 'UTF-8') .'\\'; 555 $content = mb_convert_case($link->getTitle(), MB_CASE_LOWER, 'UTF-8') .'\\';
541 $content .= mb_convert_case($link->getDescription(), MB_CASE_LOWER, 'UTF-8') .'\\'; 556 $content .= mb_convert_case($link->getDescription(), MB_CASE_LOWER, 'UTF-8') .'\\';
542 $content .= mb_convert_case($link->getUrl(), MB_CASE_LOWER, 'UTF-8') .'\\'; 557 $content .= mb_convert_case($link->getUrl(), MB_CASE_LOWER, 'UTF-8') .'\\';
543 $content .= mb_convert_case($link->getTagsString(), MB_CASE_LOWER, 'UTF-8') .'\\'; 558 $content .= mb_convert_case($tagString, MB_CASE_LOWER, 'UTF-8') .'\\';
544 559
545 $lengths['title'] = ['start' => 0, 'end' => mb_strlen($link->getTitle())]; 560 $lengths['title'] = ['start' => 0, 'end' => mb_strlen($link->getTitle())];
546 $nextField = $lengths['title']['end'] + 1; 561 $nextField = $lengths['title']['end'] + 1;
@@ -548,7 +563,7 @@ class BookmarkFilter
548 $nextField = $lengths['description']['end'] + 1; 563 $nextField = $lengths['description']['end'] + 1;
549 $lengths['url'] = ['start' => $nextField, 'end' => $nextField + mb_strlen($link->getUrl())]; 564 $lengths['url'] = ['start' => $nextField, 'end' => $nextField + mb_strlen($link->getUrl())];
550 $nextField = $lengths['url']['end'] + 1; 565 $nextField = $lengths['url']['end'] + 1;
551 $lengths['tags'] = ['start' => $nextField, 'end' => $nextField + mb_strlen($link->getTagsString())]; 566 $lengths['tags'] = ['start' => $nextField, 'end' => $nextField + mb_strlen($tagString)];
552 567
553 return $content; 568 return $content;
554 } 569 }
diff --git a/application/bookmark/LinkUtils.php b/application/bookmark/LinkUtils.php
index 17c37979..9493b0aa 100644
--- a/application/bookmark/LinkUtils.php
+++ b/application/bookmark/LinkUtils.php
@@ -176,3 +176,49 @@ function is_note($linkUrl)
176{ 176{
177 return isset($linkUrl[0]) && $linkUrl[0] === '?'; 177 return isset($linkUrl[0]) && $linkUrl[0] === '?';
178} 178}
179
180/**
181 * Extract an array of tags from a given tag string, with provided separator.
182 *
183 * @param string|null $tags String containing a list of tags separated by $separator.
184 * @param string $separator Shaarli's default: ' ' (whitespace)
185 *
186 * @return array List of tags
187 */
188function tags_str2array(?string $tags, string $separator): array
189{
190 // For whitespaces, we use the special \s regex character
191 $separator = $separator === ' ' ? '\s' : $separator;
192
193 return preg_split('/\s*' . $separator . '+\s*/', trim($tags) ?? '', -1, PREG_SPLIT_NO_EMPTY);
194}
195
196/**
197 * Return a tag string with provided separator from a list of tags.
198 * Note that given array is clean up by tags_filter().
199 *
200 * @param array|null $tags List of tags
201 * @param string $separator
202 *
203 * @return string
204 */
205function tags_array2str(?array $tags, string $separator): string
206{
207 return implode($separator, tags_filter($tags, $separator));
208}
209
210/**
211 * Clean an array of tags: trim + remove empty entries
212 *
213 * @param array|null $tags List of tags
214 * @param string $separator
215 *
216 * @return array
217 */
218function tags_filter(?array $tags, string $separator): array
219{
220 $trimDefault = " \t\n\r\0\x0B";
221 return array_values(array_filter(array_map(function (string $entry) use ($separator, $trimDefault): string {
222 return trim($entry, $trimDefault . $separator);
223 }, $tags ?? [])));
224}