diff --git a/Libraries/LibWeb/Editing/Commands.cpp b/Libraries/LibWeb/Editing/Commands.cpp index f52e38eec1129..2dc03ac38177b 100644 --- a/Libraries/LibWeb/Editing/Commands.cpp +++ b/Libraries/LibWeb/Editing/Commands.cpp @@ -231,22 +231,62 @@ bool command_delete_action(DOM::Document& document, String const&) break; } - // FIXME: 10. If offset is zero, and node has an editable inclusive ancestor in the same editing host + // 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. + if (offset == 0) { + auto inclusive_ancestor = node; + bool has_matching_inclusive_ancestor = false; + while (inclusive_ancestor) { + if (inclusive_ancestor->is_editable() && is_in_same_editing_host(*inclusive_ancestor, *node) + && is_indentation_element(*inclusive_ancestor)) { + has_matching_inclusive_ancestor = true; + break; + } + inclusive_ancestor = inclusive_ancestor->parent(); + } + if (has_matching_inclusive_ancestor) { + // 1. Block-extend the range whose start and end are both (node, 0), and let new range be + // the result. + auto new_range = block_extend_a_range(DOM::Range::create(*node, 0, *node, 0)); + + // 2. Let node list be a list of nodes, initially empty. + Vector> node_list; + + // 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. + auto common_ancestor = new_range->common_ancestor_container(); + common_ancestor->for_each_in_subtree([&](GC::Ref current_node) { + if (!new_range->contains_node(current_node)) + return TraversalDecision::SkipChildrenAndContinue; + + if (!node_list.is_empty() && node_list.last()->is_ancestor_of(current_node)) + return TraversalDecision::SkipChildrenAndContinue; + + if (!current_node->is_editable()) + return TraversalDecision::Continue; + + bool has_editable_descendant = false; + current_node->for_each_in_subtree([&](DOM::Node const& descendant) { + if (descendant.is_editable()) { + has_editable_descendant = true; + return TraversalDecision::Break; + } + return TraversalDecision::Continue; + }); + if (!has_editable_descendant) + node_list.append(current_node); + + return TraversalDecision::Continue; + }); + + // 4. Outdent each node in node list. + for (auto current_node : node_list) + outdent(current_node); - // 5. Return true. - return true; + // 5. Return true. + return true; + } } // 11. If the child of start node with index start offset is a table, return true. diff --git a/Libraries/LibWeb/Editing/Internal/Algorithms.cpp b/Libraries/LibWeb/Editing/Internal/Algorithms.cpp index 582b1f3806f5d..849fe5c266c4c 100644 --- a/Libraries/LibWeb/Editing/Internal/Algorithms.cpp +++ b/Libraries/LibWeb/Editing/Internal/Algorithms.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -1525,6 +1526,47 @@ void force_the_value(GC::Ref node, FlyString const& command, Optional } } +// https://w3c.github.io/editing/docs/execCommand/#indent +void indent(Vector> node_list) +{ + // 1. If node list is empty, do nothing and abort these steps. + if (node_list.is_empty()) + return; + + // 2. Let first node be the first member of node list. + auto first_node = node_list.first(); + + // 3. If first node's parent is an ol or ul: + if (is(first_node->parent()) || is(first_node->parent())) { + // 1. Let tag be the local name of the parent of first node. + auto tag = static_cast(first_node->parent())->local_name(); + + // 2. Wrap node list, with sibling criteria returning true for an HTML element with local name tag and false + // otherwise, and new parent instructions returning the result of calling createElement(tag) on the + // ownerDocument of first node. + wrap( + node_list, + [&](GC::Ref sibling) { + return is(*sibling) && static_cast(*sibling).local_name() == tag; + }, + [&] { return MUST(DOM::create_element(*first_node->owner_document(), tag, Namespace::HTML)); }); + + // 3. Abort these steps. + return; + } + + // 4. Wrap node list, with sibling criteria returning true for a simple indentation element and false otherwise, and + // new parent instructions returning the result of calling createElement("blockquote") on the ownerDocument of + // first node. Let new parent be the result. + auto new_parent = wrap( + node_list, + [&](GC::Ref sibling) { return is_simple_indentation_element(sibling); }, + [&] { return MUST(DOM::create_element(*first_node->owner_document(), HTML::TagNames::blockquote, Namespace::HTML)); }); + + // 5. Fix disallowed ancestors of new parent. + fix_disallowed_ancestors_of_node(*new_parent); +} + // https://w3c.github.io/editing/docs/execCommand/#allowed-child bool is_allowed_child_of_node(Variant, FlyString> child, Variant, FlyString> parent) { @@ -2000,6 +2042,26 @@ bool is_in_same_editing_host(GC::Ref node_a, GC::Ref node_ return editing_host_a && editing_host_a == editing_host_b; } +// https://w3c.github.io/editing/docs/execCommand/#indentation-element +bool is_indentation_element(GC::Ref node) +{ + // An indentation element is either a blockquote, + if (!is(*node)) + return false; + auto& element = static_cast(*node); + if (element.local_name() == HTML::TagNames::blockquote) + return true; + + // or a div that has a style attribute that sets "margin" or some subproperty of it. + auto inline_style = element.inline_style(); + return is(element) + && element.has_attribute(HTML::AttributeNames::style) + && inline_style + && (!inline_style->margin().is_empty() || !inline_style->margin_top().is_empty() + || !inline_style->margin_right().is_empty() || !inline_style->margin_bottom().is_empty() + || !inline_style->margin_left().is_empty()); +} + // https://w3c.github.io/editing/docs/execCommand/#inline-node bool is_inline_node(GC::Ref node) { @@ -2178,6 +2240,57 @@ bool is_prohibited_paragraph_child_name(FlyString const& local_name) HTML::TagNames::xmp); } +// https://w3c.github.io/editing/docs/execCommand/#simple-indentation-element +bool is_simple_indentation_element(GC::Ref node) +{ + // A simple indentation element is an indentation element + if (!is_indentation_element(node)) + return false; + auto const& element = static_cast(*node); + auto inline_style = element.inline_style(); + + // that has no attributes except possibly + bool has_only_valid_attributes = true; + element.for_each_attribute([&](DOM::Attr const& attribute) { + // * a style attribute that sets no properties other than "margin", "border", "padding", or subproperties of + // those; + if (attribute.local_name() == HTML::AttributeNames::style) { + if (!inline_style) + return; + for (auto& style_property : inline_style->properties()) { + switch (style_property.property_id) { + case CSS::PropertyID::Border: + case CSS::PropertyID::BorderBottom: + case CSS::PropertyID::BorderLeft: + case CSS::PropertyID::BorderRight: + case CSS::PropertyID::BorderTop: + case CSS::PropertyID::Margin: + case CSS::PropertyID::MarginBottom: + case CSS::PropertyID::MarginLeft: + case CSS::PropertyID::MarginRight: + case CSS::PropertyID::MarginTop: + case CSS::PropertyID::Padding: + case CSS::PropertyID::PaddingBottom: + case CSS::PropertyID::PaddingLeft: + case CSS::PropertyID::PaddingRight: + case CSS::PropertyID::PaddingTop: + // Allowed + break; + default: + has_only_valid_attributes = false; + return; + } + } + } + + // * and/or a dir attribute. + else if (attribute.local_name() != HTML::AttributeNames::dir) { + has_only_valid_attributes = false; + } + }); + return has_only_valid_attributes; +} + // https://w3c.github.io/editing/docs/execCommand/#simple-modifiable-element bool is_simple_modifiable_element(GC::Ref node) { @@ -2533,6 +2646,168 @@ void normalize_sublists_in_node(GC::Ref item) } } +// https://w3c.github.io/editing/docs/execCommand/#outdent +void outdent(GC::Ref node) +{ + // 1. If node is not editable, abort these steps. + if (!node->is_editable()) + return; + + // 2. If node is a simple indentation element, remove node, preserving its descendants. Then abort these steps. + if (is_simple_indentation_element(node)) { + remove_node_preserving_its_descendants(node); + return; + } + + // 3. If node is an indentation element: + if (is_indentation_element(node)) { + // 1. Unset the dir attribute of node, if any. + auto& element = static_cast(*node); + element.remove_attribute(HTML::AttributeNames::dir); + + // 2. Unset the margin, padding, and border CSS properties of node. + if (auto inline_style = element.inline_style()) { + MUST(inline_style->remove_property(CSS::string_from_property_id(CSS::PropertyID::Border))); + MUST(inline_style->remove_property(CSS::string_from_property_id(CSS::PropertyID::Margin))); + MUST(inline_style->remove_property(CSS::string_from_property_id(CSS::PropertyID::Padding))); + } + + // 3. Set the tag name of node to "div". + set_the_tag_name(element, HTML::TagNames::div); + + // 4. Abort these steps. + return; + } + + // 4. Let current ancestor be node's parent. + GC::Ptr current_ancestor = node->parent(); + + // 5. Let ancestor list be a list of nodes, initially empty. + Vector> ancestor_list; + + // 6. While current ancestor is an editable Element that is neither a simple indentation element nor an ol nor a ul, + // append current ancestor to ancestor list and then set current ancestor to its parent. + while (is(current_ancestor.ptr()) + && current_ancestor->is_editable() + && !is_simple_indentation_element(*current_ancestor) + && !is(*current_ancestor) + && !is(*current_ancestor)) { + ancestor_list.append(*current_ancestor); + current_ancestor = current_ancestor->parent(); + } + + // 7. If current ancestor is not an editable simple indentation element: + if (!current_ancestor || !current_ancestor->is_editable() || !is_simple_indentation_element(*current_ancestor)) { + // 1. Let current ancestor be node's parent. + current_ancestor = node->parent(); + + // 2. Let ancestor list be the empty list. + ancestor_list.clear_with_capacity(); + + // 3. While current ancestor is an editable Element that is neither an indentation element nor an ol nor a ul, + // append current ancestor to ancestor list and then set current ancestor to its parent. + while (is(current_ancestor.ptr()) + && current_ancestor->is_editable() + && !is_indentation_element(*current_ancestor) + && !is(*current_ancestor) + && !is(*current_ancestor)) { + ancestor_list.append(*current_ancestor); + current_ancestor = current_ancestor->parent(); + } + } + + // 8. If node is an ol or ul and current ancestor is not an editable indentation element: + if ((is(*node) || is(*node)) + && !(current_ancestor->is_editable() && is_indentation_element(*current_ancestor))) { + // 1. Unset the reversed, start, and type attributes of node, if any are set. + auto& node_element = static_cast(*node); + node_element.remove_attribute(HTML::AttributeNames::reversed); + node_element.remove_attribute(HTML::AttributeNames::start); + node_element.remove_attribute(HTML::AttributeNames::type); + + // 2. Let children be the children of node. + Vector> children; + for (auto* child = node->first_child(); child; child = child->next_sibling()) + children.append(*child); + + // 3. If node has attributes, and its parent is not an ol or ul, set the tag name of node to "div". + if (node_element.has_attributes() && !is(node->parent()) + && !is(node->parent())) { + set_the_tag_name(node_element, HTML::TagNames::div); + } + + // 4. Otherwise: + else { + // 1. Record the values of node's children, and let values be the result. + auto values = record_the_values_of_nodes(children); + + // 2. Remove node, preserving its descendants. + remove_node_preserving_its_descendants(node); + + // 3. Restore the values from values. + restore_the_values_of_nodes(values); + } + + // 5. Fix disallowed ancestors of each member of children. + for (auto child : children) + fix_disallowed_ancestors_of_node(*child); + + // 6. Abort these steps. + return; + } + + // 9. If current ancestor is not an editable indentation element, abort these steps. + if (!current_ancestor || !current_ancestor->is_editable() || !is_indentation_element(*current_ancestor)) + return; + + // 10. Append current ancestor to ancestor list. + ancestor_list.append(*current_ancestor); + + // 11. Let original ancestor be current ancestor. + auto original_ancestor = current_ancestor; + + // 12. While ancestor list is not empty: + while (!ancestor_list.is_empty()) { + // 1. Let current ancestor be the last member of ancestor list. + // 2. Remove the last member from ancestor list. + current_ancestor = ancestor_list.take_last(); + + // 3. Let target be the child of current ancestor that is equal to either node or the last member of ancestor + // list. + GC::Ptr target; + for (auto* child = current_ancestor->first_child(); child; child = child->next_sibling()) { + if (child == node.ptr() || child == ancestor_list.last().ptr()) { + target = child; + break; + } + } + VERIFY(target); + + // 4. If target is an inline node that is not a br, and its nextSibling is a br, remove target's nextSibling + // from its parent. + if (is_inline_node(*target) && !is(*target) && is(target->next_sibling())) + target->next_sibling()->remove(); + + // 5. Let preceding siblings be the precedings siblings of target, and let following siblings be the followings + // siblings of target. + Vector> preceding_siblings; + for (auto* sibling = target->previous_sibling(); sibling; sibling = sibling->previous_sibling()) + preceding_siblings.append(*sibling); + Vector> following_siblings; + for (auto* sibling = target->next_sibling(); sibling; sibling = sibling->next_sibling()) + following_siblings.append(*sibling); + + // 6. Indent preceding siblings. + indent(preceding_siblings); + + // 7. Indent following siblings. + indent(following_siblings); + } + + // 13. Outdent original ancestor. + outdent(*original_ancestor); +} + // https://w3c.github.io/editing/docs/execCommand/#precedes-a-line-break bool precedes_a_line_break(GC::Ref node) { diff --git a/Libraries/LibWeb/Editing/Internal/Algorithms.h b/Libraries/LibWeb/Editing/Internal/Algorithms.h index 769464e290770..e314a50354651 100644 --- a/Libraries/LibWeb/Editing/Internal/Algorithms.h +++ b/Libraries/LibWeb/Editing/Internal/Algorithms.h @@ -44,6 +44,7 @@ DOM::BoundaryPoint first_equivalent_point(DOM::BoundaryPoint); void fix_disallowed_ancestors_of_node(GC::Ref); bool follows_a_line_break(GC::Ref); void force_the_value(GC::Ref, FlyString const&, Optional); +void indent(Vector>); bool is_allowed_child_of_node(Variant, FlyString> child, Variant, FlyString> parent); bool is_block_boundary_point(DOM::BoundaryPoint); bool is_block_end_point(DOM::BoundaryPoint); @@ -57,6 +58,7 @@ bool is_element_with_inline_contents(GC::Ref); bool is_extraneous_line_break(GC::Ref); bool is_formattable_node(GC::Ref); bool is_in_same_editing_host(GC::Ref, GC::Ref); +bool is_indentation_element(GC::Ref); bool is_inline_node(GC::Ref); bool is_invisible_node(GC::Ref); bool is_modifiable_element(GC::Ref); @@ -64,6 +66,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_simple_indentation_element(GC::Ref); bool is_simple_modifiable_element(GC::Ref); bool is_single_line_container(GC::Ref); bool is_visible_node(GC::Ref); @@ -73,6 +76,7 @@ String legacy_font_size(DOM::Document&, int); void move_node_preserving_ranges(GC::Ref, GC::Ref new_parent, u32 new_index); Optional next_equivalent_point(DOM::BoundaryPoint); void normalize_sublists_in_node(GC::Ref); +void outdent(GC::Ref); bool precedes_a_line_break(GC::Ref); Optional previous_equivalent_point(DOM::BoundaryPoint); void push_down_values(FlyString const&, GC::Ref, Optional);