Skip to content

Commit 3d180f5

Browse files
committed
Implement Element::closest()
1 parent 39c75f5 commit 3d180f5

File tree

7 files changed

+131
-1
lines changed

7 files changed

+131
-1
lines changed

ext/dom/element.c

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1760,4 +1760,20 @@ PHP_METHOD(DOM_Element, matches)
17601760
dom_element_matches(thisp, intern, return_value, selectors_str);
17611761
}
17621762

1763+
PHP_METHOD(DOM_Element, closest)
1764+
{
1765+
zend_string *selectors_str;
1766+
1767+
ZEND_PARSE_PARAMETERS_START(1, 1)
1768+
Z_PARAM_STR(selectors_str)
1769+
ZEND_PARSE_PARAMETERS_END();
1770+
1771+
xmlNodePtr thisp;
1772+
dom_object *intern;
1773+
zval *id;
1774+
DOM_GET_THIS_OBJ(thisp, id, xmlNodePtr, intern);
1775+
1776+
dom_element_closest(thisp, intern, return_value, selectors_str);
1777+
}
1778+
17631779
#endif

ext/dom/parentnode/css_selectors.c

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,4 +231,33 @@ void dom_element_matches(xmlNodePtr thisp, dom_object *intern, zval *return_valu
231231
}
232232
}
233233

234+
/* https://dom.spec.whatwg.org/#dom-element-closest */
235+
void dom_element_closest(xmlNodePtr thisp, dom_object *intern, zval *return_value, zend_string *selectors_str)
236+
{
237+
lxb_css_parser_t parser;
238+
lxb_selectors_t selectors;
239+
240+
lxb_css_selector_list_t *list = php_dom_parse_selector(&parser, &selectors, selectors_str, LXB_SELECTORS_OPT_MATCH_FIRST);
241+
if (UNEXPECTED(list == NULL)) {
242+
RETURN_THROWS();
243+
} else {
244+
xmlNodePtr current = thisp;
245+
while (current != NULL) {
246+
dom_query_selector_matches_ctx ctx = { current, false };
247+
lxb_status_t status = lxb_selectors_match_node(&selectors, current, list, php_dom_query_selector_find_matches_callback, &ctx);
248+
status = php_dom_check_css_execution_status(status);
249+
if (UNEXPECTED(status != LXB_STATUS_OK)) {
250+
break;
251+
}
252+
if (ctx.result) {
253+
DOM_RET_OBJ(current, intern);
254+
break;
255+
}
256+
current = current->parent;
257+
}
258+
}
259+
260+
php_dom_selector_cleanup(&parser, &selectors, list);
261+
}
262+
234263
#endif

ext/dom/php_dom.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ bool php_dom_pre_insert_is_parent_invalid(xmlNodePtr parent);
199199
void dom_parent_node_query_selector(xmlNodePtr thisp, dom_object *intern, zval *return_value, zend_string *selectors_str);
200200
void dom_parent_node_query_selector_all(xmlNodePtr thisp, dom_object *intern, zval *return_value, zend_string *selectors_str);
201201
void dom_element_matches(xmlNodePtr thisp, dom_object *intern, zval *return_value, zend_string *selectors_str);
202+
void dom_element_closest(xmlNodePtr thisp, dom_object *intern, zval *return_value, zend_string *selectors_str);
202203

203204
/* nodemap and nodelist APIs */
204205
xmlNodePtr php_dom_named_node_map_get_named_item(dom_nnodemap_object *objmap, const zend_string *named, bool may_transform);

ext/dom/php_dom.stub.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1379,6 +1379,7 @@ public function replaceChildren(Node|string ...$nodes): void {}
13791379

13801380
public function querySelector(string $selectors): ?Element {}
13811381
public function querySelectorAll(string $selectors): NodeList {}
1382+
public function closest(string $selectors): ?Element {}
13821383
public function matches(string $selectors): bool {}
13831384
}
13841385

ext/dom/php_dom_arginfo.h

Lines changed: 5 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
--TEST--
2+
Test DOM\Element::closest() method: legit cases
3+
--EXTENSIONS--
4+
dom
5+
--FILE--
6+
<?php
7+
8+
$xml = <<<XML
9+
<root>
10+
<a/>
11+
<div class="foo" xml:id="div1">
12+
<div xml:id="div2">
13+
<div class="bar" xml:id="div3"/>
14+
</div>
15+
</div>
16+
</root>
17+
XML;
18+
19+
$dom = DOM\XMLDocument::createFromString($xml);
20+
21+
function test($el, $selector) {
22+
echo "--- Selector: $selector ---\n";
23+
var_dump($el->closest($selector)?->getAttribute('xml:id'));
24+
}
25+
26+
test($dom->getElementById('div3'), 'div');
27+
test($dom->getElementById('div3'), '[class="foo"]');
28+
test($dom->getElementById('div3'), ':not(root)');
29+
test($dom->getElementById('div3'), ':not(div)');
30+
test($dom->getElementById('div3'), 'a');
31+
test($dom->getElementById('div3'), 'root :not(div[class])');
32+
test($dom->getElementById('div3'), 'root > :not(div[class])');
33+
34+
?>
35+
--EXPECT--
36+
--- Selector: div ---
37+
string(4) "div3"
38+
--- Selector: [class="foo"] ---
39+
string(4) "div1"
40+
--- Selector: :not(root) ---
41+
string(4) "div3"
42+
--- Selector: :not(div) ---
43+
NULL
44+
--- Selector: a ---
45+
NULL
46+
--- Selector: root :not(div[class]) ---
47+
string(4) "div2"
48+
--- Selector: root > :not(div[class]) ---
49+
NULL
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
--TEST--
2+
Test DOM\Element::closest() method: invalid selector
3+
--EXTENSIONS--
4+
dom
5+
--FILE--
6+
<?php
7+
8+
$html = <<<HTML
9+
<article>
10+
<div id="div-01">
11+
Here is div-01
12+
<div id="div-02">
13+
Here is div-02
14+
<div id="div-03">Here is div-03</div>
15+
</div>
16+
</div>
17+
</article>
18+
HTML;
19+
20+
$dom = DOM\XMLDocument::createFromString("<root/>");
21+
22+
try {
23+
var_dump($dom->documentElement->closest('@invalid'));
24+
} catch (DOMException $e) {
25+
echo $e->getMessage();
26+
}
27+
28+
?>
29+
--EXPECT--
30+
Invalid selector (Selectors. Unexpected token: @invalid)

0 commit comments

Comments
 (0)