/*
 * Copyright (c) 2018-2024, Andreas Kling <andreas@ladybird.org>
 * Copyright (c) 2025, Jelle Raaijmakers <jelle@ladybird.org>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include <AK/StringBuilder.h>
#include <LibJS/Runtime/NativeFunction.h>
#include <LibWeb/ARIA/Roles.h>
#include <LibWeb/Bindings/ExceptionOrUtils.h>
#include <LibWeb/Bindings/HTMLElementPrototype.h>
#include <LibWeb/CSS/ComputedProperties.h>
#include <LibWeb/CSS/StyleValues/DisplayStyleValue.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/EditingHostManager.h>
#include <LibWeb/DOM/ElementFactory.h>
#include <LibWeb/DOM/IDLEventListener.h>
#include <LibWeb/DOM/LiveNodeList.h>
#include <LibWeb/DOM/Position.h>
#include <LibWeb/DOM/Range.h>
#include <LibWeb/DOM/ShadowRoot.h>
#include <LibWeb/HTML/BrowsingContext.h>
#include <LibWeb/HTML/CloseWatcher.h>
#include <LibWeb/HTML/CustomElements/CustomElementDefinition.h>
#include <LibWeb/HTML/ElementInternals.h>
#include <LibWeb/HTML/EventHandler.h>
#include <LibWeb/HTML/HTMLAnchorElement.h>
#include <LibWeb/HTML/HTMLBRElement.h>
#include <LibWeb/HTML/HTMLBaseElement.h>
#include <LibWeb/HTML/HTMLBodyElement.h>
#include <LibWeb/HTML/HTMLDialogElement.h>
#include <LibWeb/HTML/HTMLElement.h>
#include <LibWeb/HTML/HTMLInputElement.h>
#include <LibWeb/HTML/HTMLLabelElement.h>
#include <LibWeb/HTML/HTMLObjectElement.h>
#include <LibWeb/HTML/HTMLParagraphElement.h>
#include <LibWeb/HTML/PopoverInvokerElement.h>
#include <LibWeb/HTML/ToggleEvent.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/Infra/CharacterTypes.h>
#include <LibWeb/Infra/Strings.h>
#include <LibWeb/Layout/Box.h>
#include <LibWeb/Layout/TextNode.h>
#include <LibWeb/Namespace.h>
#include <LibWeb/Painting/PaintableBox.h>
#include <LibWeb/Selection/Selection.h>
#include <LibWeb/UIEvents/EventNames.h>
#include <LibWeb/UIEvents/PointerEvent.h>
#include <LibWeb/WebIDL/DOMException.h>
#include <LibWeb/WebIDL/ExceptionOr.h>

namespace Web::HTML {

GC_DEFINE_ALLOCATOR(HTMLElement);

HTMLElement::HTMLElement(DOM::Document& document, DOM::QualifiedName qualified_name)
    : Element(document, move(qualified_name))
{
}

HTMLElement::~HTMLElement() = default;

void HTMLElement::initialize(JS::Realm& realm)
{
    WEB_SET_PROTOTYPE_FOR_INTERFACE(HTMLElement);
    Base::initialize(realm);
}

void HTMLElement::visit_edges(Cell::Visitor& visitor)
{
    Base::visit_edges(visitor);
    HTMLOrSVGElement::visit_edges(visitor);
    visitor.visit(m_labels);
    visitor.visit(m_attached_internals);
    visitor.visit(m_popover_invoker);
    visitor.visit(m_popover_close_watcher);
}

// https://html.spec.whatwg.org/multipage/dom.html#block-rendering
void HTMLElement::block_rendering()
{
    // 1. Let document be el's node document.
    auto& document = this->document();

    // 2. If document allows adding render-blocking elements, then append el to document's render-blocking element set.
    if (document.allows_adding_render_blocking_elements()) {
        document.add_render_blocking_element(*this);
    }
}

// https://html.spec.whatwg.org/multipage/dom.html#unblock-rendering
void HTMLElement::unblock_rendering()
{
    // 1. Let document be el's node document.
    auto& document = this->document();

    // 2. Remove el from document's render-blocking element set.
    document.remove_render_blocking_element(*this);
}

// https://html.spec.whatwg.org/multipage/urls-and-fetching.html#potentially-render-blocking
bool HTMLElement::is_potentially_render_blocking()
{
    // An element is potentially render-blocking if
    // FIXME: its blocking tokens set contains "render",
    // or if it is implicitly potentially render-blocking, which will be defined at the individual elements.
    return is_implicitly_potentially_render_blocking();
}

// https://html.spec.whatwg.org/multipage/dom.html#dom-translate
bool HTMLElement::translate() const
{
    // The translate IDL attribute must, on getting, return true if the element's translation mode is
    // translate-enabled, and false otherwise
    return translation_mode() == TranslationMode::TranslateEnabled;
}

// https://html.spec.whatwg.org/multipage/dom.html#dom-translate
void HTMLElement::set_translate(bool new_value)
{
    // On setting, it must set the content attribute's value to "yes" if the new value is true, and set the content
    // attribute's value to "no" otherwise.
    if (new_value)
        MUST(set_attribute(HTML::AttributeNames::translate, "yes"_string));
    else
        MUST(set_attribute(HTML::AttributeNames::translate, "no"_string));
}

// https://html.spec.whatwg.org/multipage/dom.html#dom-dir
StringView HTMLElement::dir() const
{
    // FIXME: This should probably be `Reflect` in the IDL.
    // The dir IDL attribute on an element must reflect the dir content attribute of that element, limited to only known values.
    auto dir = get_attribute_value(HTML::AttributeNames::dir);
#define __ENUMERATE_HTML_ELEMENT_DIR_ATTRIBUTE(keyword) \
    if (dir.equals_ignoring_ascii_case(#keyword##sv))   \
        return #keyword##sv;
    ENUMERATE_HTML_ELEMENT_DIR_ATTRIBUTES
#undef __ENUMERATE_HTML_ELEMENT_DIR_ATTRIBUTE

    return {};
}

void HTMLElement::set_dir(String const& dir)
{
    MUST(set_attribute(HTML::AttributeNames::dir, dir));
}

bool HTMLElement::is_focusable() const
{
    return is_editing_host() || get_attribute(HTML::AttributeNames::tabindex).has_value();
}

// https://html.spec.whatwg.org/multipage/interaction.html#dom-iscontenteditable
bool HTMLElement::is_content_editable() const
{
    // The isContentEditable IDL attribute, on getting, must return true if the element is either an editing host or
    // editable, and false otherwise.
    return is_editable_or_editing_host();
}

StringView HTMLElement::content_editable() const
{
    switch (m_content_editable_state) {
    case ContentEditableState::True:
        return "true"sv;
    case ContentEditableState::False:
        return "false"sv;
    case ContentEditableState::PlaintextOnly:
        return "plaintext-only"sv;
    case ContentEditableState::Inherit:
        return "inherit"sv;
    }
    VERIFY_NOT_REACHED();
}

// https://html.spec.whatwg.org/multipage/interaction.html#contenteditable
WebIDL::ExceptionOr<void> HTMLElement::set_content_editable(StringView content_editable)
{
    if (content_editable.equals_ignoring_ascii_case("inherit"sv)) {
        remove_attribute(HTML::AttributeNames::contenteditable);
        return {};
    }
    if (content_editable.equals_ignoring_ascii_case("true"sv)) {
        MUST(set_attribute(HTML::AttributeNames::contenteditable, "true"_string));
        return {};
    }
    if (content_editable.equals_ignoring_ascii_case("plaintext-only"sv)) {
        MUST(set_attribute(HTML::AttributeNames::contenteditable, "plaintext-only"_string));
        return {};
    }
    if (content_editable.equals_ignoring_ascii_case("false"sv)) {
        MUST(set_attribute(HTML::AttributeNames::contenteditable, "false"_string));
        return {};
    }
    return WebIDL::SyntaxError::create(realm(), "Invalid contentEditable value, must be 'true', 'false', 'plaintext-only' or 'inherit'"_utf16);
}

// https://html.spec.whatwg.org/multipage/dom.html#set-the-inner-text-steps
void HTMLElement::set_inner_text(Utf16View const& text)
{
    // 1. Let fragment be the rendered text fragment for value given element's node document.
    auto fragment = rendered_text_fragment(text);

    // 2. Replace all with fragment within element.
    replace_all(fragment);

    set_needs_style_update(true);
}

// https://html.spec.whatwg.org/multipage/dom.html#merge-with-the-next-text-node
static void merge_with_the_next_text_node(DOM::Text& node)
{
    // 1. Let next be node's next sibling.
    auto next = node.next_sibling();

    // 2. If next is not a Text node, then return.
    auto* text = as_if<DOM::Text>(next);
    if (!text)
        return;

    // 3. Replace data with node, node's data's length, 0, and next's data.
    MUST(node.replace_data(node.length_in_utf16_code_units(), 0, text->data()));

    // 4. Remove next.
    next->remove();
}

// https://html.spec.whatwg.org/multipage/dom.html#the-innertext-idl-attribute:dom-outertext-2
WebIDL::ExceptionOr<void> HTMLElement::set_outer_text(Utf16View const& value)
{
    // 1. If this's parent is null, then throw a "NoModificationAllowedError" DOMException.
    if (!parent())
        return WebIDL::NoModificationAllowedError::create(realm(), "setOuterText: parent is null"_utf16);

    // 2. Let next be this's next sibling.
    auto* next = next_sibling();

    // 3. Let previous be this's previous sibling.
    auto* previous = previous_sibling();

    // 4. Let fragment be the rendered text fragment for the given value given this's node document.
    auto fragment = rendered_text_fragment(value);

    // 5. If fragment has no children, then append a new Text node whose data is the empty string and node document is this's node document to fragment.
    if (!fragment->has_children())
        MUST(fragment->append_child(document().create_text_node({})));

    // 6. Replace this with fragment within this's parent.
    MUST(parent()->replace_child(fragment, *this));

    // 7. If next is non-null and next's previous sibling is a Text node, then merge with the next text node given next's previous sibling.
    if (next && is<DOM::Text>(next->previous_sibling()))
        merge_with_the_next_text_node(static_cast<DOM::Text&>(*next->previous_sibling()));

    // 8. If previous is a Text node, then merge with the next text node given previous.
    if (auto* previous_text = as_if<DOM::Text>(previous))
        merge_with_the_next_text_node(*previous_text);

    set_needs_style_update(true);
    return {};
}

// https://html.spec.whatwg.org/multipage/dom.html#rendered-text-fragment
GC::Ref<DOM::DocumentFragment> HTMLElement::rendered_text_fragment(Utf16View const& input)
{
    // 1. Let fragment be a new DocumentFragment whose node document is document.
    auto fragment = realm().create<DOM::DocumentFragment>(document());

    // 2. Let position be a position variable for input, initially pointing at the start of input.
    size_t position = 0;

    // 3. Let text be the empty string.
    // 4. While position is not past the end of input:
    while (position < input.length_in_code_units()) {
        auto start = position;

        // 1. Collect a sequence of code points that are not U+000A LF or U+000D CR from input given position, and set
        //    text to the result.
        while (position < input.length_in_code_units() && !first_is_one_of(input.code_unit_at(position), u'\n', u'\r'))
            ++position;

        auto text = input.substring_view(start, position - start);

        // 2. If text is not the empty string, then append a new Text node whose data is text and node document is
        //    document to fragment.
        if (!text.is_empty()) {
            MUST(fragment->append_child(document().create_text_node(Utf16String::from_utf16(text))));
        }

        // 3. While position is not past the end of input, and the code point at position is either U+000A LF or U+000D CR:
        while (position < input.length_in_code_units() && first_is_one_of(input.code_unit_at(position), u'\n', u'\r')) {
            // 1. If the code point at position is U+000D CR and the next code point is U+000A LF, then advance position
            //    to the next code point in input.
            if (input.code_unit_at(position) == '\r') {
                if (position + 1 < input.length_in_code_units() && input.code_unit_at(position + 1) == '\n')
                    ++position;
            }

            // 2. Advance position to the next code point in input.
            ++position;

            // 3. Append the result of creating an element given document, "br", and the HTML namespace to fragment.
            auto br_element = MUST(DOM::create_element(document(), HTML::TagNames::br, Namespace::HTML));
            MUST(fragment->append_child(br_element));
        }
    }

    // 5. Return fragment.
    return fragment;
}

struct RequiredLineBreakCount {
    int count { 0 };
};

// https://html.spec.whatwg.org/multipage/dom.html#rendered-text-collection-steps
static Vector<Variant<Utf16String, RequiredLineBreakCount>> rendered_text_collection_steps(DOM::Node const& node)
{
    // 1. Let items be the result of running the rendered text collection steps with each child node of node in tree
    //    order, and then concatenating the results to a single list.
    Vector<Variant<Utf16String, RequiredLineBreakCount>> items;
    node.for_each_child([&](auto const& child) {
        auto child_items = rendered_text_collection_steps(child);
        items.extend(move(child_items));
        return IterationDecision::Continue;
    });

    // NOTE: Steps are re-ordered here a bit.

    // 3. If node is not being rendered, then return items.
    //    For the purpose of this step, the following elements must act as described
    //    if the computed value of the 'display' property is not 'none':
    //    FIXME: - select elements have an associated non-replaced inline CSS box whose child boxes include only those of optgroup and option element child nodes;
    //    FIXME: - optgroup elements have an associated non-replaced block-level CSS box whose child boxes include only those of option element child nodes; and
    //    FIXME: - option elements have an associated non-replaced block-level CSS box whose child boxes are as normal for non-replaced block-level CSS boxes.
    auto* layout_node = node.layout_node();
    if (!layout_node)
        return items;

    auto const& computed_values = layout_node->computed_values();

    // 2. If node's computed value of 'visibility' is not 'visible', then return items.
    if (computed_values.visibility() != CSS::Visibility::Visible)
        return items;

    // AD-HOC: If node's computed value of 'content-visibility' is 'hidden', then return items.
    if (computed_values.content_visibility() == CSS::ContentVisibility::Hidden)
        return items;

    // 4. If node is a Text node, then for each CSS text box produced by node, in content order, compute the text of the
    //    box after application of the CSS 'white-space' processing rules and 'text-transform' rules, set items to the
    //    list of the resulting strings, and return items.

    //    FIXME: The CSS 'white-space' processing rules are slightly modified: collapsible spaces at the end of lines are
    //    always collapsed, but they are only removed if the line is the last line of the block, or it ends with a br
    //    element. Soft hyphens should be preserved. [CSSTEXT]

    if (auto const* layout_text_node = as_if<Layout::TextNode>(layout_node)) {
        Layout::TextNode::ChunkIterator iterator { *layout_text_node, false, false };
        while (true) {
            auto chunk = iterator.next();
            if (!chunk.has_value())
                break;
            items.append(Utf16String::from_utf16(chunk.release_value().view));
        }
        return items;
    }

    // 5. If node is a br element, then append a string containing a single U+000A LF code point to items.
    if (is<HTML::HTMLBRElement>(node)) {
        items.append("\n"_utf16);
        return items;
    }

    auto display = computed_values.display();

    // 6. If node's computed value of 'display' is 'table-cell', and node's CSS box is not the last 'table-cell' box of its enclosing 'table-row' box, then append a string containing a single U+0009 TAB code point to items.
    if (display.is_table_cell() && node.next_sibling())
        items.append("\t"_utf16);

    // 7. If node's computed value of 'display' is 'table-row', and node's CSS box is not the last 'table-row' box of the nearest ancestor 'table' box, then append a string containing a single U+000A LF code point to items.
    if (display.is_table_row() && node.next_sibling())
        items.append("\n"_utf16);

    // 8. If node is a p element, then append 2 (a required line break count) at the beginning and end of items.
    if (is<HTML::HTMLParagraphElement>(node)) {
        items.prepend(RequiredLineBreakCount { 2 });
        items.append(RequiredLineBreakCount { 2 });
    }

    // 9. If node's used value of 'display' is block-level or 'table-caption', then append 1 (a required line break count) at the beginning and end of items. [CSSDISPLAY]
    if (display.is_block_outside() || display.is_table_caption()) {
        items.prepend(RequiredLineBreakCount { 1 });
        items.append(RequiredLineBreakCount { 1 });
    }

    // 10. Return items.
    return items;
}

// https://html.spec.whatwg.org/multipage/dom.html#get-the-text-steps
Utf16String HTMLElement::get_the_text_steps()
{
    // 1. If element is not being rendered or if the user agent is a non-CSS user agent, then return element's descendant text content.
    document().update_layout(DOM::UpdateLayoutReason::HTMLElementGetTheTextSteps);
    if (!layout_node())
        return descendant_text_content();

    // 2. Let results be a new empty list.
    Vector<Variant<Utf16String, RequiredLineBreakCount>> results;

    // 3. For each child node node of element:
    for_each_child([&](Node const& node) {
        // 1. Let current be the list resulting in running the rendered text collection steps with node.
        //    Each item in results will either be a string or a positive integer (a required line break count).
        auto current = rendered_text_collection_steps(node);

        // 2. For each item item in current, append item to results.
        results.extend(move(current));
        return IterationDecision::Continue;
    });

    // 4. Remove any items from results that are the empty string.
    results.remove_all_matching([](auto& item) {
        return item.visit(
            [](Utf16String const& string) { return string.is_empty(); },
            [](RequiredLineBreakCount const&) { return false; });
    });

    // 5. Remove any runs of consecutive required line break count items at the start or end of results.
    while (!results.is_empty() && results.first().has<RequiredLineBreakCount>())
        results.take_first();
    while (!results.is_empty() && results.last().has<RequiredLineBreakCount>())
        results.take_last();

    // 6. Replace each remaining run of consecutive required line break count items with a string consisting of as many
    //    U+000A LF code points as the maximum of the values in the required line break count items.
    StringBuilder builder(StringBuilder::Mode::UTF16);
    for (size_t i = 0; i < results.size(); ++i) {
        results[i].visit(
            [&](Utf16String const& string) {
                builder.append(string);
            },
            [&](RequiredLineBreakCount const& line_break_count) {
                int max_line_breaks = line_break_count.count;
                size_t j = i + 1;
                while (j < results.size() && results[j].has<RequiredLineBreakCount>()) {
                    max_line_breaks = max(max_line_breaks, results[j].get<RequiredLineBreakCount>().count);
                    ++j;
                }

                // Skip over the run of required line break counts.
                i = j - 1;

                builder.append_repeated('\n', max_line_breaks);
            });
    }

    // 7. Return the concatenation of the string items in results.
    return builder.to_utf16_string();
}

// https://html.spec.whatwg.org/multipage/dom.html#dom-innertext
Utf16String HTMLElement::inner_text()
{
    // The innerText and outerText getter steps are to return the result of running get the text steps with this.
    return get_the_text_steps();
}

// https://html.spec.whatwg.org/multipage/dom.html#dom-outertext
Utf16String HTMLElement::outer_text()
{
    // The innerText and outerText getter steps are to return the result of running get the text steps with this.
    return get_the_text_steps();
}

// https://drafts.csswg.org/cssom-view/#dom-htmlelement-scrollparent
GC::Ptr<DOM::Element> HTMLElement::scroll_parent() const
{
    // NOTE: We have to ensure that the layout is up-to-date before querying the layout tree.
    const_cast<DOM::Document&>(document()).update_layout(DOM::UpdateLayoutReason::HTMLElementScrollParent);

    // 1. If any of the following holds true, return null and terminate this algorithm:
    //    - The element does not have an associated box.
    //    - The element is the root element.
    //    - The element is the body element.
    //    - FIXME: The element’s computed value of the position property is fixed and no ancestor establishes a fixed position containing block.
    if (!layout_node())
        return nullptr;
    if (is_document_element())
        return nullptr;
    if (is_html_body_element())
        return nullptr;

    // 2. Let ancestor be the containing block of the element in the flat tree and repeat these substeps:
    auto ancestor = layout_node()->containing_block();
    while (ancestor) {
        // 1. If ancestor is the initial containing block, return the scrollingElement for the element’s document if it
        //    is not closed-shadow-hidden from the element, otherwise return null.
        if (ancestor->is_viewport()) {
            auto const scrolling_element = document().scrolling_element();
            if (scrolling_element && !scrolling_element->is_closed_shadow_hidden_from(*this))
                return const_cast<Element*>(scrolling_element.ptr());
            return nullptr;
        }

        // 2. If ancestor is not closed-shadow-hidden from the element, and is a scroll container, terminate this
        //    algorithm and return ancestor.
        if ((ancestor->dom_node() && !ancestor->dom_node()->is_closed_shadow_hidden_from(*this))
            && ancestor->is_scroll_container()) {
            return const_cast<Element*>(static_cast<DOM::Element const*>(ancestor->dom_node()));
        }

        // FIXME: 3. If the computed value of the position property of ancestor is fixed, and no ancestor establishes a fixed
        //    position containing block, terminate this algorithm and return null.

        // 4. Let ancestor be the containing block of ancestor in the flat tree.
        ancestor = ancestor->containing_block();
    }

    return nullptr;
}

// https://www.w3.org/TR/cssom-view-1/#dom-htmlelement-offsetparent
GC::Ptr<DOM::Element> HTMLElement::offset_parent() const
{
    const_cast<DOM::Document&>(document()).update_layout(DOM::UpdateLayoutReason::HTMLElementOffsetParent);

    // 1. If any of the following holds true return null and terminate this algorithm:
    //    - The element does not have an associated box.
    //    - The element is the root element.
    //    - The element is the HTML body element.
    //    - The element’s computed value of the position property is fixed.
    if (!layout_node())
        return nullptr;
    if (is_document_element())
        return nullptr;
    if (is_html_body_element())
        return nullptr;
    if (layout_node()->is_fixed_position())
        return nullptr;

    // 2. Let ancestor be the parent of the element in the flat tree and repeat these substeps:
    auto ancestor = shadow_including_first_ancestor_of_type<DOM::Element>();
    while (true) {
        bool ancestor_is_closed_shadow_hidden = ancestor->is_closed_shadow_hidden_from(*this);
        // 1. If ancestor is closed-shadow-hidden from the element and its computed value of the position property is
        //   fixed, terminate this algorithm and return null.
        if (ancestor_is_closed_shadow_hidden && ancestor->computed_properties()->position() == CSS::Positioning::Fixed)
            return nullptr;

        // 2. If ancestor is not closed-shadow-hidden from the element and satisfies at least one of the following,
        //    terminate this algorithm and return ancestor.
        if (!ancestor_is_closed_shadow_hidden) {
            // - ancestor is a containing block of absolutely-positioned descendants (regardless of whether there are
            //    any absolutely-positioned descendants).
            if (ancestor->layout_node()->is_positioned())
                return const_cast<Element*>(ancestor);

            // - FIXME: The element has a different effective zoom than ancestor.

            // - It is the body element.
            if (ancestor->is_html_body_element())
                return const_cast<Element*>(ancestor);

            // - The computed value of the position property of the element is static and the ancestor is one of the following HTML elements: td, th, or table.
            if (computed_properties()->position() == CSS::Positioning::Static && ancestor->local_name().is_one_of(HTML::TagNames::td, HTML::TagNames::th, HTML::TagNames::table))
                return const_cast<Element*>(ancestor);
        }

        // 3. If there is no more parent of ancestor in the flat tree, terminate this algorithm and return null.
        auto parent_of_ancestor = ancestor->shadow_including_first_ancestor_of_type<DOM::Element>();
        if (!parent_of_ancestor)
            return nullptr;

        // 4. Let ancestor be the parent of ancestor in the flat tree.
        ancestor = parent_of_ancestor;
    }
}

// https://www.w3.org/TR/cssom-view-1/#dom-htmlelement-offsettop
int HTMLElement::offset_top() const
{
    // 1. If the element is the HTML body element or does not have any associated CSS layout box
    //    return zero and terminate this algorithm.
    if (is<HTML::HTMLBodyElement>(*this))
        return 0;

    // NOTE: Ensure that layout is up-to-date before looking at metrics.
    const_cast<DOM::Document&>(document()).update_layout(DOM::UpdateLayoutReason::HTMLElementOffsetTop);

    if (!paintable_box())
        return 0;

    CSSPixels top_border_edge_of_element = paintable_box()->absolute_united_border_box_rect().y();

    // 2. If the offsetParent of the element is null
    //    return the y-coordinate of the top border edge of the first CSS layout box associated with the element,
    //    relative to the initial containing block origin,
    //    ignoring any transforms that apply to the element and its ancestors, and terminate this algorithm.
    auto offset_parent = this->offset_parent();
    if (!offset_parent || !offset_parent->layout_node()) {
        return top_border_edge_of_element.to_int();
    }

    // 3. Return the result of subtracting the y-coordinate of the top padding edge
    //    of the first box associated with the offsetParent of the element
    //    from the y-coordinate of the top border edge of the first box associated with the element,
    //    relative to the initial containing block origin,
    //    ignoring any transforms that apply to the element and its ancestors.

    // NOTE: We give special treatment to the body element to match other browsers.
    //       Spec bug: https://github.com/w3c/csswg-drafts/issues/10549

    CSSPixels top_padding_edge_of_offset_parent;
    if (offset_parent->is_html_body_element() && !offset_parent->paintable_box()->is_positioned()) {
        top_padding_edge_of_offset_parent = 0;
    } else {
        top_padding_edge_of_offset_parent = offset_parent->paintable_box()->absolute_united_padding_box_rect().y();
    }
    return (top_border_edge_of_element - top_padding_edge_of_offset_parent).to_int();
}

// https://www.w3.org/TR/cssom-view-1/#dom-htmlelement-offsetleft
int HTMLElement::offset_left() const
{
    // 1. If the element is the HTML body element or does not have any associated CSS layout box return zero and terminate this algorithm.
    if (is<HTML::HTMLBodyElement>(*this))
        return 0;

    // NOTE: Ensure that layout is up-to-date before looking at metrics.
    const_cast<DOM::Document&>(document()).update_layout(DOM::UpdateLayoutReason::HTMLElementOffsetLeft);

    if (!paintable_box())
        return 0;

    CSSPixels left_border_edge_of_element = paintable_box()->absolute_united_border_box_rect().x();

    // 2. If the offsetParent of the element is null
    //    return the x-coordinate of the left border edge of the first CSS layout box associated with the element,
    //    relative to the initial containing block origin,
    //    ignoring any transforms that apply to the element and its ancestors, and terminate this algorithm.
    auto offset_parent = this->offset_parent();
    if (!offset_parent || !offset_parent->layout_node()) {
        return left_border_edge_of_element.to_int();
    }

    // 3. Return the result of subtracting the x-coordinate of the left padding edge
    //    of the first CSS layout box associated with the offsetParent of the element
    //    from the x-coordinate of the left border edge of the first CSS layout box associated with the element,
    //    relative to the initial containing block origin,
    //    ignoring any transforms that apply to the element and its ancestors.

    // NOTE: We give special treatment to the body element to match other browsers.
    //       Spec bug: https://github.com/w3c/csswg-drafts/issues/10549

    CSSPixels left_padding_edge_of_offset_parent;
    if (offset_parent->is_html_body_element() && !offset_parent->paintable_box()->is_positioned()) {
        left_padding_edge_of_offset_parent = 0;
    } else {
        left_padding_edge_of_offset_parent = offset_parent->paintable_box()->absolute_united_padding_box_rect().x();
    }
    return (left_border_edge_of_element - left_padding_edge_of_offset_parent).to_int();
}

// https://drafts.csswg.org/cssom-view/#dom-htmlelement-offsetwidth
int HTMLElement::offset_width() const
{
    // NOTE: Ensure that layout is up-to-date before looking at metrics.
    const_cast<DOM::Document&>(document()).update_layout(DOM::UpdateLayoutReason::HTMLElementOffsetWidth);

    // 1. If the element does not have any associated box return zero and terminate this algorithm.
    auto const* box = paintable_box();
    if (!box)
        return 0;

    // 2. Return the unscaled width of the axis-aligned bounding box of the border boxes of all fragments generated by
    //    the element’s principal box, ignoring any transforms that apply to the element and its ancestors.
    //
    //    If the element’s principal box is an inline-level box which was "split" by a block-level descendant, also
    //    include fragments generated by the block-level descendants, unless they are zero width or height.
    return box->absolute_united_border_box_rect().width().to_int();
}

// https://drafts.csswg.org/cssom-view/#dom-htmlelement-offsetheight
int HTMLElement::offset_height() const
{
    // NOTE: Ensure that layout is up-to-date before looking at metrics.
    const_cast<DOM::Document&>(document()).update_layout(DOM::UpdateLayoutReason::HTMLElementOffsetHeight);

    // 1. If the element does not have any associated box return zero and terminate this algorithm.
    auto const* box = paintable_box();
    if (!box)
        return 0;

    // 2. Return the unscaled height of the axis-aligned bounding box of the border boxes of all fragments generated by
    //    the element’s principal box, ignoring any transforms that apply to the element and its ancestors.
    //
    //    If the element’s principal box is an inline-level box which was "split" by a block-level descendant, also
    //    include fragments generated by the block-level descendants, unless they are zero width or height.
    return box->absolute_united_border_box_rect().height().to_int();
}

// https://html.spec.whatwg.org/multipage/links.html#cannot-navigate
bool HTMLElement::cannot_navigate() const
{
    // An element element cannot navigate if one of the following is true:

    // - element's node document is not fully active
    if (!document().is_fully_active())
        return true;

    // - element is not an a element and is not connected.
    return !is<HTML::HTMLAnchorElement>(this) && !is_connected();
}

void HTMLElement::attribute_changed(FlyString const& name, Optional<String> const& old_value, Optional<String> const& value, Optional<FlyString> const& namespace_)
{
    Base::attribute_changed(name, old_value, value, namespace_);
    HTMLOrSVGElement::attribute_changed(name, old_value, value, namespace_);

    if (name == HTML::AttributeNames::contenteditable) {
        if (!value.has_value()) {
            // No value maps to the "inherit" state.
            m_content_editable_state = ContentEditableState::Inherit;
        } else if (value->is_empty() || value->equals_ignoring_ascii_case("true"sv)) {
            // "true", an empty string or a missing value map to the "true" state.
            m_content_editable_state = ContentEditableState::True;
        } else if (value->equals_ignoring_ascii_case("false"sv)) {
            // "false" maps to the "false" state.
            m_content_editable_state = ContentEditableState::False;
        } else if (value->equals_ignoring_ascii_case("plaintext-only"sv)) {
            // "plaintext-only" maps to the "plaintext-only" state.
            m_content_editable_state = ContentEditableState::PlaintextOnly;
        } else {
            // Having an invalid value maps to the "inherit" state.
            m_content_editable_state = ContentEditableState::Inherit;
        }
    } else if (name == HTML::AttributeNames::inert) {
        // https://html.spec.whatwg.org/multipage/interaction.html#the-inert-attribute
        // The inert attribute is a boolean attribute that indicates, by its presence, that the element and all its flat tree descendants which don't otherwise escape inertness
        // (such as modal dialogs) are to be made inert by the user agent.
        auto is_inert = value.has_value();
        set_subtree_inertness(is_inert);
    }

    // 1. If namespace is not null, or localName is not the name of an event handler content attribute on element, then return.
    // FIXME: Add the namespace part once we support attribute namespaces.
#undef __ENUMERATE
#define __ENUMERATE(attribute_name, event_name)                     \
    if (name == HTML::AttributeNames::attribute_name) {             \
        element_event_handler_attribute_changed(event_name, value); \
    }
    ENUMERATE_GLOBAL_EVENT_HANDLERS(__ENUMERATE)
#undef __ENUMERATE

    [&]() {
        // https://html.spec.whatwg.org/multipage/popover.html#the-popover-attribute:concept-element-attributes-change-ext
        // https://whatpr.org/html/9457/popover.html#the-popover-attribute:concept-element-attributes-change-ext
        // The following attribute change steps, given element, localName, oldValue, value, and namespace, are used for all HTML elements:

        // 1. If namespace is not null, then return.
        if (namespace_.has_value())
            return;

        // 2. If localName is not popover, then return.
        if (name != HTML::AttributeNames::popover)
            return;

        // 3. If element's popover visibility state is in the showing state
        //    and oldValue and value are in different states,
        //    then run the hide popover algorithm given element, true, true, false, true, and null.
        if (m_popover_visibility_state == PopoverVisibilityState::Showing
            && popover_value_to_state(old_value) != popover_value_to_state(value))
            MUST(hide_popover(FocusPreviousElement::Yes, FireEvents::Yes, ThrowExceptions::No, IgnoreDomState::Yes, nullptr));
    }();
}

void HTMLElement::set_subtree_inertness(bool is_inert)
{
    set_inert(is_inert);
    for_each_in_subtree_of_type<HTMLElement>([&](auto& html_element) {
        if (html_element.has_attribute(HTML::AttributeNames::inert))
            return TraversalDecision::SkipChildrenAndContinue;
        // FIXME: Exclude elements that should escape inertness.
        html_element.set_inert(is_inert);
        return TraversalDecision::Continue;
    });
}

WebIDL::ExceptionOr<void> HTMLElement::cloned(Web::DOM::Node& copy, bool clone_children) const
{
    TRY(Base::cloned(copy, clone_children));
    TRY(HTMLOrSVGElement::cloned(copy, clone_children));
    return {};
}

void HTMLElement::inserted()
{
    Base::inserted();
    HTMLOrSVGElement::inserted();

    if (auto* parent_html_element = first_ancestor_of_type<HTMLElement>(); parent_html_element && parent_html_element->is_inert() && !has_attribute(HTML::AttributeNames::inert))
        set_subtree_inertness(true);
}

// https://html.spec.whatwg.org/multipage/webappapis.html#fire-a-synthetic-pointer-event
bool HTMLElement::fire_a_synthetic_pointer_event(FlyString const& type, DOM::Element& target, bool not_trusted)
{
    // 1. Let event be the result of creating an event using PointerEvent.
    // 2. Initialize event's type attribute to e.
    auto event = UIEvents::PointerEvent::create(realm(), type);

    // 3. Initialize event's bubbles and cancelable attributes to true.
    event->set_bubbles(true);
    event->set_cancelable(true);

    // 4. Set event's composed flag.
    event->set_composed(true);

    // 5. If the not trusted flag is set, initialize event's isTrusted attribute to false.
    if (not_trusted) {
        event->set_is_trusted(false);
    }

    // FIXME: 6. Initialize event's ctrlKey, shiftKey, altKey, and metaKey attributes according to the current state
    //           of the key input device, if any (false for any keys that are not available).

    // FIXME: 7. Initialize event's view attribute to target's node document's Window object, if any, and null otherwise.

    // FIXME: 8. event's getModifierState() method is to return values appropriately describing the current state of the key input device.

    // 9. Return the result of dispatching event at target.
    return target.dispatch_event(event);
}

// https://html.spec.whatwg.org/multipage/forms.html#dom-lfe-labels-dev
GC::Ptr<DOM::NodeList> HTMLElement::labels()
{
    // Labelable elements and all input elements have a live NodeList object associated with them that represents the list of label elements, in tree order,
    // whose labeled control is the element in question. The labels IDL attribute of labelable elements that are not form-associated custom elements,
    // and the labels IDL attribute of input elements, on getting, must return that NodeList object, and that same value must always be returned,
    // unless this element is an input element whose type attribute is in the Hidden state, in which case it must instead return null.
    if (!is_labelable())
        return {};

    if (!m_labels) {
        m_labels = DOM::LiveNodeList::create(realm(), root(), DOM::LiveNodeList::Scope::Descendants, [&](auto& node) {
            auto* label_element = as_if<HTMLLabelElement>(node);
            return label_element && label_element->control() == this;
        });
    }

    return m_labels;
}

// https://html.spec.whatwg.org/multipage/interaction.html#dom-hidden
Variant<bool, double, String> HTMLElement::hidden() const
{
    // 1. If the hidden attribute is in the hidden until found state, then return "until-found".
    auto const& hidden = get_attribute(HTML::AttributeNames::hidden);
    if (hidden.has_value() && hidden->equals_ignoring_ascii_case("until-found"sv))
        return "until-found"_string;
    // 2. If the hidden attribute is set, then return true.
    if (hidden.has_value())
        return true;
    // 3. Return false.
    return false;
}

void HTMLElement::set_hidden(Variant<bool, double, String> const& given_value)
{
    // 1. If the given value is a string that is an ASCII case-insensitive match for "until-found", then set the hidden attribute to "until-found".
    if (given_value.has<String>()) {
        auto const& string = given_value.get<String>();
        if (string.equals_ignoring_ascii_case("until-found"sv)) {
            MUST(set_attribute(HTML::AttributeNames::hidden, "until-found"_string));
            return;
        }
        // 3. Otherwise, if the given value is the empty string, then remove the hidden attribute.
        if (string.is_empty()) {
            remove_attribute(HTML::AttributeNames::hidden);
            return;
        }
        // 4. Otherwise, if the given value is null, then remove the hidden attribute.
        if (string.equals_ignoring_ascii_case("null"sv) || string.equals_ignoring_ascii_case("undefined"sv)) {
            remove_attribute(HTML::AttributeNames::hidden);
            return;
        }
    }
    // 2. Otherwise, if the given value is false, then remove the hidden attribute.
    else if (given_value.has<bool>()) {
        if (!given_value.get<bool>()) {
            remove_attribute(HTML::AttributeNames::hidden);
            return;
        }
    }
    // 5. Otherwise, if the given value is 0, then remove the hidden attribute.
    // 6. Otherwise, if the given value is NaN, then remove the hidden attribute.
    else if (given_value.has<double>()) {
        auto const& double_value = given_value.get<double>();
        if (double_value == 0 || isnan(double_value)) {
            remove_attribute(HTML::AttributeNames::hidden);
            return;
        }
    }
    // 7. Otherwise, set the hidden attribute to the empty string.
    MUST(set_attribute(HTML::AttributeNames::hidden, ""_string));
}

// https://html.spec.whatwg.org/multipage/interaction.html#dom-click
void HTMLElement::click()
{
    // 1. If this element is a form control that is disabled, then return.
    if (auto* form_control = as_if<FormAssociatedElement>(this)) {
        if (!form_control->enabled())
            return;
    }

    // 2. If this element's click in progress flag is set, then return.
    if (m_click_in_progress)
        return;

    // 3. Set this element's click in progress flag.
    m_click_in_progress = true;

    // 4. Fire a synthetic pointer event named click at this element, with the not trusted flag set.
    fire_a_synthetic_pointer_event(HTML::EventNames::click, *this, true);

    // 5. Unset this element's click in progress flag.
    m_click_in_progress = false;
}

// https://html.spec.whatwg.org/multipage/custom-elements.html#form-associated-custom-element
bool HTMLElement::is_form_associated_custom_element()
{
    // An autonomous custom element is called a form-associated custom element if the element is associated with a
    // custom element definition whose form-associated field is set to true.
    auto definition = document().lookup_custom_element_definition(namespace_uri(), local_name(), is_value());
    return definition->form_associated();
}

Optional<ARIA::Role> HTMLElement::default_role() const
{
    // https://www.w3.org/TR/html-aria/#el-address
    if (local_name() == TagNames::address)
        return ARIA::Role::group;
    // https://www.w3.org/TR/html-aria/#el-article
    if (local_name() == TagNames::article)
        return ARIA::Role::article;
    // https://www.w3.org/TR/html-aria/#el-aside
    if (local_name() == TagNames::aside) {
        // https://w3c.github.io/html-aam/#el-aside
        for (auto ancestor = parent_element(); ancestor; ancestor = ancestor->parent_element()) {
            if (ancestor->local_name().is_one_of(TagNames::article, TagNames::aside, TagNames::nav, TagNames::section)
                && accessible_name(document()).value().is_empty())
                return ARIA::Role::generic;
        }
        // https://w3c.github.io/html-aam/#el-aside-ancestorbodymain
        return ARIA::Role::complementary;
    }
    // https://www.w3.org/TR/html-aria/#el-b
    if (local_name() == TagNames::b)
        return ARIA::Role::generic;
    // https://www.w3.org/TR/html-aria/#el-bdi
    if (local_name() == TagNames::bdi)
        return ARIA::Role::generic;
    // https://www.w3.org/TR/html-aria/#el-bdo
    if (local_name() == TagNames::bdo)
        return ARIA::Role::generic;
    // https://www.w3.org/TR/html-aria/#el-code
    if (local_name() == TagNames::code)
        return ARIA::Role::code;
    // https://w3c.github.io/html-aam/#el-dd
    if (local_name() == TagNames::dd)
        return ARIA::Role::definition;
    // https://wpt.fyi/results/html-aam/dir-role.tentative.html
    if (local_name() == TagNames::dir)
        return ARIA::Role::list;
    // https://w3c.github.io/html-aam/#el-dt
    if (local_name() == TagNames::dt)
        return ARIA::Role::term;
    // https://www.w3.org/TR/html-aria/#el-dfn
    if (local_name() == TagNames::dfn)
        return ARIA::Role::term;
    // https://www.w3.org/TR/html-aria/#el-em
    if (local_name() == TagNames::em)
        return ARIA::Role::emphasis;
    // https://www.w3.org/TR/html-aria/#el-figure
    if (local_name() == TagNames::figure)
        return ARIA::Role::figure;
    // https://www.w3.org/TR/html-aria/#el-footer
    // https://www.w3.org/TR/html-aria/#el-header
    if (local_name() == TagNames::footer || local_name() == TagNames::header) {
        // If not a descendant of an article, aside, main, nav or section element, or an element with role=article,
        // complementary, main, navigation or region then (footer) role=contentinfo (header) role=banner. Otherwise,
        // role=generic.
        for (auto ancestor = parent_element(); ancestor; ancestor = ancestor->parent_element()) {
            if (ancestor->local_name().is_one_of(TagNames::article, TagNames::aside, TagNames::main, TagNames::nav, TagNames::section)) {
                if (local_name() == TagNames::footer)
                    return ARIA::Role::sectionfooter;
                return ARIA::Role::sectionheader;
            }
            if (first_is_one_of(ancestor->role_or_default(), ARIA::Role::article, ARIA::Role::complementary, ARIA::Role::main, ARIA::Role::navigation, ARIA::Role::region)) {
                if (local_name() == TagNames::footer)
                    return ARIA::Role::sectionfooter;
                return ARIA::Role::sectionheader;
            }
        }
        // then (footer) role=contentinfo.
        if (local_name() == TagNames::footer)
            return ARIA::Role::contentinfo;
        // (header) role=banner
        return ARIA::Role::banner;
    }
    // https://www.w3.org/TR/html-aria/#el-hgroup
    if (local_name() == TagNames::hgroup)
        return ARIA::Role::group;
    // https://www.w3.org/TR/html-aria/#el-i
    if (local_name() == TagNames::i)
        return ARIA::Role::generic;
    // https://www.w3.org/TR/html-aria/#el-main
    if (local_name() == TagNames::main)
        return ARIA::Role::main;
    // https://www.w3.org/TR/html-aria/#el-mark
    if (local_name() == TagNames::mark)
        return ARIA::Role::mark;
    // https://www.w3.org/TR/html-aria/#el-nav
    if (local_name() == TagNames::nav)
        return ARIA::Role::navigation;
    // https://www.w3.org/TR/html-aria/#el-s
    if (local_name() == TagNames::s)
        return ARIA::Role::deletion;
    // https://www.w3.org/TR/html-aria/#el-samp
    if (local_name() == TagNames::samp)
        return ARIA::Role::generic;
    // https://www.w3.org/TR/html-aria/#el-search
    if (local_name() == TagNames::search)
        return ARIA::Role::search;
    // https://www.w3.org/TR/html-aria/#el-section
    if (local_name() == TagNames::section) {
        // role=region if the section element has an accessible name
        if (!accessible_name(document()).value().is_empty())
            return ARIA::Role::region;
        // Otherwise, role=generic
        return ARIA::Role::generic;
    }
    // https://www.w3.org/TR/html-aria/#el-small
    if (local_name() == TagNames::small)
        return ARIA::Role::generic;
    // https://www.w3.org/TR/html-aria/#el-strong
    if (local_name() == TagNames::strong)
        return ARIA::Role::strong;
    // https://www.w3.org/TR/html-aria/#el-sub
    if (local_name() == TagNames::sub)
        return ARIA::Role::subscript;
    // https://www.w3.org/TR/html-aria/#el-summary
    if (local_name() == TagNames::summary)
        return ARIA::Role::button;
    // https://www.w3.org/TR/html-aria/#el-sup
    if (local_name() == TagNames::sup)
        return ARIA::Role::superscript;
    // https://www.w3.org/TR/html-aria/#el-u
    if (local_name() == TagNames::u)
        return ARIA::Role::generic;

    return {};
}

// https://html.spec.whatwg.org/multipage/semantics.html#get-an-element's-target
String HTMLElement::get_an_elements_target(Optional<String> target) const
{
    // To get an element's target, given an a, area, or form element element, and an optional string-or-null target (default null), run these steps:

    // 1. If target is null, then:
    if (!target.has_value()) {
        // 1. If element has a target attribute, then set target to that attribute's value.
        if (auto maybe_target = attribute(AttributeNames::target); maybe_target.has_value()) {
            target = maybe_target.release_value();
        }
        // 2. Otherwise, if element's node document contains a base element with a target attribute,
        //    set target to the value of the target attribute of the first such base element.
        if (auto base_element = document().first_base_element_with_target_in_tree_order())
            target = base_element->attribute(AttributeNames::target);
    }

    // 2. If target is not null, and contains an ASCII tab or newline and a U+003C (<), then set target to "_blank".
    if (target.has_value() && target->bytes_as_string_view().contains("\t\n\r"sv) && target->contains('<'))
        target = "_blank"_string;

    // 3. Return target.
    return target.value_or({});
}

// https://html.spec.whatwg.org/multipage/links.html#get-an-element's-noopener
TokenizedFeature::NoOpener HTMLElement::get_an_elements_noopener(URL::URL const& url, StringView target) const
{
    // To get an element's noopener, given an a, area, or form element element, a URL record url, and a string target,
    // perform the following steps. They return a boolean.
    auto rel = MUST(get_attribute_value(HTML::AttributeNames::rel).to_lowercase());
    auto link_types = rel.bytes_as_string_view().split_view_if(Infra::is_ascii_whitespace);

    // 1. If element's link types include the noopener or noreferrer keyword, then return true.
    if (link_types.contains_slow("noopener"sv) || link_types.contains_slow("noreferrer"sv))
        return TokenizedFeature::NoOpener::Yes;

    // 2. If element's link types do not include the opener keyword and
    //    target is an ASCII case-insensitive match for "_blank", then return true.
    if (!link_types.contains_slow("opener"sv) && target.equals_ignoring_ascii_case("_blank"sv))
        return TokenizedFeature::NoOpener::Yes;

    // 3. If url's blob URL entry is not null:
    if (url.blob_url_entry().has_value()) {
        // 1. Let blobOrigin be url's blob URL entry's environment's origin.
        auto const& blob_origin = url.blob_url_entry()->environment.origin;

        // 2. Let topLevelOrigin be element's relevant settings object's top-level origin.
        auto const& top_level_origin = relevant_settings_object(*this).top_level_origin;

        // 3. If blobOrigin is not same site with topLevelOrigin, then return true.
        if (!blob_origin.is_same_site(top_level_origin.value()))
            return TokenizedFeature::NoOpener::Yes;
    }

    // 4. Return false.
    return TokenizedFeature::NoOpener::No;
}

WebIDL::ExceptionOr<GC::Ref<ElementInternals>> HTMLElement::attach_internals()
{
    // 1. If this's is value is not null, then throw a "NotSupportedError" DOMException.
    if (is_value().has_value())
        return WebIDL::NotSupportedError::create(realm(), "ElementInternals cannot be attached to a customized built-in element"_utf16);

    // 2. Let definition be the result of looking up a custom element definition given this's node document, its namespace, its local name, and null as the is value.
    auto definition = document().lookup_custom_element_definition(namespace_uri(), local_name(), is_value());

    // 3. If definition is null, then throw an "NotSupportedError" DOMException.
    if (!definition)
        return WebIDL::NotSupportedError::create(realm(), "ElementInternals cannot be attached to an element that is not a custom element"_utf16);

    // 4. If definition's disable internals is true, then throw a "NotSupportedError" DOMException.
    if (definition->disable_internals())
        return WebIDL::NotSupportedError::create(realm(), "ElementInternals are disabled for this custom element"_utf16);

    // 5. If this's attached internals is non-null, then throw an "NotSupportedError" DOMException.
    if (m_attached_internals)
        return WebIDL::NotSupportedError::create(realm(), "ElementInternals already attached"_utf16);

    // 6. If this's custom element state is not "precustomized" or "custom", then throw a "NotSupportedError" DOMException.
    if (!first_is_one_of(custom_element_state(), DOM::CustomElementState::Precustomized, DOM::CustomElementState::Custom))
        return WebIDL::NotSupportedError::create(realm(), "Custom element is in an invalid state to attach ElementInternals"_utf16);

    // 7. Set this's attached internals to a new ElementInternals instance whose target element is this.
    auto internals = ElementInternals::create(realm(), *this);

    m_attached_internals = internals;

    // 8. Return this's attached internals.
    return { internals };
}

Optional<String> HTMLElement::popover_value_to_state(Optional<String> value)
{
    if (!value.has_value())
        return {};

    if (value.value().is_empty() || value.value().equals_ignoring_ascii_case("auto"sv))
        return "auto"_string;

    if (value.value().equals_ignoring_ascii_case("hint"sv))
        return "hint"_string;

    return "manual"_string;
}

// https://html.spec.whatwg.org/multipage/popover.html#dom-popover
Optional<String> HTMLElement::popover() const
{
    // FIXME: This should probably be `Reflect` in the IDL.
    // The popover IDL attribute must reflect the popover attribute, limited to only known values.
    auto value = get_attribute(HTML::AttributeNames::popover);
    return popover_value_to_state(value);
}

// https://html.spec.whatwg.org/multipage/popover.html#dom-popover
WebIDL::ExceptionOr<void> HTMLElement::set_popover(Optional<String> value)
{
    // FIXME: This should probably be `Reflect` in the IDL.
    // The popover IDL attribute must reflect the popover attribute, limited to only known values.
    if (value.has_value())
        return set_attribute(HTML::AttributeNames::popover, value.release_value());

    remove_attribute(HTML::AttributeNames::popover);
    return {};
}

void HTMLElement::adjust_computed_style(CSS::ComputedProperties& style)
{
    // https://drafts.csswg.org/css-display-3/#unbox
    if (local_name() == HTML::TagNames::wbr) {
        if (style.display().is_contents())
            style.set_property(CSS::PropertyID::Display, CSS::DisplayStyleValue::create(CSS::Display::from_short(CSS::Display::Short::None)));
    }
}

// https://html.spec.whatwg.org/multipage/popover.html#check-popover-validity
// https://whatpr.org/html/9457/popover.html#check-popover-validity
WebIDL::ExceptionOr<bool> HTMLElement::check_popover_validity(ExpectedToBeShowing expected_to_be_showing, ThrowExceptions throw_exceptions, GC::Ptr<DOM::Document> expected_document, IgnoreDomState ignore_dom_state)
{
    // 1. If ignoreDomState is false and element's popover attribute is in the No Popover state, then:
    if (ignore_dom_state == IgnoreDomState::No && !popover().has_value()) {
        // 1.1. If throwExceptions is true, then throw a "NotSupportedError" DOMException.
        if (throw_exceptions == ThrowExceptions::Yes)
            return WebIDL::NotSupportedError::create(realm(), "Element is not a popover"_utf16);
        // 1.2. Return false.
        return false;
    }

    // 2. If any of the following are true:
    // - expectedToBeShowing is true and element's popover visibility state is not showing; or
    // - expectedToBeShowing is false and element's popover visibility state is not hidden,
    if ((expected_to_be_showing == ExpectedToBeShowing::Yes && m_popover_visibility_state != PopoverVisibilityState::Showing) || (expected_to_be_showing == ExpectedToBeShowing::No && m_popover_visibility_state != PopoverVisibilityState::Hidden)) {
        // then return false.
        return false;
    }

    // 3. If any of the following are true:
    // - ignoreDomState is false and element is not connected;
    // - element's node document is not fully active;
    // - ignoreDomState is false and expectedDocument is not null and element's node document is not expectedDocument;
    // - element is a dialog element and its is modal flag is set to true; or
    // - FIXME: element's fullscreen flag is set,
    // then:
    // 3.1 If throwExceptions is true, then throw an "InvalidStateError" DOMException.
    // 3.2 Return false.

    if ((ignore_dom_state == IgnoreDomState::No && !is_connected())
        || !document().is_fully_active()
        || (ignore_dom_state == IgnoreDomState::No && expected_document && &document() != expected_document)
        || (is<HTMLDialogElement>(*this) && as<HTMLDialogElement>(*this).is_modal())) {
        if (throw_exceptions == ThrowExceptions::Yes)
            return WebIDL::InvalidStateError::create(realm(), "Element is not in a valid state to show a popover"_utf16);
        return false;
    }

    // 4. Return true.
    return true;
}

// https://html.spec.whatwg.org/multipage/popover.html#dom-showpopover
WebIDL::ExceptionOr<void> HTMLElement::show_popover_for_bindings(ShowPopoverOptions const& options)
{
    // 1. Let invoker be options["source"] if it exists; otherwise, null.
    auto invoker = options.source;
    // 2. Run show popover given this, true, and invoker.
    return show_popover(ThrowExceptions::Yes, invoker);
}

// https://html.spec.whatwg.org/multipage/popover.html#show-popover
// https://whatpr.org/html/9457/popover.html#show-popover
WebIDL::ExceptionOr<void> HTMLElement::show_popover(ThrowExceptions throw_exceptions, GC::Ptr<HTMLElement> invoker)
{
    // 1. If the result of running check popover validity given element, false, throwExceptions, null and false is false, then return.
    if (!TRY(check_popover_validity(ExpectedToBeShowing::No, throw_exceptions, nullptr, IgnoreDomState::No)))
        return {};

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

    // 3. Assert: element's popover invoker is null.
    VERIFY(!m_popover_invoker);

    // 4. Assert: element is not in document's top layer.
    VERIFY(!in_top_layer());

    // 5. Let nestedShow be element's popover showing or hiding.
    auto nested_show = m_popover_showing_or_hiding;

    // 6. Let fireEvents be the boolean negation of nestedShow.
    FireEvents fire_events = nested_show ? FireEvents::No : FireEvents::Yes;

    // 7. Set element's popover showing or hiding to true.
    m_popover_showing_or_hiding = true;

    // 8. Let cleanupShowingFlag be the following steps:
    auto cleanup_showing_flag = [&nested_show, this] {
        // 8.1. If nestedShow is false, then set element's popover showing or hiding to false.
        if (!nested_show)
            m_popover_showing_or_hiding = false;
    };

    // 9. If the result of firing an event named beforetoggle, using ToggleEvent, with the cancelable attribute initialized to true, the oldState attribute initialized to "closed", the newState attribute initialized to "open" at element, and the source attribute initialized to invoker at element is false, then run cleanupShowingFlag and return.
    ToggleEventInit event_init {};
    event_init.old_state = "closed"_string;
    event_init.new_state = "open"_string;
    event_init.cancelable = true;
    if (!dispatch_event(ToggleEvent::create(realm(), HTML::EventNames::beforetoggle, move(event_init), invoker))) {
        cleanup_showing_flag();
        return {};
    }

    // 10. If the result of running check popover validity given element, false, throwExceptions, document, and false is false, then run cleanupShowingFlag and return.
    if (!TRY(check_popover_validity(ExpectedToBeShowing::No, throw_exceptions, document, IgnoreDomState::No))) {
        cleanup_showing_flag();
        return {};
    }

    // 11. Let shouldRestoreFocus be false.
    auto should_restore_focus = FocusPreviousElement::No;

    // 12. Let originalType be the current state of element's popover attribute.
    auto original_type = popover();

    // 13. Let stackToAppendTo be null.
    enum class StackToAppendTo : u8 {
        Null,
        Auto,
        Hint,
    };
    StackToAppendTo stack_to_append_to = StackToAppendTo::Null;

    // 16. If originalType is the auto state, then:
    if (original_type == "auto"sv) {
        // 1. Run close entire popover list given document's showing hint popover list, shouldRestoreFocus, and fireEvents.
        close_entire_popover_list(document.showing_hint_popover_list(), should_restore_focus, fire_events);

        // 2. Let ancestor be the result of running the topmost popover ancestor algorithm given element, document's showing auto popover list, invoker, and true.
        Variant<GC::Ptr<HTMLElement>, GC::Ptr<DOM::Document>> ancestor = topmost_popover_ancestor(this, document.showing_auto_popover_list(), invoker, IsPopover::Yes);

        // 3. If ancestor is null, then set ancestor to document.
        if (!ancestor.get<GC::Ptr<HTMLElement>>())
            ancestor = GC::Ptr(document);

        // 4. Run hide all popovers until given ancestor, shouldRestoreFocus, and fireEvents.
        hide_all_popovers_until(ancestor, should_restore_focus, fire_events);

        // 5. Set stackToAppendTo to "auto".
        stack_to_append_to = StackToAppendTo::Auto;
    }

    // 17. If originalType is the hint state, then:
    if (original_type == "hint"sv) {

        // AD-HOC: Steps 14 and 15 have been moved here to avoid hitting the `popover != manual` assertion in the topmost popover ancestor algorithm.
        // Spec issue: https://github.com/whatwg/html/issues/10988.
        // 14. Let autoAncestor be the result of running the topmost popover ancestor algorithm given element, document's showing auto popover list, invoker, and true.
        auto auto_ancestor = topmost_popover_ancestor(this, document.showing_auto_popover_list(), invoker, IsPopover::Yes);

        // 15. Let hintAncestor be the result of running the topmost popover ancestor algorithm given element, document's showing hint popover list, invoker, and true.
        auto hint_ancestor = topmost_popover_ancestor(this, document.showing_hint_popover_list(), invoker, IsPopover::Yes);

        // 1. If hintAncestor is not null, then:
        if (hint_ancestor) {
            // 1. Run hide all popovers until given hintAncestor, shouldRestoreFocus, and fireEvents.
            hide_all_popovers_until(hint_ancestor, should_restore_focus, fire_events);

            // 2. Set stackToAppendTo to "hint".
            stack_to_append_to = StackToAppendTo::Hint;
        }
        // 2. Otherwise:
        else {
            // 1. Run close entire popover list given document's showing hint popover list, shouldRestoreFocus, and fireEvents.
            close_entire_popover_list(document.showing_hint_popover_list(), should_restore_focus, fire_events);

            // 2. If autoAncestor is not null, then:
            if (auto_ancestor) {
                // 1. Run hide all popovers until given autoAncestor, shouldRestoreFocus, and fireEvents.
                hide_all_popovers_until(auto_ancestor, should_restore_focus, fire_events);

                // 2. Set stackToAppendTo to "auto".
                stack_to_append_to = StackToAppendTo::Auto;
            }
            // 3. Otherwise, set stackToAppendTo to "hint".
            else {
                stack_to_append_to = StackToAppendTo::Hint;
            }
        }
    }

    // 18. If originalType is auto or hint, then:
    if (original_type.has_value() && original_type.value().is_one_of("auto", "hint")) {
        // 1. Assert: stackToAppendTo is not null.
        VERIFY(stack_to_append_to != StackToAppendTo::Null);

        // 2. If originalType is not equal to the value of element's popover attribute, then:
        if (original_type != popover()) {
            // 1. If throwExceptions is true, then throw an "InvalidStateError" DOMException.
            if (throw_exceptions == ThrowExceptions::Yes)
                return WebIDL::InvalidStateError::create(realm(), "Element is not in a valid state to show a popover"_utf16);

            // 2. Return.
            return {};
        }

        // 3. If the result of running check popover validity given element, false, throwExceptions, document, and false is false, then run cleanupShowingFlag and return.
        if (!TRY(check_popover_validity(ExpectedToBeShowing::No, throw_exceptions, document, IgnoreDomState::No))) {
            cleanup_showing_flag();
            return {};
        }

        // FIXME: 4. If the result of running topmost auto or hint popover on document is null, then set shouldRestoreFocus to true.

        // 5. If stackToAppendTo is "auto":
        if (stack_to_append_to == StackToAppendTo::Auto) {
            // 1. Assert: document's showing auto popover list does not contain element.
            VERIFY(!document.showing_auto_popover_list().contains_slow(GC::Ref(*this)));

            // AD-HOC: Append element to the document's showing auto popover list.
            // Spec issue: https://github.com/whatwg/html/issues/11007
            document.showing_auto_popover_list().append(*this);

            // 2. Set element's opened in popover mode to "auto".
            m_opened_in_popover_mode = "auto"_string;
        }
        // Otherwise:
        else {
            // 1. Assert: stackToAppendTo is "hint".
            VERIFY(stack_to_append_to == StackToAppendTo::Hint);

            // 2. Assert: document's showing hint popover list does not contain element.
            VERIFY(!document.showing_hint_popover_list().contains_slow(GC::Ref(*this)));

            // AD-HOC: Append element to the document's showing hint popover list.
            // Spec issue: https://github.com/whatwg/html/issues/11007
            document.showing_hint_popover_list().append(*this);

            // 3. Set element's opened in popover mode to "hint".
            m_opened_in_popover_mode = "hint"_string;
        }

        // 6. Set element's popover close watcher to the result of establishing a close watcher given element's relevant global object, with:
        m_popover_close_watcher = CloseWatcher::establish(*document.window());
        // - cancelAction being to return true.
        // We simply don't add an event listener for the cancel action.
        // - closeAction being to hide a popover given element, true, true, false, and null.
        auto close_callback_function = JS::NativeFunction::create(
            realm(), [this](JS::VM&) {
                MUST(hide_popover(FocusPreviousElement::Yes, FireEvents::Yes, ThrowExceptions::No, IgnoreDomState::No, nullptr));

                return JS::js_undefined();
            },
            0, Utf16FlyString {}, &realm());
        auto close_callback = realm().heap().allocate<WebIDL::CallbackType>(*close_callback_function, realm());
        m_popover_close_watcher->add_event_listener_without_options(HTML::EventNames::close, DOM::IDLEventListener::create(realm(), close_callback));
        // - getEnabledState being to return true.
        m_popover_close_watcher->set_enabled(true);
    }
    // FIXME: 19. Set element's previously focused element to null.
    // FIXME: 20. Let originallyFocusedElement be document's focused area of the document's DOM anchor.
    // 21. Add an element to the top layer given element.
    document.add_an_element_to_the_top_layer(*this);
    // 22. Set element's popover visibility state to showing.
    m_popover_visibility_state = PopoverVisibilityState::Showing;
    // 23. Set element's popover invoker to invoker.
    m_popover_invoker = invoker;
    // FIXME: 24. Set element's implicit anchor element to invoker.
    // FIXME: 25. Run the popover focusing steps given element.
    // FIXME: 26. If shouldRestoreFocus is true and element's popover attribute is not in the No Popover state, then set element's previously focused element to originallyFocusedElement.
    // 27. Queue a popover toggle event task given element, "closed", "open", and invoker.
    queue_a_popover_toggle_event_task("closed"_string, "open"_string, invoker);
    // 28. Run cleanupShowingFlag.
    cleanup_showing_flag();

    return {};
}

// https://html.spec.whatwg.org/multipage/popover.html#dom-hidepopover
// https://whatpr.org/html/9457/popover.html#dom-hidepopover
WebIDL::ExceptionOr<void> HTMLElement::hide_popover_for_bindings()
{
    // The hidePopover() method steps are to run the hide popover algorithm given this, true, true, true, false, and null.
    return hide_popover(FocusPreviousElement::Yes, FireEvents::Yes, ThrowExceptions::Yes, IgnoreDomState::No, nullptr);
}

// https://html.spec.whatwg.org/multipage/popover.html#hide-popover-algorithm
// https://whatpr.org/html/9457/popover.html#hide-popover-algorithm
WebIDL::ExceptionOr<void> HTMLElement::hide_popover(FocusPreviousElement focus_previous_element, FireEvents fire_events, ThrowExceptions throw_exceptions, IgnoreDomState ignore_dom_state, GC::Ptr<HTMLElement> source)
{
    // 1. If the result of running check popover validity given element, true, throwExceptions, null and ignoreDomState is false, then return.
    if (!TRY(check_popover_validity(ExpectedToBeShowing::Yes, throw_exceptions, nullptr, ignore_dom_state)))
        return {};

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

    // 3. Let nestedHide be element's popover showing or hiding.
    auto nested_hide = m_popover_showing_or_hiding;

    // 4. Set element's popover showing or hiding to true.
    m_popover_showing_or_hiding = true;

    // 5. If nestedHide is true, then set fireEvents to false.
    if (nested_hide)
        fire_events = FireEvents::No;

    // 6. Let cleanupSteps be the following steps:
    auto cleanup_steps = [&nested_hide, this] {
        // 6.1. If nestedHide is false, then set element's popover showing or hiding to false.
        if (nested_hide)
            m_popover_showing_or_hiding = false;
        // 6.2. If element's popover close watcher is not null, then:
        if (m_popover_close_watcher) {
            // 6.2.1. Destroy element's popover close watcher.
            m_popover_close_watcher->destroy();
            // 6.2.2. Set element's popover close watcher to null.
            m_popover_close_watcher = nullptr;
        }
    };

    // 7. If element's opened in popover mode is "auto" or "hint", then:
    if (m_opened_in_popover_mode.has_value() && m_opened_in_popover_mode.value().is_one_of("auto", "hint")) {
        // 7.1. Run hide all popovers until given element, focusPreviousElement, and fireEvents.
        hide_all_popovers_until(GC::Ptr(this), focus_previous_element, fire_events);

        // 7.2. If the result of running check popover validity given element, true, throwExceptions, and ignoreDomState is false, then run cleanupSteps and return.
        if (!TRY(check_popover_validity(ExpectedToBeShowing::Yes, throw_exceptions, nullptr, ignore_dom_state))) {
            cleanup_steps();
            return {};
        }
    }
    // 8. Let autoPopoverListContainsElement be true if document's showing auto popover list's last item is element, otherwise false.
    auto const& showing_popovers = document.showing_auto_popover_list();
    bool auto_popover_list_contains_element = !showing_popovers.is_empty() && showing_popovers.last() == this;

    // 9. If fireEvents is true:
    if (fire_events == FireEvents::Yes) {
        // 9.1. Fire an event named beforetoggle, using ToggleEvent, with the oldState attribute initialized to "open", the newState attribute initialized to "closed", and the source attribute set to source at element.
        ToggleEventInit event_init {};
        event_init.old_state = "open"_string;
        event_init.new_state = "closed"_string;
        dispatch_event(ToggleEvent::create(realm(), HTML::EventNames::beforetoggle, move(event_init), source));

        // 9.2. If autoPopoverListContainsElement is true and document's showing auto popover list's last item is not element, then run hide all popovers until given element, focusPreviousElement, and false.
        if (auto_popover_list_contains_element && (showing_popovers.is_empty() || showing_popovers.last() != this))
            hide_all_popovers_until(GC::Ptr(this), focus_previous_element, FireEvents::No);

        // 9.3. If the result of running check popover validity given element, true, throwExceptions, null, and ignoreDomState is false, then run cleanupSteps and return.
        if (!TRY(check_popover_validity(ExpectedToBeShowing::Yes, throw_exceptions, nullptr, ignore_dom_state))) {
            cleanup_steps();
            return {};
        }
        // 9.4. Request an element to be removed from the top layer given element.
        document.request_an_element_to_be_remove_from_the_top_layer(*this);
    } else {
        // 10. Otherwise, remove an element from the top layer immediately given element.
        document.remove_an_element_from_the_top_layer_immediately(*this);
    }

    // AD-HOC: The following block of code is all ad-hoc.
    // Spec issue: https://github.com/whatwg/html/issues/11007

    // If element's opened in popover mode is "auto" or "hint":
    if (m_opened_in_popover_mode.has_value() && m_opened_in_popover_mode.value().is_one_of("auto", "hint")) {
        // If document's showing hint popover list's last item is element:
        auto& hint_popovers = document.showing_hint_popover_list();
        if (!hint_popovers.is_empty() && hint_popovers.last() == this) {
            // Assert: element's opened in popover mode is "hint".
            VERIFY(m_opened_in_popover_mode == "hint"sv);

            // Remove the last item from document's showing hint popover list.
            hint_popovers.remove(hint_popovers.size() - 1);
        }
        // Otherwise:
        else {
            // Assert: document's showing auto popover list's last item is element.
            auto& auto_popovers = document.showing_auto_popover_list();
            VERIFY(!auto_popovers.is_empty() && auto_popovers.last() == this);

            // Remove the last item from document's showing auto popover list.
            auto_popovers.remove(auto_popovers.size() - 1);
        }
    }

    // 11. Set element's popover invoker to null.
    m_popover_invoker = nullptr;

    // 12. Set element's opened in popover mode to null.
    m_opened_in_popover_mode = {};

    // 13. Set element's popover visibility state to hidden.
    m_popover_visibility_state = PopoverVisibilityState::Hidden;

    // 14. If fireEvents is true, then queue a popover toggle event task given element, "open", "closed", and source.
    if (fire_events == FireEvents::Yes)
        queue_a_popover_toggle_event_task("open"_string, "closed"_string, source);

    // FIXME: 15. Let previouslyFocusedElement be element's previously focused element.

    // FIXME: 16. If previouslyFocusedElement is not null, then:
    // FIXME: 16.1. Set element's previously focused element to null.
    // FIXME: 16.2. If focusPreviousElement is true and document's focused area of the document's DOM anchor is a shadow-including inclusive descendant of element, then run the focusing steps for previouslyFocusedElement; the viewport should not be scrolled by doing this step.

    // 17. Run cleanupSteps.
    cleanup_steps();

    return {};
}

// https://html.spec.whatwg.org/multipage/popover.html#dom-togglepopover
// https://whatpr.org/html/9457/popover.html#dom-togglepopover
WebIDL::ExceptionOr<bool> HTMLElement::toggle_popover(TogglePopoverOptionsOrForceBoolean const& options)
{
    // 1. Let force be null.
    Optional<bool> force;
    GC::Ptr<HTMLElement> invoker;

    // 2. If options is a boolean, set force to options.
    options.visit(
        [&force](bool forceBool) {
            force = forceBool;
        },
        [&force, &invoker](TogglePopoverOptions options) {
            // 3. Otherwise, if options["force"] exists, set force to options["force"].
            force = options.force;
            // 4. Let invoker be options["source"] if it exists; otherwise, null.
            invoker = options.source;
        });

    // 5. If this's popover visibility state is showing, and force is null or false, then run the hide popover algorithm given this, true, true, true, false, and null.
    if (popover_visibility_state() == PopoverVisibilityState::Showing && (!force.has_value() || !force.value()))
        TRY(hide_popover(FocusPreviousElement::Yes, FireEvents::Yes, ThrowExceptions::Yes, IgnoreDomState::No, nullptr));
    // 6. Otherwise, if force is not present or true, then run show popover given this true, and invoker.
    else if (!force.has_value() || force.value())
        TRY(show_popover(ThrowExceptions::Yes, invoker));
    // 7. Otherwise:
    else {
        // 7.1 Let expectedToBeShowing be true if this's popover visibility state is showing; otherwise false.
        ExpectedToBeShowing expected_to_be_showing = popover_visibility_state() == PopoverVisibilityState::Showing ? ExpectedToBeShowing::Yes : ExpectedToBeShowing::No;
        // 7.2 Run check popover validity given expectedToBeShowing, true, null, and false.
        TRY(check_popover_validity(expected_to_be_showing, ThrowExceptions::Yes, nullptr, IgnoreDomState::No));
    }
    // 8. Return true if this's popover visibility state is showing; otherwise false.
    return popover_visibility_state() == PopoverVisibilityState::Showing;
}

// AD-HOC: This implementation checks "opened in popover mode" instead of the current popover state.
// Spec issue: https://github.com/whatwg/html/issues/10996.
// https://html.spec.whatwg.org/multipage/popover.html#hide-all-popovers-until
void HTMLElement::hide_all_popovers_until(Variant<GC::Ptr<HTMLElement>, GC::Ptr<DOM::Document>> endpoint, FocusPreviousElement focus_previous_element, FireEvents fire_events)
{
    // To hide all popovers until, given an HTML element or Document endpoint, a boolean focusPreviousElement, and a boolean fireEvents:

    // 1. If endpoint is an HTML element and endpoint is not in the popover showing state, then return.
    if (endpoint.has<GC::Ptr<HTMLElement>>() && endpoint.get<GC::Ptr<HTMLElement>>()->popover_visibility_state() != PopoverVisibilityState::Showing)
        return;

    // 2. Let document be endpoint's node document.
    auto const* document = endpoint.visit([](auto endpoint) { return &endpoint->document(); });

    // 3. Assert: endpoint is a Document or endpoint's popover visibility state is showing.
    VERIFY(endpoint.has<GC::Ptr<DOM::Document>>() || endpoint.get<GC::Ptr<HTMLElement>>()->popover_visibility_state() == PopoverVisibilityState::Showing);

    // 4. Assert: endpoint is a Document or endpoint's popover attribute is in the auto state or endpoint's popover attribute is in the hint state.
    VERIFY(endpoint.has<GC::Ptr<DOM::Document>>() || endpoint.get<GC::Ptr<HTMLElement>>()->m_opened_in_popover_mode->is_one_of("auto", "hint"));

    // 5. If endpoint is a Document:
    if (endpoint.has<GC::Ptr<DOM::Document>>()) {
        // 1. Run close entire popover list given document's showing hint popover list, focusPreviousElement, and fireEvents.
        close_entire_popover_list(document->showing_hint_popover_list(), focus_previous_element, fire_events);

        // 2. Run close entire popover list given document's showing auto popover list, focusPreviousElement, and fireEvents.
        close_entire_popover_list(document->showing_auto_popover_list(), focus_previous_element, fire_events);

        // 3. Return.
        return;
    }

    // 6. If document's showing hint popover list contains endpoint:
    auto endpoint_element = endpoint.get<GC::Ptr<HTMLElement>>();
    if (document->showing_hint_popover_list().contains_slow(GC::Ref(*endpoint_element))) {
        // 1. Assert: endpoint's popover attribute is in the hint state.
        VERIFY(endpoint_element->m_opened_in_popover_mode == "hint"sv);

        // 2. Run hide popover stack until given endpoint, document's showing hint popover list, focusPreviousElement, and fireEvents.
        endpoint_element->hide_popover_stack_until(document->showing_hint_popover_list(), focus_previous_element, fire_events);

        // 3. Return.
        return;
    }

    // 7. Run close entire popover list given document's showing hint popover list, focusPreviousElement, and fireEvents.
    close_entire_popover_list(document->showing_hint_popover_list(), focus_previous_element, fire_events);

    // 8. If document's showing auto popover list does not contain endpoint, then return.
    if (!document->showing_auto_popover_list().contains_slow(GC::Ref(*endpoint_element)))
        return;

    // 9. Run hide popover stack until given endpoint, document's showing auto popover list, focusPreviousElement, and fireEvents.
    endpoint_element->hide_popover_stack_until(document->showing_auto_popover_list(), focus_previous_element, fire_events);
}

// https://html.spec.whatwg.org/multipage/popover.html#hide-popover-stack-until
void HTMLElement::hide_popover_stack_until(Vector<GC::Ref<HTMLElement>> const& popover_list, FocusPreviousElement focus_previous_element, FireEvents fire_events)
{
    // To hide popover stack until, given an HTML element endpoint, a list popoverList, a boolean focusPreviousElement, and a boolean fireEvents:

    // 1. Let repeatingHide be false.
    bool repeating_hide = false;

    // 2. Perform the following steps at least once:
    do {
        // 1. Let lastToHide be null.
        GC::Ptr<HTMLElement> last_to_hide;

        // 2. For each popover in popoverList:
        // AD-HOC: This needs to be iterated in reverse because step 4 hides items in reverse.
        for (auto const& popover : popover_list.in_reverse()) {
            // 1. If popover is endpoint, then break.
            if (popover == this)
                break;

            // 2. Set lastToHide to popover.
            last_to_hide = popover;
        }

        // 3. If lastToHide is null, then return.
        if (!last_to_hide)
            return;

        // 4. While lastToHide's popover visibility state is showing:
        while (last_to_hide->popover_visibility_state() == PopoverVisibilityState::Showing) {
            // 1. Assert: popoverList is not empty.
            VERIFY(!popover_list.is_empty());

            // 2. Run the hide popover algorithm given the last item in popoverList, focusPreviousElement, fireEvents, false, and null.
            MUST(popover_list.last()->hide_popover(focus_previous_element, fire_events, ThrowExceptions::No, IgnoreDomState::No, nullptr));
        }

        // 5. Assert: repeatingHide is false or popoverList's last item is endpoint.
        VERIFY(!repeating_hide || popover_list.last() == this);

        // 6. Set repeatingHide to true if popoverList contains endpoint and popoverList's last item is not endpoint, otherwise false.
        repeating_hide = popover_list.contains_slow(GC::Ref(*this)) && popover_list.last() != this;

        // 7. If repeatingHide is true, then set fireEvents to false.
        if (repeating_hide)
            fire_events = FireEvents::No;

    } while (repeating_hide);
    // and keep performing them while repeatingHide is true.
}

// https://html.spec.whatwg.org/multipage/popover.html#close-entire-popover-list
void HTMLElement::close_entire_popover_list(Vector<GC::Ref<HTMLElement>> const& popover_list, FocusPreviousElement focus_previous_element, FireEvents fire_events)
{
    // To close entire popover list given a list popoverList, a boolean focusPreviousElement, and a boolean fireEvents:

    // FIXME: If an event handler opens a new popover then this could be an infinite loop.
    // 1. While popoverList is not empty:
    while (!popover_list.is_empty()) {
        // 1. Run the hide popover algorithm given popoverList's last item, focusPreviousElement, fireEvents, false, and null.
        MUST(popover_list.last()->hide_popover(focus_previous_element, fire_events, ThrowExceptions::No, IgnoreDomState::No, nullptr));
    }
}

// https://html.spec.whatwg.org/multipage/popover.html#topmost-popover-ancestor
GC::Ptr<HTMLElement> HTMLElement::topmost_popover_ancestor(GC::Ptr<DOM::Node> new_popover_or_top_layer_element, Vector<GC::Ref<HTMLElement>> const& popover_list, GC::Ptr<HTMLElement> invoker, IsPopover is_popover)
{
    // To find the topmost popover ancestor, given a Node newPopoverOrTopLayerElement, a list popoverList, an HTML element or null invoker, and a boolean isPopover, perform the following steps. They return an HTML element or null.

    // 1. If isPopover is true:
    auto* new_popover = as_if<HTML::HTMLElement>(*new_popover_or_top_layer_element);
    if (is_popover == IsPopover::Yes) {
        // 1. Assert: newPopoverOrTopLayerElement is an HTML element.
        VERIFY(new_popover);

        // 2. Assert: newPopoverOrTopLayerElement's popover attribute is not in the No Popover state or the manual state.
        VERIFY(!new_popover->popover().has_value() || new_popover->popover().value() != "manual"sv);

        // 3. Assert: newPopoverOrTopLayerElement's popover visibility state is not in the popover showing state.
        VERIFY(new_popover->popover_visibility_state() != PopoverVisibilityState::Showing);
    }
    // 2. Otherwise:
    else {
        // 1. Assert: invoker is null.
        VERIFY(!invoker);
    }

    // 3. Let popoverPositions be an empty ordered map.
    OrderedHashMap<GC::Ref<HTMLElement>, int> popover_positions;

    // 4. Let index be 0.
    int index = 0;

    // 5. For each popover of popoverList:
    for (auto const& popover : popover_list) {
        // 1. Set popoverPositions[popover] to index.
        popover_positions.set(*popover, index);

        // 2. Increment index by 1.
        index++;
    }

    // 6. If isPopover is true, then set popoverPositions[newPopoverOrTopLayerElement] to index.
    if (is_popover == IsPopover::Yes)
        popover_positions.set(*new_popover, index);

    // 7. Increment index by 1.
    index++;

    // 8. Let topmostPopoverAncestor be null.
    GC::Ptr<HTMLElement> topmost_popover_ancestor;

    // 9. Let checkAncestor be an algorithm which performs the following steps given candidate:
    auto check_ancestor = [&](auto candidate) {
        // 1. If candidate is null, then return.
        if (!candidate)
            return;

        // 2. Let okNesting be false.
        bool ok_nesting = false;

        // 3. Let candidateAncestor be null.
        GC::Ptr<HTMLElement> candidate_ancestor;

        // 4. While okNesting is false:
        while (!ok_nesting) {
            // 1. Set candidateAncestor to the result of running nearest inclusive open popover given candidate.
            candidate_ancestor = candidate->nearest_inclusive_open_popover();

            // 2. If candidateAncestor is null or popoverPositions does not contain candidateAncestor, then return.
            if (!candidate_ancestor || !popover_positions.contains(*candidate_ancestor))
                return;

            // 3. Assert: candidateAncestor's popover attribute is not in the manual or none state.
            VERIFY(!candidate_ancestor->popover().has_value() || candidate_ancestor->popover().value() != "manual"sv);

            // 4. Set okNesting to true if isPopover is false, newPopoverOrTopLayerElement's popover attribute is in the hint state, or candidateAncestor's popover attribute is in the auto state.
            if (is_popover == IsPopover::No || new_popover->popover() == "hint"sv || candidate_ancestor->popover() == "auto"sv)
                ok_nesting = true;

            // 5. If okNesting is false, then set candidate to candidateAncestor's parent in the flat tree.
            if (!ok_nesting)
                candidate = candidate_ancestor->shadow_including_first_ancestor_of_type<HTMLElement>();
        }

        // 5. Let candidatePosition be popoverPositions[candidateAncestor].
        auto candidate_position = popover_positions.get(*candidate_ancestor).value();

        // 6. If topmostPopoverAncestor is null or popoverPositions[topmostPopoverAncestor] is less than candidatePosition, then set topmostPopoverAncestor to candidateAncestor.
        if (!topmost_popover_ancestor || popover_positions.get(*topmost_popover_ancestor).value() < candidate_position)
            topmost_popover_ancestor = candidate_ancestor;
    };

    // 10. Run checkAncestor given newPopoverOrTopLayerElement's parent node within the flat tree.
    check_ancestor(new_popover_or_top_layer_element->shadow_including_first_ancestor_of_type<HTMLElement>());

    // 11. Run checkAncestor given invoker.
    check_ancestor(invoker.ptr());

    // 12. Return topmostPopoverAncestor.
    return topmost_popover_ancestor;
}

// https://html.spec.whatwg.org/multipage/popover.html#nearest-inclusive-open-popover
GC::Ptr<HTMLElement> HTMLElement::nearest_inclusive_open_popover()
{
    // To find the nearest inclusive open popover given a Node node, perform the following steps. They return an HTML element or null.

    // 1. Let currentNode be node.
    auto* current_node = this;

    // 2. While currentNode is not null:
    while (current_node) {
        // 1. If currentNode's popover attribute is in the Auto state or the Hint state, and currentNode's popover visibility state is showing, then return currentNode.
        if (current_node->popover().has_value() && current_node->popover().value().is_one_of("auto", "hint") && current_node->popover_visibility_state() == PopoverVisibilityState::Showing)
            return current_node;

        // 2. Set currentNode to currentNode's parent in the flat tree.
        current_node = current_node->shadow_including_first_ancestor_of_type<HTMLElement>();
    }

    // 3. Return null.
    return {};
}

// https://html.spec.whatwg.org/multipage/popover.html#nearest-inclusive-target-popover-for-invoker
GC::Ptr<HTMLElement> HTMLElement::nearest_inclusive_target_popover_for_invoker()
{
    // To find the nearest inclusive target popover for invoker given a Node node:

    // 1. Let currentNode be node.
    auto* current_node = this;

    // 2. While currentNode is not null:
    while (current_node) {
        // 1. Let targetPopover be currentNode's popover target element.
        auto target_popover = PopoverInvokerElement::get_the_popover_target_element(*current_node);

        // 2. If targetPopover is not null and targetPopover's popover attribute is in the Auto state or the Hint state, and targetPopover's popover visibility state is showing, then return targetPopover.
        if (target_popover) {
            if (target_popover->popover().has_value() && target_popover->popover().value().is_one_of("auto", "hint") && target_popover->popover_visibility_state() == PopoverVisibilityState::Showing)
                return target_popover;
        }

        // 3. Set currentNode to currentNode's ancestor in the flat tree.
        current_node = current_node->shadow_including_first_ancestor_of_type<HTMLElement>();
    }

    return {};
}

// https://html.spec.whatwg.org/multipage/popover.html#queue-a-popover-toggle-event-task
void HTMLElement::queue_a_popover_toggle_event_task(String old_state, String new_state, GC::Ptr<HTMLElement> source)
{
    // 1. If element's popover toggle task tracker is not null, then:
    if (m_popover_toggle_task_tracker.has_value()) {
        // 1. Set oldState to element's popover toggle task tracker's old state.
        old_state = move(m_popover_toggle_task_tracker->old_state);

        // 2. Remove element's popover toggle task tracker's task from its task queue.
        HTML::main_thread_event_loop().task_queue().remove_tasks_matching([&](auto const& task) {
            return task.id() == m_popover_toggle_task_tracker->task_id;
        });

        // 3. Set element's popover toggle task tracker to null.
        m_popover_toggle_task_tracker->task_id = {};
    }

    // 2. Queue an element task given the DOM manipulation task source and element to run the following steps:
    auto task_id = queue_an_element_task(HTML::Task::Source::DOMManipulation, [this, old_state, new_state = move(new_state), source]() mutable {
        // 1. Fire an event named toggle at element, using ToggleEvent, with the oldState attribute initialized to
        //    oldState, the newState attribute initialized to newState, and the source attribute initialized to source.
        ToggleEventInit event_init {};
        event_init.old_state = move(old_state);
        event_init.new_state = move(new_state);

        dispatch_event(ToggleEvent::create(realm(), HTML::EventNames::toggle, move(event_init), source));

        // 2. Set element's popover toggle task tracker to null.
        m_popover_toggle_task_tracker = {};
    });

    // 3. Set element's popover toggle task tracker to a struct with task set to the just-queued task and old state set to oldState.
    m_popover_toggle_task_tracker = ToggleTaskTracker {
        .task_id = task_id,
        .old_state = move(old_state),
    };
}

// https://html.spec.whatwg.org/multipage/popover.html#light-dismiss-open-popovers
void HTMLElement::light_dismiss_open_popovers(UIEvents::PointerEvent const& event, GC::Ptr<DOM::Node> const target)
{
    // To light dismiss open popovers, given a PointerEvent event:

    // 1. Assert: event's isTrusted attribute is true.
    VERIFY(event.is_trusted());

    // 2. Let target be event's target.
    // FIXME: The event's target hasn't been initialized yet, so it's passed as an argument

    // 3. Let document be target's node document.
    auto& document = target->document();

    // 4. Let topmostPopover be the result of running topmost auto popover given document.
    auto topmost_popover = document.topmost_auto_or_hint_popover();

    // 5. If topmostPopover is null, then return.
    if (!topmost_popover)
        return;

    // 6. If event's type is "pointerdown", then: set document's popover pointerdown target to the result of running topmost clicked popover given target.
    if (event.type() == UIEvents::EventNames::pointerdown)
        document.set_popover_pointerdown_target(topmost_clicked_popover(target));

    // 7. If event's type is "pointerup", then:
    if (event.type() == UIEvents::EventNames::pointerup) {
        // 1. Let ancestor be the result of running topmost clicked popover given target.
        auto const ancestor = topmost_clicked_popover(target);

        // 2. Let sameTarget be true if ancestor is document's popover pointerdown target.
        bool const same_target = ancestor == document.popover_pointerdown_target();

        // 3. Set document's popover pointerdown target to null.
        document.set_popover_pointerdown_target({});

        // 4. If ancestor is null, then set ancestor to document.
        Variant<GC::Ptr<HTMLElement>, GC::Ptr<DOM::Document>> ancestor_or_document = ancestor;
        if (!ancestor)
            ancestor_or_document = GC::Ptr(document);

        // 5. If sameTarget is true, then run hide all popovers until given ancestor, false, and true.
        if (same_target)
            hide_all_popovers_until(ancestor_or_document, FocusPreviousElement::No, FireEvents::Yes);
    }
}

// https://html.spec.whatwg.org/multipage/popover.html#get-the-popover-stack-position
size_t HTMLElement::popover_stack_position()
{
    // To get the popover stack position, given an HTML element popover:

    // 1. Let hintList be popover's node document's showing hint popover list.
    auto const& hint_list = document().showing_hint_popover_list();

    // 2. Let autoList be popover's node document's showing auto popover list.
    auto const& auto_list = document().showing_auto_popover_list();

    // 3. If popover is in hintList, then return the index of popover in hintList + the size of autoList + 1.
    if (hint_list.contains_slow(GC::Ref(*this)))
        return hint_list.find_first_index(GC::Ref(*this)).value() + auto_list.size() + 1;

    // 4. If popover is in autoList, then return the index of popover in autoList + 1.
    if (auto_list.contains_slow(GC::Ref(*this)))
        return auto_list.find_first_index(GC::Ref(*this)).value() + 1;

    // 5. Return 0.
    return 0;
}

// https://html.spec.whatwg.org/multipage/popover.html#topmost-clicked-popover
GC::Ptr<HTMLElement> HTMLElement::topmost_clicked_popover(GC::Ptr<DOM::Node> node)
{
    // To find the topmost clicked popover, given a Node node:

    GC::Ptr<HTMLElement> nearest_element = as_if<HTMLElement>(*node);
    if (!nearest_element)
        nearest_element = node->shadow_including_first_ancestor_of_type<HTMLElement>();

    if (!nearest_element)
        return {};

    // 1. Let clickedPopover be the result of running nearest inclusive open popover given node.
    auto clicked_popover = nearest_element->nearest_inclusive_open_popover();

    // 2. Let invokerPopover be the result of running nearest inclusive target popover for invoker given node.
    auto invoker_popover = nearest_element->nearest_inclusive_target_popover_for_invoker();

    if (!clicked_popover)
        return invoker_popover;

    if (!invoker_popover)
        return clicked_popover;

    // 3. If the result of getting the popover stack position given clickedPopover is greater than the result of getting the popover stack position given invokerPopover, then return clickedPopover.
    if (clicked_popover->popover_stack_position() > invoker_popover->popover_stack_position())
        return clicked_popover;

    // 4. Return invokerPopover.
    return invoker_popover;
}

void HTMLElement::did_receive_focus()
{
    if (!first_is_one_of(m_content_editable_state, ContentEditableState::True, ContentEditableState::PlaintextOnly))
        return;

    auto editing_host = document().editing_host_manager();
    editing_host->set_active_contenteditable_element(this);

    // Don't update the selection if we're already part of the active range.
    if (auto range = document().get_selection()->range()) {
        if (is_inclusive_ancestor_of(range->start_container()) || is_inclusive_ancestor_of(range->end_container()))
            return;
    }

    DOM::Text* text = nullptr;
    for_each_in_inclusive_subtree_of_type<DOM::Text>([&](auto& node) {
        text = &node;
        return TraversalDecision::Continue;
    });

    if (!text) {
        editing_host->set_selection_anchor(*this, 0);
        return;
    }
    editing_host->set_selection_anchor(*text, text->length());
}

void HTMLElement::did_lose_focus()
{
    if (!first_is_one_of(m_content_editable_state, ContentEditableState::True, ContentEditableState::PlaintextOnly))
        return;

    document().editing_host_manager()->set_active_contenteditable_element(nullptr);
}

void HTMLElement::removed_from(Node* old_parent, Node& old_root)
{
    Element::removed_from(old_parent, old_root);

    // https://html.spec.whatwg.org/multipage/infrastructure.html#dom-trees:concept-node-remove-ext
    // If removedNode's popover attribute is not in the No Popover state, then run the hide popover algorithm given removedNode, false, false, false, true, and null.
    if (popover().has_value())
        MUST(hide_popover(FocusPreviousElement::No, FireEvents::No, ThrowExceptions::No, IgnoreDomState::Yes, nullptr));

    if (old_parent) {
        auto* parent_html_element = as_if<HTMLElement>(old_parent);
        if (!parent_html_element)
            parent_html_element = old_parent->first_ancestor_of_type<HTMLElement>();
        if (parent_html_element && parent_html_element->is_inert() && !has_attribute(HTML::AttributeNames::inert))
            set_subtree_inertness(false);
    }
}

// https://html.spec.whatwg.org/multipage/interaction.html#dom-accesskeylabel
String HTMLElement::access_key_label() const
{
    dbgln("FIXME: Implement HTMLElement::access_key_label()");
    return String {};
}

// https://html.spec.whatwg.org/multipage/dnd.html#dom-draggable
bool HTMLElement::draggable() const
{
    auto attribute = get_attribute(HTML::AttributeNames::draggable);

    // If an element's draggable content attribute has the state True, the draggable IDL attribute must return true.
    if (attribute.has_value() && attribute->equals_ignoring_ascii_case("true"sv)) {
        return true;
    }

    // If an element's draggable content attribute has the state False, the draggable IDL attribute must return false.
    if (attribute.has_value() && attribute->equals_ignoring_ascii_case("false"sv)) {
        return false;
    }

    // Otherwise, the element's draggable content attribute has the state Auto.

    // If the element is an img element, the draggable IDL attribute must return true.
    if (is<HTML::HTMLImageElement>(*this)) {
        return true;
    }

    // If the element is an object element that represents an image, the draggable IDL attribute must return true.
    if (is<HTML::HTMLObjectElement>(*this)) {
        if (auto type_attribute = get_attribute(HTML::AttributeNames::type); type_attribute.has_value() && type_attribute->equals_ignoring_ascii_case("image"sv))
            return true;
    }

    // If the element is an a element with an href content attribute, the draggable IDL attribute must return true.
    if (is<HTML::HTMLAnchorElement>(*this) && has_attribute(HTML::AttributeNames::href)) {
        return true;
    }

    // Otherwise, the draggable IDL attribute must return false.
    return false;
}

// https://html.spec.whatwg.org/multipage/interaction.html#dom-spellcheck
bool HTMLElement::spellcheck() const
{
    // The spellcheck attribute is an enumerated attribute with the following keywords and states:
    // Keyword            | State | Brief description
    // true               | True  | Spelling and grammar will be checked.
    // (the empty string) |       |
    // false              | False | and grammar will not be checked.

    // The attribute's missing value default and invalid value default are both the Default state. The default state
    // indicates that the element is to act according to a default behavior, possibly based on the parent element's
    // own spellcheck state, as defined below.

    // For each element, user agents must establish a default behavior, either through defaults or through preferences
    // expressed by the user. There are three possible default behaviors for each element:

    // true-by-default
    //     The element will be checked for spelling and grammar if its contents are editable and spellchecking is not
    //     explicitly disabled through the spellcheck attribute.
    // false-by-default
    //     The element will never be checked for spelling and grammar unless spellchecking is explicitly enabled
    //     through the spellcheck attribute.
    // inherit-by-default
    //     The element's default behavior is the same as its parent element's. Elements that have no parent element
    //     cannot have this as their default behavior.

    // NOTE: We use "true-by-default" for elements which are editable, editing hosts, or form associated text control
    //       elements "false-by-default" for root elements, and "inherit-by-default" for other elements.

    auto maybe_spellcheck_attribute = attribute(HTML::AttributeNames::spellcheck);

    // The spellcheck IDL attribute, on getting, must return true if the element's spellcheck content attribute is in the True state,
    if (maybe_spellcheck_attribute.has_value() && (maybe_spellcheck_attribute.value().equals_ignoring_ascii_case("true"sv) || maybe_spellcheck_attribute.value().is_empty()))
        return true;

    if (!maybe_spellcheck_attribute.has_value() || !maybe_spellcheck_attribute.value().equals_ignoring_ascii_case("false"sv)) {
        // or if the element's spellcheck content attribute is in the Default state and the element's default behavior is true-by-default,
        if (is_editable_or_editing_host() || is<FormAssociatedTextControlElement>(this))
            return true;

        // or if the element's spellcheck content attribute is in the Default state and the element's default behavior is inherit-by-default
        if (auto* parent_html_element = first_ancestor_of_type<HTMLElement>()) {
            // and the element's parent element's spellcheck IDL attribute would return true;
            if (parent_html_element->spellcheck())
                return true;
        }
    }

    // if none of those conditions applies, then the attribute must instead return false.
    return false;
}

// https://html.spec.whatwg.org/multipage/interaction.html#dom-spellcheck
void HTMLElement::set_spellcheck(bool spellcheck)
{
    // On setting, if the new value is true, then the element's spellcheck content attribute must be set to "true", otherwise it must be set to "false".
    if (spellcheck)
        MUST(set_attribute(HTML::AttributeNames::spellcheck, "true"_string));
    else
        MUST(set_attribute(HTML::AttributeNames::spellcheck, "false"_string));
}

// https://html.spec.whatwg.org/multipage/interaction.html#dom-writingsuggestions
String HTMLElement::writing_suggestions() const
{
    // The writingsuggestions content attribute is an enumerated attribute with the following keywords and states:
    // Keyword            | State | Brief description
    // true               | True  | Writing suggestions should be offered on this element.
    // (the empty string) |       |
    // false              | False | Writing suggestions should not be offered on this element.

    // The attribute's missing value default is the Default state. The default state indicates that the element is to
    // act according to a default behavior, possibly based on the parent element's own writingsuggestions state, as
    // defined below.

    // The attribute's invalid value default is the True state.

    // 1. If element's writingsuggestions content attribute is in the False state, return "false".
    auto maybe_writing_suggestions_attribute = attribute(HTML::AttributeNames::writingsuggestions);

    if (maybe_writing_suggestions_attribute.has_value() && maybe_writing_suggestions_attribute.value().equals_ignoring_ascii_case("false"sv))
        return "false"_string;

    // 2. If element's writingsuggestions content attribute is in the Default state, element has a parent element, and the computed writing suggestions value of element's parent element is "false", then return "false".
    if (!maybe_writing_suggestions_attribute.has_value() && first_ancestor_of_type<HTMLElement>() && first_ancestor_of_type<HTMLElement>()->writing_suggestions() == "false"sv) {
        return "false"_string;
    }

    // 3. Return "true".
    return "true"_string;
}

// https://html.spec.whatwg.org/multipage/interaction.html#dom-writingsuggestions
void HTMLElement::set_writing_suggestions(String const& given_value)
{
    // 1. Set this's writingsuggestions content attribute to the given value.
    MUST(set_attribute(HTML::AttributeNames::writingsuggestions, given_value));
}

// https://html.spec.whatwg.org/multipage/interaction.html#own-autocapitalization-hint
HTMLElement::AutocapitalizationHint HTMLElement::own_autocapitalization_hint() const
{
    // The autocapitalization processing model is based on selecting among five autocapitalization hints, defined as follows:
    //
    // default
    //     The user agent and input method should make their own determination of whether or not to enable autocapitalization.
    // none
    //     No autocapitalization should be applied (all letters should default to lowercase).
    // sentences
    //     The first letter of each sentence should default to a capital letter; all other letters should default to lowercase.
    // words
    //     The first letter of each word should default to a capital letter; all other letters should default to lowercase.
    // characters
    //     All letters should default to uppercase.

    // The autocapitalize attribute is an enumerated attribute whose states are the possible autocapitalization hints.
    // The autocapitalization hint specified by the attribute's state combines with other considerations to form the
    // used autocapitalization hint, which informs the behavior of the user agent. The keywords for this attribute and
    // their state mappings are as follows:

    // Keyword    | State
    // off        | none
    // none       |
    // on         | sentences
    // sentences  |
    // words      | words
    // characters | characters

    // The attribute's missing value default is the default state, and its invalid value default is the sentences state.

    // To compute the own autocapitalization hint of an element element, run the following steps:
    // 1. If the autocapitalize content attribute is present on element, and its value is not the empty string, return the
    //    state of the attribute.
    auto maybe_autocapitalize_attribute = attribute(HTML::AttributeNames::autocapitalize);

    if (maybe_autocapitalize_attribute.has_value() && !maybe_autocapitalize_attribute.value().is_empty()) {
        auto autocapitalize_attribute_string_view = maybe_autocapitalize_attribute.value().bytes_as_string_view();

        if (autocapitalize_attribute_string_view.is_one_of_ignoring_ascii_case("off"sv, "none"sv))
            return AutocapitalizationHint::None;

        if (autocapitalize_attribute_string_view.equals_ignoring_ascii_case("words"sv))
            return AutocapitalizationHint::Words;

        if (autocapitalize_attribute_string_view.equals_ignoring_ascii_case("characters"sv))
            return AutocapitalizationHint::Characters;

        return AutocapitalizationHint::Sentences;
    }

    // If element is an autocapitalize-and-autocorrect inheriting element and has a non-null form owner, return the own autocapitalization hint of element's form owner.
    auto const* form_associated_element = as_if<FormAssociatedElement>(this);
    if (form_associated_element && form_associated_element->is_autocapitalize_and_autocorrect_inheriting() && form_associated_element->form())
        return form_associated_element->form()->own_autocapitalization_hint();

    // 3. Return default.
    return AutocapitalizationHint::Default;
}

// https://html.spec.whatwg.org/multipage/interaction.html#attr-autocapitalize
String HTMLElement::autocapitalize() const
{
    // The autocapitalize getter steps are to:
    // 1. Let state be the own autocapitalization hint of this.
    auto state = own_autocapitalization_hint();

    // 2. If state is default, then return the empty string.
    // 3. If state is none, then return "none".
    // 4. If state is sentences, then return "sentences".
    // 5. Return the keyword value corresponding to state.
    switch (state) {
    case AutocapitalizationHint::Default:
        return String {};
    case AutocapitalizationHint::None:
        return "none"_string;
    case AutocapitalizationHint::Sentences:
        return "sentences"_string;
    case AutocapitalizationHint::Words:
        return "words"_string;
    case AutocapitalizationHint::Characters:
        return "characters"_string;
    }

    VERIFY_NOT_REACHED();
}

void HTMLElement::set_autocapitalize(String const& given_value)
{
    // The autocapitalize setter steps are to set the autocapitalize content attribute to the given value.
    MUST(set_attribute(HTML::AttributeNames::autocapitalize, given_value));
}

// https://html.spec.whatwg.org/multipage/interaction.html#used-autocorrection-state
HTMLElement::AutocorrectionState HTMLElement::used_autocorrection_state() const
{
    // The autocorrect attribute is an enumerated attribute with the following keywords and states:
    // Keyword            | State | Brief description
    // on                 | on    | The user agent is permitted to automatically correct spelling errors while the user
    // (the empty string) |       | types. Whether spelling is automatically corrected while typing left is for the user
    //                    |       | agent to decide, and may depend on the element as well as the user's preferences.
    // off                | off   | The user agent is not allowed to automatically correct spelling while the user types.

    // The attribute's invalid value default and missing value default are both the on state.

    auto autocorrect_attribute_state = [](Optional<String> attribute) {
        if (attribute.has_value() && attribute.value().equals_ignoring_ascii_case("off"sv))
            return AutocorrectionState::Off;

        return AutocorrectionState::On;
    };

    // To compute the used autocorrection state of an element element, run these steps:
    // 1. If element is an input element whose type attribute is in one of the URL, E-mail, or Password states, then return off.
    if (auto const* input_element = as_if<HTMLInputElement>(this)) {
        if (first_is_one_of(input_element->type_state(), HTMLInputElement::TypeAttributeState::URL, HTMLInputElement::TypeAttributeState::Email, HTMLInputElement::TypeAttributeState::Password))
            return AutocorrectionState::Off;
    }

    // 2. If the autocorrect content attribute is present on element, then return the state of the attribute.
    auto maybe_autocorrect_attribute = attribute(HTML::AttributeNames::autocorrect);

    if (maybe_autocorrect_attribute.has_value())
        return autocorrect_attribute_state(maybe_autocorrect_attribute);

    // 3. If element is an autocapitalize-and-autocorrect inheriting element and has a non-null form owner, then return
    //    the state of element's form owner's autocorrect attribute.
    if (auto const* form_associated_element = as_if<FormAssociatedElement>(this)) {
        if (form_associated_element->is_autocapitalize_and_autocorrect_inheriting() && form_associated_element->form())
            return autocorrect_attribute_state(form_associated_element->form()->attribute(HTML::AttributeNames::autocorrect));
    }

    // 4. Return on.
    return AutocorrectionState::On;
}

// https://html.spec.whatwg.org/multipage/interaction.html#dom-autocorrect
bool HTMLElement::autocorrect() const
{
    // The autocorrect getter steps are: return true if the element's used autocorrection state is on and false if the element's used autocorrection state is off.
    return used_autocorrection_state() == AutocorrectionState::On;
}

// https://html.spec.whatwg.org/multipage/interaction.html#dom-autocorrect
void HTMLElement::set_autocorrect(bool given_value)
{
    // The setter steps are: if the given value is true, then the element's autocorrect attribute must be set to "on"; otherwise it must be set to "off".
    if (given_value)
        MUST(set_attribute(HTML::AttributeNames::autocorrect, "on"_string));
    else
        MUST(set_attribute(HTML::AttributeNames::autocorrect, "off"_string));
}

}
