Passed
Push — feat/markdown-notes ( fd235e...a83d5e )
by Arnaud
07:44 queued 03:51
created

Parsedown::parseAttributeData()   B

Complexity

Conditions 7
Paths 16

Size

Total Lines 30
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

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