So it allows to have multiple words tags.
Breaking change: commas ',' are no longer a default separator.
Fixes #594
/**
* Initialize a link from array data. Especially useful to create a Bookmark from former link storage format.
*
- * @param array $data
+ * @param array $data
+ * @param string $tagsSeparator Tags separator loaded from the config file.
+ * This is a context data, and it should *never* be stored in the Bookmark object.
*
* @return $this
*/
- public function fromArray(array $data): Bookmark
+ public function fromArray(array $data, string $tagsSeparator = ' '): Bookmark
{
$this->id = $data['id'] ?? null;
$this->shortUrl = $data['shorturl'] ?? null;
if (is_array($data['tags'])) {
$this->tags = $data['tags'];
} else {
- $this->tags = preg_split('/\s+/', $data['tags'] ?? '', -1, PREG_SPLIT_NO_EMPTY);
+ $this->tags = tags_str2array($data['tags'] ?? '', $tagsSeparator);
}
if (! empty($data['updated'])) {
$this->updated = $data['updated'];
*/
public function setTags(?array $tags): Bookmark
{
- $this->setTagsString(implode(' ', $tags ?? []));
+ $this->tags = array_map(
+ function (string $tag): string {
+ return $tag[0] === '-' ? substr($tag, 1) : $tag;
+ },
+ tags_filter($tags, ' ')
+ );
return $this;
}
}
/**
- * @return string Bookmark's tags as a string, separated by a space
+ * @param string $separator Tags separator loaded from the config file.
+ *
+ * @return string Bookmark's tags as a string, separated by a separator
*/
- public function getTagsString(): string
+ public function getTagsString(string $separator = ' '): string
{
- return implode(' ', $this->getTags());
+ return tags_array2str($this->getTags(), $separator);
}
/**
* - trailing dash in tags will be removed
*
* @param string|null $tags
+ * @param string $separator Tags separator loaded from the config file.
*
* @return $this
*/
- public function setTagsString(?string $tags): Bookmark
+ public function setTagsString(?string $tags, string $separator = ' '): Bookmark
{
- // Remove first '-' char in tags.
- $tags = preg_replace('/(^| )\-/', '$1', $tags ?? '');
- // Explode all tags separted by spaces or commas
- $tags = preg_split('/[\s,]+/', $tags);
- // Remove eventual empty values
- $tags = array_values(array_filter($tags));
-
- $this->tags = $tags;
+ $this->setTags(tags_str2array($tags, $separator));
return $this;
}
*/
public function renameTag(string $fromTag, string $toTag): void
{
- if (($pos = array_search($fromTag, $this->tags)) !== false) {
+ if (($pos = array_search($fromTag, $this->tags ?? [])) !== false) {
$this->tags[$pos] = trim($toTag);
}
}
*/
public function deleteTag(string $tag): void
{
- if (($pos = array_search($tag, $this->tags)) !== false) {
+ if (($pos = array_search($tag, $this->tags ?? [])) !== false) {
unset($this->tags[$pos]);
$this->tags = array_values($this->tags);
}
}
}
- $this->bookmarkFilter = new BookmarkFilter($this->bookmarks);
+ $this->bookmarkFilter = new BookmarkFilter($this->bookmarks, $this->conf);
}
/**
use Exception;
use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
+use Shaarli\Config\ConfigManager;
/**
* Class LinkFilter.
*/
private $bookmarks;
+ /** @var ConfigManager */
+ protected $conf;
+
/**
* @param Bookmark[] $bookmarks initialization.
*/
- public function __construct($bookmarks)
+ public function __construct($bookmarks, ConfigManager $conf)
{
$this->bookmarks = $bookmarks;
+ $this->conf = $conf;
}
/**
$filtered = $this->bookmarks;
}
if (!empty($request[0])) {
- $filtered = (new BookmarkFilter($filtered))->filterTags($request[0], $casesensitive, $visibility);
+ $filtered = (new BookmarkFilter($filtered, $this->conf))
+ ->filterTags($request[0], $casesensitive, $visibility)
+ ;
}
if (!empty($request[1])) {
- $filtered = (new BookmarkFilter($filtered))->filterFulltext($request[1], $visibility);
+ $filtered = (new BookmarkFilter($filtered, $this->conf))
+ ->filterFulltext($request[1], $visibility)
+ ;
}
return $filtered;
case self::$FILTER_TEXT:
*
* @return string generated regex fragment
*/
- private static function tag2regex(string $tag): string
+ protected function tag2regex(string $tag): string
{
+ $tagsSeparator = $this->conf->get('general.tags_separator', ' ');
$len = strlen($tag);
if (!$len || $tag === "-" || $tag === "*") {
// nothing to search, return empty regex
$i = 0; // start at first character
$regex = '(?='; // use positive lookahead
}
- $regex .= '.*(?:^| )'; // before tag may only be a space or the beginning
+ // before tag may only be the separator or the beginning
+ $regex .= '.*(?:^|' . $tagsSeparator . ')';
// iterate over string, separating it into placeholder and content
for (; $i < $len; $i++) {
if ($tag[$i] === '*') {
// placeholder found
- $regex .= '[^ ]*?';
+ $regex .= '[^' . $tagsSeparator . ']*?';
} else {
// regular characters
$offset = strpos($tag, '*', $i);
$i = $offset;
}
}
- $regex .= '(?:$| ))'; // after the tag may only be a space or the end
+ // after the tag may only be the separator or the end
+ $regex .= '(?:$|' . $tagsSeparator . '))';
return $regex;
}
*/
public function filterTags($tags, bool $casesensitive = false, string $visibility = 'all')
{
+ $tagsSeparator = $this->conf->get('general.tags_separator', ' ');
// get single tags (we may get passed an array, even though the docs say different)
$inputTags = $tags;
if (!is_array($tags)) {
// we got an input string, split tags
- $inputTags = preg_split('/(?:\s+)|,/', $inputTags, -1, PREG_SPLIT_NO_EMPTY);
+ $inputTags = tags_str2array($inputTags, $tagsSeparator);
}
- if (!count($inputTags)) {
+ if (count($inputTags) === 0) {
// no input tags
return $this->noFilter($visibility);
}
}
// build regex from all tags
- $re = '/^' . implode(array_map("self::tag2regex", $inputTags)) . '.*$/';
+ $re = '/^' . implode(array_map([$this, 'tag2regex'], $inputTags)) . '.*$/';
if (!$casesensitive) {
// make regex case insensitive
$re .= 'i';
continue;
}
}
- $search = $link->getTagsString(); // build search string, start with tags of current link
+ // build search string, start with tags of current link
+ $search = $link->getTagsString($tagsSeparator);
if (strlen(trim($link->getDescription())) && strpos($link->getDescription(), '#') !== false) {
// description given and at least one possible tag found
$descTags = array();
);
if (count($descTags[1])) {
// there were some tags in the description, add them to the search string
- $search .= ' ' . implode(' ', $descTags[1]);
+ $search .= $tagsSeparator . tags_array2str($descTags[1], $tagsSeparator);
}
- };
+ }
// match regular expression with search string
if (!preg_match($re, $search)) {
// this entry does _not_ match our regex
}
}
- if (empty(trim($link->getTagsString()))) {
+ if (empty($link->getTags())) {
$filtered[$key] = $link;
}
}
*/
protected function buildFullTextSearchableLink(Bookmark $link, array &$lengths): string
{
+ $tagString = $link->getTagsString($this->conf->get('general.tags_separator', ' '));
$content = mb_convert_case($link->getTitle(), MB_CASE_LOWER, 'UTF-8') .'\\';
$content .= mb_convert_case($link->getDescription(), MB_CASE_LOWER, 'UTF-8') .'\\';
$content .= mb_convert_case($link->getUrl(), MB_CASE_LOWER, 'UTF-8') .'\\';
- $content .= mb_convert_case($link->getTagsString(), MB_CASE_LOWER, 'UTF-8') .'\\';
+ $content .= mb_convert_case($tagString, MB_CASE_LOWER, 'UTF-8') .'\\';
$lengths['title'] = ['start' => 0, 'end' => mb_strlen($link->getTitle())];
$nextField = $lengths['title']['end'] + 1;
$nextField = $lengths['description']['end'] + 1;
$lengths['url'] = ['start' => $nextField, 'end' => $nextField + mb_strlen($link->getUrl())];
$nextField = $lengths['url']['end'] + 1;
- $lengths['tags'] = ['start' => $nextField, 'end' => $nextField + mb_strlen($link->getTagsString())];
+ $lengths['tags'] = ['start' => $nextField, 'end' => $nextField + mb_strlen($tagString)];
return $content;
}
{
return isset($linkUrl[0]) && $linkUrl[0] === '?';
}
+
+/**
+ * Extract an array of tags from a given tag string, with provided separator.
+ *
+ * @param string|null $tags String containing a list of tags separated by $separator.
+ * @param string $separator Shaarli's default: ' ' (whitespace)
+ *
+ * @return array List of tags
+ */
+function tags_str2array(?string $tags, string $separator): array
+{
+ // For whitespaces, we use the special \s regex character
+ $separator = $separator === ' ' ? '\s' : $separator;
+
+ return preg_split('/\s*' . $separator . '+\s*/', trim($tags) ?? '', -1, PREG_SPLIT_NO_EMPTY);
+}
+
+/**
+ * Return a tag string with provided separator from a list of tags.
+ * Note that given array is clean up by tags_filter().
+ *
+ * @param array|null $tags List of tags
+ * @param string $separator
+ *
+ * @return string
+ */
+function tags_array2str(?array $tags, string $separator): string
+{
+ return implode($separator, tags_filter($tags, $separator));
+}
+
+/**
+ * Clean an array of tags: trim + remove empty entries
+ *
+ * @param array|null $tags List of tags
+ * @param string $separator
+ *
+ * @return array
+ */
+function tags_filter(?array $tags, string $separator): array
+{
+ $trimDefault = " \t\n\r\0\x0B";
+ return array_values(array_filter(array_map(function (string $entry) use ($separator, $trimDefault): string {
+ return trim($entry, $trimDefault . $separator);
+ }, $tags ?? [])));
+}
$this->setEmpty('general.default_note_title', 'Note: ');
$this->setEmpty('general.retrieve_description', true);
$this->setEmpty('general.enable_async_metadata', true);
+ $this->setEmpty('general.tags_separator', ' ');
$this->setEmpty('updates.check_updates', false);
$this->setEmpty('updates.check_updates_branch', 'stable');
*/
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);
*/
protected function formatTagString($bookmark)
{
- return implode(' ', $this->formatTagList($bookmark));
+ return implode($this->conf->get('general.tags_separator'), $this->formatTagList($bookmark));
}
/**
*/
protected function formatTagString($bookmark)
{
- return implode(' ', $this->formatTagList($bookmark));
+ return implode($this->conf->get('general.tags_separator', ' '), $this->formatTagList($bookmark));
}
/**
/**
* Format tag list, e.g. remove private tags if the user is not logged in.
+ * TODO: this method is called multiple time to format tags, the result should be cached.
*
* @param array $tags
*
$fromTag = $request->getParam('fromtag') ?? '';
$this->assignView('fromtag', escape($fromTag));
+ $separator = escape($this->container->conf->get('general.tags_separator', ' '));
+ if ($separator === ' ') {
+ $separator = ' ';
+ $this->assignView('tags_separator_desc', t('whitespace'));
+ }
+ $this->assignView('tags_separator', $separator);
$this->assignView(
'pagetitle',
t('Manage tags') .' - '. $this->container->conf->get('general.title', 'Shaarli')
return $this->redirect($response, $redirect);
}
+
+ /**
+ * POST /admin/tags/change-separator - Change tag separator
+ */
+ public function changeSeparator(Request $request, Response $response): Response
+ {
+ $this->checkToken($request);
+
+ $reservedCharacters = ['-', '.', '*'];
+ $newSeparator = $request->getParam('separator');
+ if ($newSeparator === null || mb_strlen($newSeparator) !== 1) {
+ $this->saveErrorMessage(t('Tags separator must be a single character.'));
+ } elseif (in_array($newSeparator, $reservedCharacters, true)) {
+ $reservedCharacters = implode(' ', array_map(function (string $character) {
+ return '<code>' . $character . '</code>';
+ }, $reservedCharacters));
+ $this->saveErrorMessage(
+ t('These characters are reserved and can\'t be used as tags separator: ') . $reservedCharacters
+ );
+ } else {
+ $this->container->conf->set('general.tags_separator', $newSeparator, true, true);
+
+ $this->saveSuccessMessage('Your tags separator setting has been updated!');
+ }
+
+ return $this->redirect($response, '/admin/tags');
+ }
}
// To preserve backward compatibility with 3rd parties, plugins still use arrays
$data = $formatter->format($bookmark);
$this->executePageHooks('save_link', $data);
- $bookmark->fromArray($data);
+ $bookmark->fromArray($data, $this->container->conf->get('general.tags_separator', ' '));
$this->container->bookmarkService->set($bookmark, false);
++$count;
// To preserve backward compatibility with 3rd parties, plugins still use arrays
$data = $formatter->format($bookmark);
$this->executePageHooks('save_link', $data);
- $bookmark->fromArray($data);
+ $bookmark->fromArray($data, $this->container->conf->get('general.tags_separator', ' '));
$this->container->bookmarkService->set($bookmark);
$bookmark->setDescription($request->getParam('lf_description'));
$bookmark->setUrl($request->getParam('lf_url'), $this->container->conf->get('security.allowed_protocols', []));
$bookmark->setPrivate(filter_var($request->getParam('lf_private'), FILTER_VALIDATE_BOOLEAN));
- $bookmark->setTagsString($request->getParam('lf_tags'));
+ $bookmark->setTagsString(
+ $request->getParam('lf_tags'),
+ $this->container->conf->get('general.tags_separator', ' ')
+ );
if ($this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE
&& true !== $this->container->conf->get('general.enable_async_metadata', true)
$data = $formatter->format($bookmark);
$this->executePageHooks('save_link', $data);
- $bookmark->fromArray($data);
+ $bookmark->fromArray($data, $this->container->conf->get('general.tags_separator', ' '));
$this->container->bookmarkService->set($bookmark);
// If we are called from the bookmarklet, we must close the popup:
protected function buildFormData(array $link, bool $isNew, Request $request): array
{
+ $link['tags'] = strlen($link['tags']) > 0
+ ? $link['tags'] . $this->container->conf->get('general.tags_separator', ' ')
+ : $link['tags']
+ ;
+
return escape([
'link' => $link,
'link_is_new' => $isNew,
$next_page_url = '?page=' . ($page - 1) . $searchtermUrl . $searchtagsUrl;
}
+ $tagsSeparator = $this->container->conf->get('general.tags_separator', ' ');
+ $searchTagsUrlEncoded = array_map('urlencode', tags_str2array($searchTags, $tagsSeparator));
+ $searchTags = !empty($searchTags) ? trim($searchTags, $tagsSeparator) . $tagsSeparator : '';
+
// Fill all template fields.
$data = array_merge(
$this->initializeTemplateVars(),
'result_count' => count($linksToDisplay),
'search_term' => escape($searchTerm),
'search_tags' => escape($searchTags),
- 'search_tags_url' => array_map('urlencode', explode(' ', $searchTags)),
+ 'search_tags_url' => $searchTagsUrlEncoded,
'visibility' => $visibility,
'links' => $linkDisp,
]
return '[' . $tag . ']';
};
$data['pagetitle'] .= ! empty($searchTags)
- ? implode(' ', array_map($bracketWrap, preg_split('/\s+/', $searchTags))) . ' '
- : '';
+ ? implode(' ', array_map($bracketWrap, tags_str2array($searchTags, $tagsSeparator))) . ' '
+ : ''
+ ;
$data['pagetitle'] .= '- ';
}
*/
protected function processRequest(string $type, Request $request, Response $response): Response
{
+ $tagsSeparator = $this->container->conf->get('general.tags_separator', ' ');
if ($this->container->loginManager->isLoggedIn() === true) {
$visibility = $this->container->sessionManager->getSessionParameter('visibility');
}
$sort = $request->getQueryParam('sort');
$searchTags = $request->getQueryParam('searchtags');
- $filteringTags = $searchTags !== null ? explode(' ', $searchTags) : [];
+ $filteringTags = $searchTags !== null ? explode($tagsSeparator, $searchTags) : [];
$tags = $this->container->bookmarkService->bookmarksCountPerTag($filteringTags, $visibility ?? null);
$tagsUrl[escape($tag)] = urlencode((string) $tag);
}
- $searchTags = implode(' ', escape($filteringTags));
- $searchTagsUrl = urlencode(implode(' ', $filteringTags));
+ $searchTags = tags_array2str($filteringTags, $tagsSeparator);
+ $searchTags = !empty($searchTags) ? trim($searchTags, $tagsSeparator) . $tagsSeparator : '';
+ $searchTagsUrl = urlencode($searchTags);
$data = [
'search_tags' => escape($searchTags),
'search_tags_url' => $searchTagsUrl,
$this->executePageHooks('render_tag' . $type, $data, 'tag.' . $type);
$this->assignAllView($data);
- $searchTags = !empty($searchTags) ? $searchTags .' - ' : '';
+ $searchTags = !empty($searchTags) ? trim(str_replace($tagsSeparator, ' ', $searchTags)) .' - ' : '';
$this->assignView(
'pagetitle',
$searchTags . t('Tag '. $type) .' - '. $this->container->conf->get('general.title', 'Shaarli')
unset($params['addtag']);
}
+ $tagsSeparator = $this->container->conf->get('general.tags_separator', ' ');
// Check if this tag is already in the search query and ignore it if it is.
// Each tag is always separated by a space
- $currentTags = isset($params['searchtags']) ? explode(' ', $params['searchtags']) : [];
+ $currentTags = tags_str2array($params['searchtags'] ?? '', $tagsSeparator);
$addtag = true;
foreach ($currentTags as $value) {
$currentTags[] = trim($newTag);
}
- $params['searchtags'] = trim(implode(' ', $currentTags));
+ $params['searchtags'] = tags_array2str($currentTags, $tagsSeparator);
// We also remove page (keeping the same page has no sense, since the results are different)
unset($params['page']);
}
if (isset($params['searchtags'])) {
- $tags = explode(' ', $params['searchtags']);
+ $tagsSeparator = $this->container->conf->get('general.tags_separator', ' ');
+ $tags = tags_str2array($params['searchtags'] ?? '', $tagsSeparator);
// Remove value from array $tags.
$tags = array_diff($tags, [$tagToRemove]);
- $params['searchtags'] = implode(' ', $tags);
+ $params['searchtags'] = tags_array2str($tags, $tagsSeparator);
if (empty($params['searchtags'])) {
unset($params['searchtags']);
&$title,
&$description,
&$keywords,
- $retrieveDescription
+ $retrieveDescription,
+ $tagsSeparator
) {
return get_curl_download_callback(
$charset,
$title,
$description,
$keywords,
- $retrieveDescription
+ $retrieveDescription,
+ $tagsSeparator
);
}
&$title,
&$description,
&$keywords,
- $retrieveDescription
+ $retrieveDescription,
+ $tagsSeparator
) {
$currentChunk = 0;
$foundChunk = null;
*/
return function ($ch, $data) use (
$retrieveDescription,
+ $tagsSeparator,
&$charset,
&$title,
&$description,
if (! empty($keywords)) {
$foundChunk = $currentChunk;
// Keywords use the format tag1, tag2 multiple words, tag
- // So we format them to match Shaarli's separator and glue multiple words with '-'
- $keywords = implode(' ', array_map(function($keyword) {
- return implode('-', preg_split('/\s+/', trim($keyword)));
- }, explode(',', $keywords)));
+ // So we split the result with `,`, then if a tag contains the separator we replace it by `-`.
+ $keywords = tags_array2str(array_map(function(string $keyword) use ($tagsSeparator): string {
+ return tags_array2str(tags_str2array($keyword, $tagsSeparator), '-');
+ }, tags_str2array($keywords, ',')), $tagsSeparator);
}
}
$title = null;
$description = null;
$tags = null;
- $retrieveDescription = $this->conf->get('general.retrieve_description');
// Short timeout to keep the application responsive
// The callback will fill $charset and $title with data from the downloaded page.
$title,
$description,
$tags,
- $retrieveDescription
+ $this->conf->get('general.retrieve_description'),
+ $this->conf->get('general.tags_separator', ' ')
)
);
$linksArray = new BookmarkArray();
foreach ($this->linkDB as $key => $link) {
- $linksArray[$key] = (new Bookmark())->fromArray($link);
+ $linksArray[$key] = (new Bookmark())->fromArray($link, $this->conf->get('general.tags_separator', ' '));
}
$linksIo = new BookmarkIO($this->conf);
$linksIo->write($linksArray);
// Add tags to all imported bookmarks?
if (empty($post['default_tags'])) {
- $defaultTags = array();
+ $defaultTags = [];
} else {
- $defaultTags = preg_split(
- '/[\s,]+/',
- escape($post['default_tags'])
+ $defaultTags = tags_str2array(
+ escape($post['default_tags']),
+ $this->conf->get('general.tags_separator', ' ')
);
}
$link->setUrl($bkm['uri'], $this->conf->get('security.allowed_protocols'));
$link->setDescription($bkm['note']);
$link->setPrivate($private);
- $link->setTagsString($bkm['tags']);
+ $link->setTags($bkm['tags']);
$this->bookmarkService->addOrSet($link, false);
$importCount++;
$this->tpl->assign('formatter', $this->conf->get('formatter', 'default'));
$this->tpl->assign('links_per_page', $this->session['LINKS_PER_PAGE'] ?? 20);
+ $this->tpl->assign('tags_separator', $this->conf->get('general.tags_separator', ' '));
// To be removed with a proper theme configuration.
$this->tpl->assign('conf', $this->conf);
xhr.send();
}
-function createAwesompleteInstance(element, tags = []) {
+function createAwesompleteInstance(element, separator, tags = []) {
const awesome = new Awesomplete(Awesomplete.$(element));
- // Tags are separated by a space
- awesome.filter = (text, input) => Awesomplete.FILTER_CONTAINS(text, input.match(/[^ ]*$/)[0]);
+
+ // Tags are separated by separator
+ awesome.filter = (text, input) => Awesomplete.FILTER_CONTAINS(text, input.match(new RegExp(`[^${separator}]*$`))[0]);
// Insert new selected tag in the input
awesome.replace = (text) => {
- const before = awesome.input.value.match(/^.+ \s*|/)[0];
- awesome.input.value = `${before}${text} `;
+ const before = awesome.input.value.match(new RegExp(`^.+${separator}+|`))[0];
+ awesome.input.value = `${before}${text}${separator}`;
};
// Highlight found items
- awesome.item = (text, input) => Awesomplete.ITEM(text, input.match(/[^ ]*$/)[0]);
+ awesome.item = (text, input) => Awesomplete.ITEM(text, input.match(new RegExp(`[^${separator}]*$`))[0]);
// Don't display already selected items
- const reg = /(\w+) /g;
+ const reg = new RegExp(`/(\w+)${separator}/g`);
let match;
awesome.data = (item, input) => {
while ((match = reg.exec(input))) {
* @param selector CSS selector
* @param tags Array of tags
* @param instances List of existing awesomplete instances
+ * @param separator Tags separator character
*/
-function updateAwesompleteList(selector, tags, instances) {
+function updateAwesompleteList(selector, tags, instances, separator) {
if (instances.length === 0) {
// First load: create Awesomplete instances
const elements = document.querySelectorAll(selector);
[...elements].forEach((element) => {
- instances.push(createAwesompleteInstance(element, tags));
+ instances.push(createAwesompleteInstance(element, separator, tags));
});
} else {
// Update awesomplete tag list
(() => {
const basePath = document.querySelector('input[name="js_base_path"]').value;
+ const tagsSeparatorElement = document.querySelector('input[name="tags_separator"]');
+ const tagsSeparator = tagsSeparatorElement ? tagsSeparatorElement.value || '\s' : '\s';
/**
* Handle responsive menu.
// Refresh awesomplete values
existingTags = existingTags.map((tag) => (tag === fromtag ? totag : tag));
- awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes);
+ awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes, tagsSeparator);
}
};
xhr.send(`renametag=1&fromtag=${fromtagUrl}&totag=${encodeURIComponent(totag)}&token=${refreshedToken}`);
refreshToken(basePath);
existingTags = existingTags.filter((tagItem) => tagItem !== tag);
- awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes);
+ awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes, tagsSeparator);
}
});
});
const autocompleteFields = document.querySelectorAll('input[data-multiple]');
[...autocompleteFields].forEach((autocompleteField) => {
- awesomepletes.push(createAwesompleteInstance(autocompleteField));
+ awesomepletes.push(createAwesompleteInstance(autocompleteField, tagsSeparator));
});
const exportForm = document.querySelector('#exportform');
}
}
+.page-form,
+.pure-alert {
+ code {
+ display: inline-block;
+ padding: 0 2px;
+ color: $dark-grey;
+ background-color: var(--background-color);
+ }
+}
+
// Make pure-extras alert closable.
.pure-alert-closable {
.fa-times {
"timezone": "Europe\/Paris",
"title": "My Shaarli",
"header_link": "?"
+ "tags_separator": " "
},
"dev": {
"debug": false,
- **enable_async_metadata** (boolean): Retrieve external bookmark metadata asynchronously to prevent bookmark creation slowdown.
- **retrieve_description** (boolean): If set to true, for every new Shaare Shaarli will try to retrieve the description and keywords from the HTML meta tags.
- **root_url**: Overrides automatic discovery of Shaarli instance's URL (e.g.) `https://sub.domain.tld/shaarli-folder/`.
+- **tags_separator**: Defines your tags separator (default: whitespace).
### Security
$this->post('/configure', '\Shaarli\Front\Controller\Admin\ConfigureController:save');
$this->get('/tags', '\Shaarli\Front\Controller\Admin\ManageTagController:index');
$this->post('/tags', '\Shaarli\Front\Controller\Admin\ManageTagController:save');
+ $this->post('/tags/change-separator', '\Shaarli\Front\Controller\Admin\ManageTagController:changeSeparator');
$this->get('/add-shaare', '\Shaarli\Front\Controller\Admin\ShaareAddController:addShaare');
$this->get('/shaare', '\Shaarli\Front\Controller\Admin\ShaarePublishController:displayCreateForm');
$this->get('/shaare/{id:[0-9]+}', '\Shaarli\Front\Controller\Admin\ShaarePublishController:displayEditForm');
self::$refDB->write(self::$testDatastore);
$history = new History('sandbox/history.php');
self::$bookmarkService = new \FakeBookmarkService($conf, $history, $mutex, true);
- self::$linkFilter = new BookmarkFilter(self::$bookmarkService->getBookmarks());
+ self::$linkFilter = new BookmarkFilter(self::$bookmarkService->getBookmarks(), $conf);
}
/**
$this->assertTrue($bookmark->isNote());
}
+ /**
+ * Test fromArray() with a link with a custom tags separator
+ */
+ public function testFromArrayCustomTagsSeparator()
+ {
+ $data = [
+ 'id' => 1,
+ 'tags' => ['tag1', 'tag2', 'chair'],
+ ];
+
+ $bookmark = (new Bookmark())->fromArray($data, '@');
+ $this->assertEquals($data['id'], $bookmark->getId());
+ $this->assertEquals($data['tags'], $bookmark->getTags());
+ $this->assertEquals('tag1@tag2@chair', $bookmark->getTagsString('@'));
+ }
+
+
/**
* Test validate() with a valid minimal bookmark
*/
{
$bookmark = new Bookmark();
- $str = 'tag1 tag2 tag3.tag3-2, tag4 , -tag5 ';
+ $str = 'tag1 tag2 tag3.tag3-2 tag4 -tag5 ';
$bookmark->setTagsString($str);
$this->assertEquals(
[
$array = [
'tag1 ',
' tag2',
- 'tag3.tag3-2,',
- ', tag4',
- ', ',
+ 'tag3.tag3-2',
+ ' tag4',
+ ' ',
'-tag5 ',
];
$bookmark->setTags($array);
$title,
$desc,
$keywords,
- false
+ false,
+ ' '
);
$data = [
$title,
$desc,
$keywords,
- false
+ false,
+ ' '
);
$data = [
$title,
$desc,
$keywords,
- false
+ false,
+ ' '
);
$data = [
$title,
$desc,
$keywords,
- false
+ false,
+ ' '
);
$data = [
$title,
$desc,
$keywords,
- true
+ true,
+ ' '
);
$data = [
'th=device-width">'
$this->assertFalse(is_note('https://github.com/shaarli/Shaarli/?hi'));
}
+ /**
+ * Test tags_str2array with whitespace separator.
+ */
+ public function testTagsStr2ArrayWithSpaceSeparator(): void
+ {
+ $separator = ' ';
+
+ static::assertSame(['tag1', 'tag2', 'tag3'], tags_str2array('tag1 tag2 tag3', $separator));
+ static::assertSame(['tag1', 'tag2', 'tag3'], tags_str2array('tag1 tag2 tag3', $separator));
+ static::assertSame(['tag1', 'tag2', 'tag3'], tags_str2array(' tag1 tag2 tag3 ', $separator));
+ static::assertSame(['tag1@', 'tag2,', '.tag3'], tags_str2array(' tag1@ tag2, .tag3 ', $separator));
+ static::assertSame([], tags_str2array('', $separator));
+ static::assertSame([], tags_str2array(' ', $separator));
+ static::assertSame([], tags_str2array(null, $separator));
+ }
+
+ /**
+ * Test tags_str2array with @ separator.
+ */
+ public function testTagsStr2ArrayWithCharSeparator(): void
+ {
+ $separator = '@';
+
+ static::assertSame(['tag1', 'tag2', 'tag3'], tags_str2array('tag1@tag2@tag3', $separator));
+ static::assertSame(['tag1', 'tag2', 'tag3'], tags_str2array('tag1@@@@tag2@@@@tag3', $separator));
+ static::assertSame(['tag1', 'tag2', 'tag3'], tags_str2array('@@@tag1@@@tag2@@@@tag3@@', $separator));
+ static::assertSame(
+ ['tag1#', 'tag2, and other', '.tag3'],
+ tags_str2array('@@@ tag1# @@@ tag2, and other @@@@.tag3@@', $separator)
+ );
+ static::assertSame([], tags_str2array('', $separator));
+ static::assertSame([], tags_str2array(' ', $separator));
+ static::assertSame([], tags_str2array(null, $separator));
+ }
+
+ /**
+ * Test tags_array2str with ' ' separator.
+ */
+ public function testTagsArray2StrWithSpaceSeparator(): void
+ {
+ $separator = ' ';
+
+ static::assertSame('tag1 tag2 tag3', tags_array2str(['tag1', 'tag2', 'tag3'], $separator));
+ static::assertSame('tag1, tag2@ tag3', tags_array2str(['tag1,', 'tag2@', 'tag3'], $separator));
+ static::assertSame('tag1 tag2 tag3', tags_array2str([' tag1 ', 'tag2', 'tag3 '], $separator));
+ static::assertSame('tag1 tag2 tag3', tags_array2str([' tag1 ', ' ', 'tag2', ' ', 'tag3 '], $separator));
+ static::assertSame('tag1', tags_array2str([' tag1 '], $separator));
+ static::assertSame('', tags_array2str([' '], $separator));
+ static::assertSame('', tags_array2str([], $separator));
+ static::assertSame('', tags_array2str(null, $separator));
+ }
+
+ /**
+ * Test tags_array2str with @ separator.
+ */
+ public function testTagsArray2StrWithCharSeparator(): void
+ {
+ $separator = '@';
+
+ static::assertSame('tag1@tag2@tag3', tags_array2str(['tag1', 'tag2', 'tag3'], $separator));
+ static::assertSame('tag1,@tag2@tag3', tags_array2str(['tag1,', 'tag2@', 'tag3'], $separator));
+ static::assertSame(
+ 'tag1@tag2, and other@tag3',
+ tags_array2str(['@@@@ tag1@@@', ' @tag2, and other @', 'tag3@@@@'], $separator)
+ );
+ static::assertSame('tag1@tag2@tag3', tags_array2str(['@@@tag1@@@', '@', 'tag2', '@@@', 'tag3@@@'], $separator));
+ static::assertSame('tag1', tags_array2str(['@@@@tag1@@@@'], $separator));
+ static::assertSame('', tags_array2str(['@@@'], $separator));
+ static::assertSame('', tags_array2str([], $separator));
+ static::assertSame('', tags_array2str(null, $separator));
+ }
+
+ /**
+ * Test tags_array2str with @ separator.
+ */
+ public function testTagsFilterWithSpaceSeparator(): void
+ {
+ $separator = ' ';
+
+ static::assertSame(['tag1', 'tag2', 'tag3'], tags_filter(['tag1', 'tag2', 'tag3'], $separator));
+ static::assertSame(['tag1,', 'tag2@', 'tag3'], tags_filter(['tag1,', 'tag2@', 'tag3'], $separator));
+ static::assertSame(['tag1', 'tag2', 'tag3'], tags_filter([' tag1 ', 'tag2', 'tag3 '], $separator));
+ static::assertSame(['tag1', 'tag2', 'tag3'], tags_filter([' tag1 ', ' ', 'tag2', ' ', 'tag3 '], $separator));
+ static::assertSame(['tag1'], tags_filter([' tag1 '], $separator));
+ static::assertSame([], tags_filter([' '], $separator));
+ static::assertSame([], tags_filter([], $separator));
+ static::assertSame([], tags_filter(null, $separator));
+ }
+
+ /**
+ * Test tags_array2str with @ separator.
+ */
+ public function testTagsArrayFilterWithSpaceSeparator(): void
+ {
+ $separator = '@';
+
+ static::assertSame(['tag1', 'tag2', 'tag3'], tags_filter(['tag1', 'tag2', 'tag3'], $separator));
+ static::assertSame(['tag1,', 'tag2#', 'tag3'], tags_filter(['tag1,', 'tag2#', 'tag3'], $separator));
+ static::assertSame(
+ ['tag1', 'tag2, and other', 'tag3'],
+ tags_filter(['@@@@ tag1@@@', ' @tag2, and other @', 'tag3@@@@'], $separator)
+ );
+ static::assertSame(['tag1', 'tag2', 'tag3'], tags_filter(['@@@tag1@@@', '@', 'tag2', '@@@', 'tag3@@@'], $separator));
+ static::assertSame(['tag1'], tags_filter(['@@@@tag1@@@@'], $separator));
+ static::assertSame([], tags_filter(['@@@'], $separator));
+ static::assertSame([], tags_filter([], $separator));
+ static::assertSame([], tags_filter(null, $separator));
+ }
+
/**
* Util function to build an hashtag link.
*
use Shaarli\Bookmark\Bookmark;
use Shaarli\Bookmark\BookmarkFilter;
+use Shaarli\Config\ConfigManager;
use Shaarli\Front\Exception\WrongTokenException;
use Shaarli\Security\SessionManager;
use Shaarli\TestCase;
static::assertSame('changetag', (string) $result->getBody());
static::assertSame('fromtag', $assignedVariables['fromtag']);
+ static::assertSame('@', $assignedVariables['tags_separator']);
static::assertSame('Manage tags - Shaarli', $assignedVariables['pagetitle']);
}
+ /**
+ * Test displaying manage tag page
+ */
+ public function testIndexWhitespaceSeparator(): void
+ {
+ $assignedVariables = [];
+ $this->assignTemplateVars($assignedVariables);
+
+ $this->container->conf = $this->createMock(ConfigManager::class);
+ $this->container->conf->method('get')->willReturnCallback(function (string $key) {
+ return $key === 'general.tags_separator' ? ' ' : $key;
+ });
+
+ $request = $this->createMock(Request::class);
+ $response = new Response();
+
+ $this->controller->index($request, $response);
+
+ static::assertSame(' ', $assignedVariables['tags_separator']);
+ static::assertSame('whitespace', $assignedVariables['tags_separator_desc']);
+ }
+
/**
* Test posting a tag update - rename tag - valid info provided.
*/
static::assertArrayNotHasKey(SessionManager::KEY_SUCCESS_MESSAGES, $session);
static::assertSame(['Invalid tags provided.'], $session[SessionManager::KEY_WARNING_MESSAGES]);
}
+
+ /**
+ * Test changeSeparator to '#': redirection + success message.
+ */
+ public function testChangeSeparatorValid(): void
+ {
+ $toSeparator = '#';
+
+ $session = [];
+ $this->assignSessionVars($session);
+
+ $request = $this->createMock(Request::class);
+ $request
+ ->expects(static::atLeastOnce())
+ ->method('getParam')
+ ->willReturnCallback(function (string $key) use ($toSeparator): ?string {
+ return $key === 'separator' ? $toSeparator : $key;
+ })
+ ;
+ $response = new Response();
+
+ $this->container->conf
+ ->expects(static::once())
+ ->method('set')
+ ->with('general.tags_separator', $toSeparator, true, true)
+ ;
+
+ $result = $this->controller->changeSeparator($request, $response);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/admin/tags'], $result->getHeader('location'));
+
+ static::assertArrayNotHasKey(SessionManager::KEY_ERROR_MESSAGES, $session);
+ static::assertArrayNotHasKey(SessionManager::KEY_WARNING_MESSAGES, $session);
+ static::assertArrayHasKey(SessionManager::KEY_SUCCESS_MESSAGES, $session);
+ static::assertSame(
+ ['Your tags separator setting has been updated!'],
+ $session[SessionManager::KEY_SUCCESS_MESSAGES]
+ );
+ }
+
+ /**
+ * Test changeSeparator to '#@' (too long): redirection + error message.
+ */
+ public function testChangeSeparatorInvalidTooLong(): void
+ {
+ $toSeparator = '#@';
+
+ $session = [];
+ $this->assignSessionVars($session);
+
+ $request = $this->createMock(Request::class);
+ $request
+ ->expects(static::atLeastOnce())
+ ->method('getParam')
+ ->willReturnCallback(function (string $key) use ($toSeparator): ?string {
+ return $key === 'separator' ? $toSeparator : $key;
+ })
+ ;
+ $response = new Response();
+
+ $this->container->conf->expects(static::never())->method('set');
+
+ $result = $this->controller->changeSeparator($request, $response);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/admin/tags'], $result->getHeader('location'));
+
+ static::assertArrayNotHasKey(SessionManager::KEY_SUCCESS_MESSAGES, $session);
+ static::assertArrayNotHasKey(SessionManager::KEY_WARNING_MESSAGES, $session);
+ static::assertArrayHasKey(SessionManager::KEY_ERROR_MESSAGES, $session);
+ static::assertSame(
+ ['Tags separator must be a single character.'],
+ $session[SessionManager::KEY_ERROR_MESSAGES]
+ );
+ }
+
+ /**
+ * Test changeSeparator to '#@' (too long): redirection + error message.
+ */
+ public function testChangeSeparatorInvalidReservedCharacter(): void
+ {
+ $toSeparator = '*';
+
+ $session = [];
+ $this->assignSessionVars($session);
+
+ $request = $this->createMock(Request::class);
+ $request
+ ->expects(static::atLeastOnce())
+ ->method('getParam')
+ ->willReturnCallback(function (string $key) use ($toSeparator): ?string {
+ return $key === 'separator' ? $toSeparator : $key;
+ })
+ ;
+ $response = new Response();
+
+ $this->container->conf->expects(static::never())->method('set');
+
+ $result = $this->controller->changeSeparator($request, $response);
+
+ static::assertSame(302, $result->getStatusCode());
+ static::assertSame(['/subfolder/admin/tags'], $result->getHeader('location'));
+
+ static::assertArrayNotHasKey(SessionManager::KEY_SUCCESS_MESSAGES, $session);
+ static::assertArrayNotHasKey(SessionManager::KEY_WARNING_MESSAGES, $session);
+ static::assertArrayHasKey(SessionManager::KEY_ERROR_MESSAGES, $session);
+ static::assertStringStartsWith(
+ 'These characters are reserved and can\'t be used as tags separator',
+ $session[SessionManager::KEY_ERROR_MESSAGES][0]
+ );
+ }
}
static::assertSame($expectedUrl, $assignedVariables['link']['url']);
static::assertSame($remoteTitle, $assignedVariables['link']['title']);
static::assertSame($remoteDesc, $assignedVariables['link']['description']);
- static::assertSame($remoteTags, $assignedVariables['link']['tags']);
+ static::assertSame($remoteTags . ' ', $assignedVariables['link']['tags']);
static::assertFalse($assignedVariables['link']['private']);
static::assertTrue($assignedVariables['link_is_new']);
'post' => 'http://url.tld/other?part=3&utm_ad=pay#hash',
'title' => 'Provided Title',
'description' => 'Provided description.',
- 'tags' => 'abc def',
+ 'tags' => 'abc@def',
'private' => '1',
'source' => 'apps',
];
static::assertSame($expectedUrl, $assignedVariables['link']['url']);
static::assertSame($parameters['title'], $assignedVariables['link']['title']);
static::assertSame($parameters['description'], $assignedVariables['link']['description']);
- static::assertSame($parameters['tags'], $assignedVariables['link']['tags']);
+ static::assertSame($parameters['tags'] . '@', $assignedVariables['link']['tags']);
static::assertTrue($assignedVariables['link']['private']);
static::assertTrue($assignedVariables['link_is_new']);
static::assertSame($parameters['source'], $assignedVariables['source']);
static::assertSame($expectedUrl, $assignedVariables['link']['url']);
static::assertSame($title, $assignedVariables['link']['title']);
static::assertSame($description, $assignedVariables['link']['description']);
- static::assertSame(implode(' ', $tags), $assignedVariables['link']['tags']);
+ static::assertSame(implode('@', $tags) . '@', $assignedVariables['link']['tags']);
static::assertTrue($assignedVariables['link']['private']);
static::assertSame($createdAt, $assignedVariables['link']['created']);
}
static::assertSame($url, $assignedVariables['link']['url']);
static::assertSame($title, $assignedVariables['link']['title']);
static::assertSame($description, $assignedVariables['link']['description']);
- static::assertSame(implode(' ', $tags), $assignedVariables['link']['tags']);
+ static::assertSame(implode('@', $tags) . '@', $assignedVariables['link']['tags']);
static::assertTrue($assignedVariables['link']['private']);
static::assertSame($createdAt, $assignedVariables['link']['created']);
}
$request = $this->createMock(Request::class);
$request->method('getParam')->willReturnCallback(function (string $key) {
if ('searchtags' === $key) {
- return 'abc def';
+ return 'abc@def';
}
if ('searchterm' === $key) {
return 'ghi jkl';
->expects(static::once())
->method('search')
->with(
- ['searchtags' => 'abc def', 'searchterm' => 'ghi jkl'],
+ ['searchtags' => 'abc@def', 'searchterm' => 'ghi jkl'],
'private',
false,
true
static::assertSame('linklist', (string) $result->getBody());
static::assertSame('Search: ghi jkl [abc] [def] - Shaarli', $assignedVariables['pagetitle']);
- static::assertSame('?page=2&searchterm=ghi+jkl&searchtags=abc+def', $assignedVariables['previous_page_url']);
+ static::assertSame('?page=2&searchterm=ghi+jkl&searchtags=abc%40def', $assignedVariables['previous_page_url']);
}
/**
// Config
$this->container->conf = $this->createMock(ConfigManager::class);
$this->container->conf->method('get')->willReturnCallback(function (string $parameter, $default) {
+ if ($parameter === 'general.tags_separator') {
+ return '@';
+ }
+
return $default === null ? $parameter : $default;
});
->with()
->willReturnCallback(function (string $key): ?string {
if ('searchtags' === $key) {
- return 'ghi def';
+ return 'ghi@def';
}
return null;
->withConsecutive(['render_tagcloud'])
->willReturnCallback(function (string $hook, array $data, array $param): array {
if ('render_tagcloud' === $hook) {
- static::assertSame('ghi def', $data['search_tags']);
+ static::assertSame('ghi@def@', $data['search_tags']);
static::assertCount(1, $data['tags']);
static::assertArrayHasKey('loggedin', $param);
static::assertSame('tag.cloud', (string) $result->getBody());
static::assertSame('ghi def - Tag cloud - Shaarli', $assignedVariables['pagetitle']);
- static::assertSame('ghi def', $assignedVariables['search_tags']);
+ static::assertSame('ghi@def@', $assignedVariables['search_tags']);
static::assertCount(1, $assignedVariables['tags']);
static::assertArrayHasKey('abc', $assignedVariables['tags']);
->with()
->willReturnCallback(function (string $key): ?string {
if ('searchtags' === $key) {
- return 'ghi def';
+ return 'ghi@def';
} elseif ('sort' === $key) {
return 'alpha';
}
->withConsecutive(['render_taglist'])
->willReturnCallback(function (string $hook, array $data, array $param): array {
if ('render_taglist' === $hook) {
- static::assertSame('ghi def', $data['search_tags']);
+ static::assertSame('ghi@def@', $data['search_tags']);
static::assertCount(1, $data['tags']);
static::assertArrayHasKey('loggedin', $param);
static::assertSame('tag.list', (string) $result->getBody());
static::assertSame('ghi def - Tag list - Shaarli', $assignedVariables['pagetitle']);
- static::assertSame('ghi def', $assignedVariables['search_tags']);
+ static::assertSame('ghi@def@', $assignedVariables['search_tags']);
static::assertCount(1, $assignedVariables['tags']);
static::assertSame(3, $assignedVariables['tags']['abc']);
}
static::assertInstanceOf(Response::class, $result);
static::assertSame(302, $result->getStatusCode());
- static::assertSame(['/controller/?searchtags=def+abc'], $result->getHeader('location'));
+ static::assertSame(['/controller/?searchtags=def%40abc'], $result->getHeader('location'));
}
public function testAddTagWithoutRefererAndExistingSearch(): void
static::assertInstanceOf(Response::class, $result);
static::assertSame(302, $result->getStatusCode());
- static::assertSame(['/controller/?searchtags=def+abc'], $result->getHeader('location'));
+ static::assertSame(['/controller/?searchtags=def%40abc'], $result->getHeader('location'));
}
public function testAddTagResetPagination(): void
static::assertInstanceOf(Response::class, $result);
static::assertSame(302, $result->getStatusCode());
- static::assertSame(['/controller/?searchtags=def+abc'], $result->getHeader('location'));
+ static::assertSame(['/controller/?searchtags=def%40abc'], $result->getHeader('location'));
}
public function testAddTagWithRefererAndEmptySearch(): void
{
$post = array(
'privacy' => 'public',
- 'default_tags' => 'tag1,tag2 tag3'
+ 'default_tags' => 'tag1 tag2 tag3'
);
$files = file2array('netscape_basic.htm');
$this->assertStringMatchesFormat(
{
$post = array(
'privacy' => 'public',
- 'default_tags' => 'tag1&,tag2 "tag3"'
+ 'default_tags' => 'tag1& tag2 "tag3"'
);
$files = file2array('netscape_basic.htm');
$this->assertStringMatchesFormat(
);
}
+ /**
+ * Add user-specified tags to all imported bookmarks
+ */
+ public function testSetDefaultTagsWithCustomSeparator()
+ {
+ $separator = '@';
+ $this->conf->set('general.tags_separator', $separator);
+ $post = [
+ 'privacy' => 'public',
+ 'default_tags' => 'tag1@tag2@tag3@multiple words tag'
+ ];
+ $files = file2array('netscape_basic.htm');
+ $this->assertStringMatchesFormat(
+ 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
+ .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
+ $this->netscapeBookmarkUtils->import($post, $files)
+ );
+ $this->assertEquals(2, $this->bookmarkService->count());
+ $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
+ $this->assertEquals(
+ 'tag1@tag2@tag3@multiple words tag@private@secret',
+ $this->bookmarkService->get(0)->getTagsString($separator)
+ );
+ $this->assertEquals(
+ ['tag1', 'tag2', 'tag3', 'multiple words tag', 'private', 'secret'],
+ $this->bookmarkService->get(0)->getTags()
+ );
+ $this->assertEquals(
+ 'tag1@tag2@tag3@multiple words tag@public@hello@world',
+ $this->bookmarkService->get(1)->getTagsString($separator)
+ );
+ $this->assertEquals(
+ ['tag1', 'tag2', 'tag3', 'multiple words tag', 'public', 'hello', 'world'],
+ $this->bookmarkService->get(1)->getTags()
+ );
+ }
+
/**
* Ensure each imported bookmark has a unique id
*
<p>{'You can also edit tags in the'|t} <a href="{$base_path}/tags/list?sort=usage">{'tag list'|t}</a>.</p>
</div>
</div>
+
+<div class="pure-g">
+ <div class="pure-u-lg-1-3 pure-u-1-24"></div>
+ <div class="page-form page-form-light pure-u-lg-1-3 pure-u-22-24">
+ <h2 class="window-title">{"Change tags separator"|t}</h2>
+ <form method="POST" action="{$base_path}/admin/tags/change-separator" name="changeseparator" id="changeseparator">
+ <p>
+ {'Your current tag separator is'|t} <code>{$tags_separator}</code>{if="!empty($tags_separator_desc)"} ({$tags_separator_desc}){/if}.
+ </p>
+ <div>
+ <input type="text" name="separator" placeholder="{'New separator'|t}"
+ id="separator">
+ </div>
+ <input type="hidden" name="token" value="{$token}">
+ <div>
+ <input type="submit" value="{'Save'|t}" name="saveseparator">
+ </div>
+ <p>
+ {'Note that hashtags won\'t fully work with a non-whitespace separator.'|t}
+ </p>
+ </form>
+ </div>
+</div>
{include="page.footer"}
</body>
</html>
{'for'|t} <em><strong>{$search_term}</strong></em>
{/if}
{if="!empty($search_tags)"}
- {$exploded_tags=explode(' ', $search_tags)}
+ {$exploded_tags=tags_str2array($search_tags, $tags_separator)}
{'tagged'|t}
{loop="$exploded_tags"}
<span class="label label-tag" title="{'Remove tag'|t}">
<div class="pure-u-2-24"></div>
</div>
-<input type="hidden" name="token" value="{$token}" id="token" />
-
{loop="$plugins_footer.endofpage"}
{$value}
{/loop}
</div>
<input type="hidden" name="js_base_path" value="{$base_path}" />
+<input type="hidden" name="token" value="{$token}" id="token" />
+<input type="hidden" name="tags_separator" value="{$tags_separator}" id="tags_separator" />
+
<script src="{$asset_path}/js/shaarli.min.js?v={$version_hash}#"></script>
<div id="cloudtag" class="cloudtag-container">
{loop="tags"}
- <a href="{$base_path}/?searchtags={$tags_url.$key1} {$search_tags_url}" style="font-size:{$value.size}em;">{$key}</a
+ <a href="{$base_path}/?searchtags={$tags_url.$key1}{$tags_separator|urlencode}{$search_tags_url}" style="font-size:{$value.size}em;">{$key}</a
><a href="{$base_path}/add-tag/{$tags_url.$key1}" title="{'Filter by tag'|t}" class="count">{$value.count}</a>
{loop="$value.tag_plugin"}
{$value}