Passed
Push — master ( a34110...9000cd )
by Arnaud
04:52
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
        // pages.body.images.resize
305
        if (
306 1
            \is_int($this->config->get('pages.body.images.resize'))
307 1
            && $this->config->get('pages.body.images.resize') > 0
308 1
            && $width > $this->config->get('pages.body.images.resize')
309
        ) {
310
            $shouldResize = true;
311
            $width = $this->config->get('pages.body.images.resize');
312
        }
313
        // width attribute
314
        if (
315 1
            isset($InlineImage['element']['attributes']['width'])
316 1
            && $width > (int) $InlineImage['element']['attributes']['width']
317
        ) {
318 1
            $shouldResize = true;
319 1
            $width = (int) $InlineImage['element']['attributes']['width'];
320
        }
321
        // responsive images
322
        if (
323 1
            $this->config->isEnabled('pages.body.images.responsive')
324 1
            && !empty($this->config->getAssetsImagesWidths())
325 1
            && $width > max($this->config->getAssetsImagesWidths())
326
        ) {
327
            $shouldResize = true;
328
            $width = max($this->config->getAssetsImagesWidths());
329
        }
330 1
        if ($shouldResize) {
331
            try {
332 1
                $assetResized = $asset->resize($width);
333 1
                $InlineImage['element']['attributes']['src'] = $assetResized;
334
            } catch (\Exception $e) {
335
                $this->builder->getLogger()->debug($e->getMessage());
336
337
                return $InlineImage;
338
            }
339
        }
340
341
        // set width
342 1
        $InlineImage['element']['attributes']['width'] = $width;
343
        // set height
344 1
        $InlineImage['element']['attributes']['height'] = $assetResized['height'] ?? $asset['height'];
345
346
        // placeholder
347
        if (
348 1
            (!empty($this->config->get('pages.body.images.placeholder')) || isset($InlineImage['element']['attributes']['placeholder']))
349 1
            && \in_array($InlineImage['element']['attributes']['src']['subtype'], ['image/jpeg', 'image/png', 'image/gif'])
350
        ) {
351 1
            if (!\array_key_exists('placeholder', $InlineImage['element']['attributes'])) {
352 1
                $InlineImage['element']['attributes']['placeholder'] = (string) $this->config->get('pages.body.images.placeholder');
353
            }
354 1
            if (!\array_key_exists('style', $InlineImage['element']['attributes'])) {
355 1
                $InlineImage['element']['attributes']['style'] = '';
356
            }
357 1
            $InlineImage['element']['attributes']['style'] = trim($InlineImage['element']['attributes']['style'], ';');
358 1
            switch ($InlineImage['element']['attributes']['placeholder']) {
359 1
                case 'color':
360 1
                    $InlineImage['element']['attributes']['style'] .= \sprintf(';max-width:100%%;height:auto;background-color:%s;', Image::getDominantColor($InlineImage['element']['attributes']['src']));
361 1
                    break;
362 1
                case 'lqip':
363
                    // aborts if animated GIF for performance reasons
364 1
                    if (Image::isAnimatedGif($InlineImage['element']['attributes']['src'])) {
365
                        break;
366
                    }
367 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']));
368 1
                    break;
369
            }
370 1
            unset($InlineImage['element']['attributes']['placeholder']);
371 1
            $InlineImage['element']['attributes']['style'] = trim($InlineImage['element']['attributes']['style']);
372
        }
373
374
        /*
375
         * Should be responsive?
376
         */
377 1
        $sizes = '';
378 1
        if ($this->config->isEnabled('pages.body.images.responsive')) {
379
            try {
380
                if (
381 1
                    $srcset = Image::buildSrcset(
382 1
                        $assetResized ?? $asset,
383 1
                        $this->config->getAssetsImagesWidths()
384 1
                    )
385
                ) {
386 1
                    $InlineImage['element']['attributes']['srcset'] = $srcset;
387 1
                    $sizes = Image::getSizes($InlineImage['element']['attributes']['class'] ?? '', (array) $this->config->getAssetsImagesSizes());
388 1
                    $InlineImage['element']['attributes']['sizes'] = $sizes;
389
                }
390 1
            } catch (\Exception $e) {
391 1
                $this->builder->getLogger()->debug($e->getMessage());
392
            }
393
        }
394
395
        /*
396
        <!-- if title: a <figure> is required to put in it a <figcaption> -->
397
        <figure>
398
            <!-- if formats: a <picture> is required for each <source> -->
399
            <picture>
400
                <source type="image/avif"
401
                    srcset="..."
402
                    sizes="..."
403
                >
404
                <source type="image/webp"
405
                    srcset="..."
406
                    sizes="..."
407
                >
408
                <img src="..."
409
                    srcset="..."
410
                    sizes="..."
411
                >
412
            </picture>
413
            <figcaption><!-- title --></figcaption>
414
        </figure>
415
        */
416
417 1
        $image = $InlineImage;
418
419
        // converts image to formats and put them in picture > source
420
        if (
421 1
            \count($formats = ((array) $this->config->get('pages.body.images.formats'))) > 0
422 1
            && \in_array($InlineImage['element']['attributes']['src']['subtype'], ['image/jpeg', 'image/png', 'image/gif'])
423
        ) {
424
            try {
425
                // InlineImage src must be an Asset instance
426 1
                if (!$InlineImage['element']['attributes']['src'] instanceof Asset) {
427
                    throw new RuntimeException(\sprintf('Asset "%s" can\'t be converted.', $InlineImage['element']['attributes']['src']));
428
                }
429
                // abord if InlineImage is an animated GIF
430 1
                if (Image::isAnimatedGif($InlineImage['element']['attributes']['src'])) {
431 1
                    $filepath = Util::joinFile($this->config->getOutputPath(), $InlineImage['element']['attributes']['src']['path']);
432 1
                    throw new RuntimeException(\sprintf('Asset "%s" is not converted (animated GIF).', $filepath));
433
                }
434 1
                $sources = [];
435 1
                foreach ($formats as $format) {
436 1
                    $srcset = '';
437
                    try {
438 1
                        $assetConverted = $InlineImage['element']['attributes']['src']->$format();
439
                    } catch (\Exception $e) {
440
                        $this->builder->getLogger()->debug($e->getMessage());
441
                        continue;
442
                    }
443
                    // build responsive images?
444 1
                    if ($this->config->isEnabled('pages.body.images.responsive')) {
445
                        try {
446 1
                            $srcset = Image::buildSrcset($assetConverted, $this->config->getAssetsImagesWidths());
447
                        } catch (\Exception $e) {
448
                            $this->builder->getLogger()->debug($e->getMessage());
449
                        }
450
                    }
451
                    // if not, use default image as srcset
452 1
                    if (empty($srcset)) {
453 1
                        $srcset = (string) $assetConverted;
454
                    }
455
                    // add format to <sources>
456 1
                    $sources[] = [
457 1
                        'name'       => 'source',
458 1
                        'attributes' => [
459 1
                            'type'   => "image/$format",
460 1
                            'srcset' => $srcset,
461 1
                            'sizes'  => $sizes,
462 1
                            'width'  => $InlineImage['element']['attributes']['width'],
463 1
                            'height' => $InlineImage['element']['attributes']['height'],
464 1
                        ],
465 1
                    ];
466
                }
467 1
                if (\count($sources) > 0) {
468 1
                    $picture = [
469 1
                        'extent'  => $InlineImage['extent'],
470 1
                        'element' => [
471 1
                            'name'       => 'picture',
472 1
                            'handler'    => 'elements',
473 1
                            'attributes' => [
474 1
                                'title' => $image['element']['attributes']['title'],
475 1
                            ],
476 1
                        ],
477 1
                    ];
478 1
                    $picture['element']['text'] = $sources;
479 1
                    unset($image['element']['attributes']['title']); // @phpstan-ignore unset.offset
480 1
                    $picture['element']['text'][] = $image['element'];
481 1
                    $image = $picture;
482
                }
483 1
            } catch (\Exception $e) {
484 1
                $this->builder->getLogger()->debug($e->getMessage());
485
            }
486
        }
487
488
        // if title: put the <img> (or <picture>) in a <figure> and create a <figcaption>
489 1
        if ($this->config->isEnabled('pages.body.images.caption')) {
490 1
            return $this->createFigure($image);
491
        }
492
493
        return $image;
494
    }
495
496
    /**
497
     * Image block.
498
     */
499 1
    protected function blockImage($Excerpt)
500
    {
501 1
        if (1 !== preg_match($this->regexImage, $Excerpt['text'])) {
502
            return;
503
        }
504
505 1
        $InlineImage = $this->inlineImage($Excerpt);
506 1
        if (!isset($InlineImage)) {
507
            return;
508
        }
509
510 1
        return $InlineImage;
511
    }
512
513
    /**
514
     * Note block-level markup.
515
     *
516
     * :::tip
517
     * **Tip:** This is an advice.
518
     * :::
519
     *
520
     * Code inspired by https://github.com/sixlive/parsedown-alert from TJ Miller (@sixlive).
521
     */
522 1
    protected function blockNote($block)
523
    {
524 1
        if (preg_match('/:::(.*)/', $block['text'], $matches)) {
525 1
            $block = [
526 1
                'char'    => ':',
527 1
                'element' => [
528 1
                    'name'       => 'aside',
529 1
                    'text'       => '',
530 1
                    'attributes' => [
531 1
                        'class' => 'note',
532 1
                    ],
533 1
                ],
534 1
            ];
535 1
            if (!empty($matches[1])) {
536 1
                $block['element']['attributes']['class'] .= " note-{$matches[1]}";
537
            }
538
539 1
            return $block;
540
        }
541
    }
542
543 1
    protected function blockNoteContinue($line, $block)
544
    {
545 1
        if (isset($block['complete'])) {
546 1
            return;
547
        }
548 1
        if (preg_match('/:::/', $line['text'])) {
549 1
            $block['complete'] = true;
550
551 1
            return $block;
552
        }
553 1
        $block['element']['text'] .= $line['text'] . "\n";
554
555 1
        return $block;
556
    }
557
558 1
    protected function blockNoteComplete($block)
559
    {
560 1
        $block['element']['rawHtml'] = $this->text($block['element']['text']);
561 1
        unset($block['element']['text']);
562
563 1
        return $block;
564
    }
565
566
    /**
567
     * Apply Highlight to code blocks.
568
     */
569 1
    protected function blockFencedCodeComplete($block)
570
    {
571 1
        if (!$this->config->isEnabled('pages.body.highlight')) {
572
            return $block;
573
        }
574 1
        if (!isset($block['element']['text']['attributes'])) {
575
            return $block;
576
        }
577
578
        try {
579 1
            $code = $block['element']['text']['text'];
580 1
            $languageClass = $block['element']['text']['attributes']['class'];
581 1
            $language = explode('-', $languageClass);
582 1
            $highlighted = $this->highlighter->highlight($language[1], $code);
583 1
            $block['element']['text']['attributes']['class'] = vsprintf('%s hljs %s', [
584 1
                $languageClass,
585 1
                $highlighted->language,
586 1
            ]);
587 1
            $block['element']['text']['rawHtml'] = $highlighted->value;
588 1
            $block['element']['text']['allowRawHtmlInSafeMode'] = true;
589 1
            unset($block['element']['text']['text']);
590
        } catch (\Exception $e) {
591
            $this->builder->getLogger()->debug($e->getMessage());
592
        } finally {
593 1
            return $block;
594
        }
595
    }
596
597
    /**
598
     * {@inheritdoc}
599
     */
600 1
    protected function parseAttributeData($attributeString)
601
    {
602 1
        $attributes = preg_split('/[ ]+/', $attributeString, -1, PREG_SPLIT_NO_EMPTY);
603 1
        $Data = [];
604 1
        $HtmlAtt = [];
605
606 1
        if (is_iterable($attributes)) {
607 1
            foreach ($attributes as $attribute) {
608 1
                switch ($attribute[0]) {
609 1
                    case '#': // ID
610 1
                        $Data['id'] = substr($attribute, 1);
611 1
                        break;
612 1
                    case '.': // Classes
613 1
                        $classes[] = substr($attribute, 1);
614 1
                        break;
615
                    default:  // Attributes
616 1
                        parse_str($attribute, $parsed);
617 1
                        $HtmlAtt = array_merge($HtmlAtt, $parsed);
618
                }
619
            }
620
621 1
            if (isset($classes)) {
622 1
                $Data['class'] = implode(' ', $classes);
623
            }
624 1
            if (!empty($HtmlAtt)) {
625 1
                foreach ($HtmlAtt as $a => $v) {
626 1
                    $Data[$a] = trim($v, '"');
627
                }
628
            }
629
        }
630
631 1
        return $Data;
632
    }
633
634
    /**
635
     * {@inheritdoc}
636
     *
637
     * Converts XHTML '<br />' tag to '<br>'.
638
     *
639
     * @return string
640
     */
641 1
    protected function unmarkedText($text)
642
    {
643 1
        return str_replace('<br />', '<br>', parent::unmarkedText($text)); // @phpstan-ignore staticMethod.notFound
644
    }
645
646
    /**
647
     * {@inheritdoc}
648
     *
649
     * XHTML closing tag to HTML5 closing tag.
650
     *
651
     * @return string
652
     */
653 1
    protected function element(array $Element)
654
    {
655 1
        return str_replace(' />', '>', parent::element($Element)); // @phpstan-ignore staticMethod.notFound
656
    }
657
658
    /**
659
     * Turns a path relative to static or assets into a website relative path.
660
     *
661
     *   "../../assets/images/img.jpeg"
662
     *   ->
663
     *   "/images/img.jpeg"
664
     */
665 1
    private function normalizePath(string $path): string
666
    {
667
        // https://regex101.com/r/Rzguzh/1
668 1
        $pattern = \sprintf(
669 1
            '(\.\.\/)+(\b%s|%s\b)+(\/.*)',
670 1
            (string) $this->config->get('static.dir'),
671 1
            (string) $this->config->get('assets.dir')
672 1
        );
673 1
        $path = Util::joinPath($path);
674 1
        if (!preg_match('/' . $pattern . '/is', $path, $matches)) {
675 1
            return $path;
676
        }
677
678 1
        return $matches[3];
679
    }
680
681
    /**
682
     * Create a media (video or audio) element from a link.
683
     */
684 1
    private function createMediaFromLink(array $link, string $type = 'video'): array
685
    {
686 1
        $block = [
687 1
            'extent'  => $link['extent'],
688 1
            'element' => [
689 1
                'text' => $link['element']['text'],
690 1
            ],
691 1
        ];
692 1
        $block['element']['attributes'] = $link['element']['attributes'];
693 1
        unset($block['element']['attributes']['href']);
694 1
        $block['element']['attributes']['src'] = (string) new Asset($this->builder, $link['element']['attributes']['href'], ['leading_slash' => false]);
695
        switch ($type) {
696 1
            case 'video':
697 1
                $block['element']['name'] = 'video';
698
                // no controls = autoplay, loop, muted, playsinline
699 1
                if (!isset($block['element']['attributes']['controls'])) {
700 1
                    $block['element']['attributes']['autoplay'] = '';
701 1
                    $block['element']['attributes']['loop'] = '';
702 1
                    $block['element']['attributes']['muted'] = '';
703 1
                    $block['element']['attributes']['playsinline'] = '';
704
                }
705 1
                if (isset($block['element']['attributes']['poster'])) {
706 1
                    $block['element']['attributes']['poster'] = (string) new Asset($this->builder, $block['element']['attributes']['poster'], ['leading_slash' => false]);
707
                }
708 1
                if (!\array_key_exists('style', $block['element']['attributes'])) {
709 1
                    $block['element']['attributes']['style'] = '';
710
                }
711 1
                $block['element']['attributes']['style'] .= ';max-width:100%;height:auto;background-color: #d8d8d8;'; // background color if offline
712
713 1
                return $block;
714 1
            case 'audio':
715 1
                $block['element']['name'] = 'audio';
716
717 1
                return $block;
718
        }
719
720
        throw new \Exception(\sprintf('Can\'t create %s from "%s".', $type, $link['element']['attributes']['href']));
721
    }
722
723
    /**
724
     * Create a figure / caption element.
725
     */
726 1
    private function createFigure(array $inline): array
727
    {
728 1
        if (empty($inline['element']['attributes']['title'])) {
729 1
            return $inline;
730
        }
731
732 1
        $titleRawHtml = $this->line($inline['element']['attributes']['title']); // @phpstan-ignore method.notFound
733 1
        $inline['element']['attributes']['title'] = strip_tags($titleRawHtml);
734
735 1
        $figcaption = [
736 1
            'element' => [
737 1
                'name'                   => 'figcaption',
738 1
                'allowRawHtmlInSafeMode' => true,
739 1
                'rawHtml'                => $titleRawHtml,
740 1
            ],
741 1
        ];
742 1
        $figure = [
743 1
            'extent'  => $inline['extent'],
744 1
            'element' => [
745 1
                'name'    => 'figure',
746 1
                'handler' => 'elements',
747 1
                'text'    => [
748 1
                    $inline['element'],
749 1
                    $figcaption['element'],
750 1
                ],
751 1
            ],
752 1
        ];
753
754 1
        return $figure;
755
    }
756
757
    /**
758
     * Handle an external link.
759
     */
760 1
    private function handleExternalLink(array $link): array
761
    {
762
        if (
763 1
            str_starts_with($link['element']['attributes']['href'], 'http')
764 1
            && (!empty($this->config->get('baseurl')) && !str_starts_with($link['element']['attributes']['href'], (string) $this->config->get('baseurl')))
765
        ) {
766 1
            if ($this->config->isEnabled('pages.body.links.external.blank')) {
767 1
                $link['element']['attributes']['target'] = '_blank';
768
            }
769 1
            if (!\array_key_exists('rel', $link['element']['attributes'])) {
770 1
                $link['element']['attributes']['rel'] = '';
771
            }
772 1
            if ($this->config->isEnabled('pages.body.links.external.noopener')) {
773 1
                $link['element']['attributes']['rel'] .= ' noopener';
774
            }
775 1
            if ($this->config->isEnabled('pages.body.links.external.noreferrer')) {
776 1
                $link['element']['attributes']['rel'] .= ' noreferrer';
777
            }
778 1
            if ($this->config->isEnabled('pages.body.links.external.nofollow')) {
779 1
                $link['element']['attributes']['rel'] .= ' nofollow';
780
            }
781 1
            $link['element']['attributes']['rel'] = trim($link['element']['attributes']['rel']);
782
        }
783
784 1
        return $link;
785
    }
786
}
787