1
0
Fork 0
mirror of https://github.com/LadybirdBrowser/ladybird.git synced 2025-06-09 09:34:57 +09:00

LibWeb: Implement the "insertText" editing command

Minus the autolinking algorithm.
This commit is contained in:
Jelle Raaijmakers 2025-01-10 13:28:32 +01:00 committed by Andreas Kling
parent 199eaf0d3e
commit 26cadf06d2
Notes: github-actions[bot] 2025-01-10 22:35:07 +00:00
6 changed files with 216 additions and 11 deletions

View file

@ -1726,6 +1726,121 @@ bool command_insert_paragraph_action(DOM::Document& document, String const&)
return true;
}
// https://w3c.github.io/editing/docs/execCommand/#the-inserttext-command
bool command_insert_text_action(DOM::Document& document, String const& value)
{
// 1. Delete the selection, with strip wrappers false.
auto& selection = *document.get_selection();
delete_the_selection(selection, true, false);
// 2. If the active range's start node is neither editable nor an editing host, return true.
auto range = active_range(document);
if (!range->start_container()->is_editable_or_editing_host())
return true;
// 3. If value's length is greater than one:
if (value.code_points().length() > 1) {
// 1. For each code unit el in value, take the action for the insertText command, with value equal to el.
for (auto el : value.code_points())
command_insert_text_action(document, String::from_code_point(el));
// 2. Return true.
return true;
}
// 4. If value is the empty string, return true.
if (value.is_empty())
return true;
// 5. If value is a newline (U+000A), take the action for the insertParagraph command and return true.
if (value == "\n"sv) {
command_insert_paragraph_action(document, {});
return true;
}
// 6. Let node and offset be the active range's start node and offset.
auto node = range->start_container();
auto offset = range->start_offset();
// 7. If node has a child whose index is offset 1, and that child is a Text node, set node to that child, then set
// offset to node's length.
if (is<DOM::Text>(node->child_at_index(offset - 1))) {
node = *node->child_at_index(offset - 1);
offset = node->length();
}
// 8. If node has a child whose index is offset, and that child is a Text node, set node to that child, then set
// offset to zero.
if (is<DOM::Text>(node->child_at_index(offset))) {
node = *node->child_at_index(offset);
offset = 0;
}
// 9. Record current overrides, and let overrides be the result.
auto overrides = record_current_overrides(document);
// 10. Call collapse(node, offset) on the context object's selection.
MUST(selection.collapse(node, offset));
// 11. Canonicalize whitespace at (node, offset).
canonicalize_whitespace({ node, offset });
// 12. Let (node, offset) be the active range's start.
range = *active_range(document);
node = range->start_container();
offset = range->start_offset();
// 13. If node is a Text node:
if (is<DOM::Text>(*node)) {
// 1. Call insertData(offset, value) on node.
MUST(static_cast<DOM::Text&>(*node).insert_data(offset, value));
// 2. Call collapse(node, offset) on the context object's selection.
MUST(selection.collapse(node, offset));
// 3. Call extend(node, offset + 1) on the context object's selection.
MUST(selection.extend(node, offset + 1));
}
// 14. Otherwise:
else {
// 1. If node has only one child, which is a collapsed line break, remove its child from it.
if (node->child_count() == 1 && is_collapsed_line_break(*node->first_child()))
node->first_child()->remove();
// 2. Let text be the result of calling createTextNode(value) on the context object.
auto text = document.create_text_node(value);
// 3. Call insertNode(text) on the active range.
MUST(active_range(document)->insert_node(text));
// 4. Call collapse(text, 0) on the context object's selection.
MUST(selection.collapse(text, 0));
// 5. Call extend(text, 1) on the context object's selection.
MUST(selection.extend(text, 1));
}
// 15. Restore states and values from overrides.
restore_states_and_values(document, overrides);
// 16. Canonicalize whitespace at the active range's start, with fix collapsed space false.
canonicalize_whitespace(active_range(document)->start(), false);
// 17. Canonicalize whitespace at the active range's end, with fix collapsed space false.
canonicalize_whitespace(active_range(document)->end(), false);
// 18. If value is a space character, autolink the active range's start.
if (value == " "sv)
autolink(active_range(document)->start());
// 19. Call collapseToEnd() on the context object's selection.
MUST(selection.collapse_to_end());
// 20. Return true.
return true;
}
// https://w3c.github.io/editing/docs/execCommand/#the-italic-command
bool command_italic_action(DOM::Document& document, String const&)
{
@ -2125,6 +2240,11 @@ static Array const commands {
.action = command_insert_paragraph_action,
.preserves_overrides = true,
},
// https://w3c.github.io/editing/docs/execCommand/#the-inserttext-command
CommandDefinition {
.command = CommandNames::insertText,
.action = command_insert_text_action,
},
// https://w3c.github.io/editing/docs/execCommand/#the-italic-command
CommandDefinition {
.command = CommandNames::italic,

View file

@ -52,6 +52,7 @@ bool command_insert_ordered_list_action(DOM::Document&, String const&);
bool command_insert_ordered_list_indeterminate(DOM::Document const&);
bool command_insert_ordered_list_state(DOM::Document const&);
bool command_insert_paragraph_action(DOM::Document&, String const&);
bool command_insert_text_action(DOM::Document&, String const&);
bool command_italic_action(DOM::Document&, String const&);
bool command_remove_format_action(DOM::Document&, String const&);
bool command_strikethrough_action(DOM::Document&, String const&);

View file

@ -55,6 +55,68 @@ GC::Ptr<DOM::Range> active_range(DOM::Document const& document)
return selection->range();
}
// https://w3c.github.io/editing/docs/execCommand/#autolink
void autolink(DOM::BoundaryPoint point)
{
// 1. While (node, end offset)'s previous equivalent point is not null, set it to its previous equivalent point.
while (true) {
auto previous_point = previous_equivalent_point(point);
if (!previous_point.has_value())
break;
point = previous_point.release_value();
}
// 2. If node is not a Text node, or has an a ancestor, do nothing and abort these steps.
if (!is<DOM::Text>(*point.node) || point.node->first_ancestor_of_type<HTML::HTMLAnchorElement>())
return;
// FIXME: 3. Let search be the largest substring of node's data whose end is end offset and that contains no space
// characters.
// FIXME: 4. If some substring of search is an autolinkable URL:
String href;
if (false) {
// FIXME: 1. While there is no substring of node's data ending at end offset that is an autolinkable URL, decrement end
// offset.
// FIXME: 2. Let start offset be the start index of the longest substring of node's data that is an autolinkable URL
// ending at end offset.
// FIXME: 3. Let href be the substring of node's data starting at start offset and ending at end offset.
}
// FIXME: 5. Otherwise, if some substring of search is a valid e-mail address:
else if (false) {
// FIXME: 1. While there is no substring of node's data ending at end offset that is a valid e-mail address, decrement
// end offset.
// FIXME: 2. Let start offset be the start index of the longest substring of node's data that is a valid e-mail address
// ending at end offset.
// FIXME: 3. Let href be "mailto:" concatenated with the substring of node's data starting at start offset and ending
// at end offset.
}
// 6. Otherwise, do nothing and abort these steps.
else {
return;
}
// 7. Let original range be the active range.
auto& document = point.node->document();
auto original_range = active_range(document);
// FIXME: 8. Create a new range with start (node, start offset) and end (node, end offset), and set the context object's
// selection's range to it.
// 9. Take the action for "createLink", with value equal to href.
take_the_action_for_command(document, CommandNames::createLink, href);
// 10. Set the context object's selection's range to original range.
if (original_range)
document.get_selection()->add_range(*original_range);
}
// https://w3c.github.io/editing/docs/execCommand/#block-extend
GC::Ref<DOM::Range> block_extend_a_range(GC::Ref<DOM::Range> range)
{
@ -3268,19 +3330,12 @@ void restore_states_and_values(DOM::Document& document, Vector<RecordedOverride>
// 2. If node is not null,
if (node) {
auto take_the_action_for_command = [&document](FlyString const& command, String const& value) {
auto const& command_definition = find_command_definition(command);
// FIXME: replace with VERIFY(command_definition.has_value()) as soon as all command definitions are in place.
if (command_definition.has_value())
command_definition->action(document, value);
};
// then for each (command, override) pair in overrides, in order:
for (auto override : overrides) {
// 1. If override is a boolean, and queryCommandState(command) returns something different from override,
// take the action for command, with value equal to the empty string.
if (override.value.has<bool>() && document.query_command_state(override.command) != override.value.get<bool>()) {
take_the_action_for_command(override.command, {});
take_the_action_for_command(document, override.command, {});
}
// 2. Otherwise, if override is a string, and command is neither "createLink" nor "fontSize", and
@ -3288,7 +3343,7 @@ void restore_states_and_values(DOM::Document& document, Vector<RecordedOverride>
// with value equal to override.
else if (override.value.has<String>() && !override.command.is_one_of(CommandNames::createLink, CommandNames::fontSize)
&& document.query_command_value(override.command) != override.value.get<String>()) {
take_the_action_for_command(override.command, override.value.get<String>());
take_the_action_for_command(document, override.command, override.value.get<String>());
}
// 3. Otherwise, if override is a string; and command is "createLink"; and either there is a value override
@ -3300,7 +3355,7 @@ void restore_states_and_values(DOM::Document& document, Vector<RecordedOverride>
&& ((value_override.has_value() && value_override.value() != override.value.get<String>())
|| (!value_override.has_value()
&& effective_command_value(node, CommandNames::createLink) != override.value.get<String>()))) {
take_the_action_for_command(CommandNames::createLink, override.value.get<String>());
take_the_action_for_command(document, CommandNames::createLink, override.value.get<String>());
}
// 4. Otherwise, if override is a string; and command is "fontSize"; and either there is a value override
@ -3320,7 +3375,7 @@ void restore_states_and_values(DOM::Document& document, Vector<RecordedOverride>
override.value = legacy_font_size(override_pixel_size.to_int());
// 2. Take the action for "fontSize", with value equal to override.
take_the_action_for_command(CommandNames::fontSize, override.value.get<String>());
take_the_action_for_command(document, CommandNames::fontSize, override.value.get<String>());
}
// 5. Otherwise, continue this loop from the beginning.
@ -4526,6 +4581,14 @@ Optional<NonnullRefPtr<CSS::CSSStyleValue const>> resolved_value(GC::Ref<DOM::No
return optional_style_property.value().value;
}
void take_the_action_for_command(DOM::Document& document, FlyString const& command, String const& value)
{
auto const& command_definition = find_command_definition(command);
// FIXME: replace with VERIFY(command_definition.has_value()) as soon as all command definitions are in place.
if (command_definition.has_value())
command_definition->action(document, value);
}
bool value_list_contains_keyword(CSS::StyleValueList const& value_list, CSS::Keyword keyword)
{
for (auto& css_style_value : value_list.values()) {

View file

@ -42,6 +42,7 @@ using Selection::Selection;
// https://w3c.github.io/editing/docs/execCommand/#assorted-common-algorithms
GC::Ptr<DOM::Range> active_range(DOM::Document const&);
void autolink(DOM::BoundaryPoint);
GC::Ref<DOM::Range> block_extend_a_range(GC::Ref<DOM::Range>);
GC::Ptr<DOM::Node> block_node_of_node(GC::Ref<DOM::Node>);
String canonical_space_sequence(u32 length, bool non_breaking_start, bool non_breaking_end);
@ -125,6 +126,7 @@ Optional<NonnullRefPtr<CSS::CSSStyleValue const>> property_in_style_attribute(GC
Optional<CSS::Display> resolved_display(GC::Ref<DOM::Node>);
Optional<CSS::Keyword> resolved_keyword(GC::Ref<DOM::Node>, CSS::PropertyID);
Optional<NonnullRefPtr<CSS::CSSStyleValue const>> resolved_value(GC::Ref<DOM::Node>, CSS::PropertyID);
void take_the_action_for_command(DOM::Document&, FlyString const&, String const&);
bool value_list_contains_keyword(CSS::StyleValueList const&, CSS::Keyword);
}