Skip to content

Commit 6a1aaac

Browse files
committed
WIP
1 parent f11ea2a commit 6a1aaac

File tree

8 files changed

+226
-4
lines changed

8 files changed

+226
-4
lines changed

ext/dom/element.c

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -842,6 +842,44 @@ PHP_METHOD(Dom_Element, getElementsByTagName)
842842
}
843843
/* }}} end dom_element_get_elements_by_tag_name */
844844

845+
PHP_METHOD(Dom_Element, getElementsByClassName)
846+
{
847+
dom_object *intern, *namednode;
848+
zend_string *class_names;
849+
850+
if (zend_parse_parameters(ZEND_NUM_ARGS(), "P", &class_names) == FAILURE) {
851+
RETURN_THROWS();
852+
}
853+
854+
if (ZSTR_LEN(class_names) > INT_MAX) {
855+
zend_argument_value_error(1, "is too long");
856+
RETURN_THROWS();
857+
}
858+
859+
DOM_GET_THIS_INTERN(intern);
860+
861+
object_init_ex(return_value, dom_html_collection_class_entry);
862+
namednode = Z_DOMOBJ_P(return_value);
863+
864+
HashTable *token_set;
865+
ALLOC_HASHTABLE(token_set);
866+
zend_hash_init(token_set, 0, NULL, NULL, false);
867+
dom_ordered_set_parser(token_set, ZSTR_VAL(class_names), intern->document->quirks_mode == PHP_LIBXML_QUIRKS);
868+
869+
if (zend_hash_num_elements(token_set) == 0) {
870+
php_dom_create_obj_map(intern, namednode, NULL, NULL, NULL, &php_dom_obj_map_noop);
871+
872+
zend_hash_destroy(token_set);
873+
FREE_HASHTABLE(token_set);
874+
} else {
875+
php_dom_create_obj_map(intern, namednode, NULL, NULL, NULL, &php_dom_obj_map_by_class_name);
876+
877+
dom_nnodemap_object *map = namednode->ptr;
878+
map->array = token_set;
879+
map->release_array = true;
880+
}
881+
}
882+
845883
/* should_free_result must be initialized to false */
846884
static const xmlChar *dom_get_attribute_ns(dom_object *intern, xmlNodePtr elemp, const char *uri, size_t uri_len, const char *name, bool *should_free_result)
847885
{

ext/dom/obj_map.c

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
#if defined(HAVE_LIBXML) && defined(HAVE_DOM)
2525
#include "php_dom.h"
2626
#include "obj_map.h"
27+
#include "token_list.h"
2728

2829
static zend_always_inline void objmap_cache_release_cached_obj(dom_nnodemap_object *objmap)
2930
{
@@ -40,6 +41,30 @@ static zend_always_inline void reset_objmap_cache(dom_nnodemap_object *objmap)
4041
objmap->cached_length = -1;
4142
}
4243

44+
static bool dom_matches_class_name(const dom_nnodemap_object *map, const xmlNode *nodep)
45+
{
46+
bool ret = false;
47+
48+
if (nodep->type == XML_ELEMENT_NODE) {
49+
xmlAttrPtr classes = xmlHasNsProp(nodep, BAD_CAST "class", NULL);
50+
if (classes != NULL) {
51+
bool should_free;
52+
xmlChar *value = php_libxml_attr_value(classes, &should_free);
53+
54+
bool quirks = map->baseobj->document->quirks_mode == PHP_LIBXML_QUIRKS;
55+
if (dom_ordered_set_all_contained(map->array, (const char *) value, quirks)) {
56+
ret = true;
57+
}
58+
59+
if (should_free) {
60+
xmlFree(value);
61+
}
62+
}
63+
}
64+
65+
return ret;
66+
}
67+
4368
/**************************
4469
* === Length methods === *
4570
**************************/
@@ -106,6 +131,24 @@ static zend_long dom_map_get_by_tag_name_length(dom_nnodemap_object *map)
106131
return count;
107132
}
108133

134+
static zend_long dom_map_get_by_class_name_length(dom_nnodemap_object *map)
135+
{
136+
xmlNodePtr nodep = dom_object_get_node(map->baseobj);
137+
zend_long count = 0;
138+
if (nodep) {
139+
xmlNodePtr basep = nodep;
140+
nodep = php_dom_first_child_of_container_node(basep);
141+
142+
while (nodep != NULL) {
143+
if (dom_matches_class_name(map, nodep)) {
144+
count++;
145+
}
146+
nodep = php_dom_next_in_tree_order(nodep, basep);
147+
}
148+
}
149+
return count;
150+
}
151+
109152
static zend_long dom_map_get_zero_length(dom_nnodemap_object *map)
110153
{
111154
return 0;
@@ -292,6 +335,32 @@ static void dom_map_get_by_tag_name_item(dom_nnodemap_object *map, zend_long ind
292335
}
293336
}
294337

338+
static void dom_map_get_by_class_name_item(dom_nnodemap_object *map, zend_long index, zval *return_value)
339+
{
340+
xmlNodePtr nodep = dom_object_get_node(map->baseobj);
341+
xmlNodePtr itemnode = NULL;
342+
if (nodep && index >= 0) {
343+
xmlNodePtr basep = nodep;
344+
dom_node_idx_pair start_point = dom_obj_map_get_start_point(map, nodep, index);
345+
if (start_point.node) {
346+
itemnode = php_dom_next_in_tree_order(start_point.node, basep);
347+
} else {
348+
itemnode = php_dom_first_child_of_container_node(nodep);
349+
}
350+
351+
do {
352+
--start_point.index;
353+
while (itemnode != NULL && !dom_matches_class_name(map, itemnode)) {
354+
itemnode = php_dom_next_in_tree_order(itemnode, basep);
355+
}
356+
} while (start_point.index > 0 && itemnode);
357+
}
358+
dom_ret_node_to_zobj(map, itemnode, return_value);
359+
if (itemnode) {
360+
dom_map_cache_obj(map, itemnode, index, return_value);
361+
}
362+
}
363+
295364
static void dom_map_collection_named_item_by_tag_name_iter(dom_nnodemap_object *map, php_dom_obj_map_collection_iter *iter)
296365
{
297366
iter->candidate = dom_get_elements_by_tag_name_ns_raw(iter->basep, iter->candidate, map->ns, map->local, map->local_lower, &iter->cur, iter->next);
@@ -478,6 +547,16 @@ const php_dom_obj_map_handler php_dom_obj_map_by_tag_name = {
478547
.nameless = true,
479548
};
480549

550+
const php_dom_obj_map_handler php_dom_obj_map_by_class_name = {
551+
.length = dom_map_get_by_class_name_length,
552+
.get_item = dom_map_get_by_class_name_item,
553+
.get_ns_named_item = dom_map_get_ns_named_item_null,
554+
.has_ns_named_item = dom_map_has_ns_named_item_null,
555+
.collection_named_item_iter = NULL, // TODO ???
556+
.use_cache = true,
557+
.nameless = true,
558+
};
559+
481560
const php_dom_obj_map_handler php_dom_obj_map_child_nodes = {
482561
.length = dom_map_get_nodes_length,
483562
.get_item = dom_map_get_nodes_item,
@@ -533,7 +612,7 @@ const php_dom_obj_map_handler php_dom_obj_map_noop = {
533612
.get_item = dom_map_get_null_item,
534613
.get_ns_named_item = dom_map_get_ns_named_item_null,
535614
.has_ns_named_item = dom_map_has_ns_named_item_null,
536-
.collection_named_item_iter = NULL,
615+
.collection_named_item_iter = NULL, // TODO
537616
.use_cache = false,
538617
.nameless = true,
539618
};

ext/dom/obj_map.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ zend_long php_dom_get_nodelist_length(dom_object *obj);
6363

6464
extern const php_dom_obj_map_handler php_dom_obj_map_attributes;
6565
extern const php_dom_obj_map_handler php_dom_obj_map_by_tag_name;
66+
extern const php_dom_obj_map_handler php_dom_obj_map_by_class_name;
6667
extern const php_dom_obj_map_handler php_dom_obj_map_child_elements;
6768
extern const php_dom_obj_map_handler php_dom_obj_map_child_nodes;
6869
extern const php_dom_obj_map_handler php_dom_obj_map_nodeset;

ext/dom/php_dom.stub.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1659,6 +1659,7 @@ public function removeAttributeNode(Attr $attr) : Attr {}
16591659

16601660
public function getElementsByTagName(string $qualifiedName): HTMLCollection {}
16611661
public function getElementsByTagNameNS(?string $namespace, string $localName): HTMLCollection {}
1662+
public function getElementsByClassName(string $classNames): HTMLCollection {}
16621663

16631664
public function insertAdjacentElement(AdjacentPosition $where, Element $element): ?Element {}
16641665
public function insertAdjacentText(AdjacentPosition $where, string $data): void {}
@@ -1986,6 +1987,8 @@ abstract class Document extends Node implements ParentNode
19861987
public function getElementsByTagName(string $qualifiedName): HTMLCollection {}
19871988
/** @implementation-alias Dom\Element::getElementsByTagNameNS */
19881989
public function getElementsByTagNameNS(?string $namespace, string $localName): HTMLCollection {}
1990+
/** @implementation-alias Dom\Element::getElementsByClassName */
1991+
public function getElementsByClassName(string $classNames): HTMLCollection {}
19891992

19901993
public function createElement(string $localName): Element {}
19911994
public function createElementNS(?string $namespace, string $qualifiedName): Element {}

ext/dom/php_dom_arginfo.h

Lines changed: 10 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
--TEST--
2+
Dom\Element::getElementsByClassName()
3+
--EXTENSIONS--
4+
dom
5+
--FILE--
6+
<?php
7+
8+
$dom = Dom\HTMLDocument::createFromString(<<<HTML
9+
<div class=" foo bar ">
10+
<p class="bar">
11+
<p class="bar"></p>
12+
</p>
13+
<b class="bars"></b>
14+
</div>
15+
HTML, LIBXML_NOERROR);
16+
$collection = $dom->documentElement->getElementsByClassName("bar");
17+
18+
echo "There are {$dom->getElementsByClassName("foo \n bar")->count()} items in the document in total\n";
19+
20+
echo "There are {$dom->getElementsByClassName("")->count()} items that match set \"\" in the document in total\n";
21+
22+
echo "There are {$dom->getElementsByClassName(" ")->count()} items that match set \" \" in the document in total\n";
23+
24+
echo "There are {$collection->count()} items\n";
25+
26+
foreach ($collection as $key => $node) {
27+
var_dump($key, $node->tagName);
28+
var_dump($node === $collection->item($key));
29+
}
30+
31+
?>
32+
--EXPECT--
33+
There are 3 items
34+
There are 3 items
35+
int(0)
36+
string(3) "DIV"
37+
bool(false)
38+
int(1)
39+
string(1) "P"
40+
bool(false)

ext/dom/token_list.c

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ static zend_always_inline void dom_add_token(HashTable *ht, zend_string *token)
5151

5252
/* https://dom.spec.whatwg.org/#concept-ordered-set-parser
5353
* and https://infra.spec.whatwg.org/#split-on-ascii-whitespace */
54-
static void dom_ordered_set_parser(HashTable *token_set, const char *position)
54+
void dom_ordered_set_parser(HashTable *token_set, const char *position, bool to_lowercase)
5555
{
5656
/* Adapted steps from "split on ASCII whitespace" such that that loop directly appends to the token set. */
5757

@@ -72,6 +72,9 @@ static void dom_ordered_set_parser(HashTable *token_set, const char *position)
7272

7373
/* 4.2. Append token to tokens. */
7474
zend_string *token = zend_string_init(start, length, false);
75+
if (to_lowercase) {
76+
zend_str_tolower(ZSTR_VAL(token), length);
77+
}
7578
dom_add_token(token_set, token);
7679
zend_string_release_ex(token, false);
7780

@@ -83,6 +86,53 @@ static void dom_ordered_set_parser(HashTable *token_set, const char *position)
8386
* => That's the token set. */
8487
}
8588

89+
/* This returns true if all tokens in "token_set" are found in "value". */
90+
bool dom_ordered_set_all_contained(HashTable *token_set, const char *value, bool to_lowercase)
91+
{
92+
/* This code is conceptually close to dom_ordered_set_parser(),
93+
* but without building a hash table.
94+
* Since the storage of the token set maps a value on itself,
95+
* we can reuse that storage as a "seen" flag by setting it to NULL. */
96+
zval *zv;
97+
98+
uint32_t still_needed = zend_hash_num_elements(token_set);
99+
100+
value += strspn(value, ascii_whitespace);
101+
102+
while (*value != '\0' && still_needed > 0) {
103+
const char *start = value;
104+
value += strcspn(value, ascii_whitespace);
105+
size_t length = value - start;
106+
107+
if (to_lowercase) {
108+
ALLOCA_FLAG(use_heap)
109+
char *lc_str = zend_str_tolower_copy(do_alloca(length + 1, use_heap), start, length);
110+
zv = zend_hash_str_find(token_set, lc_str, length);
111+
free_alloca(lc_str, use_heap);
112+
} else {
113+
zv = zend_hash_str_find(token_set, start, length);
114+
}
115+
if (zv) {
116+
if (Z_STR_P(zv)) {
117+
still_needed--;
118+
Z_STR_P(zv) = NULL;
119+
}
120+
}
121+
122+
value += strspn(value, ascii_whitespace);
123+
}
124+
125+
/* Restore "seen" flag. */
126+
zend_string *k;
127+
ZEND_HASH_FOREACH_STR_KEY_VAL(token_set, k, zv) {
128+
if (!Z_STR_P(zv)) {
129+
Z_STR_P(zv) = k;
130+
}
131+
} ZEND_HASH_FOREACH_END();
132+
133+
return still_needed == 0;
134+
}
135+
86136
/* https://dom.spec.whatwg.org/#concept-ordered-set-serializer */
87137
static char *dom_ordered_set_serializer(HashTable *token_set)
88138
{
@@ -166,7 +216,7 @@ static void dom_token_list_update_set(dom_token_list_object *intern, HashTable *
166216
xmlChar *value = dom_token_list_get_class_value(attr, &should_free);
167217
if (value != NULL) {
168218
/* 2. Otherwise, parse the token set. */
169-
dom_ordered_set_parser(token_set, (const char *) value);
219+
dom_ordered_set_parser(token_set, (const char *) value, false);
170220
intern->cached_string = estrdup((const char *) value);
171221
} else {
172222
intern->cached_string = NULL;

ext/dom/token_list.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ static inline dom_token_list_object *php_dom_token_list_from_dom_obj(dom_object
3535
return (dom_token_list_object *)((char *) obj - XtOffsetOf(dom_token_list_object, dom));
3636
}
3737

38+
void dom_ordered_set_parser(HashTable *token_set, const char *position, bool to_lowercase);
39+
bool dom_ordered_set_all_contained(HashTable *token_set, const char *value, bool to_lowercase);
3840
void dom_token_list_ctor(dom_token_list_object *intern, dom_object *element_obj);
3941
void dom_token_list_free_obj(zend_object *object);
4042
zval *dom_token_list_read_dimension(zend_object *object, zval *offset, int type, zval *rv);

0 commit comments

Comments
 (0)