Skip to content

Commit 9732e19

Browse files
committed
Ensure unique nodes in query and query_by_id
1 parent 66a9947 commit 9732e19

2 files changed

Lines changed: 90 additions & 13 deletions

File tree

c_src/lazy_html.cpp

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -697,17 +697,27 @@ ExLazyHTML query(ErlNifEnv *env, ExLazyHTML ex_lazy_html,
697697
LXB_SELECTORS_OPT_MATCH_ROOT));
698698

699699
auto nodes = std::vector<lxb_dom_node_t *>();
700+
auto inserted_nodes = std::unordered_set<lxb_dom_node_t *>();
701+
702+
struct FindCtx {
703+
std::vector<lxb_dom_node_t *> *nodes;
704+
std::unordered_set<lxb_dom_node_t *> *inserted_nodes;
705+
};
706+
707+
auto ctx = FindCtx{&nodes, &inserted_nodes};
700708

701709
for (auto node : ex_lazy_html.resource->nodes) {
702710
status = lxb_selectors_find(
703711
selectors, node, css_selector_list,
704712
[](lxb_dom_node_t *node, lxb_css_selector_specificity_t spec,
705713
void *ctx) -> lxb_status_t {
706-
auto nodes_ptr = static_cast<std::vector<lxb_dom_node_t *> *>(ctx);
707-
nodes_ptr->push_back(node);
714+
auto find_ctx = static_cast<FindCtx *>(ctx);
715+
if (find_ctx->inserted_nodes->insert(node).second) {
716+
find_ctx->nodes->push_back(node);
717+
}
708718
return LXB_STATUS_OK;
709719
},
710-
&nodes);
720+
&ctx);
711721
if (status != LXB_STATUS_OK) {
712722
throw std::runtime_error("failed to run find");
713723
}
@@ -789,22 +799,31 @@ bool matches_id(lxb_dom_node_t *node, ErlNifBinary *id) {
789799
ExLazyHTML query_by_id(ErlNifEnv *env, ExLazyHTML ex_lazy_html,
790800
ErlNifBinary id) {
791801
auto nodes = std::vector<lxb_dom_node_t *>();
802+
auto seen = std::unordered_set<lxb_dom_node_t *>();
792803

793-
auto ctx = std::make_tuple(&nodes, &id);
804+
struct WalkCtx {
805+
std::vector<lxb_dom_node_t *> *nodes;
806+
std::unordered_set<lxb_dom_node_t *> *seen;
807+
ErlNifBinary *id;
808+
};
809+
810+
auto ctx = WalkCtx{&nodes, &seen, &id};
794811

795812
for (auto node : ex_lazy_html.resource->nodes) {
796813
if (matches_id(node, &id)) {
797-
nodes.push_back(node);
814+
if (seen.insert(node).second) {
815+
nodes.push_back(node);
816+
}
798817
}
799818

800819
lxb_dom_node_simple_walk(
801820
node,
802821
[](lxb_dom_node_t *node, void *ctx) -> lexbor_action_t {
803-
auto [nodes_ptr, id_ptr] = *static_cast<
804-
std::tuple<std::vector<lxb_dom_node_t *> *, ErlNifBinary *> *>(
805-
ctx);
806-
if (matches_id(node, id_ptr)) {
807-
nodes_ptr->push_back(node);
822+
auto walk_ctx = static_cast<WalkCtx *>(ctx);
823+
if (matches_id(node, walk_ctx->id)) {
824+
if (walk_ctx->seen->insert(node).second) {
825+
walk_ctx->nodes->push_back(node);
826+
}
808827
}
809828

810829
return LEXBOR_ACTION_OK;
@@ -843,9 +862,7 @@ ExLazyHTML parent_node(ErlNifEnv *env, ExLazyHTML ex_lazy_html) {
843862
auto parent = lxb_dom_node_parent(node);
844863
if (parent != NULL && parent->type == LXB_DOM_NODE_TYPE_ELEMENT &&
845864
(is_document || !lxb_html_tree_node_is(parent, LXB_TAG_HTML))) {
846-
auto inserted_node = inserted_nodes.find(parent);
847-
if (inserted_node == inserted_nodes.end()) {
848-
inserted_nodes.insert(parent);
865+
if (inserted_nodes.insert(parent).second) {
849866
nodes.push_back(parent);
850867
}
851868
}

test/lazy_html_test.exs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,36 @@ defmodule LazyHTMLTest do
248248
LazyHTML.query(lazy_html, "hover:")
249249
end
250250
end
251+
252+
test "does not include duplicated elements in the result set" do
253+
fragment =
254+
LazyHTML.from_fragment(~S"""
255+
<div>
256+
<div>1</div>
257+
<div>2</div>
258+
</div>
259+
""")
260+
261+
result = fragment |> LazyHTML.query("div") |> LazyHTML.query("div")
262+
263+
# If nodes were not deduplicated, the second query would inflate
264+
# the result to 5 nodes. We expect only 3 unique nodes.
265+
266+
assert inspect(result) == """
267+
#LazyHTML<
268+
3 nodes (from selector)
269+
#1
270+
<div>
271+
<div>1</div>
272+
<div>2</div>
273+
</div>
274+
#2
275+
<div>1</div>
276+
#3
277+
<div>2</div>
278+
>\
279+
"""
280+
end
251281
end
252282

253283
describe "parent_node/1" do
@@ -379,6 +409,36 @@ defmodule LazyHTMLTest do
379409
result = LazyHTML.query_by_id(lazy_html, "root")
380410
assert Enum.count(result) == 1
381411
end
412+
413+
test "does not include duplicated elements in the result set" do
414+
# A proper HTML document should not have duplicated ids, but it
415+
# can be the case.
416+
fragment =
417+
LazyHTML.from_fragment(~S"""
418+
<div id="1">
419+
<div id="1">1</div>
420+
<div>2</div>
421+
</div>
422+
""")
423+
424+
result = fragment |> LazyHTML.query_by_id("1") |> LazyHTML.query_by_id("1")
425+
426+
# If nodes were not deduplicated, the second query would inflate
427+
# the result to 3 nodes. We expect only 2 unique nodes.
428+
429+
assert inspect(result) == """
430+
#LazyHTML<
431+
2 nodes (from selector)
432+
#1
433+
<div id=\"1\">
434+
<div id=\"1\">1</div>
435+
<div>2</div>
436+
</div>
437+
#2
438+
<div id=\"1\">1</div>
439+
>\
440+
"""
441+
end
382442
end
383443

384444
describe "text/1" do

0 commit comments

Comments
 (0)