@@ -11,31 +11,31 @@ |
||
| 11 | 11 | */ |
| 12 | 12 | interface Traverser |
| 13 | 13 | { |
| 14 | - /** |
|
| 15 | - * Process a CSS selector and find matches. |
|
| 16 | - * |
|
| 17 | - * This specifies a query to be run by the Traverser. A given |
|
| 18 | - * Traverser may, in practice, delay the finding until some later time |
|
| 19 | - * but must return the found results when getMatches() is called. |
|
| 20 | - * |
|
| 21 | - * @param string $selector |
|
| 22 | - * A selector. Typically this is a CSS 3 Selector. |
|
| 23 | - * |
|
| 24 | - * @return \Traverser |
|
| 25 | - * The Traverser that can return matches. |
|
| 26 | - */ |
|
| 27 | - public function find($selector); |
|
| 14 | + /** |
|
| 15 | + * Process a CSS selector and find matches. |
|
| 16 | + * |
|
| 17 | + * This specifies a query to be run by the Traverser. A given |
|
| 18 | + * Traverser may, in practice, delay the finding until some later time |
|
| 19 | + * but must return the found results when getMatches() is called. |
|
| 20 | + * |
|
| 21 | + * @param string $selector |
|
| 22 | + * A selector. Typically this is a CSS 3 Selector. |
|
| 23 | + * |
|
| 24 | + * @return \Traverser |
|
| 25 | + * The Traverser that can return matches. |
|
| 26 | + */ |
|
| 27 | + public function find($selector); |
|
| 28 | 28 | |
| 29 | - /** |
|
| 30 | - * Get the results of a find() operation. |
|
| 31 | - * |
|
| 32 | - * Return an array of matching items. |
|
| 33 | - * |
|
| 34 | - * @return array |
|
| 35 | - * An array of matched values. The specific data type in the matches |
|
| 36 | - * will differ depending on the data type searched, but in the core |
|
| 37 | - * QueryPath implementation, this will be an array of DOMNode |
|
| 38 | - * objects. |
|
| 39 | - */ |
|
| 40 | - public function matches(); |
|
| 29 | + /** |
|
| 30 | + * Get the results of a find() operation. |
|
| 31 | + * |
|
| 32 | + * Return an array of matching items. |
|
| 33 | + * |
|
| 34 | + * @return array |
|
| 35 | + * An array of matched values. The specific data type in the matches |
|
| 36 | + * will differ depending on the data type searched, but in the core |
|
| 37 | + * QueryPath implementation, this will be an array of DOMNode |
|
| 38 | + * objects. |
|
| 39 | + */ |
|
| 40 | + public function matches(); |
|
| 41 | 41 | } |
@@ -14,74 +14,74 @@ |
||
| 14 | 14 | */ |
| 15 | 15 | final class Token |
| 16 | 16 | { |
| 17 | - public const CHAR = 0x0; |
|
| 18 | - public const STAR = 0x1; |
|
| 19 | - public const RANGLE = 0x2; |
|
| 20 | - public const DOT = 0x3; |
|
| 21 | - public const OCTO = 0x4; |
|
| 22 | - public const RSQUARE = 0x5; |
|
| 23 | - public const LSQUARE = 0x6; |
|
| 24 | - public const COLON = 0x7; |
|
| 25 | - public const RPAREN = 0x8; |
|
| 26 | - public const LPAREN = 0x9; |
|
| 27 | - public const PLUS = 0xA; |
|
| 28 | - public const TILDE = 0xB; |
|
| 29 | - public const EQ = 0xC; |
|
| 30 | - public const PIPE = 0xD; |
|
| 31 | - public const COMMA = 0xE; |
|
| 32 | - public const WHITE = 0xF; |
|
| 33 | - public const QUOTE = 0x10; |
|
| 34 | - public const SQUOTE = 0x11; |
|
| 35 | - public const BSLASH = 0x12; |
|
| 36 | - public const CARAT = 0x13; |
|
| 37 | - public const DOLLAR = 0x14; |
|
| 38 | - public const AT = 0x15; // This is not in the spec. Apparently, old broken CSS uses it. |
|
| 17 | + public const CHAR = 0x0; |
|
| 18 | + public const STAR = 0x1; |
|
| 19 | + public const RANGLE = 0x2; |
|
| 20 | + public const DOT = 0x3; |
|
| 21 | + public const OCTO = 0x4; |
|
| 22 | + public const RSQUARE = 0x5; |
|
| 23 | + public const LSQUARE = 0x6; |
|
| 24 | + public const COLON = 0x7; |
|
| 25 | + public const RPAREN = 0x8; |
|
| 26 | + public const LPAREN = 0x9; |
|
| 27 | + public const PLUS = 0xA; |
|
| 28 | + public const TILDE = 0xB; |
|
| 29 | + public const EQ = 0xC; |
|
| 30 | + public const PIPE = 0xD; |
|
| 31 | + public const COMMA = 0xE; |
|
| 32 | + public const WHITE = 0xF; |
|
| 33 | + public const QUOTE = 0x10; |
|
| 34 | + public const SQUOTE = 0x11; |
|
| 35 | + public const BSLASH = 0x12; |
|
| 36 | + public const CARAT = 0x13; |
|
| 37 | + public const DOLLAR = 0x14; |
|
| 38 | + public const AT = 0x15; // This is not in the spec. Apparently, old broken CSS uses it. |
|
| 39 | 39 | |
| 40 | - // In legal range for string. |
|
| 41 | - public const STRING_LEGAL = 0x63; |
|
| 40 | + // In legal range for string. |
|
| 41 | + public const STRING_LEGAL = 0x63; |
|
| 42 | 42 | |
| 43 | - /** |
|
| 44 | - * Get a name for a given constant. Used for error handling. |
|
| 45 | - */ |
|
| 46 | - public static function name($const_int) |
|
| 47 | - { |
|
| 48 | - $a = [ |
|
| 49 | - 'character', |
|
| 50 | - 'star', |
|
| 51 | - 'right angle bracket', |
|
| 52 | - 'dot', |
|
| 53 | - 'octothorp', |
|
| 54 | - 'right square bracket', |
|
| 55 | - 'left square bracket', |
|
| 56 | - 'colon', |
|
| 57 | - 'right parenthesis', |
|
| 58 | - 'left parenthesis', |
|
| 59 | - 'plus', |
|
| 60 | - 'tilde', |
|
| 61 | - 'equals', |
|
| 62 | - 'vertical bar', |
|
| 63 | - 'comma', |
|
| 64 | - 'space', |
|
| 65 | - 'quote', |
|
| 66 | - 'single quote', |
|
| 67 | - 'backslash', |
|
| 68 | - 'carat', |
|
| 69 | - 'dollar', |
|
| 70 | - 'at', |
|
| 71 | - ]; |
|
| 43 | + /** |
|
| 44 | + * Get a name for a given constant. Used for error handling. |
|
| 45 | + */ |
|
| 46 | + public static function name($const_int) |
|
| 47 | + { |
|
| 48 | + $a = [ |
|
| 49 | + 'character', |
|
| 50 | + 'star', |
|
| 51 | + 'right angle bracket', |
|
| 52 | + 'dot', |
|
| 53 | + 'octothorp', |
|
| 54 | + 'right square bracket', |
|
| 55 | + 'left square bracket', |
|
| 56 | + 'colon', |
|
| 57 | + 'right parenthesis', |
|
| 58 | + 'left parenthesis', |
|
| 59 | + 'plus', |
|
| 60 | + 'tilde', |
|
| 61 | + 'equals', |
|
| 62 | + 'vertical bar', |
|
| 63 | + 'comma', |
|
| 64 | + 'space', |
|
| 65 | + 'quote', |
|
| 66 | + 'single quote', |
|
| 67 | + 'backslash', |
|
| 68 | + 'carat', |
|
| 69 | + 'dollar', |
|
| 70 | + 'at', |
|
| 71 | + ]; |
|
| 72 | 72 | |
| 73 | - if (isset($a[$const_int]) && is_numeric($const_int)) { |
|
| 74 | - return $a[$const_int]; |
|
| 75 | - } |
|
| 73 | + if (isset($a[$const_int]) && is_numeric($const_int)) { |
|
| 74 | + return $a[$const_int]; |
|
| 75 | + } |
|
| 76 | 76 | |
| 77 | - if ($const_int === self::STRING_LEGAL) { |
|
| 78 | - return 'a legal non-alphanumeric character'; |
|
| 79 | - } |
|
| 77 | + if ($const_int === self::STRING_LEGAL) { |
|
| 78 | + return 'a legal non-alphanumeric character'; |
|
| 79 | + } |
|
| 80 | 80 | |
| 81 | - if ($const_int === false) { |
|
| 82 | - return 'end of file'; |
|
| 83 | - } |
|
| 81 | + if ($const_int === false) { |
|
| 82 | + return 'end of file'; |
|
| 83 | + } |
|
| 84 | 84 | |
| 85 | - return sprintf('illegal character (%s)', $const_int); |
|
| 86 | - } |
|
| 85 | + return sprintf('illegal character (%s)', $const_int); |
|
| 86 | + } |
|
| 87 | 87 | } |
@@ -54,133 +54,133 @@ |
||
| 54 | 54 | class Selector implements EventHandler, IteratorAggregate, Countable |
| 55 | 55 | { |
| 56 | 56 | |
| 57 | - protected $selectors = []; |
|
| 58 | - protected $currSelector; |
|
| 59 | - protected $selectorGroups = []; |
|
| 60 | - protected $groupIndex = 0; |
|
| 61 | - |
|
| 62 | - public function __construct() |
|
| 63 | - { |
|
| 64 | - $this->currSelector = new SimpleSelector(); |
|
| 65 | - |
|
| 66 | - $this->selectors[$this->groupIndex][] = $this->currSelector; |
|
| 67 | - } |
|
| 68 | - |
|
| 69 | - public function getIterator(): Traversable |
|
| 70 | - { |
|
| 71 | - return new ArrayIterator($this->selectors); |
|
| 72 | - } |
|
| 73 | - |
|
| 74 | - /** |
|
| 75 | - * Get the array of SimpleSelector objects. |
|
| 76 | - * |
|
| 77 | - * Normally, one iterates over a Selector. However, if it is |
|
| 78 | - * necessary to get the selector array and manipulate it, this |
|
| 79 | - * method can be used. |
|
| 80 | - */ |
|
| 81 | - public function toArray() |
|
| 82 | - { |
|
| 83 | - return $this->selectors; |
|
| 84 | - } |
|
| 85 | - |
|
| 86 | - public function count(): int |
|
| 87 | - { |
|
| 88 | - return count($this->selectors); |
|
| 89 | - } |
|
| 90 | - |
|
| 91 | - public function elementID($id) |
|
| 92 | - { |
|
| 93 | - $this->currSelector->id = $id; |
|
| 94 | - } |
|
| 95 | - |
|
| 96 | - public function element($name) |
|
| 97 | - { |
|
| 98 | - $this->currSelector->element = $name; |
|
| 99 | - } |
|
| 100 | - |
|
| 101 | - public function elementNS($name, $namespace = null) |
|
| 102 | - { |
|
| 103 | - $this->currSelector->ns = $namespace; |
|
| 104 | - $this->currSelector->element = $name; |
|
| 105 | - } |
|
| 106 | - |
|
| 107 | - public function anyElement() |
|
| 108 | - { |
|
| 109 | - $this->currSelector->element = '*'; |
|
| 110 | - } |
|
| 111 | - |
|
| 112 | - public function anyElementInNS($ns) |
|
| 113 | - { |
|
| 114 | - $this->currSelector->ns = $ns; |
|
| 115 | - $this->currSelector->element = '*'; |
|
| 116 | - } |
|
| 117 | - |
|
| 118 | - public function elementClass($name) |
|
| 119 | - { |
|
| 120 | - $this->currSelector->classes[] = $name; |
|
| 121 | - } |
|
| 122 | - |
|
| 123 | - public function attribute($name, $value = null, $operation = EventHandler::IS_EXACTLY) |
|
| 124 | - { |
|
| 125 | - $this->currSelector->attributes[] = [ |
|
| 126 | - 'name' => $name, |
|
| 127 | - 'value' => $value, |
|
| 128 | - 'op' => $operation, |
|
| 129 | - ]; |
|
| 130 | - } |
|
| 131 | - |
|
| 132 | - public function attributeNS($name, $ns, $value = null, $operation = EventHandler::IS_EXACTLY) |
|
| 133 | - { |
|
| 134 | - $this->currSelector->attributes[] = [ |
|
| 135 | - 'name' => $name, |
|
| 136 | - 'value' => $value, |
|
| 137 | - 'op' => $operation, |
|
| 138 | - 'ns' => $ns, |
|
| 139 | - ]; |
|
| 140 | - } |
|
| 141 | - |
|
| 142 | - public function pseudoClass($name, $value = null) |
|
| 143 | - { |
|
| 144 | - $this->currSelector->pseudoClasses[] = ['name' => $name, 'value' => $value]; |
|
| 145 | - } |
|
| 146 | - |
|
| 147 | - public function pseudoElement($name) |
|
| 148 | - { |
|
| 149 | - $this->currSelector->pseudoElements[] = $name; |
|
| 150 | - } |
|
| 151 | - |
|
| 152 | - public function combinator($combinatorName) |
|
| 153 | - { |
|
| 154 | - $this->currSelector->combinator = $combinatorName; |
|
| 155 | - $this->currSelector = new SimpleSelector(); |
|
| 156 | - array_unshift($this->selectors[$this->groupIndex], $this->currSelector); |
|
| 157 | - //$this->selectors[]= $this->currSelector; |
|
| 158 | - } |
|
| 159 | - |
|
| 160 | - public function directDescendant() |
|
| 161 | - { |
|
| 162 | - $this->combinator(SimpleSelector::DIRECT_DESCENDANT); |
|
| 163 | - } |
|
| 164 | - |
|
| 165 | - public function adjacent() |
|
| 166 | - { |
|
| 167 | - $this->combinator(SimpleSelector::ADJACENT); |
|
| 168 | - } |
|
| 169 | - |
|
| 170 | - public function anotherSelector() |
|
| 171 | - { |
|
| 172 | - $this->groupIndex++; |
|
| 173 | - $this->currSelector = new SimpleSelector(); |
|
| 174 | - $this->selectors[$this->groupIndex] = [$this->currSelector]; |
|
| 175 | - } |
|
| 176 | - |
|
| 177 | - public function sibling() |
|
| 178 | - { |
|
| 179 | - $this->combinator(SimpleSelector::SIBLING); |
|
| 180 | - } |
|
| 181 | - |
|
| 182 | - public function anyDescendant() |
|
| 183 | - { |
|
| 184 | - $this->combinator(SimpleSelector::ANY_DESCENDANT); |
|
| 185 | - } |
|
| 57 | + protected $selectors = []; |
|
| 58 | + protected $currSelector; |
|
| 59 | + protected $selectorGroups = []; |
|
| 60 | + protected $groupIndex = 0; |
|
| 61 | + |
|
| 62 | + public function __construct() |
|
| 63 | + { |
|
| 64 | + $this->currSelector = new SimpleSelector(); |
|
| 65 | + |
|
| 66 | + $this->selectors[$this->groupIndex][] = $this->currSelector; |
|
| 67 | + } |
|
| 68 | + |
|
| 69 | + public function getIterator(): Traversable |
|
| 70 | + { |
|
| 71 | + return new ArrayIterator($this->selectors); |
|
| 72 | + } |
|
| 73 | + |
|
| 74 | + /** |
|
| 75 | + * Get the array of SimpleSelector objects. |
|
| 76 | + * |
|
| 77 | + * Normally, one iterates over a Selector. However, if it is |
|
| 78 | + * necessary to get the selector array and manipulate it, this |
|
| 79 | + * method can be used. |
|
| 80 | + */ |
|
| 81 | + public function toArray() |
|
| 82 | + { |
|
| 83 | + return $this->selectors; |
|
| 84 | + } |
|
| 85 | + |
|
| 86 | + public function count(): int |
|
| 87 | + { |
|
| 88 | + return count($this->selectors); |
|
| 89 | + } |
|
| 90 | + |
|
| 91 | + public function elementID($id) |
|
| 92 | + { |
|
| 93 | + $this->currSelector->id = $id; |
|
| 94 | + } |
|
| 95 | + |
|
| 96 | + public function element($name) |
|
| 97 | + { |
|
| 98 | + $this->currSelector->element = $name; |
|
| 99 | + } |
|
| 100 | + |
|
| 101 | + public function elementNS($name, $namespace = null) |
|
| 102 | + { |
|
| 103 | + $this->currSelector->ns = $namespace; |
|
| 104 | + $this->currSelector->element = $name; |
|
| 105 | + } |
|
| 106 | + |
|
| 107 | + public function anyElement() |
|
| 108 | + { |
|
| 109 | + $this->currSelector->element = '*'; |
|
| 110 | + } |
|
| 111 | + |
|
| 112 | + public function anyElementInNS($ns) |
|
| 113 | + { |
|
| 114 | + $this->currSelector->ns = $ns; |
|
| 115 | + $this->currSelector->element = '*'; |
|
| 116 | + } |
|
| 117 | + |
|
| 118 | + public function elementClass($name) |
|
| 119 | + { |
|
| 120 | + $this->currSelector->classes[] = $name; |
|
| 121 | + } |
|
| 122 | + |
|
| 123 | + public function attribute($name, $value = null, $operation = EventHandler::IS_EXACTLY) |
|
| 124 | + { |
|
| 125 | + $this->currSelector->attributes[] = [ |
|
| 126 | + 'name' => $name, |
|
| 127 | + 'value' => $value, |
|
| 128 | + 'op' => $operation, |
|
| 129 | + ]; |
|
| 130 | + } |
|
| 131 | + |
|
| 132 | + public function attributeNS($name, $ns, $value = null, $operation = EventHandler::IS_EXACTLY) |
|
| 133 | + { |
|
| 134 | + $this->currSelector->attributes[] = [ |
|
| 135 | + 'name' => $name, |
|
| 136 | + 'value' => $value, |
|
| 137 | + 'op' => $operation, |
|
| 138 | + 'ns' => $ns, |
|
| 139 | + ]; |
|
| 140 | + } |
|
| 141 | + |
|
| 142 | + public function pseudoClass($name, $value = null) |
|
| 143 | + { |
|
| 144 | + $this->currSelector->pseudoClasses[] = ['name' => $name, 'value' => $value]; |
|
| 145 | + } |
|
| 146 | + |
|
| 147 | + public function pseudoElement($name) |
|
| 148 | + { |
|
| 149 | + $this->currSelector->pseudoElements[] = $name; |
|
| 150 | + } |
|
| 151 | + |
|
| 152 | + public function combinator($combinatorName) |
|
| 153 | + { |
|
| 154 | + $this->currSelector->combinator = $combinatorName; |
|
| 155 | + $this->currSelector = new SimpleSelector(); |
|
| 156 | + array_unshift($this->selectors[$this->groupIndex], $this->currSelector); |
|
| 157 | + //$this->selectors[]= $this->currSelector; |
|
| 158 | + } |
|
| 159 | + |
|
| 160 | + public function directDescendant() |
|
| 161 | + { |
|
| 162 | + $this->combinator(SimpleSelector::DIRECT_DESCENDANT); |
|
| 163 | + } |
|
| 164 | + |
|
| 165 | + public function adjacent() |
|
| 166 | + { |
|
| 167 | + $this->combinator(SimpleSelector::ADJACENT); |
|
| 168 | + } |
|
| 169 | + |
|
| 170 | + public function anotherSelector() |
|
| 171 | + { |
|
| 172 | + $this->groupIndex++; |
|
| 173 | + $this->currSelector = new SimpleSelector(); |
|
| 174 | + $this->selectors[$this->groupIndex] = [$this->currSelector]; |
|
| 175 | + } |
|
| 176 | + |
|
| 177 | + public function sibling() |
|
| 178 | + { |
|
| 179 | + $this->combinator(SimpleSelector::SIBLING); |
|
| 180 | + } |
|
| 181 | + |
|
| 182 | + public function anyDescendant() |
|
| 183 | + { |
|
| 184 | + $this->combinator(SimpleSelector::ANY_DESCENDANT); |
|
| 185 | + } |
|
| 186 | 186 | } |
@@ -60,166 +60,166 @@ discard block |
||
| 60 | 60 | */ |
| 61 | 61 | class DOMTraverser implements Traverser |
| 62 | 62 | { |
| 63 | - protected $matches = []; |
|
| 64 | - protected $selector; |
|
| 65 | - protected $dom; |
|
| 66 | - protected $initialized = true; |
|
| 67 | - protected $psHandler; |
|
| 68 | - protected $scopeNode; |
|
| 69 | - |
|
| 70 | - /** |
|
| 71 | - * Build a new DOMTraverser. |
|
| 72 | - * |
|
| 73 | - * This requires a DOM-like object or collection of DOM nodes. |
|
| 74 | - * |
|
| 75 | - * @param SplObjectStorage $splos |
|
| 76 | - * @param bool $initialized |
|
| 77 | - * @param null $scopeNode |
|
| 78 | - */ |
|
| 79 | - public function __construct(SplObjectStorage $splos, bool $initialized = false, $scopeNode = null) |
|
| 80 | - { |
|
| 81 | - $this->psHandler = new PseudoClass(); |
|
| 82 | - $this->initialized = $initialized; |
|
| 83 | - |
|
| 84 | - // Re-use the initial splos |
|
| 85 | - $this->matches = $splos; |
|
| 86 | - |
|
| 87 | - if (count($splos) !== 0) { |
|
| 88 | - $splos->rewind(); |
|
| 89 | - $first = $splos->current(); |
|
| 90 | - if ($first instanceof DOMDocument) { |
|
| 91 | - $this->dom = $first;//->documentElement; |
|
| 92 | - } else { |
|
| 93 | - $this->dom = $first->ownerDocument;//->documentElement; |
|
| 94 | - } |
|
| 95 | - |
|
| 96 | - $this->scopeNode = $scopeNode; |
|
| 97 | - if (empty($scopeNode)) { |
|
| 98 | - $this->scopeNode = $this->dom->documentElement; |
|
| 99 | - } |
|
| 100 | - } |
|
| 101 | - |
|
| 102 | - // This assumes a DOM. Need to also accomodate the case |
|
| 103 | - // where we get a set of elements. |
|
| 104 | - /* |
|
| 63 | + protected $matches = []; |
|
| 64 | + protected $selector; |
|
| 65 | + protected $dom; |
|
| 66 | + protected $initialized = true; |
|
| 67 | + protected $psHandler; |
|
| 68 | + protected $scopeNode; |
|
| 69 | + |
|
| 70 | + /** |
|
| 71 | + * Build a new DOMTraverser. |
|
| 72 | + * |
|
| 73 | + * This requires a DOM-like object or collection of DOM nodes. |
|
| 74 | + * |
|
| 75 | + * @param SplObjectStorage $splos |
|
| 76 | + * @param bool $initialized |
|
| 77 | + * @param null $scopeNode |
|
| 78 | + */ |
|
| 79 | + public function __construct(SplObjectStorage $splos, bool $initialized = false, $scopeNode = null) |
|
| 80 | + { |
|
| 81 | + $this->psHandler = new PseudoClass(); |
|
| 82 | + $this->initialized = $initialized; |
|
| 83 | + |
|
| 84 | + // Re-use the initial splos |
|
| 85 | + $this->matches = $splos; |
|
| 86 | + |
|
| 87 | + if (count($splos) !== 0) { |
|
| 88 | + $splos->rewind(); |
|
| 89 | + $first = $splos->current(); |
|
| 90 | + if ($first instanceof DOMDocument) { |
|
| 91 | + $this->dom = $first;//->documentElement; |
|
| 92 | + } else { |
|
| 93 | + $this->dom = $first->ownerDocument;//->documentElement; |
|
| 94 | + } |
|
| 95 | + |
|
| 96 | + $this->scopeNode = $scopeNode; |
|
| 97 | + if (empty($scopeNode)) { |
|
| 98 | + $this->scopeNode = $this->dom->documentElement; |
|
| 99 | + } |
|
| 100 | + } |
|
| 101 | + |
|
| 102 | + // This assumes a DOM. Need to also accomodate the case |
|
| 103 | + // where we get a set of elements. |
|
| 104 | + /* |
|
| 105 | 105 | $this->dom = $dom; |
| 106 | 106 | $this->matches = new \SplObjectStorage(); |
| 107 | 107 | $this->matches->attach($this->dom); |
| 108 | 108 | */ |
| 109 | - } |
|
| 110 | - |
|
| 111 | - public function debug($msg) |
|
| 112 | - { |
|
| 113 | - fwrite(STDOUT, PHP_EOL . $msg); |
|
| 114 | - } |
|
| 115 | - |
|
| 116 | - /** |
|
| 117 | - * Given a selector, find the matches in the given DOM. |
|
| 118 | - * |
|
| 119 | - * This is the main function for querying the DOM using a CSS |
|
| 120 | - * selector. |
|
| 121 | - * |
|
| 122 | - * @param string $selector |
|
| 123 | - * The selector. |
|
| 124 | - * |
|
| 125 | - * @return DOMTraverser a list of matched |
|
| 126 | - * DOMNode objects. |
|
| 127 | - * @throws ParseException |
|
| 128 | - */ |
|
| 129 | - public function find($selector): DOMTraverser |
|
| 130 | - { |
|
| 131 | - // Setup |
|
| 132 | - $handler = new Selector(); |
|
| 133 | - $parser = new Parser($selector, $handler); |
|
| 134 | - $parser->parse(); |
|
| 135 | - $this->selector = $handler; |
|
| 136 | - |
|
| 137 | - //$selector = $handler->toArray(); |
|
| 138 | - $found = $this->newMatches(); |
|
| 139 | - foreach ($handler as $selectorGroup) { |
|
| 140 | - // Initialize matches if necessary. |
|
| 141 | - if ($this->initialized) { |
|
| 142 | - $candidates = $this->matches; |
|
| 143 | - } else { |
|
| 144 | - $candidates = $this->initialMatch($selectorGroup[0], $this->matches); |
|
| 145 | - } |
|
| 146 | - |
|
| 147 | - /** @var DOMElement $candidate */ |
|
| 148 | - foreach ($candidates as $candidate) { |
|
| 149 | - // fprintf(STDOUT, "Testing %s against %s.\n", $candidate->tagName, $selectorGroup[0]); |
|
| 150 | - if ($this->matchesSelector($candidate, $selectorGroup)) { |
|
| 151 | - // $this->debug('Attaching ' . $candidate->nodeName); |
|
| 152 | - $found->attach($candidate); |
|
| 153 | - } |
|
| 154 | - } |
|
| 155 | - } |
|
| 156 | - $this->setMatches($found); |
|
| 157 | - |
|
| 158 | - return $this; |
|
| 159 | - } |
|
| 160 | - |
|
| 161 | - public function matches() |
|
| 162 | - { |
|
| 163 | - return $this->matches; |
|
| 164 | - } |
|
| 165 | - |
|
| 166 | - /** |
|
| 167 | - * Check whether the given node matches the given selector. |
|
| 168 | - * |
|
| 169 | - * A selector is a group of one or more simple selectors combined |
|
| 170 | - * by combinators. This determines if a given selector |
|
| 171 | - * matches the given node. |
|
| 172 | - * |
|
| 173 | - * @attention |
|
| 174 | - * Evaluation of selectors is done recursively. Thus the length |
|
| 175 | - * of the selector is limited to the recursion depth allowed by |
|
| 176 | - * the PHP configuration. This should only cause problems for |
|
| 177 | - * absolutely huge selectors or for versions of PHP tuned to |
|
| 178 | - * strictly limit recursion depth. |
|
| 179 | - * |
|
| 180 | - * @param DOMElement $node |
|
| 181 | - * The DOMNode to check. |
|
| 182 | - * @param $selector |
|
| 183 | - * |
|
| 184 | - * @return boolean |
|
| 185 | - * A boolean TRUE if the node matches, false otherwise. |
|
| 186 | - */ |
|
| 187 | - public function matchesSelector(DOMElement $node, $selector) |
|
| 188 | - { |
|
| 189 | - return $this->matchesSimpleSelector($node, $selector, 0); |
|
| 190 | - } |
|
| 191 | - |
|
| 192 | - /** |
|
| 193 | - * Performs a match check on a SimpleSelector. |
|
| 194 | - * |
|
| 195 | - * Where matchesSelector() does a check on an entire selector, |
|
| 196 | - * this checks only a simple selector (plus an optional |
|
| 197 | - * combinator). |
|
| 198 | - * |
|
| 199 | - * @param DOMElement $node |
|
| 200 | - * @param $selectors |
|
| 201 | - * @param $index |
|
| 202 | - * |
|
| 203 | - * @return boolean |
|
| 204 | - * A boolean TRUE if the node matches, false otherwise. |
|
| 205 | - * @throws NotImplementedException |
|
| 206 | - */ |
|
| 207 | - public function matchesSimpleSelector(DOMElement $node, $selectors, $index) |
|
| 208 | - { |
|
| 209 | - $selector = $selectors[$index]; |
|
| 210 | - // Note that this will short circuit as soon as one of these |
|
| 211 | - // returns FALSE. |
|
| 212 | - $result = $this->matchElement($node, $selector->element, $selector->ns) |
|
| 213 | - && $this->matchAttributes($node, $selector->attributes) |
|
| 214 | - && $this->matchId($node, $selector->id) |
|
| 215 | - && $this->matchClasses($node, $selector->classes) |
|
| 216 | - && $this->matchPseudoClasses($node, $selector->pseudoClasses) |
|
| 217 | - && $this->matchPseudoElements($node, $selector->pseudoElements); |
|
| 218 | - |
|
| 219 | - $isNextRule = isset($selectors[++$index]); |
|
| 220 | - // If there is another selector, we process that if there a match |
|
| 221 | - // hasn't been found. |
|
| 222 | - /* |
|
| 109 | + } |
|
| 110 | + |
|
| 111 | + public function debug($msg) |
|
| 112 | + { |
|
| 113 | + fwrite(STDOUT, PHP_EOL . $msg); |
|
| 114 | + } |
|
| 115 | + |
|
| 116 | + /** |
|
| 117 | + * Given a selector, find the matches in the given DOM. |
|
| 118 | + * |
|
| 119 | + * This is the main function for querying the DOM using a CSS |
|
| 120 | + * selector. |
|
| 121 | + * |
|
| 122 | + * @param string $selector |
|
| 123 | + * The selector. |
|
| 124 | + * |
|
| 125 | + * @return DOMTraverser a list of matched |
|
| 126 | + * DOMNode objects. |
|
| 127 | + * @throws ParseException |
|
| 128 | + */ |
|
| 129 | + public function find($selector): DOMTraverser |
|
| 130 | + { |
|
| 131 | + // Setup |
|
| 132 | + $handler = new Selector(); |
|
| 133 | + $parser = new Parser($selector, $handler); |
|
| 134 | + $parser->parse(); |
|
| 135 | + $this->selector = $handler; |
|
| 136 | + |
|
| 137 | + //$selector = $handler->toArray(); |
|
| 138 | + $found = $this->newMatches(); |
|
| 139 | + foreach ($handler as $selectorGroup) { |
|
| 140 | + // Initialize matches if necessary. |
|
| 141 | + if ($this->initialized) { |
|
| 142 | + $candidates = $this->matches; |
|
| 143 | + } else { |
|
| 144 | + $candidates = $this->initialMatch($selectorGroup[0], $this->matches); |
|
| 145 | + } |
|
| 146 | + |
|
| 147 | + /** @var DOMElement $candidate */ |
|
| 148 | + foreach ($candidates as $candidate) { |
|
| 149 | + // fprintf(STDOUT, "Testing %s against %s.\n", $candidate->tagName, $selectorGroup[0]); |
|
| 150 | + if ($this->matchesSelector($candidate, $selectorGroup)) { |
|
| 151 | + // $this->debug('Attaching ' . $candidate->nodeName); |
|
| 152 | + $found->attach($candidate); |
|
| 153 | + } |
|
| 154 | + } |
|
| 155 | + } |
|
| 156 | + $this->setMatches($found); |
|
| 157 | + |
|
| 158 | + return $this; |
|
| 159 | + } |
|
| 160 | + |
|
| 161 | + public function matches() |
|
| 162 | + { |
|
| 163 | + return $this->matches; |
|
| 164 | + } |
|
| 165 | + |
|
| 166 | + /** |
|
| 167 | + * Check whether the given node matches the given selector. |
|
| 168 | + * |
|
| 169 | + * A selector is a group of one or more simple selectors combined |
|
| 170 | + * by combinators. This determines if a given selector |
|
| 171 | + * matches the given node. |
|
| 172 | + * |
|
| 173 | + * @attention |
|
| 174 | + * Evaluation of selectors is done recursively. Thus the length |
|
| 175 | + * of the selector is limited to the recursion depth allowed by |
|
| 176 | + * the PHP configuration. This should only cause problems for |
|
| 177 | + * absolutely huge selectors or for versions of PHP tuned to |
|
| 178 | + * strictly limit recursion depth. |
|
| 179 | + * |
|
| 180 | + * @param DOMElement $node |
|
| 181 | + * The DOMNode to check. |
|
| 182 | + * @param $selector |
|
| 183 | + * |
|
| 184 | + * @return boolean |
|
| 185 | + * A boolean TRUE if the node matches, false otherwise. |
|
| 186 | + */ |
|
| 187 | + public function matchesSelector(DOMElement $node, $selector) |
|
| 188 | + { |
|
| 189 | + return $this->matchesSimpleSelector($node, $selector, 0); |
|
| 190 | + } |
|
| 191 | + |
|
| 192 | + /** |
|
| 193 | + * Performs a match check on a SimpleSelector. |
|
| 194 | + * |
|
| 195 | + * Where matchesSelector() does a check on an entire selector, |
|
| 196 | + * this checks only a simple selector (plus an optional |
|
| 197 | + * combinator). |
|
| 198 | + * |
|
| 199 | + * @param DOMElement $node |
|
| 200 | + * @param $selectors |
|
| 201 | + * @param $index |
|
| 202 | + * |
|
| 203 | + * @return boolean |
|
| 204 | + * A boolean TRUE if the node matches, false otherwise. |
|
| 205 | + * @throws NotImplementedException |
|
| 206 | + */ |
|
| 207 | + public function matchesSimpleSelector(DOMElement $node, $selectors, $index) |
|
| 208 | + { |
|
| 209 | + $selector = $selectors[$index]; |
|
| 210 | + // Note that this will short circuit as soon as one of these |
|
| 211 | + // returns FALSE. |
|
| 212 | + $result = $this->matchElement($node, $selector->element, $selector->ns) |
|
| 213 | + && $this->matchAttributes($node, $selector->attributes) |
|
| 214 | + && $this->matchId($node, $selector->id) |
|
| 215 | + && $this->matchClasses($node, $selector->classes) |
|
| 216 | + && $this->matchPseudoClasses($node, $selector->pseudoClasses) |
|
| 217 | + && $this->matchPseudoElements($node, $selector->pseudoElements); |
|
| 218 | + |
|
| 219 | + $isNextRule = isset($selectors[++$index]); |
|
| 220 | + // If there is another selector, we process that if there a match |
|
| 221 | + // hasn't been found. |
|
| 222 | + /* |
|
| 223 | 223 | if ($isNextRule && $selectors[$index]->combinator == SimpleSelector::anotherSelector) { |
| 224 | 224 | // We may need to re-initialize the match set for the next selector. |
| 225 | 225 | if (!$this->initialized) { |
@@ -231,457 +231,457 @@ discard block |
||
| 231 | 231 | // If we have a match and we have a combinator, we need to |
| 232 | 232 | // recurse up the tree. |
| 233 | 233 | else*/ |
| 234 | - if ($isNextRule && $result) { |
|
| 235 | - $result = $this->combine($node, $selectors, $index); |
|
| 236 | - } |
|
| 237 | - |
|
| 238 | - return $result; |
|
| 239 | - } |
|
| 240 | - |
|
| 241 | - /** |
|
| 242 | - * Combine the next selector with the given match |
|
| 243 | - * using the next combinator. |
|
| 244 | - * |
|
| 245 | - * If the next selector is combined with another |
|
| 246 | - * selector, that will be evaluated too, and so on. |
|
| 247 | - * So if this function returns TRUE, it means that all |
|
| 248 | - * child selectors are also matches. |
|
| 249 | - * |
|
| 250 | - * @param DOMNode $node |
|
| 251 | - * The DOMNode to test. |
|
| 252 | - * @param array $selectors |
|
| 253 | - * The array of simple selectors. |
|
| 254 | - * @param int $index |
|
| 255 | - * The index of the current selector. |
|
| 256 | - * |
|
| 257 | - * @return boolean |
|
| 258 | - * TRUE if the next selector(s) match. |
|
| 259 | - */ |
|
| 260 | - public function combine(DOMElement $node, $selectors, $index) |
|
| 261 | - { |
|
| 262 | - $selector = $selectors[$index]; |
|
| 263 | - //$this->debug(implode(' ', $selectors)); |
|
| 264 | - switch ($selector->combinator) { |
|
| 265 | - case SimpleSelector::ADJACENT: |
|
| 266 | - return $this->combineAdjacent($node, $selectors, $index); |
|
| 267 | - case SimpleSelector::SIBLING: |
|
| 268 | - return $this->combineSibling($node, $selectors, $index); |
|
| 269 | - case SimpleSelector::DIRECT_DESCENDANT: |
|
| 270 | - return $this->combineDirectDescendant($node, $selectors, $index); |
|
| 271 | - case SimpleSelector::ANY_DESCENDANT: |
|
| 272 | - return $this->combineAnyDescendant($node, $selectors, $index); |
|
| 273 | - case SimpleSelector::ANOTHER_SELECTOR: |
|
| 274 | - // fprintf(STDOUT, "Next selector: %s\n", $selectors[$index]); |
|
| 275 | - return $this->matchesSimpleSelector($node, $selectors, $index); |
|
| 276 | - } |
|
| 277 | - |
|
| 278 | - return false; |
|
| 279 | - } |
|
| 280 | - |
|
| 281 | - /** |
|
| 282 | - * Process an Adjacent Sibling. |
|
| 283 | - * |
|
| 284 | - * The spec does not indicate whether Adjacent should ignore non-Element |
|
| 285 | - * nodes, so we choose to ignore them. |
|
| 286 | - * |
|
| 287 | - * @param DOMNode $node |
|
| 288 | - * A DOM Node. |
|
| 289 | - * @param array $selectors |
|
| 290 | - * The selectors array. |
|
| 291 | - * @param int $index |
|
| 292 | - * The current index to the operative simple selector in the selectors |
|
| 293 | - * array. |
|
| 294 | - * |
|
| 295 | - * @return boolean |
|
| 296 | - * TRUE if the combination matches, FALSE otherwise. |
|
| 297 | - */ |
|
| 298 | - public function combineAdjacent($node, $selectors, $index) |
|
| 299 | - { |
|
| 300 | - while (! empty($node->previousSibling)) { |
|
| 301 | - $node = $node->previousSibling; |
|
| 302 | - if ($node->nodeType == XML_ELEMENT_NODE) { |
|
| 303 | - //$this->debug(sprintf('Testing %s against "%s"', $node->tagName, $selectors[$index])); |
|
| 304 | - return $this->matchesSimpleSelector($node, $selectors, $index); |
|
| 305 | - } |
|
| 306 | - } |
|
| 307 | - |
|
| 308 | - return false; |
|
| 309 | - } |
|
| 310 | - |
|
| 311 | - /** |
|
| 312 | - * Check all siblings. |
|
| 313 | - * |
|
| 314 | - * According to the spec, this only tests elements LEFT of the provided |
|
| 315 | - * node. |
|
| 316 | - * |
|
| 317 | - * @param DOMNode $node |
|
| 318 | - * A DOM Node. |
|
| 319 | - * @param array $selectors |
|
| 320 | - * The selectors array. |
|
| 321 | - * @param int $index |
|
| 322 | - * The current index to the operative simple selector in the selectors |
|
| 323 | - * array. |
|
| 324 | - * |
|
| 325 | - * @return boolean |
|
| 326 | - * TRUE if the combination matches, FALSE otherwise. |
|
| 327 | - */ |
|
| 328 | - public function combineSibling($node, $selectors, $index) |
|
| 329 | - { |
|
| 330 | - while (! empty($node->previousSibling)) { |
|
| 331 | - $node = $node->previousSibling; |
|
| 332 | - if ($node->nodeType == XML_ELEMENT_NODE && $this->matchesSimpleSelector($node, $selectors, $index)) { |
|
| 333 | - return true; |
|
| 334 | - } |
|
| 335 | - } |
|
| 336 | - |
|
| 337 | - return false; |
|
| 338 | - } |
|
| 339 | - |
|
| 340 | - /** |
|
| 341 | - * Handle a Direct Descendant combination. |
|
| 342 | - * |
|
| 343 | - * Check whether the given node is a rightly-related descendant |
|
| 344 | - * of its parent node. |
|
| 345 | - * |
|
| 346 | - * @param DOMNode $node |
|
| 347 | - * A DOM Node. |
|
| 348 | - * @param array $selectors |
|
| 349 | - * The selectors array. |
|
| 350 | - * @param int $index |
|
| 351 | - * The current index to the operative simple selector in the selectors |
|
| 352 | - * array. |
|
| 353 | - * |
|
| 354 | - * @return boolean |
|
| 355 | - * TRUE if the combination matches, FALSE otherwise. |
|
| 356 | - */ |
|
| 357 | - public function combineDirectDescendant($node, $selectors, $index) |
|
| 358 | - { |
|
| 359 | - $parent = $node->parentNode; |
|
| 360 | - if (empty($parent)) { |
|
| 361 | - return false; |
|
| 362 | - } |
|
| 363 | - |
|
| 364 | - return $this->matchesSimpleSelector($parent, $selectors, $index); |
|
| 365 | - } |
|
| 366 | - |
|
| 367 | - /** |
|
| 368 | - * Handle Any Descendant combinations. |
|
| 369 | - * |
|
| 370 | - * This checks to see if there are any matching routes from the |
|
| 371 | - * selector beginning at the present node. |
|
| 372 | - * |
|
| 373 | - * @param DOMNode $node |
|
| 374 | - * A DOM Node. |
|
| 375 | - * @param array $selectors |
|
| 376 | - * The selectors array. |
|
| 377 | - * @param int $index |
|
| 378 | - * The current index to the operative simple selector in the selectors |
|
| 379 | - * array. |
|
| 380 | - * |
|
| 381 | - * @return boolean |
|
| 382 | - * TRUE if the combination matches, FALSE otherwise. |
|
| 383 | - */ |
|
| 384 | - public function combineAnyDescendant($node, $selectors, $index) |
|
| 385 | - { |
|
| 386 | - while (! empty($node->parentNode)) { |
|
| 387 | - $node = $node->parentNode; |
|
| 388 | - |
|
| 389 | - // Catch case where element is child of something |
|
| 390 | - // else. This should really only happen with a |
|
| 391 | - // document element. |
|
| 392 | - if ($node->nodeType != XML_ELEMENT_NODE) { |
|
| 393 | - continue; |
|
| 394 | - } |
|
| 395 | - |
|
| 396 | - if ($this->matchesSimpleSelector($node, $selectors, $index)) { |
|
| 397 | - return true; |
|
| 398 | - } |
|
| 399 | - } |
|
| 400 | - } |
|
| 401 | - |
|
| 402 | - /** |
|
| 403 | - * Get the intial match set. |
|
| 404 | - * |
|
| 405 | - * This should only be executed when not working with |
|
| 406 | - * an existing match set. |
|
| 407 | - * |
|
| 408 | - * @param \QueryPath\CSS\SimpleSelector $selector |
|
| 409 | - * @param SplObjectStorage $matches |
|
| 410 | - * |
|
| 411 | - * @return SplObjectStorage |
|
| 412 | - */ |
|
| 413 | - protected function initialMatch(SimpleSelector $selector, SplObjectStorage $matches): SplObjectStorage |
|
| 414 | - { |
|
| 415 | - $element = $selector->element; |
|
| 416 | - |
|
| 417 | - // If no element is specified, we have to start with the |
|
| 418 | - // entire document. |
|
| 419 | - if ($element === null) { |
|
| 420 | - $element = '*'; |
|
| 421 | - } |
|
| 422 | - |
|
| 423 | - // We try to do some optimization here to reduce the |
|
| 424 | - // number of matches to the bare minimum. This will |
|
| 425 | - // reduce the subsequent number of operations that |
|
| 426 | - // must be performed in the query. |
|
| 427 | - |
|
| 428 | - // Experimental: ID queries use XPath to match, since |
|
| 429 | - // this should give us only a single matched element |
|
| 430 | - // to work with. |
|
| 431 | - if (/*$element == '*' &&*/ |
|
| 432 | - ! empty($selector->id)) { |
|
| 433 | - $initialMatches = $this->initialMatchOnID($selector, $matches); |
|
| 434 | - } // If a namespace is set, find the namespace matches. |
|
| 435 | - elseif (! empty($selector->ns)) { |
|
| 436 | - $initialMatches = $this->initialMatchOnElementNS($selector, $matches); |
|
| 437 | - } |
|
| 438 | - // If the element is a wildcard, using class can |
|
| 439 | - // substantially reduce the number of elements that |
|
| 440 | - // we start with. |
|
| 441 | - elseif ($element === '*' && ! empty($selector->classes)) { |
|
| 442 | - $initialMatches = $this->initialMatchOnClasses($selector, $matches); |
|
| 443 | - } else { |
|
| 444 | - $initialMatches = $this->initialMatchOnElement($selector, $matches); |
|
| 445 | - } |
|
| 446 | - |
|
| 447 | - return $initialMatches; |
|
| 448 | - } |
|
| 449 | - |
|
| 450 | - /** |
|
| 451 | - * Shortcut for finding initial match by ID. |
|
| 452 | - * |
|
| 453 | - * If the element is set to '*' and an ID is |
|
| 454 | - * set, then this should be used to find by ID, |
|
| 455 | - * which will drastically reduce the amount of |
|
| 456 | - * comparison operations done in PHP. |
|
| 457 | - * |
|
| 458 | - * @param \QueryPath\CSS\SimpleSelector $selector |
|
| 459 | - * @param SplObjectStorage $matches |
|
| 460 | - * |
|
| 461 | - * @return SplObjectStorage |
|
| 462 | - */ |
|
| 463 | - protected function initialMatchOnID(SimpleSelector $selector, SplObjectStorage $matches): SplObjectStorage |
|
| 464 | - { |
|
| 465 | - $id = $selector->id; |
|
| 466 | - $found = $this->newMatches(); |
|
| 467 | - |
|
| 468 | - // Issue #145: DOMXPath will through an exception if the DOM is |
|
| 469 | - // not set. |
|
| 470 | - if (! ($this->dom instanceof DOMDocument)) { |
|
| 471 | - return $found; |
|
| 472 | - } |
|
| 473 | - $baseQuery = ".//*[@id='{$id}']"; |
|
| 474 | - $xpath = new DOMXPath($this->dom); |
|
| 475 | - |
|
| 476 | - // Now we try to find any matching IDs. |
|
| 477 | - /** @var DOMElement $node */ |
|
| 478 | - foreach ($matches as $node) { |
|
| 479 | - if ($node->getAttribute('id') === $id) { |
|
| 480 | - $found->attach($node); |
|
| 481 | - } |
|
| 482 | - |
|
| 483 | - $nl = $this->initialXpathQuery($xpath, $node, $baseQuery); |
|
| 484 | - if (! empty($nl) && $nl instanceof DOMNodeList) { |
|
| 485 | - $this->attachNodeList($nl, $found); |
|
| 486 | - } |
|
| 487 | - } |
|
| 488 | - // Unset the ID selector. |
|
| 489 | - $selector->id = null; |
|
| 490 | - |
|
| 491 | - return $found; |
|
| 492 | - } |
|
| 493 | - |
|
| 494 | - /** |
|
| 495 | - * Shortcut for setting the intial match. |
|
| 496 | - * |
|
| 497 | - * This shortcut should only be used when the initial |
|
| 498 | - * element is '*' and there are classes set. |
|
| 499 | - * |
|
| 500 | - * In any other case, the element finding algo is |
|
| 501 | - * faster and should be used instead. |
|
| 502 | - * |
|
| 503 | - * @param \QueryPath\CSS\SimpleSelector $selector |
|
| 504 | - * @param $matches |
|
| 505 | - * |
|
| 506 | - * @return SplObjectStorage |
|
| 507 | - */ |
|
| 508 | - protected function initialMatchOnClasses(SimpleSelector $selector, SplObjectStorage $matches): SplObjectStorage |
|
| 509 | - { |
|
| 510 | - $found = $this->newMatches(); |
|
| 511 | - |
|
| 512 | - // Issue #145: DOMXPath will through an exception if the DOM is |
|
| 513 | - // not set. |
|
| 514 | - if (! ($this->dom instanceof DOMDocument)) { |
|
| 515 | - return $found; |
|
| 516 | - } |
|
| 517 | - $baseQuery = './/*[@class]'; |
|
| 518 | - $xpath = new DOMXPath($this->dom); |
|
| 519 | - |
|
| 520 | - // Now we try to find any matching IDs. |
|
| 521 | - /** @var DOMElement $node */ |
|
| 522 | - foreach ($matches as $node) { |
|
| 523 | - // Refactor me! |
|
| 524 | - if ($node->hasAttribute('class')) { |
|
| 525 | - $intersect = array_intersect($selector->classes, explode(' ', $node->getAttribute('class'))); |
|
| 526 | - if (count($intersect) === count($selector->classes)) { |
|
| 527 | - $found->attach($node); |
|
| 528 | - } |
|
| 529 | - } |
|
| 530 | - |
|
| 531 | - $nl = $this->initialXpathQuery($xpath, $node, $baseQuery); |
|
| 532 | - /** @var DOMElement $subNode */ |
|
| 533 | - foreach ($nl as $subNode) { |
|
| 534 | - $classes = $subNode->getAttribute('class'); |
|
| 535 | - $classArray = explode(' ', $classes); |
|
| 536 | - |
|
| 537 | - $intersect = array_intersect($selector->classes, $classArray); |
|
| 538 | - if (count($intersect) === count($selector->classes)) { |
|
| 539 | - $found->attach($subNode); |
|
| 540 | - } |
|
| 541 | - } |
|
| 542 | - } |
|
| 543 | - |
|
| 544 | - // Unset the classes selector. |
|
| 545 | - $selector->classes = []; |
|
| 546 | - |
|
| 547 | - return $found; |
|
| 548 | - } |
|
| 549 | - |
|
| 550 | - /** |
|
| 551 | - * Internal xpath query. |
|
| 552 | - * |
|
| 553 | - * This is optimized for very specific use, and is not a general |
|
| 554 | - * purpose function. |
|
| 555 | - * |
|
| 556 | - * @param DOMXPath $xpath |
|
| 557 | - * @param DOMElement $node |
|
| 558 | - * @param string $query |
|
| 559 | - * |
|
| 560 | - * @return DOMNodeList |
|
| 561 | - */ |
|
| 562 | - private function initialXpathQuery(DOMXPath $xpath, DOMElement $node, string $query): DOMNodeList |
|
| 563 | - { |
|
| 564 | - // This works around a bug in which the document element |
|
| 565 | - // does not correctly search with the $baseQuery. |
|
| 566 | - if ($node->isSameNode($this->dom->documentElement)) { |
|
| 567 | - $query = mb_substr($query, 1); |
|
| 568 | - } |
|
| 569 | - |
|
| 570 | - return $xpath->query($query, $node); |
|
| 571 | - } |
|
| 572 | - |
|
| 573 | - /** |
|
| 574 | - * Shortcut for setting the initial match. |
|
| 575 | - * |
|
| 576 | - * @param $selector |
|
| 577 | - * @param $matches |
|
| 578 | - * |
|
| 579 | - * @return SplObjectStorage |
|
| 580 | - */ |
|
| 581 | - protected function initialMatchOnElement(SimpleSelector $selector, SplObjectStorage $matches): SplObjectStorage |
|
| 582 | - { |
|
| 583 | - $element = $selector->element; |
|
| 584 | - if (null === $element) { |
|
| 585 | - $element = '*'; |
|
| 586 | - } |
|
| 587 | - $found = $this->newMatches(); |
|
| 588 | - /** @var DOMDocument $node */ |
|
| 589 | - foreach ($matches as $node) { |
|
| 590 | - // Capture the case where the initial element is the root element. |
|
| 591 | - if ($node->tagName === $element |
|
| 592 | - || ($element === '*' && $node->parentNode instanceof DOMDocument)) { |
|
| 593 | - $found->attach($node); |
|
| 594 | - } |
|
| 595 | - $nl = $node->getElementsByTagName($element); |
|
| 596 | - if (! empty($nl) && $nl instanceof DOMNodeList) { |
|
| 597 | - $this->attachNodeList($nl, $found); |
|
| 598 | - } |
|
| 599 | - } |
|
| 600 | - |
|
| 601 | - $selector->element = null; |
|
| 602 | - |
|
| 603 | - return $found; |
|
| 604 | - } |
|
| 605 | - |
|
| 606 | - /** |
|
| 607 | - * Get elements and filter by namespace. |
|
| 608 | - * |
|
| 609 | - * @param \QueryPath\CSS\SimpleSelector $selector |
|
| 610 | - * @param SplObjectStorage $matches |
|
| 611 | - * |
|
| 612 | - * @return SplObjectStorage |
|
| 613 | - */ |
|
| 614 | - protected function initialMatchOnElementNS(SimpleSelector $selector, SplObjectStorage $matches): SplObjectStorage |
|
| 615 | - { |
|
| 616 | - $ns = $selector->ns; |
|
| 617 | - |
|
| 618 | - $elements = $this->initialMatchOnElement($selector, $matches); |
|
| 619 | - |
|
| 620 | - // "any namespace" matches anything. |
|
| 621 | - if ($ns === '*') { |
|
| 622 | - return $elements; |
|
| 623 | - } |
|
| 624 | - |
|
| 625 | - // Loop through and make a list of items that need to be filtered |
|
| 626 | - // out, then filter them. This is required b/c ObjectStorage iterates |
|
| 627 | - // wrongly when an item is detached in an access loop. |
|
| 628 | - $detach = []; |
|
| 629 | - foreach ($elements as $node) { |
|
| 630 | - // This lookup must be done PER NODE. |
|
| 631 | - $nsuri = $node->lookupNamespaceURI($ns); |
|
| 632 | - if (empty($nsuri) || $node->namespaceURI !== $nsuri) { |
|
| 633 | - $detach[] = $node; |
|
| 634 | - } |
|
| 635 | - } |
|
| 636 | - foreach ($detach as $rem) { |
|
| 637 | - $elements->detach($rem); |
|
| 638 | - } |
|
| 639 | - $selector->ns = null; |
|
| 640 | - |
|
| 641 | - return $elements; |
|
| 642 | - } |
|
| 643 | - |
|
| 644 | - /** |
|
| 645 | - * Checks to see if the DOMNode matches the given element selector. |
|
| 646 | - * |
|
| 647 | - * This handles the following cases: |
|
| 648 | - * |
|
| 649 | - * - element (foo) |
|
| 650 | - * - namespaced element (ns|foo) |
|
| 651 | - * - namespaced wildcard (ns|*) |
|
| 652 | - * - wildcard (* or *|*) |
|
| 653 | - * |
|
| 654 | - * @param DOMElement $node |
|
| 655 | - * @param $element |
|
| 656 | - * @param null $ns |
|
| 657 | - * |
|
| 658 | - * @return bool |
|
| 659 | - */ |
|
| 660 | - protected function matchElement(DOMElement $node, $element, $ns = null): bool |
|
| 661 | - { |
|
| 662 | - if (empty($element)) { |
|
| 663 | - return true; |
|
| 664 | - } |
|
| 665 | - |
|
| 666 | - // Handle namespace. |
|
| 667 | - if (! empty($ns) && $ns !== '*') { |
|
| 668 | - // Check whether we have a matching NS URI. |
|
| 669 | - $nsuri = $node->lookupNamespaceURI($ns); |
|
| 670 | - if (empty($nsuri) || $node->namespaceURI !== $nsuri) { |
|
| 671 | - return false; |
|
| 672 | - } |
|
| 673 | - } |
|
| 674 | - |
|
| 675 | - // Compare local name to given element name. |
|
| 676 | - return $element === '*' || $node->localName === $element; |
|
| 677 | - } |
|
| 678 | - |
|
| 679 | - /** |
|
| 680 | - * Checks to see if the given DOMNode matches an "any element" (*). |
|
| 681 | - * |
|
| 682 | - * This does not handle namespaced whildcards. |
|
| 683 | - */ |
|
| 684 | - /* |
|
| 234 | + if ($isNextRule && $result) { |
|
| 235 | + $result = $this->combine($node, $selectors, $index); |
|
| 236 | + } |
|
| 237 | + |
|
| 238 | + return $result; |
|
| 239 | + } |
|
| 240 | + |
|
| 241 | + /** |
|
| 242 | + * Combine the next selector with the given match |
|
| 243 | + * using the next combinator. |
|
| 244 | + * |
|
| 245 | + * If the next selector is combined with another |
|
| 246 | + * selector, that will be evaluated too, and so on. |
|
| 247 | + * So if this function returns TRUE, it means that all |
|
| 248 | + * child selectors are also matches. |
|
| 249 | + * |
|
| 250 | + * @param DOMNode $node |
|
| 251 | + * The DOMNode to test. |
|
| 252 | + * @param array $selectors |
|
| 253 | + * The array of simple selectors. |
|
| 254 | + * @param int $index |
|
| 255 | + * The index of the current selector. |
|
| 256 | + * |
|
| 257 | + * @return boolean |
|
| 258 | + * TRUE if the next selector(s) match. |
|
| 259 | + */ |
|
| 260 | + public function combine(DOMElement $node, $selectors, $index) |
|
| 261 | + { |
|
| 262 | + $selector = $selectors[$index]; |
|
| 263 | + //$this->debug(implode(' ', $selectors)); |
|
| 264 | + switch ($selector->combinator) { |
|
| 265 | + case SimpleSelector::ADJACENT: |
|
| 266 | + return $this->combineAdjacent($node, $selectors, $index); |
|
| 267 | + case SimpleSelector::SIBLING: |
|
| 268 | + return $this->combineSibling($node, $selectors, $index); |
|
| 269 | + case SimpleSelector::DIRECT_DESCENDANT: |
|
| 270 | + return $this->combineDirectDescendant($node, $selectors, $index); |
|
| 271 | + case SimpleSelector::ANY_DESCENDANT: |
|
| 272 | + return $this->combineAnyDescendant($node, $selectors, $index); |
|
| 273 | + case SimpleSelector::ANOTHER_SELECTOR: |
|
| 274 | + // fprintf(STDOUT, "Next selector: %s\n", $selectors[$index]); |
|
| 275 | + return $this->matchesSimpleSelector($node, $selectors, $index); |
|
| 276 | + } |
|
| 277 | + |
|
| 278 | + return false; |
|
| 279 | + } |
|
| 280 | + |
|
| 281 | + /** |
|
| 282 | + * Process an Adjacent Sibling. |
|
| 283 | + * |
|
| 284 | + * The spec does not indicate whether Adjacent should ignore non-Element |
|
| 285 | + * nodes, so we choose to ignore them. |
|
| 286 | + * |
|
| 287 | + * @param DOMNode $node |
|
| 288 | + * A DOM Node. |
|
| 289 | + * @param array $selectors |
|
| 290 | + * The selectors array. |
|
| 291 | + * @param int $index |
|
| 292 | + * The current index to the operative simple selector in the selectors |
|
| 293 | + * array. |
|
| 294 | + * |
|
| 295 | + * @return boolean |
|
| 296 | + * TRUE if the combination matches, FALSE otherwise. |
|
| 297 | + */ |
|
| 298 | + public function combineAdjacent($node, $selectors, $index) |
|
| 299 | + { |
|
| 300 | + while (! empty($node->previousSibling)) { |
|
| 301 | + $node = $node->previousSibling; |
|
| 302 | + if ($node->nodeType == XML_ELEMENT_NODE) { |
|
| 303 | + //$this->debug(sprintf('Testing %s against "%s"', $node->tagName, $selectors[$index])); |
|
| 304 | + return $this->matchesSimpleSelector($node, $selectors, $index); |
|
| 305 | + } |
|
| 306 | + } |
|
| 307 | + |
|
| 308 | + return false; |
|
| 309 | + } |
|
| 310 | + |
|
| 311 | + /** |
|
| 312 | + * Check all siblings. |
|
| 313 | + * |
|
| 314 | + * According to the spec, this only tests elements LEFT of the provided |
|
| 315 | + * node. |
|
| 316 | + * |
|
| 317 | + * @param DOMNode $node |
|
| 318 | + * A DOM Node. |
|
| 319 | + * @param array $selectors |
|
| 320 | + * The selectors array. |
|
| 321 | + * @param int $index |
|
| 322 | + * The current index to the operative simple selector in the selectors |
|
| 323 | + * array. |
|
| 324 | + * |
|
| 325 | + * @return boolean |
|
| 326 | + * TRUE if the combination matches, FALSE otherwise. |
|
| 327 | + */ |
|
| 328 | + public function combineSibling($node, $selectors, $index) |
|
| 329 | + { |
|
| 330 | + while (! empty($node->previousSibling)) { |
|
| 331 | + $node = $node->previousSibling; |
|
| 332 | + if ($node->nodeType == XML_ELEMENT_NODE && $this->matchesSimpleSelector($node, $selectors, $index)) { |
|
| 333 | + return true; |
|
| 334 | + } |
|
| 335 | + } |
|
| 336 | + |
|
| 337 | + return false; |
|
| 338 | + } |
|
| 339 | + |
|
| 340 | + /** |
|
| 341 | + * Handle a Direct Descendant combination. |
|
| 342 | + * |
|
| 343 | + * Check whether the given node is a rightly-related descendant |
|
| 344 | + * of its parent node. |
|
| 345 | + * |
|
| 346 | + * @param DOMNode $node |
|
| 347 | + * A DOM Node. |
|
| 348 | + * @param array $selectors |
|
| 349 | + * The selectors array. |
|
| 350 | + * @param int $index |
|
| 351 | + * The current index to the operative simple selector in the selectors |
|
| 352 | + * array. |
|
| 353 | + * |
|
| 354 | + * @return boolean |
|
| 355 | + * TRUE if the combination matches, FALSE otherwise. |
|
| 356 | + */ |
|
| 357 | + public function combineDirectDescendant($node, $selectors, $index) |
|
| 358 | + { |
|
| 359 | + $parent = $node->parentNode; |
|
| 360 | + if (empty($parent)) { |
|
| 361 | + return false; |
|
| 362 | + } |
|
| 363 | + |
|
| 364 | + return $this->matchesSimpleSelector($parent, $selectors, $index); |
|
| 365 | + } |
|
| 366 | + |
|
| 367 | + /** |
|
| 368 | + * Handle Any Descendant combinations. |
|
| 369 | + * |
|
| 370 | + * This checks to see if there are any matching routes from the |
|
| 371 | + * selector beginning at the present node. |
|
| 372 | + * |
|
| 373 | + * @param DOMNode $node |
|
| 374 | + * A DOM Node. |
|
| 375 | + * @param array $selectors |
|
| 376 | + * The selectors array. |
|
| 377 | + * @param int $index |
|
| 378 | + * The current index to the operative simple selector in the selectors |
|
| 379 | + * array. |
|
| 380 | + * |
|
| 381 | + * @return boolean |
|
| 382 | + * TRUE if the combination matches, FALSE otherwise. |
|
| 383 | + */ |
|
| 384 | + public function combineAnyDescendant($node, $selectors, $index) |
|
| 385 | + { |
|
| 386 | + while (! empty($node->parentNode)) { |
|
| 387 | + $node = $node->parentNode; |
|
| 388 | + |
|
| 389 | + // Catch case where element is child of something |
|
| 390 | + // else. This should really only happen with a |
|
| 391 | + // document element. |
|
| 392 | + if ($node->nodeType != XML_ELEMENT_NODE) { |
|
| 393 | + continue; |
|
| 394 | + } |
|
| 395 | + |
|
| 396 | + if ($this->matchesSimpleSelector($node, $selectors, $index)) { |
|
| 397 | + return true; |
|
| 398 | + } |
|
| 399 | + } |
|
| 400 | + } |
|
| 401 | + |
|
| 402 | + /** |
|
| 403 | + * Get the intial match set. |
|
| 404 | + * |
|
| 405 | + * This should only be executed when not working with |
|
| 406 | + * an existing match set. |
|
| 407 | + * |
|
| 408 | + * @param \QueryPath\CSS\SimpleSelector $selector |
|
| 409 | + * @param SplObjectStorage $matches |
|
| 410 | + * |
|
| 411 | + * @return SplObjectStorage |
|
| 412 | + */ |
|
| 413 | + protected function initialMatch(SimpleSelector $selector, SplObjectStorage $matches): SplObjectStorage |
|
| 414 | + { |
|
| 415 | + $element = $selector->element; |
|
| 416 | + |
|
| 417 | + // If no element is specified, we have to start with the |
|
| 418 | + // entire document. |
|
| 419 | + if ($element === null) { |
|
| 420 | + $element = '*'; |
|
| 421 | + } |
|
| 422 | + |
|
| 423 | + // We try to do some optimization here to reduce the |
|
| 424 | + // number of matches to the bare minimum. This will |
|
| 425 | + // reduce the subsequent number of operations that |
|
| 426 | + // must be performed in the query. |
|
| 427 | + |
|
| 428 | + // Experimental: ID queries use XPath to match, since |
|
| 429 | + // this should give us only a single matched element |
|
| 430 | + // to work with. |
|
| 431 | + if (/*$element == '*' &&*/ |
|
| 432 | + ! empty($selector->id)) { |
|
| 433 | + $initialMatches = $this->initialMatchOnID($selector, $matches); |
|
| 434 | + } // If a namespace is set, find the namespace matches. |
|
| 435 | + elseif (! empty($selector->ns)) { |
|
| 436 | + $initialMatches = $this->initialMatchOnElementNS($selector, $matches); |
|
| 437 | + } |
|
| 438 | + // If the element is a wildcard, using class can |
|
| 439 | + // substantially reduce the number of elements that |
|
| 440 | + // we start with. |
|
| 441 | + elseif ($element === '*' && ! empty($selector->classes)) { |
|
| 442 | + $initialMatches = $this->initialMatchOnClasses($selector, $matches); |
|
| 443 | + } else { |
|
| 444 | + $initialMatches = $this->initialMatchOnElement($selector, $matches); |
|
| 445 | + } |
|
| 446 | + |
|
| 447 | + return $initialMatches; |
|
| 448 | + } |
|
| 449 | + |
|
| 450 | + /** |
|
| 451 | + * Shortcut for finding initial match by ID. |
|
| 452 | + * |
|
| 453 | + * If the element is set to '*' and an ID is |
|
| 454 | + * set, then this should be used to find by ID, |
|
| 455 | + * which will drastically reduce the amount of |
|
| 456 | + * comparison operations done in PHP. |
|
| 457 | + * |
|
| 458 | + * @param \QueryPath\CSS\SimpleSelector $selector |
|
| 459 | + * @param SplObjectStorage $matches |
|
| 460 | + * |
|
| 461 | + * @return SplObjectStorage |
|
| 462 | + */ |
|
| 463 | + protected function initialMatchOnID(SimpleSelector $selector, SplObjectStorage $matches): SplObjectStorage |
|
| 464 | + { |
|
| 465 | + $id = $selector->id; |
|
| 466 | + $found = $this->newMatches(); |
|
| 467 | + |
|
| 468 | + // Issue #145: DOMXPath will through an exception if the DOM is |
|
| 469 | + // not set. |
|
| 470 | + if (! ($this->dom instanceof DOMDocument)) { |
|
| 471 | + return $found; |
|
| 472 | + } |
|
| 473 | + $baseQuery = ".//*[@id='{$id}']"; |
|
| 474 | + $xpath = new DOMXPath($this->dom); |
|
| 475 | + |
|
| 476 | + // Now we try to find any matching IDs. |
|
| 477 | + /** @var DOMElement $node */ |
|
| 478 | + foreach ($matches as $node) { |
|
| 479 | + if ($node->getAttribute('id') === $id) { |
|
| 480 | + $found->attach($node); |
|
| 481 | + } |
|
| 482 | + |
|
| 483 | + $nl = $this->initialXpathQuery($xpath, $node, $baseQuery); |
|
| 484 | + if (! empty($nl) && $nl instanceof DOMNodeList) { |
|
| 485 | + $this->attachNodeList($nl, $found); |
|
| 486 | + } |
|
| 487 | + } |
|
| 488 | + // Unset the ID selector. |
|
| 489 | + $selector->id = null; |
|
| 490 | + |
|
| 491 | + return $found; |
|
| 492 | + } |
|
| 493 | + |
|
| 494 | + /** |
|
| 495 | + * Shortcut for setting the intial match. |
|
| 496 | + * |
|
| 497 | + * This shortcut should only be used when the initial |
|
| 498 | + * element is '*' and there are classes set. |
|
| 499 | + * |
|
| 500 | + * In any other case, the element finding algo is |
|
| 501 | + * faster and should be used instead. |
|
| 502 | + * |
|
| 503 | + * @param \QueryPath\CSS\SimpleSelector $selector |
|
| 504 | + * @param $matches |
|
| 505 | + * |
|
| 506 | + * @return SplObjectStorage |
|
| 507 | + */ |
|
| 508 | + protected function initialMatchOnClasses(SimpleSelector $selector, SplObjectStorage $matches): SplObjectStorage |
|
| 509 | + { |
|
| 510 | + $found = $this->newMatches(); |
|
| 511 | + |
|
| 512 | + // Issue #145: DOMXPath will through an exception if the DOM is |
|
| 513 | + // not set. |
|
| 514 | + if (! ($this->dom instanceof DOMDocument)) { |
|
| 515 | + return $found; |
|
| 516 | + } |
|
| 517 | + $baseQuery = './/*[@class]'; |
|
| 518 | + $xpath = new DOMXPath($this->dom); |
|
| 519 | + |
|
| 520 | + // Now we try to find any matching IDs. |
|
| 521 | + /** @var DOMElement $node */ |
|
| 522 | + foreach ($matches as $node) { |
|
| 523 | + // Refactor me! |
|
| 524 | + if ($node->hasAttribute('class')) { |
|
| 525 | + $intersect = array_intersect($selector->classes, explode(' ', $node->getAttribute('class'))); |
|
| 526 | + if (count($intersect) === count($selector->classes)) { |
|
| 527 | + $found->attach($node); |
|
| 528 | + } |
|
| 529 | + } |
|
| 530 | + |
|
| 531 | + $nl = $this->initialXpathQuery($xpath, $node, $baseQuery); |
|
| 532 | + /** @var DOMElement $subNode */ |
|
| 533 | + foreach ($nl as $subNode) { |
|
| 534 | + $classes = $subNode->getAttribute('class'); |
|
| 535 | + $classArray = explode(' ', $classes); |
|
| 536 | + |
|
| 537 | + $intersect = array_intersect($selector->classes, $classArray); |
|
| 538 | + if (count($intersect) === count($selector->classes)) { |
|
| 539 | + $found->attach($subNode); |
|
| 540 | + } |
|
| 541 | + } |
|
| 542 | + } |
|
| 543 | + |
|
| 544 | + // Unset the classes selector. |
|
| 545 | + $selector->classes = []; |
|
| 546 | + |
|
| 547 | + return $found; |
|
| 548 | + } |
|
| 549 | + |
|
| 550 | + /** |
|
| 551 | + * Internal xpath query. |
|
| 552 | + * |
|
| 553 | + * This is optimized for very specific use, and is not a general |
|
| 554 | + * purpose function. |
|
| 555 | + * |
|
| 556 | + * @param DOMXPath $xpath |
|
| 557 | + * @param DOMElement $node |
|
| 558 | + * @param string $query |
|
| 559 | + * |
|
| 560 | + * @return DOMNodeList |
|
| 561 | + */ |
|
| 562 | + private function initialXpathQuery(DOMXPath $xpath, DOMElement $node, string $query): DOMNodeList |
|
| 563 | + { |
|
| 564 | + // This works around a bug in which the document element |
|
| 565 | + // does not correctly search with the $baseQuery. |
|
| 566 | + if ($node->isSameNode($this->dom->documentElement)) { |
|
| 567 | + $query = mb_substr($query, 1); |
|
| 568 | + } |
|
| 569 | + |
|
| 570 | + return $xpath->query($query, $node); |
|
| 571 | + } |
|
| 572 | + |
|
| 573 | + /** |
|
| 574 | + * Shortcut for setting the initial match. |
|
| 575 | + * |
|
| 576 | + * @param $selector |
|
| 577 | + * @param $matches |
|
| 578 | + * |
|
| 579 | + * @return SplObjectStorage |
|
| 580 | + */ |
|
| 581 | + protected function initialMatchOnElement(SimpleSelector $selector, SplObjectStorage $matches): SplObjectStorage |
|
| 582 | + { |
|
| 583 | + $element = $selector->element; |
|
| 584 | + if (null === $element) { |
|
| 585 | + $element = '*'; |
|
| 586 | + } |
|
| 587 | + $found = $this->newMatches(); |
|
| 588 | + /** @var DOMDocument $node */ |
|
| 589 | + foreach ($matches as $node) { |
|
| 590 | + // Capture the case where the initial element is the root element. |
|
| 591 | + if ($node->tagName === $element |
|
| 592 | + || ($element === '*' && $node->parentNode instanceof DOMDocument)) { |
|
| 593 | + $found->attach($node); |
|
| 594 | + } |
|
| 595 | + $nl = $node->getElementsByTagName($element); |
|
| 596 | + if (! empty($nl) && $nl instanceof DOMNodeList) { |
|
| 597 | + $this->attachNodeList($nl, $found); |
|
| 598 | + } |
|
| 599 | + } |
|
| 600 | + |
|
| 601 | + $selector->element = null; |
|
| 602 | + |
|
| 603 | + return $found; |
|
| 604 | + } |
|
| 605 | + |
|
| 606 | + /** |
|
| 607 | + * Get elements and filter by namespace. |
|
| 608 | + * |
|
| 609 | + * @param \QueryPath\CSS\SimpleSelector $selector |
|
| 610 | + * @param SplObjectStorage $matches |
|
| 611 | + * |
|
| 612 | + * @return SplObjectStorage |
|
| 613 | + */ |
|
| 614 | + protected function initialMatchOnElementNS(SimpleSelector $selector, SplObjectStorage $matches): SplObjectStorage |
|
| 615 | + { |
|
| 616 | + $ns = $selector->ns; |
|
| 617 | + |
|
| 618 | + $elements = $this->initialMatchOnElement($selector, $matches); |
|
| 619 | + |
|
| 620 | + // "any namespace" matches anything. |
|
| 621 | + if ($ns === '*') { |
|
| 622 | + return $elements; |
|
| 623 | + } |
|
| 624 | + |
|
| 625 | + // Loop through and make a list of items that need to be filtered |
|
| 626 | + // out, then filter them. This is required b/c ObjectStorage iterates |
|
| 627 | + // wrongly when an item is detached in an access loop. |
|
| 628 | + $detach = []; |
|
| 629 | + foreach ($elements as $node) { |
|
| 630 | + // This lookup must be done PER NODE. |
|
| 631 | + $nsuri = $node->lookupNamespaceURI($ns); |
|
| 632 | + if (empty($nsuri) || $node->namespaceURI !== $nsuri) { |
|
| 633 | + $detach[] = $node; |
|
| 634 | + } |
|
| 635 | + } |
|
| 636 | + foreach ($detach as $rem) { |
|
| 637 | + $elements->detach($rem); |
|
| 638 | + } |
|
| 639 | + $selector->ns = null; |
|
| 640 | + |
|
| 641 | + return $elements; |
|
| 642 | + } |
|
| 643 | + |
|
| 644 | + /** |
|
| 645 | + * Checks to see if the DOMNode matches the given element selector. |
|
| 646 | + * |
|
| 647 | + * This handles the following cases: |
|
| 648 | + * |
|
| 649 | + * - element (foo) |
|
| 650 | + * - namespaced element (ns|foo) |
|
| 651 | + * - namespaced wildcard (ns|*) |
|
| 652 | + * - wildcard (* or *|*) |
|
| 653 | + * |
|
| 654 | + * @param DOMElement $node |
|
| 655 | + * @param $element |
|
| 656 | + * @param null $ns |
|
| 657 | + * |
|
| 658 | + * @return bool |
|
| 659 | + */ |
|
| 660 | + protected function matchElement(DOMElement $node, $element, $ns = null): bool |
|
| 661 | + { |
|
| 662 | + if (empty($element)) { |
|
| 663 | + return true; |
|
| 664 | + } |
|
| 665 | + |
|
| 666 | + // Handle namespace. |
|
| 667 | + if (! empty($ns) && $ns !== '*') { |
|
| 668 | + // Check whether we have a matching NS URI. |
|
| 669 | + $nsuri = $node->lookupNamespaceURI($ns); |
|
| 670 | + if (empty($nsuri) || $node->namespaceURI !== $nsuri) { |
|
| 671 | + return false; |
|
| 672 | + } |
|
| 673 | + } |
|
| 674 | + |
|
| 675 | + // Compare local name to given element name. |
|
| 676 | + return $element === '*' || $node->localName === $element; |
|
| 677 | + } |
|
| 678 | + |
|
| 679 | + /** |
|
| 680 | + * Checks to see if the given DOMNode matches an "any element" (*). |
|
| 681 | + * |
|
| 682 | + * This does not handle namespaced whildcards. |
|
| 683 | + */ |
|
| 684 | + /* |
|
| 685 | 685 | protected function matchAnyElement($node) { |
| 686 | 686 | $ancestors = $this->ancestors($node); |
| 687 | 687 | |
@@ -689,211 +689,211 @@ discard block |
||
| 689 | 689 | } |
| 690 | 690 | */ |
| 691 | 691 | |
| 692 | - /** |
|
| 693 | - * Get a list of ancestors to the present node. |
|
| 694 | - */ |
|
| 695 | - protected function ancestors($node) |
|
| 696 | - { |
|
| 697 | - $buffer = []; |
|
| 698 | - $parent = $node; |
|
| 699 | - while (($parent = $parent->parentNode) !== null) { |
|
| 700 | - $buffer[] = $parent; |
|
| 701 | - } |
|
| 702 | - |
|
| 703 | - return $buffer; |
|
| 704 | - } |
|
| 705 | - |
|
| 706 | - /** |
|
| 707 | - * Check to see if DOMNode has all of the given attributes. |
|
| 708 | - * |
|
| 709 | - * This can handle namespaced attributes, including namespace |
|
| 710 | - * wildcards. |
|
| 711 | - * |
|
| 712 | - * @param DOMElement $node |
|
| 713 | - * @param $attributes |
|
| 714 | - * |
|
| 715 | - * @return bool |
|
| 716 | - */ |
|
| 717 | - protected function matchAttributes(DOMElement $node, $attributes): bool |
|
| 718 | - { |
|
| 719 | - if (empty($attributes)) { |
|
| 720 | - return true; |
|
| 721 | - } |
|
| 722 | - |
|
| 723 | - foreach ($attributes as $attr) { |
|
| 724 | - $val = isset($attr['value']) ? $attr['value'] : null; |
|
| 725 | - |
|
| 726 | - // Namespaced attributes. |
|
| 727 | - if (isset($attr['ns']) && $attr['ns'] !== '*') { |
|
| 728 | - $nsuri = $node->lookupNamespaceURI($attr['ns']); |
|
| 729 | - if (empty($nsuri) || ! $node->hasAttributeNS($nsuri, $attr['name'])) { |
|
| 730 | - return false; |
|
| 731 | - } |
|
| 732 | - $matches = Util::matchesAttributeNS($node, $attr['name'], $nsuri, $val, $attr['op']); |
|
| 733 | - } elseif (isset($attr['ns']) && $attr['ns'] === '*' && $node->hasAttributes()) { |
|
| 734 | - // Cycle through all of the attributes in the node. Note that |
|
| 735 | - // these are DOMAttr objects. |
|
| 736 | - $matches = false; |
|
| 737 | - $name = $attr['name']; |
|
| 738 | - foreach ($node->attributes as $attrNode) { |
|
| 739 | - if ($attrNode->localName === $name) { |
|
| 740 | - $nsuri = $attrNode->namespaceURI; |
|
| 741 | - $matches = Util::matchesAttributeNS($node, $name, $nsuri, $val, $attr['op']); |
|
| 742 | - } |
|
| 743 | - } |
|
| 744 | - } // No namespace. |
|
| 745 | - else { |
|
| 746 | - $matches = Util::matchesAttribute($node, $attr['name'], $val, $attr['op']); |
|
| 747 | - } |
|
| 748 | - |
|
| 749 | - if (! $matches) { |
|
| 750 | - return false; |
|
| 751 | - } |
|
| 752 | - } |
|
| 753 | - |
|
| 754 | - return true; |
|
| 755 | - } |
|
| 756 | - |
|
| 757 | - /** |
|
| 758 | - * Check that the given DOMNode has the given ID. |
|
| 759 | - * |
|
| 760 | - * @param DOMElement $node |
|
| 761 | - * @param $id |
|
| 762 | - * |
|
| 763 | - * @return bool |
|
| 764 | - */ |
|
| 765 | - protected function matchId(DOMElement $node, $id): bool |
|
| 766 | - { |
|
| 767 | - if (empty($id)) { |
|
| 768 | - return true; |
|
| 769 | - } |
|
| 770 | - |
|
| 771 | - return $node->hasAttribute('id') && $node->getAttribute('id') === $id; |
|
| 772 | - } |
|
| 773 | - |
|
| 774 | - /** |
|
| 775 | - * Check that the given DOMNode has all of the given classes. |
|
| 776 | - * |
|
| 777 | - * @param DOMElement $node |
|
| 778 | - * @param $classes |
|
| 779 | - * |
|
| 780 | - * @return bool |
|
| 781 | - */ |
|
| 782 | - protected function matchClasses(DOMElement $node, $classes): bool |
|
| 783 | - { |
|
| 784 | - if (empty($classes)) { |
|
| 785 | - return true; |
|
| 786 | - } |
|
| 787 | - |
|
| 788 | - if (! $node->hasAttribute('class')) { |
|
| 789 | - return false; |
|
| 790 | - } |
|
| 791 | - |
|
| 792 | - $eleClasses = preg_split('/\s+/', $node->getAttribute('class')); |
|
| 793 | - if (empty($eleClasses)) { |
|
| 794 | - return false; |
|
| 795 | - } |
|
| 796 | - |
|
| 797 | - // The intersection should match the given $classes. |
|
| 798 | - $missing = array_diff($classes, array_intersect($classes, $eleClasses)); |
|
| 799 | - |
|
| 800 | - return count($missing) === 0; |
|
| 801 | - } |
|
| 802 | - |
|
| 803 | - /** |
|
| 804 | - * @param DOMElement $node |
|
| 805 | - * @param $pseudoClasses |
|
| 806 | - * |
|
| 807 | - * @return bool |
|
| 808 | - * @throws NotImplementedException |
|
| 809 | - * @throws ParseException |
|
| 810 | - */ |
|
| 811 | - protected function matchPseudoClasses(DOMElement $node, $pseudoClasses): bool |
|
| 812 | - { |
|
| 813 | - $ret = true; |
|
| 814 | - foreach ($pseudoClasses as $pseudoClass) { |
|
| 815 | - $name = $pseudoClass['name']; |
|
| 816 | - // Avoid E_STRICT violation. |
|
| 817 | - $value = $pseudoClass['value'] ?? null; |
|
| 818 | - $ret &= $this->psHandler->elementMatches($name, $node, $this->scopeNode, $value); |
|
| 819 | - } |
|
| 820 | - |
|
| 821 | - return $ret; |
|
| 822 | - } |
|
| 823 | - |
|
| 824 | - /** |
|
| 825 | - * Test whether the given node matches the pseudoElements. |
|
| 826 | - * |
|
| 827 | - * If any pseudo-elements are passed, this will test to see |
|
| 828 | - * <i>if conditions obtain that would allow the pseudo-element |
|
| 829 | - * to be created</i>. This does not modify the match in any way. |
|
| 830 | - * |
|
| 831 | - * @param DOMElement $node |
|
| 832 | - * @param $pseudoElements |
|
| 833 | - * |
|
| 834 | - * @return bool |
|
| 835 | - * @throws NotImplementedException |
|
| 836 | - */ |
|
| 837 | - protected function matchPseudoElements(DOMElement $node, $pseudoElements): bool |
|
| 838 | - { |
|
| 839 | - if (empty($pseudoElements)) { |
|
| 840 | - return true; |
|
| 841 | - } |
|
| 842 | - |
|
| 843 | - foreach ($pseudoElements as $pse) { |
|
| 844 | - switch ($pse) { |
|
| 845 | - case 'first-line': |
|
| 846 | - case 'first-letter': |
|
| 847 | - case 'before': |
|
| 848 | - case 'after': |
|
| 849 | - return strlen($node->textContent) > 0; |
|
| 850 | - case 'selection': |
|
| 851 | - throw new NotImplementedException("::$pse is not implemented."); |
|
| 852 | - } |
|
| 853 | - } |
|
| 854 | - |
|
| 855 | - return false; |
|
| 856 | - } |
|
| 857 | - |
|
| 858 | - protected function newMatches() |
|
| 859 | - { |
|
| 860 | - return new SplObjectStorage(); |
|
| 861 | - } |
|
| 862 | - |
|
| 863 | - /** |
|
| 864 | - * Get the internal match set. |
|
| 865 | - * Internal utility function. |
|
| 866 | - */ |
|
| 867 | - protected function getMatches() |
|
| 868 | - { |
|
| 869 | - return $this->matches(); |
|
| 870 | - } |
|
| 871 | - |
|
| 872 | - /** |
|
| 873 | - * Set the internal match set. |
|
| 874 | - * |
|
| 875 | - * Internal utility function. |
|
| 876 | - */ |
|
| 877 | - protected function setMatches($matches) |
|
| 878 | - { |
|
| 879 | - $this->matches = $matches; |
|
| 880 | - } |
|
| 881 | - |
|
| 882 | - /** |
|
| 883 | - * Attach all nodes in a node list to the given \SplObjectStorage. |
|
| 884 | - * |
|
| 885 | - * @param DOMNodeList $nodeList |
|
| 886 | - * @param SplObjectStorage $splos |
|
| 887 | - */ |
|
| 888 | - public function attachNodeList(DOMNodeList $nodeList, SplObjectStorage $splos) |
|
| 889 | - { |
|
| 890 | - foreach ($nodeList as $item) { |
|
| 891 | - $splos->attach($item); |
|
| 892 | - } |
|
| 893 | - } |
|
| 894 | - |
|
| 895 | - public function getDocument() |
|
| 896 | - { |
|
| 897 | - return $this->dom; |
|
| 898 | - } |
|
| 692 | + /** |
|
| 693 | + * Get a list of ancestors to the present node. |
|
| 694 | + */ |
|
| 695 | + protected function ancestors($node) |
|
| 696 | + { |
|
| 697 | + $buffer = []; |
|
| 698 | + $parent = $node; |
|
| 699 | + while (($parent = $parent->parentNode) !== null) { |
|
| 700 | + $buffer[] = $parent; |
|
| 701 | + } |
|
| 702 | + |
|
| 703 | + return $buffer; |
|
| 704 | + } |
|
| 705 | + |
|
| 706 | + /** |
|
| 707 | + * Check to see if DOMNode has all of the given attributes. |
|
| 708 | + * |
|
| 709 | + * This can handle namespaced attributes, including namespace |
|
| 710 | + * wildcards. |
|
| 711 | + * |
|
| 712 | + * @param DOMElement $node |
|
| 713 | + * @param $attributes |
|
| 714 | + * |
|
| 715 | + * @return bool |
|
| 716 | + */ |
|
| 717 | + protected function matchAttributes(DOMElement $node, $attributes): bool |
|
| 718 | + { |
|
| 719 | + if (empty($attributes)) { |
|
| 720 | + return true; |
|
| 721 | + } |
|
| 722 | + |
|
| 723 | + foreach ($attributes as $attr) { |
|
| 724 | + $val = isset($attr['value']) ? $attr['value'] : null; |
|
| 725 | + |
|
| 726 | + // Namespaced attributes. |
|
| 727 | + if (isset($attr['ns']) && $attr['ns'] !== '*') { |
|
| 728 | + $nsuri = $node->lookupNamespaceURI($attr['ns']); |
|
| 729 | + if (empty($nsuri) || ! $node->hasAttributeNS($nsuri, $attr['name'])) { |
|
| 730 | + return false; |
|
| 731 | + } |
|
| 732 | + $matches = Util::matchesAttributeNS($node, $attr['name'], $nsuri, $val, $attr['op']); |
|
| 733 | + } elseif (isset($attr['ns']) && $attr['ns'] === '*' && $node->hasAttributes()) { |
|
| 734 | + // Cycle through all of the attributes in the node. Note that |
|
| 735 | + // these are DOMAttr objects. |
|
| 736 | + $matches = false; |
|
| 737 | + $name = $attr['name']; |
|
| 738 | + foreach ($node->attributes as $attrNode) { |
|
| 739 | + if ($attrNode->localName === $name) { |
|
| 740 | + $nsuri = $attrNode->namespaceURI; |
|
| 741 | + $matches = Util::matchesAttributeNS($node, $name, $nsuri, $val, $attr['op']); |
|
| 742 | + } |
|
| 743 | + } |
|
| 744 | + } // No namespace. |
|
| 745 | + else { |
|
| 746 | + $matches = Util::matchesAttribute($node, $attr['name'], $val, $attr['op']); |
|
| 747 | + } |
|
| 748 | + |
|
| 749 | + if (! $matches) { |
|
| 750 | + return false; |
|
| 751 | + } |
|
| 752 | + } |
|
| 753 | + |
|
| 754 | + return true; |
|
| 755 | + } |
|
| 756 | + |
|
| 757 | + /** |
|
| 758 | + * Check that the given DOMNode has the given ID. |
|
| 759 | + * |
|
| 760 | + * @param DOMElement $node |
|
| 761 | + * @param $id |
|
| 762 | + * |
|
| 763 | + * @return bool |
|
| 764 | + */ |
|
| 765 | + protected function matchId(DOMElement $node, $id): bool |
|
| 766 | + { |
|
| 767 | + if (empty($id)) { |
|
| 768 | + return true; |
|
| 769 | + } |
|
| 770 | + |
|
| 771 | + return $node->hasAttribute('id') && $node->getAttribute('id') === $id; |
|
| 772 | + } |
|
| 773 | + |
|
| 774 | + /** |
|
| 775 | + * Check that the given DOMNode has all of the given classes. |
|
| 776 | + * |
|
| 777 | + * @param DOMElement $node |
|
| 778 | + * @param $classes |
|
| 779 | + * |
|
| 780 | + * @return bool |
|
| 781 | + */ |
|
| 782 | + protected function matchClasses(DOMElement $node, $classes): bool |
|
| 783 | + { |
|
| 784 | + if (empty($classes)) { |
|
| 785 | + return true; |
|
| 786 | + } |
|
| 787 | + |
|
| 788 | + if (! $node->hasAttribute('class')) { |
|
| 789 | + return false; |
|
| 790 | + } |
|
| 791 | + |
|
| 792 | + $eleClasses = preg_split('/\s+/', $node->getAttribute('class')); |
|
| 793 | + if (empty($eleClasses)) { |
|
| 794 | + return false; |
|
| 795 | + } |
|
| 796 | + |
|
| 797 | + // The intersection should match the given $classes. |
|
| 798 | + $missing = array_diff($classes, array_intersect($classes, $eleClasses)); |
|
| 799 | + |
|
| 800 | + return count($missing) === 0; |
|
| 801 | + } |
|
| 802 | + |
|
| 803 | + /** |
|
| 804 | + * @param DOMElement $node |
|
| 805 | + * @param $pseudoClasses |
|
| 806 | + * |
|
| 807 | + * @return bool |
|
| 808 | + * @throws NotImplementedException |
|
| 809 | + * @throws ParseException |
|
| 810 | + */ |
|
| 811 | + protected function matchPseudoClasses(DOMElement $node, $pseudoClasses): bool |
|
| 812 | + { |
|
| 813 | + $ret = true; |
|
| 814 | + foreach ($pseudoClasses as $pseudoClass) { |
|
| 815 | + $name = $pseudoClass['name']; |
|
| 816 | + // Avoid E_STRICT violation. |
|
| 817 | + $value = $pseudoClass['value'] ?? null; |
|
| 818 | + $ret &= $this->psHandler->elementMatches($name, $node, $this->scopeNode, $value); |
|
| 819 | + } |
|
| 820 | + |
|
| 821 | + return $ret; |
|
| 822 | + } |
|
| 823 | + |
|
| 824 | + /** |
|
| 825 | + * Test whether the given node matches the pseudoElements. |
|
| 826 | + * |
|
| 827 | + * If any pseudo-elements are passed, this will test to see |
|
| 828 | + * <i>if conditions obtain that would allow the pseudo-element |
|
| 829 | + * to be created</i>. This does not modify the match in any way. |
|
| 830 | + * |
|
| 831 | + * @param DOMElement $node |
|
| 832 | + * @param $pseudoElements |
|
| 833 | + * |
|
| 834 | + * @return bool |
|
| 835 | + * @throws NotImplementedException |
|
| 836 | + */ |
|
| 837 | + protected function matchPseudoElements(DOMElement $node, $pseudoElements): bool |
|
| 838 | + { |
|
| 839 | + if (empty($pseudoElements)) { |
|
| 840 | + return true; |
|
| 841 | + } |
|
| 842 | + |
|
| 843 | + foreach ($pseudoElements as $pse) { |
|
| 844 | + switch ($pse) { |
|
| 845 | + case 'first-line': |
|
| 846 | + case 'first-letter': |
|
| 847 | + case 'before': |
|
| 848 | + case 'after': |
|
| 849 | + return strlen($node->textContent) > 0; |
|
| 850 | + case 'selection': |
|
| 851 | + throw new NotImplementedException("::$pse is not implemented."); |
|
| 852 | + } |
|
| 853 | + } |
|
| 854 | + |
|
| 855 | + return false; |
|
| 856 | + } |
|
| 857 | + |
|
| 858 | + protected function newMatches() |
|
| 859 | + { |
|
| 860 | + return new SplObjectStorage(); |
|
| 861 | + } |
|
| 862 | + |
|
| 863 | + /** |
|
| 864 | + * Get the internal match set. |
|
| 865 | + * Internal utility function. |
|
| 866 | + */ |
|
| 867 | + protected function getMatches() |
|
| 868 | + { |
|
| 869 | + return $this->matches(); |
|
| 870 | + } |
|
| 871 | + |
|
| 872 | + /** |
|
| 873 | + * Set the internal match set. |
|
| 874 | + * |
|
| 875 | + * Internal utility function. |
|
| 876 | + */ |
|
| 877 | + protected function setMatches($matches) |
|
| 878 | + { |
|
| 879 | + $this->matches = $matches; |
|
| 880 | + } |
|
| 881 | + |
|
| 882 | + /** |
|
| 883 | + * Attach all nodes in a node list to the given \SplObjectStorage. |
|
| 884 | + * |
|
| 885 | + * @param DOMNodeList $nodeList |
|
| 886 | + * @param SplObjectStorage $splos |
|
| 887 | + */ |
|
| 888 | + public function attachNodeList(DOMNodeList $nodeList, SplObjectStorage $splos) |
|
| 889 | + { |
|
| 890 | + foreach ($nodeList as $item) { |
|
| 891 | + $splos->attach($item); |
|
| 892 | + } |
|
| 893 | + } |
|
| 894 | + |
|
| 895 | + public function getDocument() |
|
| 896 | + { |
|
| 897 | + return $this->dom; |
|
| 898 | + } |
|
| 899 | 899 | } |
@@ -88,9 +88,9 @@ discard block |
||
| 88 | 88 | $splos->rewind(); |
| 89 | 89 | $first = $splos->current(); |
| 90 | 90 | if ($first instanceof DOMDocument) { |
| 91 | - $this->dom = $first;//->documentElement; |
|
| 91 | + $this->dom = $first; //->documentElement; |
|
| 92 | 92 | } else { |
| 93 | - $this->dom = $first->ownerDocument;//->documentElement; |
|
| 93 | + $this->dom = $first->ownerDocument; //->documentElement; |
|
| 94 | 94 | } |
| 95 | 95 | |
| 96 | 96 | $this->scopeNode = $scopeNode; |
@@ -297,7 +297,7 @@ discard block |
||
| 297 | 297 | */ |
| 298 | 298 | public function combineAdjacent($node, $selectors, $index) |
| 299 | 299 | { |
| 300 | - while (! empty($node->previousSibling)) { |
|
| 300 | + while (!empty($node->previousSibling)) { |
|
| 301 | 301 | $node = $node->previousSibling; |
| 302 | 302 | if ($node->nodeType == XML_ELEMENT_NODE) { |
| 303 | 303 | //$this->debug(sprintf('Testing %s against "%s"', $node->tagName, $selectors[$index])); |
@@ -327,7 +327,7 @@ discard block |
||
| 327 | 327 | */ |
| 328 | 328 | public function combineSibling($node, $selectors, $index) |
| 329 | 329 | { |
| 330 | - while (! empty($node->previousSibling)) { |
|
| 330 | + while (!empty($node->previousSibling)) { |
|
| 331 | 331 | $node = $node->previousSibling; |
| 332 | 332 | if ($node->nodeType == XML_ELEMENT_NODE && $this->matchesSimpleSelector($node, $selectors, $index)) { |
| 333 | 333 | return true; |
@@ -383,7 +383,7 @@ discard block |
||
| 383 | 383 | */ |
| 384 | 384 | public function combineAnyDescendant($node, $selectors, $index) |
| 385 | 385 | { |
| 386 | - while (! empty($node->parentNode)) { |
|
| 386 | + while (!empty($node->parentNode)) { |
|
| 387 | 387 | $node = $node->parentNode; |
| 388 | 388 | |
| 389 | 389 | // Catch case where element is child of something |
@@ -429,16 +429,16 @@ discard block |
||
| 429 | 429 | // this should give us only a single matched element |
| 430 | 430 | // to work with. |
| 431 | 431 | if (/*$element == '*' &&*/ |
| 432 | - ! empty($selector->id)) { |
|
| 432 | + !empty($selector->id)) { |
|
| 433 | 433 | $initialMatches = $this->initialMatchOnID($selector, $matches); |
| 434 | 434 | } // If a namespace is set, find the namespace matches. |
| 435 | - elseif (! empty($selector->ns)) { |
|
| 435 | + elseif (!empty($selector->ns)) { |
|
| 436 | 436 | $initialMatches = $this->initialMatchOnElementNS($selector, $matches); |
| 437 | 437 | } |
| 438 | 438 | // If the element is a wildcard, using class can |
| 439 | 439 | // substantially reduce the number of elements that |
| 440 | 440 | // we start with. |
| 441 | - elseif ($element === '*' && ! empty($selector->classes)) { |
|
| 441 | + elseif ($element === '*' && !empty($selector->classes)) { |
|
| 442 | 442 | $initialMatches = $this->initialMatchOnClasses($selector, $matches); |
| 443 | 443 | } else { |
| 444 | 444 | $initialMatches = $this->initialMatchOnElement($selector, $matches); |
@@ -467,7 +467,7 @@ discard block |
||
| 467 | 467 | |
| 468 | 468 | // Issue #145: DOMXPath will through an exception if the DOM is |
| 469 | 469 | // not set. |
| 470 | - if (! ($this->dom instanceof DOMDocument)) { |
|
| 470 | + if (!($this->dom instanceof DOMDocument)) { |
|
| 471 | 471 | return $found; |
| 472 | 472 | } |
| 473 | 473 | $baseQuery = ".//*[@id='{$id}']"; |
@@ -481,7 +481,7 @@ discard block |
||
| 481 | 481 | } |
| 482 | 482 | |
| 483 | 483 | $nl = $this->initialXpathQuery($xpath, $node, $baseQuery); |
| 484 | - if (! empty($nl) && $nl instanceof DOMNodeList) { |
|
| 484 | + if (!empty($nl) && $nl instanceof DOMNodeList) { |
|
| 485 | 485 | $this->attachNodeList($nl, $found); |
| 486 | 486 | } |
| 487 | 487 | } |
@@ -511,7 +511,7 @@ discard block |
||
| 511 | 511 | |
| 512 | 512 | // Issue #145: DOMXPath will through an exception if the DOM is |
| 513 | 513 | // not set. |
| 514 | - if (! ($this->dom instanceof DOMDocument)) { |
|
| 514 | + if (!($this->dom instanceof DOMDocument)) { |
|
| 515 | 515 | return $found; |
| 516 | 516 | } |
| 517 | 517 | $baseQuery = './/*[@class]'; |
@@ -593,7 +593,7 @@ discard block |
||
| 593 | 593 | $found->attach($node); |
| 594 | 594 | } |
| 595 | 595 | $nl = $node->getElementsByTagName($element); |
| 596 | - if (! empty($nl) && $nl instanceof DOMNodeList) { |
|
| 596 | + if (!empty($nl) && $nl instanceof DOMNodeList) { |
|
| 597 | 597 | $this->attachNodeList($nl, $found); |
| 598 | 598 | } |
| 599 | 599 | } |
@@ -664,7 +664,7 @@ discard block |
||
| 664 | 664 | } |
| 665 | 665 | |
| 666 | 666 | // Handle namespace. |
| 667 | - if (! empty($ns) && $ns !== '*') { |
|
| 667 | + if (!empty($ns) && $ns !== '*') { |
|
| 668 | 668 | // Check whether we have a matching NS URI. |
| 669 | 669 | $nsuri = $node->lookupNamespaceURI($ns); |
| 670 | 670 | if (empty($nsuri) || $node->namespaceURI !== $nsuri) { |
@@ -726,7 +726,7 @@ discard block |
||
| 726 | 726 | // Namespaced attributes. |
| 727 | 727 | if (isset($attr['ns']) && $attr['ns'] !== '*') { |
| 728 | 728 | $nsuri = $node->lookupNamespaceURI($attr['ns']); |
| 729 | - if (empty($nsuri) || ! $node->hasAttributeNS($nsuri, $attr['name'])) { |
|
| 729 | + if (empty($nsuri) || !$node->hasAttributeNS($nsuri, $attr['name'])) { |
|
| 730 | 730 | return false; |
| 731 | 731 | } |
| 732 | 732 | $matches = Util::matchesAttributeNS($node, $attr['name'], $nsuri, $val, $attr['op']); |
@@ -746,7 +746,7 @@ discard block |
||
| 746 | 746 | $matches = Util::matchesAttribute($node, $attr['name'], $val, $attr['op']); |
| 747 | 747 | } |
| 748 | 748 | |
| 749 | - if (! $matches) { |
|
| 749 | + if (!$matches) { |
|
| 750 | 750 | return false; |
| 751 | 751 | } |
| 752 | 752 | } |
@@ -785,7 +785,7 @@ discard block |
||
| 785 | 785 | return true; |
| 786 | 786 | } |
| 787 | 787 | |
| 788 | - if (! $node->hasAttribute('class')) { |
|
| 788 | + if (!$node->hasAttribute('class')) { |
|
| 789 | 789 | return false; |
| 790 | 790 | } |
| 791 | 791 | |
@@ -815,7 +815,7 @@ discard block |
||
| 815 | 815 | $name = $pseudoClass['name']; |
| 816 | 816 | // Avoid E_STRICT violation. |
| 817 | 817 | $value = $pseudoClass['value'] ?? null; |
| 818 | - $ret &= $this->psHandler->elementMatches($name, $node, $this->scopeNode, $value); |
|
| 818 | + $ret &= $this->psHandler->elementMatches($name, $node, $this->scopeNode, $value); |
|
| 819 | 819 | } |
| 820 | 820 | |
| 821 | 821 | return $ret; |
@@ -25,72 +25,72 @@ discard block |
||
| 25 | 25 | */ |
| 26 | 26 | class Parser |
| 27 | 27 | { |
| 28 | - protected $scanner; |
|
| 29 | - protected $buffer = ''; |
|
| 30 | - protected $handler; |
|
| 31 | - private $strict = false; |
|
| 32 | - |
|
| 33 | - protected $DEBUG = false; |
|
| 34 | - |
|
| 35 | - /** |
|
| 36 | - * Construct a new CSS parser object. This will attempt to |
|
| 37 | - * parse the string as a CSS selector. As it parses, it will |
|
| 38 | - * send events to the EventHandler implementation. |
|
| 39 | - * |
|
| 40 | - * @param string $string |
|
| 41 | - * @param EventHandler $handler |
|
| 42 | - */ |
|
| 43 | - public function __construct(string $string, EventHandler $handler) |
|
| 44 | - { |
|
| 45 | - $this->originalString = $string; |
|
| 46 | - $is = new InputStream($string); |
|
| 47 | - $this->scanner = new Scanner($is); |
|
| 48 | - $this->handler = $handler; |
|
| 49 | - } |
|
| 50 | - |
|
| 51 | - /** |
|
| 52 | - * Parse the selector. |
|
| 53 | - * |
|
| 54 | - * This begins an event-based parsing process that will |
|
| 55 | - * fire events as the selector is handled. A EventHandler |
|
| 56 | - * implementation will be responsible for handling the events. |
|
| 57 | - * |
|
| 58 | - * @throws ParseException |
|
| 59 | - * @throws Exception |
|
| 60 | - */ |
|
| 61 | - public function parse(): void |
|
| 62 | - { |
|
| 63 | - $this->scanner->nextToken(); |
|
| 64 | - |
|
| 65 | - while ($this->scanner->token !== false) { |
|
| 66 | - // Primitive recursion detection. |
|
| 67 | - $position = $this->scanner->position(); |
|
| 68 | - |
|
| 69 | - if ($this->DEBUG) { |
|
| 70 | - echo 'PARSE ' . $this->scanner->token . PHP_EOL; |
|
| 71 | - } |
|
| 72 | - $this->selector(); |
|
| 73 | - |
|
| 74 | - $finalPosition = $this->scanner->position(); |
|
| 75 | - if ($this->scanner->token !== false && $finalPosition === $position) { |
|
| 76 | - // If we get here, then the scanner did not pop a single character |
|
| 77 | - // off of the input stream during a full run of the parser, which |
|
| 78 | - // means that the current input does not match any recognizable |
|
| 79 | - // pattern. |
|
| 80 | - throw new ParseException('CSS selector is not well formed.'); |
|
| 81 | - } |
|
| 82 | - } |
|
| 83 | - } |
|
| 84 | - |
|
| 85 | - /** |
|
| 86 | - * A restricted parser that can only parse simple selectors. |
|
| 87 | - * The pseudoClass handler for this parser will throw an |
|
| 88 | - * exception if it encounters a pseudo-element or the |
|
| 89 | - * negation pseudo-class. |
|
| 90 | - * |
|
| 91 | - * @deprecated This is not used anywhere in QueryPath and |
|
| 92 | - * may be removed. |
|
| 93 | - *//* |
|
| 28 | + protected $scanner; |
|
| 29 | + protected $buffer = ''; |
|
| 30 | + protected $handler; |
|
| 31 | + private $strict = false; |
|
| 32 | + |
|
| 33 | + protected $DEBUG = false; |
|
| 34 | + |
|
| 35 | + /** |
|
| 36 | + * Construct a new CSS parser object. This will attempt to |
|
| 37 | + * parse the string as a CSS selector. As it parses, it will |
|
| 38 | + * send events to the EventHandler implementation. |
|
| 39 | + * |
|
| 40 | + * @param string $string |
|
| 41 | + * @param EventHandler $handler |
|
| 42 | + */ |
|
| 43 | + public function __construct(string $string, EventHandler $handler) |
|
| 44 | + { |
|
| 45 | + $this->originalString = $string; |
|
| 46 | + $is = new InputStream($string); |
|
| 47 | + $this->scanner = new Scanner($is); |
|
| 48 | + $this->handler = $handler; |
|
| 49 | + } |
|
| 50 | + |
|
| 51 | + /** |
|
| 52 | + * Parse the selector. |
|
| 53 | + * |
|
| 54 | + * This begins an event-based parsing process that will |
|
| 55 | + * fire events as the selector is handled. A EventHandler |
|
| 56 | + * implementation will be responsible for handling the events. |
|
| 57 | + * |
|
| 58 | + * @throws ParseException |
|
| 59 | + * @throws Exception |
|
| 60 | + */ |
|
| 61 | + public function parse(): void |
|
| 62 | + { |
|
| 63 | + $this->scanner->nextToken(); |
|
| 64 | + |
|
| 65 | + while ($this->scanner->token !== false) { |
|
| 66 | + // Primitive recursion detection. |
|
| 67 | + $position = $this->scanner->position(); |
|
| 68 | + |
|
| 69 | + if ($this->DEBUG) { |
|
| 70 | + echo 'PARSE ' . $this->scanner->token . PHP_EOL; |
|
| 71 | + } |
|
| 72 | + $this->selector(); |
|
| 73 | + |
|
| 74 | + $finalPosition = $this->scanner->position(); |
|
| 75 | + if ($this->scanner->token !== false && $finalPosition === $position) { |
|
| 76 | + // If we get here, then the scanner did not pop a single character |
|
| 77 | + // off of the input stream during a full run of the parser, which |
|
| 78 | + // means that the current input does not match any recognizable |
|
| 79 | + // pattern. |
|
| 80 | + throw new ParseException('CSS selector is not well formed.'); |
|
| 81 | + } |
|
| 82 | + } |
|
| 83 | + } |
|
| 84 | + |
|
| 85 | + /** |
|
| 86 | + * A restricted parser that can only parse simple selectors. |
|
| 87 | + * The pseudoClass handler for this parser will throw an |
|
| 88 | + * exception if it encounters a pseudo-element or the |
|
| 89 | + * negation pseudo-class. |
|
| 90 | + * |
|
| 91 | + * @deprecated This is not used anywhere in QueryPath and |
|
| 92 | + * may be removed. |
|
| 93 | + *//* |
|
| 94 | 94 | public function parseSimpleSelector() { |
| 95 | 95 | while ($this->scanner->token !== FALSE) { |
| 96 | 96 | if ($this->DEBUG) print "SIMPLE SELECTOR\n"; |
@@ -105,261 +105,261 @@ discard block |
||
| 105 | 105 | } |
| 106 | 106 | }*/ |
| 107 | 107 | |
| 108 | - /** |
|
| 109 | - * Handle an entire CSS selector. |
|
| 110 | - * |
|
| 111 | - * @throws ParseException |
|
| 112 | - * @throws Exception |
|
| 113 | - */ |
|
| 114 | - private function selector(): void |
|
| 115 | - { |
|
| 116 | - if ($this->DEBUG) { |
|
| 117 | - print 'SELECTOR' . $this->scanner->position() . PHP_EOL; |
|
| 118 | - } |
|
| 119 | - |
|
| 120 | - $this->consumeWhitespace(); // Remove leading whitespace |
|
| 121 | - $this->simpleSelectors(); |
|
| 122 | - $this->combinator(); |
|
| 123 | - } |
|
| 124 | - |
|
| 125 | - /** |
|
| 126 | - * Consume whitespace and return a count of the number of whitespace consumed. |
|
| 127 | - * |
|
| 128 | - * @throws ParseException |
|
| 129 | - * @throws Exception |
|
| 130 | - */ |
|
| 131 | - private function consumeWhitespace(): int |
|
| 132 | - { |
|
| 133 | - if ($this->DEBUG) { |
|
| 134 | - echo 'CONSUME WHITESPACE' . PHP_EOL; |
|
| 135 | - } |
|
| 136 | - |
|
| 137 | - $white = 0; |
|
| 138 | - while ($this->scanner->token === Token::WHITE) { |
|
| 139 | - $this->scanner->nextToken(); |
|
| 140 | - ++$white; |
|
| 141 | - } |
|
| 142 | - |
|
| 143 | - return $white; |
|
| 144 | - } |
|
| 145 | - |
|
| 146 | - /** |
|
| 147 | - * Handle one of the five combinators: '>', '+', ' ', '~', and ','. |
|
| 148 | - * This will call the appropriate event handlers. |
|
| 149 | - * |
|
| 150 | - * @throws ParseException |
|
| 151 | - * @throws Exception |
|
| 152 | - * @see EventHandler::anyDescendant(), |
|
| 153 | - * @see EventHandler::anotherSelector(). |
|
| 154 | - * @see EventHandler::directDescendant(), |
|
| 155 | - * @see EventHandler::adjacent(), |
|
| 156 | - */ |
|
| 157 | - private function combinator(): void |
|
| 158 | - { |
|
| 159 | - if ($this->DEBUG) { |
|
| 160 | - echo 'COMBINATOR' . PHP_EOL; |
|
| 161 | - } |
|
| 162 | - /* |
|
| 108 | + /** |
|
| 109 | + * Handle an entire CSS selector. |
|
| 110 | + * |
|
| 111 | + * @throws ParseException |
|
| 112 | + * @throws Exception |
|
| 113 | + */ |
|
| 114 | + private function selector(): void |
|
| 115 | + { |
|
| 116 | + if ($this->DEBUG) { |
|
| 117 | + print 'SELECTOR' . $this->scanner->position() . PHP_EOL; |
|
| 118 | + } |
|
| 119 | + |
|
| 120 | + $this->consumeWhitespace(); // Remove leading whitespace |
|
| 121 | + $this->simpleSelectors(); |
|
| 122 | + $this->combinator(); |
|
| 123 | + } |
|
| 124 | + |
|
| 125 | + /** |
|
| 126 | + * Consume whitespace and return a count of the number of whitespace consumed. |
|
| 127 | + * |
|
| 128 | + * @throws ParseException |
|
| 129 | + * @throws Exception |
|
| 130 | + */ |
|
| 131 | + private function consumeWhitespace(): int |
|
| 132 | + { |
|
| 133 | + if ($this->DEBUG) { |
|
| 134 | + echo 'CONSUME WHITESPACE' . PHP_EOL; |
|
| 135 | + } |
|
| 136 | + |
|
| 137 | + $white = 0; |
|
| 138 | + while ($this->scanner->token === Token::WHITE) { |
|
| 139 | + $this->scanner->nextToken(); |
|
| 140 | + ++$white; |
|
| 141 | + } |
|
| 142 | + |
|
| 143 | + return $white; |
|
| 144 | + } |
|
| 145 | + |
|
| 146 | + /** |
|
| 147 | + * Handle one of the five combinators: '>', '+', ' ', '~', and ','. |
|
| 148 | + * This will call the appropriate event handlers. |
|
| 149 | + * |
|
| 150 | + * @throws ParseException |
|
| 151 | + * @throws Exception |
|
| 152 | + * @see EventHandler::anyDescendant(), |
|
| 153 | + * @see EventHandler::anotherSelector(). |
|
| 154 | + * @see EventHandler::directDescendant(), |
|
| 155 | + * @see EventHandler::adjacent(), |
|
| 156 | + */ |
|
| 157 | + private function combinator(): void |
|
| 158 | + { |
|
| 159 | + if ($this->DEBUG) { |
|
| 160 | + echo 'COMBINATOR' . PHP_EOL; |
|
| 161 | + } |
|
| 162 | + /* |
|
| 163 | 163 | * Problem: ' ' and ' > ' are both valid combinators. |
| 164 | 164 | * So we have to track whitespace consumption to see |
| 165 | 165 | * if we are hitting the ' ' combinator or if the |
| 166 | 166 | * selector just has whitespace padding another combinator. |
| 167 | 167 | */ |
| 168 | 168 | |
| 169 | - // Flag to indicate that post-checks need doing |
|
| 170 | - $inCombinator = false; |
|
| 171 | - $white = $this->consumeWhitespace(); |
|
| 172 | - $t = $this->scanner->token; |
|
| 173 | - |
|
| 174 | - if ($t === Token::RANGLE) { |
|
| 175 | - $this->handler->directDescendant(); |
|
| 176 | - $this->scanner->nextToken(); |
|
| 177 | - $inCombinator = true; |
|
| 178 | - //$this->simpleSelectors(); |
|
| 179 | - } elseif ($t === Token::PLUS) { |
|
| 180 | - $this->handler->adjacent(); |
|
| 181 | - $this->scanner->nextToken(); |
|
| 182 | - $inCombinator = true; |
|
| 183 | - //$this->simpleSelectors(); |
|
| 184 | - } elseif ($t === Token::COMMA) { |
|
| 185 | - $this->handler->anotherSelector(); |
|
| 186 | - $this->scanner->nextToken(); |
|
| 187 | - $inCombinator = true; |
|
| 188 | - //$this->scanner->selectors(); |
|
| 189 | - } elseif ($t === Token::TILDE) { |
|
| 190 | - $this->handler->sibling(); |
|
| 191 | - $this->scanner->nextToken(); |
|
| 192 | - $inCombinator = true; |
|
| 193 | - } |
|
| 194 | - |
|
| 195 | - // Check that we don't get two combinators in a row. |
|
| 196 | - if ($inCombinator) { |
|
| 197 | - if ($this->DEBUG) { |
|
| 198 | - print 'COMBINATOR: ' . Token::name($t) . "\n"; |
|
| 199 | - } |
|
| 200 | - $this->consumeWhitespace(); |
|
| 201 | - if ($this->isCombinator($this->scanner->token)) { |
|
| 202 | - throw new ParseException('Illegal combinator: Cannot have two combinators in sequence.'); |
|
| 203 | - } |
|
| 204 | - } // Check to see if we have whitespace combinator: |
|
| 205 | - elseif ($white > 0) { |
|
| 206 | - if ($this->DEBUG) { |
|
| 207 | - echo 'COMBINATOR: any descendant' . PHP_EOL; |
|
| 208 | - } |
|
| 209 | - $this->handler->anyDescendant(); |
|
| 210 | - } else { |
|
| 211 | - if ($this->DEBUG) { |
|
| 212 | - echo 'COMBINATOR: no combinator found.' . PHP_EOL; |
|
| 213 | - } |
|
| 214 | - } |
|
| 215 | - } |
|
| 216 | - |
|
| 217 | - /** |
|
| 218 | - * Check if the token is a combinator. |
|
| 219 | - * |
|
| 220 | - * @param int $tok |
|
| 221 | - * |
|
| 222 | - * @return bool |
|
| 223 | - */ |
|
| 224 | - private function isCombinator(int $tok): bool |
|
| 225 | - { |
|
| 226 | - return in_array($tok, [Token::PLUS, Token::RANGLE, Token::COMMA, Token::TILDE], true); |
|
| 227 | - } |
|
| 228 | - |
|
| 229 | - /** |
|
| 230 | - * Handle a simple selector. |
|
| 231 | - * |
|
| 232 | - * @throws ParseException |
|
| 233 | - */ |
|
| 234 | - private function simpleSelectors(): void |
|
| 235 | - { |
|
| 236 | - if ($this->DEBUG) { |
|
| 237 | - print 'SIMPLE SELECTOR' . PHP_EOL; |
|
| 238 | - } |
|
| 239 | - $this->allElements(); |
|
| 240 | - $this->elementName(); |
|
| 241 | - $this->elementClass(); |
|
| 242 | - $this->elementID(); |
|
| 243 | - $this->pseudoClass(); |
|
| 244 | - $this->attribute(); |
|
| 245 | - } |
|
| 246 | - |
|
| 247 | - /** |
|
| 248 | - * Handles CSS ID selectors. |
|
| 249 | - * This will call EventHandler::elementID(). |
|
| 250 | - * |
|
| 251 | - * @throws ParseException |
|
| 252 | - * @throws Exception |
|
| 253 | - */ |
|
| 254 | - private function elementID(): void |
|
| 255 | - { |
|
| 256 | - if ($this->DEBUG) { |
|
| 257 | - echo 'ELEMENT ID' . PHP_EOL; |
|
| 258 | - } |
|
| 259 | - |
|
| 260 | - if ($this->scanner->token === Token::OCTO) { |
|
| 261 | - $this->scanner->nextToken(); |
|
| 262 | - if ($this->scanner->token !== Token::CHAR) { |
|
| 263 | - throw new ParseException("Expected string after #"); |
|
| 264 | - } |
|
| 265 | - $id = $this->scanner->getNameString(); |
|
| 266 | - $this->handler->elementID($id); |
|
| 267 | - } |
|
| 268 | - } |
|
| 269 | - |
|
| 270 | - /** |
|
| 271 | - * Handles CSS class selectors. |
|
| 272 | - * This will call the EventHandler::elementClass() method. |
|
| 273 | - */ |
|
| 274 | - private function elementClass(): void |
|
| 275 | - { |
|
| 276 | - if ($this->DEBUG) { |
|
| 277 | - print 'ELEMENT CLASS' . PHP_EOL; |
|
| 278 | - } |
|
| 279 | - if ($this->scanner->token == Token::DOT) { |
|
| 280 | - $this->scanner->nextToken(); |
|
| 281 | - $this->consumeWhitespace(); // We're very fault tolerent. This should prob through error. |
|
| 282 | - $cssClass = $this->scanner->getNameString(); |
|
| 283 | - $this->handler->elementClass($cssClass); |
|
| 284 | - } |
|
| 285 | - } |
|
| 286 | - |
|
| 287 | - /** |
|
| 288 | - * Handle a pseudo-class and pseudo-element. |
|
| 289 | - * |
|
| 290 | - * CSS 3 selectors support separate pseudo-elements, using :: instead |
|
| 291 | - * of : for separator. This is now supported, and calls the pseudoElement |
|
| 292 | - * handler, EventHandler::pseudoElement(). |
|
| 293 | - * |
|
| 294 | - * This will call EventHandler::pseudoClass() when a |
|
| 295 | - * pseudo-class is parsed. |
|
| 296 | - * |
|
| 297 | - * @throws ParseException |
|
| 298 | - * @throws Exception |
|
| 299 | - */ |
|
| 300 | - private function pseudoClass($restricted = false): void |
|
| 301 | - { |
|
| 302 | - if ($this->DEBUG) { |
|
| 303 | - echo 'PSEUDO-CLASS' . PHP_EOL; |
|
| 304 | - } |
|
| 305 | - if ($this->scanner->token === Token::COLON) { |
|
| 306 | - // Check for CSS 3 pseudo element: |
|
| 307 | - $isPseudoElement = false; |
|
| 308 | - if ($this->scanner->nextToken() === Token::COLON) { |
|
| 309 | - $isPseudoElement = true; |
|
| 310 | - $this->scanner->nextToken(); |
|
| 311 | - } |
|
| 312 | - |
|
| 313 | - $name = $this->scanner->getNameString(); |
|
| 314 | - if ($restricted && $name === 'not') { |
|
| 315 | - throw new ParseException("The 'not' pseudo-class is illegal in this context."); |
|
| 316 | - } |
|
| 317 | - |
|
| 318 | - $value = null; |
|
| 319 | - if ($this->scanner->token === Token::LPAREN) { |
|
| 320 | - if ($isPseudoElement) { |
|
| 321 | - throw new ParseException('Illegal left paren. Pseudo-Element cannot have arguments.'); |
|
| 322 | - } |
|
| 323 | - $value = $this->pseudoClassValue(); |
|
| 324 | - } |
|
| 325 | - |
|
| 326 | - // FIXME: This should throw errors when pseudo element has values. |
|
| 327 | - if ($isPseudoElement) { |
|
| 328 | - if ($restricted) { |
|
| 329 | - throw new ParseException('Pseudo-Elements are illegal in this context.'); |
|
| 330 | - } |
|
| 331 | - $this->handler->pseudoElement($name); |
|
| 332 | - $this->consumeWhitespace(); |
|
| 333 | - |
|
| 334 | - // Per the spec, pseudo-elements must be the last items in a selector, so we |
|
| 335 | - // check to make sure that we are either at the end of the stream or that a |
|
| 336 | - // new selector is starting. Only one pseudo-element is allowed per selector. |
|
| 337 | - if ($this->scanner->token !== false && $this->scanner->token !== Token::COMMA) { |
|
| 338 | - throw new ParseException('A Pseudo-Element must be the last item in a selector.'); |
|
| 339 | - } |
|
| 340 | - } else { |
|
| 341 | - $this->handler->pseudoClass($name, $value); |
|
| 342 | - } |
|
| 343 | - } |
|
| 344 | - } |
|
| 345 | - |
|
| 346 | - /** |
|
| 347 | - * Get the value of a pseudo-classes. |
|
| 348 | - * |
|
| 349 | - * @return string |
|
| 350 | - * Returns the value found from a pseudo-class. |
|
| 351 | - * |
|
| 352 | - * @todo Pseudoclasses can be passed pseudo-elements and |
|
| 353 | - * other pseudo-classes as values, which means :pseudo(::pseudo) |
|
| 354 | - * is legal. |
|
| 355 | - */ |
|
| 356 | - private function pseudoClassValue() |
|
| 357 | - { |
|
| 358 | - if ($this->scanner->token === Token::LPAREN) { |
|
| 359 | - $buf = ''; |
|
| 360 | - |
|
| 361 | - // For now, just leave pseudoClass value vague. |
|
| 362 | - /* |
|
| 169 | + // Flag to indicate that post-checks need doing |
|
| 170 | + $inCombinator = false; |
|
| 171 | + $white = $this->consumeWhitespace(); |
|
| 172 | + $t = $this->scanner->token; |
|
| 173 | + |
|
| 174 | + if ($t === Token::RANGLE) { |
|
| 175 | + $this->handler->directDescendant(); |
|
| 176 | + $this->scanner->nextToken(); |
|
| 177 | + $inCombinator = true; |
|
| 178 | + //$this->simpleSelectors(); |
|
| 179 | + } elseif ($t === Token::PLUS) { |
|
| 180 | + $this->handler->adjacent(); |
|
| 181 | + $this->scanner->nextToken(); |
|
| 182 | + $inCombinator = true; |
|
| 183 | + //$this->simpleSelectors(); |
|
| 184 | + } elseif ($t === Token::COMMA) { |
|
| 185 | + $this->handler->anotherSelector(); |
|
| 186 | + $this->scanner->nextToken(); |
|
| 187 | + $inCombinator = true; |
|
| 188 | + //$this->scanner->selectors(); |
|
| 189 | + } elseif ($t === Token::TILDE) { |
|
| 190 | + $this->handler->sibling(); |
|
| 191 | + $this->scanner->nextToken(); |
|
| 192 | + $inCombinator = true; |
|
| 193 | + } |
|
| 194 | + |
|
| 195 | + // Check that we don't get two combinators in a row. |
|
| 196 | + if ($inCombinator) { |
|
| 197 | + if ($this->DEBUG) { |
|
| 198 | + print 'COMBINATOR: ' . Token::name($t) . "\n"; |
|
| 199 | + } |
|
| 200 | + $this->consumeWhitespace(); |
|
| 201 | + if ($this->isCombinator($this->scanner->token)) { |
|
| 202 | + throw new ParseException('Illegal combinator: Cannot have two combinators in sequence.'); |
|
| 203 | + } |
|
| 204 | + } // Check to see if we have whitespace combinator: |
|
| 205 | + elseif ($white > 0) { |
|
| 206 | + if ($this->DEBUG) { |
|
| 207 | + echo 'COMBINATOR: any descendant' . PHP_EOL; |
|
| 208 | + } |
|
| 209 | + $this->handler->anyDescendant(); |
|
| 210 | + } else { |
|
| 211 | + if ($this->DEBUG) { |
|
| 212 | + echo 'COMBINATOR: no combinator found.' . PHP_EOL; |
|
| 213 | + } |
|
| 214 | + } |
|
| 215 | + } |
|
| 216 | + |
|
| 217 | + /** |
|
| 218 | + * Check if the token is a combinator. |
|
| 219 | + * |
|
| 220 | + * @param int $tok |
|
| 221 | + * |
|
| 222 | + * @return bool |
|
| 223 | + */ |
|
| 224 | + private function isCombinator(int $tok): bool |
|
| 225 | + { |
|
| 226 | + return in_array($tok, [Token::PLUS, Token::RANGLE, Token::COMMA, Token::TILDE], true); |
|
| 227 | + } |
|
| 228 | + |
|
| 229 | + /** |
|
| 230 | + * Handle a simple selector. |
|
| 231 | + * |
|
| 232 | + * @throws ParseException |
|
| 233 | + */ |
|
| 234 | + private function simpleSelectors(): void |
|
| 235 | + { |
|
| 236 | + if ($this->DEBUG) { |
|
| 237 | + print 'SIMPLE SELECTOR' . PHP_EOL; |
|
| 238 | + } |
|
| 239 | + $this->allElements(); |
|
| 240 | + $this->elementName(); |
|
| 241 | + $this->elementClass(); |
|
| 242 | + $this->elementID(); |
|
| 243 | + $this->pseudoClass(); |
|
| 244 | + $this->attribute(); |
|
| 245 | + } |
|
| 246 | + |
|
| 247 | + /** |
|
| 248 | + * Handles CSS ID selectors. |
|
| 249 | + * This will call EventHandler::elementID(). |
|
| 250 | + * |
|
| 251 | + * @throws ParseException |
|
| 252 | + * @throws Exception |
|
| 253 | + */ |
|
| 254 | + private function elementID(): void |
|
| 255 | + { |
|
| 256 | + if ($this->DEBUG) { |
|
| 257 | + echo 'ELEMENT ID' . PHP_EOL; |
|
| 258 | + } |
|
| 259 | + |
|
| 260 | + if ($this->scanner->token === Token::OCTO) { |
|
| 261 | + $this->scanner->nextToken(); |
|
| 262 | + if ($this->scanner->token !== Token::CHAR) { |
|
| 263 | + throw new ParseException("Expected string after #"); |
|
| 264 | + } |
|
| 265 | + $id = $this->scanner->getNameString(); |
|
| 266 | + $this->handler->elementID($id); |
|
| 267 | + } |
|
| 268 | + } |
|
| 269 | + |
|
| 270 | + /** |
|
| 271 | + * Handles CSS class selectors. |
|
| 272 | + * This will call the EventHandler::elementClass() method. |
|
| 273 | + */ |
|
| 274 | + private function elementClass(): void |
|
| 275 | + { |
|
| 276 | + if ($this->DEBUG) { |
|
| 277 | + print 'ELEMENT CLASS' . PHP_EOL; |
|
| 278 | + } |
|
| 279 | + if ($this->scanner->token == Token::DOT) { |
|
| 280 | + $this->scanner->nextToken(); |
|
| 281 | + $this->consumeWhitespace(); // We're very fault tolerent. This should prob through error. |
|
| 282 | + $cssClass = $this->scanner->getNameString(); |
|
| 283 | + $this->handler->elementClass($cssClass); |
|
| 284 | + } |
|
| 285 | + } |
|
| 286 | + |
|
| 287 | + /** |
|
| 288 | + * Handle a pseudo-class and pseudo-element. |
|
| 289 | + * |
|
| 290 | + * CSS 3 selectors support separate pseudo-elements, using :: instead |
|
| 291 | + * of : for separator. This is now supported, and calls the pseudoElement |
|
| 292 | + * handler, EventHandler::pseudoElement(). |
|
| 293 | + * |
|
| 294 | + * This will call EventHandler::pseudoClass() when a |
|
| 295 | + * pseudo-class is parsed. |
|
| 296 | + * |
|
| 297 | + * @throws ParseException |
|
| 298 | + * @throws Exception |
|
| 299 | + */ |
|
| 300 | + private function pseudoClass($restricted = false): void |
|
| 301 | + { |
|
| 302 | + if ($this->DEBUG) { |
|
| 303 | + echo 'PSEUDO-CLASS' . PHP_EOL; |
|
| 304 | + } |
|
| 305 | + if ($this->scanner->token === Token::COLON) { |
|
| 306 | + // Check for CSS 3 pseudo element: |
|
| 307 | + $isPseudoElement = false; |
|
| 308 | + if ($this->scanner->nextToken() === Token::COLON) { |
|
| 309 | + $isPseudoElement = true; |
|
| 310 | + $this->scanner->nextToken(); |
|
| 311 | + } |
|
| 312 | + |
|
| 313 | + $name = $this->scanner->getNameString(); |
|
| 314 | + if ($restricted && $name === 'not') { |
|
| 315 | + throw new ParseException("The 'not' pseudo-class is illegal in this context."); |
|
| 316 | + } |
|
| 317 | + |
|
| 318 | + $value = null; |
|
| 319 | + if ($this->scanner->token === Token::LPAREN) { |
|
| 320 | + if ($isPseudoElement) { |
|
| 321 | + throw new ParseException('Illegal left paren. Pseudo-Element cannot have arguments.'); |
|
| 322 | + } |
|
| 323 | + $value = $this->pseudoClassValue(); |
|
| 324 | + } |
|
| 325 | + |
|
| 326 | + // FIXME: This should throw errors when pseudo element has values. |
|
| 327 | + if ($isPseudoElement) { |
|
| 328 | + if ($restricted) { |
|
| 329 | + throw new ParseException('Pseudo-Elements are illegal in this context.'); |
|
| 330 | + } |
|
| 331 | + $this->handler->pseudoElement($name); |
|
| 332 | + $this->consumeWhitespace(); |
|
| 333 | + |
|
| 334 | + // Per the spec, pseudo-elements must be the last items in a selector, so we |
|
| 335 | + // check to make sure that we are either at the end of the stream or that a |
|
| 336 | + // new selector is starting. Only one pseudo-element is allowed per selector. |
|
| 337 | + if ($this->scanner->token !== false && $this->scanner->token !== Token::COMMA) { |
|
| 338 | + throw new ParseException('A Pseudo-Element must be the last item in a selector.'); |
|
| 339 | + } |
|
| 340 | + } else { |
|
| 341 | + $this->handler->pseudoClass($name, $value); |
|
| 342 | + } |
|
| 343 | + } |
|
| 344 | + } |
|
| 345 | + |
|
| 346 | + /** |
|
| 347 | + * Get the value of a pseudo-classes. |
|
| 348 | + * |
|
| 349 | + * @return string |
|
| 350 | + * Returns the value found from a pseudo-class. |
|
| 351 | + * |
|
| 352 | + * @todo Pseudoclasses can be passed pseudo-elements and |
|
| 353 | + * other pseudo-classes as values, which means :pseudo(::pseudo) |
|
| 354 | + * is legal. |
|
| 355 | + */ |
|
| 356 | + private function pseudoClassValue() |
|
| 357 | + { |
|
| 358 | + if ($this->scanner->token === Token::LPAREN) { |
|
| 359 | + $buf = ''; |
|
| 360 | + |
|
| 361 | + // For now, just leave pseudoClass value vague. |
|
| 362 | + /* |
|
| 363 | 363 | // We have to peek to see if next char is a colon because |
| 364 | 364 | // pseudo-classes and pseudo-elements are legal strings here. |
| 365 | 365 | print $this->scanner->peek(); |
@@ -390,242 +390,242 @@ discard block |
||
| 390 | 390 | } |
| 391 | 391 | return $buf; |
| 392 | 392 | */ |
| 393 | - //$buf .= $this->scanner->getQuotedString(); |
|
| 394 | - $buf .= $this->scanner->getPseudoClassString(); |
|
| 395 | - |
|
| 396 | - return $buf; |
|
| 397 | - } |
|
| 398 | - } |
|
| 399 | - |
|
| 400 | - /** |
|
| 401 | - * Handle element names. |
|
| 402 | - * This will call the EventHandler::elementName(). |
|
| 403 | - * |
|
| 404 | - * This handles: |
|
| 405 | - * <code> |
|
| 406 | - * name (EventHandler::element()) |
|
| 407 | - * |name (EventHandler::element()) |
|
| 408 | - * ns|name (EventHandler::elementNS()) |
|
| 409 | - * ns|* (EventHandler::elementNS()) |
|
| 410 | - * </code> |
|
| 411 | - */ |
|
| 412 | - private function elementName() |
|
| 413 | - { |
|
| 414 | - if ($this->DEBUG) { |
|
| 415 | - print "ELEMENT NAME\n"; |
|
| 416 | - } |
|
| 417 | - if ($this->scanner->token === Token::PIPE) { |
|
| 418 | - // We have '|name', which is equiv to 'name' |
|
| 419 | - $this->scanner->nextToken(); |
|
| 420 | - $this->consumeWhitespace(); |
|
| 421 | - $elementName = $this->scanner->getNameString(); |
|
| 422 | - $this->handler->element($elementName); |
|
| 423 | - } elseif ($this->scanner->token === Token::CHAR) { |
|
| 424 | - $elementName = $this->scanner->getNameString(); |
|
| 425 | - if ($this->scanner->token == Token::PIPE) { |
|
| 426 | - // Get ns|name |
|
| 427 | - $elementNS = $elementName; |
|
| 428 | - $this->scanner->nextToken(); |
|
| 429 | - $this->consumeWhitespace(); |
|
| 430 | - if ($this->scanner->token === Token::STAR) { |
|
| 431 | - // We have ns|* |
|
| 432 | - $this->handler->anyElementInNS($elementNS); |
|
| 433 | - $this->scanner->nextToken(); |
|
| 434 | - } elseif ($this->scanner->token !== Token::CHAR) { |
|
| 435 | - $this->throwError(Token::CHAR, $this->scanner->token); |
|
| 436 | - } else { |
|
| 437 | - $elementName = $this->scanner->getNameString(); |
|
| 438 | - // We have ns|name |
|
| 439 | - $this->handler->elementNS($elementName, $elementNS); |
|
| 440 | - } |
|
| 441 | - } else { |
|
| 442 | - $this->handler->element($elementName); |
|
| 443 | - } |
|
| 444 | - } |
|
| 445 | - } |
|
| 446 | - |
|
| 447 | - /** |
|
| 448 | - * Check for all elements designators. Due to the new CSS 3 namespace |
|
| 449 | - * support, this is slightly more complicated, now, as it handles |
|
| 450 | - * the *|name and *|* cases as well as *. |
|
| 451 | - * |
|
| 452 | - * Calls EventHandler::anyElement() or EventHandler::elementName(). |
|
| 453 | - */ |
|
| 454 | - private function allElements() |
|
| 455 | - { |
|
| 456 | - if ($this->scanner->token === Token::STAR) { |
|
| 457 | - $this->scanner->nextToken(); |
|
| 458 | - if ($this->scanner->token === Token::PIPE) { |
|
| 459 | - $this->scanner->nextToken(); |
|
| 460 | - if ($this->scanner->token === Token::STAR) { |
|
| 461 | - // We got *|*. According to spec, this requires |
|
| 462 | - // that the element has a namespace, so we pass it on |
|
| 463 | - // to the handler: |
|
| 464 | - $this->scanner->nextToken(); |
|
| 465 | - $this->handler->anyElementInNS('*'); |
|
| 466 | - } else { |
|
| 467 | - // We got *|name, which means the name MUST be in a namespce, |
|
| 468 | - // so we pass this off to elementNameNS(). |
|
| 469 | - $name = $this->scanner->getNameString(); |
|
| 470 | - $this->handler->elementNS($name, '*'); |
|
| 471 | - } |
|
| 472 | - } else { |
|
| 473 | - $this->handler->anyElement(); |
|
| 474 | - } |
|
| 475 | - } |
|
| 476 | - } |
|
| 477 | - |
|
| 478 | - /** |
|
| 479 | - * Handler an attribute. |
|
| 480 | - * An attribute can be in one of two forms: |
|
| 481 | - * <code>[attrName]</code> |
|
| 482 | - * or |
|
| 483 | - * <code>[attrName="AttrValue"]</code> |
|
| 484 | - * |
|
| 485 | - * This may call the following event handlers: EventHandler::attribute(). |
|
| 486 | - * |
|
| 487 | - * @throws ParseException |
|
| 488 | - * @throws Exception |
|
| 489 | - */ |
|
| 490 | - private function attribute() |
|
| 491 | - { |
|
| 492 | - if ($this->scanner->token === Token::LSQUARE) { |
|
| 493 | - $attrVal = $op = $ns = null; |
|
| 494 | - |
|
| 495 | - $this->scanner->nextToken(); |
|
| 496 | - $this->consumeWhitespace(); |
|
| 497 | - |
|
| 498 | - if ($this->scanner->token === Token::AT) { |
|
| 499 | - if ($this->strict) { |
|
| 500 | - throw new ParseException('The @ is illegal in attributes.'); |
|
| 501 | - } |
|
| 502 | - |
|
| 503 | - $this->scanner->nextToken(); |
|
| 504 | - $this->consumeWhitespace(); |
|
| 505 | - } |
|
| 506 | - |
|
| 507 | - if ($this->scanner->token === Token::STAR) { |
|
| 508 | - // Global namespace... requires that attr be prefixed, |
|
| 509 | - // so we pass this on to a namespace handler. |
|
| 510 | - $ns = '*'; |
|
| 511 | - $this->scanner->nextToken(); |
|
| 512 | - } |
|
| 513 | - if ($this->scanner->token === Token::PIPE) { |
|
| 514 | - // Skip this. It's a global namespace. |
|
| 515 | - $this->scanner->nextToken(); |
|
| 516 | - $this->consumeWhitespace(); |
|
| 517 | - } |
|
| 518 | - |
|
| 519 | - $attrName = $this->scanner->getNameString(); |
|
| 520 | - $this->consumeWhitespace(); |
|
| 521 | - |
|
| 522 | - // Check for namespace attribute: ns|attr. We have to peek() to make |
|
| 523 | - // sure that we haven't hit the |= operator, which looks the same. |
|
| 524 | - if ($this->scanner->token === Token::PIPE && $this->scanner->peek() !== '=') { |
|
| 525 | - // We have a namespaced attribute. |
|
| 526 | - $ns = $attrName; |
|
| 527 | - $this->scanner->nextToken(); |
|
| 528 | - $attrName = $this->scanner->getNameString(); |
|
| 529 | - $this->consumeWhitespace(); |
|
| 530 | - } |
|
| 531 | - |
|
| 532 | - // Note: We require that operators do not have spaces |
|
| 533 | - // between characters, e.g. ~= , not ~ =. |
|
| 534 | - |
|
| 535 | - // Get the operator: |
|
| 536 | - switch ($this->scanner->token) { |
|
| 537 | - case Token::EQ: |
|
| 538 | - $this->consumeWhitespace(); |
|
| 539 | - $op = EventHandler::IS_EXACTLY; |
|
| 540 | - break; |
|
| 541 | - case Token::TILDE: |
|
| 542 | - if ($this->scanner->nextToken() !== Token::EQ) { |
|
| 543 | - $this->throwError(Token::EQ, $this->scanner->token); |
|
| 544 | - } |
|
| 545 | - $op = EventHandler::CONTAINS_WITH_SPACE; |
|
| 546 | - break; |
|
| 547 | - case Token::PIPE: |
|
| 548 | - if ($this->scanner->nextToken() !== Token::EQ) { |
|
| 549 | - $this->throwError(Token::EQ, $this->scanner->token); |
|
| 550 | - } |
|
| 551 | - $op = EventHandler::CONTAINS_WITH_HYPHEN; |
|
| 552 | - break; |
|
| 553 | - case Token::STAR: |
|
| 554 | - if ($this->scanner->nextToken() !== Token::EQ) { |
|
| 555 | - $this->throwError(Token::EQ, $this->scanner->token); |
|
| 556 | - } |
|
| 557 | - $op = EventHandler::CONTAINS_IN_STRING; |
|
| 558 | - break; |
|
| 559 | - case Token::DOLLAR: |
|
| 560 | - if ($this->scanner->nextToken() !== Token::EQ) { |
|
| 561 | - $this->throwError(Token::EQ, $this->scanner->token); |
|
| 562 | - } |
|
| 563 | - $op = EventHandler::ENDS_WITH; |
|
| 564 | - break; |
|
| 565 | - case Token::CARAT: |
|
| 566 | - if ($this->scanner->nextToken() !== Token::EQ) { |
|
| 567 | - $this->throwError(Token::EQ, $this->scanner->token); |
|
| 568 | - } |
|
| 569 | - $op = EventHandler::BEGINS_WITH; |
|
| 570 | - break; |
|
| 571 | - } |
|
| 572 | - |
|
| 573 | - if (isset($op)) { |
|
| 574 | - // Consume '=' and go on. |
|
| 575 | - $this->scanner->nextToken(); |
|
| 576 | - $this->consumeWhitespace(); |
|
| 577 | - |
|
| 578 | - // So... here we have a problem. The grammer suggests that the |
|
| 579 | - // value here is String1 or String2, both of which are enclosed |
|
| 580 | - // in quotes of some sort, and both of which allow lots of special |
|
| 581 | - // characters. But the spec itself includes examples like this: |
|
| 582 | - // [lang=fr] |
|
| 583 | - // So some bareword support is assumed. To get around this, we assume |
|
| 584 | - // that bare words follow the NAME rules, while quoted strings follow |
|
| 585 | - // the String1/String2 rules. |
|
| 586 | - |
|
| 587 | - if ($this->scanner->token === Token::QUOTE || $this->scanner->token === Token::SQUOTE) { |
|
| 588 | - $attrVal = $this->scanner->getQuotedString(); |
|
| 589 | - } else { |
|
| 590 | - $attrVal = $this->scanner->getNameString(); |
|
| 591 | - } |
|
| 592 | - |
|
| 593 | - if ($this->DEBUG) { |
|
| 594 | - print "ATTR: $attrVal AND OP: $op\n"; |
|
| 595 | - } |
|
| 596 | - } |
|
| 597 | - |
|
| 598 | - $this->consumeWhitespace(); |
|
| 599 | - |
|
| 600 | - if ($this->scanner->token !== Token::RSQUARE) { |
|
| 601 | - $this->throwError(Token::RSQUARE, $this->scanner->token); |
|
| 602 | - } |
|
| 603 | - |
|
| 604 | - if (isset($ns)) { |
|
| 605 | - $this->handler->attributeNS($attrName, $ns, $attrVal, $op); |
|
| 606 | - } elseif (isset($attrVal)) { |
|
| 607 | - $this->handler->attribute($attrName, $attrVal, $op); |
|
| 608 | - } else { |
|
| 609 | - $this->handler->attribute($attrName); |
|
| 610 | - } |
|
| 611 | - $this->scanner->nextToken(); |
|
| 612 | - } |
|
| 613 | - } |
|
| 614 | - |
|
| 615 | - /** |
|
| 616 | - * Utility for throwing a consistantly-formatted parse error. |
|
| 617 | - */ |
|
| 618 | - private function throwError($expected, $got) |
|
| 619 | - { |
|
| 620 | - $filter = sprintf('Expected %s, got %s', Token::name($expected), Token::name($got)); |
|
| 621 | - throw new ParseException($filter); |
|
| 622 | - } |
|
| 623 | - |
|
| 624 | - /** |
|
| 625 | - * @return Scanner |
|
| 626 | - */ |
|
| 627 | - public function getScanner(): Scanner |
|
| 628 | - { |
|
| 629 | - return $this->scanner; |
|
| 630 | - } |
|
| 393 | + //$buf .= $this->scanner->getQuotedString(); |
|
| 394 | + $buf .= $this->scanner->getPseudoClassString(); |
|
| 395 | + |
|
| 396 | + return $buf; |
|
| 397 | + } |
|
| 398 | + } |
|
| 399 | + |
|
| 400 | + /** |
|
| 401 | + * Handle element names. |
|
| 402 | + * This will call the EventHandler::elementName(). |
|
| 403 | + * |
|
| 404 | + * This handles: |
|
| 405 | + * <code> |
|
| 406 | + * name (EventHandler::element()) |
|
| 407 | + * |name (EventHandler::element()) |
|
| 408 | + * ns|name (EventHandler::elementNS()) |
|
| 409 | + * ns|* (EventHandler::elementNS()) |
|
| 410 | + * </code> |
|
| 411 | + */ |
|
| 412 | + private function elementName() |
|
| 413 | + { |
|
| 414 | + if ($this->DEBUG) { |
|
| 415 | + print "ELEMENT NAME\n"; |
|
| 416 | + } |
|
| 417 | + if ($this->scanner->token === Token::PIPE) { |
|
| 418 | + // We have '|name', which is equiv to 'name' |
|
| 419 | + $this->scanner->nextToken(); |
|
| 420 | + $this->consumeWhitespace(); |
|
| 421 | + $elementName = $this->scanner->getNameString(); |
|
| 422 | + $this->handler->element($elementName); |
|
| 423 | + } elseif ($this->scanner->token === Token::CHAR) { |
|
| 424 | + $elementName = $this->scanner->getNameString(); |
|
| 425 | + if ($this->scanner->token == Token::PIPE) { |
|
| 426 | + // Get ns|name |
|
| 427 | + $elementNS = $elementName; |
|
| 428 | + $this->scanner->nextToken(); |
|
| 429 | + $this->consumeWhitespace(); |
|
| 430 | + if ($this->scanner->token === Token::STAR) { |
|
| 431 | + // We have ns|* |
|
| 432 | + $this->handler->anyElementInNS($elementNS); |
|
| 433 | + $this->scanner->nextToken(); |
|
| 434 | + } elseif ($this->scanner->token !== Token::CHAR) { |
|
| 435 | + $this->throwError(Token::CHAR, $this->scanner->token); |
|
| 436 | + } else { |
|
| 437 | + $elementName = $this->scanner->getNameString(); |
|
| 438 | + // We have ns|name |
|
| 439 | + $this->handler->elementNS($elementName, $elementNS); |
|
| 440 | + } |
|
| 441 | + } else { |
|
| 442 | + $this->handler->element($elementName); |
|
| 443 | + } |
|
| 444 | + } |
|
| 445 | + } |
|
| 446 | + |
|
| 447 | + /** |
|
| 448 | + * Check for all elements designators. Due to the new CSS 3 namespace |
|
| 449 | + * support, this is slightly more complicated, now, as it handles |
|
| 450 | + * the *|name and *|* cases as well as *. |
|
| 451 | + * |
|
| 452 | + * Calls EventHandler::anyElement() or EventHandler::elementName(). |
|
| 453 | + */ |
|
| 454 | + private function allElements() |
|
| 455 | + { |
|
| 456 | + if ($this->scanner->token === Token::STAR) { |
|
| 457 | + $this->scanner->nextToken(); |
|
| 458 | + if ($this->scanner->token === Token::PIPE) { |
|
| 459 | + $this->scanner->nextToken(); |
|
| 460 | + if ($this->scanner->token === Token::STAR) { |
|
| 461 | + // We got *|*. According to spec, this requires |
|
| 462 | + // that the element has a namespace, so we pass it on |
|
| 463 | + // to the handler: |
|
| 464 | + $this->scanner->nextToken(); |
|
| 465 | + $this->handler->anyElementInNS('*'); |
|
| 466 | + } else { |
|
| 467 | + // We got *|name, which means the name MUST be in a namespce, |
|
| 468 | + // so we pass this off to elementNameNS(). |
|
| 469 | + $name = $this->scanner->getNameString(); |
|
| 470 | + $this->handler->elementNS($name, '*'); |
|
| 471 | + } |
|
| 472 | + } else { |
|
| 473 | + $this->handler->anyElement(); |
|
| 474 | + } |
|
| 475 | + } |
|
| 476 | + } |
|
| 477 | + |
|
| 478 | + /** |
|
| 479 | + * Handler an attribute. |
|
| 480 | + * An attribute can be in one of two forms: |
|
| 481 | + * <code>[attrName]</code> |
|
| 482 | + * or |
|
| 483 | + * <code>[attrName="AttrValue"]</code> |
|
| 484 | + * |
|
| 485 | + * This may call the following event handlers: EventHandler::attribute(). |
|
| 486 | + * |
|
| 487 | + * @throws ParseException |
|
| 488 | + * @throws Exception |
|
| 489 | + */ |
|
| 490 | + private function attribute() |
|
| 491 | + { |
|
| 492 | + if ($this->scanner->token === Token::LSQUARE) { |
|
| 493 | + $attrVal = $op = $ns = null; |
|
| 494 | + |
|
| 495 | + $this->scanner->nextToken(); |
|
| 496 | + $this->consumeWhitespace(); |
|
| 497 | + |
|
| 498 | + if ($this->scanner->token === Token::AT) { |
|
| 499 | + if ($this->strict) { |
|
| 500 | + throw new ParseException('The @ is illegal in attributes.'); |
|
| 501 | + } |
|
| 502 | + |
|
| 503 | + $this->scanner->nextToken(); |
|
| 504 | + $this->consumeWhitespace(); |
|
| 505 | + } |
|
| 506 | + |
|
| 507 | + if ($this->scanner->token === Token::STAR) { |
|
| 508 | + // Global namespace... requires that attr be prefixed, |
|
| 509 | + // so we pass this on to a namespace handler. |
|
| 510 | + $ns = '*'; |
|
| 511 | + $this->scanner->nextToken(); |
|
| 512 | + } |
|
| 513 | + if ($this->scanner->token === Token::PIPE) { |
|
| 514 | + // Skip this. It's a global namespace. |
|
| 515 | + $this->scanner->nextToken(); |
|
| 516 | + $this->consumeWhitespace(); |
|
| 517 | + } |
|
| 518 | + |
|
| 519 | + $attrName = $this->scanner->getNameString(); |
|
| 520 | + $this->consumeWhitespace(); |
|
| 521 | + |
|
| 522 | + // Check for namespace attribute: ns|attr. We have to peek() to make |
|
| 523 | + // sure that we haven't hit the |= operator, which looks the same. |
|
| 524 | + if ($this->scanner->token === Token::PIPE && $this->scanner->peek() !== '=') { |
|
| 525 | + // We have a namespaced attribute. |
|
| 526 | + $ns = $attrName; |
|
| 527 | + $this->scanner->nextToken(); |
|
| 528 | + $attrName = $this->scanner->getNameString(); |
|
| 529 | + $this->consumeWhitespace(); |
|
| 530 | + } |
|
| 531 | + |
|
| 532 | + // Note: We require that operators do not have spaces |
|
| 533 | + // between characters, e.g. ~= , not ~ =. |
|
| 534 | + |
|
| 535 | + // Get the operator: |
|
| 536 | + switch ($this->scanner->token) { |
|
| 537 | + case Token::EQ: |
|
| 538 | + $this->consumeWhitespace(); |
|
| 539 | + $op = EventHandler::IS_EXACTLY; |
|
| 540 | + break; |
|
| 541 | + case Token::TILDE: |
|
| 542 | + if ($this->scanner->nextToken() !== Token::EQ) { |
|
| 543 | + $this->throwError(Token::EQ, $this->scanner->token); |
|
| 544 | + } |
|
| 545 | + $op = EventHandler::CONTAINS_WITH_SPACE; |
|
| 546 | + break; |
|
| 547 | + case Token::PIPE: |
|
| 548 | + if ($this->scanner->nextToken() !== Token::EQ) { |
|
| 549 | + $this->throwError(Token::EQ, $this->scanner->token); |
|
| 550 | + } |
|
| 551 | + $op = EventHandler::CONTAINS_WITH_HYPHEN; |
|
| 552 | + break; |
|
| 553 | + case Token::STAR: |
|
| 554 | + if ($this->scanner->nextToken() !== Token::EQ) { |
|
| 555 | + $this->throwError(Token::EQ, $this->scanner->token); |
|
| 556 | + } |
|
| 557 | + $op = EventHandler::CONTAINS_IN_STRING; |
|
| 558 | + break; |
|
| 559 | + case Token::DOLLAR: |
|
| 560 | + if ($this->scanner->nextToken() !== Token::EQ) { |
|
| 561 | + $this->throwError(Token::EQ, $this->scanner->token); |
|
| 562 | + } |
|
| 563 | + $op = EventHandler::ENDS_WITH; |
|
| 564 | + break; |
|
| 565 | + case Token::CARAT: |
|
| 566 | + if ($this->scanner->nextToken() !== Token::EQ) { |
|
| 567 | + $this->throwError(Token::EQ, $this->scanner->token); |
|
| 568 | + } |
|
| 569 | + $op = EventHandler::BEGINS_WITH; |
|
| 570 | + break; |
|
| 571 | + } |
|
| 572 | + |
|
| 573 | + if (isset($op)) { |
|
| 574 | + // Consume '=' and go on. |
|
| 575 | + $this->scanner->nextToken(); |
|
| 576 | + $this->consumeWhitespace(); |
|
| 577 | + |
|
| 578 | + // So... here we have a problem. The grammer suggests that the |
|
| 579 | + // value here is String1 or String2, both of which are enclosed |
|
| 580 | + // in quotes of some sort, and both of which allow lots of special |
|
| 581 | + // characters. But the spec itself includes examples like this: |
|
| 582 | + // [lang=fr] |
|
| 583 | + // So some bareword support is assumed. To get around this, we assume |
|
| 584 | + // that bare words follow the NAME rules, while quoted strings follow |
|
| 585 | + // the String1/String2 rules. |
|
| 586 | + |
|
| 587 | + if ($this->scanner->token === Token::QUOTE || $this->scanner->token === Token::SQUOTE) { |
|
| 588 | + $attrVal = $this->scanner->getQuotedString(); |
|
| 589 | + } else { |
|
| 590 | + $attrVal = $this->scanner->getNameString(); |
|
| 591 | + } |
|
| 592 | + |
|
| 593 | + if ($this->DEBUG) { |
|
| 594 | + print "ATTR: $attrVal AND OP: $op\n"; |
|
| 595 | + } |
|
| 596 | + } |
|
| 597 | + |
|
| 598 | + $this->consumeWhitespace(); |
|
| 599 | + |
|
| 600 | + if ($this->scanner->token !== Token::RSQUARE) { |
|
| 601 | + $this->throwError(Token::RSQUARE, $this->scanner->token); |
|
| 602 | + } |
|
| 603 | + |
|
| 604 | + if (isset($ns)) { |
|
| 605 | + $this->handler->attributeNS($attrName, $ns, $attrVal, $op); |
|
| 606 | + } elseif (isset($attrVal)) { |
|
| 607 | + $this->handler->attribute($attrName, $attrVal, $op); |
|
| 608 | + } else { |
|
| 609 | + $this->handler->attribute($attrName); |
|
| 610 | + } |
|
| 611 | + $this->scanner->nextToken(); |
|
| 612 | + } |
|
| 613 | + } |
|
| 614 | + |
|
| 615 | + /** |
|
| 616 | + * Utility for throwing a consistantly-formatted parse error. |
|
| 617 | + */ |
|
| 618 | + private function throwError($expected, $got) |
|
| 619 | + { |
|
| 620 | + $filter = sprintf('Expected %s, got %s', Token::name($expected), Token::name($got)); |
|
| 621 | + throw new ParseException($filter); |
|
| 622 | + } |
|
| 623 | + |
|
| 624 | + /** |
|
| 625 | + * @return Scanner |
|
| 626 | + */ |
|
| 627 | + public function getScanner(): Scanner |
|
| 628 | + { |
|
| 629 | + return $this->scanner; |
|
| 630 | + } |
|
| 631 | 631 | } |
@@ -14,55 +14,55 @@ |
||
| 14 | 14 | */ |
| 15 | 15 | class InputStream |
| 16 | 16 | { |
| 17 | - protected $stream; |
|
| 18 | - public $position = 0; |
|
| 17 | + protected $stream; |
|
| 18 | + public $position = 0; |
|
| 19 | 19 | |
| 20 | - /** |
|
| 21 | - * Build a new CSS input stream from a string. |
|
| 22 | - * |
|
| 23 | - * @param string |
|
| 24 | - * String to turn into an input stream. |
|
| 25 | - */ |
|
| 26 | - public function __construct($string) |
|
| 27 | - { |
|
| 28 | - $this->stream = str_split($string); |
|
| 29 | - } |
|
| 20 | + /** |
|
| 21 | + * Build a new CSS input stream from a string. |
|
| 22 | + * |
|
| 23 | + * @param string |
|
| 24 | + * String to turn into an input stream. |
|
| 25 | + */ |
|
| 26 | + public function __construct($string) |
|
| 27 | + { |
|
| 28 | + $this->stream = str_split($string); |
|
| 29 | + } |
|
| 30 | 30 | |
| 31 | - /** |
|
| 32 | - * Look ahead one character. |
|
| 33 | - * |
|
| 34 | - * @return char |
|
| 35 | - * Returns the next character, but does not remove it from |
|
| 36 | - * the stream. |
|
| 37 | - */ |
|
| 38 | - public function peek() |
|
| 39 | - { |
|
| 40 | - return $this->stream[0]; |
|
| 41 | - } |
|
| 31 | + /** |
|
| 32 | + * Look ahead one character. |
|
| 33 | + * |
|
| 34 | + * @return char |
|
| 35 | + * Returns the next character, but does not remove it from |
|
| 36 | + * the stream. |
|
| 37 | + */ |
|
| 38 | + public function peek() |
|
| 39 | + { |
|
| 40 | + return $this->stream[0]; |
|
| 41 | + } |
|
| 42 | 42 | |
| 43 | - /** |
|
| 44 | - * Get the next unconsumed character in the stream. |
|
| 45 | - * This will remove that character from the front of the |
|
| 46 | - * stream and return it. |
|
| 47 | - */ |
|
| 48 | - public function consume() |
|
| 49 | - { |
|
| 50 | - $ret = array_shift($this->stream); |
|
| 51 | - if (! empty($ret)) { |
|
| 52 | - $this->position++; |
|
| 53 | - } |
|
| 43 | + /** |
|
| 44 | + * Get the next unconsumed character in the stream. |
|
| 45 | + * This will remove that character from the front of the |
|
| 46 | + * stream and return it. |
|
| 47 | + */ |
|
| 48 | + public function consume() |
|
| 49 | + { |
|
| 50 | + $ret = array_shift($this->stream); |
|
| 51 | + if (! empty($ret)) { |
|
| 52 | + $this->position++; |
|
| 53 | + } |
|
| 54 | 54 | |
| 55 | - return $ret; |
|
| 56 | - } |
|
| 55 | + return $ret; |
|
| 56 | + } |
|
| 57 | 57 | |
| 58 | - /** |
|
| 59 | - * Check if the stream is empty. |
|
| 60 | - * |
|
| 61 | - * @return boolean |
|
| 62 | - * Returns TRUE when the stream is empty, FALSE otherwise. |
|
| 63 | - */ |
|
| 64 | - public function isEmpty() |
|
| 65 | - { |
|
| 66 | - return count($this->stream) === 0; |
|
| 67 | - } |
|
| 58 | + /** |
|
| 59 | + * Check if the stream is empty. |
|
| 60 | + * |
|
| 61 | + * @return boolean |
|
| 62 | + * Returns TRUE when the stream is empty, FALSE otherwise. |
|
| 63 | + */ |
|
| 64 | + public function isEmpty() |
|
| 65 | + { |
|
| 66 | + return count($this->stream) === 0; |
|
| 67 | + } |
|
| 68 | 68 | } |
@@ -48,7 +48,7 @@ |
||
| 48 | 48 | public function consume() |
| 49 | 49 | { |
| 50 | 50 | $ret = array_shift($this->stream); |
| 51 | - if (! empty($ret)) { |
|
| 51 | + if (!empty($ret)) { |
|
| 52 | 52 | $this->position++; |
| 53 | 53 | } |
| 54 | 54 | |
@@ -15,1065 +15,1065 @@ |
||
| 15 | 15 | trait QueryMutators |
| 16 | 16 | { |
| 17 | 17 | |
| 18 | - /** |
|
| 19 | - * Empty everything within the specified element. |
|
| 20 | - * |
|
| 21 | - * A convenience function for removeChildren(). This is equivalent to jQuery's |
|
| 22 | - * empty() function. However, `empty` is a built-in in PHP, and cannot be used as a |
|
| 23 | - * function name. |
|
| 24 | - * |
|
| 25 | - * @return DOMQuery |
|
| 26 | - * The DOMQuery object with the newly emptied elements. |
|
| 27 | - * @see removeChildren() |
|
| 28 | - * @since 2.1 |
|
| 29 | - * @author eabrand |
|
| 30 | - * @deprecated The removeChildren() function is the preferred method. |
|
| 31 | - */ |
|
| 32 | - public function emptyElement(): Query |
|
| 33 | - { |
|
| 34 | - $this->removeChildren(); |
|
| 35 | - |
|
| 36 | - return $this; |
|
| 37 | - } |
|
| 38 | - |
|
| 39 | - /** |
|
| 40 | - * Insert the given markup as the last child. |
|
| 41 | - * |
|
| 42 | - * The markup will be inserted into each match in the set. |
|
| 43 | - * |
|
| 44 | - * The same element cannot be inserted multiple times into a document. DOM |
|
| 45 | - * documents do not allow a single object to be inserted multiple times |
|
| 46 | - * into the DOM. To insert the same XML repeatedly, we must first clone |
|
| 47 | - * the object. This has one practical implication: Once you have inserted |
|
| 48 | - * an element into the object, you cannot further manipulate the original |
|
| 49 | - * element and expect the changes to be replciated in the appended object. |
|
| 50 | - * (They are not the same -- there is no shared reference.) Instead, you |
|
| 51 | - * will need to retrieve the appended object and operate on that. |
|
| 52 | - * |
|
| 53 | - * @param mixed $data |
|
| 54 | - * This can be either a string (the usual case), or a DOM Element. |
|
| 55 | - * |
|
| 56 | - * @return DOMQuery |
|
| 57 | - * The DOMQuery object. |
|
| 58 | - * @throws QueryPath::Exception |
|
| 59 | - * Thrown if $data is an unsupported object type. |
|
| 60 | - * @throws Exception |
|
| 61 | - * @see appendTo() |
|
| 62 | - * @see prepend() |
|
| 63 | - */ |
|
| 64 | - public function append($data): Query |
|
| 65 | - { |
|
| 66 | - $data = $this->prepareInsert($data); |
|
| 67 | - if (isset($data)) { |
|
| 68 | - if (empty($this->document->documentElement) && $this->matches->count() === 0) { |
|
| 69 | - // Then we assume we are writing to the doc root |
|
| 70 | - $this->document->appendChild($data); |
|
| 71 | - $found = new SplObjectStorage(); |
|
| 72 | - $found->attach($this->document->documentElement); |
|
| 73 | - $this->setMatches($found); |
|
| 74 | - } else { |
|
| 75 | - // You can only append in item once. So in cases where we |
|
| 76 | - // need to append multiple times, we have to clone the node. |
|
| 77 | - foreach ($this->matches as $m) { |
|
| 78 | - // DOMDocumentFragments are even more troublesome, as they don't |
|
| 79 | - // always clone correctly. So we have to clone their children. |
|
| 80 | - if ($data instanceof DOMDocumentFragment) { |
|
| 81 | - foreach ($data->childNodes as $n) { |
|
| 82 | - $m->appendChild($n->cloneNode(true)); |
|
| 83 | - } |
|
| 84 | - } else { |
|
| 85 | - // Otherwise a standard clone will do. |
|
| 86 | - $m->appendChild($data->cloneNode(true)); |
|
| 87 | - } |
|
| 88 | - |
|
| 89 | - } |
|
| 90 | - } |
|
| 91 | - |
|
| 92 | - } |
|
| 93 | - |
|
| 94 | - return $this; |
|
| 95 | - } |
|
| 96 | - |
|
| 97 | - /** |
|
| 98 | - * Insert the given markup as the first child. |
|
| 99 | - * |
|
| 100 | - * The markup will be inserted into each match in the set. |
|
| 101 | - * |
|
| 102 | - * @param mixed $data |
|
| 103 | - * This can be either a string (the usual case), or a DOM Element. |
|
| 104 | - * |
|
| 105 | - * @return DOMQuery |
|
| 106 | - * @throws QueryPath::Exception |
|
| 107 | - * Thrown if $data is an unsupported object type. |
|
| 108 | - * @throws Exception |
|
| 109 | - * @see after() |
|
| 110 | - * @see prependTo() |
|
| 111 | - * @see append() |
|
| 112 | - * @see before() |
|
| 113 | - */ |
|
| 114 | - public function prepend($data): Query |
|
| 115 | - { |
|
| 116 | - $data = $this->prepareInsert($data); |
|
| 117 | - if (isset($data)) { |
|
| 118 | - foreach ($this->matches as $m) { |
|
| 119 | - $ins = $data->cloneNode(true); |
|
| 120 | - if ($m->hasChildNodes()) { |
|
| 121 | - $m->insertBefore($ins, $m->childNodes->item(0)); |
|
| 122 | - } else { |
|
| 123 | - $m->appendChild($ins); |
|
| 124 | - } |
|
| 125 | - } |
|
| 126 | - } |
|
| 127 | - |
|
| 128 | - return $this; |
|
| 129 | - } |
|
| 130 | - |
|
| 131 | - /** |
|
| 132 | - * Take all nodes in the current object and prepend them to the children nodes of |
|
| 133 | - * each matched node in the passed-in DOMQuery object. |
|
| 134 | - * |
|
| 135 | - * This will iterate through each item in the current DOMQuery object and |
|
| 136 | - * add each item to the beginning of the children of each element in the |
|
| 137 | - * passed-in DOMQuery object. |
|
| 138 | - * |
|
| 139 | - * @param DOMQuery $dest |
|
| 140 | - * The destination DOMQuery object. |
|
| 141 | - * |
|
| 142 | - * @return DOMQuery |
|
| 143 | - * The original DOMQuery, unmodified. NOT the destination DOMQuery. |
|
| 144 | - * @throws QueryPath::Exception |
|
| 145 | - * Thrown if $data is an unsupported object type. |
|
| 146 | - * @see appendTo() |
|
| 147 | - * @see insertBefore() |
|
| 148 | - * @see insertAfter() |
|
| 149 | - * @see prepend() |
|
| 150 | - */ |
|
| 151 | - public function prependTo(Query $dest) |
|
| 152 | - { |
|
| 153 | - foreach ($this->matches as $m) { |
|
| 154 | - $dest->prepend($m); |
|
| 155 | - } |
|
| 156 | - |
|
| 157 | - return $this; |
|
| 158 | - } |
|
| 159 | - |
|
| 160 | - /** |
|
| 161 | - * Insert the given data before each element in the current set of matches. |
|
| 162 | - * |
|
| 163 | - * This will take the give data (XML or HTML) and put it before each of the items that |
|
| 164 | - * the DOMQuery object currently contains. Contrast this with after(). |
|
| 165 | - * |
|
| 166 | - * @param mixed $data |
|
| 167 | - * The data to be inserted. This can be XML in a string, a DomFragment, a DOMElement, |
|
| 168 | - * or the other usual suspects. (See {@link qp()}). |
|
| 169 | - * |
|
| 170 | - * @return DOMQuery |
|
| 171 | - * Returns the DOMQuery with the new modifications. The list of elements currently |
|
| 172 | - * selected will remain the same. |
|
| 173 | - * @throws QueryPath::Exception |
|
| 174 | - * Thrown if $data is an unsupported object type. |
|
| 175 | - * @throws Exception |
|
| 176 | - * @see append() |
|
| 177 | - * @see prepend() |
|
| 178 | - * @see insertBefore() |
|
| 179 | - * @see after() |
|
| 180 | - */ |
|
| 181 | - public function before($data): Query |
|
| 182 | - { |
|
| 183 | - $data = $this->prepareInsert($data); |
|
| 184 | - foreach ($this->matches as $m) { |
|
| 185 | - $ins = $data->cloneNode(true); |
|
| 186 | - $m->parentNode->insertBefore($ins, $m); |
|
| 187 | - } |
|
| 188 | - |
|
| 189 | - return $this; |
|
| 190 | - } |
|
| 191 | - |
|
| 192 | - /** |
|
| 193 | - * Insert the current elements into the destination document. |
|
| 194 | - * The items are inserted before each element in the given DOMQuery document. |
|
| 195 | - * That is, they will be siblings with the current elements. |
|
| 196 | - * |
|
| 197 | - * @param Query $dest |
|
| 198 | - * Destination DOMQuery document. |
|
| 199 | - * |
|
| 200 | - * @return DOMQuery |
|
| 201 | - * The current DOMQuery object, unaltered. Only the destination DOMQuery |
|
| 202 | - * object is altered. |
|
| 203 | - * @throws QueryPath::Exception |
|
| 204 | - * Thrown if $data is an unsupported object type. |
|
| 205 | - * @see insertAfter() |
|
| 206 | - * @see appendTo() |
|
| 207 | - * @see before() |
|
| 208 | - */ |
|
| 209 | - public function insertBefore(Query $dest): Query |
|
| 210 | - { |
|
| 211 | - foreach ($this->matches as $m) { |
|
| 212 | - $dest->before($m); |
|
| 213 | - } |
|
| 214 | - |
|
| 215 | - return $this; |
|
| 216 | - } |
|
| 217 | - |
|
| 218 | - /** |
|
| 219 | - * Insert the contents of the current DOMQuery after the nodes in the |
|
| 220 | - * destination DOMQuery object. |
|
| 221 | - * |
|
| 222 | - * @param Query $dest |
|
| 223 | - * Destination object where the current elements will be deposited. |
|
| 224 | - * |
|
| 225 | - * @return DOMQuery |
|
| 226 | - * The present DOMQuery, unaltered. Only the destination object is altered. |
|
| 227 | - * @throws QueryPath::Exception |
|
| 228 | - * Thrown if $data is an unsupported object type. |
|
| 229 | - * @see insertBefore() |
|
| 230 | - * @see append() |
|
| 231 | - * @see after() |
|
| 232 | - */ |
|
| 233 | - public function insertAfter(Query $dest): Query |
|
| 234 | - { |
|
| 235 | - foreach ($this->matches as $m) { |
|
| 236 | - $dest->after($m); |
|
| 237 | - } |
|
| 238 | - |
|
| 239 | - return $this; |
|
| 240 | - } |
|
| 241 | - |
|
| 242 | - /** |
|
| 243 | - * Insert the given data after each element in the current DOMQuery object. |
|
| 244 | - * |
|
| 245 | - * This inserts the element as a peer to the currently matched elements. |
|
| 246 | - * Contrast this with {@link append()}, which inserts the data as children |
|
| 247 | - * of matched elements. |
|
| 248 | - * |
|
| 249 | - * @param mixed $data |
|
| 250 | - * The data to be appended. |
|
| 251 | - * |
|
| 252 | - * @return DOMQuery |
|
| 253 | - * The DOMQuery object (with the items inserted). |
|
| 254 | - * @throws QueryPath::Exception |
|
| 255 | - * Thrown if $data is an unsupported object type. |
|
| 256 | - * @throws Exception |
|
| 257 | - * @see before() |
|
| 258 | - * @see append() |
|
| 259 | - */ |
|
| 260 | - public function after($data): Query |
|
| 261 | - { |
|
| 262 | - if (empty($data)) { |
|
| 263 | - return $this; |
|
| 264 | - } |
|
| 265 | - $data = $this->prepareInsert($data); |
|
| 266 | - foreach ($this->matches as $m) { |
|
| 267 | - $ins = $data->cloneNode(true); |
|
| 268 | - if (isset($m->nextSibling)) { |
|
| 269 | - $m->parentNode->insertBefore($ins, $m->nextSibling); |
|
| 270 | - } else { |
|
| 271 | - $m->parentNode->appendChild($ins); |
|
| 272 | - } |
|
| 273 | - } |
|
| 274 | - |
|
| 275 | - return $this; |
|
| 276 | - } |
|
| 277 | - |
|
| 278 | - /** |
|
| 279 | - * Replace the existing element(s) in the list with a new one. |
|
| 280 | - * |
|
| 281 | - * @param mixed $new |
|
| 282 | - * A DOMElement or XML in a string. This will replace all elements |
|
| 283 | - * currently wrapped in the DOMQuery object. |
|
| 284 | - * |
|
| 285 | - * @return DOMQuery |
|
| 286 | - * The DOMQuery object wrapping <b>the items that were removed</b>. |
|
| 287 | - * This remains consistent with the jQuery API. |
|
| 288 | - * @throws Exception |
|
| 289 | - * @throws ParseException |
|
| 290 | - * @throws QueryPath |
|
| 291 | - * @see append() |
|
| 292 | - * @see prepend() |
|
| 293 | - * @see before() |
|
| 294 | - * @see after() |
|
| 295 | - * @see remove() |
|
| 296 | - * @see replaceAll() |
|
| 297 | - */ |
|
| 298 | - public function replaceWith($new): Query |
|
| 299 | - { |
|
| 300 | - $data = $this->prepareInsert($new); |
|
| 301 | - $found = new SplObjectStorage(); |
|
| 302 | - foreach ($this->matches as $m) { |
|
| 303 | - $parent = $m->parentNode; |
|
| 304 | - $parent->insertBefore($data->cloneNode(true), $m); |
|
| 305 | - $found->attach($parent->removeChild($m)); |
|
| 306 | - } |
|
| 307 | - |
|
| 308 | - return $this->inst($found, null); |
|
| 309 | - } |
|
| 310 | - |
|
| 311 | - /** |
|
| 312 | - * Remove the parent element from the selected node or nodes. |
|
| 313 | - * |
|
| 314 | - * This takes the given list of nodes and "unwraps" them, moving them out of their parent |
|
| 315 | - * node, and then deleting the parent node. |
|
| 316 | - * |
|
| 317 | - * For example, consider this: |
|
| 318 | - * |
|
| 319 | - * @code |
|
| 320 | - * <root><wrapper><content/></wrapper></root> |
|
| 321 | - * @endcode |
|
| 322 | - * |
|
| 323 | - * Now we can run this code: |
|
| 324 | - * @code |
|
| 325 | - * qp($xml, 'content')->unwrap(); |
|
| 326 | - * @endcode |
|
| 327 | - * |
|
| 328 | - * This will result in: |
|
| 329 | - * |
|
| 330 | - * @code |
|
| 331 | - * <root><content/></root> |
|
| 332 | - * @endcode |
|
| 333 | - * This is the opposite of wrap(). |
|
| 334 | - * |
|
| 335 | - * <b>The root element cannot be unwrapped.</b> It has no parents. |
|
| 336 | - * If you attempt to use unwrap on a root element, this will throw a |
|
| 337 | - * QueryPath::Exception. (You can, however, "Unwrap" a child that is |
|
| 338 | - * a direct descendant of the root element. This will remove the root |
|
| 339 | - * element, and replace the child as the root element. Be careful, though. |
|
| 340 | - * You cannot set more than one child as a root element.) |
|
| 341 | - * |
|
| 342 | - * @return DOMQuery |
|
| 343 | - * The DOMQuery object, with the same element(s) selected. |
|
| 344 | - * @throws Exception |
|
| 345 | - * @see wrap() |
|
| 346 | - * @since 2.1 |
|
| 347 | - * @author mbutcher |
|
| 348 | - */ |
|
| 349 | - public function unwrap(): Query |
|
| 350 | - { |
|
| 351 | - // We do this in two loops in order to |
|
| 352 | - // capture the case where two matches are |
|
| 353 | - // under the same parent. Othwerwise we might |
|
| 354 | - // remove a match before we can move it. |
|
| 355 | - $parents = new SplObjectStorage(); |
|
| 356 | - foreach ($this->matches as $m) { |
|
| 357 | - |
|
| 358 | - // Cannot unwrap the root element. |
|
| 359 | - if ($m->isSameNode($m->ownerDocument->documentElement)) { |
|
| 360 | - throw new Exception('Cannot unwrap the root element.'); |
|
| 361 | - } |
|
| 362 | - |
|
| 363 | - // Move children to peer of parent. |
|
| 364 | - $parent = $m->parentNode; |
|
| 365 | - $old = $parent->removeChild($m); |
|
| 366 | - $parent->parentNode->insertBefore($old, $parent); |
|
| 367 | - $parents->attach($parent); |
|
| 368 | - } |
|
| 369 | - |
|
| 370 | - // Now that all the children are moved, we |
|
| 371 | - // remove all of the parents. |
|
| 372 | - foreach ($parents as $ele) { |
|
| 373 | - $ele->parentNode->removeChild($ele); |
|
| 374 | - } |
|
| 375 | - |
|
| 376 | - return $this; |
|
| 377 | - } |
|
| 378 | - |
|
| 379 | - /** |
|
| 380 | - * Wrap each element inside of the given markup. |
|
| 381 | - * |
|
| 382 | - * Markup is usually a string, but it can also be a DOMNode, a document |
|
| 383 | - * fragment, a SimpleXMLElement, or another DOMNode object (in which case |
|
| 384 | - * the first item in the list will be used.) |
|
| 385 | - * |
|
| 386 | - * @param mixed $markup |
|
| 387 | - * Markup that will wrap each element in the current list. |
|
| 388 | - * |
|
| 389 | - * @return DOMQuery |
|
| 390 | - * The DOMQuery object with the wrapping changes made. |
|
| 391 | - * @throws Exception |
|
| 392 | - * @throws QueryPath |
|
| 393 | - * @see wrapAll() |
|
| 394 | - * @see wrapInner() |
|
| 395 | - */ |
|
| 396 | - public function wrap($markup): Query |
|
| 397 | - { |
|
| 398 | - $data = $this->prepareInsert($markup); |
|
| 399 | - |
|
| 400 | - // If the markup passed in is empty, we don't do any wrapping. |
|
| 401 | - if (empty($data)) { |
|
| 402 | - return $this; |
|
| 403 | - } |
|
| 404 | - |
|
| 405 | - foreach ($this->matches as $m) { |
|
| 406 | - if ($data instanceof DOMDocumentFragment) { |
|
| 407 | - $copy = $data->firstChild->cloneNode(true); |
|
| 408 | - } else { |
|
| 409 | - $copy = $data->cloneNode(true); |
|
| 410 | - } |
|
| 411 | - |
|
| 412 | - // XXX: Should be able to avoid doing this over and over. |
|
| 413 | - if ($copy->hasChildNodes()) { |
|
| 414 | - $deepest = $this->deepestNode($copy); |
|
| 415 | - // FIXME: Does this need a different data structure? |
|
| 416 | - $bottom = $deepest[0]; |
|
| 417 | - } else { |
|
| 418 | - $bottom = $copy; |
|
| 419 | - } |
|
| 420 | - |
|
| 421 | - $parent = $m->parentNode; |
|
| 422 | - $parent->insertBefore($copy, $m); |
|
| 423 | - $m = $parent->removeChild($m); |
|
| 424 | - $bottom->appendChild($m); |
|
| 425 | - } |
|
| 426 | - |
|
| 427 | - return $this; |
|
| 428 | - } |
|
| 429 | - |
|
| 430 | - /** |
|
| 431 | - * Wrap all elements inside of the given markup. |
|
| 432 | - * |
|
| 433 | - * So all elements will be grouped together under this single marked up |
|
| 434 | - * item. This works by first determining the parent element of the first item |
|
| 435 | - * in the list. It then moves all of the matching elements under the wrapper |
|
| 436 | - * and inserts the wrapper where that first element was found. (This is in |
|
| 437 | - * accordance with the way jQuery works.) |
|
| 438 | - * |
|
| 439 | - * Markup is usually XML in a string, but it can also be a DOMNode, a document |
|
| 440 | - * fragment, a SimpleXMLElement, or another DOMNode object (in which case |
|
| 441 | - * the first item in the list will be used.) |
|
| 442 | - * |
|
| 443 | - * @param string $markup |
|
| 444 | - * Markup that will wrap all elements in the current list. |
|
| 445 | - * |
|
| 446 | - * @return DOMQuery |
|
| 447 | - * The DOMQuery object with the wrapping changes made. |
|
| 448 | - * @throws Exception |
|
| 449 | - * @throws QueryPath |
|
| 450 | - * @see wrap() |
|
| 451 | - * @see wrapInner() |
|
| 452 | - */ |
|
| 453 | - public function wrapAll($markup) |
|
| 454 | - { |
|
| 455 | - if ($this->matches->count() === 0) { |
|
| 456 | - return; |
|
| 457 | - } |
|
| 458 | - |
|
| 459 | - $data = $this->prepareInsert($markup); |
|
| 460 | - |
|
| 461 | - if (empty($data)) { |
|
| 462 | - return $this; |
|
| 463 | - } |
|
| 464 | - |
|
| 465 | - if ($data instanceof DOMDocumentFragment) { |
|
| 466 | - $data = $data->firstChild->cloneNode(true); |
|
| 467 | - } else { |
|
| 468 | - $data = $data->cloneNode(true); |
|
| 469 | - } |
|
| 470 | - |
|
| 471 | - if ($data->hasChildNodes()) { |
|
| 472 | - $deepest = $this->deepestNode($data); |
|
| 473 | - // FIXME: Does this need fixing? |
|
| 474 | - $bottom = $deepest[0]; |
|
| 475 | - } else { |
|
| 476 | - $bottom = $data; |
|
| 477 | - } |
|
| 478 | - |
|
| 479 | - $first = $this->getFirstMatch(); |
|
| 480 | - $parent = $first->parentNode; |
|
| 481 | - $parent->insertBefore($data, $first); |
|
| 482 | - foreach ($this->matches as $m) { |
|
| 483 | - $bottom->appendChild($m->parentNode->removeChild($m)); |
|
| 484 | - } |
|
| 485 | - |
|
| 486 | - return $this; |
|
| 487 | - } |
|
| 488 | - |
|
| 489 | - /** |
|
| 490 | - * Wrap the child elements of each item in the list with the given markup. |
|
| 491 | - * |
|
| 492 | - * Markup is usually a string, but it can also be a DOMNode, a document |
|
| 493 | - * fragment, a SimpleXMLElement, or another DOMNode object (in which case |
|
| 494 | - * the first item in the list will be used.) |
|
| 495 | - * |
|
| 496 | - * @param string $markup |
|
| 497 | - * Markup that will wrap children of each element in the current list. |
|
| 498 | - * |
|
| 499 | - * @return DOMQuery |
|
| 500 | - * The DOMQuery object with the wrapping changes made. |
|
| 501 | - * @throws Exception |
|
| 502 | - * @throws QueryPath |
|
| 503 | - * @see wrap() |
|
| 504 | - * @see wrapAll() |
|
| 505 | - */ |
|
| 506 | - public function wrapInner($markup) |
|
| 507 | - { |
|
| 508 | - $data = $this->prepareInsert($markup); |
|
| 509 | - |
|
| 510 | - // No data? Short circuit. |
|
| 511 | - if (empty($data)) { |
|
| 512 | - return $this; |
|
| 513 | - } |
|
| 514 | - |
|
| 515 | - foreach ($this->matches as $m) { |
|
| 516 | - if ($data instanceof DOMDocumentFragment) { |
|
| 517 | - $wrapper = $data->firstChild->cloneNode(true); |
|
| 518 | - } else { |
|
| 519 | - $wrapper = $data->cloneNode(true); |
|
| 520 | - } |
|
| 521 | - |
|
| 522 | - if ($wrapper->hasChildNodes()) { |
|
| 523 | - $deepest = $this->deepestNode($wrapper); |
|
| 524 | - // FIXME: ??? |
|
| 525 | - $bottom = $deepest[0]; |
|
| 526 | - } else { |
|
| 527 | - $bottom = $wrapper; |
|
| 528 | - } |
|
| 529 | - |
|
| 530 | - if ($m->hasChildNodes()) { |
|
| 531 | - while ($m->firstChild) { |
|
| 532 | - $kid = $m->removeChild($m->firstChild); |
|
| 533 | - $bottom->appendChild($kid); |
|
| 534 | - } |
|
| 535 | - } |
|
| 536 | - |
|
| 537 | - $m->appendChild($wrapper); |
|
| 538 | - } |
|
| 539 | - |
|
| 540 | - return $this; |
|
| 541 | - } |
|
| 542 | - |
|
| 543 | - /** |
|
| 544 | - * Reduce the set of matches to the deepest child node in the tree. |
|
| 545 | - * |
|
| 546 | - * This loops through the matches and looks for the deepest child node of all of |
|
| 547 | - * the matches. "Deepest", here, is relative to the nodes in the list. It is |
|
| 548 | - * calculated as the distance from the starting node to the most distant child |
|
| 549 | - * node. In other words, it is not necessarily the farthest node from the root |
|
| 550 | - * element, but the farthest note from the matched element. |
|
| 551 | - * |
|
| 552 | - * In the case where there are multiple nodes at the same depth, all of the |
|
| 553 | - * nodes at that depth will be included. |
|
| 554 | - * |
|
| 555 | - * @return DOMQuery |
|
| 556 | - * The DOMQuery wrapping the single deepest node. |
|
| 557 | - * @throws ParseException |
|
| 558 | - */ |
|
| 559 | - public function deepest(): Query |
|
| 560 | - { |
|
| 561 | - $deepest = 0; |
|
| 562 | - $winner = new SplObjectStorage(); |
|
| 563 | - foreach ($this->matches as $m) { |
|
| 564 | - $local_deepest = 0; |
|
| 565 | - $local_ele = $this->deepestNode($m, 0, null, $local_deepest); |
|
| 566 | - |
|
| 567 | - // Replace with the new deepest. |
|
| 568 | - if ($local_deepest > $deepest) { |
|
| 569 | - $winner = new SplObjectStorage(); |
|
| 570 | - foreach ($local_ele as $lele) { |
|
| 571 | - $winner->attach($lele); |
|
| 572 | - } |
|
| 573 | - $deepest = $local_deepest; |
|
| 574 | - } // Augument with other equally deep elements. |
|
| 575 | - elseif ($local_deepest === $deepest) { |
|
| 576 | - foreach ($local_ele as $lele) { |
|
| 577 | - $winner->attach($lele); |
|
| 578 | - } |
|
| 579 | - } |
|
| 580 | - } |
|
| 581 | - |
|
| 582 | - return $this->inst($winner, null); |
|
| 583 | - } |
|
| 584 | - |
|
| 585 | - /** |
|
| 586 | - * Add a class to all elements in the current DOMQuery. |
|
| 587 | - * |
|
| 588 | - * This searchers for a class attribute on each item wrapped by the current |
|
| 589 | - * DOMNode object. If no attribute is found, a new one is added and its value |
|
| 590 | - * is set to $class. If a class attribute is found, then the value is appended |
|
| 591 | - * on to the end. |
|
| 592 | - * |
|
| 593 | - * @param string $class |
|
| 594 | - * The name of the class. |
|
| 595 | - * |
|
| 596 | - * @return DOMQuery |
|
| 597 | - * Returns the DOMQuery object. |
|
| 598 | - * @see css() |
|
| 599 | - * @see attr() |
|
| 600 | - * @see removeClass() |
|
| 601 | - * @see hasClass() |
|
| 602 | - */ |
|
| 603 | - public function addClass($class) |
|
| 604 | - { |
|
| 605 | - foreach ($this->matches as $m) { |
|
| 606 | - if ($m->hasAttribute('class')) { |
|
| 607 | - $val = $m->getAttribute('class'); |
|
| 608 | - $m->setAttribute('class', $val . ' ' . $class); |
|
| 609 | - } else { |
|
| 610 | - $m->setAttribute('class', $class); |
|
| 611 | - } |
|
| 612 | - } |
|
| 613 | - |
|
| 614 | - return $this; |
|
| 615 | - } |
|
| 616 | - |
|
| 617 | - /** |
|
| 618 | - * Remove the named class from any element in the DOMQuery that has it. |
|
| 619 | - * |
|
| 620 | - * This may result in the entire class attribute being removed. If there |
|
| 621 | - * are other items in the class attribute, though, they will not be removed. |
|
| 622 | - * |
|
| 623 | - * Example: |
|
| 624 | - * Consider this XML: |
|
| 625 | - * |
|
| 626 | - * @code |
|
| 627 | - * <element class="first second"/> |
|
| 628 | - * @endcode |
|
| 629 | - * |
|
| 630 | - * Executing this fragment of code will remove only the 'first' class: |
|
| 631 | - * @code |
|
| 632 | - * qp(document, 'element')->removeClass('first'); |
|
| 633 | - * @endcode |
|
| 634 | - * |
|
| 635 | - * The resulting XML will be: |
|
| 636 | - * @code |
|
| 637 | - * <element class="second"/> |
|
| 638 | - * @endcode |
|
| 639 | - * |
|
| 640 | - * To remove the entire 'class' attribute, you should use {@see removeAttr()}. |
|
| 641 | - * |
|
| 642 | - * @param string $class |
|
| 643 | - * The class name to remove. |
|
| 644 | - * |
|
| 645 | - * @return DOMQuery |
|
| 646 | - * The modified DOMNode object. |
|
| 647 | - * @see attr() |
|
| 648 | - * @see addClass() |
|
| 649 | - * @see hasClass() |
|
| 650 | - */ |
|
| 651 | - public function removeClass($class = false): Query |
|
| 652 | - { |
|
| 653 | - if (empty($class)) { |
|
| 654 | - foreach ($this->matches as $m) { |
|
| 655 | - $m->removeAttribute('class'); |
|
| 656 | - } |
|
| 657 | - } else { |
|
| 658 | - $to_remove = array_filter(explode(' ', $class)); |
|
| 659 | - foreach ($this->matches as $m) { |
|
| 660 | - if ($m->hasAttribute('class')) { |
|
| 661 | - $vals = array_filter(explode(' ', $m->getAttribute('class'))); |
|
| 662 | - $buf = []; |
|
| 663 | - foreach ($vals as $v) { |
|
| 664 | - if (! in_array($v, $to_remove)) { |
|
| 665 | - $buf[] = $v; |
|
| 666 | - } |
|
| 667 | - } |
|
| 668 | - if (empty($buf)) { |
|
| 669 | - $m->removeAttribute('class'); |
|
| 670 | - } else { |
|
| 671 | - $m->setAttribute('class', implode(' ', $buf)); |
|
| 672 | - } |
|
| 673 | - } |
|
| 674 | - } |
|
| 675 | - } |
|
| 676 | - |
|
| 677 | - return $this; |
|
| 678 | - } |
|
| 679 | - |
|
| 680 | - /** |
|
| 681 | - * Detach any items from the list if they match the selector. |
|
| 682 | - * |
|
| 683 | - * In other words, each item that matches the selector will be removed |
|
| 684 | - * from the DOM document. The returned DOMQuery wraps the list of |
|
| 685 | - * removed elements. |
|
| 686 | - * |
|
| 687 | - * If no selector is specified, this will remove all current matches from |
|
| 688 | - * the document. |
|
| 689 | - * |
|
| 690 | - * @param string $selector |
|
| 691 | - * A CSS Selector. |
|
| 692 | - * |
|
| 693 | - * @return DOMQuery |
|
| 694 | - * The Query path wrapping a list of removed items. |
|
| 695 | - * @throws ParseException |
|
| 696 | - * @see replaceWith() |
|
| 697 | - * @see removeChildren() |
|
| 698 | - * @since 2.1 |
|
| 699 | - * @author eabrand |
|
| 700 | - * @see replaceAll() |
|
| 701 | - */ |
|
| 702 | - public function detach($selector = null): Query |
|
| 703 | - { |
|
| 704 | - if (null !== $selector) { |
|
| 705 | - $this->find($selector); |
|
| 706 | - } |
|
| 707 | - |
|
| 708 | - $found = new SplObjectStorage(); |
|
| 709 | - $this->last = $this->matches; |
|
| 710 | - foreach ($this->matches as $item) { |
|
| 711 | - // The item returned is (according to docs) different from |
|
| 712 | - // the one passed in, so we have to re-store it. |
|
| 713 | - $found->attach($item->parentNode->removeChild($item)); |
|
| 714 | - } |
|
| 715 | - |
|
| 716 | - return $this->inst($found, null); |
|
| 717 | - } |
|
| 718 | - |
|
| 719 | - /** |
|
| 720 | - * Attach any items from the list if they match the selector. |
|
| 721 | - * |
|
| 722 | - * If no selector is specified, this will remove all current matches from |
|
| 723 | - * the document. |
|
| 724 | - * |
|
| 725 | - * @param DOMQuery $dest |
|
| 726 | - * A DOMQuery Selector. |
|
| 727 | - * |
|
| 728 | - * @return DOMQuery |
|
| 729 | - * The Query path wrapping a list of removed items. |
|
| 730 | - * @throws QueryPath |
|
| 731 | - * @throws Exception |
|
| 732 | - * @see removeChildren() |
|
| 733 | - * @since 2.1 |
|
| 734 | - * @author eabrand |
|
| 735 | - * @see replaceAll() |
|
| 736 | - * @see replaceWith() |
|
| 737 | - */ |
|
| 738 | - public function attach(DOMQuery $dest): Query |
|
| 739 | - { |
|
| 740 | - foreach ($this->last as $m) { |
|
| 741 | - $dest->append($m); |
|
| 742 | - } |
|
| 743 | - |
|
| 744 | - return $this; |
|
| 745 | - } |
|
| 746 | - |
|
| 747 | - /** |
|
| 748 | - * Append the current elements to the destination passed into the function. |
|
| 749 | - * |
|
| 750 | - * This cycles through all of the current matches and appends them to |
|
| 751 | - * the context given in $destination. If a selector is provided then the |
|
| 752 | - * $destination is queried (using that selector) prior to the data being |
|
| 753 | - * appended. The data is then appended to the found items. |
|
| 754 | - * |
|
| 755 | - * @param DOMQuery $dest |
|
| 756 | - * A DOMQuery object that will be appended to. |
|
| 757 | - * |
|
| 758 | - * @return DOMQuery |
|
| 759 | - * The original DOMQuery, unaltered. Only the destination DOMQuery will |
|
| 760 | - * be modified. |
|
| 761 | - * @throws QueryPath::Exception |
|
| 762 | - * Thrown if $data is an unsupported object type. |
|
| 763 | - * @throws Exception |
|
| 764 | - * @see append() |
|
| 765 | - * @see prependTo() |
|
| 766 | - */ |
|
| 767 | - public function appendTo(DOMQuery $dest): Query |
|
| 768 | - { |
|
| 769 | - foreach ($this->matches as $m) { |
|
| 770 | - $dest->append($m); |
|
| 771 | - } |
|
| 772 | - |
|
| 773 | - return $this; |
|
| 774 | - } |
|
| 775 | - |
|
| 776 | - /** |
|
| 777 | - * Remove any items from the list if they match the selector. |
|
| 778 | - * |
|
| 779 | - * In other words, each item that matches the selector will be remove |
|
| 780 | - * from the DOM document. The returned DOMQuery wraps the list of |
|
| 781 | - * removed elements. |
|
| 782 | - * |
|
| 783 | - * If no selector is specified, this will remove all current matches from |
|
| 784 | - * the document. |
|
| 785 | - * |
|
| 786 | - * @param string $selector |
|
| 787 | - * A CSS Selector. |
|
| 788 | - * |
|
| 789 | - * @return DOMQuery |
|
| 790 | - * The Query path wrapping a list of removed items. |
|
| 791 | - * @throws ParseException |
|
| 792 | - * @see replaceWith() |
|
| 793 | - * @see removeChildren() |
|
| 794 | - * @see replaceAll() |
|
| 795 | - */ |
|
| 796 | - public function remove($selector = null): Query |
|
| 797 | - { |
|
| 798 | - if (! empty($selector)) { |
|
| 799 | - // Do a non-destructive find. |
|
| 800 | - $query = new QueryPathEventHandler($this->matches); |
|
| 801 | - $query->find($selector); |
|
| 802 | - $matches = $query->getMatches(); |
|
| 803 | - } else { |
|
| 804 | - $matches = $this->matches; |
|
| 805 | - } |
|
| 806 | - |
|
| 807 | - $found = new SplObjectStorage(); |
|
| 808 | - foreach ($matches as $item) { |
|
| 809 | - // The item returned is (according to docs) different from |
|
| 810 | - // the one passed in, so we have to re-store it. |
|
| 811 | - $found->attach($item->parentNode->removeChild($item)); |
|
| 812 | - } |
|
| 813 | - |
|
| 814 | - // Return a clone DOMQuery with just the removed items. If |
|
| 815 | - // no items are found, this will return an empty DOMQuery. |
|
| 816 | - return count($found) === 0 ? new static() : new static($found); |
|
| 817 | - } |
|
| 818 | - |
|
| 819 | - /** |
|
| 820 | - * This replaces everything that matches the selector with the first value |
|
| 821 | - * in the current list. |
|
| 822 | - * |
|
| 823 | - * This is the reverse of replaceWith. |
|
| 824 | - * |
|
| 825 | - * Unlike jQuery, DOMQuery cannot assume a default document. Consequently, |
|
| 826 | - * you must specify the intended destination document. If it is omitted, the |
|
| 827 | - * present document is assumed to be tthe document. However, that can result |
|
| 828 | - * in undefined behavior if the selector and the replacement are not sufficiently |
|
| 829 | - * distinct. |
|
| 830 | - * |
|
| 831 | - * @param string $selector |
|
| 832 | - * The selector. |
|
| 833 | - * @param DOMDocument $document |
|
| 834 | - * The destination document. |
|
| 835 | - * |
|
| 836 | - * @return DOMQuery |
|
| 837 | - * The DOMQuery wrapping the modified document. |
|
| 838 | - * @throws ParseException |
|
| 839 | - * @see remove() |
|
| 840 | - * @see replaceWith() |
|
| 841 | - * @deprecated Due to the fact that this is not a particularly friendly method, |
|
| 842 | - * and that it can be easily replicated using {@see replaceWith()}, it is to be |
|
| 843 | - * considered deprecated. |
|
| 844 | - */ |
|
| 845 | - public function replaceAll($selector, DOMDocument $document): Query |
|
| 846 | - { |
|
| 847 | - $replacement = $this->matches->count() > 0 ? $this->getFirstMatch() : $this->document->createTextNode(''); |
|
| 848 | - |
|
| 849 | - $c = new QueryPathEventHandler($document); |
|
| 850 | - $c->find($selector); |
|
| 851 | - $temp = $c->getMatches(); |
|
| 852 | - foreach ($temp as $item) { |
|
| 853 | - $node = $replacement->cloneNode(); |
|
| 854 | - $node = $document->importNode($node); |
|
| 855 | - $item->parentNode->replaceChild($node, $item); |
|
| 856 | - } |
|
| 857 | - |
|
| 858 | - return QueryPath::with($document, null, $this->options); |
|
| 859 | - } |
|
| 860 | - |
|
| 861 | - /** |
|
| 862 | - * Add more elements to the current set of matches. |
|
| 863 | - * |
|
| 864 | - * This begins the new query at the top of the DOM again. The results found |
|
| 865 | - * when running this selector are then merged into the existing results. In |
|
| 866 | - * this way, you can add additional elements to the existing set. |
|
| 867 | - * |
|
| 868 | - * @param string $selector |
|
| 869 | - * A valid selector. |
|
| 870 | - * |
|
| 871 | - * @return DOMQuery |
|
| 872 | - * The DOMQuery object with the newly added elements. |
|
| 873 | - * @see append() |
|
| 874 | - * @see after() |
|
| 875 | - * @see andSelf() |
|
| 876 | - * @see end() |
|
| 877 | - */ |
|
| 878 | - public function add($selector): Query |
|
| 879 | - { |
|
| 880 | - |
|
| 881 | - // This is destructive, so we need to set $last: |
|
| 882 | - $this->last = $this->matches; |
|
| 883 | - |
|
| 884 | - foreach (QueryPath::with($this->document, $selector, $this->options)->get() as $item) { |
|
| 885 | - $this->matches->attach($item); |
|
| 886 | - } |
|
| 887 | - |
|
| 888 | - return $this; |
|
| 889 | - } |
|
| 890 | - |
|
| 891 | - /** |
|
| 892 | - * Remove all child nodes. |
|
| 893 | - * |
|
| 894 | - * This is equivalent to jQuery's empty() function. (However, empty() is a |
|
| 895 | - * PHP built-in, and cannot be used as a method name.) |
|
| 896 | - * |
|
| 897 | - * @return DOMQuery |
|
| 898 | - * The DOMQuery object with the child nodes removed. |
|
| 899 | - * @see replaceWith() |
|
| 900 | - * @see replaceAll() |
|
| 901 | - * @see remove() |
|
| 902 | - */ |
|
| 903 | - public function removeChildren(): Query |
|
| 904 | - { |
|
| 905 | - foreach ($this->matches as $m) { |
|
| 906 | - while ($kid = $m->firstChild) { |
|
| 907 | - $m->removeChild($kid); |
|
| 908 | - } |
|
| 909 | - } |
|
| 910 | - |
|
| 911 | - return $this; |
|
| 912 | - } |
|
| 913 | - |
|
| 914 | - /** |
|
| 915 | - * Get/set an attribute. |
|
| 916 | - * - If no parameters are specified, this returns an associative array of all |
|
| 917 | - * name/value pairs. |
|
| 918 | - * - If both $name and $value are set, then this will set the attribute name/value |
|
| 919 | - * pair for all items in this object. |
|
| 920 | - * - If $name is set, and is an array, then |
|
| 921 | - * all attributes in the array will be set for all items in this object. |
|
| 922 | - * - If $name is a string and is set, then the attribute value will be returned. |
|
| 923 | - * |
|
| 924 | - * When an attribute value is retrieved, only the attribute value of the FIRST |
|
| 925 | - * match is returned. |
|
| 926 | - * |
|
| 927 | - * @param mixed $name |
|
| 928 | - * The name of the attribute or an associative array of name/value pairs. |
|
| 929 | - * @param string $value |
|
| 930 | - * A value (used only when setting an individual property). |
|
| 931 | - * |
|
| 932 | - * @return mixed |
|
| 933 | - * If this was a setter request, return the DOMQuery object. If this was |
|
| 934 | - * an access request (getter), return the string value. |
|
| 935 | - * @see removeAttr() |
|
| 936 | - * @see tag() |
|
| 937 | - * @see hasAttr() |
|
| 938 | - * @see hasClass() |
|
| 939 | - */ |
|
| 940 | - public function attr($name = null, $value = null) |
|
| 941 | - { |
|
| 942 | - // Default case: Return all attributes as an assoc array. |
|
| 943 | - if (is_null($name)) { |
|
| 944 | - if ($this->matches->count() === 0) { |
|
| 945 | - return null; |
|
| 946 | - } |
|
| 947 | - $ele = $this->getFirstMatch(); |
|
| 948 | - $buffer = []; |
|
| 949 | - |
|
| 950 | - // This does not appear to be part of the DOM |
|
| 951 | - // spec. Nor is it documented. But it works. |
|
| 952 | - foreach ($ele->attributes as $name => $attrNode) { |
|
| 953 | - $buffer[$name] = $attrNode->value; |
|
| 954 | - } |
|
| 955 | - |
|
| 956 | - return $buffer; |
|
| 957 | - } |
|
| 958 | - |
|
| 959 | - // multi-setter |
|
| 960 | - if (is_array($name)) { |
|
| 961 | - foreach ($name as $k => $v) { |
|
| 962 | - foreach ($this->matches as $m) { |
|
| 963 | - $m->setAttribute($k, $v); |
|
| 964 | - } |
|
| 965 | - } |
|
| 966 | - |
|
| 967 | - return $this; |
|
| 968 | - } |
|
| 969 | - // setter |
|
| 970 | - if (isset($value)) { |
|
| 971 | - foreach ($this->matches as $m) { |
|
| 972 | - $m->setAttribute($name, $value); |
|
| 973 | - } |
|
| 974 | - |
|
| 975 | - return $this; |
|
| 976 | - } |
|
| 977 | - |
|
| 978 | - //getter |
|
| 979 | - if ($this->matches->count() === 0) { |
|
| 980 | - return null; |
|
| 981 | - } |
|
| 982 | - |
|
| 983 | - // Special node type handler: |
|
| 984 | - if ($name === 'nodeType') { |
|
| 985 | - return $this->getFirstMatch()->nodeType; |
|
| 986 | - } |
|
| 987 | - |
|
| 988 | - // Always return first match's attr. |
|
| 989 | - return $this->getFirstMatch()->getAttribute($name); |
|
| 990 | - } |
|
| 991 | - |
|
| 992 | - /** |
|
| 993 | - * Set/get a CSS value for the current element(s). |
|
| 994 | - * This sets the CSS value for each element in the DOMQuery object. |
|
| 995 | - * It does this by setting (or getting) the style attribute (without a namespace). |
|
| 996 | - * |
|
| 997 | - * For example, consider this code: |
|
| 998 | - * |
|
| 999 | - * @code |
|
| 1000 | - * <?php |
|
| 1001 | - * qp(HTML_STUB, 'body')->css('background-color','red')->html(); |
|
| 1002 | - * ?> |
|
| 1003 | - * @endcode |
|
| 1004 | - * This will return the following HTML: |
|
| 1005 | - * @code |
|
| 1006 | - * <body style="background-color: red"/> |
|
| 1007 | - * @endcode |
|
| 1008 | - * |
|
| 1009 | - * If no parameters are passed into this function, then the current style |
|
| 1010 | - * element will be returned unparsed. Example: |
|
| 1011 | - * @code |
|
| 1012 | - * <?php |
|
| 1013 | - * qp(HTML_STUB, 'body')->css('background-color','red')->css(); |
|
| 1014 | - * ?> |
|
| 1015 | - * @endcode |
|
| 1016 | - * This will return the following: |
|
| 1017 | - * @code |
|
| 1018 | - * background-color: red |
|
| 1019 | - * @endcode |
|
| 1020 | - * |
|
| 1021 | - * As of QueryPath 2.1, existing style attributes will be merged with new attributes. |
|
| 1022 | - * (In previous versions of QueryPath, a call to css() overwrite the existing style |
|
| 1023 | - * values). |
|
| 1024 | - * |
|
| 1025 | - * @param mixed $name |
|
| 1026 | - * If this is a string, it will be used as a CSS name. If it is an array, |
|
| 1027 | - * this will assume it is an array of name/value pairs of CSS rules. It will |
|
| 1028 | - * apply all rules to all elements in the set. |
|
| 1029 | - * @param string $value |
|
| 1030 | - * The value to set. This is only set if $name is a string. |
|
| 1031 | - * |
|
| 1032 | - * @return DOMQuery |
|
| 1033 | - */ |
|
| 1034 | - public function css($name = null, $value = '') |
|
| 1035 | - { |
|
| 1036 | - if (empty($name)) { |
|
| 1037 | - return $this->attr('style'); |
|
| 1038 | - } |
|
| 1039 | - |
|
| 1040 | - // Get any existing CSS. |
|
| 1041 | - $css = []; |
|
| 1042 | - foreach ($this->matches as $match) { |
|
| 1043 | - $style = $match->getAttribute('style'); |
|
| 1044 | - if (! empty($style)) { |
|
| 1045 | - // XXX: Is this sufficient? |
|
| 1046 | - $style_array = explode(';', $style); |
|
| 1047 | - foreach ($style_array as $item) { |
|
| 1048 | - $item = trim($item); |
|
| 1049 | - |
|
| 1050 | - // Skip empty attributes. |
|
| 1051 | - if ($item === '') { |
|
| 1052 | - continue; |
|
| 1053 | - } |
|
| 1054 | - |
|
| 1055 | - [$css_att, $css_val] = explode(':', $item, 2); |
|
| 1056 | - $css[$css_att] = trim($css_val); |
|
| 1057 | - } |
|
| 1058 | - } |
|
| 1059 | - } |
|
| 1060 | - |
|
| 1061 | - if (is_array($name)) { |
|
| 1062 | - // Use array_merge instead of + to preserve order. |
|
| 1063 | - $css = array_merge($css, $name); |
|
| 1064 | - } else { |
|
| 1065 | - $css[$name] = $value; |
|
| 1066 | - } |
|
| 1067 | - |
|
| 1068 | - // Collapse CSS into a string. |
|
| 1069 | - $format = '%s: %s;'; |
|
| 1070 | - $css_string = ''; |
|
| 1071 | - foreach ($css as $n => $v) { |
|
| 1072 | - $css_string .= sprintf($format, $n, trim($v)); |
|
| 1073 | - } |
|
| 1074 | - |
|
| 1075 | - $this->attr('style', $css_string); |
|
| 1076 | - |
|
| 1077 | - return $this; |
|
| 1078 | - } |
|
| 18 | + /** |
|
| 19 | + * Empty everything within the specified element. |
|
| 20 | + * |
|
| 21 | + * A convenience function for removeChildren(). This is equivalent to jQuery's |
|
| 22 | + * empty() function. However, `empty` is a built-in in PHP, and cannot be used as a |
|
| 23 | + * function name. |
|
| 24 | + * |
|
| 25 | + * @return DOMQuery |
|
| 26 | + * The DOMQuery object with the newly emptied elements. |
|
| 27 | + * @see removeChildren() |
|
| 28 | + * @since 2.1 |
|
| 29 | + * @author eabrand |
|
| 30 | + * @deprecated The removeChildren() function is the preferred method. |
|
| 31 | + */ |
|
| 32 | + public function emptyElement(): Query |
|
| 33 | + { |
|
| 34 | + $this->removeChildren(); |
|
| 35 | + |
|
| 36 | + return $this; |
|
| 37 | + } |
|
| 38 | + |
|
| 39 | + /** |
|
| 40 | + * Insert the given markup as the last child. |
|
| 41 | + * |
|
| 42 | + * The markup will be inserted into each match in the set. |
|
| 43 | + * |
|
| 44 | + * The same element cannot be inserted multiple times into a document. DOM |
|
| 45 | + * documents do not allow a single object to be inserted multiple times |
|
| 46 | + * into the DOM. To insert the same XML repeatedly, we must first clone |
|
| 47 | + * the object. This has one practical implication: Once you have inserted |
|
| 48 | + * an element into the object, you cannot further manipulate the original |
|
| 49 | + * element and expect the changes to be replciated in the appended object. |
|
| 50 | + * (They are not the same -- there is no shared reference.) Instead, you |
|
| 51 | + * will need to retrieve the appended object and operate on that. |
|
| 52 | + * |
|
| 53 | + * @param mixed $data |
|
| 54 | + * This can be either a string (the usual case), or a DOM Element. |
|
| 55 | + * |
|
| 56 | + * @return DOMQuery |
|
| 57 | + * The DOMQuery object. |
|
| 58 | + * @throws QueryPath::Exception |
|
| 59 | + * Thrown if $data is an unsupported object type. |
|
| 60 | + * @throws Exception |
|
| 61 | + * @see appendTo() |
|
| 62 | + * @see prepend() |
|
| 63 | + */ |
|
| 64 | + public function append($data): Query |
|
| 65 | + { |
|
| 66 | + $data = $this->prepareInsert($data); |
|
| 67 | + if (isset($data)) { |
|
| 68 | + if (empty($this->document->documentElement) && $this->matches->count() === 0) { |
|
| 69 | + // Then we assume we are writing to the doc root |
|
| 70 | + $this->document->appendChild($data); |
|
| 71 | + $found = new SplObjectStorage(); |
|
| 72 | + $found->attach($this->document->documentElement); |
|
| 73 | + $this->setMatches($found); |
|
| 74 | + } else { |
|
| 75 | + // You can only append in item once. So in cases where we |
|
| 76 | + // need to append multiple times, we have to clone the node. |
|
| 77 | + foreach ($this->matches as $m) { |
|
| 78 | + // DOMDocumentFragments are even more troublesome, as they don't |
|
| 79 | + // always clone correctly. So we have to clone their children. |
|
| 80 | + if ($data instanceof DOMDocumentFragment) { |
|
| 81 | + foreach ($data->childNodes as $n) { |
|
| 82 | + $m->appendChild($n->cloneNode(true)); |
|
| 83 | + } |
|
| 84 | + } else { |
|
| 85 | + // Otherwise a standard clone will do. |
|
| 86 | + $m->appendChild($data->cloneNode(true)); |
|
| 87 | + } |
|
| 88 | + |
|
| 89 | + } |
|
| 90 | + } |
|
| 91 | + |
|
| 92 | + } |
|
| 93 | + |
|
| 94 | + return $this; |
|
| 95 | + } |
|
| 96 | + |
|
| 97 | + /** |
|
| 98 | + * Insert the given markup as the first child. |
|
| 99 | + * |
|
| 100 | + * The markup will be inserted into each match in the set. |
|
| 101 | + * |
|
| 102 | + * @param mixed $data |
|
| 103 | + * This can be either a string (the usual case), or a DOM Element. |
|
| 104 | + * |
|
| 105 | + * @return DOMQuery |
|
| 106 | + * @throws QueryPath::Exception |
|
| 107 | + * Thrown if $data is an unsupported object type. |
|
| 108 | + * @throws Exception |
|
| 109 | + * @see after() |
|
| 110 | + * @see prependTo() |
|
| 111 | + * @see append() |
|
| 112 | + * @see before() |
|
| 113 | + */ |
|
| 114 | + public function prepend($data): Query |
|
| 115 | + { |
|
| 116 | + $data = $this->prepareInsert($data); |
|
| 117 | + if (isset($data)) { |
|
| 118 | + foreach ($this->matches as $m) { |
|
| 119 | + $ins = $data->cloneNode(true); |
|
| 120 | + if ($m->hasChildNodes()) { |
|
| 121 | + $m->insertBefore($ins, $m->childNodes->item(0)); |
|
| 122 | + } else { |
|
| 123 | + $m->appendChild($ins); |
|
| 124 | + } |
|
| 125 | + } |
|
| 126 | + } |
|
| 127 | + |
|
| 128 | + return $this; |
|
| 129 | + } |
|
| 130 | + |
|
| 131 | + /** |
|
| 132 | + * Take all nodes in the current object and prepend them to the children nodes of |
|
| 133 | + * each matched node in the passed-in DOMQuery object. |
|
| 134 | + * |
|
| 135 | + * This will iterate through each item in the current DOMQuery object and |
|
| 136 | + * add each item to the beginning of the children of each element in the |
|
| 137 | + * passed-in DOMQuery object. |
|
| 138 | + * |
|
| 139 | + * @param DOMQuery $dest |
|
| 140 | + * The destination DOMQuery object. |
|
| 141 | + * |
|
| 142 | + * @return DOMQuery |
|
| 143 | + * The original DOMQuery, unmodified. NOT the destination DOMQuery. |
|
| 144 | + * @throws QueryPath::Exception |
|
| 145 | + * Thrown if $data is an unsupported object type. |
|
| 146 | + * @see appendTo() |
|
| 147 | + * @see insertBefore() |
|
| 148 | + * @see insertAfter() |
|
| 149 | + * @see prepend() |
|
| 150 | + */ |
|
| 151 | + public function prependTo(Query $dest) |
|
| 152 | + { |
|
| 153 | + foreach ($this->matches as $m) { |
|
| 154 | + $dest->prepend($m); |
|
| 155 | + } |
|
| 156 | + |
|
| 157 | + return $this; |
|
| 158 | + } |
|
| 159 | + |
|
| 160 | + /** |
|
| 161 | + * Insert the given data before each element in the current set of matches. |
|
| 162 | + * |
|
| 163 | + * This will take the give data (XML or HTML) and put it before each of the items that |
|
| 164 | + * the DOMQuery object currently contains. Contrast this with after(). |
|
| 165 | + * |
|
| 166 | + * @param mixed $data |
|
| 167 | + * The data to be inserted. This can be XML in a string, a DomFragment, a DOMElement, |
|
| 168 | + * or the other usual suspects. (See {@link qp()}). |
|
| 169 | + * |
|
| 170 | + * @return DOMQuery |
|
| 171 | + * Returns the DOMQuery with the new modifications. The list of elements currently |
|
| 172 | + * selected will remain the same. |
|
| 173 | + * @throws QueryPath::Exception |
|
| 174 | + * Thrown if $data is an unsupported object type. |
|
| 175 | + * @throws Exception |
|
| 176 | + * @see append() |
|
| 177 | + * @see prepend() |
|
| 178 | + * @see insertBefore() |
|
| 179 | + * @see after() |
|
| 180 | + */ |
|
| 181 | + public function before($data): Query |
|
| 182 | + { |
|
| 183 | + $data = $this->prepareInsert($data); |
|
| 184 | + foreach ($this->matches as $m) { |
|
| 185 | + $ins = $data->cloneNode(true); |
|
| 186 | + $m->parentNode->insertBefore($ins, $m); |
|
| 187 | + } |
|
| 188 | + |
|
| 189 | + return $this; |
|
| 190 | + } |
|
| 191 | + |
|
| 192 | + /** |
|
| 193 | + * Insert the current elements into the destination document. |
|
| 194 | + * The items are inserted before each element in the given DOMQuery document. |
|
| 195 | + * That is, they will be siblings with the current elements. |
|
| 196 | + * |
|
| 197 | + * @param Query $dest |
|
| 198 | + * Destination DOMQuery document. |
|
| 199 | + * |
|
| 200 | + * @return DOMQuery |
|
| 201 | + * The current DOMQuery object, unaltered. Only the destination DOMQuery |
|
| 202 | + * object is altered. |
|
| 203 | + * @throws QueryPath::Exception |
|
| 204 | + * Thrown if $data is an unsupported object type. |
|
| 205 | + * @see insertAfter() |
|
| 206 | + * @see appendTo() |
|
| 207 | + * @see before() |
|
| 208 | + */ |
|
| 209 | + public function insertBefore(Query $dest): Query |
|
| 210 | + { |
|
| 211 | + foreach ($this->matches as $m) { |
|
| 212 | + $dest->before($m); |
|
| 213 | + } |
|
| 214 | + |
|
| 215 | + return $this; |
|
| 216 | + } |
|
| 217 | + |
|
| 218 | + /** |
|
| 219 | + * Insert the contents of the current DOMQuery after the nodes in the |
|
| 220 | + * destination DOMQuery object. |
|
| 221 | + * |
|
| 222 | + * @param Query $dest |
|
| 223 | + * Destination object where the current elements will be deposited. |
|
| 224 | + * |
|
| 225 | + * @return DOMQuery |
|
| 226 | + * The present DOMQuery, unaltered. Only the destination object is altered. |
|
| 227 | + * @throws QueryPath::Exception |
|
| 228 | + * Thrown if $data is an unsupported object type. |
|
| 229 | + * @see insertBefore() |
|
| 230 | + * @see append() |
|
| 231 | + * @see after() |
|
| 232 | + */ |
|
| 233 | + public function insertAfter(Query $dest): Query |
|
| 234 | + { |
|
| 235 | + foreach ($this->matches as $m) { |
|
| 236 | + $dest->after($m); |
|
| 237 | + } |
|
| 238 | + |
|
| 239 | + return $this; |
|
| 240 | + } |
|
| 241 | + |
|
| 242 | + /** |
|
| 243 | + * Insert the given data after each element in the current DOMQuery object. |
|
| 244 | + * |
|
| 245 | + * This inserts the element as a peer to the currently matched elements. |
|
| 246 | + * Contrast this with {@link append()}, which inserts the data as children |
|
| 247 | + * of matched elements. |
|
| 248 | + * |
|
| 249 | + * @param mixed $data |
|
| 250 | + * The data to be appended. |
|
| 251 | + * |
|
| 252 | + * @return DOMQuery |
|
| 253 | + * The DOMQuery object (with the items inserted). |
|
| 254 | + * @throws QueryPath::Exception |
|
| 255 | + * Thrown if $data is an unsupported object type. |
|
| 256 | + * @throws Exception |
|
| 257 | + * @see before() |
|
| 258 | + * @see append() |
|
| 259 | + */ |
|
| 260 | + public function after($data): Query |
|
| 261 | + { |
|
| 262 | + if (empty($data)) { |
|
| 263 | + return $this; |
|
| 264 | + } |
|
| 265 | + $data = $this->prepareInsert($data); |
|
| 266 | + foreach ($this->matches as $m) { |
|
| 267 | + $ins = $data->cloneNode(true); |
|
| 268 | + if (isset($m->nextSibling)) { |
|
| 269 | + $m->parentNode->insertBefore($ins, $m->nextSibling); |
|
| 270 | + } else { |
|
| 271 | + $m->parentNode->appendChild($ins); |
|
| 272 | + } |
|
| 273 | + } |
|
| 274 | + |
|
| 275 | + return $this; |
|
| 276 | + } |
|
| 277 | + |
|
| 278 | + /** |
|
| 279 | + * Replace the existing element(s) in the list with a new one. |
|
| 280 | + * |
|
| 281 | + * @param mixed $new |
|
| 282 | + * A DOMElement or XML in a string. This will replace all elements |
|
| 283 | + * currently wrapped in the DOMQuery object. |
|
| 284 | + * |
|
| 285 | + * @return DOMQuery |
|
| 286 | + * The DOMQuery object wrapping <b>the items that were removed</b>. |
|
| 287 | + * This remains consistent with the jQuery API. |
|
| 288 | + * @throws Exception |
|
| 289 | + * @throws ParseException |
|
| 290 | + * @throws QueryPath |
|
| 291 | + * @see append() |
|
| 292 | + * @see prepend() |
|
| 293 | + * @see before() |
|
| 294 | + * @see after() |
|
| 295 | + * @see remove() |
|
| 296 | + * @see replaceAll() |
|
| 297 | + */ |
|
| 298 | + public function replaceWith($new): Query |
|
| 299 | + { |
|
| 300 | + $data = $this->prepareInsert($new); |
|
| 301 | + $found = new SplObjectStorage(); |
|
| 302 | + foreach ($this->matches as $m) { |
|
| 303 | + $parent = $m->parentNode; |
|
| 304 | + $parent->insertBefore($data->cloneNode(true), $m); |
|
| 305 | + $found->attach($parent->removeChild($m)); |
|
| 306 | + } |
|
| 307 | + |
|
| 308 | + return $this->inst($found, null); |
|
| 309 | + } |
|
| 310 | + |
|
| 311 | + /** |
|
| 312 | + * Remove the parent element from the selected node or nodes. |
|
| 313 | + * |
|
| 314 | + * This takes the given list of nodes and "unwraps" them, moving them out of their parent |
|
| 315 | + * node, and then deleting the parent node. |
|
| 316 | + * |
|
| 317 | + * For example, consider this: |
|
| 318 | + * |
|
| 319 | + * @code |
|
| 320 | + * <root><wrapper><content/></wrapper></root> |
|
| 321 | + * @endcode |
|
| 322 | + * |
|
| 323 | + * Now we can run this code: |
|
| 324 | + * @code |
|
| 325 | + * qp($xml, 'content')->unwrap(); |
|
| 326 | + * @endcode |
|
| 327 | + * |
|
| 328 | + * This will result in: |
|
| 329 | + * |
|
| 330 | + * @code |
|
| 331 | + * <root><content/></root> |
|
| 332 | + * @endcode |
|
| 333 | + * This is the opposite of wrap(). |
|
| 334 | + * |
|
| 335 | + * <b>The root element cannot be unwrapped.</b> It has no parents. |
|
| 336 | + * If you attempt to use unwrap on a root element, this will throw a |
|
| 337 | + * QueryPath::Exception. (You can, however, "Unwrap" a child that is |
|
| 338 | + * a direct descendant of the root element. This will remove the root |
|
| 339 | + * element, and replace the child as the root element. Be careful, though. |
|
| 340 | + * You cannot set more than one child as a root element.) |
|
| 341 | + * |
|
| 342 | + * @return DOMQuery |
|
| 343 | + * The DOMQuery object, with the same element(s) selected. |
|
| 344 | + * @throws Exception |
|
| 345 | + * @see wrap() |
|
| 346 | + * @since 2.1 |
|
| 347 | + * @author mbutcher |
|
| 348 | + */ |
|
| 349 | + public function unwrap(): Query |
|
| 350 | + { |
|
| 351 | + // We do this in two loops in order to |
|
| 352 | + // capture the case where two matches are |
|
| 353 | + // under the same parent. Othwerwise we might |
|
| 354 | + // remove a match before we can move it. |
|
| 355 | + $parents = new SplObjectStorage(); |
|
| 356 | + foreach ($this->matches as $m) { |
|
| 357 | + |
|
| 358 | + // Cannot unwrap the root element. |
|
| 359 | + if ($m->isSameNode($m->ownerDocument->documentElement)) { |
|
| 360 | + throw new Exception('Cannot unwrap the root element.'); |
|
| 361 | + } |
|
| 362 | + |
|
| 363 | + // Move children to peer of parent. |
|
| 364 | + $parent = $m->parentNode; |
|
| 365 | + $old = $parent->removeChild($m); |
|
| 366 | + $parent->parentNode->insertBefore($old, $parent); |
|
| 367 | + $parents->attach($parent); |
|
| 368 | + } |
|
| 369 | + |
|
| 370 | + // Now that all the children are moved, we |
|
| 371 | + // remove all of the parents. |
|
| 372 | + foreach ($parents as $ele) { |
|
| 373 | + $ele->parentNode->removeChild($ele); |
|
| 374 | + } |
|
| 375 | + |
|
| 376 | + return $this; |
|
| 377 | + } |
|
| 378 | + |
|
| 379 | + /** |
|
| 380 | + * Wrap each element inside of the given markup. |
|
| 381 | + * |
|
| 382 | + * Markup is usually a string, but it can also be a DOMNode, a document |
|
| 383 | + * fragment, a SimpleXMLElement, or another DOMNode object (in which case |
|
| 384 | + * the first item in the list will be used.) |
|
| 385 | + * |
|
| 386 | + * @param mixed $markup |
|
| 387 | + * Markup that will wrap each element in the current list. |
|
| 388 | + * |
|
| 389 | + * @return DOMQuery |
|
| 390 | + * The DOMQuery object with the wrapping changes made. |
|
| 391 | + * @throws Exception |
|
| 392 | + * @throws QueryPath |
|
| 393 | + * @see wrapAll() |
|
| 394 | + * @see wrapInner() |
|
| 395 | + */ |
|
| 396 | + public function wrap($markup): Query |
|
| 397 | + { |
|
| 398 | + $data = $this->prepareInsert($markup); |
|
| 399 | + |
|
| 400 | + // If the markup passed in is empty, we don't do any wrapping. |
|
| 401 | + if (empty($data)) { |
|
| 402 | + return $this; |
|
| 403 | + } |
|
| 404 | + |
|
| 405 | + foreach ($this->matches as $m) { |
|
| 406 | + if ($data instanceof DOMDocumentFragment) { |
|
| 407 | + $copy = $data->firstChild->cloneNode(true); |
|
| 408 | + } else { |
|
| 409 | + $copy = $data->cloneNode(true); |
|
| 410 | + } |
|
| 411 | + |
|
| 412 | + // XXX: Should be able to avoid doing this over and over. |
|
| 413 | + if ($copy->hasChildNodes()) { |
|
| 414 | + $deepest = $this->deepestNode($copy); |
|
| 415 | + // FIXME: Does this need a different data structure? |
|
| 416 | + $bottom = $deepest[0]; |
|
| 417 | + } else { |
|
| 418 | + $bottom = $copy; |
|
| 419 | + } |
|
| 420 | + |
|
| 421 | + $parent = $m->parentNode; |
|
| 422 | + $parent->insertBefore($copy, $m); |
|
| 423 | + $m = $parent->removeChild($m); |
|
| 424 | + $bottom->appendChild($m); |
|
| 425 | + } |
|
| 426 | + |
|
| 427 | + return $this; |
|
| 428 | + } |
|
| 429 | + |
|
| 430 | + /** |
|
| 431 | + * Wrap all elements inside of the given markup. |
|
| 432 | + * |
|
| 433 | + * So all elements will be grouped together under this single marked up |
|
| 434 | + * item. This works by first determining the parent element of the first item |
|
| 435 | + * in the list. It then moves all of the matching elements under the wrapper |
|
| 436 | + * and inserts the wrapper where that first element was found. (This is in |
|
| 437 | + * accordance with the way jQuery works.) |
|
| 438 | + * |
|
| 439 | + * Markup is usually XML in a string, but it can also be a DOMNode, a document |
|
| 440 | + * fragment, a SimpleXMLElement, or another DOMNode object (in which case |
|
| 441 | + * the first item in the list will be used.) |
|
| 442 | + * |
|
| 443 | + * @param string $markup |
|
| 444 | + * Markup that will wrap all elements in the current list. |
|
| 445 | + * |
|
| 446 | + * @return DOMQuery |
|
| 447 | + * The DOMQuery object with the wrapping changes made. |
|
| 448 | + * @throws Exception |
|
| 449 | + * @throws QueryPath |
|
| 450 | + * @see wrap() |
|
| 451 | + * @see wrapInner() |
|
| 452 | + */ |
|
| 453 | + public function wrapAll($markup) |
|
| 454 | + { |
|
| 455 | + if ($this->matches->count() === 0) { |
|
| 456 | + return; |
|
| 457 | + } |
|
| 458 | + |
|
| 459 | + $data = $this->prepareInsert($markup); |
|
| 460 | + |
|
| 461 | + if (empty($data)) { |
|
| 462 | + return $this; |
|
| 463 | + } |
|
| 464 | + |
|
| 465 | + if ($data instanceof DOMDocumentFragment) { |
|
| 466 | + $data = $data->firstChild->cloneNode(true); |
|
| 467 | + } else { |
|
| 468 | + $data = $data->cloneNode(true); |
|
| 469 | + } |
|
| 470 | + |
|
| 471 | + if ($data->hasChildNodes()) { |
|
| 472 | + $deepest = $this->deepestNode($data); |
|
| 473 | + // FIXME: Does this need fixing? |
|
| 474 | + $bottom = $deepest[0]; |
|
| 475 | + } else { |
|
| 476 | + $bottom = $data; |
|
| 477 | + } |
|
| 478 | + |
|
| 479 | + $first = $this->getFirstMatch(); |
|
| 480 | + $parent = $first->parentNode; |
|
| 481 | + $parent->insertBefore($data, $first); |
|
| 482 | + foreach ($this->matches as $m) { |
|
| 483 | + $bottom->appendChild($m->parentNode->removeChild($m)); |
|
| 484 | + } |
|
| 485 | + |
|
| 486 | + return $this; |
|
| 487 | + } |
|
| 488 | + |
|
| 489 | + /** |
|
| 490 | + * Wrap the child elements of each item in the list with the given markup. |
|
| 491 | + * |
|
| 492 | + * Markup is usually a string, but it can also be a DOMNode, a document |
|
| 493 | + * fragment, a SimpleXMLElement, or another DOMNode object (in which case |
|
| 494 | + * the first item in the list will be used.) |
|
| 495 | + * |
|
| 496 | + * @param string $markup |
|
| 497 | + * Markup that will wrap children of each element in the current list. |
|
| 498 | + * |
|
| 499 | + * @return DOMQuery |
|
| 500 | + * The DOMQuery object with the wrapping changes made. |
|
| 501 | + * @throws Exception |
|
| 502 | + * @throws QueryPath |
|
| 503 | + * @see wrap() |
|
| 504 | + * @see wrapAll() |
|
| 505 | + */ |
|
| 506 | + public function wrapInner($markup) |
|
| 507 | + { |
|
| 508 | + $data = $this->prepareInsert($markup); |
|
| 509 | + |
|
| 510 | + // No data? Short circuit. |
|
| 511 | + if (empty($data)) { |
|
| 512 | + return $this; |
|
| 513 | + } |
|
| 514 | + |
|
| 515 | + foreach ($this->matches as $m) { |
|
| 516 | + if ($data instanceof DOMDocumentFragment) { |
|
| 517 | + $wrapper = $data->firstChild->cloneNode(true); |
|
| 518 | + } else { |
|
| 519 | + $wrapper = $data->cloneNode(true); |
|
| 520 | + } |
|
| 521 | + |
|
| 522 | + if ($wrapper->hasChildNodes()) { |
|
| 523 | + $deepest = $this->deepestNode($wrapper); |
|
| 524 | + // FIXME: ??? |
|
| 525 | + $bottom = $deepest[0]; |
|
| 526 | + } else { |
|
| 527 | + $bottom = $wrapper; |
|
| 528 | + } |
|
| 529 | + |
|
| 530 | + if ($m->hasChildNodes()) { |
|
| 531 | + while ($m->firstChild) { |
|
| 532 | + $kid = $m->removeChild($m->firstChild); |
|
| 533 | + $bottom->appendChild($kid); |
|
| 534 | + } |
|
| 535 | + } |
|
| 536 | + |
|
| 537 | + $m->appendChild($wrapper); |
|
| 538 | + } |
|
| 539 | + |
|
| 540 | + return $this; |
|
| 541 | + } |
|
| 542 | + |
|
| 543 | + /** |
|
| 544 | + * Reduce the set of matches to the deepest child node in the tree. |
|
| 545 | + * |
|
| 546 | + * This loops through the matches and looks for the deepest child node of all of |
|
| 547 | + * the matches. "Deepest", here, is relative to the nodes in the list. It is |
|
| 548 | + * calculated as the distance from the starting node to the most distant child |
|
| 549 | + * node. In other words, it is not necessarily the farthest node from the root |
|
| 550 | + * element, but the farthest note from the matched element. |
|
| 551 | + * |
|
| 552 | + * In the case where there are multiple nodes at the same depth, all of the |
|
| 553 | + * nodes at that depth will be included. |
|
| 554 | + * |
|
| 555 | + * @return DOMQuery |
|
| 556 | + * The DOMQuery wrapping the single deepest node. |
|
| 557 | + * @throws ParseException |
|
| 558 | + */ |
|
| 559 | + public function deepest(): Query |
|
| 560 | + { |
|
| 561 | + $deepest = 0; |
|
| 562 | + $winner = new SplObjectStorage(); |
|
| 563 | + foreach ($this->matches as $m) { |
|
| 564 | + $local_deepest = 0; |
|
| 565 | + $local_ele = $this->deepestNode($m, 0, null, $local_deepest); |
|
| 566 | + |
|
| 567 | + // Replace with the new deepest. |
|
| 568 | + if ($local_deepest > $deepest) { |
|
| 569 | + $winner = new SplObjectStorage(); |
|
| 570 | + foreach ($local_ele as $lele) { |
|
| 571 | + $winner->attach($lele); |
|
| 572 | + } |
|
| 573 | + $deepest = $local_deepest; |
|
| 574 | + } // Augument with other equally deep elements. |
|
| 575 | + elseif ($local_deepest === $deepest) { |
|
| 576 | + foreach ($local_ele as $lele) { |
|
| 577 | + $winner->attach($lele); |
|
| 578 | + } |
|
| 579 | + } |
|
| 580 | + } |
|
| 581 | + |
|
| 582 | + return $this->inst($winner, null); |
|
| 583 | + } |
|
| 584 | + |
|
| 585 | + /** |
|
| 586 | + * Add a class to all elements in the current DOMQuery. |
|
| 587 | + * |
|
| 588 | + * This searchers for a class attribute on each item wrapped by the current |
|
| 589 | + * DOMNode object. If no attribute is found, a new one is added and its value |
|
| 590 | + * is set to $class. If a class attribute is found, then the value is appended |
|
| 591 | + * on to the end. |
|
| 592 | + * |
|
| 593 | + * @param string $class |
|
| 594 | + * The name of the class. |
|
| 595 | + * |
|
| 596 | + * @return DOMQuery |
|
| 597 | + * Returns the DOMQuery object. |
|
| 598 | + * @see css() |
|
| 599 | + * @see attr() |
|
| 600 | + * @see removeClass() |
|
| 601 | + * @see hasClass() |
|
| 602 | + */ |
|
| 603 | + public function addClass($class) |
|
| 604 | + { |
|
| 605 | + foreach ($this->matches as $m) { |
|
| 606 | + if ($m->hasAttribute('class')) { |
|
| 607 | + $val = $m->getAttribute('class'); |
|
| 608 | + $m->setAttribute('class', $val . ' ' . $class); |
|
| 609 | + } else { |
|
| 610 | + $m->setAttribute('class', $class); |
|
| 611 | + } |
|
| 612 | + } |
|
| 613 | + |
|
| 614 | + return $this; |
|
| 615 | + } |
|
| 616 | + |
|
| 617 | + /** |
|
| 618 | + * Remove the named class from any element in the DOMQuery that has it. |
|
| 619 | + * |
|
| 620 | + * This may result in the entire class attribute being removed. If there |
|
| 621 | + * are other items in the class attribute, though, they will not be removed. |
|
| 622 | + * |
|
| 623 | + * Example: |
|
| 624 | + * Consider this XML: |
|
| 625 | + * |
|
| 626 | + * @code |
|
| 627 | + * <element class="first second"/> |
|
| 628 | + * @endcode |
|
| 629 | + * |
|
| 630 | + * Executing this fragment of code will remove only the 'first' class: |
|
| 631 | + * @code |
|
| 632 | + * qp(document, 'element')->removeClass('first'); |
|
| 633 | + * @endcode |
|
| 634 | + * |
|
| 635 | + * The resulting XML will be: |
|
| 636 | + * @code |
|
| 637 | + * <element class="second"/> |
|
| 638 | + * @endcode |
|
| 639 | + * |
|
| 640 | + * To remove the entire 'class' attribute, you should use {@see removeAttr()}. |
|
| 641 | + * |
|
| 642 | + * @param string $class |
|
| 643 | + * The class name to remove. |
|
| 644 | + * |
|
| 645 | + * @return DOMQuery |
|
| 646 | + * The modified DOMNode object. |
|
| 647 | + * @see attr() |
|
| 648 | + * @see addClass() |
|
| 649 | + * @see hasClass() |
|
| 650 | + */ |
|
| 651 | + public function removeClass($class = false): Query |
|
| 652 | + { |
|
| 653 | + if (empty($class)) { |
|
| 654 | + foreach ($this->matches as $m) { |
|
| 655 | + $m->removeAttribute('class'); |
|
| 656 | + } |
|
| 657 | + } else { |
|
| 658 | + $to_remove = array_filter(explode(' ', $class)); |
|
| 659 | + foreach ($this->matches as $m) { |
|
| 660 | + if ($m->hasAttribute('class')) { |
|
| 661 | + $vals = array_filter(explode(' ', $m->getAttribute('class'))); |
|
| 662 | + $buf = []; |
|
| 663 | + foreach ($vals as $v) { |
|
| 664 | + if (! in_array($v, $to_remove)) { |
|
| 665 | + $buf[] = $v; |
|
| 666 | + } |
|
| 667 | + } |
|
| 668 | + if (empty($buf)) { |
|
| 669 | + $m->removeAttribute('class'); |
|
| 670 | + } else { |
|
| 671 | + $m->setAttribute('class', implode(' ', $buf)); |
|
| 672 | + } |
|
| 673 | + } |
|
| 674 | + } |
|
| 675 | + } |
|
| 676 | + |
|
| 677 | + return $this; |
|
| 678 | + } |
|
| 679 | + |
|
| 680 | + /** |
|
| 681 | + * Detach any items from the list if they match the selector. |
|
| 682 | + * |
|
| 683 | + * In other words, each item that matches the selector will be removed |
|
| 684 | + * from the DOM document. The returned DOMQuery wraps the list of |
|
| 685 | + * removed elements. |
|
| 686 | + * |
|
| 687 | + * If no selector is specified, this will remove all current matches from |
|
| 688 | + * the document. |
|
| 689 | + * |
|
| 690 | + * @param string $selector |
|
| 691 | + * A CSS Selector. |
|
| 692 | + * |
|
| 693 | + * @return DOMQuery |
|
| 694 | + * The Query path wrapping a list of removed items. |
|
| 695 | + * @throws ParseException |
|
| 696 | + * @see replaceWith() |
|
| 697 | + * @see removeChildren() |
|
| 698 | + * @since 2.1 |
|
| 699 | + * @author eabrand |
|
| 700 | + * @see replaceAll() |
|
| 701 | + */ |
|
| 702 | + public function detach($selector = null): Query |
|
| 703 | + { |
|
| 704 | + if (null !== $selector) { |
|
| 705 | + $this->find($selector); |
|
| 706 | + } |
|
| 707 | + |
|
| 708 | + $found = new SplObjectStorage(); |
|
| 709 | + $this->last = $this->matches; |
|
| 710 | + foreach ($this->matches as $item) { |
|
| 711 | + // The item returned is (according to docs) different from |
|
| 712 | + // the one passed in, so we have to re-store it. |
|
| 713 | + $found->attach($item->parentNode->removeChild($item)); |
|
| 714 | + } |
|
| 715 | + |
|
| 716 | + return $this->inst($found, null); |
|
| 717 | + } |
|
| 718 | + |
|
| 719 | + /** |
|
| 720 | + * Attach any items from the list if they match the selector. |
|
| 721 | + * |
|
| 722 | + * If no selector is specified, this will remove all current matches from |
|
| 723 | + * the document. |
|
| 724 | + * |
|
| 725 | + * @param DOMQuery $dest |
|
| 726 | + * A DOMQuery Selector. |
|
| 727 | + * |
|
| 728 | + * @return DOMQuery |
|
| 729 | + * The Query path wrapping a list of removed items. |
|
| 730 | + * @throws QueryPath |
|
| 731 | + * @throws Exception |
|
| 732 | + * @see removeChildren() |
|
| 733 | + * @since 2.1 |
|
| 734 | + * @author eabrand |
|
| 735 | + * @see replaceAll() |
|
| 736 | + * @see replaceWith() |
|
| 737 | + */ |
|
| 738 | + public function attach(DOMQuery $dest): Query |
|
| 739 | + { |
|
| 740 | + foreach ($this->last as $m) { |
|
| 741 | + $dest->append($m); |
|
| 742 | + } |
|
| 743 | + |
|
| 744 | + return $this; |
|
| 745 | + } |
|
| 746 | + |
|
| 747 | + /** |
|
| 748 | + * Append the current elements to the destination passed into the function. |
|
| 749 | + * |
|
| 750 | + * This cycles through all of the current matches and appends them to |
|
| 751 | + * the context given in $destination. If a selector is provided then the |
|
| 752 | + * $destination is queried (using that selector) prior to the data being |
|
| 753 | + * appended. The data is then appended to the found items. |
|
| 754 | + * |
|
| 755 | + * @param DOMQuery $dest |
|
| 756 | + * A DOMQuery object that will be appended to. |
|
| 757 | + * |
|
| 758 | + * @return DOMQuery |
|
| 759 | + * The original DOMQuery, unaltered. Only the destination DOMQuery will |
|
| 760 | + * be modified. |
|
| 761 | + * @throws QueryPath::Exception |
|
| 762 | + * Thrown if $data is an unsupported object type. |
|
| 763 | + * @throws Exception |
|
| 764 | + * @see append() |
|
| 765 | + * @see prependTo() |
|
| 766 | + */ |
|
| 767 | + public function appendTo(DOMQuery $dest): Query |
|
| 768 | + { |
|
| 769 | + foreach ($this->matches as $m) { |
|
| 770 | + $dest->append($m); |
|
| 771 | + } |
|
| 772 | + |
|
| 773 | + return $this; |
|
| 774 | + } |
|
| 775 | + |
|
| 776 | + /** |
|
| 777 | + * Remove any items from the list if they match the selector. |
|
| 778 | + * |
|
| 779 | + * In other words, each item that matches the selector will be remove |
|
| 780 | + * from the DOM document. The returned DOMQuery wraps the list of |
|
| 781 | + * removed elements. |
|
| 782 | + * |
|
| 783 | + * If no selector is specified, this will remove all current matches from |
|
| 784 | + * the document. |
|
| 785 | + * |
|
| 786 | + * @param string $selector |
|
| 787 | + * A CSS Selector. |
|
| 788 | + * |
|
| 789 | + * @return DOMQuery |
|
| 790 | + * The Query path wrapping a list of removed items. |
|
| 791 | + * @throws ParseException |
|
| 792 | + * @see replaceWith() |
|
| 793 | + * @see removeChildren() |
|
| 794 | + * @see replaceAll() |
|
| 795 | + */ |
|
| 796 | + public function remove($selector = null): Query |
|
| 797 | + { |
|
| 798 | + if (! empty($selector)) { |
|
| 799 | + // Do a non-destructive find. |
|
| 800 | + $query = new QueryPathEventHandler($this->matches); |
|
| 801 | + $query->find($selector); |
|
| 802 | + $matches = $query->getMatches(); |
|
| 803 | + } else { |
|
| 804 | + $matches = $this->matches; |
|
| 805 | + } |
|
| 806 | + |
|
| 807 | + $found = new SplObjectStorage(); |
|
| 808 | + foreach ($matches as $item) { |
|
| 809 | + // The item returned is (according to docs) different from |
|
| 810 | + // the one passed in, so we have to re-store it. |
|
| 811 | + $found->attach($item->parentNode->removeChild($item)); |
|
| 812 | + } |
|
| 813 | + |
|
| 814 | + // Return a clone DOMQuery with just the removed items. If |
|
| 815 | + // no items are found, this will return an empty DOMQuery. |
|
| 816 | + return count($found) === 0 ? new static() : new static($found); |
|
| 817 | + } |
|
| 818 | + |
|
| 819 | + /** |
|
| 820 | + * This replaces everything that matches the selector with the first value |
|
| 821 | + * in the current list. |
|
| 822 | + * |
|
| 823 | + * This is the reverse of replaceWith. |
|
| 824 | + * |
|
| 825 | + * Unlike jQuery, DOMQuery cannot assume a default document. Consequently, |
|
| 826 | + * you must specify the intended destination document. If it is omitted, the |
|
| 827 | + * present document is assumed to be tthe document. However, that can result |
|
| 828 | + * in undefined behavior if the selector and the replacement are not sufficiently |
|
| 829 | + * distinct. |
|
| 830 | + * |
|
| 831 | + * @param string $selector |
|
| 832 | + * The selector. |
|
| 833 | + * @param DOMDocument $document |
|
| 834 | + * The destination document. |
|
| 835 | + * |
|
| 836 | + * @return DOMQuery |
|
| 837 | + * The DOMQuery wrapping the modified document. |
|
| 838 | + * @throws ParseException |
|
| 839 | + * @see remove() |
|
| 840 | + * @see replaceWith() |
|
| 841 | + * @deprecated Due to the fact that this is not a particularly friendly method, |
|
| 842 | + * and that it can be easily replicated using {@see replaceWith()}, it is to be |
|
| 843 | + * considered deprecated. |
|
| 844 | + */ |
|
| 845 | + public function replaceAll($selector, DOMDocument $document): Query |
|
| 846 | + { |
|
| 847 | + $replacement = $this->matches->count() > 0 ? $this->getFirstMatch() : $this->document->createTextNode(''); |
|
| 848 | + |
|
| 849 | + $c = new QueryPathEventHandler($document); |
|
| 850 | + $c->find($selector); |
|
| 851 | + $temp = $c->getMatches(); |
|
| 852 | + foreach ($temp as $item) { |
|
| 853 | + $node = $replacement->cloneNode(); |
|
| 854 | + $node = $document->importNode($node); |
|
| 855 | + $item->parentNode->replaceChild($node, $item); |
|
| 856 | + } |
|
| 857 | + |
|
| 858 | + return QueryPath::with($document, null, $this->options); |
|
| 859 | + } |
|
| 860 | + |
|
| 861 | + /** |
|
| 862 | + * Add more elements to the current set of matches. |
|
| 863 | + * |
|
| 864 | + * This begins the new query at the top of the DOM again. The results found |
|
| 865 | + * when running this selector are then merged into the existing results. In |
|
| 866 | + * this way, you can add additional elements to the existing set. |
|
| 867 | + * |
|
| 868 | + * @param string $selector |
|
| 869 | + * A valid selector. |
|
| 870 | + * |
|
| 871 | + * @return DOMQuery |
|
| 872 | + * The DOMQuery object with the newly added elements. |
|
| 873 | + * @see append() |
|
| 874 | + * @see after() |
|
| 875 | + * @see andSelf() |
|
| 876 | + * @see end() |
|
| 877 | + */ |
|
| 878 | + public function add($selector): Query |
|
| 879 | + { |
|
| 880 | + |
|
| 881 | + // This is destructive, so we need to set $last: |
|
| 882 | + $this->last = $this->matches; |
|
| 883 | + |
|
| 884 | + foreach (QueryPath::with($this->document, $selector, $this->options)->get() as $item) { |
|
| 885 | + $this->matches->attach($item); |
|
| 886 | + } |
|
| 887 | + |
|
| 888 | + return $this; |
|
| 889 | + } |
|
| 890 | + |
|
| 891 | + /** |
|
| 892 | + * Remove all child nodes. |
|
| 893 | + * |
|
| 894 | + * This is equivalent to jQuery's empty() function. (However, empty() is a |
|
| 895 | + * PHP built-in, and cannot be used as a method name.) |
|
| 896 | + * |
|
| 897 | + * @return DOMQuery |
|
| 898 | + * The DOMQuery object with the child nodes removed. |
|
| 899 | + * @see replaceWith() |
|
| 900 | + * @see replaceAll() |
|
| 901 | + * @see remove() |
|
| 902 | + */ |
|
| 903 | + public function removeChildren(): Query |
|
| 904 | + { |
|
| 905 | + foreach ($this->matches as $m) { |
|
| 906 | + while ($kid = $m->firstChild) { |
|
| 907 | + $m->removeChild($kid); |
|
| 908 | + } |
|
| 909 | + } |
|
| 910 | + |
|
| 911 | + return $this; |
|
| 912 | + } |
|
| 913 | + |
|
| 914 | + /** |
|
| 915 | + * Get/set an attribute. |
|
| 916 | + * - If no parameters are specified, this returns an associative array of all |
|
| 917 | + * name/value pairs. |
|
| 918 | + * - If both $name and $value are set, then this will set the attribute name/value |
|
| 919 | + * pair for all items in this object. |
|
| 920 | + * - If $name is set, and is an array, then |
|
| 921 | + * all attributes in the array will be set for all items in this object. |
|
| 922 | + * - If $name is a string and is set, then the attribute value will be returned. |
|
| 923 | + * |
|
| 924 | + * When an attribute value is retrieved, only the attribute value of the FIRST |
|
| 925 | + * match is returned. |
|
| 926 | + * |
|
| 927 | + * @param mixed $name |
|
| 928 | + * The name of the attribute or an associative array of name/value pairs. |
|
| 929 | + * @param string $value |
|
| 930 | + * A value (used only when setting an individual property). |
|
| 931 | + * |
|
| 932 | + * @return mixed |
|
| 933 | + * If this was a setter request, return the DOMQuery object. If this was |
|
| 934 | + * an access request (getter), return the string value. |
|
| 935 | + * @see removeAttr() |
|
| 936 | + * @see tag() |
|
| 937 | + * @see hasAttr() |
|
| 938 | + * @see hasClass() |
|
| 939 | + */ |
|
| 940 | + public function attr($name = null, $value = null) |
|
| 941 | + { |
|
| 942 | + // Default case: Return all attributes as an assoc array. |
|
| 943 | + if (is_null($name)) { |
|
| 944 | + if ($this->matches->count() === 0) { |
|
| 945 | + return null; |
|
| 946 | + } |
|
| 947 | + $ele = $this->getFirstMatch(); |
|
| 948 | + $buffer = []; |
|
| 949 | + |
|
| 950 | + // This does not appear to be part of the DOM |
|
| 951 | + // spec. Nor is it documented. But it works. |
|
| 952 | + foreach ($ele->attributes as $name => $attrNode) { |
|
| 953 | + $buffer[$name] = $attrNode->value; |
|
| 954 | + } |
|
| 955 | + |
|
| 956 | + return $buffer; |
|
| 957 | + } |
|
| 958 | + |
|
| 959 | + // multi-setter |
|
| 960 | + if (is_array($name)) { |
|
| 961 | + foreach ($name as $k => $v) { |
|
| 962 | + foreach ($this->matches as $m) { |
|
| 963 | + $m->setAttribute($k, $v); |
|
| 964 | + } |
|
| 965 | + } |
|
| 966 | + |
|
| 967 | + return $this; |
|
| 968 | + } |
|
| 969 | + // setter |
|
| 970 | + if (isset($value)) { |
|
| 971 | + foreach ($this->matches as $m) { |
|
| 972 | + $m->setAttribute($name, $value); |
|
| 973 | + } |
|
| 974 | + |
|
| 975 | + return $this; |
|
| 976 | + } |
|
| 977 | + |
|
| 978 | + //getter |
|
| 979 | + if ($this->matches->count() === 0) { |
|
| 980 | + return null; |
|
| 981 | + } |
|
| 982 | + |
|
| 983 | + // Special node type handler: |
|
| 984 | + if ($name === 'nodeType') { |
|
| 985 | + return $this->getFirstMatch()->nodeType; |
|
| 986 | + } |
|
| 987 | + |
|
| 988 | + // Always return first match's attr. |
|
| 989 | + return $this->getFirstMatch()->getAttribute($name); |
|
| 990 | + } |
|
| 991 | + |
|
| 992 | + /** |
|
| 993 | + * Set/get a CSS value for the current element(s). |
|
| 994 | + * This sets the CSS value for each element in the DOMQuery object. |
|
| 995 | + * It does this by setting (or getting) the style attribute (without a namespace). |
|
| 996 | + * |
|
| 997 | + * For example, consider this code: |
|
| 998 | + * |
|
| 999 | + * @code |
|
| 1000 | + * <?php |
|
| 1001 | + * qp(HTML_STUB, 'body')->css('background-color','red')->html(); |
|
| 1002 | + * ?> |
|
| 1003 | + * @endcode |
|
| 1004 | + * This will return the following HTML: |
|
| 1005 | + * @code |
|
| 1006 | + * <body style="background-color: red"/> |
|
| 1007 | + * @endcode |
|
| 1008 | + * |
|
| 1009 | + * If no parameters are passed into this function, then the current style |
|
| 1010 | + * element will be returned unparsed. Example: |
|
| 1011 | + * @code |
|
| 1012 | + * <?php |
|
| 1013 | + * qp(HTML_STUB, 'body')->css('background-color','red')->css(); |
|
| 1014 | + * ?> |
|
| 1015 | + * @endcode |
|
| 1016 | + * This will return the following: |
|
| 1017 | + * @code |
|
| 1018 | + * background-color: red |
|
| 1019 | + * @endcode |
|
| 1020 | + * |
|
| 1021 | + * As of QueryPath 2.1, existing style attributes will be merged with new attributes. |
|
| 1022 | + * (In previous versions of QueryPath, a call to css() overwrite the existing style |
|
| 1023 | + * values). |
|
| 1024 | + * |
|
| 1025 | + * @param mixed $name |
|
| 1026 | + * If this is a string, it will be used as a CSS name. If it is an array, |
|
| 1027 | + * this will assume it is an array of name/value pairs of CSS rules. It will |
|
| 1028 | + * apply all rules to all elements in the set. |
|
| 1029 | + * @param string $value |
|
| 1030 | + * The value to set. This is only set if $name is a string. |
|
| 1031 | + * |
|
| 1032 | + * @return DOMQuery |
|
| 1033 | + */ |
|
| 1034 | + public function css($name = null, $value = '') |
|
| 1035 | + { |
|
| 1036 | + if (empty($name)) { |
|
| 1037 | + return $this->attr('style'); |
|
| 1038 | + } |
|
| 1039 | + |
|
| 1040 | + // Get any existing CSS. |
|
| 1041 | + $css = []; |
|
| 1042 | + foreach ($this->matches as $match) { |
|
| 1043 | + $style = $match->getAttribute('style'); |
|
| 1044 | + if (! empty($style)) { |
|
| 1045 | + // XXX: Is this sufficient? |
|
| 1046 | + $style_array = explode(';', $style); |
|
| 1047 | + foreach ($style_array as $item) { |
|
| 1048 | + $item = trim($item); |
|
| 1049 | + |
|
| 1050 | + // Skip empty attributes. |
|
| 1051 | + if ($item === '') { |
|
| 1052 | + continue; |
|
| 1053 | + } |
|
| 1054 | + |
|
| 1055 | + [$css_att, $css_val] = explode(':', $item, 2); |
|
| 1056 | + $css[$css_att] = trim($css_val); |
|
| 1057 | + } |
|
| 1058 | + } |
|
| 1059 | + } |
|
| 1060 | + |
|
| 1061 | + if (is_array($name)) { |
|
| 1062 | + // Use array_merge instead of + to preserve order. |
|
| 1063 | + $css = array_merge($css, $name); |
|
| 1064 | + } else { |
|
| 1065 | + $css[$name] = $value; |
|
| 1066 | + } |
|
| 1067 | + |
|
| 1068 | + // Collapse CSS into a string. |
|
| 1069 | + $format = '%s: %s;'; |
|
| 1070 | + $css_string = ''; |
|
| 1071 | + foreach ($css as $n => $v) { |
|
| 1072 | + $css_string .= sprintf($format, $n, trim($v)); |
|
| 1073 | + } |
|
| 1074 | + |
|
| 1075 | + $this->attr('style', $css_string); |
|
| 1076 | + |
|
| 1077 | + return $this; |
|
| 1078 | + } |
|
| 1079 | 1079 | } |
@@ -661,7 +661,7 @@ discard block |
||
| 661 | 661 | $vals = array_filter(explode(' ', $m->getAttribute('class'))); |
| 662 | 662 | $buf = []; |
| 663 | 663 | foreach ($vals as $v) { |
| 664 | - if (! in_array($v, $to_remove)) { |
|
| 664 | + if (!in_array($v, $to_remove)) { |
|
| 665 | 665 | $buf[] = $v; |
| 666 | 666 | } |
| 667 | 667 | } |
@@ -795,7 +795,7 @@ discard block |
||
| 795 | 795 | */ |
| 796 | 796 | public function remove($selector = null): Query |
| 797 | 797 | { |
| 798 | - if (! empty($selector)) { |
|
| 798 | + if (!empty($selector)) { |
|
| 799 | 799 | // Do a non-destructive find. |
| 800 | 800 | $query = new QueryPathEventHandler($this->matches); |
| 801 | 801 | $query->find($selector); |
@@ -1041,7 +1041,7 @@ discard block |
||
| 1041 | 1041 | $css = []; |
| 1042 | 1042 | foreach ($this->matches as $match) { |
| 1043 | 1043 | $style = $match->getAttribute('style'); |
| 1044 | - if (! empty($style)) { |
|
| 1044 | + if (!empty($style)) { |
|
| 1045 | 1045 | // XXX: Is this sufficient? |
| 1046 | 1046 | $style_array = explode(';', $style); |
| 1047 | 1047 | foreach ($style_array as $item) { |
@@ -20,182 +20,182 @@ |
||
| 20 | 20 | trait QueryChecks |
| 21 | 21 | { |
| 22 | 22 | |
| 23 | - /** |
|
| 24 | - * Given a selector, this checks to see if the current set has one or more matches. |
|
| 25 | - * |
|
| 26 | - * Unlike jQuery's version, this supports full selectors (not just simple ones). |
|
| 27 | - * |
|
| 28 | - * @param string|DOMNode $selector |
|
| 29 | - * The selector to search for. As of QueryPath 2.1.1, this also supports passing a |
|
| 30 | - * DOMNode object. |
|
| 31 | - * |
|
| 32 | - * @return boolean |
|
| 33 | - * TRUE if one or more elements match. FALSE if no match is found. |
|
| 34 | - * @throws Exception |
|
| 35 | - * @throws Exception |
|
| 36 | - * @see get() |
|
| 37 | - * @see eq() |
|
| 38 | - */ |
|
| 39 | - public function is($selector): bool |
|
| 40 | - { |
|
| 41 | - if (is_object($selector)) { |
|
| 42 | - if ($selector instanceof DOMNode) { |
|
| 43 | - return count($this->matches) === 1 && $selector->isSameNode($this->get(0)); |
|
| 44 | - } |
|
| 45 | - |
|
| 46 | - if ($selector instanceof Traversable) { |
|
| 47 | - if (count($selector) !== count($this->matches)) { |
|
| 48 | - return false; |
|
| 49 | - } |
|
| 50 | - // Without $seen, there is an edge case here if $selector contains the same object |
|
| 51 | - // more than once, but the counts are equal. For example, [a, a, a, a] will |
|
| 52 | - // pass an is() on [a, b, c, d]. We use the $seen SPLOS to prevent this. |
|
| 53 | - $seen = new SplObjectStorage(); |
|
| 54 | - foreach ($selector as $item) { |
|
| 55 | - if (! $this->matches->contains($item) || $seen->contains($item)) { |
|
| 56 | - return false; |
|
| 57 | - } |
|
| 58 | - $seen->attach($item); |
|
| 59 | - } |
|
| 60 | - |
|
| 61 | - return true; |
|
| 62 | - } |
|
| 63 | - throw new Exception('Cannot compare an object to a DOMQuery.'); |
|
| 64 | - } |
|
| 65 | - |
|
| 66 | - return $this->branch($selector)->count() > 0; |
|
| 67 | - } |
|
| 68 | - |
|
| 69 | - /** |
|
| 70 | - * Reduce the elements matched by DOMQuery to only those which contain the given item. |
|
| 71 | - * |
|
| 72 | - * There are two ways in which this is different from jQuery's implementation: |
|
| 73 | - * - We allow ANY DOMNode, not just DOMElements. That means this will work on |
|
| 74 | - * processor instructions, text nodes, comments, etc. |
|
| 75 | - * - Unlike jQuery, this implementation of has() follows QueryPath standard behavior |
|
| 76 | - * and modifies the existing object. It does not create a brand new object. |
|
| 77 | - * |
|
| 78 | - * @param mixed $contained |
|
| 79 | - * - If $contained is a CSS selector (e.g. '#foo'), this will test to see |
|
| 80 | - * if the current DOMQuery has any elements that contain items that match |
|
| 81 | - * the selector. |
|
| 82 | - * - If $contained is a DOMNode, then this will test to see if THE EXACT DOMNode |
|
| 83 | - * exists in the currently matched elements. (Note that you cannot match across DOM trees, even if it is the |
|
| 84 | - * same document.) |
|
| 85 | - * |
|
| 86 | - * @return DOMQuery |
|
| 87 | - * @throws ParseException |
|
| 88 | - * @todo It would be trivially easy to add support for iterating over an array or Iterable of DOMNodes. |
|
| 89 | - * @since 2.1 |
|
| 90 | - * @author eabrand |
|
| 91 | - */ |
|
| 92 | - public function has($contained): Query |
|
| 93 | - { |
|
| 94 | - /* |
|
| 23 | + /** |
|
| 24 | + * Given a selector, this checks to see if the current set has one or more matches. |
|
| 25 | + * |
|
| 26 | + * Unlike jQuery's version, this supports full selectors (not just simple ones). |
|
| 27 | + * |
|
| 28 | + * @param string|DOMNode $selector |
|
| 29 | + * The selector to search for. As of QueryPath 2.1.1, this also supports passing a |
|
| 30 | + * DOMNode object. |
|
| 31 | + * |
|
| 32 | + * @return boolean |
|
| 33 | + * TRUE if one or more elements match. FALSE if no match is found. |
|
| 34 | + * @throws Exception |
|
| 35 | + * @throws Exception |
|
| 36 | + * @see get() |
|
| 37 | + * @see eq() |
|
| 38 | + */ |
|
| 39 | + public function is($selector): bool |
|
| 40 | + { |
|
| 41 | + if (is_object($selector)) { |
|
| 42 | + if ($selector instanceof DOMNode) { |
|
| 43 | + return count($this->matches) === 1 && $selector->isSameNode($this->get(0)); |
|
| 44 | + } |
|
| 45 | + |
|
| 46 | + if ($selector instanceof Traversable) { |
|
| 47 | + if (count($selector) !== count($this->matches)) { |
|
| 48 | + return false; |
|
| 49 | + } |
|
| 50 | + // Without $seen, there is an edge case here if $selector contains the same object |
|
| 51 | + // more than once, but the counts are equal. For example, [a, a, a, a] will |
|
| 52 | + // pass an is() on [a, b, c, d]. We use the $seen SPLOS to prevent this. |
|
| 53 | + $seen = new SplObjectStorage(); |
|
| 54 | + foreach ($selector as $item) { |
|
| 55 | + if (! $this->matches->contains($item) || $seen->contains($item)) { |
|
| 56 | + return false; |
|
| 57 | + } |
|
| 58 | + $seen->attach($item); |
|
| 59 | + } |
|
| 60 | + |
|
| 61 | + return true; |
|
| 62 | + } |
|
| 63 | + throw new Exception('Cannot compare an object to a DOMQuery.'); |
|
| 64 | + } |
|
| 65 | + |
|
| 66 | + return $this->branch($selector)->count() > 0; |
|
| 67 | + } |
|
| 68 | + |
|
| 69 | + /** |
|
| 70 | + * Reduce the elements matched by DOMQuery to only those which contain the given item. |
|
| 71 | + * |
|
| 72 | + * There are two ways in which this is different from jQuery's implementation: |
|
| 73 | + * - We allow ANY DOMNode, not just DOMElements. That means this will work on |
|
| 74 | + * processor instructions, text nodes, comments, etc. |
|
| 75 | + * - Unlike jQuery, this implementation of has() follows QueryPath standard behavior |
|
| 76 | + * and modifies the existing object. It does not create a brand new object. |
|
| 77 | + * |
|
| 78 | + * @param mixed $contained |
|
| 79 | + * - If $contained is a CSS selector (e.g. '#foo'), this will test to see |
|
| 80 | + * if the current DOMQuery has any elements that contain items that match |
|
| 81 | + * the selector. |
|
| 82 | + * - If $contained is a DOMNode, then this will test to see if THE EXACT DOMNode |
|
| 83 | + * exists in the currently matched elements. (Note that you cannot match across DOM trees, even if it is the |
|
| 84 | + * same document.) |
|
| 85 | + * |
|
| 86 | + * @return DOMQuery |
|
| 87 | + * @throws ParseException |
|
| 88 | + * @todo It would be trivially easy to add support for iterating over an array or Iterable of DOMNodes. |
|
| 89 | + * @since 2.1 |
|
| 90 | + * @author eabrand |
|
| 91 | + */ |
|
| 92 | + public function has($contained): Query |
|
| 93 | + { |
|
| 94 | + /* |
|
| 95 | 95 | if (count($this->matches) == 0) { |
| 96 | 96 | return false; |
| 97 | 97 | } |
| 98 | 98 | */ |
| 99 | - $found = new SplObjectStorage(); |
|
| 100 | - |
|
| 101 | - // If it's a selector, we just get all of the DOMNodes that match the selector. |
|
| 102 | - $nodes = []; |
|
| 103 | - if (is_string($contained)) { |
|
| 104 | - // Get the list of nodes. |
|
| 105 | - $nodes = $this->branch($contained)->get(); |
|
| 106 | - } elseif ($contained instanceof DOMNode) { |
|
| 107 | - // Make a list with one node. |
|
| 108 | - $nodes = [$contained]; |
|
| 109 | - } |
|
| 110 | - |
|
| 111 | - // Now we go through each of the nodes that we are testing. We want to find |
|
| 112 | - // ALL PARENTS that are in our existing DOMQuery matches. Those are the |
|
| 113 | - // ones we add to our new matches. |
|
| 114 | - foreach ($nodes as $original_node) { |
|
| 115 | - $node = $original_node; |
|
| 116 | - while (! empty($node)/* && $node != $node->ownerDocument*/) { |
|
| 117 | - if ($this->matches->contains($node)) { |
|
| 118 | - $found->attach($node); |
|
| 119 | - } |
|
| 120 | - $node = $node->parentNode; |
|
| 121 | - } |
|
| 122 | - } |
|
| 123 | - |
|
| 124 | - return $this->inst($found, null); |
|
| 125 | - } |
|
| 126 | - |
|
| 127 | - /** |
|
| 128 | - * Returns TRUE if any of the elements in the DOMQuery have the specified class. |
|
| 129 | - * |
|
| 130 | - * @param string $class |
|
| 131 | - * The name of the class. |
|
| 132 | - * |
|
| 133 | - * @return boolean |
|
| 134 | - * TRUE if the class exists in one or more of the elements, FALSE otherwise. |
|
| 135 | - * @see addClass() |
|
| 136 | - * @see removeClass() |
|
| 137 | - */ |
|
| 138 | - public function hasClass($class): bool |
|
| 139 | - { |
|
| 140 | - foreach ($this->matches as $m) { |
|
| 141 | - if ($m->hasAttribute('class')) { |
|
| 142 | - $vals = explode(' ', $m->getAttribute('class')); |
|
| 143 | - if (in_array($class, $vals)) { |
|
| 144 | - return true; |
|
| 145 | - } |
|
| 146 | - } |
|
| 147 | - } |
|
| 148 | - |
|
| 149 | - return false; |
|
| 150 | - } |
|
| 151 | - |
|
| 152 | - /** |
|
| 153 | - * Check to see if the given attribute is present. |
|
| 154 | - * |
|
| 155 | - * This returns TRUE if <em>all</em> selected items have the attribute, or |
|
| 156 | - * FALSE if at least one item does not have the attribute. |
|
| 157 | - * |
|
| 158 | - * @param string $attrName |
|
| 159 | - * The attribute name. |
|
| 160 | - * |
|
| 161 | - * @return boolean |
|
| 162 | - * TRUE if all matches have the attribute, FALSE otherwise. |
|
| 163 | - * @since 2.0 |
|
| 164 | - * @see attr() |
|
| 165 | - * @see hasClass() |
|
| 166 | - */ |
|
| 167 | - public function hasAttr($attrName): bool |
|
| 168 | - { |
|
| 169 | - foreach ($this->matches as $match) { |
|
| 170 | - if (! $match->hasAttribute($attrName)) { |
|
| 171 | - return false; |
|
| 172 | - } |
|
| 173 | - } |
|
| 174 | - |
|
| 175 | - return true; |
|
| 176 | - } |
|
| 177 | - |
|
| 178 | - /** |
|
| 179 | - * Remove the named attribute from all elements in the current DOMQuery. |
|
| 180 | - * |
|
| 181 | - * This will remove any attribute with the given name. It will do this on each |
|
| 182 | - * item currently wrapped by DOMQuery. |
|
| 183 | - * |
|
| 184 | - * As is the case in jQuery, this operation is not considered destructive. |
|
| 185 | - * |
|
| 186 | - * @param string $name |
|
| 187 | - * Name of the parameter to remove. |
|
| 188 | - * |
|
| 189 | - * @return DOMQuery |
|
| 190 | - * The DOMQuery object with the same elements. |
|
| 191 | - * @see attr() |
|
| 192 | - */ |
|
| 193 | - public function removeAttr($name): Query |
|
| 194 | - { |
|
| 195 | - foreach ($this->matches as $m) { |
|
| 196 | - $m->removeAttribute($name); |
|
| 197 | - } |
|
| 198 | - |
|
| 199 | - return $this; |
|
| 200 | - } |
|
| 99 | + $found = new SplObjectStorage(); |
|
| 100 | + |
|
| 101 | + // If it's a selector, we just get all of the DOMNodes that match the selector. |
|
| 102 | + $nodes = []; |
|
| 103 | + if (is_string($contained)) { |
|
| 104 | + // Get the list of nodes. |
|
| 105 | + $nodes = $this->branch($contained)->get(); |
|
| 106 | + } elseif ($contained instanceof DOMNode) { |
|
| 107 | + // Make a list with one node. |
|
| 108 | + $nodes = [$contained]; |
|
| 109 | + } |
|
| 110 | + |
|
| 111 | + // Now we go through each of the nodes that we are testing. We want to find |
|
| 112 | + // ALL PARENTS that are in our existing DOMQuery matches. Those are the |
|
| 113 | + // ones we add to our new matches. |
|
| 114 | + foreach ($nodes as $original_node) { |
|
| 115 | + $node = $original_node; |
|
| 116 | + while (! empty($node)/* && $node != $node->ownerDocument*/) { |
|
| 117 | + if ($this->matches->contains($node)) { |
|
| 118 | + $found->attach($node); |
|
| 119 | + } |
|
| 120 | + $node = $node->parentNode; |
|
| 121 | + } |
|
| 122 | + } |
|
| 123 | + |
|
| 124 | + return $this->inst($found, null); |
|
| 125 | + } |
|
| 126 | + |
|
| 127 | + /** |
|
| 128 | + * Returns TRUE if any of the elements in the DOMQuery have the specified class. |
|
| 129 | + * |
|
| 130 | + * @param string $class |
|
| 131 | + * The name of the class. |
|
| 132 | + * |
|
| 133 | + * @return boolean |
|
| 134 | + * TRUE if the class exists in one or more of the elements, FALSE otherwise. |
|
| 135 | + * @see addClass() |
|
| 136 | + * @see removeClass() |
|
| 137 | + */ |
|
| 138 | + public function hasClass($class): bool |
|
| 139 | + { |
|
| 140 | + foreach ($this->matches as $m) { |
|
| 141 | + if ($m->hasAttribute('class')) { |
|
| 142 | + $vals = explode(' ', $m->getAttribute('class')); |
|
| 143 | + if (in_array($class, $vals)) { |
|
| 144 | + return true; |
|
| 145 | + } |
|
| 146 | + } |
|
| 147 | + } |
|
| 148 | + |
|
| 149 | + return false; |
|
| 150 | + } |
|
| 151 | + |
|
| 152 | + /** |
|
| 153 | + * Check to see if the given attribute is present. |
|
| 154 | + * |
|
| 155 | + * This returns TRUE if <em>all</em> selected items have the attribute, or |
|
| 156 | + * FALSE if at least one item does not have the attribute. |
|
| 157 | + * |
|
| 158 | + * @param string $attrName |
|
| 159 | + * The attribute name. |
|
| 160 | + * |
|
| 161 | + * @return boolean |
|
| 162 | + * TRUE if all matches have the attribute, FALSE otherwise. |
|
| 163 | + * @since 2.0 |
|
| 164 | + * @see attr() |
|
| 165 | + * @see hasClass() |
|
| 166 | + */ |
|
| 167 | + public function hasAttr($attrName): bool |
|
| 168 | + { |
|
| 169 | + foreach ($this->matches as $match) { |
|
| 170 | + if (! $match->hasAttribute($attrName)) { |
|
| 171 | + return false; |
|
| 172 | + } |
|
| 173 | + } |
|
| 174 | + |
|
| 175 | + return true; |
|
| 176 | + } |
|
| 177 | + |
|
| 178 | + /** |
|
| 179 | + * Remove the named attribute from all elements in the current DOMQuery. |
|
| 180 | + * |
|
| 181 | + * This will remove any attribute with the given name. It will do this on each |
|
| 182 | + * item currently wrapped by DOMQuery. |
|
| 183 | + * |
|
| 184 | + * As is the case in jQuery, this operation is not considered destructive. |
|
| 185 | + * |
|
| 186 | + * @param string $name |
|
| 187 | + * Name of the parameter to remove. |
|
| 188 | + * |
|
| 189 | + * @return DOMQuery |
|
| 190 | + * The DOMQuery object with the same elements. |
|
| 191 | + * @see attr() |
|
| 192 | + */ |
|
| 193 | + public function removeAttr($name): Query |
|
| 194 | + { |
|
| 195 | + foreach ($this->matches as $m) { |
|
| 196 | + $m->removeAttribute($name); |
|
| 197 | + } |
|
| 198 | + |
|
| 199 | + return $this; |
|
| 200 | + } |
|
| 201 | 201 | } |
@@ -52,7 +52,7 @@ discard block |
||
| 52 | 52 | // pass an is() on [a, b, c, d]. We use the $seen SPLOS to prevent this. |
| 53 | 53 | $seen = new SplObjectStorage(); |
| 54 | 54 | foreach ($selector as $item) { |
| 55 | - if (! $this->matches->contains($item) || $seen->contains($item)) { |
|
| 55 | + if (!$this->matches->contains($item) || $seen->contains($item)) { |
|
| 56 | 56 | return false; |
| 57 | 57 | } |
| 58 | 58 | $seen->attach($item); |
@@ -113,7 +113,7 @@ discard block |
||
| 113 | 113 | // ones we add to our new matches. |
| 114 | 114 | foreach ($nodes as $original_node) { |
| 115 | 115 | $node = $original_node; |
| 116 | - while (! empty($node)/* && $node != $node->ownerDocument*/) { |
|
| 116 | + while (!empty($node)/* && $node != $node->ownerDocument*/) { |
|
| 117 | 117 | if ($this->matches->contains($node)) { |
| 118 | 118 | $found->attach($node); |
| 119 | 119 | } |
@@ -167,7 +167,7 @@ discard block |
||
| 167 | 167 | public function hasAttr($attrName): bool |
| 168 | 168 | { |
| 169 | 169 | foreach ($this->matches as $match) { |
| 170 | - if (! $match->hasAttribute($attrName)) { |
|
| 170 | + if (!$match->hasAttribute($attrName)) { |
|
| 171 | 171 | return false; |
| 172 | 172 | } |
| 173 | 173 | } |
@@ -22,1128 +22,1128 @@ |
||
| 22 | 22 | trait QueryFilters |
| 23 | 23 | { |
| 24 | 24 | |
| 25 | - /** |
|
| 26 | - * Filter a list down to only elements that match the selector. |
|
| 27 | - * Use this, for example, to find all elements with a class, or with |
|
| 28 | - * certain children. |
|
| 29 | - * |
|
| 30 | - * @param string $selector |
|
| 31 | - * The selector to use as a filter. |
|
| 32 | - * |
|
| 33 | - * @return Query The DOMQuery with non-matching items filtered out.* The DOMQuery with non-matching items |
|
| 34 | - * filtered out. |
|
| 35 | - * @throws ParseException |
|
| 36 | - * @see filterCallback() |
|
| 37 | - * @see map() |
|
| 38 | - * @see find() |
|
| 39 | - * @see is() |
|
| 40 | - * @see filterLambda() |
|
| 41 | - */ |
|
| 42 | - public function filter($selector): Query |
|
| 43 | - { |
|
| 44 | - $found = new SplObjectStorage(); |
|
| 45 | - $tmp = new SplObjectStorage(); |
|
| 46 | - |
|
| 47 | - foreach ($this->matches as $m) { |
|
| 48 | - $tmp->attach($m); |
|
| 49 | - // Seems like this should be right... but it fails unit |
|
| 50 | - // tests. Need to compare to jQuery. |
|
| 51 | - // $query = new \QueryPath\CSS\DOMTraverser($tmp, TRUE, $m); |
|
| 52 | - $query = new DOMTraverser($tmp); |
|
| 53 | - $query->find($selector); |
|
| 54 | - if (count($query->matches())) { |
|
| 55 | - $found->attach($m); |
|
| 56 | - } |
|
| 57 | - $tmp->detach($m); |
|
| 58 | - } |
|
| 59 | - |
|
| 60 | - return $this->inst($found, null); |
|
| 61 | - } |
|
| 62 | - |
|
| 63 | - /** |
|
| 64 | - * Filter based on a lambda function. |
|
| 65 | - * |
|
| 66 | - * The function string will be executed as if it were the body of a |
|
| 67 | - * function. It is passed two arguments: |
|
| 68 | - * - $index: The index of the item. |
|
| 69 | - * - $item: The current Element. |
|
| 70 | - * If the function returns boolean FALSE, the item will be removed from |
|
| 71 | - * the list of elements. Otherwise it will be kept. |
|
| 72 | - * |
|
| 73 | - * Example: |
|
| 74 | - * |
|
| 75 | - * @code |
|
| 76 | - * qp('li')->filterLambda('qp($item)->attr("id") == "test"'); |
|
| 77 | - * @endcode |
|
| 78 | - * |
|
| 79 | - * The above would filter down the list to only an item whose ID is |
|
| 80 | - * 'text'. |
|
| 81 | - * |
|
| 82 | - * @param string $fn |
|
| 83 | - * Inline lambda function in a string. |
|
| 84 | - * |
|
| 85 | - * @return DOMQuery |
|
| 86 | - * @throws ParseException |
|
| 87 | - * |
|
| 88 | - * @see map() |
|
| 89 | - * @see mapLambda() |
|
| 90 | - * @see filterCallback() |
|
| 91 | - * @see filter() |
|
| 92 | - * @deprecated |
|
| 93 | - * Since PHP 5.3 supports anonymous functions -- REAL Lambdas -- this |
|
| 94 | - * method is not necessary and should be avoided. |
|
| 95 | - */ |
|
| 96 | - public function filterLambda($fn): Query |
|
| 97 | - { |
|
| 98 | - $function = create_function('$index, $item', $fn); |
|
| 99 | - $found = new SplObjectStorage(); |
|
| 100 | - $i = 0; |
|
| 101 | - foreach ($this->matches as $item) { |
|
| 102 | - if ($function($i++, $item) !== false) { |
|
| 103 | - $found->attach($item); |
|
| 104 | - } |
|
| 105 | - } |
|
| 106 | - |
|
| 107 | - return $this->inst($found, null); |
|
| 108 | - } |
|
| 109 | - |
|
| 110 | - /** |
|
| 111 | - * Use regular expressions to filter based on the text content of matched elements. |
|
| 112 | - * |
|
| 113 | - * Only items that match the given regular expression will be kept. All others will |
|
| 114 | - * be removed. |
|
| 115 | - * |
|
| 116 | - * The regular expression is run against the <i>text content</i> (the PCDATA) of the |
|
| 117 | - * elements. This is a way of filtering elements based on their content. |
|
| 118 | - * |
|
| 119 | - * Example: |
|
| 120 | - * |
|
| 121 | - * @code |
|
| 122 | - * <?xml version="1.0"?> |
|
| 123 | - * <div>Hello <i>World</i></div> |
|
| 124 | - * @endcode |
|
| 125 | - * |
|
| 126 | - * @code |
|
| 127 | - * <?php |
|
| 128 | - * // This will be 1. |
|
| 129 | - * qp($xml, 'div')->filterPreg('/World/')->matches->count(); |
|
| 130 | - * ?> |
|
| 131 | - * @endcode |
|
| 132 | - * |
|
| 133 | - * The return value above will be 1 because the text content of @codeqp($xml, 'div')@endcode is |
|
| 134 | - * @codeHello World@endcode. |
|
| 135 | - * |
|
| 136 | - * Compare this to the behavior of the <em>:contains()</em> CSS3 pseudo-class. |
|
| 137 | - * |
|
| 138 | - * @param string $regex |
|
| 139 | - * A regular expression. |
|
| 140 | - * |
|
| 141 | - * @return DOMQuery |
|
| 142 | - * @throws ParseException |
|
| 143 | - * @see filterCallback() |
|
| 144 | - * @see preg_match() |
|
| 145 | - * @see filter() |
|
| 146 | - */ |
|
| 147 | - public function filterPreg($regex): Query |
|
| 148 | - { |
|
| 149 | - $found = new SplObjectStorage(); |
|
| 150 | - |
|
| 151 | - foreach ($this->matches as $item) { |
|
| 152 | - if (preg_match($regex, $item->textContent) > 0) { |
|
| 153 | - $found->attach($item); |
|
| 154 | - } |
|
| 155 | - } |
|
| 156 | - |
|
| 157 | - return $this->inst($found, null); |
|
| 158 | - } |
|
| 159 | - |
|
| 160 | - /** |
|
| 161 | - * Filter based on a callback function. |
|
| 162 | - * |
|
| 163 | - * A callback may be any of the following: |
|
| 164 | - * - a function: 'my_func'. |
|
| 165 | - * - an object/method combo: $obj, 'myMethod' |
|
| 166 | - * - a class/method combo: 'MyClass', 'myMethod' |
|
| 167 | - * Note that classes are passed in strings. Objects are not. |
|
| 168 | - * |
|
| 169 | - * Each callback is passed to arguments: |
|
| 170 | - * - $index: The index position of the object in the array. |
|
| 171 | - * - $item: The item to be operated upon. |
|
| 172 | - * |
|
| 173 | - * If the callback function returns FALSE, the item will be removed from the |
|
| 174 | - * set of matches. Otherwise the item will be considered a match and left alone. |
|
| 175 | - * |
|
| 176 | - * @param callback $callback . |
|
| 177 | - * A callback either as a string (function) or an array (object, method OR |
|
| 178 | - * classname, method). |
|
| 179 | - * |
|
| 180 | - * @return DOMQuery |
|
| 181 | - * Query path object augmented according to the function. |
|
| 182 | - * @throws ParseException |
|
| 183 | - * @throws Exception |
|
| 184 | - * @see map() |
|
| 185 | - * @see is() |
|
| 186 | - * @see find() |
|
| 187 | - * @see filter() |
|
| 188 | - * @see filterLambda() |
|
| 189 | - */ |
|
| 190 | - public function filterCallback($callback): Query |
|
| 191 | - { |
|
| 192 | - $found = new SplObjectStorage(); |
|
| 193 | - $i = 0; |
|
| 194 | - if (is_callable($callback)) { |
|
| 195 | - foreach ($this->matches as $item) { |
|
| 196 | - if ($callback($i++, $item) !== false) { |
|
| 197 | - $found->attach($item); |
|
| 198 | - } |
|
| 199 | - } |
|
| 200 | - } else { |
|
| 201 | - throw new Exception('The specified callback is not callable.'); |
|
| 202 | - } |
|
| 203 | - |
|
| 204 | - return $this->inst($found, null); |
|
| 205 | - } |
|
| 206 | - |
|
| 207 | - /** |
|
| 208 | - * Run a function on each item in a set. |
|
| 209 | - * |
|
| 210 | - * The mapping callback can return anything. Whatever it returns will be |
|
| 211 | - * stored as a match in the set, though. This means that afer a map call, |
|
| 212 | - * there is no guarantee that the elements in the set will behave correctly |
|
| 213 | - * with other DOMQuery functions. |
|
| 214 | - * |
|
| 215 | - * Callback rules: |
|
| 216 | - * - If the callback returns NULL, the item will be removed from the array. |
|
| 217 | - * - If the callback returns an array, the entire array will be stored in |
|
| 218 | - * the results. |
|
| 219 | - * - If the callback returns anything else, it will be appended to the array |
|
| 220 | - * of matches. |
|
| 221 | - * |
|
| 222 | - * @param callback $callback |
|
| 223 | - * The function or callback to use. The callback will be passed two params: |
|
| 224 | - * - $index: The index position in the list of items wrapped by this object. |
|
| 225 | - * - $item: The current item. |
|
| 226 | - * |
|
| 227 | - * @return DOMQuery |
|
| 228 | - * The DOMQuery object wrapping a list of whatever values were returned |
|
| 229 | - * by each run of the callback. |
|
| 230 | - * |
|
| 231 | - * @throws Exception |
|
| 232 | - * @throws ParseException |
|
| 233 | - * @see find() |
|
| 234 | - * @see DOMQuery::get() |
|
| 235 | - * @see filter() |
|
| 236 | - */ |
|
| 237 | - public function map($callback): Query |
|
| 238 | - { |
|
| 239 | - $found = new SplObjectStorage(); |
|
| 240 | - |
|
| 241 | - if (is_callable($callback)) { |
|
| 242 | - $i = 0; |
|
| 243 | - foreach ($this->matches as $item) { |
|
| 244 | - $c = call_user_func($callback, $i, $item); |
|
| 245 | - if (isset($c)) { |
|
| 246 | - if (is_array($c) || $c instanceof Iterable) { |
|
| 247 | - foreach ($c as $retval) { |
|
| 248 | - if (! is_object($retval)) { |
|
| 249 | - $tmp = new stdClass(); |
|
| 250 | - $tmp->textContent = $retval; |
|
| 251 | - $retval = $tmp; |
|
| 252 | - } |
|
| 253 | - $found->attach($retval); |
|
| 254 | - } |
|
| 255 | - } else { |
|
| 256 | - if (! is_object($c)) { |
|
| 257 | - $tmp = new stdClass(); |
|
| 258 | - $tmp->textContent = $c; |
|
| 259 | - $c = $tmp; |
|
| 260 | - } |
|
| 261 | - $found->attach($c); |
|
| 262 | - } |
|
| 263 | - } |
|
| 264 | - ++$i; |
|
| 265 | - } |
|
| 266 | - } else { |
|
| 267 | - throw new Exception('Callback is not callable.'); |
|
| 268 | - } |
|
| 269 | - |
|
| 270 | - return $this->inst($found, null); |
|
| 271 | - } |
|
| 272 | - |
|
| 273 | - /** |
|
| 274 | - * Narrow the items in this object down to only a slice of the starting items. |
|
| 275 | - * |
|
| 276 | - * @param integer $start |
|
| 277 | - * Where in the list of matches to begin the slice. |
|
| 278 | - * @param integer $length |
|
| 279 | - * The number of items to include in the slice. If nothing is specified, the |
|
| 280 | - * all remaining matches (from $start onward) will be included in the sliced |
|
| 281 | - * list. |
|
| 282 | - * |
|
| 283 | - * @return DOMQuery |
|
| 284 | - * @throws ParseException |
|
| 285 | - * @see array_slice() |
|
| 286 | - */ |
|
| 287 | - public function slice($start, $length = 0): Query |
|
| 288 | - { |
|
| 289 | - $end = $length; |
|
| 290 | - $found = new SplObjectStorage(); |
|
| 291 | - if ($start >= $this->count()) { |
|
| 292 | - return $this->inst($found, null); |
|
| 293 | - } |
|
| 294 | - |
|
| 295 | - $i = $j = 0; |
|
| 296 | - foreach ($this->matches as $m) { |
|
| 297 | - if ($i >= $start) { |
|
| 298 | - if ($end > 0 && $j >= $end) { |
|
| 299 | - break; |
|
| 300 | - } |
|
| 301 | - $found->attach($m); |
|
| 302 | - ++$j; |
|
| 303 | - } |
|
| 304 | - ++$i; |
|
| 305 | - } |
|
| 306 | - |
|
| 307 | - return $this->inst($found, null); |
|
| 308 | - } |
|
| 309 | - |
|
| 310 | - /** |
|
| 311 | - * Run a callback on each item in the list of items. |
|
| 312 | - * |
|
| 313 | - * Rules of the callback: |
|
| 314 | - * - A callback is passed two variables: $index and $item. (There is no |
|
| 315 | - * special treatment of $this, as there is in jQuery.) |
|
| 316 | - * - You will want to pass $item by reference if it is not an |
|
| 317 | - * object (DOMNodes are all objects). |
|
| 318 | - * - A callback that returns FALSE will stop execution of the each() loop. This |
|
| 319 | - * works like break in a standard loop. |
|
| 320 | - * - A TRUE return value from the callback is analogous to a continue statement. |
|
| 321 | - * - All other return values are ignored. |
|
| 322 | - * |
|
| 323 | - * @param callback $callback |
|
| 324 | - * The callback to run. |
|
| 325 | - * |
|
| 326 | - * @return DOMQuery |
|
| 327 | - * The DOMQuery. |
|
| 328 | - * @throws Exception |
|
| 329 | - * @see filter() |
|
| 330 | - * @see map() |
|
| 331 | - * @see eachLambda() |
|
| 332 | - */ |
|
| 333 | - public function each($callback): Query |
|
| 334 | - { |
|
| 335 | - if (is_callable($callback)) { |
|
| 336 | - $i = 0; |
|
| 337 | - foreach ($this->matches as $item) { |
|
| 338 | - if (call_user_func($callback, $i, $item) === false) { |
|
| 339 | - return $this; |
|
| 340 | - } |
|
| 341 | - ++$i; |
|
| 342 | - } |
|
| 343 | - } else { |
|
| 344 | - throw new Exception('Callback is not callable.'); |
|
| 345 | - } |
|
| 346 | - |
|
| 347 | - return $this; |
|
| 348 | - } |
|
| 349 | - |
|
| 350 | - /** |
|
| 351 | - * An each() iterator that takes a lambda function. |
|
| 352 | - * |
|
| 353 | - * @param string $lambda |
|
| 354 | - * The lambda function. This will be passed ($index, &$item). |
|
| 355 | - * |
|
| 356 | - * @return DOMQuery |
|
| 357 | - * The DOMQuery object. |
|
| 358 | - * @deprecated |
|
| 359 | - * Since PHP 5.3 supports anonymous functions -- REAL Lambdas -- this |
|
| 360 | - * method is not necessary and should be avoided. |
|
| 361 | - * @see each() |
|
| 362 | - * @see filterLambda() |
|
| 363 | - * @see filterCallback() |
|
| 364 | - * @see map() |
|
| 365 | - */ |
|
| 366 | - public function eachLambda($lambda): Query |
|
| 367 | - { |
|
| 368 | - $index = 0; |
|
| 369 | - foreach ($this->matches as $item) { |
|
| 370 | - $fn = create_function('$index, &$item', $lambda); |
|
| 371 | - if ($fn($index, $item) === false) { |
|
| 372 | - return $this; |
|
| 373 | - } |
|
| 374 | - ++$index; |
|
| 375 | - } |
|
| 376 | - |
|
| 377 | - return $this; |
|
| 378 | - } |
|
| 379 | - |
|
| 380 | - /** |
|
| 381 | - * Get the even elements, so counter-intuitively 1, 3, 5, etc. |
|
| 382 | - * |
|
| 383 | - * @return DOMQuery |
|
| 384 | - * A DOMQuery wrapping all of the children. |
|
| 385 | - * @throws ParseException |
|
| 386 | - * @see parent() |
|
| 387 | - * @see parents() |
|
| 388 | - * @see next() |
|
| 389 | - * @see prev() |
|
| 390 | - * @since 2.1 |
|
| 391 | - * @author eabrand |
|
| 392 | - * @see removeChildren() |
|
| 393 | - */ |
|
| 394 | - public function even(): Query |
|
| 395 | - { |
|
| 396 | - $found = new SplObjectStorage(); |
|
| 397 | - $even = false; |
|
| 398 | - foreach ($this->matches as $m) { |
|
| 399 | - if ($even && $m->nodeType === XML_ELEMENT_NODE) { |
|
| 400 | - $found->attach($m); |
|
| 401 | - } |
|
| 402 | - $even = $even ? false : true; |
|
| 403 | - } |
|
| 404 | - |
|
| 405 | - return $this->inst($found, null); |
|
| 406 | - } |
|
| 407 | - |
|
| 408 | - /** |
|
| 409 | - * Get the odd elements, so counter-intuitively 0, 2, 4, etc. |
|
| 410 | - * |
|
| 411 | - * @return DOMQuery |
|
| 412 | - * A DOMQuery wrapping all of the children. |
|
| 413 | - * @throws ParseException |
|
| 414 | - * @see parent() |
|
| 415 | - * @see parents() |
|
| 416 | - * @see next() |
|
| 417 | - * @see prev() |
|
| 418 | - * @since 2.1 |
|
| 419 | - * @author eabrand |
|
| 420 | - * @see removeChildren() |
|
| 421 | - */ |
|
| 422 | - public function odd(): Query |
|
| 423 | - { |
|
| 424 | - $found = new SplObjectStorage(); |
|
| 425 | - $odd = true; |
|
| 426 | - foreach ($this->matches as $m) { |
|
| 427 | - if ($odd && $m->nodeType === XML_ELEMENT_NODE) { |
|
| 428 | - $found->attach($m); |
|
| 429 | - } |
|
| 430 | - $odd = $odd ? false : true; |
|
| 431 | - } |
|
| 432 | - |
|
| 433 | - return $this->inst($found, null); |
|
| 434 | - } |
|
| 435 | - |
|
| 436 | - /** |
|
| 437 | - * Get the first matching element. |
|
| 438 | - * |
|
| 439 | - * |
|
| 440 | - * @return DOMQuery |
|
| 441 | - * A DOMQuery wrapping all of the children. |
|
| 442 | - * @throws ParseException |
|
| 443 | - * @see prev() |
|
| 444 | - * @since 2.1 |
|
| 445 | - * @author eabrand |
|
| 446 | - * @see next() |
|
| 447 | - */ |
|
| 448 | - public function first(): Query |
|
| 449 | - { |
|
| 450 | - $found = new SplObjectStorage(); |
|
| 451 | - foreach ($this->matches as $m) { |
|
| 452 | - if ($m->nodeType === XML_ELEMENT_NODE) { |
|
| 453 | - $found->attach($m); |
|
| 454 | - break; |
|
| 455 | - } |
|
| 456 | - } |
|
| 457 | - |
|
| 458 | - return $this->inst($found, null); |
|
| 459 | - } |
|
| 460 | - |
|
| 461 | - /** |
|
| 462 | - * Get the first child of the matching element. |
|
| 463 | - * |
|
| 464 | - * |
|
| 465 | - * @return DOMQuery |
|
| 466 | - * A DOMQuery wrapping all of the children. |
|
| 467 | - * @throws ParseException |
|
| 468 | - * @see prev() |
|
| 469 | - * @since 2.1 |
|
| 470 | - * @author eabrand |
|
| 471 | - * @see next() |
|
| 472 | - */ |
|
| 473 | - public function firstChild(): Query |
|
| 474 | - { |
|
| 475 | - // Could possibly use $m->firstChild http://theserverpages.com/php/manual/en/ref.dom.php |
|
| 476 | - $found = new SplObjectStorage(); |
|
| 477 | - $flag = false; |
|
| 478 | - foreach ($this->matches as $m) { |
|
| 479 | - foreach ($m->childNodes as $c) { |
|
| 480 | - if ($c->nodeType === XML_ELEMENT_NODE) { |
|
| 481 | - $found->attach($c); |
|
| 482 | - $flag = true; |
|
| 483 | - break; |
|
| 484 | - } |
|
| 485 | - } |
|
| 486 | - if ($flag) { |
|
| 487 | - break; |
|
| 488 | - } |
|
| 489 | - } |
|
| 490 | - |
|
| 491 | - return $this->inst($found, null); |
|
| 492 | - } |
|
| 493 | - |
|
| 494 | - /** |
|
| 495 | - * Get the last matching element. |
|
| 496 | - * |
|
| 497 | - * |
|
| 498 | - * @return DOMQuery |
|
| 499 | - * A DOMQuery wrapping all of the children. |
|
| 500 | - * @throws ParseException |
|
| 501 | - * @see prev() |
|
| 502 | - * @since 2.1 |
|
| 503 | - * @author eabrand |
|
| 504 | - * @see next() |
|
| 505 | - */ |
|
| 506 | - public function last(): Query |
|
| 507 | - { |
|
| 508 | - $found = new SplObjectStorage(); |
|
| 509 | - $item = null; |
|
| 510 | - foreach ($this->matches as $m) { |
|
| 511 | - if ($m->nodeType === XML_ELEMENT_NODE) { |
|
| 512 | - $item = $m; |
|
| 513 | - } |
|
| 514 | - } |
|
| 515 | - if ($item) { |
|
| 516 | - $found->attach($item); |
|
| 517 | - } |
|
| 518 | - |
|
| 519 | - return $this->inst($found, null); |
|
| 520 | - } |
|
| 521 | - |
|
| 522 | - /** |
|
| 523 | - * Get the last child of the matching element. |
|
| 524 | - * |
|
| 525 | - * |
|
| 526 | - * @return DOMQuery |
|
| 527 | - * A DOMQuery wrapping all of the children. |
|
| 528 | - * @throws ParseException |
|
| 529 | - * @see prev() |
|
| 530 | - * @since 2.1 |
|
| 531 | - * @author eabrand |
|
| 532 | - * @see next() |
|
| 533 | - */ |
|
| 534 | - public function lastChild(): Query |
|
| 535 | - { |
|
| 536 | - $found = new SplObjectStorage(); |
|
| 537 | - $item = null; |
|
| 538 | - foreach ($this->matches as $m) { |
|
| 539 | - foreach ($m->childNodes as $c) { |
|
| 540 | - if ($c->nodeType === XML_ELEMENT_NODE) { |
|
| 541 | - $item = $c; |
|
| 542 | - } |
|
| 543 | - } |
|
| 544 | - if ($item) { |
|
| 545 | - $found->attach($item); |
|
| 546 | - $item = null; |
|
| 547 | - } |
|
| 548 | - } |
|
| 549 | - |
|
| 550 | - return $this->inst($found, null); |
|
| 551 | - } |
|
| 552 | - |
|
| 553 | - /** |
|
| 554 | - * Get all siblings after an element until the selector is reached. |
|
| 555 | - * |
|
| 556 | - * For each element in the DOMQuery, get all siblings that appear after |
|
| 557 | - * it. If a selector is passed in, then only siblings that match the |
|
| 558 | - * selector will be included. |
|
| 559 | - * |
|
| 560 | - * @param string $selector |
|
| 561 | - * A valid CSS 3 selector. |
|
| 562 | - * |
|
| 563 | - * @return DOMQuery |
|
| 564 | - * The DOMQuery object, now containing the matching siblings. |
|
| 565 | - * @throws Exception |
|
| 566 | - * @see prevAll() |
|
| 567 | - * @see children() |
|
| 568 | - * @see siblings() |
|
| 569 | - * @since 2.1 |
|
| 570 | - * @author eabrand |
|
| 571 | - * @see next() |
|
| 572 | - */ |
|
| 573 | - public function nextUntil($selector = null): Query |
|
| 574 | - { |
|
| 575 | - $found = new SplObjectStorage(); |
|
| 576 | - foreach ($this->matches as $m) { |
|
| 577 | - while (isset($m->nextSibling)) { |
|
| 578 | - $m = $m->nextSibling; |
|
| 579 | - if ($m->nodeType === XML_ELEMENT_NODE) { |
|
| 580 | - if (null !== $selector && QueryPath::with($m, null, $this->options)->is($selector) > 0) { |
|
| 581 | - break; |
|
| 582 | - } |
|
| 583 | - $found->attach($m); |
|
| 584 | - } |
|
| 585 | - } |
|
| 586 | - } |
|
| 587 | - |
|
| 588 | - return $this->inst($found, null); |
|
| 589 | - } |
|
| 590 | - |
|
| 591 | - /** |
|
| 592 | - * Get the previous siblings for each element in the DOMQuery |
|
| 593 | - * until the selector is reached. |
|
| 594 | - * |
|
| 595 | - * For each element in the DOMQuery, get all previous siblings. If a |
|
| 596 | - * selector is provided, only matching siblings will be retrieved. |
|
| 597 | - * |
|
| 598 | - * @param string $selector |
|
| 599 | - * A valid CSS 3 selector. |
|
| 600 | - * |
|
| 601 | - * @return DOMQuery |
|
| 602 | - * The DOMQuery object, now wrapping previous sibling elements. |
|
| 603 | - * @throws Exception |
|
| 604 | - * @see prev() |
|
| 605 | - * @see nextAll() |
|
| 606 | - * @see siblings() |
|
| 607 | - * @see contents() |
|
| 608 | - * @see children() |
|
| 609 | - * @since 2.1 |
|
| 610 | - * @author eabrand |
|
| 611 | - */ |
|
| 612 | - public function prevUntil($selector = null): Query |
|
| 613 | - { |
|
| 614 | - $found = new SplObjectStorage(); |
|
| 615 | - foreach ($this->matches as $m) { |
|
| 616 | - while (isset($m->previousSibling)) { |
|
| 617 | - $m = $m->previousSibling; |
|
| 618 | - if ($m->nodeType === XML_ELEMENT_NODE) { |
|
| 619 | - if (null !== $selector && QueryPath::with($m, null, $this->options)->is($selector)) { |
|
| 620 | - break; |
|
| 621 | - } |
|
| 622 | - |
|
| 623 | - $found->attach($m); |
|
| 624 | - } |
|
| 625 | - } |
|
| 626 | - } |
|
| 627 | - |
|
| 628 | - return $this->inst($found, null); |
|
| 629 | - } |
|
| 630 | - |
|
| 631 | - /** |
|
| 632 | - * Get all ancestors of each element in the DOMQuery until the selector is reached. |
|
| 633 | - * |
|
| 634 | - * If a selector is present, only matching ancestors will be retrieved. |
|
| 635 | - * |
|
| 636 | - * @param string $selector |
|
| 637 | - * A valid CSS 3 Selector. |
|
| 638 | - * |
|
| 639 | - * @return DOMQuery |
|
| 640 | - * A DOMNode object containing the matching ancestors. |
|
| 641 | - * @throws Exception |
|
| 642 | - * @see siblings() |
|
| 643 | - * @see children() |
|
| 644 | - * @since 2.1 |
|
| 645 | - * @author eabrand |
|
| 646 | - * @see parent() |
|
| 647 | - */ |
|
| 648 | - public function parentsUntil($selector = null): Query |
|
| 649 | - { |
|
| 650 | - $found = new SplObjectStorage(); |
|
| 651 | - foreach ($this->matches as $m) { |
|
| 652 | - while ($m->parentNode->nodeType !== XML_DOCUMENT_NODE) { |
|
| 653 | - $m = $m->parentNode; |
|
| 654 | - // Is there any case where parent node is not an element? |
|
| 655 | - if ($m->nodeType === XML_ELEMENT_NODE) { |
|
| 656 | - if (! empty($selector)) { |
|
| 657 | - if (QueryPath::with($m, null, $this->options)->is($selector) > 0) { |
|
| 658 | - break; |
|
| 659 | - } |
|
| 660 | - $found->attach($m); |
|
| 661 | - } else { |
|
| 662 | - $found->attach($m); |
|
| 663 | - } |
|
| 664 | - } |
|
| 665 | - } |
|
| 666 | - } |
|
| 667 | - |
|
| 668 | - return $this->inst($found, null); |
|
| 669 | - } |
|
| 670 | - |
|
| 671 | - /** |
|
| 672 | - * Reduce the matched set to just one. |
|
| 673 | - * |
|
| 674 | - * This will take a matched set and reduce it to just one item -- the item |
|
| 675 | - * at the index specified. This is a destructive operation, and can be undone |
|
| 676 | - * with {@link end()}. |
|
| 677 | - * |
|
| 678 | - * @param $index |
|
| 679 | - * The index of the element to keep. The rest will be |
|
| 680 | - * discarded. |
|
| 681 | - * |
|
| 682 | - * @return DOMQuery |
|
| 683 | - * @throws ParseException |
|
| 684 | - * @see is() |
|
| 685 | - * @see end() |
|
| 686 | - * @see get() |
|
| 687 | - */ |
|
| 688 | - public function eq($index): Query |
|
| 689 | - { |
|
| 690 | - return $this->inst($this->getNthMatch($index), null); |
|
| 691 | - } |
|
| 692 | - |
|
| 693 | - /** |
|
| 694 | - * Filter a list to contain only items that do NOT match. |
|
| 695 | - * |
|
| 696 | - * @param string $selector |
|
| 697 | - * A selector to use as a negation filter. If the filter is matched, the |
|
| 698 | - * element will be removed from the list. |
|
| 699 | - * |
|
| 700 | - * @return DOMQuery |
|
| 701 | - * The DOMQuery object with matching items filtered out. |
|
| 702 | - * @throws ParseException |
|
| 703 | - * @throws Exception |
|
| 704 | - * @see find() |
|
| 705 | - */ |
|
| 706 | - public function not($selector): Query |
|
| 707 | - { |
|
| 708 | - $found = new SplObjectStorage(); |
|
| 709 | - if ($selector instanceof DOMElement) { |
|
| 710 | - foreach ($this->matches as $m) { |
|
| 711 | - if ($m !== $selector) { |
|
| 712 | - $found->attach($m); |
|
| 713 | - } |
|
| 714 | - } |
|
| 715 | - } elseif (is_array($selector)) { |
|
| 716 | - foreach ($this->matches as $m) { |
|
| 717 | - if (! in_array($m, $selector, true)) { |
|
| 718 | - $found->attach($m); |
|
| 719 | - } |
|
| 720 | - } |
|
| 721 | - } elseif ($selector instanceof SplObjectStorage) { |
|
| 722 | - foreach ($this->matches as $m) { |
|
| 723 | - if ($selector->contains($m)) { |
|
| 724 | - $found->attach($m); |
|
| 725 | - } |
|
| 726 | - } |
|
| 727 | - } else { |
|
| 728 | - foreach ($this->matches as $m) { |
|
| 729 | - if (! QueryPath::with($m, null, $this->options)->is($selector)) { |
|
| 730 | - $found->attach($m); |
|
| 731 | - } |
|
| 732 | - } |
|
| 733 | - } |
|
| 734 | - |
|
| 735 | - return $this->inst($found, null); |
|
| 736 | - } |
|
| 737 | - |
|
| 738 | - /** |
|
| 739 | - * Find the closest element matching the selector. |
|
| 740 | - * |
|
| 741 | - * This finds the closest match in the ancestry chain. It first checks the |
|
| 742 | - * present element. If the present element does not match, this traverses up |
|
| 743 | - * the ancestry chain (e.g. checks each parent) looking for an item that matches. |
|
| 744 | - * |
|
| 745 | - * It is provided for jQuery 1.3 compatibility. |
|
| 746 | - * |
|
| 747 | - * @param string $selector |
|
| 748 | - * A CSS Selector to match. |
|
| 749 | - * |
|
| 750 | - * @return DOMQuery |
|
| 751 | - * The set of matches. |
|
| 752 | - * @throws Exception |
|
| 753 | - * @since 2.0 |
|
| 754 | - */ |
|
| 755 | - public function closest($selector): Query |
|
| 756 | - { |
|
| 757 | - $found = new SplObjectStorage(); |
|
| 758 | - foreach ($this->matches as $m) { |
|
| 759 | - if (QueryPath::with($m, null, $this->options)->is($selector) > 0) { |
|
| 760 | - $found->attach($m); |
|
| 761 | - } else { |
|
| 762 | - while ($m->parentNode->nodeType !== XML_DOCUMENT_NODE) { |
|
| 763 | - $m = $m->parentNode; |
|
| 764 | - // Is there any case where parent node is not an element? |
|
| 765 | - if ($m->nodeType === XML_ELEMENT_NODE && QueryPath::with( |
|
| 766 | - $m, |
|
| 767 | - null, |
|
| 768 | - $this->options |
|
| 769 | - )->is($selector) > 0) { |
|
| 770 | - $found->attach($m); |
|
| 771 | - break; |
|
| 772 | - } |
|
| 773 | - } |
|
| 774 | - } |
|
| 775 | - } |
|
| 776 | - |
|
| 777 | - // XXX: Should this be an in-place modification? |
|
| 778 | - return $this->inst($found, null); |
|
| 779 | - } |
|
| 780 | - |
|
| 781 | - /** |
|
| 782 | - * Get the immediate parent of each element in the DOMQuery. |
|
| 783 | - * |
|
| 784 | - * If a selector is passed, this will return the nearest matching parent for |
|
| 785 | - * each element in the DOMQuery. |
|
| 786 | - * |
|
| 787 | - * @param string $selector |
|
| 788 | - * A valid CSS3 selector. |
|
| 789 | - * |
|
| 790 | - * @return DOMQuery |
|
| 791 | - * A DOMNode object wrapping the matching parents. |
|
| 792 | - * @throws Exception |
|
| 793 | - * @see siblings() |
|
| 794 | - * @see parents() |
|
| 795 | - * @see children() |
|
| 796 | - */ |
|
| 797 | - public function parent($selector = null): Query |
|
| 798 | - { |
|
| 799 | - $found = new SplObjectStorage(); |
|
| 800 | - foreach ($this->matches as $m) { |
|
| 801 | - while ($m->parentNode->nodeType !== XML_DOCUMENT_NODE) { |
|
| 802 | - $m = $m->parentNode; |
|
| 803 | - // Is there any case where parent node is not an element? |
|
| 804 | - if ($m->nodeType === XML_ELEMENT_NODE) { |
|
| 805 | - if (! empty($selector)) { |
|
| 806 | - if (QueryPath::with($m, null, $this->options)->is($selector) > 0) { |
|
| 807 | - $found->attach($m); |
|
| 808 | - break; |
|
| 809 | - } |
|
| 810 | - } else { |
|
| 811 | - $found->attach($m); |
|
| 812 | - break; |
|
| 813 | - } |
|
| 814 | - } |
|
| 815 | - } |
|
| 816 | - } |
|
| 817 | - |
|
| 818 | - return $this->inst($found, null); |
|
| 819 | - } |
|
| 820 | - |
|
| 821 | - /** |
|
| 822 | - * Get all ancestors of each element in the DOMQuery. |
|
| 823 | - * |
|
| 824 | - * If a selector is present, only matching ancestors will be retrieved. |
|
| 825 | - * |
|
| 826 | - * @param string $selector |
|
| 827 | - * A valid CSS 3 Selector. |
|
| 828 | - * |
|
| 829 | - * @return DOMQuery |
|
| 830 | - * A DOMNode object containing the matching ancestors. |
|
| 831 | - * @throws ParseException |
|
| 832 | - * @throws Exception |
|
| 833 | - * @see children() |
|
| 834 | - * @see parent() |
|
| 835 | - * @see siblings() |
|
| 836 | - */ |
|
| 837 | - public function parents($selector = null): Query |
|
| 838 | - { |
|
| 839 | - $found = new SplObjectStorage(); |
|
| 840 | - foreach ($this->matches as $m) { |
|
| 841 | - while ($m->parentNode->nodeType !== XML_DOCUMENT_NODE) { |
|
| 842 | - $m = $m->parentNode; |
|
| 843 | - // Is there any case where parent node is not an element? |
|
| 844 | - if ($m->nodeType === XML_ELEMENT_NODE) { |
|
| 845 | - if (! empty($selector)) { |
|
| 846 | - if (QueryPath::with($m, null, $this->options)->is($selector) > 0) { |
|
| 847 | - $found->attach($m); |
|
| 848 | - } |
|
| 849 | - } else { |
|
| 850 | - $found->attach($m); |
|
| 851 | - } |
|
| 852 | - } |
|
| 853 | - } |
|
| 854 | - } |
|
| 855 | - |
|
| 856 | - return $this->inst($found, null); |
|
| 857 | - } |
|
| 858 | - |
|
| 859 | - /** |
|
| 860 | - * Get the next sibling of each element in the DOMQuery. |
|
| 861 | - * |
|
| 862 | - * If a selector is provided, the next matching sibling will be returned. |
|
| 863 | - * |
|
| 864 | - * @param string $selector |
|
| 865 | - * A CSS3 selector. |
|
| 866 | - * |
|
| 867 | - * @return DOMQuery |
|
| 868 | - * The DOMQuery object. |
|
| 869 | - * @throws Exception |
|
| 870 | - * @throws ParseException |
|
| 871 | - * @see nextAll() |
|
| 872 | - * @see prev() |
|
| 873 | - * @see children() |
|
| 874 | - * @see contents() |
|
| 875 | - * @see parent() |
|
| 876 | - * @see parents() |
|
| 877 | - */ |
|
| 878 | - public function next($selector = null): Query |
|
| 879 | - { |
|
| 880 | - $found = new SplObjectStorage(); |
|
| 881 | - foreach ($this->matches as $m) { |
|
| 882 | - while (isset($m->nextSibling)) { |
|
| 883 | - $m = $m->nextSibling; |
|
| 884 | - if ($m->nodeType === XML_ELEMENT_NODE) { |
|
| 885 | - if (! empty($selector)) { |
|
| 886 | - if (QueryPath::with($m, null, $this->options)->is($selector) > 0) { |
|
| 887 | - $found->attach($m); |
|
| 888 | - break; |
|
| 889 | - } |
|
| 890 | - } else { |
|
| 891 | - $found->attach($m); |
|
| 892 | - break; |
|
| 893 | - } |
|
| 894 | - } |
|
| 895 | - } |
|
| 896 | - } |
|
| 897 | - |
|
| 898 | - return $this->inst($found, null); |
|
| 899 | - } |
|
| 900 | - |
|
| 901 | - /** |
|
| 902 | - * Get all siblings after an element. |
|
| 903 | - * |
|
| 904 | - * For each element in the DOMQuery, get all siblings that appear after |
|
| 905 | - * it. If a selector is passed in, then only siblings that match the |
|
| 906 | - * selector will be included. |
|
| 907 | - * |
|
| 908 | - * @param string $selector |
|
| 909 | - * A valid CSS 3 selector. |
|
| 910 | - * |
|
| 911 | - * @return DOMQuery |
|
| 912 | - * The DOMQuery object, now containing the matching siblings. |
|
| 913 | - * @throws Exception |
|
| 914 | - * @throws ParseException |
|
| 915 | - * @see next() |
|
| 916 | - * @see prevAll() |
|
| 917 | - * @see children() |
|
| 918 | - * @see siblings() |
|
| 919 | - */ |
|
| 920 | - public function nextAll($selector = null): Query |
|
| 921 | - { |
|
| 922 | - $found = new SplObjectStorage(); |
|
| 923 | - foreach ($this->matches as $m) { |
|
| 924 | - while (isset($m->nextSibling)) { |
|
| 925 | - $m = $m->nextSibling; |
|
| 926 | - if ($m->nodeType === XML_ELEMENT_NODE) { |
|
| 927 | - if (! empty($selector)) { |
|
| 928 | - if (QueryPath::with($m, null, $this->options)->is($selector) > 0) { |
|
| 929 | - $found->attach($m); |
|
| 930 | - } |
|
| 931 | - } else { |
|
| 932 | - $found->attach($m); |
|
| 933 | - } |
|
| 934 | - } |
|
| 935 | - } |
|
| 936 | - } |
|
| 937 | - |
|
| 938 | - return $this->inst($found, null); |
|
| 939 | - } |
|
| 940 | - |
|
| 941 | - /** |
|
| 942 | - * Get the next sibling before each element in the DOMQuery. |
|
| 943 | - * |
|
| 944 | - * For each element in the DOMQuery, this retrieves the previous sibling |
|
| 945 | - * (if any). If a selector is supplied, it retrieves the first matching |
|
| 946 | - * sibling (if any is found). |
|
| 947 | - * |
|
| 948 | - * @param string $selector |
|
| 949 | - * A valid CSS 3 selector. |
|
| 950 | - * |
|
| 951 | - * @return DOMQuery |
|
| 952 | - * A DOMNode object, now containing any previous siblings that have been |
|
| 953 | - * found. |
|
| 954 | - * @throws Exception |
|
| 955 | - * @throws ParseException |
|
| 956 | - * @see prevAll() |
|
| 957 | - * @see next() |
|
| 958 | - * @see siblings() |
|
| 959 | - * @see children() |
|
| 960 | - */ |
|
| 961 | - public function prev($selector = null): Query |
|
| 962 | - { |
|
| 963 | - $found = new SplObjectStorage(); |
|
| 964 | - foreach ($this->matches as $m) { |
|
| 965 | - while (isset($m->previousSibling)) { |
|
| 966 | - $m = $m->previousSibling; |
|
| 967 | - if ($m->nodeType === XML_ELEMENT_NODE) { |
|
| 968 | - if (! empty($selector)) { |
|
| 969 | - if (QueryPath::with($m, null, $this->options)->is($selector)) { |
|
| 970 | - $found->attach($m); |
|
| 971 | - break; |
|
| 972 | - } |
|
| 973 | - } else { |
|
| 974 | - $found->attach($m); |
|
| 975 | - break; |
|
| 976 | - } |
|
| 977 | - } |
|
| 978 | - } |
|
| 979 | - } |
|
| 980 | - |
|
| 981 | - return $this->inst($found, null); |
|
| 982 | - } |
|
| 983 | - |
|
| 984 | - /** |
|
| 985 | - * Get the previous siblings for each element in the DOMQuery. |
|
| 986 | - * |
|
| 987 | - * For each element in the DOMQuery, get all previous siblings. If a |
|
| 988 | - * selector is provided, only matching siblings will be retrieved. |
|
| 989 | - * |
|
| 990 | - * @param string $selector |
|
| 991 | - * A valid CSS 3 selector. |
|
| 992 | - * |
|
| 993 | - * @return DOMQuery |
|
| 994 | - * The DOMQuery object, now wrapping previous sibling elements. |
|
| 995 | - * @throws ParseException |
|
| 996 | - * @throws Exception |
|
| 997 | - * @see siblings() |
|
| 998 | - * @see contents() |
|
| 999 | - * @see children() |
|
| 1000 | - * @see prev() |
|
| 1001 | - * @see nextAll() |
|
| 1002 | - */ |
|
| 1003 | - public function prevAll($selector = null): Query |
|
| 1004 | - { |
|
| 1005 | - $found = new SplObjectStorage(); |
|
| 1006 | - foreach ($this->matches as $m) { |
|
| 1007 | - while (isset($m->previousSibling)) { |
|
| 1008 | - $m = $m->previousSibling; |
|
| 1009 | - if ($m->nodeType === XML_ELEMENT_NODE) { |
|
| 1010 | - if (! empty($selector)) { |
|
| 1011 | - if (QueryPath::with($m, null, $this->options)->is($selector)) { |
|
| 1012 | - $found->attach($m); |
|
| 1013 | - } |
|
| 1014 | - } else { |
|
| 1015 | - $found->attach($m); |
|
| 1016 | - } |
|
| 1017 | - } |
|
| 1018 | - } |
|
| 1019 | - } |
|
| 1020 | - |
|
| 1021 | - return $this->inst($found, null); |
|
| 1022 | - } |
|
| 1023 | - |
|
| 1024 | - /** |
|
| 1025 | - * Get the children of the elements in the DOMQuery object. |
|
| 1026 | - * |
|
| 1027 | - * If a selector is provided, the list of children will be filtered through |
|
| 1028 | - * the selector. |
|
| 1029 | - * |
|
| 1030 | - * @param string $selector |
|
| 1031 | - * A valid selector. |
|
| 1032 | - * |
|
| 1033 | - * @return DOMQuery |
|
| 1034 | - * A DOMQuery wrapping all of the children. |
|
| 1035 | - * @throws ParseException |
|
| 1036 | - * @see parent() |
|
| 1037 | - * @see parents() |
|
| 1038 | - * @see next() |
|
| 1039 | - * @see prev() |
|
| 1040 | - * @see removeChildren() |
|
| 1041 | - */ |
|
| 1042 | - public function children($selector = null): Query |
|
| 1043 | - { |
|
| 1044 | - $found = new SplObjectStorage(); |
|
| 1045 | - $filter = is_string($selector) && strlen($selector) > 0; |
|
| 1046 | - |
|
| 1047 | - if ($filter) { |
|
| 1048 | - $tmp = new SplObjectStorage(); |
|
| 1049 | - } |
|
| 1050 | - foreach ($this->matches as $m) { |
|
| 1051 | - foreach ($m->childNodes as $c) { |
|
| 1052 | - if ($c->nodeType === XML_ELEMENT_NODE) { |
|
| 1053 | - // This is basically an optimized filter() just for children(). |
|
| 1054 | - if ($filter) { |
|
| 1055 | - $tmp->attach($c); |
|
| 1056 | - $query = new DOMTraverser($tmp, true, $c); |
|
| 1057 | - $query->find($selector); |
|
| 1058 | - if (count($query->matches()) > 0) { |
|
| 1059 | - $found->attach($c); |
|
| 1060 | - } |
|
| 1061 | - $tmp->detach($c); |
|
| 1062 | - } // No filter. Just attach it. |
|
| 1063 | - else { |
|
| 1064 | - $found->attach($c); |
|
| 1065 | - } |
|
| 1066 | - } |
|
| 1067 | - } |
|
| 1068 | - } |
|
| 1069 | - |
|
| 1070 | - return $this->inst($found, null); |
|
| 1071 | - } |
|
| 1072 | - |
|
| 1073 | - /** |
|
| 1074 | - * Get all child nodes (not just elements) of all items in the matched set. |
|
| 1075 | - * |
|
| 1076 | - * It gets only the immediate children, not all nodes in the subtree. |
|
| 1077 | - * |
|
| 1078 | - * This does not process iframes. Xinclude processing is dependent on the |
|
| 1079 | - * DOM implementation and configuration. |
|
| 1080 | - * |
|
| 1081 | - * @return DOMQuery |
|
| 1082 | - * A DOMNode object wrapping all child nodes for all elements in the |
|
| 1083 | - * DOMNode object. |
|
| 1084 | - * @throws ParseException |
|
| 1085 | - * @see text() |
|
| 1086 | - * @see html() |
|
| 1087 | - * @see innerHTML() |
|
| 1088 | - * @see xml() |
|
| 1089 | - * @see innerXML() |
|
| 1090 | - * @see find() |
|
| 1091 | - */ |
|
| 1092 | - public function contents(): Query |
|
| 1093 | - { |
|
| 1094 | - $found = new SplObjectStorage(); |
|
| 1095 | - foreach ($this->matches as $m) { |
|
| 1096 | - if (empty($m->childNodes)) { |
|
| 1097 | - continue; |
|
| 1098 | - } |
|
| 1099 | - foreach ($m->childNodes as $c) { |
|
| 1100 | - $found->attach($c); |
|
| 1101 | - } |
|
| 1102 | - } |
|
| 1103 | - |
|
| 1104 | - return $this->inst($found, null); |
|
| 1105 | - } |
|
| 1106 | - |
|
| 1107 | - /** |
|
| 1108 | - * Get a list of siblings for elements currently wrapped by this object. |
|
| 1109 | - * |
|
| 1110 | - * This will compile a list of every sibling of every element in the |
|
| 1111 | - * current list of elements. |
|
| 1112 | - * |
|
| 1113 | - * Note that if two siblings are present in the DOMQuery object to begin with, |
|
| 1114 | - * then both will be returned in the matched set, since they are siblings of each |
|
| 1115 | - * other. In other words,if the matches contain a and b, and a and b are siblings of |
|
| 1116 | - * each other, than running siblings will return a set that contains |
|
| 1117 | - * both a and b. |
|
| 1118 | - * |
|
| 1119 | - * @param string $selector |
|
| 1120 | - * If the optional selector is provided, siblings will be filtered through |
|
| 1121 | - * this expression. |
|
| 1122 | - * |
|
| 1123 | - * @return DOMQuery |
|
| 1124 | - * The DOMQuery containing the matched siblings. |
|
| 1125 | - * @throws ParseException |
|
| 1126 | - * @throws ParseException |
|
| 1127 | - * @see parent() |
|
| 1128 | - * @see parents() |
|
| 1129 | - * @see contents() |
|
| 1130 | - * @see children() |
|
| 1131 | - */ |
|
| 1132 | - public function siblings($selector = null): Query |
|
| 1133 | - { |
|
| 1134 | - $found = new SplObjectStorage(); |
|
| 1135 | - foreach ($this->matches as $m) { |
|
| 1136 | - $parent = $m->parentNode; |
|
| 1137 | - foreach ($parent->childNodes as $n) { |
|
| 1138 | - if ($n->nodeType === XML_ELEMENT_NODE && $n !== $m) { |
|
| 1139 | - $found->attach($n); |
|
| 1140 | - } |
|
| 1141 | - } |
|
| 1142 | - } |
|
| 1143 | - if (empty($selector)) { |
|
| 1144 | - return $this->inst($found, null); |
|
| 1145 | - } |
|
| 1146 | - |
|
| 1147 | - return $this->inst($found, null)->filter($selector); |
|
| 1148 | - } |
|
| 25 | + /** |
|
| 26 | + * Filter a list down to only elements that match the selector. |
|
| 27 | + * Use this, for example, to find all elements with a class, or with |
|
| 28 | + * certain children. |
|
| 29 | + * |
|
| 30 | + * @param string $selector |
|
| 31 | + * The selector to use as a filter. |
|
| 32 | + * |
|
| 33 | + * @return Query The DOMQuery with non-matching items filtered out.* The DOMQuery with non-matching items |
|
| 34 | + * filtered out. |
|
| 35 | + * @throws ParseException |
|
| 36 | + * @see filterCallback() |
|
| 37 | + * @see map() |
|
| 38 | + * @see find() |
|
| 39 | + * @see is() |
|
| 40 | + * @see filterLambda() |
|
| 41 | + */ |
|
| 42 | + public function filter($selector): Query |
|
| 43 | + { |
|
| 44 | + $found = new SplObjectStorage(); |
|
| 45 | + $tmp = new SplObjectStorage(); |
|
| 46 | + |
|
| 47 | + foreach ($this->matches as $m) { |
|
| 48 | + $tmp->attach($m); |
|
| 49 | + // Seems like this should be right... but it fails unit |
|
| 50 | + // tests. Need to compare to jQuery. |
|
| 51 | + // $query = new \QueryPath\CSS\DOMTraverser($tmp, TRUE, $m); |
|
| 52 | + $query = new DOMTraverser($tmp); |
|
| 53 | + $query->find($selector); |
|
| 54 | + if (count($query->matches())) { |
|
| 55 | + $found->attach($m); |
|
| 56 | + } |
|
| 57 | + $tmp->detach($m); |
|
| 58 | + } |
|
| 59 | + |
|
| 60 | + return $this->inst($found, null); |
|
| 61 | + } |
|
| 62 | + |
|
| 63 | + /** |
|
| 64 | + * Filter based on a lambda function. |
|
| 65 | + * |
|
| 66 | + * The function string will be executed as if it were the body of a |
|
| 67 | + * function. It is passed two arguments: |
|
| 68 | + * - $index: The index of the item. |
|
| 69 | + * - $item: The current Element. |
|
| 70 | + * If the function returns boolean FALSE, the item will be removed from |
|
| 71 | + * the list of elements. Otherwise it will be kept. |
|
| 72 | + * |
|
| 73 | + * Example: |
|
| 74 | + * |
|
| 75 | + * @code |
|
| 76 | + * qp('li')->filterLambda('qp($item)->attr("id") == "test"'); |
|
| 77 | + * @endcode |
|
| 78 | + * |
|
| 79 | + * The above would filter down the list to only an item whose ID is |
|
| 80 | + * 'text'. |
|
| 81 | + * |
|
| 82 | + * @param string $fn |
|
| 83 | + * Inline lambda function in a string. |
|
| 84 | + * |
|
| 85 | + * @return DOMQuery |
|
| 86 | + * @throws ParseException |
|
| 87 | + * |
|
| 88 | + * @see map() |
|
| 89 | + * @see mapLambda() |
|
| 90 | + * @see filterCallback() |
|
| 91 | + * @see filter() |
|
| 92 | + * @deprecated |
|
| 93 | + * Since PHP 5.3 supports anonymous functions -- REAL Lambdas -- this |
|
| 94 | + * method is not necessary and should be avoided. |
|
| 95 | + */ |
|
| 96 | + public function filterLambda($fn): Query |
|
| 97 | + { |
|
| 98 | + $function = create_function('$index, $item', $fn); |
|
| 99 | + $found = new SplObjectStorage(); |
|
| 100 | + $i = 0; |
|
| 101 | + foreach ($this->matches as $item) { |
|
| 102 | + if ($function($i++, $item) !== false) { |
|
| 103 | + $found->attach($item); |
|
| 104 | + } |
|
| 105 | + } |
|
| 106 | + |
|
| 107 | + return $this->inst($found, null); |
|
| 108 | + } |
|
| 109 | + |
|
| 110 | + /** |
|
| 111 | + * Use regular expressions to filter based on the text content of matched elements. |
|
| 112 | + * |
|
| 113 | + * Only items that match the given regular expression will be kept. All others will |
|
| 114 | + * be removed. |
|
| 115 | + * |
|
| 116 | + * The regular expression is run against the <i>text content</i> (the PCDATA) of the |
|
| 117 | + * elements. This is a way of filtering elements based on their content. |
|
| 118 | + * |
|
| 119 | + * Example: |
|
| 120 | + * |
|
| 121 | + * @code |
|
| 122 | + * <?xml version="1.0"?> |
|
| 123 | + * <div>Hello <i>World</i></div> |
|
| 124 | + * @endcode |
|
| 125 | + * |
|
| 126 | + * @code |
|
| 127 | + * <?php |
|
| 128 | + * // This will be 1. |
|
| 129 | + * qp($xml, 'div')->filterPreg('/World/')->matches->count(); |
|
| 130 | + * ?> |
|
| 131 | + * @endcode |
|
| 132 | + * |
|
| 133 | + * The return value above will be 1 because the text content of @codeqp($xml, 'div')@endcode is |
|
| 134 | + * @codeHello World@endcode. |
|
| 135 | + * |
|
| 136 | + * Compare this to the behavior of the <em>:contains()</em> CSS3 pseudo-class. |
|
| 137 | + * |
|
| 138 | + * @param string $regex |
|
| 139 | + * A regular expression. |
|
| 140 | + * |
|
| 141 | + * @return DOMQuery |
|
| 142 | + * @throws ParseException |
|
| 143 | + * @see filterCallback() |
|
| 144 | + * @see preg_match() |
|
| 145 | + * @see filter() |
|
| 146 | + */ |
|
| 147 | + public function filterPreg($regex): Query |
|
| 148 | + { |
|
| 149 | + $found = new SplObjectStorage(); |
|
| 150 | + |
|
| 151 | + foreach ($this->matches as $item) { |
|
| 152 | + if (preg_match($regex, $item->textContent) > 0) { |
|
| 153 | + $found->attach($item); |
|
| 154 | + } |
|
| 155 | + } |
|
| 156 | + |
|
| 157 | + return $this->inst($found, null); |
|
| 158 | + } |
|
| 159 | + |
|
| 160 | + /** |
|
| 161 | + * Filter based on a callback function. |
|
| 162 | + * |
|
| 163 | + * A callback may be any of the following: |
|
| 164 | + * - a function: 'my_func'. |
|
| 165 | + * - an object/method combo: $obj, 'myMethod' |
|
| 166 | + * - a class/method combo: 'MyClass', 'myMethod' |
|
| 167 | + * Note that classes are passed in strings. Objects are not. |
|
| 168 | + * |
|
| 169 | + * Each callback is passed to arguments: |
|
| 170 | + * - $index: The index position of the object in the array. |
|
| 171 | + * - $item: The item to be operated upon. |
|
| 172 | + * |
|
| 173 | + * If the callback function returns FALSE, the item will be removed from the |
|
| 174 | + * set of matches. Otherwise the item will be considered a match and left alone. |
|
| 175 | + * |
|
| 176 | + * @param callback $callback . |
|
| 177 | + * A callback either as a string (function) or an array (object, method OR |
|
| 178 | + * classname, method). |
|
| 179 | + * |
|
| 180 | + * @return DOMQuery |
|
| 181 | + * Query path object augmented according to the function. |
|
| 182 | + * @throws ParseException |
|
| 183 | + * @throws Exception |
|
| 184 | + * @see map() |
|
| 185 | + * @see is() |
|
| 186 | + * @see find() |
|
| 187 | + * @see filter() |
|
| 188 | + * @see filterLambda() |
|
| 189 | + */ |
|
| 190 | + public function filterCallback($callback): Query |
|
| 191 | + { |
|
| 192 | + $found = new SplObjectStorage(); |
|
| 193 | + $i = 0; |
|
| 194 | + if (is_callable($callback)) { |
|
| 195 | + foreach ($this->matches as $item) { |
|
| 196 | + if ($callback($i++, $item) !== false) { |
|
| 197 | + $found->attach($item); |
|
| 198 | + } |
|
| 199 | + } |
|
| 200 | + } else { |
|
| 201 | + throw new Exception('The specified callback is not callable.'); |
|
| 202 | + } |
|
| 203 | + |
|
| 204 | + return $this->inst($found, null); |
|
| 205 | + } |
|
| 206 | + |
|
| 207 | + /** |
|
| 208 | + * Run a function on each item in a set. |
|
| 209 | + * |
|
| 210 | + * The mapping callback can return anything. Whatever it returns will be |
|
| 211 | + * stored as a match in the set, though. This means that afer a map call, |
|
| 212 | + * there is no guarantee that the elements in the set will behave correctly |
|
| 213 | + * with other DOMQuery functions. |
|
| 214 | + * |
|
| 215 | + * Callback rules: |
|
| 216 | + * - If the callback returns NULL, the item will be removed from the array. |
|
| 217 | + * - If the callback returns an array, the entire array will be stored in |
|
| 218 | + * the results. |
|
| 219 | + * - If the callback returns anything else, it will be appended to the array |
|
| 220 | + * of matches. |
|
| 221 | + * |
|
| 222 | + * @param callback $callback |
|
| 223 | + * The function or callback to use. The callback will be passed two params: |
|
| 224 | + * - $index: The index position in the list of items wrapped by this object. |
|
| 225 | + * - $item: The current item. |
|
| 226 | + * |
|
| 227 | + * @return DOMQuery |
|
| 228 | + * The DOMQuery object wrapping a list of whatever values were returned |
|
| 229 | + * by each run of the callback. |
|
| 230 | + * |
|
| 231 | + * @throws Exception |
|
| 232 | + * @throws ParseException |
|
| 233 | + * @see find() |
|
| 234 | + * @see DOMQuery::get() |
|
| 235 | + * @see filter() |
|
| 236 | + */ |
|
| 237 | + public function map($callback): Query |
|
| 238 | + { |
|
| 239 | + $found = new SplObjectStorage(); |
|
| 240 | + |
|
| 241 | + if (is_callable($callback)) { |
|
| 242 | + $i = 0; |
|
| 243 | + foreach ($this->matches as $item) { |
|
| 244 | + $c = call_user_func($callback, $i, $item); |
|
| 245 | + if (isset($c)) { |
|
| 246 | + if (is_array($c) || $c instanceof Iterable) { |
|
| 247 | + foreach ($c as $retval) { |
|
| 248 | + if (! is_object($retval)) { |
|
| 249 | + $tmp = new stdClass(); |
|
| 250 | + $tmp->textContent = $retval; |
|
| 251 | + $retval = $tmp; |
|
| 252 | + } |
|
| 253 | + $found->attach($retval); |
|
| 254 | + } |
|
| 255 | + } else { |
|
| 256 | + if (! is_object($c)) { |
|
| 257 | + $tmp = new stdClass(); |
|
| 258 | + $tmp->textContent = $c; |
|
| 259 | + $c = $tmp; |
|
| 260 | + } |
|
| 261 | + $found->attach($c); |
|
| 262 | + } |
|
| 263 | + } |
|
| 264 | + ++$i; |
|
| 265 | + } |
|
| 266 | + } else { |
|
| 267 | + throw new Exception('Callback is not callable.'); |
|
| 268 | + } |
|
| 269 | + |
|
| 270 | + return $this->inst($found, null); |
|
| 271 | + } |
|
| 272 | + |
|
| 273 | + /** |
|
| 274 | + * Narrow the items in this object down to only a slice of the starting items. |
|
| 275 | + * |
|
| 276 | + * @param integer $start |
|
| 277 | + * Where in the list of matches to begin the slice. |
|
| 278 | + * @param integer $length |
|
| 279 | + * The number of items to include in the slice. If nothing is specified, the |
|
| 280 | + * all remaining matches (from $start onward) will be included in the sliced |
|
| 281 | + * list. |
|
| 282 | + * |
|
| 283 | + * @return DOMQuery |
|
| 284 | + * @throws ParseException |
|
| 285 | + * @see array_slice() |
|
| 286 | + */ |
|
| 287 | + public function slice($start, $length = 0): Query |
|
| 288 | + { |
|
| 289 | + $end = $length; |
|
| 290 | + $found = new SplObjectStorage(); |
|
| 291 | + if ($start >= $this->count()) { |
|
| 292 | + return $this->inst($found, null); |
|
| 293 | + } |
|
| 294 | + |
|
| 295 | + $i = $j = 0; |
|
| 296 | + foreach ($this->matches as $m) { |
|
| 297 | + if ($i >= $start) { |
|
| 298 | + if ($end > 0 && $j >= $end) { |
|
| 299 | + break; |
|
| 300 | + } |
|
| 301 | + $found->attach($m); |
|
| 302 | + ++$j; |
|
| 303 | + } |
|
| 304 | + ++$i; |
|
| 305 | + } |
|
| 306 | + |
|
| 307 | + return $this->inst($found, null); |
|
| 308 | + } |
|
| 309 | + |
|
| 310 | + /** |
|
| 311 | + * Run a callback on each item in the list of items. |
|
| 312 | + * |
|
| 313 | + * Rules of the callback: |
|
| 314 | + * - A callback is passed two variables: $index and $item. (There is no |
|
| 315 | + * special treatment of $this, as there is in jQuery.) |
|
| 316 | + * - You will want to pass $item by reference if it is not an |
|
| 317 | + * object (DOMNodes are all objects). |
|
| 318 | + * - A callback that returns FALSE will stop execution of the each() loop. This |
|
| 319 | + * works like break in a standard loop. |
|
| 320 | + * - A TRUE return value from the callback is analogous to a continue statement. |
|
| 321 | + * - All other return values are ignored. |
|
| 322 | + * |
|
| 323 | + * @param callback $callback |
|
| 324 | + * The callback to run. |
|
| 325 | + * |
|
| 326 | + * @return DOMQuery |
|
| 327 | + * The DOMQuery. |
|
| 328 | + * @throws Exception |
|
| 329 | + * @see filter() |
|
| 330 | + * @see map() |
|
| 331 | + * @see eachLambda() |
|
| 332 | + */ |
|
| 333 | + public function each($callback): Query |
|
| 334 | + { |
|
| 335 | + if (is_callable($callback)) { |
|
| 336 | + $i = 0; |
|
| 337 | + foreach ($this->matches as $item) { |
|
| 338 | + if (call_user_func($callback, $i, $item) === false) { |
|
| 339 | + return $this; |
|
| 340 | + } |
|
| 341 | + ++$i; |
|
| 342 | + } |
|
| 343 | + } else { |
|
| 344 | + throw new Exception('Callback is not callable.'); |
|
| 345 | + } |
|
| 346 | + |
|
| 347 | + return $this; |
|
| 348 | + } |
|
| 349 | + |
|
| 350 | + /** |
|
| 351 | + * An each() iterator that takes a lambda function. |
|
| 352 | + * |
|
| 353 | + * @param string $lambda |
|
| 354 | + * The lambda function. This will be passed ($index, &$item). |
|
| 355 | + * |
|
| 356 | + * @return DOMQuery |
|
| 357 | + * The DOMQuery object. |
|
| 358 | + * @deprecated |
|
| 359 | + * Since PHP 5.3 supports anonymous functions -- REAL Lambdas -- this |
|
| 360 | + * method is not necessary and should be avoided. |
|
| 361 | + * @see each() |
|
| 362 | + * @see filterLambda() |
|
| 363 | + * @see filterCallback() |
|
| 364 | + * @see map() |
|
| 365 | + */ |
|
| 366 | + public function eachLambda($lambda): Query |
|
| 367 | + { |
|
| 368 | + $index = 0; |
|
| 369 | + foreach ($this->matches as $item) { |
|
| 370 | + $fn = create_function('$index, &$item', $lambda); |
|
| 371 | + if ($fn($index, $item) === false) { |
|
| 372 | + return $this; |
|
| 373 | + } |
|
| 374 | + ++$index; |
|
| 375 | + } |
|
| 376 | + |
|
| 377 | + return $this; |
|
| 378 | + } |
|
| 379 | + |
|
| 380 | + /** |
|
| 381 | + * Get the even elements, so counter-intuitively 1, 3, 5, etc. |
|
| 382 | + * |
|
| 383 | + * @return DOMQuery |
|
| 384 | + * A DOMQuery wrapping all of the children. |
|
| 385 | + * @throws ParseException |
|
| 386 | + * @see parent() |
|
| 387 | + * @see parents() |
|
| 388 | + * @see next() |
|
| 389 | + * @see prev() |
|
| 390 | + * @since 2.1 |
|
| 391 | + * @author eabrand |
|
| 392 | + * @see removeChildren() |
|
| 393 | + */ |
|
| 394 | + public function even(): Query |
|
| 395 | + { |
|
| 396 | + $found = new SplObjectStorage(); |
|
| 397 | + $even = false; |
|
| 398 | + foreach ($this->matches as $m) { |
|
| 399 | + if ($even && $m->nodeType === XML_ELEMENT_NODE) { |
|
| 400 | + $found->attach($m); |
|
| 401 | + } |
|
| 402 | + $even = $even ? false : true; |
|
| 403 | + } |
|
| 404 | + |
|
| 405 | + return $this->inst($found, null); |
|
| 406 | + } |
|
| 407 | + |
|
| 408 | + /** |
|
| 409 | + * Get the odd elements, so counter-intuitively 0, 2, 4, etc. |
|
| 410 | + * |
|
| 411 | + * @return DOMQuery |
|
| 412 | + * A DOMQuery wrapping all of the children. |
|
| 413 | + * @throws ParseException |
|
| 414 | + * @see parent() |
|
| 415 | + * @see parents() |
|
| 416 | + * @see next() |
|
| 417 | + * @see prev() |
|
| 418 | + * @since 2.1 |
|
| 419 | + * @author eabrand |
|
| 420 | + * @see removeChildren() |
|
| 421 | + */ |
|
| 422 | + public function odd(): Query |
|
| 423 | + { |
|
| 424 | + $found = new SplObjectStorage(); |
|
| 425 | + $odd = true; |
|
| 426 | + foreach ($this->matches as $m) { |
|
| 427 | + if ($odd && $m->nodeType === XML_ELEMENT_NODE) { |
|
| 428 | + $found->attach($m); |
|
| 429 | + } |
|
| 430 | + $odd = $odd ? false : true; |
|
| 431 | + } |
|
| 432 | + |
|
| 433 | + return $this->inst($found, null); |
|
| 434 | + } |
|
| 435 | + |
|
| 436 | + /** |
|
| 437 | + * Get the first matching element. |
|
| 438 | + * |
|
| 439 | + * |
|
| 440 | + * @return DOMQuery |
|
| 441 | + * A DOMQuery wrapping all of the children. |
|
| 442 | + * @throws ParseException |
|
| 443 | + * @see prev() |
|
| 444 | + * @since 2.1 |
|
| 445 | + * @author eabrand |
|
| 446 | + * @see next() |
|
| 447 | + */ |
|
| 448 | + public function first(): Query |
|
| 449 | + { |
|
| 450 | + $found = new SplObjectStorage(); |
|
| 451 | + foreach ($this->matches as $m) { |
|
| 452 | + if ($m->nodeType === XML_ELEMENT_NODE) { |
|
| 453 | + $found->attach($m); |
|
| 454 | + break; |
|
| 455 | + } |
|
| 456 | + } |
|
| 457 | + |
|
| 458 | + return $this->inst($found, null); |
|
| 459 | + } |
|
| 460 | + |
|
| 461 | + /** |
|
| 462 | + * Get the first child of the matching element. |
|
| 463 | + * |
|
| 464 | + * |
|
| 465 | + * @return DOMQuery |
|
| 466 | + * A DOMQuery wrapping all of the children. |
|
| 467 | + * @throws ParseException |
|
| 468 | + * @see prev() |
|
| 469 | + * @since 2.1 |
|
| 470 | + * @author eabrand |
|
| 471 | + * @see next() |
|
| 472 | + */ |
|
| 473 | + public function firstChild(): Query |
|
| 474 | + { |
|
| 475 | + // Could possibly use $m->firstChild http://theserverpages.com/php/manual/en/ref.dom.php |
|
| 476 | + $found = new SplObjectStorage(); |
|
| 477 | + $flag = false; |
|
| 478 | + foreach ($this->matches as $m) { |
|
| 479 | + foreach ($m->childNodes as $c) { |
|
| 480 | + if ($c->nodeType === XML_ELEMENT_NODE) { |
|
| 481 | + $found->attach($c); |
|
| 482 | + $flag = true; |
|
| 483 | + break; |
|
| 484 | + } |
|
| 485 | + } |
|
| 486 | + if ($flag) { |
|
| 487 | + break; |
|
| 488 | + } |
|
| 489 | + } |
|
| 490 | + |
|
| 491 | + return $this->inst($found, null); |
|
| 492 | + } |
|
| 493 | + |
|
| 494 | + /** |
|
| 495 | + * Get the last matching element. |
|
| 496 | + * |
|
| 497 | + * |
|
| 498 | + * @return DOMQuery |
|
| 499 | + * A DOMQuery wrapping all of the children. |
|
| 500 | + * @throws ParseException |
|
| 501 | + * @see prev() |
|
| 502 | + * @since 2.1 |
|
| 503 | + * @author eabrand |
|
| 504 | + * @see next() |
|
| 505 | + */ |
|
| 506 | + public function last(): Query |
|
| 507 | + { |
|
| 508 | + $found = new SplObjectStorage(); |
|
| 509 | + $item = null; |
|
| 510 | + foreach ($this->matches as $m) { |
|
| 511 | + if ($m->nodeType === XML_ELEMENT_NODE) { |
|
| 512 | + $item = $m; |
|
| 513 | + } |
|
| 514 | + } |
|
| 515 | + if ($item) { |
|
| 516 | + $found->attach($item); |
|
| 517 | + } |
|
| 518 | + |
|
| 519 | + return $this->inst($found, null); |
|
| 520 | + } |
|
| 521 | + |
|
| 522 | + /** |
|
| 523 | + * Get the last child of the matching element. |
|
| 524 | + * |
|
| 525 | + * |
|
| 526 | + * @return DOMQuery |
|
| 527 | + * A DOMQuery wrapping all of the children. |
|
| 528 | + * @throws ParseException |
|
| 529 | + * @see prev() |
|
| 530 | + * @since 2.1 |
|
| 531 | + * @author eabrand |
|
| 532 | + * @see next() |
|
| 533 | + */ |
|
| 534 | + public function lastChild(): Query |
|
| 535 | + { |
|
| 536 | + $found = new SplObjectStorage(); |
|
| 537 | + $item = null; |
|
| 538 | + foreach ($this->matches as $m) { |
|
| 539 | + foreach ($m->childNodes as $c) { |
|
| 540 | + if ($c->nodeType === XML_ELEMENT_NODE) { |
|
| 541 | + $item = $c; |
|
| 542 | + } |
|
| 543 | + } |
|
| 544 | + if ($item) { |
|
| 545 | + $found->attach($item); |
|
| 546 | + $item = null; |
|
| 547 | + } |
|
| 548 | + } |
|
| 549 | + |
|
| 550 | + return $this->inst($found, null); |
|
| 551 | + } |
|
| 552 | + |
|
| 553 | + /** |
|
| 554 | + * Get all siblings after an element until the selector is reached. |
|
| 555 | + * |
|
| 556 | + * For each element in the DOMQuery, get all siblings that appear after |
|
| 557 | + * it. If a selector is passed in, then only siblings that match the |
|
| 558 | + * selector will be included. |
|
| 559 | + * |
|
| 560 | + * @param string $selector |
|
| 561 | + * A valid CSS 3 selector. |
|
| 562 | + * |
|
| 563 | + * @return DOMQuery |
|
| 564 | + * The DOMQuery object, now containing the matching siblings. |
|
| 565 | + * @throws Exception |
|
| 566 | + * @see prevAll() |
|
| 567 | + * @see children() |
|
| 568 | + * @see siblings() |
|
| 569 | + * @since 2.1 |
|
| 570 | + * @author eabrand |
|
| 571 | + * @see next() |
|
| 572 | + */ |
|
| 573 | + public function nextUntil($selector = null): Query |
|
| 574 | + { |
|
| 575 | + $found = new SplObjectStorage(); |
|
| 576 | + foreach ($this->matches as $m) { |
|
| 577 | + while (isset($m->nextSibling)) { |
|
| 578 | + $m = $m->nextSibling; |
|
| 579 | + if ($m->nodeType === XML_ELEMENT_NODE) { |
|
| 580 | + if (null !== $selector && QueryPath::with($m, null, $this->options)->is($selector) > 0) { |
|
| 581 | + break; |
|
| 582 | + } |
|
| 583 | + $found->attach($m); |
|
| 584 | + } |
|
| 585 | + } |
|
| 586 | + } |
|
| 587 | + |
|
| 588 | + return $this->inst($found, null); |
|
| 589 | + } |
|
| 590 | + |
|
| 591 | + /** |
|
| 592 | + * Get the previous siblings for each element in the DOMQuery |
|
| 593 | + * until the selector is reached. |
|
| 594 | + * |
|
| 595 | + * For each element in the DOMQuery, get all previous siblings. If a |
|
| 596 | + * selector is provided, only matching siblings will be retrieved. |
|
| 597 | + * |
|
| 598 | + * @param string $selector |
|
| 599 | + * A valid CSS 3 selector. |
|
| 600 | + * |
|
| 601 | + * @return DOMQuery |
|
| 602 | + * The DOMQuery object, now wrapping previous sibling elements. |
|
| 603 | + * @throws Exception |
|
| 604 | + * @see prev() |
|
| 605 | + * @see nextAll() |
|
| 606 | + * @see siblings() |
|
| 607 | + * @see contents() |
|
| 608 | + * @see children() |
|
| 609 | + * @since 2.1 |
|
| 610 | + * @author eabrand |
|
| 611 | + */ |
|
| 612 | + public function prevUntil($selector = null): Query |
|
| 613 | + { |
|
| 614 | + $found = new SplObjectStorage(); |
|
| 615 | + foreach ($this->matches as $m) { |
|
| 616 | + while (isset($m->previousSibling)) { |
|
| 617 | + $m = $m->previousSibling; |
|
| 618 | + if ($m->nodeType === XML_ELEMENT_NODE) { |
|
| 619 | + if (null !== $selector && QueryPath::with($m, null, $this->options)->is($selector)) { |
|
| 620 | + break; |
|
| 621 | + } |
|
| 622 | + |
|
| 623 | + $found->attach($m); |
|
| 624 | + } |
|
| 625 | + } |
|
| 626 | + } |
|
| 627 | + |
|
| 628 | + return $this->inst($found, null); |
|
| 629 | + } |
|
| 630 | + |
|
| 631 | + /** |
|
| 632 | + * Get all ancestors of each element in the DOMQuery until the selector is reached. |
|
| 633 | + * |
|
| 634 | + * If a selector is present, only matching ancestors will be retrieved. |
|
| 635 | + * |
|
| 636 | + * @param string $selector |
|
| 637 | + * A valid CSS 3 Selector. |
|
| 638 | + * |
|
| 639 | + * @return DOMQuery |
|
| 640 | + * A DOMNode object containing the matching ancestors. |
|
| 641 | + * @throws Exception |
|
| 642 | + * @see siblings() |
|
| 643 | + * @see children() |
|
| 644 | + * @since 2.1 |
|
| 645 | + * @author eabrand |
|
| 646 | + * @see parent() |
|
| 647 | + */ |
|
| 648 | + public function parentsUntil($selector = null): Query |
|
| 649 | + { |
|
| 650 | + $found = new SplObjectStorage(); |
|
| 651 | + foreach ($this->matches as $m) { |
|
| 652 | + while ($m->parentNode->nodeType !== XML_DOCUMENT_NODE) { |
|
| 653 | + $m = $m->parentNode; |
|
| 654 | + // Is there any case where parent node is not an element? |
|
| 655 | + if ($m->nodeType === XML_ELEMENT_NODE) { |
|
| 656 | + if (! empty($selector)) { |
|
| 657 | + if (QueryPath::with($m, null, $this->options)->is($selector) > 0) { |
|
| 658 | + break; |
|
| 659 | + } |
|
| 660 | + $found->attach($m); |
|
| 661 | + } else { |
|
| 662 | + $found->attach($m); |
|
| 663 | + } |
|
| 664 | + } |
|
| 665 | + } |
|
| 666 | + } |
|
| 667 | + |
|
| 668 | + return $this->inst($found, null); |
|
| 669 | + } |
|
| 670 | + |
|
| 671 | + /** |
|
| 672 | + * Reduce the matched set to just one. |
|
| 673 | + * |
|
| 674 | + * This will take a matched set and reduce it to just one item -- the item |
|
| 675 | + * at the index specified. This is a destructive operation, and can be undone |
|
| 676 | + * with {@link end()}. |
|
| 677 | + * |
|
| 678 | + * @param $index |
|
| 679 | + * The index of the element to keep. The rest will be |
|
| 680 | + * discarded. |
|
| 681 | + * |
|
| 682 | + * @return DOMQuery |
|
| 683 | + * @throws ParseException |
|
| 684 | + * @see is() |
|
| 685 | + * @see end() |
|
| 686 | + * @see get() |
|
| 687 | + */ |
|
| 688 | + public function eq($index): Query |
|
| 689 | + { |
|
| 690 | + return $this->inst($this->getNthMatch($index), null); |
|
| 691 | + } |
|
| 692 | + |
|
| 693 | + /** |
|
| 694 | + * Filter a list to contain only items that do NOT match. |
|
| 695 | + * |
|
| 696 | + * @param string $selector |
|
| 697 | + * A selector to use as a negation filter. If the filter is matched, the |
|
| 698 | + * element will be removed from the list. |
|
| 699 | + * |
|
| 700 | + * @return DOMQuery |
|
| 701 | + * The DOMQuery object with matching items filtered out. |
|
| 702 | + * @throws ParseException |
|
| 703 | + * @throws Exception |
|
| 704 | + * @see find() |
|
| 705 | + */ |
|
| 706 | + public function not($selector): Query |
|
| 707 | + { |
|
| 708 | + $found = new SplObjectStorage(); |
|
| 709 | + if ($selector instanceof DOMElement) { |
|
| 710 | + foreach ($this->matches as $m) { |
|
| 711 | + if ($m !== $selector) { |
|
| 712 | + $found->attach($m); |
|
| 713 | + } |
|
| 714 | + } |
|
| 715 | + } elseif (is_array($selector)) { |
|
| 716 | + foreach ($this->matches as $m) { |
|
| 717 | + if (! in_array($m, $selector, true)) { |
|
| 718 | + $found->attach($m); |
|
| 719 | + } |
|
| 720 | + } |
|
| 721 | + } elseif ($selector instanceof SplObjectStorage) { |
|
| 722 | + foreach ($this->matches as $m) { |
|
| 723 | + if ($selector->contains($m)) { |
|
| 724 | + $found->attach($m); |
|
| 725 | + } |
|
| 726 | + } |
|
| 727 | + } else { |
|
| 728 | + foreach ($this->matches as $m) { |
|
| 729 | + if (! QueryPath::with($m, null, $this->options)->is($selector)) { |
|
| 730 | + $found->attach($m); |
|
| 731 | + } |
|
| 732 | + } |
|
| 733 | + } |
|
| 734 | + |
|
| 735 | + return $this->inst($found, null); |
|
| 736 | + } |
|
| 737 | + |
|
| 738 | + /** |
|
| 739 | + * Find the closest element matching the selector. |
|
| 740 | + * |
|
| 741 | + * This finds the closest match in the ancestry chain. It first checks the |
|
| 742 | + * present element. If the present element does not match, this traverses up |
|
| 743 | + * the ancestry chain (e.g. checks each parent) looking for an item that matches. |
|
| 744 | + * |
|
| 745 | + * It is provided for jQuery 1.3 compatibility. |
|
| 746 | + * |
|
| 747 | + * @param string $selector |
|
| 748 | + * A CSS Selector to match. |
|
| 749 | + * |
|
| 750 | + * @return DOMQuery |
|
| 751 | + * The set of matches. |
|
| 752 | + * @throws Exception |
|
| 753 | + * @since 2.0 |
|
| 754 | + */ |
|
| 755 | + public function closest($selector): Query |
|
| 756 | + { |
|
| 757 | + $found = new SplObjectStorage(); |
|
| 758 | + foreach ($this->matches as $m) { |
|
| 759 | + if (QueryPath::with($m, null, $this->options)->is($selector) > 0) { |
|
| 760 | + $found->attach($m); |
|
| 761 | + } else { |
|
| 762 | + while ($m->parentNode->nodeType !== XML_DOCUMENT_NODE) { |
|
| 763 | + $m = $m->parentNode; |
|
| 764 | + // Is there any case where parent node is not an element? |
|
| 765 | + if ($m->nodeType === XML_ELEMENT_NODE && QueryPath::with( |
|
| 766 | + $m, |
|
| 767 | + null, |
|
| 768 | + $this->options |
|
| 769 | + )->is($selector) > 0) { |
|
| 770 | + $found->attach($m); |
|
| 771 | + break; |
|
| 772 | + } |
|
| 773 | + } |
|
| 774 | + } |
|
| 775 | + } |
|
| 776 | + |
|
| 777 | + // XXX: Should this be an in-place modification? |
|
| 778 | + return $this->inst($found, null); |
|
| 779 | + } |
|
| 780 | + |
|
| 781 | + /** |
|
| 782 | + * Get the immediate parent of each element in the DOMQuery. |
|
| 783 | + * |
|
| 784 | + * If a selector is passed, this will return the nearest matching parent for |
|
| 785 | + * each element in the DOMQuery. |
|
| 786 | + * |
|
| 787 | + * @param string $selector |
|
| 788 | + * A valid CSS3 selector. |
|
| 789 | + * |
|
| 790 | + * @return DOMQuery |
|
| 791 | + * A DOMNode object wrapping the matching parents. |
|
| 792 | + * @throws Exception |
|
| 793 | + * @see siblings() |
|
| 794 | + * @see parents() |
|
| 795 | + * @see children() |
|
| 796 | + */ |
|
| 797 | + public function parent($selector = null): Query |
|
| 798 | + { |
|
| 799 | + $found = new SplObjectStorage(); |
|
| 800 | + foreach ($this->matches as $m) { |
|
| 801 | + while ($m->parentNode->nodeType !== XML_DOCUMENT_NODE) { |
|
| 802 | + $m = $m->parentNode; |
|
| 803 | + // Is there any case where parent node is not an element? |
|
| 804 | + if ($m->nodeType === XML_ELEMENT_NODE) { |
|
| 805 | + if (! empty($selector)) { |
|
| 806 | + if (QueryPath::with($m, null, $this->options)->is($selector) > 0) { |
|
| 807 | + $found->attach($m); |
|
| 808 | + break; |
|
| 809 | + } |
|
| 810 | + } else { |
|
| 811 | + $found->attach($m); |
|
| 812 | + break; |
|
| 813 | + } |
|
| 814 | + } |
|
| 815 | + } |
|
| 816 | + } |
|
| 817 | + |
|
| 818 | + return $this->inst($found, null); |
|
| 819 | + } |
|
| 820 | + |
|
| 821 | + /** |
|
| 822 | + * Get all ancestors of each element in the DOMQuery. |
|
| 823 | + * |
|
| 824 | + * If a selector is present, only matching ancestors will be retrieved. |
|
| 825 | + * |
|
| 826 | + * @param string $selector |
|
| 827 | + * A valid CSS 3 Selector. |
|
| 828 | + * |
|
| 829 | + * @return DOMQuery |
|
| 830 | + * A DOMNode object containing the matching ancestors. |
|
| 831 | + * @throws ParseException |
|
| 832 | + * @throws Exception |
|
| 833 | + * @see children() |
|
| 834 | + * @see parent() |
|
| 835 | + * @see siblings() |
|
| 836 | + */ |
|
| 837 | + public function parents($selector = null): Query |
|
| 838 | + { |
|
| 839 | + $found = new SplObjectStorage(); |
|
| 840 | + foreach ($this->matches as $m) { |
|
| 841 | + while ($m->parentNode->nodeType !== XML_DOCUMENT_NODE) { |
|
| 842 | + $m = $m->parentNode; |
|
| 843 | + // Is there any case where parent node is not an element? |
|
| 844 | + if ($m->nodeType === XML_ELEMENT_NODE) { |
|
| 845 | + if (! empty($selector)) { |
|
| 846 | + if (QueryPath::with($m, null, $this->options)->is($selector) > 0) { |
|
| 847 | + $found->attach($m); |
|
| 848 | + } |
|
| 849 | + } else { |
|
| 850 | + $found->attach($m); |
|
| 851 | + } |
|
| 852 | + } |
|
| 853 | + } |
|
| 854 | + } |
|
| 855 | + |
|
| 856 | + return $this->inst($found, null); |
|
| 857 | + } |
|
| 858 | + |
|
| 859 | + /** |
|
| 860 | + * Get the next sibling of each element in the DOMQuery. |
|
| 861 | + * |
|
| 862 | + * If a selector is provided, the next matching sibling will be returned. |
|
| 863 | + * |
|
| 864 | + * @param string $selector |
|
| 865 | + * A CSS3 selector. |
|
| 866 | + * |
|
| 867 | + * @return DOMQuery |
|
| 868 | + * The DOMQuery object. |
|
| 869 | + * @throws Exception |
|
| 870 | + * @throws ParseException |
|
| 871 | + * @see nextAll() |
|
| 872 | + * @see prev() |
|
| 873 | + * @see children() |
|
| 874 | + * @see contents() |
|
| 875 | + * @see parent() |
|
| 876 | + * @see parents() |
|
| 877 | + */ |
|
| 878 | + public function next($selector = null): Query |
|
| 879 | + { |
|
| 880 | + $found = new SplObjectStorage(); |
|
| 881 | + foreach ($this->matches as $m) { |
|
| 882 | + while (isset($m->nextSibling)) { |
|
| 883 | + $m = $m->nextSibling; |
|
| 884 | + if ($m->nodeType === XML_ELEMENT_NODE) { |
|
| 885 | + if (! empty($selector)) { |
|
| 886 | + if (QueryPath::with($m, null, $this->options)->is($selector) > 0) { |
|
| 887 | + $found->attach($m); |
|
| 888 | + break; |
|
| 889 | + } |
|
| 890 | + } else { |
|
| 891 | + $found->attach($m); |
|
| 892 | + break; |
|
| 893 | + } |
|
| 894 | + } |
|
| 895 | + } |
|
| 896 | + } |
|
| 897 | + |
|
| 898 | + return $this->inst($found, null); |
|
| 899 | + } |
|
| 900 | + |
|
| 901 | + /** |
|
| 902 | + * Get all siblings after an element. |
|
| 903 | + * |
|
| 904 | + * For each element in the DOMQuery, get all siblings that appear after |
|
| 905 | + * it. If a selector is passed in, then only siblings that match the |
|
| 906 | + * selector will be included. |
|
| 907 | + * |
|
| 908 | + * @param string $selector |
|
| 909 | + * A valid CSS 3 selector. |
|
| 910 | + * |
|
| 911 | + * @return DOMQuery |
|
| 912 | + * The DOMQuery object, now containing the matching siblings. |
|
| 913 | + * @throws Exception |
|
| 914 | + * @throws ParseException |
|
| 915 | + * @see next() |
|
| 916 | + * @see prevAll() |
|
| 917 | + * @see children() |
|
| 918 | + * @see siblings() |
|
| 919 | + */ |
|
| 920 | + public function nextAll($selector = null): Query |
|
| 921 | + { |
|
| 922 | + $found = new SplObjectStorage(); |
|
| 923 | + foreach ($this->matches as $m) { |
|
| 924 | + while (isset($m->nextSibling)) { |
|
| 925 | + $m = $m->nextSibling; |
|
| 926 | + if ($m->nodeType === XML_ELEMENT_NODE) { |
|
| 927 | + if (! empty($selector)) { |
|
| 928 | + if (QueryPath::with($m, null, $this->options)->is($selector) > 0) { |
|
| 929 | + $found->attach($m); |
|
| 930 | + } |
|
| 931 | + } else { |
|
| 932 | + $found->attach($m); |
|
| 933 | + } |
|
| 934 | + } |
|
| 935 | + } |
|
| 936 | + } |
|
| 937 | + |
|
| 938 | + return $this->inst($found, null); |
|
| 939 | + } |
|
| 940 | + |
|
| 941 | + /** |
|
| 942 | + * Get the next sibling before each element in the DOMQuery. |
|
| 943 | + * |
|
| 944 | + * For each element in the DOMQuery, this retrieves the previous sibling |
|
| 945 | + * (if any). If a selector is supplied, it retrieves the first matching |
|
| 946 | + * sibling (if any is found). |
|
| 947 | + * |
|
| 948 | + * @param string $selector |
|
| 949 | + * A valid CSS 3 selector. |
|
| 950 | + * |
|
| 951 | + * @return DOMQuery |
|
| 952 | + * A DOMNode object, now containing any previous siblings that have been |
|
| 953 | + * found. |
|
| 954 | + * @throws Exception |
|
| 955 | + * @throws ParseException |
|
| 956 | + * @see prevAll() |
|
| 957 | + * @see next() |
|
| 958 | + * @see siblings() |
|
| 959 | + * @see children() |
|
| 960 | + */ |
|
| 961 | + public function prev($selector = null): Query |
|
| 962 | + { |
|
| 963 | + $found = new SplObjectStorage(); |
|
| 964 | + foreach ($this->matches as $m) { |
|
| 965 | + while (isset($m->previousSibling)) { |
|
| 966 | + $m = $m->previousSibling; |
|
| 967 | + if ($m->nodeType === XML_ELEMENT_NODE) { |
|
| 968 | + if (! empty($selector)) { |
|
| 969 | + if (QueryPath::with($m, null, $this->options)->is($selector)) { |
|
| 970 | + $found->attach($m); |
|
| 971 | + break; |
|
| 972 | + } |
|
| 973 | + } else { |
|
| 974 | + $found->attach($m); |
|
| 975 | + break; |
|
| 976 | + } |
|
| 977 | + } |
|
| 978 | + } |
|
| 979 | + } |
|
| 980 | + |
|
| 981 | + return $this->inst($found, null); |
|
| 982 | + } |
|
| 983 | + |
|
| 984 | + /** |
|
| 985 | + * Get the previous siblings for each element in the DOMQuery. |
|
| 986 | + * |
|
| 987 | + * For each element in the DOMQuery, get all previous siblings. If a |
|
| 988 | + * selector is provided, only matching siblings will be retrieved. |
|
| 989 | + * |
|
| 990 | + * @param string $selector |
|
| 991 | + * A valid CSS 3 selector. |
|
| 992 | + * |
|
| 993 | + * @return DOMQuery |
|
| 994 | + * The DOMQuery object, now wrapping previous sibling elements. |
|
| 995 | + * @throws ParseException |
|
| 996 | + * @throws Exception |
|
| 997 | + * @see siblings() |
|
| 998 | + * @see contents() |
|
| 999 | + * @see children() |
|
| 1000 | + * @see prev() |
|
| 1001 | + * @see nextAll() |
|
| 1002 | + */ |
|
| 1003 | + public function prevAll($selector = null): Query |
|
| 1004 | + { |
|
| 1005 | + $found = new SplObjectStorage(); |
|
| 1006 | + foreach ($this->matches as $m) { |
|
| 1007 | + while (isset($m->previousSibling)) { |
|
| 1008 | + $m = $m->previousSibling; |
|
| 1009 | + if ($m->nodeType === XML_ELEMENT_NODE) { |
|
| 1010 | + if (! empty($selector)) { |
|
| 1011 | + if (QueryPath::with($m, null, $this->options)->is($selector)) { |
|
| 1012 | + $found->attach($m); |
|
| 1013 | + } |
|
| 1014 | + } else { |
|
| 1015 | + $found->attach($m); |
|
| 1016 | + } |
|
| 1017 | + } |
|
| 1018 | + } |
|
| 1019 | + } |
|
| 1020 | + |
|
| 1021 | + return $this->inst($found, null); |
|
| 1022 | + } |
|
| 1023 | + |
|
| 1024 | + /** |
|
| 1025 | + * Get the children of the elements in the DOMQuery object. |
|
| 1026 | + * |
|
| 1027 | + * If a selector is provided, the list of children will be filtered through |
|
| 1028 | + * the selector. |
|
| 1029 | + * |
|
| 1030 | + * @param string $selector |
|
| 1031 | + * A valid selector. |
|
| 1032 | + * |
|
| 1033 | + * @return DOMQuery |
|
| 1034 | + * A DOMQuery wrapping all of the children. |
|
| 1035 | + * @throws ParseException |
|
| 1036 | + * @see parent() |
|
| 1037 | + * @see parents() |
|
| 1038 | + * @see next() |
|
| 1039 | + * @see prev() |
|
| 1040 | + * @see removeChildren() |
|
| 1041 | + */ |
|
| 1042 | + public function children($selector = null): Query |
|
| 1043 | + { |
|
| 1044 | + $found = new SplObjectStorage(); |
|
| 1045 | + $filter = is_string($selector) && strlen($selector) > 0; |
|
| 1046 | + |
|
| 1047 | + if ($filter) { |
|
| 1048 | + $tmp = new SplObjectStorage(); |
|
| 1049 | + } |
|
| 1050 | + foreach ($this->matches as $m) { |
|
| 1051 | + foreach ($m->childNodes as $c) { |
|
| 1052 | + if ($c->nodeType === XML_ELEMENT_NODE) { |
|
| 1053 | + // This is basically an optimized filter() just for children(). |
|
| 1054 | + if ($filter) { |
|
| 1055 | + $tmp->attach($c); |
|
| 1056 | + $query = new DOMTraverser($tmp, true, $c); |
|
| 1057 | + $query->find($selector); |
|
| 1058 | + if (count($query->matches()) > 0) { |
|
| 1059 | + $found->attach($c); |
|
| 1060 | + } |
|
| 1061 | + $tmp->detach($c); |
|
| 1062 | + } // No filter. Just attach it. |
|
| 1063 | + else { |
|
| 1064 | + $found->attach($c); |
|
| 1065 | + } |
|
| 1066 | + } |
|
| 1067 | + } |
|
| 1068 | + } |
|
| 1069 | + |
|
| 1070 | + return $this->inst($found, null); |
|
| 1071 | + } |
|
| 1072 | + |
|
| 1073 | + /** |
|
| 1074 | + * Get all child nodes (not just elements) of all items in the matched set. |
|
| 1075 | + * |
|
| 1076 | + * It gets only the immediate children, not all nodes in the subtree. |
|
| 1077 | + * |
|
| 1078 | + * This does not process iframes. Xinclude processing is dependent on the |
|
| 1079 | + * DOM implementation and configuration. |
|
| 1080 | + * |
|
| 1081 | + * @return DOMQuery |
|
| 1082 | + * A DOMNode object wrapping all child nodes for all elements in the |
|
| 1083 | + * DOMNode object. |
|
| 1084 | + * @throws ParseException |
|
| 1085 | + * @see text() |
|
| 1086 | + * @see html() |
|
| 1087 | + * @see innerHTML() |
|
| 1088 | + * @see xml() |
|
| 1089 | + * @see innerXML() |
|
| 1090 | + * @see find() |
|
| 1091 | + */ |
|
| 1092 | + public function contents(): Query |
|
| 1093 | + { |
|
| 1094 | + $found = new SplObjectStorage(); |
|
| 1095 | + foreach ($this->matches as $m) { |
|
| 1096 | + if (empty($m->childNodes)) { |
|
| 1097 | + continue; |
|
| 1098 | + } |
|
| 1099 | + foreach ($m->childNodes as $c) { |
|
| 1100 | + $found->attach($c); |
|
| 1101 | + } |
|
| 1102 | + } |
|
| 1103 | + |
|
| 1104 | + return $this->inst($found, null); |
|
| 1105 | + } |
|
| 1106 | + |
|
| 1107 | + /** |
|
| 1108 | + * Get a list of siblings for elements currently wrapped by this object. |
|
| 1109 | + * |
|
| 1110 | + * This will compile a list of every sibling of every element in the |
|
| 1111 | + * current list of elements. |
|
| 1112 | + * |
|
| 1113 | + * Note that if two siblings are present in the DOMQuery object to begin with, |
|
| 1114 | + * then both will be returned in the matched set, since they are siblings of each |
|
| 1115 | + * other. In other words,if the matches contain a and b, and a and b are siblings of |
|
| 1116 | + * each other, than running siblings will return a set that contains |
|
| 1117 | + * both a and b. |
|
| 1118 | + * |
|
| 1119 | + * @param string $selector |
|
| 1120 | + * If the optional selector is provided, siblings will be filtered through |
|
| 1121 | + * this expression. |
|
| 1122 | + * |
|
| 1123 | + * @return DOMQuery |
|
| 1124 | + * The DOMQuery containing the matched siblings. |
|
| 1125 | + * @throws ParseException |
|
| 1126 | + * @throws ParseException |
|
| 1127 | + * @see parent() |
|
| 1128 | + * @see parents() |
|
| 1129 | + * @see contents() |
|
| 1130 | + * @see children() |
|
| 1131 | + */ |
|
| 1132 | + public function siblings($selector = null): Query |
|
| 1133 | + { |
|
| 1134 | + $found = new SplObjectStorage(); |
|
| 1135 | + foreach ($this->matches as $m) { |
|
| 1136 | + $parent = $m->parentNode; |
|
| 1137 | + foreach ($parent->childNodes as $n) { |
|
| 1138 | + if ($n->nodeType === XML_ELEMENT_NODE && $n !== $m) { |
|
| 1139 | + $found->attach($n); |
|
| 1140 | + } |
|
| 1141 | + } |
|
| 1142 | + } |
|
| 1143 | + if (empty($selector)) { |
|
| 1144 | + return $this->inst($found, null); |
|
| 1145 | + } |
|
| 1146 | + |
|
| 1147 | + return $this->inst($found, null)->filter($selector); |
|
| 1148 | + } |
|
| 1149 | 1149 | } |
@@ -245,7 +245,7 @@ discard block |
||
| 245 | 245 | if (isset($c)) { |
| 246 | 246 | if (is_array($c) || $c instanceof Iterable) { |
| 247 | 247 | foreach ($c as $retval) { |
| 248 | - if (! is_object($retval)) { |
|
| 248 | + if (!is_object($retval)) { |
|
| 249 | 249 | $tmp = new stdClass(); |
| 250 | 250 | $tmp->textContent = $retval; |
| 251 | 251 | $retval = $tmp; |
@@ -253,7 +253,7 @@ discard block |
||
| 253 | 253 | $found->attach($retval); |
| 254 | 254 | } |
| 255 | 255 | } else { |
| 256 | - if (! is_object($c)) { |
|
| 256 | + if (!is_object($c)) { |
|
| 257 | 257 | $tmp = new stdClass(); |
| 258 | 258 | $tmp->textContent = $c; |
| 259 | 259 | $c = $tmp; |
@@ -653,7 +653,7 @@ discard block |
||
| 653 | 653 | $m = $m->parentNode; |
| 654 | 654 | // Is there any case where parent node is not an element? |
| 655 | 655 | if ($m->nodeType === XML_ELEMENT_NODE) { |
| 656 | - if (! empty($selector)) { |
|
| 656 | + if (!empty($selector)) { |
|
| 657 | 657 | if (QueryPath::with($m, null, $this->options)->is($selector) > 0) { |
| 658 | 658 | break; |
| 659 | 659 | } |
@@ -714,7 +714,7 @@ discard block |
||
| 714 | 714 | } |
| 715 | 715 | } elseif (is_array($selector)) { |
| 716 | 716 | foreach ($this->matches as $m) { |
| 717 | - if (! in_array($m, $selector, true)) { |
|
| 717 | + if (!in_array($m, $selector, true)) { |
|
| 718 | 718 | $found->attach($m); |
| 719 | 719 | } |
| 720 | 720 | } |
@@ -726,7 +726,7 @@ discard block |
||
| 726 | 726 | } |
| 727 | 727 | } else { |
| 728 | 728 | foreach ($this->matches as $m) { |
| 729 | - if (! QueryPath::with($m, null, $this->options)->is($selector)) { |
|
| 729 | + if (!QueryPath::with($m, null, $this->options)->is($selector)) { |
|
| 730 | 730 | $found->attach($m); |
| 731 | 731 | } |
| 732 | 732 | } |
@@ -802,7 +802,7 @@ discard block |
||
| 802 | 802 | $m = $m->parentNode; |
| 803 | 803 | // Is there any case where parent node is not an element? |
| 804 | 804 | if ($m->nodeType === XML_ELEMENT_NODE) { |
| 805 | - if (! empty($selector)) { |
|
| 805 | + if (!empty($selector)) { |
|
| 806 | 806 | if (QueryPath::with($m, null, $this->options)->is($selector) > 0) { |
| 807 | 807 | $found->attach($m); |
| 808 | 808 | break; |
@@ -842,7 +842,7 @@ discard block |
||
| 842 | 842 | $m = $m->parentNode; |
| 843 | 843 | // Is there any case where parent node is not an element? |
| 844 | 844 | if ($m->nodeType === XML_ELEMENT_NODE) { |
| 845 | - if (! empty($selector)) { |
|
| 845 | + if (!empty($selector)) { |
|
| 846 | 846 | if (QueryPath::with($m, null, $this->options)->is($selector) > 0) { |
| 847 | 847 | $found->attach($m); |
| 848 | 848 | } |
@@ -882,7 +882,7 @@ discard block |
||
| 882 | 882 | while (isset($m->nextSibling)) { |
| 883 | 883 | $m = $m->nextSibling; |
| 884 | 884 | if ($m->nodeType === XML_ELEMENT_NODE) { |
| 885 | - if (! empty($selector)) { |
|
| 885 | + if (!empty($selector)) { |
|
| 886 | 886 | if (QueryPath::with($m, null, $this->options)->is($selector) > 0) { |
| 887 | 887 | $found->attach($m); |
| 888 | 888 | break; |
@@ -924,7 +924,7 @@ discard block |
||
| 924 | 924 | while (isset($m->nextSibling)) { |
| 925 | 925 | $m = $m->nextSibling; |
| 926 | 926 | if ($m->nodeType === XML_ELEMENT_NODE) { |
| 927 | - if (! empty($selector)) { |
|
| 927 | + if (!empty($selector)) { |
|
| 928 | 928 | if (QueryPath::with($m, null, $this->options)->is($selector) > 0) { |
| 929 | 929 | $found->attach($m); |
| 930 | 930 | } |
@@ -965,7 +965,7 @@ discard block |
||
| 965 | 965 | while (isset($m->previousSibling)) { |
| 966 | 966 | $m = $m->previousSibling; |
| 967 | 967 | if ($m->nodeType === XML_ELEMENT_NODE) { |
| 968 | - if (! empty($selector)) { |
|
| 968 | + if (!empty($selector)) { |
|
| 969 | 969 | if (QueryPath::with($m, null, $this->options)->is($selector)) { |
| 970 | 970 | $found->attach($m); |
| 971 | 971 | break; |
@@ -1007,7 +1007,7 @@ discard block |
||
| 1007 | 1007 | while (isset($m->previousSibling)) { |
| 1008 | 1008 | $m = $m->previousSibling; |
| 1009 | 1009 | if ($m->nodeType === XML_ELEMENT_NODE) { |
| 1010 | - if (! empty($selector)) { |
|
| 1010 | + if (!empty($selector)) { |
|
| 1011 | 1011 | if (QueryPath::with($m, null, $this->options)->is($selector)) { |
| 1012 | 1012 | $found->attach($m); |
| 1013 | 1013 | } |