Serenity Operating System
at master 1840 lines 82 kB view raw
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([&current_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}