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