From aee8a75c4006b9be8dfe074651fdd9f19de66cfe Mon Sep 17 00:00:00 2001 From: Jelle Raaijmakers Date: Wed, 8 Jan 2025 16:47:11 +0100 Subject: [PATCH] LibWeb: Implement the "removeFormat" editing command --- Libraries/LibWeb/Editing/Commands.cpp | 80 +++++++++++++++++++ Libraries/LibWeb/Editing/Commands.h | 1 + .../LibWeb/Editing/Internal/Algorithms.cpp | 42 ++++++++++ .../LibWeb/Editing/Internal/Algorithms.h | 1 + .../Editing/execCommand-removeFormat.txt | 2 + .../Editing/execCommand-removeFormat.html | 17 ++++ 6 files changed, 143 insertions(+) create mode 100644 Tests/LibWeb/Text/expected/Editing/execCommand-removeFormat.txt create mode 100644 Tests/LibWeb/Text/input/Editing/execCommand-removeFormat.html diff --git a/Libraries/LibWeb/Editing/Commands.cpp b/Libraries/LibWeb/Editing/Commands.cpp index e444454ba1c..a8e013822d1 100644 --- a/Libraries/LibWeb/Editing/Commands.cpp +++ b/Libraries/LibWeb/Editing/Commands.cpp @@ -1206,6 +1206,81 @@ bool command_italic_action(DOM::Document& document, String const&) return true; } +// https://w3c.github.io/editing/docs/execCommand/#the-removeformat-command +bool command_remove_format_action(DOM::Document& document, String const&) +{ + // 1. Let elements to remove be a list of every removeFormat candidate effectively contained in the active range. + Vector> elements_to_remove; + for_each_node_effectively_contained_in_range(active_range(document), [&](GC::Ref descendant) { + if (is_remove_format_candidate(descendant)) + elements_to_remove.append(static_cast(*descendant)); + return TraversalDecision::Continue; + }); + + // 2. For each element in elements to remove: + for (auto element : elements_to_remove) { + // 1. While element has children, insert the first child of element into the parent of element immediately + // before element, preserving ranges. + auto element_index = element->index(); + while (element->has_children()) + move_node_preserving_ranges(*element->first_child(), *element->parent(), element_index++); + + // 2. Remove element from its parent. + element->remove(); + } + + // 3. If the active range's start node is an editable Text node, and its start offset is neither zero nor its start + // node's length, call splitText() on the active range's start node, with argument equal to the active range's + // start offset. Then set the active range's start node to the result, and its start offset to zero. + auto range = active_range(document); + auto start = range->start(); + if (start.node->is_editable() && is(*start.node) && start.offset != 0 && start.offset != start.node->length()) { + auto new_node = MUST(static_cast(*start.node).split_text(start.offset)); + MUST(range->set_start(new_node, 0)); + } + + // 4. If the active range's end node is an editable Text node, and its end offset is neither zero nor its end node's + // length, call splitText() on the active range's end node, with argument equal to the active range's end offset. + auto end = range->end(); + if (end.node->is_editable() && is(*end.node) && end.offset != 0 && end.offset != end.node->length()) + MUST(static_cast(*end.node).split_text(end.offset)); + + // 5. Let node list consist of all editable nodes effectively contained in the active range. + Vector> node_list; + for_each_node_effectively_contained_in_range(active_range(document), [&](GC::Ref descendant) { + if (descendant->is_editable()) + node_list.append(descendant); + return TraversalDecision::Continue; + }); + + // 6. For each node in node list, while node's parent is a removeFormat candidate in the same editing host as node, + // split the parent of the one-node list consisting of node. + for (auto node : node_list) { + while (node->parent() && is_remove_format_candidate(*node->parent()) && is_in_same_editing_host(*node->parent(), node)) + split_the_parent_of_nodes({ node }); + } + + // 7. For each of the entries in the following list, in the given order, set the selection's value to null, with + // command as given. + // 1. subscript + // 2. bold + // 3. fontName + // 4. fontSize + // 5. foreColor + // 6. hiliteColor + // 7. italic + // 8. strikethrough + // 9. underline + for (auto command_name : { CommandNames::subscript, CommandNames::bold, CommandNames::fontName, + CommandNames::fontSize, CommandNames::foreColor, CommandNames::hiliteColor, CommandNames::italic, + CommandNames::strikethrough, CommandNames::underline }) { + set_the_selections_value(document, command_name, {}); + } + + // 8. Return true. + return true; +} + // https://w3c.github.io/editing/docs/execCommand/#the-stylewithcss-command bool command_style_with_css_action(DOM::Document& document, String const& value) { @@ -1305,6 +1380,11 @@ static Array const commands { .relevant_css_property = CSS::PropertyID::FontStyle, .inline_activated_values = { "italic"sv, "oblique"sv }, }, + // https://w3c.github.io/editing/docs/execCommand/#the-removeformat-command + CommandDefinition { + .command = CommandNames::removeFormat, + .action = command_remove_format_action, + }, // https://w3c.github.io/editing/docs/execCommand/#the-stylewithcss-command CommandDefinition { .command = CommandNames::styleWithCSS, diff --git a/Libraries/LibWeb/Editing/Commands.h b/Libraries/LibWeb/Editing/Commands.h index 1acd8a6475c..702582c1711 100644 --- a/Libraries/LibWeb/Editing/Commands.h +++ b/Libraries/LibWeb/Editing/Commands.h @@ -43,6 +43,7 @@ bool command_forward_delete_action(DOM::Document&, String const&); bool command_insert_linebreak_action(DOM::Document&, String const&); bool command_insert_paragraph_action(DOM::Document&, String const&); bool command_italic_action(DOM::Document&, String const&); +bool command_remove_format_action(DOM::Document&, String const&); bool command_style_with_css_action(DOM::Document&, String const&); bool command_style_with_css_state(DOM::Document const&); diff --git a/Libraries/LibWeb/Editing/Internal/Algorithms.cpp b/Libraries/LibWeb/Editing/Internal/Algorithms.cpp index 5d0bc7900a4..ec429554e2c 100644 --- a/Libraries/LibWeb/Editing/Internal/Algorithms.cpp +++ b/Libraries/LibWeb/Editing/Internal/Algorithms.cpp @@ -2246,6 +2246,48 @@ bool is_prohibited_paragraph_child_name(FlyString const& local_name) HTML::TagNames::xmp); } +// https://w3c.github.io/editing/docs/execCommand/#removeformat-candidate +bool is_remove_format_candidate(GC::Ref node) +{ + // A removeFormat candidate is an editable HTML element with local name "abbr", "acronym", "b", "bdi", "bdo", "big", + // "blink", "cite", "code", "dfn", "em", "font", "i", "ins", "kbd", "mark", "nobr", "q", "s", "samp", "small", + // "span", "strike", "strong", "sub", "sup", "tt", "u", or "var". + if (!node->is_editable()) + return false; + if (!is(*node)) + return false; + return static_cast(*node).local_name().is_one_of( + HTML::TagNames::abbr, + HTML::TagNames::acronym, + HTML::TagNames::b, + HTML::TagNames::bdi, + HTML::TagNames::bdo, + HTML::TagNames::big, + HTML::TagNames::blink, + HTML::TagNames::cite, + HTML::TagNames::code, + HTML::TagNames::dfn, + HTML::TagNames::em, + HTML::TagNames::font, + HTML::TagNames::i, + HTML::TagNames::ins, + HTML::TagNames::kbd, + HTML::TagNames::mark, + HTML::TagNames::nobr, + HTML::TagNames::q, + HTML::TagNames::s, + HTML::TagNames::samp, + HTML::TagNames::small, + HTML::TagNames::span, + HTML::TagNames::strike, + HTML::TagNames::strong, + HTML::TagNames::sub, + HTML::TagNames::sup, + HTML::TagNames::tt, + HTML::TagNames::u, + HTML::TagNames::var); +} + // https://w3c.github.io/editing/docs/execCommand/#simple-indentation-element bool is_simple_indentation_element(GC::Ref node) { diff --git a/Libraries/LibWeb/Editing/Internal/Algorithms.h b/Libraries/LibWeb/Editing/Internal/Algorithms.h index 3d138a006e7..c4f4083f08c 100644 --- a/Libraries/LibWeb/Editing/Internal/Algorithms.h +++ b/Libraries/LibWeb/Editing/Internal/Algorithms.h @@ -67,6 +67,7 @@ bool is_name_of_an_element_with_inline_contents(FlyString const&); bool is_non_list_single_line_container(GC::Ref); bool is_prohibited_paragraph_child(GC::Ref); bool is_prohibited_paragraph_child_name(FlyString const&); +bool is_remove_format_candidate(GC::Ref); bool is_simple_indentation_element(GC::Ref); bool is_simple_modifiable_element(GC::Ref); bool is_single_line_container(GC::Ref); diff --git a/Tests/LibWeb/Text/expected/Editing/execCommand-removeFormat.txt b/Tests/LibWeb/Text/expected/Editing/execCommand-removeFormat.txt new file mode 100644 index 00000000000..9bb0b07d966 --- /dev/null +++ b/Tests/LibWeb/Text/expected/Editing/execCommand-removeFormat.txt @@ -0,0 +1,2 @@ +Div contents: "foobar" +Div contents: "foobar" diff --git a/Tests/LibWeb/Text/input/Editing/execCommand-removeFormat.html b/Tests/LibWeb/Text/input/Editing/execCommand-removeFormat.html new file mode 100644 index 00000000000..efa4abdb49f --- /dev/null +++ b/Tests/LibWeb/Text/input/Editing/execCommand-removeFormat.html @@ -0,0 +1,17 @@ + +
foobar
+