Parsedown   F
last analyzed

Complexity

Total Complexity 109

Size/Duplication

Total Lines 762
Duplicated Lines 0 %

Test Coverage

Coverage 86.38%

Importance

Changes 11
Bugs 6 Features 1
Metric Value
eloc 361
c 11
b 6
f 1
dl 0
loc 762
ccs 336
cts 389
cp 0.8638
rs 2
wmc 109

20 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 24 1
A inlineInsert() 0 13 4
A inlineUrl() 0 10 2
A element() 0 3 1
B handleExternalLink() 0 25 9
A inlineUrlTag() 0 10 2
A unmarkedText() 0 3 1
A blockNoteContinue() 0 13 3
A blockNoteComplete() 0 6 1
A createFigure() 0 33 3
A blockFencedCodeComplete() 0 25 4
C inlineLink() 0 75 13
B parseAttributeData() 0 32 8
A createScriptFromLink() 0 10 1
A blockNote() 0 18 3
F inlineImage() 0 226 41
A createEmbeddedVideoFromLink() 0 31 1
B createMediaFromLink() 0 37 6
A normalizePath() 0 14 2
A blockImage() 0 12 3

How to fix   Complexity   

Complex Class

Complex classes like Parsedown often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Parsedown, and based on these observations, apply Extract Interface, too.

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