Passed
Push — main ( 1a7f0e...7d39cf )
by Oscar
03:17
created

DomHelper::getElementAttributes()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 2
c 0
b 0
f 0
dl 0
loc 6
ccs 3
cts 3
cp 1
rs 10
cc 2
nc 2
nop 1
crap 2
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\Util;
13
14
use function BenTools\IterableFunctions\iterable_to_array;
15
16
use Ocubom\Twig\Extension\Svg\Exception\RuntimeException;
17
use Symfony\Component\OptionsResolver\Options;
18
use Symfony\Component\OptionsResolver\OptionsResolver;
19
20
/** @internal */
21
final class DomHelper
22
{
23
    /** @var OptionsResolver[] */
24
    private static array $resolver = [];
25
26
    // @codeCoverageIgnoreStart
27
    private function __construct()
28
    {
29
    }
30
31
    // @codeCoverageIgnoreEnd
32
33 7
    public static function createDocument(iterable $options = null): \DOMDocument
34
    {
35 7
        $resolver = self::$resolver[__METHOD__] = self::$resolver[__METHOD__]
36 1
            ?? self::configureDocumentOptions();
37
38
        // Resolve options
39 7
        $options = $resolver->resolve(iterable_to_array($options ?? []));
40
41
        // Create document
42 7
        $doc = new \DOMDocument($options['version'], $options['encoding']);
43
44
        // Apply options
45 7
        foreach ($options as $key => $val) {
46 7
            if (in_array($key, ['version', 'encoding', 'standalone'])) {
47 7
                continue; // Ignore constructor arguments and deprecated options
48
            }
49
50
            // Apply property
51 7
            $doc->{$key} = $val;
52
        }
53
54 7
        return $doc;
55
    }
56
57 3
    public static function appendChildElement(
58
        \DOMElement $element,
59
        \DOMNode $parent
60
    ): \DOMElement {
61 3
        $child = self::appendChildNode($element, $parent);
62
        assert($child instanceof \DOMElement);
63
64 3
        return $child;
65
    }
66
67 3
    public static function appendChildNode(
68
        \DOMNode $node,
69
        \DOMNode $parent
70
    ): \DOMNode {
71
        assert($parent->ownerDocument instanceof \DOMDocument);
72
73 3
        $child = $parent->appendChild(
74 3
            $parent->ownerDocument === $node->ownerDocument
75 3
                ? $node->cloneNode(true)
76 3
                : $parent->ownerDocument->importNode($node, true)
77 3
        );
78
        assert($child instanceof \DOMNode);
79
80 3
        return $child;
81
    }
82
83 7
    public static function cloneElement(
84
        \DOMElement $element,
85
        bool $deep = true,
86
        \DOMDocument $document = null
87
    ): \DOMElement {
88 7
        $clone = self::cloneNode($element, $deep, $document);
89
90
        assert($clone instanceof \DOMElement);
91
92 7
        return $clone;
93
    }
94
95 7
    public static function cloneNode(
96
        \DOMNode $node,
97
        bool $deep = true,
98
        \DOMDocument $document = null
99
    ): \DOMNode {
100 7
        $doc = $document ?? self::createDocument();
101
102 7
        $clone = $node->ownerDocument === $doc ? $node->cloneNode($deep) : $doc->importNode($node, $deep);
103
        assert($clone instanceof \DOMNode);
104
105 7
        return $clone;
106
    }
107
108 1
    public static function createComment(
109
        string $data = '',
110
        \DOMNode $node = null,
111
        bool $before = false
112
    ): \DOMComment {
113
        // Get node document ($node if is a DOMdocument)
114 1
        $doc = $node instanceof \DOMNode
115 1
            ? ($node->ownerDocument ?? $node)
116
            : self::createDocument();
117
        assert($doc instanceof \DOMDocument);
118
119 1
        $new = $doc->createComment($data);
120 1
        if (null === $node || $doc === $node) {
121
            // Append child to the document
122
            $new = $doc->appendChild($new);
123 1
        } elseif ($before) {
124
            // Insert child before the element
125
            assert($node->parentNode instanceof \DOMNode);
126 1
            $new = $node->parentNode->insertBefore($new, $node);
127
        } else {
128
            // Insert child before the end of the element
129
            $new = $node->insertBefore($new);
130
        }
131
        assert($new instanceof \DOMComment);
132
133 1
        return $new;
134
    }
135
136 6
    public static function createElement(
137
        string $name,
138
        string $value = '',
139
        \DOMNode $node = null,
140
        bool $before = false
141
    ): \DOMElement {
142
        try {
143
            // Get node document ($node if is a DOMdocument)
144 6
            $doc = $node instanceof \DOMNode
145 6
                ? ($node->ownerDocument ?? $node)
146 6
                : self::createDocument();
147
            assert($doc instanceof \DOMDocument);
148
149 6
            $new = $doc->createElement($name, $value);
150 6
            if (null === $node || $doc === $node) {
151
                // Append child to the document
152 6
                $new = $doc->appendChild($new);
153 6
            } elseif ($before) {
154
                // Insert child before the element
155
                assert($node->parentNode instanceof \DOMNode);
156 4
                $new = $node->parentNode->insertBefore($new, $node);
157
            } else {
158
                // Insert child before the end of the element
159 3
                $new = $node->insertBefore($new);
160
            }
161
            assert($new instanceof \DOMElement);
162
163 6
            return $new;
164
        } catch (\DOMException $exc) { // @codeCoverageIgnore
165
            throw new RuntimeException($exc->getMessage(), $exc->getCode(), $exc); // @codeCoverageIgnore
166
        }
167
    }
168
169 1
    public static function query(
170
        string $expression,
171
        \DOMNode $node,
172
        bool $registerNodeNS = true
173
    ): \DOMNodeList {
174 1
        $doc = $node instanceof \DOMDocument ? $node : $node->ownerDocument;
175
        assert($doc instanceof \DOMDocument);
176
177 1
        $nodes = (new \DOMXPath($doc))->query(
178 1
            $expression,
179 1
            $node instanceof \DOMDocument ? null : $node,
180 1
            $registerNodeNS
181 1
        );
182
183
        assert($nodes instanceof \DOMNodeList);
184
185 1
        return $nodes;
186
    }
187
188 1
    public static function removeNode(
189
        \DOMNode $node
190
    ): void {
191
        assert($node->parentNode instanceof \DOMNode);
192
193 1
        $node->parentNode->removeChild($node);
194
    }
195
196 4
    public static function replaceNode(
197
        \DOMNode $old,
198
        \DOMNode $new
199
    ): \DOMNode {
200 4
        $doc = $old->ownerDocument;
201
        assert($doc instanceof \DOMDocument);
202
203
        // Clone new node in the document
204 4
        $new = $doc->importNode($new, true);
205
        assert($new instanceof \DOMNode);
206
207
        // Obtain parent
208 4
        $parent = $old->parentNode;
209
        assert($parent instanceof \DOMNode);
210
211 4
        $parent->replaceChild($new, $old);
212
213 4
        return $new;
214
    }
215
216 7
    public static function getElementAttributes(
217
        \DOMElement $node
218
    ): iterable {
219
        /** @var \DOMAttr $attribute */
220 7
        foreach ($node->attributes as $attribute) {
221 7
            yield $attribute->name => $attribute->value;
222
        }
223
    }
224
225 2
    public static function toHtml(
226
        \DOMNode $node
227
    ): string {
228
        assert($node->ownerDocument instanceof \DOMDocument);
229
230 2
        $output = $node->ownerDocument->saveHTML($node);
231 2
        if (false === $output) {
232
            // @codeCoverageIgnoreStart
233
            throw new RuntimeException(sprintf(
234
                'Unable to convert %s to HTML',
235
                get_class($node)
236
            ));
237
            // @codeCoverageIgnoreEnd
238
        }
239
240 2
        return $output;
241
    }
242
243 7
    public static function toXml(
244
        \DOMNode $node,
245
        int $options = 0
246
    ): string {
247
        assert($node->ownerDocument instanceof \DOMDocument);
248
249 7
        $output = $node->ownerDocument->saveXML($node, $options);
250 7
        if (false === $output) {
251
            // @codeCoverageIgnoreStart
252
            throw new RuntimeException(sprintf(
253
                'Unable to convert %s to XML',
254
                get_class($node)
255
            ));
256
            // @codeCoverageIgnoreEnd
257
        }
258
259 7
        return $output;
260
    }
261
262 7
    private static function configureDocumentOptions(OptionsResolver $resolver = null): OptionsResolver
263
    {
264 1
        $resolver = $resolver ?? new OptionsResolver();
265
266 1
        $resolver->define('encoding')
267 1
            ->default('')
268 1
            ->allowedTypes('string')
269 1
            ->info('Encoding of the document, as specified by the XML declaration');
270
271 1
        $resolver->define('standalone')
272 1
            ->default(true)
273 1
            ->allowedTypes('bool')
274 1
            ->deprecated(
275 1
                'ocubom/twig-svg-extension',
276 1
                '1.0',
277 1
                'The option "standalone" is deprecated, use "xmlStandalone" instead.'
278 1
            )
279 1
            ->info('Whether or not the document is standalone, as specified by the XML declaration');
280
281 1
        $resolver->define('xmlStandalone')
282 1
            ->default(true)
283 1
            ->allowedTypes('null', 'bool')
284 1
            ->normalize(function (Options $options, ?bool $value): bool {
285 7
                return $value ?? (bool) $options
286 7
                    ->/* @scrutinizer ignore-call */ offsetGet('standalone', false);
287 1
            })
288 1
            ->info('Whether or not the document is standalone, as specified by the XML declaration');
289
290 1
        $resolver->define('version')
291 1
            ->default('1.0')
292 1
            ->allowedTypes('string')
293 1
            ->deprecated(
294 1
                'ocubom/twig-svg-extension',
295 1
                '1.0',
296 1
                'The option "version" is deprecated, use "xmlVersion" instead.'
297 1
            )
298 1
            ->info('The version number of this document, as specified by the XML declaration');
299
300 1
        $resolver->define('xmlVersion')
301 1
            ->default('1.0')
302 1
            ->allowedTypes('null', 'string')
303 1
            ->normalize(function (Options $options, ?string $value): string {
304 7
                return $value ?? (string) $options
305 7
                    ->/* @scrutinizer ignore-call */ offsetGet('version', false);
306 1
            })
307 1
            ->info('The version number of this document, as specified by the XML declaration');
308
309 1
        $resolver->define('strictErrorChecking')
310 1
            ->default(true)
311 1
            ->allowedTypes('bool')
312 1
            ->info('Throws \DOMException on errors');
313
314 1
        $resolver->define('documentURI')
315 1
            ->default(null)
316 1
            ->allowedTypes('null', 'string')
317 1
            ->info('The location of the document');
318
319 1
        $resolver->define('formatOutput')
320 1
            ->default(true) // LibXML default is false
321 1
            ->allowedTypes('bool')
322 1
            ->info('Nicely formats output with indentation and extra space');
323
324 1
        $resolver->define('validateOnParse')
325 1
            ->default(false)
326 1
            ->allowedTypes('bool')
327 1
            ->info('Loads and validates against the DTD');
328
329 1
        $resolver->define('resolveExternals')
330 1
            ->default(false)
331 1
            ->allowedTypes('bool')
332 1
            ->info('Load external entities from a doctype declaration');
333
334 1
        $resolver->define('preserveWhiteSpace')
335 1
            ->default(true)
336 1
            ->allowedTypes('bool')
337 1
            ->normalize(function (Options $options, bool $value): bool {
338
                // Must be false to enable formatOutput
339 7
                return $options['formatOutput'] ? false : $value;
340 1
            })
341 1
            ->info('Do not remove redundant white space');
342
343
        // LibXML specific
344 1
        $resolver->define('recover')
345 1
            ->default(false)
346 1
            ->allowedTypes('bool')
347 1
            ->info('Enables recovery mode');
348
349
        // LibXML specific
350 1
        $resolver->define('substituteEntities')
351 1
            ->default(false)
352 1
            ->allowedTypes('bool')
353 1
            ->info('Whether or not to substitute entities');
354
355 1
        return $resolver;
356
    }
357
}
358