Parsedown::inlineImage()   F
last analyzed

Complexity

Conditions 42
Paths > 20000

Size

Total Lines 230
Code Lines 127

Duplication

Lines 0
Ratio 0 %

Importance

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