Parsedown::inlineLink()   C
last analyzed

Complexity

Conditions 16
Paths 38

Size

Total Lines 96
Code Lines 52

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 51
CRAP Score 16.1823

Importance

Changes 6
Bugs 3 Features 1
Metric Value
cc 16
eloc 52
c 6
b 3
f 1
nc 38
nop 1
dl 0
loc 96
ccs 51
cts 56
cp 0.9107
crap 16.1823
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\Asset;
17
use Cecil\Asset\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'] = new Url($this->builder, $link['element']['attributes']['href']);
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'] = new Url($this->builder, $link['element']['attributes']['href']);
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
        // Youtube link?
172
        // https://regex101.com/r/gznM1j/1
173 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,})';
174 1
        if (preg_match('/' . $pattern . '/is', (string) $link['element']['attributes']['href'], $matches)) {
175 1
            return $this->createEmbeddedVideoFromLink($link, 'https://www.youtube.com/embed/', $matches[1]);
176
        }
177
        // Vimeo link?
178
        // https://regex101.com/r/wCEFhd/1
179 1
        $pattern = 'https:\/\/vimeo\.com\/([0-9]+)';
180 1
        if (preg_match('/' . $pattern . '/is', (string) $link['element']['attributes']['href'], $matches)) {
181
            return $this->createEmbeddedVideoFromLink($link, 'https://player.vimeo.com/video/', $matches[1]);
182
        }
183
        // GitHub Gist link?
184
        // https://regex101.com/r/KWVMYI/1
185 1
        $pattern = 'https:\/\/gist\.github\.com\/[-a-zA-Z0-9_]+\/[-a-zA-Z0-9_]+';
186 1
        if (preg_match('/' . $pattern . '/is', (string) $link['element']['attributes']['href'], $matches)) {
187 1
            $gist = [
188 1
                'extent'  => $link['extent'],
189 1
                'element' => [
190 1
                    'name'       => 'script',
191 1
                    'text'       => $link['element']['text'],
192 1
                    'attributes' => [
193 1
                        'src'   => $matches[0] . '.js',
194 1
                        'title' => $link['element']['attributes']['title'],
195 1
                    ],
196 1
                ],
197 1
            ];
198 1
            if ($this->config->isEnabled('pages.body.images.caption')) {
199 1
                return $this->createFigure($gist);
200
            }
201
202
            return $gist;
203
        }
204
205 1
        return $link;
206
    }
207
208
    /**
209
     * {@inheritdoc}
210
     */
211 1
    protected function inlineUrl($Excerpt)
212
    {
213 1
        $link = parent::inlineUrl($Excerpt); // @phpstan-ignore staticMethod.notFound
214
215 1
        if (!isset($link)) {
216 1
            return;
217
        }
218
219
        // External link
220
        return $this->handleExternalLink($link);
221
    }
222
223
    /**
224
     * {@inheritdoc}
225
     */
226
    protected function inlineUrlTag($Excerpt)
227
    {
228
        $link = parent::inlineUrlTag($Excerpt); // @phpstan-ignore staticMethod.notFound
229
230
        if (!isset($link)) {
231
            return;
232
        }
233
234
        // External link
235
        return $this->handleExternalLink($link);
236
    }
237
238
    /**
239
     * {@inheritdoc}
240
     */
241 1
    protected function inlineImage($Excerpt)
242
    {
243 1
        $InlineImage = parent::inlineImage($Excerpt); // @phpstan-ignore staticMethod.notFound
244 1
        if (!isset($InlineImage)) {
245
            return null;
246
        }
247
248
        // normalize path
249 1
        $InlineImage['element']['attributes']['src'] = $this->normalizePath($InlineImage['element']['attributes']['src']);
250
251
        // should be lazy loaded?
252 1
        if ($this->config->isEnabled('pages.body.images.lazy') && !isset($InlineImage['element']['attributes']['loading'])) {
253 1
            $InlineImage['element']['attributes']['loading'] = 'lazy';
254
        }
255
        // should be decoding async?
256 1
        if ($this->config->isEnabled('pages.body.images.decoding') && !isset($InlineImage['element']['attributes']['decoding'])) {
257 1
            $InlineImage['element']['attributes']['decoding'] = 'async';
258
        }
259
        // add default class?
260 1
        if ((string) $this->config->get('pages.body.images.class')) {
261 1
            if (!\array_key_exists('class', $InlineImage['element']['attributes'])) {
262 1
                $InlineImage['element']['attributes']['class'] = '';
263
            }
264 1
            $InlineImage['element']['attributes']['class'] .= ' ' . (string) $this->config->get('pages.body.images.class');
265 1
            $InlineImage['element']['attributes']['class'] = trim($InlineImage['element']['attributes']['class']);
266
        }
267
268
        // disable remote image handling?
269 1
        if (Util\File::isRemote($InlineImage['element']['attributes']['src']) && !$this->config->isEnabled('pages.body.images.remote')) {
270
            return $InlineImage;
271
        }
272
273
        // create asset
274 1
        $assetOptions = ['leading_slash' => false];
275 1
        if ($this->config->isEnabled('pages.body.images.remote.fallback')) {
276 1
            $assetOptions = ['leading_slash' => true];
277 1
            $assetOptions += ['fallback' => (string) $this->config->get('pages.body.images.remote.fallback')];
278
        }
279 1
        $asset = new Asset($this->builder, $InlineImage['element']['attributes']['src'], $assetOptions);
280 1
        $InlineImage['element']['attributes']['src'] = new Url($this->builder, $asset);
281 1
        $width = $asset['width'];
282
283
        /*
284
         * Should be resized?
285
         */
286 1
        $shouldResize = false;
287 1
        $assetResized = null;
288
        // pages.body.images.resize
289
        if (
290 1
            \is_int($this->config->get('pages.body.images.resize'))
291 1
            && $this->config->get('pages.body.images.resize') > 0
292 1
            && $width > $this->config->get('pages.body.images.resize')
293
        ) {
294
            $shouldResize = true;
295
            $width = $this->config->get('pages.body.images.resize');
296
        }
297
        // width attribute
298
        if (
299 1
            isset($InlineImage['element']['attributes']['width'])
300 1
            && $width > (int) $InlineImage['element']['attributes']['width']
301
        ) {
302 1
            $shouldResize = true;
303 1
            $width = (int) $InlineImage['element']['attributes']['width'];
304
        }
305
        // responsive images
306
        if (
307 1
            $this->config->isEnabled('pages.body.images.responsive')
308 1
            && !empty($this->config->getAssetsImagesWidths())
309 1
            && $width > max($this->config->getAssetsImagesWidths())
310
        ) {
311
            $shouldResize = true;
312
            $width = max($this->config->getAssetsImagesWidths());
313
        }
314 1
        if ($shouldResize) {
315
            try {
316 1
                $assetResized = $asset->resize($width);
317
            } catch (\Exception $e) {
318
                $this->builder->getLogger()->debug($e->getMessage());
319
320
                return $InlineImage;
321
            }
322
        }
323
324
        // set width
325 1
        $InlineImage['element']['attributes']['width'] = $width;
326
        // set height
327 1
        $InlineImage['element']['attributes']['height'] = $assetResized['height'] ?? $asset['height'];
328
329
        // placeholder
330
        if (
331 1
            (!empty($this->config->get('pages.body.images.placeholder')) || isset($InlineImage['element']['attributes']['placeholder']))
332 1
            && \in_array($assetResized['subtype'] ?? $asset['subtype'], ['image/jpeg', 'image/png', 'image/gif'])
333
        ) {
334 1
            if (!\array_key_exists('placeholder', $InlineImage['element']['attributes'])) {
335 1
                $InlineImage['element']['attributes']['placeholder'] = (string) $this->config->get('pages.body.images.placeholder');
336
            }
337 1
            if (!\array_key_exists('style', $InlineImage['element']['attributes'])) {
338 1
                $InlineImage['element']['attributes']['style'] = '';
339
            }
340 1
            $InlineImage['element']['attributes']['style'] = trim($InlineImage['element']['attributes']['style'], ';');
341 1
            switch ($InlineImage['element']['attributes']['placeholder']) {
342 1
                case 'color':
343 1
                    $InlineImage['element']['attributes']['style'] .= \sprintf(';max-width:100%%;height:auto;background-color:%s;', Image::getDominantColor($assetResized ?? $asset));
344 1
                    break;
345 1
                case 'lqip':
346
                    // aborts if animated GIF for performance reasons
347 1
                    if (Image::isAnimatedGif($assetResized ?? $asset)) {
348
                        break;
349
                    }
350 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($asset));
351 1
                    break;
352
            }
353 1
            unset($InlineImage['element']['attributes']['placeholder']);
354 1
            $InlineImage['element']['attributes']['style'] = trim($InlineImage['element']['attributes']['style']);
355
        }
356
357
        /*
358
         * Should be responsive?
359
         */
360 1
        $sizes = '';
361 1
        if ($this->config->isEnabled('pages.body.images.responsive')) {
362
            try {
363
                if (
364 1
                    $srcset = Image::buildHtmlSrcset(
365 1
                        $assetResized ?? $asset,
366 1
                        $this->config->getAssetsImagesWidths()
367 1
                    )
368
                ) {
369 1
                    $InlineImage['element']['attributes']['srcset'] = $srcset;
370 1
                    $sizes = Image::getHtmlSizes($InlineImage['element']['attributes']['class'] ?? '', (array) $this->config->getAssetsImagesSizes());
371 1
                    $InlineImage['element']['attributes']['sizes'] = $sizes;
372
                }
373 1
            } catch (\Exception $e) {
374 1
                $this->builder->getLogger()->debug($e->getMessage());
375
            }
376
        }
377
378
        /*
379
        <!-- if title: a <figure> is required to put in it a <figcaption> -->
380
        <figure>
381
            <!-- if formats: a <picture> is required for each <source> -->
382
            <picture>
383
                <source type="image/avif"
384
                    srcset="..."
385
                    sizes="..."
386
                >
387
                <source type="image/webp"
388
                    srcset="..."
389
                    sizes="..."
390
                >
391
                <img src="..."
392
                    srcset="..."
393
                    sizes="..."
394
                >
395
            </picture>
396
            <figcaption><!-- title --></figcaption>
397
        </figure>
398
        */
399
400 1
        $image = $InlineImage;
401
402
        // converts image to formats and put them in picture > source
403
        if (
404 1
            \count($formats = ((array) $this->config->get('pages.body.images.formats'))) > 0
405 1
            && \in_array($assetResized['subtype'] ?? $asset['subtype'], ['image/jpeg', 'image/png', 'image/gif'])
406
        ) {
407
            try {
408
                // abord if InlineImage is an animated GIF
409 1
                if (Image::isAnimatedGif($assetResized ?? $asset)) {
410 1
                    $filepath = Util::joinFile($this->config->getOutputPath(), $assetResized['path'] ?? $asset['path']);
0 ignored issues
show
Bug introduced by
It seems like $assetResized['path'] ?? $asset['path'] can also be of type null; however, parameter $path of Cecil\Util::joinFile() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

410
                    $filepath = Util::joinFile($this->config->getOutputPath(), /** @scrutinizer ignore-type */ $assetResized['path'] ?? $asset['path']);
Loading history...
411 1
                    throw new RuntimeException(\sprintf('Asset "%s" is not converted (animated GIF).', $filepath));
412
                }
413 1
                $sources = [];
414 1
                foreach ($formats as $format) {
415 1
                    $srcset = '';
416
                    try {
417 1
                        $assetConverted = ($assetResized ?? $asset)->convert($format);
418 1
                    } catch (\Exception $e) {
419 1
                        $this->builder->getLogger()->error($e->getMessage());
420 1
                        continue;
421
                    }
422
                    // build responsive images?
423
                    if ($this->config->isEnabled('pages.body.images.responsive')) {
424
                        try {
425
                            $srcset = Image::buildHtmlSrcset($assetConverted, $this->config->getAssetsImagesWidths());
426
                        } catch (\Exception $e) {
427
                            $this->builder->getLogger()->debug($e->getMessage());
428
                        }
429
                    }
430
                    // if not, use default image as srcset
431
                    if (empty($srcset)) {
432
                        $srcset = (string) $assetConverted;
433
                    }
434
                    // add format to <sources>
435
                    $sources[] = [
436
                        'name'       => 'source',
437
                        'attributes' => [
438
                            'type'   => "image/$format",
439
                            'srcset' => $srcset,
440
                            'sizes'  => $sizes,
441
                            'width'  => $InlineImage['element']['attributes']['width'],
442
                            'height' => $InlineImage['element']['attributes']['height'],
443
                        ],
444
                    ];
445
                }
446 1
                if (\count($sources) > 0) {
447
                    $picture = [
448
                        'extent'  => $InlineImage['extent'],
449
                        'element' => [
450
                            'name'       => 'picture',
451
                            'handler'    => 'elements',
452
                            'attributes' => [
453
                                'title' => $image['element']['attributes']['title'],
454
                            ],
455
                        ],
456
                    ];
457
                    $picture['element']['text'] = $sources;
458
                    unset($image['element']['attributes']['title']); // @phpstan-ignore unset.offset
459
                    $picture['element']['text'][] = $image['element'];
460 1
                    $image = $picture;
461
                }
462 1
            } catch (\Exception $e) {
463 1
                $this->builder->getLogger()->debug($e->getMessage());
464
            }
465
        }
466
467
        // if title: put the <img> (or <picture>) in a <figure> and create a <figcaption>
468 1
        if ($this->config->isEnabled('pages.body.images.caption')) {
469 1
            return $this->createFigure($image);
470
        }
471
472
        return $image;
473
    }
474
475
    /**
476
     * Image block.
477
     */
478 1
    protected function blockImage($Excerpt)
479
    {
480 1
        if (1 !== preg_match($this->regexImage, $Excerpt['text'])) {
481
            return;
482
        }
483
484 1
        $InlineImage = $this->inlineImage($Excerpt);
485 1
        if (!isset($InlineImage)) {
486
            return;
487
        }
488
489 1
        return $InlineImage;
490
    }
491
492
    /**
493
     * Note block-level markup.
494
     *
495
     * :::tip
496
     * **Tip:** This is an advice.
497
     * :::
498
     *
499
     * Code inspired by https://github.com/sixlive/parsedown-alert from TJ Miller (@sixlive).
500
     */
501 1
    protected function blockNote($block)
502
    {
503 1
        if (preg_match('/:::(.*)/', $block['text'], $matches)) {
504 1
            $block = [
505 1
                'char'    => ':',
506 1
                'element' => [
507 1
                    'name'       => 'aside',
508 1
                    'text'       => '',
509 1
                    'attributes' => [
510 1
                        'class' => 'note',
511 1
                    ],
512 1
                ],
513 1
            ];
514 1
            if (!empty($matches[1])) {
515 1
                $block['element']['attributes']['class'] .= " note-{$matches[1]}";
516
            }
517
518 1
            return $block;
519
        }
520
    }
521
522 1
    protected function blockNoteContinue($line, $block)
523
    {
524 1
        if (isset($block['complete'])) {
525 1
            return;
526
        }
527 1
        if (preg_match('/:::/', $line['text'])) {
528 1
            $block['complete'] = true;
529
530 1
            return $block;
531
        }
532 1
        $block['element']['text'] .= $line['text'] . "\n";
533
534 1
        return $block;
535
    }
536
537 1
    protected function blockNoteComplete($block)
538
    {
539 1
        $block['element']['rawHtml'] = $this->text($block['element']['text']);
540 1
        unset($block['element']['text']);
541
542 1
        return $block;
543
    }
544
545
    /**
546
     * Apply Highlight to code blocks.
547
     */
548 1
    protected function blockFencedCodeComplete($block)
549
    {
550 1
        if (!$this->config->isEnabled('pages.body.highlight')) {
551
            return $block;
552
        }
553 1
        if (!isset($block['element']['text']['attributes'])) {
554
            return $block;
555
        }
556
557
        try {
558 1
            $code = $block['element']['text']['text'];
559 1
            $languageClass = $block['element']['text']['attributes']['class'];
560 1
            $language = explode('-', $languageClass);
561 1
            $highlighted = $this->highlighter->highlight($language[1], $code);
562 1
            $block['element']['text']['attributes']['class'] = vsprintf('%s hljs %s', [
563 1
                $languageClass,
564 1
                $highlighted->language,
565 1
            ]);
566 1
            $block['element']['text']['rawHtml'] = $highlighted->value;
567 1
            $block['element']['text']['allowRawHtmlInSafeMode'] = true;
568 1
            unset($block['element']['text']['text']);
569
        } catch (\Exception $e) {
570
            $this->builder->getLogger()->debug("Highlighter: " . $e->getMessage());
571
        } finally {
572 1
            return $block;
573
        }
574
    }
575
576
    /**
577
     * {@inheritdoc}
578
     */
579 1
    protected function parseAttributeData($attributeString)
580
    {
581 1
        $attributes = preg_split('/[ ]+/', $attributeString, -1, PREG_SPLIT_NO_EMPTY);
582 1
        $Data = [];
583 1
        $HtmlAtt = [];
584
585 1
        if (is_iterable($attributes)) {
586 1
            foreach ($attributes as $attribute) {
587 1
                switch ($attribute[0]) {
588 1
                    case '#': // ID
589 1
                        $Data['id'] = substr($attribute, 1);
590 1
                        break;
591 1
                    case '.': // Classes
592 1
                        $classes[] = substr($attribute, 1);
593 1
                        break;
594
                    default:  // Attributes
595 1
                        parse_str($attribute, $parsed);
596 1
                        $HtmlAtt = array_merge($HtmlAtt, $parsed);
597
                }
598
            }
599
600 1
            if (isset($classes)) {
601 1
                $Data['class'] = implode(' ', $classes);
602
            }
603 1
            if (!empty($HtmlAtt)) {
604 1
                foreach ($HtmlAtt as $a => $v) {
605 1
                    $Data[$a] = trim($v, '"');
606
                }
607
            }
608
        }
609
610 1
        return $Data;
611
    }
612
613
    /**
614
     * {@inheritdoc}
615
     *
616
     * Converts XHTML '<br />' tag to '<br>'.
617
     *
618
     * @return string
619
     */
620 1
    protected function unmarkedText($text)
621
    {
622 1
        return str_replace('<br />', '<br>', parent::unmarkedText($text)); // @phpstan-ignore staticMethod.notFound
623
    }
624
625
    /**
626
     * {@inheritdoc}
627
     *
628
     * XHTML closing tag to HTML5 closing tag.
629
     *
630
     * @return string
631
     */
632 1
    protected function element(array $Element)
633
    {
634 1
        return str_replace(' />', '>', parent::element($Element)); // @phpstan-ignore staticMethod.notFound
635
    }
636
637
    /**
638
     * Turns a path relative to static or assets into a website relative path.
639
     *
640
     *   "../../assets/images/img.jpeg"
641
     *   ->
642
     *   "/images/img.jpeg"
643
     */
644 1
    private function normalizePath(string $path): string
645
    {
646
        // https://regex101.com/r/Rzguzh/1
647 1
        $pattern = \sprintf(
648 1
            '(\.\.\/)+(\b%s|%s\b)+(\/.*)',
649 1
            (string) $this->config->get('static.dir'),
650 1
            (string) $this->config->get('assets.dir')
651 1
        );
652 1
        $path = Util::joinPath($path);
653 1
        if (!preg_match('/' . $pattern . '/is', $path, $matches)) {
654 1
            return $path;
655
        }
656
657 1
        return $matches[3];
658
    }
659
660
    /**
661
     * Create a media (video or audio) element from a link.
662
     */
663 1
    private function createMediaFromLink(array $link, string $type = 'video'): array
664
    {
665 1
        $block = [
666 1
            'extent'  => $link['extent'],
667 1
            'element' => [
668 1
                'text' => $link['element']['text'],
669 1
            ],
670 1
        ];
671 1
        $block['element']['attributes'] = $link['element']['attributes'];
672 1
        unset($block['element']['attributes']['href']);
673 1
        $block['element']['attributes']['src'] = new Url($this->builder, new Asset($this->builder, $link['element']['attributes']['href']));
674
        switch ($type) {
675 1
            case 'video':
676 1
                $block['element']['name'] = 'video';
677
                // no controls = autoplay, loop, muted, playsinline
678 1
                if (!isset($block['element']['attributes']['controls'])) {
679 1
                    $block['element']['attributes']['autoplay'] = '';
680 1
                    $block['element']['attributes']['loop'] = '';
681 1
                    $block['element']['attributes']['muted'] = '';
682 1
                    $block['element']['attributes']['playsinline'] = '';
683
                }
684 1
                if (isset($block['element']['attributes']['poster'])) {
685 1
                    $block['element']['attributes']['poster'] = new Url($this->builder, new Asset($this->builder, $block['element']['attributes']['poster']));
686
                }
687 1
                if (!\array_key_exists('style', $block['element']['attributes'])) {
688 1
                    $block['element']['attributes']['style'] = '';
689
                }
690 1
                $block['element']['attributes']['style'] .= ';max-width:100%;height:auto;background-color:#d8d8d8;'; // background color if offline
691
692 1
                return $block;
693 1
            case 'audio':
694 1
                $block['element']['name'] = 'audio';
695
696 1
                return $block;
697
        }
698
699
        throw new \Exception(\sprintf('Can\'t create %s from "%s".', $type, $link['element']['attributes']['href']));
700
    }
701
702
    /**
703
     * Create an embedded video element from a link.
704
     *
705
     * $baseSrc is the base URL to embed the video
706
     * $match is the video ID or the rest of the URL to append to $baseSrc
707
     */
708 1
    private function createEmbeddedVideoFromLink(array $link, string $baseSrc, string $match): array
709
    {
710 1
        $iframe = [
711 1
            'element' => [
712 1
                'name'       => 'iframe',
713 1
                'text'       => $link['element']['text'],
714 1
                'attributes' => [
715 1
                    'width'           => '640',
716 1
                    'height'          => '360',
717 1
                    'title'           => $link['element']['text'],
718 1
                    'src'             => Util::joinPath($baseSrc, $match),
719 1
                    'frameborder'     => '0',
720 1
                    'allow'           => 'accelerometer;autoplay;encrypted-media;gyroscope;picture-in-picture;',
721 1
                    'allowfullscreen' => '',
722 1
                    'style'           => 'position:absolute;top:0;left:0;width:100%;height:100%;border:0;background-color:#d8d8d8;',
723 1
                ],
724 1
            ],
725 1
        ];
726 1
        $div = [
727 1
            'extent'  => $link['extent'],
728 1
            'element' => [
729 1
                'name'    => 'div',
730 1
                'handler' => 'elements',
731 1
                'text'    => [
732 1
                    $iframe['element'],
733 1
                ],
734 1
                'attributes' => [
735 1
                    'style' => 'position:relative;padding-bottom:56.25%;height:0;overflow:hidden;',
736 1
                    'title' => $link['element']['attributes']['title'],
737 1
                ],
738 1
            ],
739 1
        ];
740 1
        if ($this->config->isEnabled('pages.body.images.caption')) {
741 1
            return $this->createFigure($div);
742
        }
743
744
        return $div;
745
    }
746
747
    /**
748
     * Create a figure / caption element.
749
     */
750 1
    private function createFigure(array $inline): array
751
    {
752 1
        if (empty($inline['element']['attributes']['title'])) {
753 1
            return $inline;
754
        }
755
756 1
        $titleRawHtml = $this->line($inline['element']['attributes']['title']); // @phpstan-ignore method.notFound
757 1
        $inline['element']['attributes']['title'] = strip_tags($titleRawHtml);
758
759 1
        $figcaption = [
760 1
            'element' => [
761 1
                'name'                   => 'figcaption',
762 1
                'allowRawHtmlInSafeMode' => true,
763 1
                'rawHtml'                => $titleRawHtml,
764 1
            ],
765 1
        ];
766 1
        $figure = [
767 1
            'extent'  => $inline['extent'],
768 1
            'element' => [
769 1
                'name'    => 'figure',
770 1
                'handler' => 'elements',
771 1
                'text'    => [
772 1
                    $inline['element'],
773 1
                    $figcaption['element'],
774 1
                ],
775 1
            ],
776 1
        ];
777
778 1
        return $figure;
779
    }
780
781
    /**
782
     * Handle an external link.
783
     */
784 1
    private function handleExternalLink(array $link): array
785
    {
786
        if (
787 1
            str_starts_with($link['element']['attributes']['href'], 'http')
788 1
            && (!empty($this->config->get('baseurl')) && !str_starts_with($link['element']['attributes']['href'], (string) $this->config->get('baseurl')))
789
        ) {
790 1
            if ($this->config->isEnabled('pages.body.links.external.blank')) {
791 1
                $link['element']['attributes']['target'] = '_blank';
792
            }
793 1
            if (!\array_key_exists('rel', $link['element']['attributes'])) {
794 1
                $link['element']['attributes']['rel'] = '';
795
            }
796 1
            if ($this->config->isEnabled('pages.body.links.external.noopener')) {
797 1
                $link['element']['attributes']['rel'] .= ' noopener';
798
            }
799 1
            if ($this->config->isEnabled('pages.body.links.external.noreferrer')) {
800 1
                $link['element']['attributes']['rel'] .= ' noreferrer';
801
            }
802 1
            if ($this->config->isEnabled('pages.body.links.external.nofollow')) {
803 1
                $link['element']['attributes']['rel'] .= ' nofollow';
804
            }
805 1
            $link['element']['attributes']['rel'] = trim($link['element']['attributes']['rel']);
806
        }
807
808 1
        return $link;
809
    }
810
}
811