Passed
Push — analysis-M1kQmj ( 05e4b4 )
by Arnaud
04:45
created

Parsedown   A

Complexity

Total Complexity 42

Size/Duplication

Total Lines 300
Duplicated Lines 0 %

Importance

Changes 8
Bugs 3 Features 0
Metric Value
eloc 137
c 8
b 3
f 0
dl 0
loc 300
rs 9.0399
wmc 42

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 12
B blockImage() 0 86 9

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