/src/serenity/Userland/Libraries/LibWeb/DOM/ParentNode.cpp
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) 2020, Luke Wilde <lukew@serenityos.org> |
3 | | * Copyright (c) 2022, Andreas Kling <kling@serenityos.org> |
4 | | * Copyright (c) 2023, Shannon Booth <shannon@serenityos.org> |
5 | | * |
6 | | * SPDX-License-Identifier: BSD-2-Clause |
7 | | */ |
8 | | |
9 | | #include <LibWeb/CSS/Parser/Parser.h> |
10 | | #include <LibWeb/CSS/SelectorEngine.h> |
11 | | #include <LibWeb/DOM/Document.h> |
12 | | #include <LibWeb/DOM/HTMLCollection.h> |
13 | | #include <LibWeb/DOM/NodeOperations.h> |
14 | | #include <LibWeb/DOM/ParentNode.h> |
15 | | #include <LibWeb/DOM/ShadowRoot.h> |
16 | | #include <LibWeb/DOM/StaticNodeList.h> |
17 | | #include <LibWeb/Dump.h> |
18 | | #include <LibWeb/Infra/CharacterTypes.h> |
19 | | #include <LibWeb/Infra/Strings.h> |
20 | | #include <LibWeb/Namespace.h> |
21 | | |
22 | | namespace Web::DOM { |
23 | | |
24 | | JS_DEFINE_ALLOCATOR(ParentNode); |
25 | | |
26 | | // https://dom.spec.whatwg.org/#dom-parentnode-queryselector |
27 | | WebIDL::ExceptionOr<JS::GCPtr<Element>> ParentNode::query_selector(StringView selector_text) |
28 | 0 | { |
29 | | // The querySelector(selectors) method steps are to return the first result of running scope-match a selectors string selectors against this, |
30 | | // if the result is not an empty list; otherwise null. |
31 | | |
32 | | // https://dom.spec.whatwg.org/#scope-match-a-selectors-string |
33 | | // To scope-match a selectors string selectors against a node, run these steps: |
34 | | // 1. Let s be the result of parse a selector selectors. |
35 | 0 | auto maybe_selectors = parse_selector(CSS::Parser::ParsingContext(*this), selector_text); |
36 | | |
37 | | // 2. If s is failure, then throw a "SyntaxError" DOMException. |
38 | 0 | if (!maybe_selectors.has_value()) |
39 | 0 | return WebIDL::SyntaxError::create(realm(), "Failed to parse selector"_string); |
40 | | |
41 | 0 | auto selectors = maybe_selectors.value(); |
42 | | |
43 | | // 3. Return the result of match a selector against a tree with s and node’s root using scoping root node. |
44 | 0 | JS::GCPtr<Element> result; |
45 | | // FIXME: This should be shadow-including. https://drafts.csswg.org/selectors-4/#match-a-selector-against-a-tree |
46 | 0 | for_each_in_subtree_of_type<Element>([&](auto& element) { |
47 | 0 | for (auto& selector : selectors) { |
48 | 0 | if (SelectorEngine::matches(selector, {}, element, nullptr, {}, this)) { |
49 | 0 | result = &element; |
50 | 0 | return TraversalDecision::Break; |
51 | 0 | } |
52 | 0 | } |
53 | 0 | return TraversalDecision::Continue; |
54 | 0 | }); |
55 | |
|
56 | 0 | return result; |
57 | 0 | } |
58 | | |
59 | | // https://dom.spec.whatwg.org/#dom-parentnode-queryselectorall |
60 | | WebIDL::ExceptionOr<JS::NonnullGCPtr<NodeList>> ParentNode::query_selector_all(StringView selector_text) |
61 | 0 | { |
62 | | // The querySelectorAll(selectors) method steps are to return the static result of running scope-match a selectors string selectors against this. |
63 | | |
64 | | // https://dom.spec.whatwg.org/#scope-match-a-selectors-string |
65 | | // To scope-match a selectors string selectors against a node, run these steps: |
66 | | // 1. Let s be the result of parse a selector selectors. |
67 | 0 | auto maybe_selectors = parse_selector(CSS::Parser::ParsingContext(*this), selector_text); |
68 | | |
69 | | // 2. If s is failure, then throw a "SyntaxError" DOMException. |
70 | 0 | if (!maybe_selectors.has_value()) |
71 | 0 | return WebIDL::SyntaxError::create(realm(), "Failed to parse selector"_string); |
72 | | |
73 | 0 | auto selectors = maybe_selectors.value(); |
74 | | |
75 | | // 3. Return the result of match a selector against a tree with s and node’s root using scoping root node. |
76 | 0 | Vector<JS::Handle<Node>> elements; |
77 | | // FIXME: This should be shadow-including. https://drafts.csswg.org/selectors-4/#match-a-selector-against-a-tree |
78 | 0 | for_each_in_subtree_of_type<Element>([&](auto& element) { |
79 | 0 | for (auto& selector : selectors) { |
80 | 0 | if (SelectorEngine::matches(selector, {}, element, nullptr, {}, this)) { |
81 | 0 | elements.append(&element); |
82 | 0 | } |
83 | 0 | } |
84 | 0 | return TraversalDecision::Continue; |
85 | 0 | }); |
86 | |
|
87 | 0 | return StaticNodeList::create(realm(), move(elements)); |
88 | 0 | } |
89 | | |
90 | | JS::GCPtr<Element> ParentNode::first_element_child() |
91 | 0 | { |
92 | 0 | return first_child_of_type<Element>(); |
93 | 0 | } |
94 | | |
95 | | JS::GCPtr<Element> ParentNode::last_element_child() |
96 | 0 | { |
97 | 0 | return last_child_of_type<Element>(); |
98 | 0 | } |
99 | | |
100 | | // https://dom.spec.whatwg.org/#dom-parentnode-childelementcount |
101 | | u32 ParentNode::child_element_count() const |
102 | 0 | { |
103 | 0 | u32 count = 0; |
104 | 0 | for (auto* child = first_child(); child; child = child->next_sibling()) { |
105 | 0 | if (is<Element>(child)) |
106 | 0 | ++count; |
107 | 0 | } |
108 | 0 | return count; |
109 | 0 | } |
110 | | |
111 | | void ParentNode::visit_edges(Cell::Visitor& visitor) |
112 | 0 | { |
113 | 0 | Base::visit_edges(visitor); |
114 | 0 | visitor.visit(m_children); |
115 | 0 | } |
116 | | |
117 | | // https://dom.spec.whatwg.org/#dom-parentnode-children |
118 | | JS::NonnullGCPtr<HTMLCollection> ParentNode::children() |
119 | 0 | { |
120 | | // The children getter steps are to return an HTMLCollection collection rooted at this matching only element children. |
121 | 0 | if (!m_children) { |
122 | 0 | m_children = HTMLCollection::create(*this, HTMLCollection::Scope::Children, [](Element const&) { |
123 | 0 | return true; |
124 | 0 | }); |
125 | 0 | } |
126 | 0 | return *m_children; |
127 | 0 | } |
128 | | |
129 | | // https://dom.spec.whatwg.org/#concept-getelementsbytagname |
130 | | // NOTE: This method is only exposed on Document and Element, but is in ParentNode to prevent code duplication. |
131 | | JS::NonnullGCPtr<HTMLCollection> ParentNode::get_elements_by_tag_name(FlyString const& qualified_name) |
132 | 0 | { |
133 | | // 1. If qualifiedName is "*" (U+002A), return a HTMLCollection rooted at root, whose filter matches only descendant elements. |
134 | 0 | if (qualified_name == "*") { |
135 | 0 | return HTMLCollection::create(*this, HTMLCollection::Scope::Descendants, [](Element const&) { |
136 | 0 | return true; |
137 | 0 | }); |
138 | 0 | } |
139 | | |
140 | | // 2. Otherwise, if root’s node document is an HTML document, return a HTMLCollection rooted at root, whose filter matches the following descendant elements: |
141 | 0 | if (root().document().document_type() == Document::Type::HTML) { |
142 | 0 | FlyString qualified_name_in_ascii_lowercase = qualified_name.to_ascii_lowercase(); |
143 | 0 | return HTMLCollection::create(*this, HTMLCollection::Scope::Descendants, [qualified_name, qualified_name_in_ascii_lowercase](Element const& element) { |
144 | | // - Whose namespace is the HTML namespace and whose qualified name is qualifiedName, in ASCII lowercase. |
145 | 0 | if (element.namespace_uri() == Namespace::HTML) |
146 | 0 | return element.qualified_name() == qualified_name_in_ascii_lowercase; |
147 | | |
148 | | // - Whose namespace is not the HTML namespace and whose qualified name is qualifiedName. |
149 | 0 | return element.qualified_name() == qualified_name; |
150 | 0 | }); |
151 | 0 | } |
152 | | |
153 | | // 3. Otherwise, return a HTMLCollection rooted at root, whose filter matches descendant elements whose qualified name is qualifiedName. |
154 | 0 | return HTMLCollection::create(*this, HTMLCollection::Scope::Descendants, [qualified_name](Element const& element) { |
155 | 0 | return element.qualified_name() == qualified_name; |
156 | 0 | }); |
157 | 0 | } |
158 | | |
159 | | // https://dom.spec.whatwg.org/#concept-getelementsbytagnamens |
160 | | // NOTE: This method is only exposed on Document and Element, but is in ParentNode to prevent code duplication. |
161 | | JS::NonnullGCPtr<HTMLCollection> ParentNode::get_elements_by_tag_name_ns(Optional<FlyString> namespace_, FlyString const& local_name) |
162 | 0 | { |
163 | | // 1. If namespace is the empty string, set it to null. |
164 | 0 | if (namespace_ == FlyString {}) |
165 | 0 | namespace_ = OptionalNone {}; |
166 | | |
167 | | // 2. If both namespace and localName are "*" (U+002A), return a HTMLCollection rooted at root, whose filter matches descendant elements. |
168 | 0 | if (namespace_ == "*" && local_name == "*") { |
169 | 0 | return HTMLCollection::create(*this, HTMLCollection::Scope::Descendants, [](Element const&) { |
170 | 0 | return true; |
171 | 0 | }); |
172 | 0 | } |
173 | | |
174 | | // 3. Otherwise, if namespace is "*" (U+002A), return a HTMLCollection rooted at root, whose filter matches descendant elements whose local name is localName. |
175 | 0 | if (namespace_ == "*") { |
176 | 0 | return HTMLCollection::create(*this, HTMLCollection::Scope::Descendants, [local_name](Element const& element) { |
177 | 0 | return element.local_name() == local_name; |
178 | 0 | }); |
179 | 0 | } |
180 | | |
181 | | // 4. Otherwise, if localName is "*" (U+002A), return a HTMLCollection rooted at root, whose filter matches descendant elements whose namespace is namespace. |
182 | 0 | if (local_name == "*") { |
183 | 0 | return HTMLCollection::create(*this, HTMLCollection::Scope::Descendants, [namespace_](Element const& element) { |
184 | 0 | return element.namespace_uri() == namespace_; |
185 | 0 | }); |
186 | 0 | } |
187 | | |
188 | | // 5. Otherwise, return a HTMLCollection rooted at root, whose filter matches descendant elements whose namespace is namespace and local name is localName. |
189 | 0 | return HTMLCollection::create(*this, HTMLCollection::Scope::Descendants, [namespace_, local_name](Element const& element) { |
190 | 0 | return element.namespace_uri() == namespace_ && element.local_name() == local_name; |
191 | 0 | }); |
192 | 0 | } |
193 | | |
194 | | // https://dom.spec.whatwg.org/#dom-parentnode-prepend |
195 | | WebIDL::ExceptionOr<void> ParentNode::prepend(Vector<Variant<JS::Handle<Node>, String>> const& nodes) |
196 | 0 | { |
197 | | // 1. Let node be the result of converting nodes into a node given nodes and this’s node document. |
198 | 0 | auto node = TRY(convert_nodes_to_single_node(nodes, document())); |
199 | | |
200 | | // 2. Pre-insert node into this before this’s first child. |
201 | 0 | (void)TRY(pre_insert(node, first_child())); |
202 | |
|
203 | 0 | return {}; |
204 | 0 | } |
205 | | |
206 | | WebIDL::ExceptionOr<void> ParentNode::append(Vector<Variant<JS::Handle<Node>, String>> const& nodes) |
207 | 0 | { |
208 | | // 1. Let node be the result of converting nodes into a node given nodes and this’s node document. |
209 | 0 | auto node = TRY(convert_nodes_to_single_node(nodes, document())); |
210 | | |
211 | | // 2. Append node to this. |
212 | 0 | (void)TRY(append_child(node)); |
213 | |
|
214 | 0 | return {}; |
215 | 0 | } |
216 | | |
217 | | WebIDL::ExceptionOr<void> ParentNode::replace_children(Vector<Variant<JS::Handle<Node>, String>> const& nodes) |
218 | 0 | { |
219 | | // 1. Let node be the result of converting nodes into a node given nodes and this’s node document. |
220 | 0 | auto node = TRY(convert_nodes_to_single_node(nodes, document())); |
221 | | |
222 | | // 2. Ensure pre-insertion validity of node into this before null. |
223 | 0 | TRY(ensure_pre_insertion_validity(node, nullptr)); |
224 | | |
225 | | // 3. Replace all with node within this. |
226 | 0 | replace_all(*node); |
227 | 0 | return {}; |
228 | 0 | } |
229 | | |
230 | | // https://dom.spec.whatwg.org/#dom-document-getelementsbyclassname |
231 | | JS::NonnullGCPtr<HTMLCollection> ParentNode::get_elements_by_class_name(StringView class_names) |
232 | 0 | { |
233 | 0 | Vector<FlyString> list_of_class_names; |
234 | 0 | for (auto& name : class_names.split_view_if(Infra::is_ascii_whitespace)) { |
235 | 0 | list_of_class_names.append(FlyString::from_utf8(name).release_value_but_fixme_should_propagate_errors()); |
236 | 0 | } |
237 | 0 | return HTMLCollection::create(*this, HTMLCollection::Scope::Descendants, [list_of_class_names = move(list_of_class_names), quirks_mode = document().in_quirks_mode()](Element const& element) { |
238 | 0 | for (auto& name : list_of_class_names) { |
239 | 0 | if (!element.has_class(name, quirks_mode ? CaseSensitivity::CaseInsensitive : CaseSensitivity::CaseSensitive)) |
240 | 0 | return false; |
241 | 0 | } |
242 | 0 | return !list_of_class_names.is_empty(); |
243 | 0 | }); |
244 | 0 | } |
245 | | |
246 | | } |