Skip to content

Commit c58c718

Browse files
committed
Implemented nth-of-type pseudoclass support
1 parent 08156dc commit c58c718

File tree

5 files changed

+99
-19
lines changed

5 files changed

+99
-19
lines changed

src/PHPHtmlParser/DTO/Selector/RuleDTO.php

+19-7
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ final class RuleDTO
3636
*/
3737
private $alterNext;
3838

39+
/**
40+
* @var bool
41+
*/
42+
private $isNthOfType;
43+
3944
private function __construct(array $values)
4045
{
4146
$this->tag = $values['tag'];
@@ -44,21 +49,23 @@ private function __construct(array $values)
4449
$this->value = $values['value'];
4550
$this->noKey = $values['noKey'];
4651
$this->alterNext = $values['alterNext'];
52+
$this->isNthOfType = $values['isNthOfType'];
4753
}
4854

4955
/**
5056
* @param string|array|null $key
5157
* @param string|array|null $value
5258
*/
53-
public static function makeFromPrimitives(string $tag, string $operator, $key, $value, bool $noKey, bool $alterNext): RuleDTO
59+
public static function makeFromPrimitives(string $tag, string $operator, $key, $value, bool $noKey, bool $alterNext, bool $isNthOfType = false): RuleDTO
5460
{
5561
return new RuleDTO([
56-
'tag' => $tag,
57-
'operator' => $operator,
58-
'key' => $key,
59-
'value' => $value,
60-
'noKey' => $noKey,
61-
'alterNext' => $alterNext,
62+
'tag' => $tag,
63+
'operator' => $operator,
64+
'key' => $key,
65+
'value' => $value,
66+
'noKey' => $noKey,
67+
'alterNext' => $alterNext,
68+
'isNthOfType' => $isNthOfType
6269
]);
6370
}
6471

@@ -97,4 +104,9 @@ public function isAlterNext(): bool
97104
{
98105
return $this->alterNext;
99106
}
107+
108+
public function isNthOfType(): bool
109+
{
110+
return $this->isNthOfType;
111+
}
100112
}

src/PHPHtmlParser/Dom/Node/InnerNode.php

+8
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,14 @@ public function childElements(): array
111111
return !$el->isTextNode();
112112
}));
113113
}
114+
115+
public function childElementsOfType(string $tag): array
116+
{
117+
return array_values(array_filter($this->getChildren(), function ($el) use ($tag) {
118+
return $el instanceof HtmlNode && $el->getTag()->name() == $tag;
119+
}));
120+
}
121+
114122
/**
115123
* Adds a child node to this node and returns the id of the child for this
116124
* parent.

src/PHPHtmlParser/Selector/Parser.php

+10-7
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public function parseSelectorString(string $selector): ParsedSelectorCollectionD
4040
$value = null;
4141
$noKey = false;
4242
$alterNext = false;
43+
$isNthOfType = false;
4344

4445
// check for elements that alter the behavior of the next element
4546
if ($tag == '>') {
@@ -64,13 +65,14 @@ public function parseSelectorString(string $selector): ParsedSelectorCollectionD
6465
$tag = '*';
6566
$value = \substr($match[0], 1);
6667

67-
if (preg_match("/^nth-child\(\d+\)$/", \trim($value, ', '))) {
68-
preg_match_all("/^nth-child\((\d+)\)$/", \trim($value, ', '), $matches, PREG_SET_ORDER);
69-
$key = (int) $matches[0][1];
70-
} else if (preg_match("/^nth-last-child\(\d+\)$/", \trim($value, ', '))) {
71-
preg_match_all("/^nth-last-child\((\d+)\)$/", \trim($value, ', '), $matches, PREG_SET_ORDER);
72-
$key = - (int) $matches[0][1];
68+
if (preg_match("/^(nth-child|nth-of-type)\(\d+\)$/", \trim($value, ', '))) {
69+
preg_match_all("/^(nth-child|nth-of-type)\((\d+)\)$/", \trim($value, ', '), $matches, PREG_SET_ORDER);
70+
$key = (int) $matches[0][2];
71+
} else if (preg_match("/^(nth-last-child|nth-last-of-type)\(\d+\)$/", \trim($value, ', '))) {
72+
preg_match_all("/^(nth-last-child|nth-last-of-type)\((\d+)\)$/", \trim($value, ', '), $matches, PREG_SET_ORDER);
73+
$key = - (int) $matches[0][2];
7374
}
75+
$isNthOfType = preg_match("/^nth(-last)?-of-type\(\d+\)$/", \trim($value, ', '));
7476
}
7577

7678
// and final attribute selector
@@ -113,7 +115,8 @@ public function parseSelectorString(string $selector): ParsedSelectorCollectionD
113115
$key,
114116
$value,
115117
$noKey,
116-
$alterNext
118+
$alterNext,
119+
$isNthOfType
117120
);
118121
if (isset($match[7]) && \is_string($match[7]) && \trim($match[7]) == ',') {
119122
$selectors[] = ParsedSelectorDTO::makeFromRules($rules);

src/PHPHtmlParser/Selector/Seeker.php

+5-5
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,11 @@ public function seek(array $nodes, RuleDTO $rule, array $options): array
5353
$pass = true;
5454

5555
if ($rule->getTag() !== null && \is_numeric($rule->getKey()) && $node instanceof HtmlNode) {
56-
if (strpos($rule->getValue(), 'nth-') === 0) {
57-
$children = $node->childElements();
58-
$n = $rule->getKey() < 0 ? count($children) + $rule->getKey() : $rule->getKey()-1;
59-
$pass = $n >= 0 && $n < count($children) && $child == $children[$n];
60-
}
56+
$children = $rule->isNthOfType() ?
57+
$node->childElementsOfType($child->getTag()->name()) :
58+
$node->childElements();
59+
$n = $rule->getKey() < 0 ? count($children) + $rule->getKey() : $rule->getKey()-1;
60+
$pass = $n >= 0 && $n < count($children) && $child == $children[$n];
6161
}
6262

6363
if ($pass) {

tests/Selector/SeekerTest.php

+57
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,61 @@ public function testSeekNthChild()
6262
$this->assertCount(1, $results);
6363
$this->assertEquals('p', $results[0]->getTag()->name());
6464
}
65+
66+
public function testSeekNthOfType()
67+
{
68+
$ruleDTO = RuleDTO::makeFromPrimitives(
69+
'div',
70+
'=',
71+
1,
72+
null,
73+
false,
74+
false,
75+
true
76+
);
77+
78+
$test = new HtmlNode('div');
79+
$p1 = new HtmlNode('p');
80+
$div = new HtmlNode('div');
81+
$p2 = new HtmlNode('p');
82+
$test->addChild($p1);
83+
$test->addChild($div);
84+
$test->addChild($p2);
85+
86+
$seeker = new Seeker();
87+
88+
$results = $seeker->seek([$test], $ruleDTO, []);
89+
$this->assertCount(1, $results);
90+
$this->assertEquals('div', $results[0]->getTag()->name());
91+
92+
$ruleDTO = RuleDTO::makeFromPrimitives(
93+
'p',
94+
'=',
95+
2,
96+
null,
97+
false,
98+
false,
99+
true
100+
);
101+
102+
$results = $seeker->seek([$test], $ruleDTO, []);
103+
$this->assertCount(1, $results);
104+
$this->assertEquals('p', $results[0]->getTag()->name());
105+
$this->assertTrue($results[0] === $test->lastChild());
106+
107+
$ruleDTO = RuleDTO::makeFromPrimitives(
108+
'p',
109+
'=',
110+
-1,
111+
null,
112+
false,
113+
false,
114+
true
115+
);
116+
117+
$results = $seeker->seek([$test], $ruleDTO, []);
118+
$this->assertCount(1, $results);
119+
$this->assertEquals('p', $results[0]->getTag()->name());
120+
$this->assertTrue($results[0] === $test->lastChild());
121+
}
65122
}

0 commit comments

Comments
 (0)