Passed
Push — main ( d61a4b...a3ea10 )
by Oscar
12:17
created

Svg::configureOptions()   C

Complexity

Conditions 14
Paths 1

Size

Total Lines 115
Code Lines 75

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 82
CRAP Score 14

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 75
dl 0
loc 115
ccs 82
cts 82
cp 1
rs 5.5587
c 1
b 0
f 0
cc 14
nc 1
nop 1
crap 14

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
/*
4
 * This file is part of ocubom/twig-svg-extension
5
 *
6
 * © Oscar Cubo Medina <https://ocubom.github.io>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Ocubom\Twig\Extension\Svg;
13
14
use enshrined\svgSanitize\Sanitizer;
15
use Ocubom\Twig\Extension\Svg\Exception\FileNotFoundException;
16
use Ocubom\Twig\Extension\Svg\Exception\ParseException;
17
use Ocubom\Twig\Extension\Svg\Processor\ClassProcessor;
18
use Ocubom\Twig\Extension\Svg\Processor\RemoveAttributeProcessor;
19
use Ocubom\Twig\Extension\Svg\Processor\TitleProcessor;
20
use Ocubom\Twig\Extension\Svg\Util\DomUtil;
21
use Symfony\Component\OptionsResolver\Options;
22
use Symfony\Component\OptionsResolver\OptionsResolver;
23
24
use function BenTools\IterableFunctions\iterable_merge;
25
use function BenTools\IterableFunctions\iterable_to_array;
26
use function Ocubom\Twig\Extension\is_string;
27
28
class Svg implements SvgInterface
29
{
30
    use SvgTrait;
31
32
    /** @var array<string, array<string, array<int, callable>|callable>> */
33
    private static array $processors = [];
34
35
    /**
36
     * @param Svg|\DOMNode|\SplFileInfo|\Stringable|string $data The SVG data
37
     */
38 11
    public function __construct($data, iterable $options = null)
39
    {
40
        switch (true) {
41 11
            case $data instanceof Svg : // "Copy" constructor
42
                $node = $this->constructFromString(DomUtil::toXml($data->svg)); // @codeCoverageIgnore
43
                break; // @codeCoverageIgnore
44
45 11
            case $data instanceof \DOMNode :
46 3
                $node = $this->constructFromString(DomUtil::toXml($data));
47 3
                break;
48
49 8
            case $data instanceof \SplFileInfo :
50 5
                $node = $this->constructFromFile($data);
51 4
                break;
52
53 4
            case is_string($data) :
54 4
                $node = $this->constructFromString($data);
55 4
                break;
56
57
            default:
58
                throw new ParseException(sprintf('Unable to create an SVG from "%s"', get_debug_type($data))); // @codeCoverageIgnore
59
        }
60
61
        // Merge options with SVG attributes
62 10
        $options = iterable_to_array(iterable_merge(
63 10
            DomUtil::getElementAttributes($node),
64 10
            $options ?? /* @scrutinizer ignore-type */ []
65 10
        ));
66
67
        // Define SVG attributes and resolve options + attributes
68 10
        $options = static::configureOptions()
69 10
            ->setDefined(array_keys($options)) // enables all optional options
70 10
            ->resolve($options);
71
72
        // Retrieve processors for this class with a naive cache
73 10
        $processors = self::$processors[static::class]
74 10
            ?? self::$processors[static::class] = array_map(
75 10
                function ($processors): array {
76 2
                    return is_iterable($processors) ? $processors : [$processors];
77 10
                },
78 10
                static::getProcessors()
79 10
            );
80 10
        if (isset($processors[''])) {
81 3
            $options[''] = null; // Force execution of global processors.
82
        }
83
84
        // Apply processors on a flesh clone in new DOM document
85 10
        $this->svg = DomUtil::cloneElement($node);
86 10
        foreach ($options as $key => $val) {
87 10
            if (isset($processors[$key])) {
88
                // Apply processors
89 10
                foreach ($processors[$key] as $processor) {
90 10
                    $this->svg = $processor($this->svg, $options);
91
                }
92 10
            } elseif (empty($val)) {
93
                // Remove empty attributes
94 4
                $this->svg->removeAttribute($key);
95
            } else {
96
                // Set attribute value
97 10
                $this->svg->setAttribute($key, $val);
98
            }
99
        }
100
    }
101
102 5
    protected function constructFromFile(\SplFileInfo $path): \DOMElement
103
    {
104 5
        $path = (string) $path;
105
106 5
        if (!is_file($path)) {
107
            throw new FileNotFoundException(sprintf('File "%s" does not exist.', $path)); // @codeCoverageIgnore
108
        }
109
110 5
        if (!is_readable($path)) {
111
            throw new FileNotFoundException(sprintf('File "%s" cannot be read.', $path)); // @codeCoverageIgnore
112
        }
113
114
        try {
115 5
            return $this->constructFromString(file_get_contents($path));
116 1
        } catch (ParseException $exc) {
117 1
            throw new ParseException(sprintf('File "%s" does not contain a valid SVG.', $path), 0, $exc);
118
        }
119
    }
120
121 11
    protected function constructFromString(string $contents): \DOMElement
122
    {
123
        // Sanitize contents (if enshrined\svgSanitize is installed)
124 11
        if (class_exists(Sanitizer::class)) {
125 11
            $contents = (new Sanitizer())->sanitize($contents);
126
        }
127
128
        // Remove all namespaces
129 11
        $contents = preg_replace('@xmlns(:.*)?=(\"[^\"]*\"|\'[^\']*\')@Uis', '', $contents);
130 11
        if (empty($contents)) {
131 1
            throw new ParseException(sprintf('Invalid SVG string "%s".', func_get_arg(0)));
132
        }
133
134
        // Parse contents into DOM
135 10
        $doc = DomUtil::createDocument();
136 10
        if (false === $doc->loadXML($contents)) {
137
            throw new ParseException(sprintf('Unable to load SVG string "%s".', func_get_arg(0))); // @codeCoverageIgnore
138
        }
139
140
        // Get first svg item
141 10
        $node = $doc->getElementsByTagName('svg')->item(0);
142 10
        if ($node instanceof \DOMElement) {
143 10
            return $node; // Return first SVG element
144
        }
145
146
        throw new ParseException(sprintf('String "%s" does not contain any SVG.', func_get_arg(0))); // @codeCoverageIgnore
147
    }
148
149
    /**
150
     * @return array<string, array<int, callable>|callable>
151
     */
152 2
    protected static function getProcessors(): array
153
    {
154 2
        return [
155
            // Options will be ignored & removed
156 2
            'debug' => new RemoveAttributeProcessor('debug'),
157 2
            'minimize' => new RemoveAttributeProcessor('minimize'),
158 2
            'class_block' => new RemoveAttributeProcessor('class_block'),
159
160
            // Custom process
161 2
            'class' => new ClassProcessor(),
162 2
            'title' => new TitleProcessor(),
163
164
            // Ignore attributes that depends on other options
165 2
            'aria-labelledby' => [], // generated on TitleProcessor
166 2
        ];
167
    }
168
169
    /** @psalm-suppress MissingClosureParamType */
170 10
    protected static function configureOptions(OptionsResolver $resolver = null): OptionsResolver
171
    {
172 10
        $resolver = $resolver ?? new OptionsResolver();
173
174
        // Options
175
176 10
        $resolver->define('debug')
177 10
            ->default(false)
178 10
            ->allowedTypes('bool')
179 10
            ->info('Enable debug output');
180
181 10
        $resolver->define('minimize')
182 10
            ->default(true)
183 10
            ->allowedTypes('bool')
184 10
            ->info('Minimize output');
185
186
        // Attributes
187
188
        /** @psalm-suppress MissingClosureParamType */
189 10
        $normalizeClass = function (Options $options, $value): array {
190 10
            return array_filter(
191 10
                is_string($value) ? preg_split('@\s+@Uis', $value) : ($value ?? []),
192 10
                function (string $item): bool {
193 10
                    return !empty($item);
194 10
                }
195 10
            );
196 10
        };
197
198 10
        $resolver->define('class')
199 10
            ->default('')
200 10
            ->allowedTypes('null', 'string', 'string[]')
201 10
            ->normalize($normalizeClass)
202 10
            ->info('Classes to apply');
203
204 10
        $resolver->define('class_block')
205 10
            ->default('')
206 10
            ->allowedTypes('null', 'string', 'string[]')
207 10
            ->normalize($normalizeClass)
208 10
            ->info('Classes to block');
209
210 10
        $resolver->define('width')
211 10
            ->default('1em')
212 10
            ->allowedTypes('string')
213 10
            ->info('Width of the element');
214
215 10
        $resolver->define('height')
216 10
            ->default('1em')
217 10
            ->allowedTypes('string')
218 10
            ->info('Height of the element');
219
220 10
        $resolver->define('title')
221 10
            ->default(null)
222 10
            ->allowedTypes('null', 'string')
223 10
            ->normalize(function (Options $options, ?string $value): ?string {
224 10
                return empty($value ?? '') ? null : $value;
225 10
            })
226 10
            ->info('Title of the icon. Used for semantic icons.');
227
228 10
        $resolver->define('focusable')
229 10
            ->default('false')
230 10
            ->allowedTypes('null', 'bool', 'string')
231 10
            ->normalize(function (Options $options, $value): ?string {
232 10
                if (is_bool($value)) {
233 1
                    return $value ? 'true' : 'false';
234
                }
235
236 10
                return empty($value) ? null : $value;
237 10
            })
238 10
            ->info('Indicates if the element can take the focus');
239
240 10
        $resolver->define('role')
241 10
            ->default('img')
242 10
            ->allowedTypes('null', 'string')
243 10
            ->info('Indicates the semantic meaning of the content');
244
245 10
        $resolver->define('aria-hidden')
246 10
            ->default(null)
247 10
            ->allowedTypes('null', 'bool', 'string')
248 10
            ->allowedValues(null, true, 'true', false, 'false')
249 10
            ->normalize(function (Options $options, $value): ?string {
250
                // Decorative icon: no title and not previously defined aria-hidden value
251
                if (
252 10
                    null === $value
253 10
                    && empty($options['aria-labelledby'])
254 10
                    && empty($options['title'])
255
                ) {
256 8
                    return 'true';
257
                }
258
259
                // Convert bool value into text
260 4
                if (is_bool($value)) {
261 1
                    return $value ? 'true' : 'false';
262
                }
263
264 4
                return empty($value)
265 4
                    ? null
266 4
                    : ('true' === $value ? 'true' : 'false');
267 10
            })
268 10
            ->info('Indicates whether the element is exposed to an accessibility API');
269
270 10
        $resolver->define('aria-labelledby')
271 10
            ->default(null)
272 10
            ->allowedTypes('null', 'string')
273 10
            ->normalize(function (Options $options, $value): ?string {
274
                // Decorative icon: no title and not previously defined aria-hidden value
275 10
                if (empty($options['title'])) {
276 9
                    return null;
277
                }
278
279
                // Generate an identifier based on title contents
280 4
                return $value ?? DomUtil::generateId(DomUtil::createElement('title', $options['title']));
281 10
            })
282 10
            ->info('Identifies the element that labels this element');
283
284 10
        return $resolver;
285
    }
286
}
287