Passed
Pull Request — master (#12)
by Jake
02:34
created
src/ExtensionRegistry.php 1 patch
Indentation   +112 added lines, -112 removed lines patch added patch discarded remove patch
@@ -28,124 +28,124 @@
 block discarded – undo
28 28
 class ExtensionRegistry
29 29
 {
30 30
 
31
-	/**
32
-	 * Internal flag indicating whether or not the registry should
33
-	 * be used for automatic extension loading. If this is false, then
34
-	 * implementations should not automatically load extensions.
35
-	 */
36
-	public static $useRegistry = true;
37
-	/**
38
-	 * The extension registry. This should consist of an array of class
39
-	 * names.
40
-	 */
41
-	protected static $extensionRegistry = [];
42
-	protected static $extensionMethodRegistry = [];
31
+    /**
32
+     * Internal flag indicating whether or not the registry should
33
+     * be used for automatic extension loading. If this is false, then
34
+     * implementations should not automatically load extensions.
35
+     */
36
+    public static $useRegistry = true;
37
+    /**
38
+     * The extension registry. This should consist of an array of class
39
+     * names.
40
+     */
41
+    protected static $extensionRegistry = [];
42
+    protected static $extensionMethodRegistry = [];
43 43
 
44
-	/**
45
-	 * Extend a Query with the given extension class.
46
-	 */
47
-	public static function extend($classname)
48
-	{
49
-		self::$extensionRegistry[] = $classname;
50
-		$class                     = new ReflectionClass($classname);
51
-		$methods                   = $class->getMethods();
52
-		foreach ($methods as $method) {
53
-			self::$extensionMethodRegistry[$method->getName()] = $classname;
54
-		}
55
-	}
44
+    /**
45
+     * Extend a Query with the given extension class.
46
+     */
47
+    public static function extend($classname)
48
+    {
49
+        self::$extensionRegistry[] = $classname;
50
+        $class                     = new ReflectionClass($classname);
51
+        $methods                   = $class->getMethods();
52
+        foreach ($methods as $method) {
53
+            self::$extensionMethodRegistry[$method->getName()] = $classname;
54
+        }
55
+    }
56 56
 
57
-	/**
58
-	 * Check to see if a method is known.
59
-	 * This checks to see if the given method name belongs to one of the
60
-	 * registered extensions. If it does, then this will return TRUE.
61
-	 *
62
-	 * @param string $name
63
-	 *  The name of the method to search for.
64
-	 *
65
-	 * @return boolean
66
-	 *  TRUE if the method exists, false otherwise.
67
-	 */
68
-	public static function hasMethod($name)
69
-	{
70
-		return isset(self::$extensionMethodRegistry[$name]);
71
-	}
57
+    /**
58
+     * Check to see if a method is known.
59
+     * This checks to see if the given method name belongs to one of the
60
+     * registered extensions. If it does, then this will return TRUE.
61
+     *
62
+     * @param string $name
63
+     *  The name of the method to search for.
64
+     *
65
+     * @return boolean
66
+     *  TRUE if the method exists, false otherwise.
67
+     */
68
+    public static function hasMethod($name)
69
+    {
70
+        return isset(self::$extensionMethodRegistry[$name]);
71
+    }
72 72
 
73
-	/**
74
-	 * Check to see if the given extension class is registered.
75
-	 * Given a class name for a QueryPath::Extension class, this
76
-	 * will check to see if that class is registered. If so, it will return
77
-	 * TRUE.
78
-	 *
79
-	 * @param string $name
80
-	 *  The name of the class.
81
-	 *
82
-	 * @return boolean
83
-	 *  TRUE if the class is registered, FALSE otherwise.
84
-	 */
85
-	public static function hasExtension($name)
86
-	{
87
-		return in_array($name, self::$extensionRegistry);
88
-	}
73
+    /**
74
+     * Check to see if the given extension class is registered.
75
+     * Given a class name for a QueryPath::Extension class, this
76
+     * will check to see if that class is registered. If so, it will return
77
+     * TRUE.
78
+     *
79
+     * @param string $name
80
+     *  The name of the class.
81
+     *
82
+     * @return boolean
83
+     *  TRUE if the class is registered, FALSE otherwise.
84
+     */
85
+    public static function hasExtension($name)
86
+    {
87
+        return in_array($name, self::$extensionRegistry);
88
+    }
89 89
 
90
-	/**
91
-	 * Get the class that a given method belongs to.
92
-	 * Given a method name, this will check all registered extension classes
93
-	 * to see if any of them has the named method. If so, this will return
94
-	 * the classname.
95
-	 *
96
-	 * Note that if two extensions are registered that contain the same
97
-	 * method name, the last one registred will be the only one recognized.
98
-	 *
99
-	 * @param string $name
100
-	 *  The name of the method.
101
-	 *
102
-	 * @return string
103
-	 *  The name of the class.
104
-	 */
105
-	public static function getMethodClass($name)
106
-	{
107
-		return self::$extensionMethodRegistry[$name];
108
-	}
90
+    /**
91
+     * Get the class that a given method belongs to.
92
+     * Given a method name, this will check all registered extension classes
93
+     * to see if any of them has the named method. If so, this will return
94
+     * the classname.
95
+     *
96
+     * Note that if two extensions are registered that contain the same
97
+     * method name, the last one registred will be the only one recognized.
98
+     *
99
+     * @param string $name
100
+     *  The name of the method.
101
+     *
102
+     * @return string
103
+     *  The name of the class.
104
+     */
105
+    public static function getMethodClass($name)
106
+    {
107
+        return self::$extensionMethodRegistry[$name];
108
+    }
109 109
 
110
-	/**
111
-	 * Get extensions for the given Query object.
112
-	 *
113
-	 * Given a Query object, this will return
114
-	 * an associative array of extension names to (new) instances.
115
-	 * Generally, this is intended to be used internally.
116
-	 *
117
-	 * @param Query $qp
118
-	 *  The Query into which the extensions should be registered.
119
-	 *
120
-	 * @return array
121
-	 *  An associative array of classnames to instances.
122
-	 */
123
-	public static function getExtensions(Query $qp)
124
-	{
125
-		$extInstances = [];
126
-		foreach (self::$extensionRegistry as $ext) {
127
-			$extInstances[$ext] = new $ext($qp);
128
-		}
110
+    /**
111
+     * Get extensions for the given Query object.
112
+     *
113
+     * Given a Query object, this will return
114
+     * an associative array of extension names to (new) instances.
115
+     * Generally, this is intended to be used internally.
116
+     *
117
+     * @param Query $qp
118
+     *  The Query into which the extensions should be registered.
119
+     *
120
+     * @return array
121
+     *  An associative array of classnames to instances.
122
+     */
123
+    public static function getExtensions(Query $qp)
124
+    {
125
+        $extInstances = [];
126
+        foreach (self::$extensionRegistry as $ext) {
127
+            $extInstances[$ext] = new $ext($qp);
128
+        }
129 129
 
130
-		return $extInstances;
131
-	}
130
+        return $extInstances;
131
+    }
132 132
 
133
-	public static function extensionNames(): array
134
-	{
135
-		return self::$extensionRegistry;
136
-	}
133
+    public static function extensionNames(): array
134
+    {
135
+        return self::$extensionRegistry;
136
+    }
137 137
 
138
-	/**
139
-	 * Enable or disable automatic extension loading.
140
-	 *
141
-	 * If extension autoloading is disabled, then QueryPath will not
142
-	 * automatically load all registred extensions when a new Query
143
-	 * object is created using qp().
144
-	 *
145
-	 * @param bool $boolean
146
-	 */
147
-	public static function autoloadExtensions($boolean = true): void
148
-	{
149
-		self::$useRegistry = $boolean;
150
-	}
138
+    /**
139
+     * Enable or disable automatic extension loading.
140
+     *
141
+     * If extension autoloading is disabled, then QueryPath will not
142
+     * automatically load all registred extensions when a new Query
143
+     * object is created using qp().
144
+     *
145
+     * @param bool $boolean
146
+     */
147
+    public static function autoloadExtensions($boolean = true): void
148
+    {
149
+        self::$useRegistry = $boolean;
150
+    }
151 151
 }
Please login to merge, or discard this patch.
src/Query.php 1 patch
Indentation   +8 added lines, -8 removed lines patch added patch discarded remove patch
@@ -13,19 +13,19 @@
 block discarded – undo
13 13
 interface Query
14 14
 {
15 15
 
16
-	public function __construct($document = null, $selector = null, $options = []);
16
+    public function __construct($document = null, $selector = null, $options = []);
17 17
 
18
-	public function find($selector);
18
+    public function find($selector);
19 19
 
20
-	public function top($selector = null);
20
+    public function top($selector = null);
21 21
 
22
-	public function next($selector = null);
22
+    public function next($selector = null);
23 23
 
24
-	public function prev($selector = null);
24
+    public function prev($selector = null);
25 25
 
26
-	public function siblings($selector = null);
26
+    public function siblings($selector = null);
27 27
 
28
-	public function parent($selector = null);
28
+    public function parent($selector = null);
29 29
 
30
-	public function children($selector = null);
30
+    public function children($selector = null);
31 31
 }
Please login to merge, or discard this patch.
src/qp_functions.php 1 patch
Indentation   +3 added lines, -3 removed lines patch added patch discarded remove patch
@@ -157,7 +157,7 @@  discard block
 block discarded – undo
157 157
  */
158 158
 function qp($document = null, $string = '', array $options = [])
159 159
 {
160
-	return QueryPath::with($document, $string, $options);
160
+    return QueryPath::with($document, $string, $options);
161 161
 }
162 162
 
163 163
 /**
@@ -193,7 +193,7 @@  discard block
 block discarded – undo
193 193
 function htmlqp($document = null, $selector = '', $options = [])
194 194
 {
195 195
 
196
-	return QueryPath::withHTML($document, $selector, $options);
196
+    return QueryPath::withHTML($document, $selector, $options);
197 197
 }
198 198
 
199 199
 /**
@@ -222,5 +222,5 @@  discard block
 block discarded – undo
222 222
  */
223 223
 function html5qp($document = null, $selector = null, array $options = [])
224 224
 {
225
-	return QueryPath::withHTML5($document, $selector, $options);
225
+    return QueryPath::withHTML5($document, $selector, $options);
226 226
 }
Please login to merge, or discard this patch.
src/Extension.php 1 patch
Indentation   +1 added lines, -1 removed lines patch added patch discarded remove patch
@@ -93,5 +93,5 @@
 block discarded – undo
93 93
  */
94 94
 interface Extension
95 95
 {
96
-	public function __construct(Query $qp);
96
+    public function __construct(Query $qp);
97 97
 }
Please login to merge, or discard this patch.
src/Options.php 1 patch
Indentation   +56 added lines, -56 removed lines patch added patch discarded remove patch
@@ -31,64 +31,64 @@
 block discarded – undo
31 31
 class Options
32 32
 {
33 33
 
34
-	/**
35
-	 * This is the static options array.
36
-	 *
37
-	 * Use the {@link set()}, {@link get()}, and {@link merge()} to
38
-	 * modify this array.
39
-	 */
40
-	static $options = [];
34
+    /**
35
+     * This is the static options array.
36
+     *
37
+     * Use the {@link set()}, {@link get()}, and {@link merge()} to
38
+     * modify this array.
39
+     */
40
+    static $options = [];
41 41
 
42
-	/**
43
-	 * Set the default options.
44
-	 *
45
-	 * The passed-in array will be used as the default options list.
46
-	 *
47
-	 * @param array $array
48
-	 *  An associative array of options.
49
-	 */
50
-	static function set($array)
51
-	{
52
-		self::$options = $array;
53
-	}
42
+    /**
43
+     * Set the default options.
44
+     *
45
+     * The passed-in array will be used as the default options list.
46
+     *
47
+     * @param array $array
48
+     *  An associative array of options.
49
+     */
50
+    static function set($array)
51
+    {
52
+        self::$options = $array;
53
+    }
54 54
 
55
-	/**
56
-	 * Get the default options.
57
-	 *
58
-	 * Get all options currently set as default.
59
-	 *
60
-	 * @return array
61
-	 *  An array of options. Note that only explicitly set options are
62
-	 *  returned. {@link QueryPath} defines default options which are not
63
-	 *  stored in this object.
64
-	 */
65
-	static function get()
66
-	{
67
-		return self::$options;
68
-	}
55
+    /**
56
+     * Get the default options.
57
+     *
58
+     * Get all options currently set as default.
59
+     *
60
+     * @return array
61
+     *  An array of options. Note that only explicitly set options are
62
+     *  returned. {@link QueryPath} defines default options which are not
63
+     *  stored in this object.
64
+     */
65
+    static function get()
66
+    {
67
+        return self::$options;
68
+    }
69 69
 
70
-	/**
71
-	 * Merge the provided array with existing options.
72
-	 *
73
-	 * On duplicate keys, the value in $array will overwrite the
74
-	 * value stored in the options.
75
-	 *
76
-	 * @param array $array
77
-	 *  Associative array of options to merge into the existing options.
78
-	 */
79
-	static function merge($array)
80
-	{
81
-		self::$options = $array + self::$options;
82
-	}
70
+    /**
71
+     * Merge the provided array with existing options.
72
+     *
73
+     * On duplicate keys, the value in $array will overwrite the
74
+     * value stored in the options.
75
+     *
76
+     * @param array $array
77
+     *  Associative array of options to merge into the existing options.
78
+     */
79
+    static function merge($array)
80
+    {
81
+        self::$options = $array + self::$options;
82
+    }
83 83
 
84
-	/**
85
-	 * Returns true of the specified key is already overridden in this object.
86
-	 *
87
-	 * @param string $key
88
-	 *  The key to search for.
89
-	 */
90
-	static function has($key)
91
-	{
92
-		return array_key_exists($key, self::$options);
93
-	}
84
+    /**
85
+     * Returns true of the specified key is already overridden in this object.
86
+     *
87
+     * @param string $key
88
+     *  The key to search for.
89
+     */
90
+    static function has($key)
91
+    {
92
+        return array_key_exists($key, self::$options);
93
+    }
94 94
 }
Please login to merge, or discard this patch.
src/QueryPathIterator.php 2 patches
Indentation   +13 added lines, -13 removed lines patch added patch discarded remove patch
@@ -23,19 +23,19 @@
 block discarded – undo
23 23
 class QueryPathIterator extends IteratorIterator
24 24
 {
25 25
 
26
-	public $options = [];
27
-	private $qp;
26
+    public $options = [];
27
+    private $qp;
28 28
 
29
-	public function current()
30
-	{
31
-		if (! isset($this->qp)) {
32
-			$this->qp = QueryPath::with(parent::current(), null, $this->options);
33
-		} else {
34
-			$splos = new SplObjectStorage();
35
-			$splos->attach(parent::current());
36
-			$this->qp->setMatches($splos);
37
-		}
29
+    public function current()
30
+    {
31
+        if (! isset($this->qp)) {
32
+            $this->qp = QueryPath::with(parent::current(), null, $this->options);
33
+        } else {
34
+            $splos = new SplObjectStorage();
35
+            $splos->attach(parent::current());
36
+            $this->qp->setMatches($splos);
37
+        }
38 38
 
39
-		return $this->qp;
40
-	}
39
+        return $this->qp;
40
+    }
41 41
 }
Please login to merge, or discard this patch.
Spacing   +1 added lines, -1 removed lines patch added patch discarded remove patch
@@ -28,7 +28,7 @@
 block discarded – undo
28 28
 
29 29
 	public function current()
30 30
 	{
31
-		if (! isset($this->qp)) {
31
+		if (!isset($this->qp)) {
32 32
 			$this->qp = QueryPath::with(parent::current(), null, $this->options);
33 33
 		} else {
34 34
 			$splos = new SplObjectStorage();
Please login to merge, or discard this patch.
src/DOMQuery.php 2 patches
Indentation   +1517 added lines, -1517 removed lines patch added patch discarded remove patch
@@ -49,1521 +49,1521 @@
 block discarded – undo
49 49
 class DOMQuery extends DOM
50 50
 {
51 51
 
52
-	use QueryFilters, QueryMutators, QueryChecks;
53
-
54
-	/**
55
-	 * The last array of matches.
56
-	 */
57
-	protected $last = []; // Last set of matches.
58
-	private $ext = []; // Extensions array.
59
-
60
-	/**
61
-	 * The number of current matches.
62
-	 *
63
-	 * @see count()
64
-	 */
65
-	public $length = 0;
66
-
67
-	/**
68
-	 * Get the effective options for the current DOMQuery object.
69
-	 *
70
-	 * This returns an associative array of all of the options as set
71
-	 * for the current DOMQuery object. This includes default options,
72
-	 * options directly passed in via {@link qp()} or the constructor,
73
-	 * an options set in the QueryPath::Options object.
74
-	 *
75
-	 * The order of merging options is this:
76
-	 *  - Options passed in using qp() are highest priority, and will
77
-	 *    override other options.
78
-	 *  - Options set with QueryPath::Options will override default options,
79
-	 *    but can be overridden by options passed into qp().
80
-	 *  - Default options will be used when no overrides are present.
81
-	 *
82
-	 * This function will return the options currently used, with the above option
83
-	 * overriding having been calculated already.
84
-	 *
85
-	 * @return array
86
-	 *  An associative array of options, calculated from defaults and overridden
87
-	 *  options.
88
-	 * @see   qp()
89
-	 * @see   QueryPath::Options::set()
90
-	 * @see   QueryPath::Options::merge()
91
-	 * @since 2.0
92
-	 */
93
-	public function getOptions(): array
94
-	{
95
-		return $this->options;
96
-	}
97
-
98
-	/**
99
-	 * Select the root element of the document.
100
-	 *
101
-	 * This sets the current match to the document's root element. For
102
-	 * practical purposes, this is the same as:
103
-	 *
104
-	 * @code
105
-	 * qp($someDoc)->find(':root');
106
-	 * @endcode
107
-	 * However, since it doesn't invoke a parser, it has less overhead. It also
108
-	 * works in cases where the QueryPath has been reduced to zero elements (a
109
-	 * case that is not handled by find(':root') because there is no element
110
-	 * whose root can be found).
111
-	 *
112
-	 * @param string $selector
113
-	 *  A selector. If this is supplied, QueryPath will navigate to the
114
-	 *  document root and then run the query. (Added in QueryPath 2.0 Beta 2)
115
-	 *
116
-	 * @return DOMQuery
117
-	 *  The DOMQuery object, wrapping the root element (document element)
118
-	 *  for the current document.
119
-	 * @throws CSS\ParseException
120
-	 */
121
-	public function top($selector = null): Query
122
-	{
123
-		return $this->inst($this->document->documentElement, $selector);
124
-	}
125
-
126
-	/**
127
-	 * Given a CSS Selector, find matching items.
128
-	 *
129
-	 * @param string $selector
130
-	 *   CSS 3 Selector
131
-	 *
132
-	 * @return DOMQuery
133
-	 * @throws CSS\ParseException
134
-	 * @see  is()
135
-	 * @todo If a find() returns zero matches, then a subsequent find() will
136
-	 *       also return zero matches, even if that find has a selector like :root.
137
-	 *       The reason for this is that the {@link QueryPathEventHandler} does
138
-	 *       not set the root of the document tree if it cannot find any elements
139
-	 *       from which to determine what the root is. The workaround is to use
140
-	 *       {@link top()} to select the root element again.
141
-	 * @see  filter()
142
-	 */
143
-	public function find($selector): Query
144
-	{
145
-		$query = new DOMTraverser($this->matches);
146
-		$query->find($selector);
147
-
148
-		return $this->inst($query->matches(), null);
149
-	}
150
-
151
-	/**
152
-	 * @param $selector
153
-	 *
154
-	 * @return $this
155
-	 * @throws CSS\ParseException
156
-	 */
157
-	public function findInPlace($selector)
158
-	{
159
-		$query = new DOMTraverser($this->matches);
160
-		$query->find($selector);
161
-		$this->setMatches($query->matches());
162
-
163
-		return $this;
164
-	}
165
-
166
-	/**
167
-	 * Execute an XPath query and store the results in the QueryPath.
168
-	 *
169
-	 * Most methods in this class support CSS 3 Selectors. Sometimes, though,
170
-	 * XPath provides a finer-grained query language. Use this to execute
171
-	 * XPath queries.
172
-	 *
173
-	 * Beware, though. DOMQuery works best on DOM Elements, but an XPath
174
-	 * query can return other nodes, strings, and values. These may not work with
175
-	 * other QueryPath functions (though you will be able to access the
176
-	 * values with {@link get()}).
177
-	 *
178
-	 * @param string $query
179
-	 *      An XPath query.
180
-	 * @param array  $options
181
-	 *      Currently supported options are:
182
-	 *      - 'namespace_prefix': And XML namespace prefix to be used as the default. Used
183
-	 *      in conjunction with 'namespace_uri'
184
-	 *      - 'namespace_uri': The URI to be used as the default namespace URI. Used
185
-	 *      with 'namespace_prefix'
186
-	 *
187
-	 * @return DOMQuery
188
-	 *      A DOMQuery object wrapping the results of the query.
189
-	 * @throws CSS\ParseException
190
-	 * @author M Butcher
191
-	 * @author Xavier Prud'homme
192
-	 * @see    find()
193
-	 */
194
-	public function xpath($query, $options = [])
195
-	{
196
-		$xpath = new DOMXPath($this->document);
197
-
198
-		// Register a default namespace.
199
-		if (! empty($options['namespace_prefix']) && ! empty($options['namespace_uri'])) {
200
-			$xpath->registerNamespace($options['namespace_prefix'], $options['namespace_uri']);
201
-		}
202
-
203
-		$found = new SplObjectStorage();
204
-		foreach ($this->matches as $item) {
205
-			$nl = $xpath->query($query, $item);
206
-			if ($nl->length > 0) {
207
-				for ($i = 0; $i < $nl->length; ++$i) {
208
-					$found->attach($nl->item($i));
209
-				}
210
-			}
211
-		}
212
-
213
-		return $this->inst($found, null);
214
-	}
215
-
216
-	/**
217
-	 * Get the number of elements currently wrapped by this object.
218
-	 *
219
-	 * Note that there is no length property on this object.
220
-	 *
221
-	 * @return int
222
-	 *  Number of items in the object.
223
-	 * @deprecated QueryPath now implements Countable, so use count().
224
-	 */
225
-	public function size()
226
-	{
227
-		return $this->matches->count();
228
-	}
229
-
230
-	/**
231
-	 * Get the number of elements currently wrapped by this object.
232
-	 *
233
-	 * Since DOMQuery is Countable, the PHP count() function can also
234
-	 * be used on a DOMQuery.
235
-	 *
236
-	 * @code
237
-	 * <?php
238
-	 *  count(qp($xml, 'div'));
239
-	 * ?>
240
-	 * @endcode
241
-	 *
242
-	 * @return int
243
-	 *  The number of matches in the DOMQuery.
244
-	 */
245
-	public function count(): int
246
-	{
247
-		return $this->matches->count();
248
-	}
249
-
250
-	/**
251
-	 * Get one or all elements from this object.
252
-	 *
253
-	 * When called with no paramaters, this returns all objects wrapped by
254
-	 * the DOMQuery. Typically, these are DOMElement objects (unless you have
255
-	 * used map(), xpath(), or other methods that can select
256
-	 * non-elements).
257
-	 *
258
-	 * When called with an index, it will return the item in the DOMQuery with
259
-	 * that index number.
260
-	 *
261
-	 * Calling this method does not change the DOMQuery (e.g. it is
262
-	 * non-destructive).
263
-	 *
264
-	 * You can use qp()->get() to iterate over all elements matched. You can
265
-	 * also iterate over qp() itself (DOMQuery implementations must be Traversable).
266
-	 * In the later case, though, each item
267
-	 * will be wrapped in a DOMQuery object. To learn more about iterating
268
-	 * in QueryPath, see {@link examples/techniques.php}.
269
-	 *
270
-	 * @param int     $index
271
-	 *   If specified, then only this index value will be returned. If this
272
-	 *   index is out of bounds, a NULL will be returned.
273
-	 * @param boolean $asObject
274
-	 *   If this is TRUE, an SplObjectStorage object will be returned
275
-	 *   instead of an array. This is the preferred method for extensions to use.
276
-	 *
277
-	 * @return mixed
278
-	 *   If an index is passed, one element will be returned. If no index is
279
-	 *   present, an array of all matches will be returned.
280
-	 * @see eq()
281
-	 * @see SplObjectStorage
282
-	 */
283
-	public function get($index = null, $asObject = false)
284
-	{
285
-		if ($index !== null) {
286
-			return ($this->count() > $index) ? $this->getNthMatch($index) : null;
287
-		}
288
-		// Retain support for legacy.
289
-		if (! $asObject) {
290
-			$matches = [];
291
-			foreach ($this->matches as $m) {
292
-				$matches[] = $m;
293
-			}
294
-
295
-			return $matches;
296
-		}
297
-
298
-		return $this->matches;
299
-	}
300
-
301
-	/**
302
-	 * Get the namespace of the current element.
303
-	 *
304
-	 * If QP is currently pointed to a list of elements, this will get the
305
-	 * namespace of the first element.
306
-	 */
307
-	public function ns()
308
-	{
309
-		return $this->get(0)->namespaceURI;
310
-	}
311
-
312
-	/**
313
-	 * Get the DOMDocument that we currently work with.
314
-	 *
315
-	 * This returns the current DOMDocument. Any changes made to this document will be
316
-	 * accessible to DOMQuery, as both will share access to the same object.
317
-	 *
318
-	 * @return DOMDocument
319
-	 */
320
-	public function document()
321
-	{
322
-		return $this->document;
323
-	}
324
-
325
-	/**
326
-	 * On an XML document, load all XIncludes.
327
-	 *
328
-	 * @return DOMQuery
329
-	 */
330
-	public function xinclude()
331
-	{
332
-		$this->document->xinclude();
333
-
334
-		return $this;
335
-	}
336
-
337
-	/**
338
-	 * Get all current elements wrapped in an array.
339
-	 * Compatibility function for jQuery 1.4, but identical to calling {@link get()}
340
-	 * with no parameters.
341
-	 *
342
-	 * @return array
343
-	 *  An array of DOMNodes (typically DOMElements).
344
-	 */
345
-	public function toArray()
346
-	{
347
-		return $this->get();
348
-	}
349
-
350
-	/**
351
-	 * Insert or retrieve a Data URL.
352
-	 *
353
-	 * When called with just $attr, it will fetch the result, attempt to decode it, and
354
-	 * return an array with the MIME type and the application data.
355
-	 *
356
-	 * When called with both $attr and $data, it will inject the data into all selected elements
357
-	 * So @code$qp->dataURL('src', file_get_contents('my.png'), 'image/png')@endcode will inject
358
-	 * the given PNG image into the selected elements.
359
-	 *
360
-	 * The current implementation only knows how to encode and decode Base 64 data.
361
-	 *
362
-	 * Note that this is known *not* to work on IE 6, but should render fine in other browsers.
363
-	 *
364
-	 * @param string   $attr
365
-	 *    The name of the attribute.
366
-	 * @param mixed    $data
367
-	 *    The contents to inject as the data. The value can be any one of the following:
368
-	 *    - A URL: If this is given, then the subsystem will read the content from that URL. THIS
369
-	 *    MUST BE A FULL URL, not a relative path.
370
-	 *    - A string of data: If this is given, then the subsystem will encode the string.
371
-	 *    - A stream or file handle: If this is given, the stream's contents will be encoded
372
-	 *    and inserted as data.
373
-	 *    (Note that we make the assumption here that you would never want to set data to be
374
-	 *    a URL. If this is an incorrect assumption, file a bug.)
375
-	 * @param string   $mime
376
-	 *    The MIME type of the document.
377
-	 * @param resource $context
378
-	 *    A valid context. Use this only if you need to pass a stream context. This is only necessary
379
-	 *    if $data is a URL. (See {@link stream_context_create()}).
380
-	 *
381
-	 * @return DOMQuery|string
382
-	 *    If this is called as a setter, this will return a DOMQuery object. Otherwise, it
383
-	 *    will attempt to fetch data out of the attribute and return that.
384
-	 * @see   http://en.wikipedia.org/wiki/Data:_URL
385
-	 * @see   attr()
386
-	 * @since 2.1
387
-	 */
388
-	public function dataURL($attr, $data = null, $mime = 'application/octet-stream', $context = null)
389
-	{
390
-		if (is_null($data)) {
391
-			// Attempt to fetch the data
392
-			$data = $this->attr($attr);
393
-			if (empty($data) || is_array($data) || strpos($data, 'data:') !== 0) {
394
-				return;
395
-			}
396
-
397
-			// So 1 and 2 should be MIME types, and 3 should be the base64-encoded data.
398
-			$regex   = '/^data:([a-zA-Z0-9]+)\/([a-zA-Z0-9]+);base64,(.*)$/';
399
-			$matches = [];
400
-			preg_match($regex, $data, $matches);
401
-
402
-			if (! empty($matches)) {
403
-				$result = [
404
-					'mime' => $matches[1] . '/' . $matches[2],
405
-					'data' => base64_decode($matches[3]),
406
-				];
407
-
408
-				return $result;
409
-			}
410
-		} else {
411
-			$attVal = QueryPath::encodeDataURL($data, $mime, $context);
412
-
413
-			return $this->attr($attr, $attVal);
414
-		}
415
-	}
416
-
417
-	/**
418
-	 * Sort the contents of the QueryPath object.
419
-	 *
420
-	 * By default, this does not change the order of the elements in the
421
-	 * DOM. Instead, it just sorts the internal list. However, if TRUE
422
-	 * is passed in as the second parameter then QueryPath will re-order
423
-	 * the DOM, too.
424
-	 *
425
-	 * @attention
426
-	 * DOM re-ordering is done by finding the location of the original first
427
-	 * item in the list, and then placing the sorted list at that location.
428
-	 *
429
-	 * The argument $compartor is a callback, such as a function name or a
430
-	 * closure. The callback receives two DOMNode objects, which you can use
431
-	 * as DOMNodes, or wrap in QueryPath objects.
432
-	 *
433
-	 * A simple callback:
434
-	 * @code
435
-	 * <?php
436
-	 * $comp = function (\DOMNode $a, \DOMNode $b) {
437
-	 *   if ($a->textContent == $b->textContent) {
438
-	 *     return 0;
439
-	 *   }
440
-	 *   return $a->textContent > $b->textContent ? 1 : -1;
441
-	 * };
442
-	 * $qp = QueryPath::with($xml, $selector)->sort($comp);
443
-	 * ?>
444
-	 * @endcode
445
-	 *
446
-	 * The above sorts the matches into lexical order using the text of each node.
447
-	 * If you would prefer to work with QueryPath objects instead of DOMNode
448
-	 * objects, you may prefer something like this:
449
-	 *
450
-	 * @code
451
-	 * <?php
452
-	 * $comp = function (\DOMNode $a, \DOMNode $b) {
453
-	 *   $qpa = qp($a);
454
-	 *   $qpb = qp($b);
455
-	 *
456
-	 *   if ($qpa->text() == $qpb->text()) {
457
-	 *     return 0;
458
-	 *   }
459
-	 *   return $qpa->text()> $qpb->text()? 1 : -1;
460
-	 * };
461
-	 *
462
-	 * $qp = QueryPath::with($xml, $selector)->sort($comp);
463
-	 * ?>
464
-	 * @endcode
465
-	 *
466
-	 * @param callback $comparator
467
-	 *   A callback. This will be called during sorting to compare two DOMNode
468
-	 *   objects.
469
-	 * @param boolean  $modifyDOM
470
-	 *   If this is TRUE, the sorted results will be inserted back into
471
-	 *   the DOM at the position of the original first element.
472
-	 *
473
-	 * @return DOMQuery
474
-	 *   This object.
475
-	 * @throws CSS\ParseException
476
-	 */
477
-	public function sort($comparator, $modifyDOM = false): Query
478
-	{
479
-		// Sort as an array.
480
-		$list = iterator_to_array($this->matches);
481
-
482
-		if (empty($list)) {
483
-			return $this;
484
-		}
485
-
486
-		$oldFirst = $list[0];
487
-
488
-		usort($list, $comparator);
489
-
490
-		// Copy back into SplObjectStorage.
491
-		$found = new SplObjectStorage();
492
-		foreach ($list as $node) {
493
-			$found->attach($node);
494
-		}
495
-		//$this->setMatches($found);
496
-
497
-
498
-		// Do DOM modifications only if necessary.
499
-		if ($modifyDOM) {
500
-			$placeholder = $oldFirst->ownerDocument->createElement('_PLACEHOLDER_');
501
-			$placeholder = $oldFirst->parentNode->insertBefore($placeholder, $oldFirst);
502
-			$len         = count($list);
503
-			for ($i = 0; $i < $len; ++$i) {
504
-				$node = $list[$i];
505
-				$node = $node->parentNode->removeChild($node);
506
-				$placeholder->parentNode->insertBefore($node, $placeholder);
507
-			}
508
-			$placeholder->parentNode->removeChild($placeholder);
509
-		}
510
-
511
-		return $this->inst($found, null);
512
-	}
513
-
514
-	/**
515
-	 * Get an item's index.
516
-	 *
517
-	 * Given a DOMElement, get the index from the matches. This is the
518
-	 * converse of {@link get()}.
519
-	 *
520
-	 * @param DOMElement $subject
521
-	 *  The item to match.
522
-	 *
523
-	 * @return mixed
524
-	 *  The index as an integer (if found), or boolean FALSE. Since 0 is a
525
-	 *  valid index, you should use strong equality (===) to test..
526
-	 * @see get()
527
-	 * @see is()
528
-	 */
529
-	public function index($subject)
530
-	{
531
-		$i = 0;
532
-		foreach ($this->matches as $m) {
533
-			if ($m === $subject) {
534
-				return $i;
535
-			}
536
-			++$i;
537
-		}
538
-
539
-		return false;
540
-	}
541
-
542
-	/**
543
-	 * The tag name of the first element in the list.
544
-	 *
545
-	 * This returns the tag name of the first element in the list of matches. If
546
-	 * the list is empty, an empty string will be used.
547
-	 *
548
-	 * @return string
549
-	 *  The tag name of the first element in the list.
550
-	 * @see replaceWith()
551
-	 * @see replaceAll()
552
-	 */
553
-	public function tag()
554
-	{
555
-		return ($this->matches->count() > 0) ? $this->getFirstMatch()->tagName : '';
556
-	}
557
-
558
-	/**
559
-	 * Revert to the previous set of matches.
560
-	 *
561
-	 * <b>DEPRECATED</b> Do not use.
562
-	 *
563
-	 * This will revert back to the last set of matches (before the last
564
-	 * "destructive" set of operations). This undoes any change made to the set of
565
-	 * matched objects. Functions like find() and filter() change the
566
-	 * list of matched objects. The end() function will revert back to the last set of
567
-	 * matched items.
568
-	 *
569
-	 * Note that functions that modify the document, but do not change the list of
570
-	 * matched objects, are not "destructive". Thus, calling append('something')->end()
571
-	 * will not undo the append() call.
572
-	 *
573
-	 * Only one level of changes is stored. Reverting beyond that will result in
574
-	 * an empty set of matches. Example:
575
-	 *
576
-	 * @code
577
-	 * // The line below returns the same thing as qp(document, 'p');
578
-	 * qp(document, 'p')->find('div')->end();
579
-	 * // This returns an empty array:
580
-	 * qp(document, 'p')->end();
581
-	 * // This returns an empty array:
582
-	 * qp(document, 'p')->find('div')->find('span')->end()->end();
583
-	 * @endcode
584
-	 *
585
-	 * The last one returns an empty array because only one level of changes is stored.
586
-	 *
587
-	 * @return DOMQuery
588
-	 *  A DOMNode object reflecting the list of matches prior to the last destructive
589
-	 *  operation.
590
-	 * @see        andSelf()
591
-	 * @see        add()
592
-	 * @deprecated This function will be removed.
593
-	 */
594
-	public function end()
595
-	{
596
-		// Note that this does not use setMatches because it must set the previous
597
-		// set of matches to empty array.
598
-		$this->matches = $this->last;
599
-		$this->last    = new SplObjectStorage();
600
-
601
-		return $this;
602
-	}
603
-
604
-	/**
605
-	 * Combine the current and previous set of matched objects.
606
-	 *
607
-	 * Example:
608
-	 *
609
-	 * @code
610
-	 * qp(document, 'p')->find('div')->andSelf();
611
-	 * @endcode
612
-	 *
613
-	 * The code above will contain a list of all p elements and all div elements that
614
-	 * are beneath p elements.
615
-	 *
616
-	 * @return DOMQuery
617
-	 *  A DOMNode object with the results of the last two "destructive" operations.
618
-	 * @see end();
619
-	 * @see add()
620
-	 * @see end()
621
-	 */
622
-	public function andSelf()
623
-	{
624
-		// This is destructive, so we need to set $last:
625
-		$last = $this->matches;
626
-
627
-		foreach ($this->last as $item) {
628
-			$this->matches->attach($item);
629
-		}
630
-
631
-		$this->last = $last;
632
-
633
-		return $this;
634
-	}
635
-
636
-	/**
637
-	 * Set or get the markup for an element.
638
-	 *
639
-	 * If $markup is set, then the giving markup will be injected into each
640
-	 * item in the set. All other children of that node will be deleted, and this
641
-	 * new code will be the only child or children. The markup MUST BE WELL FORMED.
642
-	 *
643
-	 * If no markup is given, this will return a string representing the child
644
-	 * markup of the first node.
645
-	 *
646
-	 * <b>Important:</b> This differs from jQuery's html() function. This function
647
-	 * returns <i>the current node</i> and all of its children. jQuery returns only
648
-	 * the children. This means you do not need to do things like this:
649
-	 * @code$qp->parent()->html()@endcode.
650
-	 *
651
-	 * By default, this is HTML 4.01, not XHTML. Use {@link xml()} for XHTML.
652
-	 *
653
-	 * @param string $markup
654
-	 *  The text to insert.
655
-	 *
656
-	 * @return mixed
657
-	 *  A string if no markup was passed, or a DOMQuery if markup was passed.
658
-	 * @throws Exception
659
-	 * @throws QueryPath
660
-	 * @see xml()
661
-	 * @see text()
662
-	 * @see contents()
663
-	 */
664
-	public function html($markup = null)
665
-	{
666
-		if (isset($markup)) {
667
-			if ($this->options['replace_entities']) {
668
-				$markup = Entities::replaceAllEntities($markup);
669
-			}
670
-
671
-			// Parse the HTML and insert it into the DOM
672
-			//$doc = DOMDocument::loadHTML($markup);
673
-			$doc = $this->document->createDocumentFragment();
674
-			$doc->appendXML($markup);
675
-			$this->removeChildren();
676
-			$this->append($doc);
677
-
678
-			return $this;
679
-		}
680
-		$length = $this->matches->count();
681
-		if ($length === 0) {
682
-			return null;
683
-		}
684
-		// Only return the first item -- that's what JQ does.
685
-		$first = $this->getFirstMatch();
686
-
687
-		// Catch cases where first item is not a legit DOM object.
688
-		if (! ($first instanceof DOMNode)) {
689
-			return null;
690
-		}
691
-
692
-		// Added by eabrand.
693
-		if (! $first->ownerDocument->documentElement) {
694
-			return null;
695
-		}
696
-
697
-		if ($first instanceof DOMDocument || $first->isSameNode($first->ownerDocument->documentElement)) {
698
-			return $this->document->saveHTML();
699
-		}
700
-
701
-		// saveHTML cannot take a node and serialize it.
702
-		return $this->document->saveXML($first);
703
-	}
704
-
705
-	/**
706
-	 * Write the QueryPath document to HTML5.
707
-	 *
708
-	 * See html()
709
-	 *
710
-	 * @param null $markup
711
-	 *
712
-	 * @return null|DOMQuery|string
713
-	 * @throws QueryPath
714
-	 * @throws \QueryPath\Exception
715
-	 */
716
-	public function html5($markup = null)
717
-	{
718
-		$html5 = new HTML5($this->options);
719
-
720
-		// append HTML to existing
721
-		if ($markup === null) {
722
-			// Parse the HTML and insert it into the DOM
723
-			$doc = $html5->loadHTMLFragment($markup);
724
-			$this->removeChildren();
725
-			$this->append($doc);
726
-
727
-			return $this;
728
-		}
729
-
730
-		$length = $this->count();
731
-		if ($length === 0) {
732
-			return null;
733
-		}
734
-		// Only return the first item -- that's what JQ does.
735
-		$first = $this->getFirstMatch();
736
-
737
-		// Catch cases where first item is not a legit DOM object.
738
-		if (! ($first instanceof DOMNode)) {
739
-			return null;
740
-		}
741
-
742
-		// Added by eabrand.
743
-		if (! $first->ownerDocument->documentElement) {
744
-			return null;
745
-		}
746
-
747
-		if ($first instanceof DOMDocument || $first->isSameNode($first->ownerDocument->documentElement)) {
748
-			return $html5->saveHTML($this->document); //$this->document->saveHTML();
749
-		}
750
-
751
-		return $html5->saveHTML($first);
752
-	}
753
-
754
-	/**
755
-	 * Fetch the HTML contents INSIDE of the first DOMQuery item.
756
-	 *
757
-	 * <b>This behaves the way jQuery's @codehtml()@endcode function behaves.</b>
758
-	 *
759
-	 * This gets all children of the first match in DOMQuery.
760
-	 *
761
-	 * Consider this fragment:
762
-	 *
763
-	 * @code
764
-	 * <div>
765
-	 * test <p>foo</p> test
766
-	 * </div>
767
-	 * @endcode
768
-	 *
769
-	 * We can retrieve just the contents of this code by doing something like
770
-	 * this:
771
-	 * @code
772
-	 * qp($xml, 'div')->innerHTML();
773
-	 * @endcode
774
-	 *
775
-	 * This would return the following:
776
-	 * @codetest <p>foo</p> test@endcode
777
-	 *
778
-	 * @return string
779
-	 *  Returns a string representation of the child nodes of the first
780
-	 *  matched element.
781
-	 * @see   html()
782
-	 * @see   innerXML()
783
-	 * @see   innerXHTML()
784
-	 * @since 2.0
785
-	 */
786
-	public function innerHTML()
787
-	{
788
-		return $this->innerXML();
789
-	}
790
-
791
-	/**
792
-	 * Fetch child (inner) nodes of the first match.
793
-	 *
794
-	 * This will return the children of the present match. For an example,
795
-	 * see {@link innerHTML()}.
796
-	 *
797
-	 * @return string
798
-	 *  Returns a string of XHTML that represents the children of the present
799
-	 *  node.
800
-	 * @see   innerXML()
801
-	 * @see   innerHTML()
802
-	 * @since 2.0
803
-	 */
804
-	public function innerXHTML()
805
-	{
806
-		$length = $this->matches->count();
807
-		if ($length === 0) {
808
-			return null;
809
-		}
810
-		// Only return the first item -- that's what JQ does.
811
-		$first = $this->getFirstMatch();
812
-
813
-		// Catch cases where first item is not a legit DOM object.
814
-		if (! ($first instanceof DOMNode)) {
815
-			return null;
816
-		}
817
-
818
-		if (! $first->hasChildNodes()) {
819
-			return '';
820
-		}
821
-
822
-		$buffer = '';
823
-		foreach ($first->childNodes as $child) {
824
-			$buffer .= $this->document->saveXML($child, LIBXML_NOEMPTYTAG);
825
-		}
826
-
827
-		return $buffer;
828
-	}
829
-
830
-	/**
831
-	 * Fetch child (inner) nodes of the first match.
832
-	 *
833
-	 * This will return the children of the present match. For an example,
834
-	 * see {@link innerHTML()}.
835
-	 *
836
-	 * @return string
837
-	 *  Returns a string of XHTML that represents the children of the present
838
-	 *  node.
839
-	 * @see   innerXHTML()
840
-	 * @see   innerHTML()
841
-	 * @since 2.0
842
-	 */
843
-	public function innerXML()
844
-	{
845
-		$length = $this->matches->count();
846
-		if ($length === 0) {
847
-			return null;
848
-		}
849
-		// Only return the first item -- that's what JQ does.
850
-		$first = $this->getFirstMatch();
851
-
852
-		// Catch cases where first item is not a legit DOM object.
853
-		if (! ($first instanceof DOMNode)) {
854
-			return null;
855
-		}
856
-
857
-		if (! $first->hasChildNodes()) {
858
-			return '';
859
-		}
860
-
861
-		$buffer = '';
862
-		foreach ($first->childNodes as $child) {
863
-			$buffer .= $this->document->saveXML($child);
864
-		}
865
-
866
-		return $buffer;
867
-	}
868
-
869
-	/**
870
-	 * Get child elements as an HTML5 string.
871
-	 *
872
-	 * TODO: This is a very simple alteration of innerXML. Do we need better
873
-	 * support?
874
-	 */
875
-	public function innerHTML5()
876
-	{
877
-		$length = $this->matches->count();
878
-		if ($length === 0) {
879
-			return null;
880
-		}
881
-		// Only return the first item -- that's what JQ does.
882
-		$first = $this->getFirstMatch();
883
-
884
-		// Catch cases where first item is not a legit DOM object.
885
-		if (! ($first instanceof DOMNode)) {
886
-			return null;
887
-		}
888
-
889
-		if (! $first->hasChildNodes()) {
890
-			return '';
891
-		}
892
-
893
-		$html5  = new HTML5($this->options);
894
-		$buffer = '';
895
-		foreach ($first->childNodes as $child) {
896
-			$buffer .= $html5->saveHTML($child);
897
-		}
898
-
899
-		return $buffer;
900
-	}
901
-
902
-	/**
903
-	 * Retrieve the text of each match and concatenate them with the given separator.
904
-	 *
905
-	 * This has the effect of looping through all children, retrieving their text
906
-	 * content, and then concatenating the text with a separator.
907
-	 *
908
-	 * @param string  $sep
909
-	 *  The string used to separate text items. The default is a comma followed by a
910
-	 *  space.
911
-	 * @param boolean $filterEmpties
912
-	 *  If this is true, empty items will be ignored.
913
-	 *
914
-	 * @return string
915
-	 *  The text contents, concatenated together with the given separator between
916
-	 *  every pair of items.
917
-	 * @see   implode()
918
-	 * @see   text()
919
-	 * @since 2.0
920
-	 */
921
-	public function textImplode($sep = ', ', $filterEmpties = true): string
922
-	{
923
-		$tmp = [];
924
-		foreach ($this->matches as $m) {
925
-			$txt     = $m->textContent;
926
-			$trimmed = trim($txt);
927
-			// If filter empties out, then we only add items that have content.
928
-			if ($filterEmpties) {
929
-				if (strlen($trimmed) > 0) {
930
-					$tmp[] = $txt;
931
-				}
932
-			} // Else add all content, even if it's empty.
933
-			else {
934
-				$tmp[] = $txt;
935
-			}
936
-		}
937
-
938
-		return implode($sep, $tmp);
939
-	}
940
-
941
-	/**
942
-	 * Get the text contents from just child elements.
943
-	 *
944
-	 * This is a specialized variant of textImplode() that implodes text for just the
945
-	 * child elements of the current element.
946
-	 *
947
-	 * @param string $separator
948
-	 *  The separator that will be inserted between found text content.
949
-	 *
950
-	 * @return string
951
-	 *  The concatenated values of all children.
952
-	 * @throws CSS\ParseException
953
-	 */
954
-	public function childrenText($separator = ' '): string
955
-	{
956
-		// Branch makes it non-destructive.
957
-		return $this->branch()->xpath('descendant::text()')->textImplode($separator);
958
-	}
959
-
960
-	/**
961
-	 * Get or set the text contents of a node.
962
-	 *
963
-	 * @param string $text
964
-	 *  If this is not NULL, this value will be set as the text of the node. It
965
-	 *  will replace any existing content.
966
-	 *
967
-	 * @return mixed
968
-	 *  A DOMQuery if $text is set, or the text content if no text
969
-	 *  is passed in as a pram.
970
-	 * @see html()
971
-	 * @see xml()
972
-	 * @see contents()
973
-	 */
974
-	public function text($text = null)
975
-	{
976
-		if (isset($text)) {
977
-			$this->removeChildren();
978
-			foreach ($this->matches as $m) {
979
-				$m->appendChild($this->document->createTextNode($text));
980
-			}
981
-
982
-			return $this;
983
-		}
984
-		// Returns all text as one string:
985
-		$buf = '';
986
-		foreach ($this->matches as $m) {
987
-			$buf .= $m->textContent;
988
-		}
989
-
990
-		return $buf;
991
-	}
992
-
993
-	/**
994
-	 * Get or set the text before each selected item.
995
-	 *
996
-	 * If $text is passed in, the text is inserted before each currently selected item.
997
-	 *
998
-	 * If no text is given, this will return the concatenated text after each selected element.
999
-	 *
1000
-	 * @code
1001
-	 * <?php
1002
-	 * $xml = '<?xml version="1.0"?><root>Foo<a>Bar</a><b/></root>';
1003
-	 *
1004
-	 * // This will return 'Foo'
1005
-	 * qp($xml, 'a')->textBefore();
1006
-	 *
1007
-	 * // This will insert 'Baz' right before <b/>.
1008
-	 * qp($xml, 'b')->textBefore('Baz');
1009
-	 * ?>
1010
-	 * @endcode
1011
-	 *
1012
-	 * @param string $text
1013
-	 *  If this is set, it will be inserted before each node in the current set of
1014
-	 *  selected items.
1015
-	 *
1016
-	 * @return mixed
1017
-	 *  Returns the DOMQuery object if $text was set, and returns a string (possibly empty)
1018
-	 *  if no param is passed.
1019
-	 * @throws Exception
1020
-	 * @throws QueryPath
1021
-	 */
1022
-	public function textBefore($text = null)
1023
-	{
1024
-		if (isset($text)) {
1025
-			$textNode = $this->document->createTextNode($text);
1026
-
1027
-			return $this->before($textNode);
1028
-		}
1029
-		$buffer = '';
1030
-		foreach ($this->matches as $m) {
1031
-			$p = $m;
1032
-			while (isset($p->previousSibling) && $p->previousSibling->nodeType === XML_TEXT_NODE) {
1033
-				$p      = $p->previousSibling;
1034
-				$buffer .= $p->textContent;
1035
-			}
1036
-		}
1037
-
1038
-		return $buffer;
1039
-	}
1040
-
1041
-	public function textAfter($text = null)
1042
-	{
1043
-		if (isset($text)) {
1044
-			$textNode = $this->document->createTextNode($text);
1045
-
1046
-			return $this->after($textNode);
1047
-		}
1048
-		$buffer = '';
1049
-		foreach ($this->matches as $m) {
1050
-			$n = $m;
1051
-			while (isset($n->nextSibling) && $n->nextSibling->nodeType === XML_TEXT_NODE) {
1052
-				$n      = $n->nextSibling;
1053
-				$buffer .= $n->textContent;
1054
-			}
1055
-		}
1056
-
1057
-		return $buffer;
1058
-	}
1059
-
1060
-	/**
1061
-	 * Set or get the value of an element's 'value' attribute.
1062
-	 *
1063
-	 * The 'value' attribute is common in HTML form elements. This is a
1064
-	 * convenience function for accessing the values. Since this is not  common
1065
-	 * task on the server side, this method may be removed in future releases. (It
1066
-	 * is currently provided for jQuery compatibility.)
1067
-	 *
1068
-	 * If a value is provided in the params, then the value will be set for all
1069
-	 * matches. If no params are given, then the value of the first matched element
1070
-	 * will be returned. This may be NULL.
1071
-	 *
1072
-	 * @deprecated Just use attr(). There's no reason to use this on the server.
1073
-	 * @see        attr()
1074
-	 *
1075
-	 * @param string $value
1076
-	 *
1077
-	 * @return mixed
1078
-	 *  Returns a DOMQuery if a string was passed in, and a string if no string
1079
-	 *  was passed in. In the later case, an error will produce NULL.
1080
-	 */
1081
-	public function val($value = null)
1082
-	{
1083
-		if (isset($value)) {
1084
-			$this->attr('value', $value);
1085
-
1086
-			return $this;
1087
-		}
1088
-
1089
-		return $this->attr('value');
1090
-	}
1091
-
1092
-	/**
1093
-	 * Set or get XHTML markup for an element or elements.
1094
-	 *
1095
-	 * This differs from {@link html()} in that it processes (and produces)
1096
-	 * strictly XML 1.0 compliant markup.
1097
-	 *
1098
-	 * Like {@link xml()} and {@link html()}, this functions as both a
1099
-	 * setter and a getter.
1100
-	 *
1101
-	 * This is a convenience function for fetching HTML in XML format.
1102
-	 * It does no processing of the markup (such as schema validation).
1103
-	 *
1104
-	 * @param string $markup
1105
-	 *  A string containing XML data.
1106
-	 *
1107
-	 * @return mixed
1108
-	 *  If markup is passed in, a DOMQuery is returned. If no markup is passed
1109
-	 *  in, XML representing the first matched element is returned.
1110
-	 * @see html()
1111
-	 * @see innerXHTML()
1112
-	 */
1113
-	public function xhtml($markup = null)
1114
-	{
1115
-
1116
-		// XXX: This is a minor reworking of the original xml() method.
1117
-		// This should be refactored, probably.
1118
-		// See http://github.com/technosophos/querypath/issues#issue/10
1119
-
1120
-		$omit_xml_decl = $this->options['omit_xml_declaration'];
1121
-		if ($markup === true) {
1122
-			// Basically, we handle the special case where we don't
1123
-			// want the XML declaration to be displayed.
1124
-			$omit_xml_decl = true;
1125
-		} elseif (isset($markup)) {
1126
-			return $this->xml($markup);
1127
-		}
1128
-
1129
-		$length = $this->matches->count();
1130
-		if ($length === 0) {
1131
-			return null;
1132
-		}
1133
-
1134
-		// Only return the first item -- that's what JQ does.
1135
-		$first = $this->getFirstMatch();
1136
-		// Catch cases where first item is not a legit DOM object.
1137
-		if (! ($first instanceof DOMNode)) {
1138
-			return null;
1139
-		}
1140
-
1141
-		if ($first instanceof DOMDocument || $first->isSameNode($first->ownerDocument->documentElement)) {
1142
-			// Has the unfortunate side-effect of stripping doctype.
1143
-			//$text = ($omit_xml_decl ? $this->document->saveXML($first->ownerDocument->documentElement, LIBXML_NOEMPTYTAG) : $this->document->saveXML(NULL, LIBXML_NOEMPTYTAG));
1144
-			$text = $this->document->saveXML(null, LIBXML_NOEMPTYTAG);
1145
-		} else {
1146
-			$text = $this->document->saveXML($first, LIBXML_NOEMPTYTAG);
1147
-		}
1148
-
1149
-		// Issue #47: Using the old trick for removing the XML tag also removed the
1150
-		// doctype. So we remove it with a regex:
1151
-		if ($omit_xml_decl) {
1152
-			$text = preg_replace('/<\?xml\s[^>]*\?>/', '', $text);
1153
-		}
1154
-
1155
-		// This is slightly lenient: It allows for cases where code incorrectly places content
1156
-		// inside of these supposedly unary elements.
1157
-		$unary = '/<(area|base|basefont|br|col|frame|hr|img|input|isindex|link|meta|param)(?(?=\s)([^>\/]+))><\/[^>]*>/i';
1158
-		$text  = preg_replace($unary, '<\\1\\2 />', $text);
1159
-
1160
-		// Experimental: Support for enclosing CDATA sections with comments to be both XML compat
1161
-		// and HTML 4/5 compat
1162
-		$cdata   = '/(<!\[CDATA\[|\]\]>)/i';
1163
-		$replace = $this->options['escape_xhtml_js_css_sections'];
1164
-		$text    = preg_replace($cdata, $replace, $text);
1165
-
1166
-		return $text;
1167
-	}
1168
-
1169
-	/**
1170
-	 * Set or get the XML markup for an element or elements.
1171
-	 *
1172
-	 * Like {@link html()}, this functions in both a setter and a getter mode.
1173
-	 *
1174
-	 * In setter mode, the string passed in will be parsed and then appended to the
1175
-	 * elements wrapped by this DOMNode object.When in setter mode, this parses
1176
-	 * the XML using the DOMFragment parser. For that reason, an XML declaration
1177
-	 * is not necessary.
1178
-	 *
1179
-	 * In getter mode, the first element wrapped by this DOMNode object will be
1180
-	 * converted to an XML string and returned.
1181
-	 *
1182
-	 * @param string $markup
1183
-	 *  A string containing XML data.
1184
-	 *
1185
-	 * @return mixed
1186
-	 *  If markup is passed in, a DOMQuery is returned. If no markup is passed
1187
-	 *  in, XML representing the first matched element is returned.
1188
-	 * @see xhtml()
1189
-	 * @see html()
1190
-	 * @see text()
1191
-	 * @see content()
1192
-	 * @see innerXML()
1193
-	 */
1194
-	public function xml($markup = null)
1195
-	{
1196
-		$omit_xml_decl = $this->options['omit_xml_declaration'];
1197
-		if ($markup === true) {
1198
-			// Basically, we handle the special case where we don't
1199
-			// want the XML declaration to be displayed.
1200
-			$omit_xml_decl = true;
1201
-		} elseif (isset($markup)) {
1202
-			if ($this->options['replace_entities']) {
1203
-				$markup = Entities::replaceAllEntities($markup);
1204
-			}
1205
-			$doc = $this->document->createDocumentFragment();
1206
-			$doc->appendXML($markup);
1207
-			$this->removeChildren();
1208
-			$this->append($doc);
1209
-
1210
-			return $this;
1211
-		}
1212
-		$length = $this->matches->count();
1213
-		if ($length === 0) {
1214
-			return null;
1215
-		}
1216
-		// Only return the first item -- that's what JQ does.
1217
-		$first = $this->getFirstMatch();
1218
-
1219
-		// Catch cases where first item is not a legit DOM object.
1220
-		if (! ($first instanceof DOMNode)) {
1221
-			return null;
1222
-		}
1223
-
1224
-		if ($first instanceof DOMDocument || $first->isSameNode($first->ownerDocument->documentElement)) {
1225
-			return ($omit_xml_decl ? $this->document->saveXML($first->ownerDocument->documentElement) : $this->document->saveXML());
1226
-		}
1227
-
1228
-		return $this->document->saveXML($first);
1229
-	}
1230
-
1231
-	/**
1232
-	 * Send the XML document to the client.
1233
-	 *
1234
-	 * Write the document to a file path, if given, or
1235
-	 * to stdout (usually the client).
1236
-	 *
1237
-	 * This prints the entire document.
1238
-	 *
1239
-	 * @param string $path
1240
-	 *  The path to the file into which the XML should be written. if
1241
-	 *  this is NULL, data will be written to STDOUT, which is usually
1242
-	 *  sent to the remote browser.
1243
-	 * @param int    $options
1244
-	 *  (As of QueryPath 2.1) Pass libxml options to the saving mechanism.
1245
-	 *
1246
-	 * @return DOMQuery
1247
-	 *  The DOMQuery object, unmodified.
1248
-	 * @throws Exception
1249
-	 *  In the event that a file cannot be written, an Exception will be thrown.
1250
-	 * @see innerXML()
1251
-	 * @see writeXHTML()
1252
-	 * @see xml()
1253
-	 */
1254
-	public function writeXML($path = null, $options = 0)
1255
-	{
1256
-		// Backwards compatibility fix for PHP8+
1257
-		if (is_null($options)) {
1258
-			$options = 0;
1259
-		}
1260
-
1261
-		if ($path === null) {
1262
-			print $this->document->saveXML(null, $options);
1263
-		} else {
1264
-			try {
1265
-				set_error_handler([IOException::class, 'initializeFromError']);
1266
-				$this->document->save($path, $options);
1267
-			} catch (Exception $e) {
1268
-				restore_error_handler();
1269
-				throw $e;
1270
-			}
1271
-			restore_error_handler();
1272
-		}
1273
-
1274
-		return $this;
1275
-	}
1276
-
1277
-	/**
1278
-	 * Writes HTML to output.
1279
-	 *
1280
-	 * HTML is formatted as HTML 4.01, without strict XML unary tags. This is for
1281
-	 * legacy HTML content. Modern XHTML should be written using {@link toXHTML()}.
1282
-	 *
1283
-	 * Write the document to stdout (usually the client) or to a file.
1284
-	 *
1285
-	 * @param string $path
1286
-	 *  The path to the file into which the XML should be written. if
1287
-	 *  this is NULL, data will be written to STDOUT, which is usually
1288
-	 *  sent to the remote browser.
1289
-	 *
1290
-	 * @return DOMQuery
1291
-	 *  The DOMQuery object, unmodified.
1292
-	 * @throws Exception
1293
-	 *  In the event that a file cannot be written, an Exception will be thrown.
1294
-	 * @see innerHTML()
1295
-	 * @see html()
1296
-	 */
1297
-	public function writeHTML($path = null)
1298
-	{
1299
-		if ($path === null) {
1300
-			print $this->document->saveHTML();
1301
-		} else {
1302
-			try {
1303
-				set_error_handler(['\QueryPath\ParseException', 'initializeFromError']);
1304
-				$this->document->saveHTMLFile($path);
1305
-			} catch (Exception $e) {
1306
-				restore_error_handler();
1307
-				throw $e;
1308
-			}
1309
-			restore_error_handler();
1310
-		}
1311
-
1312
-		return $this;
1313
-	}
1314
-
1315
-	/**
1316
-	 * Write the document to HTML5.
1317
-	 *
1318
-	 * This works the same as the other write* functions, but it encodes the output
1319
-	 * as HTML5 with UTF-8.
1320
-	 *
1321
-	 * @throws Exception
1322
-	 *  In the event that a file cannot be written, an Exception will be thrown.
1323
-	 * @see innerHTML5()
1324
-	 * @see html5()
1325
-	 */
1326
-	public function writeHTML5($path = null)
1327
-	{
1328
-		$html5 = new HTML5();
1329
-		if ($path === null) {
1330
-			// Print the document to stdout.
1331
-			print $html5->saveHTML($this->document);
1332
-
1333
-			return;
1334
-		}
1335
-
1336
-		$html5->save($this->document, $path);
1337
-	}
1338
-
1339
-	/**
1340
-	 * Write an XHTML file to output.
1341
-	 *
1342
-	 * Typically, you should use this instead of {@link writeHTML()}.
1343
-	 *
1344
-	 * Currently, this functions identically to {@link toXML()} <i>except that</i>
1345
-	 * it always uses closing tags (e.g. always @code<script></script>@endcode,
1346
-	 * never @code<script/>@endcode). It will
1347
-	 * write the file as well-formed XML. No XHTML schema validation is done.
1348
-	 *
1349
-	 * @param string $path
1350
-	 *  The filename of the file to write to.
1351
-	 *
1352
-	 * @return DOMQuery
1353
-	 *  Returns the DOMQuery, unmodified.
1354
-	 * @throws Exception
1355
-	 *  In the event that the output file cannot be written, an exception is
1356
-	 *  thrown.
1357
-	 * @see   innerXHTML()
1358
-	 * @see   xhtml()
1359
-	 * @see   writeXML()
1360
-	 * @see   xml()
1361
-	 * @see   writeHTML()
1362
-	 * @since 2.0
1363
-	 */
1364
-	public function writeXHTML($path = null)
1365
-	{
1366
-		return $this->writeXML($path, LIBXML_NOEMPTYTAG);
1367
-	}
1368
-
1369
-	/**
1370
-	 * Branch the base DOMQuery into another one with the same matches.
1371
-	 *
1372
-	 * This function makes a copy of the DOMQuery object, but keeps the new copy
1373
-	 * (initially) pointed at the same matches. This object can then be queried without
1374
-	 * changing the original DOMQuery. However, changes to the elements inside of this
1375
-	 * DOMQuery will show up in the DOMQuery from which it is branched.
1376
-	 *
1377
-	 * Compare this operation with {@link cloneAll()}. The cloneAll() call takes
1378
-	 * the current DOMNode object and makes a copy of all of its matches. You continue
1379
-	 * to operate on the same DOMNode object, but the elements inside of the DOMQuery
1380
-	 * are copies of those before the call to cloneAll().
1381
-	 *
1382
-	 * This, on the other hand, copies <i>the DOMQuery</i>, but keeps valid
1383
-	 * references to the document and the wrapped elements. A new query branch is
1384
-	 * created, but any changes will be written back to the same document.
1385
-	 *
1386
-	 * In practice, this comes in handy when you want to do multiple queries on a part
1387
-	 * of the document, but then return to a previous set of matches. (see {@link QPTPL}
1388
-	 * for examples of this in practice).
1389
-	 *
1390
-	 * Example:
1391
-	 *
1392
-	 * @code
1393
-	 * <?php
1394
-	 * $qp = qp( QueryPath::HTML_STUB);
1395
-	 * $branch = $qp->branch();
1396
-	 * $branch->find('title')->text('Title');
1397
-	 * $qp->find('body')->text('This is the body')->writeHTML;
1398
-	 * ?>
1399
-	 * @endcode
1400
-	 *
1401
-	 * Notice that in the code, each of the DOMQuery objects is doing its own
1402
-	 * query. However, both are modifying the same document. The result of the above
1403
-	 * would look something like this:
1404
-	 *
1405
-	 * @code
1406
-	 * <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
1407
-	 * <html xmlns="http://www.w3.org/1999/xhtml">
1408
-	 * <head>
1409
-	 *    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"></meta>
1410
-	 *    <title>Title</title>
1411
-	 * </head>
1412
-	 * <body>This is the body</body>
1413
-	 * </html>
1414
-	 * @endcode
1415
-	 *
1416
-	 * Notice that while $qp and $banch were performing separate queries, they
1417
-	 * both modified the same document.
1418
-	 *
1419
-	 * In jQuery or a browser-based solution, you generally do not need a branching
1420
-	 * function because there is (implicitly) only one document. In QueryPath, there
1421
-	 * is no implicit document. Every document must be explicitly specified (and,
1422
-	 * in most cases, parsed -- which is costly). Branching makes it possible to
1423
-	 * work on one document with multiple DOMNode objects.
1424
-	 *
1425
-	 * @param string $selector
1426
-	 *  If a selector is passed in, an additional {@link find()} will be executed
1427
-	 *  on the branch before it is returned. (Added in QueryPath 2.0.)
1428
-	 *
1429
-	 * @return DOMQuery
1430
-	 *  A copy of the DOMQuery object that points to the same set of elements that
1431
-	 *  the original DOMQuery was pointing to.
1432
-	 * @throws CSS\ParseException
1433
-	 * @see   cloneAll()
1434
-	 * @see   find()
1435
-	 * @since 1.1
1436
-	 */
1437
-	public function branch($selector = null)
1438
-	{
1439
-		$temp = QueryPath::with($this->matches, null, $this->options);
1440
-		//if (isset($selector)) $temp->find($selector);
1441
-		$temp->document = $this->document;
1442
-		if (isset($selector)) {
1443
-			$temp->findInPlace($selector);
1444
-		}
1445
-
1446
-		return $temp;
1447
-	}
1448
-
1449
-	/**
1450
-	 * @param $matches
1451
-	 * @param $selector
1452
-	 *
1453
-	 * @return DOMQuery
1454
-	 * @throws CSS\ParseException
1455
-	 */
1456
-	protected function inst($matches, $selector): Query
1457
-	{
1458
-		$dolly = clone $this;
1459
-		$dolly->setMatches($matches);
1460
-
1461
-		if (isset($selector)) {
1462
-			$dolly->findInPlace($selector);
1463
-		}
1464
-
1465
-		return $dolly;
1466
-	}
1467
-
1468
-	/**
1469
-	 * Perform a deep clone of each node in the DOMQuery.
1470
-	 *
1471
-	 * @attention
1472
-	 *   This is an in-place modification of the current QueryPath object.
1473
-	 *
1474
-	 * This does not clone the DOMQuery object, but instead clones the
1475
-	 * list of nodes wrapped by the DOMQuery. Every element is deeply
1476
-	 * cloned.
1477
-	 *
1478
-	 * This method is analogous to jQuery's clone() method.
1479
-	 *
1480
-	 * This is a destructive operation, which means that end() will revert
1481
-	 * the list back to the clone's original.
1482
-	 * @return DOMQuery
1483
-	 * @see qp()
1484
-	 */
1485
-	public function cloneAll(): Query
1486
-	{
1487
-		$found = new SplObjectStorage();
1488
-		foreach ($this->matches as $m) {
1489
-			$found->attach($m->cloneNode(true));
1490
-		}
1491
-		$this->setMatches($found);
1492
-
1493
-		return $this;
1494
-	}
1495
-
1496
-	/**
1497
-	 * Clone the DOMQuery.
1498
-	 *
1499
-	 * This makes a deep clone of the elements inside of the DOMQuery.
1500
-	 *
1501
-	 * This clones only the QueryPathImpl, not all of the decorators. The
1502
-	 * clone operator in PHP should handle the cloning of the decorators.
1503
-	 */
1504
-	public function __clone()
1505
-	{
1506
-		//XXX: Should we clone the document?
1507
-
1508
-		// Make sure we clone the kids.
1509
-		$this->cloneAll();
1510
-	}
1511
-
1512
-	/**
1513
-	 * Call extension methods.
1514
-	 *
1515
-	 * This function is used to invoke extension methods. It searches the
1516
-	 * registered extenstensions for a matching function name. If one is found,
1517
-	 * it is executed with the arguments in the $arguments array.
1518
-	 *
1519
-	 * @throws ReflectionException
1520
-	 * @throws QueryPath::Exception
1521
-	 *  An exception is thrown if a non-existent method is called.
1522
-	 * @throws Exception
1523
-	 */
1524
-	public function __call($name, $arguments)
1525
-	{
1526
-
1527
-		if (! ExtensionRegistry::$useRegistry) {
1528
-			throw new Exception("No method named $name found (Extensions disabled).");
1529
-		}
1530
-
1531
-		// Loading of extensions is deferred until the first time a
1532
-		// non-core method is called. This makes constructing faster, but it
1533
-		// may make the first invocation of __call() slower (if there are
1534
-		// enough extensions.)
1535
-		//
1536
-		// The main reason for moving this out of the constructor is that most
1537
-		// new DOMQuery instances do not use extensions. Charging qp() calls
1538
-		// with the additional hit is not a good idea.
1539
-		//
1540
-		// Also, this will at least limit the number of circular references.
1541
-		if (empty($this->ext)) {
1542
-			// Load the registry
1543
-			$this->ext = ExtensionRegistry::getExtensions($this);
1544
-		}
1545
-
1546
-		// Note that an empty ext registry indicates that extensions are disabled.
1547
-		if (! empty($this->ext) && ExtensionRegistry::hasMethod($name)) {
1548
-			$owner  = ExtensionRegistry::getMethodClass($name);
1549
-			$method = new ReflectionMethod($owner, $name);
1550
-
1551
-			return $method->invokeArgs($this->ext[$owner], $arguments);
1552
-		}
1553
-		throw new Exception("No method named $name found. Possibly missing an extension.");
1554
-	}
1555
-
1556
-	/**
1557
-	 * Get an iterator for the matches in this object.
1558
-	 *
1559
-	 * @return Iterable
1560
-	 *  Returns an iterator.
1561
-	 */
1562
-	public function getIterator(): Traversable
1563
-	{
1564
-		$i          = new QueryPathIterator($this->matches);
1565
-		$i->options = $this->options;
1566
-
1567
-		return $i;
1568
-	}
52
+    use QueryFilters, QueryMutators, QueryChecks;
53
+
54
+    /**
55
+     * The last array of matches.
56
+     */
57
+    protected $last = []; // Last set of matches.
58
+    private $ext = []; // Extensions array.
59
+
60
+    /**
61
+     * The number of current matches.
62
+     *
63
+     * @see count()
64
+     */
65
+    public $length = 0;
66
+
67
+    /**
68
+     * Get the effective options for the current DOMQuery object.
69
+     *
70
+     * This returns an associative array of all of the options as set
71
+     * for the current DOMQuery object. This includes default options,
72
+     * options directly passed in via {@link qp()} or the constructor,
73
+     * an options set in the QueryPath::Options object.
74
+     *
75
+     * The order of merging options is this:
76
+     *  - Options passed in using qp() are highest priority, and will
77
+     *    override other options.
78
+     *  - Options set with QueryPath::Options will override default options,
79
+     *    but can be overridden by options passed into qp().
80
+     *  - Default options will be used when no overrides are present.
81
+     *
82
+     * This function will return the options currently used, with the above option
83
+     * overriding having been calculated already.
84
+     *
85
+     * @return array
86
+     *  An associative array of options, calculated from defaults and overridden
87
+     *  options.
88
+     * @see   qp()
89
+     * @see   QueryPath::Options::set()
90
+     * @see   QueryPath::Options::merge()
91
+     * @since 2.0
92
+     */
93
+    public function getOptions(): array
94
+    {
95
+        return $this->options;
96
+    }
97
+
98
+    /**
99
+     * Select the root element of the document.
100
+     *
101
+     * This sets the current match to the document's root element. For
102
+     * practical purposes, this is the same as:
103
+     *
104
+     * @code
105
+     * qp($someDoc)->find(':root');
106
+     * @endcode
107
+     * However, since it doesn't invoke a parser, it has less overhead. It also
108
+     * works in cases where the QueryPath has been reduced to zero elements (a
109
+     * case that is not handled by find(':root') because there is no element
110
+     * whose root can be found).
111
+     *
112
+     * @param string $selector
113
+     *  A selector. If this is supplied, QueryPath will navigate to the
114
+     *  document root and then run the query. (Added in QueryPath 2.0 Beta 2)
115
+     *
116
+     * @return DOMQuery
117
+     *  The DOMQuery object, wrapping the root element (document element)
118
+     *  for the current document.
119
+     * @throws CSS\ParseException
120
+     */
121
+    public function top($selector = null): Query
122
+    {
123
+        return $this->inst($this->document->documentElement, $selector);
124
+    }
125
+
126
+    /**
127
+     * Given a CSS Selector, find matching items.
128
+     *
129
+     * @param string $selector
130
+     *   CSS 3 Selector
131
+     *
132
+     * @return DOMQuery
133
+     * @throws CSS\ParseException
134
+     * @see  is()
135
+     * @todo If a find() returns zero matches, then a subsequent find() will
136
+     *       also return zero matches, even if that find has a selector like :root.
137
+     *       The reason for this is that the {@link QueryPathEventHandler} does
138
+     *       not set the root of the document tree if it cannot find any elements
139
+     *       from which to determine what the root is. The workaround is to use
140
+     *       {@link top()} to select the root element again.
141
+     * @see  filter()
142
+     */
143
+    public function find($selector): Query
144
+    {
145
+        $query = new DOMTraverser($this->matches);
146
+        $query->find($selector);
147
+
148
+        return $this->inst($query->matches(), null);
149
+    }
150
+
151
+    /**
152
+     * @param $selector
153
+     *
154
+     * @return $this
155
+     * @throws CSS\ParseException
156
+     */
157
+    public function findInPlace($selector)
158
+    {
159
+        $query = new DOMTraverser($this->matches);
160
+        $query->find($selector);
161
+        $this->setMatches($query->matches());
162
+
163
+        return $this;
164
+    }
165
+
166
+    /**
167
+     * Execute an XPath query and store the results in the QueryPath.
168
+     *
169
+     * Most methods in this class support CSS 3 Selectors. Sometimes, though,
170
+     * XPath provides a finer-grained query language. Use this to execute
171
+     * XPath queries.
172
+     *
173
+     * Beware, though. DOMQuery works best on DOM Elements, but an XPath
174
+     * query can return other nodes, strings, and values. These may not work with
175
+     * other QueryPath functions (though you will be able to access the
176
+     * values with {@link get()}).
177
+     *
178
+     * @param string $query
179
+     *      An XPath query.
180
+     * @param array  $options
181
+     *      Currently supported options are:
182
+     *      - 'namespace_prefix': And XML namespace prefix to be used as the default. Used
183
+     *      in conjunction with 'namespace_uri'
184
+     *      - 'namespace_uri': The URI to be used as the default namespace URI. Used
185
+     *      with 'namespace_prefix'
186
+     *
187
+     * @return DOMQuery
188
+     *      A DOMQuery object wrapping the results of the query.
189
+     * @throws CSS\ParseException
190
+     * @author M Butcher
191
+     * @author Xavier Prud'homme
192
+     * @see    find()
193
+     */
194
+    public function xpath($query, $options = [])
195
+    {
196
+        $xpath = new DOMXPath($this->document);
197
+
198
+        // Register a default namespace.
199
+        if (! empty($options['namespace_prefix']) && ! empty($options['namespace_uri'])) {
200
+            $xpath->registerNamespace($options['namespace_prefix'], $options['namespace_uri']);
201
+        }
202
+
203
+        $found = new SplObjectStorage();
204
+        foreach ($this->matches as $item) {
205
+            $nl = $xpath->query($query, $item);
206
+            if ($nl->length > 0) {
207
+                for ($i = 0; $i < $nl->length; ++$i) {
208
+                    $found->attach($nl->item($i));
209
+                }
210
+            }
211
+        }
212
+
213
+        return $this->inst($found, null);
214
+    }
215
+
216
+    /**
217
+     * Get the number of elements currently wrapped by this object.
218
+     *
219
+     * Note that there is no length property on this object.
220
+     *
221
+     * @return int
222
+     *  Number of items in the object.
223
+     * @deprecated QueryPath now implements Countable, so use count().
224
+     */
225
+    public function size()
226
+    {
227
+        return $this->matches->count();
228
+    }
229
+
230
+    /**
231
+     * Get the number of elements currently wrapped by this object.
232
+     *
233
+     * Since DOMQuery is Countable, the PHP count() function can also
234
+     * be used on a DOMQuery.
235
+     *
236
+     * @code
237
+     * <?php
238
+     *  count(qp($xml, 'div'));
239
+     * ?>
240
+     * @endcode
241
+     *
242
+     * @return int
243
+     *  The number of matches in the DOMQuery.
244
+     */
245
+    public function count(): int
246
+    {
247
+        return $this->matches->count();
248
+    }
249
+
250
+    /**
251
+     * Get one or all elements from this object.
252
+     *
253
+     * When called with no paramaters, this returns all objects wrapped by
254
+     * the DOMQuery. Typically, these are DOMElement objects (unless you have
255
+     * used map(), xpath(), or other methods that can select
256
+     * non-elements).
257
+     *
258
+     * When called with an index, it will return the item in the DOMQuery with
259
+     * that index number.
260
+     *
261
+     * Calling this method does not change the DOMQuery (e.g. it is
262
+     * non-destructive).
263
+     *
264
+     * You can use qp()->get() to iterate over all elements matched. You can
265
+     * also iterate over qp() itself (DOMQuery implementations must be Traversable).
266
+     * In the later case, though, each item
267
+     * will be wrapped in a DOMQuery object. To learn more about iterating
268
+     * in QueryPath, see {@link examples/techniques.php}.
269
+     *
270
+     * @param int     $index
271
+     *   If specified, then only this index value will be returned. If this
272
+     *   index is out of bounds, a NULL will be returned.
273
+     * @param boolean $asObject
274
+     *   If this is TRUE, an SplObjectStorage object will be returned
275
+     *   instead of an array. This is the preferred method for extensions to use.
276
+     *
277
+     * @return mixed
278
+     *   If an index is passed, one element will be returned. If no index is
279
+     *   present, an array of all matches will be returned.
280
+     * @see eq()
281
+     * @see SplObjectStorage
282
+     */
283
+    public function get($index = null, $asObject = false)
284
+    {
285
+        if ($index !== null) {
286
+            return ($this->count() > $index) ? $this->getNthMatch($index) : null;
287
+        }
288
+        // Retain support for legacy.
289
+        if (! $asObject) {
290
+            $matches = [];
291
+            foreach ($this->matches as $m) {
292
+                $matches[] = $m;
293
+            }
294
+
295
+            return $matches;
296
+        }
297
+
298
+        return $this->matches;
299
+    }
300
+
301
+    /**
302
+     * Get the namespace of the current element.
303
+     *
304
+     * If QP is currently pointed to a list of elements, this will get the
305
+     * namespace of the first element.
306
+     */
307
+    public function ns()
308
+    {
309
+        return $this->get(0)->namespaceURI;
310
+    }
311
+
312
+    /**
313
+     * Get the DOMDocument that we currently work with.
314
+     *
315
+     * This returns the current DOMDocument. Any changes made to this document will be
316
+     * accessible to DOMQuery, as both will share access to the same object.
317
+     *
318
+     * @return DOMDocument
319
+     */
320
+    public function document()
321
+    {
322
+        return $this->document;
323
+    }
324
+
325
+    /**
326
+     * On an XML document, load all XIncludes.
327
+     *
328
+     * @return DOMQuery
329
+     */
330
+    public function xinclude()
331
+    {
332
+        $this->document->xinclude();
333
+
334
+        return $this;
335
+    }
336
+
337
+    /**
338
+     * Get all current elements wrapped in an array.
339
+     * Compatibility function for jQuery 1.4, but identical to calling {@link get()}
340
+     * with no parameters.
341
+     *
342
+     * @return array
343
+     *  An array of DOMNodes (typically DOMElements).
344
+     */
345
+    public function toArray()
346
+    {
347
+        return $this->get();
348
+    }
349
+
350
+    /**
351
+     * Insert or retrieve a Data URL.
352
+     *
353
+     * When called with just $attr, it will fetch the result, attempt to decode it, and
354
+     * return an array with the MIME type and the application data.
355
+     *
356
+     * When called with both $attr and $data, it will inject the data into all selected elements
357
+     * So @code$qp->dataURL('src', file_get_contents('my.png'), 'image/png')@endcode will inject
358
+     * the given PNG image into the selected elements.
359
+     *
360
+     * The current implementation only knows how to encode and decode Base 64 data.
361
+     *
362
+     * Note that this is known *not* to work on IE 6, but should render fine in other browsers.
363
+     *
364
+     * @param string   $attr
365
+     *    The name of the attribute.
366
+     * @param mixed    $data
367
+     *    The contents to inject as the data. The value can be any one of the following:
368
+     *    - A URL: If this is given, then the subsystem will read the content from that URL. THIS
369
+     *    MUST BE A FULL URL, not a relative path.
370
+     *    - A string of data: If this is given, then the subsystem will encode the string.
371
+     *    - A stream or file handle: If this is given, the stream's contents will be encoded
372
+     *    and inserted as data.
373
+     *    (Note that we make the assumption here that you would never want to set data to be
374
+     *    a URL. If this is an incorrect assumption, file a bug.)
375
+     * @param string   $mime
376
+     *    The MIME type of the document.
377
+     * @param resource $context
378
+     *    A valid context. Use this only if you need to pass a stream context. This is only necessary
379
+     *    if $data is a URL. (See {@link stream_context_create()}).
380
+     *
381
+     * @return DOMQuery|string
382
+     *    If this is called as a setter, this will return a DOMQuery object. Otherwise, it
383
+     *    will attempt to fetch data out of the attribute and return that.
384
+     * @see   http://en.wikipedia.org/wiki/Data:_URL
385
+     * @see   attr()
386
+     * @since 2.1
387
+     */
388
+    public function dataURL($attr, $data = null, $mime = 'application/octet-stream', $context = null)
389
+    {
390
+        if (is_null($data)) {
391
+            // Attempt to fetch the data
392
+            $data = $this->attr($attr);
393
+            if (empty($data) || is_array($data) || strpos($data, 'data:') !== 0) {
394
+                return;
395
+            }
396
+
397
+            // So 1 and 2 should be MIME types, and 3 should be the base64-encoded data.
398
+            $regex   = '/^data:([a-zA-Z0-9]+)\/([a-zA-Z0-9]+);base64,(.*)$/';
399
+            $matches = [];
400
+            preg_match($regex, $data, $matches);
401
+
402
+            if (! empty($matches)) {
403
+                $result = [
404
+                    'mime' => $matches[1] . '/' . $matches[2],
405
+                    'data' => base64_decode($matches[3]),
406
+                ];
407
+
408
+                return $result;
409
+            }
410
+        } else {
411
+            $attVal = QueryPath::encodeDataURL($data, $mime, $context);
412
+
413
+            return $this->attr($attr, $attVal);
414
+        }
415
+    }
416
+
417
+    /**
418
+     * Sort the contents of the QueryPath object.
419
+     *
420
+     * By default, this does not change the order of the elements in the
421
+     * DOM. Instead, it just sorts the internal list. However, if TRUE
422
+     * is passed in as the second parameter then QueryPath will re-order
423
+     * the DOM, too.
424
+     *
425
+     * @attention
426
+     * DOM re-ordering is done by finding the location of the original first
427
+     * item in the list, and then placing the sorted list at that location.
428
+     *
429
+     * The argument $compartor is a callback, such as a function name or a
430
+     * closure. The callback receives two DOMNode objects, which you can use
431
+     * as DOMNodes, or wrap in QueryPath objects.
432
+     *
433
+     * A simple callback:
434
+     * @code
435
+     * <?php
436
+     * $comp = function (\DOMNode $a, \DOMNode $b) {
437
+     *   if ($a->textContent == $b->textContent) {
438
+     *     return 0;
439
+     *   }
440
+     *   return $a->textContent > $b->textContent ? 1 : -1;
441
+     * };
442
+     * $qp = QueryPath::with($xml, $selector)->sort($comp);
443
+     * ?>
444
+     * @endcode
445
+     *
446
+     * The above sorts the matches into lexical order using the text of each node.
447
+     * If you would prefer to work with QueryPath objects instead of DOMNode
448
+     * objects, you may prefer something like this:
449
+     *
450
+     * @code
451
+     * <?php
452
+     * $comp = function (\DOMNode $a, \DOMNode $b) {
453
+     *   $qpa = qp($a);
454
+     *   $qpb = qp($b);
455
+     *
456
+     *   if ($qpa->text() == $qpb->text()) {
457
+     *     return 0;
458
+     *   }
459
+     *   return $qpa->text()> $qpb->text()? 1 : -1;
460
+     * };
461
+     *
462
+     * $qp = QueryPath::with($xml, $selector)->sort($comp);
463
+     * ?>
464
+     * @endcode
465
+     *
466
+     * @param callback $comparator
467
+     *   A callback. This will be called during sorting to compare two DOMNode
468
+     *   objects.
469
+     * @param boolean  $modifyDOM
470
+     *   If this is TRUE, the sorted results will be inserted back into
471
+     *   the DOM at the position of the original first element.
472
+     *
473
+     * @return DOMQuery
474
+     *   This object.
475
+     * @throws CSS\ParseException
476
+     */
477
+    public function sort($comparator, $modifyDOM = false): Query
478
+    {
479
+        // Sort as an array.
480
+        $list = iterator_to_array($this->matches);
481
+
482
+        if (empty($list)) {
483
+            return $this;
484
+        }
485
+
486
+        $oldFirst = $list[0];
487
+
488
+        usort($list, $comparator);
489
+
490
+        // Copy back into SplObjectStorage.
491
+        $found = new SplObjectStorage();
492
+        foreach ($list as $node) {
493
+            $found->attach($node);
494
+        }
495
+        //$this->setMatches($found);
496
+
497
+
498
+        // Do DOM modifications only if necessary.
499
+        if ($modifyDOM) {
500
+            $placeholder = $oldFirst->ownerDocument->createElement('_PLACEHOLDER_');
501
+            $placeholder = $oldFirst->parentNode->insertBefore($placeholder, $oldFirst);
502
+            $len         = count($list);
503
+            for ($i = 0; $i < $len; ++$i) {
504
+                $node = $list[$i];
505
+                $node = $node->parentNode->removeChild($node);
506
+                $placeholder->parentNode->insertBefore($node, $placeholder);
507
+            }
508
+            $placeholder->parentNode->removeChild($placeholder);
509
+        }
510
+
511
+        return $this->inst($found, null);
512
+    }
513
+
514
+    /**
515
+     * Get an item's index.
516
+     *
517
+     * Given a DOMElement, get the index from the matches. This is the
518
+     * converse of {@link get()}.
519
+     *
520
+     * @param DOMElement $subject
521
+     *  The item to match.
522
+     *
523
+     * @return mixed
524
+     *  The index as an integer (if found), or boolean FALSE. Since 0 is a
525
+     *  valid index, you should use strong equality (===) to test..
526
+     * @see get()
527
+     * @see is()
528
+     */
529
+    public function index($subject)
530
+    {
531
+        $i = 0;
532
+        foreach ($this->matches as $m) {
533
+            if ($m === $subject) {
534
+                return $i;
535
+            }
536
+            ++$i;
537
+        }
538
+
539
+        return false;
540
+    }
541
+
542
+    /**
543
+     * The tag name of the first element in the list.
544
+     *
545
+     * This returns the tag name of the first element in the list of matches. If
546
+     * the list is empty, an empty string will be used.
547
+     *
548
+     * @return string
549
+     *  The tag name of the first element in the list.
550
+     * @see replaceWith()
551
+     * @see replaceAll()
552
+     */
553
+    public function tag()
554
+    {
555
+        return ($this->matches->count() > 0) ? $this->getFirstMatch()->tagName : '';
556
+    }
557
+
558
+    /**
559
+     * Revert to the previous set of matches.
560
+     *
561
+     * <b>DEPRECATED</b> Do not use.
562
+     *
563
+     * This will revert back to the last set of matches (before the last
564
+     * "destructive" set of operations). This undoes any change made to the set of
565
+     * matched objects. Functions like find() and filter() change the
566
+     * list of matched objects. The end() function will revert back to the last set of
567
+     * matched items.
568
+     *
569
+     * Note that functions that modify the document, but do not change the list of
570
+     * matched objects, are not "destructive". Thus, calling append('something')->end()
571
+     * will not undo the append() call.
572
+     *
573
+     * Only one level of changes is stored. Reverting beyond that will result in
574
+     * an empty set of matches. Example:
575
+     *
576
+     * @code
577
+     * // The line below returns the same thing as qp(document, 'p');
578
+     * qp(document, 'p')->find('div')->end();
579
+     * // This returns an empty array:
580
+     * qp(document, 'p')->end();
581
+     * // This returns an empty array:
582
+     * qp(document, 'p')->find('div')->find('span')->end()->end();
583
+     * @endcode
584
+     *
585
+     * The last one returns an empty array because only one level of changes is stored.
586
+     *
587
+     * @return DOMQuery
588
+     *  A DOMNode object reflecting the list of matches prior to the last destructive
589
+     *  operation.
590
+     * @see        andSelf()
591
+     * @see        add()
592
+     * @deprecated This function will be removed.
593
+     */
594
+    public function end()
595
+    {
596
+        // Note that this does not use setMatches because it must set the previous
597
+        // set of matches to empty array.
598
+        $this->matches = $this->last;
599
+        $this->last    = new SplObjectStorage();
600
+
601
+        return $this;
602
+    }
603
+
604
+    /**
605
+     * Combine the current and previous set of matched objects.
606
+     *
607
+     * Example:
608
+     *
609
+     * @code
610
+     * qp(document, 'p')->find('div')->andSelf();
611
+     * @endcode
612
+     *
613
+     * The code above will contain a list of all p elements and all div elements that
614
+     * are beneath p elements.
615
+     *
616
+     * @return DOMQuery
617
+     *  A DOMNode object with the results of the last two "destructive" operations.
618
+     * @see end();
619
+     * @see add()
620
+     * @see end()
621
+     */
622
+    public function andSelf()
623
+    {
624
+        // This is destructive, so we need to set $last:
625
+        $last = $this->matches;
626
+
627
+        foreach ($this->last as $item) {
628
+            $this->matches->attach($item);
629
+        }
630
+
631
+        $this->last = $last;
632
+
633
+        return $this;
634
+    }
635
+
636
+    /**
637
+     * Set or get the markup for an element.
638
+     *
639
+     * If $markup is set, then the giving markup will be injected into each
640
+     * item in the set. All other children of that node will be deleted, and this
641
+     * new code will be the only child or children. The markup MUST BE WELL FORMED.
642
+     *
643
+     * If no markup is given, this will return a string representing the child
644
+     * markup of the first node.
645
+     *
646
+     * <b>Important:</b> This differs from jQuery's html() function. This function
647
+     * returns <i>the current node</i> and all of its children. jQuery returns only
648
+     * the children. This means you do not need to do things like this:
649
+     * @code$qp->parent()->html()@endcode.
650
+     *
651
+     * By default, this is HTML 4.01, not XHTML. Use {@link xml()} for XHTML.
652
+     *
653
+     * @param string $markup
654
+     *  The text to insert.
655
+     *
656
+     * @return mixed
657
+     *  A string if no markup was passed, or a DOMQuery if markup was passed.
658
+     * @throws Exception
659
+     * @throws QueryPath
660
+     * @see xml()
661
+     * @see text()
662
+     * @see contents()
663
+     */
664
+    public function html($markup = null)
665
+    {
666
+        if (isset($markup)) {
667
+            if ($this->options['replace_entities']) {
668
+                $markup = Entities::replaceAllEntities($markup);
669
+            }
670
+
671
+            // Parse the HTML and insert it into the DOM
672
+            //$doc = DOMDocument::loadHTML($markup);
673
+            $doc = $this->document->createDocumentFragment();
674
+            $doc->appendXML($markup);
675
+            $this->removeChildren();
676
+            $this->append($doc);
677
+
678
+            return $this;
679
+        }
680
+        $length = $this->matches->count();
681
+        if ($length === 0) {
682
+            return null;
683
+        }
684
+        // Only return the first item -- that's what JQ does.
685
+        $first = $this->getFirstMatch();
686
+
687
+        // Catch cases where first item is not a legit DOM object.
688
+        if (! ($first instanceof DOMNode)) {
689
+            return null;
690
+        }
691
+
692
+        // Added by eabrand.
693
+        if (! $first->ownerDocument->documentElement) {
694
+            return null;
695
+        }
696
+
697
+        if ($first instanceof DOMDocument || $first->isSameNode($first->ownerDocument->documentElement)) {
698
+            return $this->document->saveHTML();
699
+        }
700
+
701
+        // saveHTML cannot take a node and serialize it.
702
+        return $this->document->saveXML($first);
703
+    }
704
+
705
+    /**
706
+     * Write the QueryPath document to HTML5.
707
+     *
708
+     * See html()
709
+     *
710
+     * @param null $markup
711
+     *
712
+     * @return null|DOMQuery|string
713
+     * @throws QueryPath
714
+     * @throws \QueryPath\Exception
715
+     */
716
+    public function html5($markup = null)
717
+    {
718
+        $html5 = new HTML5($this->options);
719
+
720
+        // append HTML to existing
721
+        if ($markup === null) {
722
+            // Parse the HTML and insert it into the DOM
723
+            $doc = $html5->loadHTMLFragment($markup);
724
+            $this->removeChildren();
725
+            $this->append($doc);
726
+
727
+            return $this;
728
+        }
729
+
730
+        $length = $this->count();
731
+        if ($length === 0) {
732
+            return null;
733
+        }
734
+        // Only return the first item -- that's what JQ does.
735
+        $first = $this->getFirstMatch();
736
+
737
+        // Catch cases where first item is not a legit DOM object.
738
+        if (! ($first instanceof DOMNode)) {
739
+            return null;
740
+        }
741
+
742
+        // Added by eabrand.
743
+        if (! $first->ownerDocument->documentElement) {
744
+            return null;
745
+        }
746
+
747
+        if ($first instanceof DOMDocument || $first->isSameNode($first->ownerDocument->documentElement)) {
748
+            return $html5->saveHTML($this->document); //$this->document->saveHTML();
749
+        }
750
+
751
+        return $html5->saveHTML($first);
752
+    }
753
+
754
+    /**
755
+     * Fetch the HTML contents INSIDE of the first DOMQuery item.
756
+     *
757
+     * <b>This behaves the way jQuery's @codehtml()@endcode function behaves.</b>
758
+     *
759
+     * This gets all children of the first match in DOMQuery.
760
+     *
761
+     * Consider this fragment:
762
+     *
763
+     * @code
764
+     * <div>
765
+     * test <p>foo</p> test
766
+     * </div>
767
+     * @endcode
768
+     *
769
+     * We can retrieve just the contents of this code by doing something like
770
+     * this:
771
+     * @code
772
+     * qp($xml, 'div')->innerHTML();
773
+     * @endcode
774
+     *
775
+     * This would return the following:
776
+     * @codetest <p>foo</p> test@endcode
777
+     *
778
+     * @return string
779
+     *  Returns a string representation of the child nodes of the first
780
+     *  matched element.
781
+     * @see   html()
782
+     * @see   innerXML()
783
+     * @see   innerXHTML()
784
+     * @since 2.0
785
+     */
786
+    public function innerHTML()
787
+    {
788
+        return $this->innerXML();
789
+    }
790
+
791
+    /**
792
+     * Fetch child (inner) nodes of the first match.
793
+     *
794
+     * This will return the children of the present match. For an example,
795
+     * see {@link innerHTML()}.
796
+     *
797
+     * @return string
798
+     *  Returns a string of XHTML that represents the children of the present
799
+     *  node.
800
+     * @see   innerXML()
801
+     * @see   innerHTML()
802
+     * @since 2.0
803
+     */
804
+    public function innerXHTML()
805
+    {
806
+        $length = $this->matches->count();
807
+        if ($length === 0) {
808
+            return null;
809
+        }
810
+        // Only return the first item -- that's what JQ does.
811
+        $first = $this->getFirstMatch();
812
+
813
+        // Catch cases where first item is not a legit DOM object.
814
+        if (! ($first instanceof DOMNode)) {
815
+            return null;
816
+        }
817
+
818
+        if (! $first->hasChildNodes()) {
819
+            return '';
820
+        }
821
+
822
+        $buffer = '';
823
+        foreach ($first->childNodes as $child) {
824
+            $buffer .= $this->document->saveXML($child, LIBXML_NOEMPTYTAG);
825
+        }
826
+
827
+        return $buffer;
828
+    }
829
+
830
+    /**
831
+     * Fetch child (inner) nodes of the first match.
832
+     *
833
+     * This will return the children of the present match. For an example,
834
+     * see {@link innerHTML()}.
835
+     *
836
+     * @return string
837
+     *  Returns a string of XHTML that represents the children of the present
838
+     *  node.
839
+     * @see   innerXHTML()
840
+     * @see   innerHTML()
841
+     * @since 2.0
842
+     */
843
+    public function innerXML()
844
+    {
845
+        $length = $this->matches->count();
846
+        if ($length === 0) {
847
+            return null;
848
+        }
849
+        // Only return the first item -- that's what JQ does.
850
+        $first = $this->getFirstMatch();
851
+
852
+        // Catch cases where first item is not a legit DOM object.
853
+        if (! ($first instanceof DOMNode)) {
854
+            return null;
855
+        }
856
+
857
+        if (! $first->hasChildNodes()) {
858
+            return '';
859
+        }
860
+
861
+        $buffer = '';
862
+        foreach ($first->childNodes as $child) {
863
+            $buffer .= $this->document->saveXML($child);
864
+        }
865
+
866
+        return $buffer;
867
+    }
868
+
869
+    /**
870
+     * Get child elements as an HTML5 string.
871
+     *
872
+     * TODO: This is a very simple alteration of innerXML. Do we need better
873
+     * support?
874
+     */
875
+    public function innerHTML5()
876
+    {
877
+        $length = $this->matches->count();
878
+        if ($length === 0) {
879
+            return null;
880
+        }
881
+        // Only return the first item -- that's what JQ does.
882
+        $first = $this->getFirstMatch();
883
+
884
+        // Catch cases where first item is not a legit DOM object.
885
+        if (! ($first instanceof DOMNode)) {
886
+            return null;
887
+        }
888
+
889
+        if (! $first->hasChildNodes()) {
890
+            return '';
891
+        }
892
+
893
+        $html5  = new HTML5($this->options);
894
+        $buffer = '';
895
+        foreach ($first->childNodes as $child) {
896
+            $buffer .= $html5->saveHTML($child);
897
+        }
898
+
899
+        return $buffer;
900
+    }
901
+
902
+    /**
903
+     * Retrieve the text of each match and concatenate them with the given separator.
904
+     *
905
+     * This has the effect of looping through all children, retrieving their text
906
+     * content, and then concatenating the text with a separator.
907
+     *
908
+     * @param string  $sep
909
+     *  The string used to separate text items. The default is a comma followed by a
910
+     *  space.
911
+     * @param boolean $filterEmpties
912
+     *  If this is true, empty items will be ignored.
913
+     *
914
+     * @return string
915
+     *  The text contents, concatenated together with the given separator between
916
+     *  every pair of items.
917
+     * @see   implode()
918
+     * @see   text()
919
+     * @since 2.0
920
+     */
921
+    public function textImplode($sep = ', ', $filterEmpties = true): string
922
+    {
923
+        $tmp = [];
924
+        foreach ($this->matches as $m) {
925
+            $txt     = $m->textContent;
926
+            $trimmed = trim($txt);
927
+            // If filter empties out, then we only add items that have content.
928
+            if ($filterEmpties) {
929
+                if (strlen($trimmed) > 0) {
930
+                    $tmp[] = $txt;
931
+                }
932
+            } // Else add all content, even if it's empty.
933
+            else {
934
+                $tmp[] = $txt;
935
+            }
936
+        }
937
+
938
+        return implode($sep, $tmp);
939
+    }
940
+
941
+    /**
942
+     * Get the text contents from just child elements.
943
+     *
944
+     * This is a specialized variant of textImplode() that implodes text for just the
945
+     * child elements of the current element.
946
+     *
947
+     * @param string $separator
948
+     *  The separator that will be inserted between found text content.
949
+     *
950
+     * @return string
951
+     *  The concatenated values of all children.
952
+     * @throws CSS\ParseException
953
+     */
954
+    public function childrenText($separator = ' '): string
955
+    {
956
+        // Branch makes it non-destructive.
957
+        return $this->branch()->xpath('descendant::text()')->textImplode($separator);
958
+    }
959
+
960
+    /**
961
+     * Get or set the text contents of a node.
962
+     *
963
+     * @param string $text
964
+     *  If this is not NULL, this value will be set as the text of the node. It
965
+     *  will replace any existing content.
966
+     *
967
+     * @return mixed
968
+     *  A DOMQuery if $text is set, or the text content if no text
969
+     *  is passed in as a pram.
970
+     * @see html()
971
+     * @see xml()
972
+     * @see contents()
973
+     */
974
+    public function text($text = null)
975
+    {
976
+        if (isset($text)) {
977
+            $this->removeChildren();
978
+            foreach ($this->matches as $m) {
979
+                $m->appendChild($this->document->createTextNode($text));
980
+            }
981
+
982
+            return $this;
983
+        }
984
+        // Returns all text as one string:
985
+        $buf = '';
986
+        foreach ($this->matches as $m) {
987
+            $buf .= $m->textContent;
988
+        }
989
+
990
+        return $buf;
991
+    }
992
+
993
+    /**
994
+     * Get or set the text before each selected item.
995
+     *
996
+     * If $text is passed in, the text is inserted before each currently selected item.
997
+     *
998
+     * If no text is given, this will return the concatenated text after each selected element.
999
+     *
1000
+     * @code
1001
+     * <?php
1002
+     * $xml = '<?xml version="1.0"?><root>Foo<a>Bar</a><b/></root>';
1003
+     *
1004
+     * // This will return 'Foo'
1005
+     * qp($xml, 'a')->textBefore();
1006
+     *
1007
+     * // This will insert 'Baz' right before <b/>.
1008
+     * qp($xml, 'b')->textBefore('Baz');
1009
+     * ?>
1010
+     * @endcode
1011
+     *
1012
+     * @param string $text
1013
+     *  If this is set, it will be inserted before each node in the current set of
1014
+     *  selected items.
1015
+     *
1016
+     * @return mixed
1017
+     *  Returns the DOMQuery object if $text was set, and returns a string (possibly empty)
1018
+     *  if no param is passed.
1019
+     * @throws Exception
1020
+     * @throws QueryPath
1021
+     */
1022
+    public function textBefore($text = null)
1023
+    {
1024
+        if (isset($text)) {
1025
+            $textNode = $this->document->createTextNode($text);
1026
+
1027
+            return $this->before($textNode);
1028
+        }
1029
+        $buffer = '';
1030
+        foreach ($this->matches as $m) {
1031
+            $p = $m;
1032
+            while (isset($p->previousSibling) && $p->previousSibling->nodeType === XML_TEXT_NODE) {
1033
+                $p      = $p->previousSibling;
1034
+                $buffer .= $p->textContent;
1035
+            }
1036
+        }
1037
+
1038
+        return $buffer;
1039
+    }
1040
+
1041
+    public function textAfter($text = null)
1042
+    {
1043
+        if (isset($text)) {
1044
+            $textNode = $this->document->createTextNode($text);
1045
+
1046
+            return $this->after($textNode);
1047
+        }
1048
+        $buffer = '';
1049
+        foreach ($this->matches as $m) {
1050
+            $n = $m;
1051
+            while (isset($n->nextSibling) && $n->nextSibling->nodeType === XML_TEXT_NODE) {
1052
+                $n      = $n->nextSibling;
1053
+                $buffer .= $n->textContent;
1054
+            }
1055
+        }
1056
+
1057
+        return $buffer;
1058
+    }
1059
+
1060
+    /**
1061
+     * Set or get the value of an element's 'value' attribute.
1062
+     *
1063
+     * The 'value' attribute is common in HTML form elements. This is a
1064
+     * convenience function for accessing the values. Since this is not  common
1065
+     * task on the server side, this method may be removed in future releases. (It
1066
+     * is currently provided for jQuery compatibility.)
1067
+     *
1068
+     * If a value is provided in the params, then the value will be set for all
1069
+     * matches. If no params are given, then the value of the first matched element
1070
+     * will be returned. This may be NULL.
1071
+     *
1072
+     * @deprecated Just use attr(). There's no reason to use this on the server.
1073
+     * @see        attr()
1074
+     *
1075
+     * @param string $value
1076
+     *
1077
+     * @return mixed
1078
+     *  Returns a DOMQuery if a string was passed in, and a string if no string
1079
+     *  was passed in. In the later case, an error will produce NULL.
1080
+     */
1081
+    public function val($value = null)
1082
+    {
1083
+        if (isset($value)) {
1084
+            $this->attr('value', $value);
1085
+
1086
+            return $this;
1087
+        }
1088
+
1089
+        return $this->attr('value');
1090
+    }
1091
+
1092
+    /**
1093
+     * Set or get XHTML markup for an element or elements.
1094
+     *
1095
+     * This differs from {@link html()} in that it processes (and produces)
1096
+     * strictly XML 1.0 compliant markup.
1097
+     *
1098
+     * Like {@link xml()} and {@link html()}, this functions as both a
1099
+     * setter and a getter.
1100
+     *
1101
+     * This is a convenience function for fetching HTML in XML format.
1102
+     * It does no processing of the markup (such as schema validation).
1103
+     *
1104
+     * @param string $markup
1105
+     *  A string containing XML data.
1106
+     *
1107
+     * @return mixed
1108
+     *  If markup is passed in, a DOMQuery is returned. If no markup is passed
1109
+     *  in, XML representing the first matched element is returned.
1110
+     * @see html()
1111
+     * @see innerXHTML()
1112
+     */
1113
+    public function xhtml($markup = null)
1114
+    {
1115
+
1116
+        // XXX: This is a minor reworking of the original xml() method.
1117
+        // This should be refactored, probably.
1118
+        // See http://github.com/technosophos/querypath/issues#issue/10
1119
+
1120
+        $omit_xml_decl = $this->options['omit_xml_declaration'];
1121
+        if ($markup === true) {
1122
+            // Basically, we handle the special case where we don't
1123
+            // want the XML declaration to be displayed.
1124
+            $omit_xml_decl = true;
1125
+        } elseif (isset($markup)) {
1126
+            return $this->xml($markup);
1127
+        }
1128
+
1129
+        $length = $this->matches->count();
1130
+        if ($length === 0) {
1131
+            return null;
1132
+        }
1133
+
1134
+        // Only return the first item -- that's what JQ does.
1135
+        $first = $this->getFirstMatch();
1136
+        // Catch cases where first item is not a legit DOM object.
1137
+        if (! ($first instanceof DOMNode)) {
1138
+            return null;
1139
+        }
1140
+
1141
+        if ($first instanceof DOMDocument || $first->isSameNode($first->ownerDocument->documentElement)) {
1142
+            // Has the unfortunate side-effect of stripping doctype.
1143
+            //$text = ($omit_xml_decl ? $this->document->saveXML($first->ownerDocument->documentElement, LIBXML_NOEMPTYTAG) : $this->document->saveXML(NULL, LIBXML_NOEMPTYTAG));
1144
+            $text = $this->document->saveXML(null, LIBXML_NOEMPTYTAG);
1145
+        } else {
1146
+            $text = $this->document->saveXML($first, LIBXML_NOEMPTYTAG);
1147
+        }
1148
+
1149
+        // Issue #47: Using the old trick for removing the XML tag also removed the
1150
+        // doctype. So we remove it with a regex:
1151
+        if ($omit_xml_decl) {
1152
+            $text = preg_replace('/<\?xml\s[^>]*\?>/', '', $text);
1153
+        }
1154
+
1155
+        // This is slightly lenient: It allows for cases where code incorrectly places content
1156
+        // inside of these supposedly unary elements.
1157
+        $unary = '/<(area|base|basefont|br|col|frame|hr|img|input|isindex|link|meta|param)(?(?=\s)([^>\/]+))><\/[^>]*>/i';
1158
+        $text  = preg_replace($unary, '<\\1\\2 />', $text);
1159
+
1160
+        // Experimental: Support for enclosing CDATA sections with comments to be both XML compat
1161
+        // and HTML 4/5 compat
1162
+        $cdata   = '/(<!\[CDATA\[|\]\]>)/i';
1163
+        $replace = $this->options['escape_xhtml_js_css_sections'];
1164
+        $text    = preg_replace($cdata, $replace, $text);
1165
+
1166
+        return $text;
1167
+    }
1168
+
1169
+    /**
1170
+     * Set or get the XML markup for an element or elements.
1171
+     *
1172
+     * Like {@link html()}, this functions in both a setter and a getter mode.
1173
+     *
1174
+     * In setter mode, the string passed in will be parsed and then appended to the
1175
+     * elements wrapped by this DOMNode object.When in setter mode, this parses
1176
+     * the XML using the DOMFragment parser. For that reason, an XML declaration
1177
+     * is not necessary.
1178
+     *
1179
+     * In getter mode, the first element wrapped by this DOMNode object will be
1180
+     * converted to an XML string and returned.
1181
+     *
1182
+     * @param string $markup
1183
+     *  A string containing XML data.
1184
+     *
1185
+     * @return mixed
1186
+     *  If markup is passed in, a DOMQuery is returned. If no markup is passed
1187
+     *  in, XML representing the first matched element is returned.
1188
+     * @see xhtml()
1189
+     * @see html()
1190
+     * @see text()
1191
+     * @see content()
1192
+     * @see innerXML()
1193
+     */
1194
+    public function xml($markup = null)
1195
+    {
1196
+        $omit_xml_decl = $this->options['omit_xml_declaration'];
1197
+        if ($markup === true) {
1198
+            // Basically, we handle the special case where we don't
1199
+            // want the XML declaration to be displayed.
1200
+            $omit_xml_decl = true;
1201
+        } elseif (isset($markup)) {
1202
+            if ($this->options['replace_entities']) {
1203
+                $markup = Entities::replaceAllEntities($markup);
1204
+            }
1205
+            $doc = $this->document->createDocumentFragment();
1206
+            $doc->appendXML($markup);
1207
+            $this->removeChildren();
1208
+            $this->append($doc);
1209
+
1210
+            return $this;
1211
+        }
1212
+        $length = $this->matches->count();
1213
+        if ($length === 0) {
1214
+            return null;
1215
+        }
1216
+        // Only return the first item -- that's what JQ does.
1217
+        $first = $this->getFirstMatch();
1218
+
1219
+        // Catch cases where first item is not a legit DOM object.
1220
+        if (! ($first instanceof DOMNode)) {
1221
+            return null;
1222
+        }
1223
+
1224
+        if ($first instanceof DOMDocument || $first->isSameNode($first->ownerDocument->documentElement)) {
1225
+            return ($omit_xml_decl ? $this->document->saveXML($first->ownerDocument->documentElement) : $this->document->saveXML());
1226
+        }
1227
+
1228
+        return $this->document->saveXML($first);
1229
+    }
1230
+
1231
+    /**
1232
+     * Send the XML document to the client.
1233
+     *
1234
+     * Write the document to a file path, if given, or
1235
+     * to stdout (usually the client).
1236
+     *
1237
+     * This prints the entire document.
1238
+     *
1239
+     * @param string $path
1240
+     *  The path to the file into which the XML should be written. if
1241
+     *  this is NULL, data will be written to STDOUT, which is usually
1242
+     *  sent to the remote browser.
1243
+     * @param int    $options
1244
+     *  (As of QueryPath 2.1) Pass libxml options to the saving mechanism.
1245
+     *
1246
+     * @return DOMQuery
1247
+     *  The DOMQuery object, unmodified.
1248
+     * @throws Exception
1249
+     *  In the event that a file cannot be written, an Exception will be thrown.
1250
+     * @see innerXML()
1251
+     * @see writeXHTML()
1252
+     * @see xml()
1253
+     */
1254
+    public function writeXML($path = null, $options = 0)
1255
+    {
1256
+        // Backwards compatibility fix for PHP8+
1257
+        if (is_null($options)) {
1258
+            $options = 0;
1259
+        }
1260
+
1261
+        if ($path === null) {
1262
+            print $this->document->saveXML(null, $options);
1263
+        } else {
1264
+            try {
1265
+                set_error_handler([IOException::class, 'initializeFromError']);
1266
+                $this->document->save($path, $options);
1267
+            } catch (Exception $e) {
1268
+                restore_error_handler();
1269
+                throw $e;
1270
+            }
1271
+            restore_error_handler();
1272
+        }
1273
+
1274
+        return $this;
1275
+    }
1276
+
1277
+    /**
1278
+     * Writes HTML to output.
1279
+     *
1280
+     * HTML is formatted as HTML 4.01, without strict XML unary tags. This is for
1281
+     * legacy HTML content. Modern XHTML should be written using {@link toXHTML()}.
1282
+     *
1283
+     * Write the document to stdout (usually the client) or to a file.
1284
+     *
1285
+     * @param string $path
1286
+     *  The path to the file into which the XML should be written. if
1287
+     *  this is NULL, data will be written to STDOUT, which is usually
1288
+     *  sent to the remote browser.
1289
+     *
1290
+     * @return DOMQuery
1291
+     *  The DOMQuery object, unmodified.
1292
+     * @throws Exception
1293
+     *  In the event that a file cannot be written, an Exception will be thrown.
1294
+     * @see innerHTML()
1295
+     * @see html()
1296
+     */
1297
+    public function writeHTML($path = null)
1298
+    {
1299
+        if ($path === null) {
1300
+            print $this->document->saveHTML();
1301
+        } else {
1302
+            try {
1303
+                set_error_handler(['\QueryPath\ParseException', 'initializeFromError']);
1304
+                $this->document->saveHTMLFile($path);
1305
+            } catch (Exception $e) {
1306
+                restore_error_handler();
1307
+                throw $e;
1308
+            }
1309
+            restore_error_handler();
1310
+        }
1311
+
1312
+        return $this;
1313
+    }
1314
+
1315
+    /**
1316
+     * Write the document to HTML5.
1317
+     *
1318
+     * This works the same as the other write* functions, but it encodes the output
1319
+     * as HTML5 with UTF-8.
1320
+     *
1321
+     * @throws Exception
1322
+     *  In the event that a file cannot be written, an Exception will be thrown.
1323
+     * @see innerHTML5()
1324
+     * @see html5()
1325
+     */
1326
+    public function writeHTML5($path = null)
1327
+    {
1328
+        $html5 = new HTML5();
1329
+        if ($path === null) {
1330
+            // Print the document to stdout.
1331
+            print $html5->saveHTML($this->document);
1332
+
1333
+            return;
1334
+        }
1335
+
1336
+        $html5->save($this->document, $path);
1337
+    }
1338
+
1339
+    /**
1340
+     * Write an XHTML file to output.
1341
+     *
1342
+     * Typically, you should use this instead of {@link writeHTML()}.
1343
+     *
1344
+     * Currently, this functions identically to {@link toXML()} <i>except that</i>
1345
+     * it always uses closing tags (e.g. always @code<script></script>@endcode,
1346
+     * never @code<script/>@endcode). It will
1347
+     * write the file as well-formed XML. No XHTML schema validation is done.
1348
+     *
1349
+     * @param string $path
1350
+     *  The filename of the file to write to.
1351
+     *
1352
+     * @return DOMQuery
1353
+     *  Returns the DOMQuery, unmodified.
1354
+     * @throws Exception
1355
+     *  In the event that the output file cannot be written, an exception is
1356
+     *  thrown.
1357
+     * @see   innerXHTML()
1358
+     * @see   xhtml()
1359
+     * @see   writeXML()
1360
+     * @see   xml()
1361
+     * @see   writeHTML()
1362
+     * @since 2.0
1363
+     */
1364
+    public function writeXHTML($path = null)
1365
+    {
1366
+        return $this->writeXML($path, LIBXML_NOEMPTYTAG);
1367
+    }
1368
+
1369
+    /**
1370
+     * Branch the base DOMQuery into another one with the same matches.
1371
+     *
1372
+     * This function makes a copy of the DOMQuery object, but keeps the new copy
1373
+     * (initially) pointed at the same matches. This object can then be queried without
1374
+     * changing the original DOMQuery. However, changes to the elements inside of this
1375
+     * DOMQuery will show up in the DOMQuery from which it is branched.
1376
+     *
1377
+     * Compare this operation with {@link cloneAll()}. The cloneAll() call takes
1378
+     * the current DOMNode object and makes a copy of all of its matches. You continue
1379
+     * to operate on the same DOMNode object, but the elements inside of the DOMQuery
1380
+     * are copies of those before the call to cloneAll().
1381
+     *
1382
+     * This, on the other hand, copies <i>the DOMQuery</i>, but keeps valid
1383
+     * references to the document and the wrapped elements. A new query branch is
1384
+     * created, but any changes will be written back to the same document.
1385
+     *
1386
+     * In practice, this comes in handy when you want to do multiple queries on a part
1387
+     * of the document, but then return to a previous set of matches. (see {@link QPTPL}
1388
+     * for examples of this in practice).
1389
+     *
1390
+     * Example:
1391
+     *
1392
+     * @code
1393
+     * <?php
1394
+     * $qp = qp( QueryPath::HTML_STUB);
1395
+     * $branch = $qp->branch();
1396
+     * $branch->find('title')->text('Title');
1397
+     * $qp->find('body')->text('This is the body')->writeHTML;
1398
+     * ?>
1399
+     * @endcode
1400
+     *
1401
+     * Notice that in the code, each of the DOMQuery objects is doing its own
1402
+     * query. However, both are modifying the same document. The result of the above
1403
+     * would look something like this:
1404
+     *
1405
+     * @code
1406
+     * <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
1407
+     * <html xmlns="http://www.w3.org/1999/xhtml">
1408
+     * <head>
1409
+     *    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"></meta>
1410
+     *    <title>Title</title>
1411
+     * </head>
1412
+     * <body>This is the body</body>
1413
+     * </html>
1414
+     * @endcode
1415
+     *
1416
+     * Notice that while $qp and $banch were performing separate queries, they
1417
+     * both modified the same document.
1418
+     *
1419
+     * In jQuery or a browser-based solution, you generally do not need a branching
1420
+     * function because there is (implicitly) only one document. In QueryPath, there
1421
+     * is no implicit document. Every document must be explicitly specified (and,
1422
+     * in most cases, parsed -- which is costly). Branching makes it possible to
1423
+     * work on one document with multiple DOMNode objects.
1424
+     *
1425
+     * @param string $selector
1426
+     *  If a selector is passed in, an additional {@link find()} will be executed
1427
+     *  on the branch before it is returned. (Added in QueryPath 2.0.)
1428
+     *
1429
+     * @return DOMQuery
1430
+     *  A copy of the DOMQuery object that points to the same set of elements that
1431
+     *  the original DOMQuery was pointing to.
1432
+     * @throws CSS\ParseException
1433
+     * @see   cloneAll()
1434
+     * @see   find()
1435
+     * @since 1.1
1436
+     */
1437
+    public function branch($selector = null)
1438
+    {
1439
+        $temp = QueryPath::with($this->matches, null, $this->options);
1440
+        //if (isset($selector)) $temp->find($selector);
1441
+        $temp->document = $this->document;
1442
+        if (isset($selector)) {
1443
+            $temp->findInPlace($selector);
1444
+        }
1445
+
1446
+        return $temp;
1447
+    }
1448
+
1449
+    /**
1450
+     * @param $matches
1451
+     * @param $selector
1452
+     *
1453
+     * @return DOMQuery
1454
+     * @throws CSS\ParseException
1455
+     */
1456
+    protected function inst($matches, $selector): Query
1457
+    {
1458
+        $dolly = clone $this;
1459
+        $dolly->setMatches($matches);
1460
+
1461
+        if (isset($selector)) {
1462
+            $dolly->findInPlace($selector);
1463
+        }
1464
+
1465
+        return $dolly;
1466
+    }
1467
+
1468
+    /**
1469
+     * Perform a deep clone of each node in the DOMQuery.
1470
+     *
1471
+     * @attention
1472
+     *   This is an in-place modification of the current QueryPath object.
1473
+     *
1474
+     * This does not clone the DOMQuery object, but instead clones the
1475
+     * list of nodes wrapped by the DOMQuery. Every element is deeply
1476
+     * cloned.
1477
+     *
1478
+     * This method is analogous to jQuery's clone() method.
1479
+     *
1480
+     * This is a destructive operation, which means that end() will revert
1481
+     * the list back to the clone's original.
1482
+     * @return DOMQuery
1483
+     * @see qp()
1484
+     */
1485
+    public function cloneAll(): Query
1486
+    {
1487
+        $found = new SplObjectStorage();
1488
+        foreach ($this->matches as $m) {
1489
+            $found->attach($m->cloneNode(true));
1490
+        }
1491
+        $this->setMatches($found);
1492
+
1493
+        return $this;
1494
+    }
1495
+
1496
+    /**
1497
+     * Clone the DOMQuery.
1498
+     *
1499
+     * This makes a deep clone of the elements inside of the DOMQuery.
1500
+     *
1501
+     * This clones only the QueryPathImpl, not all of the decorators. The
1502
+     * clone operator in PHP should handle the cloning of the decorators.
1503
+     */
1504
+    public function __clone()
1505
+    {
1506
+        //XXX: Should we clone the document?
1507
+
1508
+        // Make sure we clone the kids.
1509
+        $this->cloneAll();
1510
+    }
1511
+
1512
+    /**
1513
+     * Call extension methods.
1514
+     *
1515
+     * This function is used to invoke extension methods. It searches the
1516
+     * registered extenstensions for a matching function name. If one is found,
1517
+     * it is executed with the arguments in the $arguments array.
1518
+     *
1519
+     * @throws ReflectionException
1520
+     * @throws QueryPath::Exception
1521
+     *  An exception is thrown if a non-existent method is called.
1522
+     * @throws Exception
1523
+     */
1524
+    public function __call($name, $arguments)
1525
+    {
1526
+
1527
+        if (! ExtensionRegistry::$useRegistry) {
1528
+            throw new Exception("No method named $name found (Extensions disabled).");
1529
+        }
1530
+
1531
+        // Loading of extensions is deferred until the first time a
1532
+        // non-core method is called. This makes constructing faster, but it
1533
+        // may make the first invocation of __call() slower (if there are
1534
+        // enough extensions.)
1535
+        //
1536
+        // The main reason for moving this out of the constructor is that most
1537
+        // new DOMQuery instances do not use extensions. Charging qp() calls
1538
+        // with the additional hit is not a good idea.
1539
+        //
1540
+        // Also, this will at least limit the number of circular references.
1541
+        if (empty($this->ext)) {
1542
+            // Load the registry
1543
+            $this->ext = ExtensionRegistry::getExtensions($this);
1544
+        }
1545
+
1546
+        // Note that an empty ext registry indicates that extensions are disabled.
1547
+        if (! empty($this->ext) && ExtensionRegistry::hasMethod($name)) {
1548
+            $owner  = ExtensionRegistry::getMethodClass($name);
1549
+            $method = new ReflectionMethod($owner, $name);
1550
+
1551
+            return $method->invokeArgs($this->ext[$owner], $arguments);
1552
+        }
1553
+        throw new Exception("No method named $name found. Possibly missing an extension.");
1554
+    }
1555
+
1556
+    /**
1557
+     * Get an iterator for the matches in this object.
1558
+     *
1559
+     * @return Iterable
1560
+     *  Returns an iterator.
1561
+     */
1562
+    public function getIterator(): Traversable
1563
+    {
1564
+        $i          = new QueryPathIterator($this->matches);
1565
+        $i->options = $this->options;
1566
+
1567
+        return $i;
1568
+    }
1569 1569
 }
Please login to merge, or discard this patch.
Spacing   +19 added lines, -19 removed lines patch added patch discarded remove patch
@@ -196,7 +196,7 @@  discard block
 block discarded – undo
196 196
 		$xpath = new DOMXPath($this->document);
197 197
 
198 198
 		// Register a default namespace.
199
-		if (! empty($options['namespace_prefix']) && ! empty($options['namespace_uri'])) {
199
+		if (!empty($options['namespace_prefix']) && !empty($options['namespace_uri'])) {
200 200
 			$xpath->registerNamespace($options['namespace_prefix'], $options['namespace_uri']);
201 201
 		}
202 202
 
@@ -286,7 +286,7 @@  discard block
 block discarded – undo
286 286
 			return ($this->count() > $index) ? $this->getNthMatch($index) : null;
287 287
 		}
288 288
 		// Retain support for legacy.
289
-		if (! $asObject) {
289
+		if (!$asObject) {
290 290
 			$matches = [];
291 291
 			foreach ($this->matches as $m) {
292 292
 				$matches[] = $m;
@@ -399,7 +399,7 @@  discard block
 block discarded – undo
399 399
 			$matches = [];
400 400
 			preg_match($regex, $data, $matches);
401 401
 
402
-			if (! empty($matches)) {
402
+			if (!empty($matches)) {
403 403
 				$result = [
404 404
 					'mime' => $matches[1] . '/' . $matches[2],
405 405
 					'data' => base64_decode($matches[3]),
@@ -685,12 +685,12 @@  discard block
 block discarded – undo
685 685
 		$first = $this->getFirstMatch();
686 686
 
687 687
 		// Catch cases where first item is not a legit DOM object.
688
-		if (! ($first instanceof DOMNode)) {
688
+		if (!($first instanceof DOMNode)) {
689 689
 			return null;
690 690
 		}
691 691
 
692 692
 		// Added by eabrand.
693
-		if (! $first->ownerDocument->documentElement) {
693
+		if (!$first->ownerDocument->documentElement) {
694 694
 			return null;
695 695
 		}
696 696
 
@@ -735,12 +735,12 @@  discard block
 block discarded – undo
735 735
 		$first = $this->getFirstMatch();
736 736
 
737 737
 		// Catch cases where first item is not a legit DOM object.
738
-		if (! ($first instanceof DOMNode)) {
738
+		if (!($first instanceof DOMNode)) {
739 739
 			return null;
740 740
 		}
741 741
 
742 742
 		// Added by eabrand.
743
-		if (! $first->ownerDocument->documentElement) {
743
+		if (!$first->ownerDocument->documentElement) {
744 744
 			return null;
745 745
 		}
746 746
 
@@ -811,11 +811,11 @@  discard block
 block discarded – undo
811 811
 		$first = $this->getFirstMatch();
812 812
 
813 813
 		// Catch cases where first item is not a legit DOM object.
814
-		if (! ($first instanceof DOMNode)) {
814
+		if (!($first instanceof DOMNode)) {
815 815
 			return null;
816 816
 		}
817 817
 
818
-		if (! $first->hasChildNodes()) {
818
+		if (!$first->hasChildNodes()) {
819 819
 			return '';
820 820
 		}
821 821
 
@@ -850,11 +850,11 @@  discard block
 block discarded – undo
850 850
 		$first = $this->getFirstMatch();
851 851
 
852 852
 		// Catch cases where first item is not a legit DOM object.
853
-		if (! ($first instanceof DOMNode)) {
853
+		if (!($first instanceof DOMNode)) {
854 854
 			return null;
855 855
 		}
856 856
 
857
-		if (! $first->hasChildNodes()) {
857
+		if (!$first->hasChildNodes()) {
858 858
 			return '';
859 859
 		}
860 860
 
@@ -882,11 +882,11 @@  discard block
 block discarded – undo
882 882
 		$first = $this->getFirstMatch();
883 883
 
884 884
 		// Catch cases where first item is not a legit DOM object.
885
-		if (! ($first instanceof DOMNode)) {
885
+		if (!($first instanceof DOMNode)) {
886 886
 			return null;
887 887
 		}
888 888
 
889
-		if (! $first->hasChildNodes()) {
889
+		if (!$first->hasChildNodes()) {
890 890
 			return '';
891 891
 		}
892 892
 
@@ -1030,7 +1030,7 @@  discard block
 block discarded – undo
1030 1030
 		foreach ($this->matches as $m) {
1031 1031
 			$p = $m;
1032 1032
 			while (isset($p->previousSibling) && $p->previousSibling->nodeType === XML_TEXT_NODE) {
1033
-				$p      = $p->previousSibling;
1033
+				$p = $p->previousSibling;
1034 1034
 				$buffer .= $p->textContent;
1035 1035
 			}
1036 1036
 		}
@@ -1049,7 +1049,7 @@  discard block
 block discarded – undo
1049 1049
 		foreach ($this->matches as $m) {
1050 1050
 			$n = $m;
1051 1051
 			while (isset($n->nextSibling) && $n->nextSibling->nodeType === XML_TEXT_NODE) {
1052
-				$n      = $n->nextSibling;
1052
+				$n = $n->nextSibling;
1053 1053
 				$buffer .= $n->textContent;
1054 1054
 			}
1055 1055
 		}
@@ -1134,7 +1134,7 @@  discard block
 block discarded – undo
1134 1134
 		// Only return the first item -- that's what JQ does.
1135 1135
 		$first = $this->getFirstMatch();
1136 1136
 		// Catch cases where first item is not a legit DOM object.
1137
-		if (! ($first instanceof DOMNode)) {
1137
+		if (!($first instanceof DOMNode)) {
1138 1138
 			return null;
1139 1139
 		}
1140 1140
 
@@ -1217,7 +1217,7 @@  discard block
 block discarded – undo
1217 1217
 		$first = $this->getFirstMatch();
1218 1218
 
1219 1219
 		// Catch cases where first item is not a legit DOM object.
1220
-		if (! ($first instanceof DOMNode)) {
1220
+		if (!($first instanceof DOMNode)) {
1221 1221
 			return null;
1222 1222
 		}
1223 1223
 
@@ -1524,7 +1524,7 @@  discard block
 block discarded – undo
1524 1524
 	public function __call($name, $arguments)
1525 1525
 	{
1526 1526
 
1527
-		if (! ExtensionRegistry::$useRegistry) {
1527
+		if (!ExtensionRegistry::$useRegistry) {
1528 1528
 			throw new Exception("No method named $name found (Extensions disabled).");
1529 1529
 		}
1530 1530
 
@@ -1544,7 +1544,7 @@  discard block
 block discarded – undo
1544 1544
 		}
1545 1545
 
1546 1546
 		// Note that an empty ext registry indicates that extensions are disabled.
1547
-		if (! empty($this->ext) && ExtensionRegistry::hasMethod($name)) {
1547
+		if (!empty($this->ext) && ExtensionRegistry::hasMethod($name)) {
1548 1548
 			$owner  = ExtensionRegistry::getMethodClass($name);
1549 1549
 			$method = new ReflectionMethod($owner, $name);
1550 1550
 
Please login to merge, or discard this patch.
src/DOM.php 2 patches
Indentation   +524 added lines, -524 removed lines patch added patch discarded remove patch
@@ -23,528 +23,528 @@
 block discarded – undo
23 23
 abstract class DOM implements Query, IteratorAggregate, Countable
24 24
 {
25 25
 
26
-	/**
27
-	 * The array of matches.
28
-	 */
29
-	protected $matches = [];
30
-
31
-	/**
32
-	 * Default parser flags.
33
-	 *
34
-	 * These are flags that will be used if no global or local flags override them.
35
-	 *
36
-	 * @since 2.0
37
-	 */
38
-	public const DEFAULT_PARSER_FLAGS = null;
39
-
40
-	public const JS_CSS_ESCAPE_CDATA = '\\1';
41
-	public const JS_CSS_ESCAPE_CDATA_CCOMMENT = '/* \\1 */';
42
-	public const JS_CSS_ESCAPE_CDATA_DOUBLESLASH = '// \\1';
43
-	public const JS_CSS_ESCAPE_NONE = '';
44
-
45
-	protected $errTypes = 771; //E_ERROR; | E_USER_ERROR;
46
-
47
-	protected $document;
48
-	/**
49
-	 * The base DOMDocument.
50
-	 */
51
-	protected $options = [
52
-		'parser_flags'                 => null,
53
-		'omit_xml_declaration'         => false,
54
-		'replace_entities'             => false,
55
-		'exception_level'              => 771, // E_ERROR | E_USER_ERROR | E_USER_WARNING | E_WARNING
56
-		'ignore_parser_warnings'       => false,
57
-		'escape_xhtml_js_css_sections' => self::JS_CSS_ESCAPE_CDATA_CCOMMENT,
58
-	];
59
-
60
-	/**
61
-	 * Constructor.
62
-	 *
63
-	 * Typically, a new DOMQuery is created by QueryPath::with(), QueryPath::withHTML(),
64
-	 * qp(), or htmlqp().
65
-	 *
66
-	 * @param mixed  $document
67
-	 *   A document-like object.
68
-	 * @param string $string
69
-	 *   A CSS 3 Selector
70
-	 * @param array  $options
71
-	 *   An associative array of options.
72
-	 *
73
-	 * @throws Exception
74
-	 * @see qp()
75
-	 */
76
-	public function __construct($document = null, $string = '', $options = [])
77
-	{
78
-		// Backwards compatibility fix for PHP8+
79
-		if (is_null($string)) {
80
-			$string = '';
81
-		}
82
-
83
-		$string        = trim($string);
84
-		$this->options = $options + Options::get() + $this->options;
85
-
86
-		$parser_flags = $options['parser_flags'] ?? self::DEFAULT_PARSER_FLAGS;
87
-		if (! empty($this->options['ignore_parser_warnings'])) {
88
-			// Don't convert parser warnings into exceptions.
89
-			$this->errTypes = 257; //E_ERROR | E_USER_ERROR;
90
-		} elseif (isset($this->options['exception_level'])) {
91
-			// Set the error level at which exceptions will be thrown. By default,
92
-			// QueryPath will throw exceptions for
93
-			// E_ERROR | E_USER_ERROR | E_WARNING | E_USER_WARNING.
94
-			$this->errTypes = $this->options['exception_level'];
95
-		}
96
-
97
-		// Empty: Just create an empty QP.
98
-		if (empty($document)) {
99
-			$this->document = isset($this->options['encoding']) ? new DOMDocument(
100
-				'1.0',
101
-				$this->options['encoding']
102
-			) : new DOMDocument();
103
-			$this->setMatches(new SplObjectStorage());
104
-		} // Figure out if document is DOM, HTML/XML, or a filename
105
-		elseif (is_object($document)) {
106
-
107
-			// This is the most frequent object type.
108
-			if ($document instanceof SplObjectStorage) {
109
-				$this->matches = $document;
110
-				if ($document->count() !== 0) {
111
-					$first = $this->getFirstMatch();
112
-					if (! empty($first->ownerDocument)) {
113
-						$this->document = $first->ownerDocument;
114
-					}
115
-				}
116
-			} elseif ($document instanceof self) {
117
-				//$this->matches = $document->get(NULL, TRUE);
118
-				$this->setMatches($document->get(null, true));
119
-				if ($this->matches->count() > 0) {
120
-					$this->document = $this->getFirstMatch()->ownerDocument;
121
-				}
122
-			} elseif ($document instanceof DOMDocument) {
123
-				$this->document = $document;
124
-				//$this->matches = $this->matches($document->documentElement);
125
-				$this->setMatches($document->documentElement);
126
-			} elseif ($document instanceof DOMNode) {
127
-				$this->document = $document->ownerDocument;
128
-				//$this->matches = array($document);
129
-				$this->setMatches($document);
130
-			} elseif ($document instanceof HTML5) {
131
-				$this->document = $document;
132
-				$this->setMatches($document->documentElement);
133
-			} elseif ($document instanceof SimpleXMLElement) {
134
-				$import         = dom_import_simplexml($document);
135
-				$this->document = $import->ownerDocument;
136
-				//$this->matches = array($import);
137
-				$this->setMatches($import);
138
-			} else {
139
-				throw new Exception('Unsupported class type: ' . get_class($document));
140
-			}
141
-		} elseif (is_array($document)) {
142
-			//trigger_error('Detected deprecated array support', E_USER_NOTICE);
143
-			if (! empty($document) && $document[0] instanceof DOMNode) {
144
-				$found = new SplObjectStorage();
145
-				foreach ($document as $item) {
146
-					$found->attach($item);
147
-				}
148
-				//$this->matches = $found;
149
-				$this->setMatches($found);
150
-				$this->document = $this->getFirstMatch()->ownerDocument;
151
-			}
152
-		} elseif ($this->isXMLish($document)) {
153
-			// $document is a string with XML
154
-			$this->document = $this->parseXMLString($document);
155
-			$this->setMatches($this->document->documentElement);
156
-		} else {
157
-
158
-			// $document is a filename
159
-			$context        = empty($options['context']) ? null : $options['context'];
160
-			$this->document = $this->parseXMLFile($document, $parser_flags, $context);
161
-			$this->setMatches($this->document->documentElement);
162
-		}
163
-
164
-		// Globally set the output option.
165
-		$this->document->formatOutput = true;
166
-		if (isset($this->options['format_output']) && $this->options['format_output'] === false) {
167
-			$this->document->formatOutput = false;
168
-		}
169
-
170
-		// Do a find if the second param was set.
171
-		if (strlen($string) > 0) {
172
-			// We don't issue a find because that creates a new DOMQuery.
173
-			//$this->find($string);
174
-
175
-			$query = new DOMTraverser($this->matches);
176
-			$query->find($string);
177
-			$this->setMatches($query->matches());
178
-		}
179
-	}
180
-
181
-	private function parseXMLString($string, $flags = 0)
182
-	{
183
-		$document = new DOMDocument('1.0');
184
-		$lead     = strtolower(substr($string, 0, 5)); // <?xml
185
-		try {
186
-			set_error_handler([ParseException::class, 'initializeFromError'], $this->errTypes);
187
-
188
-			if (isset($this->options['convert_to_encoding'])) {
189
-				// Is there another way to do this?
190
-
191
-				$from_enc = $this->options['convert_from_encoding'] ?? 'auto';
192
-				$to_enc   = $this->options['convert_to_encoding'];
193
-
194
-				if (function_exists('mb_convert_encoding')) {
195
-					$string = mb_convert_encoding($string, $to_enc, $from_enc);
196
-				}
197
-
198
-			}
199
-
200
-			// This is to avoid cases where low ascii digits have slipped into HTML.
201
-			// AFAIK, it should not adversly effect UTF-8 documents.
202
-			if (! empty($this->options['strip_low_ascii'])) {
203
-				$string = filter_var($string, FILTER_UNSAFE_RAW, FILTER_FLAG_ENCODE_LOW);
204
-			}
205
-
206
-			// Allow users to override parser settings.
207
-			$useParser = '';
208
-			if (! empty($this->options['use_parser'])) {
209
-				$useParser = strtolower($this->options['use_parser']);
210
-			}
211
-
212
-			// If HTML parser is requested, we use it.
213
-			if ($useParser === 'html') {
214
-				$document->loadHTML($string);
215
-			} // Parse as XML if it looks like XML, or if XML parser is requested.
216
-			elseif ($lead === '<?xml' || $useParser === 'xml') {
217
-				if ($this->options['replace_entities']) {
218
-					$string = Entities::replaceAllEntities($string);
219
-				}
220
-				$document->loadXML($string, $flags);
221
-			} // In all other cases, we try the HTML parser.
222
-			else {
223
-				$document->loadHTML($string);
224
-			}
225
-		} // Emulate 'finally' behavior.
226
-		catch (Exception $e) {
227
-			restore_error_handler();
228
-			throw $e;
229
-		}
230
-		restore_error_handler();
231
-
232
-		if (empty($document)) {
233
-			throw new ParseException('Unknown parser exception.');
234
-		}
235
-
236
-		return $document;
237
-	}
238
-
239
-	/**
240
-	 * EXPERT: Be very, very careful using this.
241
-	 * A utility function for setting the current set of matches.
242
-	 * It makes sure the last matches buffer is set (for end() and andSelf()).
243
-	 *
244
-	 * @param $matches
245
-	 *
246
-	 * @since 2.0
247
-	 */
248
-	public function setMatches($matches)
249
-	{
250
-		// This causes a lot of overhead....
251
-		//if ($unique) $matches = self::unique($matches);
252
-		$this->last = $this->matches;
253
-
254
-		// Just set current matches.
255
-		if ($matches instanceof SplObjectStorage) {
256
-			$this->matches = $matches;
257
-		} // This is likely legacy code that needs conversion.
258
-		elseif (is_array($matches)) {
259
-			trigger_error('Legacy array detected.');
260
-			$tmp = new SplObjectStorage();
261
-			foreach ($matches as $m) {
262
-				$tmp->attach($m);
263
-			}
264
-			$this->matches = $tmp;
265
-		}
266
-		// For non-arrays, try to create a new match set and
267
-		// add this object.
268
-		else {
269
-			$found = new SplObjectStorage();
270
-			if (isset($matches)) {
271
-				$found->attach($matches);
272
-			}
273
-			$this->matches = $found;
274
-		}
275
-
276
-		// EXPERIMENTAL: Support for qp()->length.
277
-		$this->length = $this->matches->count();
278
-	}
279
-
280
-	/**
281
-	 * A depth-checking function. Typically, it only needs to be
282
-	 * invoked with the first parameter. The rest are used for recursion.
283
-	 *
284
-	 * @param DOMNode $ele
285
-	 *  The element.
286
-	 * @param int     $depth
287
-	 *  The depth guage
288
-	 * @param mixed   $current
289
-	 *  The current set.
290
-	 * @param DOMNode $deepest
291
-	 *  A reference to the current deepest node.
292
-	 *
293
-	 * @return array
294
-	 *  Returns an array of DOM nodes.
295
-	 * @see deepest();
296
-	 */
297
-	protected function deepestNode(DOMNode $ele, $depth = 0, $current = null, &$deepest = null)
298
-	{
299
-		// FIXME: Should this use SplObjectStorage?
300
-		if (! isset($current)) {
301
-			$current = [$ele];
302
-		}
303
-		if (! isset($deepest)) {
304
-			$deepest = $depth;
305
-		}
306
-		if ($ele->hasChildNodes()) {
307
-			foreach ($ele->childNodes as $child) {
308
-				if ($child->nodeType === XML_ELEMENT_NODE) {
309
-					$current = $this->deepestNode($child, $depth + 1, $current, $deepest);
310
-				}
311
-			}
312
-		} elseif ($depth > $deepest) {
313
-			$current = [$ele];
314
-			$deepest = $depth;
315
-		} elseif ($depth === $deepest) {
316
-			$current[] = $ele;
317
-		}
318
-
319
-		return $current;
320
-	}
321
-
322
-	/**
323
-	 * Prepare an item for insertion into a DOM.
324
-	 *
325
-	 * This handles a variety of boilerplate tasks that need doing before an
326
-	 * indeterminate object can be inserted into a DOM tree.
327
-	 * - If item is a string, this is converted into a document fragment and returned.
328
-	 * - If item is a DOMQuery, then all items are retrieved and converted into
329
-	 *   a document fragment and returned.
330
-	 * - If the item is a DOMNode, it is imported into the current DOM if necessary.
331
-	 * - If the item is a SimpleXMLElement, it is converted into a DOM node and then
332
-	 *   imported.
333
-	 *
334
-	 * @param mixed $item
335
-	 *  Item to prepare for insert.
336
-	 *
337
-	 * @return mixed
338
-	 *  Returns the prepared item.
339
-	 * @throws QueryPath::Exception
340
-	 *  Thrown if the object passed in is not of a supprted object type.
341
-	 * @throws Exception
342
-	 */
343
-	protected function prepareInsert($item)
344
-	{
345
-		if (empty($item)) {
346
-			return null;
347
-		}
348
-
349
-		if (is_string($item)) {
350
-			// If configured to do so, replace all entities.
351
-			if ($this->options['replace_entities']) {
352
-				$item = Entities::replaceAllEntities($item);
353
-			}
354
-
355
-			$frag = $this->document->createDocumentFragment();
356
-			try {
357
-				set_error_handler([ParseException::class, 'initializeFromError'], $this->errTypes);
358
-				$frag->appendXML($item);
359
-			} // Simulate a finally block.
360
-			catch (Exception $e) {
361
-				restore_error_handler();
362
-				throw $e;
363
-			}
364
-			restore_error_handler();
365
-
366
-			return $frag;
367
-		}
368
-
369
-		if ($item instanceof self) {
370
-			if ($item->count() === 0) {
371
-				return null;
372
-			}
373
-
374
-			$frag = $this->document->createDocumentFragment();
375
-			foreach ($item->matches as $m) {
376
-				$frag->appendXML($item->document->saveXML($m));
377
-			}
378
-
379
-			return $frag;
380
-		}
381
-
382
-		if ($item instanceof DOMNode) {
383
-			if ($item->ownerDocument !== $this->document) {
384
-				// Deep clone this and attach it to this document
385
-				$item = $this->document->importNode($item, true);
386
-			}
387
-
388
-			return $item;
389
-		}
390
-
391
-		if ($item instanceof SimpleXMLElement) {
392
-			$element = dom_import_simplexml($item);
393
-
394
-			return $this->document->importNode($element, true);
395
-		}
396
-		// What should we do here?
397
-		//var_dump($item);
398
-		throw new Exception('Cannot prepare item of unsupported type: ' . gettype($item));
399
-	}
400
-
401
-	/**
402
-	 * Convenience function for getNthMatch(0).
403
-	 */
404
-	protected function getFirstMatch()
405
-	{
406
-		$this->matches->rewind();
407
-
408
-		return $this->matches->current();
409
-	}
410
-
411
-	/**
412
-	 * Parse an XML or HTML file.
413
-	 *
414
-	 * This attempts to autodetect the type of file, and then parse it.
415
-	 *
416
-	 * @param string   $filename
417
-	 *  The file name to parse.
418
-	 * @param int      $flags
419
-	 *  The OR-combined flags accepted by the DOM parser. See the PHP documentation
420
-	 *  for DOM or for libxml.
421
-	 * @param resource $context
422
-	 *  The stream context for the file IO. If this is set, then an alternate
423
-	 *  parsing path is followed: The file is loaded by PHP's stream-aware IO
424
-	 *  facilities, read entirely into memory, and then handed off to
425
-	 *  {@link parseXMLString()}. On large files, this can have a performance impact.
426
-	 *
427
-	 * @throws ParseException
428
-	 *  Thrown when a file cannot be loaded or parsed.
429
-	 */
430
-	private function parseXMLFile($filename, $flags = 0, $context = null)
431
-	{
432
-		// Backwards compatibility fix for PHP8+
433
-		if (is_null($flags)) {
434
-			$flags = 0;
435
-		}
436
-
437
-		// If a context is specified, we basically have to do the reading in
438
-		// two steps:
439
-		if (! empty($context)) {
440
-			try {
441
-				set_error_handler(['\QueryPath\ParseException', 'initializeFromError'], $this->errTypes);
442
-				$contents = file_get_contents($filename, false, $context);
443
-			}
444
-				// Apparently there is no 'finally' in PHP, so we have to restore the error
445
-				// handler this way:
446
-			catch (Exception $e) {
447
-				restore_error_handler();
448
-				throw $e;
449
-			}
450
-			restore_error_handler();
451
-
452
-			if ($contents == false) {
453
-				throw new ParseException(sprintf(
454
-					'Contents of the file %s could not be retrieved.',
455
-					$filename
456
-				));
457
-			}
458
-
459
-			return $this->parseXMLString($contents, $flags);
460
-		}
461
-
462
-		$document = new DOMDocument();
463
-		$lastDot  = strrpos($filename, '.');
464
-
465
-		$htmlExtensions = [
466
-			'.html' => 1,
467
-			'.htm'  => 1,
468
-		];
469
-
470
-		// Allow users to override parser settings.
471
-		if (empty($this->options['use_parser'])) {
472
-			$useParser = '';
473
-		} else {
474
-			$useParser = strtolower($this->options['use_parser']);
475
-		}
476
-
477
-		$ext = $lastDot !== false ? strtolower(substr($filename, $lastDot)) : '';
478
-
479
-		try {
480
-			set_error_handler([ParseException::class, 'initializeFromError'], $this->errTypes);
481
-
482
-			// If the parser is explicitly set to XML, use that parser.
483
-			if ($useParser === 'xml') {
484
-				$document->load($filename, $flags);
485
-			} // Otherwise, see if it looks like HTML.
486
-			elseif ($useParser === 'html' || isset($htmlExtensions[$ext])) {
487
-				// Try parsing it as HTML.
488
-				$document->loadHTMLFile($filename);
489
-			} // Default to XML.
490
-			else {
491
-				$document->load($filename, $flags);
492
-			}
493
-
494
-		} // Emulate 'finally' behavior.
495
-		catch (Exception $e) {
496
-			restore_error_handler();
497
-			throw $e;
498
-		}
499
-		restore_error_handler();
500
-
501
-		return $document;
502
-	}
503
-
504
-	/**
505
-	 * Determine whether a given string looks like XML or not.
506
-	 *
507
-	 * Basically, this scans a portion of the supplied string, checking to see
508
-	 * if it has a tag-like structure. It is possible to "confuse" this, which
509
-	 * may subsequently result in parse errors, but in the vast majority of
510
-	 * cases, this method serves as a valid inicator of whether or not the
511
-	 * content looks like XML.
512
-	 *
513
-	 * Things that are intentional excluded:
514
-	 * - plain text with no markup.
515
-	 * - strings that look like filesystem paths.
516
-	 *
517
-	 * Subclasses SHOULD NOT OVERRIDE THIS. Altering it may be altering
518
-	 * core assumptions about how things work. Instead, classes should
519
-	 * override the constructor and pass in only one of the parsed types
520
-	 * that this class expects.
521
-	 */
522
-	protected function isXMLish($string)
523
-	{
524
-		return (strpos($string, '<') !== false && strpos($string, '>') !== false);
525
-	}
526
-
527
-	/**
528
-	 * A utility function for retriving a match by index.
529
-	 *
530
-	 * The internal data structure used in DOMQuery does not have
531
-	 * strong random access support, so we suppliment it with this method.
532
-	 *
533
-	 * @param $index
534
-	 *
535
-	 * @return object|void
536
-	 */
537
-	protected function getNthMatch(int $index)
538
-	{
539
-		if ($index < 0 || $index > $this->matches->count()) {
540
-			return;
541
-		}
542
-
543
-		$i = 0;
544
-		foreach ($this->matches as $m) {
545
-			if ($i++ === $index) {
546
-				return $m;
547
-			}
548
-		}
549
-	}
26
+    /**
27
+     * The array of matches.
28
+     */
29
+    protected $matches = [];
30
+
31
+    /**
32
+     * Default parser flags.
33
+     *
34
+     * These are flags that will be used if no global or local flags override them.
35
+     *
36
+     * @since 2.0
37
+     */
38
+    public const DEFAULT_PARSER_FLAGS = null;
39
+
40
+    public const JS_CSS_ESCAPE_CDATA = '\\1';
41
+    public const JS_CSS_ESCAPE_CDATA_CCOMMENT = '/* \\1 */';
42
+    public const JS_CSS_ESCAPE_CDATA_DOUBLESLASH = '// \\1';
43
+    public const JS_CSS_ESCAPE_NONE = '';
44
+
45
+    protected $errTypes = 771; //E_ERROR; | E_USER_ERROR;
46
+
47
+    protected $document;
48
+    /**
49
+     * The base DOMDocument.
50
+     */
51
+    protected $options = [
52
+        'parser_flags'                 => null,
53
+        'omit_xml_declaration'         => false,
54
+        'replace_entities'             => false,
55
+        'exception_level'              => 771, // E_ERROR | E_USER_ERROR | E_USER_WARNING | E_WARNING
56
+        'ignore_parser_warnings'       => false,
57
+        'escape_xhtml_js_css_sections' => self::JS_CSS_ESCAPE_CDATA_CCOMMENT,
58
+    ];
59
+
60
+    /**
61
+     * Constructor.
62
+     *
63
+     * Typically, a new DOMQuery is created by QueryPath::with(), QueryPath::withHTML(),
64
+     * qp(), or htmlqp().
65
+     *
66
+     * @param mixed  $document
67
+     *   A document-like object.
68
+     * @param string $string
69
+     *   A CSS 3 Selector
70
+     * @param array  $options
71
+     *   An associative array of options.
72
+     *
73
+     * @throws Exception
74
+     * @see qp()
75
+     */
76
+    public function __construct($document = null, $string = '', $options = [])
77
+    {
78
+        // Backwards compatibility fix for PHP8+
79
+        if (is_null($string)) {
80
+            $string = '';
81
+        }
82
+
83
+        $string        = trim($string);
84
+        $this->options = $options + Options::get() + $this->options;
85
+
86
+        $parser_flags = $options['parser_flags'] ?? self::DEFAULT_PARSER_FLAGS;
87
+        if (! empty($this->options['ignore_parser_warnings'])) {
88
+            // Don't convert parser warnings into exceptions.
89
+            $this->errTypes = 257; //E_ERROR | E_USER_ERROR;
90
+        } elseif (isset($this->options['exception_level'])) {
91
+            // Set the error level at which exceptions will be thrown. By default,
92
+            // QueryPath will throw exceptions for
93
+            // E_ERROR | E_USER_ERROR | E_WARNING | E_USER_WARNING.
94
+            $this->errTypes = $this->options['exception_level'];
95
+        }
96
+
97
+        // Empty: Just create an empty QP.
98
+        if (empty($document)) {
99
+            $this->document = isset($this->options['encoding']) ? new DOMDocument(
100
+                '1.0',
101
+                $this->options['encoding']
102
+            ) : new DOMDocument();
103
+            $this->setMatches(new SplObjectStorage());
104
+        } // Figure out if document is DOM, HTML/XML, or a filename
105
+        elseif (is_object($document)) {
106
+
107
+            // This is the most frequent object type.
108
+            if ($document instanceof SplObjectStorage) {
109
+                $this->matches = $document;
110
+                if ($document->count() !== 0) {
111
+                    $first = $this->getFirstMatch();
112
+                    if (! empty($first->ownerDocument)) {
113
+                        $this->document = $first->ownerDocument;
114
+                    }
115
+                }
116
+            } elseif ($document instanceof self) {
117
+                //$this->matches = $document->get(NULL, TRUE);
118
+                $this->setMatches($document->get(null, true));
119
+                if ($this->matches->count() > 0) {
120
+                    $this->document = $this->getFirstMatch()->ownerDocument;
121
+                }
122
+            } elseif ($document instanceof DOMDocument) {
123
+                $this->document = $document;
124
+                //$this->matches = $this->matches($document->documentElement);
125
+                $this->setMatches($document->documentElement);
126
+            } elseif ($document instanceof DOMNode) {
127
+                $this->document = $document->ownerDocument;
128
+                //$this->matches = array($document);
129
+                $this->setMatches($document);
130
+            } elseif ($document instanceof HTML5) {
131
+                $this->document = $document;
132
+                $this->setMatches($document->documentElement);
133
+            } elseif ($document instanceof SimpleXMLElement) {
134
+                $import         = dom_import_simplexml($document);
135
+                $this->document = $import->ownerDocument;
136
+                //$this->matches = array($import);
137
+                $this->setMatches($import);
138
+            } else {
139
+                throw new Exception('Unsupported class type: ' . get_class($document));
140
+            }
141
+        } elseif (is_array($document)) {
142
+            //trigger_error('Detected deprecated array support', E_USER_NOTICE);
143
+            if (! empty($document) && $document[0] instanceof DOMNode) {
144
+                $found = new SplObjectStorage();
145
+                foreach ($document as $item) {
146
+                    $found->attach($item);
147
+                }
148
+                //$this->matches = $found;
149
+                $this->setMatches($found);
150
+                $this->document = $this->getFirstMatch()->ownerDocument;
151
+            }
152
+        } elseif ($this->isXMLish($document)) {
153
+            // $document is a string with XML
154
+            $this->document = $this->parseXMLString($document);
155
+            $this->setMatches($this->document->documentElement);
156
+        } else {
157
+
158
+            // $document is a filename
159
+            $context        = empty($options['context']) ? null : $options['context'];
160
+            $this->document = $this->parseXMLFile($document, $parser_flags, $context);
161
+            $this->setMatches($this->document->documentElement);
162
+        }
163
+
164
+        // Globally set the output option.
165
+        $this->document->formatOutput = true;
166
+        if (isset($this->options['format_output']) && $this->options['format_output'] === false) {
167
+            $this->document->formatOutput = false;
168
+        }
169
+
170
+        // Do a find if the second param was set.
171
+        if (strlen($string) > 0) {
172
+            // We don't issue a find because that creates a new DOMQuery.
173
+            //$this->find($string);
174
+
175
+            $query = new DOMTraverser($this->matches);
176
+            $query->find($string);
177
+            $this->setMatches($query->matches());
178
+        }
179
+    }
180
+
181
+    private function parseXMLString($string, $flags = 0)
182
+    {
183
+        $document = new DOMDocument('1.0');
184
+        $lead     = strtolower(substr($string, 0, 5)); // <?xml
185
+        try {
186
+            set_error_handler([ParseException::class, 'initializeFromError'], $this->errTypes);
187
+
188
+            if (isset($this->options['convert_to_encoding'])) {
189
+                // Is there another way to do this?
190
+
191
+                $from_enc = $this->options['convert_from_encoding'] ?? 'auto';
192
+                $to_enc   = $this->options['convert_to_encoding'];
193
+
194
+                if (function_exists('mb_convert_encoding')) {
195
+                    $string = mb_convert_encoding($string, $to_enc, $from_enc);
196
+                }
197
+
198
+            }
199
+
200
+            // This is to avoid cases where low ascii digits have slipped into HTML.
201
+            // AFAIK, it should not adversly effect UTF-8 documents.
202
+            if (! empty($this->options['strip_low_ascii'])) {
203
+                $string = filter_var($string, FILTER_UNSAFE_RAW, FILTER_FLAG_ENCODE_LOW);
204
+            }
205
+
206
+            // Allow users to override parser settings.
207
+            $useParser = '';
208
+            if (! empty($this->options['use_parser'])) {
209
+                $useParser = strtolower($this->options['use_parser']);
210
+            }
211
+
212
+            // If HTML parser is requested, we use it.
213
+            if ($useParser === 'html') {
214
+                $document->loadHTML($string);
215
+            } // Parse as XML if it looks like XML, or if XML parser is requested.
216
+            elseif ($lead === '<?xml' || $useParser === 'xml') {
217
+                if ($this->options['replace_entities']) {
218
+                    $string = Entities::replaceAllEntities($string);
219
+                }
220
+                $document->loadXML($string, $flags);
221
+            } // In all other cases, we try the HTML parser.
222
+            else {
223
+                $document->loadHTML($string);
224
+            }
225
+        } // Emulate 'finally' behavior.
226
+        catch (Exception $e) {
227
+            restore_error_handler();
228
+            throw $e;
229
+        }
230
+        restore_error_handler();
231
+
232
+        if (empty($document)) {
233
+            throw new ParseException('Unknown parser exception.');
234
+        }
235
+
236
+        return $document;
237
+    }
238
+
239
+    /**
240
+     * EXPERT: Be very, very careful using this.
241
+     * A utility function for setting the current set of matches.
242
+     * It makes sure the last matches buffer is set (for end() and andSelf()).
243
+     *
244
+     * @param $matches
245
+     *
246
+     * @since 2.0
247
+     */
248
+    public function setMatches($matches)
249
+    {
250
+        // This causes a lot of overhead....
251
+        //if ($unique) $matches = self::unique($matches);
252
+        $this->last = $this->matches;
253
+
254
+        // Just set current matches.
255
+        if ($matches instanceof SplObjectStorage) {
256
+            $this->matches = $matches;
257
+        } // This is likely legacy code that needs conversion.
258
+        elseif (is_array($matches)) {
259
+            trigger_error('Legacy array detected.');
260
+            $tmp = new SplObjectStorage();
261
+            foreach ($matches as $m) {
262
+                $tmp->attach($m);
263
+            }
264
+            $this->matches = $tmp;
265
+        }
266
+        // For non-arrays, try to create a new match set and
267
+        // add this object.
268
+        else {
269
+            $found = new SplObjectStorage();
270
+            if (isset($matches)) {
271
+                $found->attach($matches);
272
+            }
273
+            $this->matches = $found;
274
+        }
275
+
276
+        // EXPERIMENTAL: Support for qp()->length.
277
+        $this->length = $this->matches->count();
278
+    }
279
+
280
+    /**
281
+     * A depth-checking function. Typically, it only needs to be
282
+     * invoked with the first parameter. The rest are used for recursion.
283
+     *
284
+     * @param DOMNode $ele
285
+     *  The element.
286
+     * @param int     $depth
287
+     *  The depth guage
288
+     * @param mixed   $current
289
+     *  The current set.
290
+     * @param DOMNode $deepest
291
+     *  A reference to the current deepest node.
292
+     *
293
+     * @return array
294
+     *  Returns an array of DOM nodes.
295
+     * @see deepest();
296
+     */
297
+    protected function deepestNode(DOMNode $ele, $depth = 0, $current = null, &$deepest = null)
298
+    {
299
+        // FIXME: Should this use SplObjectStorage?
300
+        if (! isset($current)) {
301
+            $current = [$ele];
302
+        }
303
+        if (! isset($deepest)) {
304
+            $deepest = $depth;
305
+        }
306
+        if ($ele->hasChildNodes()) {
307
+            foreach ($ele->childNodes as $child) {
308
+                if ($child->nodeType === XML_ELEMENT_NODE) {
309
+                    $current = $this->deepestNode($child, $depth + 1, $current, $deepest);
310
+                }
311
+            }
312
+        } elseif ($depth > $deepest) {
313
+            $current = [$ele];
314
+            $deepest = $depth;
315
+        } elseif ($depth === $deepest) {
316
+            $current[] = $ele;
317
+        }
318
+
319
+        return $current;
320
+    }
321
+
322
+    /**
323
+     * Prepare an item for insertion into a DOM.
324
+     *
325
+     * This handles a variety of boilerplate tasks that need doing before an
326
+     * indeterminate object can be inserted into a DOM tree.
327
+     * - If item is a string, this is converted into a document fragment and returned.
328
+     * - If item is a DOMQuery, then all items are retrieved and converted into
329
+     *   a document fragment and returned.
330
+     * - If the item is a DOMNode, it is imported into the current DOM if necessary.
331
+     * - If the item is a SimpleXMLElement, it is converted into a DOM node and then
332
+     *   imported.
333
+     *
334
+     * @param mixed $item
335
+     *  Item to prepare for insert.
336
+     *
337
+     * @return mixed
338
+     *  Returns the prepared item.
339
+     * @throws QueryPath::Exception
340
+     *  Thrown if the object passed in is not of a supprted object type.
341
+     * @throws Exception
342
+     */
343
+    protected function prepareInsert($item)
344
+    {
345
+        if (empty($item)) {
346
+            return null;
347
+        }
348
+
349
+        if (is_string($item)) {
350
+            // If configured to do so, replace all entities.
351
+            if ($this->options['replace_entities']) {
352
+                $item = Entities::replaceAllEntities($item);
353
+            }
354
+
355
+            $frag = $this->document->createDocumentFragment();
356
+            try {
357
+                set_error_handler([ParseException::class, 'initializeFromError'], $this->errTypes);
358
+                $frag->appendXML($item);
359
+            } // Simulate a finally block.
360
+            catch (Exception $e) {
361
+                restore_error_handler();
362
+                throw $e;
363
+            }
364
+            restore_error_handler();
365
+
366
+            return $frag;
367
+        }
368
+
369
+        if ($item instanceof self) {
370
+            if ($item->count() === 0) {
371
+                return null;
372
+            }
373
+
374
+            $frag = $this->document->createDocumentFragment();
375
+            foreach ($item->matches as $m) {
376
+                $frag->appendXML($item->document->saveXML($m));
377
+            }
378
+
379
+            return $frag;
380
+        }
381
+
382
+        if ($item instanceof DOMNode) {
383
+            if ($item->ownerDocument !== $this->document) {
384
+                // Deep clone this and attach it to this document
385
+                $item = $this->document->importNode($item, true);
386
+            }
387
+
388
+            return $item;
389
+        }
390
+
391
+        if ($item instanceof SimpleXMLElement) {
392
+            $element = dom_import_simplexml($item);
393
+
394
+            return $this->document->importNode($element, true);
395
+        }
396
+        // What should we do here?
397
+        //var_dump($item);
398
+        throw new Exception('Cannot prepare item of unsupported type: ' . gettype($item));
399
+    }
400
+
401
+    /**
402
+     * Convenience function for getNthMatch(0).
403
+     */
404
+    protected function getFirstMatch()
405
+    {
406
+        $this->matches->rewind();
407
+
408
+        return $this->matches->current();
409
+    }
410
+
411
+    /**
412
+     * Parse an XML or HTML file.
413
+     *
414
+     * This attempts to autodetect the type of file, and then parse it.
415
+     *
416
+     * @param string   $filename
417
+     *  The file name to parse.
418
+     * @param int      $flags
419
+     *  The OR-combined flags accepted by the DOM parser. See the PHP documentation
420
+     *  for DOM or for libxml.
421
+     * @param resource $context
422
+     *  The stream context for the file IO. If this is set, then an alternate
423
+     *  parsing path is followed: The file is loaded by PHP's stream-aware IO
424
+     *  facilities, read entirely into memory, and then handed off to
425
+     *  {@link parseXMLString()}. On large files, this can have a performance impact.
426
+     *
427
+     * @throws ParseException
428
+     *  Thrown when a file cannot be loaded or parsed.
429
+     */
430
+    private function parseXMLFile($filename, $flags = 0, $context = null)
431
+    {
432
+        // Backwards compatibility fix for PHP8+
433
+        if (is_null($flags)) {
434
+            $flags = 0;
435
+        }
436
+
437
+        // If a context is specified, we basically have to do the reading in
438
+        // two steps:
439
+        if (! empty($context)) {
440
+            try {
441
+                set_error_handler(['\QueryPath\ParseException', 'initializeFromError'], $this->errTypes);
442
+                $contents = file_get_contents($filename, false, $context);
443
+            }
444
+                // Apparently there is no 'finally' in PHP, so we have to restore the error
445
+                // handler this way:
446
+            catch (Exception $e) {
447
+                restore_error_handler();
448
+                throw $e;
449
+            }
450
+            restore_error_handler();
451
+
452
+            if ($contents == false) {
453
+                throw new ParseException(sprintf(
454
+                    'Contents of the file %s could not be retrieved.',
455
+                    $filename
456
+                ));
457
+            }
458
+
459
+            return $this->parseXMLString($contents, $flags);
460
+        }
461
+
462
+        $document = new DOMDocument();
463
+        $lastDot  = strrpos($filename, '.');
464
+
465
+        $htmlExtensions = [
466
+            '.html' => 1,
467
+            '.htm'  => 1,
468
+        ];
469
+
470
+        // Allow users to override parser settings.
471
+        if (empty($this->options['use_parser'])) {
472
+            $useParser = '';
473
+        } else {
474
+            $useParser = strtolower($this->options['use_parser']);
475
+        }
476
+
477
+        $ext = $lastDot !== false ? strtolower(substr($filename, $lastDot)) : '';
478
+
479
+        try {
480
+            set_error_handler([ParseException::class, 'initializeFromError'], $this->errTypes);
481
+
482
+            // If the parser is explicitly set to XML, use that parser.
483
+            if ($useParser === 'xml') {
484
+                $document->load($filename, $flags);
485
+            } // Otherwise, see if it looks like HTML.
486
+            elseif ($useParser === 'html' || isset($htmlExtensions[$ext])) {
487
+                // Try parsing it as HTML.
488
+                $document->loadHTMLFile($filename);
489
+            } // Default to XML.
490
+            else {
491
+                $document->load($filename, $flags);
492
+            }
493
+
494
+        } // Emulate 'finally' behavior.
495
+        catch (Exception $e) {
496
+            restore_error_handler();
497
+            throw $e;
498
+        }
499
+        restore_error_handler();
500
+
501
+        return $document;
502
+    }
503
+
504
+    /**
505
+     * Determine whether a given string looks like XML or not.
506
+     *
507
+     * Basically, this scans a portion of the supplied string, checking to see
508
+     * if it has a tag-like structure. It is possible to "confuse" this, which
509
+     * may subsequently result in parse errors, but in the vast majority of
510
+     * cases, this method serves as a valid inicator of whether or not the
511
+     * content looks like XML.
512
+     *
513
+     * Things that are intentional excluded:
514
+     * - plain text with no markup.
515
+     * - strings that look like filesystem paths.
516
+     *
517
+     * Subclasses SHOULD NOT OVERRIDE THIS. Altering it may be altering
518
+     * core assumptions about how things work. Instead, classes should
519
+     * override the constructor and pass in only one of the parsed types
520
+     * that this class expects.
521
+     */
522
+    protected function isXMLish($string)
523
+    {
524
+        return (strpos($string, '<') !== false && strpos($string, '>') !== false);
525
+    }
526
+
527
+    /**
528
+     * A utility function for retriving a match by index.
529
+     *
530
+     * The internal data structure used in DOMQuery does not have
531
+     * strong random access support, so we suppliment it with this method.
532
+     *
533
+     * @param $index
534
+     *
535
+     * @return object|void
536
+     */
537
+    protected function getNthMatch(int $index)
538
+    {
539
+        if ($index < 0 || $index > $this->matches->count()) {
540
+            return;
541
+        }
542
+
543
+        $i = 0;
544
+        foreach ($this->matches as $m) {
545
+            if ($i++ === $index) {
546
+                return $m;
547
+            }
548
+        }
549
+    }
550 550
 }
Please login to merge, or discard this patch.
Spacing   +8 added lines, -8 removed lines patch added patch discarded remove patch
@@ -84,7 +84,7 @@  discard block
 block discarded – undo
84 84
 		$this->options = $options + Options::get() + $this->options;
85 85
 
86 86
 		$parser_flags = $options['parser_flags'] ?? self::DEFAULT_PARSER_FLAGS;
87
-		if (! empty($this->options['ignore_parser_warnings'])) {
87
+		if (!empty($this->options['ignore_parser_warnings'])) {
88 88
 			// Don't convert parser warnings into exceptions.
89 89
 			$this->errTypes = 257; //E_ERROR | E_USER_ERROR;
90 90
 		} elseif (isset($this->options['exception_level'])) {
@@ -109,7 +109,7 @@  discard block
 block discarded – undo
109 109
 				$this->matches = $document;
110 110
 				if ($document->count() !== 0) {
111 111
 					$first = $this->getFirstMatch();
112
-					if (! empty($first->ownerDocument)) {
112
+					if (!empty($first->ownerDocument)) {
113 113
 						$this->document = $first->ownerDocument;
114 114
 					}
115 115
 				}
@@ -140,7 +140,7 @@  discard block
 block discarded – undo
140 140
 			}
141 141
 		} elseif (is_array($document)) {
142 142
 			//trigger_error('Detected deprecated array support', E_USER_NOTICE);
143
-			if (! empty($document) && $document[0] instanceof DOMNode) {
143
+			if (!empty($document) && $document[0] instanceof DOMNode) {
144 144
 				$found = new SplObjectStorage();
145 145
 				foreach ($document as $item) {
146 146
 					$found->attach($item);
@@ -199,13 +199,13 @@  discard block
 block discarded – undo
199 199
 
200 200
 			// This is to avoid cases where low ascii digits have slipped into HTML.
201 201
 			// AFAIK, it should not adversly effect UTF-8 documents.
202
-			if (! empty($this->options['strip_low_ascii'])) {
202
+			if (!empty($this->options['strip_low_ascii'])) {
203 203
 				$string = filter_var($string, FILTER_UNSAFE_RAW, FILTER_FLAG_ENCODE_LOW);
204 204
 			}
205 205
 
206 206
 			// Allow users to override parser settings.
207 207
 			$useParser = '';
208
-			if (! empty($this->options['use_parser'])) {
208
+			if (!empty($this->options['use_parser'])) {
209 209
 				$useParser = strtolower($this->options['use_parser']);
210 210
 			}
211 211
 
@@ -297,10 +297,10 @@  discard block
 block discarded – undo
297 297
 	protected function deepestNode(DOMNode $ele, $depth = 0, $current = null, &$deepest = null)
298 298
 	{
299 299
 		// FIXME: Should this use SplObjectStorage?
300
-		if (! isset($current)) {
300
+		if (!isset($current)) {
301 301
 			$current = [$ele];
302 302
 		}
303
-		if (! isset($deepest)) {
303
+		if (!isset($deepest)) {
304 304
 			$deepest = $depth;
305 305
 		}
306 306
 		if ($ele->hasChildNodes()) {
@@ -436,7 +436,7 @@  discard block
 block discarded – undo
436 436
 
437 437
 		// If a context is specified, we basically have to do the reading in
438 438
 		// two steps:
439
-		if (! empty($context)) {
439
+		if (!empty($context)) {
440 440
 			try {
441 441
 				set_error_handler(['\QueryPath\ParseException', 'initializeFromError'], $this->errTypes);
442 442
 				$contents = file_get_contents($filename, false, $context);
Please login to merge, or discard this patch.
src/QueryPath.php 1 patch
Indentation   +233 added lines, -233 removed lines patch added patch discarded remove patch
@@ -96,45 +96,45 @@  discard block
 block discarded – undo
96 96
 class QueryPath
97 97
 {
98 98
 
99
-	/**
100
-	 * The version string for this version of QueryPath.
101
-	 *
102
-	 * Standard releases will be of the following form: <MAJOR>.<MINOR>[.<PATCH>][-STABILITY].
103
-	 *
104
-	 * Examples:
105
-	 * - 2.0
106
-	 * - 2.1.1
107
-	 * - 2.0-alpha1
108
-	 *
109
-	 * Developer releases will always be of the form dev-<DATE>.
110
-	 *
111
-	 * @since 2.0
112
-	 */
113
-	public const VERSION = '3.0.x';
99
+    /**
100
+     * The version string for this version of QueryPath.
101
+     *
102
+     * Standard releases will be of the following form: <MAJOR>.<MINOR>[.<PATCH>][-STABILITY].
103
+     *
104
+     * Examples:
105
+     * - 2.0
106
+     * - 2.1.1
107
+     * - 2.0-alpha1
108
+     *
109
+     * Developer releases will always be of the form dev-<DATE>.
110
+     *
111
+     * @since 2.0
112
+     */
113
+    public const VERSION = '3.0.x';
114 114
 
115
-	/**
116
-	 * Major version number.
117
-	 *
118
-	 * Examples:
119
-	 * - 3
120
-	 * - 4
121
-	 *
122
-	 * @since 3.0.1
123
-	 */
124
-	public const VERSION_MAJOR = 3;
115
+    /**
116
+     * Major version number.
117
+     *
118
+     * Examples:
119
+     * - 3
120
+     * - 4
121
+     *
122
+     * @since 3.0.1
123
+     */
124
+    public const VERSION_MAJOR = 3;
125 125
 
126
-	/**
127
-	 * This is a stub HTML 4.01 document.
128
-	 *
129
-	 * <b>Using {@link QueryPath::XHTML_STUB} is preferred.</b>
130
-	 *
131
-	 * This is primarily for generating legacy HTML content. Modern web applications
132
-	 * should use QueryPath::XHTML_STUB.
133
-	 *
134
-	 * Use this stub with the HTML familiy of methods (QueryPath::Query::html(),
135
-	 * QueryPath::Query::writeHTML(), QueryPath::Query::innerHTML()).
136
-	 */
137
-	public const HTML_STUB = '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
126
+    /**
127
+     * This is a stub HTML 4.01 document.
128
+     *
129
+     * <b>Using {@link QueryPath::XHTML_STUB} is preferred.</b>
130
+     *
131
+     * This is primarily for generating legacy HTML content. Modern web applications
132
+     * should use QueryPath::XHTML_STUB.
133
+     *
134
+     * Use this stub with the HTML familiy of methods (QueryPath::Query::html(),
135
+     * QueryPath::Query::writeHTML(), QueryPath::Query::innerHTML()).
136
+     */
137
+    public const HTML_STUB = '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
138 138
   <html lang="en">
139 139
   <head>
140 140
   <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
@@ -143,7 +143,7 @@  discard block
 block discarded – undo
143 143
   <body></body>
144 144
   </html>';
145 145
 
146
-	public const HTML5_STUB = '<!DOCTYPE html>
146
+    public const HTML5_STUB = '<!DOCTYPE html>
147 147
     <html>
148 148
     <head>
149 149
     <title>Untitled</title>
@@ -151,24 +151,24 @@  discard block
 block discarded – undo
151 151
     <body></body>
152 152
     </html>';
153 153
 
154
-	/**
155
-	 * This is a stub XHTML document.
156
-	 *
157
-	 * Since XHTML is an XML format, you should use XML functions with this document
158
-	 * fragment. For example, you should use {@link xml()}, {@link innerXML()}, and
159
-	 * {@link writeXML()}.
160
-	 *
161
-	 * This can be passed into {@link qp()} to begin a new basic HTML document.
162
-	 *
163
-	 * Example:
164
-	 *
165
-	 * @code
166
-	 * $qp = qp(QueryPath::XHTML_STUB); // Creates a new XHTML document
167
-	 * $qp->writeXML(); // Writes the document as well-formed XHTML.
168
-	 * @endcode
169
-	 * @since 2.0
170
-	 */
171
-	public const XHTML_STUB = '<?xml version="1.0"?>
154
+    /**
155
+     * This is a stub XHTML document.
156
+     *
157
+     * Since XHTML is an XML format, you should use XML functions with this document
158
+     * fragment. For example, you should use {@link xml()}, {@link innerXML()}, and
159
+     * {@link writeXML()}.
160
+     *
161
+     * This can be passed into {@link qp()} to begin a new basic HTML document.
162
+     *
163
+     * Example:
164
+     *
165
+     * @code
166
+     * $qp = qp(QueryPath::XHTML_STUB); // Creates a new XHTML document
167
+     * $qp->writeXML(); // Writes the document as well-formed XHTML.
168
+     * @endcode
169
+     * @since 2.0
170
+     */
171
+    public const XHTML_STUB = '<?xml version="1.0"?>
172 172
   <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
173 173
   <html xmlns="http://www.w3.org/1999/xhtml">
174 174
   <head>
@@ -179,197 +179,197 @@  discard block
 block discarded – undo
179 179
   </html>';
180 180
 
181 181
 
182
-	/**
183
-	 * @param null  $document
184
-	 * @param null  $selector
185
-	 * @param array $options
186
-	 *
187
-	 * @return mixed|DOMQuery
188
-	 */
189
-	public static function with($document = null, $selector = '', array $options = [])
190
-	{
191
-		$qpClass = $options['QueryPath_class'] ?? '\QueryPath\DOMQuery';
182
+    /**
183
+     * @param null  $document
184
+     * @param null  $selector
185
+     * @param array $options
186
+     *
187
+     * @return mixed|DOMQuery
188
+     */
189
+    public static function with($document = null, $selector = '', array $options = [])
190
+    {
191
+        $qpClass = $options['QueryPath_class'] ?? '\QueryPath\DOMQuery';
192 192
 
193
-		return new $qpClass($document, $selector, $options);
194
-	}
193
+        return new $qpClass($document, $selector, $options);
194
+    }
195 195
 
196
-	public static function withXML($source = null, $selector = '', array $options = [])
197
-	{
198
-		$options += [
199
-			'use_parser' => 'xml',
200
-		];
196
+    public static function withXML($source = null, $selector = '', array $options = [])
197
+    {
198
+        $options += [
199
+            'use_parser' => 'xml',
200
+        ];
201 201
 
202
-		return self::with($source, $selector, $options);
203
-	}
202
+        return self::with($source, $selector, $options);
203
+    }
204 204
 
205
-	public static function withHTML($source = null, $selector = '', array $options = [])
206
-	{
207
-		// Need a way to force an HTML parse instead of an XML parse when the
208
-		// doctype is XHTML, since many XHTML documents are not valid XML
209
-		// (because of coding errors, not by design).
205
+    public static function withHTML($source = null, $selector = '', array $options = [])
206
+    {
207
+        // Need a way to force an HTML parse instead of an XML parse when the
208
+        // doctype is XHTML, since many XHTML documents are not valid XML
209
+        // (because of coding errors, not by design).
210 210
 
211
-		$options += [
212
-			'ignore_parser_warnings' => true,
213
-			'convert_to_encoding'    => 'ISO-8859-1',
214
-			'convert_from_encoding'  => 'auto',
215
-			//'replace_entities' => TRUE,
216
-			'use_parser'             => 'html',
217
-			// This is stripping actually necessary low ASCII.
218
-			//'strip_low_ascii' => TRUE,
219
-		];
211
+        $options += [
212
+            'ignore_parser_warnings' => true,
213
+            'convert_to_encoding'    => 'ISO-8859-1',
214
+            'convert_from_encoding'  => 'auto',
215
+            //'replace_entities' => TRUE,
216
+            'use_parser'             => 'html',
217
+            // This is stripping actually necessary low ASCII.
218
+            //'strip_low_ascii' => TRUE,
219
+        ];
220 220
 
221
-		return @self::with($source, $selector, $options);
222
-	}
221
+        return @self::with($source, $selector, $options);
222
+    }
223 223
 
224
-	/**
225
-	 * Parse HTML5 documents.
226
-	 *
227
-	 * This uses HTML5-PHP to parse the document. In actuality, this parser does
228
-	 * a fine job with pre-HTML5 documents in most cases, though really old HTML
229
-	 * (like 2.0) may have some substantial quirks.
230
-	 *
231
-	 * <b>Supported Options</b>
232
-	 * Any options supported by HTML5-PHP are allowed here. Additionally, the
233
-	 * following options have meaning to QueryPath.
234
-	 * - QueryPath_class
235
-	 *
236
-	 *
237
-	 * @param mixed  $source
238
-	 *   A document as an HTML string, or a path/URL. For compatibility with
239
-	 *   existing functions, a DOMDocument, SimpleXMLElement, DOMNode or array
240
-	 *   of DOMNodes will be passed through as well. However, these types are not
241
-	 *   validated in any way.
242
-	 *
243
-	 * @param string $selector
244
-	 *   A CSS3 selector.
245
-	 *
246
-	 * @param array  $options
247
-	 *   An associative array of options, which is passed on into HTML5-PHP. Note
248
-	 *   that the standard QueryPath options may be ignored for this function,
249
-	 *   since it uses a different parser.
250
-	 *
251
-	 * @return QueryPath
252
-	 */
253
-	public static function withHTML5($source = null, $selector = '', $options = [])
254
-	{
255
-		$qpClass = $options['QueryPath_class'] ?? '\QueryPath\DOMQuery';
224
+    /**
225
+     * Parse HTML5 documents.
226
+     *
227
+     * This uses HTML5-PHP to parse the document. In actuality, this parser does
228
+     * a fine job with pre-HTML5 documents in most cases, though really old HTML
229
+     * (like 2.0) may have some substantial quirks.
230
+     *
231
+     * <b>Supported Options</b>
232
+     * Any options supported by HTML5-PHP are allowed here. Additionally, the
233
+     * following options have meaning to QueryPath.
234
+     * - QueryPath_class
235
+     *
236
+     *
237
+     * @param mixed  $source
238
+     *   A document as an HTML string, or a path/URL. For compatibility with
239
+     *   existing functions, a DOMDocument, SimpleXMLElement, DOMNode or array
240
+     *   of DOMNodes will be passed through as well. However, these types are not
241
+     *   validated in any way.
242
+     *
243
+     * @param string $selector
244
+     *   A CSS3 selector.
245
+     *
246
+     * @param array  $options
247
+     *   An associative array of options, which is passed on into HTML5-PHP. Note
248
+     *   that the standard QueryPath options may be ignored for this function,
249
+     *   since it uses a different parser.
250
+     *
251
+     * @return QueryPath
252
+     */
253
+    public static function withHTML5($source = null, $selector = '', $options = [])
254
+    {
255
+        $qpClass = $options['QueryPath_class'] ?? '\QueryPath\DOMQuery';
256 256
 
257
-		if (is_string($source)) {
258
-			$html5 = new HTML5();
259
-			if (strpos($source, '<') !== false && strpos($source, '>') !== false) {
260
-				$source = $html5->loadHTML($source);
261
-			} else {
262
-				$source = $html5->load($source);
263
-			}
264
-		}
257
+        if (is_string($source)) {
258
+            $html5 = new HTML5();
259
+            if (strpos($source, '<') !== false && strpos($source, '>') !== false) {
260
+                $source = $html5->loadHTML($source);
261
+            } else {
262
+                $source = $html5->load($source);
263
+            }
264
+        }
265 265
 
266
-		$qp = new $qpClass($source, $selector, $options);
266
+        $qp = new $qpClass($source, $selector, $options);
267 267
 
268
-		return $qp;
269
-	}
268
+        return $qp;
269
+    }
270 270
 
271
-	/**
272
-	 * Enable one or more extensions.
273
-	 *
274
-	 * Extensions provide additional features to QueryPath. To enable and
275
-	 * extension, you can use this method.
276
-	 *
277
-	 * In this example, we enable the QPTPL extension:
278
-	 *
279
-	 * @code
280
-	 * <?php
281
-	 * QueryPath::enable('\QueryPath\QPTPL');
282
-	 * ?>
283
-	 * @endcode
284
-	 *
285
-	 * Note that the name is a fully qualified class name.
286
-	 *
287
-	 * We can enable more than one extension at a time like this:
288
-	 *
289
-	 * @code
290
-	 * <?php
291
-	 * $extensions = array('\QueryPath\QPXML', '\QueryPath\QPDB');
292
-	 * QueryPath::enable($extensions);
293
-	 * ?>
294
-	 * @endcode
295
-	 *
296
-	 * @attention If you are not using an autoloader, you will need to
297
-	 * manually `require` or `include` the files that contain the
298
-	 * extensions.
299
-	 *
300
-	 * @param mixed $extensionNames
301
-	 *   The name of an extension or an array of extension names.
302
-	 *   QueryPath assumes that these are extension class names,
303
-	 *   and attempts to register these as QueryPath extensions.
304
-	 */
305
-	public static function enable($extensionNames): void
306
-	{
307
-		if (is_array($extensionNames)) {
308
-			foreach ($extensionNames as $extension) {
309
-				ExtensionRegistry::extend($extension);
310
-			}
311
-		} else {
312
-			ExtensionRegistry::extend($extensionNames);
313
-		}
314
-	}
271
+    /**
272
+     * Enable one or more extensions.
273
+     *
274
+     * Extensions provide additional features to QueryPath. To enable and
275
+     * extension, you can use this method.
276
+     *
277
+     * In this example, we enable the QPTPL extension:
278
+     *
279
+     * @code
280
+     * <?php
281
+     * QueryPath::enable('\QueryPath\QPTPL');
282
+     * ?>
283
+     * @endcode
284
+     *
285
+     * Note that the name is a fully qualified class name.
286
+     *
287
+     * We can enable more than one extension at a time like this:
288
+     *
289
+     * @code
290
+     * <?php
291
+     * $extensions = array('\QueryPath\QPXML', '\QueryPath\QPDB');
292
+     * QueryPath::enable($extensions);
293
+     * ?>
294
+     * @endcode
295
+     *
296
+     * @attention If you are not using an autoloader, you will need to
297
+     * manually `require` or `include` the files that contain the
298
+     * extensions.
299
+     *
300
+     * @param mixed $extensionNames
301
+     *   The name of an extension or an array of extension names.
302
+     *   QueryPath assumes that these are extension class names,
303
+     *   and attempts to register these as QueryPath extensions.
304
+     */
305
+    public static function enable($extensionNames): void
306
+    {
307
+        if (is_array($extensionNames)) {
308
+            foreach ($extensionNames as $extension) {
309
+                ExtensionRegistry::extend($extension);
310
+            }
311
+        } else {
312
+            ExtensionRegistry::extend($extensionNames);
313
+        }
314
+    }
315 315
 
316
-	/**
317
-	 * Get a list of all of the enabled extensions.
318
-	 *
319
-	 * This example dumps a list of extensions to standard output:
320
-	 *
321
-	 * @code
322
-	 * <?php
323
-	 * $extensions = QueryPath::enabledExtensions();
324
-	 * print_r($extensions);
325
-	 * ?>
326
-	 * @endcode
327
-	 *
328
-	 * @return array
329
-	 *   An array of extension names.
330
-	 *
331
-	 * @see QueryPath::ExtensionRegistry
332
-	 */
333
-	public static function enabledExtensions(): array
334
-	{
335
-		return ExtensionRegistry::extensionNames();
336
-	}
316
+    /**
317
+     * Get a list of all of the enabled extensions.
318
+     *
319
+     * This example dumps a list of extensions to standard output:
320
+     *
321
+     * @code
322
+     * <?php
323
+     * $extensions = QueryPath::enabledExtensions();
324
+     * print_r($extensions);
325
+     * ?>
326
+     * @endcode
327
+     *
328
+     * @return array
329
+     *   An array of extension names.
330
+     *
331
+     * @see QueryPath::ExtensionRegistry
332
+     */
333
+    public static function enabledExtensions(): array
334
+    {
335
+        return ExtensionRegistry::extensionNames();
336
+    }
337 337
 
338 338
 
339
-	/**
340
-	 * A static function for transforming data into a Data URL.
341
-	 *
342
-	 * This can be used to create Data URLs for injection into CSS, JavaScript, or other
343
-	 * non-XML/HTML content. If you are working with QP objects, you may want to use
344
-	 * dataURL() instead.
345
-	 *
346
-	 * @param mixed    $data
347
-	 *    The contents to inject as the data. The value can be any one of the following:
348
-	 *    - A URL: If this is given, then the subsystem will read the content from that URL. THIS
349
-	 *    MUST BE A FULL URL, not a relative path.
350
-	 *    - A string of data: If this is given, then the subsystem will encode the string.
351
-	 *    - A stream or file handle: If this is given, the stream's contents will be encoded
352
-	 *    and inserted as data.
353
-	 *    (Note that we make the assumption here that you would never want to set data to be
354
-	 *    a URL. If this is an incorrect assumption, file a bug.)
355
-	 * @param string   $mime
356
-	 *    The MIME type of the document.
357
-	 * @param resource $context
358
-	 *    A valid context. Use this only if you need to pass a stream context. This is only necessary
359
-	 *    if $data is a URL. (See {@link stream_context_create()}).
360
-	 *
361
-	 * @return string An encoded data URL.
362
-	 */
363
-	public static function encodeDataURL($data, $mime = 'application/octet-stream', $context = null): string
364
-	{
365
-		if (is_resource($data)) {
366
-			$data = stream_get_contents($data);
367
-		} elseif (filter_var($data, FILTER_VALIDATE_URL)) {
368
-			$data = file_get_contents($data, false, $context);
369
-		}
339
+    /**
340
+     * A static function for transforming data into a Data URL.
341
+     *
342
+     * This can be used to create Data URLs for injection into CSS, JavaScript, or other
343
+     * non-XML/HTML content. If you are working with QP objects, you may want to use
344
+     * dataURL() instead.
345
+     *
346
+     * @param mixed    $data
347
+     *    The contents to inject as the data. The value can be any one of the following:
348
+     *    - A URL: If this is given, then the subsystem will read the content from that URL. THIS
349
+     *    MUST BE A FULL URL, not a relative path.
350
+     *    - A string of data: If this is given, then the subsystem will encode the string.
351
+     *    - A stream or file handle: If this is given, the stream's contents will be encoded
352
+     *    and inserted as data.
353
+     *    (Note that we make the assumption here that you would never want to set data to be
354
+     *    a URL. If this is an incorrect assumption, file a bug.)
355
+     * @param string   $mime
356
+     *    The MIME type of the document.
357
+     * @param resource $context
358
+     *    A valid context. Use this only if you need to pass a stream context. This is only necessary
359
+     *    if $data is a URL. (See {@link stream_context_create()}).
360
+     *
361
+     * @return string An encoded data URL.
362
+     */
363
+    public static function encodeDataURL($data, $mime = 'application/octet-stream', $context = null): string
364
+    {
365
+        if (is_resource($data)) {
366
+            $data = stream_get_contents($data);
367
+        } elseif (filter_var($data, FILTER_VALIDATE_URL)) {
368
+            $data = file_get_contents($data, false, $context);
369
+        }
370 370
 
371
-		$encoded = base64_encode($data);
371
+        $encoded = base64_encode($data);
372 372
 
373
-		return 'data:' . $mime . ';base64,' . $encoded;
374
-	}
373
+        return 'data:' . $mime . ';base64,' . $encoded;
374
+    }
375 375
 }
Please login to merge, or discard this patch.