• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Cecilapp / Cecil / 12021297559

26 Nov 2024 12:25AM UTC coverage: 83.555% (-0.2%) from 83.781%
12021297559

push

github

web-flow
refactor: rebuild configuration (#2068)

63 of 85 new or added lines in 11 files covered. (74.12%)

6 existing lines in 1 file now uncovered.

2957 of 3539 relevant lines covered (83.55%)

0.84 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.95
/src/Converter/Parsedown.php
1
<?php
2

3
declare(strict_types=1);
4

5
/*
6
 * This file is part of Cecil.
7
 *
8
 * Copyright (c) Arnaud Ligny <arnaud@ligny.fr>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13

14
namespace Cecil\Converter;
15

16
use Cecil\Assets\Asset;
17
use Cecil\Assets\Image;
18
use Cecil\Builder;
19
use Cecil\Exception\RuntimeException;
20
use Cecil\Util;
21
use Highlight\Highlighter;
22

23
/**
24
 * @property array $InlineTypes
25
 * @property string $inlineMarkerList
26
 * @property array $specialCharacters
27
 * @property array $BlockTypes
28
 */
29
class Parsedown extends \ParsedownToc
30
{
31
    /** @var Builder */
32
    protected $builder;
33

34
    /** @var \Cecil\Config */
35
    protected $config;
36

37
    /** {@inheritdoc} */
38
    protected $regexAttribute = '(?:[#.][-\w:\\\]+[ ]*|[-\w:\\\]+(?:=(?:["\'][^\n]*?["\']|[^\s]+)?)?[ ]*)';
39

40
    /** Regex who's looking for images */
41
    protected $regexImage = "~^!\[.*?\]\(.*?\)~";
42

43
    /** @var Highlighter */
44
    protected $highlighter;
45

46
    public function __construct(Builder $builder, ?array $options = null)
47
    {
48
        $this->builder = $builder;
1✔
49
        $this->config = $builder->getConfig();
1✔
50

51
        // "insert" line block: ++text++ -> <ins>text</ins>
52
        $this->InlineTypes['+'][] = 'Insert';
1✔
53
        $this->inlineMarkerList = implode('', array_keys($this->InlineTypes));
1✔
54
        $this->specialCharacters[] = '+';
1✔
55

56
        // Image block (to avoid paragraph)
57
        $this->BlockTypes['!'][] = 'Image';
1✔
58

59
        // "notes" block
60
        $this->BlockTypes[':'][] = 'Note';
1✔
61

62
        // code highlight
63
        $this->highlighter = new Highlighter();
1✔
64

65
        // options
66
        $options = array_merge(['selectors' => (array) $this->config->get('pages.body.toc')], $options ?? []);
1✔
67

68
        parent::__construct();
1✔
69
        parent::setOptions($options);
1✔
70
    }
71

72
    /**
73
     * Insert inline.
74
     * e.g.: ++text++ -> <ins>text</ins>.
75
     */
76
    protected function inlineInsert($Excerpt)
77
    {
78
        if (!isset($Excerpt['text'][1])) {
1✔
79
            return;
×
80
        }
81

82
        if ($Excerpt['text'][1] === '+' && preg_match('/^\+\+(?=\S)(.+?)(?<=\S)\+\+/', $Excerpt['text'], $matches)) {
1✔
83
            return [
1✔
84
                'extent'  => \strlen($matches[0]),
1✔
85
                'element' => [
1✔
86
                    'name'    => 'ins',
1✔
87
                    'text'    => $matches[1],
1✔
88
                    'handler' => 'line',
1✔
89
                ],
1✔
90
            ];
1✔
91
        }
92
    }
93

94
    /**
95
     * {@inheritdoc}
96
     */
97
    protected function inlineLink($Excerpt)
98
    {
99
        $link = parent::inlineLink($Excerpt); // @phpstan-ignore staticMethod.notFound
1✔
100

101
        if (!isset($link)) {
1✔
102
            return null;
×
103
        }
104

105
        // Link to a page with "page:page_id" as URL
106
        if (Util\Str::startsWith($link['element']['attributes']['href'], 'page:')) {
1✔
107
            $link['element']['attributes']['href'] = new \Cecil\Assets\Url($this->builder, substr($link['element']['attributes']['href'], 5, \strlen($link['element']['attributes']['href'])));
1✔
108

109
            return $link;
1✔
110
        }
111

112
        // External link
113
        if (str_starts_with($link['element']['attributes']['href'], 'http') && !str_starts_with($link['element']['attributes']['href'], (string) $this->config->get('baseurl'))) {
1✔
NEW
114
            if ($this->config->get('pages.body.links.external.blank')) {
×
115
                $link['element']['attributes']['target'] = '_blank';
×
116
            }
UNCOV
117
            if (!\array_key_exists('rel', $link['element']['attributes'])) {
×
UNCOV
118
                $link['element']['attributes']['rel'] = '';
×
119
            }
NEW
120
            if ($this->config->get('pages.body.links.external.noopener')) {
×
UNCOV
121
                $link['element']['attributes']['rel'] .= ' noopener';
×
122
            }
NEW
123
            if ($this->config->get('pages.body.links.external.noreferrer')) {
×
UNCOV
124
                $link['element']['attributes']['rel'] .= ' noreferrer';
×
125
            }
NEW
126
            if ($this->config->get('pages.body.links.external.nofollow')) {
×
UNCOV
127
                $link['element']['attributes']['rel'] .= ' nofollow';
×
128
            }
UNCOV
129
            $link['element']['attributes']['rel'] = trim($link['element']['attributes']['rel']);
×
130
        }
131

132
        /*
133
         * Embed link?
134
         */
135
        $embed = false;
1✔
136
        $embed = (bool) $this->config->get('pages.body.links.embed.enabled');
1✔
137
        if (isset($link['element']['attributes']['embed'])) {
1✔
138
            $embed = true;
1✔
139
            if ($link['element']['attributes']['embed'] == 'false') {
1✔
140
                $embed = false;
1✔
141
            }
142
            unset($link['element']['attributes']['embed']);
1✔
143
        }
144
        // video or audio?
145
        $extension = pathinfo($link['element']['attributes']['href'], PATHINFO_EXTENSION);
1✔
146
        if (\in_array($extension, (array) $this->config->get('pages.body.links.embed.video.ext'))) {
1✔
147
            if (!$embed) {
1✔
148
                $link['element']['attributes']['href'] = (string) new Asset($this->builder, $link['element']['attributes']['href'], ['force_slash' => false]);
1✔
149

150
                return $link;
1✔
151
            }
152
            $video = $this->createMediaFromLink($link, 'video');
1✔
153
            if ((bool) $this->config->get('pages.body.images.caption.enabled')) {
1✔
154
                return $this->createFigure($video);
1✔
155
            }
156

157
            return $video;
×
158
        }
159
        if (\in_array($extension, (array) $this->config->get('pages.body.links.embed.audio.ext'))) {
1✔
160
            if (!$embed) {
1✔
161
                $link['element']['attributes']['href'] = (string) new Asset($this->builder, $link['element']['attributes']['href'], ['force_slash' => false]);
1✔
162

163
                return $link;
1✔
164
            }
165
            $audio = $this->createMediaFromLink($link, 'audio');
1✔
166
            if ((bool) $this->config->get('pages.body.images.caption.enabled')) {
1✔
167
                return $this->createFigure($audio);
1✔
168
            }
169

170
            return $audio;
×
171
        }
172
        if (!$embed) {
1✔
173
            return $link;
1✔
174
        }
175
        // GitHub Gist link?
176
        // https://regex101.com/r/QmCiAL/1
177
        $pattern = 'https:\/\/gist\.github.com\/[-a-zA-Z0-9_]+\/[-a-zA-Z0-9_]+';
1✔
178
        if (preg_match('/' . $pattern . '/is', (string) $link['element']['attributes']['href'], $matches)) {
1✔
179
            $gist = [
1✔
180
                'extent'  => $link['extent'],
1✔
181
                'element' => [
1✔
182
                    'name'       => 'script',
1✔
183
                    'text'       => $link['element']['text'],
1✔
184
                    'attributes' => [
1✔
185
                        'src'   => $matches[0] . '.js',
1✔
186
                        'title' => $link['element']['attributes']['title'],
1✔
187
                    ],
1✔
188
                ],
1✔
189
            ];
1✔
190
            if ((bool) $this->config->get('pages.body.images.caption.enabled')) {
1✔
191
                return $this->createFigure($gist);
1✔
192
            }
193

194
            return $gist;
×
195
        }
196
        // Youtube link?
197
        // https://regex101.com/r/gznM1j/1
198
        $pattern = '(?:https?:\/\/)?(?:www\.)?youtu(?:\.be\/|be.com\/\S*(?:watch|embed)(?:(?:(?=\/[-a-zA-Z0-9_]{11,}(?!\S))\/)|(?:\S*v=|v\/)))([-a-zA-Z0-9_]{11,})';
1✔
199
        if (preg_match('/' . $pattern . '/is', (string) $link['element']['attributes']['href'], $matches)) {
1✔
200
            $iframe = [
1✔
201
                'element' => [
1✔
202
                    'name'       => 'iframe',
1✔
203
                    'text'       => $link['element']['text'],
1✔
204
                    'attributes' => [
1✔
205
                        'width'           => '560',
1✔
206
                        'height'          => '315',
1✔
207
                        'title'           => $link['element']['text'],
1✔
208
                        'src'             => 'https://www.youtube.com/embed/' . $matches[1],
1✔
209
                        'frameborder'     => '0',
1✔
210
                        'allow'           => 'accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture',
1✔
211
                        'allowfullscreen' => '',
1✔
212
                        'style'           => 'position:absolute; top:0; left:0; width:100%; height:100%; border:0',
1✔
213
                    ],
1✔
214
                ],
1✔
215
            ];
1✔
216
            $youtube = [
1✔
217
                'extent'  => $link['extent'],
1✔
218
                'element' => [
1✔
219
                    'name'    => 'div',
1✔
220
                    'handler' => 'elements',
1✔
221
                    'text'    => [
1✔
222
                        $iframe['element'],
1✔
223
                    ],
1✔
224
                    'attributes' => [
1✔
225
                        'style' => 'position:relative; padding-bottom:56.25%; height:0; overflow:hidden',
1✔
226
                        'title' => $link['element']['attributes']['title'],
1✔
227
                    ],
1✔
228
                ],
1✔
229
            ];
1✔
230
            if ((bool) $this->config->get('pages.body.images.caption.enabled')) {
1✔
231
                return $this->createFigure($youtube);
1✔
232
            }
233

234
            return $youtube;
×
235
        }
236

237
        return $link;
×
238
    }
239

240
    /**
241
     * {@inheritdoc}
242
     */
243
    protected function inlineImage($Excerpt)
244
    {
245
        $InlineImage = parent::inlineImage($Excerpt); // @phpstan-ignore staticMethod.notFound
1✔
246
        if (!isset($InlineImage)) {
1✔
247
            return null;
×
248
        }
249

250
        // normalize path
251
        $InlineImage['element']['attributes']['src'] = $this->normalizePath($InlineImage['element']['attributes']['src']);
1✔
252

253
        // should be lazy loaded?
254
        if ((bool) $this->config->get('pages.body.images.lazy.enabled') && !isset($InlineImage['element']['attributes']['loading'])) {
1✔
255
            $InlineImage['element']['attributes']['loading'] = 'lazy';
1✔
256
        }
257
        // should be decoding async?
258
        if ((bool) $this->config->get('pages.body.images.decoding.enabled') && !isset($InlineImage['element']['attributes']['decoding'])) {
1✔
259
            $InlineImage['element']['attributes']['decoding'] = 'async';
1✔
260
        }
261
        // add default class?
262
        if ((string) $this->config->get('pages.body.images.class')) {
1✔
263
            if (!\array_key_exists('class', $InlineImage['element']['attributes'])) {
1✔
264
                $InlineImage['element']['attributes']['class'] = '';
1✔
265
            }
266
            $InlineImage['element']['attributes']['class'] .= ' ' . (string) $this->config->get('pages.body.images.class');
1✔
267
            $InlineImage['element']['attributes']['class'] = trim($InlineImage['element']['attributes']['class']);
1✔
268
        }
269

270
        // disable remote image handling?
271
        if (Util\Url::isUrl($InlineImage['element']['attributes']['src']) && !(bool) $this->config->get('pages.body.images.remote.enabled')) {
1✔
272
            return $InlineImage;
×
273
        }
274

275
        // create asset
276
        $assetOptions = ['force_slash' => false];
1✔
277
        if ((bool) $this->config->get('pages.body.images.remote.fallback.enabled')) {
1✔
278
            $assetOptions += ['remote_fallback' => (string) $this->config->get('pages.body.images.remote.fallback.path')];
1✔
279
        }
280
        $asset = new Asset($this->builder, $InlineImage['element']['attributes']['src'], $assetOptions);
1✔
281
        $InlineImage['element']['attributes']['src'] = $asset;
1✔
282
        $width = $asset['width'];
1✔
283

284
        /*
285
         * Should be resized?
286
         */
287
        $shouldResize = false;
1✔
288
        $assetResized = null;
1✔
289
        if (
290
            (bool) $this->config->get('pages.body.images.resize.enabled')
1✔
291
            && isset($InlineImage['element']['attributes']['width'])
1✔
292
            && $width > (int) $InlineImage['element']['attributes']['width']
1✔
293
        ) {
294
            $shouldResize = true;
1✔
295
            $width = (int) $InlineImage['element']['attributes']['width'];
1✔
296
        }
297
        if (
298
            (bool) $this->config->get('pages.body.images.responsive.enabled')
1✔
299
            && !empty($this->config->getAssetsImagesWidths())
1✔
300
            && $width > max($this->config->getAssetsImagesWidths())
1✔
301
        ) {
302
            $shouldResize = true;
×
303
            $width = max($this->config->getAssetsImagesWidths());
×
304
        }
305
        if ($shouldResize) {
1✔
306
            try {
307
                $assetResized = $asset->resize($width);
1✔
308
                $InlineImage['element']['attributes']['src'] = $assetResized;
1✔
309
            } catch (\Exception $e) {
×
310
                $this->builder->getLogger()->debug($e->getMessage());
×
311

312
                return $InlineImage;
×
313
            }
314
        }
315

316
        // set width
317
        $InlineImage['element']['attributes']['width'] = $width;
1✔
318
        // set height
319
        $InlineImage['element']['attributes']['height'] = $assetResized['height'] ?? $asset['height'];
1✔
320

321
        // placeholder
322
        if (
323
            (!empty($this->config->get('pages.body.images.placeholder')) || isset($InlineImage['element']['attributes']['placeholder']))
1✔
324
            && \in_array($InlineImage['element']['attributes']['src']['subtype'], ['image/jpeg', 'image/png', 'image/gif'])
1✔
325
        ) {
326
            if (!\array_key_exists('placeholder', $InlineImage['element']['attributes'])) {
1✔
327
                $InlineImage['element']['attributes']['placeholder'] = (string) $this->config->get('pages.body.images.placeholder');
×
328
            }
329
            if (!\array_key_exists('style', $InlineImage['element']['attributes'])) {
1✔
330
                $InlineImage['element']['attributes']['style'] = '';
1✔
331
            }
332
            $InlineImage['element']['attributes']['style'] = trim($InlineImage['element']['attributes']['style'], ';');
1✔
333
            switch ($InlineImage['element']['attributes']['placeholder']) {
1✔
334
                case 'color':
1✔
335
                    $InlineImage['element']['attributes']['style'] .= \sprintf(';max-width:100%%;height:auto;background-color:%s;', Image::getDominantColor($InlineImage['element']['attributes']['src']));
1✔
336
                    break;
1✔
337
                case 'lqip':
1✔
338
                    $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']));
1✔
339
                    break;
1✔
340
            }
341
            unset($InlineImage['element']['attributes']['placeholder']);
1✔
342
            $InlineImage['element']['attributes']['style'] = trim($InlineImage['element']['attributes']['style']);
1✔
343
        }
344

345
        /*
346
         * Should be responsive?
347
         */
348
        $sizes = '';
1✔
349
        if ((bool) $this->config->get('pages.body.images.responsive.enabled')) {
1✔
350
            try {
351
                if (
352
                    $srcset = Image::buildSrcset(
1✔
353
                        $assetResized ?? $asset,
1✔
354
                        $this->config->getAssetsImagesWidths()
1✔
355
                    )
1✔
356
                ) {
357
                    $InlineImage['element']['attributes']['srcset'] = $srcset;
1✔
358
                    $sizes = Image::getSizes($InlineImage['element']['attributes']['class'] ?? '', (array) $this->config->getAssetsImagesSizes());
1✔
359
                    $InlineImage['element']['attributes']['sizes'] = $sizes;
1✔
360
                }
361
            } catch (\Exception $e) {
×
362
                $this->builder->getLogger()->debug($e->getMessage());
×
363
            }
364
        }
365

366
        /*
367
        <!-- if title: a <figure> is required to put in it a <figcaption> -->
368
        <figure>
369
            <!-- if formats: a <picture> is required for each <source> -->
370
            <picture>
371
                <source type="image/avif"
372
                    srcset="..."
373
                    sizes="..."
374
                >
375
                <source type="image/webp"
376
                    srcset="..."
377
                    sizes="..."
378
                >
379
                <img src="..."
380
                    srcset="..."
381
                    sizes="..."
382
                >
383
            </picture>
384
            <figcaption><!-- title --></figcaption>
385
        </figure>
386
        */
387

388
        $image = $InlineImage;
1✔
389

390
        // converts image to formats and put them in picture > source
391
        if (
392
            \count($formats = ((array) $this->config->get('pages.body.images.formats'))) > 0
1✔
393
            && \in_array($InlineImage['element']['attributes']['src']['subtype'], ['image/jpeg', 'image/png', 'image/gif'])
1✔
394
        ) {
395
            try {
396
                // InlineImage src must be an Asset instance
397
                if (!$InlineImage['element']['attributes']['src'] instanceof Asset) {
1✔
398
                    throw new RuntimeException(\sprintf('Asset "%s" can\'t be converted.', $InlineImage['element']['attributes']['src']));
×
399
                }
400
                // abord if InlineImage is an animated GIF
401
                if (Image::isAnimatedGif($InlineImage['element']['attributes']['src'])) {
1✔
402
                    throw new RuntimeException(\sprintf('Asset "%s" is an animated GIF.', $InlineImage['element']['attributes']['src']));
1✔
403
                }
404
                $sources = [];
1✔
405
                foreach ($formats as $format) {
1✔
406
                    $assetConverted = $InlineImage['element']['attributes']['src']->$format();
1✔
407
                    $srcset = '';
1✔
408
                    // build responsive images?
409
                    if ((bool) $this->config->get('pages.body.images.responsive.enabled')) {
1✔
410
                        try {
411
                            $srcset = Image::buildSrcset($assetConverted, $this->config->getAssetsImagesWidths());
1✔
412
                        } catch (\Exception $e) {
×
413
                            $this->builder->getLogger()->debug($e->getMessage());
×
414
                        }
415
                    }
416
                    // if not, use default image as srcset
417
                    if (empty($srcset)) {
1✔
418
                        $srcset = (string) $assetConverted;
1✔
419
                    }
420
                    $sources[] = [
1✔
421
                        'name'       => 'source',
1✔
422
                        'attributes' => [
1✔
423
                            'type'   => "image/$format",
1✔
424
                            'srcset' => $srcset,
1✔
425
                            'sizes'  => $sizes,
1✔
426
                            'width'  => $InlineImage['element']['attributes']['width'],
1✔
427
                            'height' => $InlineImage['element']['attributes']['height'],
1✔
428
                        ],
1✔
429
                    ];
1✔
430
                }
431
                if (\count($sources) > 0) {
1✔
432
                    $picture = [
1✔
433
                        'extent'  => $InlineImage['extent'],
1✔
434
                        'element' => [
1✔
435
                            'name'       => 'picture',
1✔
436
                            'handler'    => 'elements',
1✔
437
                            'attributes' => [
1✔
438
                                'title' => $image['element']['attributes']['title'],
1✔
439
                            ],
1✔
440
                        ],
1✔
441
                    ];
1✔
442
                    $picture['element']['text'] = $sources;
1✔
443
                    unset($image['element']['attributes']['title']); // @phpstan-ignore unset.offset
1✔
444
                    $picture['element']['text'][] = $image['element'];
1✔
445
                    $image = $picture;
1✔
446
                }
447
            } catch (\Exception $e) {
1✔
448
                $this->builder->getLogger()->debug($e->getMessage());
1✔
449
            }
450
        }
451

452
        // if title: put the <img> (or <picture>) in a <figure> and create a <figcaption>
453
        if ((bool) $this->config->get('pages.body.images.caption.enabled')) {
1✔
454
            return $this->createFigure($image);
1✔
455
        }
456

457
        return $image;
×
458
    }
459

460
    /**
461
     * Image block.
462
     */
463
    protected function blockImage($Excerpt)
464
    {
465
        if (1 !== preg_match($this->regexImage, $Excerpt['text'])) {
1✔
466
            return;
×
467
        }
468

469
        $InlineImage = $this->inlineImage($Excerpt);
1✔
470
        if (!isset($InlineImage)) {
1✔
471
            return;
×
472
        }
473

474
        return $InlineImage;
1✔
475
    }
476

477
    /**
478
     * Note block-level markup.
479
     *
480
     * :::tip
481
     * **Tip:** This is an advice.
482
     * :::
483
     *
484
     * Code inspired by https://github.com/sixlive/parsedown-alert from TJ Miller (@sixlive).
485
     */
486
    protected function blockNote($block)
487
    {
488
        if (preg_match('/:::(.*)/', $block['text'], $matches)) {
1✔
489
            $block = [
1✔
490
                'char'    => ':',
1✔
491
                'element' => [
1✔
492
                    'name'       => 'aside',
1✔
493
                    'text'       => '',
1✔
494
                    'attributes' => [
1✔
495
                        'class' => 'note',
1✔
496
                    ],
1✔
497
                ],
1✔
498
            ];
1✔
499
            if (!empty($matches[1])) {
1✔
500
                $block['element']['attributes']['class'] .= " note-{$matches[1]}";
1✔
501
            }
502

503
            return $block;
1✔
504
        }
505
    }
506

507
    protected function blockNoteContinue($line, $block)
508
    {
509
        if (isset($block['complete'])) {
1✔
510
            return;
1✔
511
        }
512
        if (preg_match('/:::/', $line['text'])) {
1✔
513
            $block['complete'] = true;
1✔
514

515
            return $block;
1✔
516
        }
517
        $block['element']['text'] .= $line['text'] . "\n";
1✔
518

519
        return $block;
1✔
520
    }
521

522
    protected function blockNoteComplete($block)
523
    {
524
        $block['element']['rawHtml'] = $this->text($block['element']['text']);
1✔
525
        unset($block['element']['text']);
1✔
526

527
        return $block;
1✔
528
    }
529

530
    /**
531
     * Apply Highlight to code blocks.
532
     */
533
    protected function blockFencedCodeComplete($block)
534
    {
535
        if (!(bool) $this->config->get('pages.body.highlight.enabled')) {
1✔
536
            return $block;
×
537
        }
538
        if (!isset($block['element']['text']['attributes'])) {
1✔
539
            return $block;
×
540
        }
541

542
        try {
543
            $code = $block['element']['text']['text'];
1✔
544
            $languageClass = $block['element']['text']['attributes']['class'];
1✔
545
            $language = explode('-', $languageClass);
1✔
546
            $highlighted = $this->highlighter->highlight($language[1], $code);
1✔
547
            $block['element']['text']['attributes']['class'] = vsprintf('%s hljs %s', [
1✔
548
                $languageClass,
1✔
549
                $highlighted->language,
1✔
550
            ]);
1✔
551
            $block['element']['text']['rawHtml'] = $highlighted->value;
1✔
552
            $block['element']['text']['allowRawHtmlInSafeMode'] = true;
1✔
553
            unset($block['element']['text']['text']);
1✔
554
        } catch (\Exception $e) {
×
555
            $this->builder->getLogger()->debug($e->getMessage());
×
556
        } finally {
557
            return $block;
1✔
558
        }
559
    }
560

561
    /**
562
     * {@inheritdoc}
563
     */
564
    protected function parseAttributeData($attributeString)
565
    {
566
        $attributes = preg_split('/[ ]+/', $attributeString, -1, PREG_SPLIT_NO_EMPTY);
1✔
567
        $Data = [];
1✔
568
        $HtmlAtt = [];
1✔
569

570
        foreach ($attributes as $attribute) {
1✔
571
            switch ($attribute[0]) {
1✔
572
                case '#': // ID
1✔
573
                    $Data['id'] = substr($attribute, 1);
1✔
574
                    break;
1✔
575
                case '.': // Classes
1✔
576
                    $classes[] = substr($attribute, 1);
1✔
577
                    break;
1✔
578
                default:  // Attributes
579
                    parse_str($attribute, $parsed);
1✔
580
                    $HtmlAtt = array_merge($HtmlAtt, $parsed);
1✔
581
            }
582
        }
583

584
        if (isset($classes)) {
1✔
585
            $Data['class'] = implode(' ', $classes);
1✔
586
        }
587
        if (!empty($HtmlAtt)) {
1✔
588
            foreach ($HtmlAtt as $a => $v) {
1✔
589
                $Data[$a] = trim($v, '"');
1✔
590
            }
591
        }
592

593
        return $Data;
1✔
594
    }
595

596
    /**
597
     * Turns a path relative to static or assets into a website relative path.
598
     *
599
     *   "../../assets/images/img.jpeg"
600
     *   ->
601
     *   "/images/img.jpeg"
602
     */
603
    private function normalizePath(string $path): string
604
    {
605
        // https://regex101.com/r/Rzguzh/1
606
        $pattern = \sprintf(
1✔
607
            '(\.\.\/)+(\b%s|%s\b)+(\/.*)',
1✔
608
            (string) $this->config->get('static.dir'),
1✔
609
            (string) $this->config->get('assets.dir')
1✔
610
        );
1✔
611
        $path = Util::joinPath($path);
1✔
612
        if (!preg_match('/' . $pattern . '/is', $path, $matches)) {
1✔
613
            return $path;
1✔
614
        }
615

616
        return $matches[3];
1✔
617
    }
618

619
    /**
620
     * Create a media (video or audio) element from a link.
621
     */
622
    private function createMediaFromLink(array $link, string $type = 'video'): array
623
    {
624
        $block = [
1✔
625
            'extent'  => $link['extent'],
1✔
626
            'element' => [
1✔
627
                'text' => $link['element']['text'],
1✔
628
            ],
1✔
629
        ];
1✔
630
        $block['element']['attributes'] = $link['element']['attributes'];
1✔
631
        unset($block['element']['attributes']['href']);
1✔
632
        $block['element']['attributes']['src'] = (string) new Asset($this->builder, $link['element']['attributes']['href'], ['force_slash' => false]);
1✔
633
        switch ($type) {
634
            case 'video':
1✔
635
                $block['element']['name'] = 'video';
1✔
636
                if (!isset($block['element']['attributes']['controls'])) {
1✔
637
                    $block['element']['attributes']['autoplay'] = '';
1✔
638
                    $block['element']['attributes']['loop'] = '';
1✔
639
                }
640
                if (isset($block['element']['attributes']['poster'])) {
1✔
641
                    $block['element']['attributes']['poster'] = (string) new Asset($this->builder, $block['element']['attributes']['poster'], ['force_slash' => false]);
1✔
642
                }
643

644
                return $block;
1✔
645
            case 'audio':
1✔
646
                $block['element']['name'] = 'audio';
1✔
647

648
                return $block;
1✔
649
        }
650

651
        throw new \Exception(\sprintf('Can\'t create %s from "%s".', $type, $link['element']['attributes']['href']));
×
652
    }
653

654
    /**
655
     * Create a figure / caption element.
656
     */
657
    private function createFigure(array $inline): array
658
    {
659
        if (empty($inline['element']['attributes']['title'])) {
1✔
660
            return $inline;
1✔
661
        }
662

663
        $titleRawHtml = $this->line($inline['element']['attributes']['title']); // @phpstan-ignore method.notFound
1✔
664
        $inline['element']['attributes']['title'] = strip_tags($titleRawHtml);
1✔
665

666
        $figcaption = [
1✔
667
            'element' => [
1✔
668
                'name'                   => 'figcaption',
1✔
669
                'allowRawHtmlInSafeMode' => true,
1✔
670
                'rawHtml'                => $titleRawHtml,
1✔
671
            ],
1✔
672
        ];
1✔
673
        $figure = [
1✔
674
            'extent'  => $inline['extent'],
1✔
675
            'element' => [
1✔
676
                'name'    => 'figure',
1✔
677
                'handler' => 'elements',
1✔
678
                'text'    => [
1✔
679
                    $inline['element'],
1✔
680
                    $figcaption['element'],
1✔
681
                ],
1✔
682
            ],
1✔
683
        ];
1✔
684

685
        return $figure;
1✔
686
    }
687
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc