Passed
Push — fallback-intl ( 2f4c17...1f6a03 )
by Arnaud
03:32
created

Parsedown::inlineImage()   C

Complexity

Conditions 15
Paths 55

Size

Total Lines 68
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 26
CRAP Score 19.8236

Importance

Changes 5
Bugs 2 Features 0
Metric Value
cc 15
eloc 33
c 5
b 2
f 0
nc 55
nop 1
dl 0
loc 68
ccs 26
cts 36
cp 0.7221
crap 19.8236
rs 5.9166

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