Passed
Push — analysis-EAnJ5O ( 11cafe )
by Arnaud
05:03 queued 10s
created

Parsedown   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 307
Duplicated Lines 0 %

Importance

Changes 8
Bugs 3 Features 0
Metric Value
eloc 142
c 8
b 3
f 0
dl 0
loc 307
rs 8.8798
wmc 44

9 Methods

Rating   Name   Duplication   Size   Complexity  
A blockNoteContinue() 0 13 3
A blockNoteComplete() 0 6 1
B parseAttributeData() 0 30 7
A __construct() 0 18 3
A blockNote() 0 10 2
A removeQuery() 0 3 1
A inlineInsert() 0 13 4
C inlineImage() 0 62 13
B blockImage() 0 93 10

How to fix   Complexity   

Complex Class

Complex classes like Parsedown often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Parsedown, and based on these observations, apply Extract Interface, too.

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
22
class Parsedown extends \ParsedownToC
23
{
24
    /** @var Builder */
25
    protected $builder;
26
27
    /** {@inheritdoc} */
28
    protected $regexAttribute = '(?:[#.][-\w:\\\]+[ ]*|[-\w:\\\]+(?:=(?:["\'][^\n]*?["\']|[^\s]+)?)?[ ]*)';
29
30
    /** Regex to verify there is an image in <figure> block */
31
    private $MarkdownImageRegex = "~^!\[.*?\]\(.*?\)~";
32
33
    public function __construct(Builder $builder)
34
    {
35
        $this->builder = $builder;
36
37
        // "insert" line block: ++text++ -> <ins>text</ins>
38
        $this->InlineTypes['+'][] = 'Insert';
39
        $this->inlineMarkerList = implode('', array_keys($this->InlineTypes));
40
        $this->specialCharacters[] = '+';
41
        // add caption to image block
42
        if ($this->builder->getConfig()->get('body.images.caption.enabled')) {
43
            $this->BlockTypes['!'][] = 'Image';
44
        }
45
        // "notes" block
46
        if ($this->builder->getConfig()->get('body.notes.enabled')) {
47
            $this->BlockTypes[':'][] = 'Note';
48
        }
49
50
        parent::__construct(['selectors' => $this->builder->getConfig()->get('body.toc')]);
51
    }
52
53
    /**
54
     * Insert inline.
55
     * e.g.: ++text++ -> <ins>text</ins>.
56
     */
57
    protected function inlineInsert($Excerpt)
58
    {
59
        if (!isset($Excerpt['text'][1])) {
60
            return;
61
        }
62
63
        if ($Excerpt['text'][1] === '+' && preg_match('/^\+\+(?=\S)(.+?)(?<=\S)\+\+/', $Excerpt['text'], $matches)) {
64
            return [
65
                'extent'  => strlen($matches[0]),
66
                'element' => [
67
                    'name'    => 'ins',
68
                    'text'    => $matches[1],
69
                    'handler' => 'line',
70
                ],
71
            ];
72
        }
73
    }
74
75
    /**
76
     * {@inheritdoc}
77
     */
78
    protected function inlineImage($excerpt)
79
    {
80
        $image = parent::inlineImage($excerpt);
81
        if (!isset($image)) {
82
            return null;
83
        }
84
        // clean source path / URL
85
        $image['element']['attributes']['src'] = trim($this->removeQuery($image['element']['attributes']['src']));
86
        // should be lazy loaded?
87
        if ($this->builder->getConfig()->get('body.images.lazy.enabled')) {
88
            $image['element']['attributes']['loading'] = 'lazy';
89
        }
90
        // disable remote image handling
91
        if (Util\Url::isUrl($image['element']['attributes']['src']) && !$this->builder->getConfig()->get('body.images.remote.enabled') ?? true) {
92
            return $image;
93
        }
94
        // create asset
95
        $asset = new Asset($this->builder, $image['element']['attributes']['src'], ['force_slash' => false]);
96
        // get width
97
        $width = $asset->getWidth();
98
        $image['element']['attributes']['src'] = $asset;
99
        /**
100
         * Should be resized?
101
         */
102
        $assetResized = null;
103
        if (isset($image['element']['attributes']['width'])
104
            && (int) $image['element']['attributes']['width'] < $width
105
            && $this->builder->getConfig()->get('body.images.resize.enabled')
106
        ) {
107
            $width = (int) $image['element']['attributes']['width'];
108
109
            try {
110
                $assetResized = $asset->resize($width);
111
            } catch (\Exception $e) {
112
                $this->builder->getLogger()->debug($e->getMessage());
113
114
                return $image;
115
            }
116
            $image['element']['attributes']['src'] = $assetResized;
117
        }
118
        // set width
119
        if (!isset($image['element']['attributes']['width'])) {
120
            $image['element']['attributes']['width'] = $width;
121
        }
122
        // set height
123
        if (!isset($image['element']['attributes']['height'])) {
124
            $image['element']['attributes']['height'] = $asset->getHeight();
125
        }
126
        /**
127
         * Should be responsive?
128
         */
129
        if ($this->builder->getConfig()->get('body.images.responsive.enabled')) {
130
            if ($srcset = Image::buildSrcset(
131
                $assetResized ?? $asset,
132
                $this->builder->getConfig()->get('assets.images.responsive.widths') ?? [480, 640, 768, 1024, 1366, 1600, 1920]
133
            )) {
134
                $image['element']['attributes']['srcset'] = $srcset;
135
                $image['element']['attributes']['sizes'] = $this->builder->getConfig()->get('assets.images.responsive.sizes.default');
136
            }
137
        }
138
139
        return $image;
140
    }
141
142
    /**
143
     * {@inheritdoc}
144
     */
145
    protected function parseAttributeData($attributeString)
146
    {
147
        $attributes = preg_split('/[ ]+/', $attributeString, -1, PREG_SPLIT_NO_EMPTY);
148
        $Data = [];
149
        $HtmlAtt = [];
150
151
        foreach ($attributes as $attribute) {
152
            switch ($attribute[0]) {
153
                case '#': // ID
154
                    $Data['id'] = substr($attribute, 1);
155
                    break;
156
                case '.': // Classes
157
                    $classes[] = substr($attribute, 1);
158
                    break;
159
                default:  // Attributes
160
                    parse_str($attribute, $parsed);
161
                    $HtmlAtt = array_merge($HtmlAtt, $parsed);
162
            }
163
        }
164
165
        if (isset($classes)) {
166
            $Data['class'] = implode(' ', $classes);
167
        }
168
        if (!empty($HtmlAtt)) {
169
            foreach ($HtmlAtt as $a => $v) {
170
                $Data[$a] = trim($v, '"');
171
            }
172
        }
173
174
        return $Data;
175
    }
176
177
    /**
178
     * Enhanced image block with <figure>/<figcaption>.
179
     */
180
    protected function blockImage($Line)
181
    {
182
        if (1 !== preg_match($this->MarkdownImageRegex, $Line['text'])) {
183
            return;
184
        }
185
186
        $InlineImage = $this->inlineImage($Line);
187
        if (!isset($InlineImage)) {
188
            return;
189
        }
190
191
        $block = $InlineImage;
192
193
        /*
194
        <figure>
195
            <picture>
196
                <source type="image/webp"
197
                    srcset="..."
198
                    sizes="..."
199
                >
200
                <img src="..."
201
                    srcset="..."
202
                    sizes="..."
203
                >
204
            </picture>
205
            <figcaption>...</figcaption>
206
        </figure>
207
        */
208
209
        // creates a <picture> element with a <source> (WebP) and an <img> element
210
        if ($this->builder->getConfig()->get('body.images.webp.enabled') ?? false && !Image::isAnimatedGif($InlineImage['element']['attributes']['src'])) {
211
            try {
212
                if (is_string($InlineImage['element']['attributes']['src'])) {
213
                    throw new RuntimeException(\sprintf('Can\'t convert "%s" to WebP', $InlineImage['element']['attributes']['src']));
214
                }
215
                $assetWebp = Image::convertTopWebp($InlineImage['element']['attributes']['src'], $this->builder->getConfig()->get('assets.images.quality') ?? 75);
216
                $srcset = '';
217
                if ($this->builder->getConfig()->get('body.images.responsive.enabled')) {
218
                    $srcset = Image::buildSrcset(
219
                        $assetWebp,
220
                        $this->builder->getConfig()->get('assets.images.responsive.widths') ?? [480, 640, 768, 1024, 1366, 1600, 1920]
221
                    );
222
                }
223
                if (empty($srcset)) {
224
                    $srcset = (string) $assetWebp;
225
                }
226
                $PictureBlock = [
227
                    'element' => [
228
                        'name'    => 'picture',
229
                        'handler' => 'elements',
230
                    ],
231
                ];
232
                $source = [
233
                    'element' => [
234
                        'name'       => 'source',
235
                        'attributes' => [
236
                            'type'   => 'image/webp',
237
                            'srcset' => $srcset,
238
                            'sizes'  => $this->builder->getConfig()->get('assets.images.responsive.sizes.default'),
239
                        ],
240
                    ],
241
                ];
242
                $PictureBlock['element']['text'][] = $source['element'];
243
                $PictureBlock['element']['text'][] = $InlineImage['element'];
244
                $block = $PictureBlock;
245
            } catch (RuntimeException $e) {
246
                $this->builder->getLogger()->debug($e->getMessage());
247
            }
248
        }
249
250
        // put <img> (or <picture>) in a <figure> element if there is a title (<figcaption>)
251
        if (!empty($InlineImage['element']['attributes']['title'])) {
252
            $FigureBlock = [
253
                'element' => [
254
                    'name'    => 'figure',
255
                    'handler' => 'elements',
256
                    'text'    => [
257
                        $block['element'],
258
                    ],
259
                ],
260
            ];
261
            $InlineFigcaption = [
262
                'element' => [
263
                    'name' => 'figcaption',
264
                    'text' => $InlineImage['element']['attributes']['title'],
265
                ],
266
            ];
267
            $FigureBlock['element']['text'][] = $InlineFigcaption['element'];
268
269
            return $FigureBlock;
270
        }
271
272
        return $block;
273
    }
274
275
    /**
276
     * Note block-level markup.
277
     *
278
     * :::tip
279
     * **Tip:** This is an advice.
280
     * :::
281
     *
282
     * Code inspired by https://github.com/sixlive/parsedown-alert from TJ Miller (@sixlive).
283
     */
284
    protected function blockNote($block)
285
    {
286
        if (preg_match('/:::(.*)/', $block['text'], $matches)) {
287
            return [
288
                'char'    => ':',
289
                'element' => [
290
                    'name'       => 'aside',
291
                    'text'       => '',
292
                    'attributes' => [
293
                        'class' => "note note-{$matches[1]}",
294
                    ],
295
                ],
296
            ];
297
        }
298
    }
299
300
    protected function blockNoteContinue($line, $block)
301
    {
302
        if (isset($block['complete'])) {
303
            return;
304
        }
305
        if (preg_match('/:::/', $line['text'])) {
306
            $block['complete'] = true;
307
308
            return $block;
309
        }
310
        $block['element']['text'] .= $line['text']."\n";
311
312
        return $block;
313
    }
314
315
    protected function blockNoteComplete($block)
316
    {
317
        $block['element']['rawHtml'] = $this->text($block['element']['text']);
318
        unset($block['element']['text']);
319
320
        return $block;
321
    }
322
323
    /**
324
     * Removes query string from URL.
325
     */
326
    private function removeQuery(string $path): string
327
    {
328
        return strtok($path, '?');
329
    }
330
}
331