Passed
Push — php-8.2 ( c584aa...c3c2f5 )
by Arnaud
03:55
created

Parsedown::inlineLink()   C

Complexity

Conditions 13
Paths 26

Size

Total Lines 75
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 37
CRAP Score 13.003

Importance

Changes 5
Bugs 2 Features 1
Metric Value
cc 13
eloc 35
c 5
b 2
f 1
nc 26
nop 1
dl 0
loc 75
ccs 37
cts 38
cp 0.9737
crap 13.003
rs 6.6166

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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