@@ -28,124 +28,124 @@ |
||
| 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 | } |
@@ -13,19 +13,19 @@ |
||
| 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 | } |
@@ -157,7 +157,7 @@ discard block |
||
| 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 |
||
| 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 |
||
| 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 | } |
@@ -93,5 +93,5 @@ |
||
| 93 | 93 | */ |
| 94 | 94 | interface Extension |
| 95 | 95 | { |
| 96 | - public function __construct(Query $qp); |
|
| 96 | + public function __construct(Query $qp); |
|
| 97 | 97 | } |
@@ -31,64 +31,64 @@ |
||
| 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 | } |
@@ -23,19 +23,19 @@ |
||
| 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 | } |
@@ -28,7 +28,7 @@ |
||
| 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(); |
@@ -49,1521 +49,1521 @@ |
||
| 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 | } |
@@ -196,7 +196,7 @@ discard block |
||
| 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 |
||
| 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 |
||
| 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 |
||
| 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 |
||
| 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 |
||
| 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 |
||
| 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 |
||
| 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 |
||
| 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 |
||
| 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 |
||
| 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 |
||
| 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 |
||
| 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 |
||
| 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 | |
@@ -23,528 +23,528 @@ |
||
| 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 | } |
@@ -84,7 +84,7 @@ discard block |
||
| 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 |
||
| 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 |
||
| 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 |
||
| 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 |
||
| 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 |
||
| 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); |
@@ -96,45 +96,45 @@ discard block |
||
| 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 |
||
| 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 |
||
| 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 |
||
| 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 | } |