aboutsummaryrefslogtreecommitdiffhomepage
path: root/application/formatter
diff options
context:
space:
mode:
Diffstat (limited to 'application/formatter')
-rw-r--r--application/formatter/BookmarkDefaultFormatter.php140
-rw-r--r--application/formatter/BookmarkFormatter.php82
-rw-r--r--application/formatter/BookmarkMarkdownExtraFormatter.php24
-rw-r--r--application/formatter/BookmarkMarkdownFormatter.php20
-rw-r--r--application/formatter/BookmarkRawFormatter.php4
-rw-r--r--application/formatter/FormatterFactory.php2
6 files changed, 247 insertions, 25 deletions
diff --git a/application/formatter/BookmarkDefaultFormatter.php b/application/formatter/BookmarkDefaultFormatter.php
index 9d4a0fa0..7e0afafc 100644
--- a/application/formatter/BookmarkDefaultFormatter.php
+++ b/application/formatter/BookmarkDefaultFormatter.php
@@ -12,10 +12,13 @@ namespace Shaarli\Formatter;
12 */ 12 */
13class BookmarkDefaultFormatter extends BookmarkFormatter 13class BookmarkDefaultFormatter extends BookmarkFormatter
14{ 14{
15 protected const SEARCH_HIGHLIGHT_OPEN = '|@@HIGHLIGHT';
16 protected 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,33 @@ 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 $description = format_description(
50 escape($description),
51 $indexUrl,
52 $this->conf->get('formatter_settings.autolink', true)
53 );
54
55 return $this->replaceTokens($description);
30 } 56 }
31 57
32 /** 58 /**
@@ -40,15 +66,36 @@ class BookmarkDefaultFormatter extends BookmarkFormatter
40 /** 66 /**
41 * @inheritdoc 67 * @inheritdoc
42 */ 68 */
43 public function formatTagString($bookmark) 69 protected function formatTagListHtml($bookmark)
44 { 70 {
45 return implode(' ', $this->formatTagList($bookmark)); 71 $tagsSeparator = $this->conf->get('general.tags_separator', ' ');
72 if (empty($bookmark->getAdditionalContentEntry('search_highlight')['tags'])) {
73 return $this->formatTagList($bookmark);
74 }
75
76 $tags = $this->tokenizeSearchHighlightField(
77 $bookmark->getTagsString($tagsSeparator),
78 $bookmark->getAdditionalContentEntry('search_highlight')['tags']
79 );
80 $tags = $this->filterTagList(tags_str2array($tags, $tagsSeparator));
81 $tags = escape($tags);
82 $tags = $this->replaceTokensArray($tags);
83
84 return $tags;
46 } 85 }
47 86
48 /** 87 /**
49 * @inheritdoc 88 * @inheritdoc
50 */ 89 */
51 public function formatUrl($bookmark) 90 protected function formatTagString($bookmark)
91 {
92 return implode($this->conf->get('general.tags_separator'), $this->formatTagList($bookmark));
93 }
94
95 /**
96 * @inheritdoc
97 */
98 protected function formatUrl($bookmark)
52 { 99 {
53 if ($bookmark->isNote() && isset($this->contextData['index_url'])) { 100 if ($bookmark->isNote() && isset($this->contextData['index_url'])) {
54 return rtrim($this->contextData['index_url'], '/') . '/' . escape(ltrim($bookmark->getUrl(), '/')); 101 return rtrim($this->contextData['index_url'], '/') . '/' . escape(ltrim($bookmark->getUrl(), '/'));
@@ -80,8 +127,89 @@ class BookmarkDefaultFormatter extends BookmarkFormatter
80 /** 127 /**
81 * @inheritdoc 128 * @inheritdoc
82 */ 129 */
130 protected function formatUrlHtml($bookmark)
131 {
132 $url = $this->tokenizeSearchHighlightField(
133 $bookmark->getUrl() ?? '',
134 $bookmark->getAdditionalContentEntry('search_highlight')['url'] ?? []
135 );
136
137 return $this->replaceTokens(escape($url));
138 }
139
140 /**
141 * @inheritdoc
142 */
83 protected function formatThumbnail($bookmark) 143 protected function formatThumbnail($bookmark)
84 { 144 {
85 return escape($bookmark->getThumbnail()); 145 return escape($bookmark->getThumbnail());
86 } 146 }
147
148 /**
149 * Insert search highlight token in provided field content based on a list of search result positions
150 *
151 * @param string $fieldContent
152 * @param array|null $positions List of of search results with 'start' and 'end' positions.
153 *
154 * @return string Updated $fieldContent.
155 */
156 protected function tokenizeSearchHighlightField(string $fieldContent, ?array $positions): string
157 {
158 if (empty($positions)) {
159 return $fieldContent;
160 }
161
162 $insertedTokens = 0;
163 $tokenLength = strlen(static::SEARCH_HIGHLIGHT_OPEN);
164 foreach ($positions as $position) {
165 $position = [
166 'start' => $position['start'] + ($insertedTokens * $tokenLength),
167 'end' => $position['end'] + ($insertedTokens * $tokenLength),
168 ];
169
170 $content = mb_substr($fieldContent, 0, $position['start']);
171 $content .= static::SEARCH_HIGHLIGHT_OPEN;
172 $content .= mb_substr($fieldContent, $position['start'], $position['end'] - $position['start']);
173 $content .= static::SEARCH_HIGHLIGHT_CLOSE;
174 $content .= mb_substr($fieldContent, $position['end']);
175
176 $fieldContent = $content;
177
178 $insertedTokens += 2;
179 }
180
181 return $fieldContent;
182 }
183
184 /**
185 * Replace search highlight tokens with HTML highlighted span.
186 *
187 * @param string $fieldContent
188 *
189 * @return string updated content.
190 */
191 protected function replaceTokens(string $fieldContent): string
192 {
193 return str_replace(
194 [static::SEARCH_HIGHLIGHT_OPEN, static::SEARCH_HIGHLIGHT_CLOSE],
195 ['<span class="search-highlight">', '</span>'],
196 $fieldContent
197 );
198 }
199
200 /**
201 * Apply replaceTokens to an array of content strings.
202 *
203 * @param string[] $fieldContents
204 *
205 * @return array
206 */
207 protected function replaceTokensArray(array $fieldContents): array
208 {
209 foreach ($fieldContents as &$entry) {
210 $entry = $this->replaceTokens($entry);
211 }
212
213 return $fieldContents;
214 }
87} 215}
diff --git a/application/formatter/BookmarkFormatter.php b/application/formatter/BookmarkFormatter.php
index 0042dafe..124ce78b 100644
--- a/application/formatter/BookmarkFormatter.php
+++ b/application/formatter/BookmarkFormatter.php
@@ -2,7 +2,7 @@
2 2
3namespace Shaarli\Formatter; 3namespace Shaarli\Formatter;
4 4
5use DateTime; 5use DateTimeInterface;
6use Shaarli\Bookmark\Bookmark; 6use Shaarli\Bookmark\Bookmark;
7use Shaarli\Config\ConfigManager; 7use 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 */
16abstract class BookmarkFormatter 39abstract 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
@@ -204,7 +267,7 @@ abstract class BookmarkFormatter
204 */ 267 */
205 protected function formatTagString($bookmark) 268 protected function formatTagString($bookmark)
206 { 269 {
207 return implode(' ', $this->formatTagList($bookmark)); 270 return implode($this->conf->get('general.tags_separator', ' '), $this->formatTagList($bookmark));
208 } 271 }
209 272
210 /** 273 /**
@@ -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 {
@@ -288,6 +351,7 @@ abstract class BookmarkFormatter
288 351
289 /** 352 /**
290 * Format tag list, e.g. remove private tags if the user is not logged in. 353 * Format tag list, e.g. remove private tags if the user is not logged in.
354 * TODO: this method is called multiple time to format tags, the result should be cached.
291 * 355 *
292 * @param array $tags 356 * @param array $tags
293 * 357 *
diff --git a/application/formatter/BookmarkMarkdownExtraFormatter.php b/application/formatter/BookmarkMarkdownExtraFormatter.php
new file mode 100644
index 00000000..0694b23f
--- /dev/null
+++ b/application/formatter/BookmarkMarkdownExtraFormatter.php
@@ -0,0 +1,24 @@
1<?php
2
3namespace Shaarli\Formatter;
4
5use Shaarli\Config\ConfigManager;
6
7/**
8 * Class BookmarkMarkdownExtraFormatter
9 *
10 * Format bookmark description into MarkdownExtra format.
11 *
12 * @see https://michelf.ca/projects/php-markdown/extra/
13 *
14 * @package Shaarli\Formatter
15 */
16class BookmarkMarkdownExtraFormatter extends BookmarkMarkdownFormatter
17{
18 public function __construct(ConfigManager $conf, bool $isLoggedIn)
19 {
20 parent::__construct($conf, $isLoggedIn);
21
22 $this->parsedown = new \ParsedownExtra();
23 }
24}
diff --git a/application/formatter/BookmarkMarkdownFormatter.php b/application/formatter/BookmarkMarkdownFormatter.php
index 5d244d4c..ee4e8dca 100644
--- a/application/formatter/BookmarkMarkdownFormatter.php
+++ b/application/formatter/BookmarkMarkdownFormatter.php
@@ -16,7 +16,7 @@ class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter
16 /** 16 /**
17 * When this tag is present in a bookmark, its description should not be processed with Markdown 17 * When this tag is present in a bookmark, its description should not be processed with Markdown
18 */ 18 */
19 const NO_MD_TAG = 'nomarkdown'; 19 public const NO_MD_TAG = 'nomarkdown';
20 20
21 /** @var \Parsedown instance */ 21 /** @var \Parsedown instance */
22 protected $parsedown; 22 protected $parsedown;
@@ -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,9 +68,10 @@ 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>';
71 } 75 }
72 76
73 return $processedDescription; 77 return $processedDescription;
@@ -106,7 +110,7 @@ class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter
106 function ($match) use ($allowedProtocols, $indexUrl) { 110 function ($match) use ($allowedProtocols, $indexUrl) {
107 $link = startsWith($match[1], '?') || startsWith($match[1], '/') ? $indexUrl : ''; 111 $link = startsWith($match[1], '?') || startsWith($match[1], '/') ? $indexUrl : '';
108 $link .= whitelist_protocols($match[1], $allowedProtocols); 112 $link .= whitelist_protocols($match[1], $allowedProtocols);
109 return ']('. $link.')'; 113 return '](' . $link . ')';
110 }, 114 },
111 $description 115 $description
112 ); 116 );
@@ -133,7 +137,7 @@ class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter
133 * \p{Mn} - any non marking space (accents, umlauts, etc) 137 * \p{Mn} - any non marking space (accents, umlauts, etc)
134 */ 138 */
135 $regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui'; 139 $regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui';
136 $replacement = '$1[#$2]('. $indexUrl .'./add-tag/$2)'; 140 $replacement = '$1[#$2](' . $indexUrl . './add-tag/$2)';
137 141
138 $descriptionLines = explode(PHP_EOL, $description); 142 $descriptionLines = explode(PHP_EOL, $description);
139 $descriptionOut = ''; 143 $descriptionOut = '';
@@ -174,17 +178,17 @@ class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter
174 */ 178 */
175 protected function sanitizeHtml($description) 179 protected function sanitizeHtml($description)
176 { 180 {
177 $escapeTags = array( 181 $escapeTags = [
178 'script', 182 'script',
179 'style', 183 'style',
180 'link', 184 'link',
181 'iframe', 185 'iframe',
182 'frameset', 186 'frameset',
183 'frame', 187 'frame',
184 ); 188 ];
185 foreach ($escapeTags as $tag) { 189 foreach ($escapeTags as $tag) {
186 $description = preg_replace_callback( 190 $description = preg_replace_callback(
187 '#<\s*'. $tag .'[^>]*>(.*</\s*'. $tag .'[^>]*>)?#is', 191 '#<\s*' . $tag . '[^>]*>(.*</\s*' . $tag . '[^>]*>)?#is',
188 function ($match) { 192 function ($match) {
189 return escape($match[0]); 193 return escape($match[0]);
190 }, 194 },
diff --git a/application/formatter/BookmarkRawFormatter.php b/application/formatter/BookmarkRawFormatter.php
index bc372273..4ff07cdf 100644
--- a/application/formatter/BookmarkRawFormatter.php
+++ b/application/formatter/BookmarkRawFormatter.php
@@ -10,4 +10,6 @@ namespace Shaarli\Formatter;
10 * 10 *
11 * @package Shaarli\Formatter 11 * @package Shaarli\Formatter
12 */ 12 */
13class BookmarkRawFormatter extends BookmarkFormatter {} 13class BookmarkRawFormatter extends BookmarkFormatter
14{
15}
diff --git a/application/formatter/FormatterFactory.php b/application/formatter/FormatterFactory.php
index a029579f..bb865aed 100644
--- a/application/formatter/FormatterFactory.php
+++ b/application/formatter/FormatterFactory.php
@@ -41,7 +41,7 @@ class FormatterFactory
41 public function getFormatter(string $type = null): BookmarkFormatter 41 public function getFormatter(string $type = null): BookmarkFormatter
42 { 42 {
43 $type = $type ? $type : $this->conf->get('formatter', 'default'); 43 $type = $type ? $type : $this->conf->get('formatter', 'default');
44 $className = '\\Shaarli\\Formatter\\Bookmark'. ucfirst($type) .'Formatter'; 44 $className = '\\Shaarli\\Formatter\\Bookmark' . ucfirst($type) . 'Formatter';
45 if (!class_exists($className)) { 45 if (!class_exists($className)) {
46 $className = '\\Shaarli\\Formatter\\BookmarkDefaultFormatter'; 46 $className = '\\Shaarli\\Formatter\\BookmarkDefaultFormatter';
47 } 47 }