Serenity Operating System
1/*
2 * Copyright (c) 2018-2022, Andreas Kling <kling@serenityos.org>
3 * Copyright (c) 2021-2022, Linus Groh <linusg@serenityos.org>
4 * Copyright (c) 2021, Luke Wilde <lukew@serenityos.org>
5 *
6 * SPDX-License-Identifier: BSD-2-Clause
7 */
8
9#include <AK/HashTable.h>
10#include <AK/IDAllocator.h>
11#include <AK/StringBuilder.h>
12#include <LibJS/Heap/DeferGC.h>
13#include <LibJS/Runtime/FunctionObject.h>
14#include <LibRegex/Regex.h>
15#include <LibWeb/Bindings/MainThreadVM.h>
16#include <LibWeb/Bindings/NodePrototype.h>
17#include <LibWeb/DOM/Comment.h>
18#include <LibWeb/DOM/DocumentType.h>
19#include <LibWeb/DOM/Element.h>
20#include <LibWeb/DOM/ElementFactory.h>
21#include <LibWeb/DOM/Event.h>
22#include <LibWeb/DOM/EventDispatcher.h>
23#include <LibWeb/DOM/IDLEventListener.h>
24#include <LibWeb/DOM/LiveNodeList.h>
25#include <LibWeb/DOM/MutationType.h>
26#include <LibWeb/DOM/Node.h>
27#include <LibWeb/DOM/NodeIterator.h>
28#include <LibWeb/DOM/ProcessingInstruction.h>
29#include <LibWeb/DOM/Range.h>
30#include <LibWeb/DOM/ShadowRoot.h>
31#include <LibWeb/DOM/StaticNodeList.h>
32#include <LibWeb/HTML/BrowsingContextContainer.h>
33#include <LibWeb/HTML/HTMLAnchorElement.h>
34#include <LibWeb/HTML/HTMLStyleElement.h>
35#include <LibWeb/HTML/Origin.h>
36#include <LibWeb/HTML/Parser/HTMLParser.h>
37#include <LibWeb/Infra/CharacterTypes.h>
38#include <LibWeb/Layout/Node.h>
39#include <LibWeb/Layout/TextNode.h>
40#include <LibWeb/Layout/Viewport.h>
41
42namespace Web::DOM {
43
44static IDAllocator s_node_id_allocator;
45static HashMap<i32, Node*> s_node_directory;
46
47static i32 allocate_node_id(Node* node)
48{
49 i32 id = s_node_id_allocator.allocate();
50 s_node_directory.set(id, node);
51 return id;
52}
53
54static void deallocate_node_id(i32 node_id)
55{
56 if (!s_node_directory.remove(node_id))
57 VERIFY_NOT_REACHED();
58 s_node_id_allocator.deallocate(node_id);
59}
60
61Node* Node::from_id(i32 node_id)
62{
63 return s_node_directory.get(node_id).value_or(nullptr);
64}
65
66Node::Node(JS::Realm& realm, Document& document, NodeType type)
67 : EventTarget(realm)
68 , m_document(&document)
69 , m_type(type)
70 , m_id(allocate_node_id(this))
71{
72}
73
74Node::Node(Document& document, NodeType type)
75 : Node(document.realm(), document, type)
76{
77}
78
79Node::~Node() = default;
80
81void Node::finalize()
82{
83 Base::finalize();
84
85 if (layout_node() && layout_node()->parent())
86 layout_node()->parent()->remove_child(*layout_node());
87
88 deallocate_node_id(m_id);
89}
90
91void Node::visit_edges(Cell::Visitor& visitor)
92{
93 Base::visit_edges(visitor);
94 visitor.visit(m_document.ptr());
95 visitor.visit(m_parent.ptr());
96 visitor.visit(m_first_child.ptr());
97 visitor.visit(m_last_child.ptr());
98 visitor.visit(m_next_sibling.ptr());
99 visitor.visit(m_previous_sibling.ptr());
100 visitor.visit(m_child_nodes);
101
102 visitor.visit(m_layout_node);
103
104 for (auto& registered_observer : m_registered_observer_list)
105 visitor.visit(registered_observer);
106}
107
108// https://dom.spec.whatwg.org/#dom-node-baseuri
109DeprecatedString Node::base_uri() const
110{
111 // Return this’s node document’s document base URL, serialized.
112 return document().base_url().to_deprecated_string();
113}
114
115const HTML::HTMLAnchorElement* Node::enclosing_link_element() const
116{
117 for (auto* node = this; node; node = node->parent()) {
118 if (!is<HTML::HTMLAnchorElement>(*node))
119 continue;
120 auto const& anchor_element = static_cast<HTML::HTMLAnchorElement const&>(*node);
121 if (anchor_element.has_attribute(HTML::AttributeNames::href))
122 return &anchor_element;
123 }
124 return nullptr;
125}
126
127const HTML::HTMLElement* Node::enclosing_html_element() const
128{
129 return first_ancestor_of_type<HTML::HTMLElement>();
130}
131
132const HTML::HTMLElement* Node::enclosing_html_element_with_attribute(DeprecatedFlyString const& attribute) const
133{
134 for (auto* node = this; node; node = node->parent()) {
135 if (is<HTML::HTMLElement>(*node) && verify_cast<HTML::HTMLElement>(*node).has_attribute(attribute))
136 return verify_cast<HTML::HTMLElement>(node);
137 }
138 return nullptr;
139}
140
141// https://dom.spec.whatwg.org/#concept-descendant-text-content
142DeprecatedString Node::descendant_text_content() const
143{
144 StringBuilder builder;
145 for_each_in_subtree_of_type<Text>([&](auto& text_node) {
146 builder.append(text_node.data());
147 return IterationDecision::Continue;
148 });
149 return builder.to_deprecated_string();
150}
151
152// https://dom.spec.whatwg.org/#dom-node-textcontent
153DeprecatedString Node::text_content() const
154{
155 // The textContent getter steps are to return the following, switching on the interface this implements:
156
157 // If DocumentFragment or Element, return the descendant text content of this.
158 if (is<DocumentFragment>(this) || is<Element>(this))
159 return descendant_text_content();
160
161 // If CharacterData, return this’s data.
162 if (is<CharacterData>(this))
163 return static_cast<CharacterData const&>(*this).data();
164
165 // If Attr node, return this's value.
166 if (is<Attr>(*this))
167 return static_cast<Attr const&>(*this).value();
168
169 // Otherwise, return null
170 return {};
171}
172
173// https://dom.spec.whatwg.org/#ref-for-dom-node-textcontent%E2%91%A0
174void Node::set_text_content(DeprecatedString const& content)
175{
176 // The textContent setter steps are to, if the given value is null, act as if it was the empty string instead,
177 // and then do as described below, switching on the interface this implements:
178
179 // If DocumentFragment or Element, string replace all with the given value within this.
180 if (is<DocumentFragment>(this) || is<Element>(this)) {
181 string_replace_all(content);
182 }
183
184 // If CharacterData, replace data with node this, offset 0, count this’s length, and data the given value.
185 else if (is<CharacterData>(this)) {
186
187 auto* character_data_node = verify_cast<CharacterData>(this);
188 character_data_node->set_data(content);
189
190 // FIXME: CharacterData::set_data is not spec compliant. Make this match the spec when set_data becomes spec compliant.
191 // Do note that this will make this function able to throw an exception.
192 }
193
194 // If Attr, set an existing attribute value with this and the given value.
195 if (is<Attr>(*this)) {
196 static_cast<Attr&>(*this).set_value(content);
197 }
198
199 // Otherwise, do nothing.
200
201 set_needs_style_update(true);
202}
203
204// https://dom.spec.whatwg.org/#dom-node-nodevalue
205DeprecatedString Node::node_value() const
206{
207 // The nodeValue getter steps are to return the following, switching on the interface this implements:
208
209 // If Attr, return this’s value.
210 if (is<Attr>(this)) {
211 return verify_cast<Attr>(this)->value();
212 }
213
214 // If CharacterData, return this’s data.
215 if (is<CharacterData>(this)) {
216 return verify_cast<CharacterData>(this)->data();
217 }
218
219 // Otherwise, return null.
220 return {};
221}
222
223// https://dom.spec.whatwg.org/#ref-for-dom-node-nodevalue%E2%91%A0
224void Node::set_node_value(DeprecatedString const& value)
225{
226 // The nodeValue setter steps are to, if the given value is null, act as if it was the empty string instead,
227 // and then do as described below, switching on the interface this implements:
228
229 // If Attr, set an existing attribute value with this and the given value.
230 if (is<Attr>(this)) {
231 verify_cast<Attr>(this)->set_value(value);
232 } else if (is<CharacterData>(this)) {
233 // If CharacterData, replace data with node this, offset 0, count this’s length, and data the given value.
234 verify_cast<CharacterData>(this)->set_data(value);
235 }
236
237 // Otherwise, do nothing.
238}
239
240void Node::invalidate_style()
241{
242 if (is_document()) {
243 auto& document = static_cast<DOM::Document&>(*this);
244 document.set_needs_full_style_update(true);
245 document.schedule_style_update();
246 return;
247 }
248
249 for_each_in_inclusive_subtree([&](Node& node) {
250 node.m_needs_style_update = true;
251 if (node.has_children())
252 node.m_child_needs_style_update = true;
253 if (auto* shadow_root = node.is_element() ? static_cast<DOM::Element&>(node).shadow_root_internal() : nullptr) {
254 node.m_child_needs_style_update = true;
255 shadow_root->m_needs_style_update = true;
256 if (shadow_root->has_children())
257 shadow_root->m_child_needs_style_update = true;
258 }
259 return IterationDecision::Continue;
260 });
261 for (auto* ancestor = parent_or_shadow_host(); ancestor; ancestor = ancestor->parent_or_shadow_host())
262 ancestor->m_child_needs_style_update = true;
263 document().schedule_style_update();
264}
265
266DeprecatedString Node::child_text_content() const
267{
268 if (!is<ParentNode>(*this))
269 return DeprecatedString::empty();
270
271 StringBuilder builder;
272 verify_cast<ParentNode>(*this).for_each_child([&](auto& child) {
273 if (is<Text>(child))
274 builder.append(verify_cast<Text>(child).text_content());
275 });
276 return builder.to_deprecated_string();
277}
278
279// https://dom.spec.whatwg.org/#concept-tree-root
280Node& Node::root()
281{
282 // The root of an object is itself, if its parent is null, or else it is the root of its parent.
283 // The root of a tree is any object participating in that tree whose parent is null.
284 Node* root = this;
285 while (root->parent())
286 root = root->parent();
287 return *root;
288}
289
290// https://dom.spec.whatwg.org/#concept-shadow-including-root
291Node& Node::shadow_including_root()
292{
293 // The shadow-including root of an object is its root’s host’s shadow-including root,
294 // if the object’s root is a shadow root; otherwise its root.
295 auto& node_root = root();
296 if (is<ShadowRoot>(node_root))
297 return static_cast<ShadowRoot&>(node_root).host()->shadow_including_root();
298 return node_root;
299}
300
301// https://dom.spec.whatwg.org/#connected
302bool Node::is_connected() const
303{
304 // An element is connected if its shadow-including root is a document.
305 return shadow_including_root().is_document();
306}
307
308Element* Node::parent_element()
309{
310 if (!parent() || !is<Element>(parent()))
311 return nullptr;
312 return verify_cast<Element>(parent());
313}
314
315Element const* Node::parent_element() const
316{
317 if (!parent() || !is<Element>(parent()))
318 return nullptr;
319 return verify_cast<Element>(parent());
320}
321
322// https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity
323WebIDL::ExceptionOr<void> Node::ensure_pre_insertion_validity(JS::NonnullGCPtr<Node> node, JS::GCPtr<Node> child) const
324{
325 // 1. If parent is not a Document, DocumentFragment, or Element node, then throw a "HierarchyRequestError" DOMException.
326 if (!is<Document>(this) && !is<DocumentFragment>(this) && !is<Element>(this))
327 return WebIDL::HierarchyRequestError::create(realm(), "Can only insert into a document, document fragment or element");
328
329 // 2. If node is a host-including inclusive ancestor of parent, then throw a "HierarchyRequestError" DOMException.
330 if (node->is_host_including_inclusive_ancestor_of(*this))
331 return WebIDL::HierarchyRequestError::create(realm(), "New node is an ancestor of this node");
332
333 // 3. If child is non-null and its parent is not parent, then throw a "NotFoundError" DOMException.
334 if (child && child->parent() != this)
335 return WebIDL::NotFoundError::create(realm(), "This node is not the parent of the given child");
336
337 // FIXME: All the following "Invalid node type for insertion" messages could be more descriptive.
338 // 4. If node is not a DocumentFragment, DocumentType, Element, or CharacterData node, then throw a "HierarchyRequestError" DOMException.
339 if (!is<DocumentFragment>(*node) && !is<DocumentType>(*node) && !is<Element>(*node) && !is<Text>(*node) && !is<Comment>(*node) && !is<ProcessingInstruction>(*node))
340 return WebIDL::HierarchyRequestError::create(realm(), "Invalid node type for insertion");
341
342 // 5. If either node is a Text node and parent is a document, or node is a doctype and parent is not a document, then throw a "HierarchyRequestError" DOMException.
343 if ((is<Text>(*node) && is<Document>(this)) || (is<DocumentType>(*node) && !is<Document>(this)))
344 return WebIDL::HierarchyRequestError::create(realm(), "Invalid node type for insertion");
345
346 // 6. If parent is a document, and any of the statements below, switched on the interface node implements, are true, then throw a "HierarchyRequestError" DOMException.
347 if (is<Document>(this)) {
348 // DocumentFragment
349 if (is<DocumentFragment>(*node)) {
350 // If node has more than one element child or has a Text node child.
351 // Otherwise, if node has one element child and either parent has an element child, child is a doctype, or child is non-null and a doctype is following child.
352 auto node_element_child_count = verify_cast<DocumentFragment>(*node).child_element_count();
353 if ((node_element_child_count > 1 || node->has_child_of_type<Text>())
354 || (node_element_child_count == 1 && (has_child_of_type<Element>() || is<DocumentType>(child.ptr()) || (child && child->has_following_node_of_type_in_tree_order<DocumentType>())))) {
355 return WebIDL::HierarchyRequestError::create(realm(), "Invalid node type for insertion");
356 }
357 } else if (is<Element>(*node)) {
358 // Element
359 // If parent has an element child, child is a doctype, or child is non-null and a doctype is following child.
360 if (has_child_of_type<Element>() || is<DocumentType>(child.ptr()) || (child && child->has_following_node_of_type_in_tree_order<DocumentType>()))
361 return WebIDL::HierarchyRequestError::create(realm(), "Invalid node type for insertion");
362 } else if (is<DocumentType>(*node)) {
363 // DocumentType
364 // parent has a doctype child, child is non-null and an element is preceding child, or child is null and parent has an element child.
365 if (has_child_of_type<DocumentType>() || (child && child->has_preceding_node_of_type_in_tree_order<Element>()) || (!child && has_child_of_type<Element>()))
366 return WebIDL::HierarchyRequestError::create(realm(), "Invalid node type for insertion");
367 }
368 }
369
370 return {};
371}
372
373// https://dom.spec.whatwg.org/#concept-node-insert
374void Node::insert_before(JS::NonnullGCPtr<Node> node, JS::GCPtr<Node> child, bool suppress_observers)
375{
376 // 1. Let nodes be node’s children, if node is a DocumentFragment node; otherwise « node ».
377 Vector<JS::Handle<Node>> nodes;
378 if (is<DocumentFragment>(*node))
379 nodes = node->children_as_vector();
380 else
381 nodes.append(JS::make_handle(*node));
382
383 // 2. Let count be nodes’s size.
384 auto count = nodes.size();
385
386 // 3. If count is 0, then return.
387 if (count == 0)
388 return;
389
390 // 4. If node is a DocumentFragment node, then:
391 if (is<DocumentFragment>(*node)) {
392 // 1. Remove its children with the suppress observers flag set.
393 node->remove_all_children(true);
394
395 // 2. Queue a tree mutation record for node with « », nodes, null, and null.
396 // NOTE: This step intentionally does not pay attention to the suppress observers flag.
397 auto added_node_list = StaticNodeList::create(realm(), {}).release_value_but_fixme_should_propagate_errors();
398 auto removed_node_list = StaticNodeList::create(realm(), nodes).release_value_but_fixme_should_propagate_errors();
399 node->queue_tree_mutation_record(added_node_list, removed_node_list, nullptr, nullptr);
400 }
401
402 // 5. If child is non-null, then:
403 if (child) {
404 // 1. For each live range whose start node is parent and start offset is greater than child’s index, increase its start offset by count.
405 for (auto& range : Range::live_ranges()) {
406 if (range->start_container() == this && range->start_offset() > child->index())
407 MUST(range->set_start(*range->start_container(), range->start_offset() + count));
408 }
409
410 // 2. For each live range whose end node is parent and end offset is greater than child’s index, increase its end offset by count.
411 for (auto& range : Range::live_ranges()) {
412 if (range->end_container() == this && range->end_offset() > child->index())
413 MUST(range->set_end(*range->end_container(), range->end_offset() + count));
414 }
415 }
416
417 // 6. Let previousSibling be child’s previous sibling or parent’s last child if child is null.
418 JS::GCPtr<Node> previous_sibling;
419 if (child)
420 previous_sibling = child->previous_sibling();
421 else
422 previous_sibling = last_child();
423
424 // 7. For each node in nodes, in tree order:
425 // FIXME: In tree order
426 for (auto& node_to_insert : nodes) {
427 // 1. Adopt node into parent’s node document.
428 document().adopt_node(*node_to_insert);
429
430 // 2. If child is null, then append node to parent’s children.
431 if (!child)
432 append_child_impl(*node_to_insert);
433 // 3. Otherwise, insert node into parent’s children before child’s index.
434 else
435 insert_before_impl(*node_to_insert, child);
436
437 // FIXME: 4. If parent is a shadow host and node is a slottable, then assign a slot for node.
438 // FIXME: 5. If parent’s root is a shadow root, and parent is a slot whose assigned nodes is the empty list, then run signal a slot change for parent.
439 // FIXME: 6. Run assign slottables for a tree with node’s root.
440
441 // FIXME: This should be shadow-including.
442 // 7. For each shadow-including inclusive descendant inclusiveDescendant of node, in shadow-including tree order:
443 node_to_insert->for_each_in_inclusive_subtree([&](Node& inclusive_descendant) {
444 // 1. Run the insertion steps with inclusiveDescendant.
445 inclusive_descendant.inserted();
446
447 // 2. If inclusiveDescendant is connected, then:
448 if (inclusive_descendant.is_connected()) {
449 // FIXME: 1. If inclusiveDescendant is custom, then enqueue a custom element callback reaction with inclusiveDescendant, callback name "connectedCallback", and an empty argument list.
450
451 // FIXME: 2. Otherwise, try to upgrade inclusiveDescendant.
452 // NOTE: If this successfully upgrades inclusiveDescendant, its connectedCallback will be enqueued automatically during the upgrade an element algorithm.
453 }
454
455 return IterationDecision::Continue;
456 });
457 }
458
459 // 8. If suppress observers flag is unset, then queue a tree mutation record for parent with nodes, « », previousSibling, and child.
460 if (!suppress_observers) {
461 auto added_node_list = StaticNodeList::create(realm(), move(nodes)).release_value_but_fixme_should_propagate_errors();
462 auto removed_node_list = StaticNodeList::create(realm(), {}).release_value_but_fixme_should_propagate_errors();
463 queue_tree_mutation_record(added_node_list, removed_node_list, previous_sibling.ptr(), child.ptr());
464 }
465
466 // 9. Run the children changed steps for parent.
467 children_changed();
468
469 // FIXME: This will need to become smarter when we implement the :has() selector.
470 invalidate_style();
471}
472
473// https://dom.spec.whatwg.org/#concept-node-pre-insert
474WebIDL::ExceptionOr<JS::NonnullGCPtr<Node>> Node::pre_insert(JS::NonnullGCPtr<Node> node, JS::GCPtr<Node> child)
475{
476 // 1. Ensure pre-insertion validity of node into parent before child.
477 TRY(ensure_pre_insertion_validity(node, child));
478
479 // 2. Let referenceChild be child.
480 auto reference_child = child;
481
482 // 3. If referenceChild is node, then set referenceChild to node’s next sibling.
483 if (reference_child == node)
484 reference_child = node->next_sibling();
485
486 // 4. Insert node into parent before referenceChild.
487 insert_before(node, reference_child);
488
489 // 5. Return node.
490 return node;
491}
492
493// https://dom.spec.whatwg.org/#dom-node-removechild
494WebIDL::ExceptionOr<JS::NonnullGCPtr<Node>> Node::remove_child(JS::NonnullGCPtr<Node> child)
495{
496 // The removeChild(child) method steps are to return the result of pre-removing child from this.
497 return pre_remove(child);
498}
499
500// https://dom.spec.whatwg.org/#concept-node-pre-remove
501WebIDL::ExceptionOr<JS::NonnullGCPtr<Node>> Node::pre_remove(JS::NonnullGCPtr<Node> child)
502{
503 // 1. If child’s parent is not parent, then throw a "NotFoundError" DOMException.
504 if (child->parent() != this)
505 return WebIDL::NotFoundError::create(realm(), "Child does not belong to this node");
506
507 // 2. Remove child.
508 child->remove();
509
510 // 3. Return child.
511 return child;
512}
513
514// https://dom.spec.whatwg.org/#concept-node-append
515WebIDL::ExceptionOr<JS::NonnullGCPtr<Node>> Node::append_child(JS::NonnullGCPtr<Node> node)
516{
517 // To append a node to a parent, pre-insert node into parent before null.
518 return pre_insert(node, nullptr);
519}
520
521// https://dom.spec.whatwg.org/#concept-node-remove
522void Node::remove(bool suppress_observers)
523{
524 // 1. Let parent be node’s parent
525 auto* parent = this->parent();
526
527 // 2. Assert: parent is non-null.
528 VERIFY(parent);
529
530 // 3. Let index be node’s index.
531 auto index = this->index();
532
533 // 4. For each live range whose start node is an inclusive descendant of node, set its start to (parent, index).
534 for (auto& range : Range::live_ranges()) {
535 if (range->start_container()->is_inclusive_descendant_of(*this))
536 MUST(range->set_start(*parent, index));
537 }
538
539 // 5. For each live range whose end node is an inclusive descendant of node, set its end to (parent, index).
540 for (auto& range : Range::live_ranges()) {
541 if (range->end_container()->is_inclusive_descendant_of(*this))
542 MUST(range->set_end(*parent, index));
543 }
544
545 // 6. For each live range whose start node is parent and start offset is greater than index, decrease its start offset by 1.
546 for (auto& range : Range::live_ranges()) {
547 if (range->start_container() == parent && range->start_offset() > index)
548 MUST(range->set_start(*range->start_container(), range->start_offset() - 1));
549 }
550
551 // 7. For each live range whose end node is parent and end offset is greater than index, decrease its end offset by 1.
552 for (auto& range : Range::live_ranges()) {
553 if (range->end_container() == parent && range->end_offset() > index)
554 MUST(range->set_end(*range->end_container(), range->end_offset() - 1));
555 }
556
557 // 8. For each NodeIterator object iterator whose root’s node document is node’s node document, run the NodeIterator pre-removing steps given node and iterator.
558 document().for_each_node_iterator([&](NodeIterator& node_iterator) {
559 node_iterator.run_pre_removing_steps(*this);
560 });
561
562 // 9. Let oldPreviousSibling be node’s previous sibling.
563 JS::GCPtr<Node> old_previous_sibling = previous_sibling();
564
565 // 10. Let oldNextSibling be node’s next sibling.
566 JS::GCPtr<Node> old_next_sibling = next_sibling();
567
568 // 11. Remove node from its parent’s children.
569 parent->remove_child_impl(*this);
570
571 // FIXME: 12. If node is assigned, then run assign slottables for node’s assigned slot.
572
573 // FIXME: 13. If parent’s root is a shadow root, and parent is a slot whose assigned nodes is the empty list, then run signal a slot change for parent.
574
575 // FIXME: 14. If node has an inclusive descendant that is a slot, then:
576 // 1. Run assign slottables for a tree with parent’s root.
577 // 2. Run assign slottables for a tree with node.
578
579 // 15. Run the removing steps with node and parent.
580 removed_from(parent);
581
582 // FIXME: 16. Let isParentConnected be parent’s connected. (Currently unused so not included)
583
584 // FIXME: 17. If node is custom and isParentConnected is true, then enqueue a custom element callback reaction with node,
585 // callback name "disconnectedCallback", and an empty argument list.
586 // NOTE: It is intentional for now that custom elements do not get parent passed. This might change in the future if there is a need.
587
588 // FIXME: This should be shadow-including.
589 // 18. For each shadow-including descendant descendant of node, in shadow-including tree order, then:
590 for_each_in_subtree([&](Node& descendant) {
591 // 1. Run the removing steps with descendant
592 descendant.removed_from(nullptr);
593
594 // FIXME: 2. If descendant is custom and isParentConnected is true, then enqueue a custom element callback reaction with descendant,
595 // callback name "disconnectedCallback", and an empty argument list.
596
597 return IterationDecision::Continue;
598 });
599
600 // 19. For each inclusive ancestor inclusiveAncestor of parent, and then for each registered of inclusiveAncestor’s registered observer list,
601 // if registered’s options["subtree"] is true, then append a new transient registered observer
602 // whose observer is registered’s observer, options is registered’s options, and source is registered to node’s registered observer list.
603 for (auto* inclusive_ancestor = parent; inclusive_ancestor; inclusive_ancestor = inclusive_ancestor->parent()) {
604 for (auto& registered : inclusive_ancestor->m_registered_observer_list) {
605 if (registered.options().subtree) {
606 auto transient_observer = TransientRegisteredObserver::create(registered.observer(), registered.options(), registered);
607 m_registered_observer_list.append(move(transient_observer));
608 }
609 }
610 }
611
612 // 20. If suppress observers flag is unset, then queue a tree mutation record for parent with « », « node », oldPreviousSibling, and oldNextSibling.
613 if (!suppress_observers) {
614 Vector<JS::Handle<Node>> removed_nodes;
615 removed_nodes.append(JS::make_handle(*this));
616 auto added_node_list = StaticNodeList::create(realm(), {}).release_value_but_fixme_should_propagate_errors();
617 auto removed_node_list = StaticNodeList::create(realm(), move(removed_nodes)).release_value_but_fixme_should_propagate_errors();
618 parent->queue_tree_mutation_record(added_node_list, removed_node_list, old_previous_sibling.ptr(), old_next_sibling.ptr());
619 }
620
621 // 21. Run the children changed steps for parent.
622 parent->children_changed();
623
624 document().invalidate_layout();
625}
626
627// https://dom.spec.whatwg.org/#concept-node-replace
628WebIDL::ExceptionOr<JS::NonnullGCPtr<Node>> Node::replace_child(JS::NonnullGCPtr<Node> node, JS::NonnullGCPtr<Node> child)
629{
630 // If parent is not a Document, DocumentFragment, or Element node, then throw a "HierarchyRequestError" DOMException.
631 if (!is<Document>(this) && !is<DocumentFragment>(this) && !is<Element>(this))
632 return WebIDL::HierarchyRequestError::create(realm(), "Can only insert into a document, document fragment or element");
633
634 // 2. If node is a host-including inclusive ancestor of parent, then throw a "HierarchyRequestError" DOMException.
635 if (node->is_host_including_inclusive_ancestor_of(*this))
636 return WebIDL::HierarchyRequestError::create(realm(), "New node is an ancestor of this node");
637
638 // 3. If child’s parent is not parent, then throw a "NotFoundError" DOMException.
639 if (child->parent() != this)
640 return WebIDL::NotFoundError::create(realm(), "This node is not the parent of the given child");
641
642 // FIXME: All the following "Invalid node type for insertion" messages could be more descriptive.
643
644 // 4. If node is not a DocumentFragment, DocumentType, Element, or CharacterData node, then throw a "HierarchyRequestError" DOMException.
645 if (!is<DocumentFragment>(*node) && !is<DocumentType>(*node) && !is<Element>(*node) && !is<Text>(*node) && !is<Comment>(*node) && !is<ProcessingInstruction>(*node))
646 return WebIDL::HierarchyRequestError::create(realm(), "Invalid node type for insertion");
647
648 // 5. If either node is a Text node and parent is a document, or node is a doctype and parent is not a document, then throw a "HierarchyRequestError" DOMException.
649 if ((is<Text>(*node) && is<Document>(this)) || (is<DocumentType>(*node) && !is<Document>(this)))
650 return WebIDL::HierarchyRequestError::create(realm(), "Invalid node type for insertion");
651
652 // If parent is a document, and any of the statements below, switched on the interface node implements, are true, then throw a "HierarchyRequestError" DOMException.
653 if (is<Document>(this)) {
654 // DocumentFragment
655 if (is<DocumentFragment>(*node)) {
656 // If node has more than one element child or has a Text node child.
657 // Otherwise, if node has one element child and either parent has an element child that is not child or a doctype is following child.
658 auto node_element_child_count = verify_cast<DocumentFragment>(*node).child_element_count();
659 if ((node_element_child_count > 1 || node->has_child_of_type<Text>())
660 || (node_element_child_count == 1 && (first_child_of_type<Element>() != child || child->has_following_node_of_type_in_tree_order<DocumentType>()))) {
661 return WebIDL::HierarchyRequestError::create(realm(), "Invalid node type for insertion");
662 }
663 } else if (is<Element>(*node)) {
664 // Element
665 // parent has an element child that is not child or a doctype is following child.
666 if (first_child_of_type<Element>() != child || child->has_following_node_of_type_in_tree_order<DocumentType>())
667 return WebIDL::HierarchyRequestError::create(realm(), "Invalid node type for insertion");
668 } else if (is<DocumentType>(*node)) {
669 // DocumentType
670 // parent has a doctype child that is not child, or an element is preceding child.
671 if (first_child_of_type<DocumentType>() != node || child->has_preceding_node_of_type_in_tree_order<Element>())
672 return WebIDL::HierarchyRequestError::create(realm(), "Invalid node type for insertion");
673 }
674 }
675
676 // 7. Let referenceChild be child’s next sibling.
677 JS::GCPtr<Node> reference_child = child->next_sibling();
678
679 // 8. If referenceChild is node, then set referenceChild to node’s next sibling.
680 if (reference_child == node)
681 reference_child = node->next_sibling();
682
683 // 9. Let previousSibling be child’s previous sibling.
684 JS::GCPtr<Node> previous_sibling = child->previous_sibling();
685
686 // 10. Let removedNodes be the empty set.
687 Vector<JS::Handle<Node>> removed_nodes;
688
689 // 11. If child’s parent is non-null, then:
690 // NOTE: The above can only be false if child is node.
691 if (child->parent()) {
692 // 1. Set removedNodes to « child ».
693 removed_nodes.append(JS::make_handle(*child));
694
695 // 2. Remove child with the suppress observers flag set.
696 child->remove(true);
697 }
698
699 // 12. Let nodes be node’s children if node is a DocumentFragment node; otherwise « node ».
700 Vector<JS::Handle<Node>> nodes;
701 if (is<DocumentFragment>(*node))
702 nodes = node->children_as_vector();
703 else
704 nodes.append(JS::make_handle(*node));
705
706 // 13. Insert node into parent before referenceChild with the suppress observers flag set.
707 insert_before(node, reference_child, true);
708
709 // 14. Queue a tree mutation record for parent with nodes, removedNodes, previousSibling, and referenceChild.
710 auto added_node_list = TRY(StaticNodeList::create(realm(), move(nodes)));
711 auto removed_node_list = TRY(StaticNodeList::create(realm(), move(removed_nodes)));
712 queue_tree_mutation_record(added_node_list, removed_node_list, previous_sibling.ptr(), reference_child.ptr());
713
714 // 15. Return child.
715 return child;
716}
717
718// https://dom.spec.whatwg.org/#concept-node-clone
719JS::NonnullGCPtr<Node> Node::clone_node(Document* document, bool clone_children)
720{
721 // 1. If document is not given, let document be node’s node document.
722 if (!document)
723 document = m_document.ptr();
724 JS::GCPtr<Node> copy;
725
726 // 2. If node is an element, then:
727 if (is<Element>(this)) {
728 // 1. Let copy be the result of creating an element, given document, node’s local name, node’s namespace, node’s namespace prefix, and node’s is value, with the synchronous custom elements flag unset.
729 auto& element = *verify_cast<Element>(this);
730 auto element_copy = DOM::create_element(*document, element.local_name(), element.namespace_() /* FIXME: node’s namespace prefix, and node’s is value, with the synchronous custom elements flag unset */).release_value_but_fixme_should_propagate_errors();
731
732 // 2. For each attribute in node’s attribute list:
733 element.for_each_attribute([&](auto& name, auto& value) {
734 // 1. Let copyAttribute be a clone of attribute.
735 // 2. Append copyAttribute to copy.
736 MUST(element_copy->set_attribute(name, value));
737 });
738 copy = move(element_copy);
739
740 }
741 // 3. Otherwise, let copy be a node that implements the same interfaces as node, and fulfills these additional requirements, switching on the interface node implements:
742 else if (is<Document>(this)) {
743 // Document
744 auto document_ = verify_cast<Document>(this);
745 auto document_copy = Document::create(this->realm(), document_->url()).release_value_but_fixme_should_propagate_errors();
746
747 // Set copy’s encoding, content type, URL, origin, type, and mode to those of node.
748 document_copy->set_encoding(document_->encoding());
749 document_copy->set_content_type(document_->content_type());
750 document_copy->set_url(document_->url());
751 document_copy->set_origin(document_->origin());
752 document_copy->set_document_type(document_->document_type());
753 document_copy->set_quirks_mode(document_->mode());
754 copy = move(document_copy);
755 } else if (is<DocumentType>(this)) {
756 // DocumentType
757 auto document_type = verify_cast<DocumentType>(this);
758 auto document_type_copy = heap().allocate<DocumentType>(realm(), *document).release_allocated_value_but_fixme_should_propagate_errors();
759
760 // Set copy’s name, public ID, and system ID to those of node.
761 document_type_copy->set_name(document_type->name());
762 document_type_copy->set_public_id(document_type->public_id());
763 document_type_copy->set_system_id(document_type->system_id());
764 copy = move(document_type_copy);
765 } else if (is<Attr>(this)) {
766 // Attr
767 // Set copy’s namespace, namespace prefix, local name, and value to those of node.
768 auto& attr = static_cast<Attr&>(*this);
769 copy = attr.clone(*document);
770 } else if (is<Text>(this)) {
771 // Text
772 auto text = verify_cast<Text>(this);
773
774 // Set copy’s data to that of node.
775 auto text_copy = heap().allocate<Text>(realm(), *document, text->data()).release_allocated_value_but_fixme_should_propagate_errors();
776 copy = move(text_copy);
777 } else if (is<Comment>(this)) {
778 // Comment
779 auto comment = verify_cast<Comment>(this);
780
781 // Set copy’s data to that of node.
782 auto comment_copy = heap().allocate<Comment>(realm(), *document, comment->data()).release_allocated_value_but_fixme_should_propagate_errors();
783 copy = move(comment_copy);
784 } else if (is<ProcessingInstruction>(this)) {
785 // ProcessingInstruction
786 auto processing_instruction = verify_cast<ProcessingInstruction>(this);
787
788 // Set copy’s target and data to those of node.
789 auto processing_instruction_copy = heap().allocate<ProcessingInstruction>(realm(), *document, processing_instruction->data(), processing_instruction->target()).release_allocated_value_but_fixme_should_propagate_errors();
790 copy = processing_instruction_copy;
791 }
792 // Otherwise, Do nothing.
793 else if (is<DocumentFragment>(this)) {
794 copy = heap().allocate<DocumentFragment>(realm(), *document).release_allocated_value_but_fixme_should_propagate_errors();
795 }
796
797 // FIXME: 4. Set copy’s node document and document to copy, if copy is a document, and set copy’s node document to document otherwise.
798
799 // 5. Run any cloning steps defined for node in other applicable specifications and pass copy, node, document and the clone children flag if set, as parameters.
800 cloned(*copy, clone_children);
801
802 // 6. If the clone children flag is set, clone all the children of node and append them to copy, with document as specified and the clone children flag being set.
803 if (clone_children) {
804 for_each_child([&](auto& child) {
805 MUST(copy->append_child(child.clone_node(document, true)));
806 });
807 }
808
809 // 7. Return copy.
810 return *copy;
811}
812
813// https://dom.spec.whatwg.org/#dom-node-clonenode
814WebIDL::ExceptionOr<JS::NonnullGCPtr<Node>> Node::clone_node_binding(bool deep)
815{
816 // 1. If this is a shadow root, then throw a "NotSupportedError" DOMException.
817 if (is<ShadowRoot>(*this))
818 return WebIDL::NotSupportedError::create(realm(), "Cannot clone shadow root");
819
820 // 2. Return a clone of this, with the clone children flag set if deep is true.
821 return clone_node(nullptr, deep);
822}
823
824void Node::set_document(Badge<Document>, Document& document)
825{
826 if (m_document.ptr() == &document)
827 return;
828
829 m_document = &document;
830
831 if (needs_style_update() || child_needs_style_update()) {
832 // NOTE: We unset and reset the "needs style update" flag here.
833 // This ensures that there's a pending style update in the new document
834 // that will eventually assign some style to this node if needed.
835 set_needs_style_update(false);
836 set_needs_style_update(true);
837 }
838}
839
840bool Node::is_editable() const
841{
842 return parent() && parent()->is_editable();
843}
844
845void Node::set_layout_node(Badge<Layout::Node>, JS::NonnullGCPtr<Layout::Node> layout_node)
846{
847 m_layout_node = layout_node;
848}
849
850void Node::detach_layout_node(Badge<DOM::Document>)
851{
852 m_layout_node = nullptr;
853}
854
855EventTarget* Node::get_parent(Event const&)
856{
857 // FIXME: returns the node’s assigned slot, if node is assigned, and node’s parent otherwise.
858 return parent();
859}
860
861void Node::set_needs_style_update(bool value)
862{
863 if (m_needs_style_update == value)
864 return;
865 m_needs_style_update = value;
866
867 if (m_needs_style_update) {
868 for (auto* ancestor = parent_or_shadow_host(); ancestor; ancestor = ancestor->parent_or_shadow_host()) {
869 ancestor->m_child_needs_style_update = true;
870 }
871 document().schedule_style_update();
872 }
873}
874
875void Node::inserted()
876{
877 set_needs_style_update(true);
878}
879
880ParentNode* Node::parent_or_shadow_host()
881{
882 if (is<ShadowRoot>(*this))
883 return static_cast<ShadowRoot&>(*this).host();
884 return verify_cast<ParentNode>(parent());
885}
886
887Element* Node::parent_or_shadow_host_element()
888{
889 if (is<ShadowRoot>(*this))
890 return static_cast<ShadowRoot&>(*this).host();
891 if (!parent())
892 return nullptr;
893 if (is<Element>(*parent()))
894 return static_cast<Element*>(parent());
895 if (is<ShadowRoot>(*parent()))
896 return static_cast<ShadowRoot&>(*parent()).host();
897 return nullptr;
898}
899
900JS::NonnullGCPtr<NodeList> Node::child_nodes()
901{
902 if (!m_child_nodes) {
903 m_child_nodes = LiveNodeList::create(realm(), *this, [this](auto& node) {
904 return is_parent_of(node);
905 }).release_value_but_fixme_should_propagate_errors();
906 }
907 return *m_child_nodes;
908}
909
910Vector<JS::Handle<Node>> Node::children_as_vector() const
911{
912 Vector<JS::Handle<Node>> nodes;
913
914 for_each_child([&](auto& child) {
915 nodes.append(JS::make_handle(child));
916 });
917
918 return nodes;
919}
920
921void Node::remove_all_children(bool suppress_observers)
922{
923 while (JS::GCPtr<Node> child = first_child())
924 child->remove(suppress_observers);
925}
926
927// https://dom.spec.whatwg.org/#dom-node-comparedocumentposition
928u16 Node::compare_document_position(JS::GCPtr<Node> other)
929{
930 enum Position : u16 {
931 DOCUMENT_POSITION_EQUAL = 0,
932 DOCUMENT_POSITION_DISCONNECTED = 1,
933 DOCUMENT_POSITION_PRECEDING = 2,
934 DOCUMENT_POSITION_FOLLOWING = 4,
935 DOCUMENT_POSITION_CONTAINS = 8,
936 DOCUMENT_POSITION_CONTAINED_BY = 16,
937 DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = 32,
938 };
939
940 // 1. If this is other, then return zero.
941 if (this == other.ptr())
942 return DOCUMENT_POSITION_EQUAL;
943
944 // 2. Let node1 be other and node2 be this.
945 Node* node1 = other.ptr();
946 Node* node2 = this;
947
948 // 3. Let attr1 and attr2 be null.
949 Attr* attr1;
950 Attr* attr2;
951
952 // 4. If node1 is an attribute, then set attr1 to node1 and node1 to attr1’s element.
953 if (is<Attr>(node1)) {
954 attr1 = verify_cast<Attr>(node1);
955 node1 = const_cast<Element*>(attr1->owner_element());
956 }
957
958 // 5. If node2 is an attribute, then:
959 if (is<Attr>(node2)) {
960 // 1. Set attr2 to node2 and node2 to attr2’s element.
961 attr2 = verify_cast<Attr>(node2);
962 node2 = const_cast<Element*>(attr2->owner_element());
963
964 // 2. If attr1 and node1 are non-null, and node2 is node1, then:
965 if (attr1 && node1 && node2 == node1) {
966 // FIXME: 1. For each attr in node2’s attribute list:
967 // 1. If attr equals attr1, then return the result of adding DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC and DOCUMENT_POSITION_PRECEDING.
968 // 2. If attr equals attr2, then return the result of adding DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC and DOCUMENT_POSITION_FOLLOWING.
969 }
970 }
971
972 // 6. If node1 or node2 is null, or node1’s root is not node2’s root, then return the result of adding
973 // DOCUMENT_POSITION_DISCONNECTED, DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC, and either DOCUMENT_POSITION_PRECEDING or DOCUMENT_POSITION_FOLLOWING, with the constraint that this is to be consistent, together.
974 if ((node1 == nullptr || node2 == nullptr) || (&node1->root() != &node2->root()))
975 return DOCUMENT_POSITION_DISCONNECTED | DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC | (node1 > node2 ? DOCUMENT_POSITION_PRECEDING : DOCUMENT_POSITION_FOLLOWING);
976
977 // 7. If node1 is an ancestor of node2 and attr1 is null, or node1 is node2 and attr2 is non-null, then return the result of adding DOCUMENT_POSITION_CONTAINS to DOCUMENT_POSITION_PRECEDING.
978 if ((node1->is_ancestor_of(*node2) && !attr1) || (node1 == node2 && attr2))
979 return DOCUMENT_POSITION_CONTAINS | DOCUMENT_POSITION_PRECEDING;
980
981 // 8. If node1 is a descendant of node2 and attr2 is null, or node1 is node2 and attr1 is non-null, then return the result of adding DOCUMENT_POSITION_CONTAINED_BY to DOCUMENT_POSITION_FOLLOWING.
982 if ((node2->is_ancestor_of(*node1) && !attr2) || (node1 == node2 && attr1))
983 return DOCUMENT_POSITION_CONTAINED_BY | DOCUMENT_POSITION_FOLLOWING;
984
985 // 9. If node1 is preceding node2, then return DOCUMENT_POSITION_PRECEDING.
986 if (node1->is_before(*node2))
987 return DOCUMENT_POSITION_PRECEDING;
988
989 // 10. Return DOCUMENT_POSITION_FOLLOWING.
990 return DOCUMENT_POSITION_FOLLOWING;
991}
992
993// https://dom.spec.whatwg.org/#concept-tree-host-including-inclusive-ancestor
994bool Node::is_host_including_inclusive_ancestor_of(Node const& other) const
995{
996 // An object A is a host-including inclusive ancestor of an object B,
997 // if either A is an inclusive ancestor of B,
998 if (is_inclusive_ancestor_of(other))
999 return true;
1000
1001 // or if B’s root has a non-null host and A is a host-including inclusive ancestor of B’s root’s host
1002 if (is<DocumentFragment>(other.root())
1003 && static_cast<DocumentFragment const&>(other.root()).host()
1004 && is_inclusive_ancestor_of(*static_cast<DocumentFragment const&>(other.root()).host())) {
1005 return true;
1006 }
1007 return false;
1008}
1009
1010// https://dom.spec.whatwg.org/#dom-node-ownerdocument
1011JS::GCPtr<Document> Node::owner_document() const
1012{
1013 // The ownerDocument getter steps are to return null, if this is a document; otherwise this’s node document.
1014 if (is_document())
1015 return nullptr;
1016 return m_document;
1017}
1018
1019// This function tells us whether a node is interesting enough to show up
1020// in the DOM inspector. This hides two things:
1021// - Non-rendered whitespace
1022// - Rendered whitespace between block-level elements
1023bool Node::is_uninteresting_whitespace_node() const
1024{
1025 if (!is<Text>(*this))
1026 return false;
1027 if (!static_cast<Text const&>(*this).data().is_whitespace())
1028 return false;
1029 if (!layout_node())
1030 return true;
1031 if (auto parent = layout_node()->parent(); parent && parent->is_anonymous())
1032 return true;
1033 return false;
1034}
1035
1036void Node::serialize_tree_as_json(JsonObjectSerializer<StringBuilder>& object) const
1037{
1038 MUST(object.add("name"sv, node_name().view()));
1039 MUST(object.add("id"sv, id()));
1040 if (is_document()) {
1041 MUST(object.add("type"sv, "document"));
1042 } else if (is_element()) {
1043 MUST(object.add("type"sv, "element"));
1044
1045 auto const* element = static_cast<DOM::Element const*>(this);
1046 if (element->has_attributes()) {
1047 auto attributes = MUST(object.add_object("attributes"sv));
1048 element->for_each_attribute([&attributes](auto& name, auto& value) {
1049 MUST(attributes.add(name, value));
1050 });
1051 MUST(attributes.finish());
1052 }
1053
1054 if (element->is_browsing_context_container()) {
1055 auto const* container = static_cast<HTML::BrowsingContextContainer const*>(element);
1056 if (auto const* content_document = container->content_document()) {
1057 auto children = MUST(object.add_array("children"sv));
1058 JsonObjectSerializer<StringBuilder> content_document_object = MUST(children.add_object());
1059 content_document->serialize_tree_as_json(content_document_object);
1060 MUST(content_document_object.finish());
1061 MUST(children.finish());
1062 }
1063 }
1064 } else if (is_text()) {
1065 MUST(object.add("type"sv, "text"));
1066
1067 auto text_node = static_cast<DOM::Text const*>(this);
1068 MUST(object.add("text"sv, text_node->data()));
1069 } else if (is_comment()) {
1070 MUST(object.add("type"sv, "comment"sv));
1071 MUST(object.add("data"sv, static_cast<DOM::Comment const&>(*this).data()));
1072 }
1073
1074 MUST((object.add("visible"sv, !!layout_node())));
1075
1076 if (has_child_nodes()) {
1077 auto children = MUST(object.add_array("children"sv));
1078 for_each_child([&children](DOM::Node& child) {
1079 if (child.is_uninteresting_whitespace_node())
1080 return;
1081 JsonObjectSerializer<StringBuilder> child_object = MUST(children.add_object());
1082 child.serialize_tree_as_json(child_object);
1083 MUST(child_object.finish());
1084 });
1085
1086 // Pseudo-elements don't have DOM nodes,so we have to add them separately.
1087 if (is_element()) {
1088 auto const* element = static_cast<DOM::Element const*>(this);
1089 element->serialize_pseudo_elements_as_json(children);
1090 }
1091
1092 MUST(children.finish());
1093 }
1094}
1095
1096// https://html.spec.whatwg.org/multipage/webappapis.html#concept-n-script
1097bool Node::is_scripting_enabled() const
1098{
1099 // Scripting is enabled for a node node if node's node document's browsing context is non-null, and scripting is enabled for node's relevant settings object.
1100 return document().browsing_context() && const_cast<Document&>(document()).relevant_settings_object().is_scripting_enabled();
1101}
1102
1103// https://html.spec.whatwg.org/multipage/webappapis.html#concept-n-noscript
1104bool Node::is_scripting_disabled() const
1105{
1106 // Scripting is disabled for a node when scripting is not enabled, i.e., when its node document's browsing context is null or when scripting is disabled for its relevant settings object.
1107 return !is_scripting_enabled();
1108}
1109
1110// https://dom.spec.whatwg.org/#dom-node-contains
1111bool Node::contains(JS::GCPtr<Node> other) const
1112{
1113 // The contains(other) method steps are to return true if other is an inclusive descendant of this; otherwise false (including when other is null).
1114 return other && other->is_inclusive_descendant_of(*this);
1115}
1116
1117// https://dom.spec.whatwg.org/#concept-shadow-including-descendant
1118bool Node::is_shadow_including_descendant_of(Node const& other) const
1119{
1120 // An object A is a shadow-including descendant of an object B,
1121 // if A is a descendant of B,
1122 if (is_descendant_of(other))
1123 return true;
1124
1125 // or A’s root is a shadow root
1126 if (!is<ShadowRoot>(root()))
1127 return false;
1128
1129 // and A’s root’s host is a shadow-including inclusive descendant of B.
1130 auto& shadow_root = verify_cast<ShadowRoot>(root());
1131 // NOTE: While host is nullable because of inheriting from DocumentFragment, shadow roots always have a host.
1132 return shadow_root.host()->is_shadow_including_inclusive_descendant_of(other);
1133}
1134
1135// https://dom.spec.whatwg.org/#concept-shadow-including-inclusive-descendant
1136bool Node::is_shadow_including_inclusive_descendant_of(Node const& other) const
1137{
1138 // A shadow-including inclusive descendant is an object or one of its shadow-including descendants.
1139 return &other == this || is_shadow_including_descendant_of(other);
1140}
1141
1142// https://dom.spec.whatwg.org/#concept-shadow-including-ancestor
1143bool Node::is_shadow_including_ancestor_of(Node const& other) const
1144{
1145 // An object A is a shadow-including ancestor of an object B, if and only if B is a shadow-including descendant of A.
1146 return other.is_shadow_including_descendant_of(*this);
1147}
1148
1149// https://dom.spec.whatwg.org/#concept-shadow-including-inclusive-ancestor
1150bool Node::is_shadow_including_inclusive_ancestor_of(Node const& other) const
1151{
1152 // A shadow-including inclusive ancestor is an object or one of its shadow-including ancestors.
1153 return other.is_shadow_including_inclusive_descendant_of(*this);
1154}
1155
1156// https://dom.spec.whatwg.org/#concept-node-replace-all
1157void Node::replace_all(JS::GCPtr<Node> node)
1158{
1159 // 1. Let removedNodes be parent’s children.
1160 auto removed_nodes = children_as_vector();
1161
1162 // 2. Let addedNodes be the empty set.
1163 Vector<JS::Handle<Node>> added_nodes;
1164
1165 // 3. If node is a DocumentFragment node, then set addedNodes to node’s children.
1166 if (node && is<DocumentFragment>(*node)) {
1167 added_nodes = node->children_as_vector();
1168 }
1169 // 4. Otherwise, if node is non-null, set addedNodes to « node ».
1170 else if (node) {
1171 added_nodes.append(JS::make_handle(*node));
1172 }
1173
1174 // 5. Remove all parent’s children, in tree order, with the suppress observers flag set.
1175 remove_all_children(true);
1176
1177 // 6. If node is non-null, then insert node into parent before null with the suppress observers flag set.
1178 if (node)
1179 insert_before(*node, nullptr, true);
1180
1181 // 7. If either addedNodes or removedNodes is not empty, then queue a tree mutation record for parent with addedNodes, removedNodes, null, and null.
1182 if (!added_nodes.is_empty() || !removed_nodes.is_empty()) {
1183 auto added_node_list = StaticNodeList::create(realm(), move(added_nodes)).release_value_but_fixme_should_propagate_errors();
1184 auto removed_node_list = StaticNodeList::create(realm(), move(removed_nodes)).release_value_but_fixme_should_propagate_errors();
1185 queue_tree_mutation_record(added_node_list, removed_node_list, nullptr, nullptr);
1186 }
1187}
1188
1189// https://dom.spec.whatwg.org/#string-replace-all
1190void Node::string_replace_all(DeprecatedString const& string)
1191{
1192 // 1. Let node be null.
1193 JS::GCPtr<Node> node;
1194
1195 // 2. If string is not the empty string, then set node to a new Text node whose data is string and node document is parent’s node document.
1196 if (!string.is_empty())
1197 node = heap().allocate<Text>(realm(), document(), string).release_allocated_value_but_fixme_should_propagate_errors();
1198
1199 // 3. Replace all with node within parent.
1200 replace_all(node);
1201}
1202
1203// https://w3c.github.io/DOM-Parsing/#dfn-fragment-serializing-algorithm
1204WebIDL::ExceptionOr<DeprecatedString> Node::serialize_fragment(DOMParsing::RequireWellFormed require_well_formed) const
1205{
1206 // 1. Let context document be the value of node's node document.
1207 auto const& context_document = document();
1208
1209 // 2. If context document is an HTML document, return an HTML serialization of node.
1210 if (context_document.is_html_document())
1211 return HTML::HTMLParser::serialize_html_fragment(*this);
1212
1213 // 3. Otherwise, context document is an XML document; return an XML serialization of node passing the flag require well-formed.
1214 return DOMParsing::serialize_node_to_xml_string(*this, require_well_formed);
1215}
1216
1217// https://dom.spec.whatwg.org/#dom-node-issamenode
1218bool Node::is_same_node(Node const* other_node) const
1219{
1220 // The isSameNode(otherNode) method steps are to return true if otherNode is this; otherwise false.
1221 return this == other_node;
1222}
1223
1224// https://dom.spec.whatwg.org/#dom-node-isequalnode
1225bool Node::is_equal_node(Node const* other_node) const
1226{
1227 // The isEqualNode(otherNode) method steps are to return true if otherNode is non-null and this equals otherNode; otherwise false.
1228 if (!other_node)
1229 return false;
1230
1231 // Fast path for testing a node against itself.
1232 if (this == other_node)
1233 return true;
1234
1235 // A node A equals a node B if all of the following conditions are true:
1236
1237 // A and B implement the same interfaces.
1238 if (node_name() != other_node->node_name())
1239 return false;
1240
1241 // The following are equal, switching on the interface A implements:
1242 switch (node_type()) {
1243 case (u16)NodeType::DOCUMENT_TYPE_NODE: {
1244 // Its name, public ID, and system ID.
1245 auto& this_doctype = verify_cast<DocumentType>(*this);
1246 auto& other_doctype = verify_cast<DocumentType>(*other_node);
1247 if (this_doctype.name() != other_doctype.name()
1248 || this_doctype.public_id() != other_doctype.public_id()
1249 || this_doctype.system_id() != other_doctype.system_id())
1250 return false;
1251 break;
1252 }
1253 case (u16)NodeType::ELEMENT_NODE: {
1254 // Its namespace, namespace prefix, local name, and its attribute list’s size.
1255 auto& this_element = verify_cast<Element>(*this);
1256 auto& other_element = verify_cast<Element>(*other_node);
1257 if (this_element.namespace_() != other_element.namespace_()
1258 || this_element.prefix() != other_element.prefix()
1259 || this_element.local_name() != other_element.local_name()
1260 || this_element.attribute_list_size() != other_element.attribute_list_size())
1261 return false;
1262 // If A is an element, each attribute in its attribute list has an attribute that equals an attribute in B’s attribute list.
1263 bool has_same_attributes = true;
1264 this_element.for_each_attribute([&](auto& name, auto& value) {
1265 if (other_element.get_attribute(name) != value)
1266 has_same_attributes = false;
1267 });
1268 if (!has_same_attributes)
1269 return false;
1270 break;
1271 }
1272 case (u16)NodeType::COMMENT_NODE:
1273 case (u16)NodeType::TEXT_NODE: {
1274 // Its data.
1275 auto& this_cdata = verify_cast<CharacterData>(*this);
1276 auto& other_cdata = verify_cast<CharacterData>(*other_node);
1277 if (this_cdata.data() != other_cdata.data())
1278 return false;
1279 break;
1280 }
1281 case (u16)NodeType::ATTRIBUTE_NODE: {
1282 // Its namespace, local name, and value.
1283 auto& this_attr = verify_cast<Attr>(*this);
1284 auto& other_attr = verify_cast<Attr>(*other_node);
1285 if (this_attr.namespace_uri() != other_attr.namespace_uri())
1286 return false;
1287 if (this_attr.local_name() != other_attr.local_name())
1288 return false;
1289 if (this_attr.value() != other_attr.value())
1290 return false;
1291 break;
1292 }
1293 case (u16)NodeType::PROCESSING_INSTRUCTION_NODE: {
1294 // Its target and data.
1295 auto& this_processing_instruction = verify_cast<ProcessingInstruction>(*this);
1296 auto& other_processing_instruction = verify_cast<ProcessingInstruction>(*other_node);
1297 if (this_processing_instruction.target() != other_processing_instruction.target())
1298 return false;
1299 if (this_processing_instruction.data() != other_processing_instruction.data())
1300 return false;
1301 break;
1302 }
1303 default:
1304 break;
1305 }
1306
1307 // A and B have the same number of children.
1308 size_t this_child_count = child_count();
1309 size_t other_child_count = other_node->child_count();
1310 if (this_child_count != other_child_count)
1311 return false;
1312
1313 // Each child of A equals the child of B at the identical index.
1314 // FIXME: This can be made nicer. child_at_index() is O(n).
1315 for (size_t i = 0; i < this_child_count; ++i) {
1316 auto* this_child = child_at_index(i);
1317 auto* other_child = other_node->child_at_index(i);
1318 VERIFY(this_child);
1319 VERIFY(other_child);
1320 if (!this_child->is_equal_node(other_child))
1321 return false;
1322 }
1323
1324 return true;
1325}
1326
1327// https://dom.spec.whatwg.org/#in-a-document-tree
1328bool Node::in_a_document_tree() const
1329{
1330 // An element is in a document tree if its root is a document.
1331 return root().is_document();
1332}
1333
1334// https://dom.spec.whatwg.org/#dom-node-getrootnode
1335JS::NonnullGCPtr<Node> Node::get_root_node(GetRootNodeOptions const& options)
1336{
1337 // The getRootNode(options) method steps are to return this’s shadow-including root if options["composed"] is true;
1338 if (options.composed)
1339 return shadow_including_root();
1340
1341 // otherwise this’s root.
1342 return root();
1343}
1344
1345DeprecatedString Node::debug_description() const
1346{
1347 StringBuilder builder;
1348 builder.append(node_name().to_lowercase());
1349 if (is_element()) {
1350 auto& element = static_cast<DOM::Element const&>(*this);
1351 if (auto id = element.get_attribute(HTML::AttributeNames::id); !id.is_null())
1352 builder.appendff("#{}", id);
1353 for (auto const& class_name : element.class_names())
1354 builder.appendff(".{}", class_name);
1355 }
1356 return builder.to_deprecated_string();
1357}
1358
1359// https://dom.spec.whatwg.org/#concept-node-length
1360size_t Node::length() const
1361{
1362 // 1. If node is a DocumentType or Attr node, then return 0.
1363 if (is_document_type() || is_attribute())
1364 return 0;
1365
1366 // 2. If node is a CharacterData node, then return node’s data’s length.
1367 if (is_character_data()) {
1368 auto* character_data_node = verify_cast<CharacterData>(this);
1369 return character_data_node->data().length();
1370 }
1371
1372 // 3. Return the number of node’s children.
1373 return child_count();
1374}
1375
1376Painting::Paintable const* Node::paintable() const
1377{
1378 if (!layout_node())
1379 return nullptr;
1380 return layout_node()->paintable();
1381}
1382
1383Painting::PaintableBox const* Node::paint_box() const
1384{
1385 if (!layout_node())
1386 return nullptr;
1387 if (!layout_node()->is_box())
1388 return nullptr;
1389 return static_cast<Layout::Box const&>(*layout_node()).paint_box();
1390}
1391
1392// https://dom.spec.whatwg.org/#queue-a-mutation-record
1393void Node::queue_mutation_record(DeprecatedFlyString const& type, DeprecatedString attribute_name, DeprecatedString attribute_namespace, DeprecatedString old_value, JS::NonnullGCPtr<NodeList> added_nodes, JS::NonnullGCPtr<NodeList> removed_nodes, Node* previous_sibling, Node* next_sibling) const
1394{
1395 // NOTE: We defer garbage collection until the end of the scope, since we can't safely use MutationObserver* as a hashmap key otherwise.
1396 // FIXME: This is a total hack.
1397 JS::DeferGC defer_gc(heap());
1398
1399 // 1. Let interestedObservers be an empty map.
1400 // mutationObserver -> mappedOldValue
1401 OrderedHashMap<MutationObserver*, DeprecatedString> interested_observers;
1402
1403 // 2. Let nodes be the inclusive ancestors of target.
1404 Vector<JS::Handle<Node const>> nodes;
1405 nodes.append(JS::make_handle(*this));
1406
1407 for (auto* parent_node = parent(); parent_node; parent_node = parent_node->parent())
1408 nodes.append(JS::make_handle(*parent_node));
1409
1410 // 3. For each node in nodes, and then for each registered of node’s registered observer list:
1411 for (auto& node : nodes) {
1412 for (auto& registered_observer : node->m_registered_observer_list) {
1413 // 1. Let options be registered’s options.
1414 auto& options = registered_observer.options();
1415
1416 // 2. If none of the following are true
1417 // - node is not target and options["subtree"] is false
1418 // - type is "attributes" and options["attributes"] either does not exist or is false
1419 // - type is "attributes", options["attributeFilter"] exists, and options["attributeFilter"] does not contain name or namespace is non-null
1420 // - type is "characterData" and options["characterData"] either does not exist or is false
1421 // - type is "childList" and options["childList"] is false
1422 // then:
1423 if (!(node.ptr() != this && !options.subtree)
1424 && !(type == MutationType::attributes && (!options.attributes.has_value() || !options.attributes.value()))
1425 && !(type == MutationType::attributes && options.attribute_filter.has_value() && (!attribute_namespace.is_null() || !options.attribute_filter->contains_slow(attribute_name)))
1426 && !(type == MutationType::characterData && (!options.character_data.has_value() || !options.character_data.value()))
1427 && !(type == MutationType::childList && !options.child_list)) {
1428 // 1. Let mo be registered’s observer.
1429 auto mutation_observer = registered_observer.observer();
1430
1431 // 2. If interestedObservers[mo] does not exist, then set interestedObservers[mo] to null.
1432 if (!interested_observers.contains(mutation_observer))
1433 interested_observers.set(mutation_observer, {});
1434
1435 // 3. If either type is "attributes" and options["attributeOldValue"] is true, or type is "characterData" and options["characterDataOldValue"] is true, then set interestedObservers[mo] to oldValue.
1436 if ((type == MutationType::attributes && options.attribute_old_value.has_value() && options.attribute_old_value.value()) || (type == MutationType::characterData && options.character_data_old_value.has_value() && options.character_data_old_value.value()))
1437 interested_observers.set(mutation_observer, old_value);
1438 }
1439 }
1440 }
1441
1442 // 4. For each observer → mappedOldValue of interestedObservers:
1443 for (auto& interested_observer : interested_observers) {
1444 // 1. Let record be a new MutationRecord object with its type set to type, target set to target, attributeName set to name, attributeNamespace set to namespace, oldValue set to mappedOldValue,
1445 // addedNodes set to addedNodes, removedNodes set to removedNodes, previousSibling set to previousSibling, and nextSibling set to nextSibling.
1446 auto record = MutationRecord::create(realm(), type, *this, added_nodes, removed_nodes, previous_sibling, next_sibling, attribute_name, attribute_namespace, /* mappedOldValue */ interested_observer.value).release_value_but_fixme_should_propagate_errors();
1447
1448 // 2. Enqueue record to observer’s record queue.
1449 interested_observer.key->enqueue_record({}, move(record));
1450 }
1451
1452 // 5. Queue a mutation observer microtask.
1453 Bindings::queue_mutation_observer_microtask(document());
1454}
1455
1456// https://dom.spec.whatwg.org/#queue-a-tree-mutation-record
1457void Node::queue_tree_mutation_record(JS::NonnullGCPtr<NodeList> added_nodes, JS::NonnullGCPtr<NodeList> removed_nodes, Node* previous_sibling, Node* next_sibling)
1458{
1459 // 1. Assert: either addedNodes or removedNodes is not empty.
1460 VERIFY(added_nodes->length() > 0 || removed_nodes->length() > 0);
1461
1462 // 2. Queue a mutation record of "childList" for target with null, null, null, addedNodes, removedNodes, previousSibling, and nextSibling.
1463 queue_mutation_record(MutationType::childList, {}, {}, {}, move(added_nodes), move(removed_nodes), previous_sibling, next_sibling);
1464}
1465
1466void Node::append_child_impl(JS::NonnullGCPtr<Node> node)
1467{
1468 VERIFY(!node->m_parent);
1469
1470 if (!is_child_allowed(*node))
1471 return;
1472
1473 if (m_last_child)
1474 m_last_child->m_next_sibling = node.ptr();
1475 node->m_previous_sibling = m_last_child;
1476 node->m_parent = this;
1477 m_last_child = node.ptr();
1478 if (!m_first_child)
1479 m_first_child = m_last_child;
1480}
1481
1482void Node::insert_before_impl(JS::NonnullGCPtr<Node> node, JS::GCPtr<Node> child)
1483{
1484 if (!child)
1485 return append_child_impl(move(node));
1486
1487 VERIFY(!node->m_parent);
1488 VERIFY(child->parent() == this);
1489
1490 node->m_previous_sibling = child->m_previous_sibling;
1491 node->m_next_sibling = child;
1492
1493 if (child->m_previous_sibling)
1494 child->m_previous_sibling->m_next_sibling = node;
1495
1496 if (m_first_child == child)
1497 m_first_child = node;
1498
1499 child->m_previous_sibling = node;
1500
1501 node->m_parent = this;
1502}
1503
1504void Node::remove_child_impl(JS::NonnullGCPtr<Node> node)
1505{
1506 VERIFY(node->m_parent.ptr() == this);
1507
1508 if (m_first_child == node)
1509 m_first_child = node->m_next_sibling;
1510
1511 if (m_last_child == node)
1512 m_last_child = node->m_previous_sibling;
1513
1514 if (node->m_next_sibling)
1515 node->m_next_sibling->m_previous_sibling = node->m_previous_sibling;
1516
1517 if (node->m_previous_sibling)
1518 node->m_previous_sibling->m_next_sibling = node->m_next_sibling;
1519
1520 node->m_next_sibling = nullptr;
1521 node->m_previous_sibling = nullptr;
1522 node->m_parent = nullptr;
1523}
1524
1525bool Node::is_ancestor_of(Node const& other) const
1526{
1527 for (auto* ancestor = other.parent(); ancestor; ancestor = ancestor->parent()) {
1528 if (ancestor == this)
1529 return true;
1530 }
1531 return false;
1532}
1533
1534bool Node::is_inclusive_ancestor_of(Node const& other) const
1535{
1536 return &other == this || is_ancestor_of(other);
1537}
1538
1539bool Node::is_descendant_of(Node const& other) const
1540{
1541 return other.is_ancestor_of(*this);
1542}
1543
1544bool Node::is_inclusive_descendant_of(Node const& other) const
1545{
1546 return other.is_inclusive_ancestor_of(*this);
1547}
1548
1549// https://dom.spec.whatwg.org/#concept-tree-following
1550bool Node::is_following(Node const& other) const
1551{
1552 // An object A is following an object B if A and B are in the same tree and A comes after B in tree order.
1553 for (auto* node = previous_in_pre_order(); node; node = node->previous_in_pre_order()) {
1554 if (node == &other)
1555 return true;
1556 }
1557
1558 return false;
1559}
1560
1561void Node::build_accessibility_tree(AccessibilityTreeNode& parent)
1562{
1563 if (is_uninteresting_whitespace_node())
1564 return;
1565
1566 if (is_document()) {
1567 auto* document = static_cast<DOM::Document*>(this);
1568 auto* document_element = document->document_element();
1569 if (document_element) {
1570 parent.set_value(document_element);
1571 if (document_element->has_child_nodes())
1572 document_element->for_each_child([&parent](DOM::Node& child) {
1573 child.build_accessibility_tree(parent);
1574 });
1575 }
1576 } else if (is_element()) {
1577 auto const* element = static_cast<DOM::Element const*>(this);
1578
1579 if (is<HTML::HTMLScriptElement>(element) || is<HTML::HTMLStyleElement>(element))
1580 return;
1581
1582 if (element->include_in_accessibility_tree()) {
1583 auto current_node = AccessibilityTreeNode::create(&document(), this).release_value_but_fixme_should_propagate_errors();
1584 parent.append_child(current_node);
1585 if (has_child_nodes()) {
1586 for_each_child([¤t_node](DOM::Node& child) {
1587 child.build_accessibility_tree(*current_node);
1588 });
1589 }
1590 } else if (has_child_nodes()) {
1591 for_each_child([&parent](DOM::Node& child) {
1592 child.build_accessibility_tree(parent);
1593 });
1594 }
1595 } else if (is_text()) {
1596 parent.append_child(AccessibilityTreeNode::create(&document(), this).release_value_but_fixme_should_propagate_errors());
1597 if (has_child_nodes()) {
1598 for_each_child([&parent](DOM::Node& child) {
1599 child.build_accessibility_tree(parent);
1600 });
1601 }
1602 }
1603}
1604
1605// https://www.w3.org/TR/accname-1.2/#mapping_additional_nd_te
1606ErrorOr<String> Node::name_or_description(NameOrDescription target, Document const& document, HashTable<i32>& visited_nodes) const
1607{
1608 // The text alternative for a given element is computed as follows:
1609 // 1. Set the root node to the given element, the current node to the root node, and the total accumulated text to the empty string (""). If the root node's role prohibits naming, return the empty string ("").
1610 auto const* root_node = this;
1611 auto const* current_node = root_node;
1612 StringBuilder total_accumulated_text;
1613 visited_nodes.set(id());
1614
1615 if (is_element()) {
1616 auto const* element = static_cast<DOM::Element const*>(this);
1617 // 2. Compute the text alternative for the current node:
1618 // A. If the current node is hidden and is not directly referenced by aria-labelledby or aria-describedby, nor directly referenced by a native host language text alternative element (e.g. label in HTML) or attribute, return the empty string.
1619 // FIXME: Check for references
1620 if (element->aria_hidden() == "true" || !layout_node())
1621 return String {};
1622 // B. Otherwise:
1623 // - if computing a name, and the current node has an aria-labelledby attribute that contains at least one valid IDREF, and the current node is not already part of an aria-labelledby traversal,
1624 // process its IDREFs in the order they occur:
1625 // - or, if computing a description, and the current node has an aria-describedby attribute that contains at least one valid IDREF, and the current node is not already part of an aria-describedby traversal,
1626 // process its IDREFs in the order they occur:
1627 if ((target == NameOrDescription::Name && Node::first_valid_id(element->aria_labelled_by(), document).has_value())
1628 || (target == NameOrDescription::Description && Node::first_valid_id(element->aria_described_by(), document).has_value())) {
1629
1630 // i. Set the accumulated text to the empty string.
1631 total_accumulated_text.clear();
1632
1633 Vector<StringView> id_list;
1634 if (target == NameOrDescription::Name) {
1635 id_list = element->aria_labelled_by().split_view(Infra::is_ascii_whitespace);
1636 } else {
1637 id_list = element->aria_described_by().split_view(Infra::is_ascii_whitespace);
1638 }
1639 // ii. For each IDREF:
1640 for (auto const& id_ref : id_list) {
1641 auto node = document.get_element_by_id(id_ref);
1642 if (!node)
1643 continue;
1644
1645 if (visited_nodes.contains(node->id()))
1646 continue;
1647 // a. Set the current node to the node referenced by the IDREF.
1648 current_node = node;
1649 // b. Compute the text alternative of the current node beginning with step 2. Set the result to that text alternative.
1650 auto result = TRY(node->name_or_description(target, document, visited_nodes));
1651 // c. Append the result, with a space, to the accumulated text.
1652 TRY(Node::append_with_space(total_accumulated_text, result));
1653 }
1654 // iii. Return the accumulated text.
1655 return total_accumulated_text.to_string();
1656 }
1657 // C. Otherwise, if computing a name, and if the current node has an aria-label attribute whose value is not the empty string, nor, when trimmed of white space, is not the empty string:
1658 if (target == NameOrDescription::Name && !element->aria_label().is_empty() && !element->aria_label().trim_whitespace().is_empty()) {
1659 // TODO: - If traversal of the current node is due to recursion and the current node is an embedded control as defined in step 2E, ignore aria-label and skip to rule 2E.
1660 // - Otherwise, return the value of aria-label.
1661 return String::from_deprecated_string(element->aria_label());
1662 }
1663 // TODO: D. Otherwise, if the current node's native markup provides an attribute (e.g. title) or element (e.g. HTML label) that defines a text alternative,
1664 // return that alternative in the form of a flat string as defined by the host language, unless the element is marked as presentational (role="presentation" or role="none").
1665
1666 // TODO: E. Otherwise, if the current node is a control embedded within the label (e.g. the label element in HTML or any element directly referenced by aria-labelledby) for another widget, where the user can adjust the embedded
1667 // control's value, then include the embedded control as part of the text alternative in the following manner:
1668 // - If the embedded control has role textbox, return its value.
1669 // - If the embedded control has role menu button, return the text alternative of the button.
1670 // - If the embedded control has role combobox or listbox, return the text alternative of the chosen option.
1671 // - If the embedded control has role range (e.g., a spinbutton or slider):
1672 // - If the aria-valuetext property is present, return its value,
1673 // - Otherwise, if the aria-valuenow property is present, return its value,
1674 // - Otherwise, use the value as specified by a host language attribute.
1675
1676 // F. Otherwise, if the current node's role allows name from content, or if the current node is referenced by aria-labelledby, aria-describedby, or is a native host language text alternative element (e.g. label in HTML), or is a descendant of a native host language text alternative element:
1677 auto role = element->role_or_default();
1678 if (role.has_value() && ARIA::allows_name_from_content(role.value())) {
1679 // i. Set the accumulated text to the empty string.
1680 total_accumulated_text.clear();
1681 // ii. Check for CSS generated textual content associated with the current node and include it in the accumulated text. The CSS :before and :after pseudo elements [CSS2] can provide textual content for elements that have a content model.
1682 auto before = element->get_pseudo_element_node(CSS::Selector::PseudoElement::Before);
1683 auto after = element->get_pseudo_element_node(CSS::Selector::PseudoElement::After);
1684 // - For :before pseudo elements, User agents MUST prepend CSS textual content, without a space, to the textual content of the current node.
1685 if (before)
1686 TRY(Node::prepend_without_space(total_accumulated_text, before->computed_values().content().data));
1687
1688 // - For :after pseudo elements, User agents MUST append CSS textual content, without a space, to the textual content of the current node.
1689 if (after)
1690 TRY(Node::append_without_space(total_accumulated_text, after->computed_values().content().data));
1691
1692 // iii. For each child node of the current node:
1693 element->for_each_child([&total_accumulated_text, current_node, target, &document, visited_nodes](
1694 DOM::Node const& child_node) mutable {
1695 if (visited_nodes.contains(child_node.id()))
1696 return;
1697
1698 // a. Set the current node to the child node.
1699 current_node = &child_node;
1700
1701 // b. Compute the text alternative of the current node beginning with step 2. Set the result to that text alternative.
1702 auto result = MUST(current_node->name_or_description(target, document, visited_nodes));
1703
1704 // c. Append the result to the accumulated text.
1705 total_accumulated_text.append(result);
1706 });
1707 // iv. Return the accumulated text.
1708 return total_accumulated_text.to_string();
1709 // Important: Each node in the subtree is consulted only once. If text has been collected from a descendant, but is referenced by another IDREF in some descendant node, then that second, or subsequent, reference is not followed. This is done to avoid infinite loops.
1710 }
1711 }
1712
1713 // G. Otherwise, if the current node is a Text node, return its textual contents.
1714 if (is_text()) {
1715 auto const* text_node = static_cast<DOM::Text const*>(this);
1716 return String::from_deprecated_string(text_node->data());
1717 }
1718 // TODO: H. Otherwise, if the current node is a descendant of an element whose Accessible Name or Accessible Description is being computed, and contains descendants, proceed to 2F.i.
1719
1720 // I. Otherwise, if the current node has a Tooltip attribute, return its value.
1721 // https://www.w3.org/TR/accname-1.2/#dfn-tooltip-attribute
1722 // Any host language attribute that would result in a user agent generating a tooltip such as in response to a mouse hover in desktop user agents.
1723 // FIXME: Support SVG tooltips and CSS tooltips
1724 if (is<HTML::HTMLElement>(this)) {
1725 auto const* element = static_cast<HTML::HTMLElement const*>(this);
1726 auto tooltip = element->title();
1727 if (!tooltip.is_empty() && !tooltip.is_null())
1728 return String::from_deprecated_string(tooltip);
1729 }
1730 // Append the result of each step above, with a space, to the total accumulated text.
1731 //
1732 // After all steps are completed, the total accumulated text is used as the accessible name or accessible description of the element that initiated the computation.
1733 return total_accumulated_text.to_string();
1734}
1735
1736// https://www.w3.org/TR/accname-1.2/#mapping_additional_nd_name
1737ErrorOr<String> Node::accessible_name(Document const& document) const
1738{
1739 HashTable<i32> visited_nodes;
1740 // User agents MUST compute an accessible name using the rules outlined below in the section titled Accessible Name and Description Computation.
1741 return name_or_description(NameOrDescription::Name, document, visited_nodes);
1742}
1743
1744// https://www.w3.org/TR/accname-1.2/#mapping_additional_nd_description
1745ErrorOr<String> Node::accessible_description(Document const& document) const
1746{
1747 // If aria-describedby is present, user agents MUST compute the accessible description by concatenating the text alternatives for elements referenced by an aria-describedby attribute on the current element.
1748 // The text alternatives for the referenced elements are computed using a number of methods, outlined below in the section titled Accessible Name and Description Computation.
1749 if (is_element()) {
1750 HashTable<i32> visited_nodes;
1751 StringBuilder builder;
1752 auto const* element = static_cast<Element const*>(this);
1753 auto id_list = element->aria_described_by().split_view(Infra::is_ascii_whitespace);
1754 for (auto const& id : id_list) {
1755 if (auto description_element = document.get_element_by_id(id)) {
1756 auto description = TRY(
1757 description_element->name_or_description(NameOrDescription::Description, document,
1758 visited_nodes));
1759 if (!description.is_empty()) {
1760 if (builder.is_empty()) {
1761 builder.append(description);
1762 } else {
1763 builder.append(" "sv);
1764 builder.append(description);
1765 }
1766 }
1767 }
1768 }
1769 return builder.to_string();
1770 }
1771 return String {};
1772}
1773
1774Optional<StringView> Node::first_valid_id(DeprecatedString const& value, Document const& document)
1775{
1776 auto id_list = value.split_view(Infra::is_ascii_whitespace);
1777 for (auto const& id : id_list) {
1778 if (document.get_element_by_id(id))
1779 return id;
1780 }
1781 return {};
1782}
1783
1784// https://www.w3.org/TR/accname-1.2/#mapping_additional_nd_te
1785ErrorOr<void> Node::append_without_space(StringBuilder x, StringView const& result)
1786{
1787 // - If X is empty, copy the result to X.
1788 // - If X is non-empty, copy the result to the end of X.
1789 TRY(x.try_append(result));
1790 return {};
1791}
1792
1793// https://www.w3.org/TR/accname-1.2/#mapping_additional_nd_te
1794ErrorOr<void> Node::append_with_space(StringBuilder x, StringView const& result)
1795{
1796 // - If X is empty, copy the result to X.
1797 if (x.is_empty()) {
1798 TRY(x.try_append(result));
1799 } else {
1800 // - If X is non-empty, add a space to the end of X and then copy the result to X after the space.
1801 TRY(x.try_append(" "sv));
1802 TRY(x.try_append(result));
1803 }
1804 return {};
1805}
1806
1807// https://www.w3.org/TR/accname-1.2/#mapping_additional_nd_te
1808ErrorOr<void> Node::prepend_without_space(StringBuilder x, StringView const& result)
1809{
1810 // - If X is empty, copy the result to X.
1811 if (x.is_empty()) {
1812 x.append(result);
1813 } else {
1814 // - If X is non-empty, copy the result to the start of X.
1815 auto temp = TRY(x.to_string());
1816 x.clear();
1817 TRY(x.try_append(result));
1818 TRY(x.try_append(temp));
1819 }
1820 return {};
1821}
1822
1823// https://www.w3.org/TR/accname-1.2/#mapping_additional_nd_te
1824ErrorOr<void> Node::prepend_with_space(StringBuilder x, StringView const& result)
1825{
1826 // - If X is empty, copy the result to X.
1827 if (x.is_empty()) {
1828 TRY(x.try_append(result));
1829 } else {
1830 // - If X is non-empty, copy the result to the start of X, and add a space after the copy.
1831 auto temp = TRY(x.to_string());
1832 x.clear();
1833 TRY(x.try_append(result));
1834 TRY(x.try_append(" "sv));
1835 TRY(x.try_append(temp));
1836 }
1837 return {};
1838}
1839
1840}