Passed
Push — analysis-4x3x7n ( 165a16 )
by Arnaud
05:24 queued 12s
created

Parsedown::blockNote()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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