Parsedown::inlineLink()   C
last analyzed

Complexity

Conditions 13
Paths 29

Size

Total Lines 87
Code Lines 46

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 48
CRAP Score 13.0108

Importance

Changes 7
Bugs 4 Features 1
Metric Value
cc 13
eloc 46
c 7
b 4
f 1
nc 29
nop 1
dl 0
loc 87
ccs 48
cts 50
cp 0.96
crap 13.0108
rs 6.6166

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\Asset;
17
use Cecil\Asset\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
149 1
            return $this->createFigure($video);
150
        }
151
        // audio?
152 1
        if (\in_array($extension, $this->config->get('pages.body.links.embed.audio') ?? [])) {
153 1
            if (!$embed) {
154 1
                $link['element']['attributes']['href'] = new Url($this->builder, $link['element']['attributes']['href']);
155
156 1
                return $link;
157
            }
158 1
            $audio = $this->createMediaFromLink($link, 'audio');
159
160 1
            return $this->createFigure($audio);
161
        }
162 1
        if (!$embed) {
163 1
            return $link;
164
        }
165
        // Youtube link?
166
        // https://regex101.com/r/gznM1j/1
167 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,})';
168 1
        if (preg_match('/' . $pattern . '/is', (string) $link['element']['attributes']['href'], $matches)) {
169 1
            return $this->createFigure($this->createEmbeddedVideoFromLink($link, 'https://www.youtube.com/embed/', $matches[1]));
170
        }
171
        // Vimeo link?
172
        // https://regex101.com/r/wCEFhd/1
173 1
        $pattern = 'https:\/\/vimeo\.com\/([0-9]+)';
174 1
        if (preg_match('/' . $pattern . '/is', (string) $link['element']['attributes']['href'], $matches)) {
175
            return $this->createFigure($this->createEmbeddedVideoFromLink($link, 'https://player.vimeo.com/video/', $matches[1]));
176
        }
177
        // GitHub Gist link?
178
        // https://regex101.com/r/KWVMYI/1
179 1
        $pattern = 'https:\/\/gist\.github\.com\/[-a-zA-Z0-9_]+\/[-a-zA-Z0-9_]+';
180 1
        if (preg_match('/' . $pattern . '/is', (string) $link['element']['attributes']['href'], $matches)) {
181 1
            $gist = [
182 1
                'extent'  => $link['extent'],
183 1
                'element' => [
184 1
                    'name'       => 'script',
185 1
                    'text'       => $link['element']['text'],
186 1
                    'attributes' => [
187 1
                        'src'   => $matches[0] . '.js',
188 1
                        'title' => $link['element']['attributes']['title'],
189 1
                    ],
190 1
                ],
191 1
            ];
192
193 1
            return $this->createFigure($gist);
194
        }
195
196 1
        return $link;
197
    }
198
199
    /**
200
     * {@inheritdoc}
201
     */
202 1
    protected function inlineUrl($Excerpt)
203
    {
204 1
        $link = parent::inlineUrl($Excerpt); // @phpstan-ignore staticMethod.notFound
205
206 1
        if (!isset($link)) {
207 1
            return;
208
        }
209
210
        // External link
211
        return $this->handleExternalLink($link);
212
    }
213
214
    /**
215
     * {@inheritdoc}
216
     */
217
    protected function inlineUrlTag($Excerpt)
218
    {
219
        $link = parent::inlineUrlTag($Excerpt); // @phpstan-ignore staticMethod.notFound
220
221
        if (!isset($link)) {
222
            return;
223
        }
224
225
        // External link
226
        return $this->handleExternalLink($link);
227
    }
228
229
    /**
230
     * {@inheritdoc}
231
     */
232 1
    protected function inlineImage($Excerpt)
233
    {
234 1
        $InlineImage = parent::inlineImage($Excerpt); // @phpstan-ignore staticMethod.notFound
235 1
        if (!isset($InlineImage)) {
236
            return null;
237
        }
238
239
        // remove link attributes
240 1
        unset($InlineImage['element']['attributes']['target'], $InlineImage['element']['attributes']['rel']);
241
242
        // normalize path
243 1
        $InlineImage['element']['attributes']['src'] = $this->normalizePath($InlineImage['element']['attributes']['src']);
244
245
        // should be lazy loaded?
246 1
        if ($this->config->isEnabled('pages.body.images.lazy') && !isset($InlineImage['element']['attributes']['loading'])) {
247 1
            $InlineImage['element']['attributes']['loading'] = 'lazy';
248
        }
249
        // should be decoding async?
250 1
        if ($this->config->isEnabled('pages.body.images.decoding') && !isset($InlineImage['element']['attributes']['decoding'])) {
251 1
            $InlineImage['element']['attributes']['decoding'] = 'async';
252
        }
253
        // add default class?
254 1
        if ((string) $this->config->get('pages.body.images.class')) {
255 1
            if (!\array_key_exists('class', $InlineImage['element']['attributes'])) {
256 1
                $InlineImage['element']['attributes']['class'] = '';
257
            }
258 1
            $InlineImage['element']['attributes']['class'] .= ' ' . (string) $this->config->get('pages.body.images.class');
259 1
            $InlineImage['element']['attributes']['class'] = trim($InlineImage['element']['attributes']['class']);
260
        }
261
262
        // disable remote image handling?
263 1
        if (Util\File::isRemote($InlineImage['element']['attributes']['src']) && !$this->config->isEnabled('pages.body.images.remote')) {
264
            return $this->createFigure($InlineImage);
265
        }
266
267
        // create asset
268 1
        $assetOptions = ['leading_slash' => false];
269 1
        if ($this->config->isEnabled('pages.body.images.remote.fallback')) {
270 1
            $assetOptions = ['leading_slash' => true];
271 1
            $assetOptions += ['fallback' => (string) $this->config->get('pages.body.images.remote.fallback')];
272
        }
273 1
        $asset = new Asset($this->builder, $InlineImage['element']['attributes']['src'], $assetOptions);
274 1
        $InlineImage['element']['attributes']['src'] = new Url($this->builder, $asset);
275 1
        $width = $asset['width'];
276
277
        /*
278
         * Should be resized?
279
         */
280 1
        $shouldResize = false;
281 1
        $assetResized = null;
282
        // pages.body.images.resize
283
        if (
284 1
            \is_int($this->config->get('pages.body.images.resize'))
285 1
            && $this->config->get('pages.body.images.resize') > 0
286 1
            && $width > $this->config->get('pages.body.images.resize')
287
        ) {
288
            $shouldResize = true;
289
            $width = $this->config->get('pages.body.images.resize');
290
        }
291
        // width attribute
292
        if (
293 1
            isset($InlineImage['element']['attributes']['width'])
294 1
            && $width > (int) $InlineImage['element']['attributes']['width']
295
        ) {
296 1
            $shouldResize = true;
297 1
            $width = (int) $InlineImage['element']['attributes']['width'];
298
        }
299
        // responsive images
300
        if (
301 1
            $this->config->isEnabled('pages.body.images.responsive')
302 1
            && !empty($this->config->getAssetsImagesWidths())
303 1
            && $width > max($this->config->getAssetsImagesWidths())
304
        ) {
305
            $shouldResize = true;
306
            $width = max($this->config->getAssetsImagesWidths());
307
        }
308 1
        if ($shouldResize) {
309
            try {
310 1
                $assetResized = $asset->resize($width);
311
            } catch (\Exception $e) {
312
                $this->builder->getLogger()->debug($e->getMessage());
313
314
                return $this->createFigure($InlineImage);
315
            }
316
        }
317
318
        // set width
319 1
        $InlineImage['element']['attributes']['width'] = $width;
320
        // set height
321 1
        $InlineImage['element']['attributes']['height'] = $assetResized['height'] ?? $asset['height'];
322
323
        // placeholder
324
        if (
325 1
            (!empty($this->config->get('pages.body.images.placeholder')) || isset($InlineImage['element']['attributes']['placeholder']))
326 1
            && \in_array($assetResized['subtype'] ?? $asset['subtype'], ['image/jpeg', 'image/png', 'image/gif'])
327
        ) {
328 1
            if (!\array_key_exists('placeholder', $InlineImage['element']['attributes'])) {
329 1
                $InlineImage['element']['attributes']['placeholder'] = (string) $this->config->get('pages.body.images.placeholder');
330
            }
331 1
            if (!\array_key_exists('style', $InlineImage['element']['attributes'])) {
332 1
                $InlineImage['element']['attributes']['style'] = '';
333
            }
334 1
            $InlineImage['element']['attributes']['style'] = trim($InlineImage['element']['attributes']['style'], ';');
335 1
            switch ($InlineImage['element']['attributes']['placeholder']) {
336 1
                case 'color':
337 1
                    $InlineImage['element']['attributes']['style'] .= \sprintf(';max-width:100%%;height:auto;background-color:%s;', Image::getDominantColor($assetResized ?? $asset));
338 1
                    break;
339 1
                case 'lqip':
340
                    // aborts if animated GIF for performance reasons
341 1
                    if (Image::isAnimatedGif($assetResized ?? $asset)) {
342
                        break;
343
                    }
344 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));
345 1
                    break;
346
            }
347 1
            unset($InlineImage['element']['attributes']['placeholder']);
348 1
            $InlineImage['element']['attributes']['style'] = trim($InlineImage['element']['attributes']['style']);
349
        }
350
351
        /*
352
         * Should be responsive?
353
         */
354 1
        $sizes = '';
355 1
        if ($this->config->isEnabled('pages.body.images.responsive')) {
356
            try {
357
                if (
358 1
                    $srcset = Image::buildHtmlSrcset(
359 1
                        $assetResized ?? $asset,
360 1
                        $this->config->getAssetsImagesWidths()
361 1
                    )
362
                ) {
363 1
                    $InlineImage['element']['attributes']['srcset'] = $srcset;
364 1
                    $sizes = Image::getHtmlSizes($InlineImage['element']['attributes']['class'] ?? '', (array) $this->config->getAssetsImagesSizes());
365 1
                    $InlineImage['element']['attributes']['sizes'] = $sizes;
366
                }
367 1
            } catch (\Exception $e) {
368 1
                $this->builder->getLogger()->debug($e->getMessage());
369
            }
370
        }
371
372
        /*
373
        <!-- if title: a <figure> is required to put in it a <figcaption> -->
374
        <figure>
375
            <!-- if formats: a <picture> is required for each <source> -->
376
            <picture>
377
                <source type="image/avif"
378
                    srcset="..."
379
                    sizes="..."
380
                >
381
                <source type="image/webp"
382
                    srcset="..."
383
                    sizes="..."
384
                >
385
                <img src="..."
386
                    srcset="..."
387
                    sizes="..."
388
                >
389
            </picture>
390
            <figcaption><!-- title --></figcaption>
391
        </figure>
392
        */
393
394 1
        $image = $InlineImage;
395
396
        // converts image to formats and put them in picture > source
397
        if (
398 1
            \count($formats = ((array) $this->config->get('pages.body.images.formats'))) > 0
399 1
            && \in_array($assetResized['subtype'] ?? $asset['subtype'], ['image/jpeg', 'image/png', 'image/gif'])
400
        ) {
401
            try {
402
                // abord if InlineImage is an animated GIF
403 1
                if (Image::isAnimatedGif($assetResized ?? $asset)) {
404 1
                    $filepath = Util::joinFile($this->config->getOutputPath(), $assetResized['path'] ?? $asset['path'] ?? '');
405 1
                    throw new RuntimeException(\sprintf('Asset "%s" is not converted (animated GIF).', $filepath));
406
                }
407 1
                $sources = [];
408 1
                foreach ($formats as $format) {
409 1
                    $srcset = '';
410
                    try {
411 1
                        $assetConverted = ($assetResized ?? $asset)->convert($format);
412 1
                    } catch (\Exception $e) {
413 1
                        $this->builder->getLogger()->error($e->getMessage());
414 1
                        continue;
415
                    }
416
                    // build responsive images?
417
                    if ($this->config->isEnabled('pages.body.images.responsive')) {
418
                        try {
419
                            $srcset = Image::buildHtmlSrcset($assetConverted, $this->config->getAssetsImagesWidths());
420
                        } catch (\Exception $e) {
421
                            $this->builder->getLogger()->debug($e->getMessage());
422
                        }
423
                    }
424
                    // if not, use default image as srcset
425
                    if (empty($srcset)) {
426
                        $srcset = (string) $assetConverted;
427
                    }
428
                    // add format to <sources>
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 1
                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 1
                    $image = $picture;
455
                }
456 1
            } catch (\Exception $e) {
457 1
                $this->builder->getLogger()->debug($e->getMessage());
458
            }
459
        }
460
461 1
        return $this->createFigure($image);
462
    }
463
464
    /**
465
     * Image block.
466
     */
467 1
    protected function blockImage($Excerpt)
468
    {
469 1
        if (1 !== preg_match($this->regexImage, $Excerpt['text'])) {
470
            return;
471
        }
472
473 1
        $InlineImage = $this->inlineImage($Excerpt);
474 1
        if (!isset($InlineImage)) {
475
            return;
476
        }
477
478 1
        return $InlineImage;
479
    }
480
481
    /**
482
     * Note block-level markup.
483
     *
484
     * :::tip
485
     * **Tip:** This is an advice.
486
     * :::
487
     *
488
     * Code inspired by https://github.com/sixlive/parsedown-alert from TJ Miller (@sixlive).
489
     */
490 1
    protected function blockNote($block)
491
    {
492 1
        if (preg_match('/:::(.*)/', $block['text'], $matches)) {
493 1
            $block = [
494 1
                'char'    => ':',
495 1
                'element' => [
496 1
                    'name'       => 'aside',
497 1
                    'text'       => '',
498 1
                    'attributes' => [
499 1
                        'class' => 'note',
500 1
                    ],
501 1
                ],
502 1
            ];
503 1
            if (!empty($matches[1])) {
504 1
                $block['element']['attributes']['class'] .= " note-{$matches[1]}";
505
            }
506
507 1
            return $block;
508
        }
509
    }
510
511 1
    protected function blockNoteContinue($line, $block)
512
    {
513 1
        if (isset($block['complete'])) {
514 1
            return;
515
        }
516 1
        if (preg_match('/:::/', $line['text'])) {
517 1
            $block['complete'] = true;
518
519 1
            return $block;
520
        }
521 1
        $block['element']['text'] .= $line['text'] . "\n";
522
523 1
        return $block;
524
    }
525
526 1
    protected function blockNoteComplete($block)
527
    {
528 1
        $block['element']['rawHtml'] = $this->text($block['element']['text']);
529 1
        unset($block['element']['text']);
530
531 1
        return $block;
532
    }
533
534
    /**
535
     * Apply Highlight to code blocks.
536
     */
537 1
    protected function blockFencedCodeComplete($block)
538
    {
539 1
        if (!$this->config->isEnabled('pages.body.highlight')) {
540
            return $block;
541
        }
542 1
        if (!isset($block['element']['text']['attributes'])) {
543
            return $block;
544
        }
545
546
        try {
547 1
            $code = $block['element']['text']['text'];
548 1
            $languageClass = $block['element']['text']['attributes']['class'];
549 1
            $language = explode('-', $languageClass);
550 1
            $highlighted = $this->highlighter->highlight($language[1], $code);
551 1
            $block['element']['text']['attributes']['class'] = vsprintf('%s hljs %s', [
552 1
                $languageClass,
553 1
                $highlighted->language,
554 1
            ]);
555 1
            $block['element']['text']['rawHtml'] = $highlighted->value;
556 1
            $block['element']['text']['allowRawHtmlInSafeMode'] = true;
557 1
            unset($block['element']['text']['text']);
558
        } catch (\Exception $e) {
559
            $this->builder->getLogger()->debug("Highlighter: " . $e->getMessage());
560
        } finally {
561 1
            return $block;
562
        }
563
    }
564
565
    /**
566
     * {@inheritdoc}
567
     */
568 1
    protected function parseAttributeData($attributeString)
569
    {
570 1
        $attributes = preg_split('/[ ]+/', $attributeString, -1, PREG_SPLIT_NO_EMPTY);
571 1
        $Data = [];
572 1
        $HtmlAtt = [];
573
574 1
        if (is_iterable($attributes)) {
575 1
            foreach ($attributes as $attribute) {
576 1
                switch ($attribute[0]) {
577 1
                    case '#': // ID
578 1
                        $Data['id'] = substr($attribute, 1);
579 1
                        break;
580 1
                    case '.': // Classes
581 1
                        $classes[] = substr($attribute, 1);
582 1
                        break;
583
                    default:  // Attributes
584 1
                        parse_str($attribute, $parsed);
585 1
                        $HtmlAtt = array_merge($HtmlAtt, $parsed);
586
                }
587
            }
588
589 1
            if (isset($classes)) {
590 1
                $Data['class'] = implode(' ', $classes);
591
            }
592 1
            if (!empty($HtmlAtt)) {
593 1
                foreach ($HtmlAtt as $a => $v) {
594 1
                    $Data[$a] = trim($v, '"');
595
                }
596
            }
597
        }
598
599 1
        return $Data;
600
    }
601
602
    /**
603
     * {@inheritdoc}
604
     *
605
     * Converts XHTML '<br />' tag to '<br>'.
606
     *
607
     * @return string
608
     */
609 1
    protected function unmarkedText($text)
610
    {
611 1
        return str_replace('<br />', '<br>', parent::unmarkedText($text)); // @phpstan-ignore staticMethod.notFound
612
    }
613
614
    /**
615
     * {@inheritdoc}
616
     *
617
     * XHTML closing tag to HTML5 closing tag.
618
     *
619
     * @return string
620
     */
621 1
    protected function element(array $Element)
622
    {
623 1
        return str_replace(' />', '>', parent::element($Element)); // @phpstan-ignore staticMethod.notFound
624
    }
625
626
    /**
627
     * Turns a path relative to static or assets into a website relative path.
628
     *
629
     *   "../../assets/images/img.jpeg"
630
     *   ->
631
     *   "/images/img.jpeg"
632
     */
633 1
    private function normalizePath(string $path): string
634
    {
635
        // https://regex101.com/r/Rzguzh/1
636 1
        $pattern = \sprintf(
637 1
            '(\.\.\/)+(\b%s|%s\b)+(\/.*)',
638 1
            (string) $this->config->get('static.dir'),
639 1
            (string) $this->config->get('assets.dir')
640 1
        );
641 1
        $path = Util::joinPath($path);
642 1
        if (!preg_match('/' . $pattern . '/is', $path, $matches)) {
643 1
            return $path;
644
        }
645
646 1
        return $matches[3];
647
    }
648
649
    /**
650
     * Create a media (video or audio) element from a link.
651
     */
652 1
    private function createMediaFromLink(array $link, string $type = 'video'): array
653
    {
654 1
        $block = [
655 1
            'extent'  => $link['extent'],
656 1
            'element' => [
657 1
                'text' => $link['element']['text'],
658 1
            ],
659 1
        ];
660 1
        $block['element']['attributes'] = $link['element']['attributes'];
661 1
        unset($block['element']['attributes']['href']);
662 1
        $block['element']['attributes']['src'] = new Url($this->builder, new Asset($this->builder, $link['element']['attributes']['href']));
663
        switch ($type) {
664 1
            case 'video':
665 1
                $block['element']['name'] = 'video';
666
                // no controls = autoplay, loop, muted, playsinline
667 1
                if (!isset($block['element']['attributes']['controls'])) {
668 1
                    $block['element']['attributes']['autoplay'] = '';
669 1
                    $block['element']['attributes']['loop'] = '';
670 1
                    $block['element']['attributes']['muted'] = '';
671 1
                    $block['element']['attributes']['playsinline'] = '';
672
                }
673 1
                if (isset($block['element']['attributes']['poster'])) {
674 1
                    $block['element']['attributes']['poster'] = new Url($this->builder, new Asset($this->builder, $block['element']['attributes']['poster']));
675
                }
676 1
                if (!\array_key_exists('style', $block['element']['attributes'])) {
677 1
                    $block['element']['attributes']['style'] = '';
678
                }
679 1
                $block['element']['attributes']['style'] .= ';max-width:100%;height:auto;background-color:#d8d8d8;'; // background color if offline
680
681 1
                return $block;
682 1
            case 'audio':
683 1
                $block['element']['name'] = 'audio';
684
685 1
                return $block;
686
        }
687
688
        throw new \Exception(\sprintf('Unable to create %s from "%s".', $type, $link['element']['attributes']['href']));
689
    }
690
691
    /**
692
     * Create an embedded video element from a link.
693
     *
694
     * $baseSrc is the base URL to embed the video
695
     * $match is the video ID or the rest of the URL to append to $baseSrc
696
     */
697 1
    private function createEmbeddedVideoFromLink(array $link, string $baseSrc, string $match): array
698
    {
699 1
        $iframe = [
700 1
            'element' => [
701 1
                'name'       => 'iframe',
702 1
                'text'       => $link['element']['text'],
703 1
                'attributes' => [
704 1
                    'width'           => '640',
705 1
                    'height'          => '360',
706 1
                    'title'           => $link['element']['text'],
707 1
                    'src'             => Util::joinPath($baseSrc, $match),
708 1
                    'frameborder'     => '0',
709 1
                    'allow'           => 'accelerometer;autoplay;encrypted-media;gyroscope;picture-in-picture;',
710 1
                    'allowfullscreen' => '',
711 1
                    'style'           => 'position:absolute;top:0;left:0;width:100%;height:100%;border:0;background-color:#d8d8d8;',
712 1
                ],
713 1
            ],
714 1
        ];
715
716 1
        return [
717 1
            'extent'  => $link['extent'],
718 1
            'element' => [
719 1
                'name'    => 'div',
720 1
                'handler' => 'elements',
721 1
                'text'    => [
722 1
                    $iframe['element'],
723 1
                ],
724 1
                'attributes' => [
725 1
                    'style' => 'position:relative;padding-bottom:56.25%;height:0;overflow:hidden;',
726 1
                    'title' => $link['element']['attributes']['title'],
727 1
                ],
728 1
            ],
729 1
        ];
730
    }
731
732
    /**
733
     * Create a figure > figcaption element.
734
     */
735 1
    private function createFigure(array $inline): array
736
    {
737 1
        if (!$this->config->isEnabled('pages.body.images.caption')) {
738
            return $inline;
739
        }
740
741 1
        if (empty($inline['element']['attributes']['title'])) {
742 1
            return $inline;
743
        }
744
745 1
        $titleRawHtml = $this->line($inline['element']['attributes']['title']); // @phpstan-ignore method.notFound
746 1
        $inline['element']['attributes']['title'] = strip_tags($titleRawHtml);
747
748 1
        $figcaption = [
749 1
            'element' => [
750 1
                'name'                   => 'figcaption',
751 1
                'allowRawHtmlInSafeMode' => true,
752 1
                'rawHtml'                => $titleRawHtml,
753 1
            ],
754 1
        ];
755 1
        $figure = [
756 1
            'extent'  => $inline['extent'],
757 1
            'element' => [
758 1
                'name'    => 'figure',
759 1
                'handler' => 'elements',
760 1
                'text'    => [
761 1
                    $inline['element'],
762 1
                    $figcaption['element'],
763 1
                ],
764 1
            ],
765 1
        ];
766
767 1
        return $figure;
768
    }
769
770
    /**
771
     * Handle an external link.
772
     */
773 1
    private function handleExternalLink(array $link): array
774
    {
775
        if (
776 1
            str_starts_with($link['element']['attributes']['href'], 'http')
777 1
            && (!empty($this->config->get('baseurl')) && !str_starts_with($link['element']['attributes']['href'], (string) $this->config->get('baseurl')))
778
        ) {
779 1
            if ($this->config->isEnabled('pages.body.links.external.blank')) {
780 1
                $link['element']['attributes']['target'] = '_blank';
781
            }
782 1
            if (!\array_key_exists('rel', $link['element']['attributes'])) {
783 1
                $link['element']['attributes']['rel'] = '';
784
            }
785 1
            if ($this->config->isEnabled('pages.body.links.external.noopener')) {
786 1
                $link['element']['attributes']['rel'] .= ' noopener';
787
            }
788 1
            if ($this->config->isEnabled('pages.body.links.external.noreferrer')) {
789 1
                $link['element']['attributes']['rel'] .= ' noreferrer';
790
            }
791 1
            if ($this->config->isEnabled('pages.body.links.external.nofollow')) {
792 1
                $link['element']['attributes']['rel'] .= ' nofollow';
793
            }
794 1
            $link['element']['attributes']['rel'] = trim($link['element']['attributes']['rel']);
795
        }
796
797 1
        return $link;
798
    }
799
}
800