Passed
Push — master ( 3fa515...b061c6 )
by
unknown
05:09
created

Parsedown::inlineLink()   C

Complexity

Conditions 14
Paths 32

Size

Total Lines 93
Code Lines 49

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 52
CRAP Score 14.0013

Importance

Changes 6
Bugs 3 Features 1
Metric Value
cc 14
eloc 49
c 6
b 3
f 1
nc 32
nop 1
dl 0
loc 93
ccs 52
cts 53
cp 0.9811
crap 14.0013
rs 6.2666

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