/*
 * Copyright (c) 2018-2025, Andreas Kling <andreas@ladybird.org>
 * Copyright (c) 2021-2023, Linus Groh <linusg@serenityos.org>
 * Copyright (c) 2021-2025, Luke Wilde <luke@ladybird.org>
 * Copyright (c) 2021-2026, Sam Atkins <sam@ladybird.org>
 * Copyright (c) 2024, Matthew Olsson <mattco@serenityos.org>
 * Copyright (c) 2025, Jelle Raaijmakers <jelle@ladybird.org>
 * Copyright (c) 2025, Simon Farre <simon.farre.cx@gmail.com>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include <AK/Bitmap.h>
#include <AK/CharacterTypes.h>
#include <AK/Debug.h>
#include <AK/GenericLexer.h>
#include <AK/InsertionSort.h>
#include <AK/JsonObjectSerializer.h>
#include <AK/Random.h>
#include <AK/StringBuilder.h>
#include <AK/Time.h>
#include <AK/Utf8View.h>
#include <LibCore/Timer.h>
#include <LibGC/RootVector.h>
#include <LibGC/Timer.h>
#include <LibHTTP/Cookie/Cookie.h>
#include <LibHTTP/Cookie/ParsedCookie.h>
#include <LibJS/Console.h>
#include <LibJS/Runtime/Array.h>
#include <LibJS/Runtime/FunctionObject.h>
#include <LibJS/Runtime/NativeFunction.h>
#include <LibTextCodec/Decoder.h>
#include <LibURL/Origin.h>
#include <LibURL/Parser.h>
#include <LibUnicode/Segmenter.h>
#include <LibWeb/Animations/Animation.h>
#include <LibWeb/Animations/AnimationPlaybackEvent.h>
#include <LibWeb/Animations/AnimationTimeline.h>
#include <LibWeb/Animations/DocumentTimeline.h>
#include <LibWeb/Animations/TimeValue.h>
#include <LibWeb/Bindings/Document.h>
#include <LibWeb/Bindings/IntersectionObserver.h>
#include <LibWeb/Bindings/MainThreadVM.h>
#include <LibWeb/Bindings/PrincipalHostDefined.h>
#include <LibWeb/CSS/AnimationEvent.h>
#include <LibWeb/CSS/CSSAnimation.h>
#include <LibWeb/CSS/CSSImportRule.h>
#include <LibWeb/CSS/CSSPropertyRule.h>
#include <LibWeb/CSS/CSSStyleSheet.h>
#include <LibWeb/CSS/CSSTransition.h>
#include <LibWeb/CSS/ComputedProperties.h>
#include <LibWeb/CSS/FontComputer.h>
#include <LibWeb/CSS/FontFaceSet.h>
#include <LibWeb/CSS/Invalidation/HasMutationInvalidator.h>
#include <LibWeb/CSS/Invalidation/MediaQueryInvalidator.h>
#include <LibWeb/CSS/Invalidation/PseudoClassInvalidator.h>
#include <LibWeb/CSS/Invalidation/SlotInvalidator.h>
#include <LibWeb/CSS/Invalidation/StyleInvalidator.h>
#include <LibWeb/CSS/InvalidationSet.h>
#include <LibWeb/CSS/MediaQueryList.h>
#include <LibWeb/CSS/MediaQueryListEvent.h>
#include <LibWeb/CSS/Parser/Parser.h>
#include <LibWeb/CSS/SelectorEngine.h>
#include <LibWeb/CSS/StyleComputer.h>
#include <LibWeb/CSS/StyleInvalidation.h>
#include <LibWeb/CSS/StyleSheetIdentifier.h>
#include <LibWeb/CSS/StyleSheetList.h>
#include <LibWeb/CSS/StyleValues/ColorSchemeStyleValue.h>
#include <LibWeb/CSS/StyleValues/GuaranteedInvalidStyleValue.h>
#include <LibWeb/CSS/StyleValues/RandomValueSharingStyleValue.h>
#include <LibWeb/CSS/SystemColor.h>
#include <LibWeb/CSS/TransitionEvent.h>
#include <LibWeb/CSS/VisualViewport.h>
#include <LibWeb/Compositor/AsyncScrollingState.h>
#include <LibWeb/ContentSecurityPolicy/Directives/Directive.h>
#include <LibWeb/ContentSecurityPolicy/Policy.h>
#include <LibWeb/ContentSecurityPolicy/PolicyList.h>
#include <LibWeb/DOM/AbstractElement.h>
#include <LibWeb/DOM/AccessibilityTreeNode.h>
#include <LibWeb/DOM/AdoptedStyleSheets.h>
#include <LibWeb/DOM/Attr.h>
#include <LibWeb/DOM/CDATASection.h>
#include <LibWeb/DOM/Comment.h>
#include <LibWeb/DOM/CustomEvent.h>
#include <LibWeb/DOM/DOMImplementation.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/DocumentFragment.h>
#include <LibWeb/DOM/DocumentObserver.h>
#include <LibWeb/DOM/DocumentOrShadowRoot.h>
#include <LibWeb/DOM/DocumentType.h>
#include <LibWeb/DOM/EditingHostManager.h>
#include <LibWeb/DOM/Element.h>
#include <LibWeb/DOM/ElementByIdMap.h>
#include <LibWeb/DOM/ElementFactory.h>
#include <LibWeb/DOM/Event.h>
#include <LibWeb/DOM/HTMLCollection.h>
#include <LibWeb/DOM/InputEventsTarget.h>
#include <LibWeb/DOM/LiveNodeList.h>
#include <LibWeb/DOM/NodeIterator.h>
#include <LibWeb/DOM/Position.h>
#include <LibWeb/DOM/ProcessingInstruction.h>
#include <LibWeb/DOM/Range.h>
#include <LibWeb/DOM/ShadowRoot.h>
#include <LibWeb/DOM/Text.h>
#include <LibWeb/DOM/TreeWalker.h>
#include <LibWeb/DOM/Utils.h>
#include <LibWeb/DOMURL/DOMURL.h>
#include <LibWeb/Dump.h>
#include <LibWeb/Fetch/Infrastructure/HTTP/Responses.h>
#include <LibWeb/FileAPI/BlobURLStore.h>
#include <LibWeb/HTML/AttributeNames.h>
#include <LibWeb/HTML/BeforeUnloadEvent.h>
#include <LibWeb/HTML/BrowsingContext.h>
#include <LibWeb/HTML/BrowsingContextGroup.h>
#include <LibWeb/HTML/CustomElements/CustomElementDefinition.h>
#include <LibWeb/HTML/CustomElements/CustomElementReactionNames.h>
#include <LibWeb/HTML/CustomElements/CustomElementRegistry.h>
#include <LibWeb/HTML/DocumentState.h>
#include <LibWeb/HTML/DragEvent.h>
#include <LibWeb/HTML/EventLoop/EventLoop.h>
#include <LibWeb/HTML/EventNames.h>
#include <LibWeb/HTML/Focus.h>
#include <LibWeb/HTML/HTMLAllCollection.h>
#include <LibWeb/HTML/HTMLAnchorElement.h>
#include <LibWeb/HTML/HTMLAreaElement.h>
#include <LibWeb/HTML/HTMLBaseElement.h>
#include <LibWeb/HTML/HTMLBodyElement.h>
#include <LibWeb/HTML/HTMLDialogElement.h>
#include <LibWeb/HTML/HTMLDocument.h>
#include <LibWeb/HTML/HTMLEmbedElement.h>
#include <LibWeb/HTML/HTMLFormElement.h>
#include <LibWeb/HTML/HTMLFrameSetElement.h>
#include <LibWeb/HTML/HTMLHeadElement.h>
#include <LibWeb/HTML/HTMLHtmlElement.h>
#include <LibWeb/HTML/HTMLIFrameElement.h>
#include <LibWeb/HTML/HTMLImageElement.h>
#include <LibWeb/HTML/HTMLInputElement.h>
#include <LibWeb/HTML/HTMLLinkElement.h>
#include <LibWeb/HTML/HTMLMetaElement.h>
#include <LibWeb/HTML/HTMLObjectElement.h>
#include <LibWeb/HTML/HTMLScriptElement.h>
#include <LibWeb/HTML/HTMLStyleElement.h>
#include <LibWeb/HTML/HTMLTextAreaElement.h>
#include <LibWeb/HTML/HTMLTitleElement.h>
#include <LibWeb/HTML/HashChangeEvent.h>
#include <LibWeb/HTML/History.h>
#include <LibWeb/HTML/ListOfAvailableImages.h>
#include <LibWeb/HTML/Location.h>
#include <LibWeb/HTML/MessageEvent.h>
#include <LibWeb/HTML/MessagePort.h>
#include <LibWeb/HTML/Navigable.h>
#include <LibWeb/HTML/NavigableContainer.h>
#include <LibWeb/HTML/Navigation.h>
#include <LibWeb/HTML/NavigationParams.h>
#include <LibWeb/HTML/Numbers.h>
#include <LibWeb/HTML/Parser/HTMLParser.h>
#include <LibWeb/HTML/PolicyContainers.h>
#include <LibWeb/HTML/PopStateEvent.h>
#include <LibWeb/HTML/Scripting/Agent.h>
#include <LibWeb/HTML/Scripting/ClassicScript.h>
#include <LibWeb/HTML/Scripting/Environments.h>
#include <LibWeb/HTML/Scripting/ExceptionReporter.h>
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
#include <LibWeb/HTML/Scripting/WindowEnvironmentSettingsObject.h>
#include <LibWeb/HTML/SharedResourceRequest.h>
#include <LibWeb/HTML/Storage.h>
#include <LibWeb/HTML/StorageEvent.h>
#include <LibWeb/HTML/StructuredSerialize.h>
#include <LibWeb/HTML/TraversableNavigable.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/HTML/WindowProxy.h>
#include <LibWeb/HighResolutionTime/Performance.h>
#include <LibWeb/HighResolutionTime/TimeOrigin.h>
#include <LibWeb/Infra/CharacterTypes.h>
#include <LibWeb/Infra/Strings.h>
#include <LibWeb/IntersectionObserver/IntersectionObserver.h>
#include <LibWeb/Layout/BlockFormattingContext.h>
#include <LibWeb/Layout/SVGFormattingContext.h>
#include <LibWeb/Layout/SVGSVGBox.h>
#include <LibWeb/Layout/TreeBuilder.h>
#include <LibWeb/Layout/Viewport.h>
#include <LibWeb/Namespace.h>
#include <LibWeb/Page/EventHandler.h>
#include <LibWeb/Page/Page.h>
#include <LibWeb/Painting/AccumulatedVisualContext.h>
#include <LibWeb/Painting/DisplayList.h>
#include <LibWeb/Painting/DisplayListCommand.h>
#include <LibWeb/Painting/DisplayListRecorder.h>
#include <LibWeb/Painting/PaintableBox.h>
#include <LibWeb/Painting/StackingContext.h>
#include <LibWeb/Painting/ViewportPaintable.h>
#include <LibWeb/PermissionsPolicy/AutoplayAllowlist.h>
#include <LibWeb/Platform/EventLoopPlugin.h>
#include <LibWeb/ResizeObserver/ResizeObserver.h>
#include <LibWeb/ResizeObserver/ResizeObserverEntry.h>
#include <LibWeb/SVG/SVGDecodedImageData.h>
#include <LibWeb/SVG/SVGElement.h>
#include <LibWeb/SVG/SVGSVGElement.h>
#include <LibWeb/SVG/SVGScriptElement.h>
#include <LibWeb/SVG/SVGStyleElement.h>
#include <LibWeb/SVG/SVGTitleElement.h>
#include <LibWeb/Selection/Selection.h>
#include <LibWeb/TrustedTypes/RequireTrustedTypesForDirective.h>
#include <LibWeb/TrustedTypes/TrustedTypePolicy.h>
#include <LibWeb/UIEvents/CompositionEvent.h>
#include <LibWeb/UIEvents/EventNames.h>
#include <LibWeb/UIEvents/FocusEvent.h>
#include <LibWeb/UIEvents/KeyboardEvent.h>
#include <LibWeb/UIEvents/MouseEvent.h>
#include <LibWeb/UIEvents/PointerEvent.h>
#include <LibWeb/UIEvents/PointerTypes.h>
#include <LibWeb/UIEvents/TextEvent.h>
#include <LibWeb/ViewTransition/ViewTransition.h>
#include <LibWeb/WebIDL/AbstractOperations.h>
#include <LibWeb/WebIDL/DOMException.h>
#include <LibWeb/WebIDL/ExceptionOr.h>
#include <LibWeb/WebIDL/ObservableArray.h>
#include <LibWeb/XPath/XPath.h>

namespace Web::DOM {

GC_DEFINE_ALLOCATOR(Document);

static Optional<u64> s_style_invalidation_counter_dump_interval;

static void dump_style_invalidation_counters(Document const& document)
{
    auto const& counters = document.style_invalidation_counters();
    dbgln("Style invalidation counters for {}: styleInvalidations={}, fullStyleInvalidations={}, elementStyleRecomputations={}, elementStyleNoopRecomputations={}, elementInheritedStyleRecomputations={}, elementInheritedStyleNoopRecomputations={}, previousSiblingInvalidationWalkVisits={}, hasAncestorWalkInvocations={}, hasAncestorWalkVisits={}, hasAncestorSiblingElementChecks={}, hasInvalidationMetadataCandidates={}, hasMatchInvocations={}, hasResultCacheHits={}, hasResultCacheMisses={}",
        document.url_string(),
        counters.style_invalidations,
        counters.full_style_invalidations,
        counters.element_style_recomputations,
        counters.element_style_noop_recomputations,
        counters.element_inherited_style_recomputations,
        counters.element_inherited_style_noop_recomputations,
        counters.previous_sibling_invalidation_walk_visits,
        counters.has_ancestor_walk_invocations,
        counters.has_ancestor_walk_visits,
        counters.has_ancestor_sibling_element_checks,
        counters.has_invalidation_metadata_candidates,
        counters.has_match_invocations,
        counters.has_result_cache_hits,
        counters.has_result_cache_misses);
}

// https://html.spec.whatwg.org/multipage/origin.html#obtain-browsing-context-navigation
static GC::Ref<HTML::BrowsingContext> obtain_a_browsing_context_to_use_for_a_navigation_response(HTML::NavigationParams const& navigation_params)
{
    // 1. Let browsingContext be navigationParams's navigable's active browsing context.
    auto& browsing_context = *navigation_params.navigable->active_browsing_context();

    // 2. If browsingContext is not a top-level browsing context, return browsingContext.
    if (!browsing_context.is_top_level())
        return browsing_context;

    // 3. Let coopEnforcementResult be navigationParams's COOP enforcement result.
    auto& coop_enforcement_result = navigation_params.coop_enforcement_result;

    // 4. Let swapGroup be coopEnforcementResult's needs a browsing context group switch.
    auto swap_group = coop_enforcement_result.needs_a_browsing_context_group_switch;

    // 5. Let sourceOrigin be browsingContext's active document's origin.
    auto& source_origin = browsing_context.active_document()->origin();

    // 6. Let destinationOrigin be navigationParams's origin.
    auto& destination_origin = navigation_params.origin;

    // 7. If sourceOrigin is not same site with destinationOrigin:
    if (!source_origin.is_same_site(destination_origin)) {
        // FIXME: 1. If either of sourceOrigin or destinationOrigin have a scheme that is not an HTTP(S) scheme
        //    and the user agent considers it necessary for sourceOrigin and destinationOrigin to be
        //    isolated from each other (for implementation-defined reasons), optionally set swapGroup to true.

        // FIXME: 2. If navigationParams's user involvement is "browser UI", optionally set swapGroup to true.
    }

    // FIXME: 8. If browsingContext's group's browsing context set's size is 1, optionally set swapGroup to true.

    // 9. If swapGroup is false, then:
    if (!swap_group) {
        // 1. If coopEnforcementResult's would need a browsing context group switch due to report-only is true,
        //    set browsingContext's virtual browsing context group ID to a new unique identifier.
        if (coop_enforcement_result.would_need_a_browsing_context_group_switch_due_to_report_only) {
            // FIXME: set browsingContext's virtual browsing context group ID to a new unique identifier.
        }

        // 2. Return browsingContext.
        return browsing_context;
    }

    // 10. Let newBrowsingContext be the first return value of creating a new top-level browsing context and document.
    auto browsing_context_and_document = HTML::create_a_new_top_level_browsing_context_and_document(browsing_context.page());
    auto new_browsing_context = browsing_context_and_document.browsing_context;

    // 11. Let navigationCOOP be navigationParams's cross-origin opener policy.
    auto navigation_coop = navigation_params.opener_policy;

    // FIXME: 12. If navigationCOOP's value is "same-origin-plus-COEP", then set newBrowsingContext's group's cross-origin
    //     isolation mode to either "logical" or "concrete". The choice of which is implementation-defined.

    // 13. Let sandboxFlags be a clone of navigationParams's final sandboxing flag set.
    auto sandbox_flags = navigation_params.final_sandboxing_flag_set;

    // 14. If sandboxFlags is not empty, then:
    if (!is_empty(sandbox_flags)) {
        // 1. Assert: navigationCOOP's value is "unsafe-none".
        VERIFY(navigation_coop.value == HTML::OpenerPolicyValue::UnsafeNone);

        // 2. Assert: newBrowsingContext's popup sandboxing flag set is empty.
        VERIFY(is_empty(new_browsing_context->popup_sandboxing_flag_set()));

        // 3. Set newBrowsingContext's popup sandboxing flag set to sandboxFlags.
        new_browsing_context->set_popup_sandboxing_flag_set(sandbox_flags);
    }

    // 15. Return newBrowsingContext.
    return new_browsing_context;
}

// https://html.spec.whatwg.org/multipage/browsing-the-web.html#initialise-the-document-object
WebIDL::ExceptionOr<GC::Ref<Document>> Document::create_and_initialize(Type type, String content_type, HTML::NavigationParams const& navigation_params)
{
    // 1. Let browsingContext be the result of obtaining a browsing context to use for a navigation response given navigationParams.
    auto browsing_context = obtain_a_browsing_context_to_use_for_a_navigation_response(navigation_params);

    // FIXME: 2. Let permissionsPolicy be the result of creating a permissions policy from a response given navigationParams's navigable's container, navigationParams's origin, and navigationParams's response.

    // 3. Let creationURL be navigationParams's response's URL.
    auto creation_url = navigation_params.response->url();

    // 4. If navigationParams's request is non-null, then set creationURL to navigationParams's request's current URL.
    if (navigation_params.request) {
        creation_url = navigation_params.request->current_url();
    }

    // 5. Let window be null.
    GC::Ptr<HTML::Window> window;

    // 6. If browsingContext's active document's is initial about:blank is true,
    //    and browsingContext's active document's origin is same origin-domain with navigationParams's origin,
    //    then set window to browsingContext's active window.
    // FIXME: still_on_its_initial_about_blank_document() is not in the spec anymore.
    //        However, replacing this with the spec-mandated is_initial_about_blank() results in the browsing context
    //        holding an incorrect active document for the replace from initial about:blank to the real document.
    //        See #22293 for more details.
    if (false
        && (browsing_context->active_document() && browsing_context->active_document()->origin().is_same_origin(navigation_params.origin))) {
        window = browsing_context->active_window();
    }

    // 7. Otherwise:
    else {
        // FIXME: 1. Let oacHeader be the result of getting a structured field value given `Origin-Agent-Cluster` and "item" from response's header list.

        // FIXME: 2. Let requestsOAC be true if oacHeader is not null and oacHeader[0] is the boolean true; otherwise false.
        [[maybe_unused]] auto requests_oac = false;

        // FIXME: 3. If navigationParams's reserved environment is a non-secure context, then set requestsOAC to false.

        // FIXME: 4. Let agent be the result of obtaining a similar-origin window agent given navigationParams's origin, browsingContext's group, and requestsOAC.

        // 5. Let realm execution context be the result of creating a new JavaScript realm given agent and the following customizations:
        auto realm_execution_context = Bindings::create_a_new_javascript_realm(
            Bindings::main_thread_vm(),
            [&](JS::Realm& realm) -> JS::Object* {
                // - For the global object, create a new Window object.
                window = HTML::Window::create(realm);
                return window;
            },
            [&](JS::Realm&) -> JS::Object* {
                // - For the global this binding, use browsingContext's WindowProxy object.
                return browsing_context->window_proxy();
            });

        // 6. Set window to the global object of realmExecutionContext's Realm component.
        window = as<HTML::Window>(realm_execution_context->realm->global_object());

        // 7. Let topLevelCreationURL be creationURL.
        auto top_level_creation_url = creation_url;

        // 8. Let topLevelOrigin be navigationParams's origin.
        auto top_level_origin = navigation_params.origin;

        // 9. If navigable's container is not null, then:
        if (navigation_params.navigable->container()) {
            // 1. Let parentEnvironment be navigable's container's relevant settings object.
            auto& parent_environment = HTML::relevant_settings_object(*navigation_params.navigable->container());

            // 2. Set topLevelCreationURL to parentEnvironment's top-level creation URL.
            top_level_creation_url = parent_environment.top_level_creation_url;

            // 3. Set topLevelOrigin to parentEnvironment's top-level origin.
            top_level_origin = parent_environment.top_level_origin.value();
        }

        // 10. Set up a window environment settings object with creationURL, realm execution context,
        //    navigationParams's reserved environment, topLevelCreationURL, and topLevelOrigin.

        // FIXME: Why do we assume `creation_url` is non-empty here? Is this a spec bug?
        // FIXME: Why do we assume `top_level_creation_url` is non-empty here? Is this a spec bug?
        HTML::WindowEnvironmentSettingsObject::setup(
            browsing_context->page(),
            creation_url.value(),
            move(realm_execution_context),
            navigation_params.reserved_environment,
            top_level_creation_url.value(),
            top_level_origin);
    }

    // 8. Let loadTimingInfo be a new document load timing info with its navigation start time set to navigationParams's response's timing info's start time.
    DOM::DocumentLoadTimingInfo load_timing_info;
    // AD-HOC: The response object no longer has an associated timing info object. For now, we use response's non-standard response time property,
    //         which represents the time that the time that the response object was created.
    auto response_creation_time = navigation_params.response->monotonic_response_time().nanoseconds() / 1e6;
    load_timing_info.navigation_start_time = HighResolutionTime::coarsen_time(response_creation_time, HTML::relevant_settings_object(*window).cross_origin_isolated_capability());

    // 9. Let document be a new Document, with
    //    type: type
    //    content type: contentType
    //    origin: navigationParams's origin
    //    browsing context: browsingContext
    //    policy container: navigationParams's policy container
    //    FIXME: permissions policy: permissionsPolicy
    //    active sandboxing flag set: navigationParams's final sandboxing flag set
    //    FIXME: opener policy: navigationParams's opener policy
    //    load timing info: loadTimingInfo
    //    FIXME: was created via cross-origin redirects: navigationParams's response's has cross-origin redirects
    //    during-loading navigation ID for WebDriver BiDi: navigationParams's id
    //    URL: creationURL
    //    current document readiness: "loading"
    //    about base URL: navigationParams's about base URL
    //    allow declarative shadow roots: true
    //    custom element registry: A new CustomElementRegistry object.
    auto& realm = window->realm();
    auto document = HTML::HTMLDocument::create(realm);
    document->m_type = type;
    document->m_content_type = move(content_type);
    document->set_origin(navigation_params.origin);
    document->set_browsing_context(browsing_context);
    document->m_policy_container = navigation_params.policy_container;
    document->m_active_sandboxing_flag_set = navigation_params.final_sandboxing_flag_set;
    document->m_navigation_id = navigation_params.id;
    document->set_load_timing_info(load_timing_info);
    document->set_url(*creation_url);
    document->m_readiness = HTML::DocumentReadyState::Loading;
    document->m_about_base_url = navigation_params.about_base_url;
    document->set_allow_declarative_shadow_roots(true);
    document->set_custom_element_registry(realm.create<HTML::CustomElementRegistry>(realm));

    document->m_window = window;

    // NOTE: Non-standard: Pull out the Last-Modified header for use in the lastModified property.
    if (auto maybe_last_modified = navigation_params.response->header_list()->get("Last-Modified"sv); maybe_last_modified.has_value()) {
        // rfc9110, 8.8.2: The Last-Modified header field must be in GMT time zone.
        // document->m_last_modified is in local time zone.
        document->m_last_modified = AK::UnixDateTime::parse("%a, %d %b %Y %H:%M:%S GMT"sv, maybe_last_modified.value(), true);
    }

    // NOTE: Non-standard: Pull out the Content-Language header to determine the document's language.
    if (auto maybe_http_content_language = navigation_params.response->header_list()->get("Content-Language"sv); maybe_http_content_language.has_value()) {
        if (auto maybe_content_language = String::from_byte_string(maybe_http_content_language.value()); !maybe_content_language.is_error())
            document->m_http_content_language = maybe_content_language.release_value();
    }

    // 10. Set window's associated Document to document.
    window->set_associated_document(*document);

    // 11. Run CSP initialization for a Document given document.
    document->run_csp_initialization();

    // 12. If navigationParams's request is non-null, then:
    if (navigation_params.request) {
        // 1. Set document's referrer to the empty string.
        document->m_referrer = String {};

        // 2. Let referrer be navigationParams's request's referrer.
        auto const& referrer = navigation_params.request->referrer();

        // 3. If referrer is a URL record, then set document's referrer to the serialization of referrer.
        if (referrer.has<URL::URL>()) {
            document->m_referrer = referrer.get<URL::URL>().serialize();
        }
    }

    // FIXME: 13: If navigationParams's fetch controller is not null, then:

    // FIXME: 14. Create the navigation timing entry for document, with navigationParams's response's timing info, redirectCount, navigationParams's navigation timing type, and
    //            navigationParams's response's service worker timing info.

    // 15. If navigationParams's response has a `Refresh` header, then:
    if (auto maybe_refresh = navigation_params.response->header_list()->get("Refresh"sv); maybe_refresh.has_value()) {
        // 1. Let value be the isomorphic decoding of the value of the header.
        auto value = TextCodec::isomorphic_decode(maybe_refresh.value());

        // 2. Run the shared declarative refresh steps with document and value.
        document->shared_declarative_refresh_steps(value, nullptr);
    }

    // FIXME: 16. If navigationParams's commit early hints is not null, then call navigationParams's commit early hints with document.

    // FIXME: 17. Process link headers given document, navigationParams's response, and "pre-media".

    // FIXME: 18. Potentially free deferred fetch quota for document.

    // 19. Return document.
    return document;
}

// https://dom.spec.whatwg.org/#dom-document-document
GC::Ref<Document> Document::construct_impl(JS::Realm& realm)
{
    // The new Document() constructor steps are to set this’s origin to the origin of current global object’s associated Document. [HTML]
    auto document = Document::create(realm);
    document->set_origin(as<HTML::Window>(HTML::current_global_object()).associated_document().origin());
    return document;
}

GC::Ref<Document> Document::create(JS::Realm& realm, URL::URL const& url)
{
    return realm.create<Document>(realm, url);
}

GC::Ref<Document> Document::create_for_fragment_parsing(JS::Realm& realm)
{
    return realm.create<Document>(realm, URL::about_blank(), TemporaryDocumentForFragmentParsing::Yes);
}

Document::Document(JS::Realm& realm, URL::URL const& url, TemporaryDocumentForFragmentParsing temporary_document_for_fragment_parsing)
    : ParentNode(realm, *this, NodeType::DOCUMENT_NODE)
    , m_page(Bindings::principal_host_defined_page(realm))
    , m_style_computer(realm.heap().allocate<CSS::StyleComputer>(*this))
    , m_font_computer(realm.heap().allocate<CSS::FontComputer>(*this))
    , m_url(url)
    , m_fonts(CSS::FontFaceSet::create(realm))
    , m_temporary_document_for_fragment_parsing(temporary_document_for_fragment_parsing)
    , m_editing_host_manager(EditingHostManager::create(realm, *this))
    , m_dynamic_view_transition_style_sheet(parse_css_stylesheet(CSS::Parser::ParsingParams(realm), ""sv, {}))
    , m_style_invalidator(realm.heap().allocate<CSS::Invalidation::StyleInvalidator>())
    , m_style_scope(*this)
{
    m_legacy_platform_object_flags = PlatformObject::LegacyPlatformObjectFlags {
        .supports_named_properties = true,
        .has_legacy_override_built_ins_interface_extended_attribute = true,
    };

    m_is_decoded_svg = m_page->client().is_svg_page_client();

    m_cursor_blink_timer = heap().allocate<GC::Timer>();
    m_cursor_blink_timer->start_repeating(500, GC::create_function(heap(), [this] {
        auto cursor_position = this->cursor_position();
        if (!cursor_position)
            return;

        auto navigable = this->navigable();
        if (!navigable || !navigable->is_focused())
            return;

        auto node = cursor_position->node();
        if (node->unsafe_paintable()) {
            m_cursor_blink_state = !m_cursor_blink_state;
            node->set_needs_repaint();
        }
    }));

    HTML::main_thread_event_loop().register_document({}, *this);
}

Document::~Document() = default;

void Document::set_style_invalidation_counter_dump_interval(Optional<u64> interval)
{
    s_style_invalidation_counter_dump_interval = interval;
}

void Document::reset_style_invalidation_counters() const
{
    m_style_invalidation_counters = {};
    m_style_invalidations_since_last_counter_dump = 0;
}

void Document::record_style_invalidation() const
{
    ++m_style_invalidation_counters.style_invalidations;

    if (!s_style_invalidation_counter_dump_interval.has_value())
        return;

    if (++m_style_invalidations_since_last_counter_dump < *s_style_invalidation_counter_dump_interval)
        return;

    m_style_invalidations_since_last_counter_dump = 0;
    dump_style_invalidation_counters(*this);
}

void Document::record_full_style_invalidation() const
{
    ++m_style_invalidation_counters.full_style_invalidations;

    if (!s_style_invalidation_counter_dump_interval.has_value())
        return;

    if (++m_style_invalidations_since_last_counter_dump < *s_style_invalidation_counter_dump_interval)
        return;

    m_style_invalidations_since_last_counter_dump = 0;
    dump_style_invalidation_counters(*this);
}

void Document::finalize()
{
    Base::finalize();
    HTML::main_thread_event_loop().unregister_document({}, *this);
}

void Document::initialize(JS::Realm& realm)
{
    WEB_SET_PROTOTYPE_FOR_INTERFACE(Document);
    Base::initialize(realm);
    Bindings::DocumentPrototype::define_unforgeable_attributes(realm, *this);

    m_selection = realm.create<Selection::Selection>(realm, *this);

    m_list_of_available_images = realm.create<HTML::ListOfAvailableImages>();

    page().client().page_did_create_new_document(*this);

    ensure_cookie_version_index(m_url);
}

// https://html.spec.whatwg.org/multipage/document-lifecycle.html#populate-with-html/head/body
WebIDL::ExceptionOr<void> Document::populate_with_html_head_and_body()
{
    // 1. Let html be the result of creating an element given document, "html", and the HTML namespace.
    auto html = TRY(DOM::create_element(*this, HTML::TagNames::html, Namespace::HTML));

    // 2. Let head be the result of creating an element given document, "head", and the HTML namespace.
    auto head = TRY(DOM::create_element(*this, HTML::TagNames::head, Namespace::HTML));

    // 3. Let body be the result of creating an element given document, "body", and the HTML namespace.
    auto body = TRY(DOM::create_element(*this, HTML::TagNames::body, Namespace::HTML));

    // 4. Append html to document.
    TRY(append_child(html));

    // 5. Append head to html.
    TRY(html->append_child(head));

    // 6. Append body to html.
    TRY(html->append_child(body));

    return {};
}

void Document::visit_edges(Cell::Visitor& visitor)
{
    Base::visit_edges(visitor);
    m_style_scope.visit_edges(visitor);
    visitor.visit(m_pending_css_import_rules);
    visitor.visit(m_page);
    visitor.visit(m_window);
    visitor.visit(m_layout_root);
    visitor.visit(m_style_sheets);
    visitor.visit(m_hovered_node);
    visitor.visit(m_inspected_node);
    visitor.visit(m_highlighted_node);
    visitor.visit(m_active_favicon);
    visitor.visit(m_browsing_context);
    visitor.visit(m_focused_area);
    visitor.visit(m_active_element);
    visitor.visit(m_target_element);
    visitor.visit(m_autofocus_candidates);
    visitor.visit(m_implementation);
    visitor.visit(m_current_script);
    visitor.visit(m_associated_inert_template_document);
    visitor.visit(m_appropriate_template_contents_owner_document);
    visitor.visit(m_pending_parsing_blocking_script);
    visitor.visit(m_pending_parsing_blocking_svg_script);
    visitor.visit(m_history);
    visitor.visit(m_html_parser_end_state);
    visitor.visit(m_style_computer);
    visitor.visit(m_font_computer);
    visitor.visit(m_browsing_context);

    visitor.visit(m_applets);
    visitor.visit(m_anchors);
    visitor.visit(m_images);
    visitor.visit(m_embeds);
    visitor.visit(m_links);
    visitor.visit(m_forms);
    visitor.visit(m_scripts);
    visitor.visit(m_all);
    visitor.visit(m_fonts);
    visitor.visit(m_selection);
    visitor.visit(m_first_base_element_with_href_in_tree_order);
    visitor.visit(m_first_base_element_with_target_in_tree_order);
    visitor.visit(m_parser);
    visitor.visit(m_lazy_load_intersection_observer);
    visitor.visit(m_visual_viewport);
    visitor.visit(m_default_timeline);
    visitor.visit(m_scripts_to_execute_when_parsing_has_finished);
    visitor.visit(m_scripts_to_execute_in_order_as_soon_as_possible);
    visitor.visit(m_scripts_to_execute_as_soon_as_possible);
    visitor.visit(m_node_iterators);
    visitor.visit(m_document_observers_being_notified);
    for (auto& pending_scroll_event : m_pending_scroll_events)
        visitor.visit(pending_scroll_event.event_target);
    for (auto& resize_observer : m_resize_observers)
        visitor.visit(resize_observer);

    visitor.visit(m_svg_roots_needing_relayout);

    visitor.visit(m_shared_resource_requests);

    visitor.visit(m_associated_animation_timelines);
    visitor.visit(m_list_of_available_images);
    for (auto& it : m_map_of_preloaded_resources)
        visitor.visit(it.value);

    for (auto* form_associated_element : m_form_associated_elements_with_form_attribute)
        visitor.visit(form_associated_element->form_associated_element_to_html_element());

    visitor.visit(m_potentially_named_elements);
    m_anchor_name_map.visit_edges(visitor);

    for (auto& event : m_pending_animation_event_queue) {
        visitor.visit(event.event);
        visitor.visit(event.animation);
        visitor.visit(event.target);
    }

    for (auto& event : m_pending_fullscreen_events) {
        visitor.visit(event.element);
    }

    visitor.visit(m_adopted_style_sheets);
    visitor.visit(m_script_blocking_style_sheet_set);

    visitor.visit(m_active_view_transition);
    visitor.visit(m_dynamic_view_transition_style_sheet);

    for (auto& view_transition : m_update_callback_queue)
        visitor.visit(view_transition);

    visitor.visit(m_top_layer_elements);
    visitor.visit(m_top_layer_pending_removals);
    visitor.visit(m_showing_auto_popover_list);
    visitor.visit(m_showing_hint_popover_list);
    visitor.visit(m_popover_pointerdown_target);
    visitor.visit(m_open_dialogs_list);
    visitor.visit(m_dialog_pointerdown_target);
    visitor.visit(m_console_client);
    visitor.visit(m_cursor_blink_timer);
    visitor.visit(m_editing_host_manager);
    visitor.visit(m_local_storage_holder);
    visitor.visit(m_session_storage_holder);
    visitor.visit(m_render_blocking_elements);
    visitor.visit(m_policy_container);
    visitor.visit(m_style_invalidator);
    visitor.visit(m_deferred_parser_start);
    visitor.visit(m_custom_element_registry);
}

// https://w3c.github.io/selection-api/#dom-document-getselection
GC::Ptr<Selection::Selection> Document::get_selection() const
{
    // The method must return the selection associated with this if this has an associated browsing context,
    // and it must return null otherwise.
    if (!browsing_context())
        return {};
    return m_selection;
}

// https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#dom-document-write
WebIDL::ExceptionOr<void> Document::write(Vector<TrustedTypes::TrustedHTMLOrString> const& text)
{
    // The document.write(...text) method steps are to run the document write steps with this, text, false, and "Document write".
    return run_the_document_write_steps(text, AddLineFeed::No, TrustedTypes::InjectionSink::Document_write);
}

// https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#dom-document-writeln
WebIDL::ExceptionOr<void> Document::writeln(Vector<TrustedTypes::TrustedHTMLOrString> const& text)
{
    // The document.writeln(...text) method steps are to run the document write steps with this, text, true, and "Document writeln".
    return run_the_document_write_steps(text, AddLineFeed::Yes, TrustedTypes::InjectionSink::Document_writeln);
}

// https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#document-write-steps
WebIDL::ExceptionOr<void> Document::run_the_document_write_steps(Vector<TrustedTypes::TrustedHTMLOrString> const& text, AddLineFeed line_feed, TrustedTypes::InjectionSink sink)
{
    // 1. Let string be the empty string.
    StringBuilder string;

    // 2. Let isTrusted be false if text contains a string; otherwise true.
    auto is_trusted = true;
    for (auto const& value : text) {
        if (value.has<Utf16String>()) {
            is_trusted = false;
            break;
        }
    }

    // 3. For each value of text:
    for (auto const& value : text) {
        string.append(value.visit(
                               // 1. If value is a TrustedHTML object, then append value's associated data to string.
                               [](GC::Root<TrustedTypes::TrustedHTML> const& value) { return value->to_string(); },
                               // 2. Otherwise, append value to string.
                               [](Utf16String const& value) { return value; })
                .to_utf8_but_should_be_ported_to_utf16());
    }

    // 4. If isTrusted is false, set string to the result of invoking the Get Trusted Type compliant string algorithm
    //    with TrustedHTML, this's relevant global object, string, sink, and "script".
    if (!is_trusted) {
        auto const new_string = TRY(TrustedTypes::get_trusted_type_compliant_string(
            TrustedTypes::TrustedTypeName::TrustedHTML,
            relevant_global_object(*this),
            Utf16String::from_utf8(MUST(string.to_string())),
            sink,
            TrustedTypes::Script.to_string()));
        string.clear();
        string.append(new_string.to_utf8_but_should_be_ported_to_utf16());
    }

    // 5. If lineFeed is true, append U+000A LINE FEED to string.
    if (line_feed == AddLineFeed::Yes)
        string.append('\n');

    // 6. If document is an XML document, then throw an "InvalidStateError" DOMException.
    if (m_type == Type::XML)
        return WebIDL::InvalidStateError::create(realm(), "write() called on XML document."_utf16);

    // 7. 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 WebIDL::InvalidStateError::create(realm(), "throw-on-dynamic-markup-insertion-counter greater than zero."_utf16);

    // 8. If document's active parser was aborted is true, then return.
    if (m_active_parser_was_aborted)
        return {};

    // 9. 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.
        TRY(open());
    }

    // 10. Insert string into the input stream just before the insertion point.
    m_parser->tokenizer().insert_input_at_insertion_point(string.string_view());

    // 11. If document's pending parsing-blocking script is null, then have the HTML parser process string, 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 (!has_pending_parsing_blocking_script())
        m_parser->run(HTML::HTMLTokenizer::StopAtInsertionPoint::Yes);

    return {};
}

// https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#dom-document-open
WebIDL::ExceptionOr<Document*> Document::open(Optional<String> const&, Optional<String> const&)
{
    // 1. If document is an XML document, then throw an "InvalidStateError" DOMException.
    if (m_type == Type::XML)
        return WebIDL::InvalidStateError::create(realm(), "open() called on XML document."_utf16);

    // 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 WebIDL::InvalidStateError::create(realm(), "throw-on-dynamic-markup-insertion-counter greater than zero."_utf16);

    // 3. Let entryDocument be the entry global object's associated Document.
    auto& entry_document = as<HTML::Window>(HTML::entry_global_object()).associated_document();

    // 4. If document's origin is not same origin to entryDocument's origin, then throw a "SecurityError" DOMException.
    if (origin() != entry_document.origin())
        return WebIDL::SecurityError::create(realm(), "Document.origin() not the same as entryDocument's."_utf16);

    // 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;

    // 8. If document's node navigable is non-null and document's node navigable's ongoing navigation is a navigation ID, then stop loading document's node navigable.
    // AD-HOC: Pending navigations can also sit in m_pending_navigations, so we need to cancel those too.
    if (auto navigable = this->navigable()) {
        navigable->clear_pending_navigations();
        if (navigable->ongoing_navigation().has<String>())
            navigable->stop_loading();
    }

    // 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);

    // https://w3c.github.io/editing/docs/execCommand/#state-override
    // When document.open() is called and a document's singleton objects are all replaced by new instances of those
    // objects, editing state associated with that document (including the CSS styling flag, default single-line
    // container name, and any state overrides or value overrides) must be reset.
    set_css_styling_flag(false);
    set_default_single_line_container_name(HTML::TagNames::div);
    reset_command_state_overrides();
    reset_command_value_overrides();

    // 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({});

        // 3. Run the URL and history update steps with document and newURL.
        HTML::perform_url_and_history_update_steps(*this, move(new_url));
    }

    // AD-HOC: Record that this document was an initial about:blank before document.open() cleared the flag, so the
    //         Navigation API can keep entries and events disabled.
    if (is_initial_about_blank())
        as<HTML::Window>(HTML::relevant_global_object(*this)).navigation()->set_was_initial_about_blank_opened(true);

    // 13. Set document's is initial about:blank to false.
    set_is_initial_about_blank(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#dom-document-open-window
WebIDL::ExceptionOr<GC::Ptr<HTML::WindowProxy>> Document::open(StringView url, StringView name, StringView features)
{
    // 1. If this is not fully active, then throw an "InvalidAccessError" DOMException.
    if (!is_fully_active())
        return WebIDL::InvalidAccessError::create(realm(), "Cannot perform open on a document that isn't fully active."_utf16);

    // 2. Return the result of running the window open steps with url, name, and features.
    return window()->window_open_steps(url, name, features);
}

// https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#closing-the-input-stream
WebIDL::ExceptionOr<void> Document::close()
{
    // 1. If document is an XML document, then throw an "InvalidStateError" DOMException exception.
    if (m_type == Type::XML)
        return WebIDL::InvalidStateError::create(realm(), "close() called on XML document."_utf16);

    // 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 WebIDL::InvalidStateError::create(realm(), "throw-on-dynamic-markup-insertion-counter greater than zero."_utf16);

    // 3. If there is no script-created parser associated with the document, then return.
    if (!m_parser || !m_parser->is_script_created())
        return {};

    // 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 (has_pending_parsing_blocking_script()) {
        m_parser->set_post_parse_action([this] { completely_finish_loading(); });
        return {};
    }

    // 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();

    // run() may have paused on a blocking script (e.g. from document.write inside an inline script).
    if (has_pending_parsing_blocking_script()) {
        m_parser->set_post_parse_action([this] { completely_finish_loading(); });
        return {};
    }

    // AD-HOC: This ensures that a load event is fired if the node navigable's container is an iframe.
    completely_finish_loading();

    return {};
}

// https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-document-defaultview
GC::Ptr<HTML::WindowProxy> Document::default_view()
{
    // If this's browsing context is null, then return null.
    if (!browsing_context())
        return {};

    // 2. Return this's browsing context's WindowProxy object.
    return browsing_context()->window_proxy();
}

GC::Ptr<HTML::WindowProxy const> Document::default_view() const
{
    return const_cast<Document*>(this)->default_view();
}

URL::Origin const& Document::origin() const
{
    return m_origin;
}

void Document::set_origin(URL::Origin const& origin)
{
    m_origin = origin;
}

bool Document::is_child_allowed(Node const& node) const
{
    switch (node.type()) {
    case NodeType::DOCUMENT_NODE:
    case NodeType::TEXT_NODE:
        return false;
    case NodeType::COMMENT_NODE:
    case NodeType::PROCESSING_INSTRUCTION_NODE:
        return true;
    case NodeType::DOCUMENT_TYPE_NODE:
        return !first_child_of_type<DocumentType>();
    case NodeType::ELEMENT_NODE:
        return !first_child_of_type<Element>();
    default:
        return false;
    }
}

Element* Document::document_element()
{
    return first_child_of_type<Element>();
}

Element const* Document::document_element() const
{
    return first_child_of_type<Element>();
}

// https://www.w3.org/TR/SVG2/struct.html#InterfaceDocumentExtensions
GC::Ptr<SVG::SVGSVGElement> Document::root_element()
{
    return as_if<SVG::SVGSVGElement>(document_element());
}

// https://html.spec.whatwg.org/multipage/dom.html#the-html-element-2
HTML::HTMLHtmlElement* Document::html_element()
{
    // The html element of a document is its document element, if it's an html element, and null otherwise.
    auto* html = document_element();
    return as_if<HTML::HTMLHtmlElement>(html);
}

// https://html.spec.whatwg.org/multipage/dom.html#the-head-element-2
HTML::HTMLHeadElement* Document::head()
{
    // The head element of a document is the first head element that is a child of the html element, if there is one,
    // or null otherwise.
    auto* html = html_element();
    if (!html)
        return nullptr;
    return html->first_child_of_type<HTML::HTMLHeadElement>();
}

// https://html.spec.whatwg.org/multipage/dom.html#the-title-element-2
GC::Ptr<HTML::HTMLTitleElement> Document::title_element()
{
    // The title element of a document is the first title element in the document (in tree order), if there is one, or
    // null otherwise.
    GC::Ptr<HTML::HTMLTitleElement> title_element = nullptr;

    for_each_in_subtree_of_type<HTML::HTMLTitleElement>([&](auto& title_element_in_tree) {
        title_element = title_element_in_tree;
        return TraversalDecision::Break;
    });

    return title_element;
}

// https://html.spec.whatwg.org/multipage/dom.html#dom-document-dir
StringView Document::dir() const
{
    // The dir IDL attribute on Document objects must reflect the dir content attribute of the html
    // element, if any, limited to only known values. If there is no such element, then the
    // attribute must return the empty string and do nothing on setting.
    if (auto html = html_element())
        return html->dir();

    return ""sv;
}

// https://html.spec.whatwg.org/multipage/dom.html#dom-document-dir
void Document::set_dir(String const& dir)
{
    // The dir IDL attribute on Document objects must reflect the dir content attribute of the html
    // element, if any, limited to only known values. If there is no such element, then the
    // attribute must return the empty string and do nothing on setting.
    if (auto html = html_element())
        html->set_dir(dir);
}

// https://html.spec.whatwg.org/multipage/dom.html#the-body-element-2
HTML::HTMLElement* Document::body()
{
    // The body element of a document is the first of the html element's children that is either
    // a body element or a frameset element, or null if there is no such element.
    auto* html = html_element();
    if (!html)
        return nullptr;
    for (auto* child = html->first_child(); child; child = child->next_sibling()) {
        if (is<HTML::HTMLBodyElement>(*child) || is<HTML::HTMLFrameSetElement>(*child))
            return static_cast<HTML::HTMLElement*>(child);
    }
    return nullptr;
}

// https://html.spec.whatwg.org/multipage/dom.html#dom-document-body
WebIDL::ExceptionOr<void> Document::set_body(HTML::HTMLElement* new_body)
{
    if (!is<HTML::HTMLBodyElement>(new_body) && !is<HTML::HTMLFrameSetElement>(new_body))
        return WebIDL::HierarchyRequestError::create(realm(), "Invalid document body element, must be 'body' or 'frameset'"_utf16);

    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 WebIDL::HierarchyRequestError::create(realm(), "Missing document element"_utf16);

    (void)TRY(document_element->append_child(*new_body));
    return {};
}

// https://html.spec.whatwg.org/multipage/dom.html#document.title
Utf16String Document::title() const
{
    Utf16String value;

    // 1. If the document element is an SVG svg element, then let value be the child text content of the first SVG title
    //    element that is a child of the document element.
    if (auto const* document_element = this->document_element(); is<SVG::SVGSVGElement>(document_element)) {
        if (auto const* title_element = document_element->first_child_of_type<SVG::SVGTitleElement>())
            value = title_element->child_text_content();
    }

    // 2. Otherwise, let value be the child text content of the title element, or the empty string if the title element
    //    is null.
    else if (auto title_element = this->title_element()) {
        value = title_element->text_content().value_or({});
    }

    // 3. Strip and collapse ASCII whitespace in value.
    auto title = Infra::strip_and_collapse_whitespace(value);

    // 4. Return value.
    return title;
}

// https://html.spec.whatwg.org/multipage/dom.html#document.title
WebIDL::ExceptionOr<void> Document::set_title(Utf16String const& title)
{
    auto* document_element = this->document_element();

    // -> If the document element is an SVG svg element
    if (is<SVG::SVGSVGElement>(document_element)) {
        GC::Ptr<Element> element;

        // 1. If there is an SVG title element that is a child of the document element, let element be the first such
        //    element.
        if (auto* title_element = document_element->first_child_of_type<SVG::SVGTitleElement>()) {
            element = title_element;
        }
        // 2. Otherwise:
        else {
            // 1. Let element be the result of creating an element given the document element's node document, "title",
            //    and the SVG namespace.
            element = TRY(DOM::create_element(*this, HTML::TagNames::title, Namespace::SVG));

            // 2. Insert element as the first child of the document element.
            document_element->insert_before(*element, document_element->first_child());
        }

        // 3. String replace all with the given value within element.
        element->string_replace_all(title);
    }

    // -> If the document element is in the HTML namespace
    else if (document_element && document_element->namespace_uri() == Namespace::HTML) {
        auto title_element = this->title_element();
        auto* head_element = this->head();

        // 1. If the title element is null and the head element is null, then return.
        if (title_element == nullptr && head_element == nullptr)
            return {};

        GC::Ptr<Element> element;

        // 2. If the title element is non-null, let element be the title element.
        if (title_element) {
            element = title_element;
        }
        // 3. Otherwise:
        else {
            // 1. Let element be the result of creating an element given the document element's node document, "title",
            //    and the HTML namespace.
            element = TRY(DOM::create_element(*this, HTML::TagNames::title, Namespace::HTML));

            // 2. Append element to the head element.
            TRY(head_element->append_child(*element));
        }

        // 4. String replace all with the given value within element.
        element->string_replace_all(title);
    }

    // -> Otherwise
    else {
        // Do nothing.
        return {};
    }

    return {};
}

void Document::tear_down_layout_tree()
{
    m_layout_root = nullptr;
    m_paintable = nullptr;
    m_needs_full_layout_tree_update = true;
}

void Document::clear_layout_and_paintable_nodes_for_inactive_document()
{
    for_each_in_inclusive_subtree([&](auto& node) {
        node.clear_layout_node_and_paintable({});
        if (auto* element = as_if<Element>(node))
            element->clear_pseudo_element_layout_nodes({});
        return TraversalDecision::Continue;
    });
}

Color Document::background_color() const
{
    // CSS2 says we should use the HTML element's background color unless it's transparent...
    // NB: Called during painting inside update_layout().
    if (auto* html_element = this->html_element(); html_element && html_element->unsafe_layout_node()) {
        auto color = html_element->unsafe_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->unsafe_layout_node()) {
        auto color = body_element->unsafe_layout_node()->computed_values().background_color();
        return color;
    }

    // By default, the document is transparent.
    // The outermost canvas is colored by the PageHost.
    return Color::Transparent;
}

Vector<CSS::BackgroundLayerData> const* Document::background_layers() const
{
    auto* body_element = body();
    if (!body_element)
        return {};

    // NB: Called during painting inside update_layout().
    auto body_layout_node = body_element->unsafe_layout_node();
    if (!body_layout_node)
        return {};

    return &body_layout_node->background_layers();
}

CSS::ImageRendering Document::background_image_rendering() const
{
    auto* body_element = body();
    if (!body_element)
        return CSS::ImageRendering::Auto;

    // NB: Called during painting inside update_layout().
    auto body_layout_node = body_element->unsafe_layout_node();
    if (!body_layout_node)
        return CSS::ImageRendering::Auto;

    return body_layout_node->computed_values().image_rendering();
}

void Document::update_base_element(Badge<HTML::HTMLBaseElement>)
{
    GC::Ptr<HTML::HTMLBaseElement> base_element_with_href = nullptr;
    GC::Ptr<HTML::HTMLBaseElement> base_element_with_target = nullptr;

    for_each_in_subtree_of_type<HTML::HTMLBaseElement>([&base_element_with_href, &base_element_with_target](HTML::HTMLBaseElement& base_element_in_tree) {
        if (!base_element_with_href && base_element_in_tree.has_attribute(HTML::AttributeNames::href)) {
            base_element_with_href = &base_element_in_tree;
            if (base_element_with_target)
                return TraversalDecision::Break;
        }
        if (!base_element_with_target && base_element_in_tree.has_attribute(HTML::AttributeNames::target)) {
            base_element_with_target = &base_element_in_tree;
            if (base_element_with_href)
                return TraversalDecision::Break;
        }

        return TraversalDecision::Continue;
    });

    m_first_base_element_with_href_in_tree_order = base_element_with_href;
    m_first_base_element_with_target_in_tree_order = base_element_with_target;
}

GC::Ptr<HTML::HTMLBaseElement> Document::first_base_element_with_href_in_tree_order() const
{
    return m_first_base_element_with_href_in_tree_order;
}

GC::Ptr<HTML::HTMLBaseElement> Document::first_base_element_with_target_in_tree_order() const
{
    return m_first_base_element_with_target_in_tree_order;
}

// https://html.spec.whatwg.org/multipage/urls-and-fetching.html#respond-to-base-url-changes
void Document::respond_to_base_url_changes(URL::URL const& old_document_url, URL::URL const& old_base_url)
{
    // To respond to base URL changes for a Document document:

    // 1. The user agent should update any user interface elements which are displaying affected URLs, or data derived
    //    from such URLs, to the user. Examples of such user interface elements would be a status bar that displays a
    //    hyperlink's url, or some user interface which displays the URL specified by a q, blockquote, ins, or del
    //    element's cite attribute.
    // FIXME: Update those UI elements.

    // 2. Ensure that the CSS :link/:visited/etc. pseudo-classes are updated appropriately.
    // FIXME: When we track which links have been visited, then :link and :visited will also depend on the new URL and
    //        and this early return will need to be replaced with something that identifies which links will be
    //        affected by the URL change.
    auto any_scope_has_local_link = style_scope().have_local_link_selectors();
    if (!any_scope_has_local_link) {
        for_each_shadow_including_descendant([&](Node& node) {
            auto* element = as_if<Element>(node);
            if (!element)
                return TraversalDecision::Continue;
            auto shadow_root = element->shadow_root();
            if (shadow_root && shadow_root->style_scope().have_local_link_selectors()) {
                any_scope_has_local_link = true;
                return TraversalDecision::Break;
            }
            return TraversalDecision::Continue;
        });
    }
    if (!any_scope_has_local_link)
        return;

    auto const& new_document_url = m_url;
    auto new_base_url = base_url();
    bool const base_url_unchanged = (old_base_url == new_base_url);
    if (base_url_unchanged && old_document_url == new_document_url)
        return;

    auto encoding = encoding_or_default();
    auto local_link_match = [&](Optional<URL::URL> const& target_url, URL::URL const& document_url) {
        if (!target_url.has_value())
            return false;
        if (target_url->fragment().has_value())
            return document_url.equals(*target_url, URL::ExcludeFragment::No);
        return document_url.equals(*target_url, URL::ExcludeFragment::Yes);
    };

    for_each_shadow_including_descendant([&](Node& node) {
        auto* element = as_if<Element>(node);
        if (!element || !element->matches_link_pseudo_class())
            return TraversalDecision::Continue;

        auto href = element->get_attribute_value(HTML::AttributeNames::href);
        auto old_target_url = DOMURL::parse(href, old_base_url, encoding);
        auto new_target_url = base_url_unchanged ? old_target_url : DOMURL::parse(href, new_base_url, encoding);

        if (local_link_match(old_target_url, old_document_url) != local_link_match(new_target_url, new_document_url)) {
            element->invalidate_style(
                StyleInvalidationReason::BaseURLChanged,
                { { .type = CSS::InvalidationSet::Property::Type::PseudoClass, .value = CSS::PseudoClass::LocalLink } },
                {});
        }

        return TraversalDecision::Continue;
    });

    // FIXME: 3. For each descendant of document's shadow-including descendants:
    //        ...

    // FIXME: 4. Consider speculative loads given document.
}

// https://html.spec.whatwg.org/multipage/urls-and-fetching.html#set-the-url
void Document::set_url(URL::URL const& url)
{
    // OPTIMIZATION: Avoid unnecessary work if the URL is already set.
    if (m_url == url)
        return;

    ensure_cookie_version_index(url, m_url);

    auto old_document_url = m_url;
    auto old_base_url = base_url();

    // To set the URL for a Document document to a URL record url:

    // 1. Set document's URL to url.
    m_url = url;

    // 2. Respond to base URL changes given document.
    respond_to_base_url_changes(old_document_url, old_base_url);
}

// https://html.spec.whatwg.org/multipage/urls-and-fetching.html#fallback-base-url
URL::URL Document::fallback_base_url() const
{
    // 1. If document is an iframe srcdoc document, then:
    if (HTML::url_matches_about_srcdoc(m_url)) {
        // 1. Assert: document's about base URL is non-null.
        VERIFY(m_about_base_url.has_value());

        // 2. Return document's about base URL.
        return m_about_base_url.value();
    }

    // 2. If document's URL matches about:blank and document's about base URL is non-null, then return document's about base URL.
    if (HTML::url_matches_about_blank(m_url) && m_about_base_url.has_value())
        return m_about_base_url.value();

    // 3. Return document's URL.
    return m_url;
}

// https://html.spec.whatwg.org/multipage/urls-and-fetching.html#document-base-url
URL::URL Document::base_url() const
{
    // 1. If document has no descendant base element that has an href attribute, then return document's fallback base URL.
    auto base_element = first_base_element_with_href_in_tree_order();
    if (!base_element)
        return fallback_base_url();

    // 2. Otherwise, return the frozen base URL of the first base element in document that has an href attribute, in tree order.
    return base_element->frozen_base_url();
}

// https://html.spec.whatwg.org/multipage/urls-and-fetching.html#encoding-parsing-a-url
Optional<URL::URL> Document::encoding_parse_url(StringView url) const
{
    // 1. Let encoding be UTF-8.
    // 2. If environment is a Document object, then set encoding to environment's character encoding.
    auto encoding = encoding_or_default();

    // 3. Otherwise, if environment's relevant global object is a Window object, set encoding to environment's relevant
    //    global object's associated Document's character encoding.

    // 4. Let baseURL be environment's base URL, if environment is a Document object; otherwise environment's API base URL.
    auto base_url = this->base_url();

    // 5. Return the result of applying the URL parser to url, with baseURL and encoding.
    return DOMURL::parse(url, base_url, encoding);
}

// https://html.spec.whatwg.org/multipage/urls-and-fetching.html#encoding-parsing-and-serializing-a-url
Optional<String> Document::encoding_parse_and_serialize_url(StringView url) const
{
    // 1. Let url be the result of encoding-parsing a URL given url, relative to environment.
    auto parsed_url = encoding_parse_url(url);

    // 2. If url is failure, then return failure.
    if (!parsed_url.has_value())
        return {};

    // 3. Return the result of applying the URL serializer to url.
    return parsed_url->serialize();
}

void Document::invalidate_layout_tree(InvalidateLayoutTreeReason reason)
{
    if (m_layout_root)
        dbgln_if(UPDATE_LAYOUT_DEBUG, "DROP TREE {}", to_string(reason));
    tear_down_layout_tree();
}

void Document::mark_svg_root_as_needing_relayout(Layout::SVGSVGBox& svg_root)
{
    m_svg_roots_needing_relayout.set(svg_root);
}

static void relayout_svg_root(Layout::SVGSVGBox& svg_root)
{
    Layout::LayoutState layout_state(svg_root);

    // Pre-populate the svg_root itself.
    if (auto paintable = svg_root.paintable_box())
        layout_state.populate_from_paintable(svg_root, *paintable);

    // Pre-populate SVGGraphicsBox ancestors (up to outer SVG) for get_parent_svg_transform().
    for (auto* ancestor = svg_root.parent(); ancestor; ancestor = ancestor->parent()) {
        if (auto const* svg_graphics_ancestor = as_if<Layout::SVGGraphicsBox>(*ancestor)) {
            if (auto paintable = svg_graphics_ancestor->paintable_box())
                layout_state.populate_from_paintable(*svg_graphics_ancestor, *paintable);
        }
        if (is<Layout::SVGSVGBox>(*ancestor))
            break;
    }

    // Pre-populate the viewport for position:fixed elements inside <foreignObject>.
    auto& viewport = svg_root.root();
    if (auto paintable = viewport.paintable_box())
        layout_state.populate_from_paintable(viewport, *paintable);

    auto const& svg_state = layout_state.get(svg_root);
    auto content_width = svg_state.content_width();
    auto content_height = svg_state.content_height();

    Layout::SVGFormattingContext svg_context(layout_state, Layout::LayoutMode::Normal, svg_root, nullptr);
    svg_context.run(Layout::AvailableSpace(Layout::AvailableSize::make_definite(content_width), Layout::AvailableSize::make_definite(content_height)));
    layout_state.commit(svg_root);

    svg_root.for_each_in_inclusive_subtree([](auto& node) {
        node.reset_needs_layout_update();
        return TraversalDecision::Continue;
    });
}

static void propagate_scrollbar_width_to_viewport(Element& root_element, Layout::Viewport& viewport)
{
    // https://drafts.csswg.org/css-scrollbars/#scrollbar-width
    // UAs must apply the scrollbar-color value set on the root element to the viewport.
    auto& viewport_computed_values = viewport.mutable_computed_values();
    // NB: Called during layout tree construction.
    auto& root_element_computed_values = root_element.unsafe_layout_node()->computed_values();
    viewport_computed_values.set_scrollbar_width(root_element_computed_values.scrollbar_width());
}

// https://drafts.csswg.org/css-overflow-3/#overflow-propagation
static void propagate_overflow_to_viewport(Element& root_element, Layout::Viewport& viewport)
{
    // https://drafts.csswg.org/css-contain-2/#contain-property
    // Additionally, when any containments are active on either the HTML <html> or <body> elements, propagation of
    // properties from the <body> element to the initial containing block, the viewport, or the canvas background, is
    // disabled. Notably, this affects:
    // - 'overflow' and its longhands (see CSS Overflow 3 § 3.3 Overflow Viewport Propagation)
    if (root_element.is_html_html_element() && !root_element.computed_properties()->contain().is_empty())
        return;

    auto* body_element = root_element.first_child_of_type<HTML::HTMLBodyElement>();
    if (body_element && !body_element->computed_properties()->contain().is_empty())
        return;

    // UAs must apply the overflow-* values set on the root element to the viewport
    // when the root element’s display value is not none.
    // NB: Called during layout tree construction.
    auto overflow_origin_node = root_element.unsafe_layout_node();
    auto& viewport_computed_values = viewport.mutable_computed_values();

    // However, when the root element is an [HTML] html element (including XML syntax for HTML)
    // whose overflow value is visible (in both axes), and that element has as a child
    // a body element whose display value is also not none,
    // user agents must instead apply the overflow-* values of the first such child element to the viewport.
    if (root_element.is_html_html_element()) {
        auto root_element_layout_node = root_element.unsafe_layout_node();
        auto& root_element_computed_values = root_element_layout_node->mutable_computed_values();
        if (root_element_computed_values.overflow_x() == CSS::Overflow::Visible && root_element_computed_values.overflow_y() == CSS::Overflow::Visible) {
            auto* body_element = root_element.first_child_of_type<HTML::HTMLBodyElement>();
            if (body_element && body_element->unsafe_layout_node())
                overflow_origin_node = body_element->unsafe_layout_node();
        }
    }

    // If 'visible' is applied to the viewport, it must be interpreted as 'auto'. If 'clip' is applied to the viewport, it must be interpreted as 'hidden'.
    auto& overflow_origin_computed_values = overflow_origin_node->mutable_computed_values();
    auto overflow_x_to_apply = overflow_origin_computed_values.overflow_x();
    if (overflow_x_to_apply == CSS::Overflow::Visible) {
        overflow_x_to_apply = CSS::Overflow::Auto;
    } else if (overflow_x_to_apply == CSS::Overflow::Clip) {
        overflow_x_to_apply = CSS::Overflow::Hidden;
    }
    auto overflow_y_to_apply = overflow_origin_computed_values.overflow_y();
    if (overflow_y_to_apply == CSS::Overflow::Visible) {
        overflow_y_to_apply = CSS::Overflow::Auto;
    } else if (overflow_y_to_apply == CSS::Overflow::Clip) {
        overflow_y_to_apply = CSS::Overflow::Hidden;
    }
    viewport_computed_values.set_overflow_x(overflow_x_to_apply);
    viewport_computed_values.set_overflow_y(overflow_y_to_apply);

    // The element from which the value is propagated must then have a used overflow value of visible.
    // FIXME: Apply this to the used values, not the computed ones.
    overflow_origin_computed_values.set_overflow_x(CSS::Overflow::Visible);
    overflow_origin_computed_values.set_overflow_y(CSS::Overflow::Visible);
}

void Document::update_layout_if_needed_for_node(Node const& node, UpdateLayoutReason reason)
{
    if (node.is_connected())
        update_layout(reason);
}

void Document::update_layout(UpdateLayoutReason reason)
{
    auto navigable = this->navigable();
    if (!navigable || navigable->active_document() != this)
        return;

    VERIFY(!m_is_running_update_layout);
    ScopeGuard guard = [&] { m_is_running_update_layout = false; };
    m_is_running_update_layout = true;

    update_style();

    if (layout_is_up_to_date())
        return;

    auto svg_roots_to_relayout = move(m_svg_roots_needing_relayout);

    // NOTE: If this is a document hosting <template> contents, layout is unnecessary.
    if (m_created_for_appropriate_template_contents)
        return;

    auto const needs_layout_tree_rebuild = !m_layout_root || needs_layout_tree_update() || child_needs_layout_tree_update() || needs_full_layout_tree_update();

    // Partial SVG relayout
    if (!needs_layout_tree_rebuild && !svg_roots_to_relayout.is_empty() && !m_layout_root->needs_layout_update()) {
        for (auto const& svg_root : svg_roots_to_relayout)
            relayout_svg_root(*svg_root);

        invalidate_stacking_context_tree();
        set_needs_to_record_display_list();

        set_needs_accumulated_visual_contexts_update(true);
        update_paint_and_hit_testing_properties_if_needed();
        m_document->set_needs_repaint();
        return;
    }

    // Clear text blocks cache so we rebuild them on the next find action.
    if (m_layout_root)
        m_layout_root->invalidate_text_blocks_cache();

    set_needs_to_record_display_list();

    auto* document_element = this->document_element();
    auto viewport_rect = navigable->viewport_rect();

    auto timer = Core::ElapsedTimer::start_new(Core::TimerType::Precise);

    if (needs_layout_tree_rebuild) {
        Layout::TreeBuilder tree_builder;
        m_layout_root = as<Layout::Viewport>(*tree_builder.build(*this));

        // NB: Called during layout update.
        if (document_element && document_element->unsafe_layout_node()) {
            propagate_overflow_to_viewport(*document_element, *m_layout_root);
            propagate_scrollbar_width_to_viewport(*document_element, *m_layout_root);
        }

        set_needs_full_layout_tree_update(false);

        if constexpr (UPDATE_LAYOUT_DEBUG) {
            dbgln("TREEBUILD {} µs", timer.elapsed_time().to_microseconds());
        }
    }

    u32 layout_index_counter = 0;
    m_layout_root->for_each_in_inclusive_subtree([&](auto& layout_node) {
        if (auto* node_with_style = as_if<Layout::NodeWithStyle>(layout_node))
            node_with_style->set_layout_index(layout_index_counter++);

        layout_node.recompute_containing_block({});

        auto* box = as_if<Layout::Box>(layout_node);
        if (!box)
            return TraversalDecision::Continue;

        box->clear_contained_abspos_children();

        if (!box->is_absolutely_positioned())
            return TraversalDecision::Continue;

        if (auto containing_block = box->containing_block()) {
            auto closest_box_that_establishes_formatting_context = containing_block;
            while (closest_box_that_establishes_formatting_context) {
                if (closest_box_that_establishes_formatting_context == m_layout_root)
                    break;
                if (Layout::FormattingContext::formatting_context_type_created_by_box(*closest_box_that_establishes_formatting_context).has_value())
                    break;
                closest_box_that_establishes_formatting_context = closest_box_that_establishes_formatting_context->containing_block();
            }
            VERIFY(closest_box_that_establishes_formatting_context);
            closest_box_that_establishes_formatting_context->add_contained_abspos_child(*box);
        }

        return TraversalDecision::Continue;
    });

    Layout::LayoutState layout_state;
    layout_state.ensure_capacity(layout_index_counter);

    {
        auto& viewport = static_cast<Layout::Viewport&>(*m_layout_root);
        auto& viewport_state = layout_state.get_mutable(viewport);
        viewport_state.set_content_width(viewport_rect.width());
        viewport_state.set_content_height(viewport_rect.height());

        // NB: Called during layout update.
        if (document_element && document_element->unsafe_layout_node()) {
            auto& icb_state = layout_state.get_mutable(as<Layout::NodeWithStyleAndBoxModelMetrics>(*document_element->unsafe_layout_node()));
            icb_state.set_content_width(viewport_rect.width());
        }

        auto available_space = Layout::AvailableSpace(
            Layout::AvailableSize::make_definite(viewport_rect.width()),
            Layout::AvailableSize::make_definite(viewport_rect.height()));

        if (m_layout_root->first_child() && m_layout_root->first_child()->is_svg_svg_box()) {
            // NOTE: If we are laying out a standalone SVG document, we give it some special treatment:
            //       The root <svg> container gets the same size as the viewport,
            //       and we call directly into the SVG layout code from here.
            auto const& svg_root = as<Layout::SVGSVGBox>(*m_layout_root->first_child());
            auto content_height = layout_state.get(*svg_root.containing_block()).content_height();
            layout_state.get_mutable(svg_root).set_content_height(content_height);
            Layout::SVGFormattingContext svg_formatting_context(layout_state, Layout::LayoutMode::Normal, svg_root, nullptr);
            svg_formatting_context.run(available_space);
        } else {
            Layout::BlockFormattingContext root_formatting_context(layout_state, Layout::LayoutMode::Normal, *m_layout_root, nullptr);
            root_formatting_context.run(available_space);
        }
    }

    layout_state.commit(*m_layout_root);

    // Broadcast the current viewport rect to any new paintables, so they know whether they're visible or not.
    inform_all_viewport_clients_about_the_current_viewport_rect();

    m_document->set_needs_repaint();

    // NB: Called during layout update.
    unsafe_paintable()->assign_scroll_frames();

    set_needs_accumulated_visual_contexts_update(true);
    update_paint_and_hit_testing_properties_if_needed();

    if (auto range = get_selection()->range()) {
        unsafe_paintable()->recompute_selection_states(*range);
    }

    // Collect elements with content-visibility: auto. This is used in the HTML event loop to avoid traversing the whole tree every time.
    Vector<WeakPtr<Painting::PaintableBox>> paintable_boxes_with_auto_content_visibility;
    unsafe_paintable()->for_each_in_subtree_of_type<Painting::PaintableBox>([&](auto& paintable_box) {
        if (paintable_box.dom_node()
            && paintable_box.dom_node()->is_element()
            && paintable_box.computed_values().content_visibility() == CSS::ContentVisibility::Auto) {
            paintable_boxes_with_auto_content_visibility.append(paintable_box);
        }
        return TraversalDecision::Continue;
    });
    unsafe_paintable()->set_paintable_boxes_with_auto_content_visibility(move(paintable_boxes_with_auto_content_visibility));

    m_layout_root->for_each_in_inclusive_subtree([](auto& node) {
        node.reset_needs_layout_update();
        return TraversalDecision::Continue;
    });

    if constexpr (UPDATE_LAYOUT_DEBUG) {
        dbgln("LAYOUT {} {} µs", to_string(reason), timer.elapsed_time().to_microseconds());
    }

    VERIFY(layout_is_up_to_date());
}

bool Document::layout_is_up_to_date() const
{
    if (!navigable() || navigable()->active_document() != this)
        return true;
    if (!m_layout_root)
        return false;
    return !m_layout_root->needs_layout_update()
        && !needs_layout_tree_update()
        && !child_needs_layout_tree_update()
        && !needs_full_layout_tree_update()
        && m_svg_roots_needing_relayout.is_empty();
}

[[nodiscard]] static CSS::RequiredInvalidationAfterStyleChange update_style_recursively(
    Node& node,
    CSS::StyleComputer& style_computer,
    bool needs_inherited_style_update,
    bool recompute_elements_depending_on_custom_properties,
    bool parent_display_changed,
    bool ancestor_needs_descendant_style_recompute)
{
    bool const needs_full_style_update = node.document().needs_full_style_update();
    CSS::RequiredInvalidationAfterStyleChange invalidation;

    if (node.is_element())
        style_computer.push_ancestor(static_cast<Element const&>(node));

    // NOTE: If the current node has `display:none`, we can disregard all invalidation
    //       caused by its children, as they will not be rendered anyway.
    //       We will still recompute style for the children, though.
    bool is_display_none = false;

    bool did_change_custom_properties = false;
    CSS::RequiredInvalidationAfterStyleChange node_invalidation;
    if (is<Element>(node)) {
        auto& element = static_cast<Element&>(node);

        // FIXME: We can avoid some unnecessary re-computations by, skipping if a) the contained if() functions don't
        //        include media conditions, or b) the data used to resolve media queries hasn't changed.
        bool const needs_style_update_due_to_if_media = element.style_uses_if_css_function();

        if (needs_full_style_update
            || node.needs_style_update()
            || parent_display_changed
            || ancestor_needs_descendant_style_recompute
            || (recompute_elements_depending_on_custom_properties && (element.style_uses_var_css_function() || element.style_uses_inherit_css_function()))
            || needs_style_update_due_to_if_media) {
            node_invalidation = element.recompute_style(did_change_custom_properties);
        } else {
            if (needs_inherited_style_update)
                node_invalidation = element.recompute_inherited_style();
            if (recompute_elements_depending_on_custom_properties && element.refresh_inherited_custom_property_data())
                did_change_custom_properties = true;
        }
        is_display_none = static_cast<Element&>(node).computed_properties()->display().is_none();

        if (!node_invalidation.is_none())
            CSS::Invalidation::invalidate_assigned_slottables_after_slot_style_change(element);
    }
    if (node_invalidation.relayout)
        node.set_needs_layout_update(SetNeedsLayoutReason::StyleChange);
    if (node_invalidation.rebuild_layout_tree) {
        // We mark layout tree for rebuild starting from parent element to correctly invalidate
        // "display" property change to/from "contents" value.
        if (auto parent_element = node.parent_element()) {
            parent_element->set_needs_layout_tree_update(true, SetNeedsLayoutTreeUpdateReason::StyleChange);
        } else {
            node.set_needs_layout_tree_update(true, SetNeedsLayoutTreeUpdateReason::StyleChange);
        }
    }
    node.set_needs_style_update(false);
    invalidation |= node_invalidation;

    if (did_change_custom_properties) {
        recompute_elements_depending_on_custom_properties = true;
    }

    bool children_need_inherited_style_update = !invalidation.is_none();
    // NB: When display changes to/from flex/grid/contents, children may need to be blockified or un-blockified.
    //     This requires a full style recompute, not just inherited style update.
    bool children_need_full_style_recompute = node_invalidation.rebuild_layout_tree;
    bool descendant_style_recompute_needed = ancestor_needs_descendant_style_recompute || node_invalidation.recompute_descendant_styles;
    if (needs_full_style_update
        || node.child_needs_style_update()
        || children_need_inherited_style_update
        || recompute_elements_depending_on_custom_properties
        || children_need_full_style_recompute
        || descendant_style_recompute_needed) {
        if (node.is_element()) {
            if (auto shadow_root = static_cast<DOM::Element&>(node).shadow_root()) {
                if (needs_full_style_update
                    || shadow_root->needs_style_update()
                    || shadow_root->child_needs_style_update()
                    || children_need_inherited_style_update
                    || recompute_elements_depending_on_custom_properties
                    || children_need_full_style_recompute
                    || descendant_style_recompute_needed) {
                    auto subtree_invalidation = update_style_recursively(
                        *shadow_root,
                        style_computer,
                        children_need_inherited_style_update,
                        recompute_elements_depending_on_custom_properties,
                        children_need_full_style_recompute,
                        descendant_style_recompute_needed);
                    if (!is_display_none)
                        invalidation |= subtree_invalidation;
                }
            }
        }

        node.for_each_child([&](auto& child) {
            if (needs_full_style_update
                || child.needs_style_update()
                || children_need_inherited_style_update
                || child.child_needs_style_update()
                || recompute_elements_depending_on_custom_properties
                || children_need_full_style_recompute
                || descendant_style_recompute_needed) {
                auto subtree_invalidation = update_style_recursively(
                    child,
                    style_computer,
                    children_need_inherited_style_update,
                    recompute_elements_depending_on_custom_properties,
                    children_need_full_style_recompute,
                    descendant_style_recompute_needed);
                if (!is_display_none)
                    invalidation |= subtree_invalidation;
            }
            return IterationDecision::Continue;
        });
    }

    node.set_child_needs_style_update(false);

    if (node.is_element())
        style_computer.pop_ancestor(static_cast<Element const&>(node));

    return invalidation;
}

void Document::update_style()
{
    // NOTE: If our parent document needs a relayout, we must do that *first*. This is required as it may cause the
    // viewport to change which will can affect media query evaluation and the value of the `vw` unit.
    if (auto navigable = this->navigable(); navigable && navigable->container() && &navigable->container()->document() != this)
        navigable->container()->document().update_layout(UpdateLayoutReason::ChildDocumentStyleUpdate);

    if (!browsing_context())
        return;

    update_animated_style_if_needed();

    // Associated with each top-level browsing context is a current transition generation that is incremented on each
    // style change event. [CSS-Transitions-2]
    m_transition_generation++;

    if (m_needs_invalidation_of_elements_affected_by_has) {
        m_needs_invalidation_of_elements_affected_by_has = false;
        CSS::Invalidation::invalidate_style_for_pending_has_mutations(*this);
    }

    if (!m_style_invalidator->has_pending_invalidations() && !needs_full_style_update() && !needs_style_update() && !child_needs_style_update())
        return;

    m_style_invalidator->invalidate(*this);

    // NOTE: If this is a document hosting <template> contents, style update is unnecessary.
    if (m_created_for_appropriate_template_contents)
        return;

    // Fetch the viewport rect once, instead of repeatedly, during style computation.
    style_computer().set_viewport_rect({}, viewport_rect());

    if (m_needs_media_query_evaluation)
        evaluate_media_rules();

    style_computer().reset_has_result_cache();
    style_computer().reset_ancestor_filter();

    build_registered_properties_cache();

    auto invalidation = update_style_recursively(*this, style_computer(), false, false, false, false);
    if (!invalidation.is_none())
        set_needs_to_record_display_list();

    if (invalidation.rebuild_accumulated_visual_contexts)
        set_needs_accumulated_visual_contexts_update(true);

    if (invalidation.rebuild_stacking_context_tree)
        invalidate_stacking_context_tree();
    m_needs_full_style_update = false;
}

void Document::update_style_if_needed_for_element(AbstractElement const& abstract_element)
{
    if (element_needs_style_update(abstract_element))
        update_style();
}

bool Document::element_needs_style_update(AbstractElement const& abstract_element) const
{
    // If there are document-level reasons to update style, we can't skip.
    if (m_needs_full_style_update)
        return true;
    if (m_needs_animated_style_update)
        return true;
    if (m_needs_invalidation_of_elements_affected_by_has)
        return true;
    if (m_style_invalidator->has_pending_invalidations())
        return true;

    // Check the element itself.
    if (abstract_element.element().needs_style_update())
        return true;
    if (abstract_element.element().entire_subtree_needs_style_update())
        return true;

    // Walk the inheritance ancestor chain. We use element_to_inherit_style_from()
    // because style inheritance follows the flat tree (slotted elements inherit
    // from their assigned slot, not their DOM parent). If any ancestor on the
    // path has its style marked dirty, the target element's computed style could
    // change via inherited properties, so we must update.
    for (auto ancestor = abstract_element.element_to_inherit_style_from(); ancestor.has_value(); ancestor = ancestor->element_to_inherit_style_from()) {
        if (ancestor->element().needs_style_update())
            return true;
        if (ancestor->element().entire_subtree_needs_style_update())
            return true;
    }

    // If the navigable has a container and that container needs a style update, then we need one as well, since the
    // container's style can affect this element (e.g. via media queries or viewport units).
    if (auto navigable = this->navigable()) {
        if (auto container = navigable->container()) {
            if (container->document().element_needs_style_update(AbstractElement { *container }))
                return true;
        }
    }

    return false;
}

void Document::update_animated_style_if_needed()
{
    if (!m_needs_animated_style_update)
        return;

    Animations::AnimationUpdateContext context;

    for (auto& timeline : m_associated_animation_timelines) {
        for (auto& animation : timeline->associated_animations()) {
            if (animation.is_idle())
                continue;
            if (auto effect = animation.effect())
                effect->update_computed_properties(context);
        }
    }

    m_needs_animated_style_update = false;
}

void Document::set_needs_animated_style_update()
{
    if (m_needs_animated_style_update)
        return;

    m_needs_animated_style_update = true;
    set_needs_repaint(InvalidateDisplayList::No);
}

void Document::update_paint_and_hit_testing_properties_if_needed()
{
    // NB: Called during paint property resolution.
    if (auto paintable = this->unsafe_paintable()) {
        paintable->refresh_scroll_state();
    }

    if (m_needs_accumulated_visual_contexts_update) {
        m_needs_accumulated_visual_contexts_update = false;
        if (auto paintable = this->unsafe_paintable()) {
            paintable->assign_accumulated_visual_contexts();
        }
    }
}

void Document::set_normal_link_color(Color color)
{
    m_normal_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;
}

Optional<Vector<String> const&> Document::supported_color_schemes() const
{
    return m_supported_color_schemes;
}

// https://html.spec.whatwg.org/multipage/semantics.html#meta-color-scheme
void Document::obtain_supported_color_schemes()
{
    m_supported_color_schemes = {};

    // 1. Let candidate elements be the list of all meta elements that meet the following criteria, in tree order:
    for_each_in_subtree_of_type<HTML::HTMLMetaElement>([&](HTML::HTMLMetaElement& element) {
        //     * the element is in a document tree;
        //     * the element has a name attribute, whose value is an ASCII case-insensitive match for color-scheme; and
        //     * the element has a content attribute.

        // 2. For each element in candidate elements:
        auto content = element.attribute(HTML::AttributeNames::content);
        if (element.name().has_value() && element.name()->equals_ignoring_ascii_case("color-scheme"sv) && content.has_value()) {
            // 1. Let parsed be the result of parsing a list of component values given the value of element's content attribute.
            auto context = CSS::Parser::ParsingParams { document() };
            auto parsed = parse_css_value(context, content.value(), CSS::PropertyID::ColorScheme);

            // 2. If parsed is a valid CSS 'color-scheme' property value, then return parsed.
            if (!parsed.is_null() && parsed->is_color_scheme()) {
                m_supported_color_schemes = parsed->as_color_scheme().schemes();
                return TraversalDecision::Break;
            }
        }

        return TraversalDecision::Continue;
    });

    // 3. Return null.
}

// https://html.spec.whatwg.org/multipage/semantics.html#meta-theme-color
void Document::obtain_theme_color()
{
    Color theme_color = Color::Transparent;

    // 1. Let candidate elements be the list of all meta elements that meet the following criteria, in tree order:
    for_each_in_subtree_of_type<HTML::HTMLMetaElement>([&](HTML::HTMLMetaElement& element) {
        //     * the element is in a document tree;
        //     * the element has a name attribute, whose value is an ASCII case-insensitive match for theme-color; and
        //     * the element has a content attribute.

        // 2. For each element in candidate elements:
        auto content = element.attribute(HTML::AttributeNames::content);
        if (element.name().has_value() && element.name()->equals_ignoring_ascii_case("theme-color"sv) && content.has_value()) {
            // 1. If element has a media attribute and the value of element's media attribute does not match the environment, then continue.
            auto context = CSS::Parser::ParsingParams { document() };
            auto media = element.attribute(HTML::AttributeNames::media);
            if (media.has_value()) {
                auto query = parse_media_query(context, media.value());
                if (query.is_null() || !query->evaluate(*this))
                    return TraversalDecision::Continue;
            }

            // 2. Let value be the result of stripping leading and trailing ASCII whitespace from the value of element's content attribute.
            auto value = content->bytes_as_string_view().trim(Infra::ASCII_WHITESPACE);

            // 3. Let color be the result of parsing value.
            auto css_value = parse_css_value(context, value, CSS::PropertyID::Color);

            // 4. If color is not failure, then return color.
            if (!css_value.is_null() && css_value->has_color()) {
                CSS::ColorResolutionContext color_resolution_context {};
                // NB: Called during theme color computation, layout may be stale.
                if (html_element() && html_element()->unsafe_layout_node()) {
                    color_resolution_context = CSS::ColorResolutionContext::for_layout_node_with_style(*html_element()->unsafe_layout_node());
                }

                theme_color = css_value->to_color(color_resolution_context).value();
                return TraversalDecision::Break;
            }
        }

        return TraversalDecision::Continue;
    });

    // 3. Return nothing(the page has no theme color).
    document().page().client().page_did_change_theme_color(theme_color);
}

Layout::Viewport const* Document::layout_node() const
{
    return static_cast<Layout::Viewport const*>(Node::layout_node());
}

Layout::Viewport* Document::layout_node()
{
    return static_cast<Layout::Viewport*>(Node::layout_node());
}

Layout::Viewport const* Document::unsafe_layout_node() const
{
    return static_cast<Layout::Viewport const*>(Node::unsafe_layout_node());
}

Layout::Viewport* Document::unsafe_layout_node()
{
    return static_cast<Layout::Viewport*>(Node::unsafe_layout_node());
}

void Document::set_inspected_node(GC::Ptr<Node> node)
{
    m_inspected_node = node;
}

void Document::set_highlighted_node(GC::Ptr<Node> node, Optional<CSS::PseudoElement> pseudo_element)
{
    if (m_highlighted_node == node && m_highlighted_pseudo_element == pseudo_element)
        return;

    if (auto layout_node = highlighted_layout_node(); layout_node && layout_node->first_paintable())
        layout_node->first_paintable()->set_needs_repaint();

    m_highlighted_node = node;
    m_highlighted_pseudo_element = pseudo_element;

    if (auto layout_node = highlighted_layout_node(); layout_node && layout_node->first_paintable())
        layout_node->first_paintable()->set_needs_repaint();
}

GC::Ptr<Layout::Node> Document::highlighted_layout_node()
{
    if (!m_highlighted_node)
        return nullptr;

    // NB: Called during painting inside update_layout().
    if (!m_highlighted_pseudo_element.has_value() || !m_highlighted_node->is_element())
        return m_highlighted_node->unsafe_layout_node();

    auto const& element = static_cast<Element const&>(*m_highlighted_node);
    return element.get_pseudo_element_node(m_highlighted_pseudo_element.value());
}

static Node* find_common_ancestor(Node* a, Node* b)
{
    if (!a || !b)
        return nullptr;

    if (a == b)
        return a;

    HashTable<Node*> 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(GC::Ptr<Node> node)
{
    if (m_hovered_node == node)
        return;

    GC::Ptr<Node> old_hovered_node = move(m_hovered_node);
    auto* common_ancestor = find_common_ancestor(old_hovered_node, node);

    GC::Ptr<Node> old_hovered_node_root = nullptr;
    GC::Ptr<Node> new_hovered_node_root = nullptr;
    if (old_hovered_node)
        old_hovered_node_root = old_hovered_node->root();
    if (node)
        new_hovered_node_root = node->root();
    if (old_hovered_node_root != new_hovered_node_root) {
        if (old_hovered_node_root)
            CSS::Invalidation::invalidate_style_after_pseudo_class_state_change(CSS::PseudoClass::Hover, *this, m_hovered_node, *old_hovered_node_root, node);
        if (new_hovered_node_root)
            CSS::Invalidation::invalidate_style_after_pseudo_class_state_change(CSS::PseudoClass::Hover, *this, m_hovered_node, *new_hovered_node_root, node);
    } else {
        CSS::Invalidation::invalidate_style_after_pseudo_class_state_change(CSS::PseudoClass::Hover, *this, m_hovered_node, *common_ancestor, node);
    }

    m_hovered_node = node;

    // https://w3c.github.io/pointerevents/#the-pointerout-event
    if (old_hovered_node && old_hovered_node != m_hovered_node) {
        Bindings::PointerEventInit pointer_event_init {};
        pointer_event_init.bubbles = true;
        pointer_event_init.cancelable = true;
        pointer_event_init.composed = true;
        pointer_event_init.related_target = GC::Root<DOM::EventTarget> { m_hovered_node.ptr() };
        pointer_event_init.is_primary = true;
        pointer_event_init.pointer_type = UIEvents::PointerTypes::Mouse;
        if (auto navigable = this->navigable())
            pointer_event_init.view = navigable->active_window_proxy();
        auto event = UIEvents::PointerEvent::create(realm(), UIEvents::EventNames::pointerout, pointer_event_init);
        old_hovered_node->dispatch_event(event);
    }

    // https://w3c.github.io/uievents/#mouseout
    if (old_hovered_node && old_hovered_node != m_hovered_node) {
        Bindings::MouseEventInit mouse_event_init {};
        mouse_event_init.bubbles = true;
        mouse_event_init.cancelable = true;
        mouse_event_init.composed = true;
        mouse_event_init.related_target = GC::Root<DOM::EventTarget> { m_hovered_node.ptr() };
        if (auto navigable = this->navigable())
            mouse_event_init.view = navigable->active_window_proxy();
        auto event = UIEvents::MouseEvent::create(realm(), UIEvents::EventNames::mouseout, mouse_event_init);
        old_hovered_node->dispatch_event(event);
    }

    // https://w3c.github.io/pointerevents/#the-pointerleave-event
    if (old_hovered_node && (!m_hovered_node || !m_hovered_node->is_descendant_of(*old_hovered_node))) {
        for (auto target = old_hovered_node; target && target.ptr() != common_ancestor; target = target->parent()) {
            // FIXME: Populate the event with mouse coordinates, etc.
            Bindings::PointerEventInit pointer_event_init {};
            pointer_event_init.related_target = GC::Root<DOM::EventTarget> { m_hovered_node.ptr() };
            pointer_event_init.is_primary = true;
            pointer_event_init.pointer_type = UIEvents::PointerTypes::Mouse;
            if (auto navigable = this->navigable())
                pointer_event_init.view = navigable->active_window_proxy();
            target->dispatch_event(UIEvents::PointerEvent::create(realm(), UIEvents::EventNames::pointerleave, pointer_event_init));
        }
    }

    // https://w3c.github.io/uievents/#mouseleave
    if (old_hovered_node && (!m_hovered_node || !m_hovered_node->is_descendant_of(*old_hovered_node))) {
        for (auto target = old_hovered_node; target && target.ptr() != common_ancestor; target = target->parent_or_shadow_host()) {
            // FIXME: Populate the event with mouse coordinates, etc.
            Bindings::MouseEventInit mouse_event_init {};
            mouse_event_init.related_target = GC::Root<DOM::EventTarget> { m_hovered_node.ptr() };
            target->dispatch_event(UIEvents::MouseEvent::create(realm(), UIEvents::EventNames::mouseleave, mouse_event_init));
        }
    }

    // https://w3c.github.io/pointerevents/#the-pointerover-event
    if (m_hovered_node && m_hovered_node != old_hovered_node) {
        Bindings::PointerEventInit pointer_event_init {};
        pointer_event_init.bubbles = true;
        pointer_event_init.cancelable = true;
        pointer_event_init.composed = true;
        pointer_event_init.related_target = GC::Root<DOM::EventTarget> { old_hovered_node.ptr() };
        pointer_event_init.is_primary = true;
        pointer_event_init.pointer_type = UIEvents::PointerTypes::Mouse;
        if (auto navigable = this->navigable())
            pointer_event_init.view = navigable->active_window_proxy();
        auto event = UIEvents::PointerEvent::create(realm(), UIEvents::EventNames::pointerover, pointer_event_init);
        m_hovered_node->dispatch_event(event);
    }

    // https://w3c.github.io/uievents/#mouseover
    if (m_hovered_node && m_hovered_node != old_hovered_node) {
        Bindings::MouseEventInit mouse_event_init {};
        mouse_event_init.bubbles = true;
        mouse_event_init.cancelable = true;
        mouse_event_init.composed = true;
        mouse_event_init.related_target = GC::Root<DOM::EventTarget> { old_hovered_node.ptr() };
        if (auto navigable = this->navigable())
            mouse_event_init.view = navigable->active_window_proxy();
        auto event = UIEvents::MouseEvent::create(realm(), UIEvents::EventNames::mouseover, mouse_event_init);
        m_hovered_node->dispatch_event(event);
    }

    // https://w3c.github.io/pointerevents/#the-pointerenter-event
    // https://w3c.github.io/uievents/#mouseenter
    // Enter events are dispatched from ancestor to descendant.
    // Leave events are dispatched in the opposite order.
    if (m_hovered_node && (!old_hovered_node || !m_hovered_node->is_ancestor_of(*old_hovered_node))) {
        Vector<GC::Ref<Node>> entered_ancestors;
        for (auto target = m_hovered_node; target && target.ptr() != common_ancestor; target = target->parent_or_shadow_host())
            entered_ancestors.append(*target);

        for (auto target : entered_ancestors.in_reverse()) {
            // FIXME: Populate the events with mouse coordinates, etc.
            Bindings::PointerEventInit pointer_event_init {};
            pointer_event_init.related_target = GC::Root<DOM::EventTarget> { old_hovered_node.ptr() };
            pointer_event_init.is_primary = true;
            pointer_event_init.pointer_type = UIEvents::PointerTypes::Mouse;
            if (auto navigable = this->navigable())
                pointer_event_init.view = navigable->active_window_proxy();
            target->dispatch_event(UIEvents::PointerEvent::create(realm(), UIEvents::EventNames::pointerenter, pointer_event_init));
            Bindings::MouseEventInit mouse_event_init {};
            mouse_event_init.related_target = GC::Root<DOM::EventTarget> { old_hovered_node.ptr() };
            target->dispatch_event(UIEvents::MouseEvent::create(realm(), UIEvents::EventNames::mouseenter, mouse_event_init));
        }
    }
}

// https://html.spec.whatwg.org/multipage/dom.html#dom-document-getelementsbyname
GC::Ref<NodeList> Document::get_elements_by_name(FlyString const& name)
{
    return LiveNodeList::create(realm(), *this, LiveNodeList::Scope::Descendants, [name](auto const& node) {
        if (!is<HTML::HTMLElement>(node))
            return false;
        return as<HTML::HTMLElement>(node).name() == name;
    });
}

// https://html.spec.whatwg.org/multipage/obsolete.html#dom-document-applets
GC::Ref<HTMLCollection> Document::applets()
{
    if (!m_applets)
        m_applets = HTMLCollection::create(*this, HTMLCollection::Scope::Descendants, [](auto&) { return false; });
    return *m_applets;
}

// https://html.spec.whatwg.org/multipage/obsolete.html#dom-document-anchors
GC::Ref<HTMLCollection> Document::anchors()
{
    if (!m_anchors) {
        m_anchors = HTMLCollection::create(*this, HTMLCollection::Scope::Descendants, [](Element const& element) {
            return is<HTML::HTMLAnchorElement>(element) && element.name().has_value();
        });
    }
    return *m_anchors;
}

// https://html.spec.whatwg.org/multipage/dom.html#dom-document-images
GC::Ref<HTMLCollection> Document::images()
{
    if (!m_images) {
        m_images = HTMLCollection::create(*this, HTMLCollection::Scope::Descendants, [](Element const& element) {
            return is<HTML::HTMLImageElement>(element);
        });
    }
    return *m_images;
}

// https://html.spec.whatwg.org/multipage/dom.html#dom-document-embeds
GC::Ref<HTMLCollection> Document::embeds()
{
    if (!m_embeds) {
        m_embeds = HTMLCollection::create(*this, HTMLCollection::Scope::Descendants, [](Element const& element) {
            return is<HTML::HTMLEmbedElement>(element);
        });
    }
    return *m_embeds;
}

// https://html.spec.whatwg.org/multipage/dom.html#dom-document-plugins
GC::Ref<HTMLCollection> Document::plugins()
{
    return embeds();
}

// https://html.spec.whatwg.org/multipage/dom.html#dom-document-links
GC::Ref<HTMLCollection> Document::links()
{
    if (!m_links) {
        m_links = HTMLCollection::create(*this, HTMLCollection::Scope::Descendants, [](Element const& element) {
            return (is<HTML::HTMLAnchorElement>(element) || is<HTML::HTMLAreaElement>(element)) && element.has_attribute(HTML::AttributeNames::href);
        });
    }
    return *m_links;
}

// https://html.spec.whatwg.org/multipage/dom.html#dom-document-forms
GC::Ref<HTMLCollection> Document::forms()
{
    if (!m_forms) {
        m_forms = HTMLCollection::create(*this, HTMLCollection::Scope::Descendants, [](Element const& element) {
            return is<HTML::HTMLFormElement>(element);
        });
    }
    return *m_forms;
}

// https://html.spec.whatwg.org/multipage/dom.html#dom-document-scripts
GC::Ref<HTMLCollection> Document::scripts()
{
    if (!m_scripts) {
        m_scripts = HTMLCollection::create(*this, HTMLCollection::Scope::Descendants, [](Element const& element) {
            return is<HTML::HTMLScriptElement>(element);
        });
    }
    return *m_scripts;
}

// https://html.spec.whatwg.org/multipage/dom.html#dom-document-all
GC::Ref<HTML::HTMLAllCollection> Document::all()
{
    if (!m_all) {
        // The all attribute must return an HTMLAllCollection rooted at the Document node, whose filter matches all elements.
        m_all = HTML::HTMLAllCollection::create(*this, HTML::HTMLAllCollection::Scope::Descendants, [](Element const&) {
            return true;
        });
    }
    return *m_all;
}

// https://drafts.csswg.org/css-font-loading/#font-source
GC::Ref<CSS::FontFaceSet> Document::fonts()
{
    return m_fonts;
}

// https://html.spec.whatwg.org/multipage/obsolete.html#dom-document-clear
void Document::clear()
{
    // Do nothing
}

// https://html.spec.whatwg.org/multipage/obsolete.html#dom-document-captureevents
void Document::capture_events()
{
    // Do nothing
}

// https://html.spec.whatwg.org/multipage/obsolete.html#dom-document-releaseevents
void Document::release_events()
{
    // Do nothing
}

Optional<Color> Document::normal_link_color() const
{
    return m_normal_link_color;
}

Optional<Color> Document::active_link_color() const
{
    return m_active_link_color;
}

Optional<Color> Document::visited_link_color() const
{
    return m_visited_link_color;
}

// https://html.spec.whatwg.org/multipage/webappapis.html#relevant-settings-object
HTML::EnvironmentSettingsObject& Document::relevant_settings_object() const
{
    // Then, the relevant settings object for a platform object o is the environment settings object of the relevant Realm for o.
    return Bindings::principal_host_defined_environment_settings_object(realm());
}

// https://dom.spec.whatwg.org/#dom-document-createelement
WebIDL::ExceptionOr<GC::Ref<Element>> Document::create_element(String const& local_name, Variant<String, Bindings::ElementCreationOptions> const& options)
{
    // 1. If localName is not a valid element local name, then throw an "InvalidCharacterError" DOMException.
    if (!is_valid_element_local_name(local_name))
        return WebIDL::InvalidCharacterError::create(realm(), "Invalid character in tag name."_utf16);

    // 2. If this is an HTML document, then set localName to localName in ASCII lowercase.
    auto local_name_lower = document_type() == Type::HTML
        ? local_name.to_ascii_lowercase()
        : local_name;

    // 3. Let registry and is be the result of flattening element creation options given options and this.
    auto [registry, is_value] = TRY(flatten_element_creation_options(options));

    // 4. Let namespace be the HTML namespace, if this is an HTML document or this’s content type is
    //    "application/xhtml+xml"; otherwise null.
    Optional<FlyString> namespace_;
    if (document_type() == Type::HTML || content_type() == "application/xhtml+xml"sv)
        namespace_ = Namespace::HTML;

    // 5. Return the result of creating an element given this, localName, namespace, null, is, true, and registry.
    return TRY(DOM::create_element(*this, FlyString::from_utf8_without_validation(local_name_lower.bytes()), move(namespace_), {}, move(is_value), true, registry));
}

// https://dom.spec.whatwg.org/#dom-document-createelementns
// https://dom.spec.whatwg.org/#internal-createelementns-steps
WebIDL::ExceptionOr<GC::Ref<Element>> Document::create_element_ns(Optional<FlyString> const& namespace_, String const& qualified_name, Variant<String, Bindings::ElementCreationOptions> const& options)
{
    // 1. Let (namespace, prefix, localName) be the result of validating and extracting namespace and qualifiedName
    //    given "element".
    auto extracted_qualified_name = TRY(validate_and_extract(realm(), namespace_, qualified_name, ValidationContext::Element));

    // 2. Let registry and is be the result of flattening element creation options given options and this.
    auto [registry, is_value] = TRY(flatten_element_creation_options(options));

    // 3. Return the result of creating an element given document, localName, namespace, prefix, is, true, and registry.
    return TRY(DOM::create_element(*this, extracted_qualified_name.local_name(), extracted_qualified_name.namespace_(), extracted_qualified_name.prefix(), move(is_value), true, registry));
}

GC::Ref<DocumentFragment> Document::create_document_fragment()
{
    return realm().create<DocumentFragment>(*this);
}

GC::Ref<Text> Document::create_text_node(Utf16String data)
{
    return realm().create<Text>(*this, move(data));
}

// https://dom.spec.whatwg.org/#dom-document-createcdatasection
WebIDL::ExceptionOr<GC::Ref<CDATASection>> Document::create_cdata_section(Utf16String data)
{
    // 1. If this is an HTML document, then throw a "NotSupportedError" DOMException.
    if (is_html_document())
        return WebIDL::NotSupportedError::create(realm(), "This operation is not supported for HTML documents"_utf16);

    // 2. If data contains the string "]]>", then throw an "InvalidCharacterError" DOMException.
    if (data.contains("]]>"sv))
        return WebIDL::InvalidCharacterError::create(realm(), "String may not contain ']]>'"_utf16);

    // 3. Return a new CDATASection node with its data set to data and node document set to this.
    return realm().create<CDATASection>(*this, move(data));
}

GC::Ref<Comment> Document::create_comment(Utf16String data)
{
    return realm().create<Comment>(*this, move(data));
}

// https://dom.spec.whatwg.org/#dom-document-createprocessinginstruction
WebIDL::ExceptionOr<GC::Ref<ProcessingInstruction>> Document::create_processing_instruction(String const& target, Utf16String data)
{
    // 1. If target does not match the Name production, then throw an "InvalidCharacterError" DOMException.
    if (!is_valid_name(target))
        return WebIDL::InvalidCharacterError::create(realm(), "Invalid character in target name."_utf16);

    // 2. If data contains the string "?>", then throw an "InvalidCharacterError" DOMException.
    if (data.contains("?>"sv))
        return WebIDL::InvalidCharacterError::create(realm(), "String may not contain '?>'"_utf16);

    // 3. Return a new ProcessingInstruction node, with target set to target, data set to data, and node document set to this.
    return realm().create<ProcessingInstruction>(*this, move(data), target);
}

GC::Ref<Range> Document::create_range()
{
    return Range::create(*this);
}

// https://dom.spec.whatwg.org/#dom-document-createevent
WebIDL::ExceptionOr<GC::Ref<Event>> Document::create_event(StringView interface)
{
    auto& realm = this->realm();

    // NOTE: This is named event here, since we do step 5 and 6 as soon as possible for each case.
    // 1. Let constructor be null.
    GC::Ptr<Event> event;

    // 2. If interface is an ASCII case-insensitive match for any of the strings in the first column in the following table,
    //      then set constructor to the interface in the second column on the same row as the matching string:
    if (interface.equals_ignoring_ascii_case("beforeunloadevent"sv)) {
        event = HTML::BeforeUnloadEvent::create(realm, FlyString {});
    } else if (interface.equals_ignoring_ascii_case("compositionevent"sv)) {
        event = UIEvents::CompositionEvent::create(realm, String {});
    } else if (interface.equals_ignoring_ascii_case("customevent"sv)) {
        event = CustomEvent::create(realm, FlyString {});
    } else if (interface.equals_ignoring_ascii_case("devicemotionevent"sv)) {
        event = Event::create(realm, FlyString {}); // FIXME: Create DeviceMotionEvent
    } else if (interface.equals_ignoring_ascii_case("deviceorientationevent"sv)) {
        event = Event::create(realm, FlyString {}); // FIXME: Create DeviceOrientationEvent
    } else if (interface.equals_ignoring_ascii_case("dragevent"sv)) {
        event = HTML::DragEvent::create(realm, FlyString {});
    } else if (interface.equals_ignoring_ascii_case("event"sv)
        || interface.equals_ignoring_ascii_case("events"sv)) {
        event = Event::create(realm, FlyString {});
    } else if (interface.equals_ignoring_ascii_case("focusevent"sv)) {
        event = UIEvents::FocusEvent::create(realm, FlyString {});
    } else if (interface.equals_ignoring_ascii_case("hashchangeevent"sv)) {
        event = HTML::HashChangeEvent::create(realm, FlyString {}, {});
    } else if (interface.equals_ignoring_ascii_case("htmlevents"sv)) {
        event = Event::create(realm, FlyString {});
    } else if (interface.equals_ignoring_ascii_case("keyboardevent"sv)) {
        event = UIEvents::KeyboardEvent::create(realm, String {});
    } else if (interface.equals_ignoring_ascii_case("messageevent"sv)) {
        event = HTML::MessageEvent::create(realm, String {});
    } else if (interface.equals_ignoring_ascii_case("mouseevent"sv)
        || interface.equals_ignoring_ascii_case("mouseevents"sv)) {
        event = UIEvents::MouseEvent::create(realm, FlyString {});
    } else if (interface.equals_ignoring_ascii_case("storageevent"sv)) {
        event = HTML::StorageEvent::create(realm, FlyString {});
    } else if (interface.equals_ignoring_ascii_case("svgevents"sv)) {
        event = Event::create(realm, FlyString {});
    } else if (interface.equals_ignoring_ascii_case("textevent"sv)) {
        event = UIEvents::TextEvent::create(realm, FlyString {});
    } else if (interface.equals_ignoring_ascii_case("touchevent"sv)) {
        event = Event::create(realm, FlyString {}); // FIXME: Create TouchEvent
    } else if (interface.equals_ignoring_ascii_case("uievent"sv)
        || interface.equals_ignoring_ascii_case("uievents"sv)) {
        event = UIEvents::UIEvent::create(realm, FlyString {});
    }

    // 3. If constructor is null, then throw a "NotSupportedError" DOMException.
    if (!event) {
        return WebIDL::NotSupportedError::create(realm, "No constructor for interface found"_utf16);
    }

    // FIXME: 4. If the interface indicated by constructor is not exposed on the relevant global object of this, then throw a "NotSupportedError" DOMException.

    // NOTE: These are done in the if-chain above
    // 5. Let event be the result of creating an event given constructor.
    // 6. Initialize event’s type attribute to the empty string.
    // 7. Initialize event’s timeStamp attribute to the result of calling current high resolution time with this’s relevant global object.
    // NOTE: This is handled by each constructor.

    // 8. Initialize event’s isTrusted attribute to false.
    event->set_is_trusted(false);

    // 9. Unset event’s initialized flag.
    event->set_initialized(false);

    // 10. Return event.
    return GC::Ref(*event);
}

void Document::set_pending_parsing_blocking_script(HTML::HTMLScriptElement* script)
{
    m_pending_parsing_blocking_script = script;
}

GC::Ref<HTML::HTMLScriptElement> Document::take_pending_parsing_blocking_script(Badge<HTML::HTMLParser>)
{
    VERIFY(m_pending_parsing_blocking_script);
    auto script = m_pending_parsing_blocking_script;
    m_pending_parsing_blocking_script = nullptr;
    return *script;
}

void Document::set_pending_parsing_blocking_svg_script(SVG::SVGScriptElement* script)
{
    m_pending_parsing_blocking_svg_script = script;
}

GC::Ref<SVG::SVGScriptElement> Document::take_pending_parsing_blocking_svg_script(Badge<HTML::HTMLParser>)
{
    VERIFY(m_pending_parsing_blocking_svg_script);
    auto script = m_pending_parsing_blocking_svg_script;
    m_pending_parsing_blocking_svg_script = nullptr;
    return *script;
}

void Document::add_script_to_execute_when_parsing_has_finished(Badge<HTML::HTMLScriptElement>, HTML::HTMLScriptElement& script)
{
    m_scripts_to_execute_when_parsing_has_finished.append(script);
}

Vector<GC::Root<HTML::HTMLScriptElement>> Document::take_scripts_to_execute_when_parsing_has_finished(Badge<HTML::HTMLParser>)
{
    Vector<GC::Root<HTML::HTMLScriptElement>> handles;
    for (auto script : m_scripts_to_execute_when_parsing_has_finished)
        handles.append(GC::make_root(script));
    m_scripts_to_execute_when_parsing_has_finished.clear();
    return handles;
}

void Document::add_script_to_execute_as_soon_as_possible(Badge<HTML::HTMLScriptElement>, HTML::HTMLScriptElement& script)
{
    m_scripts_to_execute_as_soon_as_possible.append(script);
}

Vector<GC::Root<HTML::HTMLScriptElement>> Document::take_scripts_to_execute_as_soon_as_possible(Badge<HTML::HTMLParser>)
{
    Vector<GC::Root<HTML::HTMLScriptElement>> handles;
    for (auto script : m_scripts_to_execute_as_soon_as_possible)
        handles.append(GC::make_root(script));
    m_scripts_to_execute_as_soon_as_possible.clear();
    return handles;
}

void Document::add_script_to_execute_in_order_as_soon_as_possible(Badge<HTML::HTMLScriptElement>, HTML::HTMLScriptElement& script)
{
    m_scripts_to_execute_in_order_as_soon_as_possible.append(script);
}

Vector<GC::Root<HTML::HTMLScriptElement>> Document::take_scripts_to_execute_in_order_as_soon_as_possible(Badge<HTML::HTMLParser>)
{
    Vector<GC::Root<HTML::HTMLScriptElement>> handles;
    for (auto script : m_scripts_to_execute_in_order_as_soon_as_possible)
        handles.append(GC::make_root(script));
    m_scripts_to_execute_in_order_as_soon_as_possible.clear();
    return handles;
}

// https://dom.spec.whatwg.org/#dom-document-importnode
WebIDL::ExceptionOr<GC::Ref<Node>> Document::import_node(GC::Ref<Node> node, Variant<bool, Bindings::ImportNodeOptions> options)
{
    // 1. If node is a document or shadow root, then throw a "NotSupportedError" DOMException.
    if (is<Document>(*node) || is<ShadowRoot>(*node))
        return WebIDL::NotSupportedError::create(realm(), "Cannot import a document or shadow root."_utf16);

    // 2. Let subtree be false.
    bool subtree = false;

    // 3. Let registry be null.
    GC::Ptr<HTML::CustomElementRegistry> registry;

    TRY(options.visit(
        // 4. If options is a boolean, then set subtree to options.
        [&subtree](bool const& value) -> WebIDL::ExceptionOr<void> {
            subtree = value;
            return {};
        },
        // 5. Otherwise:
        [&subtree, &registry, this](Bindings::ImportNodeOptions const& options) -> WebIDL::ExceptionOr<void> {
            // 1. Set subtree to the negation of options["selfOnly"].
            subtree = !options.self_only;

            // 2. If options["customElementRegistry"] exists, then set registry to it.
            if (options.custom_element_registry.has_value())
                registry = options.custom_element_registry->ptr();

            // 3. If registry’s is scoped is false and registry is not this’s custom element registry, then throw a
            //    "NotSupportedError" DOMException.
            if (registry && !registry->is_scoped() && registry != custom_element_registry())
                return WebIDL::NotSupportedError::create(realm(), "'customElementRegistry' in ImportNodeOptions must either be scoped or the document's custom element registry."_utf16);
            return {};
        }));

    // 6. If registry is null, then set registry to the result of looking up a custom element registry given this.
    if (!registry)
        registry = HTML::look_up_a_custom_element_registry(*this);

    // 7. Return the result of cloning a node given node with document set to this, subtree set to subtree, and
    //   fallbackRegistry set to registry.
    return node->clone_node(this, subtree, nullptr, registry);
}

// https://dom.spec.whatwg.org/#concept-node-adopt
void Document::adopt_node(Node& node)
{
    // 1. Let oldDocument be node’s node document.
    auto& old_document = node.document();

    // 2. If node’s parent is non-null, then remove node.
    if (node.parent())
        node.remove();

    // 3. If document is not oldDocument, then:
    if (&old_document != this) {
        // 1. For each inclusiveDescendant in node’s shadow-including inclusive descendants:
        node.for_each_shadow_including_inclusive_descendant([&](DOM::Node& inclusive_descendant) {
            // 1. Set inclusiveDescendant’s node document to document.
            inclusive_descendant.set_document(Badge<Document> {}, *this);

            // 2. If inclusiveDescendant is a shadow root and if any of the following are true:
            //    - inclusiveDescendant’s custom element registry is null and inclusiveDescendant’s keep custom element
            //      registry null is false; or
            //    - inclusiveDescendant’s custom element registry is a global custom element registry,
            //    then set inclusiveDescendant’s custom element registry to document’s effective global custom element
            //    registry.
            if (auto* shadow_root = as_if<ShadowRoot>(inclusive_descendant); shadow_root
                && ((shadow_root->custom_element_registry() == nullptr && !shadow_root->keep_custom_element_registry_null())
                    || HTML::is_a_global_custom_element_registry(shadow_root->custom_element_registry()))) {

                shadow_root->set_custom_element_registry(effective_global_custom_element_registry());
            }

            // 3. Otherwise, if inclusiveDescendant is an element:
            else if (auto* element = as_if<Element>(inclusive_descendant)) {
                // FIXME: 1. Set the node document of each attribute in inclusiveDescendant’s attribute list to document.

                // 2. If inclusiveDescendant’s custom element registry is null or inclusiveDescendant’s custom element
                //    registry’s is scoped is false, then set inclusiveDescendant’s custom element registry to
                //    document’s effective global custom element registry.
                if (!element->custom_element_registry() || !element->custom_element_registry()->is_scoped()) {
                    element->set_custom_element_registry(effective_global_custom_element_registry());
                }
            }

            return TraversalDecision::Continue;
        });

        // 2. For each inclusiveDescendant in node’s shadow-including inclusive descendants that is custom, in
        //    shadow-including tree order:
        //    enqueue a custom element callback reaction with inclusiveDescendant, callback name "adoptedCallback", and
        //    « oldDocument, document ».
        node.for_each_shadow_including_inclusive_descendant([&](DOM::Node& inclusive_descendant) {
            if (auto* element = as_if<Element>(inclusive_descendant); element && element->is_custom()) {
                auto& vm = this->vm();

                GC::RootVector<JS::Value> arguments { vm.heap() };
                arguments.append(&old_document);
                arguments.append(this);

                element->enqueue_a_custom_element_callback_reaction(HTML::CustomElementReactionNames::adoptedCallback, move(arguments));
            }

            return TraversalDecision::Continue;
        });

        // 3. For each inclusiveDescendant in node’s shadow-including inclusive descendants, in shadow-including tree
        //     order:
        //    run the adopting steps with inclusiveDescendant and oldDocument.
        node.for_each_shadow_including_inclusive_descendant([&](auto& inclusive_descendant) {
            inclusive_descendant.adopted_from(old_document);
            return TraversalDecision::Continue;
        });

        // AD-HOC: Transfer NodeIterators rooted at `node` from old_document to this document.
        Vector<NodeIterator&> node_iterators_to_transfer;
        for (auto node_iterator : old_document.m_node_iterators) {
            if (node_iterator->root().ptr() == &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
WebIDL::ExceptionOr<GC::Ref<Node>> Document::adopt_node_binding(GC::Ref<Node> node)
{
    if (is<Document>(*node))
        return WebIDL::NotSupportedError::create(realm(), "Cannot adopt a document into a document"_utf16);

    if (is<ShadowRoot>(*node))
        return WebIDL::HierarchyRequestError::create(realm(), "Cannot adopt a shadow root into a document"_utf16);

    if (auto* fragment = as_if<DocumentFragment>(*node); fragment && fragment->host())
        return node;

    adopt_node(*node);

    return node;
}

DocumentType const* Document::doctype() const
{
    return first_child_of_type<DocumentType>();
}

String const& Document::compat_mode() const
{
    static String const back_compat = "BackCompat"_string;
    static String const css1_compat = "CSS1Compat"_string;

    if (m_quirks_mode == QuirksMode::Yes)
        return back_compat;

    return css1_compat;
}

void Document::update_active_element()
{
    set_active_element(calculate_active_element(*this));
}

void Document::set_focused_area(GC::Ptr<Node> node)
{
    if (m_focused_area == node)
        return;

    GC::Ptr old_focused_area = m_focused_area;

    if (auto* old_focused_element = as_if<Element>(old_focused_area.ptr()))
        old_focused_element->did_lose_focus();

    auto* common_ancestor = find_common_ancestor(old_focused_area, node);

    GC::Ptr<Node> old_focused_node_root = nullptr;
    GC::Ptr<Node> new_focused_node_root = nullptr;
    if (old_focused_area)
        old_focused_node_root = old_focused_area->root();
    if (node)
        new_focused_node_root = node->root();
    if (old_focused_node_root != new_focused_node_root) {
        if (old_focused_node_root) {
            CSS::Invalidation::invalidate_style_after_pseudo_class_state_change(CSS::PseudoClass::Focus, *this, m_focused_area, *old_focused_node_root, node);
            CSS::Invalidation::invalidate_style_after_pseudo_class_state_change(CSS::PseudoClass::FocusWithin, *this, m_focused_area, *old_focused_node_root, node);
            CSS::Invalidation::invalidate_style_after_pseudo_class_state_change(CSS::PseudoClass::FocusVisible, *this, m_focused_area, *old_focused_node_root, node);
        }
        if (new_focused_node_root) {
            CSS::Invalidation::invalidate_style_after_pseudo_class_state_change(CSS::PseudoClass::Focus, *this, m_focused_area, *new_focused_node_root, node);
            CSS::Invalidation::invalidate_style_after_pseudo_class_state_change(CSS::PseudoClass::FocusWithin, *this, m_focused_area, *new_focused_node_root, node);
            CSS::Invalidation::invalidate_style_after_pseudo_class_state_change(CSS::PseudoClass::FocusVisible, *this, m_focused_area, *new_focused_node_root, node);
        }
    } else {
        CSS::Invalidation::invalidate_style_after_pseudo_class_state_change(CSS::PseudoClass::Focus, *this, m_focused_area, *common_ancestor, node);
        CSS::Invalidation::invalidate_style_after_pseudo_class_state_change(CSS::PseudoClass::FocusWithin, *this, m_focused_area, *common_ancestor, node);
        CSS::Invalidation::invalidate_style_after_pseudo_class_state_change(CSS::PseudoClass::FocusVisible, *this, m_focused_area, *common_ancestor, node);
    }

    m_focused_area = node;

    auto* new_focused_element = as_if<Element>(node.ptr());
    if (new_focused_element)
        new_focused_element->did_receive_focus();

    reset_cursor_blink_cycle();

    set_needs_repaint();

    // Scroll the viewport if necessary to make the newly focused element visible.
    if (new_focused_element) {
        new_focused_element->queue_an_element_task(HTML::Task::Source::UserInteraction, [new_focused_element] {
            if (new_focused_element->document().focused_area().ptr() != new_focused_element)
                return;
            Bindings::ScrollIntoViewOptions scroll_options;
            scroll_options.block = Bindings::ScrollLogicalPosition::Nearest;
            scroll_options.inline_ = Bindings::ScrollLogicalPosition::Nearest;
            (void)new_focused_element->scroll_into_view(scroll_options);
        });
    }

    update_active_element();
}

Element const* Document::active_element() const
{
    return m_active_element ? m_active_element.ptr() : body();
}

void Document::set_active_element(GC::Ptr<Element> element)
{
    if (m_active_element.ptr() == element)
        return;

    m_active_element = element;

    set_needs_repaint();
}

void Document::set_target_element(GC::Ptr<Element> element)
{
    if (m_target_element.ptr() == element)
        return;

    GC::Ptr<Element> old_target_element = move(m_target_element);

    auto* common_ancestor = find_common_ancestor(old_target_element, element);

    GC::Ptr<Node> old_target_node_root = nullptr;
    GC::Ptr<Node> new_target_node_root = nullptr;
    if (old_target_element)
        old_target_node_root = old_target_element->root();
    if (element)
        new_target_node_root = element->root();
    if (old_target_node_root != new_target_node_root) {
        if (old_target_node_root) {
            CSS::Invalidation::invalidate_style_after_pseudo_class_state_change(CSS::PseudoClass::Target, *this, m_target_element, *old_target_node_root, element);
        }
        if (new_target_node_root) {
            CSS::Invalidation::invalidate_style_after_pseudo_class_state_change(CSS::PseudoClass::Target, *this, m_target_element, *new_target_node_root, element);
        }
    } else {
        CSS::Invalidation::invalidate_style_after_pseudo_class_state_change(CSS::PseudoClass::Target, *this, m_target_element, *common_ancestor, element);
    }

    m_target_element = element;

    set_needs_repaint();
}

// https://html.spec.whatwg.org/multipage/interaction.html#flush-autofocus-candidates
void Document::flush_autofocus_candidates()
{
    // 1. If topDocument's autofocus processed flag is true, then return.
    if (m_autofocus_processed_flag)
        return;

    // 2. Let candidates be topDocument's autofocus candidates.
    auto& candidates = m_autofocus_candidates;

    // 3. If candidates is empty, then return.
    if (candidates.is_empty())
        return;

    // 4. If topDocument's focused area is not topDocument itself, or topDocument has non-null target element, then:
    if ((m_focused_area && m_focused_area.ptr() != this) || m_target_element) {
        // 1. Empty candidates.
        candidates.clear();

        // 2. Set topDocument's autofocus processed flag to true.
        m_autofocus_processed_flag = true;

        // 3. Return.
        return;
    }

    // 5. While candidates is not empty:
    while (!candidates.is_empty()) {
        // 1. Let element be candidates[0].
        GC::Ref<Element> element = candidates.first();

        // 2. Let doc be element's node document.
        auto& doc = element->document();

        // 3. If doc is not fully active, then remove element from candidates, and continue.
        if (!doc.is_fully_active()) {
            candidates.take_first();
            continue;
        }

        // 4. If doc's node navigable's top-level traversable is not the same as topDocument's node navigable, then
        //    remove element from candidates, and continue.
        auto doc_navigable = doc.navigable();
        if (!doc_navigable || doc_navigable->top_level_traversable() != navigable()) {
            candidates.take_first();
            continue;
        }

        // 5. If doc's script-blocking style sheet set is not empty, then return.
        if (!doc.script_blocking_style_sheet_set().is_empty())
            return;

        // 6. Remove element from candidates.
        candidates.take_first();

        // 7. Let inclusiveAncestorDocuments be a list consisting of the active document of doc's inclusive ancestor navigables.
        GC::RootVector<GC::Ref<Document>> inclusive_ancestor_documents(heap());
        inclusive_ancestor_documents.append(doc);
        auto ancestor_navigable = doc_navigable->parent();
        while (ancestor_navigable) {
            if (auto active = ancestor_navigable->active_document())
                inclusive_ancestor_documents.append(*active);
            ancestor_navigable = ancestor_navigable->parent();
        }

        // 8. If any Document in inclusiveAncestorDocuments has non-null target element, then continue.
        auto any_ancestor_has_target = false;
        for (auto& ancestor : inclusive_ancestor_documents) {
            if (ancestor->target_element()) {
                any_ancestor_has_target = true;
                break;
            }
        }
        if (any_ancestor_has_target)
            continue;

        // 9. Let target be element.
        GC::Ptr<Element> target = element;

        // FIXME: 10. If target is not a focusable area, then set target to the result of getting the
        //            focusable area for target.
        // AD-HOC: We don't implement "get the focusable area" so for now, treat unconnected and non-focusable elements
        //         as having no focusable area.
        if (!target->is_connected() || !target->is_focusable())
            target = nullptr;

        // 11. If target is not null, then:
        if (target) {
            // 1. Empty candidates.
            candidates.clear();

            // 2. Set topDocument's autofocus processed flag to true.
            m_autofocus_processed_flag = true;

            // 3. Run the focusing steps for target.
            HTML::run_focusing_steps(target.ptr());
        }
    }
}

// https://html.spec.whatwg.org/multipage/browsing-the-web.html#the-indicated-part-of-the-document
Document::IndicatedPart Document::determine_the_indicated_part() const
{
    // For an HTML document document, the following processing model must be followed to determine its indicated part:

    // 1. Let fragment be document's URL's fragment.
    auto fragment = url().fragment();

    // 2. If fragment is the empty string, then return the special value top of the document.
    if (!fragment.has_value() || fragment->is_empty())
        return Document::TopOfTheDocument {};

    // 3. Let potentialIndicatedElement be the result of finding a potential indicated element given document and fragment.
    auto* potential_indicated_element = find_a_potential_indicated_element(*fragment);

    // 4. If potentialIndicatedElement is not null, then return potentialIndicatedElement.
    if (potential_indicated_element)
        return potential_indicated_element;

    // 5. Let fragmentBytes be the result of percent-decoding fragment.
    // 6. Let decodedFragment be the result of running UTF-8 decode without BOM on fragmentBytes.
    auto decoded_fragment = String::from_utf8_with_replacement_character(URL::percent_decode(*fragment), String::WithBOMHandling::No);

    // 7. Set potentialIndicatedElement to the result of finding a potential indicated element given document and decodedFragment.
    potential_indicated_element = find_a_potential_indicated_element(decoded_fragment);

    // 8. If potentialIndicatedElement is not null, then return potentialIndicatedElement.
    if (potential_indicated_element)
        return potential_indicated_element;

    // 9. If decodedFragment is an ASCII case-insensitive match for the string top, then return the top of the document.
    if (decoded_fragment.equals_ignoring_ascii_case("top"sv))
        return Document::TopOfTheDocument {};

    // 10. Return null.
    return nullptr;
}

// https://html.spec.whatwg.org/multipage/browsing-the-web.html#find-a-potential-indicated-element
Element* Document::find_a_potential_indicated_element(FlyString const& fragment) const
{
    // To find a potential indicated element given a Document document and a string fragment, run these steps:

    // 1. If there is an element in the document tree whose root is document and that has an ID equal to
    //    fragment, then return the first such element in tree order.
    if (auto element = get_element_by_id(fragment))
        return const_cast<Element*>(element.ptr());

    // 2. If there is an a element in the document tree whose root is document that has a name attribute
    //    whose value is equal to fragment, then return the first such element in tree order.
    Element* element_with_name = nullptr;
    root().for_each_in_subtree_of_type<Element>([&](Element const& element) {
        if (element.name() == fragment) {
            element_with_name = const_cast<Element*>(&element);
            return TraversalDecision::Break;
        }
        return TraversalDecision::Continue;
    });
    if (element_with_name)
        return element_with_name;

    // 3. Return null.
    return nullptr;
}

// https://drafts.csswg.org/css-transitions-2/#event-dispatch
void Document::dispatch_events_for_transition(GC::Ref<CSS::CSSTransition> transition)
{
    auto previous_phase = transition->previous_phase();

    using Phase = CSS::CSSTransition::Phase;
    // The transition phase of a transition is initially ‘idle’ and is updated on each
    // animation frame according to the first matching condition from below:
    auto transition_phase = Phase::Idle;

    if (!transition->effect()) {
        // If the transition has no associated effect,
        if (!transition->current_time().has_value()) {
            // If the transition has an unresolved current time,
            //   The transition phase is ‘idle’.
        } else if (transition->current_time()->value < 0) {
            // If the transition has a current time < 0,
            //   The transition phase is ‘before’.
            transition_phase = Phase::Before;
        } else {
            // Otherwise,
            //   The transition phase is ‘after’.
            transition_phase = Phase::After;
        }
    } else if (transition->pending() && (previous_phase == Phase::Idle || previous_phase == Phase::Pending)) {
        // If the transition has a pending play task or a pending pause task
        // and its phase was previously ‘idle’ or ‘pending’,
        //   The transition phase is ‘pending’.
        transition_phase = Phase::Pending;
    } else {
        // Otherwise,
        //   The transition phase is the phase of its associated effect.
        transition_phase = Phase(to_underlying(transition->effect()->phase()));
    }

    enum class Interval : u8 {
        Start,
        End,
        ActiveTime,
    };

    auto dispatch_event = [&](FlyString const& type, Interval interval) {
        // The target for a transition event is the transition’s owning element. If there is no owning element,
        // no transition events are dispatched.
        if (!transition->effect() || !transition->owning_element().has_value())
            return;

        auto effect = transition->effect();

        Animations::TimeValue elapsed_time = [&]() {
            if (interval == Interval::Start)
                return max(min(-effect->start_delay(), effect->active_duration()), Animations::TimeValue::create_zero(transition->timeline()));
            if (interval == Interval::End)
                return max(min(transition->associated_effect_end() - effect->start_delay(), effect->active_duration()), Animations::TimeValue::create_zero(transition->timeline()));
            if (interval == Interval::ActiveTime) {
                // The active time of the animation at the moment it was canceled calculated using a fill mode of both.
                // FIXME: Compute this properly.
                return Animations::TimeValue::create_zero(transition->timeline());
            }
            VERIFY_NOT_REACHED();
        }();

        double elapsed_time_output;

        switch (elapsed_time.type) {
        case Animations::TimeValue::Type::Milliseconds:
            elapsed_time_output = elapsed_time.value / 1000;
            break;
        case Animations::TimeValue::Type::Percentage:
            // FIXME: The spec doesn't specify how to handle this case
            elapsed_time_output = 0;
            break;
        }

        Bindings::TransitionEventInit event_init {};
        event_init.bubbles = true;
        event_init.property_name = MUST(String::from_utf8(transition->transition_property()));
        event_init.elapsed_time = elapsed_time_output;
        event_init.pseudo_element = transition->owning_element()->pseudo_element().map([](auto it) {
                                                                                      return MUST(String::formatted("::{}", CSS::pseudo_element_name(it)));
                                                                                  })
                                        .value_or({});

        append_pending_animation_event({
            .event = CSS::TransitionEvent::create(
                transition->owning_element()->element().realm(),
                type,
                event_init),
            .animation = transition,
            .target = transition->owning_element()->element(),
            .scheduled_event_time = HighResolutionTime::unsafe_shared_current_time(),
        });
    };

    if (previous_phase == Phase::Idle) {
        if (transition_phase == Phase::Pending || transition_phase == Phase::Before)
            dispatch_event(HTML::EventNames::transitionrun, Interval::Start);

        if (transition_phase == Phase::Active) {
            dispatch_event(HTML::EventNames::transitionrun, Interval::Start);
            dispatch_event(HTML::EventNames::transitionstart, Interval::Start);
        }

        if (transition_phase == Phase::After) {
            dispatch_event(HTML::EventNames::transitionrun, Interval::Start);
            dispatch_event(HTML::EventNames::transitionstart, Interval::Start);
            dispatch_event(HTML::EventNames::transitionend, Interval::End);
        }
    } else if (previous_phase == Phase::Pending || previous_phase == Phase::Before) {
        if (transition_phase == Phase::Active)
            dispatch_event(HTML::EventNames::transitionstart, Interval::Start);

        if (transition_phase == Phase::After) {
            dispatch_event(HTML::EventNames::transitionstart, Interval::Start);
            dispatch_event(HTML::EventNames::transitionend, Interval::End);
        }
    } else if (previous_phase == Phase::Active) {
        if (transition_phase == Phase::After)
            dispatch_event(HTML::EventNames::transitionend, Interval::End);

        if (transition_phase == Phase::Before)
            dispatch_event(HTML::EventNames::transitionend, Interval::Start);
    } else if (previous_phase == Phase::After) {
        if (transition_phase == Phase::Active)
            dispatch_event(HTML::EventNames::transitionstart, Interval::End);

        if (transition_phase == Phase::Before) {
            dispatch_event(HTML::EventNames::transitionstart, Interval::End);
            dispatch_event(HTML::EventNames::transitionend, Interval::Start);
        }
    }

    if (transition_phase == Phase::Idle) {
        if (previous_phase != Phase::Idle && previous_phase != Phase::After)
            dispatch_event(HTML::EventNames::animationstart, Interval::ActiveTime);
    }

    transition->set_previous_phase(transition_phase);
}

// https://www.w3.org/TR/css-animations-2/#event-dispatch
void Document::dispatch_events_for_animation_if_necessary(GC::Ref<Animations::Animation> animation)
{
    if (animation->is_css_transition()) {
        dispatch_events_for_transition(as<CSS::CSSTransition>(*animation));
        return;
    }

    // Each time a new animation frame is established and the animation does not have a pending play task or pending
    // pause task, the events to dispatch are determined by comparing the animation’s phase before and after
    // establishing the new animation frame as follows:
    auto effect = animation->effect();
    if (!effect || !effect->is_keyframe_effect() || !animation->is_css_animation() || animation->pending())
        return;

    auto& css_animation = as<CSS::CSSAnimation>(*animation);

    GC::Ptr<Element> target = effect->target();
    if (!target)
        return;

    auto previous_phase = effect->previous_phase();
    auto current_phase = effect->phase();
    auto current_iteration = effect->current_iteration().value_or(0.0);

    auto owning_element = css_animation.owning_element();

    auto dispatch_event = [&](FlyString const& name, Animations::TimeValue elapsed_time) {
        double elapsed_time_output;
        switch (elapsed_time.type) {
        case Animations::TimeValue::Type::Milliseconds:
            elapsed_time_output = elapsed_time.value / 1000;
            break;
        case Animations::TimeValue::Type::Percentage:
            // FIXME: The spec doesn't specify how to handle this case
            elapsed_time_output = 0;
            break;
        }

        Bindings::AnimationEventInit event_init {};
        event_init.bubbles = true;
        event_init.animation_name = static_cast<String>(css_animation.animation_name());
        event_init.elapsed_time = elapsed_time_output;
        event_init.pseudo_element = owning_element->pseudo_element().map([](auto it) {
                                                                        return MUST(String::formatted("::{}", CSS::pseudo_element_name(it)));
                                                                    })
                                        .value_or({});

        append_pending_animation_event({
            .event = CSS::AnimationEvent::create(
                owning_element->element().realm(),
                name,
                event_init),
            .animation = css_animation,
            .target = *target,
            .scheduled_event_time = HighResolutionTime::unsafe_shared_current_time(),
        });
    };

    // For calculating the elapsedTime of each event, the following definitions are used:

    // - interval start = max(min(-start delay, active duration), 0)
    auto interval_start = max(min(-effect->start_delay(), effect->active_duration()), Animations::TimeValue::create_zero(animation->timeline()));

    // - interval end = max(min(associated effect end - start delay, active duration), 0)
    auto interval_end = max(min(effect->end_time() - effect->start_delay(), effect->active_duration()), Animations::TimeValue::create_zero(animation->timeline()));

    switch (previous_phase) {
    case Animations::AnimationEffect::Phase::Before:
        [[fallthrough]];
    case Animations::AnimationEffect::Phase::Idle:
        if (current_phase == Animations::AnimationEffect::Phase::Active) {
            dispatch_event(HTML::EventNames::animationstart, interval_start);
        } else if (current_phase == Animations::AnimationEffect::Phase::After) {
            dispatch_event(HTML::EventNames::animationstart, interval_start);
            dispatch_event(HTML::EventNames::animationend, interval_end);
        }
        break;
    case Animations::AnimationEffect::Phase::Active:
        if (current_phase == Animations::AnimationEffect::Phase::Before) {
            dispatch_event(HTML::EventNames::animationend, interval_start);
        } else if (current_phase == Animations::AnimationEffect::Phase::Active) {
            auto previous_current_iteration = effect->previous_current_iteration();
            if (previous_current_iteration != current_iteration) {
                // The elapsed time for an animationiteration event is defined as follows:

                // 1. Let previous current iteration be the current iteration from the previous animation frame.

                // 2. If previous current iteration is greater than current iteration, let iteration boundary be current iteration + 1,
                //    otherwise let it be current iteration.
                auto iteration_boundary = previous_current_iteration > current_iteration ? current_iteration + 1 : current_iteration;

                // 3. The elapsed time is the result of evaluating (iteration boundary - iteration start) × iteration duration).
                auto elapsed_time = effect->iteration_duration() * (iteration_boundary - effect->iteration_start());

                dispatch_event(HTML::EventNames::animationiteration, elapsed_time);
            }
        } else if (current_phase == Animations::AnimationEffect::Phase::After) {
            dispatch_event(HTML::EventNames::animationend, interval_end);
        }
        break;
    case Animations::AnimationEffect::Phase::After:
        if (current_phase == Animations::AnimationEffect::Phase::Active) {
            dispatch_event(HTML::EventNames::animationstart, interval_end);
        } else if (current_phase == Animations::AnimationEffect::Phase::Before) {
            dispatch_event(HTML::EventNames::animationstart, interval_end);
            dispatch_event(HTML::EventNames::animationend, interval_start);
        }
        break;
    }

    if (current_phase == Animations::AnimationEffect::Phase::Idle && previous_phase != Animations::AnimationEffect::Phase::Idle && previous_phase != Animations::AnimationEffect::Phase::After) {
        // FIXME: Calculate a non-zero time when the animation is cancelled by means other than calling cancel()
        auto cancel_time = animation->release_saved_cancel_time().value_or(Animations::TimeValue::create_zero(animation->timeline()));
        dispatch_event(HTML::EventNames::animationcancel, cancel_time);
    }

    effect->set_previous_phase(current_phase);
    effect->set_previous_current_iteration(current_iteration);
}

// https://html.spec.whatwg.org/multipage/browsing-the-web.html#scroll-to-the-fragment-identifier
void Document::scroll_to_the_fragment()
{
    // To scroll to the fragment given a Document document:

    // 1. If document's indicated part is null, then set document's target element to null.
    auto indicated_part = determine_the_indicated_part();
    if (indicated_part.has<Element*>() && indicated_part.get<Element*>() == nullptr) {
        set_target_element(nullptr);
    }

    // 2. Otherwise, if document's indicated part is top of the document, then:
    else if (indicated_part.has<TopOfTheDocument>()) {
        // 1. Set document's target element to null.
        set_target_element(nullptr);

        // 2. Scroll to the beginning of the document for document. [CSSOMVIEW]
        scroll_to_the_beginning_of_the_document();

        // 3. Return.
        return;
    }

    // 3. Otherwise:
    else {
        // 1. Assert: document's indicated part is an element.
        VERIFY(indicated_part.has<Element*>());

        // 2. Let target be document's indicated part.
        auto target = indicated_part.get<Element*>();

        // 3. Set document's target element to target.
        set_target_element(target);

        // FIXME: 4. Run the ancestor revealing algorithm on target.

        // 5. Scroll target into view, with behavior set to "auto", block set to "start", and inline set to "nearest". [CSSOMVIEW]
        Bindings::ScrollIntoViewOptions scroll_options;
        scroll_options.block = Bindings::ScrollLogicalPosition::Start;
        scroll_options.inline_ = Bindings::ScrollLogicalPosition::Nearest;
        (void)target->scroll_into_view(scroll_options);

        // 6. Run the focusing steps for target, with the Document's viewport as the fallback target.
        // FIXME: Pass the Document's viewport somehow.
        HTML::run_focusing_steps(target);

        // FIXME: 7. Move the sequential focus navigation starting point to target.
    }
}

// https://html.spec.whatwg.org/multipage/browsing-the-web.html#try-to-scroll-to-the-fragment
void Document::try_to_scroll_to_the_fragment()
{
    // FIXME: According to the spec we should only scroll here if document has no parser or parsing has stopped.
    //        It should be ok to remove this after we implement navigation events and scrolling will happen in
    //        "process scroll behavior".
    //  To try to scroll to the fragment for a Document document, perform the following steps in parallel:
    //  1. Wait for an implementation-defined amount of time. (This is intended to allow the user agent to
    //     optimize the user experience in the face of performance concerns.)
    //  2. Queue a global task on the navigation and traversal task source given document's relevant global
    //     object to run these steps:
    //      1. If document has no parser, or its parser has stopped parsing, or the user agent has reason to
    //         believe the user is no longer interested in scrolling to the fragment, then abort these steps.
    //      2. Scroll to the fragment given document.
    //      3. If document's indicated part is still null, then try to scroll to the fragment for document.

    scroll_to_the_fragment();
}

// https://drafts.csswg.org/cssom-view-1/#scroll-to-the-beginning-of-the-document
void Document::scroll_to_the_beginning_of_the_document()
{
    // FIXME: Actually implement this algorithm
    if (auto navigable = this->navigable())
        navigable->perform_scroll_of_viewport_scrolling_box({ 0, 0 });
}

StringView 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/multipage/dom.html#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;

    // 2. Set document's current document readiness to readinessValue.
    m_readiness = readiness_value;

    if (readiness_value != HTML::DocumentReadyState::Loading)
        page().client().request_frame();

    // 3. If document is associated with an HTML parser, then:
    if (m_parser) {
        // 1. Let now be the current high resolution time given document's relevant global object.
        auto now = HighResolutionTime::current_high_resolution_time(relevant_global_object(*this));

        // 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.
        if (readiness_value == HTML::DocumentReadyState::Complete && m_load_timing_info.dom_complete_time == 0) {
            m_load_timing_info.dom_complete_time = now;
        }
        // 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.
        else if (readiness_value == HTML::DocumentReadyState::Interactive && m_load_timing_info.dom_interactive_time == 0) {
            m_load_timing_info.dom_interactive_time = now;
        }
    }

    // 4. Fire an event named readystatechange at document.
    dispatch_event(Event::create(realm(), HTML::EventNames::readystatechange));

    if (readiness_value == HTML::DocumentReadyState::Complete) {
        auto navigable = this->navigable();
        if (navigable && navigable->is_traversable()) {
            if (!is_decoded_svg()) {
                HTML::HTMLLinkElement::load_fallback_favicon_if_needed(*this);
            }
            navigable->traversable_navigable()->page().client().page_did_finish_loading(url());
        } else {
            m_needs_to_call_page_did_load = true;
        }
    }

    notify_each_document_observer([&](auto const& document_observer) {
        return document_observer.document_readiness_observer();
    },
        m_readiness);
}

// https://html.spec.whatwg.org/multipage/dom.html#dom-document-lastmodified
String Document::last_modified() const
{
    // The lastModified attribute, on getting, must return the date and time of the Document's source file's
    // last modification, in the user's local time zone, in the following format:

    // 1. The month component of the date.
    // 2. A U+002F SOLIDUS character (/).
    // 3. The day component of the date.
    // 4. A U+002F SOLIDUS character (/).
    // 5. The year component of the date.
    // 6. A U+0020 SPACE character.
    // 7. The hours component of the time.
    // 8. A U+003A COLON character (:).
    // 9. The minutes component of the time.
    // 10. A U+003A COLON character (:).
    // 11. The seconds component of the time.

    // The Document's source file's last modification date and time must be derived from relevant features
    // of the networking protocols used, e.g. from the value of the HTTP `Last-Modified` header of the document,
    // or from metadata in the file system for local files. If the last modification date and time are not known,
    // the attribute must return the current date and time in the above format.
    constexpr auto format_string = "%m/%d/%Y %H:%M:%S"sv;

    if (m_last_modified.has_value())
        return MUST(m_last_modified.value().to_string(format_string));

    return MUST(AK::UnixDateTime::now().to_string(format_string));
}

Page& Document::page()
{
    return m_page;
}

Page const& Document::page() const
{
    return m_page;
}

EventTarget* Document::get_parent(Event const& event)
{
    if (event.type() == HTML::EventNames::load)
        return nullptr;

    return m_window;
}

// https://html.spec.whatwg.org/multipage/document-lifecycle.html#completely-loaded
bool Document::is_completely_loaded() const
{
    return m_completely_loaded_time.has_value();
}

// https://html.spec.whatwg.org/multipage/browsing-the-web.html#completely-finish-loading
void Document::completely_finish_loading()
{
    // 2. Set document's completely loaded time to the current time.
    // AD-HOC: Set this unconditionally, even if the document has no navigable yet.
    //         In the async state machine, documents created during populate may complete
    //         loading before they are activated. The timestamp must still be recorded so
    //         that activate_history_entry can detect this and re-trigger the container
    //         notification steps.
    if (!m_completely_loaded_time.has_value())
        m_completely_loaded_time = AK::UnixDateTime::now();

    auto navigable = this->navigable();
    if (!navigable) {
        m_completely_loaded_deferred = true;
        return;
    }
    m_completely_loaded_deferred = false;

    ScopeGuard notify_observers = [this] {
        notify_each_document_observer([&](auto const& document_observer) {
            return document_observer.document_completely_loaded();
        });
    };

    // 1. Assert: document's browsing context is non-null.
    VERIFY(browsing_context());

    // NOTE: See the end of shared_declarative_refresh_steps.
    if (m_active_refresh_timer)
        m_active_refresh_timer->start();

    // 3. Let container be document's browsing context's container.
    if (!navigable->container())
        return;

    auto container = GC::make_root(navigable->container());

    // 4. 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<HTML::HTMLIFrameElement>(*container)) {
        container->queue_an_element_task(HTML::Task::Source::DOMManipulation, [container] {
            run_iframe_load_event_steps(static_cast<HTML::HTMLIFrameElement&>(*container));
        });
    }
    // 5. 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] {
            container->dispatch_event(DOM::Event::create(container->realm(), HTML::EventNames::load));
        });
    }

    // AD-HOC: Script-created parsers (iframe document.open/write/close) don't reach set_ready_for_post_load_tasks, so
    //         the parent's load-event-delay phase wouldn't otherwise be re-evaluated when this iframe finishes loading.
    container->document().schedule_html_parser_end_check();
}

// https://html.spec.whatwg.org/multipage/dom.html#dom-document-cookie
WebIDL::ExceptionOr<String> Document::cookie()
{
    // On getting, if the document is a cookie-averse Document object, then the user agent must return the empty string.
    if (is_cookie_averse())
        return String {};

    // Otherwise, if the Document's origin is an opaque origin, the user agent must throw a "SecurityError" DOMException.
    if (origin().is_opaque())
        return WebIDL::SecurityError::create(realm(), "Document origin is opaque"_utf16);

    // Otherwise, the user agent must return the cookie-string for the document's URL for a "non-HTTP" API, decoded using
    // UTF-8 decode without BOM.
    if (m_cookie_version_index.has_value()) {
        if (m_cookie_version == page().client().page_did_request_document_cookie_version(*m_cookie_version_index))
            return m_cookie;
    }

    auto [cookie_version, cookie] = page().client().page_did_request_cookie(m_url, HTTP::Cookie::Source::NonHttp);

    if (cookie_version.has_value()) {
        m_cookie_version = *cookie_version;
        m_cookie = cookie;
    }

    return cookie;
}

// https://html.spec.whatwg.org/multipage/dom.html#dom-document-cookie
WebIDL::ExceptionOr<void> Document::set_cookie(StringView cookie_string)
{
    // On setting, if the document is a cookie-averse Document object, then the user agent must do nothing.
    if (is_cookie_averse())
        return {};

    // Otherwise, if the Document's origin is an opaque origin, the user agent must throw a "SecurityError" DOMException.
    if (origin().is_opaque())
        return WebIDL::SecurityError::create(realm(), "Document origin is opaque"_utf16);

    // Otherwise, the user agent must act as it would when receiving a set-cookie-string for the document's URL via a
    // "non-HTTP" API, consisting of the new value encoded as UTF-8.
    if (auto cookie = HTTP::Cookie::parse_cookie(url(), cookie_string); cookie.has_value())
        page().client().page_did_set_cookie(m_url, cookie.value(), HTTP::Cookie::Source::NonHttp);

    return {};
}

// https://html.spec.whatwg.org/multipage/dom.html#cookie-averse-document-object
bool Document::is_cookie_averse() const
{
    // A Document object that falls into one of the following conditions is a cookie-averse Document object:

    // * A Document object whose browsing context is null.
    if (!browsing_context())
        return true;

    // * A Document whose URL's scheme is not an HTTP(S) scheme.
    if (!url().scheme().is_one_of("http"sv, "https"sv))
        return true;

    return false;
}

String Document::fg_color() const
{
    if (auto* body_element = body(); body_element && !is<HTML::HTMLFrameSetElement>(*body_element))
        return body_element->get_attribute_value(HTML::AttributeNames::text);
    return ""_string;
}

void Document::set_fg_color(String const& value)
{
    if (auto* body_element = body(); body_element && !is<HTML::HTMLFrameSetElement>(*body_element))
        body_element->set_attribute_value(HTML::AttributeNames::text, value);
}

String Document::link_color() const
{
    if (auto* body_element = body(); body_element && !is<HTML::HTMLFrameSetElement>(*body_element))
        return body_element->get_attribute_value(HTML::AttributeNames::link);
    return ""_string;
}

void Document::set_link_color(String const& value)
{
    if (auto* body_element = body(); body_element && !is<HTML::HTMLFrameSetElement>(*body_element))
        body_element->set_attribute_value(HTML::AttributeNames::link, value);
}

String Document::vlink_color() const
{
    if (auto* body_element = body(); body_element && !is<HTML::HTMLFrameSetElement>(*body_element))
        return body_element->get_attribute_value(HTML::AttributeNames::vlink);
    return ""_string;
}

void Document::set_vlink_color(String const& value)
{
    if (auto* body_element = body(); body_element && !is<HTML::HTMLFrameSetElement>(*body_element))
        body_element->set_attribute_value(HTML::AttributeNames::vlink, value);
}

String Document::alink_color() const
{
    if (auto* body_element = body(); body_element && !is<HTML::HTMLFrameSetElement>(*body_element))
        return body_element->get_attribute_value(HTML::AttributeNames::alink);
    return ""_string;
}

void Document::set_alink_color(String const& value)
{
    if (auto* body_element = body(); body_element && !is<HTML::HTMLFrameSetElement>(*body_element))
        body_element->set_attribute_value(HTML::AttributeNames::alink, value);
}

String Document::bg_color() const
{
    if (auto* body_element = body(); body_element && !is<HTML::HTMLFrameSetElement>(*body_element))
        return body_element->get_attribute_value(HTML::AttributeNames::bgcolor);
    return ""_string;
}

void Document::set_bg_color(String const& value)
{
    if (auto* body_element = body(); body_element && !is<HTML::HTMLFrameSetElement>(*body_element))
        body_element->set_attribute_value(HTML::AttributeNames::bgcolor, value);
}

String Document::dump_dom_tree_as_json() const
{
    const_cast<Document&>(*this).update_layout(UpdateLayoutReason::InspectDOMTree);

    StringBuilder builder;
    auto json = MUST(JsonObjectSerializer<>::try_create(builder));
    serialize_tree_as_json(json);

    MUST(json.finish());
    return MUST(builder.to_string());
}

// https://html.spec.whatwg.org/multipage/semantics.html#has-no-style-sheet-that-is-blocking-scripts
bool Document::has_no_style_sheet_that_is_blocking_scripts() const
{
    // A Document has no style sheet that is blocking scripts if it does not have a style sheet that is blocking scripts.
    return !has_a_style_sheet_that_is_blocking_scripts();
}

// 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
{
    // 1. If document's script-blocking style sheet set is not empty, then return true.
    if (!m_script_blocking_style_sheet_set.is_empty())
        return true;

    // 2. If document's node navigable is null, then return false.
    auto navigable = this->navigable();
    if (!navigable)
        return false;

    // 3. Let containerDocument be document's node navigable's container document.
    auto container_document = navigable->container_document();

    // 4. If containerDocument is non-null and containerDocument's script-blocking style sheet set is not empty, then return true.
    if (container_document && !container_document->m_script_blocking_style_sheet_set.is_empty())
        return true;

    // 5. Return false
    return false;
}

String Document::referrer() const
{
    return m_referrer;
}

void Document::set_referrer(String referrer)
{
    m_referrer = move(referrer);
}

// https://html.spec.whatwg.org/multipage/document-sequences.html#fully-active
bool Document::is_fully_active() const
{
    // A Document d is said to be fully active when d is the active document of a navigable navigable, and either
    // navigable is a top-level traversable or navigable's container document is fully active.
    auto navigable = this->navigable();
    if (!navigable)
        return false;

    auto traversable = navigable->traversable_navigable();
    if (navigable == traversable && traversable->is_top_level_traversable())
        return true;

    auto container_document = navigable->container_document();
    if (container_document && container_document != this && container_document->is_fully_active())
        return true;

    return false;
}

bool Document::is_active() const
{
    auto navigable = this->navigable();
    return navigable && navigable->active_document() == this;
}

// https://html.spec.whatwg.org/multipage/history.html#dom-document-location
GC::Ptr<HTML::Location> 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()->location();
}

// https://html.spec.whatwg.org/multipage/interaction.html#dom-document-hidden
bool Document::hidden() const
{
    return m_visibility_state == HTML::VisibilityState::Hidden;
}

// https://html.spec.whatwg.org/multipage/interaction.html#dom-document-visibilitystate
StringView Document::visibility_state() const
{
    switch (m_visibility_state) {
    case HTML::VisibilityState::Hidden:
        return "hidden"sv;
    case HTML::VisibilityState::Visible:
        return "visible"sv;
    }
    VERIFY_NOT_REACHED();
}

// https://html.spec.whatwg.org/multipage/interaction.html#update-the-visibility-state
void Document::update_the_visibility_state(HTML::VisibilityState visibility_state)
{
    // 1. If document's visibility state equals visibilityState, then return.
    if (m_visibility_state == visibility_state)
        return;

    // 2. Set document's visibility state to visibilityState.
    m_visibility_state = visibility_state;

    // FIXME: 3. Queue a new VisibilityStateEntry whose visibility state is visibilityState and whose timestamp is the current
    //    high resolution time given document's relevant global object.

    // FIXME: 4. Run the screen orientation change steps with document.

    // 5. Run the view transition page visibility change steps with document.
    view_transition_page_visibility_change_steps();

    // 6. Run any page visibility change steps which may be defined in other specifications, with visibility state and
    //    document.
    notify_each_document_observer([&](auto const& document_observer) {
        return document_observer.document_visibility_state_observer();
    },
        m_visibility_state);

    // 7. Fire an event named visibilitychange at document, with its bubbles attribute initialized to true.
    auto event = DOM::Event::create(realm(), HTML::EventNames::visibilitychange);
    event->set_bubbles(true);
    dispatch_event(event);
}

// https://drafts.csswg.org/cssom-view/#document-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.
    // 2. If the VisualViewport associated with doc has had its scale, width, or height properties changed
    //    since the last time these steps were run, fire an event named resize at the VisualViewport.

    auto viewport_size = viewport_rect().size().to_type<int>();
    auto& visual_viewport = *this->visual_viewport();
    VisualViewportState visual_viewport_state = { visual_viewport.scale(), { visual_viewport.width(), visual_viewport.height() } };
    bool is_initial_size = !m_last_viewport_size.has_value();

    bool viewport_size_changed = m_last_viewport_size != viewport_size;
    bool visual_viewport_state_changed = m_last_visual_viewport_state != visual_viewport_state;

    if (!viewport_size_changed && !visual_viewport_state_changed)
        return;

    m_last_viewport_size = viewport_size;
    m_last_visual_viewport_state = visual_viewport_state;

    if (!is_initial_size) {
        if (viewport_size_changed) {
            auto window_resize_event = DOM::Event::create(realm(), UIEvents::EventNames::resize);
            window_resize_event->set_is_trusted(true);
            window()->dispatch_event(window_resize_event);
        }

        if (visual_viewport_state_changed) {
            auto visual_viewport_resize_event = DOM::Event::create(realm(), UIEvents::EventNames::resize);
            visual_viewport_resize_event->set_is_trusted(true);
            visual_viewport.dispatch_event(visual_viewport_resize_event);
        }
    }
}

// https://drafts.csswg.org/cssom-view-1/#document-run-the-scroll-steps
void Document::run_the_scroll_steps()
{
    // AD-HOC: Process auto-scroll ticks before dispatching scroll events. This is tied to the rendering update to
    //         ensure exactly one auto-scroll tick per frame.
    if (auto navigable = this->navigable())
        navigable->event_handler().process_auto_scroll();

    // FIXME: 1. For each scrolling box box that was scrolled:
    //        ...

    // 2. For each item (target, type) in doc’s pending scroll events, in the order they were added to the list, run
    //    these substeps:
    auto pending_scroll_events = move(m_pending_scroll_events);
    for (auto const& [target, type] : pending_scroll_events) {
        // 1. If target is a Document, and type is "scroll" or "scrollend", fire an event named type that bubbles at target.
        if (is<Document>(*target) && (type == HTML::EventNames::scroll || type == HTML::EventNames::scrollend)) {
            auto event = DOM::Event::create(realm(), type);
            event->set_bubbles(true);
            target->dispatch_event(event);
        }
        // FIXME: 2. Otherwise, if type is "scrollsnapchange", then:
        // FIXME: 3. Otherwise, if type is "scrollsnapchanging", then:
        // 4. Otherwise, fire an event named type at target.
        else {
            auto event = DOM::Event::create(realm(), type);
            target->dispatch_event(event);
        }
    }

    // 3. Empty doc’s pending scroll events.
    // AD-HOC: We already emptied the scroll events by moving in step 2. This prevents us from removing new scroll
    //         events that were added while dispatching the old scroll events.
}

void Document::add_media_query_list(GC::Ref<CSS::MediaQueryList> media_query_list)
{
    m_needs_media_query_evaluation = true;
    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()
{
    if (!m_needs_media_query_evaluation)
        return;
    m_needs_media_query_evaluation = false;

    // 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;
    });

    // 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. 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)
            continue;
        GC::Ptr<CSS::MediaQueryList> media_query_list = media_query_list_ptr.ptr();
        bool did_match = media_query_list->matches();
        bool now_matches = media_query_list->evaluate();

        auto did_change_internally = media_query_list->has_changed_state();
        media_query_list->set_has_changed_state(false);

        if (did_change_internally == true || did_match != now_matches) {
            Bindings::MediaQueryListEventInit init;
            init.media = media_query_list->media();
            init.matches = now_matches;
            auto event = CSS::MediaQueryListEvent::create(realm(), 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()
{
    CSS::Invalidation::evaluate_media_rules_and_invalidate_style(*this);
}

DOMImplementation* Document::implementation()
{
    if (!m_implementation)
        m_implementation = DOMImplementation::create(*this);
    return m_implementation;
}

// https://html.spec.whatwg.org/multipage/interaction.html#dom-document-hasfocus
bool Document::has_focus_for_bindings() const
{
    // The Document hasFocus() method steps are to return the result of running the has focus steps given this.
    return has_focus();
}

// https://html.spec.whatwg.org/multipage/interaction.html#has-focus-steps
bool Document::has_focus() const
{
    // 1. If target's node navigable's top-level traversable does not have system focus, then return false.
    auto navigable = this->navigable();
    if (!navigable)
        return false;

    auto traversable = navigable->traversable_navigable();
    if (!traversable || !traversable->is_focused())
        return false;

    // 2. Let candidate be target's node navigable's top-level traversable's active document.
    auto candidate = traversable->active_document();

    // 3. While true:
    while (candidate) {
        // 3.1. If candidate is target, then return true.
        if (candidate == this)
            return true;

        // 3.2. If the focused area of candidate is a navigable container with a non-null content navigable,
        //      then set candidate to the active document of that navigable container's content navigable.
        auto focused_area = candidate->focused_area();
        if (auto* navigable_container = as_if<HTML::NavigableContainer>(focused_area.ptr())) {
            if (auto content_navigable = navigable_container->content_navigable()) {
                candidate = content_navigable->active_document();
                continue;
            }
        }

        // 3.3. Otherwise, return false.
        return false;
    }

    return false;
}

// https://html.spec.whatwg.org/multipage/interaction.html#allow-focus-steps
bool Document::allow_focus() const
{
    // The allow focus steps, given a Document object target, are as follows:

    // 1. If target is allowed to use the "focus-without-user-activation" feature, then return true.
    if (is_allowed_to_use_feature(PolicyControlledFeature::FocusWithoutUserActivation))
        return true;

    // 2. If target's relevant global object has transient activation, then return true.
    if (as<HTML::Window>(HTML::relevant_global_object(*this)).has_transient_activation())
        return true;

    // 3. Return false.
    return false;
}

void Document::set_parser(Badge<HTML::HTMLParser>, HTML::HTMLParser& parser)
{
    m_parser = parser;
}

void Document::detach_parser()
{
    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);
}

// https://www.w3.org/TR/xml/#NT-Name
bool Document::is_valid_name(String const& name)
{
    if (name.is_empty())
        return false;
    auto code_points = name.code_points();
    auto it = code_points.begin();

    if (!is_valid_name_start_character(*it))
        return false;
    ++it;

    for (; it != code_points.end(); ++it) {
        if (!is_valid_name_character(*it))
            return false;
    }

    return true;
}

// https://dom.spec.whatwg.org/#dom-document-createnodeiterator
GC::Ref<NodeIterator> Document::create_node_iterator(Node& root, unsigned what_to_show, GC::Ptr<NodeFilter> filter)
{
    return NodeIterator::create(realm(), root, what_to_show, filter);
}

// https://dom.spec.whatwg.org/#dom-document-createtreewalker
GC::Ref<TreeWalker> Document::create_tree_walker(Node& root, unsigned what_to_show, GC::Ptr<NodeFilter> filter)
{
    return TreeWalker::create(realm(), root, what_to_show, filter);
}

void Document::register_node_iterator(Badge<NodeIterator>, NodeIterator& node_iterator)
{
    auto result = m_node_iterators.set(&node_iterator);
    VERIFY(result == AK::HashSetResult::InsertedNewEntry);
}

void Document::unregister_node_iterator(Badge<NodeIterator>, NodeIterator& node_iterator)
{
    bool was_removed = m_node_iterators.remove(&node_iterator);
    VERIFY(was_removed);
}

void Document::register_document_observer(Badge<DocumentObserver>, DocumentObserver& document_observer)
{
    auto result = m_document_observers.set(document_observer);
    VERIFY(result == AK::HashSetResult::InsertedNewEntry);
}

void Document::unregister_document_observer(Badge<DocumentObserver>, DocumentObserver& document_observer)
{
    bool was_removed = m_document_observers.remove(document_observer);
    VERIFY(was_removed);
}

void Document::increment_number_of_things_delaying_the_load_event(Badge<DocumentLoadEventDelayer>)
{
    ++m_number_of_things_delaying_the_load_event;

    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<DocumentLoadEventDelayer>)
{
    VERIFY(m_number_of_things_delaying_the_load_event);
    --m_number_of_things_delaying_the_load_event;

    page().client().page_did_update_resource_count(m_number_of_things_delaying_the_load_event);

    schedule_html_parser_end_check();
}

void Document::set_html_parser_end_state(GC::Ptr<HTML::HTMLParserEndState> state)
{
    m_html_parser_end_state = state;
}

void Document::schedule_html_parser_end_check()
{
    if (m_html_parser_end_state)
        m_html_parser_end_state->schedule_progress_check();
    if (m_parser)
        m_parser->schedule_resume_check();
}

void Document::set_ready_for_post_load_tasks(bool ready)
{
    m_ready_for_post_load_tasks = ready;
    if (ready) {
        if (auto navigable = this->navigable()) {
            // AD-HOC: Clear the navigation load event guard now that the document is ready.
            //         This guard was set in finalize_a_cross_document_navigation to prevent the parent's
            //         load event from firing while the about:blank was still the active document.
            navigable->clear_navigation_load_event_guard();

            if (auto container = navigable->container()) {
                container->document().schedule_html_parser_end_check();
            }
        }
    }
}

bool Document::anything_is_delaying_the_load_event() const
{
    if (m_number_of_things_delaying_the_load_event > 0)
        return true;

    for (auto& navigable : descendant_navigables()) {
        if (navigable->container()->currently_delays_the_load_event())
            return true;
    }

    // FIXME: Track down anything else that is supposed to delay the load event.

    return false;
}

void Document::set_page_showing(bool page_showing)
{
    if (m_page_showing == page_showing)
        return;

    m_page_showing = page_showing;

    notify_each_document_observer([&](auto const& document_observer) {
        return document_observer.document_page_showing_observer();
    },
        m_page_showing);
}

void Document::invalidate_stacking_context_tree()
{
    // NB: Called during stacking context invalidation.
    if (auto paintable_box = this->unsafe_paintable_box())
        paintable_box->invalidate_stacking_context();
}

void Document::check_favicon_after_loading_link_resource()
{
    // https://html.spec.whatwg.org/multipage/links.html#rel-icon
    // NOTE: firefox also load favicons outside the head tag, which is against spec (see table 4.6.7)
    auto* head_element = head();
    if (!head_element)
        return;

    auto favicon_link_elements = HTMLCollection::create(*head_element, HTMLCollection::Scope::Descendants, [](Element const& element) {
        if (!is<HTML::HTMLLinkElement>(element))
            return false;

        return static_cast<HTML::HTMLLinkElement const&>(element).has_loaded_icon();
    });

    if (favicon_link_elements->length() == 0) {
        dbgln_if(SPAM_DEBUG, "No favicon found to be used");
        return;
    }

    // 4.6.7.8 Link type "icon"
    //
    // If there are multiple equally appropriate icons, user agents must use the last one declared
    // in tree order at the time that the user agent collected the list of icons.
    //
    // If multiple icons are provided, the user agent must select the most appropriate icon
    // according to the type, media, and sizes attributes.
    //
    // FIXME: There is no selective behavior yet for favicons.
    for (auto i = favicon_link_elements->length(); i-- > 0;) {
        auto favicon_element = favicon_link_elements->item(i);

        if (favicon_element == m_active_element.ptr())
            return;

        // If the user agent tries to use an icon but that icon is determined, upon closer examination,
        // to in fact be inappropriate (...), then the user agent must try the next-most-appropriate icon
        // as determined by the attributes.
        if (static_cast<HTML::HTMLLinkElement*>(favicon_element)->load_favicon_and_use_if_window_is_active()) {
            m_active_favicon = favicon_element;
            return;
        }
    }

    dbgln_if(SPAM_DEBUG, "No favicon found to be used");
}

void Document::set_window(HTML::Window& window)
{
    m_window = &window;
}

CSS::StyleSheetList& Document::style_sheets()
{
    if (!m_style_sheets)
        m_style_sheets = CSS::StyleSheetList::create(*this);
    return *m_style_sheets;
}

CSS::StyleSheetList const& Document::style_sheets() const
{
    return const_cast<Document*>(this)->style_sheets();
}

GC::Ref<HTML::History> Document::history()
{
    if (!m_history)
        m_history = HTML::History::create(realm());
    return *m_history;
}

GC::Ref<HTML::History> Document::history() const
{
    return const_cast<Document*>(this)->history();
}

// https://html.spec.whatwg.org/multipage/origin.html#dom-document-domain
String Document::domain() const
{
    // 1. Let effectiveDomain be this's origin's effective domain.
    auto effective_domain = origin().effective_domain();

    // 2. If effectiveDomain is null, then return the empty string.
    if (!effective_domain.has_value())
        return String {};

    // 3. Return effectiveDomain, serialized.
    return effective_domain->serialize();
}

// https://html.spec.whatwg.org/multipage/browsers.html#is-a-registrable-domain-suffix-of-or-is-equal-to
bool is_a_registrable_domain_suffix_of_or_is_equal_to(StringView host_suffix_string, URL::Host const& original_host)
{
    // 1. If hostSuffixString is the empty string, then return false.
    if (host_suffix_string.is_empty())
        return false;

    // 2. Let hostSuffix be the result of parsing hostSuffixString.
    auto host_suffix = URL::Parser::parse_host(host_suffix_string);

    // 3. If hostSuffix is failure, then return false.
    if (!host_suffix.has_value())
        return false;

    // 4. If hostSuffix does not equal originalHost, then:
    if (host_suffix.value() != original_host) {
        // 1. If hostSuffix or originalHost is not a domain, then return false.
        // NOTE: This excludes hosts that are IP addresses.
        if (!host_suffix->has<String>() || !original_host.has<String>())
            return false;
        auto const& host_suffix_string = host_suffix->get<String>();
        auto const& original_host_string = original_host.get<String>();

        // 2. If hostSuffix, prefixed by U+002E (.), does not match the end of originalHost, then return false.
        auto prefixed_host_suffix = MUST(String::formatted(".{}", host_suffix_string));
        if (!original_host_string.ends_with_bytes(prefixed_host_suffix))
            return false;

        // 3. If any of the following are true:
        //     * hostSuffix equals hostSuffix's public suffix; or
        //     * hostSuffix, prefixed by U+002E (.), matches the end of originalHost's public suffix,
        //    then return false. [URL]
        if (host_suffix_string == host_suffix->public_suffix())
            return false;

        auto original_host_public_suffix = original_host.public_suffix();
        VERIFY(original_host_public_suffix.has_value());

        if (original_host_public_suffix->ends_with_bytes(prefixed_host_suffix))
            return false;

        // 4. Assert: originalHost's public suffix, prefixed by U+002E (.), matches the end of hostSuffix.
        VERIFY(host_suffix_string.ends_with_bytes(MUST(String::formatted(".{}", *original_host_public_suffix))));
    }

    // 5. Return true.
    return true;
}

// https://html.spec.whatwg.org/multipage/browsers.html#dom-document-domain
WebIDL::ExceptionOr<void> Document::set_domain(String const& domain)
{
    auto& realm = this->realm();

    // 1. If this's browsing context is null, then throw a "SecurityError" DOMException.
    if (!m_browsing_context)
        return WebIDL::SecurityError::create(realm, "Document.domain setter requires a browsing context"_utf16);

    // 2. If this's active sandboxing flag set has its sandboxed document.domain browsing context flag set, then throw a "SecurityError" DOMException.
    if (has_flag(active_sandboxing_flag_set(), HTML::SandboxingFlagSet::SandboxedDocumentDomain))
        return WebIDL::SecurityError::create(realm, "Document.domain setter is sandboxed"_utf16);

    // 3. Let effectiveDomain be this's origin's effective domain.
    auto effective_domain = origin().effective_domain();

    // 4. If effectiveDomain is null, then throw a "SecurityError" DOMException.
    if (!effective_domain.has_value())
        return WebIDL::SecurityError::create(realm, "Document.domain setter called on a Document with a null effective domain"_utf16);

    // 5. If the given value is not a registrable domain suffix of and is not equal to effectiveDomain, then throw a "SecurityError" DOMException.
    if (!is_a_registrable_domain_suffix_of_or_is_equal_to(domain, effective_domain.value()))
        return WebIDL::SecurityError::create(realm, "Document.domain setter called for an invalid domain"_utf16);

    // FIXME: 6. If the surrounding agent's agent cluster's is origin-keyed is true, then return.

    // FIXME: 7. Set this's origin's domain to the result of parsing the given value.

    dbgln("(STUBBED) Document::set_domain(domain='{}')", domain);
    return {};
}

void Document::set_navigation_id(Optional<String> navigation_id)
{
    m_navigation_id = move(navigation_id);
}

Optional<String> Document::navigation_id() const
{
    return m_navigation_id;
}

HTML::SandboxingFlagSet Document::active_sandboxing_flag_set() const
{
    return m_active_sandboxing_flag_set;
}

void Document::set_active_sandboxing_flag_set(HTML::SandboxingFlagSet sandboxing_flag_set)
{
    m_active_sandboxing_flag_set = sandboxing_flag_set;
}

GC::Ref<HTML::PolicyContainer> Document::policy_container() const
{
    auto& heap = this->heap();
    if (!m_policy_container) {
        m_policy_container = heap.allocate<HTML::PolicyContainer>(heap);
    }
    return *m_policy_container;
}

void Document::set_policy_container(GC::Ref<HTML::PolicyContainer> policy_container)
{
    m_policy_container = policy_container;
}

// https://html.spec.whatwg.org/multipage/browsing-the-web.html#snapshotting-source-snapshot-params
GC::Ref<HTML::SourceSnapshotParams> Document::snapshot_source_snapshot_params() const
{
    // To snapshot source snapshot params given a Document sourceDocument, return a new source snapshot params with
    return heap().allocate<HTML::SourceSnapshotParams>(
        // has transient activation
        //    true if sourceDocument's relevant global object has transient activation; otherwise false
        as<HTML::Window>(HTML::relevant_global_object(*this)).has_transient_activation(),

        // sandboxing flags
        //     sourceDocument's active sandboxing flag set
        m_active_sandboxing_flag_set,

        // allows downloading
        //     false if sourceDocument's active sandboxing flag set has the sandboxed downloads browsing context flag set; otherwise true
        !has_flag(m_active_sandboxing_flag_set, HTML::SandboxingFlagSet::SandboxedDownloads),

        // fetch client
        //     sourceDocument's relevant settings object
        relevant_settings_object(),

        // source policy container
        //     a clone of sourceDocument's policy container
        policy_container()->clone(heap()));
}

// https://html.spec.whatwg.org/multipage/document-sequences.html#descendant-navigables
Vector<GC::Root<HTML::Navigable>> Document::descendant_navigables()
{
    // 1. Let navigables be new list.
    Vector<GC::Root<HTML::Navigable>> navigables;

    // 2. Let navigableContainers be a list of all shadow-including descendants of document that are navigable containers, in shadow-including tree order.
    // 3. For each navigableContainer of navigableContainers:
    for_each_shadow_including_descendant([&](DOM::Node& node) {
        if (is<HTML::NavigableContainer>(node)) {
            auto& navigable_container = static_cast<HTML::NavigableContainer&>(node);
            // 1. If navigableContainer's content navigable is null, then continue.
            if (!navigable_container.content_navigable())
                return TraversalDecision::Continue;

            // 2. Extend navigables with navigableContainer's content navigable's active document's inclusive descendant navigables.
            auto document = navigable_container.content_navigable()->active_document();
            // AD-HOC: If the descendant navigable doesn't have an active document, just skip over it.
            if (!document)
                return TraversalDecision::Continue;
            navigables.extend(document->inclusive_descendant_navigables());
        }
        return TraversalDecision::Continue;
    });

    // 4. Return navigables.
    return navigables;
}

Vector<GC::Root<HTML::Navigable>> const Document::descendant_navigables() const
{
    return const_cast<Document&>(*this).descendant_navigables();
}

// https://html.spec.whatwg.org/multipage/document-sequences.html#inclusive-descendant-navigables
Vector<GC::Root<HTML::Navigable>> Document::inclusive_descendant_navigables()
{
    // FIXME: The document's node navigable should not be null here. But we currently do not implement the "unload a
    //        document and its descendants" steps correctly, and the navigable becomes null during unloading. We are
    //        essentially destroying the document too early. See Document::unload_a_document_and_its_descendants. See:
    //        https://github.com/LadybirdBrowser/ladybird/issues/7825
    auto document_node_navigable = navigable();
    if (!document_node_navigable)
        return {};

    // 1. Let navigables be « document's node navigable ».
    Vector<GC::Root<HTML::Navigable>> navigables;
    navigables.append(*document_node_navigable);

    // 2. Extend navigables with document's descendant navigables.
    navigables.extend(descendant_navigables());

    // 3. Return navigables.
    return navigables;
}

// https://html.spec.whatwg.org/multipage/document-sequences.html#ancestor-navigables
Vector<GC::Root<HTML::Navigable>> Document::ancestor_navigables()
{
    // FIXME: The document's node navigable should not be null here. But we currently do not implement the "unload a
    //        document and its descendants" steps correctly, and the navigable becomes null during unloading. We are
    //        essentially destroying the document too early. See Document::unload_a_document_and_its_descendants. See:
    //        https://github.com/LadybirdBrowser/ladybird/issues/7825
    auto document_node_navigable = navigable();
    if (!document_node_navigable)
        return {};

    // 1. Let navigable be document's node navigable's parent.
    auto navigable = document_node_navigable->parent();

    // 2. Let ancestors be an empty list.
    Vector<GC::Root<HTML::Navigable>> ancestors;

    // 3. While navigable is not null:
    while (navigable) {
        // 1. Prepend navigable to ancestors.
        ancestors.prepend(*navigable);

        // 2. Set navigable to navigable's parent.
        navigable = navigable->parent();
    }

    // 4. Return ancestors.
    return ancestors;
}

Vector<GC::Root<HTML::Navigable>> const Document::ancestor_navigables() const
{
    return const_cast<Document&>(*this).ancestor_navigables();
}

// https://html.spec.whatwg.org/multipage/document-sequences.html#inclusive-ancestor-navigables
Vector<GC::Root<HTML::Navigable>> Document::inclusive_ancestor_navigables()
{
    // FIXME: The document's node navigable should not be null here. But we currently do not implement the "unload a
    //        document and its descendants" steps correctly, and the navigable becomes null during unloading. We are
    //        essentially destroying the document too early. See Document::unload_a_document_and_its_descendants. See:
    //        https://github.com/LadybirdBrowser/ladybird/issues/7825
    auto document_node_navigable = navigable();
    if (!document_node_navigable)
        return {};

    // 1. Let navigables be document's ancestor navigables.
    auto navigables = ancestor_navigables();

    // 2. Append document's node navigable to navigables.
    navigables.append(*document_node_navigable);

    // 3. Return navigables.
    return navigables;
}

// https://html.spec.whatwg.org/multipage/document-sequences.html#document-tree-child-navigables
Vector<GC::Root<HTML::Navigable>> Document::document_tree_child_navigables()
{
    // 1. If document's node navigable is null, then return the empty list.
    if (!navigable())
        return {};

    // 2. Let navigables be new list.
    Vector<GC::Root<HTML::Navigable>> navigables;

    // 3. Let navigableContainers be a list of all descendants of document that are navigable containers, in tree order.
    // 4. For each navigableContainer of navigableContainers:
    //     1. If navigableContainer's content navigable is null, then continue.
    //     2. Append navigableContainer's content navigable to navigables.
    // OPTIMIZATION: Iterate all registered navigables to avoid a full tree traversal.
    for (auto const& navigable : HTML::all_navigables()) {
        auto container = navigable->container();
        if (!container || !is_ancestor_of(*container))
            continue;
        navigables.insert_before_matching(*navigable, [&](auto const& existing_navigable) {
            return container->is_before(*existing_navigable->container());
        });
    }

    // 5. Return navigables.
    return navigables;
}

// https://html.spec.whatwg.org/multipage/document-lifecycle.html#unloading-document-cleanup-steps
void Document::run_unloading_cleanup_steps()
{
    // 1. Let window be document's relevant global object.
    auto& window = as<HTML::WindowOrWorkerGlobalScopeMixin>(HTML::relevant_global_object(*this));

    // 2. For each WebSocket object webSocket whose relevant global object is window, make disappear webSocket.
    //    If this affected any WebSocket objects, then make document unsalvageable given document and "websocket".
    auto affected_any_web_sockets = window.make_disappear_all_web_sockets();
    if (affected_any_web_sockets == HTML::WindowOrWorkerGlobalScopeMixin::AffectedAnyWebSockets::Yes)
        make_unsalvageable("websocket"_string);

    // FIXME: 3. For each WebTransport object transport whose relevant global object is window, run the context cleanup steps given transport.

    // 4. If document's salvageable state is false, then:
    if (!m_salvageable) {
        // 1. For each EventSource object eventSource whose relevant global object is equal to window, forcibly close eventSource.
        window.forcibly_close_all_event_sources();

        // 2. Clear window's map of active timers.
        window.clear_map_of_active_timers();
    }

    // https://w3c.github.io/IndexedDB/#database-connection
    // If the execution context where the connection was created is destroyed
    // (for example due to the user navigating away from that page), the connection is closed.
    // AD-HOC: We have no way to detect when the execution context that created the connection is destroyed, and
    //         making LibJS notify us of that would undoubtedly be very costly to performance. All other browsers also
    //         opt not to follow the spec exactly in regards to this, instead letting the connection stay open until
    //         GC collects it. However, we need to be proactive about this when navigating for the sake of test-web.
    window.close_all_idb_connections();

    FileAPI::run_unloading_cleanup_steps(*this);
    fully_exit_fullscreen();
}

// https://html.spec.whatwg.org/multipage/document-lifecycle.html#destroy-a-document
void Document::destroy()
{
    // FIXME: 1. Assert: this is running as part of a task queued on document's relevant agent's event loop.

    // 2. Abort document.
    abort();

    // AD-HOC: Notify document observers that this document became inactive.
    //         This allows observers (e.g. HTMLImageElement) to clear resources like
    //         DocumentLoadEventDelayers that would otherwise block the parent
    //         document's load event forever.
    //         Note: did_stop_being_active_document_in_navigable() handles the navigation case,
    //         but document destruction (e.g. iframe removal) takes a different path.
    //         The flag ensures we don't fire the callback twice for the same document
    //         (once from navigation, then again from destruction).
    if (!m_has_fired_document_became_inactive) {
        m_has_fired_document_became_inactive = true;
        notify_each_document_observer([&](auto const& document_observer) {
            return document_observer.document_became_inactive();
        });
    }

    // 3. Set document's salvageable state to false.
    m_salvageable = false;

    // 4. Let ports be the list of MessagePorts whose relevant global object's associated Document is document.
    // 5. For each port in ports, disentangle port.
    HTML::MessagePort::for_each_message_port([&](HTML::MessagePort& port) {
        auto& global = HTML::relevant_global_object(port);
        if (!is<HTML::Window>(global))
            return;

        auto& window = static_cast<HTML::Window&>(global);
        if (&window.associated_document() == this)
            port.disentangle();
    });

    // 6. Run any unloading document cleanup steps for document that are defined by this specification and other applicable specifications.
    run_unloading_cleanup_steps();

    // AD-HOC: Destruction does not go through did_stop_being_active_document_in_navigable(),
    //         but stale per-node and root layout/paintable pointers can still keep the old
    //         layout tree alive until GC runs.
    clear_layout_and_paintable_nodes_for_inactive_document();
    tear_down_layout_tree();

    // 7. Remove any tasks whose document is document from any task queue (without running those tasks).
    HTML::main_thread_event_loop().task_queue().remove_tasks_matching([this](auto& task) {
        return task.document() == this;
    });

    // AD-HOC: Mark this document as destroyed so we can remove tasks from the queue that will never be able to run.
    m_has_been_destroyed = true;

    // 8. Set document's browsing context to null.
    m_browsing_context = nullptr;

    // Not in the spec:
    for (auto& navigable_container : HTML::NavigableContainer::all_instances()) {
        if (&navigable_container->document() == this && navigable_container->content_navigable())
            navigable_container->content_navigable()->remove_from_all_navigables();
    }

    // 9. Set document's node navigable's active session history entry's document state's document to null.
    if (auto navigable = this->navigable()) {
        navigable->set_active_document(nullptr);

        // AD-HOC: We set the page's focused navigable during mouse-down events. If that navigable is this document's
        //         navigable, we must be sure to reset the page's focused navigable.
        page().navigable_document_destroyed({}, *navigable);
    }

    // FIXME: 10. Remove document from the owner set of each WorkerGlobalScope object whose set contains document.
    // FIXME: 11. For each workletGlobalScope in document's worklet global scopes, terminate workletGlobalScope.
}

// https://html.spec.whatwg.org/multipage/browsing-the-web.html#make-document-unsalvageable
void Document::make_unsalvageable([[maybe_unused]] String reason)
{
    // FIXME: 1. Let details be a new not restored reason details whose reason is reason.
    // FIXME: 2. Append details to document's bfcache blocking details.

    // 3. Set document's salvageable state to false.
    set_salvageable(false);
}

struct DocumentLifecycleState : public GC::Cell {
    GC_CELL(DocumentLifecycleState, GC::Cell);
    GC_DECLARE_ALLOCATOR(DocumentLifecycleState);

    static constexpr int TIMEOUT_MS = 15000;

    DocumentLifecycleState(GC::Ref<Document> document, size_t remaining, GC::Ref<GC::Function<void()>> finish_callback)
        : remaining_children(remaining)
        , document(document)
        , finish_callback(finish_callback)
        , timeout(Platform::Timer::create_single_shot(heap(), TIMEOUT_MS, GC::create_function(heap(), [this] {
            if (remaining_children > 0)
                dbgln("FIXME: Document unload/destruction timed out with {} remaining children", remaining_children);
        })))
    {
        timeout->start();
    }

    virtual void visit_edges(Visitor& visitor) override
    {
        Base::visit_edges(visitor);
        visitor.visit(document);
        visitor.visit(finish_callback);
        visitor.visit(timeout);
    }

    void did_process_child()
    {
        if (--remaining_children > 0)
            return;
        timeout->stop();
        queue_a_task(HTML::Task::Source::NavigationAndTraversal, nullptr, nullptr, finish_callback);
    }

    size_t remaining_children { 0 };
    GC::Ref<Document> document;
    GC::Ref<GC::Function<void()>> finish_callback;
    GC::Ref<Platform::Timer> timeout;
};

GC_DEFINE_ALLOCATOR(DocumentLifecycleState);

// https://html.spec.whatwg.org/multipage/document-lifecycle.html#destroy-a-document-and-its-descendants
void Document::destroy_a_document_and_its_descendants(GC::Ptr<GC::Function<void()>> after_all_destruction)
{
    // 1. If document is not fully active, then:
    if (!is_fully_active()) {
        // 1. Let reason be a string from user-agent specific blocking reasons. If none apply, then let reason be
        //    "masked".
        // FIXME: user-agent specific blocking reasons.
        auto reason = "masked"_string;

        // 2. Make document unsalvageable given document and reason.
        make_unsalvageable(reason);

        // FIXME: 3. If document's node navigable is a top-level traversable, build not restored reasons for a top-level
        //    traversable and its descendants given document's node navigable.
    }

    // 2. Let childNavigables be document's child navigables.
    IGNORE_USE_IN_ESCAPING_LAMBDA auto child_navigables = navigable()->child_navigables();

    // 6. Queue a global task on the navigation and traversal task source given document's relevant global object to
    //    perform the following steps:
    auto finish_callback = GC::create_function(heap(), [document = this, after_all_destruction] {
        // 1. Destroy document.
        document->destroy();

        // 2. If afterAllDestruction was given, then run it.
        if (after_all_destruction)
            after_all_destruction->function()();
    });

    // AD-HOC: We avoid allocating a DocumentLifecycleState in case there's no child navigables.
    if (child_navigables.is_empty()) {
        HTML::queue_global_task(HTML::Task::Source::NavigationAndTraversal, relevant_global_object(*this), finish_callback);
        return;
    }

    // 3. Let numberDestroyed be 0.
    auto destruction_state = heap().allocate<DocumentLifecycleState>(*this, child_navigables.size(), finish_callback);

    // 4. For each childNavigable of childNavigables, queue a global task on the navigation and traversal task source
    //    given childNavigable's active window to perform the following steps:
    for (auto& child_navigable : child_navigables) {
        queue_global_task(HTML::Task::Source::NavigationAndTraversal, *child_navigable->active_window(),
            GC::create_function(heap(), [&heap = heap(), destruction_state, child_navigable] {
                // 1. Let incrementDestroyed be an algorithm step which increments numberDestroyed.
                auto increment_destroyed = GC::create_function(heap, [destruction_state] { destruction_state->did_process_child(); });

                // 2. Destroy a document and its descendants given childNavigable's active document and incrementDestroyed.
                child_navigable->active_document()->destroy_a_document_and_its_descendants(increment_destroyed);
            }));
    }

    // 5. Wait until numberDestroyed equals childNavigable's size.
    // NB: This is handled by destruction_state.
}

// https://html.spec.whatwg.org/multipage/browsing-the-web.html#abort-a-document
void Document::abort()
{
    // 1. Assert: this is running as part of a task queued on document's relevant agent's event loop.

    // FIXME: 2. Cancel any instances of the fetch algorithm in the context of document,
    //           discarding any tasks queued for them, and discarding any further data received from the network for them.
    //           If this resulted in any instances of the fetch algorithm being canceled
    //           or any queued tasks or any network data getting discarded,
    //           then make document unsalvageable given document and "fetch".

    // 3. If document's during-loading navigation ID for WebDriver BiDi is non-null, then:
    if (m_navigation_id.has_value()) {
        // 1. FIXME: Invoke WebDriver BiDi navigation aborted with document's node navigable,
        //           and new WebDriver BiDi navigation status whose whose id is document's navigation id,
        //           status is "canceled", and url is document's URL.

        // 2. Set document's during-loading navigation ID for WebDriver BiDi to null.
        m_navigation_id = {};
    }

    // 4. If document has an active parser, then:
    if (auto parser = active_parser()) {
        // 1. Set document's active parser was aborted to true.
        m_active_parser_was_aborted = true;

        // 2. Abort that parser.
        parser->abort();

        // 3. Make document unsalvageable given document and "parser-aborted".
        make_unsalvageable("parser-aborted"_string);
    }
}

// https://html.spec.whatwg.org/multipage/document-lifecycle.html#abort-a-document-and-its-descendants
void Document::abort_a_document_and_its_descendants()
{
    // FIXME: 1. Assert: this is running as part of a task queued on document's relevant agent's event loop.

    // 2. Let descendantNavigables be document's descendant navigables.
    auto descendant_navigables = this->descendant_navigables();

    // 3. For each descendantNavigable of descendantNavigables, queue a global task on the navigation and traversal task source given descendantNavigable's active window to perform the following steps:
    for (auto& descendant_navigable : descendant_navigables) {
        HTML::queue_global_task(HTML::Task::Source::NavigationAndTraversal, *descendant_navigable->active_window(), GC::create_function(heap(), [this, descendant_navigable = descendant_navigable.ptr()] {
            // NOTE: This is not in the spec but we need to abort ongoing navigations in all descendant navigables.
            //       See https://github.com/whatwg/html/issues/9711
            descendant_navigable->set_ongoing_navigation({});

            // 1. Abort descendantNavigable's active document.
            descendant_navigable->active_document()->abort();

            // 2. If descendantNavigable's active document's salvageable is false, then set document's salvageable to false.
            if (!descendant_navigable->active_document()->m_salvageable)
                m_salvageable = false;
        }));
    }

    // 4. Abort document.
    abort();
}

// https://html.spec.whatwg.org/multipage/dom.html#active-parser
GC::Ptr<HTML::HTMLParser> Document::active_parser()
{
    if (!m_parser)
        return nullptr;

    if (m_parser->aborted() || m_parser->stopped())
        return nullptr;

    return m_parser;
}

void Document::set_browsing_context(GC::Ptr<HTML::BrowsingContext> browsing_context)
{
    m_browsing_context = browsing_context;
}

// https://html.spec.whatwg.org/multipage/document-lifecycle.html#unload-a-document
void Document::unload(GC::Ptr<Document>)
{
    // FIXME: 1. Assert: this is running as part of a task queued on oldDocument's event loop.

    // FIXME: 2. Let unloadTimingInfo be a new document unload timing info.

    // FIXME: 3. If newDocument is not given, then set unloadTimingInfo to null.

    // FIXME: 4. Otherwise, if newDocument's event loop is not oldDocument's event loop, then the user agent may be unloading
    //    oldDocument in parallel. In that case, the user agent should set unloadTimingInfo to null.

    // 5. Let intendToStoreInBfcache be true if the user agent intends to keep oldDocument alive in a session history
    //    entry, such that it can later be used for history traversal.
    auto intend_to_store_in_bfcache = false;

    // 6. Let eventLoop be oldDocument's relevant agent's event loop.
    auto& event_loop = *HTML::relevant_agent(*this).event_loop;

    // 7. Increase eventLoop's termination nesting level by 1.
    event_loop.increment_termination_nesting_level();

    // 8. Increase oldDocument's unload counter by 1.
    m_unload_counter += 1;

    // 9. If intendToKeepInBfcache is false, then set oldDocument's salvageable state to false.
    if (!intend_to_store_in_bfcache)
        m_salvageable = false;

    // 10. If oldDocument's page showing is true:
    if (m_page_showing) {
        // 1. Set oldDocument's page showing to false.
        m_page_showing = false;

        // 2. Fire a page transition event named pagehide at oldDocument's relevant global object with oldDocument's
        //    salvageable state.
        as<HTML::Window>(relevant_global_object(*this)).fire_a_page_transition_event(HTML::EventNames::pagehide, m_salvageable);

        // 3. Update the visibility state of oldDocument to "hidden".
        update_the_visibility_state(HTML::VisibilityState::Hidden);
    }

    // FIXME: 11. If unloadTimingInfo is not null, then set unloadTimingInfo's unload event start time to the current high
    //     resolution time given newDocument's relevant global object, coarsened given oldDocument's relevant settings
    //     object's cross-origin isolated capability.

    // 12. If oldDocument's salvageable state is false, then fire an event named unload at oldDocument's relevant global
    //     object, with legacy target override flag set.
    if (!m_salvageable) {
        // then fire an event named unload at document's relevant global object, with legacy target override flag set.
        // FIXME: The legacy target override flag is currently set by a virtual override of dispatch_event()
        //        We should reorganize this so that the flag appears explicitly here instead.
        auto event = DOM::Event::create(realm(), HTML::EventNames::unload);
        as<HTML::Window>(relevant_global_object(*this)).dispatch_event(event);
    }

    // FIXME: 13. If unloadTimingInfo is not null, then set unloadTimingInfo's unload event end time to the current high
    //     resolution time given newDocument's relevant global object, coarsened given oldDocument's relevant settings
    //     object's cross-origin isolated capability.

    // 14. Decrease eventLoop's termination nesting level by 1.
    event_loop.decrement_termination_nesting_level();

    // FIXME: 15. Set oldDocument's suspension time to the current high resolution time given document's relevant global object.

    // FIXME: 16. Set oldDocument's suspended timer handles to the result of getting the keys for the map of active timers.

    // FIXME: 17. Set oldDocument's has been scrolled by the user to false.

    // 18. Run any unloading document cleanup steps for oldDocument that are defined by this specification and other
    //     applicable specifications.
    run_unloading_cleanup_steps();

    // 19. If oldDocument's salvageable state is false, then destroy oldDocument.
    if (!m_salvageable)
        destroy();

    // 20. Decrease oldDocument's unload counter by 1.
    m_unload_counter -= 1;

    // FIXME: 21. If newDocument is given, newDocument's was created via cross-origin redirects is false, and newDocument's
    //     origin is the same as oldDocument's origin, then set newDocument's previous document unload timing to
    //     unloadTimingInfo.

    did_stop_being_active_document_in_navigable();
}

// https://html.spec.whatwg.org/multipage/document-lifecycle.html#unload-a-document-and-its-descendants
void Document::unload_a_document_and_its_descendants(GC::Ptr<Document> new_document, GC::Ptr<GC::Function<void()>> after_all_unloads)
{
    // FIXME: 1. Assert: this is running within document's node navigable's traversable navigable's session history traversal
    //    queue.

    // 2. Let childNavigables be document's child navigables.
    IGNORE_USE_IN_ESCAPING_LAMBDA auto child_navigables = navigable()->child_navigables();

    // 6. Queue a global task on the navigation and traversal task source given document's relevant global object to
    //    perform the following steps:
    auto finish_callback = GC::create_function(heap(), [document = this, new_document, after_all_unloads] {
        // FIXME: 1. If firePageSwapSteps is given, then run firePageSwapSteps.

        // 2. Unload document, passing along newDocument if it is not null.
        document->unload(new_document);

        // 3. If afterAllUnloads was given, then run it.
        if (after_all_unloads)
            after_all_unloads->function()();
    });

    // AD-HOC: We avoid allocating a DocumentLifecycleState in case there's no child navigables.
    //         Queue with null document to ensure the task is always runnable. The document
    //         can become non-fully-active during unloading, which would make the task stuck.
    if (child_navigables.is_empty()) {
        HTML::queue_a_task(HTML::Task::Source::NavigationAndTraversal, nullptr, nullptr, finish_callback);
        return;
    }

    // 3. Let numberUnloaded be 0.
    auto unload_state = heap().allocate<DocumentLifecycleState>(*this, child_navigables.size(), finish_callback);

    // 4. For each childNavigable of childNavigables [[ in what order? ]], queue a global task on the navigation and
    //    traversal task source given childNavigable's active window to perform the following steps:
    for (auto& child_navigable : child_navigables) {
        HTML::queue_a_task(HTML::Task::Source::NavigationAndTraversal, nullptr, nullptr,
            GC::create_function(heap(), [&heap = heap(), unload_state, child_navigable = child_navigable.ptr()] {
                // 1. Let incrementUnloaded be an algorithm step which increments numberUnloaded.
                auto increment_unloaded = GC::create_function(heap, [unload_state] { unload_state->did_process_child(); });

                // 2. Unload a document and its descendants given childNavigable's active document, null, and incrementUnloaded.
                child_navigable->active_document()->unload_a_document_and_its_descendants({}, increment_unloaded);
            }));
    }

    // 5. Wait until numberUnloaded equals childNavigables's size.
    // NB: This is handled by unload_state.
}

// https://html.spec.whatwg.org/multipage/iframe-embed-object.html#allowed-to-use
bool Document::is_allowed_to_use_feature(PolicyControlledFeature feature) const
{
    // 1. If document's browsing context is null, then return false.
    if (browsing_context() == nullptr)
        return false;

    // 2. If document is not fully active, then return false.
    if (!is_fully_active())
        return false;

    // 3. If the result of running is feature enabled in document for origin on feature, document, and document's origin
    //    is "Enabled", then return true.
    // FIXME: This is ad-hoc. Implement the Permissions Policy specification.
    switch (feature) {
    case PolicyControlledFeature::Autoplay:
        if (PermissionsPolicy::AutoplayAllowlist::the().is_allowed_for_origin(*this, origin()) == PermissionsPolicy::Decision::Enabled)
            return true;
        break;
    case PolicyControlledFeature::Camera:
        // FIXME: Implement allowlist for this.
        return true;
    case PolicyControlledFeature::FocusWithoutUserActivation:
    case PolicyControlledFeature::EncryptedMedia:
        // FIXME: Implement allowlist for this.
        return true;
    case PolicyControlledFeature::Fullscreen:
        // FIXME: Implement allowlist for this.
        return true;
    case PolicyControlledFeature::Gamepad:
        // FIXME: Implement allowlist for this.
        return true;
    case PolicyControlledFeature::Microphone:
        // FIXME: Implement allowlist for this.
        return true;
    case PolicyControlledFeature::WindowManagement:
        // FIXME: Implement allowlist for this.
        return true;
    }

    // 4. Return false.
    return false;
}

void Document::did_stop_being_active_document_in_navigable()
{
    clear_layout_and_paintable_nodes_for_inactive_document();
    tear_down_layout_tree();

    schedule_html_parser_end_check();

    if (!m_has_fired_document_became_inactive) {
        m_has_fired_document_became_inactive = true;
        notify_each_document_observer([&](auto const& document_observer) {
            return document_observer.document_became_inactive();
        });
    }
}

void Document::increment_throw_on_dynamic_markup_insertion_counter(Badge<HTML::HTMLParser>)
{
    ++m_throw_on_dynamic_markup_insertion_counter;
}

void Document::decrement_throw_on_dynamic_markup_insertion_counter(Badge<HTML::HTMLParser>)
{
    VERIFY(m_throw_on_dynamic_markup_insertion_counter);
    --m_throw_on_dynamic_markup_insertion_counter;
}

// https://html.spec.whatwg.org/multipage/scripting.html#appropriate-template-contents-owner-document
GC::Ref<DOM::Document> Document::appropriate_template_contents_owner_document()
{
    // 1. If document is not a Document created by this algorithm:
    if (!created_for_appropriate_template_contents()) {
        // 1. If document does not yet have an associated inert template document:
        if (!m_associated_inert_template_document) {
            // 1. Let newDocument be a new Document (whose browsing context is null). This is "a Document created by
            //    this algorithm" for the purposes of the step above.
            auto new_document = HTML::HTMLDocument::create(realm());
            new_document->m_created_for_appropriate_template_contents = true;

            // 2. If document is an HTML document, then mark newDocument as an HTML document also.
            if (document_type() == Type::HTML)
                new_document->set_document_type(Type::HTML);

            // AD-HOC: Copy over the "allow declarative shadow roots" flag, otherwise no elements inside templates will
            //         be able to have declarative shadow roots.
            // Spec issue: https://github.com/whatwg/html/issues/11955
            new_document->set_allow_declarative_shadow_roots(allow_declarative_shadow_roots());

            // 3. Set document's associated inert template document to newDocument.
            m_associated_inert_template_document = new_document;
        }
        // 2. Set document to document's associated inert template document.
        return *m_associated_inert_template_document;
    }
    // 2. Return document.
    return *this;
}

String Document::dump_accessibility_tree_as_json()
{
    StringBuilder builder;
    auto accessibility_tree = AccessibilityTreeNode::create(this, nullptr);
    build_accessibility_tree(*&accessibility_tree);
    auto json = MUST(JsonObjectSerializer<>::try_create(builder));

    // Empty document
    if (!accessibility_tree->value()) {
        MUST(json.add("type"sv, "element"sv));
        MUST(json.add("role"sv, "document"sv));
    } else {
        accessibility_tree->serialize_tree_as_json(json, *this);
    }

    MUST(json.finish());
    return MUST(builder.to_string());
}

// https://dom.spec.whatwg.org/#dom-document-createattribute
WebIDL::ExceptionOr<GC::Ref<Attr>> Document::create_attribute(String const& local_name)
{
    // 1. If localName is not a valid attribute local name, then throw an "InvalidCharacterError" DOMException.
    if (!is_valid_attribute_local_name(local_name))
        return WebIDL::InvalidCharacterError::create(realm(), "Invalid character in attribute name."_utf16);

    // 2. If this is an HTML document, then set localName to localName in ASCII lowercase.
    // 3. Return a new attribute whose local name is localName and node document is this.
    return Attr::create(*this, is_html_document() ? local_name.to_ascii_lowercase() : local_name);
}

// https://dom.spec.whatwg.org/#dom-document-createattributens
WebIDL::ExceptionOr<GC::Ref<Attr>> Document::create_attribute_ns(Optional<FlyString> const& namespace_, String const& qualified_name)
{
    // 1. Let (namespace, prefix, localName) be the result of validating and extracting namespace and qualifiedName given "attribute".
    auto extracted_qualified_name = TRY(validate_and_extract(realm(), namespace_, qualified_name, ValidationContext::Attribute));

    // 2. Return a new attribute whose namespace is namespace, namespace prefix is prefix, local name is localName, and node document is this.
    return Attr::create(*this, extracted_qualified_name);
}

// https://html.spec.whatwg.org/multipage/browsing-the-web.html#make-active
void Document::make_active()
{
    // 1. Let window be document's relevant global object.
    auto& window = as<HTML::Window>(HTML::relevant_global_object(*this));

    set_window(window);

    // 2. Set document's browsing context's WindowProxy's [[Window]] internal slot value to window.
    m_browsing_context->window_proxy()->set_window(window);

    if (m_browsing_context->is_top_level()) {
        page().client().page_did_change_active_document_in_top_level_browsing_context(*this);
    }

    // 3. Set window's relevant settings object's execution ready flag.
    HTML::relevant_settings_object(window).execution_ready = true;

    if (m_needs_to_call_page_did_load) {
        navigable()->traversable_navigable()->page().client().page_did_finish_loading(url());
        m_needs_to_call_page_did_load = false;
    }

    notify_each_document_observer([&](auto const& document_observer) {
        return document_observer.document_became_active();
    });
}

// https://html.spec.whatwg.org/multipage/interaction.html#set-the-initial-visibility-state
void Document::set_initial_visibility_state(HTML::VisibilityState visibility_state)
{
    // 1. Set document's visibility state to visibility state.
    m_visibility_state = visibility_state;

    // TODO: 2. Queue a new VisibilityStateEntry whose visibility state is document's visibility state and whose timestamp is 0.

    // AD-HOC: Record the initial viewport and visual viewport state so that if the viewport changes before the
    //         first rendering update (e.g. in our fullscreen tests), change events are still fired.
    if (!m_last_viewport_size.has_value()) {
        m_last_viewport_size = viewport_rect().size().to_type<int>();
        auto& current_visual_viewport = *visual_viewport();
        m_last_visual_viewport_state = VisualViewportState { current_visual_viewport.scale(), { current_visual_viewport.width(), current_visual_viewport.height() } };
    }
}

HTML::ListOfAvailableImages& Document::list_of_available_images()
{
    return *m_list_of_available_images;
}

HTML::ListOfAvailableImages const& Document::list_of_available_images() const
{
    return *m_list_of_available_images;
}

CSSPixelRect Document::viewport_rect() const
{
    if (auto const navigable = this->navigable())
        return navigable->viewport_rect();
    return CSSPixelRect {};
}

GC::Ref<CSS::VisualViewport> Document::visual_viewport()
{
    if (!m_visual_viewport)
        m_visual_viewport = CSS::VisualViewport::create(*this);
    return *m_visual_viewport;
}

void Document::register_viewport_client(ViewportClient& client)
{
    auto result = m_viewport_clients.set(&client);
    VERIFY(result == AK::HashSetResult::InsertedNewEntry);
}

void Document::unregister_viewport_client(ViewportClient& client)
{
    bool was_removed = m_viewport_clients.remove(&client);
    VERIFY(was_removed);
}

void Document::inform_all_viewport_clients_about_the_current_viewport_rect()
{
    for (auto* client : m_viewport_clients)
        client->did_set_viewport_rect(viewport_rect());
}

void Document::register_intersection_observer(Badge<IntersectionObserver::IntersectionObserver>, IntersectionObserver::IntersectionObserver& observer)
{
    auto result = m_intersection_observers.set(observer);
    VERIFY(result == AK::HashSetResult::InsertedNewEntry);
}

void Document::unregister_intersection_observer(Badge<IntersectionObserver::IntersectionObserver>, IntersectionObserver::IntersectionObserver& observer)
{
    bool was_removed = m_intersection_observers.remove(observer);
    VERIFY(was_removed);
}

void Document::register_resize_observer(Badge<ResizeObserver::ResizeObserver>, ResizeObserver::ResizeObserver& observer)
{
    m_resize_observers.append(observer);
}

void Document::unregister_resize_observer(Badge<ResizeObserver::ResizeObserver>, ResizeObserver::ResizeObserver& observer)
{
    m_resize_observers.remove(observer);
}

// https://www.w3.org/TR/intersection-observer/#queue-an-intersection-observer-task
void Document::queue_intersection_observer_task()
{
    auto window = this->window();
    if (!window)
        return;

    // 1. If document’s IntersectionObserverTaskQueued flag is set to true, return.
    if (m_intersection_observer_task_queued)
        return;

    // 2. Set document’s IntersectionObserverTaskQueued flag to true.
    m_intersection_observer_task_queued = true;

    // 3. Queue a task on the IntersectionObserver task source associated with the document's event loop to notify intersection observers.
    HTML::queue_global_task(HTML::Task::Source::IntersectionObserver, *window, GC::create_function(heap(), [this]() {
        auto& realm = this->realm();

        // https://www.w3.org/TR/intersection-observer/#notify-intersection-observers
        // 1. Set document’s IntersectionObserverTaskQueued flag to false.
        m_intersection_observer_task_queued = false;

        // 2. Let notify list be a list of all IntersectionObservers whose root is in the DOM tree of document.
        auto notify_list = GC::RootVector { heap(), m_intersection_observers.values() };

        // 3. For each IntersectionObserver object observer in notify list, run these steps:
        for (auto& observer : notify_list) {
            // 2. Let queue be a copy of observer’s internal [[QueuedEntries]] slot.
            // 3. Clear observer’s internal [[QueuedEntries]] slot.
            auto queue = observer->take_records();

            // 1. If observer’s internal [[QueuedEntries]] slot is empty, continue.
            if (queue.is_empty())
                continue;

            auto wrapped_queue = MUST(JS::Array::create(realm, 0));
            for (size_t i = 0; i < queue.size(); ++i) {
                auto& record = queue.at(i);
                auto property_index = JS::PropertyKey { i };
                MUST(wrapped_queue->create_data_property(property_index, record.ptr()));
            }

            // 4. Let callback be the value of observer’s internal [[callback]] slot.
            auto& callback = observer->callback();

            // 5. Invoke callback with queue as the first argument, observer as the second argument, and observer as the callback this value. If this throws an exception, report the exception.
            // NOTE: This does not follow the spec as written precisely, but this is the same thing we do elsewhere and there is a WPT test that relies on this.
            (void)WebIDL::invoke_callback(callback, observer.ptr(), WebIDL::ExceptionBehavior::Report, { { wrapped_queue, observer.ptr() } });
        }
    }));
}

// https://www.w3.org/TR/intersection-observer/#queue-an-intersectionobserverentry
void Document::queue_an_intersection_observer_entry(IntersectionObserver::IntersectionObserver& observer, HighResolutionTime::DOMHighResTimeStamp time, GC::Ref<Geometry::DOMRectReadOnly> root_bounds, GC::Ref<Geometry::DOMRectReadOnly> bounding_client_rect, GC::Ref<Geometry::DOMRectReadOnly> intersection_rect, bool is_intersecting, double intersection_ratio, GC::Ref<Element> target)
{
    auto& realm = this->realm();

    // 1. Construct an IntersectionObserverEntry, passing in time, rootBounds, boundingClientRect, intersectionRect, isIntersecting, and target.
    auto entry = realm.create<IntersectionObserver::IntersectionObserverEntry>(realm, time, root_bounds, bounding_client_rect, intersection_rect, is_intersecting, intersection_ratio, target);

    // 2. Append it to observer’s internal [[QueuedEntries]] slot.
    observer.queue_entry({}, entry);

    // 3. Queue an intersection observer task for document.
    queue_intersection_observer_task();
}

// https://www.w3.org/TR/intersection-observer/#compute-the-intersection
static CSSPixelRect compute_intersection(GC::Ref<Element> target, CSSPixelRect target_rect, IntersectionObserver::IntersectionObserver const& observer, RefPtr<Painting::PaintableBox> root_paintable, CSSPixelRect const& root_bounds)
{
    // 1. Let intersectionRect be the result of getting the bounding box for target.
    auto intersection_rect = target_rect;

    // 2. Let container be the containing block of target.
    // 3. While container is not root:
    if (auto target_paintable = target->paintable_box()) {
        for (auto container = target_paintable->containing_block(); container; container = container->containing_block()) {
            // Stop when we reach the intersection root.
            if (container == root_paintable)
                break;

            // FIXME: 3.1. If container is the document of a nested browsing context, update
            //             intersectionRect by clipping to the viewport of the document, and update
            //             container to be the browsing context container of container.

            // NOTE: Steps 3.2 (map to container coordinate space) and 3.5 (update container) are
            //       unnecessary here because get_bounding_client_rect() and transform_rect_to_viewport()
            //       already produce viewport-relative coordinates.

            // 3.3. If container is a scroll container, apply the observer’s [[scrollMargin]]
            //      to the container’s clip rect.
            // 3.4. If container has a content clip or a css clip-path property, update intersectionRect
            //      by applying container’s clip.
            // FIXME: Handle clip-path.
            auto overflow_x = container->computed_values().overflow_x();
            auto overflow_y = container->computed_values().overflow_y();
            bool has_content_clip = overflow_x != CSS::Overflow::Visible || overflow_y != CSS::Overflow::Visible;
            if (has_content_clip) {
                auto clip_rect = container->transform_rect_to_viewport(container->absolute_padding_box_rect());

                // Apply scroll margin to expand the scrollport for scroll containers.
                auto& scroll_margin = observer.scroll_margin_values();
                auto const& layout_node = container->layout_node_with_style_and_box_metrics();
                if (layout_node.is_scroll_container() && !scroll_margin.is_empty()) {
                    clip_rect.inflate(
                        scroll_margin[0].to_px(layout_node, clip_rect.height()),
                        scroll_margin[1].to_px(layout_node, clip_rect.width()),
                        scroll_margin[2].to_px(layout_node, clip_rect.height()),
                        scroll_margin[3].to_px(layout_node, clip_rect.width()));
                }

                intersection_rect.intersect(clip_rect);
            }
        }
    }

    // FIXME: 4. Map intersectionRect to the coordinate space of root.

    // 5. Update intersectionRect by intersecting it with the root intersection rectangle.
    intersection_rect.intersect(root_bounds);

    // FIXME: 6. Map intersectionRect to the coordinate space of the viewport of the document containing target.

    // 7. Return intersectionRect.
    return intersection_rect;
}

// https://www.w3.org/TR/intersection-observer/#run-the-update-intersection-observations-steps
void Document::run_the_update_intersection_observations_steps(HighResolutionTime::DOMHighResTimeStamp time)
{
    auto& realm = this->realm();

    // 1. Let observer list be a list of all IntersectionObservers whose root is in the DOM tree of document.
    //    For the top-level browsing context, this includes implicit root observers.
    // 2. For each observer in observer list:

    // NOTE: We make a copy of the intersection observers list to avoid modifying it while iterating.
    auto intersection_observers = GC::RootVector { heap(), m_intersection_observers.values() };

    update_paint_and_hit_testing_properties_if_needed();

    for (auto& observer : intersection_observers) {
        // 1. Let rootBounds be observer’s root intersection rectangle.
        auto root_bounds = observer->root_intersection_rectangle();

        // Pre-compute per-observer values to avoid repeated work in the per-target loop.
        auto intersection_root_node = observer->intersection_root_node();
        auto root_paintable = intersection_root_node->paintable_box();
        bool is_implicit_root = observer->is_implicit_root();
        bool root_is_element = intersection_root_node->is_element();

        // 2. For each target in observer’s internal [[ObservationTargets]] slot, processed in the same order that
        //    observe() was called on each target:
        for (auto& observed_target : observer->observation_targets()) {
            auto& target = observed_target.target;
            // 1. Let:
            // thresholdIndex be 0.
            size_t threshold_index = 0;

            // isIntersecting be false.
            bool is_intersecting = false;

            // targetRect be a DOMRectReadOnly with x, y, width, and height set to 0.
            CSSPixelRect target_rect { 0, 0, 0, 0 };

            // intersectionRect be a DOMRectReadOnly with x, y, width, and height set to 0.
            CSSPixelRect intersection_rect { 0, 0, 0, 0 };

            // SPEC ISSUE: It doesn’t pass in intersection ratio to "queue an IntersectionObserverEntry" despite needing it.
            //             This is default 0, as isIntersecting is default false, see step 9.
            double intersection_ratio = 0.0;

            // 2. If the intersection root is not the implicit root, and target is not in the same document as the intersection root, skip to step 11.
            // 3. If the intersection root is an Element, and target is not a descendant of the intersection root in the containing block chain, skip to step 11.
            // FIXME: Actually use the containing block chain.
            // NOTE: Check if target has a layout node is not in the spec but required to match other browsers.
            if (target->layout_node() && (is_implicit_root || &target->document() == &intersection_root_node->document()) && !(root_is_element && !target->is_descendant_of(*intersection_root_node))) {
                // 4. Set targetRect to the DOMRectReadOnly obtained by getting the bounding box for target.
                target_rect = target->bounding_client_rect_assuming_layout_clean();

                // 5. Let intersectionRect be the result of running the compute the intersection algorithm on target and
                //    observer’s intersection root.
                intersection_rect = compute_intersection(target, target_rect, *observer, root_paintable, root_bounds);

                // 6. Let targetArea be targetRect’s area.
                auto target_area = target_rect.width() * target_rect.height();

                // 7. Let intersectionArea be intersectionRect’s area.
                auto intersection_area = intersection_rect.size().area();

                // 8. Let isIntersecting be true if targetRect and rootBounds intersect or are edge-adjacent, even if the
                //    intersection has zero area (because rootBounds or targetRect have zero area).
                is_intersecting = target_rect.edge_adjacent_intersects(root_bounds);

                // 9. If targetArea is non-zero, let intersectionRatio be intersectionArea divided by targetArea.
                //    Otherwise, let intersectionRatio be 1 if isIntersecting is true, or 0 if isIntersecting is false.
                if (target_area != 0.0)
                    intersection_ratio = (intersection_area / target_area).to_double();
                else
                    intersection_ratio = is_intersecting ? 1.0 : 0.0;

                // 10. Set thresholdIndex to the index of the first entry in observer.thresholds whose value is greater
                //     than intersectionRatio, or the length of observer.thresholds if intersectionRatio is greater than
                //     or equal to the last entry in observer.thresholds.
                // NB: Thresholds are sorted in ascending order, so we use binary search.
                {
                    auto const& thresholds = observer->thresholds();
                    size_t lo = 0;
                    size_t hi = thresholds.size();
                    while (lo < hi) {
                        size_t mid = lo + (hi - lo) / 2;
                        if (thresholds[mid] > intersection_ratio)
                            hi = mid;
                        else
                            lo = mid + 1;
                    }
                    threshold_index = lo;
                }
            }

            // 11. Let intersectionObserverRegistration be the IntersectionObserverRegistration record in target’s
            //     internal [[RegisteredIntersectionObservers]] slot whose observer property is equal to observer.
            // NB: This implementation deviates from the spec's storage model. intersectionObserverRegistration here
            //     aliases the observer-side observation target state, since target's registered observers only keep
            //     the observer reference. This avoids an extra lookup through target on every update.
            auto& intersection_observer_registration = observed_target;

            // 12. Let previousThresholdIndex be the intersectionObserverRegistration’s previousThresholdIndex property.
            auto previous_threshold_index = intersection_observer_registration.previous_threshold_index;

            // 13. Let previousIsIntersecting be the intersectionObserverRegistration’s previousIsIntersecting property.
            auto previous_is_intersecting = intersection_observer_registration.previous_is_intersecting;

            // 14. If thresholdIndex does not equal previousThresholdIndex or if isIntersecting does not equal
            //     previousIsIntersecting, queue an IntersectionObserverEntry, passing in observer, time,
            //     rootBounds, targetRect, intersectionRect, isIntersecting, and target.
            if (threshold_index != previous_threshold_index || is_intersecting != previous_is_intersecting) {
                auto root_bounds_as_dom_rect = Geometry::DOMRectReadOnly::construct_impl(realm, static_cast<double>(root_bounds.x()), static_cast<double>(root_bounds.y()), static_cast<double>(root_bounds.width()), static_cast<double>(root_bounds.height())).release_value_but_fixme_should_propagate_errors();

                // SPEC ISSUE: It doesn't pass in intersectionRatio, but it's required.
                auto target_dom_rect = MUST(Geometry::DOMRectReadOnly::construct_impl(realm, static_cast<double>(target_rect.x()), static_cast<double>(target_rect.y()), static_cast<double>(target_rect.width()), static_cast<double>(target_rect.height())));
                auto intersection_dom_rect = MUST(Geometry::DOMRectReadOnly::construct_impl(realm, static_cast<double>(intersection_rect.x()), static_cast<double>(intersection_rect.y()), static_cast<double>(intersection_rect.width()), static_cast<double>(intersection_rect.height())));
                queue_an_intersection_observer_entry(observer, time, root_bounds_as_dom_rect, target_dom_rect, intersection_dom_rect, is_intersecting, intersection_ratio, target);
            }

            // 15. Assign thresholdIndex to intersectionObserverRegistration’s previousThresholdIndex property.
            intersection_observer_registration.previous_threshold_index = threshold_index;

            // 16. Assign isIntersecting to intersectionObserverRegistration’s previousIsIntersecting property.
            intersection_observer_registration.previous_is_intersecting = is_intersecting;
        }
    }
}

// https://html.spec.whatwg.org/multipage/urls-and-fetching.html#start-intersection-observing-a-lazy-loading-element
void Document::start_intersection_observing_a_lazy_loading_element(Element& element)
{
    VERIFY(element.is_lazy_loading());

    auto& realm = this->realm();

    // 1. Let doc be element's node document.
    VERIFY(&element.document() == this);

    // 2. If doc's lazy load intersection observer is null, set it to a new IntersectionObserver instance, initialized as follows:
    if (!m_lazy_load_intersection_observer) {
        // - The callback is these steps, with arguments entries and observer:
        auto callback = JS::NativeFunction::create(realm, Utf16FlyString {}, [this](JS::VM& vm) -> JS::ThrowCompletionOr<JS::Value> {
            // For each entry in entries using a method of iteration which does not trigger developer-modifiable array accessors or iteration hooks:
            auto& entries = as<JS::Array>(vm.argument(0).as_object());
            auto entries_length = MUST(MUST(entries.get(vm.names.length)).to_length(vm));

            for (size_t i = 0; i < entries_length; ++i) {
                auto property_key = JS::PropertyKey { i };
                auto& entry = as<IntersectionObserver::IntersectionObserverEntry>(entries.get_without_side_effects(property_key).as_object());

                // 1. Let resumptionSteps be null.
                GC::Ptr<GC::Function<void()>> resumption_steps;

                // 2. If entry.isIntersecting is true, then set resumptionSteps to entry.target's lazy load resumption steps.
                if (entry.is_intersecting()) {
                    // 5. Set entry.target's lazy load resumption steps to null.
                    VERIFY(entry.target()->is_lazy_loading());
                    resumption_steps = entry.target()->take_lazy_load_resumption_steps({});
                }

                // 3. If resumptionSteps is null, then return.
                if (!resumption_steps) {
                    // NOTE: This is wrong in the spec, since we want to keep processing
                    //       entries even if one of them doesn't have resumption steps.
                    // FIXME: Spec bug: https://github.com/whatwg/html/issues/10019
                    continue;
                }

                // 4. Stop intersection-observing a lazy loading element for entry.target.
                stop_intersection_observing_a_lazy_loading_element(entry.target());

                // 5. Set entry.target's lazy load resumption steps to null.
                entry.target()->take_lazy_load_resumption_steps({});

                // 6. Invoke resumptionSteps.
                resumption_steps->function()();
            }

            return JS::js_undefined();
        });

        // FIXME: The options is an IntersectionObserverInit dictionary with the following dictionary members: «[ "rootMargin" → lazy load root margin ]»
        // Spec Note: This allows for fetching the image during scrolling, when it does not yet — but is about to — intersect the viewport.
        auto options = Bindings::IntersectionObserverInit {};

        auto wrapped_callback = realm.heap().allocate<WebIDL::CallbackType>(callback, realm);
        m_lazy_load_intersection_observer = IntersectionObserver::IntersectionObserver::construct_impl(realm, wrapped_callback, options).release_value_but_fixme_should_propagate_errors();
    }

    // 3. Call doc's lazy load intersection observer's observe method with element as the argument.
    VERIFY(m_lazy_load_intersection_observer);
    m_lazy_load_intersection_observer->observe(element);
}

// https://html.spec.whatwg.org/multipage/urls-and-fetching.html#stop-intersection-observing-a-lazy-loading-element
void Document::stop_intersection_observing_a_lazy_loading_element(Element& element)
{
    // 1. Let doc be element's node document.
    // NOTE: It's `this`.

    // 2. Assert: doc's lazy load intersection observer is not null.
    VERIFY(m_lazy_load_intersection_observer);

    // 3. Call doc's lazy load intersection observer unobserve method with element as the argument.
    m_lazy_load_intersection_observer->unobserve(element);
}

// https://html.spec.whatwg.org/multipage/semantics.html#shared-declarative-refresh-steps
void Document::shared_declarative_refresh_steps(StringView input, GC::Ptr<HTML::HTMLMetaElement const> meta_element)
{
    // 1. If document's will declaratively refresh is true, then return.
    if (m_will_declaratively_refresh)
        return;

    // 2. Let position point at the first code point of input.
    GenericLexer lexer(input);

    // 3. Skip ASCII whitespace within input given position.
    lexer.ignore_while(Infra::is_ascii_whitespace);

    // 4. Let time be 0.
    u32 time = 0;

    // 5. Collect a sequence of code points that are ASCII digits from input given position, and let timeString be the result.
    auto time_string = lexer.consume_while(is_ascii_digit);

    // 6. If timeString is the empty string, then:
    if (time_string.is_empty()) {
        // 1. If the code point in input pointed to by position is not U+002E (.), then return.
        if (lexer.peek() != '.')
            return;
    }

    // 7. Otherwise, set time to the result of parsing timeString using the rules for parsing non-negative integers.
    auto maybe_time = Web::HTML::parse_non_negative_integer(time_string);

    // FIXME: Since we only collected ASCII digits, this can only fail because of overflow. What do we do when that happens? For now, default to 0.
    if (maybe_time.has_value() && maybe_time.value() < NumericLimits<int>::max() && !Checked<int>::multiplication_would_overflow(static_cast<int>(maybe_time.value()), 1000)) {
        time = maybe_time.value();
    }

    // 8. Collect a sequence of code points that are ASCII digits and U+002E FULL STOP characters (.) from input given
    //    position. Ignore any collected characters.
    lexer.ignore_while([](auto c) {
        return is_ascii_digit(c) || c == '.';
    });

    // 9. Let urlRecord be document's URL.
    auto url_record = url();

    // 10. If position is not past the end of input, then:
    if (!lexer.is_eof()) {
        // 1. If the code point in input pointed to by position is not U+003B (;), U+002C (,), or ASCII whitespace, then return.
        if (lexer.peek() != ';' && lexer.peek() != ',' && !Infra::is_ascii_whitespace(lexer.peek()))
            return;

        // 2. Skip ASCII whitespace within input given position.
        lexer.ignore_while(Infra::is_ascii_whitespace);

        // 3. If the code point in input pointed to by position is U+003B (;) or U+002C (,), then advance position to the next code point.
        if (lexer.peek() == ';' || lexer.peek() == ',')
            lexer.ignore(1);

        // 4. Skip ASCII whitespace within input given position.
        lexer.ignore_while(Infra::is_ascii_whitespace);
    }

    // 11. If position is not past the end of input, then:
    if (!lexer.is_eof()) {
        // 1. Let urlString be the substring of input from the code point at position to the end of the string.
        auto url_string = lexer.remaining();

        // 2. If the code point in input pointed to by position is U+0055 (U) or U+0075 (u), then advance position to the next code point. Otherwise, jump to the step labeled skip quotes.
        if (lexer.peek() == 'U' || lexer.peek() == 'u')
            lexer.ignore(1);
        else
            goto skip_quotes;

        // 3. If the code point in input pointed to by position is U+0052 (R) or U+0072 (r), then advance position to the next code point. Otherwise, jump to the step labeled parse.
        if (lexer.peek() == 'R' || lexer.peek() == 'r')
            lexer.ignore(1);
        else
            goto parse;

        // 4. If the code point in input pointed to by position is U+004C (L) or U+006C (l), then advance position to the next code point. Otherwise, jump to the step labeled parse.
        if (lexer.peek() == 'L' || lexer.peek() == 'l')
            lexer.ignore(1);
        else
            goto parse;

        // 5. Skip ASCII whitespace within input given position.
        lexer.ignore_while(Infra::is_ascii_whitespace);

        // 6. If the code point in input pointed to by position is U+003D (=), then advance position to the next code point. Otherwise, jump to the step labeled parse.
        if (lexer.peek() == '=')
            lexer.ignore(1);
        else
            goto parse;

        // 7. Skip ASCII whitespace within input given position.
        lexer.ignore_while(Infra::is_ascii_whitespace);

    skip_quotes: {
        // 8. Skip quotes: If the code point in input pointed to by position is U+0027 (') or U+0022 ("), then let
        //    quote be that code point, and advance position to the next code point. Otherwise, let quote be the empty
        //    string.
        Optional<char> quote;
        if (lexer.peek() == '\'' || lexer.peek() == '"')
            quote = lexer.consume();

        // 9. Set urlString to the substring of input from the code point at position to the end of the string.
        // 10. If quote is not the empty string, and there is a code point in urlString equal to quote, then truncate
        //     urlString at that code point, so that it and all subsequent code points are removed.
        url_string = lexer.consume_while([&quote](auto c) {
            return !quote.has_value() || c != quote.value();
        });
    }

    parse:
        // 11. Parse: Set urlRecord to the result of encoding-parsing a URL given urlString, relative to document.
        // 12. If urlRecord is failure, then return.
        auto maybe_url_record = encoding_parse_url(url_string);
        if (!maybe_url_record.has_value())
            return;

        url_record = maybe_url_record.release_value();

        // 13. If urlRecord's scheme is "javascript", then return.
        if (url_record.scheme() == "javascript"sv)
            return;
    }

    // 12. Set document's will declaratively refresh to true.
    m_will_declaratively_refresh = true;

    // 13. Perform one or more of the following steps:
    // - After the refresh has come due (as defined below), if the user has not canceled the redirect and, if meta is
    //   given, document's active sandboxing flag set does not have the sandboxed automatic features browsing context
    //   flag set, then navigate document's node navigable to urlRecord using document, with historyHandling set to
    //   "replace".
    m_active_refresh_timer = Core::Timer::create_single_shot(time * 1000, [this, has_meta_element = !!meta_element, url_record = move(url_record)]() {
        if (has_meta_element && has_flag(active_sandboxing_flag_set(), HTML::SandboxingFlagSet::SandboxedAutomaticFeatures))
            return;

        auto navigable = this->navigable();
        if (!navigable || navigable->has_been_destroyed())
            return;

        MUST(navigable->navigate({ .url = url_record, .source_document = *this, .history_handling = Bindings::NavigationHistoryBehavior::Replace }));
    });

    // For the purposes of the previous paragraph, a refresh is said to have come due as soon as the later of the
    // following two conditions occurs:

    // - At least time seconds have elapsed since document's completely loaded time, adjusted to take into
    //   account user or user agent preferences.
    // m_active_refresh_timer is started in completely_finished_loading after setting the completely loaded time.

    // - If meta is given, at least time seconds have elapsed since meta was inserted into the document document,
    // adjusted to take into account user or user agent preferences.
    // NOTE: This is only done if completely loaded time has a value because shared_declarative_refresh_steps is called
    // by HTMLMetaElement::inserted and if the document hasn't finished loading when the meta element was inserted,
    // then the document completely finishing loading will _always_ come after inserting the meta element.
    if (meta_element && m_completely_loaded_time.has_value()) {
        m_active_refresh_timer->start();
    }
}

RefPtr<Painting::ViewportPaintable const> Document::paintable() const
{
    auto paintable = Node::paintable();
    if (!paintable)
        return nullptr;
    return as<Painting::ViewportPaintable>(*paintable);
}

RefPtr<Painting::ViewportPaintable> Document::paintable()
{
    auto paintable = Node::paintable();
    if (!paintable)
        return nullptr;
    return as<Painting::ViewportPaintable>(*paintable);
}

RefPtr<Painting::ViewportPaintable const> Document::unsafe_paintable() const
{
    auto paintable = Node::unsafe_paintable();
    if (!paintable)
        return nullptr;
    return as<Painting::ViewportPaintable>(*paintable);
}

RefPtr<Painting::ViewportPaintable> Document::unsafe_paintable()
{
    auto paintable = Node::unsafe_paintable();
    if (!paintable)
        return nullptr;
    return as<Painting::ViewportPaintable>(*paintable);
}

// https://html.spec.whatwg.org/multipage/browsing-the-web.html#restore-the-history-object-state
void Document::restore_the_history_object_state(NonnullRefPtr<HTML::SessionHistoryEntry> entry)
{
    // 1. Let targetRealm be document's relevant realm.
    auto& target_realm = HTML::relevant_realm(*this);

    // 2. Let state be StructuredDeserialize(entry's classic history API state, targetRealm). If this throws an exception, catch it and let state be null.
    // 3. Set document's history object's state to state.
    auto state_or_error = HTML::structured_deserialize(target_realm.vm(), entry->classic_history_api_state(), target_realm);
    if (state_or_error.is_error())
        m_history->set_state(JS::js_null());
    else
        m_history->set_state(state_or_error.release_value());
}

// https://html.spec.whatwg.org/multipage/browsing-the-web.html#update-document-for-history-step-application
void Document::update_for_history_step_application(NonnullRefPtr<HTML::SessionHistoryEntry> entry, bool do_not_reactivate, size_t script_history_length, size_t script_history_index, Optional<Bindings::NavigationType> navigation_type, Optional<Vector<NonnullRefPtr<HTML::SessionHistoryEntry>>> entries_for_navigation_api, RefPtr<HTML::SessionHistoryEntry> previous_entry_for_activation, bool update_navigation_api)
{
    (void)previous_entry_for_activation;

    // 1. Let documentIsNew be true if document's latest entry is null; otherwise false.
    auto document_is_new = !m_latest_entry;

    // 2. Let documentsEntryChanged be true if document's latest entry is not entry; otherwise false.
    auto documents_entry_changed = m_latest_entry != entry;

    // 3. Set document's history object's index to scriptHistoryIndex.
    history()->m_index = script_history_index;

    // 4. Set document's history object's length to scriptHistoryLength.
    history()->m_length = script_history_length;

    // 5. Let navigation be history's relevant global object's navigation API.
    auto navigation = as<HTML::Window>(HTML::relevant_global_object(*this)).navigation();

    // 6. If documentsEntryChanged is true, then:
    // NOTE: documentsEntryChanged can be false for one of two reasons: either we are restoring from bfcache,
    //      or we are asynchronously finishing up a synchronous navigation which already synchronously set document's latest entry.
    //      The doNotReactivate argument distinguishes between these two cases.
    if (documents_entry_changed) {
        // 1. Let oldURL be document's latest entry's URL.
        auto old_url = m_latest_entry ? m_latest_entry->url() : URL::URL {};

        // 2. Set document's latest entry to entry.
        m_latest_entry = entry;

        // 3. Restore the history object state given document and entry.
        restore_the_history_object_state(entry);

        // 4. If documentIsNew is false, then:
        if (!document_is_new) {
            // NOTE: Not in the spec, but otherwise document's url won't be updated in case of a same-document back/forward navigation.
            set_url(entry->url());

            // AD HOC: Skip this in situations the spec steps don't account for
            if (update_navigation_api) {
                // 1. Assert: navigationType is not null.
                VERIFY(navigation_type.has_value());
                // 2. Update the navigation API entries for a same-document navigation given navigation, entry, and navigationType.
                navigation->update_the_navigation_api_entries_for_a_same_document_navigation(entry, navigation_type.value());
            }

            // 3. Fire an event named popstate at document's relevant global object, using PopStateEvent,
            //    with the state attribute initialized to document's history object's state and hasUAVisualTransition initialized to true
            //    if a visual transition, to display a cached rendered state of the latest entry, was done by the user agent.
            // FIXME: Initialise hasUAVisualTransition
            Bindings::PopStateEventInit popstate_event_init;
            popstate_event_init.state = history()->unsafe_state();
            auto& relevant_global_object = as<HTML::Window>(HTML::relevant_global_object(*this));
            auto pop_state_event = HTML::PopStateEvent::create(realm(), "popstate"_fly_string, popstate_event_init);
            relevant_global_object.dispatch_event(pop_state_event);

            // FIXME: 4. Restore persisted state given entry.

            // 5. If oldURL's fragment is not equal to entry's URL's fragment, then queue a global task on the DOM manipulation task source
            //    given document's relevant global object to fire an event named hashchange at document's relevant global object,
            //    using HashChangeEvent, with the oldURL attribute initialized to the serialization of oldURL and the newURL attribute
            //    initialized to the serialization of entry's URL.
            if (old_url.fragment() != entry->url().fragment()) {
                Bindings::HashChangeEventInit hashchange_event_init;
                hashchange_event_init.old_url = old_url.serialize();
                hashchange_event_init.new_url = entry->url().serialize();
                auto hashchange_event = HTML::HashChangeEvent::create(realm(), "hashchange"_fly_string, hashchange_event_init);
                HTML::queue_global_task(HTML::Task::Source::DOMManipulation, relevant_global_object, GC::create_function(heap(), [hashchange_event, &relevant_global_object]() {
                    relevant_global_object.dispatch_event(hashchange_event);
                }));
            }
        }

        // 5. Otherwise:
        else {
            // 1. Assert: entriesForNavigationAPI is given.
            VERIFY(entries_for_navigation_api.has_value());

            // FIXME: 2. Restore persisted state given entry.

            // 3. Initialize the navigation API entries for a new document given navigation, entriesForNavigationAPI, and entry.
            navigation->initialize_the_navigation_api_entries_for_a_new_document(*entries_for_navigation_api, entry);
        }
    }

    // FIXME: 7. If all the following are true:
    //    - previousEntryForActivation is given;
    //    - navigationType is non-null; and
    //    - navigationType is "reload" or previousEntryForActivation's document is not document, then:
    {
        // FIXME: 1. If navigation's activation is null, then set navigation's activation to a new NavigationActivation object in navigation's relevant realm.
        // FIXME: 2. Let previousEntryIndex be the result of getting the navigation API entry index of previousEntryForActivation within navigation.
        // FIXME: 3. If previousEntryIndex is non-negative, then set activation's old entry to navigation's entry list[previousEntryIndex].

        // FIXME: 4. Otherwise, if all the following are true:
        //    - navigationType is "replace";
        //    - previousEntryForActivation's document state's origin is same origin with document's origin; and
        //    - previousEntryForActivation's document's initial about:blank is false,
        //    then set activation's old entry to a new NavigationHistoryEntry in navigation's relevant realm, whose session history entry is previousEntryForActivation.

        // FIXME: 5. Set activation's new entry to navigation's current entry.
        // FIXME: 6. Set activation's navigation type to navigationType.
    }

    // 8. If documentIsNew is true, then:
    if (document_is_new) {
        // FIXME: 1. Assert: document's during-loading navigation ID for WebDriver BiDi is not null.
        // FIXME: 2. Invoke WebDriver BiDi navigation committed with navigable and a new WebDriver BiDi navigation
        //           status whose id is document's during-loading navigation ID for WebDriver BiDi, status is "committed", and url is document's URL

        // 3. Try to scroll to the fragment for document.
        try_to_scroll_to_the_fragment();

        // 4. At this point scripts may run for the newly-created document document.
        set_ready_to_run_scripts();
    }

    // 9. Otherwise, if documentsEntryChanged is false and doNotReactivate is false, then:
    // NOTE: This is for bfcache restoration
    if (!documents_entry_changed && !do_not_reactivate) {
        // FIXME: 1. Assert: entriesForNavigationAPI is given.
        // FIXME: 2. Reactivate document given entry and entriesForNavigationAPI.
    }
}

void Document::set_ready_to_run_scripts()
{
    m_ready_to_run_scripts = true;
    if (auto callback = m_deferred_parser_start) {
        m_deferred_parser_start = nullptr;
        callback->function()();
    }
}

void Document::set_deferred_parser_start(GC::Ref<GC::Function<void()>> callback)
{
    VERIFY(!m_deferred_parser_start);
    m_deferred_parser_start = callback;
}

void Document::set_latest_entry(RefPtr<HTML::SessionHistoryEntry> entry)
{
    m_latest_entry = move(entry);
}

HashMap<URL::URL, GC::Ptr<HTML::SharedResourceRequest>>& Document::shared_resource_requests()
{
    return m_shared_resource_requests;
}

// https://www.w3.org/TR/web-animations-1/#dom-document-timeline
GC::Ref<Animations::DocumentTimeline> Document::timeline()
{
    // The DocumentTimeline object representing the default document timeline. The default document timeline has an
    // origin time of zero.
    if (!m_default_timeline)
        m_default_timeline = Animations::DocumentTimeline::create(realm(), *this, 0.0);
    return *m_default_timeline;
}

void Document::associate_with_timeline(GC::Ref<Animations::AnimationTimeline> timeline)
{
    m_associated_animation_timelines.set(timeline);
}

void Document::disassociate_with_timeline(GC::Ref<Animations::AnimationTimeline> timeline)
{
    m_associated_animation_timelines.remove(timeline);
}

void Document::append_pending_animation_event(Web::DOM::Document::PendingAnimationEvent const& event)
{
    m_pending_animation_event_queue.append(event);
}

// https://www.w3.org/TR/web-animations-1/#update-animations-and-send-events
void Document::update_animations_and_send_events(double timestamp)
{
    m_last_animation_frame_timestamp = timestamp;
    auto timelines_to_update = GC::RootVector { heap(), m_associated_animation_timelines.values() };

    {
        HTML::TemporaryExecutionContext temporary_execution_context { realm() };
        // 1. Update the current time of all timelines associated with doc passing now as the timestamp.
        for (auto const& timeline : timelines_to_update)
            timeline->update_current_time(timestamp);

        // 2. Remove replaced animations for doc.
        remove_replaced_animations();

        // 3. Perform a microtask checkpoint.
        // NB: This is executed by the destructor of the TemporaryExecutionContext above.
    }

    // 4. Let events to dispatch be a copy of doc’s pending animation event queue.
    auto events_to_dispatch = GC::ConservativeVector<Document::PendingAnimationEvent> { vm().heap() };
    events_to_dispatch.extend(m_pending_animation_event_queue);

    // 5. Clear doc’s pending animation event queue.
    m_pending_animation_event_queue.clear();

    // 6. Perform a stable sort of the animation events in events to dispatch as follows:
    auto sort_events_by_composite_order = [](auto const& a, auto const& b) {
        if (!a.animation->effect())
            return true;
        if (!b.animation->effect())
            return false;
        auto& a_effect = as<Animations::KeyframeEffect>(*a.animation->effect());
        auto& b_effect = as<Animations::KeyframeEffect>(*b.animation->effect());
        return Animations::KeyframeEffect::composite_order(a_effect, b_effect) < 0;
    };

    insertion_sort(events_to_dispatch, [&](auto const& a, auto const& b) {
        // Sort the events by their scheduled event time such that events that were scheduled to occur earlier, sort
        // before events scheduled to occur later and events whose scheduled event time is unresolved sort before events
        // with a resolved scheduled event time.
        //
        // Within events with equal scheduled event times, sort by their composite order.
        if (b.scheduled_event_time.has_value()) {
            if (!a.scheduled_event_time.has_value())
                return true;

            auto a_time = a.scheduled_event_time.value();
            auto b_time = b.scheduled_event_time.value();
            if (a_time == b_time)
                return sort_events_by_composite_order(a, b);

            return a.scheduled_event_time.value() < b.scheduled_event_time.value();
        }

        if (a.scheduled_event_time.has_value())
            return false;

        return sort_events_by_composite_order(a, b);
    });

    // 7. Dispatch each of the events in events to dispatch at their corresponding target using the order established in
    //    the previous step.
    for (auto const& event : events_to_dispatch)
        event.target->dispatch_event(event.event);
}

// https://www.w3.org/TR/web-animations-1/#remove-replaced-animations
void Document::remove_replaced_animations()
{
    // When asked to remove replaced animations for a Document, doc, then for every animation, animation, that:
    // - has an associated animation effect whose effect target is a descendant of doc, and
    // - is replaceable, and
    // - has a replace state of active, and
    // - for which there exists for each target property of every animation effect associated with animation, an
    //   animation effect associated with a replaceable animation with a higher composite order than animation that
    //   includes the same target property

    Vector<GC::Ref<Animations::Animation>> replaceable_animations;
    for (auto const& timeline : m_associated_animation_timelines) {
        for (auto& animation : timeline->associated_animations()) {
            if (!animation.effect() || !animation.effect()->target() || &animation.effect()->target()->document() != this)
                continue;

            if (!animation.is_replaceable())
                continue;

            if (animation.replace_state() != Bindings::AnimationReplaceState::Active)
                continue;

            // Composite order is only defined for KeyframeEffects
            if (!animation.effect()->is_keyframe_effect())
                continue;

            replaceable_animations.append(animation);
        }
    }

    quick_sort(replaceable_animations, [](GC::Ref<Animations::Animation>& a, GC::Ref<Animations::Animation>& b) {
        VERIFY(a->effect()->is_keyframe_effect());
        VERIFY(b->effect()->is_keyframe_effect());
        auto& a_effect = *static_cast<Animations::KeyframeEffect*>(a->effect().ptr());
        auto& b_effect = *static_cast<Animations::KeyframeEffect*>(b->effect().ptr());
        return Animations::KeyframeEffect::composite_order(a_effect, b_effect) < 0;
    });

    // Lower value = higher priority
    HashMap<CSS::PropertyID, size_t> highest_property_composite_orders;
    for (int i = replaceable_animations.size() - 1; i >= 0; i--) {
        auto animation = replaceable_animations[i];
        bool has_any_highest_priority_property = false;

        for (auto const& property : animation->effect()->target_properties()) {
            if (!highest_property_composite_orders.contains(property)) {
                has_any_highest_priority_property = true;
                highest_property_composite_orders.set(property, i);
            }
        }

        if (!has_any_highest_priority_property) {
            // perform the following steps:

            // - Set animation’s replace state to removed.
            animation->set_replace_state(Bindings::AnimationReplaceState::Removed);

            // - Create an AnimationPlaybackEvent, removeEvent.
            // - Set removeEvent’s type attribute to remove.
            // - Set removeEvent’s currentTime attribute to the current time of animation.
            // - Set removeEvent’s timelineTime attribute to the current time of the timeline with which animation is
            //   associated.
            Bindings::AnimationPlaybackEventInit init;
            init.current_time = animation->current_time().has_value() ? Animations::NullableCSSNumberish { animation->current_time()->as_css_numberish(realm()) } : Animations::NullableCSSNumberish { Empty {} };
            init.timeline_time = animation->timeline()->current_time().has_value() ? Animations::NullableCSSNumberish { animation->timeline()->current_time()->as_css_numberish(realm()) } : Animations::NullableCSSNumberish { Empty {} };
            auto remove_event = Animations::AnimationPlaybackEvent::create(realm(), HTML::EventNames::remove, init);

            // - If animation has a document for timing, then append removeEvent to its document for timing's pending
            //   animation event queue along with its target, animation. For the scheduled event time, use the result of
            //   applying the procedure to convert timeline time to origin-relative time to the current time of the
            //   timeline with which animation is associated.
            if (auto document = animation->document_for_timing()) {
                PendingAnimationEvent pending_animation_event {
                    .event = remove_event,
                    .animation = animation,
                    .target = animation,
                    .scheduled_event_time = animation->timeline()->convert_a_timeline_time_to_an_origin_relative_time(animation->timeline()->current_time()),
                };
                document->append_pending_animation_event(pending_animation_event);
            }
            //   Otherwise, queue a task to dispatch removeEvent at animation. The task source for this task is the DOM
            //   manipulation task source.
            else {
                HTML::queue_global_task(HTML::Task::Source::DOMManipulation, realm().global_object(), GC::create_function(heap(), [animation, remove_event]() {
                    animation->dispatch_event(remove_event);
                }));
            }
        }
    }
}

WebIDL::ExceptionOr<Vector<GC::Ref<Animations::Animation>>> Document::get_animations()
{
    update_style();
    return calculate_get_animations(*this);
}

// https://html.spec.whatwg.org/multipage/dom.html#dom-document-nameditem-filter
static bool is_potentially_named_element(DOM::Element const& element)
{
    return is<HTML::HTMLEmbedElement>(element) || is<HTML::HTMLFormElement>(element) || is<HTML::HTMLIFrameElement>(element) || is<HTML::HTMLImageElement>(element) || is<HTML::HTMLObjectElement>(element);
}

// https://html.spec.whatwg.org/multipage/dom.html#dom-document-nameditem-filter
static bool is_potentially_named_element_by_id(DOM::Element const& element)
{
    return is<HTML::HTMLObjectElement>(element) || is<HTML::HTMLImageElement>(element);
}

static void insert_in_tree_order(Vector<GC::Ref<DOM::Element>>& elements, DOM::Element& element)
{
    for (auto& el : elements) {
        if (el == &element)
            return;
    }

    auto index = elements.find_first_index_if([&](auto& existing_element) {
        return existing_element->compare_document_position(element) & Node::DOCUMENT_POSITION_FOLLOWING;
    });
    if (index.has_value())
        elements.insert(index.value(), element);
    else
        elements.append(element);
}

void Document::element_id_changed(Badge<DOM::Element>, GC::Ref<DOM::Element> element, Optional<FlyString> old_id)
{
    for (auto* form_associated_element : m_form_associated_elements_with_form_attribute)
        form_associated_element->element_id_changed({});

    if (element->id().has_value())
        insert_in_tree_order(m_potentially_named_elements, element);
    else if (!element->name().has_value())
        (void)m_potentially_named_elements.remove_first_matching([element](auto& e) { return e == element; });

    auto new_id = element->id();
    if (old_id.has_value()) {
        element->document_or_shadow_root_element_by_id_map().remove(old_id.value(), element);
    }
    if (new_id.has_value()) {
        element->document_or_shadow_root_element_by_id_map().add(new_id.value(), element);
    }
}

void Document::element_with_id_was_added(Badge<DOM::Element>, GC::Ref<DOM::Element> element)
{
    for (auto* form_associated_element : m_form_associated_elements_with_form_attribute)
        form_associated_element->element_with_id_was_added_or_removed({});

    if (is_potentially_named_element_by_id(*element))
        insert_in_tree_order(m_potentially_named_elements, element);

    if (auto id = element->id(); id.has_value()) {
        element->document_or_shadow_root_element_by_id_map().add(id.value(), element);
    }
}

void Document::element_with_id_was_removed(Badge<DOM::Element>, GC::Ref<DOM::Element> element)
{
    for (auto* form_associated_element : m_form_associated_elements_with_form_attribute)
        form_associated_element->element_with_id_was_added_or_removed({});

    if (is_potentially_named_element_by_id(*element))
        (void)m_potentially_named_elements.remove_first_matching([element](auto& e) { return e == element; });

    if (auto id = element->id(); id.has_value()) {
        element->document_or_shadow_root_element_by_id_map().remove(id.value(), element);
    }
}

void Document::element_name_changed(Badge<DOM::Element>, GC::Ref<DOM::Element> element)
{
    if (element->name().has_value()) {
        insert_in_tree_order(m_potentially_named_elements, element);
    } else {
        if (is_potentially_named_element_by_id(element) && element->id().has_value())
            return;
        (void)m_potentially_named_elements.remove_first_matching([element](auto& e) { return e == element; });
    }
}

void Document::element_with_name_was_added(Badge<DOM::Element>, GC::Ref<DOM::Element> element)
{
    if (is_potentially_named_element(element))
        insert_in_tree_order(m_potentially_named_elements, element);
}

void Document::element_with_name_was_removed(Badge<DOM::Element>, GC::Ref<DOM::Element> element)
{
    if (is_potentially_named_element(element)) {
        if (is_potentially_named_element_by_id(element) && element->id().has_value())
            return;
        (void)m_potentially_named_elements.remove_first_matching([element](auto& e) { return e == element; });
    }
}

GC::Ptr<Element> Document::element_by_anchor_name(FlyString const& name, Node const& querying_node) const
{
    // https://drafts.csswg.org/css-shadow-1/#tree-scoped-name
    // If a tree-scoped name is global (such as @font-face names), then when a tree-scoped reference is dereferenced to
    // find it, first search only the tree-scoped names associated with the same root as the tree-scoped reference. If
    // no relevant tree-scoped name is found, and the root is a shadow root, then repeat this search in the root's
    // host's node tree (recursively).
    auto const* node = &querying_node;
    while (auto const* shadow_root = as_if<ShadowRoot>(node->root())) {
        if (auto element = shadow_root->anchor_name_map().element_by_name(name))
            return element;
        node = shadow_root->host();
        if (!node)
            return {};
    }
    return m_anchor_name_map.element_by_name(name);
}

void Document::add_form_associated_element_with_form_attribute(HTML::FormAssociatedElement& form_associated_element)
{
    m_form_associated_elements_with_form_attribute.append(&form_associated_element);
}

void Document::remove_form_associated_element_with_form_attribute(HTML::FormAssociatedElement& form_associated_element)
{
    m_form_associated_elements_with_form_attribute.remove_all_matching([&](auto* element) {
        return element == &form_associated_element;
    });
}

void Document::set_design_mode_enabled_state(bool design_mode_enabled)
{
    m_design_mode_enabled = design_mode_enabled;
    set_editable(design_mode_enabled);
    for_each_in_inclusive_subtree([](Node& node) {
        node.recompute_editable_subtree_flag();
        return TraversalDecision::Continue;
    });
}

// https://html.spec.whatwg.org/multipage/interaction.html#making-entire-documents-editable:-the-designmode-idl-attribute
String Document::design_mode() const
{
    // The designMode getter steps are to return "on" if this's design mode enabled is true; otherwise "off".
    return design_mode_enabled_state() ? "on"_string : "off"_string;
}

WebIDL::ExceptionOr<void> Document::set_design_mode(String const& design_mode)
{
    // 1. Let value be the given value, converted to ASCII lowercase.
    auto value = MUST(design_mode.to_lowercase());

    // 2. If value is "on" and this's design mode enabled is false, then:
    if (value == "on"sv && !m_design_mode_enabled) {
        // 1. Set this's design mode enabled to true.
        set_design_mode_enabled_state(true);
        // 2. Reset this's active range's start and end boundary points to be at the start of this.
        if (auto selection = get_selection()) {
            TRY(selection->collapse(this, 0));
            update_layout(UpdateLayoutReason::DocumentSetDesignMode);
        }
        // 3. Run the focusing steps for this's document element, if non-null.
        if (auto* document_element = this->document_element(); document_element)
            HTML::run_focusing_steps(document_element);
    }
    // 3. If value is "off", then set this's design mode enabled to false.
    else if (value == "off"sv) {
        set_design_mode_enabled_state(false);
    }
    return {};
}

static Element* retarget_from_ua_internal_shadow_root(Element& element)
{
    auto* result = &element;
    while (auto shadow_root = result->containing_shadow_root()) {
        if (!shadow_root->is_user_agent_internal())
            break;
        result = shadow_root->host();
    }
    return result;
}

// https://drafts.csswg.org/cssom-view/#dom-document-elementfrompoint
Element const* Document::element_from_point(double x, double y)
{
    // 1. If either argument is negative, x is greater than the viewport width excluding the size of a rendered scroll
    //    bar (if any), or y is greater than the viewport height excluding the size of a rendered scroll bar (if any), or
    //    there is no viewport associated with the document, return null and terminate these steps.
    auto viewport_rect = this->viewport_rect();
    CSSPixelPoint position { x, y };
    // FIXME: This should account for the size of the scroll bar.
    if (x < 0 || y < 0 || position.x() > viewport_rect.width() || position.y() > viewport_rect.height())
        return nullptr;

    // Ensure the layout tree exists prior to hit testing.
    update_layout(UpdateLayoutReason::DocumentElementFromPoint);

    // 2. If there is a box in the viewport that would be a target for hit testing at coordinates x,y, when applying the transforms
    //    that apply to the descendants of the viewport, return the associated element and terminate these steps.
    GC::Ptr<Element> hit_element;
    if (auto paintable_box = this->paintable_box()) {
        (void)paintable_box->hit_test(position, Painting::HitTestType::Exact, [&](Painting::HitTestResult result) {
            if (auto* element = as_if<Element>(result.dom_node())) {
                hit_element = element;
                return TraversalDecision::Break;
            }
            return TraversalDecision::Continue;
        });
    }
    if (hit_element) {
        // AD-HOC: If element is inside a UA internal shadow root, retarget to the host.
        return retarget_from_ua_internal_shadow_root(*hit_element);
    }

    // 3. If the document has a root element, return the root element and terminate these steps.
    if (auto const* root_element = document_element())
        return root_element;

    // 4. Return null.
    return nullptr;
}

// https://drafts.csswg.org/cssom-view/#dom-document-elementsfrompoint
GC::RootVector<GC::Ref<Element>> Document::elements_from_point(double x, double y)
{
    // 1. Let sequence be a new empty sequence.
    GC::RootVector<GC::Ref<Element>> sequence(heap());

    // 2. If either argument is negative, x is greater than the viewport width excluding the size of a rendered scroll bar (if any),
    //    or y is greater than the viewport height excluding the size of a rendered scroll bar (if any),
    //    or there is no viewport associated with the document, return sequence and terminate these steps.
    auto viewport_rect = this->viewport_rect();
    CSSPixelPoint position { x, y };
    // FIXME: This should account for the size of the scroll bar.
    if (x < 0 || y < 0 || position.x() > viewport_rect.width() || position.y() > viewport_rect.height())
        return sequence;

    // Ensure the layout tree exists prior to hit testing.
    update_layout(UpdateLayoutReason::DocumentElementsFromPoint);

    // 3. For each box in the viewport, in paint order, starting with the topmost box, that would be a target for
    //    hit testing at coordinates x,y even if nothing would be overlapping it, when applying the transforms that
    //    apply to the descendants of the viewport, append the associated element to sequence.
    if (auto paintable_box = this->paintable_box()) {
        (void)paintable_box->hit_test(position, Painting::HitTestType::Exact, [&](Painting::HitTestResult result) {
            if (auto* element = as_if<Element>(result.dom_node())) {
                // AD-HOC: If element is inside a UA internal shadow root, retarget to the host.
                element = retarget_from_ua_internal_shadow_root(*element);
                // AD-HOC: Avoid adding duplicates when multiple internal elements retarget to the same host.
                if (sequence.is_empty() || sequence.last() != element)
                    sequence.append(*element);
            }
            return TraversalDecision::Continue;
        });
    }

    // 4. If the document has a root element, and the last item in sequence is not the root element,
    //    append the root element to sequence.
    if (auto* root_element = document_element(); root_element && (sequence.is_empty() || (sequence.last() != root_element)))
        sequence.append(*root_element);

    // 5. Return sequence.
    return sequence;
}

// https://drafts.csswg.org/cssom-view/#dom-document-scrollingelement
GC::Ptr<Element const> Document::scrolling_element() const
{
    // 1. If the Document is in quirks mode, follow these substeps:
    if (in_quirks_mode()) {
        // 1. If the body element exists, and it is not potentially scrollable, return the body element and abort these steps.
        //    For this purpose, a value of overflow:clip on the the body element’s parent element must be treated as overflow:hidden.
        if (auto const* body_element = body(); body_element && !body_element->is_potentially_scrollable(Element::TreatOverflowClipOnBodyParentAsOverflowHidden::Yes))
            return body_element;

        // 2. Return null and abort these steps.
        return nullptr;
    }

    // 2. If there is a root element, return the root element and abort these steps.
    if (auto const* root_element = document_element(); root_element)
        return root_element;

    // 3. Return null.
    return nullptr;
}

// https://html.spec.whatwg.org/multipage/dom.html#exposed
static bool is_exposed(Element const& element)
{
    VERIFY(is<HTML::HTMLEmbedElement>(element) || is<HTML::HTMLObjectElement>(element));

    // FIXME: An embed or object element is said to be exposed if it has no exposed object ancestor, and,
    //        for object elements, is additionally either not showing its fallback content or has no object or embed descendants.
    return true;
}

// https://html.spec.whatwg.org/multipage/dom.html#dom-tree-accessors:supported-property-names
Vector<FlyString> Document::supported_property_names() const
{
    // The supported property names of a Document object document at any moment consist of the following,
    // in tree order according to the element that contributed them, ignoring later duplicates,
    // and with values from id attributes coming before values from name attributes when the same element contributes both:
    OrderedHashTable<FlyString> names;

    for (auto const& element : m_potentially_named_elements) {
        // - the value of the name content attribute for all exposed embed, form, iframe, img, and exposed object elements
        //   that have a non-empty name content attribute and are in a document tree with document as their root;
        if ((is<HTML::HTMLEmbedElement>(*element) && is_exposed(element))
            || is<HTML::HTMLFormElement>(*element)
            || is<HTML::HTMLIFrameElement>(*element)
            || is<HTML::HTMLImageElement>(*element)
            || (is<HTML::HTMLObjectElement>(*element) && is_exposed(element))) {
            if (auto name = element->name(); name.has_value()) {
                names.set(name.value());
            }
        }

        // - the value of the id content attribute for all exposed object elements that have a non-empty id content attribute
        //   and are in a document tree with document as their root; and
        if (is<HTML::HTMLObjectElement>(*element) && is_exposed(element)) {
            if (auto id = element->id(); id.has_value()) {
                names.set(id.value());
            }
        }

        // - the value of the id content attribute for all img elements that have both a non-empty id content attribute
        //   and a non-empty name content attribute, and are in a document tree with document as their root.
        if (is<HTML::HTMLImageElement>(*element)) {
            if (auto id = element->id(); id.has_value() && element->name().has_value()) {
                names.set(id.value());
            }
        }
    }

    return names.values();
}

static bool is_named_element_with_name(Element const& element, FlyString const& name)
{
    // Named elements with the name name, for the purposes of the above algorithm, are those that are either:

    // - Exposed embed, form, iframe, img, or exposed object elements that have a name content attribute whose value
    //   is name, or
    if ((is<HTML::HTMLEmbedElement>(element) && is_exposed(element))
        || is<HTML::HTMLFormElement>(element)
        || is<HTML::HTMLIFrameElement>(element)
        || is<HTML::HTMLImageElement>(element)
        || (is<HTML::HTMLObjectElement>(element) && is_exposed(element))) {
        if (element.name() == name)
            return true;
    }

    // - Exposed object elements that have an id content attribute whose value is name, or
    if (is<HTML::HTMLObjectElement>(element) && is_exposed(element)) {
        if (element.id() == name)
            return true;
    }

    // - img elements that have an id content attribute whose value is name, and that have a non-empty name content
    //   attribute present also.
    if (is<HTML::HTMLImageElement>(element)) {
        if (element.id() == name && element.name().has_value())
            return true;
    }

    return false;
}

static Vector<GC::Ref<DOM::Element>> named_elements_with_name(Document const& document, FlyString const& name)
{
    Vector<GC::Ref<DOM::Element>> named_elements;

    for (auto const& element : document.potentially_named_elements()) {
        if (is_named_element_with_name(element, name))
            named_elements.append(element);
    }

    return named_elements;
}

// https://html.spec.whatwg.org/multipage/dom.html#dom-document-nameditem
JS::Value Document::named_item_value(FlyString const& name) const
{
    // 1. Let elements be the list of named elements with the name name that are in a document tree with the Document as their root.
    // NOTE: There will be at least one such element, since the algorithm would otherwise not have been invoked by Web IDL.
    auto elements = named_elements_with_name(*this, name);

    // 2. If elements has only one element, and that element is an iframe element, and that iframe element's content navigable is not null,
    //    then return the active WindowProxy of the element's content navigable.
    if (elements.size() == 1 && is<HTML::HTMLIFrameElement>(*elements.first())) {
        auto& iframe_element = static_cast<HTML::HTMLIFrameElement&>(*elements.first());
        if (iframe_element.content_navigable() != nullptr)
            return iframe_element.content_navigable()->active_window_proxy();
    }

    // 3. Otherwise, if elements has only one element, return that element.
    if (elements.size() == 1)
        return elements.first();

    // 4. Otherwise return an HTMLCollection rooted at the Document node, whose filter matches only named elements with the name name.
    return HTMLCollection::create(*const_cast<Document*>(this), HTMLCollection::Scope::Descendants, [name](auto& element) {
        return is_named_element_with_name(element, name);
    });
}

// https://drafts.csswg.org/resize-observer-1/#calculate-depth-for-node
static size_t calculate_depth_for_node(Node const& node)
{
    // 1. Let p be the parent-traversal path from node to a root Element of this element’s flattened DOM tree.
    // 2. Return number of nodes in p.

    size_t depth = 0;
    for (auto const* current = &node; current; current = current->parent())
        ++depth;
    return depth;
}

// https://drafts.csswg.org/resize-observer-1/#gather-active-observations-h
void Document::gather_active_observations_at_depth(size_t depth)
{
    // 1. Let depth be the depth passed in.

    // 2. For each observer in [[resizeObservers]] run these steps:
    for (auto& observer : m_resize_observers) {
        // 1. Clear observer’s [[activeTargets]], and [[skippedTargets]].
        observer.active_targets().clear();
        observer.skipped_targets().clear();

        // 2. For each observation in observer.[[observationTargets]] run this step:
        for (auto& observation : observer.observation_targets()) {
            // 1. If observation.isActive() is true
            if (observation->is_active()) {
                // 1. Let targetDepth be result of calculate depth for node for observation.target.
                auto target_depth = calculate_depth_for_node(*observation->target());

                // 2. If targetDepth is greater than depth then add observation to [[activeTargets]].
                if (target_depth > depth) {
                    observer.active_targets().append(observation);
                } else {
                    // 3. Else add observation to [[skippedTargets]].
                    observer.skipped_targets().append(observation);
                }
            }
        }
    }
}

// https://drafts.csswg.org/resize-observer-1/#broadcast-active-resize-observations
size_t Document::broadcast_active_resize_observations()
{
    // 1. Let shallowestTargetDepth be ∞
    auto shallowest_target_depth = NumericLimits<size_t>::max();

    // 2. For each observer in document.[[resizeObservers]] run these steps:

    // NOTE: We make a copy of the resize observers list to avoid modifying it while iterating.
    auto resize_observers = GC::RootVector<GC::Ref<ResizeObserver::ResizeObserver>> { heap() };
    for (auto& observer : m_resize_observers)
        resize_observers.append(observer);

    for (auto const& observer : resize_observers) {
        // 1. If observer.[[activeTargets]] slot is empty, continue.
        if (observer->active_targets().is_empty()) {
            continue;
        }

        // 2. Let entries be an empty list of ResizeObserverEntryies.
        GC::RootVector<GC::Ref<ResizeObserver::ResizeObserverEntry>> entries(heap());

        // 3. For each observation in [[activeTargets]] perform these steps:
        for (auto const& observation : observer->active_targets()) {
            // 1. Let entry be the result of running create and populate a ResizeObserverEntry given observation.target.
            auto entry = ResizeObserver::ResizeObserverEntry::create_and_populate(realm(), *observation->target()).release_value_but_fixme_should_propagate_errors();

            // 2. Add entry to entries.
            entries.append(entry);

            // 3. Set observation.lastReportedSizes to matching entry sizes.
            switch (observation->observed_box()) {
            case Bindings::ResizeObserverBoxOptions::BorderBox:
                // Matching sizes are entry.borderBoxSize if observation.observedBox is "border-box"
                observation->last_reported_sizes() = entry->border_box_size();
                break;
            case Bindings::ResizeObserverBoxOptions::ContentBox:
                // Matching sizes are entry.contentBoxSize if observation.observedBox is "content-box"
                observation->last_reported_sizes() = entry->content_box_size();
                break;
            case Bindings::ResizeObserverBoxOptions::DevicePixelContentBox:
                // Matching sizes are entry.devicePixelContentBoxSize if observation.observedBox is "device-pixel-content-box"
                observation->last_reported_sizes() = entry->device_pixel_content_box_size();
                break;
            default:
                VERIFY_NOT_REACHED();
            }

            // 4. Set targetDepth to the result of calculate depth for node for observation.target.
            auto target_depth = calculate_depth_for_node(*observation->target());

            // 5. Set shallowestTargetDepth to targetDepth if targetDepth < shallowestTargetDepth
            if (target_depth < shallowest_target_depth)
                shallowest_target_depth = target_depth;
        }

        // 4. Invoke observer.[[callback]] with entries.
        observer->invoke_callback(entries);

        // 5. Clear observer.[[activeTargets]].
        observer->active_targets().clear();
    }

    return shallowest_target_depth;
}

// https://drafts.csswg.org/resize-observer-1/#has-active-observations-h
bool Document::has_active_resize_observations()
{
    // 1. For each observer in [[resizeObservers]] run this step:
    for (auto& observer : m_resize_observers) {
        // 1. If observer.[[activeTargets]] is not empty, return true.
        if (!observer.active_targets().is_empty())
            return true;
    }

    // 2. Return false.
    return false;
}

// https://drafts.csswg.org/resize-observer-1/#has-skipped-observations-h
bool Document::has_skipped_resize_observations()
{
    // 1. For each observer in [[resizeObservers]] run this step:
    for (auto& observer : m_resize_observers) {
        // 1. If observer.[[skippedTargets]] is not empty, return true.
        if (!observer.skipped_targets().is_empty())
            return true;
    }

    // 2. Return false.
    return false;
}

GC::Ref<WebIDL::ObservableArray> Document::adopted_style_sheets() const
{
    if (!m_adopted_style_sheets)
        m_adopted_style_sheets = create_adopted_style_sheets_list(const_cast<Document&>(*this));
    return *m_adopted_style_sheets;
}

WebIDL::ExceptionOr<void> Document::set_adopted_style_sheets(JS::Value new_value)
{
    if (!m_adopted_style_sheets)
        m_adopted_style_sheets = create_adopted_style_sheets_list(const_cast<Document&>(*this));

    m_adopted_style_sheets->clear();
    auto iterator_record = TRY(get_iterator(vm(), new_value, JS::IteratorHint::Sync));
    while (true) {
        auto next = TRY(iterator_step_value(vm(), iterator_record));
        if (!next.has_value())
            break;
        TRY(m_adopted_style_sheets->append(*next));
    }

    return {};
}

void Document::for_each_active_css_style_sheet(Function<void(CSS::CSSStyleSheet&)> const& callback) const
{
    if (m_style_sheets) {
        for (auto& style_sheet : m_style_sheets->sheets()) {
            if (!(style_sheet->is_alternate() && style_sheet->disabled()))
                callback(*style_sheet);
        }
    }

    if (m_adopted_style_sheets) {
        m_adopted_style_sheets->for_each<CSS::CSSStyleSheet>([&](auto& style_sheet) {
            if (!style_sheet.disabled())
                callback(style_sheet);
        });
    }

    if (m_dynamic_view_transition_style_sheet) {
        callback(*m_dynamic_view_transition_style_sheet);
    }
}

double Document::ensure_element_shared_css_random_base_value(CSS::RandomCachingKey const& random_caching_key)
{
    return m_element_shared_css_random_base_value_cache.ensure(random_caching_key, []() {
        static XorShift128PlusRNG random_number_generator;
        return random_number_generator.get();
    });
}

static Optional<CSS::CSSStyleSheet&> find_style_sheet_with_url(String const& url, CSS::CSSStyleSheet& style_sheet)
{
    if (style_sheet.href() == url)
        return style_sheet;

    for (auto& import_rule : style_sheet.import_rules()) {
        if (import_rule->loaded_style_sheet()) {
            if (auto match = find_style_sheet_with_url(url, *import_rule->loaded_style_sheet()); match.has_value())
                return match;
        }
    }

    return {};
}

Optional<String> Document::get_style_sheet_source(CSS::StyleSheetIdentifier const& identifier) const
{
    switch (identifier.type) {
    case CSS::StyleSheetIdentifier::Type::StyleElement:
        if (identifier.dom_element_unique_id.has_value()) {
            if (auto* node = Node::from_unique_id(*identifier.dom_element_unique_id)) {
                if (node->is_html_style_element()) {
                    if (auto* sheet = as<HTML::HTMLStyleElement>(*node).sheet())
                        return sheet->source_text({});
                }
                if (node->is_svg_style_element()) {
                    if (auto* sheet = as<SVG::SVGStyleElement>(*node).sheet())
                        return sheet->source_text({});
                }
            }
        }
        return {};
    case CSS::StyleSheetIdentifier::Type::LinkElement:
    case CSS::StyleSheetIdentifier::Type::ImportRule: {
        if (!identifier.url.has_value()) {
            dbgln("Attempting to get link or imported style-sheet with no url; giving up");
            return {};
        }

        if (m_style_sheets) {
            for (auto& style_sheet : m_style_sheets->sheets()) {
                if (auto match = find_style_sheet_with_url(identifier.url.value(), style_sheet); match.has_value())
                    return match->source_text({});
            }
        }

        if (m_adopted_style_sheets) {
            Optional<String> result;
            m_adopted_style_sheets->for_each<CSS::CSSStyleSheet>([&](auto& style_sheet) {
                if (result.has_value())
                    return;

                if (auto match = find_style_sheet_with_url(identifier.url.value(), style_sheet); match.has_value())
                    result = match->source_text({});
            });
            return result;
        }

        return {};
    }
    case CSS::StyleSheetIdentifier::Type::UserAgent:
        return CSS::StyleComputer::user_agent_style_sheet_source(identifier.url.value());
    case CSS::StyleSheetIdentifier::Type::UserStyle:
        return page().user_style();
    }

    return {};
}

void Document::register_shadow_root(Badge<DOM::ShadowRoot>, DOM::ShadowRoot& shadow_root)
{
    m_shadow_roots.append(shadow_root);
}

void Document::unregister_shadow_root(Badge<DOM::ShadowRoot>, DOM::ShadowRoot& shadow_root)
{
    m_shadow_roots.remove(shadow_root);
}

// https://drafts.csswg.org/css-position-4/#add-an-element-to-the-top-layer
void Document::add_an_element_to_the_top_layer(GC::Ref<Element> element)
{
    // 1. Let doc be el’s node document.

    // 2. If el is already contained in doc’s top layer:
    if (m_top_layer_elements.contains(element)) {
        // Assert: el is also in doc’s pending top layer removals. (Otherwise, this is a spec error.)
        VERIFY(m_top_layer_pending_removals.contains(element));

        // Remove el from both doc’s top layer and pending top layer removals.
        m_top_layer_elements.remove(element);
        m_top_layer_pending_removals.remove(element);
    }

    // 3. Append el to doc’s top layer.
    m_top_layer_elements.set(element);
    element->set_in_top_layer(true);

    // FIXME: 4. At the UA !important cascade origin, add a rule targeting el containing an overlay: auto declaration.
    element->set_rendered_in_top_layer(true);
    element->set_needs_style_update(true);
    invalidate_layout_tree(InvalidateLayoutTreeReason::DocumentAddAnElementToTheTopLayer);
}

// https://drafts.csswg.org/css-position-4/#request-an-element-to-be-removed-from-the-top-layer
void Document::request_an_element_to_be_remove_from_the_top_layer(GC::Ref<Element> element)
{
    // 1. Let doc be el’s node document.

    // 2. If el is not contained doc’s top layer, or el is already contained in doc’s pending top layer removals, return.
    if (!m_top_layer_elements.contains(element) || m_top_layer_pending_removals.contains(element))
        return;

    // FIXME: 3. Remove the UA !important overlay: auto rule targeting el.
    element->set_rendered_in_top_layer(false);
    element->set_needs_style_update(true);
    invalidate_layout_tree(InvalidateLayoutTreeReason::DocumentRequestAnElementToBeRemovedFromTheTopLayer);

    // 4. Append el to doc’s pending top layer removals.
    m_top_layer_pending_removals.set(element);
    element->set_in_top_layer(false);
}

// https://drafts.csswg.org/css-position-4/#remove-an-element-from-the-top-layer-immediately
void Document::remove_an_element_from_the_top_layer_immediately(GC::Ref<Element> element)
{
    // 1. Let doc be el’s node document.

    // 2. Remove el from doc’s top layer and pending top layer removals.
    m_top_layer_elements.remove(element);
    element->set_in_top_layer(false);

    // FIXME: 3. Remove the UA !important overlay: auto rule targeting el, if it exists.
    element->set_rendered_in_top_layer(false);
    element->set_needs_style_update(true);

    invalidate_layout_tree(InvalidateLayoutTreeReason::DocumentImmediatelyRemoveElementFromTheTopLayer);
}

// https://drafts.csswg.org/css-position-4/#process-top-layer-removals
void Document::process_top_layer_removals()
{
    // 1. For each element el in doc’s pending top layer removals: if el’s computed value of overlay is none, or el is
    //    not rendered, remove el from doc’s top layer and pending top layer removals.
    GC::RootVector<GC::Ref<Element>> elements_to_remove(heap());
    // NB: Called during top layer processing.
    for (auto& element : m_top_layer_pending_removals) {
        // FIXME: Implement overlay property
        if (!element->unsafe_paintable()) {
            elements_to_remove.append(element);
        }
    }

    for (auto& element : elements_to_remove) {
        m_top_layer_elements.remove(element);
        m_top_layer_pending_removals.remove(element);
    }

    if (!elements_to_remove.is_empty())
        invalidate_layout_tree(InvalidateLayoutTreeReason::DocumentPendingTopLayerRemovalsProcessed);
}

// https://html.spec.whatwg.org/multipage/popover.html#topmost-auto-popover
GC::Ptr<HTML::HTMLElement> Document::topmost_auto_or_hint_popover()
{
    // To find the topmost auto or hint popover given a Document document, perform the following steps. They return an HTML element or null.

    // 1. If document's showing hint popover list is not empty, then return document's showing hint popover list's last element.
    if (!m_showing_hint_popover_list.is_empty())
        return m_showing_hint_popover_list.last();

    // 2. If document's showing auto popover list is not empty, then return document's showing auto popover list's last element.
    if (!m_showing_auto_popover_list.is_empty())
        return m_showing_auto_popover_list.last();

    // 3. Return null.
    return {};
}

void Document::set_needs_to_refresh_scroll_state(bool b)
{
    // NB: Propagating scroll state invalidation.
    if (auto paintable = this->unsafe_paintable())
        paintable->set_needs_to_refresh_scroll_state(b);
}

Vector<GC::Root<Range>> Document::find_matching_text(String const& query, CaseSensitivity case_sensitivity)
{
    // Ensure the layout tree exists before searching for text matches.
    update_layout(UpdateLayoutReason::DocumentFindMatchingText);

    if (!layout_node())
        return {};

    auto const& text_blocks = layout_node()->text_blocks();
    if (text_blocks.is_empty())
        return {};

    auto utf16_query = Utf16String::from_utf8(query);

    Vector<GC::Root<Range>> matches;
    for (auto const& text_block : text_blocks) {
        size_t offset = 0;
        size_t i = 0;
        Utf16View text_view { text_block.text };
        auto* match_start_position = text_block.positions.data();
        while (true) {
            auto match_index = case_sensitivity == CaseSensitivity::CaseInsensitive
                ? text_view.find_code_unit_offset_ignoring_case(utf16_query, offset)
                : text_view.find_code_unit_offset(utf16_query, offset);
            if (!match_index.has_value())
                break;

            for (; i < text_block.positions.size() - 1 && match_index.value() > text_block.positions[i + 1].start_offset; ++i)
                match_start_position = &text_block.positions[i + 1];

            auto start_position = match_index.value() - match_start_position->start_offset;
            auto& start_dom_node = match_start_position->dom_node;

            auto* match_end_position = match_start_position;
            for (; i < text_block.positions.size() - 1 && (match_index.value() + utf16_query.length_in_code_units() > text_block.positions[i + 1].start_offset); ++i)
                match_end_position = &text_block.positions[i + 1];

            auto& end_dom_node = match_end_position->dom_node;
            auto end_position = match_index.value() + utf16_query.length_in_code_units() - match_end_position->start_offset;

            matches.append(Range::create(start_dom_node, start_position, end_dom_node, end_position));
            match_start_position = match_end_position;
            offset = match_index.value() + utf16_query.length_in_code_units() + 1;
            if (offset >= text_view.length_in_code_units())
                break;
        }
    }

    return matches;
}

// https://dom.spec.whatwg.org/#document-allow-declarative-shadow-roots
bool Document::allow_declarative_shadow_roots() const
{
    return m_allow_declarative_shadow_roots;
}

bool Document::is_render_blocking_element(GC::Ref<Element> element) const
{
    return m_render_blocking_elements.contains(element);
}

// https://html.spec.whatwg.org/multipage/dom.html#render-blocked
bool Document::is_render_blocked() const
{
    // A Document document is render-blocked if both of the following are true:
    // - document's render-blocking element set is non-empty, or document allows adding render-blocking elements.
    // - The current high resolution time given document's relevant global object has not exceeded an implementation-defined timeout value.

    // NOTE: This timeout is implementation-defined.
    //       Other browsers are willing to wait longer, but let's start with 30 seconds.
    static constexpr auto max_time_to_block_rendering_in_ms = 30000.0;

    auto now = HighResolutionTime::current_high_resolution_time(relevant_global_object(*this));
    if (now > max_time_to_block_rendering_in_ms)
        return false;

    // AD-HOC: Consider pending CSS @import rules as render-blocking
    if (!m_pending_css_import_rules.is_empty())
        return true;

    return !m_render_blocking_elements.is_empty() || allows_adding_render_blocking_elements();
}

// https://html.spec.whatwg.org/multipage/dom.html#allows-adding-render-blocking-elements
bool Document::allows_adding_render_blocking_elements() const
{
    // A document allows adding render-blocking elements if document's content type is "text/html" and the body element of document is null.
    return content_type() == "text/html" && !body();
}

void Document::add_render_blocking_element(GC::Ref<Element> element)
{
    m_render_blocking_elements.set(element);
}

void Document::remove_render_blocking_element(GC::Ref<Element> element)
{
    m_render_blocking_elements.remove(element);
    if (m_render_blocking_elements.is_empty())
        page().client().request_frame();
}

// https://fullscreen.spec.whatwg.org/#run-the-fullscreen-steps
void Document::run_fullscreen_steps()
{
    // 1. Let pendingEvents be document’s list of pending fullscreen events.
    auto pending_events = GC::ConservativeVector<PendingFullscreenEvent> { vm().heap() };
    pending_events.extend(m_pending_fullscreen_events);

    // 2. Empty document’s list of pending fullscreen events.
    m_pending_fullscreen_events.clear();

    // 3. For each (type, element) in pendingEvents:
    for (auto const& [type, element] : pending_events) {
        // 1. Let target be element if element is connected and its node document is document, and otherwise let target be document.
        GC::Ref<Node> target { *this };
        if (element->is_connected() && &element->document() == this)
            target = element;

        // 2. Fire an event named type, with its bubbles and composed attributes set to true, at target.
        switch (type) {
        case PendingFullscreenEvent::Type::Change:
            target->dispatch_event(Event::create(realm(), HTML::EventNames::fullscreenchange, Bindings::EventInit { .bubbles = true, .composed = true }));
            break;
        case PendingFullscreenEvent::Type::Error:
            target->dispatch_event(Event::create(realm(), HTML::EventNames::fullscreenerror, Bindings::EventInit { .bubbles = true, .composed = true }));
            break;
        }
    }
}

void Document::append_pending_fullscreen_change(PendingFullscreenEvent::Type type, GC::Ref<Element> element)
{
    m_pending_fullscreen_events.append(PendingFullscreenEvent { type, element });
    page().client().request_frame();
}

// https://fullscreen.spec.whatwg.org/#fullscreen-an-element
void Document::fullscreen_element_within_doc(GC::Ref<Element> element)
{
    // FIXME: Spec issue:  Finding topmost popover ancestor algorithm takes different parameters than those described
    //        by the fullscreen spec. Since the new algorithm takes 4 parameters, with the new "popover list", we must
    //        also account for the auto popover list.
    //        See: https://github.com/whatwg/fullscreen/issues/245
    auto const get_hide_until = [&](auto const& popover_list) {
        return HTML::HTMLElement::topmost_popover_ancestor(element, popover_list, nullptr, HTML::IsPopover::No);
    };

    // 1. Let hideUntil be the result of running topmost popover ancestor given element, null, and false.
    auto hide_until = get_hide_until(showing_hint_popover_list());

    if (hide_until == nullptr)
        hide_until = get_hide_until(showing_auto_popover_list());

    Variant<GC::Ptr<HTML::HTMLElement>, GC::Ptr<Document>> hide_until_argument { hide_until };

    // 2. If hideUntil is null, then set hideUntil to element’s node document.
    if (hide_until == nullptr)
        hide_until_argument = GC::Ptr { element->document() };

    // 3. Run hide all popovers until given hideUntil, false, and true.
    HTML::HTMLElement::hide_all_popovers_until(hide_until_argument, HTML::FocusPreviousElement::No, HTML::FireEvents::Yes);

    // 4. Set element’s fullscreen flag.
    element->set_fullscreen_flag(true);

    // 5. Remove from the top layer immediately given element.
    remove_an_element_from_the_top_layer_immediately(element);

    // 6. Add to the top layer given element.
    add_an_element_to_the_top_layer(element);
    element->invalidate_style(StyleInvalidationReason::Fullscreen);
}

// https://fullscreen.spec.whatwg.org/#fullscreen-element
GC::Ptr<Element> Document::fullscreen_element() const
{
    // All documents have an associated fullscreen element. The fullscreen element is the topmost element in the
    // document’s top layer whose fullscreen flag is set, if any, and null otherwise.
    for (auto const& el : top_layer_elements().in_reverse()) {
        if (el->is_fullscreen_element())
            return el;
    }
    return nullptr;
}

// https://fullscreen.spec.whatwg.org/#dom-document-fullscreenelement
GC::Ptr<Element> Document::fullscreen_element_for_bindings() const
{
    auto fullscreen_element = this->fullscreen_element();
    if (!fullscreen_element)
        return nullptr;

    // 1. If this is a shadow root and its host is not connected, then return null.
    // NB: We're not a shadow root. See ShadowRoot::fullscreen_element_for_bindings().

    // 2. Let candidate be the result of retargeting fullscreen element against this.
    auto* candidate = retarget(fullscreen_element, const_cast<Document*>(this));
    if (!candidate)
        return nullptr;

    // 3. If candidate and this are in the same tree, then return candidate.
    if (auto* retargeted_element = as<Element>(candidate); retargeted_element && &retargeted_element->root() == &root())
        return retargeted_element;

    // 4. Return null.
    return nullptr;
}

// https://fullscreen.spec.whatwg.org/#dom-document-fullscreen
bool Document::fullscreen() const
{
    // The fullscreen getter steps are to return false if this's fullscreen element is null, and true otherwise.
    return fullscreen_element() != nullptr;
}

// https://fullscreen.spec.whatwg.org/#dom-document-fullscreenenabled
bool Document::fullscreen_enabled() const
{
    // FIXME: Implement check policy check and "is supported" check.
    return is_allowed_to_use_feature(PolicyControlledFeature::Fullscreen);
}

// https://fullscreen.spec.whatwg.org/#fully-exit-fullscreen
void Document::fully_exit_fullscreen()
{
    // 1. If document’s fullscreen element is null, terminate these steps.
    GC::Ptr<Element> fullscreened_element = fullscreen_element();
    if (!fullscreened_element)
        return;

    // 2. Unfullscreen elements whose fullscreen flag is set, within document’s top layer, except for document’s fullscreen element.
    GC::RootVector<GC::Ref<Element>, 8> fullscreen_elements { heap() };
    for (auto const& element : top_layer_elements()) {
        if (element->is_fullscreen_element() && element != fullscreened_element)
            fullscreen_elements.append(element);
    }

    for (auto const& element : fullscreen_elements)
        unfullscreen_element(element);

    // 3. Exit fullscreen document.
    HTML::TemporaryExecutionContext context { realm(), HTML::TemporaryExecutionContext::CallbacksEnabled::Yes };
    exit_fullscreen();
}

// https://fullscreen.spec.whatwg.org/#exit-fullscreen
GC::Ref<WebIDL::Promise> Document::exit_fullscreen()
{
    auto& realm = this->realm();

    // 1. Let promise be a new promise.
    auto promise = WebIDL::create_promise(realm);

    // 2. If doc is not fully active or doc’s fullscreen element is null, then reject promise with a TypeError exception
    //    and return promise.
    if (!is_fully_active() || !fullscreen_element()) {
        WebIDL::reject_promise(realm, promise, JS::TypeError::create(realm, "Document not fully active or no fullscreen element."sv));
        return promise;
    }

    // 3. Let resize be false.
    bool resize = false;

    // 4. Let docs be the result of collecting documents to unfullscreen given doc.
    auto docs = collect_documents_to_unfullscreen();

    // 5. Let topLevelDoc be doc’s node navigable’s top-level traversable’s active document.
    auto top_level_doc = navigable()->top_level_traversable()->active_document();

    // 6. If topLevelDoc is in docs, and it is a simple fullscreen document, then set doc to topLevelDoc and resize to true.
    GC::Ref<Document> doc { *this };
    if (top_level_doc && top_level_doc->is_simple_fullscreen_document() && docs->elements().contains_slow(GC::Ref { *top_level_doc })) {
        doc = *top_level_doc;
        resize = true;
    }

    // 7. If doc’s fullscreen element is not connected:
    if (auto fullscreen_element = doc->fullscreen_element(); !fullscreen_element->is_connected()) {
        // 1. Append (fullscreenchange, doc’s fullscreen element) to doc’s list of pending fullscreen events.
        doc->append_pending_fullscreen_change(PendingFullscreenEvent::Type::Change, *fullscreen_element);

        // 2. Unfullscreen doc’s fullscreen element.
        doc->unfullscreen_element(*fullscreen_element);
    }

    // 8. Return promise, and run the remaining steps in parallel.
    page().enqueue_fullscreen_exit(doc, resize, promise);

    return promise;
}

// https://fullscreen.spec.whatwg.org/#unfullscreen-a-document
void Document::unfullscreen()
{
    // To unfullscreen a document, unfullscreen all elements, within document’s top layer, whose fullscreen flag is set.
    // NB: This has to be a copy of the list of those elements, since unfullscreen an element immediately removes it
    //     from the top layer, invalidating iterators.
    auto fullscreen_elements = heap().allocate<GC::HeapVector<GC::Ref<Element>>>();
    for (auto el : top_layer_elements()) {
        if (el->is_fullscreen_element())
            fullscreen_elements->elements().append(el);
    }

    for (auto el : fullscreen_elements->elements())
        unfullscreen_element(el);
}

// https://fullscreen.spec.whatwg.org/#simple-fullscreen-document
bool Document::is_simple_fullscreen_document() const
{
    // A document is said to be a simple fullscreen document if there is exactly one element in its top layer that has its fullscreen flag set.
    u32 total = 0;
    for (auto const& element : top_layer_elements()) {
        if (element->is_fullscreen_element())
            ++total;

        if (total > 1)
            return false;
    }
    return total == 1;
}

// https://fullscreen.spec.whatwg.org/#collect-documents-to-unfullscreen
GC::Ref<GC::HeapVector<GC::Ref<Document>>> Document::collect_documents_to_unfullscreen()
{
    // 1. Let docs be an ordered set consisting of doc.
    auto docs = heap().allocate<GC::HeapVector<GC::Ref<Document>>>();
    docs->elements().append(*this);

    // 2. While true:
    while (true) {
        // 1. Let lastDoc be docs’s last document.
        auto last_doc = docs->elements().last();

        // 2. Assert: lastDoc’s fullscreen element is not null.
        VERIFY(last_doc->fullscreen_element());

        // 3. If lastDoc is not a simple fullscreen document, break.
        if (!last_doc->is_simple_fullscreen_document())
            break;

        // 4. Let container be lastDoc’s node navigable’s container.
        // Note on spec: It doesn't first check if `node navigable` is null.
        auto container = last_doc->navigable() ? last_doc->navigable()->container() : nullptr;

        // 5. If container is null, then break.
        if (!container)
            break;

        // 6. If container’s iframe fullscreen flag is set, break.
        if (auto* iframe_element = as_if<HTML::HTMLIFrameElement>(container.ptr()); iframe_element && iframe_element->iframe_fullscreen_flag())
            break;

        // 7. Append container’s node document to docs.
        docs->elements().append(container->document());
    }

    // 3. Return docs.
    return docs;
}

// https://fullscreen.spec.whatwg.org/#unfullscreen-an-element
void Document::unfullscreen_element(GC::Ref<Element> element)
{
    // To unfullscreen an element, unset element’s fullscreen flag and iframe fullscreen flag (if any), and remove from
    // the top layer immediately given element.
    element->set_fullscreen_flag(false);
    if (auto* iframe_element = as_if<HTML::HTMLIFrameElement>(element.ptr()))
        iframe_element->set_iframe_fullscreen_flag(false);

    remove_an_element_from_the_top_layer_immediately(element);
}

// https://dom.spec.whatwg.org/#document-allow-declarative-shadow-roots
void Document::set_allow_declarative_shadow_roots(bool allow)
{
    m_allow_declarative_shadow_roots = allow;
}

// https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#parse-html-from-a-string
void Document::parse_html_from_a_string(StringView html)
{
    // 1. Set document's type to "html".
    set_document_type(DOM::Document::Type::HTML);

    // 2. Create an HTML parser parser, associated with document.
    // 3. Place html into the input stream for parser. The encoding confidence is irrelevant.
    // FIXME: We don't have the concept of encoding confidence yet.
    auto scripting_mode = is_scripting_enabled() ? HTML::ParserScriptingMode::Normal : HTML::ParserScriptingMode::Disabled;
    auto parser = HTML::HTMLParser::create(*this, html, scripting_mode, "UTF-8"sv);

    // 4. Start parser and let it run until it has consumed all the characters just inserted into the input stream.
    parser->run(as<HTML::Window>(HTML::relevant_global_object(*this)).associated_document().url());
}

// https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#dom-parsehtmlunsafe
WebIDL::ExceptionOr<GC::Root<DOM::Document>> Document::parse_html_unsafe(JS::VM& vm, TrustedTypes::TrustedHTMLOrString const& html)
{
    auto& realm = *vm.current_realm();

    // FIXME: update description once https://github.com/whatwg/html/issues/11778 gets solved
    // 1. Let compliantHTML to the result of invoking the Get Trusted Type compliant string algorithm with
    //    TrustedHTML, this's relevant global object, html, "Document parseHTMLUnsafe", and "script".
    auto const compliant_html = TRY(TrustedTypes::get_trusted_type_compliant_string(
        TrustedTypes::TrustedTypeName::TrustedHTML,
        HTML::current_global_object(),
        html,
        TrustedTypes::InjectionSink::Document_parseHTMLUnsafe,
        TrustedTypes::Script.to_string()));

    // 2. Let document be a new Document, whose content type is "text/html".
    auto document = Document::create_for_fragment_parsing(realm);
    document->set_content_type("text/html"_string);

    // 3. Set document's allow declarative shadow roots to true.
    document->set_allow_declarative_shadow_roots(true);

    // 4. Parse HTML from a string given document and compliantHTML.
    document->parse_html_from_a_string(compliant_html.to_utf8_but_should_be_ported_to_utf16());

    // AD-HOC: Setting the origin to match that of the associated document matches the behavior of existing browsers.
    auto& associated_document = as<HTML::Window>(realm.global_object()).associated_document();
    document->set_origin(associated_document.origin());

    // 5. Return document.
    return document;
}

InputEventsTarget* Document::active_input_events_target(Node const* for_node)
{
    auto focused_area = this->focused_area();
    if (!focused_area)
        return nullptr;

    if (focused_area->is_editable_or_editing_host()) {
        if (!for_node || m_editing_host_manager->is_within_active_contenteditable(*for_node))
            return m_editing_host_manager;
    }
    if (auto* form_text_element = as_if<HTML::FormAssociatedTextControlElement>(*focused_area)) {
        if (!for_node || for_node->find_in_shadow_including_ancestry([&](Node const& it) { return &it == focused_area; }))
            return form_text_element;
    }
    return nullptr;
}

GC::Ptr<DOM::Position> Document::cursor_position() const
{
    auto const focused_area = this->focused_area();
    if (!focused_area)
        return nullptr;

    Optional<HTML::FormAssociatedTextControlElement const&> target {};
    if (auto const* input_element = as_if<HTML::HTMLInputElement>(*focused_area)) {
        // Some types of <input> tags shouldn't have a cursor, like buttons
        if (!input_element->can_have_text_editing_cursor())
            return nullptr;
        target = *input_element;
    } else if (auto const* text_area_element = as_if<HTML::HTMLTextAreaElement>(*focused_area)) {
        target = *text_area_element;
    }

    if (target.has_value())
        return target->cursor_position();

    if (focused_area->is_editable_or_editing_host())
        return m_selection->cursor_position();

    return nullptr;
}

void Document::reset_cursor_blink_cycle()
{
    m_cursor_blink_state = true;

    // In testing mode, disable timed blinking so we can deterministically generate display lists.
    if (!HTML::Window::in_test_mode())
        m_cursor_blink_timer->restart();
}

// https://html.spec.whatwg.org/multipage/document-sequences.html#doc-container-document
GC::Ptr<DOM::Document> Document::container_document() const
{
    // 1. If document's node navigable is null, then return null.
    auto node_navigable = navigable();
    if (!node_navigable)
        return nullptr;

    // 2. Return document's node navigable's container document.
    return node_navigable->container_document();
}

GC::Ptr<HTML::Navigable> Document::navigable() const
{
    return m_navigable.ptr();
}

void Document::set_navigable(GC::Ptr<HTML::Navigable> navigable)
{
    if (m_navigable == navigable)
        return;
    m_navigable = navigable.ptr();
    HTML::main_thread_event_loop().document_navigable_did_change({});
}

void Document::set_needs_repaint(InvalidateDisplayList should_invalidate_display_list)
{
    auto navigable = this->navigable();

    // OPTIMIZATION: Ignore set_needs_repaint() inside navigable containers (i.e frames) with visibility: hidden.
    if (navigable && navigable->has_inclusive_ancestor_with_visibility_hidden())
        return;

    if (should_invalidate_display_list == InvalidateDisplayList::Yes) {
        set_needs_to_record_display_list();
    }

    if (!navigable)
        return;

    navigable->set_needs_repaint();

    if (navigable->is_traversable()) {
        page().client().request_frame();
        return;
    }

    if (auto container = navigable->container()) {
        container->document().set_needs_repaint(InvalidateDisplayList::No);
    }
}

void Document::set_needs_to_record_display_list()
{
    if (auto navigable = this->navigable())
        navigable->set_needs_to_record_display_list();
}

RefPtr<Painting::DisplayList> Document::record_display_list(HTML::PaintConfig config)
{
    update_paint_and_hit_testing_properties_if_needed();
    VERIFY(paintable());

    auto display_list = Painting::DisplayList::create(paintable()->visual_context_tree());
    Painting::DisplayListRecorder display_list_recorder(display_list);

    // https://drafts.csswg.org/css-color-adjust-1/#color-scheme-effect
    // On the root element, the used color scheme additionally must affect the surface color of the canvas, and the viewport’s scrollbars.
    auto color_scheme = CSS::PreferredColorScheme::Light;
    if (auto* html_element = this->html_element(); html_element && html_element->layout_node()) {
        if (html_element->layout_node()->computed_values().color_scheme() == CSS::PreferredColorScheme::Dark)
            color_scheme = CSS::PreferredColorScheme::Dark;
    }

    // .. in the case of embedded documents typically rendered over a transparent canvas
    // (such as provided via an HTML iframe element), if the used color scheme of the element
    // and the used color scheme of the embedded document’s root element do not match,
    // then the UA must use an opaque canvas of the Canvas color appropriate to the
    // embedded document’s used color scheme instead of a transparent canvas.
    bool opaque_canvas = false;
    if (auto container_element = navigable()->container(); container_element && container_element->layout_node()) {
        auto container_scheme = container_element->layout_node()->computed_values().color_scheme();
        if (container_scheme == CSS::PreferredColorScheme::Auto)
            container_scheme = CSS::PreferredColorScheme::Light;

        opaque_canvas = container_scheme != color_scheme;
    }

    if (config.canvas_fill_rect.has_value()) {
        display_list_recorder.fill_rect(config.canvas_fill_rect.value(), CSS::SystemColor::canvas(color_scheme));
    }

    auto viewport_rect = page().css_to_device_rect(this->viewport_rect());
    Gfx::IntRect bitmap_rect { {}, viewport_rect.size().to_type<int>() };

    if (opaque_canvas)
        display_list_recorder.fill_rect(bitmap_rect, CSS::SystemColor::canvas(color_scheme));

    display_list_recorder.fill_rect(bitmap_rect, background_color());

    Web::DisplayListRecordingContext context(display_list_recorder, page().palette(), page().client().device_pixels_per_css_pixel(), page().chrome_metrics());
    context.set_device_viewport_rect(viewport_rect);
    context.set_should_show_line_box_borders(config.should_show_line_box_borders);
    context.set_should_paint_overlay(config.paint_overlay);

    auto& viewport_paintable = *paintable();
    viewport_paintable.refresh_scroll_state();
    Compositor::initialize_async_scrolling_metadata_recording(context, viewport_paintable);

    viewport_paintable.paint_all_phases(context);
    Compositor::finalize_async_scrolling_metadata_recording(context, *navigable(), viewport_rect.to_type<int>());

    if (highlighted_node() && highlighted_node()->paintable()) {
        highlighted_node()->paintable()->paint_inspector_overlay(context);
    }

    return display_list;
}

Unicode::Segmenter& Document::grapheme_segmenter() const
{
    if (!m_grapheme_segmenter)
        m_grapheme_segmenter = Unicode::Segmenter::create(Unicode::SegmenterGranularity::Grapheme);
    return *m_grapheme_segmenter;
}

Unicode::Segmenter& Document::line_segmenter() const
{
    if (!m_line_segmenter)
        m_line_segmenter = Unicode::Segmenter::create(Unicode::SegmenterGranularity::Line);
    return *m_line_segmenter;
}

Unicode::Segmenter& Document::word_segmenter() const
{
    if (!m_word_segmenter)
        m_word_segmenter = Unicode::Segmenter::create(Unicode::SegmenterGranularity::Word);
    return *m_word_segmenter;
}

// https://html.spec.whatwg.org/multipage/browsing-the-web.html#steps-to-fire-beforeunload
Document::StepsToFireBeforeunloadResult Document::steps_to_fire_beforeunload(bool unload_prompt_shown)
{
    // 1. Let unloadPromptCanceled be false.
    auto unload_prompt_canceled = false;

    // 2. Increase the document's unload counter by 1.
    m_unload_counter++;

    // 3. Increase document's relevant agent's event loop's termination nesting level by 1.
    auto& event_loop = *HTML::relevant_agent(*this).event_loop;
    event_loop.increment_termination_nesting_level();

    // 4. Let eventFiringResult be the result of firing an event named beforeunload at document's relevant global object,
    //    using BeforeUnloadEvent, with the cancelable attribute initialized to true.
    auto& global_object = HTML::relevant_global_object(*this);
    auto& window = as<HTML::Window>(global_object);
    auto beforeunload_event = HTML::BeforeUnloadEvent::create(realm(), HTML::EventNames::beforeunload);
    beforeunload_event->set_cancelable(true);
    auto event_firing_result = window.dispatch_event(*beforeunload_event);

    // 5. Decrease document's relevant agent's event loop's termination nesting level by 1.
    event_loop.decrement_termination_nesting_level();

    // FIXME: 6. If all of the following are true:
    if (false &&
        //    - unloadPromptShown is false;
        !unload_prompt_shown
        //    - document's active sandboxing flag set does not have its sandboxed modals flag set;
        && !has_flag(document().active_sandboxing_flag_set(), HTML::SandboxingFlagSet::SandboxedModals)
        //    - document's relevant global object has sticky activation;
        && window.has_sticky_activation()
        //    - eventFiringResult is false, or the returnValue attribute of event is not the empty string; and
        && (!event_firing_result || !beforeunload_event->return_value().is_empty())
        //    - FIXME: showing an unload prompt is unlikely to be annoying, deceptive, or pointless
    ) {
        // FIXME: 1. Set unloadPromptShown to true.
        // FIXME: 2. Invoke WebDriver BiDi user prompt opened with document's relevant global object, "beforeunload", and "".
        // FIXME: 3. Ask the user to confirm that they wish to unload the document, and pause while waiting for the user's response.
        // FIXME: 4. If the user did not confirm the page navigation, set unloadPromptCanceled to true.
        // FIXME: 5. Invoke WebDriver BiDi user prompt closed with document's relevant global object and true if unloadPromptCanceled is false or false otherwise.
    }

    // 7. Decrease document's unload counter by 1.
    m_unload_counter--;

    // 8. Return (unloadPromptShown, unloadPromptCanceled).
    return { unload_prompt_shown, unload_prompt_canceled };
}

// https://w3c.github.io/webappsec-csp/#run-document-csp-initialization
void Document::run_csp_initialization() const
{
    // 1. For each policy of document’s policy container's CSP list:
    for (auto policy : policy_container()->csp_list->policies()) {
        // 1. For each directive of policy:
        for (auto directive : policy->directives()) {
            // 1. Execute directive’s initialization algorithm on document, and assert: its returned value is "Allowed".
            auto result = directive->initialization(GC::Ref { *this }, policy);
            VERIFY(result == ContentSecurityPolicy::Directives::Directive::Result::Allowed);
        }
    }
}

// https://dom.spec.whatwg.org/#flatten-element-creation-options
WebIDL::ExceptionOr<Document::RegistryAndIs> Document::flatten_element_creation_options(Variant<String, Bindings::ElementCreationOptions> const& options) const
{
    // 1. Let registry be the result of looking up a custom element registry given document.
    GC::Ptr<HTML::CustomElementRegistry> registry = HTML::look_up_a_custom_element_registry(*this);

    // 2. Let is be null.
    Optional<String> is;

    // 3. If options is a dictionary:
    if (auto* dictionary = options.get_pointer<Bindings::ElementCreationOptions>()) {
        // 1. If options["is"] exists, then set is to it.
        if (dictionary->is.has_value())
            is = dictionary->is;

        // 2. If options["customElementRegistry"] exists:
        if (dictionary->custom_element_registry.has_value()) {
            // 1. If is is non-null, then throw a "NotSupportedError" DOMException.
            if (is.has_value())
                return WebIDL::NotSupportedError::create(realm(), "Cannot specify both 'is' and 'customElementRegistry' in ElementCreationOptions."_utf16);

            // 2. Set registry to options["customElementRegistry"].
            registry = dictionary->custom_element_registry.value();
        }

        // 3. If registry is non-null, registry’s is scoped is false, and registry is not document’s custom element
        //    registry, then throw a "NotSupportedError" DOMException.
        if (registry && !registry->is_scoped() && registry != custom_element_registry())
            return WebIDL::NotSupportedError::create(realm(), "'customElementRegistry' in ElementCreationOptions must either be scoped or the document's custom element registry."_utf16);
    }

    // 4. Return registry and is.
    return RegistryAndIs { registry, move(is) };
}

WebIDL::CallbackType* Document::onreadystatechange()
{
    return event_handler_attribute(HTML::EventNames::readystatechange);
}

void Document::set_onreadystatechange(WebIDL::CallbackType* value)
{
    set_event_handler_attribute(HTML::EventNames::readystatechange, value);
}

WebIDL::CallbackType* Document::onvisibilitychange()
{
    return event_handler_attribute(HTML::EventNames::visibilitychange);
}

void Document::set_onvisibilitychange(WebIDL::CallbackType* value)
{
    set_event_handler_attribute(HTML::EventNames::visibilitychange, value);
}

WebIDL::CallbackType* Document::onfullscreenchange()
{
    return event_handler_attribute(HTML::EventNames::fullscreenchange);
}

void Document::set_onfullscreenchange(WebIDL::CallbackType* value)
{
    set_event_handler_attribute(HTML::EventNames::fullscreenchange, value);
}

WebIDL::CallbackType* Document::onfullscreenerror()
{
    return event_handler_attribute(HTML::EventNames::fullscreenerror);
}

void Document::set_onfullscreenerror(WebIDL::CallbackType* value)
{
    set_event_handler_attribute(HTML::EventNames::fullscreenerror, value);
}

// https://drafts.csswg.org/css-view-transitions-1/#dom-document-startviewtransition
GC::Ptr<ViewTransition::ViewTransition> Document::start_view_transition(GC::Ptr<WebIDL::CallbackType> update_callback)
{
    // The method steps for startViewTransition(updateCallback) are as follows:

    // 1. Let transition be a new ViewTransition object in this’s relevant Realm.
    auto& realm = this->realm();
    auto transition = ViewTransition::ViewTransition::create(realm);

    // 2. If updateCallback is provided, set transition’s update callback to updateCallback.
    if (update_callback != nullptr)
        transition->set_update_callback(update_callback);

    // 3. Let document be this’s relevant global object’s associated document.
    auto& document = as<HTML::Window>(relevant_global_object(*this)).associated_document();

    // 4. If document’s visibility state is "hidden", then skip transition with an "InvalidStateError" DOMException,
    //    and return transition.
    if (m_visibility_state == HTML::VisibilityState::Hidden) {
        transition->skip_the_view_transition(WebIDL::InvalidStateError::create(realm, "The document's visibility state is \"hidden\""_utf16));
        return transition;
    }

    // 5. If document’s active view transition is not null, then skip that view transition with an "AbortError"
    //    DOMException in this’s relevant Realm.
    if (document.m_active_view_transition)
        document.m_active_view_transition->skip_the_view_transition(WebIDL::AbortError::create(realm, "Document.startViewTransition() was called"_utf16));

    // 6. Set document’s active view transition to transition.
    m_active_view_transition = transition;

    // 7. Return transition.
    return transition;
}

// https://drafts.csswg.org/css-view-transitions-1/#perform-pending-transition-operations
void Document::perform_pending_transition_operations()
{
    // To perform pending transition operations given a Document document, perform the following steps:

    // 1. If document’s active view transition is not null, then:
    if (m_active_view_transition) {
        // 1. If document’s active view transition’s phase is "pending-capture", then setup view transition for
        //    document’s active view transition.
        if (m_active_view_transition->phase() == ViewTransition::ViewTransition::Phase::PendingCapture)
            m_active_view_transition->setup_view_transition();
        // 2. Otherwise, if document’s active view transition’s phase is "animating", then handle transition frame for
        //    document’s active view transition.
        else if (m_active_view_transition->phase() == ViewTransition::ViewTransition::Phase::Animating)
            m_active_view_transition->handle_transition_frame();
    }
}

// https://drafts.csswg.org/css-view-transitions-1/#flush-the-update-callback-queue
void Document::flush_the_update_callback_queue()
{
    // To flush the update callback queue given a Document document:

    // 1. For each transition in document’s update callback queue, call the update callback given transition.
    for (auto& transition : m_update_callback_queue) {
        transition->call_the_update_callback();
    }

    // 2. Set document’s update callback queue to an empty list.
    m_update_callback_queue.clear();
}

void Document::set_rendering_suppression_for_view_transitions(bool value)
{
    if (m_rendering_suppression_for_view_transitions == value)
        return;

    m_rendering_suppression_for_view_transitions = value;

    if (!value)
        page().client().request_frame();
}

// https://drafts.csswg.org/css-view-transitions-1/#view-transition-page-visibility-change-steps
void Document::view_transition_page_visibility_change_steps()
{
    // The view transition page-visibility change steps given Document document are:

    // 1. Queue a global task on the DOM manipulation task source, given document’s relevant global object, to
    //    perform the following steps:
    HTML::queue_global_task(HTML::Task::Source::DOMManipulation, HTML::relevant_global_object(*this), GC::create_function(realm().heap(), [&] {
        HTML::TemporaryExecutionContext context(realm());
        // 1. If document’s visibility state is "hidden", then:
        if (m_visibility_state == HTML::VisibilityState::Hidden) {
            // 1. If document’s active view transition is not null, then skip document’s active view transition with an
            //    "InvalidStateError" DOMException.
            if (m_active_view_transition) {
                m_active_view_transition->skip_the_view_transition(WebIDL::InvalidStateError::create(realm(), "The document's visibility state is \"hidden\"."_utf16));
            }
        }
        // 2. Otherwise, assert: active view transition is null.
        else {
            VERIFY(!m_active_view_transition);
        }
    }));
}

// https://dom.spec.whatwg.org/#dom-documentorshadowroot-customelementregistry
GC::Ptr<HTML::CustomElementRegistry> Document::custom_element_registry() const
{
    // 1. If this is a document, then return this’s custom element registry.
    // NB: Always true.
    return m_custom_element_registry;

    // 2. Assert: this is a ShadowRoot node.
    // 3. Return this’s custom element registry.
}

// https://dom.spec.whatwg.org/#effective-global-custom-element-registry
GC::Ptr<HTML::CustomElementRegistry> Document::effective_global_custom_element_registry() const
{
    // A document document’s effective global custom element registry is:

    // 1. If document’s custom element registry is a global custom element registry, then return document’s custom
    //    element registry.
    if (HTML::is_a_global_custom_element_registry(custom_element_registry()))
        return custom_element_registry();

    // 2. Return null.
    return nullptr;
}

// https://html.spec.whatwg.org/multipage/custom-elements.html#upgrade-particular-elements-within-a-document
void Document::upgrade_particular_elements(GC::Ref<HTML::CustomElementRegistry> registry, GC::Ref<HTML::CustomElementDefinition> definition, String local_name, Optional<String> maybe_name)
{
    // To upgrade particular elements within a document given a CustomElementRegistry object registry, a Document
    // object document, a custom element definition definition, a string localName, and optionally a string name
    // (default localName):
    auto name = maybe_name.value_or(local_name);

    // 1. Let upgradeCandidates be all elements that are shadow-including descendants of document, whose custom element
    //    registry is registry, whose namespace is the HTML namespace, and whose local name is localName, in
    //    shadow-including tree order.
    //    Additionally, if name is not localName, only include elements whose is value is equal to name.
    for_each_shadow_including_descendant([&](Node& inclusive_descendant) {
        auto* element = as_if<Element>(inclusive_descendant);
        if (!element
            || element->custom_element_registry() != registry
            || element->namespace_uri() != Namespace::HTML
            || element->local_name() != local_name) {

            return TraversalDecision::Continue;
        }

        if (name != local_name && element->is_value() != name)
            return TraversalDecision::Continue;

        // 2. For each element element of upgradeCandidates:
        //    enqueue a custom element upgrade reaction given element and definition.
        element->enqueue_a_custom_element_upgrade_reaction(definition);
        return TraversalDecision::Continue;
    });
}

ElementByIdMap& Document::element_by_id() const
{
    if (!m_element_by_id)
        m_element_by_id = make<ElementByIdMap>();
    return *m_element_by_id;
}

String Document::dump_display_list()
{
    update_layout(UpdateLayoutReason::DumpDisplayList);

    auto viewport_paintable = paintable();
    if (!viewport_paintable)
        return "No paintable"_string;

    auto display_list = record_display_list(HTML::PaintConfig {});
    if (!display_list)
        return "No display list"_string;

    HashMap<size_t, RefPtr<Painting::PaintableBox const>> context_id_to_paintable;
    viewport_paintable->for_each_in_inclusive_subtree_of_type<Painting::PaintableBox>([&](auto const& paintable_box) {
        auto visual_context_index = paintable_box.accumulated_visual_context_index();
        if (visual_context_index.value())
            (void)context_id_to_paintable.try_set(visual_context_index.value(), paintable_box);
        return TraversalDecision::Continue;
    });

    StringBuilder builder;
    builder.append("AccumulatedVisualContext Tree:\n"sv);

    auto const& visual_context_tree = display_list->visual_context_tree();
    HashTable<size_t> visited;
    HashMap<size_t, Vector<size_t>> children;
    Vector<size_t> root_contexts;

    display_list->for_each_command_header([&](Painting::DisplayListCommandHeader const& header, ReadonlyBytes) {
        if (!header.context_index.value())
            return;
        for (size_t node_index = header.context_index.value(); node_index && !visited.contains(node_index);) {
            visited.set(node_index);
            auto parent = visual_context_tree.node_at(Painting::VisualContextIndex(node_index)).parent_index.value();
            if (parent)
                children.ensure(parent).append(node_index);
            else if (!root_contexts.contains_slow(node_index))
                root_contexts.append(node_index);
            node_index = parent;
        }
    });

    Function<void(size_t, size_t)> dump_context = [&](size_t node_index, size_t indent) {
        builder.append_repeated(' ', indent * 2);
        builder.appendff("[{}] ", node_index);
        visual_context_tree.dump(Painting::VisualContextIndex(node_index), builder);
        if (auto it = context_id_to_paintable.find(node_index); it != context_id_to_paintable.end())
            builder.appendff(" ({})", it->value->debug_description());
        builder.append('\n');
        for (auto child_node_index : children.get(node_index).value_or({}))
            dump_context(child_node_index, indent + 1);
    };

    for (auto root : root_contexts)
        dump_context(root, 1);

    builder.append("\nDisplayList:\n"sv);

    Function<void(Painting::DisplayList const&, int)> dump_commands =
        [&](Painting::DisplayList const& list, int base_indent) {
            int indent = base_indent;
            list.for_each_command_header([&](Painting::DisplayListCommandHeader const& header, ReadonlyBytes payload) {
                auto nesting_change = Painting::display_list_command_nesting_level_change(header.type);

                if (nesting_change < 0)
                    indent = max(base_indent, indent + nesting_change);

                builder.append_repeated(' ', indent * 2);
                Optional<Painting::DisplayListResourceId> nested_display_list_id;
                Painting::visit_display_list_command(header.type, payload, [&]<typename Command>(Command const& command) {
                    builder.appendff("{}@{}", command.command_name, header.context_index.value());
                    command.dump(builder);
                    if constexpr (IsSame<Command, Painting::PaintNestedDisplayList>)
                        nested_display_list_id = command.display_list_id;
                });
                builder.append('\n');

                if (nested_display_list_id.has_value()) {
                    auto& nested_display_list = list.resource_storage().display_list(*nested_display_list_id);
                    dump_commands(nested_display_list, indent + 1);
                }

                if (nesting_change > 0)
                    indent += nesting_change;
            });
        };

    dump_commands(*display_list, 0);

    return builder.to_string_without_validation();
}

String Document::dump_stacking_context_tree()
{
    update_layout(UpdateLayoutReason::DumpDisplayList);

    auto viewport_paintable = paintable();
    if (!viewport_paintable)
        return "No paintable"_string;

    viewport_paintable->build_stacking_context_tree_if_needed();

    auto stacking_context = viewport_paintable->stacking_context();
    if (!stacking_context)
        return "No stacking context"_string;

    StringBuilder builder;
    stacking_context->dump(builder);
    return builder.to_string_without_validation();
}

Optional<Vector<CSS::Parser::ComponentValue>> Document::environment_variable_value(CSS::EnvironmentVariable environment_variable, Span<i32> indices) const
{
    auto invalid = [] {
        return Vector { CSS::Parser::ComponentValue { CSS::Parser::GuaranteedInvalidValue {} } };
    };

    switch (environment_variable) {
    case CSS::EnvironmentVariable::PreferredTextScale:
        // FIXME: Report the user's preferred text scale, once we have that as a setting.
        if (!indices.is_empty())
            return invalid();
        return Vector {
            CSS::Parser::ComponentValue {
                CSS::Parser::Token::create_number(CSS::Number { CSS::Number::Type::Number, 1 }) }
        };
    case CSS::EnvironmentVariable::SafeAreaInsetBottom:
    case CSS::EnvironmentVariable::SafeAreaInsetLeft:
    case CSS::EnvironmentVariable::SafeAreaInsetRight:
    case CSS::EnvironmentVariable::SafeAreaInsetTop:
    case CSS::EnvironmentVariable::SafeAreaMaxInsetBottom:
    case CSS::EnvironmentVariable::SafeAreaMaxInsetLeft:
    case CSS::EnvironmentVariable::SafeAreaMaxInsetRight:
    case CSS::EnvironmentVariable::SafeAreaMaxInsetTop:
        // FIXME: Report the safe area, once we can detect our display's shape (and it's not rectangular).
        if (!indices.is_empty())
            return invalid();
        return Vector {
            CSS::Parser::ComponentValue { CSS::Parser::Token::create_dimension(0, "px"_fly_string) }
        };
    case CSS::EnvironmentVariable::ViewportSegmentBottom:
    case CSS::EnvironmentVariable::ViewportSegmentHeight:
    case CSS::EnvironmentVariable::ViewportSegmentLeft:
    case CSS::EnvironmentVariable::ViewportSegmentRight:
    case CSS::EnvironmentVariable::ViewportSegmentTop:
    case CSS::EnvironmentVariable::ViewportSegmentWidth:
        // "These variables are only defined when there are at least two such segments."
        // https://drafts.csswg.org/css-env/#viewport-segments
        // FIXME: Report the viewport segment areas, once we can detect that and there are multiple of them.
        return Vector { CSS::Parser::ComponentValue { CSS::Parser::GuaranteedInvalidValue {} } };
    }
    VERIFY_NOT_REACHED();
}

HashMap<FlyString, CSS::CustomPropertyRegistration>& Document::registered_property_set()
{
    return m_registered_property_set;
}

WebIDL::ExceptionOr<GC::Ref<XPath::XPathExpression>> Document::create_expression(String const& expression, GC::Ptr<XPath::XPathNSResolver> resolver)
{
    auto& realm = this->realm();
    return XPath::create_expression(realm, expression, resolver);
}

WebIDL::ExceptionOr<GC::Ref<XPath::XPathResult>> Document::evaluate(String const& expression, DOM::Node const& context_node, GC::Ptr<XPath::XPathNSResolver> resolver, WebIDL::UnsignedShort type, GC::Ptr<XPath::XPathResult> result)
{
    auto& realm = this->realm();
    return XPath::evaluate(realm, expression, context_node, resolver, type, result);
}

GC::Ref<DOM::Node> Document::create_ns_resolver(GC::Ref<DOM::Node> node_resolver)
{
    return node_resolver;
}

// https://drafts.css-houdini.org/css-properties-values-api/#determining-registration
Optional<CSS::CustomPropertyRegistration const&> Document::get_registered_custom_property(FlyString const& name) const
{
    // If the Document’s [[registeredPropertySet]] slot contains a record with the custom property’s name, the
    // registration is that record.
    if (auto registered_property = m_registered_property_set.get(name); registered_property.has_value())
        return registered_property;

    // Otherwise, if the Document’s active stylesheets contain at least one valid @property rule representing a
    // registration with the custom property’s name, the last such one in document order is the registration.
    if (auto registered_property = m_cached_registered_properties_from_css_property_rules.get(name); registered_property.has_value())
        return registered_property;

    // Otherwise there is no registration, and the custom property is not a registered custom property.
    return {};
}

NonnullRefPtr<CSS::StyleValue const> Document::custom_property_initial_value(FlyString const& name) const
{
    auto maybe_custom_property = get_registered_custom_property(name);
    if (maybe_custom_property.has_value()) {
        if (maybe_custom_property->initial_value)
            return *maybe_custom_property->initial_value;
        return CSS::GuaranteedInvalidStyleValue::create();
    }

    // For non-registered properties, the initial value is the guaranteed-invalid value.
    // See: https://drafts.csswg.org/css-variables/#propdef-
    return CSS::GuaranteedInvalidStyleValue::create();
}

void Document::did_change_custom_property_registrations()
{
    ++m_custom_property_registration_generation;

    // Custom property registration changes can alter inheritance and initial values even when no selector matching
    // changes. Registrations only move when a stylesheet containing an @property rule is added/removed or when
    // CSS.registerProperty() is called, so a full document restyle is cheap enough in practice.
    invalidate_style(DOM::StyleInvalidationReason::Other);
}

void Document::build_registered_properties_cache()
{
    HashMap<FlyString, CSS::CustomPropertyRegistration> cached_registered_properties_from_css_property_rules;
    for_each_active_css_style_sheet([&](CSS::CSSStyleSheet const& style_sheet) {
        style_sheet.for_each_effective_rule(TraversalOrder::Preorder, [&](CSS::CSSRule const& rule) {
            if (auto* property_rule = as_if<CSS::CSSPropertyRule>(rule))
                cached_registered_properties_from_css_property_rules.set(property_rule->name(), property_rule->to_registration());
        });
    });

    if (cached_registered_properties_from_css_property_rules != m_cached_registered_properties_from_css_property_rules)
        did_change_custom_property_registrations();

    m_cached_registered_properties_from_css_property_rules = move(cached_registered_properties_from_css_property_rules);
}

void Document::ensure_cookie_version_index(URL::URL const& new_url, URL::URL const& old_url)
{
    auto new_domain = HTTP::Cookie::canonicalize_domain(new_url);
    if (!new_domain.has_value()) {
        m_cookie_version_index = {};
        return;
    }

    if (m_cookie_version_index.has_value() && *new_domain == HTTP::Cookie::canonicalize_domain(old_url))
        return;

    page().client().page_did_request_document_cookie_version_index(unique_id(), *new_domain);
    m_cookie_version_index = {};
}

StringView to_string(SetNeedsLayoutReason reason)
{
    switch (reason) {
#define ENUMERATE_SET_NEEDS_LAYOUT_REASON(e) \
    case SetNeedsLayoutReason::e:            \
        return #e##sv;
        ENUMERATE_SET_NEEDS_LAYOUT_REASONS(ENUMERATE_SET_NEEDS_LAYOUT_REASON)
#undef ENUMERATE_SET_NEEDS_LAYOUT_REASON
    }
    VERIFY_NOT_REACHED();
}

StringView to_string(SetNeedsLayoutTreeUpdateReason reason)
{
    switch (reason) {
#define ENUMERATE_SET_NEEDS_LAYOUT_TREE_UPDATE_REASON(e) \
    case SetNeedsLayoutTreeUpdateReason::e:              \
        return #e##sv;
        ENUMERATE_SET_NEEDS_LAYOUT_TREE_UPDATE_REASONS(ENUMERATE_SET_NEEDS_LAYOUT_TREE_UPDATE_REASON)
#undef ENUMERATE_SET_NEEDS_LAYOUT_TREE_UPDATE_REASON
    }
    VERIFY_NOT_REACHED();
}

StringView to_string(InvalidateLayoutTreeReason reason)
{
    switch (reason) {
#define ENUMERATE_INVALIDATE_LAYOUT_TREE_REASON(e) \
    case InvalidateLayoutTreeReason::e:            \
        return #e##sv;
        ENUMERATE_INVALIDATE_LAYOUT_TREE_REASONS(ENUMERATE_INVALIDATE_LAYOUT_TREE_REASON)
#undef ENUMERATE_INVALIDATE_LAYOUT_TREE_REASON
    }
    VERIFY_NOT_REACHED();
}

StringView to_string(UpdateLayoutReason reason)
{
    switch (reason) {
#define ENUMERATE_UPDATE_LAYOUT_REASON(e) \
    case UpdateLayoutReason::e:           \
        return #e##sv;
        ENUMERATE_UPDATE_LAYOUT_REASONS(ENUMERATE_UPDATE_LAYOUT_REASON)
#undef ENUMERATE_UPDATE_LAYOUT_REASON
    }
    VERIFY_NOT_REACHED();
}

void Document::add_pending_css_import_rule(Badge<CSS::CSSImportRule>, GC::Ref<CSS::CSSImportRule> rule)
{
    m_pending_css_import_rules.set(rule);
}

void Document::remove_pending_css_import_rule(Badge<CSS::CSSImportRule>, GC::Ref<CSS::CSSImportRule> rule)
{
    m_pending_css_import_rules.remove(rule);
    if (m_pending_css_import_rules.is_empty())
        page().client().request_frame();
}

void Document::exit_pointer_lock()
{
    dbgln("FIXME: exit_pointer_lock()");
}

static bool contains_named_namespace(CSS::SelectorList const& selectors)
{
    for (auto const& selector : selectors) {
        for (auto const& compound_selector : selector->compound_selectors()) {
            for (auto const& simple_selector : compound_selector.simple_selectors) {
                if (simple_selector.value.has<CSS::Selector::SimpleSelector::QualifiedName>()) {
                    if (simple_selector.qualified_name().namespace_type == CSS::Selector::SimpleSelector::QualifiedName::NamespaceType::Named)
                        return true;
                }

                if (simple_selector.value.has<CSS::Selector::SimpleSelector::PseudoClassSelector>()) {
                    if (contains_named_namespace(simple_selector.pseudo_class().argument_selector_list))
                        return true;
                }
            }
        }
    }
    return false;
}

Optional<CSS::SelectorList> const& Document::parse_or_cache_selector_list(StringView selector_text) const
{
    static constexpr size_t MAX_SELECTOR_QUERY_CACHE_SIZE = 512;

    auto selector_text_string = MUST(String::from_utf8(selector_text));

    if (auto it = m_selector_query_cache.find(selector_text_string); it != m_selector_query_cache.end())
        return it->value;

    auto maybe_selectors = parse_selector(CSS::Parser::ParsingParams { *this }, selector_text);

    // "Note: Support for namespaces within selectors is not planned and will not be added."
    if (maybe_selectors.has_value() && contains_named_namespace(maybe_selectors.value()))
        maybe_selectors.clear();

    if (m_selector_query_cache.size() >= MAX_SELECTOR_QUERY_CACHE_SIZE)
        m_selector_query_cache.remove(m_selector_query_cache.begin());

    m_selector_query_cache.set(selector_text_string, move(maybe_selectors));
    auto it = m_selector_query_cache.find(selector_text_string);
    VERIFY(it != m_selector_query_cache.end());
    return it->value;
}

}
