Passed
Pull Request — master (#2124)
by Arnaud
12:24 queued 06:07
created

Parsedown::parseAttributeData()   B

Complexity

Conditions 8
Paths 5

Size

Total Lines 32
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 8

Importance

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