Passed
Push — analysis-kYaJxP ( 8ff39d )
by Arnaud
12:05 queued 06:41
created

Parsedown::blockMedia()   D

Complexity

Conditions 16
Paths 194

Size

Total Lines 125
Code Lines 70

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 3 Features 0
Metric Value
cc 16
eloc 70
c 4
b 3
f 0
nc 194
nop 1
dl 0
loc 125
rs 4.7833

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
    /** Valid a media block (image, audio or video) */
32
    protected $MarkdownMediaRegex = "~^!\[.*?\]\(.*?\)~";
33
34
    /** @var Highlighter */
35
    protected $highlighter;
36
37
    public function __construct(Builder $builder)
38
    {
39
        $this->builder = $builder;
40
41
        // "insert" line block: ++text++ -> <ins>text</ins>
42
        $this->InlineTypes['+'][] = 'Insert';
43
        $this->inlineMarkerList = implode('', array_keys($this->InlineTypes));
44
        $this->specialCharacters[] = '+';
45
46
        // Media (image, audio or video) block
47
        $this->BlockTypes['!'][] = 'Media';
48
49
        // "notes" block
50
        if ($this->builder->getConfig()->get('body.notes.enabled')) {
51
            $this->BlockTypes[':'][] = 'Note';
52
        }
53
54
        // code highlight
55
        $this->highlighter = new Highlighter();
56
57
        // Table of Content
58
        parent::__construct(['selectors' => $this->builder->getConfig()->get('body.toc')]);
59
    }
60
61
    /**
62
     * Insert inline.
63
     * e.g.: ++text++ -> <ins>text</ins>.
64
     */
65
    protected function inlineInsert($Excerpt)
66
    {
67
        if (!isset($Excerpt['text'][1])) {
68
            return;
69
        }
70
71
        if ($Excerpt['text'][1] === '+' && preg_match('/^\+\+(?=\S)(.+?)(?<=\S)\+\+/', $Excerpt['text'], $matches)) {
72
            return [
73
                'extent'  => strlen($matches[0]),
74
                'element' => [
75
                    'name'    => 'ins',
76
                    'text'    => $matches[1],
77
                    'handler' => 'line',
78
                ],
79
            ];
80
        }
81
    }
82
83
    /**
84
     * {@inheritdoc}
85
     */
86
    protected function inlineImage($Excerpt)
87
    {
88
        $image = parent::inlineImage($Excerpt);
89
        if (!isset($image)) {
90
            return null;
91
        }
92
93
        // clean source path / URL
94
        $image['element']['attributes']['src'] = $this->cleanUrl($image['element']['attributes']['src']);
95
96
        // should be lazy loaded?
97
        if ($this->builder->getConfig()->get('body.images.lazy.enabled')) {
98
            $image['element']['attributes']['loading'] = 'lazy';
99
        }
100
101
        // disable remote image handling?
102
        if (Util\Url::isUrl($image['element']['attributes']['src']) && !$this->builder->getConfig()->get('body.images.remote.enabled') ?? true) {
103
            return $image;
104
        }
105
106
        // create asset
107
        $asset = new Asset($this->builder, $image['element']['attributes']['src'], ['force_slash' => false]);
108
        $image['element']['attributes']['src'] = $asset;
109
        $width = $asset->getWidth();
110
111
        /**
112
         * Should be resized?
113
         */
114
        $assetResized = null;
115
        if (isset($image['element']['attributes']['width'])
116
            && (int) $image['element']['attributes']['width'] < $width
117
            && $this->builder->getConfig()->get('body.images.resize.enabled')
118
        ) {
119
            $width = (int) $image['element']['attributes']['width'];
120
121
            try {
122
                $assetResized = $asset->resize($width);
123
            } catch (\Exception $e) {
124
                $this->builder->getLogger()->debug($e->getMessage());
125
126
                return $image;
127
            }
128
            $image['element']['attributes']['src'] = $assetResized;
129
        }
130
131
        // set width
132
        if (!isset($image['element']['attributes']['width']) && $asset['type'] == 'image') {
133
            $image['element']['attributes']['width'] = $width;
134
        }
135
        // set height
136
        if (!isset($image['element']['attributes']['height']) && $asset['type'] == 'image') {
137
            $image['element']['attributes']['height'] = ($assetResized ?? $asset)->getHeight();
138
        }
139
140
        /**
141
         * Should be responsive?
142
         */
143
        if ($this->builder->getConfig()->get('body.images.responsive.enabled')) {
144
            if ($srcset = Image::buildSrcset(
145
                $assetResized ?? $asset,
146
                $this->builder->getConfig()->get('assets.images.responsive.widths') ?? [480, 640, 768, 1024, 1366, 1600, 1920]
147
            )) {
148
                $image['element']['attributes']['srcset'] = $srcset;
149
                $image['element']['attributes']['sizes'] = $this->builder->getConfig()->get('assets.images.responsive.sizes.default');
150
            }
151
        }
152
153
        return $image;
154
    }
155
156
    /**
157
     * Media block support:
158
     * 1. <picture>/<source> for WebP images
159
     * 2. <audio> and <video> elements
160
     * 3. <figure>/<figcaption> for element with a title.
161
     */
162
    protected function blockMedia($Excerpt)
163
    {
164
        if (1 !== preg_match($this->MarkdownMediaRegex, $Excerpt['text'], $matches)) {
165
            return;
166
        }
167
168
        $InlineImage = $this->inlineImage($Excerpt);
169
        if (!isset($InlineImage)) {
170
            return;
171
        }
172
        $block = $InlineImage;
173
174
        switch ($block['element']['attributes']['alt']) {
175
            case 'audio':
176
                $audio['element'] = [
0 ignored issues
show
Comprehensibility Best Practice introduced by
$audio was never initialized. Although not strictly required by PHP, it is generally a good practice to add $audio = array(); before regardless.
Loading history...
177
                    'name'    => 'audio',
178
                    'handler' => 'element',
179
                ];
180
                $audio['element']['attributes'] = ['controls' => '', 'preload' => 'none'] + $block['element']['attributes'];
181
                unset($audio['element']['attributes']['loading']);
182
                $block = $audio;
183
                break;
184
            case 'video':
185
                $video['element'] = [
0 ignored issues
show
Comprehensibility Best Practice introduced by
$video was never initialized. Although not strictly required by PHP, it is generally a good practice to add $video = array(); before regardless.
Loading history...
186
                    'name'       => 'video',
187
                    'handler'    => 'element',
188
                ];
189
                $video['element']['attributes'] = ['controls' => '', 'preload' => 'none'] + $block['element']['attributes'];
190
                unset($video['element']['attributes']['loading']);
191
                if (isset($block['element']['attributes']['poster'])) {
192
                    $video['element']['attributes']['poster'] = new Asset($this->builder, $block['element']['attributes']['poster'], ['force_slash' => false]);
193
                }
194
                $block = $video;
195
            }
196
197
        /*
198
        <!-- if image has a title: a <figure> is required for <figcaption> -->
199
        <figure>
200
            <!-- if WebP: a <picture> is required for <source> -->
201
            <picture>
202
                <source type="image/webp"
203
                    srcset="..."
204
                    sizes="..."
205
                >
206
                <img src="..."
207
                    srcset="..."
208
                    sizes="..."
209
                >
210
            </picture>
211
            <!-- title -->
212
            <figcaption>...</figcaption>
213
        </figure>
214
        */
215
216
        // creates a <picture> used to add WebP <source> in addition to the image <img> element
217
        if ($this->builder->getConfig()->get('body.images.webp.enabled') ?? false
218
            && ($InlineImage['element']['attributes']['src'])['type'] == 'image'
219
            && ($InlineImage['element']['attributes']['src'])['subtype'] != 'image/webp') {
220
            try {
221
                // Image src must be an Asset instance
222
                if (is_string($InlineImage['element']['attributes']['src'])) {
223
                    throw new RuntimeException(\sprintf('Asset "%s" can\'t be converted to WebP', $InlineImage['element']['attributes']['src']));
224
                }
225
                // Image asset is an animated GIF
226
                if (Image::isAnimatedGif($InlineImage['element']['attributes']['src'])) {
227
                    throw new RuntimeException(\sprintf('Asset "%s" is an animated GIF and can\'t be converted to WebP', $InlineImage['element']['attributes']['src']));
228
                }
229
                $assetWebp = Image::convertTopWebp($InlineImage['element']['attributes']['src'], $this->builder->getConfig()->get('assets.images.quality') ?? 75);
230
                $srcset = '';
231
                if ($this->builder->getConfig()->get('body.images.responsive.enabled')) {
232
                    $srcset = Image::buildSrcset(
233
                        $assetWebp,
234
                        $this->builder->getConfig()->get('assets.images.responsive.widths') ?? [480, 640, 768, 1024, 1366, 1600, 1920]
235
                    );
236
                }
237
                if (empty($srcset)) {
238
                    $srcset = (string) $assetWebp;
239
                }
240
                $PictureBlock = [
241
                    'element' => [
242
                        'name'    => 'picture',
243
                        'handler' => 'elements',
244
                    ],
245
                ];
246
                $source = [
247
                    'element' => [
248
                        'name'       => 'source',
249
                        'attributes' => [
250
                            'type'   => 'image/webp',
251
                            'srcset' => $srcset,
252
                            'sizes'  => $this->builder->getConfig()->get('assets.images.responsive.sizes.default'),
253
                        ],
254
                    ],
255
                ];
256
                $PictureBlock['element']['text'][] = $source['element'];
257
                $PictureBlock['element']['text'][] = $InlineImage['element'];
258
                $block = $PictureBlock;
259
            } catch (\Exception $e) {
260
                $this->builder->getLogger()->debug($e->getMessage());
261
            }
262
        }
263
264
        // if there is a title: put the <img> (or <picture>) in a <figure> element to use the <figcaption>
265
        if ($this->builder->getConfig()->get('body.images.caption.enabled') && !empty($InlineImage['element']['attributes']['title'])) {
266
            $FigureBlock = [
267
                'element' => [
268
                    'name'    => 'figure',
269
                    'handler' => 'elements',
270
                    'text'    => [
271
                        $block['element'],
272
                    ],
273
                ],
274
            ];
275
            $InlineFigcaption = [
276
                'element' => [
277
                    'name' => 'figcaption',
278
                    'text' => $InlineImage['element']['attributes']['title'],
279
                ],
280
            ];
281
            $FigureBlock['element']['text'][] = $InlineFigcaption['element'];
282
283
            return $FigureBlock;
284
        }
285
286
        return $block;
287
    }
288
289
    /**
290
     * Note block-level markup.
291
     *
292
     * :::tip
293
     * **Tip:** This is an advice.
294
     * :::
295
     *
296
     * Code inspired by https://github.com/sixlive/parsedown-alert from TJ Miller (@sixlive).
297
     */
298
    protected function blockNote($block)
299
    {
300
        if (preg_match('/:::(.*)/', $block['text'], $matches)) {
301
            return [
302
                'char'    => ':',
303
                'element' => [
304
                    'name'       => 'aside',
305
                    'text'       => '',
306
                    'attributes' => [
307
                        'class' => "note note-{$matches[1]}",
308
                    ],
309
                ],
310
            ];
311
        }
312
    }
313
314
    protected function blockNoteContinue($line, $block)
315
    {
316
        if (isset($block['complete'])) {
317
            return;
318
        }
319
        if (preg_match('/:::/', $line['text'])) {
320
            $block['complete'] = true;
321
322
            return $block;
323
        }
324
        $block['element']['text'] .= $line['text']."\n";
325
326
        return $block;
327
    }
328
329
    protected function blockNoteComplete($block)
330
    {
331
        $block['element']['rawHtml'] = $this->text($block['element']['text']);
332
        unset($block['element']['text']);
333
334
        return $block;
335
    }
336
337
    /**
338
     * Apply Highlight to code blocks.
339
     */
340
    protected function blockFencedCodeComplete($block)
341
    {
342
        if (!$this->builder->getConfig()->get('body.highlight.enabled')) {
343
            return $block;
344
        }
345
        if (!isset($block['element']['text']['attributes'])) {
346
            return $block;
347
        }
348
349
        $code = $block['element']['text']['text'];
350
        unset($block['element']['text']['text']);
351
        $languageClass = $block['element']['text']['attributes']['class'];
352
        $language = explode('-', $languageClass);
353
        $highlighted = $this->highlighter->highlight($language[1], $code);
354
        $block['element']['text']['attributes']['class'] = vsprintf('%s hljs %s', [
355
            $languageClass,
356
            $highlighted->language,
357
        ]);
358
        $block['element']['text']['rawHtml'] = $highlighted->value;
359
        $block['element']['text']['allowRawHtmlInSafeMode'] = true;
360
361
        return $block;
362
    }
363
364
    /**
365
     * {@inheritdoc}
366
     */
367
    protected function parseAttributeData($attributeString)
368
    {
369
        $attributes = preg_split('/[ ]+/', $attributeString, -1, PREG_SPLIT_NO_EMPTY);
370
        $Data = [];
371
        $HtmlAtt = [];
372
373
        foreach ($attributes as $attribute) {
374
            switch ($attribute[0]) {
375
                case '#': // ID
376
                    $Data['id'] = substr($attribute, 1);
377
                    break;
378
                case '.': // Classes
379
                    $classes[] = substr($attribute, 1);
380
                    break;
381
                default:  // Attributes
382
                    parse_str($attribute, $parsed);
383
                    $HtmlAtt = array_merge($HtmlAtt, $parsed);
384
            }
385
        }
386
387
        if (isset($classes)) {
388
            $Data['class'] = implode(' ', $classes);
389
        }
390
        if (!empty($HtmlAtt)) {
391
            foreach ($HtmlAtt as $a => $v) {
392
                $Data[$a] = trim($v, '"');
393
            }
394
        }
395
396
        return $Data;
397
    }
398
399
    /**
400
     * Returns URL without query string.
401
     */
402
    private function cleanUrl(string $path): string
403
    {
404
        return strtok(trim($path), '?') ?: trim($path);
405
    }
406
}
407