Passed
Push — master ( 5de407...d01966 )
by
unknown
05:00
created

Parsedown   F

Complexity

Total Complexity 114

Size/Duplication

Total Lines 800
Duplicated Lines 0 %

Test Coverage

Coverage 78.56%

Importance

Changes 8
Bugs 5 Features 1
Metric Value
eloc 401
c 8
b 5
f 1
dl 0
loc 800
ccs 341
cts 434
cp 0.7856
rs 2
wmc 114

18 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 29 2
A blockFencedCodeComplete() 0 25 4
B parseAttributeData() 0 32 8
F inlineLink() 0 164 18
A blockNote() 0 18 3
F inlineImage() 0 236 44
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\Assets\Asset;
17
use Cecil\Assets\Image;
18
use Cecil\Builder;
19
use Cecil\Exception\RuntimeException;
20
use Cecil\Url;
21
use Cecil\Util;
22
use Highlight\Highlighter;
23
24
/**
25
 * Parsedown class.
26
 *
27
 * This class extends ParsedownExtra (and ParsedownToc) and provides methods to parse Markdown content
28
 * with additional features such as inline insertions, image handling, note blocks,
29
 * and code highlighting.
30
 *
31
 * @property array $InlineTypes
32
 * @property string $inlineMarkerList
33
 * @property array $specialCharacters
34
 * @property array $BlockTypes
35
 */
36
class Parsedown extends \ParsedownToc
37
{
38
    /** @var Builder */
39
    protected $builder;
40
41
    /** @var \Cecil\Config */
42
    protected $config;
43
44
    /**
45
     * Regex for attributes.
46
     * @var string
47
     */
48
    protected $regexAttribute = '(?:[#.][-\w:\\\]+[ ]*|[-\w:\\\]+(?:=(?:["\'][^\n]*?["\']|[^\s]+)?)?[ ]*)';
49
50
    /**
51
     * Regex for image block.
52
     * @var string
53
     */
54
    protected $regexImage = "~^!\[.*?\]\(.*?\)~";
55
56
    /** @var Highlighter */
57
    protected $highlighter;
58
59 1
    public function __construct(Builder $builder, ?array $options = null)
60
    {
61 1
        $this->builder = $builder;
62 1
        $this->config = $builder->getConfig();
63
64
        // "insert" line block: ++text++ -> <ins>text</ins>
65 1
        $this->InlineTypes['+'][] = 'Insert';
66 1
        $this->inlineMarkerList = implode('', array_keys($this->InlineTypes));
67 1
        $this->specialCharacters[] = '+';
68
69
        // Image block (to avoid paragraph)
70 1
        $this->BlockTypes['!'][] = 'Image';
71
72
        // "notes" block
73 1
        $this->BlockTypes[':'][] = 'Note';
74
75
        // code highlight
76 1
        $this->highlighter = new Highlighter();
77
78
        // options
79 1
        $options = array_merge(['selectors' => (array) $this->config->get('pages.body.toc')], $options ?? []);
80
81 1
        parent::__construct();
82 1
        parent::setOptions($options);
83
    }
84
85
    /**
86
     * Insert inline.
87
     * e.g.: ++text++ -> <ins>text</ins>.
88
     */
89 1
    protected function inlineInsert($Excerpt)
90
    {
91 1
        if (!isset($Excerpt['text'][1])) {
92
            return;
93
        }
94
95 1
        if ($Excerpt['text'][1] === '+' && preg_match('/^\+\+(?=\S)(.+?)(?<=\S)\+\+/', $Excerpt['text'], $matches)) {
96 1
            return [
97 1
                'extent'  => \strlen($matches[0]),
98 1
                'element' => [
99 1
                    'name'    => 'ins',
100 1
                    'text'    => $matches[1],
101 1
                    'handler' => 'line',
102 1
                ],
103 1
            ];
104
        }
105
    }
106
107
    /**
108
     * {@inheritdoc}
109
     */
110 1
    protected function inlineLink($Excerpt)
111
    {
112 1
        $link = parent::inlineLink($Excerpt); // @phpstan-ignore staticMethod.notFound
113
114 1
        if (!isset($link)) {
115
            return null;
116
        }
117
118
        // Link to a page with "page:page_id" as URL
119 1
        if (Util\Str::startsWith($link['element']['attributes']['href'], 'page:')) {
120 1
            $link['element']['attributes']['href'] = new Url($this->builder, substr($link['element']['attributes']['href'], 5, \strlen($link['element']['attributes']['href'])));
121
122 1
            return $link;
123
        }
124
125
        // External link
126 1
        $link = $this->handleExternalLink($link);
127
128
        /*
129
         * Embed link?
130
         */
131 1
        $embed = $this->config->isEnabled('pages.body.links.embed');
132 1
        if (isset($link['element']['attributes']['embed'])) {
133 1
            $embed = true;
134 1
            if ($link['element']['attributes']['embed'] == 'false') {
135 1
                $embed = false;
136
            }
137 1
            unset($link['element']['attributes']['embed']);
138
        }
139 1
        $extension = pathinfo($link['element']['attributes']['href'], PATHINFO_EXTENSION);
140
        // video?
141 1
        if (\in_array($extension, $this->config->get('pages.body.links.embed.video') ?? [])) {
142 1
            if (!$embed) {
143 1
                $link['element']['attributes']['href'] = new Url($this->builder, $link['element']['attributes']['href']);
144
145 1
                return $link;
146
            }
147 1
            $video = $this->createMediaFromLink($link, 'video');
148 1
            if ($this->config->isEnabled('pages.body.images.caption')) {
149 1
                return $this->createFigure($video);
150
            }
151
152
            return $video;
153
        }
154
        // audio?
155 1
        if (\in_array($extension, $this->config->get('pages.body.links.embed.audio') ?? [])) {
156 1
            if (!$embed) {
157 1
                $link['element']['attributes']['href'] = new Url($this->builder, $link['element']['attributes']['href']);
158
159 1
                return $link;
160
            }
161 1
            $audio = $this->createMediaFromLink($link, 'audio');
162 1
            if ($this->config->isEnabled('pages.body.images.caption')) {
163 1
                return $this->createFigure($audio);
164
            }
165
166
            return $audio;
167
        }
168 1
        if (!$embed) {
169 1
            return $link;
170
        }
171
        // GitHub Gist link?
172
        // https://regex101.com/r/KWVMYI/1
173 1
        $pattern = 'https:\/\/gist\.github\.com\/[-a-zA-Z0-9_]+\/[-a-zA-Z0-9_]+';
174 1
        if (preg_match('/' . $pattern . '/is', (string) $link['element']['attributes']['href'], $matches)) {
175 1
            $gist = [
176 1
                'extent'  => $link['extent'],
177 1
                'element' => [
178 1
                    'name'       => 'script',
179 1
                    'text'       => $link['element']['text'],
180 1
                    'attributes' => [
181 1
                        'src'   => $matches[0] . '.js',
182 1
                        'title' => $link['element']['attributes']['title'],
183 1
                    ],
184 1
                ],
185 1
            ];
186 1
            if ($this->config->isEnabled('pages.body.images.caption')) {
187 1
                return $this->createFigure($gist);
188
            }
189
190
            return $gist;
191
        }
192
        // Youtube link?
193
        // https://regex101.com/r/gznM1j/1
194 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,})';
195 1
        if (preg_match('/' . $pattern . '/is', (string) $link['element']['attributes']['href'], $matches)) {
196 1
            $iframe = [
197 1
                'element' => [
198 1
                    'name'       => 'iframe',
199 1
                    'text'       => $link['element']['text'],
200 1
                    'attributes' => [
201 1
                        'width'           => '560',
202 1
                        'height'          => '315',
203 1
                        'title'           => $link['element']['text'],
204 1
                        'src'             => 'https://www.youtube.com/embed/' . $matches[1],
205 1
                        'frameborder'     => '0',
206 1
                        'allow'           => 'accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture',
207 1
                        'allowfullscreen' => '',
208 1
                        'style'           => 'position:absolute; top:0; left:0; width:100%; height:100%; border:0',
209 1
                    ],
210 1
                ],
211 1
            ];
212 1
            $youtube = [
213 1
                'extent'  => $link['extent'],
214 1
                'element' => [
215 1
                    'name'    => 'div',
216 1
                    'handler' => 'elements',
217 1
                    'text'    => [
218 1
                        $iframe['element'],
219 1
                    ],
220 1
                    'attributes' => [
221 1
                        'style' => 'position:relative; padding-bottom:56.25%; height:0; overflow:hidden',
222 1
                        'title' => $link['element']['attributes']['title'],
223 1
                    ],
224 1
                ],
225 1
            ];
226 1
            if ($this->config->isEnabled('pages.body.images.caption')) {
227 1
                return $this->createFigure($youtube);
228
            }
229
230
            return $youtube;
231
        }
232
        // Vimeo link?
233
        // https://regex101.com/r/wCEFhd/1
234 1
        $pattern = 'https:\/\/vimeo\.com\/([0-9]+)';
235 1
        if (preg_match('/' . $pattern . '/is', (string) $link['element']['attributes']['href'], $matches)) {
236
            $iframe = [
237
                'element' => [
238
                    'name'       => 'iframe',
239
                    'text'       => $link['element']['text'],
240
                    'attributes' => [
241
                        'width'           => '640',
242
                        'height'          => '360',
243
                        'title'           => $link['element']['text'],
244
                        'src'             => 'https://player.vimeo.com/video/' . $matches[1],
245
                        'frameborder'     => '0',
246
                        'allow'           => 'accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture',
247
                        'allowfullscreen' => '',
248
                        'style'           => 'position:absolute; top:0; left:0; width:100%; height:100%; border:0',
249
                    ],
250
                ],
251
            ];
252
            $vimeo = [
253
                'extent'  => $link['extent'],
254
                'element' => [
255
                    'name'    => 'div',
256
                    'handler' => 'elements',
257
                    'text'    => [
258
                        $iframe['element'],
259
                    ],
260
                    'attributes' => [
261
                        'style' => 'position:relative; padding-bottom:56.25%; height:0; overflow:hidden',
262
                        'title' => $link['element']['attributes']['title'],
263
                    ],
264
                ],
265
            ];
266
            if ($this->config->isEnabled('pages.body.images.caption')) {
267
                return $this->createFigure($vimeo);
268
            }
269
270
            return $vimeo;
271
        }
272
273 1
        return $link;
274
    }
275
276
    /**
277
     * {@inheritdoc}
278
     */
279 1
    protected function inlineUrl($Excerpt)
280
    {
281 1
        $link = parent::inlineUrl($Excerpt); // @phpstan-ignore staticMethod.notFound
282
283 1
        if (!isset($link)) {
284 1
            return;
285
        }
286
287
        // External link
288
        return $this->handleExternalLink($link);
289
    }
290
291
    /**
292
     * {@inheritdoc}
293
     */
294
    protected function inlineUrlTag($Excerpt)
295
    {
296
        $link = parent::inlineUrlTag($Excerpt); // @phpstan-ignore staticMethod.notFound
297
298
        if (!isset($link)) {
299
            return;
300
        }
301
302
        // External link
303
        return $this->handleExternalLink($link);
304
    }
305
306
    /**
307
     * {@inheritdoc}
308
     */
309 1
    protected function inlineImage($Excerpt)
310
    {
311 1
        $InlineImage = parent::inlineImage($Excerpt); // @phpstan-ignore staticMethod.notFound
312 1
        if (!isset($InlineImage)) {
313
            return null;
314
        }
315
316
        // normalize path
317 1
        $InlineImage['element']['attributes']['src'] = $this->normalizePath($InlineImage['element']['attributes']['src']);
318
319
        // should be lazy loaded?
320 1
        if ($this->config->isEnabled('pages.body.images.lazy') && !isset($InlineImage['element']['attributes']['loading'])) {
321 1
            $InlineImage['element']['attributes']['loading'] = 'lazy';
322
        }
323
        // should be decoding async?
324 1
        if ($this->config->isEnabled('pages.body.images.decoding') && !isset($InlineImage['element']['attributes']['decoding'])) {
325 1
            $InlineImage['element']['attributes']['decoding'] = 'async';
326
        }
327
        // add default class?
328 1
        if ((string) $this->config->get('pages.body.images.class')) {
329 1
            if (!\array_key_exists('class', $InlineImage['element']['attributes'])) {
330 1
                $InlineImage['element']['attributes']['class'] = '';
331
            }
332 1
            $InlineImage['element']['attributes']['class'] .= ' ' . (string) $this->config->get('pages.body.images.class');
333 1
            $InlineImage['element']['attributes']['class'] = trim($InlineImage['element']['attributes']['class']);
334
        }
335
336
        // disable remote image handling?
337 1
        if (Util\File::isRemote($InlineImage['element']['attributes']['src']) && !$this->config->isEnabled('pages.body.images.remote')) {
338
            return $InlineImage;
339
        }
340
341
        // create asset
342 1
        $assetOptions = ['leading_slash' => false];
343 1
        if ($this->config->isEnabled('pages.body.images.remote.fallback')) {
344 1
            $assetOptions = ['leading_slash' => true];
345 1
            $assetOptions += ['fallback' => (string) $this->config->get('pages.body.images.remote.fallback')];
346
        }
347 1
        $asset = new Asset($this->builder, $InlineImage['element']['attributes']['src'], $assetOptions);
348 1
        $InlineImage['element']['attributes']['src'] = new Url($this->builder, $asset);
349 1
        $width = $asset['width'];
350
351
        /*
352
         * Should be resized?
353
         */
354 1
        $shouldResize = false;
355 1
        $assetResized = null;
356
        // pages.body.images.resize
357
        if (
358 1
            \is_int($this->config->get('pages.body.images.resize'))
359 1
            && $this->config->get('pages.body.images.resize') > 0
360 1
            && $width > $this->config->get('pages.body.images.resize')
361
        ) {
362
            $shouldResize = true;
363
            $width = $this->config->get('pages.body.images.resize');
364
        }
365
        // width attribute
366
        if (
367 1
            isset($InlineImage['element']['attributes']['width'])
368 1
            && $width > (int) $InlineImage['element']['attributes']['width']
369
        ) {
370 1
            $shouldResize = true;
371 1
            $width = (int) $InlineImage['element']['attributes']['width'];
372
        }
373
        // responsive images
374
        if (
375 1
            $this->config->isEnabled('pages.body.images.responsive')
376 1
            && !empty($this->config->getAssetsImagesWidths())
377 1
            && $width > max($this->config->getAssetsImagesWidths())
378
        ) {
379
            $shouldResize = true;
380
            $width = max($this->config->getAssetsImagesWidths());
381
        }
382 1
        if ($shouldResize) {
383
            try {
384 1
                $assetResized = $asset->resize($width);
385
            } catch (\Exception $e) {
386
                $this->builder->getLogger()->debug($e->getMessage());
387
388
                return $InlineImage;
389
            }
390
        }
391
392
        // set width
393 1
        $InlineImage['element']['attributes']['width'] = $width;
394
        // set height
395 1
        $InlineImage['element']['attributes']['height'] = $assetResized['height'] ?? $asset['height'];
396
397
        // placeholder
398
        if (
399 1
            (!empty($this->config->get('pages.body.images.placeholder')) || isset($InlineImage['element']['attributes']['placeholder']))
400 1
            && \in_array($assetResized['subtype'] ?? $asset['subtype'], ['image/jpeg', 'image/png', 'image/gif'])
401
        ) {
402 1
            if (!\array_key_exists('placeholder', $InlineImage['element']['attributes'])) {
403 1
                $InlineImage['element']['attributes']['placeholder'] = (string) $this->config->get('pages.body.images.placeholder');
404
            }
405 1
            if (!\array_key_exists('style', $InlineImage['element']['attributes'])) {
406 1
                $InlineImage['element']['attributes']['style'] = '';
407
            }
408 1
            $InlineImage['element']['attributes']['style'] = trim($InlineImage['element']['attributes']['style'], ';');
409 1
            switch ($InlineImage['element']['attributes']['placeholder']) {
410 1
                case 'color':
411 1
                    $InlineImage['element']['attributes']['style'] .= \sprintf(';max-width:100%%;height:auto;background-color:%s;', Image::getDominantColor($assetResized ?? $asset));
412 1
                    break;
413 1
                case 'lqip':
414
                    // aborts if animated GIF for performance reasons
415 1
                    if (Image::isAnimatedGif($assetResized ?? $asset)) {
416
                        break;
417
                    }
418 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));
419 1
                    break;
420
            }
421 1
            unset($InlineImage['element']['attributes']['placeholder']);
422 1
            $InlineImage['element']['attributes']['style'] = trim($InlineImage['element']['attributes']['style']);
423
        }
424
425
        /*
426
         * Should be responsive?
427
         */
428 1
        $sizes = '';
429 1
        if ($this->config->isEnabled('pages.body.images.responsive')) {
430
            try {
431
                if (
432 1
                    $srcset = Image::buildSrcset(
433 1
                        $assetResized ?? $asset,
434 1
                        $this->config->getAssetsImagesWidths()
435 1
                    )
436
                ) {
437 1
                    $InlineImage['element']['attributes']['srcset'] = $srcset;
438 1
                    $sizes = Image::getSizes($InlineImage['element']['attributes']['class'] ?? '', (array) $this->config->getAssetsImagesSizes());
439 1
                    $InlineImage['element']['attributes']['sizes'] = $sizes;
440
                }
441 1
            } catch (\Exception $e) {
442 1
                $this->builder->getLogger()->debug($e->getMessage());
443
            }
444
        }
445
446
        /*
447
        <!-- if title: a <figure> is required to put in it a <figcaption> -->
448
        <figure>
449
            <!-- if formats: a <picture> is required for each <source> -->
450
            <picture>
451
                <source type="image/avif"
452
                    srcset="..."
453
                    sizes="..."
454
                >
455
                <source type="image/webp"
456
                    srcset="..."
457
                    sizes="..."
458
                >
459
                <img src="..."
460
                    srcset="..."
461
                    sizes="..."
462
                >
463
            </picture>
464
            <figcaption><!-- title --></figcaption>
465
        </figure>
466
        */
467
468 1
        $image = $InlineImage;
469
470
        // converts image to formats and put them in picture > source
471
        if (
472 1
            \count($formats = ((array) $this->config->get('pages.body.images.formats'))) > 0
473 1
            && \in_array($assetResized['subtype'] ?? $asset['subtype'], ['image/jpeg', 'image/png', 'image/gif'])
474
        ) {
475
            try {
476
                // InlineImage src must be an Asset instance
477 1
                if (!($assetResized ?? $asset) instanceof Asset) {
0 ignored issues
show
introduced by
$assetResized ?? $asset is always a sub-type of Cecil\Assets\Asset.
Loading history...
478
                    throw new RuntimeException(\sprintf('Asset "%s" can\'t be converted.', $InlineImage['element']['attributes']['src']));
479
                }
480
                // abord if InlineImage is an animated GIF
481 1
                if (Image::isAnimatedGif($assetResized ?? $asset)) {
482 1
                    $filepath = Util::joinFile($this->config->getOutputPath(), $assetResized['path'] ?? $asset['path']);
0 ignored issues
show
Bug introduced by
It seems like $assetResized['path'] ?? $asset['path'] can also be of type null; however, parameter $path of Cecil\Util::joinFile() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

482
                    $filepath = Util::joinFile($this->config->getOutputPath(), /** @scrutinizer ignore-type */ $assetResized['path'] ?? $asset['path']);
Loading history...
483 1
                    throw new RuntimeException(\sprintf('Asset "%s" is not converted (animated GIF).', $filepath));
484
                }
485 1
                $sources = [];
486 1
                foreach ($formats as $format) {
487 1
                    $srcset = '';
488
                    try {
489 1
                        $assetConverted = ($assetResized ?? $asset)->convert($format);
490 1
                    } catch (\Exception $e) {
491 1
                        $this->builder->getLogger()->debug($e->getMessage());
492 1
                        continue;
493
                    }
494
                    // build responsive images?
495
                    if ($this->config->isEnabled('pages.body.images.responsive')) {
496
                        try {
497
                            $srcset = Image::buildSrcset($assetConverted, $this->config->getAssetsImagesWidths());
498
                        } catch (\Exception $e) {
499
                            $this->builder->getLogger()->debug($e->getMessage());
500
                        }
501
                    }
502
                    // if not, use default image as srcset
503
                    if (empty($srcset)) {
504
                        $srcset = (string) $assetConverted;
505
                    }
506
                    // add format to <sources>
507
                    $sources[] = [
508
                        'name'       => 'source',
509
                        'attributes' => [
510
                            'type'   => "image/$format",
511
                            'srcset' => $srcset,
512
                            'sizes'  => $sizes,
513
                            'width'  => $InlineImage['element']['attributes']['width'],
514
                            'height' => $InlineImage['element']['attributes']['height'],
515
                        ],
516
                    ];
517
                }
518 1
                if (\count($sources) > 0) {
519
                    $picture = [
520
                        'extent'  => $InlineImage['extent'],
521
                        'element' => [
522
                            'name'       => 'picture',
523
                            'handler'    => 'elements',
524
                            'attributes' => [
525
                                'title' => $image['element']['attributes']['title'],
526
                            ],
527
                        ],
528
                    ];
529
                    $picture['element']['text'] = $sources;
530
                    unset($image['element']['attributes']['title']); // @phpstan-ignore unset.offset
531
                    $picture['element']['text'][] = $image['element'];
532 1
                    $image = $picture;
533
                }
534 1
            } catch (\Exception $e) {
535 1
                $this->builder->getLogger()->debug($e->getMessage());
536
            }
537
        }
538
539
        // if title: put the <img> (or <picture>) in a <figure> and create a <figcaption>
540 1
        if ($this->config->isEnabled('pages.body.images.caption')) {
541 1
            return $this->createFigure($image);
542
        }
543
544
        return $image;
545
    }
546
547
    /**
548
     * Image block.
549
     */
550 1
    protected function blockImage($Excerpt)
551
    {
552 1
        if (1 !== preg_match($this->regexImage, $Excerpt['text'])) {
553
            return;
554
        }
555
556 1
        $InlineImage = $this->inlineImage($Excerpt);
557 1
        if (!isset($InlineImage)) {
558
            return;
559
        }
560
561 1
        return $InlineImage;
562
    }
563
564
    /**
565
     * Note block-level markup.
566
     *
567
     * :::tip
568
     * **Tip:** This is an advice.
569
     * :::
570
     *
571
     * Code inspired by https://github.com/sixlive/parsedown-alert from TJ Miller (@sixlive).
572
     */
573 1
    protected function blockNote($block)
574
    {
575 1
        if (preg_match('/:::(.*)/', $block['text'], $matches)) {
576 1
            $block = [
577 1
                'char'    => ':',
578 1
                'element' => [
579 1
                    'name'       => 'aside',
580 1
                    'text'       => '',
581 1
                    'attributes' => [
582 1
                        'class' => 'note',
583 1
                    ],
584 1
                ],
585 1
            ];
586 1
            if (!empty($matches[1])) {
587 1
                $block['element']['attributes']['class'] .= " note-{$matches[1]}";
588
            }
589
590 1
            return $block;
591
        }
592
    }
593
594 1
    protected function blockNoteContinue($line, $block)
595
    {
596 1
        if (isset($block['complete'])) {
597 1
            return;
598
        }
599 1
        if (preg_match('/:::/', $line['text'])) {
600 1
            $block['complete'] = true;
601
602 1
            return $block;
603
        }
604 1
        $block['element']['text'] .= $line['text'] . "\n";
605
606 1
        return $block;
607
    }
608
609 1
    protected function blockNoteComplete($block)
610
    {
611 1
        $block['element']['rawHtml'] = $this->text($block['element']['text']);
612 1
        unset($block['element']['text']);
613
614 1
        return $block;
615
    }
616
617
    /**
618
     * Apply Highlight to code blocks.
619
     */
620 1
    protected function blockFencedCodeComplete($block)
621
    {
622 1
        if (!$this->config->isEnabled('pages.body.highlight')) {
623
            return $block;
624
        }
625 1
        if (!isset($block['element']['text']['attributes'])) {
626
            return $block;
627
        }
628
629
        try {
630 1
            $code = $block['element']['text']['text'];
631 1
            $languageClass = $block['element']['text']['attributes']['class'];
632 1
            $language = explode('-', $languageClass);
633 1
            $highlighted = $this->highlighter->highlight($language[1], $code);
634 1
            $block['element']['text']['attributes']['class'] = vsprintf('%s hljs %s', [
635 1
                $languageClass,
636 1
                $highlighted->language,
637 1
            ]);
638 1
            $block['element']['text']['rawHtml'] = $highlighted->value;
639 1
            $block['element']['text']['allowRawHtmlInSafeMode'] = true;
640 1
            unset($block['element']['text']['text']);
641
        } catch (\Exception $e) {
642
            $this->builder->getLogger()->debug($e->getMessage());
643
        } finally {
644 1
            return $block;
645
        }
646
    }
647
648
    /**
649
     * {@inheritdoc}
650
     */
651 1
    protected function parseAttributeData($attributeString)
652
    {
653 1
        $attributes = preg_split('/[ ]+/', $attributeString, -1, PREG_SPLIT_NO_EMPTY);
654 1
        $Data = [];
655 1
        $HtmlAtt = [];
656
657 1
        if (is_iterable($attributes)) {
658 1
            foreach ($attributes as $attribute) {
659 1
                switch ($attribute[0]) {
660 1
                    case '#': // ID
661 1
                        $Data['id'] = substr($attribute, 1);
662 1
                        break;
663 1
                    case '.': // Classes
664 1
                        $classes[] = substr($attribute, 1);
665 1
                        break;
666
                    default:  // Attributes
667 1
                        parse_str($attribute, $parsed);
668 1
                        $HtmlAtt = array_merge($HtmlAtt, $parsed);
669
                }
670
            }
671
672 1
            if (isset($classes)) {
673 1
                $Data['class'] = implode(' ', $classes);
674
            }
675 1
            if (!empty($HtmlAtt)) {
676 1
                foreach ($HtmlAtt as $a => $v) {
677 1
                    $Data[$a] = trim($v, '"');
678
                }
679
            }
680
        }
681
682 1
        return $Data;
683
    }
684
685
    /**
686
     * {@inheritdoc}
687
     *
688
     * Converts XHTML '<br />' tag to '<br>'.
689
     *
690
     * @return string
691
     */
692 1
    protected function unmarkedText($text)
693
    {
694 1
        return str_replace('<br />', '<br>', parent::unmarkedText($text)); // @phpstan-ignore staticMethod.notFound
695
    }
696
697
    /**
698
     * {@inheritdoc}
699
     *
700
     * XHTML closing tag to HTML5 closing tag.
701
     *
702
     * @return string
703
     */
704 1
    protected function element(array $Element)
705
    {
706 1
        return str_replace(' />', '>', parent::element($Element)); // @phpstan-ignore staticMethod.notFound
707
    }
708
709
    /**
710
     * Turns a path relative to static or assets into a website relative path.
711
     *
712
     *   "../../assets/images/img.jpeg"
713
     *   ->
714
     *   "/images/img.jpeg"
715
     */
716 1
    private function normalizePath(string $path): string
717
    {
718
        // https://regex101.com/r/Rzguzh/1
719 1
        $pattern = \sprintf(
720 1
            '(\.\.\/)+(\b%s|%s\b)+(\/.*)',
721 1
            (string) $this->config->get('static.dir'),
722 1
            (string) $this->config->get('assets.dir')
723 1
        );
724 1
        $path = Util::joinPath($path);
725 1
        if (!preg_match('/' . $pattern . '/is', $path, $matches)) {
726 1
            return $path;
727
        }
728
729 1
        return $matches[3];
730
    }
731
732
    /**
733
     * Create a media (video or audio) element from a link.
734
     */
735 1
    private function createMediaFromLink(array $link, string $type = 'video'): array
736
    {
737 1
        $block = [
738 1
            'extent'  => $link['extent'],
739 1
            'element' => [
740 1
                'text' => $link['element']['text'],
741 1
            ],
742 1
        ];
743 1
        $block['element']['attributes'] = $link['element']['attributes'];
744 1
        unset($block['element']['attributes']['href']);
745 1
        $block['element']['attributes']['src'] = new Url($this->builder, new Asset($this->builder, $link['element']['attributes']['href']));
746
        switch ($type) {
747 1
            case 'video':
748 1
                $block['element']['name'] = 'video';
749
                // no controls = autoplay, loop, muted, playsinline
750 1
                if (!isset($block['element']['attributes']['controls'])) {
751 1
                    $block['element']['attributes']['autoplay'] = '';
752 1
                    $block['element']['attributes']['loop'] = '';
753 1
                    $block['element']['attributes']['muted'] = '';
754 1
                    $block['element']['attributes']['playsinline'] = '';
755
                }
756 1
                if (isset($block['element']['attributes']['poster'])) {
757 1
                    $block['element']['attributes']['poster'] = new Url($this->builder, new Asset($this->builder, $block['element']['attributes']['poster']));
758
                }
759 1
                if (!\array_key_exists('style', $block['element']['attributes'])) {
760 1
                    $block['element']['attributes']['style'] = '';
761
                }
762 1
                $block['element']['attributes']['style'] .= ';max-width:100%;height:auto;background-color:#d8d8d8;'; // background color if offline
763
764 1
                return $block;
765 1
            case 'audio':
766 1
                $block['element']['name'] = 'audio';
767
768 1
                return $block;
769
        }
770
771
        throw new \Exception(\sprintf('Can\'t create %s from "%s".', $type, $link['element']['attributes']['href']));
772
    }
773
774
    /**
775
     * Create a figure / caption element.
776
     */
777 1
    private function createFigure(array $inline): array
778
    {
779 1
        if (empty($inline['element']['attributes']['title'])) {
780 1
            return $inline;
781
        }
782
783 1
        $titleRawHtml = $this->line($inline['element']['attributes']['title']); // @phpstan-ignore method.notFound
784 1
        $inline['element']['attributes']['title'] = strip_tags($titleRawHtml);
785
786 1
        $figcaption = [
787 1
            'element' => [
788 1
                'name'                   => 'figcaption',
789 1
                'allowRawHtmlInSafeMode' => true,
790 1
                'rawHtml'                => $titleRawHtml,
791 1
            ],
792 1
        ];
793 1
        $figure = [
794 1
            'extent'  => $inline['extent'],
795 1
            'element' => [
796 1
                'name'    => 'figure',
797 1
                'handler' => 'elements',
798 1
                'text'    => [
799 1
                    $inline['element'],
800 1
                    $figcaption['element'],
801 1
                ],
802 1
            ],
803 1
        ];
804
805 1
        return $figure;
806
    }
807
808
    /**
809
     * Handle an external link.
810
     */
811 1
    private function handleExternalLink(array $link): array
812
    {
813
        if (
814 1
            str_starts_with($link['element']['attributes']['href'], 'http')
815 1
            && (!empty($this->config->get('baseurl')) && !str_starts_with($link['element']['attributes']['href'], (string) $this->config->get('baseurl')))
816
        ) {
817 1
            if ($this->config->isEnabled('pages.body.links.external.blank')) {
818 1
                $link['element']['attributes']['target'] = '_blank';
819
            }
820 1
            if (!\array_key_exists('rel', $link['element']['attributes'])) {
821 1
                $link['element']['attributes']['rel'] = '';
822
            }
823 1
            if ($this->config->isEnabled('pages.body.links.external.noopener')) {
824 1
                $link['element']['attributes']['rel'] .= ' noopener';
825
            }
826 1
            if ($this->config->isEnabled('pages.body.links.external.noreferrer')) {
827 1
                $link['element']['attributes']['rel'] .= ' noreferrer';
828
            }
829 1
            if ($this->config->isEnabled('pages.body.links.external.nofollow')) {
830 1
                $link['element']['attributes']['rel'] .= ' nofollow';
831
            }
832 1
            $link['element']['attributes']['rel'] = trim($link['element']['attributes']['rel']);
833
        }
834
835 1
        return $link;
836
    }
837
}
838