Passed
Push — master ( d07226...f7c499 )
by smiley
02:03
created

Document::getElementsByClassName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 1
c 1
b 0
f 0
dl 0
loc 2
ccs 0
cts 0
cp 0
rs 10
cc 1
nc 1
nop 1
crap 2
1
<?php
2
/**
3
 * Class Document
4
 *
5
 * @filesource   Document.php
6
 * @created      05.05.2017
7
 * @package      chillerlan\PrototypeDOM
8
 * @author       Smiley <[email protected]>
9
 * @copyright    2017 Smiley
10
 * @license      MIT
11
 */
12
13
namespace chillerlan\PrototypeDOM;
14
15
use chillerlan\PrototypeDOM\Node\{
16
	Attr, CdataSection, CharacterData, Comment, DocumentFragment, DocumentType, Element, Entity,
17
	EntityReference, Node, Notation, ProcessingInstruction, PrototypeHTMLElement, PrototypeNode, Text,
18
};
19
use DOMDocument, DOMException, DOMNode, DOMNodeList, DOMXPath;
20
use Symfony\Component\CssSelector\CssSelectorConverter;
21
22
use function count, in_array, is_file, is_iterable, is_readable, is_string;
23
24
use const LIBXML_COMPACT, LIBXML_HTML_NODEFDTD, LIBXML_HTML_NOIMPLIED, LIBXML_NOERROR, LIBXML_NONET, XML_ELEMENT_NODE;
25
26
/**
27
 *
28
 */
29
class Document extends DOMDocument{
30
31
	protected const NODE_CLASSES = [
32
		'DOMAttr'                  => Attr::class,
33
		'DOMCdataSection'          => CdataSection::class,
34
		'DOMCharacterData'         => CharacterData::class,
35
		'DOMComment'               => Comment::class,
36
		'DOMDocumentFragment'      => DocumentFragment::class,
37
		'DOMDocumentType'          => DocumentType::class,
38
		'DOMElement'               => Element::class,
39
		'DOMEntity'                => Entity::class,
40
		'DOMEntityReference'       => EntityReference::class,
41
		'DOMNode'                  => Node::class,
42
		'DOMNotation'              => Notation::class,
43
		'DOMProcessingInstruction' => ProcessingInstruction::class,
44 42
		'DOMText'                  => Text::class,
45 42
	];
46
47 42
	protected const LOAD_OPTIONS = LIBXML_COMPACT | LIBXML_NONET | LIBXML_HTML_NODEFDTD | LIBXML_HTML_NOIMPLIED | LIBXML_NOERROR;
48 42
49
	protected CssSelectorConverter $cssSelectorConverter;
50
51 42
	/**
52 2
	 * Document constructor.
53
	 *
54
	 * @param string|\DOMNodeList|\chillerlan\PrototypeDOM\NodeList $content
55 42
	 * @param bool|null                                             $xml
56
	 * @param string|null                                           $version
57
	 * @param string|null                                           $encoding
58
	 */
59
	public function __construct($content = null, bool $xml = null, string $version = null, string $encoding = null){
60
		parent::__construct($version ?? '1.0', $encoding ?? 'UTF-8');
61
62 1
		foreach($this::NODE_CLASSES as $baseClass => $extendedClass){
63 1
			$this->registerNodeClass($baseClass, $extendedClass);
64
		}
65
66 1
		if($content !== null){
67 1
			$this->loadDocument($content, $xml);
68
		}
69 1
70 1
		$this->cssSelectorConverter = new CssSelectorConverter;
71
	}
72
73 1
	/*********
74 1
	 * magic *
75
	 *********/
76 1
77
	/**
78
	 * @return string|null
79
	 */
80 1
	public function getTitle():?string{
81
		return $this->select(['head > title'])->item(0)->nodeValue ?? null;
82 1
	}
83
84
	/**
85
	 * @throws \DOMException
86
	 */
87
	public function setTitle(string $title):void{
88
		$currentTitle = $this->select(['head > title'])->item(0);
89
90 2
		if($currentTitle instanceof Element){
91
			$currentTitle->update($title);
92
93 2
			return;
94 1
		}
95 1
96
		$head         = $this->select(['head'])->item(0);
97
		$currentTitle = $this->newElement('title')->update($title);
98
99
		if(!$head){
100 1
			$html = $this->select(['html'])->first();
101 1
102
			if(!$html instanceof PrototypeHTMLElement){
103 1
				throw new DOMException('html header missing');
104 1
			}
105 1
106
			$head = $this->newElement('head');
107 1
			$html->insert_top($head);
108
		}
109
110
		$head->insert($currentTitle);
0 ignored issues
show
Bug introduced by
The method insert() does not exist on DOMNode. Did you maybe mean insertBefore()? ( Ignorable by Annotation )

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

110
		$head->/** @scrutinizer ignore-call */ 
111
         insert($currentTitle);

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...
111
	}
112
113
	/**
114
	 * @param string|\DOMNodeList|\chillerlan\PrototypeDOM\NodeList $content
115
	 * @param bool|null $xml
116 8
	 *
117
	 * @return \chillerlan\PrototypeDOM\Document
118
	 * @throws \DOMException
119 8
	 */
120 8
	public function loadDocument($content, bool $xml = null):Document{
121 7
122 7
		if($content instanceof NodeList){
123 7
			return $this->insertNodeList($content);
124
		}
125
126
		if($content instanceof DOMNodeList){
127
			return $this->insertNodeList(new NodeList($content));
128
		}
129
130
		if(!is_string($content)){
0 ignored issues
show
introduced by
The condition is_string($content) is always true.
Loading history...
131
			throw new DOMException('invalid document content');
132
		}
133
134
		if(is_file($content) && is_readable($content)){
135 8
			return $this->loadDocumentFile($content, $xml);
136 8
		}
137 8
138
		return $this->loadDocumentString($content, $xml);
139 8
	}
140
141
	/**
142
	 * @throws \DOMException
143
	 */
144
	public function loadDocumentFile(string $file, bool $xml = null, int $options = null):Document{
145
		$options = $options ?? $this::LOAD_OPTIONS;
146
147 3
		$result = $xml === true
148 3
			? $this->load($file, $options)
149
			: $this->loadHTMLFile($file, $options);
150 3
151 3
		if($result === false){
0 ignored issues
show
introduced by
The condition $result === false is always false.
Loading history...
152
			throw new DOMException('failed to load document from file: '.$file); // @codeCoverageIgnore
153
		}
154 3
155
		return $this;
156
	}
157
158
	/**
159
	 * @throws \DOMException
160
	 */
161
	public function loadDocumentString(string $documentSource, bool $xml = null, int $options = null):Document{
162
		$options = $options ?? $this::LOAD_OPTIONS;
163
164
		$result = $xml === true
165
			? $this->loadXML($documentSource, $options)
166
			: $this->loadHTML($documentSource, $options);
167
168 41
		if($result === false){
0 ignored issues
show
introduced by
The condition $result === false is always false.
Loading history...
169 41
			throw new DOMException('failed to load document from string'); // @codeCoverageIgnore
170
		}
171
172
		return $this;
173
	}
174
175
	/**
176
	 * @throws \DOMException
177
	 */
178 41
	public function toNodeList($content):NodeList{
179 41
180
		if($content instanceof NodeList){
181
			return $content;
182
		}
183
184
		if($content instanceof DOMNode || $content instanceof PrototypeNode){
185
			return new NodeList([$content]);
186
		}
187
188
		if($content instanceof DOMNodeList || is_iterable($content)){
189 41
			return new NodeList($content);
190 41
		}
191
192
		if(is_string($content)){
193
			$document = new self;
194
			$document->loadHTML('<html lang="en"><body id="-import-content">'.$content.'</body></html>');
195
196
			return $document->toNodeList($document->getElementById('-import-content')->childNodes);
197
		}
198
199
		throw new DOMException('invalid content');
200 1
	}
201 1
202
	/***********
203 1
	 * generic *
204
	 ***********/
205 1
206 1
	/**
207
	 *
208
	 */
209
	public function selector2xpath(string $selector, string $axis = null):string{
210
		return $this->cssSelectorConverter->toXPath($selector, $axis ?? '//');
211 1
	}
212
213
	/**
214
	 *
215
	 */
216
	public function query(string $xpath, DOMNode $contextNode = null):?NodeList{
217
		$q = (new DOMXPath($this))->query($xpath, $contextNode);
218
219 2
		return $q !== false ? new NodeList($q) : null;
220
	}
221
222 2
	/**
223 2
	 *
224
	 */
225
	public function querySelectorAll(string $selector, DOMNode $contextNode = null, string $axis = null):?NodeList{
226 2
		return $this->query($this->cssSelectorConverter->toXPath($selector, $axis ?? 'descendant-or-self::'), $contextNode);
227
	}
228
229
	/**
230
	 *
231
	 */
232
	public function removeElementsBySelector(array $selectors, DOMNode $contextNode = null, string $axis = null):Document{
233
		$nodes = $this->select($selectors, $contextNode, $axis ?? 'descendant-or-self::');
234
235
		if(count($nodes) > 0){
236
237
			foreach($nodes as $node){
238
				$node->removeNode();
0 ignored issues
show
Bug introduced by
The method removeNode() 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

238
				$node->/** @scrutinizer ignore-call */ 
239
           removeNode();

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...
239
			}
240
241 8
		}
242 8
243 2
		return $this;
244 8
	}
245
246
	/**
247
	 *
248
	 */
249
	public function insertNodeList(NodeList $nodeList):Document{
250
251
		foreach($nodeList as $node){
252
			$this->appendChild($this->importNode($node->cloneNode(true), true));
253
		}
254
255
		return $this;
256
	}
257 41
258
	/**
259 41
	 *
260 41
	 */
261
	public function getElementsByClassName(string $className):NodeList{
262
		return $this->select(['.'.$className]);
263 41
	}
264 2
265
	/*************
266
	 * prototype *
267 41
	 *************/
268
269 41
	/**
270
	 * @link http://api.prototypejs.org/dom/Element/inspect/
271 41
	 */
272
	public function inspect(DOMNode $context = null, bool $xml = null):string{
273 41
274 41
		if($xml === true){
275
			return $this->saveXML($context);
276
		}
277
278
		return $this->saveHTML($context);
279
	}
280
281 41
	/**
282
	 * @link http://api.prototypejs.org/dom/Element/select/
283
	 * @see https://secure.php.net/manual/dom.constants.php
284
	 */
285
	public function select(array $selectors = null, DOMNode $contextNode = null, string $axis = null, int $nodeType = null):NodeList{
286
		$nodeType = $nodeType ?? XML_ELEMENT_NODE;
287
		$elements = new NodeList;
288
289
		foreach($selectors ?? ['*'] as $selector){
290
291
			if(!is_string($selector)){
292
				continue;
293
			}
294 3
295 3
			foreach($this->querySelectorAll($selector, $contextNode, $axis ?? 'descendant-or-self::') as $element){
296
297 3
				if($element->nodeType === $nodeType){
298
					$elements[] = $element;
299 3
				}
300
301 3
			}
302 3
303
		}
304
305 3
		return $elements;
306 2
	}
307
308
	/**
309
	 * @see http://api.prototypejs.org/dom/Element/recursivelyCollect/
310
	 * @see https://secure.php.net/manual/dom.constants.php
311
	 */
312
	public function recursivelyCollect(DOMNode $element, string $property, int $maxLength = null, int $nodeType = null):NodeList{
313 3
		$nodeType  = $nodeType ?? XML_ELEMENT_NODE;
314
		$maxLength = $maxLength ?? -1;
315
		$nodes     = new NodeList;
316
317
		if(in_array($property, ['parentNode', 'previousSibling', 'nextSibling'])){
318
319
			while($element = $element->{$property}){
320
321
				if($element->nodeType === $nodeType){
322
					$nodes[] = $element;
323
				}
324
325 4
				if(count($nodes) === $maxLength){
326
					break;
327 4
				}
328
329
			}
330 4
331
		}
332 4
333 4
		return $nodes;
334
	}
335
336 4
	/**
337
	 * @see https://secure.php.net/manual/dom.constants.php
338
	 */
339
	public function recursivelyFind(PrototypeNode $element, string $property = null, string $selector = null, int $index = null, int $nodeType = null):?DOMNode{
340
		$nodeType = $nodeType ?? XML_ELEMENT_NODE;
341 4
		$index    = $index ?? 0;
342
343
		if(in_array($property, ['parentNode', 'previousSibling', 'nextSibling'])){
344
345
			while($element = $element->{$property}){
346
347
				if($element->nodeType !== $nodeType || $selector !== null && !$element->match($selector) || --$index >= 0){
348
					continue;
349
				}
350
351
				return $element;
352 4
			}
353
354 4
		}
355
356 4
		return null;
357 4
	}
358
359
	/**
360
	 * @link http://api.prototypejs.org/dom/Element/match/
361
	 */
362 4
	public function match(DOMNode $element, string $selector):bool{
363
364
		foreach($this->select([$selector]) as $match){
365
366
			if($element->isSameNode($match)){
0 ignored issues
show
Bug introduced by
It seems like $match can also be of type null; however, parameter $otherNode of DOMNode::isSameNode() does only seem to accept DOMNode, maybe add an additional type check? ( Ignorable by Annotation )

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

366
			if($element->isSameNode(/** @scrutinizer ignore-type */ $match)){
Loading history...
367
				return true;
368
			}
369
370
		}
371
372
		return false;
373 6
	}
374
375 6
	/**
376
	 * @link http://api.prototypejs.org/dom/Element/new/
377 6
	 */
378 4
	public function newElement(string $tag, array $attributes = null):PrototypeHTMLElement{
379
		/** @var \chillerlan\PrototypeDOM\Node\PrototypeHTMLElement $element */
380
		$element = $this->createElement($tag);
381 6
382
		if($attributes !== null){
383
			$element->setAttributes($attributes);
0 ignored issues
show
Bug introduced by
The method setAttributes() does not exist on chillerlan\PrototypeDOM\Node\PrototypeHTMLElement. Since it exists in all sub-types, consider adding an abstract or default implementation to chillerlan\PrototypeDOM\Node\PrototypeHTMLElement. ( Ignorable by Annotation )

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

383
			$element->/** @scrutinizer ignore-call */ 
384
             setAttributes($attributes);
Loading history...
384
		}
385
386
		return $element;
387
	}
388
389
	/**
390
	 * @inheritDoc
391
	 *
392
	 * @return \chillerlan\PrototypeDOM\Node\PrototypeHTMLElement|\DOMNode|null
393
	 * @throws \DOMException
394
	 */
395
	public function getElementById($elementId):?DOMNode{
396
397
		if(!is_string($elementId)){
398
			throw new DOMException('invalid element id');
399
		}
400
401
		return $this->select(['#'.$elementId])[0] ?? null;
402
	}
403
404
}
405