Passed
Branch dev/2.0.2 (8bf35c)
by Gilles
02:23
created

Selector::alterNext()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 1
dl 0
loc 8
ccs 5
cts 5
cp 1
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
namespace PHPHtmlParser\Selector;
3
4
use PHPHtmlParser\Dom\AbstractNode;
5
use PHPHtmlParser\Dom\Collection;
6
use PHPHtmlParser\Dom\InnerNode;
7
use PHPHtmlParser\Dom\LeafNode;
8
use PHPHtmlParser\Exceptions\ChildNotFoundException;
9
10
/**
11
 * Class Selector
12
 *
13
 * @package PHPHtmlParser
14
 */
15
class Selector
16
{
17
18
    /**
19
     * @var array
20
     */
21
    protected $selectors = [];
22
23
    /**
24
     * Constructs with the selector string
25
     *
26
     * @param string $selector
27
     */
28 252
    public function __construct(string $selector, ParserInterface $parser)
29
    {
30 252
        $this->selectors = $parser->parseSelectorString($selector);
31 252
    }
32
33
    /**
34
     * Returns the selectors that where found in __construct
35
     *
36
     * @return array
37
     */
38 12
    public function getSelectors()
39
    {
40 12
        return $this->selectors;
41
    }
42
43
    /**
44
     * Attempts to find the selectors starting from the given
45
     * node object.
46
     *
47
     * @param AbstractNode $node
48
     * @return Collection
49
     */
50 240
    public function find(AbstractNode $node): Collection
51
    {
52 240
        $results = new Collection;
53 240
        foreach ($this->selectors as $selector) {
54 240
            $nodes = [$node];
55 240
            if (count($selector) == 0) {
56
                continue;
57
            }
58
59 240
            $options = [];
60 240
            foreach ($selector as $rule) {
61 240
                if ($rule['alterNext']) {
62 3
                    $options[] = $this->alterNext($rule);
63 3
                    continue;
64
                }
65 240
                $nodes = $this->seek($nodes, $rule, $options);
66
                // clear the options
67 240
                $options = [];
68
            }
69
70
            // this is the final set of nodes
71 240
            foreach ($nodes as $result) {
72 225
                $results[] = $result;
73
            }
74
        }
75
76 240
        return $results;
77
    }
78
79
80
    /**
81
     * Attempts to find all children that match the rule
82
     * given.
83
     *
84
     * @param array $nodes
85
     * @param array $rule
86
     * @param array $options
87
     * @return array
88
     * @recursive
89
     */
90 240
    protected function seek(array $nodes, array $rule, array $options): array
91
    {
92
        // XPath index
93 240
        if (array_key_exists('tag', $rule) &&
94 240
            array_key_exists('key', $rule) &&
95 240
            is_numeric($rule['key'])
96
        ) {
97 3
            $count = 0;
98
            /** @var AbstractNode $node */
99 3
            foreach ($nodes as $node) {
100 3
                if ($rule['tag'] == '*' ||
101 3
                    $rule['tag'] == $node->getTag()->name()
102
                ) {
103 3
                    ++$count;
104 3
                    if ($count == $rule['key']) {
105
                        // found the node we wanted
106 3
                        return [$node];
107
                    }
108
                }
109
            }
110
111
            return [];
112
        }
113
114 237
        $options = $this->flattenOptions($options);
115
116 237
        $return = [];
117
        /** @var InnerNode $node */
118 237
        foreach ($nodes as $node) {
119
            // check if we are a leaf
120 237
            if ($node instanceof LeafNode ||
121 237
                ! $node->hasChildren()
122
            ) {
123 12
                continue;
124
            }
125
126 237
            $children = [];
127 237
            $child    = $node->firstChild();
128 237
            while ( ! is_null($child)) {
129
                // wild card, grab all
130 237
                if ($rule['tag'] == '*' && is_null($rule['key'])) {
131 12
                    $return[] = $child;
132
                    try {
133 12
                        $child = $node->nextChild($child->id());
134 12
                    } catch (ChildNotFoundException $e) {
135
                        // no more children
136 12
                        $child = null;
137
                    }
138 12
                    continue;
139
                }
140
141 237
                $pass = true;
142
                // check tag
143 237
                if ( ! empty($rule['tag']) && $rule['tag'] != $child->getTag()->name() &&
144 237
                    $rule['tag'] != '*'
145
                ) {
146
                    // child failed tag check
147 213
                    $pass = false;
148
                }
149
150
                // check key
151 237
                if ($pass && ! is_null($rule['key'])) {
152 84
                    if ($rule['noKey']) {
153
                        if ( ! is_null($child->getAttribute($rule['key']))) {
154
                            $pass = false;
155
                        }
156
                    } else {
157 84
                        if ($rule['key'] != 'plaintext' && !$child->hasAttribute($rule['key'])) {
158 81
                            $pass = false;
159
                        }
160
                    }
161
                }
162
163
                // compare values
164 237
                if ($pass && ! is_null($rule['key']) &&
165 237
                    ! is_null($rule['value']) && $rule['value'] != '*'
166
                ) {
167 81
                    if ($rule['key'] == 'plaintext') {
168
                        // plaintext search
169
                        $nodeValue = $child->text();
170
                    } else {
171
                        // normal search
172 81
                        $nodeValue = $child->getAttribute($rule['key']);
173
                    }
174
175 81
                    $check = $this->match($rule['operator'], $rule['value'], $nodeValue);
176
177
                    // handle multiple classes
178 81
                    if ( ! $check && $rule['key'] == 'class') {
179 36
                        $childClasses = explode(' ', $child->getAttribute('class'));
180 36
                        foreach ($childClasses as $class) {
181 36
                            if ( ! empty($class)) {
182 36
                                $check = $this->match($rule['operator'], $rule['value'], $class);
183
                            }
184 36
                            if ($check) {
185 31
                                break;
186
                            }
187
                        }
188
                    }
189
190 81
                    if ( ! $check) {
191 63
                        $pass = false;
192
                    }
193
                }
194
195 237
                if ($pass) {
196
                    // it passed all checks
197 192
                    $return[] = $child;
198
                } else {
199
                    // this child failed to be matched
200 222
                    if ($child instanceof InnerNode &&
201 222
                        $child->hasChildren()
202
                    ) {
203
                        // we still want to check its children
204 210
                        $children[] = $child;
205
                    }
206
                }
207
208
                try {
209
                    // get next child
210 237
                    $child = $node->nextChild($child->id());
211 237
                } catch (ChildNotFoundException $e) {
212
                    // no more children
213 237
                    $child = null;
214
                }
215
            }
216
217 237
            if (( ! isset($options['checkGrandChildren']) ||
218 237
                    $options['checkGrandChildren'])
219 237
                && count($children) > 0
220
            ) {
221
                // we have children that failed but are not leaves.
222 207
                $matches = $this->seek($children, $rule, $options);
223 207
                foreach ($matches as $match) {
224 193
                    $return[] = $match;
225
                }
226
            }
227
        }
228
229 237
        return $return;
230
    }
231
232
    /**
233
     * Attempts to match the given arguments with the given operator.
234
     *
235
     * @param string $operator
236
     * @param string $pattern
237
     * @param string $value
238
     * @return bool
239
     */
240 81
    protected function match(string $operator, string $pattern, string $value): bool
241
    {
242 81
        $value   = strtolower($value);
243 81
        $pattern = strtolower($pattern);
244 54
        switch ($operator) {
245 81
            case '=':
246 81
                return $value === $pattern;
247
            case '!=':
248
                return $value !== $pattern;
249
            case '^=':
250
                return preg_match('/^'.preg_quote($pattern, '/').'/', $value) == 1;
251
            case '$=':
252
                return preg_match('/'.preg_quote($pattern, '/').'$/', $value) == 1;
253
            case '*=':
254
                if ($pattern[0] == '/') {
255
                    return preg_match($pattern, $value) == 1;
256
                }
257
258
                return preg_match("/".$pattern."/i", $value) == 1;
259
        }
260
261
        return false;
262
    }
263
264
    /**
265
     * Attempts to figure out what the alteration will be for
266
     * the next element.
267
     *
268
     * @param array $rule
269
     * @return array
270
     */
271 3
    protected function alterNext(array $rule): array
272
    {
273 3
        $options = [];
274 3
        if ($rule['tag'] == '>') {
275 3
            $options['checkGrandChildren'] = false;
276
        }
277
278 3
        return $options;
279
    }
280
281
    /**
282
     * Flattens the option array.
283
     *
284
     * @param array $optionsArray
285
     * @return array
286
     */
287 237
    protected function flattenOptions(array $optionsArray)
288
    {
289 237
        $options = [];
290 237
        foreach ($optionsArray as $optionArray) {
291 3
            foreach ($optionArray as $key => $option) {
292 3
                $options[$key] = $option;
293
            }
294
        }
295
296 237
        return $options;
297
    }
298
}
299