Passed
Push — main ( 77e259...371264 )
by Oscar
08:09 queued 06:34
created

DomHelper   A

Complexity

Total Complexity 26

Size/Duplication

Total Lines 293
Duplicated Lines 0 %

Test Coverage

Coverage 91.18%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 26
eloc 145
c 1
b 0
f 0
dl 0
loc 293
ccs 124
cts 136
cp 0.9118
rs 10

13 Methods

Rating   Name   Duplication   Size   Complexity  
A createElement() 0 25 5
A replaceNode() 0 18 1
A appendChildNode() 0 14 2
B configureDocumentOptions() 0 94 2
A query() 0 17 3
A cloneNode() 0 11 2
A removeNode() 0 6 1
A toHtml() 0 16 2
A toXml() 0 17 2
A appendChildElement() 0 8 1
A cloneElement() 0 10 1
A __construct() 0 2 1
A createDocument() 0 22 3
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
    private static \DOMDocument $doc;
26
27
    // @codeCoverageIgnoreStart
28
    private function __construct()
29
    {
30
    }
31
    // @codeCoverageIgnoreEnd
32
33 5
    public static function createDocument(iterable $options = null): \DOMDocument
34
    {
35 5
        $resolver = self::$resolver[__METHOD__] = self::$resolver[__METHOD__]
36 1
            ?? self::configureDocumentOptions();
37
38
        // Resolve options
39 5
        $options = $resolver->resolve(iterable_to_array($options ?? []));
40
41
        // Create document
42 5
        $doc = new \DOMDocument($options['version'], $options['encoding']);
43
44
        // Apply options
45 5
        foreach ($options as $key => $val) {
46 5
            if (in_array($key, ['version', 'encoding', 'standalone'])) {
47 5
                continue; // Ignore constructor arguments and deprecated options
48
            }
49
50
            // Apply property
51 5
            $doc->{$key} = $val;
52
        }
53
54 5
        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 5
    public static function cloneElement(
84
        \DOMElement $element,
85
        bool $deep = true,
86
        \DOMDocument $document = null
87
    ): \DOMElement {
88 5
        $clone = self::cloneNode($element, $deep, $document);
89
90
        assert($clone instanceof \DOMElement);
91
92 5
        return $clone;
93
    }
94
95 5
    public static function cloneNode(
96
        \DOMNode $node,
97
        bool $deep = true,
98
        \DOMDocument $document = null
99
    ): \DOMNode {
100 5
        $doc = $document ?? self::createDocument();
101
102 5
        $clone = $node->ownerDocument === $doc ? $node->cloneNode($deep) : $doc->importNode($node, $deep);
103
        assert($clone instanceof \DOMNode);
104
105 5
        return $clone;
106
    }
107
108 3
    public static function createElement(
109
        string $name,
110
        string $value = '',
111
        \DOMNode $node = null
112
    ): \DOMElement {
113
        try {
114
            // Get node document ($node if is a DOMdocument)
115 3
            $doc = $node instanceof \DOMNode
116 3
                ? ($node->ownerDocument ?? $node)
117 3
                : self::createDocument();
118
            assert($doc instanceof \DOMDocument);
119
120 3
            $new = $doc->createElement($name, $value);
121 3
            if (null === $node || $doc === $node) {
122
                // Append child to the document
123 3
                $new = $doc->appendChild($new);
124
            } else {
125
                // Insert child before the end of the element
126 3
                $new = $node->insertBefore($new);
127
            }
128
            assert($new instanceof \DOMElement);
129
130 3
            return $new;
131
        } catch (\DOMException $exc) { // @codeCoverageIgnore
132
            throw new RuntimeException($exc->getMessage(), $exc->getCode(), $exc); // @codeCoverageIgnore
133
        }
134
    }
135
136
    public static function query(
137
        string $expression,
138
        \DOMNode $node,
139
        bool $registerNodeNS = true
140
    ): \DOMNodeList {
141
        $doc = $node instanceof \DOMDocument ? $node : $node->ownerDocument;
142
        assert($doc instanceof \DOMDocument);
143
144
        $nodes = (new \DOMXPath($doc))->query(
145
            $expression,
146
            $node instanceof \DOMDocument ? null : $node,
147
            $registerNodeNS
148
        );
149
150
        assert($nodes instanceof \DOMNodeList);
151
152
        return $nodes;
153
    }
154
155 1
    public static function removeNode(
156
        \DOMNode $node
157
    ): void {
158
        assert($node->parentNode instanceof \DOMNode);
159
160 1
        $node->parentNode->removeChild($node);
161
    }
162
163 3
    public static function replaceNode(
164
        \DOMNode $old,
165
        \DOMNode $new
166
    ): \DOMNode {
167 3
        $doc = $old->ownerDocument;
168
        assert($doc instanceof \DOMDocument);
169
170
        // Clone new node in the document
171 3
        $new = $doc->importNode($new, true);
172
        assert($new instanceof \DOMNode);
173
174
        // Obtain parent
175 3
        $parent = $old->parentNode;
176
        assert($parent instanceof \DOMNode);
177
178 3
        $parent->replaceChild($new, $old);
179
180 3
        return $new;
181
    }
182
183
    public static function toHtml(
184
        \DOMNode $node
185
    ): string {
186
        assert($node->ownerDocument instanceof \DOMDocument);
187
188
        $output = $node->ownerDocument->saveHTML($node);
189
        if (false === $output) {
190
            // @codeCoverageIgnoreStart
191
            throw new RuntimeException(sprintf(
192
                'Unable to convert %s to HTML',
193
                get_class($node)
194
            ));
195
            // @codeCoverageIgnoreEnd
196
        }
197
198
        return $output;
199
    }
200
201 4
    public static function toXml(
202
        \DOMNode $node,
203
        int $options = 0
204
    ): string {
205
        assert($node->ownerDocument instanceof \DOMDocument);
206
207 4
        $output = $node->ownerDocument->saveXML($node, $options);
208 4
        if (false === $output) {
209
            // @codeCoverageIgnoreStart
210
            throw new RuntimeException(sprintf(
211
                'Unable to convert %s to XML',
212
                get_class($node)
213
            ));
214
            // @codeCoverageIgnoreEnd
215
        }
216
217 4
        return $output;
218
    }
219
220 5
    private static function configureDocumentOptions(OptionsResolver $resolver = null): OptionsResolver
221
    {
222 1
        $resolver = $resolver ?? new OptionsResolver();
223
224 1
        $resolver->define('encoding')
225 1
            ->default('')
226 1
            ->allowedTypes('string')
227 1
            ->info('Encoding of the document, as specified by the XML declaration');
228
229 1
        $resolver->define('standalone')
230 1
            ->default(true)
231 1
            ->allowedTypes('bool')
232 1
            ->deprecated(
233 1
                'ocubom/twig-svg-extension',
234 1
                '1.0',
235 1
                'The option "standalone" is deprecated, use "xmlStandalone" instead.'
236 1
            )
237 1
            ->info('Whether or not the document is standalone, as specified by the XML declaration');
238
239 1
        $resolver->define('xmlStandalone')
240 1
            ->default(true)
241 1
            ->allowedTypes('null', 'bool')
242 1
            ->normalize(function (Options $options, ?bool $value): bool {
243 5
                return $value ?? (bool) $options
244 5
                    ->/* @scrutinizer ignore-call */ offsetGet('standalone', false);
245 1
            })
246 1
            ->info('Whether or not the document is standalone, as specified by the XML declaration');
247
248 1
        $resolver->define('version')
249 1
            ->default('1.0')
250 1
            ->allowedTypes('string')
251 1
            ->deprecated(
252 1
                'ocubom/twig-svg-extension',
253 1
                '1.0',
254 1
                'The option "version" is deprecated, use "xmlVersion" instead.'
255 1
            )
256 1
            ->info('The version number of this document, as specified by the XML declaration');
257
258 1
        $resolver->define('xmlVersion')
259 1
            ->default('1.0')
260 1
            ->allowedTypes('null', 'string')
261 1
            ->normalize(function (Options $options, ?string $value): string {
262 5
                return $value ?? (string) $options
263 5
                    ->/* @scrutinizer ignore-call */ offsetGet('version', false);
264 1
            })
265 1
            ->info('The version number of this document, as specified by the XML declaration');
266
267 1
        $resolver->define('strictErrorChecking')
268 1
            ->default(true)
269 1
            ->allowedTypes('bool')
270 1
            ->info('Throws \DOMException on errors');
271
272 1
        $resolver->define('documentURI')
273 1
            ->default(null)
274 1
            ->allowedTypes('null', 'string')
275 1
            ->info('The location of the document');
276
277 1
        $resolver->define('formatOutput')
278 1
            ->default(true) // LibXML default is false
279 1
            ->allowedTypes('bool')
280 1
            ->info('Nicely formats output with indentation and extra space');
281
282 1
        $resolver->define('validateOnParse')
283 1
            ->default(false)
284 1
            ->allowedTypes('bool')
285 1
            ->info('Loads and validates against the DTD');
286
287 1
        $resolver->define('resolveExternals')
288 1
            ->default(false)
289 1
            ->allowedTypes('bool')
290 1
            ->info('Load external entities from a doctype declaration');
291
292 1
        $resolver->define('preserveWhiteSpace')
293 1
            ->default(true)
294 1
            ->allowedTypes('bool')
295 1
            ->normalize(function (Options $options, bool $value): bool {
296
                // Must be false to enable formatOutput
297 5
                return $options['formatOutput'] ? false : $value;
298 1
            })
299 1
            ->info('Do not remove redundant white space');
300
301
        // LibXML specific
302 1
        $resolver->define('recover')
303 1
            ->default(false)
304 1
            ->allowedTypes('bool')
305 1
            ->info('Enables recovery mode');
306
307
        // LibXML specific
308 1
        $resolver->define('substituteEntities')
309 1
            ->default(false)
310 1
            ->allowedTypes('bool')
311 1
            ->info('Whether or not to substitute entities');
312
313 1
        return $resolver;
314
    }
315
}
316