/* * Copyright (c) 2018-2022, Andreas Kling * Copyright (c) 2021-2022, Linus Groh * Copyright (c) 2021, Luke Wilde * Copyright (c) 2021, Sam Atkins * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace Web::DOM { Document::Document(const AK::URL& url) : ParentNode(*this, NodeType::DOCUMENT_NODE) , m_style_computer(make(*this)) , m_style_sheets(CSS::StyleSheetList::create(*this)) , m_url(url) , m_window(HTML::Window::create_with_document(*this)) , m_implementation(DOMImplementation::create({}, *this)) , m_history(HTML::History::create(*this)) { HTML::main_thread_event_loop().register_document({}, *this); m_style_update_timer = Core::Timer::create_single_shot(0, [this] { update_style(); }); m_layout_update_timer = Core::Timer::create_single_shot(0, [this] { force_layout(); }); } Document::~Document() = default; void Document::removed_last_ref() { VERIFY(!ref_count()); VERIFY(!m_deletion_has_begun); if (m_referencing_node_count) { // The document has reached ref_count==0 but still has nodes keeping it alive. // At this point, sever all the node links we control. // If nodes remain elsewhere (e.g JS wrappers), they will keep the document alive. // NOTE: This makes sure we stay alive across for the duration of the cleanup below. increment_referencing_node_count(); m_focused_element = nullptr; m_hovered_node = nullptr; m_pending_parsing_blocking_script = nullptr; m_inspected_node = nullptr; m_scripts_to_execute_when_parsing_has_finished.clear(); m_scripts_to_execute_as_soon_as_possible.clear(); m_associated_inert_template_document = nullptr; m_interpreter = nullptr; { // Gather up all the descendants of this document and prune them from the tree. // FIXME: This could definitely be more elegant. NonnullRefPtrVector descendants; for_each_in_inclusive_subtree([&](auto& node) { if (&node != this) descendants.append(node); return IterationDecision::Continue; }); for (auto& node : descendants) { VERIFY(&node.document() == this); VERIFY(!node.is_document()); if (node.parent()) node.remove(); } } m_in_removed_last_ref = false; decrement_referencing_node_count(); return; } m_in_removed_last_ref = false; m_deletion_has_begun = true; HTML::main_thread_event_loop().unregister_document({}, *this); delete this; } // https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#dom-document-write ExceptionOr Document::write(Vector const& strings) { StringBuilder builder; builder.join(""sv, strings); return run_the_document_write_steps(builder.build()); } // https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#dom-document-writeln ExceptionOr Document::writeln(Vector const& strings) { StringBuilder builder; builder.join(""sv, strings); builder.append("\n"sv); return run_the_document_write_steps(builder.build()); } // https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#document-write-steps ExceptionOr Document::run_the_document_write_steps(String input) { // 1. If document is an XML document, then throw an "InvalidStateError" DOMException. if (doctype() && doctype()->name() == "xml") return DOM::InvalidStateError::create("write() called on XML document."); // 2. If document's throw-on-dynamic-markup-insertion counter is greater than 0, then throw an "InvalidStateError" DOMException. if (m_throw_on_dynamic_markup_insertion_counter > 0) return DOM::InvalidStateError::create("throw-on-dynamic-markup-insertion-counter greater than zero."); // 3. If document's active parser was aborted is true, then return. if (m_active_parser_was_aborted) return {}; // 4. If the insertion point is undefined, then: if (!(m_parser && m_parser->tokenizer().is_insertion_point_defined())) { // 1. If document's unload counter is greater than 0 or document's ignore-destructive-writes counter is greater than 0, then return. if (m_unload_counter > 0 || m_ignore_destructive_writes_counter > 0) return {}; // 2. Run the document open steps with document. open(); } // 5. Insert input into the input stream just before the insertion point. m_parser->tokenizer().insert_input_at_insertion_point(input); // 6. If there is no pending parsing-blocking script, have the HTML parser process input, one code point at a time, processing resulting tokens as they are emitted, and stopping when the tokenizer reaches the insertion point or when the processing of the tokenizer is aborted by the tree construction stage (this can happen if a script end tag token is emitted by the tokenizer). if (!pending_parsing_blocking_script()) m_parser->run(); return {}; } // https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#dom-document-open ExceptionOr Document::open(String const&, String const&) { // 1. If document is an XML document, then throw an "InvalidStateError" DOMException exception. if (doctype() && doctype()->name() == "xml") return DOM::InvalidStateError::create("open() called on XML document."); // 2. If document's throw-on-dynamic-markup-insertion counter is greater than 0, then throw an "InvalidStateError" DOMException. if (m_throw_on_dynamic_markup_insertion_counter > 0) return DOM::InvalidStateError::create("throw-on-dynamic-markup-insertion-counter greater than zero."); // FIXME: 3. Let entryDocument be the entry global object's associated Document. auto& entry_document = *this; // 4. If document's origin is not same origin to entryDocument's origin, then throw a "SecurityError" DOMException. if (origin() != entry_document.origin()) return DOM::SecurityError::create("Document.origin() not the same as entryDocument's."); // 5. If document has an active parser whose script nesting level is greater than 0, then return document. if (m_parser && m_parser->script_nesting_level() > 0) return this; // 6. Similarly, if document's unload counter is greater than 0, then return document. if (m_unload_counter > 0) return this; // 7. If document's active parser was aborted is true, then return document. if (m_active_parser_was_aborted) return this; // FIXME: 8. If document's browsing context is non-null and there is an existing attempt to navigate document's browsing context, then stop document loading given document. // FIXME: 9. For each shadow-including inclusive descendant node of document, erase all event listeners and handlers given node. // FIXME 10. If document is the associated Document of document's relevant global object, then erase all event listeners and handlers given document's relevant global object. // 11. Replace all with null within document, without firing any mutation events. replace_all(nullptr); // 12. If document is fully active, then: if (is_fully_active()) { // 1. Let newURL be a copy of entryDocument's URL. auto new_url = entry_document.url(); // 2. If entryDocument is not document, then set newURL's fragment to null. if (&entry_document != this) new_url.set_fragment(""); // FIXME: 3. Run the URL and history update steps with document and newURL. } // FIXME: 13. Set document's is initial about:blank to false. // FIXME: 14. If document's iframe load in progress flag is set, then set document's mute iframe load flag. // 15. Set document to no-quirks mode. set_quirks_mode(QuirksMode::No); // 16. Create a new HTML parser and associate it with document. This is a script-created parser (meaning that it can be closed by the document.open() and document.close() methods, and that the tokenizer will wait for an explicit call to document.close() before emitting an end-of-file token). The encoding confidence is irrelevant. m_parser = HTML::HTMLParser::create_for_scripting(*this); // 17. Set the insertion point to point at just before the end of the input stream (which at this point will be empty). m_parser->tokenizer().update_insertion_point(); // 18. Update the current document readiness of document to "loading". update_readiness(HTML::DocumentReadyState::Loading); // 19. Return document. return this; } // https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#closing-the-input-stream ExceptionOr Document::close() { // 1. If document is an XML document, then throw an "InvalidStateError" DOMException exception. if (doctype() && doctype()->name() == "xml") return DOM::InvalidStateError::create("close() called on XML document."); // 2. If document's throw-on-dynamic-markup-insertion counter is greater than 0, then throw an "InvalidStateError" DOMException. if (m_throw_on_dynamic_markup_insertion_counter > 0) return DOM::InvalidStateError::create("throw-on-dynamic-markup-insertion-counter greater than zero."); // 3. If there is no script-created parser associated with the document, then return. if (!m_parser) return {}; // FIXME: 4. Insert an explicit "EOF" character at the end of the parser's input stream. m_parser->tokenizer().insert_eof(); // 5. If there is a pending parsing-blocking script, then return. if (pending_parsing_blocking_script()) return {}; // FIXME: 6. Run the tokenizer, processing resulting tokens as they are emitted, and stopping when the tokenizer reaches the explicit "EOF" character or spins the event loop. m_parser->run(); return {}; } Origin Document::origin() const { if (!m_url.is_valid()) return {}; return { m_url.protocol(), m_url.host(), m_url.port_or_default() }; } void Document::set_origin(const Origin& origin) { m_url.set_protocol(origin.protocol()); m_url.set_host(origin.host()); m_url.set_port(origin.port()); } void Document::schedule_style_update() { if (m_style_update_timer->is_active()) return; m_style_update_timer->start(); } void Document::schedule_layout_update() { if (m_layout_update_timer->is_active()) return; m_layout_update_timer->start(); } bool Document::is_child_allowed(const Node& node) const { switch (node.type()) { case NodeType::DOCUMENT_NODE: case NodeType::TEXT_NODE: return false; case NodeType::COMMENT_NODE: return true; case NodeType::DOCUMENT_TYPE_NODE: return !first_child_of_type(); case NodeType::ELEMENT_NODE: return !first_child_of_type(); default: return false; } } Element* Document::document_element() { return first_child_of_type(); } const Element* Document::document_element() const { return first_child_of_type(); } HTML::HTMLHtmlElement* Document::html_element() { auto* html = document_element(); if (is(html)) return verify_cast(html); return nullptr; } HTML::HTMLHeadElement* Document::head() { auto* html = html_element(); if (!html) return nullptr; return html->first_child_of_type(); } HTML::HTMLElement* Document::body() { auto* html = html_element(); if (!html) return nullptr; auto* first_body = html->first_child_of_type(); if (first_body) return first_body; auto* first_frameset = html->first_child_of_type(); if (first_frameset) return first_frameset; return nullptr; } // https://html.spec.whatwg.org/multipage/dom.html#dom-document-body ExceptionOr Document::set_body(HTML::HTMLElement* new_body) { if (!is(new_body) && !is(new_body)) return DOM::HierarchyRequestError::create("Invalid document body element, must be 'body' or 'frameset'"); auto* existing_body = body(); if (existing_body) { (void)TRY(existing_body->parent()->replace_child(*new_body, *existing_body)); return {}; } auto* document_element = this->document_element(); if (!document_element) return DOM::HierarchyRequestError::create("Missing document element"); (void)TRY(document_element->append_child(*new_body)); return {}; } String Document::title() const { auto* head_element = head(); if (!head_element) return {}; auto* title_element = head_element->first_child_of_type(); if (!title_element) return {}; auto raw_title = title_element->text_content(); StringBuilder builder; bool last_was_space = false; for (auto code_point : Utf8View(raw_title)) { if (is_ascii_space(code_point)) { last_was_space = true; } else { if (last_was_space && !builder.is_empty()) builder.append(' '); builder.append_code_point(code_point); last_was_space = false; } } return builder.to_string(); } void Document::set_title(const String& title) { auto* head_element = const_cast(head()); if (!head_element) return; RefPtr title_element = head_element->first_child_of_type(); if (!title_element) { title_element = static_ptr_cast(create_element(HTML::TagNames::title).release_value()); head_element->append_child(*title_element); } title_element->remove_all_children(true); title_element->append_child(adopt_ref(*new Text(*this, title))); if (auto* page = this->page()) { if (browsing_context() == &page->top_level_browsing_context()) page->client().page_did_change_title(title); } } void Document::attach_to_browsing_context(Badge, HTML::BrowsingContext& browsing_context) { m_browsing_context = browsing_context; update_layout(); } void Document::detach_from_browsing_context(Badge, HTML::BrowsingContext& browsing_context) { VERIFY(&browsing_context == m_browsing_context); tear_down_layout_tree(); m_browsing_context = nullptr; } void Document::tear_down_layout_tree() { if (!m_layout_root) return; // Gather up all the layout nodes in a vector and detach them from parents // while the vector keeps them alive. NonnullRefPtrVector layout_nodes; m_layout_root->for_each_in_inclusive_subtree([&](auto& layout_node) { layout_nodes.append(layout_node); return IterationDecision::Continue; }); for (auto& layout_node : layout_nodes) { if (layout_node.parent()) layout_node.parent()->remove_child(layout_node); } m_layout_root = nullptr; } Color Document::background_color(Gfx::Palette const& palette) const { // CSS2 says we should use the HTML element's background color unless it's transparent... if (auto* html_element = this->html_element(); html_element && html_element->layout_node()) { auto color = html_element->layout_node()->computed_values().background_color(); if (color.alpha()) return color; } // ...in which case we use the BODY element's background color. if (auto* body_element = body(); body_element && body_element->layout_node()) { auto color = body_element->layout_node()->computed_values().background_color(); if (color.alpha()) return color; } // If both HTML and BODY are transparent, we fall back to the system's "base" palette color. return palette.base(); } Vector const* Document::background_layers() const { auto* body_element = body(); if (!body_element) return {}; auto* body_layout_node = body_element->layout_node(); if (!body_layout_node) return {}; return &body_layout_node->background_layers(); } // https://html.spec.whatwg.org/multipage/urls-and-fetching.html#parse-a-url AK::URL Document::parse_url(String const& url) const { // FIXME: Make sure we do this according to spec. return m_url.complete_url(url); } void Document::set_needs_layout() { if (m_needs_layout) return; m_needs_layout = true; schedule_layout_update(); } void Document::force_layout() { tear_down_layout_tree(); update_layout(); } void Document::invalidate_layout() { tear_down_layout_tree(); } void Document::update_layout() { // NOTE: If our parent document needs a relayout, we must do that *first*. // This is necessary as the parent layout may cause our viewport to change. if (browsing_context() && browsing_context()->container()) browsing_context()->container()->document().update_layout(); update_style(); if (!m_needs_layout && m_layout_root) return; if (!browsing_context()) return; auto viewport_rect = browsing_context()->viewport_rect(); if (!m_layout_root) { Layout::TreeBuilder tree_builder; m_layout_root = static_ptr_cast(tree_builder.build(*this)); } Layout::FormattingState formatting_state; Layout::BlockFormattingContext root_formatting_context(formatting_state, *m_layout_root, nullptr); auto& icb = static_cast(*m_layout_root); auto& icb_state = formatting_state.get_mutable(icb); icb_state.content_width = viewport_rect.width(); icb_state.content_height = viewport_rect.height(); icb.set_has_definite_width(true); icb.set_has_definite_height(true); root_formatting_context.run(*m_layout_root, Layout::LayoutMode::Normal); formatting_state.commit(); browsing_context()->set_needs_display(); if (browsing_context()->is_top_level()) { if (auto* page = this->page()) page->client().page_did_layout(); } m_needs_layout = false; m_layout_update_timer->stop(); } [[nodiscard]] static bool update_style_recursively(DOM::Node& node) { bool const needs_full_style_update = node.document().needs_full_style_update(); bool needs_relayout = false; if (is(node)) { needs_relayout |= static_cast(node).recompute_style() == Element::NeedsRelayout::Yes; } node.set_needs_style_update(false); if (needs_full_style_update || node.child_needs_style_update()) { if (node.is_element()) { if (auto* shadow_root = static_cast(node).shadow_root()) { if (needs_full_style_update || shadow_root->needs_style_update() || shadow_root->child_needs_style_update()) needs_relayout |= update_style_recursively(*shadow_root); } } node.for_each_child([&](auto& child) { if (needs_full_style_update || child.needs_style_update() || child.child_needs_style_update()) needs_relayout |= update_style_recursively(child); return IterationDecision::Continue; }); } node.set_child_needs_style_update(false); return needs_relayout; } void Document::update_style() { if (!browsing_context()) return; if (!needs_full_style_update() && !needs_style_update() && !child_needs_style_update()) return; evaluate_media_rules(); if (update_style_recursively(*this)) invalidate_layout(); m_needs_full_style_update = false; m_style_update_timer->stop(); } void Document::set_link_color(Color color) { m_link_color = color; } void Document::set_active_link_color(Color color) { m_active_link_color = color; } void Document::set_visited_link_color(Color color) { m_visited_link_color = color; } const Layout::InitialContainingBlock* Document::layout_node() const { return static_cast(Node::layout_node()); } Layout::InitialContainingBlock* Document::layout_node() { return static_cast(Node::layout_node()); } void Document::set_inspected_node(Node* node) { if (m_inspected_node == node) return; if (m_inspected_node && m_inspected_node->layout_node()) m_inspected_node->layout_node()->set_needs_display(); m_inspected_node = node; if (m_inspected_node && m_inspected_node->layout_node()) m_inspected_node->layout_node()->set_needs_display(); } static Node* find_common_ancestor(Node* a, Node* b) { if (!a || !b) return nullptr; if (a == b) return a; HashTable ancestors; for (auto* node = a; node; node = node->parent_or_shadow_host()) ancestors.set(node); for (auto* node = b; node; node = node->parent_or_shadow_host()) { if (ancestors.contains(node)) return node; } return nullptr; } void Document::set_hovered_node(Node* node) { if (m_hovered_node == node) return; RefPtr old_hovered_node = move(m_hovered_node); m_hovered_node = node; if (auto* common_ancestor = find_common_ancestor(old_hovered_node, m_hovered_node)) common_ancestor->invalidate_style(); else invalidate_style(); } NonnullRefPtr Document::get_elements_by_name(String const& name) { return HTMLCollection::create(*this, [name](Element const& element) { return element.name() == name; }); } NonnullRefPtr Document::get_elements_by_class_name(FlyString const& class_name) { return HTMLCollection::create(*this, [class_name, quirks_mode = document().in_quirks_mode()](Element const& element) { return element.has_class(class_name, quirks_mode ? CaseSensitivity::CaseInsensitive : CaseSensitivity::CaseSensitive); }); } // https://html.spec.whatwg.org/multipage/obsolete.html#dom-document-applets NonnullRefPtr Document::applets() { // FIXME: This should return the same HTMLCollection object every time, // but that would cause a reference cycle since HTMLCollection refs the root. return HTMLCollection::create(*this, [](auto&) { return false; }); } // https://html.spec.whatwg.org/multipage/obsolete.html#dom-document-anchors NonnullRefPtr Document::anchors() { // FIXME: This should return the same HTMLCollection object every time, // but that would cause a reference cycle since HTMLCollection refs the root. return HTMLCollection::create(*this, [](Element const& element) { return is(element) && element.has_attribute(HTML::AttributeNames::name); }); } // https://html.spec.whatwg.org/multipage/dom.html#dom-document-images NonnullRefPtr Document::images() { // FIXME: This should return the same HTMLCollection object every time, // but that would cause a reference cycle since HTMLCollection refs the root. return HTMLCollection::create(*this, [](Element const& element) { return is(element); }); } // https://html.spec.whatwg.org/multipage/dom.html#dom-document-embeds NonnullRefPtr Document::embeds() { // FIXME: This should return the same HTMLCollection object every time, // but that would cause a reference cycle since HTMLCollection refs the root. return HTMLCollection::create(*this, [](Element const& element) { return is(element); }); } // https://html.spec.whatwg.org/multipage/dom.html#dom-document-plugins NonnullRefPtr Document::plugins() { return embeds(); } // https://html.spec.whatwg.org/multipage/dom.html#dom-document-links NonnullRefPtr Document::links() { // FIXME: This should return the same HTMLCollection object every time, // but that would cause a reference cycle since HTMLCollection refs the root. return HTMLCollection::create(*this, [](Element const& element) { return (is(element) || is(element)) && element.has_attribute(HTML::AttributeNames::href); }); } // https://html.spec.whatwg.org/multipage/dom.html#dom-document-forms NonnullRefPtr Document::forms() { // FIXME: This should return the same HTMLCollection object every time, // but that would cause a reference cycle since HTMLCollection refs the root. return HTMLCollection::create(*this, [](Element const& element) { return is(element); }); } // https://html.spec.whatwg.org/multipage/dom.html#dom-document-scripts NonnullRefPtr Document::scripts() { // FIXME: This should return the same HTMLCollection object every time, // but that would cause a reference cycle since HTMLCollection refs the root. return HTMLCollection::create(*this, [](Element const& element) { return is(element); }); } Color Document::link_color() const { if (m_link_color.has_value()) return m_link_color.value(); if (!page()) return Color::Blue; return page()->palette().link(); } Color Document::active_link_color() const { if (m_active_link_color.has_value()) return m_active_link_color.value(); if (!page()) return Color::Red; return page()->palette().active_link(); } Color Document::visited_link_color() const { if (m_visited_link_color.has_value()) return m_visited_link_color.value(); if (!page()) return Color::Magenta; return page()->palette().visited_link(); } // https://html.spec.whatwg.org/multipage/webappapis.html#relevant-settings-object HTML::EnvironmentSettingsObject& Document::relevant_settings_object() { // Then, the relevant settings object for a platform object o is the environment settings object of the relevant Realm for o. return verify_cast(*realm().host_defined()); } JS::Realm& Document::realm() { return interpreter().realm(); } JS::Interpreter& Document::interpreter() { if (!m_interpreter) { // FIXME: This is all ad-hoc. It loosely follows steps 6.4 to 6.9 of https://html.spec.whatwg.org/#initialise-the-document-object auto& vm = Bindings::main_thread_vm(); // https://html.spec.whatwg.org/multipage/webappapis.html#creating-a-new-javascript-realm // FIXME: Put all this into it's own function that can be used outside of Document. // 1. Perform InitializeHostDefinedRealm() with the provided customizations for creating the global object and the global this binding. // FIXME: Use WindowProxy as the global this value. m_interpreter = JS::Interpreter::create(vm, *m_window); // 2. Let realm execution context be the running JavaScript execution context. auto& realm_execution_context = vm.running_execution_context(); // 3. Remove realm execution context from the JavaScript execution context stack. vm.execution_context_stack().remove_first_matching([&realm_execution_context](auto* execution_context) { return execution_context == &realm_execution_context; }); // FIXME: 4. Let realm be realm execution context's Realm component. // FIXME: 5. Set realm's agent to agent. // FIXME: 6. If agent's agent cluster's cross-origin isolation mode is "none", then: // 1. Let global be realm's global object. // 2. Let status be ! global.[[Delete]]("SharedArrayBuffer"). // 3. Assert: status is true. // FIXME: 7. Return realm execution context. (Requires being in it's own function as mentioned above) // == End of "create a JavaScript realm" == // FIXME: 6. Let topLevelCreationURL be creationURL. // FIXME: 7. Let topLevelOrigin be navigationParams's origin. // FIXME: 8. If browsingContext is not a top-level browsing context, then: // 1. Let parentEnvironment be browsingContext's container's relevant settings object. // 2. Set topLevelCreationURL to parentEnvironment's top-level creation URL. // 3. Set topLevelOrigin to parentEnvironment's top-level origin. // FIXME: 9. Set up a window environment settings object with creationURL, realm execution context, navigationParams's reserved environment, topLevelCreationURL, and topLevelOrigin. // (This is missing reserved environment, topLevelCreationURL and topLevelOrigin. It also assumes creationURL is the document's URL, when it's really "navigationParams's response's URL.") HTML::WindowEnvironmentSettingsObject::setup(m_url, realm_execution_context); } return *m_interpreter; } JS::Value Document::run_javascript(StringView source, StringView filename) { // FIXME: The only user of this function now is javascript: URLs. Refactor them to follow the spec: https://html.spec.whatwg.org/multipage/browsing-the-web.html#javascript-protocol auto& interpreter = document().interpreter(); auto script_or_error = JS::Script::parse(source, interpreter.realm(), filename); if (script_or_error.is_error()) { // FIXME: Add error logging back. return JS::js_undefined(); } auto result = interpreter.run(script_or_error.value()); if (result.is_error()) { // FIXME: I'm sure the spec could tell us something about error propagation here! HTML::report_exception(result); return {}; } return result.value(); } // https://dom.spec.whatwg.org/#dom-document-createelement // FIXME: This only implements step 6 of the algorithm and does not take in options. DOM::ExceptionOr> Document::create_element(String const& tag_name) { if (!is_valid_name(tag_name)) return DOM::InvalidCharacterError::create("Invalid character in tag name."); // FIXME: Let namespace be the HTML namespace, if this is an HTML document or this’s content type is "application/xhtml+xml", and null otherwise. return DOM::create_element(*this, tag_name, Namespace::HTML); } // https://dom.spec.whatwg.org/#dom-document-createelementns // https://dom.spec.whatwg.org/#internal-createelementns-steps // FIXME: This only implements step 4 of the algorithm and does not take in options. DOM::ExceptionOr> Document::create_element_ns(String const& namespace_, String const& qualified_name) { // 1. Let namespace, prefix, and localName be the result of passing namespace and qualifiedName to validate and extract. auto extracted_qualified_name = TRY(validate_and_extract(namespace_, qualified_name)); // FIXME: 2. Let is be null. // FIXME: 3. If options is a dictionary and options["is"] exists, then set is to it. // 4. Return the result of creating an element given document, localName, namespace, prefix, is, and with the synchronous custom elements flag set. return DOM::create_element(*this, extracted_qualified_name.local_name(), extracted_qualified_name.namespace_(), extracted_qualified_name.prefix()); } NonnullRefPtr Document::create_document_fragment() { return adopt_ref(*new DocumentFragment(*this)); } NonnullRefPtr Document::create_text_node(const String& data) { return adopt_ref(*new Text(*this, data)); } NonnullRefPtr Document::create_comment(const String& data) { return adopt_ref(*new Comment(*this, data)); } NonnullRefPtr Document::create_range() { return Range::create(*this); } // https://dom.spec.whatwg.org/#dom-document-createevent NonnullRefPtr Document::create_event(const String& interface) { auto interface_lowercase = interface.to_lowercase(); RefPtr event; if (interface_lowercase == "beforeunloadevent") { event = Event::create(""); // FIXME: Create BeforeUnloadEvent } else if (interface_lowercase == "compositionevent") { event = Event::create(""); // FIXME: Create CompositionEvent } else if (interface_lowercase == "customevent") { event = CustomEvent::create(""); } else if (interface_lowercase == "devicemotionevent") { event = Event::create(""); // FIXME: Create DeviceMotionEvent } else if (interface_lowercase == "deviceorientationevent") { event = Event::create(""); // FIXME: Create DeviceOrientationEvent } else if (interface_lowercase == "dragevent") { event = Event::create(""); // FIXME: Create DragEvent } else if (interface_lowercase.is_one_of("event", "events")) { event = Event::create(""); } else if (interface_lowercase == "focusevent") { event = UIEvents::FocusEvent::create(""); } else if (interface_lowercase == "hashchangeevent") { event = Event::create(""); // FIXME: Create HashChangeEvent } else if (interface_lowercase == "htmlevents") { event = Event::create(""); } else if (interface_lowercase == "keyboardevent") { event = UIEvents::KeyboardEvent::create(""); } else if (interface_lowercase == "messageevent") { event = HTML::MessageEvent::create(""); } else if (interface_lowercase.is_one_of("mouseevent", "mouseevents")) { event = UIEvents::MouseEvent::create("", 0, 0, 0, 0); } else if (interface_lowercase == "storageevent") { event = Event::create(""); // FIXME: Create StorageEvent } else if (interface_lowercase == "svgevents") { event = Event::create(""); } else if (interface_lowercase == "textevent") { event = Event::create(""); // FIXME: Create CompositionEvent } else if (interface_lowercase == "touchevent") { event = Event::create(""); // FIXME: Create TouchEvent } else if (interface_lowercase.is_one_of("uievent", "uievents")) { event = UIEvents::UIEvent::create(""); } else { // FIXME: // 3. If constructor is null, then throw a "NotSupportedError" DOMException. // 4. If the interface indicated by constructor is not exposed on the relevant global object of this, then throw a "NotSupportedError" DOMException. TODO(); } // Setting type to empty string is handled by each constructor. // FIXME: // 7. Initialize event’s timeStamp attribute to a DOMHighResTimeStamp representing the high resolution time from the time origin to now. event->set_is_trusted(false); event->set_initialized(false); return event.release_nonnull(); } void Document::set_pending_parsing_blocking_script(Badge, HTML::HTMLScriptElement* script) { m_pending_parsing_blocking_script = script; } NonnullRefPtr Document::take_pending_parsing_blocking_script(Badge) { return m_pending_parsing_blocking_script.release_nonnull(); } void Document::add_script_to_execute_when_parsing_has_finished(Badge, HTML::HTMLScriptElement& script) { m_scripts_to_execute_when_parsing_has_finished.append(script); } NonnullRefPtrVector Document::take_scripts_to_execute_when_parsing_has_finished(Badge) { return move(m_scripts_to_execute_when_parsing_has_finished); } void Document::add_script_to_execute_as_soon_as_possible(Badge, HTML::HTMLScriptElement& script) { m_scripts_to_execute_as_soon_as_possible.append(script); } NonnullRefPtrVector Document::take_scripts_to_execute_as_soon_as_possible(Badge) { return move(m_scripts_to_execute_as_soon_as_possible); } // https://dom.spec.whatwg.org/#dom-document-importnode ExceptionOr> Document::import_node(NonnullRefPtr node, bool deep) { // 1. If node is a document or shadow root, then throw a "NotSupportedError" DOMException. if (is(*node) || is(*node)) return DOM::NotSupportedError::create("Cannot import a document or shadow root."); // 2. Return a clone of node, with this and the clone children flag set if deep is true. return node->clone_node(this, deep); } // https://dom.spec.whatwg.org/#concept-node-adopt void Document::adopt_node(Node& node) { auto& old_document = node.document(); if (node.parent()) node.remove(); if (&old_document != this) { // FIXME: This should be shadow-including. node.for_each_in_inclusive_subtree([&](auto& inclusive_descendant) { inclusive_descendant.set_document({}, *this); // FIXME: If inclusiveDescendant is an element, then set the node document of each attribute in inclusiveDescendant’s attribute list to document. return IterationDecision::Continue; }); // FIXME: For each inclusiveDescendant in node’s shadow-including inclusive descendants that is custom, // enqueue a custom element callback reaction with inclusiveDescendant, callback name "adoptedCallback", // and an argument list containing oldDocument and document. // FIXME: This should be shadow-including. node.for_each_in_inclusive_subtree([&](auto& inclusive_descendant) { inclusive_descendant.adopted_from(old_document); return IterationDecision::Continue; }); // Transfer NodeIterators rooted at `node` from old_document to this document. Vector node_iterators_to_transfer; for (auto* node_iterator : old_document.m_node_iterators) { if (node_iterator->root() == &node) node_iterators_to_transfer.append(*node_iterator); } for (auto& node_iterator : node_iterators_to_transfer) { old_document.m_node_iterators.remove(&node_iterator); m_node_iterators.set(&node_iterator); } } } // https://dom.spec.whatwg.org/#dom-document-adoptnode ExceptionOr> Document::adopt_node_binding(NonnullRefPtr node) { if (is(*node)) return DOM::NotSupportedError::create("Cannot adopt a document into a document"); if (is(*node)) return DOM::HierarchyRequestError::create("Cannot adopt a shadow root into a document"); if (is(*node) && verify_cast(*node).host()) return node; adopt_node(*node); return node; } const DocumentType* Document::doctype() const { return first_child_of_type(); } const String& Document::compat_mode() const { static String back_compat = "BackCompat"; static String css1_compat = "CSS1Compat"; if (m_quirks_mode == QuirksMode::Yes) return back_compat; return css1_compat; } bool Document::is_editable() const { return m_editable; } void Document::set_focused_element(Element* element) { if (m_focused_element == element) return; if (m_focused_element) { m_focused_element->did_lose_focus(); m_focused_element->set_needs_style_update(true); } m_focused_element = element; if (m_focused_element) { m_focused_element->did_receive_focus(); m_focused_element->set_needs_style_update(true); } if (m_layout_root) m_layout_root->set_needs_display(); } void Document::set_active_element(Element* element) { if (m_active_element == element) return; m_active_element = element; if (m_layout_root) m_layout_root->set_needs_display(); } String Document::ready_state() const { switch (m_readiness) { case HTML::DocumentReadyState::Loading: return "loading"sv; case HTML::DocumentReadyState::Interactive: return "interactive"sv; case HTML::DocumentReadyState::Complete: return "complete"sv; } VERIFY_NOT_REACHED(); } // https://html.spec.whatwg.org/#update-the-current-document-readiness void Document::update_readiness(HTML::DocumentReadyState readiness_value) { // 1. If document's current document readiness equals readinessValue, then return. if (m_readiness == readiness_value) return; // The spec doesn't actually mention updating the current readiness value. // FIXME: https://github.com/whatwg/html/issues/7120 m_readiness = readiness_value; // FIXME: 2. If document is associated with an HTML parser, then: // FIXME: 1. If document is associated with an HTML parser, then: // FIXME: 2. If readinessValue is "complete", and document's load timing info's DOM complete time is 0, then set document's load timing info's DOM complete time to now. // FIXME: 3. Otherwise, if readinessValue is "interactive", and document's load timing info's DOM interactive time is 0, then set document's load timing info's DOM interactive time to now. // 3. Fire an event named readystatechange at document. dispatch_event(Event::create(HTML::EventNames::readystatechange)); } Page* Document::page() { return m_browsing_context ? m_browsing_context->page() : nullptr; } const Page* Document::page() const { return m_browsing_context ? m_browsing_context->page() : nullptr; } EventTarget* Document::get_parent(const Event& event) { if (event.type() == HTML::EventNames::load) return nullptr; return &window(); } // https://html.spec.whatwg.org/multipage/browsing-the-web.html#completely-finish-loading void Document::completely_finish_loading() { // 1. Assert: document's browsing context is non-null. VERIFY(browsing_context()); // FIXME: 2. Set document's completely loaded time to the current time. // 3. Let container be document's browsing context's container. auto* container = browsing_context()->container(); // If container is an iframe element, then queue an element task on the DOM manipulation task source given container to run the iframe load event steps given container. if (container && is(*container)) { container->queue_an_element_task(HTML::Task::Source::DOMManipulation, [container]() mutable { run_iframe_load_event_steps(static_cast(*container)); }); } // Otherwise, if container is non-null, then queue an element task on the DOM manipulation task source given container to fire an event named load at container. else if (container) { container->queue_an_element_task(HTML::Task::Source::DOMManipulation, [container]() mutable { container->dispatch_event(DOM::Event::create(HTML::EventNames::load)); }); } } String Document::cookie(Cookie::Source source) { if (auto* page = this->page()) return page->client().page_did_request_cookie(m_url, source); return {}; } void Document::set_cookie(String const& cookie_string, Cookie::Source source) { auto cookie = Cookie::parse_cookie(cookie_string); if (!cookie.has_value()) return; if (auto* page = this->page()) page->client().page_did_set_cookie(m_url, cookie.value(), source); } String Document::dump_dom_tree_as_json() const { StringBuilder builder; auto json = MUST(JsonObjectSerializer<>::try_create(builder)); serialize_tree_as_json(json); MUST(json.finish()); return builder.to_string(); } // https://html.spec.whatwg.org/multipage/semantics.html#has-a-style-sheet-that-is-blocking-scripts bool Document::has_a_style_sheet_that_is_blocking_scripts() const { // A Document has a style sheet that is blocking scripts if its script-blocking style sheet counter is greater than 0, if (m_script_blocking_style_sheet_counter > 0) return true; // ...or if that Document has a non-null browsing context whose container document is non-null and has a script-blocking style sheet counter greater than 0. if (!browsing_context() || !browsing_context()->container_document()) return false; return browsing_context()->container_document()->m_script_blocking_style_sheet_counter > 0; } String Document::referrer() const { // FIXME: Return the document's actual referrer. return ""; } // https://html.spec.whatwg.org/multipage/browsers.html#fully-active bool Document::is_fully_active() const { // A Document d is said to be fully active when d's browsing context is non-null, d's browsing context's active document is d, // and either d's browsing context is a top-level browsing context, or d's browsing context's container document is fully active. return browsing_context() && browsing_context()->active_document() == this && (browsing_context()->is_top_level() || browsing_context()->container_document()->is_fully_active()); } // https://html.spec.whatwg.org/multipage/browsers.html#active-document bool Document::is_active() const { // A browsing context's active document is its active window's associated Document. return browsing_context() && browsing_context()->active_document() == this; } // https://html.spec.whatwg.org/multipage/history.html#dom-document-location Bindings::LocationObject* Document::location() { // The Document object's location attribute's getter must return this Document object's relevant global object's Location object, // if this Document object is fully active, and null otherwise. if (!is_fully_active()) return nullptr; return window().wrapper()->location_object(); } // https://html.spec.whatwg.org/multipage/interaction.html#dom-document-hidden bool Document::hidden() const { return false; } // https://html.spec.whatwg.org/multipage/interaction.html#dom-document-visibilitystate String Document::visibility_state() const { return hidden() ? "hidden" : "visible"; } // https://drafts.csswg.org/cssom-view/#run-the-resize-steps void Document::run_the_resize_steps() { // 1. If doc’s viewport has had its width or height changed // (e.g. as a result of the user resizing the browser window, or changing the page zoom scale factor, // or an iframe element’s dimensions are changed) since the last time these steps were run, // fire an event named resize at the Window object associated with doc. if (!browsing_context()) return; auto viewport_size = browsing_context()->viewport_rect().size(); if (m_last_viewport_size == viewport_size) return; m_last_viewport_size = viewport_size; window().dispatch_event(DOM::Event::create(UIEvents::EventNames::resize)); update_layout(); } void Document::add_media_query_list(NonnullRefPtr& media_query_list) { m_media_query_lists.append(media_query_list); } // https://drafts.csswg.org/cssom-view/#evaluate-media-queries-and-report-changes void Document::evaluate_media_queries_and_report_changes() { // NOTE: Not in the spec, but we take this opportunity to prune null WeakPtrs. m_media_query_lists.remove_all_matching([](auto& it) { return it.is_null(); }); // 1. For each MediaQueryList object target that has doc as its document, // in the order they were created, oldest first, run these substeps: for (auto& media_query_list_ptr : m_media_query_lists) { // 1.1. If target’s matches state has changed since the last time these steps // were run, fire an event at target using the MediaQueryListEvent constructor, // with its type attribute initialized to change, its isTrusted attribute // initialized to true, its media attribute initialized to target’s media, // and its matches attribute initialized to target’s matches state. if (media_query_list_ptr.is_null()) continue; auto media_query_list = media_query_list_ptr.strong_ref(); bool did_match = media_query_list->matches(); bool now_matches = media_query_list->evaluate(); if (did_match != now_matches) { CSS::MediaQueryListEventInit init; init.media = media_query_list->media(); init.matches = now_matches; auto event = CSS::MediaQueryListEvent::create(HTML::EventNames::change, init); event->set_is_trusted(true); media_query_list->dispatch_event(event); } } // Also not in the spec, but this is as good a place as any to evaluate @media rules! evaluate_media_rules(); } void Document::evaluate_media_rules() { bool any_media_queries_changed_match_state = false; for (auto& style_sheet : style_sheets().sheets()) { if (style_sheet.evaluate_media_queries(window())) any_media_queries_changed_match_state = true; } if (any_media_queries_changed_match_state) { style_computer().invalidate_rule_cache(); invalidate_style(); } } NonnullRefPtr Document::implementation() const { return *m_implementation; } bool Document::has_focus() const { // FIXME: Return whether we actually have focus. return true; } void Document::set_parser(Badge, HTML::HTMLParser& parser) { m_parser = parser; } void Document::detach_parser(Badge) { m_parser = nullptr; } // https://www.w3.org/TR/xml/#NT-NameStartChar static bool is_valid_name_start_character(u32 code_point) { return code_point == ':' || (code_point >= 'A' && code_point <= 'Z') || code_point == '_' || (code_point >= 'a' && code_point <= 'z') || (code_point >= 0xc0 && code_point <= 0xd6) || (code_point >= 0xd8 && code_point <= 0xf6) || (code_point >= 0xf8 && code_point <= 0x2ff) || (code_point >= 0x370 && code_point <= 0x37d) || (code_point >= 0x37f && code_point <= 0x1fff) || (code_point >= 0x200c && code_point <= 0x200d) || (code_point >= 0x2070 && code_point <= 0x218f) || (code_point >= 0x2c00 && code_point <= 0x2fef) || (code_point >= 0x3001 && code_point <= 0xD7ff) || (code_point >= 0xf900 && code_point <= 0xfdcf) || (code_point >= 0xfdf0 && code_point <= 0xfffd) || (code_point >= 0x10000 && code_point <= 0xeffff); } // https://www.w3.org/TR/xml/#NT-NameChar static inline bool is_valid_name_character(u32 code_point) { return is_valid_name_start_character(code_point) || code_point == '-' || code_point == '.' || (code_point >= '0' && code_point <= '9') || code_point == 0xb7 || (code_point >= 0x300 && code_point <= 0x36f) || (code_point >= 0x203f && code_point <= 0x2040); } bool Document::is_valid_name(String const& name) { if (name.is_empty()) return false; if (!is_valid_name_start_character(name[0])) return false; for (size_t i = 1; i < name.length(); ++i) { if (!is_valid_name_character(name[i])) return false; } return true; } // https://dom.spec.whatwg.org/#validate ExceptionOr Document::validate_qualified_name(String const& qualified_name) { if (qualified_name.is_empty()) return InvalidCharacterError::create("Empty string is not a valid qualified name."); Utf8View utf8view { qualified_name }; if (!utf8view.validate()) return InvalidCharacterError::create("Invalid qualified name."); Optional colon_offset; bool at_start_of_name = true; for (auto it = utf8view.begin(); it != utf8view.end(); ++it) { auto code_point = *it; if (code_point == ':') { if (colon_offset.has_value()) return InvalidCharacterError::create("More than one colon (:) in qualified name."); colon_offset = utf8view.byte_offset_of(it); at_start_of_name = true; continue; } if (at_start_of_name) { if (!is_valid_name_start_character(code_point)) return InvalidCharacterError::create("Invalid start of qualified name."); at_start_of_name = false; continue; } if (!is_valid_name_character(code_point)) return InvalidCharacterError::create("Invalid character in qualified name."); } if (!colon_offset.has_value()) return Document::PrefixAndTagName { .prefix = {}, .tag_name = qualified_name, }; if (*colon_offset == 0) return InvalidCharacterError::create("Qualified name can't start with colon (:)."); if (*colon_offset >= (qualified_name.length() - 1)) return InvalidCharacterError::create("Qualified name can't end with colon (:)."); return Document::PrefixAndTagName { .prefix = qualified_name.substring_view(0, *colon_offset), .tag_name = qualified_name.substring_view(*colon_offset + 1), }; } // https://dom.spec.whatwg.org/#dom-document-createnodeiterator NonnullRefPtr Document::create_node_iterator(Node& root, unsigned what_to_show, RefPtr filter) { return NodeIterator::create(root, what_to_show, move(filter)); } // https://dom.spec.whatwg.org/#dom-document-createtreewalker NonnullRefPtr Document::create_tree_walker(Node& root, unsigned what_to_show, RefPtr filter) { return TreeWalker::create(root, what_to_show, move(filter)); } void Document::register_node_iterator(Badge, NodeIterator& node_iterator) { auto result = m_node_iterators.set(&node_iterator); VERIFY(result == AK::HashSetResult::InsertedNewEntry); } void Document::unregister_node_iterator(Badge, NodeIterator& node_iterator) { bool was_removed = m_node_iterators.remove(&node_iterator); VERIFY(was_removed); } void Document::increment_number_of_things_delaying_the_load_event(Badge) { ++m_number_of_things_delaying_the_load_event; if (auto* page = this->page()) page->client().page_did_update_resource_count(m_number_of_things_delaying_the_load_event); } void Document::decrement_number_of_things_delaying_the_load_event(Badge) { VERIFY(m_number_of_things_delaying_the_load_event); --m_number_of_things_delaying_the_load_event; if (auto* page = this->page()) page->client().page_did_update_resource_count(m_number_of_things_delaying_the_load_event); } void Document::invalidate_stacking_context_tree() { if (auto* paint_box = this->paint_box()) const_cast(paint_box)->invalidate_stacking_context(); } }