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

Parsedown::inlineLink()   F

Complexity

Conditions 18
Paths 44

Size

Total Lines 164
Code Lines 102

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 82
CRAP Score 28.2917

Importance

Changes 4
Bugs 3 Features 1
Metric Value
cc 18
eloc 102
c 4
b 3
f 1
nc 44
nop 1
dl 0
loc 164
ccs 82
cts 120
cp 0.6833
crap 28.2917
rs 3.8933

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * This file is part of Cecil.
5
 *
6
 * (c) Arnaud Ligny <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
13
14
namespace Cecil\Converter;
15
16
use Cecil\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