Completed
Push — master ( 14b45d...d8ec9e )
by smiley
04:40
created

Document::inspect()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 5
ccs 4
cts 4
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 4
nc 2
nop 2
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 DOMDocument, DOMNode, DOMNodeList, DOMXPath;
16
use chillerlan\PrototypeDOM\Node\{Element, PrototypeNode};
17
use chillerlan\PrototypeDOM\Traits\Magic;
18
use Symfony\Component\CssSelector\CssSelectorConverter;
19
20
/**
21
 * @property string $title
22
 */
23
class Document extends DOMDocument{
24
	use Magic;
25
26
	const NODE_CLASSES = [
27
		'Attr',
28
		'CharacterData',
29
		'Comment',
30
		'DocumentFragment',
31
		'DocumentType',
32
		'Element',
33
		'Text',
34
	];
35
36
	/**
37
	 * Document constructor.
38
	 *
39
	 * @param string      $content
40
	 * @param bool        $xml
41
	 * @param string|null $version
42
	 * @param string|null $encoding
43
	 */
44 42
	public function __construct($content = null, $xml = false, $version = '1.0', $encoding = 'UTF-8'){
45 42
		parent::__construct($version, $encoding);
46
47 42
		foreach(self::NODE_CLASSES as $nodeClass){
48 42
			$this->registerNodeClass('DOM'.$nodeClass, __NAMESPACE__.'\\Node\\'.$nodeClass);
49
		}
50
51 42
		if(!is_null($content)){
52 2
			$this->_loadDocument($content, $xml);
53
		}
54
55 42
	}
56
57
58
	/*********
59
	 * magic *
60
	 *********/
61
62 1
	public function magic_get_title(){
63 1
		return $this->select('head > title')->item(0)->nodeValue ?? null;
64
	}
65
66 1
	public function magic_set_title(string $title){
67 1
		$currentTitle = $this->select('head > title')->item(0);
68
69 1
		if($currentTitle instanceof Element){
70 1
			$currentTitle->update($title);
71
		}
72
		else{
73 1
			$currentTitle = $this->newElement('title')->update($title);
74 1
			$this->select('head')->item(0)->insert($currentTitle);
75
		}
76 1
	}
77
78
79
	/********
80
	 * ugly *
81
	 ********/
82
83 2
	public function _loadDocument($content, $xml){
84
85
		switch(true){
86 2
			case $content instanceof NodeList   : return $this->insertNodeList($content);
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
87
			case $content instanceof DOMNodeList: return $this->insertNodeList(new NodeList($content));
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
88 1
			case is_string($content)            : return $this->_loadDocumentString($content, $xml);
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a CASE statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in case statements.

switch ($selector) {
    case "A": //right
        doSomething();
        break;
    case "B" : //wrong
        doSomethingElse();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
89
			default: return $this;
0 ignored issues
show
Coding Style introduced by
The default body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a default statement must start on the line immediately following the statement.

switch ($expr) {
    default:
        doSomething(); //right
        break;
}


switch ($expr) {
    default:

        doSomething(); //wrong
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
Coding Style introduced by
Terminating statement must be on a line by itself

As per the PSR-2 coding standard, the break (or other terminating) statement must be on a line of its own.

switch ($expr) {
     case "A":
         doSomething();
         break; //wrong
     case "B":
         doSomething();
         break; //right
     case "C:":
         doSomething();
         return true; //right
 }

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
90
		}
91
	}
92
93 1
	public function _loadDocumentString(string $documentSource, bool $xml = false){
94 1
		$options = LIBXML_COMPACT|LIBXML_NONET;
95
96 1
		$xml
97 1
			? $this->loadXML($documentSource, $options)
98 1
			: $this->loadHTML($documentSource, $options);
99
100 1
		return $this;
101
	}
102
103
	/**
104
	 * @param mixed $content
105
	 *
106
	 * @return \chillerlan\PrototypeDOM\NodeList
107
	 * @throws \Exception
108
	 */
109 8
	public function _toNodeList($content):NodeList{
110
111
		switch(true){
112 8
			case $content instanceof NodeList   : return $content;
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a CASE statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in case statements.

switch ($selector) {
    case "A": //right
        doSomething();
        break;
    case "B" : //wrong
        doSomethingElse();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
Coding Style introduced by
Terminating statement must be on a line by itself

As per the PSR-2 coding standard, the break (or other terminating) statement must be on a line of its own.

switch ($expr) {
     case "A":
         doSomething();
         break; //wrong
     case "B":
         doSomething();
         break; //right
     case "C:":
         doSomething();
         return true; //right
 }

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
113 8
			case $content instanceof DOMNodeList: return new NodeList($content);
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
Coding Style introduced by
Terminating statement must be on a line by itself

As per the PSR-2 coding standard, the break (or other terminating) statement must be on a line of its own.

switch ($expr) {
     case "A":
         doSomething();
         break; //wrong
     case "B":
         doSomething();
         break; //right
     case "C:":
         doSomething();
         return true; //right
 }

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
114 3
			case $content instanceof DOMNode    : return $this->_arrayToNodeList([$content]);
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a CASE statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in case statements.

switch ($selector) {
    case "A": //right
        doSomething();
        break;
    case "B" : //wrong
        doSomethingElse();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
Coding Style introduced by
Terminating statement must be on a line by itself

As per the PSR-2 coding standard, the break (or other terminating) statement must be on a line of its own.

switch ($expr) {
     case "A":
         doSomething();
         break; //wrong
     case "B":
         doSomething();
         break; //right
     case "C:":
         doSomething();
         return true; //right
 }

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
115 7
			case is_array($content)             : return $this->_arrayToNodeList($content);
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a CASE statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in case statements.

switch ($selector) {
    case "A": //right
        doSomething();
        break;
    case "B" : //wrong
        doSomethingElse();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
Coding Style introduced by
Terminating statement must be on a line by itself

As per the PSR-2 coding standard, the break (or other terminating) statement must be on a line of its own.

switch ($expr) {
     case "A":
         doSomething();
         break; //wrong
     case "B":
         doSomething();
         break; //right
     case "C:":
         doSomething();
         return true; //right
 }

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
116 7
			case is_string($content)            : return $this->_HTMLFragmentToNodeList($content);
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a CASE statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in case statements.

switch ($selector) {
    case "A": //right
        doSomething();
        break;
    case "B" : //wrong
        doSomethingElse();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
Coding Style introduced by
Terminating statement must be on a line by itself

As per the PSR-2 coding standard, the break (or other terminating) statement must be on a line of its own.

switch ($expr) {
     case "A":
         doSomething();
         break; //wrong
     case "B":
         doSomething();
         break; //right
     case "C:":
         doSomething();
         return true; //right
 }

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
117
			default:
118
				throw new \Exception('invalid content'); // @codeCoverageIgnore
119
		}
120
121
	}
122
123
	/**
124
	 * @param string $content
125
	 *
126
	 * @return \chillerlan\PrototypeDOM\NodeList
127
	 */
128 8
	public function _HTMLFragmentToNodeList(string $content):NodeList{
129 8
		$document = new Document;
130 8
		$document->loadHTML('<html><body id="-import-content">'.$content.'</body></html>');
131
132 8
		return $document->_toNodeList($document->getElementById('-import-content')->childNodes);
133
	}
134
135
	/**
136
	 * @param array $array
137
	 *
138
	 * @return \chillerlan\PrototypeDOM\NodeList
139
	 */
140 3
	public function _arrayToNodeList(array $array):NodeList{
141 3
		$nodelist = new NodeList;
142
143 3
		foreach($array as $node){
144 3
			$nodelist[] = $node;
145
		}
146
147 3
		return $nodelist;
148
	}
149
150
151
	/***********
152
	 * generic *
153
	 ***********/
154
155
	/**
156
	 * @param string $selector
157
	 * @param string $axis
158
	 *
159
	 * @return string
160
	 */
161 41
	public function selector2xpath(string $selector, string $axis = '//'):string{
162 41
		return (new CssSelectorConverter)->toXPath($selector, $axis);
163
	}
164
165
	/**
166
	 * @param string        $xpath
167
	 * @param \DOMNode|null $contextNode
168
	 *
169
	 * @return \chillerlan\PrototypeDOM\NodeList
170
	 */
171 41
	public function query(string $xpath, DOMNode $contextNode = null):NodeList{
172 41
		return new NodeList((new DOMXPath($this))->query($xpath, $contextNode));
173
	}
174
175
	/**
176
	 * @param string        $selector
177
	 * @param \DOMNode|null $contextNode
178
	 * @param string        $axis
179
	 *
180
	 * @return \chillerlan\PrototypeDOM\NodeList
181
	 */
182 41
	public function querySelectorAll(string $selector, DOMNode $contextNode = null, string $axis = 'descendant-or-self::'):NodeList{
183 41
		return $this->query($this->selector2xpath($selector, $axis), $contextNode);
184
	}
185
186
	/**
187
	 * @param string|array  $selectors
188
	 * @param \DOMNode|null $contextNode
189
	 * @param string        $axis
190
	 *
191
	 * @return \chillerlan\PrototypeDOM\Document
192
	 */
193 1
	public function removeElementsBySelector($selectors, DOMNode $contextNode = null, string $axis = 'descendant-or-self::'):Document{
194 1
		$nodes = $this->select($selectors, $contextNode, $axis);
195
196 1
		if(count($nodes) > 0){
197
			/** @var \chillerlan\PrototypeDOM\Node\Element $node */
198 1
			foreach($nodes as $node){
199 1
				$node->remove();
200
			}
201
202
		}
203
204 1
		return $this;
205
	}
206
207
	/**
208
	 * @param \chillerlan\PrototypeDOM\NodeList $nodeList
209
	 *
210
	 * @return \chillerlan\PrototypeDOM\Document
211
	 */
212 2
	public function insertNodeList(NodeList $nodeList):Document{
213
214
		/** @var \DOMNode $node */
215 2
		foreach($nodeList as $node){
216 2
			$this->appendChild($this->importNode($node->cloneNode(true), true));
217
		}
218
219 2
		return $this;
220
	}
221
222
	/*************
223
	 * prototype *
224
	 *************/
225
226
	/**
227
	 * @link http://api.prototypejs.org/dom/Element/inspect/
228
	 *
229
	 * @param \DOMNode|null $context
230
	 * @param bool          $xml
231
	 *
232
	 * @return string
233
	 */
234 8
	public function inspect(DOMNode $context = null, $xml = false):string{
235 8
		return $xml
236 2
			? $this->saveXML($context)
237 8
			: $this->saveHTML($context);
238
	}
239
240
	/**
241
	 * @link http://api.prototypejs.org/dom/Element/select/
242
	 *
243
	 * @param string|array  $selectors
244
	 * @param \DOMNode|null $contextNode
245
	 * @param string        $axis
246
	 * @param int           $nodeType
247
	 *
248
	 * @return \chillerlan\PrototypeDOM\NodeList
249
	 */
250 41
	public function select($selectors = null, DOMNode $contextNode = null, string $axis = 'descendant-or-self::', int $nodeType = XML_ELEMENT_NODE):NodeList{
251
252 41
		if(is_string($selectors)){
253 41
			$selectors = [trim($selectors)];
254
		}
255
256 41
		if(!is_array($selectors) || empty($selectors)){
257 2
			$selectors = ['*'];
258
		}
259
260 41
		$elements = new NodeList;
261
262 41
		foreach($selectors as $selector){
263
264 41
			foreach($this->querySelectorAll($selector, $contextNode, $axis) as $element){
265
266 41
				if($element->nodeType === $nodeType){
267 41
					$elements[] = $element;
268
				}
269
270
			}
271
272
		}
273
274 41
		return $elements;
275
	}
276
277
	/**
278
	 * @link http://api.prototypejs.org/dom/Element/recursivelyCollect/
279
	 *
280
	 * @param \chillerlan\PrototypeDOM\Node\PrototypeNode $element
281
	 * @param string                                      $property
282
	 * @param int                                         $maxLength
283
	 * @param int                                         $nodeType
284
	 *
285
	 * @return \chillerlan\PrototypeDOM\NodeList
286
	 */
287 3
	public function recursivelyCollect(PrototypeNode $element, string $property, int $maxLength = -1, int $nodeType = XML_ELEMENT_NODE):NodeList{
288 3
		$nodes = new NodeList;
289
290 3
		if(in_array($property, ['parentNode', 'previousSibling', 'nextSibling'])){
291
292 3
			while($element = $element->{$property}){
293
294 3
				if($element->nodeType === $nodeType){
295 3
					$nodes[] = $element;
296
				}
297
298 3
				if(count($nodes) === $maxLength){
299 2
					break;
300
				}
301
302
			}
303
304
		}
305
306 3
		return $nodes;
307
	}
308
309
	/**
310
	 * @param \chillerlan\PrototypeDOM\Node\PrototypeNode $element
311
	 * @param string                                      $property
312
	 * @param string|null                                 $selector
313
	 * @param int                                         $index
314
	 * @param int                                         $nodeType
315
	 *
316
	 * @return \chillerlan\PrototypeDOM\Node\PrototypeElement|null
317
	 */
318 4
	public function _recursivelyFind(PrototypeNode $element, string $property, string $selector = null, int $index = 0, int $nodeType = XML_ELEMENT_NODE){
319
320 4
		if(in_array($property, ['parentNode', 'previousSibling', 'nextSibling'])){
321
322
			/** @var \chillerlan\PrototypeDOM\Node\Element $element */
323 4
			while($element = $element->{$property}){
324
325 4
				if($element->nodeType !== $nodeType || !is_null($selector) && !$element->match($selector) || --$index >= 0){
326 4
					continue;
327
				}
328
329 4
				return $element;
330
			}
331
332
		}
333
334 4
		return null;
335
	}
336
337
	/**
338
	 * @link http://api.prototypejs.org/dom/Element/match/
339
	 *
340
	 * @param \chillerlan\PrototypeDOM\Node\PrototypeNode $element
341
	 * @param string                                      $selector
342
	 *
343
	 * @return bool
344
	 */
345 4
	public function match(PrototypeNode $element, string $selector):bool{
346
347 4
		foreach($this->select($selector) as $match){
348
349 4
			if($element->isSameNode($match)){
350 4
				return true;
351
			}
352
353
		}
354
355 4
		return false;
356
	}
357
358
	/**
359
	 * @link http://api.prototypejs.org/dom/Element/new/
360
	 *
361
	 * @param string     $tag
362
	 * @param array|null $attributes
363
	 *
364
	 * @return \chillerlan\PrototypeDOM\Node\Element
365
	 */
366 6
	public function newElement(string $tag, array $attributes = null):Element{
367
		/** @var \chillerlan\PrototypeDOM\Node\Element $element */
368 6
		$element = $this->createElement($tag);
369
370 6
		if(!is_null($attributes)){
371 4
			$element->setAttributes($attributes);
372
		}
373
374 6
		return $element;
375
	}
376
377
}
378