LibWeb: Add command state & value overrides to DOM::Document

This commit is contained in:
Jelle Raaijmakers 2024-12-22 09:58:40 +01:00 committed by Andreas Kling
commit e21ee10b3c
Notes: github-actions[bot] 2025-01-10 22:38:29 +00:00
6 changed files with 173 additions and 7 deletions

View file

@ -4,6 +4,7 @@
* Copyright (c) 2021-2023, Luke Wilde <lukew@serenityos.org>
* Copyright (c) 2021-2024, Sam Atkins <sam@ladybird.org>
* Copyright (c) 2024, Matthew Olsson <mattco@serenityos.org>
* Copyright (c) 2025, Jelle Raaijmakers <jelle@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -662,6 +663,15 @@ WebIDL::ExceptionOr<Document*> Document::open(Optional<String> 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.

View file

@ -2,6 +2,7 @@
* Copyright (c) 2018-2023, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2021-2023, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2023-2024, Shannon Booth <shannon@serenityos.org>
* Copyright (c) 2025, Jelle Raaijmakers <jelle@ladybird.org>
*
* 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<bool> 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<String const&> 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<DOM::Document> container_document() const;
GC::Ptr<HTML::Storage> 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<FlyString, bool> m_command_state_override;
// https://w3c.github.io/editing/docs/execCommand/#value-override
HashMap<FlyString, String> 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<HTML::Storage> m_session_storage_holder;

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2024, Jelle Raaijmakers <jelle@ladybird.org>
* Copyright (c) 2024-2025, Jelle Raaijmakers <jelle@ladybird.org>
*
* 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<String> 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<Node> 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<Node> 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<GC::Ref<Node>> formattable_nodes;
Editing::for_each_node_effectively_contained_in_range(range, [&](GC::Ref<Node> 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> 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);
}
}

View file

@ -2745,6 +2745,23 @@ GC::Ptr<DOM::Node> wrap(
return new_parent;
}
void for_each_node_effectively_contained_in_range(GC::Ptr<DOM::Range> range, Function<TraversalDecision(GC::Ref<DOM::Node>)> 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<DOM::Node> 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<DOM::Node> node)
{
bool has_visible_child = false;

View file

@ -86,6 +86,7 @@ GC::Ptr<DOM::Node> wrap(Vector<GC::Ref<DOM::Node>>, Function<bool(GC::Ref<DOM::N
// Utility methods:
void for_each_node_effectively_contained_in_range(GC::Ptr<DOM::Range>, Function<TraversalDecision(GC::Ref<DOM::Node>)>);
bool has_visible_children(GC::Ref<DOM::Node>);
bool is_heading(FlyString const&);
Optional<CSS::Display> resolved_display(GC::Ref<DOM::Node>);

View file

@ -1,5 +1,6 @@
/*
* Copyright (c) 2021-2022, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2025, Jelle Raaijmakers <jelle@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -470,6 +471,13 @@ void Selection::set_range(GC::Ptr<DOM::Range> 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<DOM::Position> Selection::cursor_position() const