Passed
Pull Request — master (#2254)
by Arnaud
13:41 queued 07:14
created

Parsedown::inlineLink()   C

Complexity

Conditions 13
Paths 29

Size

Total Lines 78
Code Lines 43

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 46
CRAP Score 13.0016

Importance

Changes 5
Bugs 2 Features 1
Metric Value
cc 13
eloc 43
c 5
b 2
f 1
nc 29
nop 1
dl 0
loc 78
ccs 46
cts 47
cp 0.9787
crap 13.0016
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
        // External link
126 1
        $link = $this->handleExternalLink($link);
127
128
        /*
129
         * Embed link?
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
        $extension = pathinfo($link['element']['attributes']['href'], \PATHINFO_EXTENSION);
140
        // video?
141 1
        if (\in_array($extension, $this->config->get('pages.body.links.embed.video') ?? [])) {
142 1
            if (!$embed) {
143 1
                $link['element']['attributes']['href'] = new Url($this->builder, $link['element']['attributes']['href']);
144
145 1
                return $link;
146
            }
147 1
            $video = $this->createMediaFromLink($link, 'video');
148
149 1
            return $this->createFigure($video);
150
        }
151
        // audio?
152 1
        if (\in_array($extension, $this->config->get('pages.body.links.embed.audio') ?? [])) {
153 1
            if (!$embed) {
154 1
                $link['element']['attributes']['href'] = new Url($this->builder, $link['element']['attributes']['href']);
155
156 1
                return $link;
157
            }
158 1
            $audio = $this->createMediaFromLink($link, 'audio');
159
160 1
            return $this->createFigure($audio);
161
        }
162 1
        if (!$embed) {
163 1
            return $link;
164
        }
165
166 1
        if (false !== $result = Util::matchesUrlPattern((string) $link['element']['attributes']['href'])) {
167 1
            switch ($result['type']) {
168 1
                case 'video':
169 1
                    return $this->createFigure($this->createEmbeddedVideoFromLink($link, $result['url']));
170 1
                case 'script':
171 1
                    $script = [
172 1
                        'extent'  => $link['extent'],
173 1
                        'element' => [
174 1
                            'name'       => 'script',
175 1
                            'text'       => $link['element']['text'],
176 1
                            'attributes' => [
177 1
                                'src'   => $result['url'] . '.js',
178 1
                                'title' => $link['element']['attributes']['title'],
179 1
                            ],
180 1
                        ],
181 1
                    ];
182
183 1
                    return $this->createFigure($script);
184
            }
185
        }
186
187 1
        return $link;
188
    }
189
190
    /**
191
     * {@inheritdoc}
192
     */
193 1
    protected function inlineUrl($Excerpt)
194
    {
195 1
        $link = parent::inlineUrl($Excerpt); // @phpstan-ignore staticMethod.notFound
196
197 1
        if (!isset($link)) {
198 1
            return;
199
        }
200
201
        // External link
202
        return $this->handleExternalLink($link);
203
    }
204
205
    /**
206
     * {@inheritdoc}
207
     */
208
    protected function inlineUrlTag($Excerpt)
209
    {
210
        $link = parent::inlineUrlTag($Excerpt); // @phpstan-ignore staticMethod.notFound
211
212
        if (!isset($link)) {
213
            return;
214
        }
215
216
        // External link
217
        return $this->handleExternalLink($link);
218
    }
219
220
    /**
221
     * {@inheritdoc}
222
     */
223 1
    protected function inlineImage($Excerpt)
224
    {
225 1
        $InlineImage = parent::inlineImage($Excerpt); // @phpstan-ignore staticMethod.notFound
226 1
        if (!isset($InlineImage)) {
227
            return null;
228
        }
229
230
        // remove link attributes
231 1
        unset($InlineImage['element']['attributes']['target'], $InlineImage['element']['attributes']['rel']);
232
233
        // normalize path
234 1
        $InlineImage['element']['attributes']['src'] = $this->normalizePath($InlineImage['element']['attributes']['src']);
235
236
        // should be lazy loaded?
237 1
        if ($this->config->isEnabled('pages.body.images.lazy') && !isset($InlineImage['element']['attributes']['loading'])) {
238 1
            $InlineImage['element']['attributes']['loading'] = 'lazy';
239
        }
240
        // should be decoding async?
241 1
        if ($this->config->isEnabled('pages.body.images.decoding') && !isset($InlineImage['element']['attributes']['decoding'])) {
242 1
            $InlineImage['element']['attributes']['decoding'] = 'async';
243
        }
244
        // add default class?
245 1
        if ((string) $this->config->get('pages.body.images.class')) {
246 1
            if (!\array_key_exists('class', $InlineImage['element']['attributes'])) {
247 1
                $InlineImage['element']['attributes']['class'] = '';
248
            }
249 1
            $InlineImage['element']['attributes']['class'] .= ' ' . (string) $this->config->get('pages.body.images.class');
250 1
            $InlineImage['element']['attributes']['class'] = trim($InlineImage['element']['attributes']['class']);
251
        }
252
253
        // disable remote image handling?
254 1
        if (Util\File::isRemote($InlineImage['element']['attributes']['src']) && !$this->config->isEnabled('pages.body.images.remote')) {
255
            return $this->createFigure($InlineImage);
256
        }
257
258
        // create asset
259 1
        $assetOptions = ['leading_slash' => false];
260 1
        if ($this->config->isEnabled('pages.body.images.remote.fallback')) {
261 1
            $assetOptions = ['leading_slash' => true];
262 1
            $assetOptions += ['fallback' => (string) $this->config->get('pages.body.images.remote.fallback')];
263
        }
264 1
        $asset = new Asset($this->builder, $InlineImage['element']['attributes']['src'], $assetOptions);
265 1
        $InlineImage['element']['attributes']['src'] = new Url($this->builder, $asset);
266 1
        $width = $asset['width'];
267
268
        /*
269
         * Should be resized?
270
         */
271 1
        $shouldResize = false;
272 1
        $assetResized = null;
273
        // pages.body.images.resize
274
        if (
275 1
            \is_int($this->config->get('pages.body.images.resize'))
276 1
            && $this->config->get('pages.body.images.resize') > 0
277 1
            && $width > $this->config->get('pages.body.images.resize')
278
        ) {
279
            $shouldResize = true;
280
            $width = $this->config->get('pages.body.images.resize');
281
        }
282
        // width attribute
283
        if (
284 1
            isset($InlineImage['element']['attributes']['width'])
285 1
            && $width > (int) $InlineImage['element']['attributes']['width']
286
        ) {
287 1
            $shouldResize = true;
288 1
            $width = (int) $InlineImage['element']['attributes']['width'];
289
        }
290
        // responsive images
291
        if (
292 1
            $this->config->isEnabled('pages.body.images.responsive')
293 1
            && !empty($this->config->getAssetsImagesWidths())
294 1
            && $width > max($this->config->getAssetsImagesWidths())
295
        ) {
296
            $shouldResize = true;
297
            $width = max($this->config->getAssetsImagesWidths());
298
        }
299 1
        if ($shouldResize) {
300
            try {
301 1
                $assetResized = $asset->resize($width);
302
            } catch (\Exception $e) {
303
                $this->builder->getLogger()->debug($e->getMessage());
304
305
                return $this->createFigure($InlineImage);
306
            }
307
        }
308
309
        // set width
310 1
        $InlineImage['element']['attributes']['width'] = $width;
311
        // set height
312 1
        $InlineImage['element']['attributes']['height'] = $assetResized['height'] ?? $asset['height'];
313
314
        // placeholder
315
        if (
316 1
            (!empty($this->config->get('pages.body.images.placeholder')) || isset($InlineImage['element']['attributes']['placeholder']))
317 1
            && \in_array($assetResized['subtype'] ?? $asset['subtype'], ['image/jpeg', 'image/png', 'image/gif'])
318
        ) {
319 1
            if (!\array_key_exists('placeholder', $InlineImage['element']['attributes'])) {
320 1
                $InlineImage['element']['attributes']['placeholder'] = (string) $this->config->get('pages.body.images.placeholder');
321
            }
322 1
            if (!\array_key_exists('style', $InlineImage['element']['attributes'])) {
323 1
                $InlineImage['element']['attributes']['style'] = '';
324
            }
325 1
            $InlineImage['element']['attributes']['style'] = trim($InlineImage['element']['attributes']['style'], ';');
326 1
            switch ($InlineImage['element']['attributes']['placeholder']) {
327 1
                case 'color':
328 1
                    $InlineImage['element']['attributes']['style'] .= \sprintf(';max-width:100%%;height:auto;background-color:%s;', Image::getDominantColor($assetResized ?? $asset));
329 1
                    break;
330 1
                case 'lqip':
331
                    // aborts if animated GIF for performance reasons
332 1
                    if (Image::isAnimatedGif($assetResized ?? $asset)) {
333
                        break;
334
                    }
335 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));
336 1
                    break;
337
            }
338 1
            unset($InlineImage['element']['attributes']['placeholder']);
339 1
            $InlineImage['element']['attributes']['style'] = trim($InlineImage['element']['attributes']['style']);
340
        }
341
342
        /*
343
         * Should be responsive?
344
         */
345 1
        $sizes = '';
346 1
        if ($this->config->isEnabled('pages.body.images.responsive')) {
347
            try {
348
                if (
349 1
                    $srcset = Image::buildHtmlSrcsetW(
350 1
                        $assetResized ?? $asset,
351 1
                        $this->config->getAssetsImagesWidths()
352 1
                    )
353
                ) {
354 1
                    $InlineImage['element']['attributes']['srcset'] = $srcset;
355 1
                    $sizes = Image::getHtmlSizes($InlineImage['element']['attributes']['class'] ?? '', (array) $this->config->getAssetsImagesSizes());
356 1
                    $InlineImage['element']['attributes']['sizes'] = $sizes;
357
                }
358 1
            } catch (\Exception $e) {
359 1
                $this->builder->getLogger()->warning($e->getMessage());
360
            }
361
        }
362
363
        /*
364
        <!-- if title: a <figure> is required to put in it a <figcaption> -->
365
        <figure>
366
            <!-- if formats: a <picture> is required for each <source> -->
367
            <picture>
368
                <source type="image/avif"
369
                    srcset="..."
370
                    sizes="..."
371
                >
372
                <source type="image/webp"
373
                    srcset="..."
374
                    sizes="..."
375
                >
376
                <img src="..."
377
                    srcset="..."
378
                    sizes="..."
379
                >
380
            </picture>
381
            <figcaption><!-- title --></figcaption>
382
        </figure>
383
        */
384
385 1
        $image = $InlineImage;
386
387
        // converts image to formats and put them in picture > source
388
        if (
389 1
            \count($formats = ((array) $this->config->get('pages.body.images.formats'))) > 0
390 1
            && \in_array($assetResized['subtype'] ?? $asset['subtype'], ['image/jpeg', 'image/png', 'image/gif'])
391
        ) {
392
            try {
393
                // abord if InlineImage is an animated GIF
394 1
                if (Image::isAnimatedGif($assetResized ?? $asset)) {
395 1
                    $filepath = Util::joinFile($this->config->getOutputPath(), $assetResized['path'] ?? $asset['path'] ?? '');
396 1
                    throw new RuntimeException(\sprintf('Asset "%s" is not converted (animated GIF).', $filepath));
397
                }
398 1
                $sources = [];
399 1
                foreach ($formats as $format) {
400 1
                    $srcset = '';
401
                    try {
402 1
                        $assetConverted = ($assetResized ?? $asset)->convert($format);
403
                        // build responsive images?
404
                        if ($this->config->isEnabled('pages.body.images.responsive')) {
405
                            $srcset = Image::buildHtmlSrcset($assetConverted, $this->config->getAssetsImagesWidths());
406
                        }
407
                        // if not, use default image as srcset
408
                        if (empty($srcset)) {
409
                            $srcset = (string) $assetConverted;
410
                        }
411
                        // add format to <sources>
412
                        $sources[] = [
413
                            'name'       => 'source',
414
                            'attributes' => [
415
                                'type'   => "image/$format",
416
                                'srcset' => $srcset,
417
                                'sizes'  => $sizes,
418
                                'width'  => $InlineImage['element']['attributes']['width'],
419
                                'height' => $InlineImage['element']['attributes']['height'],
420
                            ],
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 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
                    'width'           => '640',
689 1
                    'height'          => '360',
690 1
                    'title'           => $link['element']['text'],
691 1
                    'src'             => $url,
692 1
                    'frameborder'     => '0',
693 1
                    'allow'           => 'accelerometer;autoplay;encrypted-media;gyroscope;picture-in-picture;fullscreen;web-share;',
694 1
                    'allowfullscreen' => '',
695 1
                    'style'           => 'position:absolute;top:0;left:0;width:100%;height:100%;border:0;background-color:#d8d8d8;',
696 1
                ],
697 1
            ],
698 1
        ];
699
700 1
        return [
701 1
            'extent'  => $link['extent'],
702 1
            'element' => [
703 1
                'name'    => 'div',
704 1
                'handler' => 'elements',
705 1
                'text'    => [
706 1
                    $iframe['element'],
707 1
                ],
708 1
                'attributes' => [
709 1
                    'style' => 'position:relative;padding-bottom:56.25%;height:0;overflow:hidden;',
710 1
                    'title' => $link['element']['attributes']['title'],
711 1
                ],
712 1
            ],
713 1
        ];
714
    }
715
716
    /**
717
     * Create a figure > figcaption element.
718
     */
719 1
    private function createFigure(array $inline): array
720
    {
721 1
        if (!$this->config->isEnabled('pages.body.images.caption')) {
722
            return $inline;
723
        }
724
725 1
        if (empty($inline['element']['attributes']['title'])) {
726 1
            return $inline;
727
        }
728
729 1
        $titleRawHtml = $this->line($inline['element']['attributes']['title']); // @phpstan-ignore method.notFound
730 1
        $inline['element']['attributes']['title'] = strip_tags($titleRawHtml);
731
732 1
        $figcaption = [
733 1
            'element' => [
734 1
                'name'                   => 'figcaption',
735 1
                'allowRawHtmlInSafeMode' => true,
736 1
                'rawHtml'                => $titleRawHtml,
737 1
            ],
738 1
        ];
739 1
        $figure = [
740 1
            'extent'  => $inline['extent'],
741 1
            'element' => [
742 1
                'name'    => 'figure',
743 1
                'handler' => 'elements',
744 1
                'text'    => [
745 1
                    $inline['element'],
746 1
                    $figcaption['element'],
747 1
                ],
748 1
            ],
749 1
        ];
750
751 1
        return $figure;
752
    }
753
754
    /**
755
     * Handle an external link.
756
     */
757 1
    private function handleExternalLink(array $link): array
758
    {
759
        if (
760 1
            str_starts_with($link['element']['attributes']['href'], 'http')
761 1
            && (!empty($this->config->get('baseurl')) && !str_starts_with($link['element']['attributes']['href'], (string) $this->config->get('baseurl')))
762
        ) {
763 1
            if ($this->config->isEnabled('pages.body.links.external.blank')) {
764 1
                $link['element']['attributes']['target'] = '_blank';
765
            }
766 1
            if (!\array_key_exists('rel', $link['element']['attributes'])) {
767 1
                $link['element']['attributes']['rel'] = '';
768
            }
769 1
            if ($this->config->isEnabled('pages.body.links.external.noopener')) {
770 1
                $link['element']['attributes']['rel'] .= ' noopener';
771
            }
772 1
            if ($this->config->isEnabled('pages.body.links.external.noreferrer')) {
773 1
                $link['element']['attributes']['rel'] .= ' noreferrer';
774
            }
775 1
            if ($this->config->isEnabled('pages.body.links.external.nofollow')) {
776 1
                $link['element']['attributes']['rel'] .= ' nofollow';
777
            }
778 1
            $link['element']['attributes']['rel'] = trim($link['element']['attributes']['rel']);
779
        }
780
781 1
        return $link;
782
    }
783
}
784