Passed
Pull Request — master (#2254)
by Arnaud
08:43 queued 03:43
created

Parsedown::inlineLink()   C

Complexity

Conditions 13
Paths 26

Size

Total Lines 75
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 37
CRAP Score 13.003

Importance

Changes 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 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';
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
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
                                'sizes'  => $sizes,
415
                                'width'  => $InlineImage['element']['attributes']['width'],
416
                                'height' => $InlineImage['element']['attributes']['height'],
417
                            ],
418
                        ];
419 1
                    } catch (\Exception $e) {
420 1
                        $this->builder->getLogger()->warning($e->getMessage());
421 1
                        continue;
422
                    }
423
                }
424 1
                if (\count($sources) > 0) {
425
                    $picture = [
426
                        'extent'  => $InlineImage['extent'],
427
                        'element' => [
428
                            'name'       => 'picture',
429
                            'handler'    => 'elements',
430
                            'attributes' => [
431
                                'title' => $image['element']['attributes']['title'],
432
                            ],
433
                        ],
434
                    ];
435
                    $picture['element']['text'] = $sources;
436
                    unset($image['element']['attributes']['title']); // phpstan-ignore unset.offset
437
                    $picture['element']['text'][] = $image['element'];
438 1
                    $image = $picture;
439
                }
440 1
            } catch (\Exception $e) {
441 1
                $this->builder->getLogger()->debug($e->getMessage());
442
            }
443
        }
444
445 1
        return $this->createFigure($image);
446
    }
447
448
    /**
449
     * Image block.
450
     */
451 1
    protected function blockImage($Excerpt)
452
    {
453 1
        if (1 !== preg_match($this->regexImage, $Excerpt['text'])) {
454
            return;
455
        }
456
457 1
        $InlineImage = $this->inlineImage($Excerpt);
458 1
        if (!isset($InlineImage)) {
459
            return;
460
        }
461
462 1
        return $InlineImage;
463
    }
464
465
    /**
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 1
    protected function blockNote($block)
475
    {
476 1
        if (preg_match('/:::(.*)/', $block['text'], $matches)) {
477 1
            $block = [
478 1
                '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
            }
490
491 1
            return $block;
492
        }
493
    }
494
495 1
    protected function blockNoteContinue($line, $block)
496
    {
497 1
        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
        }
505 1
        $block['element']['text'] .= $line['text'] . "\n";
506
507 1
        return $block;
508
    }
509
510 1
    protected function blockNoteComplete($block)
511
    {
512 1
        $block['element']['rawHtml'] = $this->text($block['element']['text']);
513 1
        unset($block['element']['text']);
514
515 1
        return $block;
516
    }
517
518
    /**
519
     * Apply Highlight to code blocks.
520
     */
521 1
    protected function blockFencedCodeComplete($block)
522
    {
523 1
        if (!$this->config->isEnabled('pages.body.highlight')) {
524
            return $block;
525
        }
526 1
        if (!isset($block['element']['text']['attributes'])) {
527
            return $block;
528
        }
529
530
        try {
531 1
            $code = $block['element']['text']['text'];
532 1
            $languageClass = $block['element']['text']['attributes']['class'];
533 1
            $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
        } catch (\Exception $e) {
543
            $this->builder->getLogger()->debug("Highlighter: " . $e->getMessage());
544
        } finally {
545 1
            return $block;
546
        }
547
    }
548
549
    /**
550
     * {@inheritdoc}
551
     */
552 1
    protected function parseAttributeData($attributeString)
553
    {
554 1
        $attributes = preg_split('/[ ]+/', $attributeString, -1, PREG_SPLIT_NO_EMPTY);
555 1
        $Data = [];
556 1
        $HtmlAtt = [];
557
558 1
        if (is_iterable($attributes)) {
559 1
            foreach ($attributes as $attribute) {
560 1
                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
                    default:  // Attributes
568 1
                        parse_str($attribute, $parsed);
569 1
                        $HtmlAtt = array_merge($HtmlAtt, $parsed);
570
                }
571
            }
572
573 1
            if (isset($classes)) {
574 1
                $Data['class'] = implode(' ', $classes);
575
            }
576 1
            if (!empty($HtmlAtt)) {
577 1
                foreach ($HtmlAtt as $a => $v) {
578 1
                    $Data[$a] = trim($v, '"');
579
                }
580
            }
581
        }
582
583 1
        return $Data;
584
    }
585
586
    /**
587
     * {@inheritdoc}
588
     *
589
     * Converts XHTML '<br />' tag to '<br>'.
590
     *
591
     * @return string
592
     */
593 1
    protected function unmarkedText($text)
594
    {
595 1
        return str_replace('<br />', '<br>', parent::unmarkedText($text)); // @phpstan-ignore staticMethod.notFound
596
    }
597
598
    /**
599
     * {@inheritdoc}
600
     *
601
     * XHTML closing tag to HTML5 closing tag.
602
     *
603
     * @return string
604
     */
605 1
    protected function element(array $Element)
606
    {
607 1
        return str_replace(' />', '>', parent::element($Element)); // @phpstan-ignore staticMethod.notFound
608
    }
609
610
    /**
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 1
    private function normalizePath(string $path): string
618
    {
619
        // https://regex101.com/r/Rzguzh/1
620 1
        $pattern = \sprintf(
621 1
            '(\.\.\/)+(\b%s|%s\b)+(\/.*)',
622 1
            (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
        }
629
630 1
        return $matches[3];
631
    }
632
633
    /**
634
     * Create a media (video or audio) element from a link.
635
     */
636 1
    private function createMediaFromLink(array $link, string $type = 'video'): array
637
    {
638 1
        $block = [
639 1
            'extent'  => $link['extent'],
640 1
            'element' => [
641 1
                'text' => $link['element']['text'],
642 1
            ],
643 1
        ];
644 1
        $block['element']['attributes'] = $link['element']['attributes'];
645 1
        unset($block['element']['attributes']['href']);
646 1
        $block['element']['attributes']['src'] = new Url($this->builder, new Asset($this->builder, $link['element']['attributes']['href']));
647
        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 1
                    $block['element']['attributes']['loop'] = '';
654 1
                    $block['element']['attributes']['muted'] = '';
655 1
                    $block['element']['attributes']['playsinline'] = '';
656
                }
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
665 1
                return $block;
666 1
            case 'audio':
667 1
                $block['element']['name'] = 'audio';
668
669 1
                return $block;
670
        }
671
672
        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 1
    private function createEmbeddedVideoFromLink(array $link, string $url): array
679
    {
680 1
        $iframe = [
681 1
            'element' => [
682 1
                '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
697
        // div wrapper
698 1
        return [
699 1
            '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
    }
713
714
    /**
715
     * Create a script element from a link element and an URL.
716
     */
717 1
    private function createScriptFromLink(array $link, string $url): array
718
    {
719 1
        return [
720 1
            'extent'  => $link['extent'],
721 1
            'element' => [
722 1
                'name'       => 'script',
723 1
                'text'       => $link['element']['text'],
724 1
                'attributes' => [
725 1
                    'src'   => $url . '.js',
726 1
                    'title' => $link['element']['attributes']['title'],
727 1
                ],
728 1
            ],
729 1
        ];
730
    }
731
732
    /**
733
     * Create a figure > figcaption element.
734
     */
735 1
    private function createFigure(array $inline): array
736
    {
737 1
        if (!$this->config->isEnabled('pages.body.images.caption')) {
738
            return $inline;
739
        }
740
741 1
        if (empty($inline['element']['attributes']['title'])) {
742 1
            return $inline;
743
        }
744
745 1
        $titleRawHtml = $this->line($inline['element']['attributes']['title']); // @phpstan-ignore method.notFound
746 1
        $inline['element']['attributes']['title'] = strip_tags($titleRawHtml);
747
748 1
        $figcaption = [
749 1
            'element' => [
750 1
                'name'                   => 'figcaption',
751 1
                'allowRawHtmlInSafeMode' => true,
752 1
                'rawHtml'                => $titleRawHtml,
753 1
            ],
754 1
        ];
755 1
        $figure = [
756 1
            'extent'  => $inline['extent'],
757 1
            'element' => [
758 1
                'name'    => 'figure',
759 1
                'handler' => 'elements',
760 1
                'text'    => [
761 1
                    $inline['element'],
762 1
                    $figcaption['element'],
763 1
                ],
764 1
            ],
765 1
        ];
766
767 1
        return $figure;
768
    }
769
770
    /**
771
     * Handle an external link.
772
     */
773 1
    private function handleExternalLink(array $link): array
774
    {
775
        if (
776 1
            str_starts_with($link['element']['attributes']['href'], 'http')
777 1
            && (!empty($this->config->get('baseurl')) && !str_starts_with($link['element']['attributes']['href'], (string) $this->config->get('baseurl')))
778
        ) {
779 1
            if ($this->config->isEnabled('pages.body.links.external.blank')) {
780 1
                $link['element']['attributes']['target'] = '_blank';
781
            }
782 1
            if (!\array_key_exists('rel', $link['element']['attributes'])) {
783 1
                $link['element']['attributes']['rel'] = '';
784
            }
785 1
            if ($this->config->isEnabled('pages.body.links.external.noopener')) {
786 1
                $link['element']['attributes']['rel'] .= ' noopener';
787
            }
788 1
            if ($this->config->isEnabled('pages.body.links.external.noreferrer')) {
789 1
                $link['element']['attributes']['rel'] .= ' noreferrer';
790
            }
791 1
            if ($this->config->isEnabled('pages.body.links.external.nofollow')) {
792 1
                $link['element']['attributes']['rel'] .= ' nofollow';
793
            }
794 1
            $link['element']['attributes']['rel'] = trim($link['element']['attributes']['rel']);
795
        }
796
797 1
        return $link;
798
    }
799
}
800