Parsedown::inlineLink()   C
last analyzed

Complexity

Conditions 16
Paths 38

Size

Total Lines 124
Code Lines 74

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 80
CRAP Score 16.052

Importance

Changes 2
Bugs 2 Features 0
Metric Value
cc 16
eloc 74
c 2
b 2
f 0
nc 38
nop 1
dl 0
loc 124
ccs 80
cts 85
cp 0.9412
crap 16.052
rs 5.5666

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * This file is part of Cecil.
5
 *
6
 * (c) Arnaud Ligny <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
13
14
namespace Cecil\Converter;
15
16
use Cecil\Assets\Asset;
17
use Cecil\Assets\Image;
18
use Cecil\Builder;
19
use Cecil\Exception\RuntimeException;
20
use Cecil\Url;
21
use Cecil\Util;
22
use Highlight\Highlighter;
23
24
/**
25
 * Parsedown class.
26
 *
27
 * This class extends ParsedownExtra (and ParsedownToc) and provides methods to parse Markdown content
28
 * with additional features such as inline insertions, image handling, note blocks,
29
 * and code highlighting.
30
 *
31
 * @property array $InlineTypes
32
 * @property string $inlineMarkerList
33
 * @property array $specialCharacters
34
 * @property array $BlockTypes
35
 */
36
class Parsedown extends \ParsedownToc
37
{
38
    /** @var Builder */
39
    protected $builder;
40
41
    /** @var \Cecil\Config */
42
    protected $config;
43
44
    /**
45
     * Regex for attributes.
46
     * @var string
47
     */
48
    protected $regexAttribute = '(?:[#.][-\w:\\\]+[ ]*|[-\w:\\\]+(?:=(?:["\'][^\n]*?["\']|[^\s]+)?)?[ ]*)';
49
50
    /**
51
     * Regex for image block.
52
     * @var string
53
     */
54
    protected $regexImage = "~^!\[.*?\]\(.*?\)~";
55
56
    /** @var Highlighter */
57
    protected $highlighter;
58
59 1
    public function __construct(Builder $builder, ?array $options = null)
60
    {
61 1
        $this->builder = $builder;
62 1
        $this->config = $builder->getConfig();
63
64
        // "insert" line block: ++text++ -> <ins>text</ins>
65 1
        $this->InlineTypes['+'][] = 'Insert';
66 1
        $this->inlineMarkerList = implode('', array_keys($this->InlineTypes));
67 1
        $this->specialCharacters[] = '+';
68
69
        // Image block (to avoid paragraph)
70 1
        $this->BlockTypes['!'][] = 'Image';
71
72
        // "notes" block
73 1
        $this->BlockTypes[':'][] = 'Note';
74
75
        // code highlight
76 1
        $this->highlighter = new Highlighter();
77
78
        // options
79 1
        $options = array_merge(['selectors' => (array) $this->config->get('pages.body.toc')], $options ?? []);
80
81 1
        parent::__construct();
82 1
        parent::setOptions($options);
83
    }
84
85
    /**
86
     * Insert inline.
87
     * e.g.: ++text++ -> <ins>text</ins>.
88
     */
89 1
    protected function inlineInsert($Excerpt)
90
    {
91 1
        if (!isset($Excerpt['text'][1])) {
92
            return;
93
        }
94
95 1
        if ($Excerpt['text'][1] === '+' && preg_match('/^\+\+(?=\S)(.+?)(?<=\S)\+\+/', $Excerpt['text'], $matches)) {
96 1
            return [
97 1
                'extent'  => \strlen($matches[0]),
98 1
                'element' => [
99 1
                    'name'    => 'ins',
100 1
                    'text'    => $matches[1],
101 1
                    'handler' => 'line',
102 1
                ],
103 1
            ];
104
        }
105
    }
106
107
    /**
108
     * {@inheritdoc}
109
     */
110 1
    protected function inlineLink($Excerpt)
111
    {
112 1
        $link = parent::inlineLink($Excerpt); // @phpstan-ignore staticMethod.notFound
113
114 1
        if (!isset($link)) {
115
            return null;
116
        }
117
118
        // Link to a page with "page:page_id" as URL
119 1
        if (Util\Str::startsWith($link['element']['attributes']['href'], 'page:')) {
120 1
            $link['element']['attributes']['href'] = new Url($this->builder, substr($link['element']['attributes']['href'], 5, \strlen($link['element']['attributes']['href'])));
121
122 1
            return $link;
123
        }
124
125
        // External link
126 1
        $link = $this->handleExternalLink($link);
127
128
        /*
129
         * Embed link?
130
         */
131 1
        $embed = $this->config->isEnabled('pages.body.links.embed');
132 1
        if (isset($link['element']['attributes']['embed'])) {
133 1
            $embed = true;
134 1
            if ($link['element']['attributes']['embed'] == 'false') {
135 1
                $embed = false;
136
            }
137 1
            unset($link['element']['attributes']['embed']);
138
        }
139 1
        $extension = pathinfo($link['element']['attributes']['href'], PATHINFO_EXTENSION);
140
        // video?
141 1
        if (\in_array($extension, $this->config->get('pages.body.links.embed.video') ?? [])) {
142 1
            if (!$embed) {
143 1
                $link['element']['attributes']['href'] = (string) new Asset($this->builder, $link['element']['attributes']['href'], ['leading_slash' => false]);
144
145 1
                return $link;
146
            }
147 1
            $video = $this->createMediaFromLink($link, 'video');
148 1
            if ($this->config->isEnabled('pages.body.images.caption')) {
149 1
                return $this->createFigure($video);
150
            }
151
152
            return $video;
153
        }
154
        // audio?
155 1
        if (\in_array($extension, $this->config->get('pages.body.links.embed.audio') ?? [])) {
156 1
            if (!$embed) {
157 1
                $link['element']['attributes']['href'] = (string) new Asset($this->builder, $link['element']['attributes']['href'], ['leading_slash' => false]);
158
159 1
                return $link;
160
            }
161 1
            $audio = $this->createMediaFromLink($link, 'audio');
162 1
            if ($this->config->isEnabled('pages.body.images.caption')) {
163 1
                return $this->createFigure($audio);
164
            }
165
166
            return $audio;
167
        }
168 1
        if (!$embed) {
169 1
            return $link;
170
        }
171
        // GitHub Gist link?
172
        // https://regex101.com/r/QmCiAL/1
173 1
        $pattern = 'https:\/\/gist\.github.com\/[-a-zA-Z0-9_]+\/[-a-zA-Z0-9_]+';
174 1
        if (preg_match('/' . $pattern . '/is', (string) $link['element']['attributes']['href'], $matches)) {
175 1
            $gist = [
176 1
                'extent'  => $link['extent'],
177 1
                'element' => [
178 1
                    'name'       => 'script',
179 1
                    'text'       => $link['element']['text'],
180 1
                    'attributes' => [
181 1
                        'src'   => $matches[0] . '.js',
182 1
                        'title' => $link['element']['attributes']['title'],
183 1
                    ],
184 1
                ],
185 1
            ];
186 1
            if ($this->config->isEnabled('pages.body.images.caption')) {
187 1
                return $this->createFigure($gist);
188
            }
189
190
            return $gist;
191
        }
192
        // Youtube link?
193
        // https://regex101.com/r/gznM1j/1
194 1
        $pattern = '(?:https?:\/\/)?(?:www\.)?youtu(?:\.be\/|be.com\/\S*(?:watch|embed)(?:(?:(?=\/[-a-zA-Z0-9_]{11,}(?!\S))\/)|(?:\S*v=|v\/)))([-a-zA-Z0-9_]{11,})';
195 1
        if (preg_match('/' . $pattern . '/is', (string) $link['element']['attributes']['href'], $matches)) {
196 1
            $iframe = [
197 1
                'element' => [
198 1
                    'name'       => 'iframe',
199 1
                    'text'       => $link['element']['text'],
200 1
                    'attributes' => [
201 1
                        'width'           => '560',
202 1
                        'height'          => '315',
203 1
                        'title'           => $link['element']['text'],
204 1
                        'src'             => 'https://www.youtube.com/embed/' . $matches[1],
205 1
                        'frameborder'     => '0',
206 1
                        'allow'           => 'accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture',
207 1
                        'allowfullscreen' => '',
208 1
                        'style'           => 'position:absolute; top:0; left:0; width:100%; height:100%; border:0',
209 1
                    ],
210 1
                ],
211 1
            ];
212 1
            $youtube = [
213 1
                'extent'  => $link['extent'],
214 1
                'element' => [
215 1
                    'name'    => 'div',
216 1
                    'handler' => 'elements',
217 1
                    'text'    => [
218 1
                        $iframe['element'],
219 1
                    ],
220 1
                    'attributes' => [
221 1
                        'style' => 'position:relative; padding-bottom:56.25%; height:0; overflow:hidden',
222 1
                        'title' => $link['element']['attributes']['title'],
223 1
                    ],
224 1
                ],
225 1
            ];
226 1
            if ($this->config->isEnabled('pages.body.images.caption')) {
227 1
                return $this->createFigure($youtube);
228
            }
229
230
            return $youtube;
231
        }
232
233 1
        return $link;
234
    }
235
236
    /**
237
     * {@inheritdoc}
238
     */
239 1
    protected function inlineUrl($Excerpt)
240
    {
241 1
        $link = parent::inlineUrl($Excerpt); // @phpstan-ignore staticMethod.notFound
242
243 1
        if (!isset($link)) {
244 1
            return;
245
        }
246
247
        // External link
248
        return $this->handleExternalLink($link);
249
    }
250
251
    /**
252
     * {@inheritdoc}
253
     */
254
    protected function inlineUrlTag($Excerpt)
255
    {
256
        $link = parent::inlineUrlTag($Excerpt); // @phpstan-ignore staticMethod.notFound
257
258
        if (!isset($link)) {
259
            return;
260
        }
261
262
        // External link
263
        return $this->handleExternalLink($link);
264
    }
265
266
    /**
267
     * {@inheritdoc}
268
     */
269 1
    protected function inlineImage($Excerpt)
270
    {
271 1
        $InlineImage = parent::inlineImage($Excerpt); // @phpstan-ignore staticMethod.notFound
272 1
        if (!isset($InlineImage)) {
273
            return null;
274
        }
275
276
        // normalize path
277 1
        $InlineImage['element']['attributes']['src'] = $this->normalizePath($InlineImage['element']['attributes']['src']);
278
279
        // should be lazy loaded?
280 1
        if ($this->config->isEnabled('pages.body.images.lazy') && !isset($InlineImage['element']['attributes']['loading'])) {
281 1
            $InlineImage['element']['attributes']['loading'] = 'lazy';
282
        }
283
        // should be decoding async?
284 1
        if ($this->config->isEnabled('pages.body.images.decoding') && !isset($InlineImage['element']['attributes']['decoding'])) {
285 1
            $InlineImage['element']['attributes']['decoding'] = 'async';
286
        }
287
        // add default class?
288 1
        if ((string) $this->config->get('pages.body.images.class')) {
289 1
            if (!\array_key_exists('class', $InlineImage['element']['attributes'])) {
290 1
                $InlineImage['element']['attributes']['class'] = '';
291
            }
292 1
            $InlineImage['element']['attributes']['class'] .= ' ' . (string) $this->config->get('pages.body.images.class');
293 1
            $InlineImage['element']['attributes']['class'] = trim($InlineImage['element']['attributes']['class']);
294
        }
295
296
        // disable remote image handling?
297 1
        if (Util\File::isRemote($InlineImage['element']['attributes']['src']) && !$this->config->isEnabled('pages.body.images.remote')) {
298
            return $InlineImage;
299
        }
300
301
        // create asset
302 1
        $assetOptions = ['leading_slash' => false];
303 1
        if ($this->config->isEnabled('pages.body.images.remote.fallback')) {
304 1
            $assetOptions = ['leading_slash' => true];
305 1
            $assetOptions += ['fallback' => (string) $this->config->get('pages.body.images.remote.fallback')];
306
        }
307 1
        $asset = new Asset($this->builder, $InlineImage['element']['attributes']['src'], $assetOptions);
308 1
        $InlineImage['element']['attributes']['src'] = $asset;
309 1
        $width = $asset['width'];
310
311
        /*
312
         * Should be resized?
313
         */
314 1
        $shouldResize = false;
315 1
        $assetResized = null;
316
        // pages.body.images.resize
317
        if (
318 1
            \is_int($this->config->get('pages.body.images.resize'))
319 1
            && $this->config->get('pages.body.images.resize') > 0
320 1
            && $width > $this->config->get('pages.body.images.resize')
321
        ) {
322
            $shouldResize = true;
323
            $width = $this->config->get('pages.body.images.resize');
324
        }
325
        // width attribute
326
        if (
327 1
            isset($InlineImage['element']['attributes']['width'])
328 1
            && $width > (int) $InlineImage['element']['attributes']['width']
329
        ) {
330 1
            $shouldResize = true;
331 1
            $width = (int) $InlineImage['element']['attributes']['width'];
332
        }
333
        // responsive images
334
        if (
335 1
            $this->config->isEnabled('pages.body.images.responsive')
336 1
            && !empty($this->config->getAssetsImagesWidths())
337 1
            && $width > max($this->config->getAssetsImagesWidths())
338
        ) {
339
            $shouldResize = true;
340
            $width = max($this->config->getAssetsImagesWidths());
341
        }
342 1
        if ($shouldResize) {
343
            try {
344 1
                $assetResized = $asset->resize($width);
345 1
                $InlineImage['element']['attributes']['src'] = $assetResized;
346
            } catch (\Exception $e) {
347
                $this->builder->getLogger()->debug($e->getMessage());
348
349
                return $InlineImage;
350
            }
351
        }
352
353
        // set width
354 1
        $InlineImage['element']['attributes']['width'] = $width;
355
        // set height
356 1
        $InlineImage['element']['attributes']['height'] = $assetResized['height'] ?? $asset['height'];
357
358
        // placeholder
359
        if (
360 1
            (!empty($this->config->get('pages.body.images.placeholder')) || isset($InlineImage['element']['attributes']['placeholder']))
361 1
            && \in_array($InlineImage['element']['attributes']['src']['subtype'], ['image/jpeg', 'image/png', 'image/gif'])
362
        ) {
363 1
            if (!\array_key_exists('placeholder', $InlineImage['element']['attributes'])) {
364 1
                $InlineImage['element']['attributes']['placeholder'] = (string) $this->config->get('pages.body.images.placeholder');
365
            }
366 1
            if (!\array_key_exists('style', $InlineImage['element']['attributes'])) {
367 1
                $InlineImage['element']['attributes']['style'] = '';
368
            }
369 1
            $InlineImage['element']['attributes']['style'] = trim($InlineImage['element']['attributes']['style'], ';');
370 1
            switch ($InlineImage['element']['attributes']['placeholder']) {
371 1
                case 'color':
372 1
                    $InlineImage['element']['attributes']['style'] .= \sprintf(';max-width:100%%;height:auto;background-color:%s;', Image::getDominantColor($InlineImage['element']['attributes']['src']));
373 1
                    break;
374 1
                case 'lqip':
375
                    // aborts if animated GIF for performance reasons
376 1
                    if (Image::isAnimatedGif($InlineImage['element']['attributes']['src'])) {
377
                        break;
378
                    }
379 1
                    $InlineImage['element']['attributes']['style'] .= \sprintf(';max-width:100%%;height:auto;background-image:url(%s);background-repeat:no-repeat;background-position:center;background-size:cover;', Image::getLqip($InlineImage['element']['attributes']['src']));
380 1
                    break;
381
            }
382 1
            unset($InlineImage['element']['attributes']['placeholder']);
383 1
            $InlineImage['element']['attributes']['style'] = trim($InlineImage['element']['attributes']['style']);
384
        }
385
386
        /*
387
         * Should be responsive?
388
         */
389 1
        $sizes = '';
390 1
        if ($this->config->isEnabled('pages.body.images.responsive')) {
391
            try {
392
                if (
393 1
                    $srcset = Image::buildSrcset(
394 1
                        $assetResized ?? $asset,
395 1
                        $this->config->getAssetsImagesWidths()
396 1
                    )
397
                ) {
398 1
                    $InlineImage['element']['attributes']['srcset'] = $srcset;
399 1
                    $sizes = Image::getSizes($InlineImage['element']['attributes']['class'] ?? '', (array) $this->config->getAssetsImagesSizes());
400 1
                    $InlineImage['element']['attributes']['sizes'] = $sizes;
401
                }
402 1
            } catch (\Exception $e) {
403 1
                $this->builder->getLogger()->debug($e->getMessage());
404
            }
405
        }
406
407
        /*
408
        <!-- if title: a <figure> is required to put in it a <figcaption> -->
409
        <figure>
410
            <!-- if formats: a <picture> is required for each <source> -->
411
            <picture>
412
                <source type="image/avif"
413
                    srcset="..."
414
                    sizes="..."
415
                >
416
                <source type="image/webp"
417
                    srcset="..."
418
                    sizes="..."
419
                >
420
                <img src="..."
421
                    srcset="..."
422
                    sizes="..."
423
                >
424
            </picture>
425
            <figcaption><!-- title --></figcaption>
426
        </figure>
427
        */
428
429 1
        $image = $InlineImage;
430
431
        // converts image to formats and put them in picture > source
432
        if (
433 1
            \count($formats = ((array) $this->config->get('pages.body.images.formats'))) > 0
434 1
            && \in_array($InlineImage['element']['attributes']['src']['subtype'], ['image/jpeg', 'image/png', 'image/gif'])
435
        ) {
436
            try {
437
                // InlineImage src must be an Asset instance
438 1
                if (!$InlineImage['element']['attributes']['src'] instanceof Asset) {
439
                    throw new RuntimeException(\sprintf('Asset "%s" can\'t be converted.', $InlineImage['element']['attributes']['src']));
440
                }
441
                // abord if InlineImage is an animated GIF
442 1
                if (Image::isAnimatedGif($InlineImage['element']['attributes']['src'])) {
443 1
                    $filepath = Util::joinFile($this->config->getOutputPath(), $InlineImage['element']['attributes']['src']['path']);
444 1
                    throw new RuntimeException(\sprintf('Asset "%s" is not converted (animated GIF).', $filepath));
445
                }
446 1
                $sources = [];
447 1
                foreach ($formats as $format) {
448 1
                    $srcset = '';
449
                    try {
450 1
                        $assetConverted = $InlineImage['element']['attributes']['src']->convert($format);
451 1
                    } catch (\Exception $e) {
452 1
                        $this->builder->getLogger()->debug($e->getMessage());
453 1
                        continue;
454
                    }
455
                    // build responsive images?
456
                    if ($this->config->isEnabled('pages.body.images.responsive')) {
457
                        try {
458
                            $srcset = Image::buildSrcset($assetConverted, $this->config->getAssetsImagesWidths());
459
                        } catch (\Exception $e) {
460
                            $this->builder->getLogger()->debug($e->getMessage());
461
                        }
462
                    }
463
                    // if not, use default image as srcset
464
                    if (empty($srcset)) {
465
                        $srcset = (string) $assetConverted;
466
                    }
467
                    // add format to <sources>
468
                    $sources[] = [
469
                        'name'       => 'source',
470
                        'attributes' => [
471
                            'type'   => "image/$format",
472
                            'srcset' => $srcset,
473
                            'sizes'  => $sizes,
474
                            'width'  => $InlineImage['element']['attributes']['width'],
475
                            'height' => $InlineImage['element']['attributes']['height'],
476
                        ],
477
                    ];
478
                }
479 1
                if (\count($sources) > 0) {
480
                    $picture = [
481
                        'extent'  => $InlineImage['extent'],
482
                        'element' => [
483
                            'name'       => 'picture',
484
                            'handler'    => 'elements',
485
                            'attributes' => [
486
                                'title' => $image['element']['attributes']['title'],
487
                            ],
488
                        ],
489
                    ];
490
                    $picture['element']['text'] = $sources;
491
                    unset($image['element']['attributes']['title']); // @phpstan-ignore unset.offset
492
                    $picture['element']['text'][] = $image['element'];
493 1
                    $image = $picture;
494
                }
495 1
            } catch (\Exception $e) {
496 1
                $this->builder->getLogger()->debug($e->getMessage());
497
            }
498
        }
499
500
        // if title: put the <img> (or <picture>) in a <figure> and create a <figcaption>
501 1
        if ($this->config->isEnabled('pages.body.images.caption')) {
502 1
            return $this->createFigure($image);
503
        }
504
505
        return $image;
506
    }
507
508
    /**
509
     * Image block.
510
     */
511 1
    protected function blockImage($Excerpt)
512
    {
513 1
        if (1 !== preg_match($this->regexImage, $Excerpt['text'])) {
514
            return;
515
        }
516
517 1
        $InlineImage = $this->inlineImage($Excerpt);
518 1
        if (!isset($InlineImage)) {
519
            return;
520
        }
521
522 1
        return $InlineImage;
523
    }
524
525
    /**
526
     * Note block-level markup.
527
     *
528
     * :::tip
529
     * **Tip:** This is an advice.
530
     * :::
531
     *
532
     * Code inspired by https://github.com/sixlive/parsedown-alert from TJ Miller (@sixlive).
533
     */
534 1
    protected function blockNote($block)
535
    {
536 1
        if (preg_match('/:::(.*)/', $block['text'], $matches)) {
537 1
            $block = [
538 1
                'char'    => ':',
539 1
                'element' => [
540 1
                    'name'       => 'aside',
541 1
                    'text'       => '',
542 1
                    'attributes' => [
543 1
                        'class' => 'note',
544 1
                    ],
545 1
                ],
546 1
            ];
547 1
            if (!empty($matches[1])) {
548 1
                $block['element']['attributes']['class'] .= " note-{$matches[1]}";
549
            }
550
551 1
            return $block;
552
        }
553
    }
554
555 1
    protected function blockNoteContinue($line, $block)
556
    {
557 1
        if (isset($block['complete'])) {
558 1
            return;
559
        }
560 1
        if (preg_match('/:::/', $line['text'])) {
561 1
            $block['complete'] = true;
562
563 1
            return $block;
564
        }
565 1
        $block['element']['text'] .= $line['text'] . "\n";
566
567 1
        return $block;
568
    }
569
570 1
    protected function blockNoteComplete($block)
571
    {
572 1
        $block['element']['rawHtml'] = $this->text($block['element']['text']);
573 1
        unset($block['element']['text']);
574
575 1
        return $block;
576
    }
577
578
    /**
579
     * Apply Highlight to code blocks.
580
     */
581 1
    protected function blockFencedCodeComplete($block)
582
    {
583 1
        if (!$this->config->isEnabled('pages.body.highlight')) {
584
            return $block;
585
        }
586 1
        if (!isset($block['element']['text']['attributes'])) {
587
            return $block;
588
        }
589
590
        try {
591 1
            $code = $block['element']['text']['text'];
592 1
            $languageClass = $block['element']['text']['attributes']['class'];
593 1
            $language = explode('-', $languageClass);
594 1
            $highlighted = $this->highlighter->highlight($language[1], $code);
595 1
            $block['element']['text']['attributes']['class'] = vsprintf('%s hljs %s', [
596 1
                $languageClass,
597 1
                $highlighted->language,
598 1
            ]);
599 1
            $block['element']['text']['rawHtml'] = $highlighted->value;
600 1
            $block['element']['text']['allowRawHtmlInSafeMode'] = true;
601 1
            unset($block['element']['text']['text']);
602
        } catch (\Exception $e) {
603
            $this->builder->getLogger()->debug($e->getMessage());
604
        } finally {
605 1
            return $block;
606
        }
607
    }
608
609
    /**
610
     * {@inheritdoc}
611
     */
612 1
    protected function parseAttributeData($attributeString)
613
    {
614 1
        $attributes = preg_split('/[ ]+/', $attributeString, -1, PREG_SPLIT_NO_EMPTY);
615 1
        $Data = [];
616 1
        $HtmlAtt = [];
617
618 1
        if (is_iterable($attributes)) {
619 1
            foreach ($attributes as $attribute) {
620 1
                switch ($attribute[0]) {
621 1
                    case '#': // ID
622 1
                        $Data['id'] = substr($attribute, 1);
623 1
                        break;
624 1
                    case '.': // Classes
625 1
                        $classes[] = substr($attribute, 1);
626 1
                        break;
627
                    default:  // Attributes
628 1
                        parse_str($attribute, $parsed);
629 1
                        $HtmlAtt = array_merge($HtmlAtt, $parsed);
630
                }
631
            }
632
633 1
            if (isset($classes)) {
634 1
                $Data['class'] = implode(' ', $classes);
635
            }
636 1
            if (!empty($HtmlAtt)) {
637 1
                foreach ($HtmlAtt as $a => $v) {
638 1
                    $Data[$a] = trim($v, '"');
639
                }
640
            }
641
        }
642
643 1
        return $Data;
644
    }
645
646
    /**
647
     * {@inheritdoc}
648
     *
649
     * Converts XHTML '<br />' tag to '<br>'.
650
     *
651
     * @return string
652
     */
653 1
    protected function unmarkedText($text)
654
    {
655 1
        return str_replace('<br />', '<br>', parent::unmarkedText($text)); // @phpstan-ignore staticMethod.notFound
656
    }
657
658
    /**
659
     * {@inheritdoc}
660
     *
661
     * XHTML closing tag to HTML5 closing tag.
662
     *
663
     * @return string
664
     */
665 1
    protected function element(array $Element)
666
    {
667 1
        return str_replace(' />', '>', parent::element($Element)); // @phpstan-ignore staticMethod.notFound
668
    }
669
670
    /**
671
     * Turns a path relative to static or assets into a website relative path.
672
     *
673
     *   "../../assets/images/img.jpeg"
674
     *   ->
675
     *   "/images/img.jpeg"
676
     */
677 1
    private function normalizePath(string $path): string
678
    {
679
        // https://regex101.com/r/Rzguzh/1
680 1
        $pattern = \sprintf(
681 1
            '(\.\.\/)+(\b%s|%s\b)+(\/.*)',
682 1
            (string) $this->config->get('static.dir'),
683 1
            (string) $this->config->get('assets.dir')
684 1
        );
685 1
        $path = Util::joinPath($path);
686 1
        if (!preg_match('/' . $pattern . '/is', $path, $matches)) {
687 1
            return $path;
688
        }
689
690 1
        return $matches[3];
691
    }
692
693
    /**
694
     * Create a media (video or audio) element from a link.
695
     */
696 1
    private function createMediaFromLink(array $link, string $type = 'video'): array
697
    {
698 1
        $block = [
699 1
            'extent'  => $link['extent'],
700 1
            'element' => [
701 1
                'text' => $link['element']['text'],
702 1
            ],
703 1
        ];
704 1
        $block['element']['attributes'] = $link['element']['attributes'];
705 1
        unset($block['element']['attributes']['href']);
706 1
        $block['element']['attributes']['src'] = (string) new Asset($this->builder, $link['element']['attributes']['href'], ['leading_slash' => false]);
707
        switch ($type) {
708 1
            case 'video':
709 1
                $block['element']['name'] = 'video';
710
                // no controls = autoplay, loop, muted, playsinline
711 1
                if (!isset($block['element']['attributes']['controls'])) {
712 1
                    $block['element']['attributes']['autoplay'] = '';
713 1
                    $block['element']['attributes']['loop'] = '';
714 1
                    $block['element']['attributes']['muted'] = '';
715 1
                    $block['element']['attributes']['playsinline'] = '';
716
                }
717 1
                if (isset($block['element']['attributes']['poster'])) {
718 1
                    $block['element']['attributes']['poster'] = (string) new Asset($this->builder, $block['element']['attributes']['poster'], ['leading_slash' => false]);
719
                }
720 1
                if (!\array_key_exists('style', $block['element']['attributes'])) {
721 1
                    $block['element']['attributes']['style'] = '';
722
                }
723 1
                $block['element']['attributes']['style'] .= ';max-width:100%;height:auto;background-color: #d8d8d8;'; // background color if offline
724
725 1
                return $block;
726 1
            case 'audio':
727 1
                $block['element']['name'] = 'audio';
728
729 1
                return $block;
730
        }
731
732
        throw new \Exception(\sprintf('Can\'t create %s from "%s".', $type, $link['element']['attributes']['href']));
733
    }
734
735
    /**
736
     * Create a figure / caption element.
737
     */
738 1
    private function createFigure(array $inline): array
739
    {
740 1
        if (empty($inline['element']['attributes']['title'])) {
741 1
            return $inline;
742
        }
743
744 1
        $titleRawHtml = $this->line($inline['element']['attributes']['title']); // @phpstan-ignore method.notFound
745 1
        $inline['element']['attributes']['title'] = strip_tags($titleRawHtml);
746
747 1
        $figcaption = [
748 1
            'element' => [
749 1
                'name'                   => 'figcaption',
750 1
                'allowRawHtmlInSafeMode' => true,
751 1
                'rawHtml'                => $titleRawHtml,
752 1
            ],
753 1
        ];
754 1
        $figure = [
755 1
            'extent'  => $inline['extent'],
756 1
            'element' => [
757 1
                'name'    => 'figure',
758 1
                'handler' => 'elements',
759 1
                'text'    => [
760 1
                    $inline['element'],
761 1
                    $figcaption['element'],
762 1
                ],
763 1
            ],
764 1
        ];
765
766 1
        return $figure;
767
    }
768
769
    /**
770
     * Handle an external link.
771
     */
772 1
    private function handleExternalLink(array $link): array
773
    {
774
        if (
775 1
            str_starts_with($link['element']['attributes']['href'], 'http')
776 1
            && (!empty($this->config->get('baseurl')) && !str_starts_with($link['element']['attributes']['href'], (string) $this->config->get('baseurl')))
777
        ) {
778 1
            if ($this->config->isEnabled('pages.body.links.external.blank')) {
779 1
                $link['element']['attributes']['target'] = '_blank';
780
            }
781 1
            if (!\array_key_exists('rel', $link['element']['attributes'])) {
782 1
                $link['element']['attributes']['rel'] = '';
783
            }
784 1
            if ($this->config->isEnabled('pages.body.links.external.noopener')) {
785 1
                $link['element']['attributes']['rel'] .= ' noopener';
786
            }
787 1
            if ($this->config->isEnabled('pages.body.links.external.noreferrer')) {
788 1
                $link['element']['attributes']['rel'] .= ' noreferrer';
789
            }
790 1
            if ($this->config->isEnabled('pages.body.links.external.nofollow')) {
791 1
                $link['element']['attributes']['rel'] .= ' nofollow';
792
            }
793 1
            $link['element']['attributes']['rel'] = trim($link['element']['attributes']['rel']);
794
        }
795
796 1
        return $link;
797
    }
798
}
799