Skip to content

Commit b3b7cb6

Browse files
committed
feat: add querySelectorAll support
1 parent 368685d commit b3b7cb6

5 files changed

Lines changed: 79 additions & 5 deletions

File tree

docs/phpmd.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@ layout: home
1313
## Design
1414

1515

16-
Fri Nov 28 15:57:58 CET 2025
16+
Fri Nov 28 17:06:30 CET 2025

src/Delegator/HTMLDocumentDelegator.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
class HTMLDocumentDelegator implements HTMLDocumentDelegatorInterface
5858
{
5959
use DelegatorTrait;
60+
use \Html\Trait\ClassResolverTrait;
6061

6162
public bool $formatOutput;
6263

@@ -132,7 +133,7 @@ public function querySelector(string $selectors): ?HTMLElementDelegator
132133
if ($element === null) {
133134
return null;
134135
}
135-
return new HTMLElementDelegator($element);
136+
return $this->getDelegatorFromElement($element);
136137
}
137138

138139
public function querySelectorAll(string $selectors): ?NodeListDelegator

src/Delegator/NodeListDelegator.php

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Html\Trait\DelegatorTrait;
1010
use Iterator;
1111
use ReflectionClass;
12+
use Traversable;
1213

1314
/**
1415
* inheritDoc
@@ -72,6 +73,7 @@
7273
class NodeListDelegator
7374
{
7475
use DelegatorTrait;
76+
use \Html\Trait\ClassResolverTrait;
7577

7678
public function __construct(
7779
private readonly NodeList|HTMLCollection $delegated
@@ -90,10 +92,27 @@ public function __call($name, $arguments)
9092
);
9193
}
9294

93-
public function item(int $index): ?NodeDelegator
95+
public function item(int $index): mixed
9496
{
9597
$node = $this->delegated->item($index);
96-
return $node ? new NodeDelegator($node) : null;
98+
if (! $node) {
99+
return null;
100+
}
101+
102+
if ($node instanceof \DOM\Element) {
103+
$delegator = $this->getDelegatorFromElement($node);
104+
if ($delegator) {
105+
return $delegator;
106+
}
107+
}
108+
return new NodeDelegator($node);
109+
}
110+
111+
public function getIterator(): Traversable
112+
{
113+
for ($i = 0, $len = $this->delegated->length; $i < $len; $i++) {
114+
yield $this->item($i);
115+
}
97116
}
98117

99118
public function getNodeList(): NodeList|HTMLCollection

src/Trait/ClassResolverTrait.php

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,20 @@ public function getElementByQualifiedName(string $qualifiedName): ?string
100100
return null;
101101
}
102102

103+
/**
104+
* Given a DOM element, return an instance of the correct delegator class (or null).
105+
* Uses getElementByQualifiedName to resolve the class.
106+
*/
107+
public function getDelegatorFromElement(\DOM\Element $element): ?HTMLElementDelegator
108+
{
109+
$tagName = strtolower($element->tagName);
110+
$class = $this->getElementByQualifiedName($tagName);
111+
if ($class && class_exists($class)) {
112+
return new $class($element);
113+
}
114+
return null;
115+
}
116+
103117
/**
104118
* Load all relevant PHP files for class scanning.
105119
*/
@@ -141,7 +155,7 @@ private function loadAllPhpFiles(string $directory): void
141155
foreach ($files as $file) {
142156
// echo $file->getPathname() . PHP_EOL;
143157
if ($file->isFile() && $file->getExtension() === 'php') {
144-
if (str_ends_with($file->getFilename(), '.tpl.php')) {
158+
if (str_ends_with($file->getFilename(), '.tpl.php') || str_contains($file->getPathname(), 'Command')) {
145159
continue;
146160
}
147161

tests/Delegator/HTMLDocumentDelegatorTest.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@
66
use Html\Delegator\HTMLElementDelegator;
77
use Html\Delegator\NodeListDelegator;
88
use Html\Element\Block\Body;
9+
use Html\Element\Block\Division;
910
use Html\Element\Block\TableData;
1011
use Html\Element\Block\TableRow;
12+
use Html\Element\Inline\Anchor;
13+
use Html\Enum\TargetEnum;
1114
use Html\TemplateGenerator\HTMLGenerator;
1215

1316
// uses(\Html\Trait\GlobalAttributesTrait::class);
@@ -316,6 +319,24 @@
316319
->toBeNull();
317320
});
318321

322+
test('can use querySelected element', function () {
323+
$html = '<!DOCTYPE html><html><head><title>Test</title></head><body><a class="test-class">Link</a></body></html>';
324+
$delegator = HTMLDocumentDelegator::createFromString($html);
325+
326+
$element = $delegator->querySelector('.test-class');
327+
$element->setTarget('_blank');
328+
$element->setTextContent('New Link Text');
329+
expect($element)
330+
->toBeInstanceOf(HTMLElementDelegator::class);
331+
expect($element)
332+
->toBeInstanceOf(Anchor::class);
333+
expect($element->getTarget())
334+
->toBe(TargetEnum::BLANK);
335+
expect($element->getTextContent())
336+
->toBe('New Link Text');
337+
});
338+
339+
319340
test('can querySelectorAll', function () {
320341
$html = '<!DOCTYPE html><html><head><title>Test</title></head><body><div class="test-class"><p class="test-class">Test</p></div></body></html>';
321342
$delegator = HTMLDocumentDelegator::createFromString($html);
@@ -332,3 +353,22 @@
332353
expect($nonExistentElement)
333354
->toBeNull();
334355
});
356+
357+
test('can use querySelectorAll element', function () {
358+
$html = '<!DOCTYPE html><html><head><title>Test</title></head><body><div class="test-class"><p class="test-class">Test</p></div></body></html>';
359+
$delegator = HTMLDocumentDelegator::createFromString($html);
360+
361+
$elements = $delegator->querySelectorAll('.test-class');
362+
expect($elements->item(1)->tagName)
363+
->toBe('P');
364+
expect($elements->item(0)->tagName)
365+
->toBe('DIV');
366+
/** @var \Html\Element\Division $div */
367+
$div = $elements->item(0);
368+
expect($div)
369+
->toBeInstanceOf(Division::class);
370+
$elements->item(1)
371+
->setTextContent('Updated Paragraph Text');
372+
expect($elements->item(1)->getTextContent())
373+
->toBe('Updated Paragraph Text');
374+
});

0 commit comments

Comments
 (0)