Passed
Push — feature/markdown-medias ( 3df5e9...488b63 )
by Arnaud
12:10 queued 09:03
created

Parsedown::inlineInsert()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 13
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 4.128

Importance

Changes 0
Metric Value
cc 4
eloc 9
c 0
b 0
f 0
nc 3
nop 1
dl 0
loc 13
ccs 4
cts 5
cp 0.8
crap 4.128
rs 9.9666
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