Passed
Push — images-optim ( b62e9b )
by Arnaud
12:46 queued 09:01
created

Parsedown::blockMedia()   F

Complexity

Conditions 17
Paths 266

Size

Total Lines 136
Code Lines 77

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 37
CRAP Score 28.2888

Importance

Changes 2
Bugs 2 Features 0
Metric Value
cc 17
eloc 77
c 2
b 2
f 0
nc 266
nop 1
dl 0
loc 136
ccs 37
cts 56
cp 0.6606
crap 28.2888
rs 3.5583

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