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); |
|
|
|
|
87
|
|
|
case $content instanceof DOMNodeList: return $this->insertNodeList(new NodeList($content)); |
|
|
|
|
88
|
1 |
|
case is_string($content) : return $this->_loadDocumentString($content, $xml); |
|
|
|
|
89
|
|
|
default: return $this; |
|
|
|
|
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; |
|
|
|
|
113
|
8 |
|
case $content instanceof DOMNodeList: return new NodeList($content); |
|
|
|
|
114
|
3 |
|
case $content instanceof DOMNode : return $this->_arrayToNodeList([$content]); |
|
|
|
|
115
|
7 |
|
case is_array($content) : return $this->_arrayToNodeList($content); |
|
|
|
|
116
|
7 |
|
case is_string($content) : return $this->_HTMLFragmentToNodeList($content); |
|
|
|
|
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
|
|
|
|
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.
To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.