Passed
Push — converter ( 50b3ae...1a12d8 )
by Arnaud
03:16
created

Parsedown::blockImage()   C

Complexity

Conditions 15
Paths 134

Size

Total Lines 117
Code Lines 60

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 49
CRAP Score 15.0135

Importance

Changes 5
Bugs 2 Features 0
Metric Value
cc 15
eloc 60
nc 134
nop 1
dl 0
loc 117
ccs 49
cts 51
cp 0.9608
crap 15.0135
rs 5.6333
c 5
b 2
f 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\Util;
21
use Highlight\Highlighter;
22
23
class Parsedown extends \ParsedownToC
24
{
25
    /** @var Builder */
26
    protected $builder;
27
28
    /** {@inheritdoc} */
29
    protected $regexAttribute = '(?:[#.][-\w:\\\]+[ ]*|[-\w:\\\]+(?:=(?:["\'][^\n]*?["\']|[^\s]+)?)?[ ]*)';
30
31
    /** @var Highlighter */
32
    protected $highlighter;
33
34
    public function __construct(Builder $builder, ?array $options = null)
35
    {
36
        $this->builder = $builder;
37 1
38
        // "insert" line block: ++text++ -> <ins>text</ins>
39 1
        $this->InlineTypes['+'][] = 'Insert';
40
        $this->inlineMarkerList = implode('', array_keys($this->InlineTypes));
41
        $this->specialCharacters[] = '+';
42 1
43 1
        // "notes" block
44 1
        $this->BlockTypes[':'][] = 'Note';
45
46
        // code highlight
47 1
        $this->highlighter = new Highlighter();
48
49
        // options
50 1
        $options = array_merge(['selectors' => $this->builder->getConfig()->get('body.toc')], $options ?? []);
51
52
        parent::__construct($options);
53 1
    }
54
55
    /**
56 1
     * Insert inline.
57
     * e.g.: ++text++ -> <ins>text</ins>.
58
     */
59
    protected function inlineInsert($Excerpt)
60
    {
61
        if (!isset($Excerpt['text'][1])) {
62
            return;
63 1
        }
64
65 1
        if ($Excerpt['text'][1] === '+' && preg_match('/^\+\+(?=\S)(.+?)(?<=\S)\+\+/', $Excerpt['text'], $matches)) {
66
            return [
67
                'extent'  => strlen($matches[0]),
68
                'element' => [
69 1
                    'name'    => 'ins',
70
                    'text'    => $matches[1],
71 1
                    'handler' => 'line',
72
                ],
73 1
            ];
74 1
        }
75
    }
76
77
    /**
78
     * {@inheritdoc}
79
     */
80
    protected function inlineLink($Excerpt)
81
    {
82
        $link = parent::inlineLink($Excerpt);
83
84 1
        if (!isset($link)) {
85
            return null;
86 1
        }
87 1
88
        // Link to a page with "page:page_id" as URL
89
        if (Util\Str::startsWith($link['element']['attributes']['href'], 'page:')) {
90
            $link['element']['attributes']['href'] = new \Cecil\Assets\Url($this->builder, substr($link['element']['attributes']['href'], 5, strlen($link['element']['attributes']['href'])));
91
92 1
            return $link;
93
        }
94
95 1
        /*
96 1
         * Embed link?
97
         */
98
        $embed = false;
99
        $embed = (bool) $this->builder->getConfig()->get('body.links.embed.enabled') ?? false;
100 1
        if (isset($link['element']['attributes']['embed'])) {
101
            $embed = true;
102
            if ($link['element']['attributes']['embed'] == 'false') {
103
                $embed = false;
104
            }
105 1
            unset($link['element']['attributes']['embed']);
106 1
        }
107 1
        if (!$embed) {
108
            return $link;
109
        }
110
        // video or audio?
111
        $extension = pathinfo($link['element']['attributes']['href'], PATHINFO_EXTENSION);
112 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

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