Passed
Push — master ( 2df374...78e7d4 )
by
unknown
15:21 queued 12s
created

Parsedown::inlineImage()   F

Complexity

Conditions 44
Paths > 20000

Size

Total Lines 237
Code Lines 131

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 86
CRAP Score 109.1187

Importance

Changes 3
Bugs 1 Features 0
Metric Value
cc 44
eloc 131
c 3
b 1
f 0
nc 2193805
nop 1
dl 0
loc 237
ccs 86
cts 127
cp 0.6772
crap 109.1187
rs 0

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']->convert($format);
439 1
                    } catch (\Exception $e) {
440 1
                        $this->builder->getLogger()->debug($e->getMessage());
441 1
                        continue;
442
                    }
443
                    // build responsive images?
444
                    if ($this->config->isEnabled('pages.body.images.responsive')) {
445
                        try {
446
                            $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
                    if (empty($srcset)) {
453
                        $srcset = (string) $assetConverted;
454
                    }
455
                    // add format to <sources>
456
                    $sources[] = [
457
                        'name'       => 'source',
458
                        'attributes' => [
459
                            'type'   => "image/$format",
460
                            'srcset' => $srcset,
461
                            'sizes'  => $sizes,
462
                            'width'  => $InlineImage['element']['attributes']['width'],
463
                            'height' => $InlineImage['element']['attributes']['height'],
464
                        ],
465
                    ];
466
                }
467 1
                if (\count($sources) > 0) {
468
                    $picture = [
469
                        'extent'  => $InlineImage['extent'],
470
                        'element' => [
471
                            'name'       => 'picture',
472
                            'handler'    => 'elements',
473
                            'attributes' => [
474
                                'title' => $image['element']['attributes']['title'],
475
                            ],
476
                        ],
477
                    ];
478
                    $picture['element']['text'] = $sources;
479
                    unset($image['element']['attributes']['title']); // @phpstan-ignore unset.offset
480
                    $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