Completed
Branch minor/InsertElement (b913e9)
by Josh
01:44
created

Element::insertText()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 2
dl 0
loc 6
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 DOMText;
15
use InvalidArgumentException;
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
* @method self prependXslValueOf(string $select)
65
* @method self prependXslValueOfSibling(string $select)
66
* @method self prependXslVariable(string $name, string $select = null)
67
* @method self prependXslVariableSibling(string $name, string $select = null)
68
* @method self prependXslWhen(string $test, string $text = '')
69
* @method self prependXslWhenSibling(string $test, string $text = '')
70
*/
71
class Element extends DOMElement
72
{
73
	public function __call(string $name, array $arguments)
74
	{
75
		$name      = strtolower($name);
76
		$positions = [
77
			'append'         => 'beforeend',
78
			'appendsibling'  => 'afterend',
79
			'prepend'        => 'afterbegin',
80
			'prependsibling' => 'beforebegin'
81
		];
82
83
		if (preg_match('(^(append|prepend)(xsl\\w+?)(sibling|)$)', $name, $m))
84
		{
85
			$localName = $m[2];
86
			$where     = $positions[$m[1] . $m[3]];
87
88
			return $this->insertXslElement($localName, $where, $arguments);
89
		}
90
		if (preg_match('(^(append|prepend)element(sibling|)$)', $name, $m))
91
		{
92
			$nodeName = $arguments[0];
93
			$text     = $arguments[1] ?? '';
94
			$where    = $positions[$m[1] . $m[2]];
95
96
			return $this->insertElement($nodeName, $where, $text);
97
		}
98
		if (preg_match('(^(append|prepend)text(sibling|)$)', $name, $m))
99
		{
100
			$text  = $arguments[0];
101
			$where = $positions[$m[1] . $m[2]];
102
103
			return $this->insertText($where, $text);
104
		}
105
106
		throw new BadMethodCallException;
107
	}
108
109
	/**
110
	* Evaluate and return the result of a given XPath expression using this element as context node
111
	*
112
	* @param  string  $expr XPath expression
113
	* @return mixed
114
	*/
115
	public function evaluate(string $expr)
116
	{
117
		return $this->ownerDocument->evaluate($expr, $this);
118
	}
119
120
	/**
121
	* Evaluate and return the first element of a given XPath query using this element as context node
122
	*
123
	* @param  string       $expr XPath expression
124
	* @return DOMNode|null
125
	*/
126
	public function firstOf(string $expr): ?DOMNode
127
	{
128
		return $this->ownerDocument->firstOf($expr, $this);
129
	}
130
131
	/**
132
	* Insert given element relative to this element's position
133
	*
134
	* @param  string $where   One of 'beforebegin', 'afterbegin', 'beforeend', 'afterend'
135
	* @param  self   $element
136
	* @return self
137
	*/
138
	public function insertAdjacentElement(string $where, self $element): self
139
	{
140
		$this->insertAdjacentNode($where, $element);
141
142
		return $element;
143
	}
144
145
	/**
146
	* Insert given text relative to this element's position
147
	*
148
	* @param  string $where One of 'beforebegin', 'afterbegin', 'beforeend', 'afterend'
149
	* @param  string $text
150
	* @return void
151
	*/
152
	public function insertAdjacentText(string $where, string $text): void
153
	{
154
		$this->insertText($where, $text);
155
	}
156
157
	/**
158
	* Insert given XML relative to this element's position
159
	*
160
	* @param  string $where One of 'beforebegin', 'afterbegin', 'beforeend', 'afterend'
161
	* @param  string $xml
162
	* @return void
163
	*/
164
	public function insertAdjacentXML(string $where, string $xml): void
165
	{
166
		$fragment = $this->ownerDocument->createDocumentFragment();
167
		$fragment->appendXML($this->addMissingNamespaceDeclarations($xml));
168
169
		$this->insertAdjacentNode($where, $fragment);
170
	}
171
172
	/**
173
	* Evaluate and return the result of a given XPath query using this element as context node
174
	*
175
	* @param  string      $expr XPath expression
176
	* @return DOMNodeList
177
	*/
178
	public function query(string $expr): DOMNodeList
179
	{
180
		return $this->ownerDocument->query($expr, $this);
181
	}
182
183
	/**
184
	* Remove this element from the document
185
	*
186
	* @return void
187
	*/
188
	public function remove(): void
189
	{
190
		$this->parentNode->removeChild($this);
191
	}
192
193
	/**
194
	* Replace this element with given nodes/text
195
	*
196
	* @param  DOMNode|string $nodes
197
	* @return void
198
	*/
199
	public function replaceWith(...$nodes): void
200
	{
201
		foreach ($nodes as $node)
202
		{
203
			if (!($node instanceof DOMNode))
204
			{
205
				$node = $this->ownerDocument->createTextNode((string) $node);
206
			}
207
			$this->parentNode->insertBefore($node, $this);
208
		}
209
		$this->parentNode->removeChild($this);
210
	}
211
212
	/**
213
	* Add namespace declarations that may be missing in given XML
214
	*
215
	* @param  string $xml Original XML
216
	* @return string      Modified XML
217
	*/
218
	protected function addMissingNamespaceDeclarations(string $xml): string
219
	{
220
		preg_match_all('(xmlns:\\K[-\\w]++(?==))', $xml, $m);
221
		$prefixes = array_flip($m[0]);
222
223
		return preg_replace_callback(
224
			'(<([-\\w]++):[^>]*?\\K\\s*/?>)',
225
			function ($m) use ($prefixes)
226
			{
227
				$return = $m[0];
228
				$prefix = $m[1];
229
				if (!isset($prefixes[$prefix]))
230
				{
231
					$nsURI  = $this->lookupNamespaceURI($prefix);
232
					$return = ' xmlns:' . $prefix . '="' . htmlspecialchars($nsURI, ENT_XML1) . '"' . $return;
233
				}
234
235
				return $return;
236
			},
237
			$xml
238
		);
239
	}
240
241
	/**
242
	* Insert given node relative to this element's position
243
	*
244
	* @param  string  $where One of 'beforebegin', 'afterbegin', 'beforeend', 'afterend'
245
	* @param  DOMNode $node
246
	* @return void
247
	*/
248
	protected function insertAdjacentNode(string $where, DOMNode $node): void
249
	{
250
		$where = strtolower($where);
251
		if ($where === 'beforebegin')
252
		{
253
			$this->parentNode->insertBefore($node, $this);
254
		}
255
		elseif ($where === 'beforeend')
256
		{
257
			$this->appendChild($node);
258
		}
259
		elseif ($where === 'afterend')
260
		{
261
			$this->parentNode->insertBefore($node, $this->nextSibling);
262
		}
263
		elseif ($where === 'afterbegin')
264
		{
265
			$this->insertBefore($node, $this->firstChild);
266
		}
267
		else
268
		{
269
			throw new InvalidArgumentException;
270
		}
271
	}
272
273
	/**
274
	* Create and insert an element at given position
275
	*
276
	* @param  string $nodeName Element's nodeName
277
	* @param  string $where    One of 'beforebegin', 'afterbegin', 'beforeend', 'afterend'
278
	* @param  string $text     Text content
279
	* @return self
280
	*/
281
	protected function insertElement(string $nodeName, string $where, string $text): self
282
	{
283
		$text = htmlspecialchars($text, ENT_NOQUOTES);
284
		$pos  = strpos($nodeName, ':');
285
		if ($pos === false)
286
		{
287
			$element = $this->ownerDocument->createElement($nodeName, $text);
288
		}
289
		else
290
		{
291
			$prefix       = substr($nodeName, 0, $pos);
292
			$namespaceURI = $this->ownerDocument->lookupNamespaceURI($prefix);
293
			$element      = $this->ownerDocument->createElementNS($namespaceURI, $nodeName, $text);
294
		}
295
296
		return $this->insertAdjacentElement($where, $element);
297
	}
298
299
	/**
300
	* Insert given text relative to this element's position
301
	*
302
	* @param  string  $where One of 'beforebegin', 'afterbegin', 'beforeend', 'afterend'
303
	* @param  string  $text
304
	* @return DOMText
305
	*/
306
	protected function insertText(string $where, string $text): DOMText
307
	{
308
		$node = $this->ownerDocument->createTextNode($text);
309
		$this->insertAdjacentNode($where, $node);
310
311
		return $node;
312
	}
313
314
	/**
315
	* Create and insert an XSL element at given position
316
	*
317
	* @param  string $localName Element's localName
318
	* @param  string $where     One of 'beforebegin', 'afterbegin', 'beforeend', 'afterend'
319
	* @param  array  $arguments Arguments passed to the Document::create* function
320
	* @return self
321
	*/
322
	protected function insertXslElement(string $localName, string $where, array $arguments): self
323
	{
324
		$callback = [$this->ownerDocument, 'create' . $localName];
325
		if (!is_callable($callback))
326
		{
327
			throw new BadMethodCallException;
328
		}
329
330
		$element = call_user_func_array($callback, $arguments);
331
332
		return $this->insertAdjacentElement($where, $element);
333
	}
334
}