diff --git a/Libraries/LibWeb/DOM/Document.cpp b/Libraries/LibWeb/DOM/Document.cpp index 77c1ee670d6..b6a2d1fff4c 100644 --- a/Libraries/LibWeb/DOM/Document.cpp +++ b/Libraries/LibWeb/DOM/Document.cpp @@ -4,6 +4,7 @@ * Copyright (c) 2021-2023, Luke Wilde * Copyright (c) 2021-2024, Sam Atkins * Copyright (c) 2024, Matthew Olsson + * Copyright (c) 2025, Jelle Raaijmakers * * SPDX-License-Identifier: BSD-2-Clause */ @@ -662,6 +663,15 @@ WebIDL::ExceptionOr Document::open(Optional const&, Optional< // 11. Replace all with null within document, without firing any mutation events. replace_all(nullptr); + // https://w3c.github.io/editing/docs/execCommand/#state-override + // When document.open() is called and a document's singleton objects are all replaced by new instances of those + // objects, editing state associated with that document (including the CSS styling flag, default single-line + // container name, and any state overrides or value overrides) must be reset. + set_css_styling_flag(false); + set_default_single_line_container_name(HTML::TagNames::div); + reset_command_state_overrides(); + reset_command_value_overrides(); + // 12. If document is fully active, then: if (is_fully_active()) { // 1. Let newURL be a copy of entryDocument's URL. diff --git a/Libraries/LibWeb/DOM/Document.h b/Libraries/LibWeb/DOM/Document.h index 10c14e4cac4..7fa45d62ef1 100644 --- a/Libraries/LibWeb/DOM/Document.h +++ b/Libraries/LibWeb/DOM/Document.h @@ -2,6 +2,7 @@ * Copyright (c) 2018-2023, Andreas Kling * Copyright (c) 2021-2023, Linus Groh * Copyright (c) 2023-2024, Shannon Booth + * Copyright (c) 2025, Jelle Raaijmakers * * SPDX-License-Identifier: BSD-2-Clause */ @@ -765,6 +766,18 @@ public: bool css_styling_flag() const { return m_css_styling_flag; } void set_css_styling_flag(bool flag) { m_css_styling_flag = flag; } + // https://w3c.github.io/editing/docs/execCommand/#state-override + Optional command_state_override(FlyString const& command) const { return m_command_state_override.get(command); } + void set_command_state_override(FlyString const& command, bool state) { m_command_state_override.set(command, state); } + void clear_command_state_override(FlyString const& command) { m_command_state_override.remove(command); } + void reset_command_state_overrides() { m_command_state_override.clear(); } + + // https://w3c.github.io/editing/docs/execCommand/#value-override + Optional command_value_override(FlyString const& command) const { return m_command_value_override.get(command); } + void set_command_value_override(FlyString const& command, String const& value); + void clear_command_value_override(FlyString const& command); + void reset_command_value_overrides() { m_command_value_override.clear(); } + GC::Ptr container_document() const; GC::Ptr session_storage_holder() { return m_session_storage_holder; } @@ -1089,6 +1102,12 @@ private: // https://w3c.github.io/editing/docs/execCommand/#css-styling-flag bool m_css_styling_flag { false }; + // https://w3c.github.io/editing/docs/execCommand/#state-override + HashMap m_command_state_override; + + // https://w3c.github.io/editing/docs/execCommand/#value-override + HashMap m_command_value_override; + // https://html.spec.whatwg.org/multipage/webstorage.html#session-storage-holder // A Document object has an associated session storage holder, which is null or a Storage object. It is initially null. GC::Ptr m_session_storage_holder; diff --git a/Libraries/LibWeb/Editing/ExecCommand.cpp b/Libraries/LibWeb/Editing/ExecCommand.cpp index 5d65ead33ef..6238c1d2be8 100644 --- a/Libraries/LibWeb/Editing/ExecCommand.cpp +++ b/Libraries/LibWeb/Editing/ExecCommand.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, Jelle Raaijmakers + * Copyright (c) 2024-2025, Jelle Raaijmakers * * SPDX-License-Identifier: BSD-2-Clause */ @@ -176,8 +176,65 @@ bool Document::query_command_indeterm(FlyString const& command) if (!optional_command.has_value()) return false; auto const& command_definition = optional_command.value(); - if (!command_definition.indeterminate) + if (!command_definition.indeterminate) { + // https://w3c.github.io/editing/docs/execCommand/#inline-command-activated-values + // If a command is a standard inline value command, it is indeterminate if among formattable nodes that are + // effectively contained in the active range, there are two that have distinct effective command values. + if (command.is_one_of(Editing::CommandNames::backColor, Editing::CommandNames::fontName, + Editing::CommandNames::foreColor, Editing::CommandNames::hiliteColor)) { + Optional first_node_value; + auto range = Editing::active_range(*this); + bool has_distinct_values = false; + Editing::for_each_node_effectively_contained_in_range(range, [&](GC::Ref descendant) { + if (!Editing::is_formattable_node(descendant)) + return TraversalDecision::Continue; + + auto node_value = Editing::effective_command_value(descendant, command); + if (!node_value.has_value()) + return TraversalDecision::Continue; + + if (!first_node_value.has_value()) { + first_node_value = node_value.value(); + } else if (first_node_value.value() != node_value.value()) { + has_distinct_values = true; + return TraversalDecision::Break; + } + + return TraversalDecision::Continue; + }); + return has_distinct_values; + } + + // If a command has inline command activated values defined but nothing else defines when it is indeterminate, + // it is indeterminate if among formattable nodes effectively contained in the active range, there is at least + // one whose effective command value is one of the given values and at least one whose effective command value + // is not one of the given values. + if (!command_definition.inline_activated_values.is_empty()) { + auto range = Editing::active_range(*this); + bool has_at_least_one_match = false; + bool has_at_least_one_mismatch = false; + Editing::for_each_node_effectively_contained_in_range(range, [&](GC::Ref descendant) { + if (!Editing::is_formattable_node(descendant)) + return TraversalDecision::Continue; + + auto node_value = Editing::effective_command_value(descendant, command); + if (!node_value.has_value()) + return TraversalDecision::Continue; + + if (command_definition.inline_activated_values.contains_slow(node_value.value())) + has_at_least_one_match = true; + else + has_at_least_one_mismatch = true; + + if (has_at_least_one_match && has_at_least_one_mismatch) + return TraversalDecision::Break; + return TraversalDecision::Continue; + }); + return has_at_least_one_match && has_at_least_one_mismatch; + } + return false; + } // 2. Return true if command is indeterminate, otherwise false. return command_definition.indeterminate(*this); @@ -191,10 +248,35 @@ bool Document::query_command_state(FlyString const& command) if (!optional_command.has_value()) return false; auto const& command_definition = optional_command.release_value(); - if (!command_definition.state) - return false; + auto state_override = command_state_override(command); + if (!command_definition.state && !state_override.has_value()) { + // https://w3c.github.io/editing/docs/execCommand/#inline-command-activated-values + // If a command has inline command activated values defined, its state is true if either no formattable node is + // effectively contained in the active range, and the active range's start node's effective command value is one + // of the given values; + auto const& inline_values = command_definition.inline_activated_values; + if (inline_values.is_empty()) + return false; + auto range = Editing::active_range(*this); + Vector> formattable_nodes; + Editing::for_each_node_effectively_contained_in_range(range, [&](GC::Ref descendant) { + if (Editing::is_formattable_node(descendant)) + formattable_nodes.append(descendant); + return TraversalDecision::Continue; + }); + if (formattable_nodes.is_empty()) + return inline_values.contains_slow(Editing::effective_command_value(range->start_container(), command).value_or({})); - // FIXME: 2. If the state override for command is set, return it. + // or if there is at least one formattable node effectively contained in the active range, and all of them have + // an effective command value equal to one of the given values. + return all_of(formattable_nodes, [&](GC::Ref node) { + return inline_values.contains_slow(Editing::effective_command_value(node, command).value_or({})); + }); + } + + // 2. If the state override for command is set, return it. + if (state_override.has_value()) + return state_override.release_value(); // 3. Return true if command's state is true, otherwise false. return command_definition.state(*this); @@ -217,16 +299,45 @@ String Document::query_command_value(FlyString const& command) if (!optional_command.has_value()) return {}; auto const& command_definition = optional_command.release_value(); - if (!command_definition.value) + auto value_override = command_value_override(command); + if (!command_definition.value && !value_override.has_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. + // 3. If the value override for command is set, return it. + if (value_override.has_value()) + return value_override.release_value(); // 4. Return command's value. return command_definition.value(*this); } +// https://w3c.github.io/editing/docs/execCommand/#value-override +void Document::set_command_value_override(FlyString const& command, String const& value) +{ + m_command_value_override.set(command, value); + + // The value override for the backColor command must be the same as the value override for the hiliteColor command, + // such that setting one sets the other to the same thing and unsetting one unsets the other. + if (command == Editing::CommandNames::backColor) + m_command_value_override.set(Editing::CommandNames::hiliteColor, value); + else if (command == Editing::CommandNames::hiliteColor) + m_command_value_override.set(Editing::CommandNames::backColor, value); +} + +// https://w3c.github.io/editing/docs/execCommand/#value-override +void Document::clear_command_value_override(FlyString const& command) +{ + m_command_value_override.remove(command); + + // The value override for the backColor command must be the same as the value override for the hiliteColor command, + // such that setting one sets the other to the same thing and unsetting one unsets the other. + if (command == Editing::CommandNames::backColor) + m_command_value_override.remove(Editing::CommandNames::hiliteColor); + else if (command == Editing::CommandNames::hiliteColor) + m_command_value_override.remove(Editing::CommandNames::backColor); +} + } diff --git a/Libraries/LibWeb/Editing/Internal/Algorithms.cpp b/Libraries/LibWeb/Editing/Internal/Algorithms.cpp index 3749eaf5444..cb2976f974e 100644 --- a/Libraries/LibWeb/Editing/Internal/Algorithms.cpp +++ b/Libraries/LibWeb/Editing/Internal/Algorithms.cpp @@ -2745,6 +2745,23 @@ GC::Ptr wrap( return new_parent; } +void for_each_node_effectively_contained_in_range(GC::Ptr range, Function)> callback) +{ + if (!range) + return; + + // A node can still be "effectively contained" in range even if it's not actually contained within the range; so we + // need to do an inclusive subtree traversal since the common ancestor could be matched as well. + range->common_ancestor_container()->for_each_in_inclusive_subtree([&](GC::Ref descendant) { + if (!is_effectively_contained_in_range(descendant, *range)) { + // NOTE: We cannot skip children here since if a descendant is not effectively contained within a range, its + // children might still be. + return TraversalDecision::Continue; + } + return callback(descendant); + }); +} + bool has_visible_children(GC::Ref node) { bool has_visible_child = false; diff --git a/Libraries/LibWeb/Editing/Internal/Algorithms.h b/Libraries/LibWeb/Editing/Internal/Algorithms.h index 496b0818ca8..add0bb7b19f 100644 --- a/Libraries/LibWeb/Editing/Internal/Algorithms.h +++ b/Libraries/LibWeb/Editing/Internal/Algorithms.h @@ -86,6 +86,7 @@ GC::Ptr wrap(Vector>, Function, Function)>); bool has_visible_children(GC::Ref); bool is_heading(FlyString const&); Optional resolved_display(GC::Ref); diff --git a/Libraries/LibWeb/Selection/Selection.cpp b/Libraries/LibWeb/Selection/Selection.cpp index 44238fe6706..683449ba123 100644 --- a/Libraries/LibWeb/Selection/Selection.cpp +++ b/Libraries/LibWeb/Selection/Selection.cpp @@ -1,5 +1,6 @@ /* * Copyright (c) 2021-2022, Andreas Kling + * Copyright (c) 2025, Jelle Raaijmakers * * SPDX-License-Identifier: BSD-2-Clause */ @@ -470,6 +471,13 @@ void Selection::set_range(GC::Ptr range) if (m_range) m_range->set_associated_selection({}, this); + + // https://w3c.github.io/editing/docs/execCommand/#state-override + // Whenever the number of ranges in the selection changes to something different, and whenever a boundary point of + // the range at a given index in the selection changes to something different, the state override and value override + // must be unset for every command. + m_document->reset_command_state_overrides(); + m_document->reset_command_value_overrides(); } GC::Ptr Selection::cursor_position() const