Passed
Pull Request — master (#2148)
by Arnaud
11:38 queued 05:02
created

Parsedown::inlineLink()   F

Complexity

Conditions 24
Paths 1190

Size

Total Lines 144
Code Lines 87

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 92
CRAP Score 24.0786

Importance

Changes 4
Bugs 1 Features 0
Metric Value
cc 24
eloc 87
c 4
b 1
f 0
nc 1190
nop 1
dl 0
loc 144
ccs 92
cts 97
cp 0.9485
crap 24.0786
rs 0

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