Parsedown::inlineLink()   C
last analyzed

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