Passed
Pull Request — feature/markdown-medias (#1405)
by Arnaud
09:04 queued 04:35
created

Parsedown::blockMedia()   C

Complexity

Conditions 14
Paths 3

Size

Total Lines 114
Code Lines 59

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 58.0086

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 14
eloc 59
c 1
b 1
f 0
nc 3
nop 1
dl 0
loc 114
ccs 20
cts 51
cp 0.3922
crap 58.0086
rs 6.2666

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 = "~^!\[.*?\]\(.*?\)\|*(audio|video)*~";
33
34
    /** @var Highlighter */
35
    protected $highlighter;
36
37
    public function __construct(Builder $builder)
38 1
    {
39
        $this->builder = $builder;
40 1
41
        // "insert" line block: ++text++ -> <ins>text</ins>
42
        $this->InlineTypes['+'][] = 'Insert';
43 1
        $this->inlineMarkerList = implode('', array_keys($this->InlineTypes));
44 1
        $this->specialCharacters[] = '+';
45 1
46
        // Media (image, audio or video) block
47
        $this->BlockTypes['!'][] = 'Media';
48 1
49 1
        // "notes" block
50
        if ($this->builder->getConfig()->get('body.notes.enabled')) {
51
            $this->BlockTypes[':'][] = 'Note';
52
        }
53 1
54 1
        // code highlight
55
        $this->highlighter = new Highlighter();
56
57
        // Table of Content
58 1
        parent::__construct(['selectors' => $this->builder->getConfig()->get('body.toc')]);
59
    }
60
61 1
    /**
62 1
     * Insert inline.
63
     * e.g.: ++text++ -> <ins>text</ins>.
64
     */
65
    protected function inlineInsert($Excerpt)
66
    {
67
        if (!isset($Excerpt['text'][1])) {
68 1
            return;
69
        }
70 1
71
        if ($Excerpt['text'][1] === '+' && preg_match('/^\+\+(?=\S)(.+?)(?<=\S)\+\+/', $Excerpt['text'], $matches)) {
72
            return [
73
                'extent'  => strlen($matches[0]),
74 1
                'element' => [
75
                    'name'    => 'ins',
76 1
                    'text'    => $matches[1],
77
                    'handler' => 'line',
78 1
                ],
79 1
            ];
80 1
        }
81
    }
82
83
    /**
84 1
     * {@inheritdoc}
85
     */
86
    protected function inlineImage($Excerpt)
87
    {
88
        $image = parent::inlineImage($Excerpt);
89 1
        if (!isset($image)) {
90
            return null;
91 1
        }
92 1
93 1
        // clean source path / URL
94
        $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
            $image['element']['attributes']['loading'] = 'lazy';
99
        }
100 1
101 1
        // 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 1
106 1
        // 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'])) {
133
            $image['element']['attributes']['width'] = $width;
134
        }
135
        // set height
136
        if (!isset($image['element']['attributes']['height'])) {
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 1
    protected function blockMedia($Excerpt)
163
    {
164 1
        if (1 !== preg_match($this->MarkdownMediaRegex, $Excerpt['text'], $matches)) {
165 1
            return;
166 1
        }
167
168 1
        switch ($matches[1]) {
169 1
            case 'audio':
170 1
                dump($matches);
171 1
172 1
                return;
173 1
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
174 1
            case 'video':
175 1
                dump($matches);
176
177
                return;
178 1
                break;
179
        }
180
181
        $InlineImage = $this->inlineImage($Excerpt);
182 1
        if (!isset($InlineImage)) {
183 1
            return;
184
        }
185 1
186
        $block = $InlineImage;
187
188
        /*
189
        <!-- if image has a title: a <figure> is required for <figcaption> -->
190
        <figure>
191 1
            <!-- if WebP: a <picture> is required for <source> -->
192
            <picture>
193
                <source type="image/webp"
194
                    srcset="..."
195
                    sizes="..."
196
                >
197 1
                <img src="..."
198
                    srcset="..."
199 1
                    sizes="..."
200
                >
201
            </picture>
202
            <!-- title -->
203
            <figcaption>...</figcaption>
204 1
        </figure>
205
        */
206
207
        // creates a <picture> used to add WebP <source> in addition to the image <img> element
208
        if ($this->builder->getConfig()->get('body.images.webp.enabled') ?? false && ($InlineImage['element']['attributes']['src'])['subtype'] != 'image/webp') {
209
            try {
210
                // Image src must be an Asset instance
211
                if (is_string($InlineImage['element']['attributes']['src'])) {
212
                    throw new RuntimeException(\sprintf('Asset "%s" can\'t be converted to WebP', $InlineImage['element']['attributes']['src']));
213
                }
214
                // Image asset is an animated GIF
215
                if (Image::isAnimatedGif($InlineImage['element']['attributes']['src'])) {
216
                    throw new RuntimeException(\sprintf('Asset "%s" is an animated GIF and can\'t be converted to WebP', $InlineImage['element']['attributes']['src']));
217
                }
218
                $assetWebp = Image::convertTopWebp($InlineImage['element']['attributes']['src'], $this->builder->getConfig()->get('assets.images.quality') ?? 75);
219
                $srcset = '';
220
                if ($this->builder->getConfig()->get('body.images.responsive.enabled')) {
221
                    $srcset = Image::buildSrcset(
222
                        $assetWebp,
223
                        $this->builder->getConfig()->get('assets.images.responsive.widths') ?? [480, 640, 768, 1024, 1366, 1600, 1920]
224
                    );
225
                }
226
                if (empty($srcset)) {
227
                    $srcset = (string) $assetWebp;
228
                }
229
                $PictureBlock = [
230
                    'element' => [
231
                        'name'    => 'picture',
232
                        'handler' => 'elements',
233
                    ],
234
                ];
235
                $source = [
236
                    'element' => [
237
                        'name'       => 'source',
238
                        'attributes' => [
239
                            'type'   => 'image/webp',
240
                            'srcset' => $srcset,
241
                            'sizes'  => $this->builder->getConfig()->get('assets.images.responsive.sizes.default'),
242
                        ],
243
                    ],
244
                ];
245
                $PictureBlock['element']['text'][] = $source['element'];
246
                $PictureBlock['element']['text'][] = $InlineImage['element'];
247
                $block = $PictureBlock;
248
            } catch (\Exception $e) {
249
                $this->builder->getLogger()->debug($e->getMessage());
250
            }
251
        }
252
253
        // if there is a title: put the <img> (or <picture>) in a <figure> element to use the <figcaption>
254
        if ($this->builder->getConfig()->get('body.images.caption.enabled') && !empty($InlineImage['element']['attributes']['title'])) {
255
            $FigureBlock = [
256
                'element' => [
257
                    'name'    => 'figure',
258
                    'handler' => 'elements',
259
                    'text'    => [
260
                        $block['element'],
261
                    ],
262
                ],
263
            ];
264
            $InlineFigcaption = [
265
                'element' => [
266
                    'name' => 'figcaption',
267
                    'text' => $InlineImage['element']['attributes']['title'],
268
                ],
269
            ];
270
            $FigureBlock['element']['text'][] = $InlineFigcaption['element'];
271
272
            return $FigureBlock;
273
        }
274
275
        return $block;
276
    }
277
278
    /**
279
     * Note block-level markup.
280
     *
281
     * :::tip
282
     * **Tip:** This is an advice.
283
     * :::
284
     *
285
     * Code inspired by https://github.com/sixlive/parsedown-alert from TJ Miller (@sixlive).
286
     */
287
    protected function blockNote($block)
288
    {
289
        if (preg_match('/:::(.*)/', $block['text'], $matches)) {
290
            return [
291
                'char'    => ':',
292
                'element' => [
293
                    'name'       => 'aside',
294
                    'text'       => '',
295
                    'attributes' => [
296
                        'class' => "note note-{$matches[1]}",
297
                    ],
298
                ],
299
            ];
300
        }
301
    }
302
303
    protected function blockNoteContinue($line, $block)
304
    {
305
        if (isset($block['complete'])) {
306
            return;
307
        }
308
        if (preg_match('/:::/', $line['text'])) {
309
            $block['complete'] = true;
310
311
            return $block;
312 1
        }
313
        $block['element']['text'] .= $line['text']."\n";
314 1
315
        return $block;
316 1
    }
317
318 1
    protected function blockNoteComplete($block)
319 1
    {
320
        $block['element']['rawHtml'] = $this->text($block['element']['text']);
321 1
        unset($block['element']['text']);
322
323
        return $block;
324
    }
325
326
    /**
327
     * Apply Highlight to code blocks.
328 1
     */
329
    protected function blockFencedCodeComplete($block)
330 1
    {
331
        if (!$this->builder->getConfig()->get('body.highlight.enabled')) {
332
            return $block;
333 1
        }
334 1
        if (!isset($block['element']['text']['attributes'])) {
335
            return $block;
336 1
        }
337
338 1
        $code = $block['element']['text']['text'];
339
        unset($block['element']['text']['text']);
340 1
        $languageClass = $block['element']['text']['attributes']['class'];
341
        $language = explode('-', $languageClass);
342
        $highlighted = $this->highlighter->highlight($language[1], $code);
343 1
        $block['element']['text']['attributes']['class'] = vsprintf('%s hljs %s', [
344
            $languageClass,
345 1
            $highlighted->language,
346 1
        ]);
347
        $block['element']['text']['rawHtml'] = $highlighted->value;
348 1
        $block['element']['text']['allowRawHtmlInSafeMode'] = true;
349
350
        return $block;
351
    }
352
353
    /**
354 1
     * {@inheritdoc}
355
     */
356 1
    protected function parseAttributeData($attributeString)
357
    {
358
        $attributes = preg_split('/[ ]+/', $attributeString, -1, PREG_SPLIT_NO_EMPTY);
359 1
        $Data = [];
360
        $HtmlAtt = [];
361
362
        foreach ($attributes as $attribute) {
363 1
            switch ($attribute[0]) {
364 1
                case '#': // ID
365 1
                    $Data['id'] = substr($attribute, 1);
366 1
                    break;
367 1
                case '.': // Classes
368 1
                    $classes[] = substr($attribute, 1);
369 1
                    break;
370 1
                default:  // Attributes
371
                    parse_str($attribute, $parsed);
372 1
                    $HtmlAtt = array_merge($HtmlAtt, $parsed);
373 1
            }
374
        }
375 1
376
        if (isset($classes)) {
377
            $Data['class'] = implode(' ', $classes);
378
        }
379
        if (!empty($HtmlAtt)) {
380
            foreach ($HtmlAtt as $a => $v) {
381 1
                $Data[$a] = trim($v, '"');
382
            }
383 1
        }
384
385
        return $Data;
386
    }
387
388
    /**
389
     * Returns URL without query string.
390
     */
391
    private function cleanUrl(string $path): string
392
    {
393
        return strtok(trim($path), '?') ?: trim($path);
394
    }
395
}
396