aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--application/bookmark/LinkUtils.php34
-rw-r--r--application/formatter/BookmarkDefaultFormatter.php4
-rw-r--r--application/formatter/BookmarkMarkdownExtraFormatter.php4
-rw-r--r--application/formatter/BookmarkMarkdownFormatter.php19
-rw-r--r--application/formatter/Parsedown/ShaarliParsedown.php10
-rw-r--r--application/formatter/Parsedown/ShaarliParsedownExtra.php10
-rw-r--r--application/formatter/Parsedown/ShaarliParsedownTrait.php50
-rw-r--r--application/front/controller/visitor/BookmarkListController.php2
-rw-r--r--tests/formatter/BookmarkDefaultFormatterTest.php11
-rw-r--r--tests/formatter/BookmarkMarkdownFormatterTest.php43
10 files changed, 173 insertions, 14 deletions
diff --git a/application/bookmark/LinkUtils.php b/application/bookmark/LinkUtils.php
index 0ab2d213..8fa2953a 100644
--- a/application/bookmark/LinkUtils.php
+++ b/application/bookmark/LinkUtils.php
@@ -1,6 +1,7 @@
1<?php 1<?php
2 2
3use Shaarli\Bookmark\Bookmark; 3use Shaarli\Bookmark\Bookmark;
4use Shaarli\Formatter\BookmarkDefaultFormatter;
4 5
5/** 6/**
6 * Extract title from an HTML document. 7 * Extract title from an HTML document.
@@ -98,7 +99,18 @@ function html_extract_tag($tag, $html)
98function text2clickable($text) 99function text2clickable($text)
99{ 100{
100 $regex = '!(((?:https?|ftp|file)://|apt:|magnet:)\S+[a-z0-9\(\)]/?)!si'; 101 $regex = '!(((?:https?|ftp|file)://|apt:|magnet:)\S+[a-z0-9\(\)]/?)!si';
101 return preg_replace($regex, '<a href="$1">$1</a>', $text); 102 $format = function (array $match): string {
103 return '<a href="' .
104 str_replace(
105 BookmarkDefaultFormatter::SEARCH_HIGHLIGHT_OPEN,
106 '',
107 str_replace(BookmarkDefaultFormatter::SEARCH_HIGHLIGHT_CLOSE, '', $match[1])
108 ) .
109 '">' . $match[1] . '</a>'
110 ;
111 };
112
113 return preg_replace_callback($regex, $format, $text);
102} 114}
103 115
104/** 116/**
@@ -111,6 +123,9 @@ function text2clickable($text)
111 */ 123 */
112function hashtag_autolink($description, $indexUrl = '') 124function hashtag_autolink($description, $indexUrl = '')
113{ 125{
126 $tokens = '(?:' . BookmarkDefaultFormatter::SEARCH_HIGHLIGHT_OPEN . ')' .
127 '(?:' . BookmarkDefaultFormatter::SEARCH_HIGHLIGHT_CLOSE . ')'
128 ;
114 /* 129 /*
115 * To support unicode: http://stackoverflow.com/a/35498078/1484919 130 * To support unicode: http://stackoverflow.com/a/35498078/1484919
116 * \p{Pc} - to match underscore 131 * \p{Pc} - to match underscore
@@ -118,9 +133,20 @@ function hashtag_autolink($description, $indexUrl = '')
118 * \p{L} - letter from any language 133 * \p{L} - letter from any language
119 * \p{Mn} - any non marking space (accents, umlauts, etc) 134 * \p{Mn} - any non marking space (accents, umlauts, etc)
120 */ 135 */
121 $regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui'; 136 $regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}' . $tokens . ']+)/mui';
122 $replacement = '$1<a href="' . $indexUrl . './add-tag/$2" title="Hashtag $2">#$2</a>'; 137 $format = function (array $match) use ($indexUrl): string {
123 return preg_replace($regex, $replacement, $description); 138 $cleanMatch = str_replace(
139 BookmarkDefaultFormatter::SEARCH_HIGHLIGHT_OPEN,
140 '',
141 str_replace(BookmarkDefaultFormatter::SEARCH_HIGHLIGHT_CLOSE, '', $match[2])
142 );
143 return $match[1] . '<a href="' . $indexUrl . './add-tag/' . $cleanMatch . '"' .
144 ' title="Hashtag ' . $cleanMatch . '">' .
145 '#' . $match[2] .
146 '</a>';
147 };
148
149 return preg_replace_callback($regex, $format, $description);
124} 150}
125 151
126/** 152/**
diff --git a/application/formatter/BookmarkDefaultFormatter.php b/application/formatter/BookmarkDefaultFormatter.php
index 7e0afafc..7e93bf71 100644
--- a/application/formatter/BookmarkDefaultFormatter.php
+++ b/application/formatter/BookmarkDefaultFormatter.php
@@ -12,8 +12,8 @@ namespace Shaarli\Formatter;
12 */ 12 */
13class BookmarkDefaultFormatter extends BookmarkFormatter 13class BookmarkDefaultFormatter extends BookmarkFormatter
14{ 14{
15 protected const SEARCH_HIGHLIGHT_OPEN = '|@@HIGHLIGHT'; 15 public const SEARCH_HIGHLIGHT_OPEN = '||O_HIGHLIGHT';
16 protected const SEARCH_HIGHLIGHT_CLOSE = 'HIGHLIGHT@@|'; 16 public const SEARCH_HIGHLIGHT_CLOSE = '||C_HIGHLIGHT';
17 17
18 /** 18 /**
19 * @inheritdoc 19 * @inheritdoc
diff --git a/application/formatter/BookmarkMarkdownExtraFormatter.php b/application/formatter/BookmarkMarkdownExtraFormatter.php
index 0694b23f..da539bfd 100644
--- a/application/formatter/BookmarkMarkdownExtraFormatter.php
+++ b/application/formatter/BookmarkMarkdownExtraFormatter.php
@@ -3,6 +3,7 @@
3namespace Shaarli\Formatter; 3namespace Shaarli\Formatter;
4 4
5use Shaarli\Config\ConfigManager; 5use Shaarli\Config\ConfigManager;
6use Shaarli\Formatter\Parsedown\ShaarliParsedownExtra;
6 7
7/** 8/**
8 * Class BookmarkMarkdownExtraFormatter 9 * Class BookmarkMarkdownExtraFormatter
@@ -18,7 +19,6 @@ class BookmarkMarkdownExtraFormatter extends BookmarkMarkdownFormatter
18 public function __construct(ConfigManager $conf, bool $isLoggedIn) 19 public function __construct(ConfigManager $conf, bool $isLoggedIn)
19 { 20 {
20 parent::__construct($conf, $isLoggedIn); 21 parent::__construct($conf, $isLoggedIn);
21 22 $this->parsedown = new ShaarliParsedownExtra();
22 $this->parsedown = new \ParsedownExtra();
23 } 23 }
24} 24}
diff --git a/application/formatter/BookmarkMarkdownFormatter.php b/application/formatter/BookmarkMarkdownFormatter.php
index ee4e8dca..d4dccee6 100644
--- a/application/formatter/BookmarkMarkdownFormatter.php
+++ b/application/formatter/BookmarkMarkdownFormatter.php
@@ -3,6 +3,7 @@
3namespace Shaarli\Formatter; 3namespace Shaarli\Formatter;
4 4
5use Shaarli\Config\ConfigManager; 5use Shaarli\Config\ConfigManager;
6use Shaarli\Formatter\Parsedown\ShaarliParsedown;
6 7
7/** 8/**
8 * Class BookmarkMarkdownFormatter 9 * Class BookmarkMarkdownFormatter
@@ -42,7 +43,7 @@ class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter
42 { 43 {
43 parent::__construct($conf, $isLoggedIn); 44 parent::__construct($conf, $isLoggedIn);
44 45
45 $this->parsedown = new \Parsedown(); 46 $this->parsedown = new ShaarliParsedown();
46 $this->escape = $conf->get('security.markdown_escape', true); 47 $this->escape = $conf->get('security.markdown_escape', true);
47 $this->allowedProtocols = $conf->get('security.allowed_protocols', []); 48 $this->allowedProtocols = $conf->get('security.allowed_protocols', []);
48 } 49 }
@@ -128,6 +129,9 @@ class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter
128 protected function formatHashTags($description) 129 protected function formatHashTags($description)
129 { 130 {
130 $indexUrl = ! empty($this->contextData['index_url']) ? $this->contextData['index_url'] : ''; 131 $indexUrl = ! empty($this->contextData['index_url']) ? $this->contextData['index_url'] : '';
132 $tokens = '(?:' . BookmarkDefaultFormatter::SEARCH_HIGHLIGHT_OPEN . ')' .
133 '(?:' . BookmarkDefaultFormatter::SEARCH_HIGHLIGHT_CLOSE . ')'
134 ;
131 135
132 /* 136 /*
133 * To support unicode: http://stackoverflow.com/a/35498078/1484919 137 * To support unicode: http://stackoverflow.com/a/35498078/1484919
@@ -136,8 +140,15 @@ class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter
136 * \p{L} - letter from any language 140 * \p{L} - letter from any language
137 * \p{Mn} - any non marking space (accents, umlauts, etc) 141 * \p{Mn} - any non marking space (accents, umlauts, etc)
138 */ 142 */
139 $regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui'; 143 $regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}' . $tokens . ']+)/mui';
140 $replacement = '$1[#$2](' . $indexUrl . './add-tag/$2)'; 144 $replacement = function (array $match) use ($indexUrl): string {
145 $cleanMatch = str_replace(
146 BookmarkDefaultFormatter::SEARCH_HIGHLIGHT_OPEN,
147 '',
148 str_replace(BookmarkDefaultFormatter::SEARCH_HIGHLIGHT_CLOSE, '', $match[2])
149 );
150 return $match[1] . '[#' . $match[2] . '](' . $indexUrl . './add-tag/' . $cleanMatch . ')';
151 };
141 152
142 $descriptionLines = explode(PHP_EOL, $description); 153 $descriptionLines = explode(PHP_EOL, $description);
143 $descriptionOut = ''; 154 $descriptionOut = '';
@@ -156,7 +167,7 @@ class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter
156 } 167 }
157 168
158 if (!$codeBlockOn && !$codeLineOn) { 169 if (!$codeBlockOn && !$codeLineOn) {
159 $descriptionLine = preg_replace($regex, $replacement, $descriptionLine); 170 $descriptionLine = preg_replace_callback($regex, $replacement, $descriptionLine);
160 } 171 }
161 172
162 $descriptionOut .= $descriptionLine; 173 $descriptionOut .= $descriptionLine;
diff --git a/application/formatter/Parsedown/ShaarliParsedown.php b/application/formatter/Parsedown/ShaarliParsedown.php
new file mode 100644
index 00000000..d577bdfa
--- /dev/null
+++ b/application/formatter/Parsedown/ShaarliParsedown.php
@@ -0,0 +1,10 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Formatter\Parsedown;
6
7class ShaarliParsedown extends \Parsedown
8{
9 use ShaarliParsedownTrait;
10}
diff --git a/application/formatter/Parsedown/ShaarliParsedownExtra.php b/application/formatter/Parsedown/ShaarliParsedownExtra.php
new file mode 100644
index 00000000..92ad26ca
--- /dev/null
+++ b/application/formatter/Parsedown/ShaarliParsedownExtra.php
@@ -0,0 +1,10 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Formatter\Parsedown;
6
7class ShaarliParsedownExtra extends \ParsedownExtra
8{
9 use ShaarliParsedownTrait;
10}
diff --git a/application/formatter/Parsedown/ShaarliParsedownTrait.php b/application/formatter/Parsedown/ShaarliParsedownTrait.php
new file mode 100644
index 00000000..e6f4dabb
--- /dev/null
+++ b/application/formatter/Parsedown/ShaarliParsedownTrait.php
@@ -0,0 +1,50 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Formatter\Parsedown;
6
7use Shaarli\Formatter\BookmarkDefaultFormatter as Formatter;
8
9trait ShaarliParsedownTrait
10{
11 protected function inlineLink($excerpt)
12 {
13 return $this->shaarliFormatLink(parent::inlineLink($excerpt), true);
14 }
15
16 protected function inlineUrl($excerpt)
17 {
18 return $this->shaarliFormatLink(parent::inlineUrl($excerpt), false);
19 }
20
21 protected function shaarliFormatLink(?array $link, bool $fullWrap): ?array
22 {
23 if (
24 is_array($link)
25 && strpos($link['element']['attributes']['href'], Formatter::SEARCH_HIGHLIGHT_OPEN) !== false
26 && strpos($link['element']['attributes']['href'], Formatter::SEARCH_HIGHLIGHT_CLOSE) !== false
27 ) {
28 $link['element']['attributes']['href'] = $this->shaarliRemoveSearchTokens(
29 $link['element']['attributes']['href']
30 );
31
32 if ($fullWrap) {
33 $link['element']['text'] = Formatter::SEARCH_HIGHLIGHT_OPEN .
34 $link['element']['text'] .
35 Formatter::SEARCH_HIGHLIGHT_CLOSE
36 ;
37 }
38 }
39
40 return $link;
41 }
42
43 protected function shaarliRemoveSearchTokens(string $entry): string
44 {
45 $entry = str_replace(Formatter::SEARCH_HIGHLIGHT_OPEN, '', $entry);
46 $entry = str_replace(Formatter::SEARCH_HIGHLIGHT_CLOSE, '', $entry);
47
48 return $entry;
49 }
50}
diff --git a/application/front/controller/visitor/BookmarkListController.php b/application/front/controller/visitor/BookmarkListController.php
index fe8231be..106440b6 100644
--- a/application/front/controller/visitor/BookmarkListController.php
+++ b/application/front/controller/visitor/BookmarkListController.php
@@ -33,6 +33,7 @@ class BookmarkListController extends ShaarliVisitorController
33 33
34 $formatter = $this->container->formatterFactory->getFormatter(); 34 $formatter = $this->container->formatterFactory->getFormatter();
35 $formatter->addContextData('base_path', $this->container->basePath); 35 $formatter->addContextData('base_path', $this->container->basePath);
36 $formatter->addContextData('index_url', index_url($this->container->environment));
36 37
37 $searchTags = normalize_spaces($request->getParam('searchtags') ?? ''); 38 $searchTags = normalize_spaces($request->getParam('searchtags') ?? '');
38 $searchTerm = escape(normalize_spaces($request->getParam('searchterm') ?? '')); 39 $searchTerm = escape(normalize_spaces($request->getParam('searchterm') ?? ''));
@@ -157,6 +158,7 @@ class BookmarkListController extends ShaarliVisitorController
157 158
158 $formatter = $this->container->formatterFactory->getFormatter(); 159 $formatter = $this->container->formatterFactory->getFormatter();
159 $formatter->addContextData('base_path', $this->container->basePath); 160 $formatter->addContextData('base_path', $this->container->basePath);
161 $formatter->addContextData('index_url', index_url($this->container->environment));
160 162
161 $data = array_merge( 163 $data = array_merge(
162 $this->initializeTemplateVars(), 164 $this->initializeTemplateVars(),
diff --git a/tests/formatter/BookmarkDefaultFormatterTest.php b/tests/formatter/BookmarkDefaultFormatterTest.php
index 4fcc5dd1..983960b6 100644
--- a/tests/formatter/BookmarkDefaultFormatterTest.php
+++ b/tests/formatter/BookmarkDefaultFormatterTest.php
@@ -211,13 +211,17 @@ class BookmarkDefaultFormatterTest extends TestCase
211 $this->formatter = new BookmarkDefaultFormatter($this->conf, false); 211 $this->formatter = new BookmarkDefaultFormatter($this->conf, false);
212 212
213 $bookmark = new Bookmark(); 213 $bookmark = new Bookmark();
214 $bookmark->setDescription('This guide extends and expands on PSR-1, the basic coding standard.'); 214 $bookmark->setDescription(
215 'This guide extends and expands on PSR-1, the basic coding standard.' . PHP_EOL .
216 'https://www.php-fig.org/psr/psr-1/'
217 );
215 $bookmark->addAdditionalContentEntry( 218 $bookmark->addAdditionalContentEntry(
216 'search_highlight', 219 'search_highlight',
217 ['description' => [ 220 ['description' => [
218 ['start' => 0, 'end' => 10], // "This guide" 221 ['start' => 0, 'end' => 10], // "This guide"
219 ['start' => 45, 'end' => 50], // basic 222 ['start' => 45, 'end' => 50], // basic
220 ['start' => 58, 'end' => 67], // standard. 223 ['start' => 58, 'end' => 67], // standard.
224 ['start' => 84, 'end' => 87], // fig
221 ]] 225 ]]
222 ); 226 );
223 227
@@ -226,7 +230,10 @@ class BookmarkDefaultFormatterTest extends TestCase
226 $this->assertSame( 230 $this->assertSame(
227 '<span class="search-highlight">This guide</span> extends and expands on PSR-1, the ' . 231 '<span class="search-highlight">This guide</span> extends and expands on PSR-1, the ' .
228 '<span class="search-highlight">basic</span> coding ' . 232 '<span class="search-highlight">basic</span> coding ' .
229 '<span class="search-highlight">standard.</span>', 233 '<span class="search-highlight">standard.</span><br />' . PHP_EOL .
234 '<a href="https://www.php-fig.org/psr/psr-1/">' .
235 'https://www.php-<span class="search-highlight">fig</span>.org/psr/psr-1/' .
236 '</a>',
230 $link['description'] 237 $link['description']
231 ); 238 );
232 } 239 }
diff --git a/tests/formatter/BookmarkMarkdownFormatterTest.php b/tests/formatter/BookmarkMarkdownFormatterTest.php
index ab6b4080..32f7b444 100644
--- a/tests/formatter/BookmarkMarkdownFormatterTest.php
+++ b/tests/formatter/BookmarkMarkdownFormatterTest.php
@@ -133,6 +133,49 @@ class BookmarkMarkdownFormatterTest extends TestCase
133 } 133 }
134 134
135 /** 135 /**
136 * Make sure that the description is properly formatted by the default formatter.
137 */
138 public function testFormatDescriptionWithSearchHighlight()
139 {
140 $description = 'This a <strong>description</strong>'. PHP_EOL;
141 $description .= 'text https://sub.domain.tld?query=here&for=real#hash more text'. PHP_EOL;
142 $description .= 'Also, there is an #hashtag added'. PHP_EOL;
143 $description .= ' A N D KEEP SPACES ! '. PHP_EOL;
144 $description .= 'And [yet another link](https://other.domain.tld)'. PHP_EOL;
145
146 $bookmark = new Bookmark();
147 $bookmark->setDescription($description);
148 $bookmark->addAdditionalContentEntry(
149 'search_highlight',
150 ['description' => [
151 ['start' => 18, 'end' => 26], // cription
152 ['start' => 49, 'end' => 52], // sub
153 ['start' => 84, 'end' => 88], // hash
154 ['start' => 118, 'end' => 123], // hasht
155 ['start' => 203, 'end' => 215], // other.domain
156 ]]
157 );
158
159 $link = $this->formatter->format($bookmark);
160
161 $description = '<div class="markdown"><p>';
162 $description .= 'This a &lt;strong&gt;des<span class="search-highlight">cription</span>&lt;/strong&gt;<br />' .
163 PHP_EOL;
164 $url = 'https://sub.domain.tld?query=here&amp;for=real#hash';
165 $highlighted = 'https://<span class="search-highlight">sub</span>.domain.tld';
166 $highlighted .= '?query=here&amp;for=real#<span class="search-highlight">hash</span>';
167 $description .= 'text <a href="'. $url .'">'. $highlighted .'</a> more text<br />'. PHP_EOL;
168 $description .= 'Also, there is an <a href="./add-tag/hashtag">#<span class="search-highlight">hasht</span>' .
169 'ag</a> added<br />'. PHP_EOL;
170 $description .= 'A N D KEEP SPACES !<br />' . PHP_EOL;
171 $description .= 'And <a href="https://other.domain.tld">' .
172 '<span class="search-highlight">yet another link</span></a>';
173 $description .= '</p></div>';
174
175 $this->assertEquals($description, $link['description']);
176 }
177
178 /**
136 * Test formatting URL with an index_url set 179 * Test formatting URL with an index_url set
137 * It should prepend relative links. 180 * It should prepend relative links.
138 */ 181 */