3 * @copyright Copyright (C) 2010-2023, the Friendica project
5 * @license GNU AGPL version 3 or any later version
7 * This program is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU Affero General Public License as
9 * published by the Free Software Foundation, either version 3 of the
10 * License, or (at your option) any later version.
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU Affero General Public License for more details.
17 * You should have received a copy of the GNU Affero General Public License
18 * along with this program. If not, see <https://www.gnu.org/licenses/>.
22 namespace Friendica\Content\Text;
26 use Friendica\Model\Photo;
27 use Friendica\Model\Post;
30 * Tumblr Neue Post Format
31 * @see https://www.tumblr.com/docs/npf
35 static public function fromBBCode(string $bbcode, int $uri_id): array
39 $bbcode = self::prepareBody($bbcode);
41 $html = BBCode::convert($bbcode, false, BBCode::CONNECTORS);
46 $doc = new DOMDocument();
47 if (!@$doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'))) {
51 $element = $doc->getElementsByTagName('body')->item(0);
52 $npf = self::routeChildren($element, $uri_id, 0, $npf);
54 return self::addLinkBlock($uri_id, 0, $npf);
57 static private function prepareBody(string $body): string
59 $shared = BBCode::fetchShareAttributes($body);
60 if (!empty($shared)) {
61 $body = $shared['shared'];
64 $body = BBCode::removeAttachment($body);
66 $body = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", '[img]$3[/img]', $body);
68 if (preg_match_all("#\[url=([^\]]+?)\]\s*\[img=([^\[\]]*)\]([^\[\]]*)\[\/img\]\s*\[/url\]#ism", $body, $pictures, PREG_SET_ORDER)) {
69 foreach ($pictures as $picture) {
70 if (preg_match('#/photo/.*-[01]\.#ism', $picture[2]) && (preg_match('#/photo/.*-0\.#ism', $picture[1]) || preg_match('#/photos/.*/image/#ism', $picture[1]))) {
71 $body = str_replace($picture[0], "\n\n[img=" . str_replace('-1.', '-0.', $picture[2]) . "]" . $picture[3] . "[/img]\n\n", $body);
76 $body = preg_replace("/\[img\=(.*?)\](.*?)\[\/img\]/ism", "\n\n[img=$1]$2[/img]\n\n", $body);
78 if (preg_match_all("#\[url=([^\]]+?)\]\s*\[img\]([^\[]+?)\[/img\]\s*\[/url\]#ism", $body, $pictures, PREG_SET_ORDER)) {
79 foreach ($pictures as $picture) {
80 if (preg_match('#/photo/.*-[01]\.#ism', $picture[2]) && (preg_match('#/photo/.*-0\.#ism', $picture[1]) || preg_match('#/photos/.*/image/#ism', $picture[1]))) {
81 $body = str_replace($picture[0], "\n\n[img]" . str_replace('-1.', '-0.', $picture[2]) . "[/img]\n\n", $body);
86 $body = preg_replace("/\[img\](.*?)\[\/img\]/ism", "\n\n[img]$1[/img]\n\n", $body);
87 $body = preg_replace("/\[audio\](.*?)\[\/audio\]/ism", "\n\n[audio]$1[/audio]\n\n", $body);
88 $body = preg_replace("/\[video\](.*?)\[\/video\]/ism", "\n\n[video]$1[/video]\n\n", $body);
92 $body = str_replace(["\n\n\n"], ["\n\n"], $body);
93 } while ($oldbody != $body);
98 static private function routeChildren(DOMElement $element, int $uri_id, int $level, array $npf): array
103 foreach ($element->childNodes as $child) {
104 switch ($child->nodeName) {
106 $npf = self::addText($text, $formatting, $npf);
107 $npf = self::addQuoteBlock($child, $uri_id, $level, $npf);
111 $npf = self::addText($text, $formatting, $npf);
112 $npf = self::addTextBlock($child, $uri_id, $level, $npf, 'heading1');
116 $npf = self::addText($text, $formatting, $npf);
117 $npf = self::addTextBlock($child, $uri_id, $level, $npf, 'heading1');
121 $npf = self::addText($text, $formatting, $npf);
122 $npf = self::addTextBlock($child, $uri_id, $level, $npf, 'heading1');
126 $npf = self::addText($text, $formatting, $npf);
127 $npf = self::addTextBlock($child, $uri_id, $level, $npf, 'heading2');
131 $npf = self::addText($text, $formatting, $npf);
132 $npf = self::addTextBlock($child, $uri_id, $level, $npf, 'heading2');
136 $npf = self::addText($text, $formatting, $npf);
137 $npf = self::addTextBlock($child, $uri_id, $level, $npf, 'heading2');
141 $npf = self::addText($text, $formatting, $npf);
142 $npf = self::addListBlock($child, $uri_id, $level, $npf, false, 0);
146 $npf = self::addText($text, $formatting, $npf);
147 $npf = self::addListBlock($child, $uri_id, $level, $npf, true, 0);
157 $npf = self::addText($text, $formatting, $npf);
158 $npf = self::addTextBlock($child, $uri_id, $level, $npf, 'indented');
162 $npf = self::addText($text, $formatting, $npf);
163 $npf = self::addMediaBlock($child, $uri_id, $level, $npf);
168 // $child->ownerDocument->saveHTML($child)
172 $npf = self::addText($text, $formatting, $npf);
173 $npf = self::addImageBlock($child, $uri_id, $level, $npf);
178 $npf = self::addText($text, $formatting, $npf);
179 $npf = self::addTextBlock($child, $uri_id, $level, $npf);
183 $text .= $child->textContent;
190 static private function addText(string $text, array $formatting, array $npf): array
200 if (!empty($formatting)) {
201 $block['formatting'] = $formatting;
209 static private function routeElement(DOMElement $element, int $uri_id, int $level, array $npf): array
211 switch ($element->nodeName) {
213 $npf = self::addQuoteBlock($element, $uri_id, $level, $npf);
217 $npf = self::addTextBlock($element, $uri_id, $level, $npf, 'heading1');
221 $npf = self::addTextBlock($element, $uri_id, $level, $npf, 'heading1');
225 $npf = self::addTextBlock($element, $uri_id, $level, $npf, 'heading1');
229 $npf = self::addTextBlock($element, $uri_id, $level, $npf, 'heading2');
233 $npf = self::addTextBlock($element, $uri_id, $level, $npf, 'heading2');
237 $npf = self::addTextBlock($element, $uri_id, $level, $npf, 'heading2');
241 $npf = self::addListBlock($element, $uri_id, $level, $npf, false, 0);
245 $npf = self::addListBlock($element, $uri_id, $level, $npf, true, 0);
254 $npf = self::addTextBlock($element, $uri_id, $level, $npf, 'indented');
258 $npf = self::addMediaBlock($element, $uri_id, $level, $npf);
263 // $element->ownerDocument->saveHTML($element)
267 $npf = self::addImageBlock($element, $uri_id, $level, $npf);
271 $npf = self::addTextBlock($element, $uri_id, $level, $npf);
277 static private function addImageBlock(DOMElement $element, int $uri_id, int $level, array $npf): array
280 foreach ($element->attributes as $key => $attribute) {
281 $attributes[$key] = $attribute->value;
283 if (empty($attributes['src'])) {
292 if (!empty($attributes['alt'])) {
293 $block['alt_text'] = $attributes['alt'];
296 if (!empty($attributes['title']) && ($attributes['alt'] ?? '' != $attributes['title'])) {
297 $block['caption'] = $attributes['title'];
300 $rid = Photo::ridFromURI($attributes['src']);
302 $photos = Photo::selectToArray([], ['resource-id' => $rid]);
303 foreach ($photos as $photo) {
304 $block['media'][] = [
305 'type' => $photo['type'],
306 'url' => str_replace('-0.', '-' . $photo['scale'] . '.', $attributes['src']),
307 'width' => $photo['width'],
308 'height' => $photo['height'],
311 if (empty($attributes['alt']) && !empty($photos[0]['desc'])) {
312 $block['alt_text'] = $photos[0]['desc'];
314 } elseif ($media = Post\Media::getByURL($uri_id, $attributes['src'], [Post\Media::IMAGE])) {
315 $block['media'][] = [
316 'type' => $media['mimetype'],
317 'url' => $media['url'],
318 'width' => $media['width'],
319 'height' => $media['height'],
321 if (empty($attributes['alt']) && !empty($media['description'])) {
322 $block['alt_text'] = $media['description'];
325 $block['media'][] = ['url' => $attributes['src']];
329 $block['indent_level'] = $level;
337 static private function addMediaBlock(DOMElement $element, int $uri_id, int $level, array $npf): array
340 foreach ($element->attributes as $key => $attribute) {
341 $attributes[$key] = $attribute->value;
343 if (empty($attributes['href'])) {
347 $media = Post\Media::getByURL($uri_id, $attributes['href'], [Post\Media::AUDIO, Post\Media::VIDEO]);
348 if (!empty($media)) {
349 switch ($media['type']) {
350 case Post\Media::AUDIO:
354 'type' => $media['mimetype'],
355 'url' => $media['url'],
359 if (!empty($media['name'])) {
360 $block['title'] = $media['name'];
361 } elseif (!empty($media['description'])) {
362 $block['title'] = $media['description'];
365 $block = self::addPoster($media, $block);
368 case Post\Media::VIDEO:
372 'type' => $media['mimetype'],
373 'url' => $media['url'],
377 $block = self::addPoster($media, $block);
383 'text' => $element->textContent,
386 'end' => strlen($element->textContent),
388 'url' => $attributes['href']
394 $block['indent_level'] = $level;
402 static private function addPoster(array $media, array $block): array
405 if (!empty($media['preview'])) {
406 $poster['url'] = $media['preview'];
408 if (!empty($media['preview-width'])) {
409 $poster['width'] = $media['preview-width'];
411 if (!empty($media['preview-height'])) {
412 $poster['height'] = $media['preview-height'];
414 if (!empty($poster)) {
415 $block['poster'] = $poster;
420 static private function getTypeForNodeName(string $nodename): string
432 return 'strikethrough';
437 static private function fetchText(DOMElement $element, array $text = ['text' => '', 'formatting' => []]): array
439 foreach ($element->childNodes as $child) {
440 $start = strlen($text['text']);
442 $type = self::getTypeForNodeName($child->nodeName);
444 if ($child->nodeName == 'br') {
445 $text['text'] .= "\n";
446 } elseif (($type != '') || in_array($child->nodeName, ['#text', 'code', 'a', 'p', 'span', 'u', 'img', 'summary', 'ul', 'blockquote', 'h3', 'ol'])) {
447 $text['text'] .= $child->textContent;
449 echo $element->ownerDocument->saveHTML($element) . "\n";
450 die($child->nodeName . "\n");
453 $text['formatting'][] = ['start' => $start, 'end' => strlen($text['text']), 'type' => $type];
459 static private function addQuoteBlock(DOMElement $element, int $uri_id, int $level, array $npf): array
461 $block = ['type' => 'text', 'subtype' => 'indented'];
464 $block['indent_level'] = $level;
469 $npf = self::routeChildren($element, $uri_id, 0, $npf);
474 static private function addTextBlock(DOMElement $element, int $uri_id, int $level, array $npf, string $subtype = ''): array
476 if (empty($subtype) && ($element->textContent == $element->firstChild->textContent) && ($element->firstChild->nodeName != '#text')) {
477 return self::routeElement($element->firstChild, $uri_id, $level, $npf);
480 $block = ['type' => 'text'];
482 if (!empty($subtype)) {
483 $block['subtype'] = $subtype;
486 $text = self::fetchText($element);
488 $block['text'] = $text['text'];
489 $block['formatting'] = $text['formatting'];
491 if (empty($subtype)) {
492 $type = self::getTypeForNodeName($element->nodeName);
494 $block['formatting'][] = ['start' => 0, 'end' => strlen($block['text']), 'type' => $type];
498 if (empty($block['formatting'])) {
499 unset($block['formatting']);
503 $block['indent_level'] = $level;
511 static private function addListBlock(DOMElement $element, int $uri_id, int $level, array $npf, bool $ordered): array
513 foreach ($element->childNodes as $child) {
514 switch ($child->nodeName) {
516 $npf = self::addListBlock($child, $uri_id, $level++, $npf, false);
518 $npf = self::addListBlock($child, $uri_id, $level++, $npf, true);
520 $text = self::fetchText($child);
524 'subtype' => $ordered ? 'ordered-list-item' : 'unordered-list-item',
525 'text' => $text['text']
528 $block['indent_level'] = $level;
530 if (!empty($text['formatting'])) {
531 $block['formatting'] = $text['formatting'];
540 static private function addLinkBlock(int $uri_id, int $level, array $npf): array
542 foreach (Post\Media::getByURIId($uri_id, [Post\Media::HTML]) as $link) {
543 $host = parse_url($link['url'], PHP_URL_HOST);
544 if (in_array($host, ['www.youtube.com', 'youtu.be'])) {
547 'provider' => 'youtube',
548 'url' => $link['url'],
550 } elseif (in_array($host, ['vimeo.com'])) {
553 'provider' => 'vimeo',
554 'url' => $link['url'],
556 } elseif (in_array($host, ['open.spotify.com'])) {
559 'provider' => 'spotify',
560 'url' => $link['url'],
565 'url' => $link['url'],
567 if (!empty($link['name'])) {
568 $block['title'] = $link['name'];
570 if (!empty($link['description'])) {
571 $block['description'] = $link['description'];
573 if (!empty($link['author-name'])) {
574 $block['author'] = $link['author-name'];
576 if (!empty($link['publisher-name'])) {
577 $block['site_name'] = $link['publisher-name'];
582 $block['indent_level'] = $level;
585 $npf[] = self::addPoster($link, $block);