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 45
CRAP Score 13.0017

Importance

Changes 6
Bugs 2 Features 1
Metric Value
cc 13
eloc 35
c 6
b 2
f 1
nc 26
nop 1
dl 0
loc 75
ccs 45
cts 46
cp 0.9783
crap 13.0017
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';
66 1
        $this->inlineMarkerList = implode('', array_keys($this->InlineTypes));
67 1
        $this->specialCharacters[] = '+';
68
69
        // Image block (to avoid paragraph)
70 1
        $this->BlockTypes['!'][] = 'Image';
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 1
167 1
        /*
168 1
         * Embed link to a service resource?
169 1
         * e.g.: YouTube, Vimeo, Dailymotion, GitHub Gist.
170 1
         */
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 1
            }
182
        }
183 1
184
        return $link;
185
    }
186
187 1
    /**
188
     * {@inheritdoc}
189
     */
190
    protected function inlineUrl($Excerpt)
191
    {
192
        $link = parent::inlineUrl($Excerpt); // @phpstan-ignore staticMethod.notFound
193 1
194
        if (!isset($link)) {
195 1
            return;
196
        }
197 1
198 1
        // 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
    protected function inlineImage($Excerpt)
221
    {
222
        $InlineImage = parent::inlineImage($Excerpt); // @phpstan-ignore staticMethod.notFound
223 1
        if (!isset($InlineImage)) {
224
            return null;
225 1
        }
226 1
227
        // remove link attributes
228
        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
            $InlineImage['element']['attributes']['loading'] = 'lazy';
236
        }
237 1
        // 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
        }
241 1
        // add default class?
242 1
        if ((string) $this->config->get('pages.body.images.class')) {
243
            if (!\array_key_exists('class', $InlineImage['element']['attributes'])) {
244
                $InlineImage['element']['attributes']['class'] = '';
245 1
            }
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 1
250 1
        // disable remote image handling?
251
        if (Util\File::isRemote($InlineImage['element']['attributes']['src']) && !$this->config->isEnabled('pages.body.images.remote')) {
252
            return $this->createFigure($InlineImage);
253
        }
254 1
255
        // create asset
256
        $assetOptions = ['leading_slash' => false];
257
        if ($this->config->isEnabled('pages.body.images.remote.fallback')) {
258
            $assetOptions = ['leading_slash' => true];
259 1
            $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 1
         * Should be resized?
267
         */
268
        $shouldResize = false;
269
        $assetResized = null;
270
        // pages.body.images.resize
271 1
        if (
272 1
            \is_int($this->config->get('pages.body.images.resize'))
273
            && $this->config->get('pages.body.images.resize') > 0
274
            && $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
        // width attribute
280
        if (
281
            isset($InlineImage['element']['attributes']['width'])
282
            && $width > (int) $InlineImage['element']['attributes']['width']
283
        ) {
284 1
            $shouldResize = true;
285 1
            $width = (int) $InlineImage['element']['attributes']['width'];
286
        }
287 1
        // responsive images
288 1
        if (
289
            $this->config->isEnabled('pages.body.images.responsive')
290
            && !empty($this->config->getAssetsImagesWidths())
291
            && $width > max($this->config->getAssetsImagesWidths())
292 1
        ) {
293 1
            $shouldResize = true;
294 1
            $width = max($this->config->getAssetsImagesWidths());
295
        }
296
        if ($shouldResize) {
297
            try {
298
                $assetResized = $asset->resize($width);
299 1
            } catch (\Exception $e) {
300
                $this->builder->getLogger()->debug($e->getMessage());
301 1
302
                return $this->createFigure($InlineImage);
303
            }
304
        }
305
306
        // set width
307
        $InlineImage['element']['attributes']['width'] = $width;
308
        // set height
309
        $InlineImage['element']['attributes']['height'] = $assetResized['height'] ?? $asset['height'];
310 1
311
        // placeholder
312 1
        if (
313
            (!empty($this->config->get('pages.body.images.placeholder')) || isset($InlineImage['element']['attributes']['placeholder']))
314
            && \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
                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 1
                    // aborts if animated GIF for performance reasons
329 1
                    if (Image::isAnimatedGif($assetResized ?? $asset)) {
330 1
                        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
                    break;
334
            }
335 1
            unset($InlineImage['element']['attributes']['placeholder']);
336 1
            $InlineImage['element']['attributes']['style'] = trim($InlineImage['element']['attributes']['style']);
337
        }
338 1
339 1
        /*
340
         * Should be responsive?
341
         */
342
        $sizes = '';
343
        if ($this->config->isEnabled('pages.body.images.responsive')) {
344
            try {
345 1
                if (
346 1
                    $srcset = Image::buildHtmlSrcsetW(
347
                        $assetResized ?? $asset,
348
                        $this->config->getAssetsImagesWidths()
349 1
                    )
350 1
                ) {
351 1
                    $InlineImage['element']['attributes']['srcset'] = $srcset;
352 1
                    $sizes = Image::getHtmlSizes($InlineImage['element']['attributes']['class'] ?? '', (array) $this->config->getAssetsImagesSizes());
353
                    $InlineImage['element']['attributes']['sizes'] = $sizes;
354 1
                }
355 1
            } catch (\Exception $e) {
356 1
                $this->builder->getLogger()->warning($e->getMessage());
357
            }
358 1
        }
359 1
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
        $image = $InlineImage;
383
384
        // converts image to formats and put them in picture > source
385 1
        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 1
            try {
390 1
                // 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 1
                }
395 1
                $sources = [];
396 1
                foreach ($formats as $format) {
397
                    $srcset = '';
398 1
                    try {
399 1
                        $assetConverted = ($assetResized ?? $asset)->convert($format);
400 1
                        // build responsive images?
401
                        if ($this->config->isEnabled('pages.body.images.responsive')) {
402 1
                            $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
                                'sizes'  => $sizes,
415
                                'width'  => $InlineImage['element']['attributes']['width'],
416
                                'height' => $InlineImage['element']['attributes']['height'],
417
                            ],
418
                        ];
419
                    } catch (\Exception $e) {
420
                        $this->builder->getLogger()->warning($e->getMessage());
421
                        continue;
422 1
                    }
423 1
                }
424 1
                if (\count($sources) > 0) {
425
                    $picture = [
426
                        'extent'  => $InlineImage['extent'],
427 1
                        '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
                    $picture['element']['text'][] = $image['element'];
438
                    $image = $picture;
439
                }
440
            } catch (\Exception $e) {
441 1
                $this->builder->getLogger()->debug($e->getMessage());
442
            }
443 1
        }
444 1
445
        return $this->createFigure($image);
446
    }
447
448 1
    /**
449
     * Image block.
450
     */
451
    protected function blockImage($Excerpt)
452
    {
453
        if (1 !== preg_match($this->regexImage, $Excerpt['text'])) {
454 1
            return;
455
        }
456 1
457
        $InlineImage = $this->inlineImage($Excerpt);
458
        if (!isset($InlineImage)) {
459
            return;
460 1
        }
461 1
462
        return $InlineImage;
463
    }
464
465 1
    /**
466
     * Note block-level markup.
467
     *
468
     * :::tip
469
     * **Tip:** This is an advice.
470
     * :::
471
     *
472
     * Code inspired by https://github.com/sixlive/parsedown-alert from TJ Miller (@sixlive).
473
     */
474
    protected function blockNote($block)
475
    {
476
        if (preg_match('/:::(.*)/', $block['text'], $matches)) {
477 1
            $block = [
478
                'char'    => ':',
479 1
                'element' => [
480 1
                    'name'       => 'aside',
481 1
                    'text'       => '',
482 1
                    'attributes' => [
483 1
                        'class' => 'note',
484 1
                    ],
485 1
                ],
486 1
            ];
487 1
            if (!empty($matches[1])) {
488 1
                $block['element']['attributes']['class'] .= " note-{$matches[1]}";
489 1
            }
490 1
491 1
            return $block;
492
        }
493
    }
494 1
495
    protected function blockNoteContinue($line, $block)
496
    {
497
        if (isset($block['complete'])) {
498 1
            return;
499
        }
500 1
        if (preg_match('/:::/', $line['text'])) {
501 1
            $block['complete'] = true;
502
503 1
            return $block;
504 1
        }
505
        $block['element']['text'] .= $line['text'] . "\n";
506 1
507
        return $block;
508 1
    }
509
510 1
    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
     * Apply Highlight to code blocks.
520
     */
521
    protected function blockFencedCodeComplete($block)
522
    {
523
        if (!$this->config->isEnabled('pages.body.highlight')) {
524 1
            return $block;
525
        }
526 1
        if (!isset($block['element']['text']['attributes'])) {
527
            return $block;
528
        }
529 1
530
        try {
531
            $code = $block['element']['text']['text'];
532
            $languageClass = $block['element']['text']['attributes']['class'];
533
            $language = explode('-', $languageClass);
534 1
            $highlighted = $this->highlighter->highlight($language[1], $code);
535 1
            $block['element']['text']['attributes']['class'] = vsprintf('%s hljs %s', [
536 1
                $languageClass,
537 1
                $highlighted->language,
538 1
            ]);
539 1
            $block['element']['text']['rawHtml'] = $highlighted->value;
540 1
            $block['element']['text']['allowRawHtmlInSafeMode'] = true;
541 1
            unset($block['element']['text']['text']);
542 1
        } catch (\Exception $e) {
543 1
            $this->builder->getLogger()->debug("Highlighter: " . $e->getMessage());
544 1
        } finally {
545
            return $block;
546
        }
547
    }
548 1
549
    /**
550
     * {@inheritdoc}
551
     */
552
    protected function parseAttributeData($attributeString)
553
    {
554
        $attributes = preg_split('/[ ]+/', $attributeString, -1, PREG_SPLIT_NO_EMPTY);
555 1
        $Data = [];
556
        $HtmlAtt = [];
557 1
558 1
        if (is_iterable($attributes)) {
559 1
            foreach ($attributes as $attribute) {
560
                switch ($attribute[0]) {
561 1
                    case '#': // ID
562 1
                        $Data['id'] = substr($attribute, 1);
563 1
                        break;
564 1
                    case '.': // Classes
565 1
                        $classes[] = substr($attribute, 1);
566 1
                        break;
567 1
                    default:  // Attributes
568 1
                        parse_str($attribute, $parsed);
569 1
                        $HtmlAtt = array_merge($HtmlAtt, $parsed);
570
                }
571 1
            }
572 1
573
            if (isset($classes)) {
574
                $Data['class'] = implode(' ', $classes);
575
            }
576 1
            if (!empty($HtmlAtt)) {
577 1
                foreach ($HtmlAtt as $a => $v) {
578
                    $Data[$a] = trim($v, '"');
579 1
                }
580 1
            }
581 1
        }
582
583
        return $Data;
584
    }
585
586 1
    /**
587
     * {@inheritdoc}
588
     *
589
     * Converts XHTML '<br />' tag to '<br>'.
590
     *
591
     * @return string
592
     */
593
    protected function unmarkedText($text)
594
    {
595
        return str_replace('<br />', '<br>', parent::unmarkedText($text)); // @phpstan-ignore staticMethod.notFound
596 1
    }
597
598 1
    /**
599
     * {@inheritdoc}
600
     *
601
     * 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 1
    }
609
610 1
    /**
611
     * Turns a path relative to static or assets into a website relative path.
612
     *
613
     *   "../../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 1
        $pattern = \sprintf(
621
            '(\.\.\/)+(\b%s|%s\b)+(\/.*)',
622
            (string) $this->config->get('static.dir'),
623 1
            (string) $this->config->get('assets.dir')
624 1
        );
625 1
        $path = Util::joinPath($path);
626 1
        if (!preg_match('/' . $pattern . '/is', $path, $matches)) {
627 1
            return $path;
628 1
        }
629 1
630 1
        return $matches[3];
631
    }
632
633 1
    /**
634
     * Create a media (video or audio) element from a link.
635
     */
636
    private function createMediaFromLink(array $link, string $type = 'video'): array
637
    {
638
        $block = [
639 1
            'extent'  => $link['extent'],
640
            '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 1
        $block['element']['attributes']['src'] = new Url($this->builder, new Asset($this->builder, $link['element']['attributes']['href']));
647 1
        switch ($type) {
648 1
            case 'video':
649 1
                $block['element']['name'] = 'video';
650
                // no controls = autoplay, loop, muted, playsinline
651 1
                if (!isset($block['element']['attributes']['controls'])) {
652 1
                    $block['element']['attributes']['autoplay'] = '';
653
                    $block['element']['attributes']['loop'] = '';
654 1
                    $block['element']['attributes']['muted'] = '';
655 1
                    $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
                }
660 1
                if (!\array_key_exists('style', $block['element']['attributes'])) {
661 1
                    $block['element']['attributes']['style'] = '';
662
                }
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
                $block['element']['name'] = 'audio';
668 1
669 1
                return $block;
670 1
        }
671
672 1
        throw new \Exception(\sprintf('Unable to create %s from "%s".', $type, $link['element']['attributes']['href']));
673
    }
674
675
    /**
676
     * Create an embedded video iframe element from a link element and an URL.
677
     */
678
    private function createEmbeddedVideoFromLink(array $link, string $url): array
679
    {
680
        $iframe = [
681 1
            'element' => [
682
                'name'       => 'iframe',
683 1
                'text'       => $link['element']['text'],
684 1
                'attributes' => [
685 1
                    'width'           => '640',
686 1
                    'height'          => '360',
687 1
                    'title'           => $link['element']['text'],
688 1
                    'src'             => $url,
689 1
                    'frameborder'     => '0',
690 1
                    'allow'           => 'accelerometer;autoplay;encrypted-media;gyroscope;picture-in-picture;fullscreen;web-share;',
691 1
                    'allowfullscreen' => '',
692 1
                    'style'           => 'position:absolute;top:0;left:0;width:100%;height:100%;border:0;background-color:#d8d8d8;',
693 1
                ],
694 1
            ],
695 1
        ];
696 1
697 1
        // div wrapper
698 1
        return [
699
            'extent'  => $link['extent'],
700 1
            '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
    /**
715
     * Create a script element from a link element and an URL.
716
     */
717
    private function createScriptFromLink(array $link, string $url): array
718
    {
719 1
        return [
720
            'extent'  => $link['extent'],
721 1
            'element' => [
722
                'name'       => 'script',
723
                'text'       => $link['element']['text'],
724
                'attributes' => [
725 1
                    'src'   => $url . '.js',
726 1
                    'title' => $link['element']['attributes']['title'],
727
                ],
728
            ],
729 1
        ];
730 1
    }
731
732 1
    /**
733 1
     * Create a figure > figcaption element.
734 1
     */
735 1
    private function createFigure(array $inline): array
736 1
    {
737 1
        if (!$this->config->isEnabled('pages.body.images.caption')) {
738 1
            return $inline;
739 1
        }
740 1
741 1
        if (empty($inline['element']['attributes']['title'])) {
742 1
            return $inline;
743 1
        }
744 1
745 1
        $titleRawHtml = $this->line($inline['element']['attributes']['title']); // @phpstan-ignore method.notFound
746 1
        $inline['element']['attributes']['title'] = strip_tags($titleRawHtml);
747 1
748 1
        $figcaption = [
749 1
            'element' => [
750
                'name'                   => 'figcaption',
751 1
                'allowRawHtmlInSafeMode' => true,
752
                'rawHtml'                => $titleRawHtml,
753
            ],
754
        ];
755
        $figure = [
756
            'extent'  => $inline['extent'],
757 1
            'element' => [
758
                'name'    => 'figure',
759
                'handler' => 'elements',
760 1
                'text'    => [
761 1
                    $inline['element'],
762
                    $figcaption['element'],
763 1
                ],
764 1
            ],
765
        ];
766 1
767 1
        return $figure;
768
    }
769 1
770 1
    /**
771
     * Handle an external link.
772 1
     */
773 1
    private function handleExternalLink(array $link): array
774
    {
775 1
        if (
776 1
            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
            if ($this->config->isEnabled('pages.body.links.external.blank')) {
780
                $link['element']['attributes']['target'] = '_blank';
781 1
            }
782
            if (!\array_key_exists('rel', $link['element']['attributes'])) {
783
                $link['element']['attributes']['rel'] = '';
784
            }
785
            if ($this->config->isEnabled('pages.body.links.external.noopener')) {
786
                $link['element']['attributes']['rel'] .= ' noopener';
787
            }
788
            if ($this->config->isEnabled('pages.body.links.external.noreferrer')) {
789
                $link['element']['attributes']['rel'] .= ' noreferrer';
790
            }
791
            if ($this->config->isEnabled('pages.body.links.external.nofollow')) {
792
                $link['element']['attributes']['rel'] .= ' nofollow';
793
            }
794
            $link['element']['attributes']['rel'] = trim($link['element']['attributes']['rel']);
795
        }
796
797
        return $link;
798
    }
799
}
800