Completed
Push — master ( 274a8c...2b20b1 )
by Josh
02:21
created

Element::insertAdjacentElement()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 2
dl 0
loc 5
rs 10
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
3
/**
4
* @package   s9e\SweetDOM
5
* @copyright Copyright (c) 2019-2020 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 DOMNode;
13
use DOMNodeList;
14
use InvalidArgumentException;
15
16
class Element extends DOMElement
17
{
18
	public function __call(string $name, array $arguments)
19
	{
20
		if (preg_match('(^(append|prepend)(Xsl\\w++)$)', $name, $m))
21
		{
22
			$callback = [$this->ownerDocument, 'create' . $m[2]];
23
			if (is_callable($callback))
24
			{
25
				$element  = call_user_func_array($callback, $arguments);
26
				$where = ($m[1] === 'append') ? 'beforeend' : 'afterbegin';
27
28
				return $this->insertAdjacentElement($where, $element);
29
			}
30
		}
31
32
		throw new BadMethodCallException;
33
	}
34
35
	/**
36
	* Evaluate and return the result of a given XPath expression using this element as context node
37
	*
38
	* @param  string  $expr XPath expression
39
	* @return mixed
40
	*/
41
	public function evaluate(string $expr)
42
	{
43
		return $this->ownerDocument->evaluate($expr, $this);
44
	}
45
46
	/**
47
	* Evaluate and return the first element of a given XPath query using this element as context node
48
	*
49
	* @param  string       $expr XPath expression
50
	* @return DOMNode|null
51
	*/
52
	public function firstOf(string $expr): ?DOMNode
53
	{
54
		return $this->ownerDocument->firstOf($expr, $this);
55
	}
56
57
	/**
58
	* Evaluate and return the result of a given XPath query using this element as context node
59
	*
60
	* @param  string      $expr XPath expression
61
	* @return DOMNodeList
62
	*/
63
	public function query(string $expr): DOMNodeList
64
	{
65
		return $this->ownerDocument->query($expr, $this);
66
	}
67
68
	/**
69
	* Insert given element relative to this element's position
70
	*
71
	* @param  string $where   One of 'beforebegin', 'afterbegin', 'beforeend', 'afterend'
72
	* @param  self   $element
73
	* @return self
74
	*/
75
	public function insertAdjacentElement(string $where, self $element): self
76
	{
77
		$this->insertAdjacentNode($where, $element);
78
79
		return $element;
80
	}
81
82
	/**
83
	* Insert given text relative to this element's position
84
	*
85
	* @param  string $where One of 'beforebegin', 'afterbegin', 'beforeend', 'afterend'
86
	* @param  string $text
87
	* @return void
88
	*/
89
	public function insertAdjacentText(string $where, string $text): void
90
	{
91
		$this->insertAdjacentXML($where, htmlspecialchars($text, ENT_XML1));
92
	}
93
94
	/**
95
	* Insert given XML relative to this element's position
96
	*
97
	* @param  string $where One of 'beforebegin', 'afterbegin', 'beforeend', 'afterend'
98
	* @param  string $xml
99
	* @return void
100
	*/
101
	public function insertAdjacentXML(string $where, string $xml): void
102
	{
103
		$fragment = $this->ownerDocument->createDocumentFragment();
104
		$fragment->appendXML($this->addMissingNamespaceDeclarations($xml));
105
106
		$this->insertAdjacentNode($where, $fragment);
107
	}
108
109
	/**
110
	* Remove this element from the document
111
	*
112
	* @return self This element
113
	*/
114
	public function remove(): self
115
	{
116
		return $this->parentNode->removeChild($this);
117
	}
118
119
	/**
120
	* Replace this element with given element
121
	*
122
	* @param  self $element Replacement element
123
	* @return self          This element
124
	*/
125
	public function replace(self $element): self
126
	{
127
		return $this->parentNode->replaceChild($element, $this);
128
	}
129
130
	/**
131
	* Add namespace declarations that may be missing in given XML
132
	*
133
	* @param  string $xml Original XML
134
	* @return string      Modified XML
135
	*/
136
	protected function addMissingNamespaceDeclarations(string $xml): string
137
	{
138
		preg_match_all('(xmlns:\\K[-\\w]++(?==))', $xml, $m);
139
		$prefixes = array_flip($m[0]);
140
141
		return preg_replace_callback(
142
			'(<([-\\w]++):[^>]++>)',
143
			function ($m) use ($prefixes)
144
			{
145
				$xml    = $m[0];
146
				$prefix = $m[1];
147
				if (isset($prefixes[$prefix]))
148
				{
149
					return $xml;
150
				}
151
152
				$nsURI         = $this->lookupNamespaceURI($prefix);
153
				$nsDeclaration = ' xmlns:' . $prefix . '="' . htmlspecialchars($nsURI, ENT_XML1) . '"';
154
155
				$pos = ($xml[-2] === '/') ? -2 : -1;
156
157
				return substr($xml, 0, $pos) . $nsDeclaration . substr($xml, $pos);
158
			},
159
			$xml
160
		);
161
	}
162
163
	/**
164
	* Insert given node relative to this element's position
165
	*
166
	* @param  string  $where One of 'beforebegin', 'afterbegin', 'beforeend', 'afterend'
167
	* @param  DOMNode $node
168
	* @return void
169
	*/
170
	protected function insertAdjacentNode(string $where, DOMNode $node): void
171
	{
172
		if ($where === 'beforebegin')
173
		{
174
			$this->parentNode->insertBefore($node, $this);
175
		}
176
		elseif ($where === 'beforeend')
177
		{
178
			$this->appendChild($node);
179
		}
180
		elseif ($where === 'afterend')
181
		{
182
			$this->parentNode->insertBefore($node, $this->nextSibling);
183
		}
184
		elseif ($where === 'afterbegin')
185
		{
186
			$this->insertBefore($node, $this->firstChild);
187
		}
188
		else
189
		{
190
			throw new InvalidArgumentException;
191
		}
192
	}
193
}