Passed
Push — figure ( 5d84ca...03296e )
by Arnaud
07:56 queued 04:18
created

Parsedown::inlineLink()   C

Complexity

Conditions 14
Paths 32

Size

Total Lines 111
Code Lines 68

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 34
CRAP Score 22.1327

Importance

Changes 7
Bugs 2 Features 0
Metric Value
cc 14
eloc 68
c 7
b 2
f 0
nc 32
nop 1
dl 0
loc 111
ccs 34
cts 52
cp 0.6538
crap 22.1327
rs 5.7115

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\Util;
21
use Highlight\Highlighter;
22
23
class Parsedown extends \ParsedownToC
24
{
25
    /** @var Builder */
26
    protected $builder;
27 1
28
    /** {@inheritdoc} */
29 1
    protected $regexAttribute = '(?:[#.][-\w:\\\]+[ ]*|[-\w:\\\]+(?:=(?:["\'][^\n]*?["\']|[^\s]+)?)?[ ]*)';
30 1
31
    protected $regexImage = "~^!\[.*?\]\(.*?\)~";
32
33 1
    protected $regexLink = "~^\[.*?\]\(.*?\)~";
34 1
35
    /** @var Highlighter */
36
    protected $highlighter;
37
38
    public function __construct(Builder $builder, ?array $options = null)
39 1
    {
40
        $this->builder = $builder;
41 1
42 1
        // "insert" line block: ++text++ -> <ins>text</ins>
43
        $this->InlineTypes['+'][] = 'Insert';
44
        $this->inlineMarkerList = implode('', array_keys($this->InlineTypes));
45 1
        $this->specialCharacters[] = '+';
46 1
47 1
        // Image block (to avoid paragraph)
48 1
        $this->BlockTypes['!'][] = 'Image';
49
50
        // Link block (to avoid paragraph)
51 1
        $this->BlockTypes['['][] = 'Link';
52
53
        // "notes" block
54
        $this->BlockTypes[':'][] = 'Note';
55 1
56 1
        // code highlight
57
        $this->highlighter = new Highlighter();
58
59
        // options
60
        $options = array_merge(['selectors' => $this->builder->getConfig()->get('body.toc')], $options ?? []);
61 1
62 1
        parent::__construct($options);
63 1
    }
64 1
65
    /**
66 1
     * Insert inline.
67
     * e.g.: ++text++ -> <ins>text</ins>.
68
     */
69 1
    protected function inlineInsert($Excerpt)
70
    {
71
        if (!isset($Excerpt['text'][1])) {
72
            return;
73
        }
74
75 1
        if ($Excerpt['text'][1] === '+' && preg_match('/^\+\+(?=\S)(.+?)(?<=\S)\+\+/', $Excerpt['text'], $matches)) {
76
            return [
77
                'extent'  => strlen($matches[0]),
78 1
                'element' => [
79 1
                    'name'    => 'ins',
80
                    'text'    => $matches[1],
81
                    'handler' => 'line',
82 1
                ],
83 1
            ];
84
        }
85
    }
86
87
    /**
88 1
     * {@inheritdoc}
89 1
     */
90 1
    protected function inlineLink($Excerpt)
91 1
    {
92 1
        $link = parent::inlineLink($Excerpt);
93 1
94 1
        if (!isset($link)) {
95 1
            return null;
96 1
        }
97
98 1
        // Link to a page with "page:page_id" as URL
99 1
        if (Util\Str::startsWith($link['element']['attributes']['href'], 'page:')) {
100 1
            $link['element']['attributes']['href'] = new \Cecil\Assets\Url($this->builder, substr($link['element']['attributes']['href'], 5, strlen($link['element']['attributes']['href'])));
101 1
102 1
            return $link;
103
        }
104
105 1
        /*
106 1
         * Embed link?
107
         */
108 1
        $embed = false;
109 1
        $embed = (bool) $this->builder->getConfig()->get('body.links.embed.enabled') ?? false;
110
        if (isset($link['element']['attributes']['embed'])) {
111
            $embed = true;
112 1
            if ($link['element']['attributes']['embed'] == 'false') {
113
                $embed = false;
114
            }
115
            unset($link['element']['attributes']['embed']);
116
        }
117
        if (!$embed) {
118 1
            return $link;
119
        }
120 1
        // video or audio?
121 1
        $extension = pathinfo($link['element']['attributes']['href'], PATHINFO_EXTENSION);
122 1
        if (in_array($extension, $this->builder->getConfig()->get('body.links.embed.video.ext'))) {
0 ignored issues
show
Bug introduced by
It seems like $this->builder->getConfi...links.embed.video.ext') can also be of type null; however, parameter $haystack of in_array() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

122
        if (in_array($extension, /** @scrutinizer ignore-type */ $this->builder->getConfig()->get('body.links.embed.video.ext'))) {
Loading history...
123
            $video = $this->createMediaFromLink($link, 'video');
124 1
            if ($this->builder->getConfig()->get('body.images.caption.enabled')) {
125 1
                return $this->createFigure($video);
126 1
            }
127
128
            return $video;
129 1
        }
130
        if (in_array($extension, $this->builder->getConfig()->get('body.links.embed.audio.ext'))) {
131
            $audio = $this->createMediaFromLink($link, 'audio');
132
            if ($this->builder->getConfig()->get('body.images.caption.enabled')) {
133 1
                return $this->createFigure($audio);
134 1
            }
135
136
            return $audio;
137
        }
138 1
        // GitHub Gist link?
139
        // https://regex101.com/r/QmCiAL/1
140
        $pattern = 'https:\/\/gist\.github.com\/[-a-zA-Z0-9_]+\/[-a-zA-Z0-9_]+';
141 1
        if (preg_match('/'.$pattern.'/is', (string) $link['element']['attributes']['href'], $matches)) {
142 1
            $gist = [
143 1
                'extent'  => $link['extent'],
144
                'element' => [
145
                    'name'       => 'script',
146
                    'text'       => $link['element']['text'],
147 1
                    'attributes' => [
148
                        'src'   => $matches[0].'.js',
149
                        'title' => $link['element']['attributes']['title'],
150
                    ],
151
                ],
152
            ];
153
            if ($this->builder->getConfig()->get('body.images.caption.enabled')) {
154
                return $this->createFigure($gist);
155
            }
156
157
            return $gist;
158
        }
159
        // Youtube link?
160
        // https://regex101.com/r/gznM1j/1
161
        $pattern = '(?:https?:\/\/)?(?:www\.)?youtu(?:\.be\/|be.com\/\S*(?:watch|embed)(?:(?:(?=\/[-a-zA-Z0-9_]{11,}(?!\S))\/)|(?:\S*v=|v\/)))([-a-zA-Z0-9_]{11,})';
162
        if (preg_match('/'.$pattern.'/is', (string) $link['element']['attributes']['href'], $matches)) {
163
            $iframe = [
164
                'element' => [
165
                    'name'       => 'iframe',
166
                    'text'       => $link['element']['text'],
167
                    'attributes' => [
168
                        'width'           => '560',
169
                        'height'          => '315',
170
                        'title'           => $link['element']['text'],
171
                        'src'             => 'https://www.youtube.com/embed/'.$matches[1],
172
                        'frameborder'     => '0',
173
                        'allow'           => 'accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture',
174
                        'allowfullscreen' => '',
175
                        'style'           => 'position:absolute; top:0; left:0; width:100%; height:100%; border:0',
176
                    ],
177
                ],
178
            ];
179
            $youtube = [
180
                'extent'  => $link['extent'],
181
                'element' => [
182
                    'name'    => 'div',
183
                    'handler' => 'elements',
184
                    'text'    => [
185
                        $iframe['element'],
186
                    ],
187 1
                    'attributes' => [
188
                        'style' => 'position:relative; padding-bottom:56.25%; height:0; overflow:hidden',
189 1
                        'title' => $link['element']['attributes']['title'],
190
                    ],
191
                ],
192
            ];
193
            if ($this->builder->getConfig()->get('body.images.caption.enabled')) {
194
                return $this->createFigure($youtube);
195
            }
196
197
            return $youtube;
198
        }
199
200
        return $link;
201
    }
202
203
    /**
204
     * {@inheritdoc}
205
     */
206
    protected function inlineImage($Excerpt)
207
    {
208
        $InlineImage = parent::inlineImage($Excerpt);
209
        if (!isset($InlineImage)) {
210
            return null;
211
        }
212
213
        // remove quesry string
214
        $InlineImage['element']['attributes']['src'] = $this->removeQueryString($InlineImage['element']['attributes']['src']);
215
216
        // normalize path
217
        $InlineImage['element']['attributes']['src'] = $this->normalizePath($InlineImage['element']['attributes']['src']);
218
219
        // should be lazy loaded?
220
        if ($this->builder->getConfig()->get('body.images.lazy.enabled') && !isset($InlineImage['element']['attributes']['loading'])) {
221
            $InlineImage['element']['attributes']['loading'] = 'lazy';
222
        }
223
224
        // add default class?
225
        if ($this->builder->getConfig()->get('body.images.class')) {
226
            $InlineImage['element']['attributes']['class'] .= ' '.$this->builder->getConfig()->get('body.images.class');
227
            $InlineImage['element']['attributes']['class'] = trim($InlineImage['element']['attributes']['class']);
228
        }
229
230
        // disable remote image handling?
231
        if (Util\Url::isUrl($InlineImage['element']['attributes']['src']) && !$this->builder->getConfig()->get('body.images.remote.enabled') ?? true) {
232
            return $InlineImage;
233
        }
234
235
        // create asset
236
        $assetOptions = ['force_slash' => false];
237
        if ($this->builder->getConfig()->get('body.images.remote.fallback.enabled')) {
238
            $assetOptions += ['remote_fallback' => $this->builder->getConfig()->get('body.images.remote.fallback.path')];
239
        }
240
        $asset = new Asset($this->builder, $InlineImage['element']['attributes']['src'], $assetOptions);
241
        $InlineImage['element']['attributes']['src'] = $asset;
242
        $width = $asset->getWidth();
243
244
        /*
245
         * Should be resized?
246
         */
247
        $assetResized = null;
248
        if (isset($InlineImage['element']['attributes']['width'])
249
            && (int) $InlineImage['element']['attributes']['width'] < $width
250
            && $this->builder->getConfig()->get('body.images.resize.enabled')
251
        ) {
252
            $width = (int) $InlineImage['element']['attributes']['width'];
253
254
            try {
255
                $assetResized = $asset->resize($width);
256
                $InlineImage['element']['attributes']['src'] = $assetResized;
257
            } catch (\Exception $e) {
258
                $this->builder->getLogger()->debug($e->getMessage());
259
260
                return $InlineImage;
261
            }
262
        }
263
264
        // set width
265
        if (!isset($InlineImage['element']['attributes']['width'])) {
266
            $InlineImage['element']['attributes']['width'] = $width;
267
        }
268
        // set height
269
        if (!isset($InlineImage['element']['attributes']['height'])) {
270
            $InlineImage['element']['attributes']['height'] = ($assetResized ?? $asset)->getHeight();
271
        }
272
273
        /*
274
         * Should be responsive?
275
         */
276
        $sizes = '';
277
        if ($this->builder->getConfig()->get('body.images.responsive.enabled')) {
278
            try {
279
                if ($srcset = Image::buildSrcset(
280
                    $assetResized ?? $asset,
281
                    $this->builder->getConfig()->get('assets.images.responsive.widths') ?? [480, 640, 768, 1024, 1366, 1600, 1920]
282
                )) {
283
                    $InlineImage['element']['attributes']['srcset'] = $srcset;
284
                    $sizes = (string) $this->builder->getConfig()->get('assets.images.responsive.sizes.default');
285
                    if (isset($InlineImage['element']['attributes']['class'])) {
286
                        $sizes = Image::getSizes($InlineImage['element']['attributes']['class'], (array) $this->builder->getConfig()->get('assets.images.responsive.sizes'));
287
                    }
288
                    $InlineImage['element']['attributes']['sizes'] = $sizes;
289
                }
290
            } catch (\Exception $e) {
291
                $this->builder->getLogger()->debug($e->getMessage());
292
            }
293
        }
294
295
        /*
296
        <!-- if title: a <figure> is required to put in it a <figcaption> -->
297
        <figure>
298
            <!-- if WebP is enabled: a <picture> is required for the WebP <source> -->
299
            <picture>
300
                <source type="image/webp"
301
                    srcset="..."
302
                    sizes="..."
303
                >
304
                <img src="..."
305
                    srcset="..."
306
                    sizes="..."
307
                >
308
            </picture>
309
            <figcaption><!-- title --></figcaption>
310
        </figure>
311
        */
312
313
        $image = $InlineImage;
314
315
        // converts image to WebP and put it in picture > source
316
        if ($this->builder->getConfig()->get('body.images.webp.enabled') ?? false
317
            && (($InlineImage['element']['attributes']['src'])['type'] == 'image'
318
                && ($InlineImage['element']['attributes']['src'])['subtype'] != 'image/webp')
319
        ) {
320
            try {
321
                // InlineImage src must be an Asset instance
322
                if (!$InlineImage['element']['attributes']['src'] instanceof Asset) {
323
                    throw new RuntimeException(\sprintf('Asset "%s" can\'t be converted to WebP', $InlineImage['element']['attributes']['src']));
324
                }
325
                // abord if InlineImage is an animated GIF
326
                if (Image::isAnimatedGif($InlineImage['element']['attributes']['src'])) {
327
                    throw new RuntimeException(\sprintf('Asset "%s" is an animated GIF and can\'t be converted to WebP', $InlineImage['element']['attributes']['src']));
328
                }
329
                $assetWebp = Image::convertTopWebp($InlineImage['element']['attributes']['src'], $this->builder->getConfig()->get('assets.images.quality') ?? 75);
330
                $srcset = '';
331
                // build responsives WebP?
332
                if ($this->builder->getConfig()->get('body.images.responsive.enabled')) {
333
                    try {
334
                        $srcset = Image::buildSrcset(
335
                            $assetWebp,
336
                            $this->builder->getConfig()->get('assets.images.responsive.widths') ?? [480, 640, 768, 1024, 1366, 1600, 1920]
337
                        );
338
                    } catch (\Exception $e) {
339
                        $this->builder->getLogger()->debug($e->getMessage());
340
                    }
341
                }
342
                // if not, default image as srcset
343
                if (empty($srcset)) {
344
                    $srcset = (string) $assetWebp;
345
                }
346
                $picture = [
347
                    'extent'  => $InlineImage['extent'],
348
                    'element' => [
349
                        'name'       => 'picture',
350
                        'handler'    => 'elements',
351
                        'attributes' => [
352
                            'title' => $image['element']['attributes']['title'],
353
                        ],
354
                    ],
355
                ];
356
                $source = [
357
                    'element' => [
358
                        'name'       => 'source',
359
                        'attributes' => [
360
                            'type'   => 'image/webp',
361
                            'srcset' => $srcset,
362
                            'sizes'  => $sizes,
363
                        ],
364
                    ],
365
                ];
366
                $picture['element']['text'][] = $source['element'];
367
                $picture['element']['text'][] = $image['element'];
368
                $image = $picture;
369
            } catch (\Exception $e) {
370
                $this->builder->getLogger()->debug($e->getMessage());
371
            }
372
        }
373
374
        // if title: put the <img> (or <picture>) in a <figure> and create a <figcaption>
375
        if ($this->builder->getConfig()->get('body.images.caption.enabled')) {
376
            return $this->createFigure($image);
377
        }
378
379
        return $image;
380
    }
381
382
    /**
383
     * Image block.
384
     */
385
    protected function blockImage($Excerpt)
386
    {
387
        if (1 !== preg_match($this->regexImage, $Excerpt['text'])) {
388
            return;
389
        }
390
391
        $InlineImage = $this->inlineImage($Excerpt);
392
        if (!isset($InlineImage)) {
393
            return;
394
        }
395
396
        return $InlineImage;
397
    }
398
399
    /**
400
     * Link block.
401
     */
402
    protected function blockLink($Excerpt)
403
    {
404
        if (1 !== preg_match($this->regexLink, $Excerpt['text'])) {
405
            return;
406
        }
407
408
        $InlineLink = $this->inlineLink($Excerpt);
409
        if (!isset($InlineLink)) {
410
            return;
411
        }
412
413
        return $InlineLink;
414
    }
415
416
    /**
417
     * Note block-level markup.
418
     *
419
     * :::tip
420
     * **Tip:** This is an advice.
421
     * :::
422
     *
423
     * Code inspired by https://github.com/sixlive/parsedown-alert from TJ Miller (@sixlive).
424
     */
425
    protected function blockNote($block)
426
    {
427
        if (preg_match('/:::(.*)/', $block['text'], $matches)) {
428
            $block = [
429
                'char'    => ':',
430
                'element' => [
431
                    'name'       => 'aside',
432
                    'text'       => '',
433
                    'attributes' => [
434
                        'class' => 'note',
435
                    ],
436
                ],
437
            ];
438
            if (!empty($matches[1])) {
439
                $block['element']['attributes']['class'] .= " note-{$matches[1]}";
440
            }
441
442
            return $block;
443
        }
444
    }
445
446
    protected function blockNoteContinue($line, $block)
447
    {
448
        if (isset($block['complete'])) {
449
            return;
450
        }
451
        if (preg_match('/:::/', $line['text'])) {
452
            $block['complete'] = true;
453
454
            return $block;
455
        }
456
        $block['element']['text'] .= $line['text']."\n";
457
458
        return $block;
459
    }
460
461
    protected function blockNoteComplete($block)
462
    {
463
        $block['element']['rawHtml'] = $this->text($block['element']['text']);
464
        unset($block['element']['text']);
465
466
        return $block;
467
    }
468
469
    /**
470
     * Apply Highlight to code blocks.
471
     */
472
    protected function blockFencedCodeComplete($block)
473
    {
474
        if (!$this->builder->getConfig()->get('body.highlight.enabled')) {
475
            return $block;
476
        }
477
        if (!isset($block['element']['text']['attributes'])) {
478
            return $block;
479
        }
480
481
        try {
482
            $code = $block['element']['text']['text'];
483
            $languageClass = $block['element']['text']['attributes']['class'];
484
            $language = explode('-', $languageClass);
485
            $highlighted = $this->highlighter->highlight($language[1], $code);
486
            $block['element']['text']['attributes']['class'] = vsprintf('%s hljs %s', [
487
                $languageClass,
488
                $highlighted->language,
489
            ]);
490
            $block['element']['text']['rawHtml'] = $highlighted->value;
491
            $block['element']['text']['allowRawHtmlInSafeMode'] = true;
492
            unset($block['element']['text']['text']);
493
        } catch (\Exception $e) {
494
            $this->builder->getLogger()->debug($e->getMessage());
495
        } finally {
496
            return $block;
497
        }
498
    }
499
500
    /**
501
     * {@inheritdoc}
502
     */
503
    protected function parseAttributeData($attributeString)
504
    {
505
        $attributes = preg_split('/[ ]+/', $attributeString, -1, PREG_SPLIT_NO_EMPTY);
506
        $Data = [];
507
        $HtmlAtt = [];
508
509
        foreach ($attributes as $attribute) {
510
            switch ($attribute[0]) {
511
                case '#': // ID
512
                    $Data['id'] = substr($attribute, 1);
513
                    break;
514
                case '.': // Classes
515
                    $classes[] = substr($attribute, 1);
516
                    break;
517
                default:  // Attributes
518
                    parse_str($attribute, $parsed);
519
                    $HtmlAtt = array_merge($HtmlAtt, $parsed);
520
            }
521
        }
522
523
        if (isset($classes)) {
524
            $Data['class'] = implode(' ', $classes);
525
        }
526
        if (!empty($HtmlAtt)) {
527
            foreach ($HtmlAtt as $a => $v) {
528
                $Data[$a] = trim($v, '"');
529
            }
530
        }
531
532
        return $Data;
533
    }
534
535
    /**
536
     * Remove query string form a path/URL.
537
     */
538
    private function removeQueryString(string $path): string
539
    {
540
        return strtok(trim($path), '?') ?: trim($path);
541
    }
542
543
    /**
544
     * Turns a path relative to static or assets into a website relative path.
545
     *
546
     *   "../../assets/images/img.jpeg"
547
     *   ->
548
     *   "/images/img.jpeg"
549
     */
550
    private function normalizePath(string $path): string
551
    {
552
        // https://regex101.com/r/Rzguzh/1
553
        $pattern = \sprintf(
554
            '(\.\.\/)+(\b%s|%s\b)+(\/.*)',
555
            $this->builder->getConfig()->get('static.dir'),
556
            $this->builder->getConfig()->get('assets.dir')
557
        );
558
        $path = Util::joinPath($path);
559
        if (!preg_match('/'.$pattern.'/is', $path, $matches)) {
560
            return $path;
561
        }
562
563
        return $matches[3];
564
    }
565
566
    /**
567
     * Create a media (video or audio) element from a link.
568
     */
569
    private function createMediaFromLink(array $link, string $type = 'video'): array
570
    {
571
        $block = [
572
            'extent'  => $link['extent'],
573
            'element' => [
574
                'text' => $link['element']['attributes']['title'],
575
            ],
576
        ];
577
        $block['element']['attributes'] = $link['element']['attributes'];
578
        unset($block['element']['attributes']['href']);
579
        $block['element']['attributes']['src'] = (string) new Asset($this->builder, $link['element']['attributes']['href'], ['force_slash' => false]);
580
        switch ($type) {
581
            case 'video':
582
                $block['element']['name'] = 'video';
583
                if (isset($link['element']['attributes']['poster'])) {
584
                    $block['element']['attributes']['poster'] = (string) new Asset($this->builder, $link['element']['attributes']['poster'], ['force_slash' => false]);
585
                }
586
587
                return $block;
588
            case 'audio':
589
                $block['element']['name'] = 'audio';
590
591
                return $block;
592
        }
593
594
        throw new \Exception(\sprintf('Can\'t create %s from "%s".', $type, $link['element']['attributes']['src']));
595
    }
596
597
    /**
598
     * Create a figure / caption element.
599
     */
600
    private function createFigure(array $inline): array
601
    {
602
        if (empty($inline['element']['attributes']['title'])) {
603
            return $inline;
604
        }
605
606
        $titleRawHtml = $this->line($inline['element']['attributes']['title']);
607
        $inline['element']['attributes']['title'] = strip_tags($titleRawHtml);
608
609
        $figcaption = [
610
            'element' => [
611
                'name'                   => 'figcaption',
612
                'allowRawHtmlInSafeMode' => true,
613
                'rawHtml'                => $this->line($titleRawHtml),
614
            ],
615
        ];
616
        $figure = [
617
            'extent'  => $inline['extent'],
618
            'element' => [
619
                'name'    => 'figure',
620
                'handler' => 'elements',
621
                'text'    => [
622
                    $inline['element'],
623
                    $figcaption['element'],
624
                ],
625
            ],
626
        ];
627
628
        return $figure;
629
    }
630
}
631