Prechádzať zdrojové kódy

LibWeb: Implement document.execCommand("delete")

To facilitate the implementation of "delete" and all associated
algorithms, split off this piece of `Document` into a separate
directory.

This sets up the infrastructure for arbitrary commands to be supported.
Jelle Raaijmakers 8 mesiacov pred
rodič
commit
7bb865052a

+ 2 - 0
Libraries/LibWeb/Bindings/MainThreadVM.cpp

@@ -26,6 +26,7 @@
 #include <LibWeb/Bindings/WindowExposedInterfaces.h>
 #include <LibWeb/DOM/Document.h>
 #include <LibWeb/DOM/MutationType.h>
+#include <LibWeb/Editing/CommandNames.h>
 #include <LibWeb/HTML/AttributeNames.h>
 #include <LibWeb/HTML/CustomElements/CustomElementDefinition.h>
 #include <LibWeb/HTML/CustomElements/CustomElementReactionNames.h>
@@ -101,6 +102,7 @@ ErrorOr<void> initialize_main_thread_vm(HTML::EventLoop::Type type)
 
     // These strings could potentially live on the VM similar to CommonPropertyNames.
     DOM::MutationType::initialize_strings();
+    Editing::CommandNames::initialize_strings();
     HTML::AttributeNames::initialize_strings();
     HTML::CustomElementReactionNames::initialize_strings();
     HTML::EventNames::initialize_strings();

+ 4 - 0
Libraries/LibWeb/CMakeLists.txt

@@ -222,6 +222,10 @@ set(SOURCES
     DOMURL/URLSearchParams.cpp
     DOMURL/URLSearchParamsIterator.cpp
     Dump.cpp
+    Editing/CommandNames.cpp
+    Editing/Commands.cpp
+    Editing/ExecCommand.cpp
+    Editing/Internal/Algorithms.cpp
     Encoding/TextDecoder.cpp
     Encoding/TextEncoder.cpp
     EntriesAPI/FileSystemEntry.cpp

+ 0 - 42
Libraries/LibWeb/DOM/Document.cpp

@@ -5077,48 +5077,6 @@ JS::Value Document::named_item_value(FlyString const& name) const
     });
 }
 
-// https://w3c.github.io/editing/docs/execCommand/#execcommand()
-bool Document::exec_command(String const& command, bool show_ui, String const& value)
-{
-    dbgln("FIXME: document.execCommand(\"{}\", {}, \"{}\")", command, show_ui, value);
-    return false;
-}
-
-// https://w3c.github.io/editing/docs/execCommand/#querycommandenabled()
-bool Document::query_command_enabled(String const& command)
-{
-    dbgln("FIXME: document.queryCommandEnabled(\"{}\")", command);
-    return false;
-}
-
-// https://w3c.github.io/editing/docs/execCommand/#querycommandindeterm()
-bool Document::query_command_indeterm(String const& command)
-{
-    dbgln("FIXME: document.queryCommandIndeterm(\"{}\")", command);
-    return false;
-}
-
-// https://w3c.github.io/editing/docs/execCommand/#querycommandstate()
-bool Document::query_command_state(String const& command)
-{
-    dbgln("FIXME: document.queryCommandState(\"{}\")", command);
-    return false;
-}
-
-// https://w3c.github.io/editing/docs/execCommand/#querycommandsupported()
-bool Document::query_command_supported(String const& command)
-{
-    dbgln("FIXME: document.queryCommandSupported(\"{}\")", command);
-    return false;
-}
-
-// https://w3c.github.io/editing/docs/execCommand/#querycommandvalue()
-String Document::query_command_value(String const& command)
-{
-    dbgln("FIXME: document.queryCommandValue(\"{}\")", command);
-    return String {};
-}
-
 // https://drafts.csswg.org/resize-observer-1/#calculate-depth-for-node
 static size_t calculate_depth_for_node(Node const& node)
 {

+ 6 - 6
Libraries/LibWeb/DOM/Document.h

@@ -569,12 +569,12 @@ public:
     void set_previous_document_unload_timing(DocumentUnloadTimingInfo const& previous_document_unload_timing) { m_previous_document_unload_timing = previous_document_unload_timing; }
 
     // https://w3c.github.io/editing/docs/execCommand/
-    bool exec_command(String const& command, bool show_ui, String const& value);
-    bool query_command_enabled(String const& command);
-    bool query_command_indeterm(String const& command);
-    bool query_command_state(String const& command);
-    bool query_command_supported(String const& command);
-    String query_command_value(String const& command);
+    bool exec_command(FlyString const& command, bool show_ui, String const& value);
+    bool query_command_enabled(FlyString const& command);
+    bool query_command_indeterm(FlyString const& command);
+    bool query_command_state(FlyString const& command);
+    bool query_command_supported(FlyString const& command);
+    String query_command_value(FlyString const& command);
 
     // https://w3c.github.io/selection-api/#dfn-has-scheduled-selectionchange-event
     bool has_scheduled_selectionchange_event() const { return m_has_scheduled_selectionchange_event; }

+ 1 - 0
Libraries/LibWeb/DOM/Document.idl

@@ -143,6 +143,7 @@ interface Document : Node {
     readonly attribute Element? scrollingElement;
 
     // https://w3c.github.io/editing/docs/execCommand/
+    // FIXME: [CEReactions] boolean execCommand(DOMString commandId, optional boolean showUI = false, optional (TrustedHTML or DOMString) value = "");
     [CEReactions] boolean execCommand(DOMString commandId, optional boolean showUI = false, optional DOMString value = "");
     boolean queryCommandEnabled(DOMString commandId);
     boolean queryCommandIndeterm(DOMString commandId);

+ 29 - 0
Libraries/LibWeb/Editing/CommandNames.cpp

@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2024, Jelle Raaijmakers <jelle@ladybird.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <LibWeb/Editing/CommandNames.h>
+
+namespace Web::Editing::CommandNames {
+
+#define __ENUMERATE_COMMAND_NAME(name) FlyString name;
+ENUMERATE_COMMAND_NAMES
+#undef __ENUMERATE_COMMAND_NAME
+FlyString delete_;
+
+void initialize_strings()
+{
+    static bool s_initialized = false;
+    VERIFY(!s_initialized);
+
+#define __ENUMERATE_COMMAND_NAME(name) name = #name##_fly_string;
+    ENUMERATE_COMMAND_NAMES
+#undef __ENUMERATE_MATHML_TAG
+    delete_ = "delete"_fly_string;
+
+    s_initialized = true;
+}
+
+}

+ 62 - 0
Libraries/LibWeb/Editing/CommandNames.h

@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) 2024, Jelle Raaijmakers <jelle@ladybird.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <AK/FlyString.h>
+
+namespace Web::Editing::CommandNames {
+
+#define ENUMERATE_COMMAND_NAMES                         \
+    __ENUMERATE_COMMAND_NAME(backColor)                 \
+    __ENUMERATE_COMMAND_NAME(bold)                      \
+    __ENUMERATE_COMMAND_NAME(copy)                      \
+    __ENUMERATE_COMMAND_NAME(createLink)                \
+    __ENUMERATE_COMMAND_NAME(cut)                       \
+    __ENUMERATE_COMMAND_NAME(defaultParagraphSeparator) \
+    __ENUMERATE_COMMAND_NAME(fontName)                  \
+    __ENUMERATE_COMMAND_NAME(fontSize)                  \
+    __ENUMERATE_COMMAND_NAME(foreColor)                 \
+    __ENUMERATE_COMMAND_NAME(formatBlock)               \
+    __ENUMERATE_COMMAND_NAME(forwardDelete)             \
+    __ENUMERATE_COMMAND_NAME(hiliteColor)               \
+    __ENUMERATE_COMMAND_NAME(indent)                    \
+    __ENUMERATE_COMMAND_NAME(insertHTML)                \
+    __ENUMERATE_COMMAND_NAME(insertHorizontalRule)      \
+    __ENUMERATE_COMMAND_NAME(insertImage)               \
+    __ENUMERATE_COMMAND_NAME(insertLineBreak)           \
+    __ENUMERATE_COMMAND_NAME(insertOrderedList)         \
+    __ENUMERATE_COMMAND_NAME(insertParagraph)           \
+    __ENUMERATE_COMMAND_NAME(insertText)                \
+    __ENUMERATE_COMMAND_NAME(insertUnorderedList)       \
+    __ENUMERATE_COMMAND_NAME(italic)                    \
+    __ENUMERATE_COMMAND_NAME(justifyCenter)             \
+    __ENUMERATE_COMMAND_NAME(justifyFull)               \
+    __ENUMERATE_COMMAND_NAME(justifyLeft)               \
+    __ENUMERATE_COMMAND_NAME(justifyRight)              \
+    __ENUMERATE_COMMAND_NAME(outdent)                   \
+    __ENUMERATE_COMMAND_NAME(paste)                     \
+    __ENUMERATE_COMMAND_NAME(redo)                      \
+    __ENUMERATE_COMMAND_NAME(removeFormat)              \
+    __ENUMERATE_COMMAND_NAME(selectAll)                 \
+    __ENUMERATE_COMMAND_NAME(strikethrough)             \
+    __ENUMERATE_COMMAND_NAME(styleWithCSS)              \
+    __ENUMERATE_COMMAND_NAME(subscript)                 \
+    __ENUMERATE_COMMAND_NAME(superscript)               \
+    __ENUMERATE_COMMAND_NAME(underline)                 \
+    __ENUMERATE_COMMAND_NAME(undo)                      \
+    __ENUMERATE_COMMAND_NAME(unlink)                    \
+    __ENUMERATE_COMMAND_NAME(useCSS)
+
+#define __ENUMERATE_COMMAND_NAME(name) extern FlyString name;
+ENUMERATE_COMMAND_NAMES
+#undef __ENUMERATE_COMMAND_NAME
+
+extern FlyString delete_;
+
+void initialize_strings();
+
+}

+ 340 - 0
Libraries/LibWeb/Editing/Commands.cpp

@@ -0,0 +1,340 @@
+/*
+ * Copyright (c) 2024, Jelle Raaijmakers <jelle@ladybird.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <LibWeb/DOM/Document.h>
+#include <LibWeb/DOM/ElementFactory.h>
+#include <LibWeb/DOM/Range.h>
+#include <LibWeb/DOM/Text.h>
+#include <LibWeb/Editing/CommandNames.h>
+#include <LibWeb/Editing/Commands.h>
+#include <LibWeb/Editing/Internal/Algorithms.h>
+#include <LibWeb/HTML/HTMLAnchorElement.h>
+#include <LibWeb/HTML/HTMLBRElement.h>
+#include <LibWeb/HTML/HTMLImageElement.h>
+#include <LibWeb/HTML/HTMLTableElement.h>
+#include <LibWeb/Namespace.h>
+
+namespace Web::Editing {
+
+// https://w3c.github.io/editing/docs/execCommand/#the-delete-command
+bool command_delete_action(DOM::Document& document, String const&)
+{
+    // 1. If the active range is not collapsed, delete the selection and return true.
+    auto& selection = *document.get_selection();
+    auto& active_range = *selection.range();
+    if (!active_range.collapsed()) {
+        delete_the_selection(selection);
+        return true;
+    }
+
+    // 2. Canonicalize whitespace at the active range's start.
+    canonicalize_whitespace(active_range.start_container(), active_range.start_offset());
+
+    // 3. Let node and offset be the active range's start node and offset.
+    auto node = active_range.start_container();
+    int offset = active_range.start_offset();
+
+    // 4. Repeat the following steps:
+    GC::Ptr<DOM::Node> offset_minus_one_child;
+    while (true) {
+        offset_minus_one_child = node->child_at_index(offset - 1);
+
+        // 1. If offset is zero and node's previousSibling is an editable invisible node, remove
+        //    node's previousSibling from its parent.
+        if (auto* previous_sibling = node->previous_sibling()) {
+            if (offset == 0 && previous_sibling->is_editable() && is_invisible_node(*previous_sibling)) {
+                previous_sibling->remove();
+                continue;
+            }
+        }
+
+        // 2. Otherwise, if node has a child with index offset − 1 and that child is an editable
+        //    invisible node, remove that child from node, then subtract one from offset.
+        if (offset_minus_one_child && offset_minus_one_child->is_editable() && is_invisible_node(*offset_minus_one_child)) {
+            offset_minus_one_child->remove();
+            --offset;
+            continue;
+        }
+
+        // 3. Otherwise, if offset is zero and node is an inline node, or if node is an invisible
+        //    node, set offset to the index of node, then set node to its parent.
+        if ((offset == 0 && is_inline_node(node)) || is_invisible_node(node)) {
+            offset = node->index();
+            node = *node->parent();
+            continue;
+        }
+
+        // 4. Otherwise, if node has a child with index offset − 1 and that child is an editable a,
+        //    remove that child from node, preserving its descendants. Then return true.
+        if (is<HTML::HTMLAnchorElement>(offset_minus_one_child.ptr()) && offset_minus_one_child->is_editable()) {
+            remove_node_preserving_its_descendants(*offset_minus_one_child);
+            return true;
+        }
+
+        // 5. Otherwise, if node has a child with index offset − 1 and that child is not a block
+        //    node or a br or an img, set node to that child, then set offset to the length of node.
+        if (offset_minus_one_child && !is_block_node(*offset_minus_one_child)
+            && !is<HTML::HTMLBRElement>(*offset_minus_one_child) && !is<HTML::HTMLImageElement>(*offset_minus_one_child)) {
+            node = *offset_minus_one_child;
+            offset = node->length();
+            continue;
+        }
+
+        // 6. Otherwise, break from this loop.
+        break;
+    }
+
+    // 5. If node is a Text node and offset is not zero, or if node is a block node that has a child
+    //    with index offset − 1 and that child is a br or hr or img:
+    bool block_node_child_is_relevant_type = false;
+    if (is_block_node(node)) {
+        if (auto* child_node = node->child_at_index(offset - 1)) {
+            auto& child_element = static_cast<DOM::Element&>(*child_node);
+            block_node_child_is_relevant_type = child_element.local_name().is_one_of(HTML::TagNames::br, HTML::TagNames::hr, HTML::TagNames::img);
+        }
+    }
+    if ((is<DOM::Text>(*node) && offset != 0) || block_node_child_is_relevant_type) {
+        // 1. Call collapse(node, offset) on the context object's selection.
+        MUST(selection.collapse(node, offset));
+
+        // 2. Call extend(node, offset − 1) on the context object's selection.
+        MUST(selection.extend(*node, offset - 1));
+
+        // 3. Delete the selection.
+        delete_the_selection(selection);
+
+        // 4. Return true.
+        return true;
+    }
+
+    // 6. If node is an inline node, return true.
+    if (is_inline_node(node))
+        return true;
+
+    // 7. If node is an li or dt or dd and is the first child of its parent, and offset is zero:
+    auto& node_element = static_cast<DOM::Element&>(*node);
+    if (offset == 0 && node->index() == 0
+        && node_element.local_name().is_one_of(HTML::TagNames::li, HTML::TagNames::dt, HTML::TagNames::dd)) {
+        // 1. Let items be a list of all lis that are ancestors of node.
+        auto items = Vector<GC::Ref<DOM::Element>>();
+        GC::Ptr<DOM::Node> ancestor = node->parent();
+        do {
+            auto& ancestor_element = static_cast<DOM::Element&>(*ancestor);
+            if (ancestor_element.local_name() == HTML::TagNames::li)
+                items.append(ancestor_element);
+            ancestor = ancestor->parent();
+        } while (ancestor);
+
+        // 2. Normalize sublists of each item in items.
+        for (auto item : items)
+            normalize_sublists_in_node(*item);
+
+        // 3. Record the values of the one-node list consisting of node, and let values be the
+        //    result.
+        auto values = record_the_values_of_nodes({ node });
+
+        // 4. Split the parent of the one-node list consisting of node.
+        split_the_parent_of_nodes({ node });
+
+        // 5. Restore the values from values.
+        restore_the_values_of_nodes(values);
+
+        // FIXME: 6. If node is a dd or dt, and it is not an allowed child of any of its ancestors in the
+        //    same editing host, set the tag name of node to the default single-line container name
+        //    and let node be the result.
+
+        // FIXME: 7. Fix disallowed ancestors of node.
+
+        // 8. Return true.
+        return true;
+    }
+
+    // 8. Let start node equal node and let start offset equal offset.
+    auto start_node = node;
+    auto start_offset = offset;
+
+    // 9. Repeat the following steps:
+    while (true) {
+        // 1. If start offset is zero, set start offset to the index of start node and then set
+        //    start node to its parent.
+        if (start_offset == 0) {
+            start_offset = start_node->index();
+            start_node = *start_node->parent();
+            continue;
+        }
+
+        // 2. Otherwise, if start node has an editable invisible child with index start offset minus
+        //    one, remove it from start node and subtract one from start offset.
+        offset_minus_one_child = start_node->child_at_index(start_offset - 1);
+        if (offset_minus_one_child && offset_minus_one_child->is_editable() && is_invisible_node(*offset_minus_one_child)) {
+            offset_minus_one_child->remove();
+            --start_offset;
+            continue;
+        }
+
+        // 3. Otherwise, break from this loop.
+        break;
+    }
+
+    // FIXME: 10. If offset is zero, and node has an editable inclusive ancestor in the same editing host
+    //     that's an indentation element:
+    if (false) {
+        // FIXME: 1. Block-extend the range whose start and end are both (node, 0), and let new range be
+        //    the result.
+
+        // FIXME: 2. Let node list be a list of nodes, initially empty.
+
+        // FIXME: 3. For each node current node contained in new range, append current node to node list if
+        //    the last member of node list (if any) is not an ancestor of current node, and current
+        //    node is editable but has no editable descendants.
+
+        // FIXME: 4. Outdent each node in node list.
+
+        // 5. Return true.
+        return true;
+    }
+
+    // 11. If the child of start node with index start offset is a table, return true.
+    if (is<HTML::HTMLTableElement>(start_node->child_at_index(start_offset)))
+        return true;
+
+    // 12. If start node has a child with index start offset − 1, and that child is a table:
+    offset_minus_one_child = start_node->child_at_index(start_offset - 1);
+    if (is<HTML::HTMLTableElement>(offset_minus_one_child.ptr())) {
+        // 1. Call collapse(start node, start offset − 1) on the context object's selection.
+        MUST(selection.collapse(start_node, start_offset - 1));
+
+        // 2. Call extend(start node, start offset) on the context object's selection.
+        MUST(selection.extend(*start_node, start_offset));
+
+        // 3. Return true.
+        return true;
+    }
+
+    // 13. If offset is zero; and either the child of start node with index start offset minus one
+    //     is an hr, or the child is a br whose previousSibling is either a br or not an inline
+    //     node:
+    if (offset == 0 && is<DOM::Element>(offset_minus_one_child.ptr())) {
+        auto& child_element = static_cast<DOM::Element&>(*offset_minus_one_child);
+        auto* previous_sibling = child_element.previous_sibling();
+        if (child_element.local_name() == HTML::TagNames::hr
+            || (is<HTML::HTMLBRElement>(child_element) && previous_sibling && (is<HTML::HTMLBRElement>(*previous_sibling) || !is_inline_node(*previous_sibling)))) {
+            // 1. Call collapse(start node, start offset − 1) on the context object's selection.
+            MUST(selection.collapse(start_node, start_offset - 1));
+
+            // 2. Call extend(start node, start offset) on the context object's selection.
+            MUST(selection.extend(*start_node, start_offset));
+
+            // 3. Delete the selection.
+            delete_the_selection(selection);
+
+            // 4. Call collapse(node, offset) on the selection.
+            MUST(selection.collapse(node, offset));
+
+            // 5. Return true.
+            return true;
+        }
+    }
+
+    // 14. If the child of start node with index start offset is an li or dt or dd, and that child's
+    //     firstChild is an inline node, and start offset is not zero:
+    auto is_li_dt_or_dd = [](DOM::Element const& node) {
+        return node.local_name().is_one_of(HTML::TagNames::li, HTML::TagNames::dt, HTML::TagNames::dd);
+    };
+    auto* start_offset_child = start_node->child_at_index(start_offset);
+    if (start_offset != 0 && is<DOM::Element>(start_offset_child)
+        && is_li_dt_or_dd(static_cast<DOM::Element&>(*start_offset_child))
+        && start_offset_child->has_children() && is_inline_node(*start_offset_child->first_child())) {
+        // 1. Let previous item be the child of start node with index start offset minus one.
+        GC::Ref<DOM::Node> previous_item = *start_node->child_at_index(start_offset - 1);
+
+        // 2. If previous item's lastChild is an inline node other than a br, call
+        //    createElement("br") on the context object and append the result as the last child of
+        //    previous item.
+        GC::Ptr<DOM::Node> previous_item_last_child = previous_item->last_child();
+        if (previous_item_last_child && is_inline_node(*previous_item_last_child) && !is<HTML::HTMLBRElement>(*previous_item_last_child)) {
+            auto br_element = MUST(DOM::create_element(previous_item->document(), HTML::TagNames::br, Namespace::HTML));
+            MUST(previous_item->append_child(br_element));
+        }
+
+        // 3. If previous item's lastChild is an inline node, call createElement("br") on the
+        //    context object and append the result as the last child of previous item.
+        if (previous_item_last_child && is_inline_node(*previous_item_last_child)) {
+            auto br_element = MUST(DOM::create_element(previous_item->document(), HTML::TagNames::br, Namespace::HTML));
+            MUST(previous_item->append_child(br_element));
+        }
+    }
+
+    // FIXME: 15. If start node's child with index start offset is an li or dt or dd, and that child's
+    //     previousSibling is also an li or dt or dd:
+    if (false) {
+        // FIXME: 1. Call cloneRange() on the active range, and let original range be the result.
+
+        // FIXME: 2. Set start node to its child with index start offset − 1.
+
+        // FIXME: 3. Set start offset to start node's length.
+
+        // FIXME: 4. Set node to start node's nextSibling.
+
+        // FIXME: 5. Call collapse(start node, start offset) on the context object's selection.
+
+        // FIXME: 6. Call extend(node, 0) on the context object's selection.
+
+        // FIXME: 7. Delete the selection.
+
+        // FIXME: 8. Call removeAllRanges() on the context object's selection.
+
+        // FIXME: 9. Call addRange(original range) on the context object's selection.
+
+        // 10. Return true.
+        return true;
+    }
+
+    // 16. While start node has a child with index start offset minus one:
+    while (start_node->child_at_index(start_offset - 1) != nullptr) {
+        // 1. If start node's child with index start offset minus one is editable and invisible,
+        //    remove it from start node, then subtract one from start offset.
+        offset_minus_one_child = start_node->child_at_index(start_offset - 1);
+        if (offset_minus_one_child->is_editable() && is_invisible_node(*offset_minus_one_child)) {
+            offset_minus_one_child->remove();
+            --start_offset;
+        }
+
+        // 2. Otherwise, set start node to its child with index start offset minus one, then set
+        //    start offset to the length of start node.
+        else {
+            start_node = *offset_minus_one_child;
+            start_offset = start_node->length();
+        }
+    }
+
+    // 17. Call collapse(start node, start offset) on the context object's selection.
+    MUST(selection.collapse(start_node, start_offset));
+
+    // 18. Call extend(node, offset) on the context object's selection.
+    MUST(selection.extend(*node, offset));
+
+    // FIXME: 19. Delete the selection, with direction "backward".
+    delete_the_selection(selection);
+
+    // 20. Return true.
+    return true;
+}
+
+static Array const commands {
+    CommandDefinition { CommandNames::delete_, command_delete_action, {}, {}, {} },
+};
+
+Optional<CommandDefinition const&> find_command_definition(FlyString const& command)
+{
+    for (auto& definition : commands) {
+        if (command.equals_ignoring_ascii_case(definition.command))
+            return definition;
+    }
+    return {};
+}
+
+}

+ 26 - 0
Libraries/LibWeb/Editing/Commands.h

@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2024, Jelle Raaijmakers <jelle@ladybird.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <LibWeb/Forward.h>
+
+namespace Web::Editing {
+
+struct CommandDefinition {
+    FlyString const& command;
+    Function<bool(DOM::Document&, String const&)> action;
+    Function<bool(DOM::Document const&)> indeterminate;
+    Function<bool(DOM::Document const&)> state;
+    Function<String(DOM::Document const&)> value;
+};
+
+Optional<CommandDefinition const&> find_command_definition(FlyString const&);
+
+// Command implementations
+bool command_delete_action(DOM::Document&, String const&);
+
+}

+ 201 - 0
Libraries/LibWeb/Editing/ExecCommand.cpp

@@ -0,0 +1,201 @@
+/*
+ * Copyright (c) 2024, Jelle Raaijmakers <jelle@ladybird.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <LibWeb/DOM/Document.h>
+#include <LibWeb/DOM/Range.h>
+#include <LibWeb/Editing/CommandNames.h>
+#include <LibWeb/Editing/Commands.h>
+#include <LibWeb/Editing/Internal/Algorithms.h>
+#include <LibWeb/Selection/Selection.h>
+
+namespace Web::DOM {
+
+// https://w3c.github.io/editing/docs/execCommand/#execcommand()
+bool Document::exec_command(FlyString const& command, [[maybe_unused]] bool show_ui, String const& value)
+{
+    // 1. If only one argument was provided, let show UI be false.
+    // 2. If only one or two arguments were provided, let value be the empty string.
+    // NOTE: these steps are dealt by the default values for both show_ui and value
+
+    // 3. If command is not supported or not enabled, return false.
+    // NOTE: query_command_enabled() also checks if command is supported
+    if (!query_command_enabled(command))
+        return false;
+
+    // 4. If command is not in the Miscellaneous commands section:
+    //
+    //    We don't fire events for copy/cut/paste/undo/redo/selectAll because they should all have
+    //    their own events. We don't fire events for styleWithCSS/useCSS because it's not obvious
+    //    where to fire them, or why anyone would want them. We don't fire events for unsupported
+    //    commands, because then if they became supported and were classified with the miscellaneous
+    //    events, we'd have to stop firing events for consistency's sake.
+    //
+    // AD-HOC: The defaultParagraphSeparator command is also in the Miscellaneous commands section
+    if (command != Editing::CommandNames::defaultParagraphSeparator
+        && command != Editing::CommandNames::redo
+        && command != Editing::CommandNames::selectAll
+        && command != Editing::CommandNames::styleWithCSS
+        && command != Editing::CommandNames::undo
+        && command != Editing::CommandNames::useCSS) {
+        // FIXME: 1. Let affected editing host be the editing host that is an inclusive ancestor of the
+        //    active range's start node and end node, and is not the ancestor of any editing host
+        //    that is an inclusive ancestor of the active range's start node and end node.
+
+        // FIXME: 2. Fire an event named "beforeinput" at affected editing host using InputEvent, with its
+        //    bubbles and cancelable attributes initialized to true, and its data attribute
+        //    initialized to null.
+
+        // FIXME: 3. If the value returned by the previous step is false, return false.
+
+        // 4. If command is not enabled, return false.
+        //
+        //    We have to check again whether the command is enabled, because the beforeinput handler
+        //    might have done something annoying like getSelection().removeAllRanges().
+        if (!query_command_enabled(command))
+            return false;
+
+        // FIXME: 5. Let affected editing host be the editing host that is an inclusive ancestor of the
+        //    active range's start node and end node, and is not the ancestor of any editing host
+        //    that is an inclusive ancestor of the active range's start node and end node.
+        //
+        //    This new affected editing host is what we'll fire the input event at in a couple of
+        //    lines. We want to compute it beforehand just to be safe: bugs in the command action
+        //    might remove the selection or something bad like that, and we don't want to have to
+        //    handle it later. We recompute it after the beforeinput event is handled so that if the
+        //    handler moves the selection to some other editing host, the input event will be fired
+        //    at the editing host that was actually affected.
+    }
+
+    // 5. Take the action for command, passing value to the instructions as an argument.
+    auto optional_command = Editing::find_command_definition(command);
+    VERIFY(optional_command.has_value());
+    auto const& command_definition = optional_command.release_value();
+    auto command_result = command_definition.action(*this, value);
+
+    // 6. If the previous step returned false, return false.
+    if (!command_result)
+        return false;
+
+    // FIXME: 7. If the action modified DOM tree, then fire an event named "input" at affected editing host
+    //    using InputEvent, with its isTrusted and bubbles attributes initialized to true, inputType
+    //    attribute initialized to the mapped value of command, and its data attribute initialized
+    //    to null.
+
+    // 8. Return true.
+    return true;
+}
+
+// https://w3c.github.io/editing/docs/execCommand/#querycommandenabled()
+bool Document::query_command_enabled(FlyString const& command)
+{
+    // 2. Return true if command is both supported and enabled, false otherwise.
+    if (!query_command_supported(command))
+        return false;
+
+    // https://w3c.github.io/editing/docs/execCommand/#enabled
+    // Among commands defined in this specification, those listed in Miscellaneous commands are
+    // always enabled, except for the cut command and the paste command.
+    // AD-HOC: Cut and Paste are not in the Miscellaneous commands section; so Copy is assumed
+    // AD-HOC: DefaultParagraphSeparator is also in the Miscellaneous commands section
+    if (command == Editing::CommandNames::copy
+        || command == Editing::CommandNames::defaultParagraphSeparator
+        || command == Editing::CommandNames::redo
+        || command == Editing::CommandNames::selectAll
+        || command == Editing::CommandNames::styleWithCSS
+        || command == Editing::CommandNames::undo
+        || command == Editing::CommandNames::useCSS)
+        return true;
+
+    // The other commands defined here are enabled if the active range is not null,
+    auto selection = get_selection();
+    if (!selection)
+        return false;
+    auto active_range = selection->range();
+    if (!active_range)
+        return false;
+
+    // its start node is either editable or an editing host,
+    auto& start_node = *active_range->start_container();
+    if (!start_node.is_editable() && !Editing::is_editing_host(start_node))
+        return false;
+
+    // FIXME: the editing host of its start node is not an EditContext editing host,
+
+    // its end node is either editable or an editing host,
+    auto& end_node = *active_range->end_container();
+    if (!end_node.is_editable() && !Editing::is_editing_host(end_node))
+        return false;
+
+    // FIXME: the editing host of its end node is not an EditContext editing host,
+
+    // FIXME: and there is some editing host that is an inclusive ancestor of both its start node and its
+    //        end node.
+
+    return true;
+}
+
+// https://w3c.github.io/editing/docs/execCommand/#querycommandindeterm()
+bool Document::query_command_indeterm(FlyString const& command)
+{
+    // 1. If command is not supported or has no indeterminacy, return false.
+    auto optional_command = Editing::find_command_definition(command);
+    if (!optional_command.has_value())
+        return false;
+    auto const& command_definition = optional_command.value();
+    if (!command_definition.indeterminate)
+        return false;
+
+    // 2. Return true if command is indeterminate, otherwise false.
+    return command_definition.indeterminate(*this);
+}
+
+// https://w3c.github.io/editing/docs/execCommand/#querycommandstate()
+bool Document::query_command_state(FlyString const& command)
+{
+    // 1. If command is not supported or has no state, return false.
+    auto optional_command = Editing::find_command_definition(command);
+    if (!optional_command.has_value())
+        return false;
+    auto const& command_definition = optional_command.release_value();
+    if (!command_definition.state)
+        return false;
+
+    // FIXME: 2. If the state override for command is set, return it.
+
+    // 3. Return true if command's state is true, otherwise false.
+    return command_definition.state(*this);
+}
+
+// https://w3c.github.io/editing/docs/execCommand/#querycommandsupported()
+bool Document::query_command_supported(FlyString const& command)
+{
+    // When the queryCommandSupported(command) method on the Document interface is invoked, the
+    // user agent must return true if command is supported and available within the current script
+    // on the current site, and false otherwise.
+    return Editing::find_command_definition(command).has_value();
+}
+
+// https://w3c.github.io/editing/docs/execCommand/#querycommandvalue()
+String Document::query_command_value(FlyString const& command)
+{
+    // 1. If command is not supported or has no value, return the empty string.
+    auto optional_command = Editing::find_command_definition(command);
+    if (!optional_command.has_value())
+        return {};
+    auto const& command_definition = optional_command.release_value();
+    if (!command_definition.value)
+        return {};
+
+    // FIXME: 2. If command is "fontSize" and its value override is set, convert the value override to an
+    //    integer number of pixels and return the legacy font size for the result.
+
+    // FIXME: 3. If the value override for command is set, return it.
+
+    // 4. Return command's value.
+    return command_definition.value(*this);
+}
+
+}

+ 1498 - 0
Libraries/LibWeb/Editing/Internal/Algorithms.cpp

@@ -0,0 +1,1498 @@
+/*
+ * Copyright (c) 2024, Jelle Raaijmakers <jelle@ladybird.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <LibWeb/DOM/CharacterData.h>
+#include <LibWeb/DOM/DocumentFragment.h>
+#include <LibWeb/DOM/DocumentType.h>
+#include <LibWeb/DOM/Element.h>
+#include <LibWeb/DOM/ElementFactory.h>
+#include <LibWeb/DOM/Range.h>
+#include <LibWeb/DOM/Text.h>
+#include <LibWeb/Editing/CommandNames.h>
+#include <LibWeb/Editing/Internal/Algorithms.h>
+#include <LibWeb/HTML/HTMLBRElement.h>
+#include <LibWeb/HTML/HTMLElement.h>
+#include <LibWeb/HTML/HTMLImageElement.h>
+#include <LibWeb/HTML/HTMLOListElement.h>
+#include <LibWeb/HTML/HTMLUListElement.h>
+#include <LibWeb/Infra/CharacterTypes.h>
+#include <LibWeb/Layout/Node.h>
+#include <LibWeb/Namespace.h>
+
+namespace Web::Editing {
+
+// https://w3c.github.io/editing/docs/execCommand/#canonical-space-sequence
+String canonical_space_sequence(u32 length, bool non_breaking_start, bool non_breaking_end)
+{
+    auto n = length;
+
+    // 1. If n is zero, return the empty string.
+    if (n == 0)
+        return {};
+
+    // 2. If n is one and both non-breaking start and non-breaking end are false, return a single
+    //    space (U+0020).
+    if (n == 1 && !non_breaking_start && !non_breaking_end)
+        return " "_string;
+
+    // 3. If n is one, return a single non-breaking space (U+00A0).
+    if (n == 1)
+        return "\u00A0"_string;
+
+    // 4. Let buffer be the empty string.
+    StringBuilder buffer;
+
+    // 5. If non-breaking start is true, let repeated pair be U+00A0 U+0020. Otherwise, let it be
+    //    U+0020 U+00A0.
+    auto repeated_pair = non_breaking_start ? "\u00A0 "sv : " \u00A0"sv;
+
+    // 6. While n is greater than three, append repeated pair to buffer and subtract two from n.
+    while (n > 3) {
+        buffer.append(repeated_pair);
+        n -= 2;
+    }
+
+    // 7. If n is three, append a three-code unit string to buffer depending on non-breaking start
+    //    and non-breaking end:
+    if (n == 3) {
+        // non-breaking start and non-breaking end false
+        // U+0020 U+00A0 U+0020
+        if (!non_breaking_start && !non_breaking_end)
+            buffer.append(" \u00A0 "sv);
+
+        // non-breaking start true, non-breaking end false
+        // U+00A0 U+00A0 U+0020
+        else if (non_breaking_start && !non_breaking_end)
+            buffer.append("\u00A0\u00A0 "sv);
+
+        // non-breaking start false, non-breaking end true
+        // U+0020 U+00A0 U+00A0
+        else if (!non_breaking_start)
+            buffer.append(" \u00A0\u00A0"sv);
+
+        // non-breaking start and non-breaking end both true
+        // U+00A0 U+0020 U+00A0
+        else
+            buffer.append("\u00A0 \u00A0"sv);
+    }
+
+    // 8. Otherwise, append a two-code unit string to buffer depending on non-breaking start and
+    //    non-breaking end:
+    else {
+        // non-breaking start and non-breaking end false
+        // non-breaking start true, non-breaking end false
+        // U+00A0 U+0020
+        if (!non_breaking_start && !non_breaking_end)
+            buffer.append("\u00A0 "sv);
+
+        // non-breaking start false, non-breaking end true
+        // U+0020 U+00A0
+        else if (!non_breaking_start)
+            buffer.append(" \u00A0"sv);
+
+        // non-breaking start and non-breaking end both true
+        // U+00A0 U+00A0
+        else
+            buffer.append("\u00A0\u00A0"sv);
+    }
+
+    // 9. Return buffer.
+    return MUST(buffer.to_string());
+}
+
+// https://w3c.github.io/editing/docs/execCommand/#canonicalize-whitespace
+void canonicalize_whitespace(GC::Ref<DOM::Node> node, u32 offset, bool fix_collapsed_space)
+{
+    // 1. If node is neither editable nor an editing host, abort these steps.
+    if (!node->is_editable() || !is_editing_host(node))
+        return;
+
+    // 2. Let start node equal node and let start offset equal offset.
+    auto start_node = node;
+    auto start_offset = offset;
+
+    // 3. Repeat the following steps:
+    while (true) {
+        // 1. If start node has a child in the same editing host with index start offset minus one,
+        //    set start node to that child, then set start offset to start node's length.
+        auto* offset_minus_one_child = start_node->child_at_index(start_offset - 1);
+        if (offset_minus_one_child && is_in_same_editing_host(*start_node, *offset_minus_one_child)) {
+            start_node = *offset_minus_one_child;
+            start_offset = start_node->length();
+            continue;
+        }
+
+        // 2. Otherwise, if start offset is zero and start node does not follow a line break and
+        //    start node's parent is in the same editing host, set start offset to start node's
+        //    index, then set start node to its parent.
+        if (start_offset == 0 && !follows_a_line_break(start_node) && is_in_same_editing_host(*start_node, *start_node->parent())) {
+            start_offset = start_node->index();
+            start_node = *start_node->parent();
+            continue;
+        }
+
+        // 3. Otherwise, if start node is a Text node and its parent's resolved value for
+        //    "white-space" is neither "pre" nor "pre-wrap" and start offset is not zero and the
+        //    (start offset − 1)st code unit of start node's data is a space (0x0020) or
+        //    non-breaking space (0x00A0), subtract one from start offset.
+        auto* layout_node = start_node->parent()->layout_node();
+        if (layout_node && is<DOM::Text>(*start_node) && start_offset != 0) {
+            auto parent_white_space = layout_node->computed_values().white_space();
+
+            // FIXME: Find a way to get code points directly from the UTF-8 string
+            auto start_node_data = *start_node->text_content();
+            auto utf16_code_units = MUST(AK::utf8_to_utf16(start_node_data));
+            auto offset_minus_one_code_point = Utf16View { utf16_code_units }.code_point_at(start_offset - 1);
+            if (parent_white_space != CSS::WhiteSpace::Pre && parent_white_space != CSS::WhiteSpace::PreWrap
+                && (offset_minus_one_code_point == 0x20 || offset_minus_one_code_point == 0xA0)) {
+                --start_offset;
+                continue;
+            }
+        }
+
+        // 4. Otherwise, break from this loop.
+        break;
+    }
+
+    // 4. Let end node equal start node and end offset equal start offset.
+    auto end_node = start_node;
+    auto end_offset = start_offset;
+
+    // 5. Let length equal zero.
+    auto length = 0;
+
+    // 6. Let collapse spaces be true if start offset is zero and start node follows a line break,
+    //    otherwise false.
+    auto collapse_spaces = start_offset == 0 && follows_a_line_break(start_node);
+
+    // 7. Repeat the following steps:
+    while (true) {
+        // 1. If end node has a child in the same editing host with index end offset, set end node
+        //    to that child, then set end offset to zero.
+        auto* offset_child = end_node->child_at_index(end_offset);
+        if (offset_child && is_in_same_editing_host(*end_node, *offset_child)) {
+            end_node = *offset_child;
+            end_offset = 0;
+            continue;
+        }
+
+        // 2. Otherwise, if end offset is end node's length and end node does not precede a line
+        //    break and end node's parent is in the same editing host, set end offset to one plus
+        //    end node's index, then set end node to its parent.
+        if (end_offset == end_node->length() && !precedes_a_line_break(end_node) && is_in_same_editing_host(*end_node, *end_node->parent())) {
+            end_offset = end_node->index() + 1;
+            end_node = *end_node->parent();
+            continue;
+        }
+
+        // 3. Otherwise, if end node is a Text node and its parent's resolved value for
+        //    "white-space" is neither "pre" nor "pre-wrap" and end offset is not end node's length
+        //    and the end offsetth code unit of end node's data is a space (0x0020) or non-breaking
+        //    space (0x00A0):
+        auto* layout_node = end_node->parent()->layout_node();
+        if (layout_node && is<DOM::Text>(*end_node) && end_offset != end_node->length()) {
+            auto parent_white_space = layout_node->computed_values().white_space();
+
+            // FIXME: Find a way to get code points directly from the UTF-8 string
+            auto end_node_data = *end_node->text_content();
+            auto utf16_code_units = MUST(AK::utf8_to_utf16(end_node_data));
+            auto offset_code_point = Utf16View { utf16_code_units }.code_point_at(end_offset);
+            if (parent_white_space != CSS::WhiteSpace::Pre && parent_white_space != CSS::WhiteSpace::PreWrap
+                && (offset_code_point == 0x20 || offset_code_point == 0xA0)) {
+                // 1. If fix collapsed space is true, and collapse spaces is true, and the end offsetth
+                //    code unit of end node's data is a space (0x0020): call deleteData(end offset, 1)
+                //    on end node, then continue this loop from the beginning.
+                if (fix_collapsed_space && collapse_spaces && offset_code_point == 0x20) {
+                    MUST(static_cast<DOM::CharacterData&>(*end_node).delete_data(end_offset, 1));
+                    continue;
+                }
+
+                // 2. Set collapse spaces to true if the end offsetth code unit of end node's data is a
+                //    space (0x0020), false otherwise.
+                collapse_spaces = offset_code_point == 0x20;
+
+                // 3. Add one to end offset.
+                ++end_offset;
+
+                // 4. Add one to length.
+                ++length;
+
+                // NOTE: We continue the loop here since we matched every condition from step 7.3
+                continue;
+            }
+        }
+
+        // 4. Otherwise, break from this loop.
+        break;
+    }
+
+    // 8. If fix collapsed space is true, then while (start node, start offset) is before (end node,
+    //    end offset):
+    if (fix_collapsed_space) {
+        while (true) {
+            auto relative_position = position_of_boundary_point_relative_to_other_boundary_point(*start_node, start_offset, *end_node, end_offset);
+            if (relative_position != DOM::RelativeBoundaryPointPosition::Before)
+                break;
+
+            // 1. If end node has a child in the same editing host with index end offset − 1, set end
+            //    node to that child, then set end offset to end node's length.
+            auto offset_minus_one_child = end_node->child_at_index(end_offset - 1);
+            if (offset_minus_one_child && is_in_same_editing_host(end_node, *offset_minus_one_child)) {
+                end_node = *offset_minus_one_child;
+                end_offset = end_node->length();
+                continue;
+            }
+
+            // 2. Otherwise, if end offset is zero and end node's parent is in the same editing host,
+            //    set end offset to end node's index, then set end node to its parent.
+            if (end_offset == 0 && is_in_same_editing_host(end_node, *end_node->parent())) {
+                end_offset = end_node->index();
+                end_node = *end_node->parent();
+                continue;
+            }
+
+            // 3. Otherwise, if end node is a Text node and its parent's resolved value for
+            //    "white-space" is neither "pre" nor "pre-wrap" and end offset is end node's length and
+            //    the last code unit of end node's data is a space (0x0020) and end node precedes a line
+            //    break:
+            auto* layout_node = end_node->parent()->layout_node();
+            if (layout_node && is<DOM::Text>(*end_node) && end_offset == end_node->length() && precedes_a_line_break(end_node)) {
+                auto parent_white_space = layout_node->computed_values().white_space();
+
+                // FIXME: Find a way to get code points directly from the UTF-8 string
+                auto end_node_data = *end_node->text_content();
+                auto utf16_code_units = MUST(AK::utf8_to_utf16(end_node_data));
+                auto utf16_view = Utf16View { utf16_code_units };
+                auto last_code_point = utf16_view.code_point_at(utf16_view.length_in_code_points() - 1);
+                if (parent_white_space != CSS::WhiteSpace::Pre && parent_white_space != CSS::WhiteSpace::PreWrap
+                    && last_code_point == 0x20) {
+                    // 1. Subtract one from end offset.
+                    --end_offset;
+
+                    // 2. Subtract one from length.
+                    --length;
+
+                    // 3. Call deleteData(end offset, 1) on end node.
+                    MUST(static_cast<DOM::CharacterData&>(*end_node).delete_data(end_offset, 1));
+
+                    // NOTE: We continue the loop here since we matched every condition from step 8.3
+                    continue;
+                }
+            }
+
+            // 4. Otherwise, break from this loop.
+            break;
+        }
+    }
+
+    // 9. Let replacement whitespace be the canonical space sequence of length length. non-breaking
+    //    start is true if start offset is zero and start node follows a line break, and false
+    //    otherwise. non-breaking end is true if end offset is end node's length and end node
+    //    precedes a line break, and false otherwise.
+    auto replacement_whitespace = canonical_space_sequence(
+        length,
+        start_offset == 0 && follows_a_line_break(start_node),
+        end_offset == end_node->length() && precedes_a_line_break(end_node));
+
+    // 10. While (start node, start offset) is before (end node, end offset):
+    while (true) {
+        auto relative_position = position_of_boundary_point_relative_to_other_boundary_point(start_node, start_offset, end_node, end_offset);
+        if (relative_position != DOM::RelativeBoundaryPointPosition::Before)
+            break;
+
+        // 1. If start node has a child with index start offset, set start node to that child, then
+        //    set start offset to zero.
+        if (start_node->child_at_index(start_offset)) {
+            start_node = *start_node->child_at_index(start_offset);
+            start_offset = 0;
+        }
+
+        // 2. Otherwise, if start node is not a Text node or if start offset is start node's length,
+        //    set start offset to one plus start node's index, then set start node to its parent.
+        else if (!is<DOM::Text>(*start_node) || start_offset == start_node->length()) {
+            start_offset = start_node->index() + 1;
+            start_node = *start_node->parent();
+        }
+
+        // 3. Otherwise:
+        else {
+            // 1. Remove the first code unit from replacement whitespace, and let element be that
+            //    code unit.
+            // FIXME: Find a way to get code points directly from the UTF-8 string
+            auto replacement_whitespace_utf16 = MUST(AK::utf8_to_utf16(replacement_whitespace));
+            auto replacement_whitespace_utf16_view = Utf16View { replacement_whitespace_utf16 };
+            replacement_whitespace = MUST(String::from_utf16({ replacement_whitespace_utf16_view.substring_view(1) }));
+            auto element = replacement_whitespace_utf16_view.code_point_at(0);
+
+            // 2. If element is not the same as the start offsetth code unit of start node's data:
+            auto start_node_data = *start_node->text_content();
+            auto start_node_utf16 = MUST(AK::utf8_to_utf16(start_node_data));
+            auto start_node_utf16_view = Utf16View { start_node_utf16 };
+            auto start_node_code_point = start_node_utf16_view.code_point_at(start_offset);
+            if (element != start_node_code_point) {
+                // 1. Call insertData(start offset, element) on start node.
+                auto& start_node_character_data = static_cast<DOM::CharacterData&>(*start_node);
+                MUST(start_node_character_data.insert_data(start_offset, String::from_code_point(element)));
+
+                // 2. Call deleteData(start offset + 1, 1) on start node.
+                MUST(start_node_character_data.delete_data(start_offset + 1, 1));
+            }
+
+            // 3. Add one to start offset.
+            ++start_offset;
+        }
+    }
+}
+
+// https://w3c.github.io/editing/docs/execCommand/#delete-the-selection
+void delete_the_selection(Selection::Selection const& selection)
+{
+    // FIXME: implement the spec
+    auto active_range = selection.range();
+    if (!active_range)
+        return;
+    MUST(active_range->delete_contents());
+}
+
+// https://w3c.github.io/editing/docs/execCommand/#editing-host-of
+GC::Ptr<DOM::Node> editing_host_of_node(GC::Ref<DOM::Node> node)
+{
+    // node itself, if node is an editing host;
+    if (is_editing_host(node))
+        return node;
+
+    // or the nearest ancestor of node that is an editing host, if node is editable.
+    if (node->is_editable()) {
+        auto* ancestor = node->parent();
+        do {
+            if (is_editing_host(*ancestor))
+                return ancestor;
+            ancestor = ancestor->parent();
+        } while (ancestor);
+        VERIFY_NOT_REACHED();
+    }
+
+    // The editing host of node is null if node is neither editable nor an editing host;
+    return {};
+}
+
+// https://w3c.github.io/editing/docs/execCommand/#follows-a-line-break
+bool follows_a_line_break(GC::Ref<DOM::Node> node)
+{
+    // 1. Let offset be zero.
+    auto offset = 0;
+
+    // 2. While (node, offset) is not a block boundary point:
+    while (!is_block_boundary_point(node, offset)) {
+        // 1. If node has a visible child with index offset minus one, return false.
+        auto* offset_minus_one_child = node->child_at_index(offset - 1);
+        if (offset_minus_one_child && is_visible_node(*offset_minus_one_child))
+            return false;
+
+        // 2. If offset is zero or node has no children, set offset to node's index, then set node
+        //    to its parent.
+        if (offset == 0 || node->child_count() == 0) {
+            offset = node->index();
+            node = *node->parent();
+        }
+
+        // 3. Otherwise, set node to its child with index offset minus one, then set offset to
+        //    node's length.
+        else {
+            node = *node->child_at_index(offset - 1);
+            offset = node->length();
+        }
+    }
+
+    // 3. Return true.
+    return true;
+}
+
+// https://w3c.github.io/editing/docs/execCommand/#allowed-child
+bool is_allowed_child_of_node(Variant<GC::Ref<DOM::Node>, FlyString> child, Variant<GC::Ref<DOM::Node>, FlyString> parent)
+{
+    GC::Ptr<DOM::Node> child_node;
+    if (child.has<GC::Ref<DOM::Node>>())
+        child_node = child.get<GC::Ref<DOM::Node>>();
+
+    // 1. If parent is "colgroup", "table", "tbody", "tfoot", "thead", "tr", or an HTML element with local name equal to
+    //    one of those, and child is a Text node whose data does not consist solely of space characters, return false.
+    auto parent_local_name = parent.visit(
+        [](FlyString local_name) { return local_name; },
+        [](DOM::Node const* node) { return static_cast<DOM::Element const*>(node)->local_name(); });
+    auto parent_is_table_like = parent_local_name.is_one_of(HTML::TagNames::colgroup, HTML::TagNames::table,
+        HTML::TagNames::tbody, HTML::TagNames::tfoot, HTML::TagNames::thead, HTML::TagNames::tr);
+    if (parent_is_table_like && is<DOM::Text>(child_node.ptr())) {
+        auto child_text_content = child_node->text_content().release_value();
+        if (!all_of(child_text_content.bytes_as_string_view(), Infra::is_ascii_whitespace))
+            return false;
+    }
+
+    // 2. If parent is "script", "style", "plaintext", or "xmp", or an HTML element with local name equal to one of
+    //    those, and child is not a Text node, return false.
+    if ((child.has<FlyString>() || !is<DOM::Text>(*child_node))
+        && parent_local_name.is_one_of(HTML::TagNames::script, HTML::TagNames::style, HTML::TagNames::plaintext, HTML::TagNames::xmp))
+        return false;
+
+    // 3. If child is a document, DocumentFragment, or DocumentType, return false.
+    if (child_node && (is<DOM::Document>(*child_node) || is<DOM::DocumentFragment>(*child_node) || is<DOM::DocumentType>(*child_node)))
+        return false;
+
+    // 4. If child is an HTML element, set child to the local name of child.
+    if (is<HTML::HTMLElement>(child_node.ptr()))
+        child = static_cast<DOM::Element&>(*child_node).local_name();
+
+    // 5. If child is not a string, return true.
+    if (!child.has<FlyString>())
+        return true;
+    auto child_local_name = child.get<FlyString>();
+
+    // 6. If parent is an HTML element:
+    auto is_heading = [](FlyString const& local_name) {
+        return local_name.is_one_of(
+            HTML::TagNames::h1,
+            HTML::TagNames::h2,
+            HTML::TagNames::h3,
+            HTML::TagNames::h4,
+            HTML::TagNames::h5,
+            HTML::TagNames::h6);
+    };
+    if (parent.has<GC::Ref<DOM::Node>>() && is<HTML::HTMLElement>(*parent.get<GC::Ref<DOM::Node>>())) {
+        auto& parent_html_element = static_cast<HTML::HTMLElement&>(*parent.get<GC::Ref<DOM::Node>>());
+
+        // 1. If child is "a", and parent or some ancestor of parent is an a, return false.
+        if (child_local_name == HTML::TagNames::a) {
+            DOM::Node* ancestor = &parent_html_element;
+            do {
+                if (is<DOM::Element>(ancestor) && static_cast<DOM::Element const&>(*ancestor).local_name() == HTML::TagNames::a)
+                    return false;
+                ancestor = ancestor->parent();
+            } while (ancestor);
+        }
+
+        // 2. If child is a prohibited paragraph child name and parent or some ancestor of parent is an element with
+        //    inline contents, return false.
+        if (is_prohibited_paragraph_child_name(child_local_name)) {
+            DOM::Node* ancestor = &parent_html_element;
+            do {
+                if (is_element_with_inline_contents(*ancestor))
+                    return false;
+                ancestor = ancestor->parent();
+            } while (ancestor);
+        }
+
+        // 3. If child is "h1", "h2", "h3", "h4", "h5", or "h6", and parent or some ancestor of parent is an HTML
+        //    element with local name "h1", "h2", "h3", "h4", "h5", or "h6", return false.
+        if (is_heading(child_local_name)) {
+            DOM::Node* ancestor = &parent_html_element;
+            do {
+                if (is<HTML::HTMLElement>(*ancestor) && is_heading(static_cast<DOM::Element&>(*ancestor).local_name()))
+                    return false;
+                ancestor = ancestor->parent();
+            } while (ancestor);
+        }
+
+        // 4. Let parent be the local name of parent.
+        parent = parent_html_element.local_name();
+    }
+
+    // 7. If parent is an Element or DocumentFragment, return true.
+    if (parent.has<GC::Ref<DOM::Node>>()) {
+        auto parent_element = parent.get<GC::Ref<DOM::Node>>();
+        if (is<DOM::Element>(*parent_element) || is<DOM::DocumentFragment>(*parent_element))
+            return true;
+    }
+
+    // 8. If parent is not a string, return false.
+    if (!parent.has<FlyString>())
+        return false;
+    parent_local_name = parent.get<FlyString>();
+
+    // 9. If parent is on the left-hand side of an entry on the following list, then return true if child is listed on
+    //    the right-hand side of that entry, and false otherwise.
+
+    // * colgroup: col
+    if (parent_local_name == HTML::TagNames::colgroup)
+        return child_local_name == HTML::TagNames::col;
+
+    // * table: caption, col, colgroup, tbody, td, tfoot, th, thead, tr
+    if (parent_local_name == HTML::TagNames::table) {
+        return child_local_name.is_one_of(
+            HTML::TagNames::caption,
+            HTML::TagNames::col,
+            HTML::TagNames::colgroup,
+            HTML::TagNames::tbody,
+            HTML::TagNames::td,
+            HTML::TagNames::tfoot,
+            HTML::TagNames::th,
+            HTML::TagNames::thead,
+            HTML::TagNames::tr);
+    }
+
+    // * tbody, tfoot, thead: td, th, tr
+    if (parent_local_name.is_one_of(HTML::TagNames::tbody, HTML::TagNames::tfoot, HTML::TagNames::thead))
+        return child_local_name.is_one_of(HTML::TagNames::td, HTML::TagNames::th, HTML::TagNames::tr);
+
+    // * tr: td, th
+    if (parent_local_name == HTML::TagNames::tr)
+        return child_local_name.is_one_of(HTML::TagNames::td, HTML::TagNames::th);
+
+    // * dl: dt, dd
+    if (parent_local_name == HTML::TagNames::dl)
+        return child_local_name.is_one_of(HTML::TagNames::dt, HTML::TagNames::dd);
+
+    // * dir, ol, ul: dir, li, ol, ul
+    if (parent_local_name.is_one_of(HTML::TagNames::dir, HTML::TagNames::ol, HTML::TagNames::ul))
+        return child_local_name.is_one_of(HTML::TagNames::dir, HTML::TagNames::li, HTML::TagNames::ol, HTML::TagNames::ul);
+
+    // * hgroup: h1, h2, h3, h4, h5, h6
+    if (parent_local_name == HTML::TagNames::hgroup)
+        return is_heading(child_local_name);
+
+    // 10. If child is "body", "caption", "col", "colgroup", "frame", "frameset", "head", "html", "tbody", "td",
+    //     "tfoot", "th", "thead", or "tr", return false.
+    if (child_local_name.is_one_of(
+            HTML::TagNames::body,
+            HTML::TagNames::caption,
+            HTML::TagNames::col,
+            HTML::TagNames::colgroup,
+            HTML::TagNames::frame,
+            HTML::TagNames::frameset,
+            HTML::TagNames::head,
+            HTML::TagNames::html,
+            HTML::TagNames::tbody,
+            HTML::TagNames::td,
+            HTML::TagNames::tfoot,
+            HTML::TagNames::th,
+            HTML::TagNames::thead,
+            HTML::TagNames::tr))
+        return false;
+
+    // 11. If child is "dd" or "dt" and parent is not "dl", return false.
+    if (child_local_name.is_one_of(HTML::TagNames::dd, HTML::TagNames::dt) && parent_local_name != HTML::TagNames::dl)
+        return false;
+
+    // 12. If child is "li" and parent is not "ol" or "ul", return false.
+    if (child_local_name == HTML::TagNames::li && parent_local_name != HTML::TagNames::ol && parent_local_name != HTML::TagNames::ul)
+        return false;
+
+    // 13. If parent is on the left-hand side of an entry on the following list and child is listed on the right-hand
+    //     side of that entry, return false.
+
+    // * a: a
+    if (parent_local_name == HTML::TagNames::a && child_local_name == HTML::TagNames::a)
+        return false;
+
+    // * dd, dt: dd, dt
+    if (parent_local_name.is_one_of(HTML::TagNames::dd, HTML::TagNames::dt)
+        && child_local_name.is_one_of(HTML::TagNames::dd, HTML::TagNames::dt))
+        return false;
+
+    // * h1, h2, h3, h4, h5, h6: h1, h2, h3, h4, h5, h6
+    if (is_heading(parent_local_name) && is_heading(child_local_name))
+        return false;
+
+    // * li: li
+    if (parent_local_name == HTML::TagNames::li && child_local_name == HTML::TagNames::li)
+        return false;
+
+    // * nobr: nobr
+    if (parent_local_name == HTML::TagNames::nobr && child_local_name == HTML::TagNames::nobr)
+        return false;
+
+    // * All names of an element with inline contents: all prohibited paragraph child names
+    if (is_name_of_an_element_with_inline_contents(parent_local_name) && is_prohibited_paragraph_child_name(child_local_name))
+        return false;
+
+    // * td, th: caption, col, colgroup, tbody, td, tfoot, th, thead, tr
+    if (parent_local_name.is_one_of(HTML::TagNames::td, HTML::TagNames::th)
+        && child_local_name.is_one_of(
+            HTML::TagNames::caption,
+            HTML::TagNames::col,
+            HTML::TagNames::colgroup,
+            HTML::TagNames::tbody,
+            HTML::TagNames::td,
+            HTML::TagNames::tfoot,
+            HTML::TagNames::th,
+            HTML::TagNames::thead,
+            HTML::TagNames::tr))
+        return false;
+
+    // 14. Return true.
+    return true;
+}
+
+// https://w3c.github.io/editing/docs/execCommand/#block-boundary-point
+bool is_block_boundary_point(GC::Ref<DOM::Node> node, u32 offset)
+{
+    // A boundary point is a block boundary point if it is either a block start point or a block end point.
+    return is_block_start_point(node, offset) || is_block_end_point(node, offset);
+}
+
+// https://w3c.github.io/editing/docs/execCommand/#block-end-point
+bool is_block_end_point(GC::Ref<DOM::Node> node, u32 offset)
+{
+    // A boundary point (node, offset) is a block end point if either node's parent is null and
+    // offset is node's length;
+    if (!node->parent() && offset == node->length())
+        return true;
+
+    // or node has a child with index offset, and that child is a visible block node.
+    auto offset_child = node->child_at_index(offset);
+    return offset_child && is_visible_node(*offset_child) && is_block_node(*offset_child);
+}
+
+// https://w3c.github.io/editing/docs/execCommand/#block-node
+bool is_block_node(GC::Ref<DOM::Node> node)
+{
+    // A block node is either an Element whose "display" property does not have resolved value
+    // "inline" or "inline-block" or "inline-table" or "none", or a document, or a DocumentFragment.
+    if (is<DOM::Document>(*node) || is<DOM::DocumentFragment>(*node))
+        return true;
+
+    auto layout_node = node->layout_node();
+    if (!layout_node)
+        return false;
+
+    auto display = layout_node->display();
+    return is<DOM::Element>(*node)
+        && !(display.is_inline_outside() && (display.is_flow_inside() || display.is_flow_root_inside() || display.is_table_inside()))
+        && !display.is_none();
+}
+
+// https://w3c.github.io/editing/docs/execCommand/#block-start-point
+bool is_block_start_point(GC::Ref<DOM::Node> node, u32 offset)
+{
+    // A boundary point (node, offset) is a block start point if either node's parent is null and
+    // offset is zero;
+    if (!node->parent() && offset == 0)
+        return true;
+
+    // or node has a child with index offset − 1, and that child is either a visible block node or a
+    // visible br.
+    auto offset_minus_one_child = node->child_at_index(offset - 1);
+    if (!offset_minus_one_child)
+        return false;
+    return is_visible_node(*offset_minus_one_child)
+        && (is_block_node(*offset_minus_one_child) || is<HTML::HTMLBRElement>(*offset_minus_one_child));
+}
+
+// https://w3c.github.io/editing/docs/execCommand/#collapsed-whitespace-node
+bool is_collapsed_whitespace_node(GC::Ref<DOM::Node> node)
+{
+    // 1. If node is not a whitespace node, return false.
+    if (!is_whitespace_node(node))
+        return false;
+
+    // 2. If node's data is the empty string, return true.
+    auto node_data = node->text_content();
+    if (!node_data.has_value() || node_data->is_empty())
+        return true;
+
+    // 3. Let ancestor be node's parent.
+    GC::Ptr<DOM::Node> ancestor = node->parent();
+
+    // 4. If ancestor is null, return true.
+    if (!ancestor)
+        return true;
+
+    // 5. If the "display" property of some ancestor of node has resolved value "none", return true.
+    if (ancestor->layout_node() && ancestor->layout_node()->display().is_none())
+        return true;
+
+    // 6. While ancestor is not a block node and its parent is not null, set ancestor to its parent.
+    while (!is_block_node(*ancestor) && ancestor->parent())
+        ancestor = ancestor->parent();
+
+    // 7. Let reference be node.
+    auto reference = node;
+
+    // 8. While reference is a descendant of ancestor:
+    while (reference->is_descendant_of(*ancestor)) {
+        // 1. Let reference be the node before it in tree order.
+        reference = *reference->previous_in_pre_order();
+
+        // 2. If reference is a block node or a br, return true.
+        if (is_block_node(reference) || is<HTML::HTMLBRElement>(*reference))
+            return true;
+
+        // 3. If reference is a Text node that is not a whitespace node, or is an img, break from
+        //    this loop.
+        if ((is<DOM::Text>(*reference) && !is_whitespace_node(reference)) || is<HTML::HTMLImageElement>(*reference))
+            break;
+    }
+
+    // 9. Let reference be node.
+    reference = node;
+
+    // 10. While reference is a descendant of ancestor:
+    while (reference->is_descendant_of(*ancestor)) {
+        // 1. Let reference be the node after it in tree order, or null if there is no such node.
+        reference = *reference->next_in_pre_order();
+
+        // 2. If reference is a block node or a br, return true.
+        if (is_block_node(reference) || is<HTML::HTMLBRElement>(*reference))
+            return true;
+
+        // 3. If reference is a Text node that is not a whitespace node, or is an img, break from
+        //    this loop.
+        if ((is<DOM::Text>(*reference) && !is_whitespace_node(reference)) || is<HTML::HTMLImageElement>(*reference))
+            break;
+    }
+
+    // 11. Return false.
+    return false;
+}
+
+// https://html.spec.whatwg.org/multipage/interaction.html#editing-host
+bool is_editing_host(GC::Ref<DOM::Node> node)
+{
+    // An editing host is either an HTML element with its contenteditable attribute in the true
+    // state or plaintext-only state, or a child HTML element of a Document whose design mode
+    // enabled is true.
+    // FIXME: check contenteditable "plaintext-only"
+    if (!is<HTML::HTMLElement>(*node))
+        return false;
+    auto const& html_element = static_cast<HTML::HTMLElement&>(*node);
+    return html_element.content_editable() == "true"sv || node->document().design_mode_enabled_state();
+}
+
+// https://w3c.github.io/editing/docs/execCommand/#element-with-inline-contents
+bool is_element_with_inline_contents(GC::Ref<DOM::Node> node)
+{
+    // An element with inline contents is an HTML element whose local name is a name of an element with inline contents.
+    return is<HTML::HTMLElement>(*node)
+        && is_name_of_an_element_with_inline_contents(static_cast<DOM::Element&>(*node).local_name());
+}
+
+// https://w3c.github.io/editing/docs/execCommand/#extraneous-line-break
+bool is_extraneous_line_break(GC::Ref<DOM::Node> node)
+{
+    // An extraneous line break is a br
+    if (!is<HTML::HTMLBRElement>(*node))
+        return false;
+
+    // ...except that a br that is the sole child of an li is not extraneous.
+    auto parent = node->parent();
+    if (parent && static_cast<DOM::Element&>(*parent).local_name() == HTML::TagNames::li && parent->child_count() == 1)
+        return false;
+
+    // FIXME: ...that has no visual effect, in that removing it from the DOM
+    //        would not change layout,
+
+    return false;
+}
+
+// https://w3c.github.io/editing/docs/execCommand/#in-the-same-editing-host
+bool is_in_same_editing_host(GC::Ref<DOM::Node> node_a, GC::Ref<DOM::Node> node_b)
+{
+    // Two nodes are in the same editing host if the editing host of the first is non-null and the
+    // same as the editing host of the second.
+    auto editing_host_a = editing_host_of_node(node_a);
+    auto editing_host_b = editing_host_of_node(node_b);
+    return editing_host_a && editing_host_a == editing_host_b;
+}
+
+// https://w3c.github.io/editing/docs/execCommand/#inline-node
+bool is_inline_node(GC::Ref<DOM::Node> node)
+{
+    // An inline node is a node that is not a block node.
+    return !is_block_node(node);
+}
+
+// https://w3c.github.io/editing/docs/execCommand/#invisible
+bool is_invisible_node(GC::Ref<DOM::Node> node)
+{
+    // Something is invisible if it is a node that is not visible.
+    return !is_visible_node(node);
+}
+
+// https://w3c.github.io/editing/docs/execCommand/#name-of-an-element-with-inline-contents
+bool is_name_of_an_element_with_inline_contents(FlyString const& local_name)
+{
+    // A name of an element with inline contents is "a", "abbr", "b", "bdi", "bdo", "cite", "code", "dfn", "em", "h1",
+    // "h2", "h3", "h4", "h5", "h6", "i", "kbd", "mark", "p", "pre", "q", "rp", "rt", "ruby", "s", "samp", "small",
+    // "span", "strong", "sub", "sup", "u", "var", "acronym", "listing", "strike", "xmp", "big", "blink", "font",
+    // "marquee", "nobr", or "tt".
+    return local_name.is_one_of(
+        HTML::TagNames::a,
+        HTML::TagNames::abbr,
+        HTML::TagNames::b,
+        HTML::TagNames::bdi,
+        HTML::TagNames::bdo,
+        HTML::TagNames::cite,
+        HTML::TagNames::code,
+        HTML::TagNames::dfn,
+        HTML::TagNames::em,
+        HTML::TagNames::h1,
+        HTML::TagNames::h2,
+        HTML::TagNames::h3,
+        HTML::TagNames::h4,
+        HTML::TagNames::h5,
+        HTML::TagNames::h6,
+        HTML::TagNames::i,
+        HTML::TagNames::kbd,
+        HTML::TagNames::mark,
+        HTML::TagNames::p,
+        HTML::TagNames::pre,
+        HTML::TagNames::q,
+        HTML::TagNames::rp,
+        HTML::TagNames::rt,
+        HTML::TagNames::ruby,
+        HTML::TagNames::s,
+        HTML::TagNames::samp,
+        HTML::TagNames::small,
+        HTML::TagNames::span,
+        HTML::TagNames::strong,
+        HTML::TagNames::sub,
+        HTML::TagNames::sup,
+        HTML::TagNames::u,
+        HTML::TagNames::var,
+        HTML::TagNames::acronym,
+        HTML::TagNames::listing,
+        HTML::TagNames::strike,
+        HTML::TagNames::xmp,
+        HTML::TagNames::big,
+        HTML::TagNames::blink,
+        HTML::TagNames::font,
+        HTML::TagNames::marquee,
+        HTML::TagNames::nobr,
+        HTML::TagNames::tt);
+}
+
+// https://w3c.github.io/editing/docs/execCommand/#prohibited-paragraph-child-name
+bool is_prohibited_paragraph_child_name(FlyString const& local_name)
+{
+    // A prohibited paragraph child name is "address", "article", "aside", "blockquote", "caption", "center", "col",
+    // "colgroup", "dd", "details", "dir", "div", "dl", "dt", "fieldset", "figcaption", "figure", "footer", "form",
+    // "h1", "h2", "h3", "h4", "h5", "h6", "header", "hgroup", "hr", "li", "listing", "menu", "nav", "ol", "p",
+    // "plaintext", "pre", "section", "summary", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "ul", or "xmp".
+    return local_name.is_one_of(
+        HTML::TagNames::address,
+        HTML::TagNames::article,
+        HTML::TagNames::aside,
+        HTML::TagNames::blockquote,
+        HTML::TagNames::caption,
+        HTML::TagNames::center,
+        HTML::TagNames::col,
+        HTML::TagNames::colgroup,
+        HTML::TagNames::dd,
+        HTML::TagNames::details,
+        HTML::TagNames::dir,
+        HTML::TagNames::div,
+        HTML::TagNames::dl,
+        HTML::TagNames::dt,
+        HTML::TagNames::fieldset,
+        HTML::TagNames::figcaption,
+        HTML::TagNames::figure,
+        HTML::TagNames::footer,
+        HTML::TagNames::form,
+        HTML::TagNames::h1,
+        HTML::TagNames::h2,
+        HTML::TagNames::h3,
+        HTML::TagNames::h4,
+        HTML::TagNames::h5,
+        HTML::TagNames::h6,
+        HTML::TagNames::header,
+        HTML::TagNames::hgroup,
+        HTML::TagNames::hr,
+        HTML::TagNames::li,
+        HTML::TagNames::listing,
+        HTML::TagNames::menu,
+        HTML::TagNames::nav,
+        HTML::TagNames::ol,
+        HTML::TagNames::p,
+        HTML::TagNames::plaintext,
+        HTML::TagNames::pre,
+        HTML::TagNames::section,
+        HTML::TagNames::summary,
+        HTML::TagNames::table,
+        HTML::TagNames::tbody,
+        HTML::TagNames::td,
+        HTML::TagNames::tfoot,
+        HTML::TagNames::th,
+        HTML::TagNames::thead,
+        HTML::TagNames::tr,
+        HTML::TagNames::ul,
+        HTML::TagNames::xmp);
+}
+
+// https://w3c.github.io/editing/docs/execCommand/#visible
+bool is_visible_node(GC::Ref<DOM::Node> node)
+{
+    // excluding any node with an inclusive ancestor Element whose "display" property has resolved
+    // value "none".
+    GC::Ptr<DOM::Node> inclusive_ancestor = node;
+    do {
+        auto* layout_node = inclusive_ancestor->layout_node();
+        if (layout_node && layout_node->display().is_none())
+            return false;
+        inclusive_ancestor = inclusive_ancestor->parent();
+    } while (inclusive_ancestor);
+
+    // Something is visible if it is a node that either is a block node,
+    if (is_block_node(node))
+        return true;
+
+    // or a Text node that is not a collapsed whitespace node,
+    if (is<DOM::Text>(*node) && !is_collapsed_whitespace_node(node))
+        return true;
+
+    // or an img,
+    if (is<HTML::HTMLImageElement>(*node))
+        return true;
+
+    // or a br that is not an extraneous line break,
+    if (is<HTML::HTMLBRElement>(*node) && !is_extraneous_line_break(node))
+        return true;
+
+    // or any node with a visible descendant;
+    // NOTE: We call into is_visible_node() recursively, so check children instead of descendants.
+    bool has_visible_child_node = false;
+    node->for_each_child([&](DOM::Node& child_node) {
+        if (is_visible_node(child_node)) {
+            has_visible_child_node = true;
+            return IterationDecision::Break;
+        }
+        return IterationDecision::Continue;
+    });
+    return has_visible_child_node;
+}
+
+// https://w3c.github.io/editing/docs/execCommand/#whitespace-node
+bool is_whitespace_node(GC::Ref<DOM::Node> node)
+{
+    // NOTE: All constraints below check that node is a Text node
+    if (!is<DOM::Text>(*node))
+        return false;
+
+    // A whitespace node is either a Text node whose data is the empty string;
+    auto& character_data = static_cast<DOM::CharacterData&>(*node);
+    if (character_data.data().is_empty())
+        return true;
+
+    // NOTE: All constraints below require a parent Element with a resolved value for "white-space"
+    GC::Ptr<DOM::Node> parent = node->parent();
+    if (!is<DOM::Element>(parent.ptr()))
+        return false;
+    auto* layout_node = parent->layout_node();
+    if (!layout_node)
+        return false;
+    auto white_space = layout_node->computed_values().white_space();
+
+    // or a Text node whose data consists only of one or more tabs (0x0009), line feeds (0x000A),
+    // carriage returns (0x000D), and/or spaces (0x0020), and whose parent is an Element whose
+    // resolved value for "white-space" is "normal" or "nowrap";
+    auto is_tab_lf_cr_or_space = [](u32 codepoint) {
+        return codepoint == '\t' || codepoint == '\n' || codepoint == '\r' || codepoint == ' ';
+    };
+    auto code_points = character_data.data().code_points();
+    if (all_of(code_points, is_tab_lf_cr_or_space) && (white_space == CSS::WhiteSpace::Normal || white_space == CSS::WhiteSpace::Nowrap))
+        return true;
+
+    // or a Text node whose data consists only of one or more tabs (0x0009), carriage returns
+    // (0x000D), and/or spaces (0x0020), and whose parent is an Element whose resolved value for
+    // "white-space" is "pre-line".
+    auto is_tab_cr_or_space = [](u32 codepoint) {
+        return codepoint == '\t' || codepoint == '\r' || codepoint == ' ';
+    };
+    if (all_of(code_points, is_tab_cr_or_space) && white_space == CSS::WhiteSpace::PreLine)
+        return true;
+
+    return false;
+}
+
+// https://w3c.github.io/editing/docs/execCommand/#preserving-ranges
+void move_node_preserving_ranges(GC::Ref<DOM::Node> node, GC::Ref<DOM::Node> new_parent, u32 new_index)
+{
+    // To move a node to a new location, preserving ranges, remove the node from its original parent
+    // (if any), then insert it in the new location. In doing so, follow these rules instead of
+    // those defined by the insert and remove algorithms:
+
+    // FIXME: Currently this is a simple range-destroying move. Implement "follow these rules" as
+    //        described above.
+
+    // 1. Let node be the moved node, old parent and old index be the old parent (which may be null)
+    //    and index, and new parent and new index be the new parent and index.
+    auto* old_parent = node->parent();
+    [[maybe_unused]] auto old_index = node->index();
+    if (old_parent)
+        node->remove();
+
+    auto* new_next_sibling = new_parent->child_at_index(new_index);
+    new_parent->insert_before(node, new_next_sibling);
+
+    // FIXME: 2. If a boundary point's node is the same as or a descendant of node, leave it unchanged, so
+    //    it moves to the new location.
+
+    // FIXME: 3. If a boundary point's node is new parent and its offset is greater than new index, add one
+    //    to its offset.
+
+    // FIXME: 4. If a boundary point's node is old parent and its offset is old index or old index + 1, set
+    //    its node to new parent and add new index − old index to its offset.
+
+    // FIXME: 5. If a boundary point's node is old parent and its offset is greater than old index + 1,
+    //    subtract one from its offset.
+}
+
+// https://w3c.github.io/editing/docs/execCommand/#normalize-sublists
+void normalize_sublists_in_node(GC::Ref<DOM::Element> item)
+{
+    // 1. If item is not an li or it is not editable or its parent is not editable, abort these
+    //    steps.
+    if (item->local_name() != HTML::TagNames::li || !item->is_editable() || !item->parent()->is_editable())
+        return;
+
+    // 2. Let new item be null.
+    GC::Ptr<DOM::Element> new_item;
+
+    // 3. While item has an ol or ul child:
+    while (item->has_child_of_type<HTML::HTMLOListElement>() || item->has_child_of_type<HTML::HTMLUListElement>()) {
+        // 1. Let child be the last child of item.
+        GC::Ref<DOM::Node> child = *item->last_child();
+
+        // 2. If child is an ol or ul, or new item is null and child is a Text node whose data
+        //    consists of zero of more space characters:
+        auto child_text = child->text_content();
+        auto text_is_all_whitespace = child_text.has_value() && all_of(child_text.value().bytes_as_string_view(), Infra::is_ascii_whitespace);
+        if ((is<HTML::HTMLOListElement>(*child) || is<HTML::HTMLUListElement>(*child))
+            || (!new_item && is<DOM::Text>(*child) && text_is_all_whitespace)) {
+            // 1. Set new item to null.
+            new_item = {};
+
+            // 2. Insert child into the parent of item immediately following item, preserving
+            //    ranges.
+            move_node_preserving_ranges(child, *item->parent(), item->index());
+        }
+
+        // 3. Otherwise:
+        else {
+            // 1. If new item is null, let new item be the result of calling createElement("li") on
+            //    the ownerDocument of item, then insert new item into the parent of item
+            //    immediately after item.
+            if (!new_item) {
+                new_item = MUST(DOM::create_element(*item->owner_document(), HTML::TagNames::li, Namespace::HTML));
+                item->parent()->insert_before(*new_item, item->next_sibling());
+            }
+
+            // 2. Insert child into new item as its first child, preserving ranges.
+            move_node_preserving_ranges(child, *new_item, 0);
+        }
+    }
+}
+
+// https://w3c.github.io/editing/docs/execCommand/#precedes-a-line-break
+bool precedes_a_line_break(GC::Ref<DOM::Node> node)
+{
+    // 1. Let offset be node's length.
+    auto offset = node->length();
+
+    // 2. While (node, offset) is not a block boundary point:
+    while (!is_block_boundary_point(node, offset)) {
+        // 1. If node has a visible child with index offset, return false.
+        auto* offset_child = node->child_at_index(offset);
+        if (offset_child && is_visible_node(*offset_child))
+            return false;
+
+        // 2. If offset is node's length or node has no children, set offset to one plus node's
+        //    index, then set node to its parent.
+        if (offset == node->length() || node->child_count() == 0) {
+            offset = node->index() + 1;
+            node = *node->parent();
+        }
+
+        // 3. Otherwise, set node to its child with index offset and set offset to zero.
+        else {
+            node = *node->child_at_index(offset);
+            offset = 0;
+        }
+    }
+
+    // 3. Return true;
+    return true;
+}
+
+// https://w3c.github.io/editing/docs/execCommand/#record-the-values
+Vector<RecordedNodeValue> record_the_values_of_nodes(Vector<GC::Ref<DOM::Node>> const& node_list)
+{
+    // 1. Let values be a list of (node, command, specified command value) triples, initially empty.
+    Vector<RecordedNodeValue> values;
+
+    // 2. For each node in node list, for each command in the list "subscript", "bold", "fontName",
+    //    "fontSize", "foreColor", "hiliteColor", "italic", "strikethrough", and "underline" in that
+    //    order:
+    Array const commands = { CommandNames::subscript, CommandNames::bold, CommandNames::fontName,
+        CommandNames::fontSize, CommandNames::foreColor, CommandNames::hiliteColor, CommandNames::italic,
+        CommandNames::strikethrough, CommandNames::underline };
+    for (auto node : node_list) {
+        for (auto command : commands) {
+            // 1. Let ancestor equal node.
+            auto ancestor = node;
+
+            // 2. If ancestor is not an Element, set it to its parent.
+            if (!is<DOM::Element>(*ancestor))
+                ancestor = *ancestor->parent();
+
+            // 3. While ancestor is an Element and its specified command value for command is null, set
+            //    it to its parent.
+            while (is<DOM::Element>(*ancestor) && !specified_command_value(static_cast<DOM::Element&>(*ancestor), command).has_value())
+                ancestor = *ancestor->parent();
+
+            // 4. If ancestor is an Element, add (node, command, ancestor's specified command value for
+            //    command) to values. Otherwise add (node, command, null) to values.
+            if (is<DOM::Element>(*ancestor))
+                values.empend(*node, command, specified_command_value(static_cast<DOM::Element&>(*ancestor), command));
+            else
+                values.empend(*node, command, OptionalNone {});
+        }
+    }
+
+    // 3. Return values.
+    return values;
+}
+
+// https://w3c.github.io/editing/docs/execCommand/#remove-extraneous-line-breaks-at-the-end-of
+void remove_extraneous_line_breaks_at_the_end_of_node(GC::Ref<DOM::Node> node)
+{
+    // 1. Let ref be node.
+    GC::Ptr<DOM::Node> ref = node;
+
+    // 2. While ref has children, set ref to its lastChild.
+    while (ref->child_count() > 0)
+        ref = ref->last_child();
+
+    // 3. While ref is invisible but not an extraneous line break, and ref does not equal node, set
+    //    ref to the node before it in tree order.
+    while (is_invisible_node(*ref)
+        && !is_extraneous_line_break(*ref)
+        && ref != node) {
+        ref = ref->previous_in_pre_order();
+    }
+
+    // 4. If ref is an editable extraneous line break:
+    if (ref->is_editable() && is_extraneous_line_break(*ref)) {
+        // 1. While ref's parent is editable and invisible, set ref to its parent.
+        while (ref->parent()->is_editable() && is_invisible_node(*ref->parent()))
+            ref = ref->parent();
+
+        // 2. Remove ref from its parent.
+        ref->remove();
+    }
+}
+
+// https://w3c.github.io/editing/docs/execCommand/#remove-extraneous-line-breaks-before
+void remove_extraneous_line_breaks_before_node(GC::Ref<DOM::Node> node)
+{
+    // 1. Let ref be the previousSibling of node.
+    GC::Ptr<DOM::Node> ref = node->previous_sibling();
+
+    // 2. If ref is null, abort these steps.
+    if (!ref)
+        return;
+
+    // 3. While ref has children, set ref to its lastChild.
+    while (ref->child_count() > 0)
+        ref = ref->last_child();
+
+    // 4. While ref is invisible but not an extraneous line break, and ref does not equal node's
+    //    parent, set ref to the node before it in tree order.
+    while (is_invisible_node(*ref)
+        && !is_extraneous_line_break(*ref)
+        && ref != node->parent()) {
+        ref = ref->previous_in_pre_order();
+    }
+
+    // 5. If ref is an editable extraneous line break, remove it from its parent.
+    if (ref->is_editable() && is_extraneous_line_break(*ref))
+        ref->remove();
+}
+
+// https://w3c.github.io/editing/docs/execCommand/#remove-extraneous-line-breaks-from
+void remove_extraneous_line_breaks_from_a_node(GC::Ref<DOM::Node> node)
+{
+    // To remove extraneous line breaks from a node, first remove extraneous line breaks before it,
+    // then remove extraneous line breaks at the end of it.
+    remove_extraneous_line_breaks_before_node(node);
+    remove_extraneous_line_breaks_at_the_end_of_node(node);
+}
+
+// https://w3c.github.io/editing/docs/execCommand/#preserving-its-descendants
+void remove_node_preserving_its_descendants(GC::Ref<DOM::Node> node)
+{
+    // To remove a node node while preserving its descendants, split the parent of node's children
+    // if it has any.
+    if (node->child_count() > 0) {
+        Vector<GC::Ref<DOM::Node>> children;
+        children.ensure_capacity(node->child_count());
+        for (auto* child = node->first_child(); child; child = child->next_sibling())
+            children.append(*child);
+        split_the_parent_of_nodes(move(children));
+        return;
+    }
+
+    // If it has no children, instead remove it from its parent.
+    node->remove();
+}
+
+// https://w3c.github.io/editing/docs/execCommand/#restore-the-values
+void restore_the_values_of_nodes(Vector<RecordedNodeValue> const& values)
+{
+    // 1. For each (node, command, value) triple in values:
+    for (auto& recorded_node_value : values) {
+        // 1. Let ancestor equal node.
+        GC::Ptr<DOM::Node> ancestor = recorded_node_value.node;
+
+        // 2. If ancestor is not an Element, set it to its parent.
+        if (!is<DOM::Element>(*ancestor))
+            ancestor = *ancestor->parent();
+
+        // 3. While ancestor is an Element and its specified command value for command is null, set it to its parent.
+        auto const& command = recorded_node_value.command;
+        while (is<DOM::Element>(*ancestor) && !specified_command_value(static_cast<DOM::Element&>(*ancestor), command).has_value())
+            ancestor = *ancestor->parent();
+
+        // FIXME: 4. If value is null and ancestor is an Element, push down values on node for command, with new value null.
+
+        // FIXME: 5. Otherwise, if ancestor is an Element and its specified command value for command is not equivalent to
+        //    value, or if ancestor is not an Element and value is not null, force the value of command to value on
+        //    node.
+    }
+}
+
+// https://w3c.github.io/editing/docs/execCommand/#set-the-tag-name
+GC::Ref<DOM::Element> set_the_tag_name(GC::Ref<DOM::Element> element, FlyString const& new_name)
+{
+    // 1. If element is an HTML element with local name equal to new name, return element.
+    if (is<HTML::HTMLElement>(*element) && static_cast<DOM::Element&>(element).local_name() == new_name)
+        return element;
+
+    // 2. If element's parent is null, return element.
+    if (!element->parent())
+        return element;
+
+    // 3. Let replacement element be the result of calling createElement(new name) on the ownerDocument of element.
+    auto replacement_element = MUST(element->owner_document()->create_element(new_name.to_string(), DOM::ElementCreationOptions {}));
+
+    // 4. Insert replacement element into element's parent immediately before element.
+    element->parent()->insert_before(replacement_element, element);
+
+    // 5. Copy all attributes of element to replacement element, in order.
+    element->for_each_attribute([&replacement_element](FlyString const& name, String const& value) {
+        MUST(replacement_element->set_attribute(name, value));
+    });
+
+    // 6. While element has children, append the first child of element as the last child of replacement element, preserving ranges.
+    while (element->has_children())
+        move_node_preserving_ranges(*element->first_child(), *replacement_element, replacement_element->child_count());
+
+    // 7. Remove element from its parent.
+    element->remove();
+
+    // 8. Return replacement element.
+    return replacement_element;
+}
+
+// https://w3c.github.io/editing/docs/execCommand/#specified-command-value
+Optional<String> specified_command_value(GC::Ref<DOM::Element> element, FlyString const& command)
+{
+    // 1. If command is "backColor" or "hiliteColor" and the Element's display property does not have resolved value "inline", return null.
+    auto layout_node = element->layout_node();
+    if ((command == CommandNames::backColor || command == CommandNames::hiliteColor) && layout_node) {
+        if (layout_node->computed_values().display().is_inline_outside())
+            return {};
+    }
+
+    // 2. If command is "createLink" or "unlink":
+    if (command == CommandNames::createLink || command == CommandNames::unlink) {
+        // 1. If element is an a element and has an href attribute, return the value of that attribute.
+        auto href_attribute = element->get_attribute(HTML::AttributeNames::href);
+        if (href_attribute.has_value())
+            return href_attribute.release_value();
+
+        // 2. Return null.
+        return {};
+    }
+
+    // 3. If command is "subscript" or "superscript":
+    if (command == CommandNames::subscript || command == CommandNames::superscript) {
+        // 1. If element is a sup, return "superscript".
+        if (element->local_name() == HTML::TagNames::sup)
+            return "superscript"_string;
+
+        // 2. If element is a sub, return "subscript".
+        if (element->local_name() == HTML::TagNames::sub)
+            return "subscript"_string;
+
+        // 3. Return null.
+        return {};
+    }
+
+    // FIXME: 4. If command is "strikethrough", and element has a style attribute set, and that attribute sets "text-decoration":
+    if (false) {
+        // FIXME: 1. If element's style attribute sets "text-decoration" to a value containing "line-through", return "line-through".
+
+        // 2. Return null.
+        return {};
+    }
+
+    // 5. If command is "strikethrough" and element is an s or strike element, return "line-through".
+    if (command == CommandNames::strikethrough && (element->local_name() == HTML::TagNames::s || element->local_name() == HTML::TagNames::strike))
+        return "line-through"_string;
+
+    // FIXME: 6. If command is "underline", and element has a style attribute set, and that attribute sets "text-decoration":
+    if (false) {
+        // FIXME: 1. If element's style attribute sets "text-decoration" to a value containing "underline", return "underline".
+
+        // 2. Return null.
+        return {};
+    }
+
+    // 7. If command is "underline" and element is a u element, return "underline".
+    if (command == CommandNames::underline && element->local_name() == HTML::TagNames::u)
+        return "underline"_string;
+
+    // FIXME: 8. Let property be the relevant CSS property for command.
+
+    // FIXME: 9. If property is null, return null.
+
+    // FIXME: 10. If element has a style attribute set, and that attribute has the effect of setting property, return the value
+    //     that it sets property to.
+
+    // FIXME: 11. If element is a font element that has an attribute whose effect is to create a presentational hint for
+    //     property, return the value that the hint sets property to. (For a size of 7, this will be the non-CSS value
+    //     "xxx-large".)
+
+    // FIXME: 12. If element is in the following list, and property is equal to the CSS property name listed for it, return the
+    //     string listed for it.
+    //     * b, strong: font-weight: "bold"
+    //     * i, em: font-style: "italic"
+
+    // 13. Return null.
+    return {};
+}
+
+// https://w3c.github.io/editing/docs/execCommand/#split-the-parent
+void split_the_parent_of_nodes(Vector<GC::Ref<DOM::Node>> const& nodes)
+{
+    VERIFY(nodes.size() > 0);
+
+    // 1. Let original parent be the parent of the first member of node list.
+    GC::Ref<DOM::Node> first_node = *nodes.first();
+    GC::Ref<DOM::Node> last_node = *nodes.last();
+    GC::Ref<DOM::Node> original_parent = *first_node->parent();
+
+    // 2. If original parent is not editable or its parent is null, do nothing and abort these
+    //    steps.
+    if (!original_parent->is_editable() || !original_parent->parent())
+        return;
+
+    // 3. If the first child of original parent is in node list, remove extraneous line breaks
+    //    before original parent.
+    GC::Ref<DOM::Node> first_child = *original_parent->first_child();
+    auto first_child_in_nodes_list = nodes.contains_slow(first_child);
+    if (first_child_in_nodes_list)
+        remove_extraneous_line_breaks_before_node(original_parent);
+
+    // 4. If the first child of original parent is in node list, and original parent follows a line
+    //    break, set follows line break to true. Otherwise, set follows line break to false.
+    auto follows_line_break = first_child_in_nodes_list && follows_a_line_break(original_parent);
+
+    // 5. If the last child of original parent is in node list, and original parent precedes a line
+    //    break, set precedes line break to true. Otherwise, set precedes line break to false.
+    GC::Ref<DOM::Node> last_child = *original_parent->last_child();
+    bool last_child_in_nodes_list = nodes.contains_slow(last_child);
+    auto precedes_line_break = last_child_in_nodes_list && precedes_a_line_break(original_parent);
+
+    // 6. If the first child of original parent is not in node list, but its last child is:
+    GC::Ref<DOM::Node> parent_of_original_parent = *original_parent->parent();
+    auto original_parent_index = original_parent->index();
+    auto& document = original_parent->document();
+    if (!first_child_in_nodes_list && last_child_in_nodes_list) {
+        // 1. For each node in node list, in reverse order, insert node into the parent of original
+        //    parent immediately after original parent, preserving ranges.
+        for (auto node : nodes.in_reverse())
+            move_node_preserving_ranges(node, parent_of_original_parent, original_parent_index + 1);
+
+        // 2. If precedes line break is true, and the last member of node list does not precede a
+        //    line break, call createElement("br") on the context object and insert the result
+        //    immediately after the last member of node list.
+        if (precedes_line_break && !precedes_a_line_break(last_node)) {
+            auto br_element = MUST(DOM::create_element(document, HTML::TagNames::br, Namespace::HTML));
+            MUST(last_node->parent()->append_child(br_element));
+        }
+
+        // 3. Remove extraneous line breaks at the end of original parent.
+        remove_extraneous_line_breaks_at_the_end_of_node(original_parent);
+
+        // 4. Abort these steps.
+        return;
+    }
+
+    // 7. If the first child of original parent is not in node list:
+    if (!first_child_in_nodes_list) {
+        // 1. Let cloned parent be the result of calling cloneNode(false) on original parent.
+        auto cloned_parent = MUST(original_parent->clone_node(nullptr, false));
+
+        // 2. If original parent has an id attribute, unset it.
+        auto& original_parent_element = static_cast<DOM::Element&>(*original_parent);
+        if (original_parent_element.has_attribute(HTML::AttributeNames::id))
+            original_parent_element.remove_attribute(HTML::AttributeNames::id);
+
+        // 3. Insert cloned parent into the parent of original parent immediately before original
+        //    parent.
+        original_parent->parent()->insert_before(cloned_parent, original_parent);
+
+        // 4. While the previousSibling of the first member of node list is not null, append the
+        //    first child of original parent as the last child of cloned parent, preserving ranges.
+        while (first_node->previous_sibling())
+            move_node_preserving_ranges(*original_parent->first_child(), cloned_parent, cloned_parent->child_count());
+    }
+
+    // 8. For each node in node list, insert node into the parent of original parent immediately
+    //    before original parent, preserving ranges.
+    for (auto node : nodes)
+        move_node_preserving_ranges(node, parent_of_original_parent, original_parent_index - 1);
+
+    // 9. If follows line break is true, and the first member of node list does not follow a line
+    //    break, call createElement("br") on the context object and insert the result immediately
+    //    before the first member of node list.
+    if (follows_line_break && !follows_a_line_break(first_node)) {
+        auto br_element = MUST(DOM::create_element(document, HTML::TagNames::br, Namespace::HTML));
+        first_node->parent()->insert_before(br_element, first_node);
+    }
+
+    // 10. If the last member of node list is an inline node other than a br, and the first child of
+    //     original parent is a br, and original parent is not an inline node, remove the first
+    //     child of original parent from original parent.
+    if (is_inline_node(last_node) && !is<HTML::HTMLBRElement>(*last_node) && is<HTML::HTMLBRElement>(*first_child) && !is_inline_node(original_parent))
+        first_child->remove();
+
+    // 11. If original parent has no children:
+    if (original_parent->child_count() == 0) {
+        // 1. Remove original parent from its parent.
+        original_parent->remove();
+
+        // 2. If precedes line break is true, and the last member of node list does not precede a
+        //    line break, call createElement("br") on the context object and insert the result
+        //    immediately after the last member of node list.
+        if (precedes_line_break && !precedes_a_line_break(last_node)) {
+            auto br_element = MUST(DOM::create_element(document, HTML::TagNames::br, Namespace::HTML));
+            last_node->parent()->insert_before(br_element, last_node->next_sibling());
+        }
+    }
+
+    // 12. Otherwise, remove extraneous line breaks before original parent.
+    else {
+        remove_extraneous_line_breaks_before_node(original_parent);
+    }
+
+    // 13. If node list's last member's nextSibling is null, but its parent is not null, remove
+    //     extraneous line breaks at the end of node list's last member's parent.
+    if (!last_node->next_sibling() && last_node->parent())
+        remove_extraneous_line_breaks_at_the_end_of_node(*last_node->parent());
+}
+
+}

+ 58 - 0
Libraries/LibWeb/Editing/Internal/Algorithms.h

@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2024, Jelle Raaijmakers <jelle@ladybird.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <AK/Vector.h>
+#include <LibWeb/DOM/Node.h>
+
+namespace Web::Editing {
+
+// https://w3c.github.io/editing/docs/execCommand/#record-the-values
+struct RecordedNodeValue {
+    GC::Ref<DOM::Node> node;
+    FlyString const& command;
+    Optional<String> specified_command_value;
+};
+
+// Below algorithms are specified here:
+// https://w3c.github.io/editing/docs/execCommand/#assorted-common-algorithms
+
+String canonical_space_sequence(u32 length, bool non_breaking_start, bool non_breaking_end);
+void canonicalize_whitespace(GC::Ref<DOM::Node>, u32 offset, bool fix_collapsed_space = true);
+void delete_the_selection(Selection::Selection const&);
+GC::Ptr<DOM::Node> editing_host_of_node(GC::Ref<DOM::Node>);
+bool follows_a_line_break(GC::Ref<DOM::Node>);
+bool is_allowed_child_of_node(Variant<GC::Ref<DOM::Node>, FlyString> child, Variant<GC::Ref<DOM::Node>, FlyString> parent);
+bool is_block_boundary_point(GC::Ref<DOM::Node>, u32 offset);
+bool is_block_end_point(GC::Ref<DOM::Node>, u32 offset);
+bool is_block_node(GC::Ref<DOM::Node>);
+bool is_block_start_point(GC::Ref<DOM::Node>, u32 offset);
+bool is_collapsed_whitespace_node(GC::Ref<DOM::Node>);
+bool is_editing_host(GC::Ref<DOM::Node>);
+bool is_element_with_inline_contents(GC::Ref<DOM::Node>);
+bool is_extraneous_line_break(GC::Ref<DOM::Node>);
+bool is_in_same_editing_host(GC::Ref<DOM::Node>, GC::Ref<DOM::Node>);
+bool is_inline_node(GC::Ref<DOM::Node>);
+bool is_invisible_node(GC::Ref<DOM::Node>);
+bool is_name_of_an_element_with_inline_contents(FlyString const&);
+bool is_prohibited_paragraph_child_name(FlyString const&);
+bool is_visible_node(GC::Ref<DOM::Node>);
+bool is_whitespace_node(GC::Ref<DOM::Node>);
+void move_node_preserving_ranges(GC::Ref<DOM::Node>, GC::Ref<DOM::Node> new_parent, u32 new_index);
+void normalize_sublists_in_node(GC::Ref<DOM::Element>);
+bool precedes_a_line_break(GC::Ref<DOM::Node>);
+Vector<RecordedNodeValue> record_the_values_of_nodes(Vector<GC::Ref<DOM::Node>> const&);
+void remove_extraneous_line_breaks_at_the_end_of_node(GC::Ref<DOM::Node>);
+void remove_extraneous_line_breaks_before_node(GC::Ref<DOM::Node>);
+void remove_extraneous_line_breaks_from_a_node(GC::Ref<DOM::Node>);
+void remove_node_preserving_its_descendants(GC::Ref<DOM::Node>);
+void restore_the_values_of_nodes(Vector<RecordedNodeValue> const&);
+GC::Ref<DOM::Element> set_the_tag_name(GC::Ref<DOM::Element>, FlyString const&);
+Optional<String> specified_command_value(GC::Ref<DOM::Element>, FlyString const& command);
+void split_the_parent_of_nodes(Vector<GC::Ref<DOM::Node>> const&);
+
+}