Passed
Push — master ( 16aeb6...6afa06 )
by Arnaud
05:29
created

Parsedown   F

Complexity

Total Complexity 95

Size/Duplication

Total Lines 651
Duplicated Lines 0 %

Test Coverage

Coverage 82.79%

Importance

Changes 4
Bugs 2 Features 1
Metric Value
eloc 340
c 4
b 2
f 1
dl 0
loc 651
ccs 303
cts 366
cp 0.8279
rs 2
wmc 95

13 Methods

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