Passed
Push — master ( b555ff...9e34ff )
by Josh
01:44
created

Element   A

Complexity

Total Complexity 30

Size/Duplication

Total Lines 286
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 5
Bugs 0 Features 0
Metric Value
eloc 86
dl 0
loc 286
ccs 46
cts 46
cp 1
rs 10
c 5
b 0
f 0
wmc 30

15 Methods

Rating   Name   Duplication   Size   Complexity  
A firstOf() 0 3 1
A insertAdjacentXML() 0 6 1
A insertElement() 0 16 2
A insertText() 0 6 1
A insertAdjacentText() 0 3 1
A insertAdjacentElement() 0 5 1
A __call() 0 34 4
A insertXslElement() 0 11 2
A query() 0 3 1
A evaluate() 0 3 1
A replaceWith() 0 13 3
A addMissingNamespaceDeclarations() 0 20 2
A remove() 0 3 1
A parentOrThrow() 0 8 2
B insertAdjacentNode() 0 28 7
1
<?php declare(strict_types=1);
2
3
/**
4
* @package   s9e\SweetDOM
5
* @copyright Copyright (c) 2019-2021 The s9e authors
6
* @license   http://www.opensource.org/licenses/mit-license.php The MIT License
7
*/
8
namespace s9e\SweetDOM;
9
10
use BadMethodCallException;
11
use DOMElement;
12
use DOMException;
13
use DOMNode;
14
use DOMNodeList;
15
use DOMText;
16
17
/**
18
* @method self appendElement(string $nodeName, $text = '')
19
* @method self appendElementSibling(string $nodeName, $text = '')
20
* @method DOMText appendText(string $text)
21
* @method DOMText appendTextSibling(string $text)
22
* @method self appendXslApplyTemplates(string $select = null)
23
* @method self appendXslApplyTemplatesSibling(string $select = null)
24
* @method self appendXslAttribute(string $name, string $text = '')
25
* @method self appendXslAttributeSibling(string $name, string $text = '')
26
* @method self appendXslChoose()
27
* @method self appendXslChooseSibling()
28
* @method self appendXslComment(string $text = '')
29
* @method self appendXslCommentSibling(string $text = '')
30
* @method self appendXslCopyOf(string $select)
31
* @method self appendXslCopyOfSibling(string $select)
32
* @method self appendXslIf(string $test, string $text = '')
33
* @method self appendXslIfSibling(string $test, string $text = '')
34
* @method self appendXslOtherwise(string $text = '')
35
* @method self appendXslOtherwiseSibling(string $text = '')
36
* @method self appendXslText(string $text = '')
37
* @method self appendXslTextSibling(string $text = '')
38
* @method self appendXslValueOf(string $select)
39
* @method self appendXslValueOfSibling(string $select)
40
* @method self appendXslVariable(string $name, string $select = null)
41
* @method self appendXslVariableSibling(string $name, string $select = null)
42
* @method self appendXslWhen(string $test, string $text = '')
43
* @method self appendXslWhenSibling(string $test, string $text = '')
44
* @method self prependElement(string $nodeName, $text = '')
45
* @method self prependElementSibling(string $nodeName, $text = '')
46
* @method DOMText prependText(string $text)
47
* @method DOMText prependTextSibling(string $text)
48
* @method self prependXslApplyTemplates(string $select = null)
49
* @method self prependXslApplyTemplatesSibling(string $select = null)
50
* @method self prependXslAttribute(string $name, string $text = '')
51
* @method self prependXslAttributeSibling(string $name, string $text = '')
52
* @method self prependXslChoose()
53
* @method self prependXslChooseSibling()
54
* @method self prependXslComment(string $text = '')
55
* @method self prependXslCommentSibling(string $text = '')
56
* @method self prependXslCopyOf(string $select)
57
* @method self prependXslCopyOfSibling(string $select)
58
* @method self prependXslIf(string $test, string $text = '')
59
* @method self prependXslIfSibling(string $test, string $text = '')
60
* @method self prependXslOtherwise(string $text = '')
61
* @method self prependXslOtherwiseSibling(string $text = '')
62
* @method self prependXslText(string $text = '')
63
* @method self prependXslTextSibling(string $text = '')
64 8
* @method self prependXslValueOf(string $select)
65
* @method self prependXslValueOfSibling(string $select)
66 8
* @method self prependXslVariable(string $name, string $select = null)
67
* @method self prependXslVariableSibling(string $name, string $select = null)
68 7
* @method self prependXslWhen(string $test, string $text = '')
69 7
* @method self prependXslWhenSibling(string $test, string $text = '')
70
* @property Document $ownerDocument
71 6
*/
72
class Element extends DOMElement
73 6
{
74
	public function __call(string $name, array $arguments)
75
	{
76
		$name      = strtolower($name);
77
		$positions = [
78
			'append'         => 'beforeend',
79 6
			'appendsibling'  => 'afterend',
80
			'prepend'        => 'afterbegin',
81
			'prependsibling' => 'beforebegin'
82
		];
83 2
84
		if (preg_match('(^(append|prepend)xsl(\\w+?)(sibling|)$)', $name, $m))
85
		{
86
			$localName = $m[2];
87
			$where     = $positions[$m[1] . $m[3]];
88
89
			return $this->insertXslElement($where, $localName, $arguments);
90
		}
91
		if (preg_match('(^(append|prepend)element(sibling|)$)', $name, $m))
92 1
		{
93
			$nodeName = $arguments[0];
94 1
			$text     = $arguments[1] ?? '';
95
			$where    = $positions[$m[1] . $m[2]];
96
97
			return $this->insertElement($where, $nodeName, $text);
98
		}
99
		if (preg_match('(^(append|prepend)text(sibling|)$)', $name, $m))
100
		{
101
			$text  = $arguments[0];
102
			$where = $positions[$m[1] . $m[2]];
103 1
104
			return $this->insertText($where, $text);
105 1
		}
106
107
		throw new BadMethodCallException;
108
	}
109
110
	/**
111
	* Evaluate and return the result of a given XPath expression using this element as context node
112
	*
113
	* @param  string  $expr XPath expression
114
	* @return mixed
115 8
	*/
116
	public function evaluate(string $expr)
117 8
	{
118
		return $this->ownerDocument->evaluate($expr, $this);
119 7
	}
120
121
	/**
122
	* Evaluate and return the first element of a given XPath query using this element as context node
123
	*
124
	* @param  string       $expr XPath expression
125
	* @return DOMNode|null
126
	*/
127
	public function firstOf(string $expr): ?DOMNode
128
	{
129 1
		return $this->ownerDocument->firstOf($expr, $this);
130
	}
131 1
132
	/**
133
	* Insert given element relative to this element's position
134
	*
135
	* @param  string $where   One of 'beforebegin', 'afterbegin', 'beforeend', 'afterend'
136
	* @param  self   $element
137
	* @return self
138
	*/
139
	public function insertAdjacentElement(string $where, self $element): self
140
	{
141 12
		$this->insertAdjacentNode($where, $element);
142
143 12
		return $element;
144 12
	}
145
146 12
	/**
147
	* Insert given text relative to this element's position
148
	*
149
	* @param  string $where One of 'beforebegin', 'afterbegin', 'beforeend', 'afterend'
150
	* @param  string $text
151
	* @return void
152
	*/
153
	public function insertAdjacentText(string $where, string $text): void
154
	{
155 1
		$this->insertText($where, $text);
156
	}
157 1
158
	/**
159
	* Insert given XML relative to this element's position
160
	*
161
	* @param  string $where One of 'beforebegin', 'afterbegin', 'beforeend', 'afterend'
162
	* @param  string $xml
163
	* @return void
164
	*/
165 1
	public function insertAdjacentXML(string $where, string $xml): void
166
	{
167 1
		$fragment = $this->ownerDocument->createDocumentFragment();
168
		$fragment->appendXML($this->addMissingNamespaceDeclarations($xml));
169
170
		$this->insertAdjacentNode($where, $fragment);
171
	}
172
173
	/**
174
	* Evaluate and return the result of a given XPath query using this element as context node
175
	*
176 1
	* @param  string      $expr XPath expression
177
	* @return DOMNodeList
178 1
	*/
179
	public function query(string $expr): DOMNodeList
180
	{
181
		return $this->ownerDocument->query($expr, $this);
182
	}
183
184
	/**
185
	* Remove this element from the document
186
	*
187 12
	* @return void
188
	*/
189 12
	public function remove(): void
190 12
	{
191
		$this->parentOrThrow()->removeChild($this);
192 12
	}
193 12
194
	/**
195
	* Replace this element with given nodes/text
196 3
	*
197 3
	* @param  DOMNode|string $nodes
198 3
	* @return void
199
	*/
200 2
	public function replaceWith(...$nodes): void
201 2
	{
202
		$parentNode = $this->parentOrThrow(new DOMException('No Modification Allowed Error', DOM_NO_MODIFICATION_ALLOWED_ERR));
203
204 3
		foreach ($nodes as $node)
205 12
		{
206 12
			if (!($node instanceof DOMNode))
207
			{
208
				$node = $this->ownerDocument->createTextNode((string) $node);
209
			}
210
			$parentNode->insertBefore($node, $this);
211
		}
212
		$parentNode->removeChild($this);
213
	}
214
215
	/**
216
	* Add namespace declarations that may be missing in given XML
217 20
	*
218
	* @param  string $xml Original XML
219 20
	* @return string      Modified XML
220 20
	*/
221
	protected function addMissingNamespaceDeclarations(string $xml): string
222 4
	{
223
		preg_match_all('(xmlns:\\K[-\\w]++(?==))', $xml, $m);
224 16
		$prefixes = array_flip($m[0]);
225
226 3
		return preg_replace_callback(
227
			'(<([-\\w]++):[^>]*?\\K\\s*/?>)',
228 13
			function ($m) use ($prefixes)
229
			{
230 7
				$return = $m[0];
231
				$prefix = $m[1];
232 6
				if (!isset($prefixes[$prefix]))
233
				{
234 5
					$nsURI  = $this->lookupNamespaceURI($prefix);
235
					$return = ' xmlns:' . $prefix . '="' . htmlspecialchars($nsURI, ENT_XML1) . '"' . $return;
236
				}
237
238 1
				return $return;
239
			},
240
			$xml
241
		);
242
	}
243
244
	/**
245
	* Insert given node relative to this element's position
246
	*
247
	* @param  string  $where One of 'beforebegin', 'afterbegin', 'beforeend', 'afterend'
248
	* @param  DOMNode $node
249
	* @return void
250
	*/
251
	protected function insertAdjacentNode(string $where, DOMNode $node): void
252
	{
253
		switch (strtolower($where))
254
		{
255
			case 'afterbegin':
256
				$this->insertBefore($node, $this->firstChild);
257
				break;
258
259
			case 'afterend':
260
				if (isset($this->parentNode))
261
				{
262
					$this->parentNode->insertBefore($node, $this->nextSibling);
0 ignored issues
show
Bug introduced by
The method insertBefore() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

262
					$this->parentNode->/** @scrutinizer ignore-call */ 
263
                        insertBefore($node, $this->nextSibling);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
263
				}
264
				break;
265
266
			case 'beforebegin':
267
				if (isset($this->parentNode))
268
				{
269
					$this->parentNode->insertBefore($node, $this);
270
				}
271
				break;
272
273
			case 'beforeend':
274
				$this->appendChild($node);
275
				break;
276
277
			default:
278
				throw new DOMException("'$where' is not one of 'beforebegin', 'afterbegin', 'beforeend', or 'afterend'", DOM_SYNTAX_ERR);
279
		}
280
	}
281
282
	/**
283
	* Create and insert an element at given position
284
	*
285
	* @param  string $where    One of 'beforebegin', 'afterbegin', 'beforeend', 'afterend'
286
	* @param  string $nodeName Element's nodeName
287
	* @param  string $text     Text content
288
	* @return self
289
	*/
290
	protected function insertElement(string $where, string $nodeName, string $text): self
291
	{
292
		$text = htmlspecialchars($text, ENT_NOQUOTES);
293
		$pos  = strpos($nodeName, ':');
294
		if ($pos === false)
295
		{
296
			$element = $this->ownerDocument->createElement($nodeName, $text);
297
		}
298
		else
299
		{
300
			$prefix       = substr($nodeName, 0, $pos);
301
			$namespaceURI = $this->ownerDocument->lookupNamespaceURI($prefix);
302
			$element      = $this->ownerDocument->createElementNS($namespaceURI, $nodeName, $text);
303
		}
304
305
		return $this->insertAdjacentElement($where, $element);
306
	}
307
308
	/**
309
	* Insert given text relative to this element's position
310
	*
311
	* @param  string  $where One of 'beforebegin', 'afterbegin', 'beforeend', 'afterend'
312
	* @param  string  $text
313
	* @return DOMText
314
	*/
315
	protected function insertText(string $where, string $text): DOMText
316
	{
317
		$node = $this->ownerDocument->createTextNode($text);
318
		$this->insertAdjacentNode($where, $node);
319
320
		return $node;
321
	}
322
323
	/**
324
	* Create and insert an XSL element at given position
325
	*
326
	* @param  string $where     One of 'beforebegin', 'afterbegin', 'beforeend', 'afterend'
327
	* @param  string $localName Element's localName
328
	* @param  array  $arguments Arguments passed to the Document::create* function
329
	* @return self
330
	*/
331
	protected function insertXslElement(string $where, string $localName, array $arguments): self
332
	{
333
		$callback = [$this->ownerDocument, 'createXsl' . ucfirst($localName)];
334
		if (!is_callable($callback))
335
		{
336
			throw new BadMethodCallException;
337
		}
338
339
		$element = call_user_func_array($callback, $arguments);
340
341
		return $this->insertAdjacentElement($where, $element);
342
	}
343
344
	/**
345
	* Return this element's parent element if available, or throw an exception
346
	*
347
	* @param  DOMException $previous Previous exception
348
	* @return DOMNode
349
	*/
350
	protected function parentOrThrow(DOMException $previous = null): DOMNode
351
	{
352
		if (isset($this->parentNode))
353
		{
354
			return $this->parentNode;
355
		}
356
357
		throw new DOMException('Not Found Error', DOM_NOT_FOUND_ERR, $previous);
358
	}
359
}