PluginProbe ʕ •ᴥ•ʔ
MailPoet – Newsletters, Email Marketing, and Automation / 5.24.0
MailPoet – Newsletters, Email Marketing, and Automation v5.24.0
5.28.1 5.28.0 5.27.0 5.26.0 5.26.1 5.25.0 5.24.0 4.43.0 4.43.1 4.44.0 4.44.1 4.45.0 4.46.0 4.47.0 4.48.0 4.48.1 4.48.2 4.49.0 4.49.1 4.5.0 4.5.1 4.5.2 4.50.0 4.50.1 4.51.0 4.51.1 4.51.2 4.52.0 4.53.0 4.54.0 4.55.0 4.56.0 4.57.0 4.58.0 4.58.1 4.58.2 4.6.0 4.6.1 4.6.2 4.7.0 4.7.1 4.8.0 4.8.1 4.9.0 5.0.0 5.0.1 5.0.2 5.1.0 5.1.1 5.10.0 5.10.1 5.11.0 5.12.0 5.12.1 5.12.10 5.12.11 5.12.12 5.12.13 5.12.2 5.12.3 5.12.4 5.12.5 5.12.6 5.12.7 5.12.8 5.12.9 5.13.0 5.13.1 5.13.2 5.14.0 5.14.1 5.14.2 5.14.3 5.15.0 5.15.1 5.16.0 5.16.1 5.16.2 5.16.3 5.16.4 5.17.0 5.17.1 5.17.2 5.17.3 5.17.4 5.17.5 5.17.6 5.18.0 5.19.0 5.2.0 5.2.1 5.2.2 5.2.3 5.20.0 5.21.0 5.21.1 5.21.2 5.21.3 5.22.0 5.22.1 5.22.2 5.22.3 5.22.4 5.23.0 5.23.1 5.23.2 5.3.0 5.3.1 5.3.2 5.3.3 5.3.4 5.3.5 5.3.6 5.3.7 5.4.0 5.4.1 5.4.2 5.5.0 5.5.1 5.5.2 5.6.0 5.6.1 5.6.2 5.6.3 5.6.4 5.7.0 5.7.1 5.8.0 5.8.1 5.9.0 3.0.0-beta.15 3.7.1 3.0.0-beta.16 3.7.2 3.0.0-beta.17 3.7.3 3.0.0-beta.18 3.7.4 3.0.0-beta.19 3.7.5 3.0.0-beta.2 3.7.6 3.0.0-beta.20 3.7.8 3.0.0-beta.21 3.70.0 3.0.0-beta.22 3.71.0 3.0.0-beta.23 3.71.1 3.0.0-beta.23.1 3.71.2 3.0.0-beta.23.2 3.71.3 3.0.0-beta.24 3.72.0 3.0.0-beta.25 3.73.0 3.0.0-beta.26 3.73.1 3.0.0-beta.27 3.73.2 3.0.0-beta.28 3.74.0 3.0.0-beta.29 3.74.1 3.0.0-beta.3 3.74.2 3.0.0-beta.30 3.74.3 3.0.0-beta.31 3.75.0 3.0.0-beta.32 3.75.1 3.0.0-beta.33 3.76.0 3.0.0-beta.33.1 3.77.0 3.0.0-beta.34.0.0 3.77.1 3.0.0-beta.36.0.0 3.78.0 3.0.0-beta.36.0.1 3.79.0 3.0.0-beta.36.2.0 3.8 3.0.0-beta.36.3.0 3.8.1 3.0.0-beta.36.3.1 3.8.2 3.0.0-beta.37.0.0 3.8.3 3.0.0-beta.4 3.8.4 3.0.0-beta.5 3.8.5 3.0.0-beta.6 3.8.6 3.0.0-beta.7 3.80.0 3.0.0-beta.7.1 3.81.0 3.0.0-beta.8 3.82.0 3.0.0-beta.9 3.83.0 3.0.0-rc.1.0.0 3.84.0 3.0.0-rc.1.0.1 3.84.1 3.0.0-rc.1.0.2 3.85.0 3.0.0-rc.1.0.3 3.85.1 3.0.0-rc.1.0.4 3.86.0 3.0.0-rc.2.0.0 3.87.0 3.0.0-rc.2.0.1 3.87.1 3.0.0-rc.2.0.2 3.87.2 3.0.0-rc.2.0.3 3.88.0 3.0.1 3.88.1 3.0.2 3.88.2 3.0.3 3.89.0 3.0.4 3.89.1 3.0.5 3.89.2 3.0.6 3.89.3 3.0.7 3.89.4 3.0.8 3.9.0 3.0.9 3.9.1 3.1.0 3.90.0 3.10 3.90.1 3.10.1 3.90.2 3.100.0 3.91.0 3.100.1 3.91.1 3.100.2 3.92.0 3.101.0 3.92.1 3.101.1 3.93.0 3.102.0 3.93.1 3.102.1 3.94.0 3.103.0 3.95.0 3.103.1 3.95.1 3.11.0 3.96.0 3.11.1 3.96.1 3.11.2 3.97.0 3.11.3 3.98.0 3.11.4 3.98.1 3.11.5 3.99.0 3.12.0 3.99.1 3.12.1 4.0.0 3.13.0 4.0.1 3.14.0 4.1.0 3.14.1 4.1.1 3.15.0 4.10.0 3.16.0 4.11.0 3.16.1 4.11.1 3.16.2 4.12.0 3.16.3 4.12.1 3.17.0 4.12.2 3.17.1 4.13.0 3.17.2 4.14.0 3.18.0 4.15.0 3.18.1 4.16.0 3.18.2 4.17.0 3.19.0 4.17.1 3.19.1 4.18.0 3.19.2 4.18.1 3.19.3 4.19.0 3.2.0 4.2.0 3.2.1 4.20.0 3.2.2 4.20.1 3.2.3 4.20.2 3.2.4 4.21.0 3.2.5 4.22.0 3.20.0 4.22.1 3.21.0 4.22.2 3.21.1 4.23.0 3.22.0 4.24.0 3.23.0 4.25.0 3.23.1 4.26.0 3.23.2 4.26.1 3.24.0 4.27.0 3.25.0 4.28.0 3.25.1 4.29.0 3.26.0 4.3.0 3.26.1 4.3.1 3.27.0 4.30.0 3.28.0 4.31.0 3.29.0 4.31.1 3.3.0 4.32.0 3.3.1 4.33.0 3.3.2 4.34.0 3.3.3 4.35.0 3.3.4 4.35.1 3.3.5 4.36.0 3.3.6 4.37.0 3.30.0 4.38.0 3.31.0 4.39.0 3.31.1 4.4.0 3.32.0 4.40.0 3.32.1 4.41.0 3.32.2 4.41.1 3.33.0 4.41.2 3.34.0 4.41.3 3.34.1 4.42.0 3.34.2 4.42.1 3.34.3 3.34.4 3.35.0 3.35.1 3.35.3 3.35.4 3.36.0 3.37.0 3.37.1 3.37.2 3.37.3 3.38.0 3.38.1 3.39.0 3.39.1 3.39.2 3.4.0 3.4.1 3.4.2 3.4.3 3.4.4 3.40.0 3.40.1 3.41.0 3.41.1 3.41.2 3.42.0 3.42.1 3.42.2 3.42.3 3.43.0 3.43.1 3.44.0 3.45.0 3.45.1 3.46.0 3.46.1 3.46.10 3.46.11 3.46.12 3.46.13 3.46.14 3.46.2 3.46.3 3.46.4 3.46.5 3.46.6 3.46.7 3.46.8 3.46.9 3.47.0 3.47.1 3.47.10 3.47.11 3.47.2 3.47.3 3.47.5 3.47.6 3.47.7 3.47.9 3.48.0 3.48.1 3.49.0 3.49.1 3.5.0 3.5.1 3.50.0 3.51.0 3.51.1 3.51.2 3.52.0 3.53.0 3.54.0 3.54.1 3.54.2 3.54.3 3.55.0 3.55.1 3.56.0 3.56.1 3.56.2 3.57.0 3.57.1 3.58.0 3.59.0 3.59.1 3.59.2 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.6.5 3.6.6 3.6.7 3.60.0 3.60.1 3.60.10 3.60.11 3.60.12 3.60.2 3.60.3 3.60.4 3.60.6 3.60.7 3.60.8 3.60.9 3.61.0 3.62.0 3.62.1 3.63.0 3.64.0 3.64.1 3.64.2 3.64.3 3.65.0 trunk 3.65.1 3.0.0 3.66.0 3.0.0-beta.1 3.67.0 3.0.0-beta.10 3.67.1 3.0.0-beta.11 3.68.0 3.0.0-beta.12 3.69.0 3.0.0-beta.13 3.69.1 3.0.0-beta.14 3.7.0
mailpoet / lib-3rd-party / pquery / gan_node_html.php
mailpoet / lib-3rd-party / pquery Last commit date
third_party 1 year ago IQuery.php 3 years ago LICENSE 4 years ago gan_formatter.php 3 years ago gan_node_html.php 4 months ago gan_parser_html.php 3 years ago gan_selector_html.php 4 months ago gan_tokenizer.php 1 year ago gan_xml2array.php 3 years ago ganon.php 4 months ago index.php 3 years ago pQuery.php 3 years ago
gan_node_html.php
2865 lines
1 <?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
2 /**
3 * @author Niels A.D.
4 * @author Todd Burry <todd@vanillaforums.com>
5 * @copyright 2010 Niels A.D., 2014 Todd Burry
6 * @license http://opensource.org/licenses/LGPL-2.1 LGPL-2.1
7 * @package pQuery
8 */
9
10 namespace MailPoetVendor\pQuery;
11
12 if (!defined('ABSPATH')) exit;
13
14
15 /**
16 * Holds (x)html/xml tag information like tag name, attributes,
17 * parent, children, self close, etc.
18 *
19 */
20 class DomNode implements IQuery {
21
22 /**
23 * Element Node, used for regular elements
24 */
25 const NODE_ELEMENT = 0;
26 /**
27 * Text Node
28 */
29 const NODE_TEXT = 1;
30 /**
31 * Comment Node
32 */
33 const NODE_COMMENT = 2;
34 /**
35 * Conditional Node (<![if]> <![endif])
36 */
37 const NODE_CONDITIONAL = 3;
38 /**
39 * CDATA Node (<![CDATA[]]>
40 */
41 const NODE_CDATA = 4;
42 /**
43 * Doctype Node
44 */
45 const NODE_DOCTYPE = 5;
46 /**
47 * XML Node, used for tags that start with ?, like <?xml and <?php
48 */
49 const NODE_XML = 6;
50 /**
51 * ASP Node
52 */
53 const NODE_ASP = 7;
54
55 #php4 Compatibility with PHP4, this gets changed to a regular var in release tool
56 #static $NODE_TYPE = self::NODE_ELEMENT;
57 #php4e
58 #php5
59 /**
60 * Node type of class
61 */
62 const NODE_TYPE = self::NODE_ELEMENT;
63 #php5e
64
65
66 /**
67 * Name of the selector class
68 * @var string
69 * @see select()
70 */
71 var $selectClass = 'MailPoetVendor\\pQuery\\HtmlSelector';
72 /**
73 * Name of the parser class
74 * @var string
75 * @see setOuterText()
76 * @see setInnerText()
77 */
78 var $parserClass = 'MailPoetVendor\\pQuery\\Html5Parser';
79
80 /**
81 * Name of the class used for {@link addChild()}
82 * @var string
83 */
84 var $childClass = __CLASS__;
85 /**
86 * Name of the class used for {@link addText()}
87 * @var string
88 */
89 var $childClass_Text = 'MailPoetVendor\\pQuery\\TextNode';
90 /**
91 * Name of the class used for {@link addComment()}
92 * @var string
93 */
94 var $childClass_Comment = 'MailPoetVendor\\pQuery\\CommentNode';
95 /**
96 * Name of the class used for {@link addContional()}
97 * @var string
98 */
99 var $childClass_Conditional = 'MailPoetVendor\\pQuery\\ConditionalTagNode';
100 /**
101 * Name of the class used for {@link addCDATA()}
102 * @var string
103 */
104 var $childClass_CDATA = 'MailPoetVendor\\pQuery\\CdataNode';
105 /**
106 * Name of the class used for {@link addDoctype()}
107 * @var string
108 */
109 var $childClass_Doctype = 'MailPoetVendor\\pQuery\\DoctypeNode';
110 /**
111 * Name of the class used for {@link addXML()}
112 * @var string
113 */
114 var $childClass_XML = 'MailPoetVendor\\pQuery\\XmlNode';
115 /**
116 * Name of the class used for {@link addASP()}
117 * @var string
118 */
119 var $childClass_ASP = 'MailPoetVendor\\pQuery\\AspEmbeddedNode';
120
121 /**
122 * Parent node, null if none
123 * @var DomNode
124 * @see changeParent()
125 */
126 var $parent = null;
127
128 /**
129 * Attributes of node
130 * @var array
131 * @internal array('attribute' => 'value')
132 * @internal Public for faster access!
133 * @see getAttribute()
134 * @see setAttribute()
135 * @access private
136 */
137 var $attributes = array();
138
139 /**
140 * Namespace info for attributes
141 * @var array
142 * @internal array('tag' => array(array('ns', 'tag', 'ns:tag', index)))
143 * @internal Public for easy outside modifications!
144 * @see findAttribute()
145 * @access private
146 */
147 var $attributes_ns = null;
148
149 /**
150 * Array of child nodes
151 * @var array
152 * @internal Public for faster access!
153 * @see childCount()
154 * @see getChild()
155 * @see addChild()
156 * @see deleteChild()
157 * @access private
158 */
159 var $children = array();
160
161 /**
162 * Full tag name (including namespace)
163 * @var string
164 * @see getTagName()
165 * @see getNamespace()
166 */
167 var $tag = '';
168
169 /**
170 * Namespace info for tag
171 * @var array
172 * @internal array('namespace', 'tag')
173 * @internal Public for easy outside modifications!
174 * @access private
175 */
176 var $tag_ns = null;
177
178 /**
179 * Is node a self closing node? No closing tag if true.
180 * @var bool
181 */
182 var $self_close = false;
183
184 /**
185 * If self close, then this will be used to close the tag
186 * @var string
187 * @see $self_close
188 */
189 var $self_close_str = ' /';
190
191 /**
192 * Use short tags for attributes? If true, then attributes
193 * with values equal to the attribute name will not output
194 * the value, e.g. selected="selected" will be selected.
195 * @var bool
196 */
197 var $attribute_shorttag = true;
198
199 /**
200 * Function map used for the selector filter
201 * @var array
202 * @internal array('root' => 'filter_root') will cause the
203 * selector to call $this->filter_root at :root
204 * @access private
205 */
206 var $filter_map = array(
207 'root' => 'filter_root',
208 'nth-child' => 'filter_nchild',
209 'eq' => 'filter_nchild', //jquery (naming) compatibility
210 'gt' => 'filter_gt',
211 'lt' => 'filter_lt',
212 'nth-last-child' => 'filter_nlastchild',
213 'nth-of-type' => 'filter_ntype',
214 'nth-last-of-type' => 'filter_nlastype',
215 'odd' => 'filter_odd',
216 'even' => 'filter_even',
217 'every' => 'filter_every',
218 'first-child' => 'filter_first',
219 'last-child' => 'filter_last',
220 'first-of-type' => 'filter_firsttype',
221 'last-of-type' => 'filter_lasttype',
222 'only-child' => 'filter_onlychild',
223 'only-of-type' => 'filter_onlytype',
224 'empty' => 'filter_empty',
225 'not-empty' => 'filter_notempty',
226 'has-text' => 'filter_hastext',
227 'no-text' => 'filter_notext',
228 'lang' => 'filter_lang',
229 'contains' => 'filter_contains',
230 'has' => 'filter_has',
231 'not' => 'filter_not',
232 'element' => 'filter_element',
233 'text' => 'filter_text',
234 'comment' => 'filter_comment',
235 'checked' => 'filter_checked',
236 'selected' => 'filter_selected',
237 );
238
239 /**
240 * Class constructor
241 * @param string|array $tag Name of the tag, or array with taginfo (array(
242 * 'tag_name' => 'tag',
243 * 'self_close' => false,
244 * 'attributes' => array('attribute' => 'value')))
245 * @param DomNode $parent Parent of node, null if none
246 */
247 function __construct($tag, $parent) {
248 $this->parent = $parent;
249
250 if (is_string($tag)) {
251 $this->tag = $tag;
252 } else {
253 $this->tag = $tag['tag_name'];
254 $this->self_close = $tag['self_close'];
255 $this->attributes = $tag['attributes'];
256 }
257 }
258
259 #php4 PHP4 class constructor compatibility
260 #function DomNode($tag, $parent) {return $this->__construct($tag, $parent);}
261 #php4e
262
263 /**
264 * Class destructor
265 * @access private
266 */
267 function __destruct() {
268 $this->delete();
269 }
270
271 /**
272 * Class toString, outputs {@link $tag}
273 * @return string
274 * @access private
275 */
276 function __toString() {
277 $string = (($this->tag === '~root~') ? $this->toString(true, true, 1) : $this->tag);
278 // because tburry/pquery contains a bug and replaces the opening non mso condition incorrectly we have to replace the opening tag with correct value
279 return str_replace('<!--[if !mso]><![endif]-->', '<!--[if !mso]><!-- -->', $string);;
280 }
281
282 /**
283 * Class magic get method, outputs {@link getAttribute()}
284 * @return string
285 * @access private
286 */
287 function __get($attribute) {
288 return $this->getAttribute($attribute);
289 }
290
291 /**
292 * Class magic set method, performs {@link setAttribute()}
293 * @access private
294 */
295 function __set($attribute, $value) {
296 $this->setAttribute($attribute, $value);
297 }
298
299 /**
300 * Class magic isset method, returns {@link hasAttribute()}
301 * @return bool
302 * @access private
303 */
304 function __isset($attribute) {
305 return $this->hasAttribute($attribute);
306 }
307
308 /**
309 * Class magic unset method, performs {@link deleteAttribute()}
310 * @access private
311 */
312 function __unset($attribute) {
313 return $this->deleteAttribute($attribute);
314 }
315
316 /**
317 * Class magic invoke method, performs {@link query()}.
318 * @param string $query The css query to run on the nodes.
319 * @return pQuery
320 */
321 function __invoke($query = '*') {
322 return $this->query($query);
323 }
324
325 /**
326 * Returns place in document
327 * @return string
328 */
329 function dumpLocation() {
330 return (($this->parent) ? (($p = $this->parent->dumpLocation()) ? $p.' > ' : '').$this->tag.'('.$this->typeIndex().')' : '');
331 }
332
333 /**
334 * Returns all the attributes and their values
335 * @return string
336 * @access private
337 */
338 protected function toString_attributes() {
339 $s = '';
340 foreach($this->attributes as $a => $v) {
341 $s .= ' '.$a;
342 if ((!$this->attribute_shorttag) || ($v !== $a)) {
343 $quote = (strpos($v, '"') === false) ? '"' : "'";
344 $s .= '='.$quote.$v.$quote;
345 }
346 }
347 return $s;
348 }
349
350 /**
351 * Returns the content of the node (child tags and text)
352 * @param bool $attributes Print attributes of child tags
353 * @param bool|int $recursive How many sublevels of childtags to print. True for all.
354 * @param bool $content_only Only print text, false will print tags too.
355 * @return string
356 * @access private
357 */
358 protected function toString_content($attributes = true, $recursive = true, $content_only = false) {
359 $s = '';
360 foreach($this->children as $c) {
361 $s .= $c->toString($attributes, $recursive, $content_only);
362 }
363 return $s;
364 }
365
366 /**
367 * Returns the node as string
368 * @param bool $attributes Print attributes (of child tags)
369 * @param bool|int $recursive How many sub-levels of child tags to print. True for all.
370 * @param bool|int $content_only Only print text, false will print tags too.
371 * @return string
372 */
373 function toString($attributes = true, $recursive = true, $content_only = false) {
374 if ($content_only) {
375 if (is_int($content_only)) {
376 --$content_only;
377 }
378 return $this->toString_content($attributes, $recursive, $content_only);
379 }
380
381 $s = '<'.$this->tag;
382 if ($attributes) {
383 $s .= $this->toString_attributes();
384 }
385 if ($this->self_close) {
386 $s .= $this->self_close_str.'>';
387 } else {
388 $s .= '>';
389 if($recursive) {
390 $s .= $this->toString_content($attributes);
391 }
392 $s .= '</'.$this->tag.'>';
393 }
394 return $s;
395 }
396
397 /**
398 * Similar to JavaScript outerText, will return full (html formatted) node
399 * @return string
400 */
401 function getOuterText() {
402 return html_entity_decode($this->toString(), ENT_QUOTES);
403 }
404
405 /**
406 * Similar to JavaScript outerText, will replace node (and child nodes) with new text
407 * @param string $text
408 * @param HtmlParserBase $parser Null to auto create instance
409 * @return bool|array True on succeed, array with errors on failure
410 */
411 function setOuterText($text, $parser = null) {
412 if (trim($text)) {
413 $index = $this->index();
414 if ($parser === null) {
415 $parser = new $this->parserClass();
416 }
417 $parser->setDoc($text);
418 $parser->parse_all();
419 $parser->root->moveChildren($this->parent, $index);
420 }
421 $this->delete();
422 return (($parser && $parser->errors) ? $parser->errors : true);
423 }
424
425 /**
426 * Return html code of node
427 * @internal jquery (naming) compatibility
428 * @param string|null $value The value to set or null to get the value.
429 * @see toString()
430 * @return string
431 */
432 function html($value = null) {
433 if ($value !== null) {
434 $this->setInnerText($value);
435 }
436 return $this->getInnerText();
437 }
438
439 /**
440 * Similar to JavaScript innerText, will return (html formatted) content
441 * @return string
442 */
443 function getInnerText() {
444 return html_entity_decode($this->toString(true, true, 1), ENT_QUOTES);
445 }
446
447 /**
448 * Similar to JavaScript innerText, will replace child nodes with new text
449 * @param string $text
450 * @param HtmlParserBase $parser Null to auto create instance
451 * @return bool|array True on succeed, array with errors on failure
452 */
453 function setInnerText($text, $parser = null) {
454 $this->clear();
455 if (trim($text)) {
456 if ($parser === null) {
457 $parser = new $this->parserClass();
458 }
459 $parser->root =& $this;
460 $parser->setDoc($text);
461 $parser->parse_all();
462 }
463 return (($parser && $parser->errors) ? $parser->errors : true);
464 }
465
466 /**
467 * Similar to JavaScript plainText, will return text in node (and subnodes)
468 * @return string
469 */
470 function getPlainText() {
471 return preg_replace('`\s+`', ' ', html_entity_decode($this->toString(true, true, true), ENT_QUOTES));
472 }
473
474 /**
475 * Return plaintext taking document encoding into account
476 * @return string
477 */
478 function getPlainTextUTF8() {
479 $txt = $this->toString(true, true, true);
480 $enc = $this->getEncoding();
481 if ($enc !== false) {
482 $txt = mb_convert_encoding($txt, 'UTF-8', $enc);
483 }
484 return preg_replace('`\s+`', ' ', html_entity_decode($txt, ENT_QUOTES, 'UTF-8'));
485 }
486
487 /**
488 * Similar to JavaScript plainText, will replace child nodes with new text (literal)
489 * @param string $text
490 */
491 function setPlainText($text) {
492 $this->clear();
493 if (trim($text)) {
494 $this->addText(htmlentities($text, ENT_QUOTES));
495 }
496 }
497
498 /**
499 * Delete node from parent and clear node
500 */
501 function delete() {
502 if (($p = $this->parent) !== null) {
503 $this->parent = null;
504 $p->deleteChild($this);
505 } else {
506 $this->clear();
507 }
508 }
509
510 /**
511 * Detach node from parent
512 * @param bool $move_children_up Only detach current node and replace it with child nodes
513 * @internal jquery (naming) compatibility
514 * @see delete()
515 */
516 function detach($move_children_up = false) {
517 if (($p = $this->parent) !== null) {
518 $index = $this->index();
519 $this->parent = null;
520
521 if ($move_children_up) {
522 $this->moveChildren($p, $index);
523 }
524 $p->deleteChild($this, true);
525 }
526 }
527
528 /**
529 * Deletes all child nodes from node
530 */
531 function clear() {
532 foreach($this->children as $c) {
533 $c->parent = null;
534 $c->delete();
535 }
536 $this->children = array();
537 }
538
539 /**
540 * Get top parent
541 * @return DomNode Root, null if node has no parent
542 */
543 function getRoot() {
544 $r = $this->parent;
545 $n = ($r === null) ? null : $r->parent;
546 while ($n !== null) {
547 $r = $n;
548 $n = $r->parent;
549 }
550
551 return $r;
552 }
553
554 /**
555 * Change parent
556 * @param null|DomNode $to New parent, null if none
557 * @param false|int $index Add child to parent if not present at index, false to not add, negative to count from end, null to append
558 */
559 #php4
560 #function changeParent($to, &$index) {
561 #php4e
562 #php5
563 function changeParent($to, &$index = null) {
564 #php5e
565 if ($this->parent !== null) {
566 $this->parent->deleteChild($this, true);
567 }
568 $this->parent = $to;
569 if ($index !== false) {
570 $new_index = $this->index();
571 if (!(is_int($new_index) && ($new_index >= 0))) {
572 $this->parent->addChild($this, $index);
573 }
574 }
575 }
576
577 /**
578 * Find out if node has (a certain) parent
579 * @param DomNode|string $tag Match against parent, string to match tag, object to fully match node, null to return if node has parent
580 * @param bool $recursive
581 * @return bool
582 */
583 function hasParent($tag = null, $recursive = false) {
584 if ($this->parent !== null) {
585 if ($tag === null) {
586 return true;
587 } elseif (is_string($tag)) {
588 return (($this->parent->tag === $tag) || ($recursive && $this->parent->hasParent($tag)));
589 } elseif (is_object($tag)) {
590 return (($this->parent === $tag) || ($recursive && $this->parent->hasParent($tag)));
591 }
592 }
593
594 return false;
595 }
596
597 /**
598 * Find out if node is parent of a certain tag
599 * @param DomNode|string $tag Match against parent, string to match tag, object to fully match node
600 * @param bool $recursive
601 * @return bool
602 * @see hasParent()
603 */
604 function isParent($tag, $recursive = false) {
605 return ($this->hasParent($tag, $recursive) === ($tag !== null));
606 }
607
608 /**
609 * Find out if node is text
610 * @return bool
611 */
612 function isText() {
613 return false;
614 }
615
616 /**
617 * Find out if node is comment
618 * @return bool
619 */
620 function isComment() {
621 return false;
622 }
623
624 /**
625 * Find out if node is text or comment node
626 * @return bool
627 */
628 function isTextOrComment() {
629 return false;
630 }
631
632 /**
633 * Move node to other node
634 * @param DomNode $to New parent, null if none
635 * @param int $new_index Add child to parent at index if not present, null to not add, negative to count from end
636 * @internal Performs {@link changeParent()}
637 */
638 #php4
639 #function move($to, &$new_index) {
640 #php4e
641 #php5
642 function move($to, &$new_index = -1) {
643 #php5e
644 $this->changeParent($to, $new_index);
645 }
646
647 /**
648 * Move child nodes to other node
649 * @param DomNode $to New parent, null if none
650 * @param int $new_index Add child to new node at index if not present, null to not add, negative to count from end
651 * @param int $start Index from child node where to start wrapping, 0 for first element
652 * @param int $end Index from child node where to end wrapping, -1 for last element
653 */
654 #php4
655 #function moveChildren($to, &$new_index, $start = 0, $end = -1) {
656 #php4e
657 #php5
658 function moveChildren($to, &$new_index = -1, $start = 0, $end = -1) {
659 #php5e
660 if ($end < 0) {
661 $end += count($this->children);
662 }
663 for ($i = $start; $i <= $end; $i++) {
664 $this->children[$start]->changeParent($to, $new_index);
665 }
666 }
667
668 /**
669 * Index of node in parent
670 * @param bool $count_all True to count all tags, false to ignore text and comments
671 * @return int -1 if not found
672 */
673 function index($count_all = true) {
674 if (!$this->parent) {
675 return -1;
676 } elseif ($count_all) {
677 return $this->parent->findChild($this);
678 } else{
679 $index = -1;
680 //foreach($this->parent->children as &$c) {
681 // if (!$c->isTextOrComment()) {
682 // ++$index;
683 // }
684 // if ($c === $this) {
685 // return $index;
686 // }
687 //}
688
689 foreach(array_keys($this->parent->children) as $k) {
690 if (!$this->parent->children[$k]->isTextOrComment()) {
691 ++$index;
692 }
693 if ($this->parent->children[$k] === $this) {
694 return $index;
695 }
696 }
697 return -1;
698 }
699 }
700
701 /**
702 * Change index of node in parent
703 * @param int $index New index
704 */
705 function setIndex($index) {
706 if ($this->parent) {
707 if ($index > $this->index()) {
708 --$index;
709 }
710 $this->delete();
711 $this->parent->addChild($this, $index);
712 }
713 }
714
715 /**
716 * Index of all similar nodes in parent
717 * @return int -1 if not found
718 */
719 function typeIndex() {
720 if (!$this->parent) {
721 return -1;
722 } else {
723 $index = -1;
724 //foreach($this->parent->children as &$c) {
725 // if (strcasecmp($this->tag, $c->tag) === 0) {
726 // ++$index;
727 // }
728 // if ($c === $this) {
729 // return $index;
730 // }
731 //}
732
733 foreach(array_keys($this->parent->children) as $k) {
734 if (strcasecmp($this->tag, $this->parent->children[$k]->tag) === 0) {
735 ++$index;
736 }
737 if ($this->parent->children[$k] === $this) {
738 return $index;
739 }
740 }
741 return -1;
742 }
743 }
744
745 /**
746 * Calculate indent of node (number of parent tags - 1)
747 * @return int
748 */
749 function indent() {
750 return (($this->parent) ? $this->parent->indent() + 1 : -1);
751 }
752
753 /**
754 * Get sibling node
755 * @param int $offset Offset from current node
756 * @return DomNode Null if not found
757 */
758 function getSibling($offset = 1) {
759 if (!$this->parent) {
760 return null;
761 }
762 $index = $this->index() + $offset;
763 if (($index >= 0) && ($index < $this->parent->childCount())) {
764 return $this->parent->getChild($index);
765 } else {
766 return null;
767 }
768 }
769
770 /**
771 * Get node next to current
772 * @param bool $skip_text_comments
773 * @return DomNode Null if not found
774 * @see getSibling()
775 * @see getPreviousSibling()
776 */
777 function getNextSibling($skip_text_comments = true) {
778 $offset = 1;
779 while (($n = $this->getSibling($offset)) !== null) {
780 if ($skip_text_comments && ($n->tag[0] === '~')) {
781 ++$offset;
782 } else {
783 break;
784 }
785 }
786
787 return $n;
788 }
789
790 /**
791 * Get node previous to current
792 * @param bool $skip_text_comments
793 * @return DomNode Null if not found
794 * @see getSibling()
795 * @see getNextSibling()
796 */
797 function getPreviousSibling($skip_text_comments = true) {
798 $offset = -1;
799 while (($n = $this->getSibling($offset)) !== null) {
800 if ($skip_text_comments && ($n->tag[0] === '~')) {
801 --$offset;
802 } else {
803 break;
804 }
805 }
806
807 return $n;
808 }
809
810 /**
811 * Get namespace of node
812 * @return string
813 * @see setNamespace()
814 */
815 function getNamespace() {
816 if ($this->tag_ns === null) {
817 $a = explode(':', $this->tag, 2);
818 if (empty($a[1])) {
819 $this->tag_ns = array('', $a[0]);
820 } else {
821 $this->tag_ns = array($a[0], $a[1]);
822 }
823 }
824
825 return $this->tag_ns[0];
826 }
827
828 /**
829 * Set namespace of node
830 * @param string $ns
831 * @see getNamespace()
832 */
833 function setNamespace($ns) {
834 if ($this->getNamespace() !== $ns) {
835 $this->tag_ns[0] = $ns;
836 $this->tag = $ns.':'.$this->tag_ns[1];
837 }
838 }
839
840 /**
841 * Get tagname of node (without namespace)
842 * @return string
843 * @see setTag()
844 */
845 function getTag() {
846 if ($this->tag_ns === null) {
847 $this->getNamespace();
848 }
849
850 return $this->tag_ns[1];
851 }
852
853 /**
854 * Set tag (with or without namespace)
855 * @param string $tag
856 * @param bool $with_ns Does $tag include namespace?
857 * @see getTag()
858 */
859 function setTag($tag, $with_ns = false) {
860 $with_ns = $with_ns || (strpos($tag, ':') !== false);
861 if ($with_ns) {
862 $this->tag = $tag;
863 $this->tag_ns = null;
864 } elseif ($this->getTag() !== $tag) {
865 $this->tag_ns[1] = $tag;
866 $this->tag = (($this->tag_ns[0]) ? $this->tag_ns[0].':' : '').$tag;
867 }
868 }
869
870 /**
871 * Try to determine the encoding of the current tag
872 * @return string|bool False if encoding could not be found
873 */
874 function getEncoding() {
875 $root = $this->getRoot();
876 if ($root !== null) {
877 if ($enc = $root->select('meta[charset]', 0, true, true)) {
878 return $enc->getAttribute("charset");
879 } elseif ($enc = $root->select('"?xml"[encoding]', 0, true, true)) {
880 return $enc->getAttribute("encoding");
881 } elseif ($enc = $root->select('meta[content*="charset="]', 0, true, true)) {
882 $enc = $enc->getAttribute("content");
883 return substr($enc, strpos($enc, "charset=")+8);
884 }
885 }
886
887 return false;
888 }
889
890 /**
891 * Number of children in node
892 * @param bool $ignore_text_comments Ignore text/comments with calculation
893 * @return int
894 */
895 function childCount($ignore_text_comments = false) {
896 if (!$ignore_text_comments) {
897 return count($this->children);
898 } else{
899 $count = 0;
900 //foreach($this->children as &$c) {
901 // if (!$c->isTextOrComment()) {
902 // ++$count;
903 // }
904 //}
905
906 foreach(array_keys($this->children) as $k) {
907 if (!$this->children[$k]->isTextOrComment()) {
908 ++$count;
909 }
910 }
911 return $count;
912 }
913 }
914
915 /**
916 * Find node in children
917 * @param DomNode $child
918 * @return int False if not found
919 */
920 function findChild($child) {
921 return array_search($child, $this->children, true);
922 }
923
924 /**
925 * Checks if node has another node as child
926 * @param DomNode $child
927 * @return bool
928 */
929 function hasChild($child) {
930 return ((bool) findChild($child));
931 }
932
933 /**
934 * Get childnode
935 * @param int|DomNode $child Index, negative to count from end
936 * @param bool $ignore_text_comments Ignore text/comments with index calculation
937 * @return DomNode
938 */
939 function &getChild($child, $ignore_text_comments = false) {
940 if (!is_int($child)) {
941 $child = $this->findChild($child);
942 } elseif ($child < 0) {
943 $child += $this->childCount($ignore_text_comments);
944 }
945
946 if ($ignore_text_comments) {
947 $count = 0;
948 $last = null;
949 //foreach($this->children as &$c) {
950 // if (!$c->isTextOrComment()) {
951 // if ($count++ === $child) {
952 // return $c;
953 // }
954 // $last = $c;
955 // }
956 //}
957
958 foreach(array_keys($this->children) as $k) {
959 if (!$this->children[$k]->isTextOrComment()) {
960 if ($count++ === $child) {
961 return $this->children[$k];
962 }
963 $last = $this->children[$k];
964 }
965 }
966 return (($child > $count) ? $last : null);
967 } else {
968 return $this->children[$child];
969 }
970 }
971
972 /**
973 * Add child node
974 * @param string|DomNode $tag Tag name or object
975 * @param int $offset Position to insert node, negative to count from end, null to append
976 * @return DomNode Added node
977 */
978 #php4
979 #function &addChild($tag, &$offset) {
980 #php4e
981 #php5
982 function &addChild($tag, &$offset = null) {
983 #php5e
984 if (is_array($tag)) {
985 $tag = new $this->childClass($tag, $this);
986 } elseif (is_string($tag)) {
987 $nodes = $this->createNodes($tag);
988 $tag = array_shift($nodes);
989
990 if ($tag && $tag->parent !== $this) {
991 $index = false;
992 $tag->changeParent($this, $index);
993 }
994 } elseif (is_object($tag) && $tag->parent !== $this) {
995 $index = false; //Needs to be passed by ref
996 $tag->changeParent($this, $index);
997 }
998
999 if (is_int($offset) && ($offset < count($this->children)) && ($offset !== -1)) {
1000 if ($offset < 0) {
1001 $offset += count($this->children);
1002 }
1003 array_splice($this->children, $offset++, 0, array(&$tag));
1004 } else {
1005 $this->children[] =& $tag;
1006 }
1007
1008 return $tag;
1009 }
1010
1011 /**
1012 * First child node
1013 * @param bool $ignore_text_comments Ignore text/comments with index calculation
1014 * @return DomNode
1015 */
1016 function &firstChild($ignore_text_comments = false) {
1017 return $this->getChild(0, $ignore_text_comments);
1018 }
1019
1020 /**
1021 * Last child node
1022 * @param bool $ignore_text_comments Ignore text/comments with index calculation
1023 * @return DomNode
1024 */
1025 function &lastChild($ignore_text_comments = false) {
1026 return $this->getChild(-1, $ignore_text_comments);
1027 }
1028
1029 /**
1030 * Insert childnode
1031 * @param string|DomNode $tag Tagname or object
1032 * @param int $offset Position to insert node, negative to count from end, null to append
1033 * @return DomNode Added node
1034 * @see addChild();
1035 */
1036 function &insertChild($tag, $index) {
1037 return $this->addChild($tag, $index);
1038 }
1039
1040 /**
1041 * Add text node
1042 * @param string $text
1043 * @param int $offset Position to insert node, negative to count from end, null to append
1044 * @return DomNode Added node
1045 * @see addChild();
1046 */
1047 #php4
1048 #function &addText($text, &$offset) {
1049 #php4e
1050 #php5
1051 function &addText($text, &$offset = null) {
1052 #php5e
1053 return $this->addChild(new $this->childClass_Text($this, $text), $offset);
1054 }
1055
1056 /**
1057 * Add comment node
1058 * @param string $text
1059 * @param int $offset Position to insert node, negative to count from end, null to append
1060 * @return DomNode Added node
1061 * @see addChild();
1062 */
1063 #php4
1064 #function &addComment($text, &$offset) {
1065 #php4e
1066 #php5
1067 function &addComment($text, &$offset = null) {
1068 #php5e
1069 return $this->addChild(new $this->childClass_Comment($this, $text), $offset);
1070 }
1071
1072 /**
1073 * Add conditional node
1074 * @param string $condition
1075 * @param bool True for <!--[if, false for <![if
1076 * @param int $offset Position to insert node, negative to count from end, null to append
1077 * @return DomNode Added node
1078 * @see addChild();
1079 */
1080 #php4
1081 #function &addConditional($condition, $hidden = true, &$offset) {
1082 #php4e
1083 #php5
1084 function &addConditional($condition, $hidden = true, &$offset = null) {
1085 #php5e
1086 return $this->addChild(new $this->childClass_Conditional($this, $condition, $hidden), $offset);
1087 }
1088
1089 /**
1090 * Add CDATA node
1091 * @param string $text
1092 * @param int $offset Position to insert node, negative to count from end, null to append
1093 * @return DomNode Added node
1094 * @see addChild();
1095 */
1096 #php4
1097 #function &addCDATA($text, &$offset) {
1098 #php4e
1099 #php5
1100 function &addCDATA($text, &$offset = null) {
1101 #php5e
1102 return $this->addChild(new $this->childClass_CDATA($this, $text), $offset);
1103 }
1104
1105 /**
1106 * Add doctype node
1107 * @param string $dtd
1108 * @param int $offset Position to insert node, negative to count from end, null to append
1109 * @return DomNode Added node
1110 * @see addChild();
1111 */
1112 #php4
1113 #function &addDoctype($dtd, &$offset) {
1114 #php4e
1115 #php5
1116 function &addDoctype($dtd, &$offset = null) {
1117 #php5e
1118 return $this->addChild(new $this->childClass_Doctype($this, $dtd), $offset);
1119 }
1120
1121 /**
1122 * Add xml node
1123 * @param string $tag Tag name after "?", e.g. "php" or "xml"
1124 * @param string $text
1125 * @param array $attributes Array of attributes (array('attribute' => 'value'))
1126 * @param int $offset Position to insert node, negative to count from end, null to append
1127 * @return DomNode Added node
1128 * @see addChild();
1129 */
1130 #php4
1131 #function &addXML($tag = 'xml', $text = '', $attributes = array(), &$offset) {
1132 #php4e
1133 #php5
1134 function &addXML($tag = 'xml', $text = '', $attributes = array(), &$offset = null) {
1135 #php5e
1136 return $this->addChild(new $this->childClass_XML($this, $tag, $text, $attributes), $offset);
1137 }
1138
1139 /**
1140 * Add ASP node
1141 * @param string $tag Tag name after "%"
1142 * @param string $text
1143 * @param array $attributes Array of attributes (array('attribute' => 'value'))
1144 * @param int $offset Position to insert node, negative to count from end, null to append
1145 * @return DomNode Added node
1146 * @see addChild();
1147 */
1148 #php4
1149 #function &addASP($tag = '', $text = '', $attributes = array(), &$offset) {
1150 #php4e
1151 #php5
1152 function &addASP($tag = '', $text = '', $attributes = array(), &$offset = null) {
1153 #php5e
1154 return $this->addChild(new $this->childClass_ASP($this, $tag, $text, $attributes), $offset);
1155 }
1156
1157 /**
1158 * Delete a child node
1159 * @param int|DomNode $child Child(index) to delete, negative to count from end
1160 * @param bool $soft_delete False to call {@link delete()} from child
1161 */
1162 function deleteChild($child, $soft_delete = false) {
1163 if (is_object($child)) {
1164 $child = $this->findChild($child);
1165 } elseif ($child < 0) {
1166 $child += count($this->children);
1167 }
1168
1169 if (!$soft_delete) {
1170 $this->children[$child]->delete();
1171 }
1172 unset($this->children[$child]);
1173
1174 //Rebuild indices
1175 $tmp = array();
1176
1177 //foreach($this->children as &$c) {
1178 // $tmp[] =& $c;
1179 //}
1180 foreach(array_keys($this->children) as $k) {
1181 $tmp[] =& $this->children[$k];
1182 }
1183 $this->children = $tmp;
1184 }
1185
1186 /**
1187 * Wrap node
1188 * @param string|DomNode $node Wrapping node, string to create new element node
1189 * @param int $wrap_index Index to insert current node in wrapping node, -1 to append
1190 * @param int $node_index Index to insert wrapping node, null to keep at same position
1191 * @return DomNode Wrapping node
1192 */
1193 function wrap($node, $wrap_index = -1, $node_index = null) {
1194 if ($node_index === null) {
1195 $node_index = $this->index();
1196 }
1197
1198 if (!is_object($node)) {
1199 $node = $this->parent->addChild($node, $node_index);
1200 } elseif ($node->parent !== $this->parent) {
1201 $node->changeParent($this->parent, $node_index);
1202 }
1203
1204 $this->changeParent($node, $wrap_index);
1205 return $node;
1206 }
1207
1208 /**
1209 * Wrap child nodes
1210 * @param string|DomNode $node Wrapping node, string to create new element node
1211 * @param int $start Index from child node where to start wrapping, 0 for first element
1212 * @param int $end Index from child node where to end wrapping, -1 for last element
1213 * @param int $wrap_index Index to insert in wrapping node, -1 to append
1214 * @param int $node_index Index to insert current node, null to keep at same position
1215 * @return DomNode Wrapping node
1216 */
1217 function wrapInner($node, $start = 0, $end = -1, $wrap_index = -1, $node_index = null) {
1218 if ($end < 0) {
1219 $end += count($this->children);
1220 }
1221 if ($node_index === null) {
1222 $node_index = $end + 1;
1223 }
1224
1225 if (!is_object($node)) {
1226 $node = $this->addChild($node, $node_index);
1227 } elseif ($node->parent !== $this) {
1228 $node->changeParent($this->parent, $node_index);
1229 }
1230
1231 $this->moveChildren($node, $wrap_index, $start, $end);
1232 return $node;
1233 }
1234
1235 /**
1236 * Number of attributes
1237 * @return int
1238 */
1239 function attributeCount() {
1240 return count($this->attributes);
1241 }
1242
1243 /**
1244 * Find attribute using namespace, name or both
1245 * @param string|int $attr Negative int to count from end
1246 * @param string $compare "namespace", "name" or "total"
1247 * @param bool $case_sensitive Compare with case sensitivity
1248 * @return array array('ns', 'attr', 'ns:attr', index)
1249 * @access private
1250 */
1251 protected function findAttribute($attr, $compare = 'total', $case_sensitive = false) {
1252 if (is_int($attr)) {
1253 if ($attr < 0) {
1254 $attr += count($this->attributes);
1255 }
1256 $keys = array_keys($this->attributes);
1257 return $this->findAttribute($keys[$attr], 'total', true);
1258 } else if ($compare === 'total') {
1259 $b = explode(':', $attr, 2);
1260 if ($case_sensitive) {
1261 $t =& $this->attributes;
1262 } else {
1263 $t = array_change_key_case($this->attributes);
1264 $attr = strtolower($attr);
1265 }
1266
1267 if (isset($t[$attr])) {
1268 $index = 0;
1269 foreach($this->attributes as $a => $v) {
1270 if (($v === $t[$attr]) && (strcasecmp($a, $attr) === 0)) {
1271 $attr = $a;
1272 $b = explode(':', $attr, 2);
1273 break;
1274 }
1275 ++$index;
1276 }
1277
1278 if (empty($b[1])) {
1279 return array(array('', $b[0], $attr, $index));
1280 } else {
1281 return array(array($b[0], $b[1], $attr, $index));
1282 }
1283 } else {
1284 return false;
1285 }
1286 } else {
1287 if ($this->attributes_ns === null) {
1288 $index = 0;
1289 foreach($this->attributes as $a => $v) {
1290 $b = explode(':', $a, 2);
1291 if (empty($b[1])) {
1292 $this->attributes_ns[$b[0]][] = array('', $b[0], $a, $index);
1293 } else {
1294 $this->attributes_ns[$b[1]][] = array($b[0], $b[1], $a, $index);
1295 }
1296 ++$index;
1297 }
1298 }
1299
1300 if ($case_sensitive) {
1301 $t =& $this->attributes_ns;
1302 } else {
1303 $t = array_change_key_case($this->attributes_ns);
1304 $attr = strtolower($attr);
1305 }
1306
1307 if ($compare === 'namespace') {
1308 $res = array();
1309 foreach($t as $ar) {
1310 foreach($ar as $a) {
1311 if ($a[0] === $attr) {
1312 $res[] = $a;
1313 }
1314 }
1315 }
1316 return $res;
1317 } elseif ($compare === 'name') {
1318 return ((isset($t[$attr])) ? $t[$attr] : false);
1319 } else {
1320 // phpcs:ignore QITStandard.PHP.DebugCode.DebugFunctionFound
1321 trigger_error('Unknown comparison mode');
1322 }
1323 }
1324 }
1325
1326 /**
1327 * Checks if node has attribute
1328 * @param string|int$attr Negative int to count from end
1329 * @param string $compare Find node using "namespace", "name" or "total"
1330 * @param bool $case_sensitive Compare with case sensitivity
1331 * @return bool
1332 */
1333 function hasAttribute($attr, $compare = 'total', $case_sensitive = false) {
1334 return ((bool) $this->findAttribute($attr, $compare, $case_sensitive));
1335 }
1336
1337 /**
1338 * Gets namespace of attribute(s)
1339 * @param string|int $attr Negative int to count from end
1340 * @param string $compare Find node using "namespace", "name" or "total"
1341 * @param bool $case_sensitive Compare with case sensitivity
1342 * @return string|array False if not found
1343 */
1344 function getAttributeNS($attr, $compare = 'name', $case_sensitive = false) {
1345 $f = $this->findAttribute($attr, $compare, $case_sensitive);
1346 if (is_array($f) && $f) {
1347 if (count($f) === 1) {
1348 return $this->attributes[$f[0][0]];
1349 } else {
1350 $res = array();
1351 foreach($f as $a) {
1352 $res[] = $a[0];
1353 }
1354 return $res;
1355 }
1356 } else {
1357 return false;
1358 }
1359 }
1360
1361 /**
1362 * Sets namespace of attribute(s)
1363 * @param string|int $attr Negative int to count from end
1364 * @param string $namespace
1365 * @param string $compare Find node using "namespace", "name" or "total"
1366 * @param bool $case_sensitive Compare with case sensitivity
1367 * @return bool
1368 */
1369 function setAttributeNS($attr, $namespace, $compare = 'name', $case_sensitive = false) {
1370 $f = $this->findAttribute($attr, $compare, $case_sensitive);
1371 if (is_array($f) && $f) {
1372 if ($namespace) {
1373 $namespace .= ':';
1374 }
1375 foreach($f as $a) {
1376 $val = $this->attributes[$a[2]];
1377 unset($this->attributes[$a[2]]);
1378 $this->attributes[$namespace.$a[1]] = $val;
1379 }
1380 $this->attributes_ns = null;
1381 return true;
1382 } else {
1383 return false;
1384 }
1385 }
1386
1387 /**
1388 * Gets value(s) of attribute(s)
1389 * @param string|int $attr Negative int to count from end
1390 * @param string $compare Find node using "namespace", "name" or "total"
1391 * @param bool $case_sensitive Compare with case sensitivity
1392 * @return string|array
1393 */
1394 function getAttribute($attr, $compare = 'total', $case_sensitive = false) {
1395 $f = $this->findAttribute($attr, $compare, $case_sensitive);
1396 if (is_array($f) && $f){
1397 if (count($f) === 1) {
1398 return $this->attributes[$f[0][2]];
1399 } else {
1400 $res = array();
1401 foreach($f as $a) {
1402 $res[] = $this->attributes[$a[2]];
1403 }
1404 return $res;
1405 }
1406 } else {
1407 return null;
1408 }
1409 }
1410
1411 /**
1412 * Sets value(s) of attribute(s)
1413 * @param string|int $attr Negative int to count from end
1414 * @param string $compare Find node using "namespace", "name" or "total"
1415 * @param bool $case_sensitive Compare with case sensitivity
1416 */
1417 function setAttribute($attr, $val, $compare = 'total', $case_sensitive = false) {
1418 if ($val === null) {
1419 return $this->deleteAttribute($attr, $compare, $case_sensitive);
1420 }
1421
1422 $f = $this->findAttribute($attr, $compare, $case_sensitive);
1423 if (is_array($f) && $f) {
1424 foreach($f as $a) {
1425 $this->attributes[$a[2]] = (string) $val;
1426 }
1427 } else {
1428 $this->attributes[$attr] = (string) $val;
1429 }
1430 }
1431
1432 /**
1433 * Add new attribute
1434 * @param string $attr
1435 * @param string $val
1436 */
1437 function addAttribute($attr, $val) {
1438 $this->setAttribute($attr, $val, 'total', true);
1439 }
1440
1441 /**
1442 * Delete attribute(s)
1443 * @param string|int $attr Negative int to count from end
1444 * @param string $compare Find node using "namespace", "name" or "total"
1445 * @param bool $case_sensitive Compare with case sensitivity
1446 */
1447 function deleteAttribute($attr, $compare = 'total', $case_sensitive = false) {
1448 $f = $this->findAttribute($attr, $compare, $case_sensitive);
1449 if (is_array($f) && $f) {
1450 foreach($f as $a) {
1451 unset($this->attributes[$a[2]]);
1452 if ($this->attributes_ns !== null) {
1453 unset($this->attributes_ns[$a[1]]);
1454 }
1455 }
1456 }
1457 }
1458
1459 /**
1460 * Determine if node has a certain class
1461 * @param string $className
1462 * @return bool
1463 */
1464 function hasClass($className) {
1465 return ($className && preg_match('`\b'.preg_quote($className).'\b`si', (string)$this->class));
1466 }
1467
1468 /**
1469 * Add new class(es)
1470 * @param string|array $className
1471 */
1472 function addClass($className) {
1473 if (!is_array($className)) {
1474 $className = array($className);
1475 }
1476 $class = $this->class;
1477 foreach ($className as $c) {
1478 if (!(preg_match('`\b'.preg_quote($c).'\b`si', (string)$class) > 0)) {
1479 $class .= ' '.$c;
1480 }
1481 }
1482 $this->class = trim((string)$class);
1483 }
1484
1485 /**
1486 * Remove clas(ses)
1487 * @param string|array $className
1488 */
1489 function removeClass($className) {
1490 if (!is_array($className)) {
1491 $className = array($className);
1492 }
1493 $class = $this->class;
1494 foreach ($className as $c) {
1495 $class = preg_replace('`\b'.preg_quote($c).'\b`si', '', $class);
1496 }
1497 if ($class) {
1498 $this->class = $class;
1499 } else {
1500 unset($this->class);
1501 }
1502 }
1503
1504 /**
1505 * Finds children using a callback function
1506 * @param callable $callback Function($node) that returns a bool
1507 * @param bool|int $recursive Check recursively
1508 * @param bool $check_self Include this node in search?
1509 * @return array
1510 */
1511 function getChildrenByCallback($callback, $recursive = true, $check_self = false) {
1512 $count = $this->childCount();
1513 if ($check_self && $callback($this)) {
1514 $res = array($this);
1515 } else {
1516 $res = array();
1517 }
1518
1519 if ($count > 0) {
1520 if (is_int($recursive)) {
1521 $recursive = (($recursive > 1) ? $recursive - 1 : false);
1522 }
1523
1524 for ($i = 0; $i < $count; $i++) {
1525 if ($callback($this->children[$i])) {
1526 $res[] = $this->children[$i];
1527 }
1528 if ($recursive) {
1529 $res = array_merge($res, $this->children[$i]->getChildrenByCallback($callback, $recursive));
1530 }
1531 }
1532 }
1533
1534 return $res;
1535 }
1536
1537 /**
1538 * Finds children using the {$link match()} function
1539 * @param $conditions See {$link match()}
1540 * @param $custom_filters See {$link match()}
1541 * @param bool|int $recursive Check recursively
1542 * @param bool $check_self Include this node in search?
1543 * @return array
1544 */
1545 function getChildrenByMatch($conditions, $recursive = true, $check_self = false, $custom_filters = array()) {
1546 $count = $this->childCount();
1547 if ($check_self && $this->match($conditions, true, $custom_filters)) {
1548 $res = array($this);
1549 } else {
1550 $res = array();
1551 }
1552
1553 if ($count > 0) {
1554 if (is_int($recursive)) {
1555 $recursive = (($recursive > 1) ? $recursive - 1 : false);
1556 }
1557
1558 for ($i = 0; $i < $count; $i++) {
1559 if ($this->children[$i]->match($conditions, true, $custom_filters)) {
1560 $res[] = $this->children[$i];
1561 }
1562 if ($recursive) {
1563 $res = array_merge($res, $this->children[$i]->getChildrenByMatch($conditions, $recursive, false, $custom_filters));
1564 }
1565 }
1566 }
1567
1568 return $res;
1569 }
1570
1571 /**
1572 * Checks if tag matches certain conditions
1573 * @param array $tags array('tag1', 'tag2') or array(array(
1574 * 'tag' => 'tag1',
1575 * 'operator' => 'or'/'and',
1576 * 'compare' => 'total'/'namespace'/'name',
1577 * 'case_sensitive' => true))
1578 * @return bool
1579 * @internal Used by selector class
1580 * @see match()
1581 * @access private
1582 */
1583 protected function match_tags($tags) {
1584 $res = false;
1585
1586 foreach($tags as $tag => $match) {
1587 if (!is_array($match)) {
1588 $match = array(
1589 'match' => $match,
1590 'operator' => 'or',
1591 'compare' => 'total',
1592 'case_sensitive' => false
1593 );
1594 } else {
1595 if (is_int($tag)) {
1596 $tag = $match['tag'];
1597 }
1598 if (!isset($match['match'])) {
1599 $match['match'] = true;
1600 }
1601 if (!isset($match['operator'])) {
1602 $match['operator'] = 'or';
1603 }
1604 if (!isset($match['compare'])) {
1605 $match['compare'] = 'total';
1606 }
1607 if (!isset($match['case_sensitive'])) {
1608 $match['case_sensitive'] = false;
1609 }
1610 }
1611
1612 if (($match['operator'] === 'and') && (!$res)) {
1613 return false;
1614 } elseif (!($res && ($match['operator'] === 'or'))) {
1615 if ($match['compare'] === 'total') {
1616 $a = $this->tag;
1617 } elseif ($match['compare'] === 'namespace') {
1618 $a = $this->getNamespace();
1619 } elseif ($match['compare'] === 'name') {
1620 $a = $this->getTag();
1621 }
1622
1623 if ($match['case_sensitive']) {
1624 $res = (($a === $tag) === $match['match']);
1625 } else {
1626 $res = ((strcasecmp($a, $tag) === 0) === $match['match']);
1627 }
1628 }
1629 }
1630
1631 return $res;
1632 }
1633
1634 /**
1635 * Checks if attributes match certain conditions
1636 * @param array $attributes array('attr' => 'val') or array(array(
1637 * 'operator_value' => 'equals'/'='/'contains_regex'/etc
1638 * 'attribute' => 'attr',
1639 * 'value' => 'val',
1640 * 'match' => true,
1641 * 'operator_result' => 'or'/'and',
1642 * 'compare' => 'total'/'namespace'/'name',
1643 * 'case_sensitive' => true))
1644 * @return bool
1645 * @internal Used by selector class
1646 * @see match()
1647 * @access private
1648 */
1649 protected function match_attributes($attributes) {
1650 $res = false;
1651
1652 foreach($attributes as $attribute => $match) {
1653 if (!is_array($match)) {
1654 $match = array(
1655 'operator_value' => 'equals',
1656 'value' => $match,
1657 'match' => true,
1658 'operator_result' => 'or',
1659 'compare' => 'total',
1660 'case_sensitive' => false
1661 );
1662 } else {
1663 if (is_int($attribute)) {
1664 $attribute = $match['attribute'];
1665 }
1666 if (!isset($match['match'])) {
1667 $match['match'] = true;
1668 }
1669 if (!isset($match['operator_result'])) {
1670 $match['operator_result'] = 'or';
1671 }
1672 if (!isset($match['compare'])) {
1673 $match['compare'] = 'total';
1674 }
1675 if (!isset($match['case_sensitive'])) {
1676 $match['case_sensitive'] = false;
1677 }
1678 }
1679
1680 if (is_string($match['value']) && (!$match['case_sensitive'])) {
1681 $match['value'] = strtolower($match['value']);
1682 }
1683
1684 if (($match['operator_result'] === 'and') && (!$res)) {
1685 return false;
1686 } elseif (!($res && ($match['operator_result'] === 'or'))) {
1687 $possibles = $this->findAttribute($attribute, $match['compare'], $match['case_sensitive']);
1688
1689 $has = (is_array($possibles) && $possibles);
1690 $res = (($match['value'] === $has) || (($match['match'] === false) && ($has === $match['match'])));
1691
1692 if ((!$res) && $has && is_string($match['value'])) {
1693 foreach($possibles as $a) {
1694 $val = $this->attributes[$a[2]];
1695 if (is_string($val) && (!$match['case_sensitive'])) {
1696 $val = strtolower($val);
1697 }
1698
1699 switch($match['operator_value']) {
1700 case '%=':
1701 case 'contains_regex':
1702 $res = ((preg_match('`'.$match['value'].'`s', $val) > 0) === $match['match']);
1703 if ($res) break 1; else break 2;
1704
1705 case '|=':
1706 case 'contains_prefix':
1707 $res = ((preg_match('`\b'.preg_quote($match['value']).'[\-\s]`s', $val) > 0) === $match['match']);
1708 if ($res) break 1; else break 2;
1709
1710 case '~=':
1711 case 'contains_word':
1712 $res = ((preg_match('`\s'.preg_quote($match['value']).'\s`s', " $val ") > 0) === $match['match']);
1713 if ($res) break 1; else break 2;
1714
1715 case '*=':
1716 case 'contains':
1717 $res = ((strpos($val, $match['value']) !== false) === $match['match']);
1718 if ($res) break 1; else break 2;
1719
1720 case '$=':
1721 case 'ends_with':
1722 $res = ((substr($val, -strlen($match['value'])) === $match['value']) === $match['match']);
1723 if ($res) break 1; else break 2;
1724
1725 case '^=':
1726 case 'starts_with':
1727 $res = ((substr($val, 0, strlen($match['value'])) === $match['value']) === $match['match']);
1728 if ($res) break 1; else break 2;
1729
1730 case '!=':
1731 case 'not_equal':
1732 $res = (($val !== $match['value']) === $match['match']);
1733 if ($res) break 1; else break 2;
1734
1735 case '=':
1736 case 'equals':
1737 $res = (($val === $match['value']) === $match['match']);
1738 if ($res) break 1; else break 2;
1739
1740 case '>=':
1741 case 'bigger_than':
1742 $res = (($val >= $match['value']) === $match['match']);
1743 if ($res) break 1; else break 2;
1744
1745 case '<=':
1746 case 'smaller_than':
1747 $res = (($val >= $match['value']) === $match['match']);
1748 if ($res) break 1; else break 2;
1749
1750 default:
1751 // phpcs:ignore QITStandard.PHP.DebugCode.DebugFunctionFound
1752 trigger_error('Unknown operator "'.esc_html($match['operator_value']).'" to match attributes!');
1753 return false;
1754 }
1755 }
1756 }
1757 }
1758 }
1759
1760 return $res;
1761 }
1762
1763 /**
1764 * Checks if node matches certain filters
1765 * @param array $tags array(array(
1766 * 'filter' => 'last-child',
1767 * 'params' => '123'))
1768 * @param array $custom_filters Custom map next to {@link $filter_map}
1769 * @return bool
1770 * @internal Used by selector class
1771 * @see match()
1772 * @access private
1773 */
1774 protected function match_filters($conditions, $custom_filters = array()) {
1775 foreach($conditions as $c) {
1776 $c['filter'] = strtolower($c['filter']);
1777 if (isset($this->filter_map[$c['filter']])) {
1778 if (!$this->{$this->filter_map[$c['filter']]}($c['params'])) {
1779 return false;
1780 }
1781 } elseif (isset($custom_filters[$c['filter']])) {
1782 if (!call_user_func($custom_filters[$c['filter']], $this, $c['params'])) {
1783 return false;
1784 }
1785 } else {
1786 // phpcs:ignore QITStandard.PHP.DebugCode.DebugFunctionFound
1787 trigger_error('Unknown filter "'.esc_html($c['filter']).'"!');
1788 return false;
1789 }
1790 }
1791
1792 return true;
1793 }
1794
1795 /**
1796 * Checks if node matches certain conditions
1797 * @param array $tags array('tags' => array(tag_conditions), 'attributes' => array(attr_conditions), 'filters' => array(filter_conditions))
1798 * @param array $match Should conditions evaluate to true?
1799 * @param array $custom_filters Custom map next to {@link $filter_map}
1800 * @return bool
1801 * @internal Used by selector class
1802 * @see match_tags();
1803 * @see match_attributes();
1804 * @see match_filters();
1805 * @access private
1806 */
1807 function match($conditions, $match = true, $custom_filters = array()) {
1808 $t = isset($conditions['tags']);
1809 $a = isset($conditions['attributes']);
1810 $f = isset($conditions['filters']);
1811
1812 if (!($t || $a || $f)) {
1813 if (is_array($conditions) && $conditions) {
1814 foreach($conditions as $c) {
1815 if ($this->match($c, $match)) {
1816 return true;
1817 }
1818 }
1819 }
1820
1821 return false;
1822 } else {
1823 if (($t && (!$this->match_tags($conditions['tags']))) === $match) {
1824 return false;
1825 }
1826
1827 if (($a && (!$this->match_attributes($conditions['attributes']))) === $match) {
1828 return false;
1829 }
1830
1831 if (($f && (!$this->match_filters($conditions['filters'], $custom_filters))) === $match) {
1832 return false;
1833 }
1834
1835 return true;
1836 }
1837 }
1838
1839 /**
1840 * Finds children that match a certain attribute
1841 * @param string $attribute
1842 * @param string $value
1843 * @param string $mode Compare mode, "equals", "|=", "contains_regex", etc.
1844 * @param string $compare "total"/"namespace"/"name"
1845 * @param bool|int $recursive
1846 * @return array
1847 */
1848 function getChildrenByAttribute($attribute, $value, $mode = 'equals', $compare = 'total', $recursive = true) {
1849 if ($this->childCount() < 1) {
1850 return array();
1851 }
1852
1853 $mode = explode(' ', strtolower($mode));
1854 $match = ((isset($mode[1]) && ($mode[1] === 'not')) ? 'false' : 'true');
1855
1856 return $this->getChildrenByMatch(
1857 array(
1858 'attributes' => array(
1859 $attribute => array(
1860 'operator_value' => $mode[0],
1861 'value' => $value,
1862 'match' => $match,
1863 'compare' => $compare
1864 )
1865 )
1866 ),
1867 $recursive
1868 );
1869 }
1870
1871 /**
1872 * Finds children that match a certain tag
1873 * @param string $tag
1874 * @param string $compare "total"/"namespace"/"name"
1875 * @param bool|int $recursive
1876 * @return array
1877 */
1878 function getChildrenByTag($tag, $compare = 'total', $recursive = true) {
1879 if ($this->childCount() < 1) {
1880 return array();
1881 }
1882
1883 $tag = explode(' ', strtolower($tag));
1884 $match = ((isset($tag[1]) && ($tag[1] === 'not')) ? 'false' : 'true');
1885
1886 return $this->getChildrenByMatch(
1887 array(
1888 'tags' => array(
1889 $tag[0] => array(
1890 'match' => $match,
1891 'compare' => $compare
1892 )
1893 )
1894 ),
1895 $recursive
1896 );
1897 }
1898
1899 /**
1900 * Finds all children using ID attribute
1901 * @param string $id
1902 * @param bool|int $recursive
1903 * @return array
1904 */
1905 function getChildrenByID($id, $recursive = true) {
1906 return $this->getChildrenByAttribute('id', $id, 'equals', 'total', $recursive);
1907 }
1908
1909 /**
1910 * Finds all children using class attribute
1911 * @param string $class
1912 * @param bool|int $recursive
1913 * @return array
1914 */
1915 function getChildrenByClass($class, $recursive = true) {
1916 return $this->getChildrenByAttribute('class', $class, 'equals', 'total', $recursive);
1917 }
1918
1919 /**
1920 * Finds all children using name attribute
1921 * @param string $name
1922 * @param bool|int $recursive
1923 * @return array
1924 */
1925 function getChildrenByName($name, $recursive = true) {
1926 return $this->getChildrenByAttribute('name', $name, 'equals', 'total', $recursive);
1927 }
1928
1929 /**
1930 * Performs a css query on the node.
1931 * @param string $query
1932 * @return IQuery Returns the matching nodes from the query.
1933 */
1934 public function query($query = '*') {
1935 $select = $this->select($query);
1936 $result = new pQuery((array)$select);
1937 return $result;
1938 }
1939
1940 /**
1941 * Performs css query on node
1942 * @param string $query
1943 * @param int|bool $index True to return node instead of array if only 1 match,
1944 * false to return array, int to return match at index, negative int to count from end
1945 * @param bool|int $recursive
1946 * @param bool $check_self Include this node in search or only search child nodes
1947 * @return DomNode[]|DomNode Returns an array of matching {@link DomNode} objects
1948 * or a single {@link DomNode} if `$index` is not false.
1949 */
1950 function select($query = '*', $index = false, $recursive = true, $check_self = false) {
1951 $s = new $this->selectClass($this, $query, $check_self, $recursive);
1952 $res = $s->result;
1953 unset($s);
1954 if (is_array($res) && ($index === true) && (count($res) === 1)) {
1955 return $res[0];
1956 } elseif (is_int($index) && is_array($res)) {
1957 if ($index < 0) {
1958 $index += count($res);
1959 }
1960 return ($index < count($res)) ? $res[$index] : null;
1961 } else {
1962 return $res;
1963 }
1964 }
1965
1966 /**
1967 * Checks if node matches css query filter ":root"
1968 * @return bool
1969 * @see match()
1970 * @access private
1971 */
1972 protected function filter_root() {
1973 return (strtolower($this->tag) === 'html');
1974 }
1975
1976 /**
1977 * Checks if node matches css query filter ":nth-child(n)"
1978 * @param string $n 1-based index
1979 * @return bool
1980 * @see match()
1981 * @access private
1982 */
1983 protected function filter_nchild($n) {
1984 return ($this->index(false)+1 === (int) $n);
1985 }
1986
1987 /**
1988 * Checks if node matches css query filter ":gt(n)"
1989 * @param string $n 0-based index
1990 * @return bool
1991 * @see match()
1992 * @access private
1993 */
1994 protected function filter_gt($n) {
1995 return ($this->index(false) > (int) $n);
1996 }
1997
1998 /**
1999 * Checks if node matches css query filter ":lt(n)"
2000 * @param string $n 0-based index
2001 * @return bool
2002 * @see match()
2003 * @access private
2004 */
2005 protected function filter_lt($n) {
2006 return ($this->index(false) < (int) $n);
2007 }
2008
2009 /**
2010 * Checks if node matches css query filter ":nth-last-child(n)"
2011 * @param string $n 1-based index
2012 * @return bool
2013 * @see match()
2014 * @access private
2015 */
2016 protected function filter_nlastchild($n) {
2017 if ($this->parent === null) {
2018 return false;
2019 } else {
2020 return ($this->parent->childCount(true) - $this->index(false) === (int) $n);
2021 }
2022 }
2023
2024 /**
2025 * Checks if node matches css query filter ":nth-of-type(n)"
2026 * @param string $n 1-based index
2027 * @return bool
2028 * @see match()
2029 * @access private
2030 */
2031 protected function filter_ntype($n) {
2032 return ($this->typeIndex()+1 === (int) $n);
2033 }
2034
2035 /**
2036 * Checks if node matches css query filter ":nth-last-of-type(n)"
2037 * @param string $n 1-based index
2038 * @return bool
2039 * @see match()
2040 * @access private
2041 */
2042 protected function filter_nlastype($n) {
2043 if ($this->parent === null) {
2044 return false;
2045 } else {
2046 return (count($this->parent->getChildrenByTag($this->tag, 'total', false)) - $this->typeIndex() === (int) $n);
2047 }
2048 }
2049
2050 /**
2051 * Checks if node matches css query filter ":odd"
2052 * @return bool
2053 * @see match()
2054 * @access private
2055 */
2056 protected function filter_odd() {
2057 return (($this->index(false) & 1) === 1);
2058 }
2059
2060 /**
2061 * Checks if node matches css query filter ":even"
2062 * @return bool
2063 * @see match()
2064 * @access private
2065 */
2066 protected function filter_even() {
2067 return (($this->index(false) & 1) === 0);
2068 }
2069
2070 /**
2071 * Checks if node matches css query filter ":every(n)"
2072 * @return bool
2073 * @see match()
2074 * @access private
2075 */
2076 protected function filter_every($n) {
2077 return (($this->index(false) % (int) $n) === 0);
2078 }
2079
2080 /**
2081 * Checks if node matches css query filter ":first"
2082 * @return bool
2083 * @see match()
2084 * @access private
2085 */
2086 protected function filter_first() {
2087 return ($this->index(false) === 0);
2088 }
2089
2090 /**
2091 * Checks if node matches css query filter ":last"
2092 * @return bool
2093 * @see match()
2094 * @access private
2095 */
2096 protected function filter_last() {
2097 if ($this->parent === null) {
2098 return false;
2099 } else {
2100 return ($this->parent->childCount(true) - 1 === $this->index(false));
2101 }
2102 }
2103
2104 /**
2105 * Checks if node matches css query filter ":first-of-type"
2106 * @return bool
2107 * @see match()
2108 * @access private
2109 */
2110 protected function filter_firsttype() {
2111 return ($this->typeIndex() === 0);
2112 }
2113
2114 /**
2115 * Checks if node matches css query filter ":last-of-type"
2116 * @return bool
2117 * @see match()
2118 * @access private
2119 */
2120 protected function filter_lasttype() {
2121 if ($this->parent === null) {
2122 return false;
2123 } else {
2124 return (count($this->parent->getChildrenByTag($this->tag, 'total', false)) - 1 === $this->typeIndex());
2125 }
2126 }
2127
2128 /**
2129 * Checks if node matches css query filter ":only-child"
2130 * @return bool
2131 * @see match()
2132 * @access private
2133 */
2134 protected function filter_onlychild() {
2135 if ($this->parent === null) {
2136 return false;
2137 } else {
2138 return ($this->parent->childCount(true) === 1);
2139 }
2140 }
2141
2142 /**
2143 * Checks if node matches css query filter ":only-of-type"
2144 * @return bool
2145 * @see match()
2146 * @access private
2147 */
2148 protected function filter_onlytype() {
2149 if ($this->parent === null) {
2150 return false;
2151 } else {
2152 return (count($this->parent->getChildrenByTag($this->tag, 'total', false)) === 1);
2153 }
2154 }
2155
2156 /**
2157 * Checks if node matches css query filter ":empty"
2158 * @return bool
2159 * @see match()
2160 * @access private
2161 */
2162 protected function filter_empty() {
2163 return ($this->childCount() === 0);
2164 }
2165
2166 /**
2167 * Checks if node matches css query filter ":not-empty"
2168 * @return bool
2169 * @see match()
2170 * @access private
2171 */
2172 protected function filter_notempty() {
2173 return ($this->childCount() !== 0);
2174 }
2175
2176 /**
2177 * Checks if node matches css query filter ":has-text"
2178 * @return bool
2179 * @see match()
2180 * @access private
2181 */
2182 protected function filter_hastext() {
2183 return ($this->getPlainText() !== '');
2184 }
2185
2186 /**
2187 * Checks if node matches css query filter ":no-text"
2188 * @return bool
2189 * @see match()
2190 * @access private
2191 */
2192 protected function filter_notext() {
2193 return ($this->getPlainText() === '');
2194 }
2195
2196 /**
2197 * Checks if node matches css query filter ":lang(s)"
2198 * @param string $lang
2199 * @return bool
2200 * @see match()
2201 * @access private
2202 */
2203 protected function filter_lang($lang) {
2204 return ($this->lang === $lang);
2205 }
2206
2207 /**
2208 * Checks if node matches css query filter ":contains(s)"
2209 * @param string $text
2210 * @return bool
2211 * @see match()
2212 * @access private
2213 */
2214 protected function filter_contains($text) {
2215 return (strpos($this->getPlainTextUTF8(), $text) !== false);
2216 }
2217
2218 /**
2219 * Checks if node matches css query filter ":has(s)"
2220 * @param string $selector
2221 * @return bool
2222 * @see match()
2223 * @access private
2224 */
2225 protected function filter_has($selector) {
2226 $s = $this->select((string) $selector, false);
2227 return (is_array($s) && (count($s) > 0));
2228 }
2229
2230 /**
2231 * Checks if node matches css query filter ":not(s)"
2232 * @param string $selector
2233 * @return bool
2234 * @see match()
2235 * @access private
2236 */
2237 protected function filter_not($selector) {
2238 $s = $this->select((string) $selector, false, true, true);
2239 return ((!is_array($s)) || (array_search($this, $s, true) === false));
2240 }
2241
2242 /**
2243 * Checks if node matches css query filter ":element"
2244 * @return bool
2245 * @see match()
2246 * @access private
2247 */
2248 protected function filter_element() {
2249 return true;
2250 }
2251
2252 /**
2253 * Checks if node matches css query filter ":text"
2254 * @return bool
2255 * @see match()
2256 * @access private
2257 */
2258 protected function filter_text() {
2259 return false;
2260 }
2261
2262 /**
2263 * Checks if a node matches css query filter ":checked"
2264 * @return bool
2265 * @see match()
2266 */
2267 protected function filter_checked() {
2268 $attr = $this->getAttribute('checked');
2269 if (is_array($attr))
2270 $attr = reset($attr);
2271 return strcasecmp($attr, 'checked') === 0;
2272 }
2273
2274 /**
2275 * Checks if node matches css query filter ":comment"
2276 * @return bool
2277 * @see match()
2278 * @access private
2279 */
2280 protected function filter_comment() {
2281 return false;
2282 }
2283
2284 /**
2285 * Checks if a node matches css query filter ":selected"
2286 * @return bool
2287 * @see match()
2288 */
2289 protected function filter_selected() {
2290 $attr = $this->getAttribute('selected');
2291 if (is_array($attr))
2292 $attr = reset($attr);
2293
2294 return strcasecmp($attr, 'selected') === 0;
2295 }
2296
2297 public function after($content) {
2298 $offset = $this->index() + 1;
2299 $parent = $this->parent;
2300 $nodes = $this->createNodes($content);
2301
2302 foreach ($nodes as $node) {
2303 $node->changeParent($parent, $offset);
2304 }
2305 return $this;
2306 }
2307
2308
2309 /**
2310 * Create a {@link DomNode} from its string representation.
2311 * @param string|DomNode $content
2312 * @return DomNode
2313 */
2314 protected function createNode($content) {
2315 $nodes = $this->createNodes($content);
2316 return reset($nodes);
2317 }
2318
2319 /**
2320 * Create an array of {@link DomNode} objects from their string representation.
2321 * @param string|DomNode $content
2322 * @return DomNode[]
2323 */
2324 protected function createNodes($content) {
2325 if (is_string($content)) {
2326 if (strpos($content, ' ') === false) {
2327 $nodes = array(new $this->childClass($content, $this));
2328 } else {
2329 $node = new $this->parserClass($content);
2330 $nodes = $node->root->children;
2331 }
2332 } else {
2333 $nodes = (array)$content;
2334 }
2335 return $nodes;
2336 }
2337
2338 public function append($content) {
2339 $nodes = $this->createNodes($content);
2340 foreach ($nodes as $node) {
2341 $node->changeParent($this);
2342 }
2343 return $this;
2344 }
2345
2346 public function attr($name, $value = null) {
2347 if ($value === null)
2348 return $this->getAttribute($name);
2349
2350 $this->setAttribute($name, $value);
2351 return $this;
2352 }
2353
2354 public function before($content) {
2355 $offset = $this->index();
2356 $parent = $this->parent;
2357 $nodes = $this->createNodes($content);
2358
2359 foreach ($nodes as $node) {
2360 $node->changeParent($parent, $offset);
2361 }
2362
2363 return $this;
2364 }
2365
2366 public function count(): int {
2367 return 1;
2368 }
2369
2370 // public function css($name, $value = null) {
2371 //
2372 // }
2373
2374 public function prepend($content = null) {
2375 $offset = 0;
2376 $parent = $this;
2377 $nodes = $this->createNodes($content);
2378
2379 foreach ($nodes as $node) {
2380 $node->changeParent($parent, $offset);
2381 }
2382
2383 return $this;
2384 }
2385
2386 public function prop($name, $value = null) {
2387 switch (strtolower($name)) {
2388 case 'checked':
2389 case 'disabled':
2390 case 'selected':
2391 if ($value !== null) {
2392 if ($value) {
2393 $this->attr($name, $name);
2394 } else {
2395 $this->removeAttr($name);
2396 }
2397 return $this;
2398 }
2399 return $this->attr($name) == $name;
2400 case 'tagname':
2401 return $this->tagName($value);
2402 }
2403 // The property is not supported, degrade gracefully
2404 if ($value === null)
2405 return $this;
2406 else
2407 return null;
2408 }
2409
2410 public function remove($selector = null) {
2411 if ($selector == null) {
2412 $this->delete();
2413 } else {
2414 $nodes = (array)$this->select($selector);
2415 foreach ($nodes as $node) {
2416 $node->delete();
2417 }
2418 }
2419 }
2420
2421 public function removeAttr($name) {
2422 $this->deleteAttribute($name);
2423
2424 return $this;
2425 }
2426
2427 function replaceWith($content) {
2428 $node_index = $this->index();
2429
2430 // Add the new node.
2431 $node = $this->createNode($content);
2432 $node->changeParent($this->parent, $node_index);
2433
2434 // Remove this node.
2435 $this->remove();
2436
2437 return $node;
2438 }
2439
2440 /**
2441 * @param type $value
2442 * @return string|DomNode
2443 */
2444 public function tagName($value = null) {
2445 if ($value !== null) {
2446 $this->setTag($value);
2447 return $this;
2448 }
2449 return $this->getTag();
2450 }
2451
2452 public function text($value = null) {
2453 if ($value === null)
2454 return $this->getPlainText();
2455
2456 $this->setPlainText($value);
2457 return $this;
2458 }
2459
2460 public function toggleClass($classname, $switch = null) {
2461 if ($switch === true) {
2462 $this->addClass($classname);
2463 } elseif ($switch === false) {
2464 $this->removeClass($classname);
2465 } else {
2466 if ($this->hasClass($classname))
2467 $this->removeClass($classname);
2468 else
2469 $this->addClass($classname);
2470 }
2471 return $this;
2472 }
2473
2474 public function unwrap() {
2475 $this->parent->detach(true);
2476 return $this;
2477 }
2478
2479 public function val($value = null) {
2480 switch (strtolower($this->tag)) {
2481 case 'select':
2482 if ($value === null) {
2483 // Return the value of a selected child.
2484 return $this->query('option:selected')->attr('value');
2485 } else {
2486 // Select the option with the right value and deselect the others.
2487 foreach ($this->query('option') as $option) {
2488 if ($option->attr('value') == $value) {
2489 $option->attr('selected', 'selected');
2490 } else {
2491 $option->removeAttr('selected');
2492 }
2493 }
2494 return $this;
2495 }
2496 case 'textarea':
2497 if ($value === null) {
2498 // Return the contents of the textarea.
2499 return $this->getInnerText();
2500 } else {
2501 // Set the contents of the textarea.
2502 $this->setInnerText($value);
2503 return $this;
2504 }
2505 case 'input':
2506 switch (strtolower($this->getAttribute('type'))) {
2507 case 'checkbox':
2508 if ($value === null)
2509 return $this->prop('checked') ? $this->getAttribute('value') : null;
2510 else {
2511 if (!$value) {
2512 $this->deleteAttribute('checked');
2513 } else {
2514 $this->setAttribute('value', $value);
2515 $this->setAttribute('checked', 'checked');
2516 }
2517 return $this;
2518 }
2519 }
2520 }
2521
2522 // Other node types can just get/set the value attribute.
2523 if ($value !== null) {
2524 $this->setAttribute('value', $value);
2525 return $this;
2526 }
2527 return $this->getAttribute('value');
2528 }
2529
2530 }
2531
2532 /**
2533 * Node subclass for text
2534 */
2535 class TextNode extends DomNode {
2536 #php4 Compatibility with PHP4, this gets changed to a regular var in release tool
2537 #static $NODE_TYPE = self::NODE_TEXT;
2538 #php4e
2539 #php5
2540 const NODE_TYPE = self::NODE_TEXT;
2541 #php5e
2542 var $tag = '~text~';
2543
2544 /**
2545 * @var string
2546 */
2547 var $text = '';
2548
2549 /**
2550 * Class constructor
2551 * @param DomNode $parent
2552 * @param string $text
2553 */
2554 function __construct($parent, $text = '') {
2555 $this->parent = $parent;
2556 $this->text = $text;
2557 }
2558
2559 #php4 PHP4 class constructor compatibility
2560 #function TextNode($parent, $text = '') {return $this->__construct($parent, $text);}
2561 #php4e
2562
2563 function isText() {return true;}
2564 function isTextOrComment() {return true;}
2565 protected function filter_element() {return false;}
2566 protected function filter_text() {return true;}
2567 function toString_attributes() {return '';}
2568 function toString_content($attributes = true, $recursive = true, $content_only = false) {return $this->text;}
2569 function toString($attributes = true, $recursive = true, $content_only = false) {return $this->text;}
2570
2571 /**
2572 * {@inheritdoc}
2573 */
2574 public function text($value = null) {
2575 if ($value !== null) {
2576 $this->text = $value;
2577 return $this;
2578 }
2579 return $this->text;
2580 }
2581
2582 /**
2583 * {@inheritdoc}
2584 */
2585 public function html($value = null) {
2586 if ($value !== null) {
2587 $this->text = $value;
2588 return $this;
2589 }
2590 return $this->text;
2591 }
2592 }
2593
2594 /**
2595 * Node subclass for comments
2596 */
2597 class CommentNode extends DomNode {
2598 #php4 Compatibility with PHP4, this gets changed to a regular var in release tool
2599 #static $NODE_TYPE = self::NODE_COMMENT;
2600 #php4e
2601 #php5
2602 const NODE_TYPE = self::NODE_COMMENT;
2603 #php5e
2604 var $tag = '~comment~';
2605
2606 /**
2607 * @var string
2608 */
2609 var $text = '';
2610
2611 /**
2612 * Class constructor
2613 * @param DomNode $parent
2614 * @param string $text
2615 */
2616 function __construct($parent, $text = '') {
2617 $this->parent = $parent;
2618 $this->text = $text;
2619 }
2620
2621 #php4 PHP4 class constructor compatibility
2622 #function CommentNode($parent, $text = '') {return $this->__construct($parent, $text);}
2623 #php4e
2624
2625 function isComment() {return true;}
2626 function isTextOrComment() {return true;}
2627 protected function filter_element() {return false;}
2628 protected function filter_comment() {return true;}
2629 function toString_attributes() {return '';}
2630 function toString_content($attributes = true, $recursive = true, $content_only = false) {return $this->text;}
2631 function toString($attributes = true, $recursive = true, $content_only = false) {return '<!--'.$this->text.'-->';}
2632 }
2633
2634 /**
2635 * Node subclass for conditional tags
2636 */
2637 class ConditionalTagNode extends DomNode {
2638 #php4 Compatibility with PHP4, this gets changed to a regular var in release tool
2639 #static $NODE_TYPE = self::NODE_CONDITIONAL;
2640 #php4e
2641 #php5
2642 const NODE_TYPE = self::NODE_CONDITIONAL;
2643 #php5e
2644 var $tag = '~conditional~';
2645
2646 /**
2647 * @var string
2648 */
2649 var $condition = '';
2650
2651 /**
2652 * Class constructor
2653 * @param DomNode $parent
2654 * @param string $condition e.g. "if IE"
2655 * @param bool $hidden <!--[if if true, <![if if false
2656 */
2657 function __construct($parent, $condition = '', $hidden = true) {
2658 $this->parent = $parent;
2659 $this->hidden = $hidden;
2660 $this->condition = $condition;
2661 }
2662
2663 #php4 PHP4 class constructor compatibility
2664 #function ConditionalTagNode($parent, $condition = '', $hidden = true) {return $this->__construct($parent, $condition, $hidden);}
2665 #php4e
2666
2667 protected function filter_element() {return false;}
2668 function toString_attributes() {return '';}
2669 function toString($attributes = true, $recursive = true, $content_only = false) {
2670 if ($content_only) {
2671 if (is_int($content_only)) {
2672 --$content_only;
2673 }
2674 return $this->toString_content($attributes, $recursive, $content_only);
2675 }
2676
2677 $s = '<!'.(($this->hidden) ? '--' : '').'['.$this->condition.']>';
2678 if($recursive) {
2679 $s .= $this->toString_content($attributes);
2680 }
2681 $s .= '<![endif]'.(($this->hidden) ? '--' : '').'>';
2682 return $s;
2683 }
2684 }
2685
2686 /**
2687 * Node subclass for CDATA tags
2688 */
2689 class CdataNode extends DomNode {
2690 #php4 Compatibility with PHP4, this gets changed to a regular var in release tool
2691 #static $NODE_TYPE = self::NODE_CDATA;
2692 #php4e
2693 #php5
2694 const NODE_TYPE = self::NODE_CDATA;
2695 #php5e
2696 var $tag = '~cdata~';
2697
2698 /**
2699 * @var string
2700 */
2701 var $text = '';
2702
2703 /**
2704 * Class constructor
2705 * @param DomNode $parent
2706 * @param string $text
2707 */
2708 function __construct($parent, $text = '') {
2709 $this->parent = $parent;
2710 $this->text = $text;
2711 }
2712
2713 #php4 PHP4 class constructor compatibility
2714 #function CdataNode($parent, $text = '') {return $this->__construct($parent, $text);}
2715 #php4e
2716
2717 protected function filter_element() {return false;}
2718 function toString_attributes() {return '';}
2719 function toString_content($attributes = true, $recursive = true, $content_only = false) {return $this->text;}
2720 function toString($attributes = true, $recursive = true, $content_only = false) {return '<![CDATA['.$this->text.']]>';}
2721 }
2722
2723 /**
2724 * Node subclass for doctype tags
2725 */
2726 class DoctypeNode extends DomNode {
2727 #php4 Compatibility with PHP4, this gets changed to a regular var in release tool
2728 #static $NODE_TYPE = self::NODE_DOCTYPE;
2729 #php4e
2730 #php5
2731 const NODE_TYPE = self::NODE_DOCTYPE;
2732 #php5e
2733 var $tag = '!DOCTYPE';
2734
2735 /**
2736 * @var string
2737 */
2738 var $dtd = '';
2739
2740 /**
2741 * Class constructor
2742 * @param DomNode $parent
2743 * @param string $dtd
2744 */
2745 function __construct($parent, $dtd = '') {
2746 $this->parent = $parent;
2747 $this->dtd = $dtd;
2748 }
2749
2750 #php4 PHP4 class constructor compatibility
2751 #function DoctypeNode($parent, $dtd = '') {return $this->__construct($parent, $dtd);}
2752 #php4e
2753
2754 protected function filter_element() {return false;}
2755 function toString_attributes() {return '';}
2756 function toString_content($attributes = true, $recursive = true, $content_only = false) {return $this->text;}
2757 function toString($attributes = true, $recursive = true, $content_only = false) {return '<'.$this->tag.' '.$this->dtd.'>';}
2758 }
2759
2760 /**
2761 * Node subclass for embedded tags like xml, php and asp
2762 */
2763 class EmbeddedNode extends DomNode {
2764
2765 /**
2766 * @var string
2767 * @internal specific char for tags, like ? for php and % for asp
2768 * @access private
2769 */
2770 var $tag_char = '';
2771
2772 /**
2773 * @var string
2774 */
2775 var $text = '';
2776
2777 /**
2778 * Class constructor
2779 * @param DomNode $parent
2780 * @param string $tag_char {@link $tag_char}
2781 * @param string $tag {@link $tag}
2782 * @param string $text
2783 * @param array $attributes array('attr' => 'val')
2784 */
2785 function __construct($parent, $tag_char = '', $tag = '', $text = '', $attributes = array()) {
2786 $this->parent = $parent;
2787 $this->tag_char = $tag_char;
2788 if ($tag[0] !== $this->tag_char) {
2789 $tag = $this->tag_char.$tag;
2790 }
2791 $this->tag = $tag;
2792 $this->text = $text;
2793 $this->attributes = $attributes;
2794 $this->self_close_str = $tag_char;
2795 }
2796
2797 #php4 PHP4 class constructor compatibility
2798 #function EmbeddedNode($parent, $tag_char = '', $tag = '', $text = '', $attributes = array()) {return $this->__construct($parent, $tag_char, $tag, $text, $attributes);}
2799 #php4e
2800
2801 protected function filter_element() {return false;}
2802 function toString($attributes = true, $recursive = true, $content_only = false) {
2803 $s = '<'.$this->tag;
2804 if ($attributes) {
2805 $s .= $this->toString_attributes();
2806 }
2807 $s .= $this->text.$this->self_close_str.'>';
2808 return $s;
2809 }
2810 }
2811
2812 /**
2813 * Node subclass for "?" tags, like php and xml
2814 */
2815 class XmlNode extends EmbeddedNode {
2816 #php4 Compatibility with PHP4, this gets changed to a regular var in release tool
2817 #static $NODE_TYPE = self::NODE_XML;
2818 #php4e
2819 #php5
2820 const NODE_TYPE = self::NODE_XML;
2821 #php5e
2822
2823 /**
2824 * Class constructor
2825 * @param DomNode $parent
2826 * @param string $tag {@link $tag}
2827 * @param string $text
2828 * @param array $attributes array('attr' => 'val')
2829 */
2830 function __construct($parent, $tag = 'xml', $text = '', $attributes = array()) {
2831 return parent::__construct($parent, '?', $tag, $text, $attributes);
2832 }
2833
2834 #php4 PHP4 class constructor compatibility
2835 #function XmlNode($parent, $tag = 'xml', $text = '', $attributes = array()) {return $this->__construct($parent, $tag, $text, $attributes);}
2836 #php4e
2837 }
2838
2839 /**
2840 * Node subclass for asp tags
2841 */
2842 class AspEmbeddedNode extends EmbeddedNode {
2843 #php4 Compatibility with PHP4, this gets changed to a regular var in release tool
2844 #static $NODE_TYPE = self::NODE_ASP;
2845 #php4e
2846 #php5
2847 const NODE_TYPE = self::NODE_ASP;
2848 #php5e
2849
2850 /**
2851 * Class constructor
2852 * @param DomNode $parent
2853 * @param string $tag {@link $tag}
2854 * @param string $text
2855 * @param array $attributes array('attr' => 'val')
2856 */
2857 function __construct($parent, $tag = '', $text = '', $attributes = array()) {
2858 return parent::__construct($parent, '%', $tag, $text, $attributes);
2859 }
2860
2861 #php4 PHP4 class constructor compatibility
2862 #function AspEmbeddedNode($parent, $tag = '', $text = '', $attributes = array()) {return $this->__construct($parent, $tag, $text, $attributes);}
2863 #php4e
2864 }
2865