Passed
Push — master ( 752f2d...1c3f97 )
by Arnaud
05:15
created

Parsedown::inlineLink()   C

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