diff --git a/Libraries/LibWeb/Editing/Commands.cpp b/Libraries/LibWeb/Editing/Commands.cpp index af3c4aac26b..e7e4b855520 100644 --- a/Libraries/LibWeb/Editing/Commands.cpp +++ b/Libraries/LibWeb/Editing/Commands.cpp @@ -1880,6 +1880,188 @@ bool command_italic_action(DOM::Document& document, String const&) return true; } +// https://w3c.github.io/editing/docs/execCommand/#the-justifycenter-command +static bool justify_indeterminate(DOM::Document const& document, JustifyAlignment alignment) +{ + // NOTE: This definition is taken from the "justifyCenter" spec and was made generic. + + // Return false if the active range is null. Otherwise, block-extend the active range. + auto range = active_range(document); + if (!range) + return false; + range = block_extend_a_range(*range); + + // Return true if among visible editable nodes that are contained in the result and have no children, at least one + // has alignment value "[alignment]" and at least one does not. Otherwise return false. + Vector> matching_nodes; + range->for_each_contained([&matching_nodes](GC::Ref node) { + if (is_visible_node(node) && node->is_editable() && !node->has_children()) + matching_nodes.append(node); + return IterationDecision::Continue; + }); + return any_of(matching_nodes, [&](GC::Ref node) { + return alignment_value_of_node(node) == alignment; + }) && any_of(matching_nodes, [&](GC::Ref node) { + return alignment_value_of_node(node) != alignment; + }); +} + +// https://w3c.github.io/editing/docs/execCommand/#the-justifycenter-command +static bool justify_state(DOM::Document const& document, JustifyAlignment alignment) +{ + // NOTE: This definition is taken from the "justifyCenter" spec and was made generic. + + // Return false if the active range is null. Otherwise, block-extend the active range. + auto range = active_range(document); + if (!range) + return false; + range = block_extend_a_range(*range); + + // Return true if there is at least one visible editable node that is contained in the result and has no children, + // and all such nodes have alignment value "[alignment]". Otherwise return false. + Vector> matching_nodes; + range->for_each_contained([&matching_nodes](GC::Ref node) { + if (is_visible_node(node) && node->is_editable() && !node->has_children()) + matching_nodes.append(node); + return IterationDecision::Continue; + }); + return !matching_nodes.is_empty() && all_of(matching_nodes, [&](GC::Ref node) { + return alignment_value_of_node(node) == alignment; + }); +} + +// https://w3c.github.io/editing/docs/execCommand/#the-justifycenter-command +static String justify_value(DOM::Document const& document) +{ + // NOTE: This definition is taken from the "justifyCenter" spec and was made generic. + + // Return the empty string if the active range is null. Otherwise, block-extend the active range, + auto range = active_range(document); + if (!range) + return {}; + range = block_extend_a_range(*range); + + // and return the alignment value of the first visible editable node that is contained in the result and has no + // children. + GC::Ptr first_match; + range->for_each_contained([&first_match](GC::Ref node) { + if (is_visible_node(node) && node->is_editable() && !node->has_children()) { + first_match = node; + return IterationDecision::Break; + } + return IterationDecision::Continue; + }); + if (first_match) + return justify_alignment_to_string(alignment_value_of_node(first_match)); + + // If there is no such node, return "left". + return justify_alignment_to_string(JustifyAlignment::Left); +} + +// https://w3c.github.io/editing/docs/execCommand/#the-justifycenter-command +bool command_justify_center_action(DOM::Document& document, String const&) +{ + // Justify the selection with alignment "center", then return true. + justify_the_selection(document, JustifyAlignment::Center); + return true; +} + +// https://w3c.github.io/editing/docs/execCommand/#the-justifycenter-command +bool command_justify_center_indeterminate(DOM::Document const& document) +{ + return justify_indeterminate(document, JustifyAlignment::Center); +} + +// https://w3c.github.io/editing/docs/execCommand/#the-justifycenter-command +bool command_justify_center_state(DOM::Document const& document) +{ + return justify_state(document, JustifyAlignment::Center); +} + +// https://w3c.github.io/editing/docs/execCommand/#the-justifycenter-command +String command_justify_center_value(DOM::Document const& document) +{ + return justify_value(document); +} + +// https://w3c.github.io/editing/docs/execCommand/#the-justifyfull-command +bool command_justify_full_action(DOM::Document& document, String const&) +{ + // Justify the selection with alignment "justify", then return true. + justify_the_selection(document, JustifyAlignment::Justify); + return true; +} + +// https://w3c.github.io/editing/docs/execCommand/#the-justifyfull-command +bool command_justify_full_indeterminate(DOM::Document const& document) +{ + return justify_indeterminate(document, JustifyAlignment::Justify); +} + +// https://w3c.github.io/editing/docs/execCommand/#the-justifyfull-command +bool command_justify_full_state(DOM::Document const& document) +{ + return justify_state(document, JustifyAlignment::Justify); +} + +// https://w3c.github.io/editing/docs/execCommand/#the-justifyfull-command +String command_justify_full_value(DOM::Document const& document) +{ + return justify_value(document); +} + +// https://w3c.github.io/editing/docs/execCommand/#the-justifyleft-command +bool command_justify_left_action(DOM::Document& document, String const&) +{ + // Justify the selection with alignment "left", then return true. + justify_the_selection(document, JustifyAlignment::Left); + return true; +} + +// https://w3c.github.io/editing/docs/execCommand/#the-justifyleft-command +bool command_justify_left_indeterminate(DOM::Document const& document) +{ + return justify_indeterminate(document, JustifyAlignment::Left); +} + +// https://w3c.github.io/editing/docs/execCommand/#the-justifyleft-command +bool command_justify_left_state(DOM::Document const& document) +{ + return justify_state(document, JustifyAlignment::Left); +} + +// https://w3c.github.io/editing/docs/execCommand/#the-justifyleft-command +String command_justify_left_value(DOM::Document const& document) +{ + return justify_value(document); +} + +// https://w3c.github.io/editing/docs/execCommand/#the-justifyright-command +bool command_justify_right_action(DOM::Document& document, String const&) +{ + // Justify the selection with alignment "right", then return true. + justify_the_selection(document, JustifyAlignment::Right); + return true; +} + +// https://w3c.github.io/editing/docs/execCommand/#the-justifyright-command +bool command_justify_right_indeterminate(DOM::Document const& document) +{ + return justify_indeterminate(document, JustifyAlignment::Right); +} + +// https://w3c.github.io/editing/docs/execCommand/#the-justifyright-command +bool command_justify_right_state(DOM::Document const& document) +{ + return justify_state(document, JustifyAlignment::Right); +} + +// https://w3c.github.io/editing/docs/execCommand/#the-justifyright-command +String command_justify_right_value(DOM::Document const& document) +{ + return justify_value(document); +} + // https://w3c.github.io/editing/docs/execCommand/#the-removeformat-command bool command_remove_format_action(DOM::Document& document, String const&) { @@ -2282,6 +2464,42 @@ 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-justifycenter-command + CommandDefinition { + .command = CommandNames::justifyCenter, + .action = command_justify_center_action, + .indeterminate = command_justify_center_indeterminate, + .state = command_justify_center_state, + .value = command_justify_center_value, + .preserves_overrides = true, + }, + // https://w3c.github.io/editing/docs/execCommand/#the-justifyfull-command + CommandDefinition { + .command = CommandNames::justifyFull, + .action = command_justify_full_action, + .indeterminate = command_justify_full_indeterminate, + .state = command_justify_full_state, + .value = command_justify_full_value, + .preserves_overrides = true, + }, + // https://w3c.github.io/editing/docs/execCommand/#the-justifyleft-command + CommandDefinition { + .command = CommandNames::justifyLeft, + .action = command_justify_left_action, + .indeterminate = command_justify_left_indeterminate, + .state = command_justify_left_state, + .value = command_justify_left_value, + .preserves_overrides = true, + }, + // https://w3c.github.io/editing/docs/execCommand/#the-justifyright-command + CommandDefinition { + .command = CommandNames::justifyRight, + .action = command_justify_right_action, + .indeterminate = command_justify_right_indeterminate, + .state = command_justify_right_state, + .value = command_justify_right_value, + .preserves_overrides = true, + }, // https://w3c.github.io/editing/docs/execCommand/#the-removeformat-command CommandDefinition { .command = CommandNames::removeFormat, diff --git a/Libraries/LibWeb/Editing/Commands.h b/Libraries/LibWeb/Editing/Commands.h index 906342b299a..d3dd36a87b0 100644 --- a/Libraries/LibWeb/Editing/Commands.h +++ b/Libraries/LibWeb/Editing/Commands.h @@ -57,6 +57,22 @@ bool command_insert_unordered_list_action(DOM::Document&, String const&); bool command_insert_unordered_list_indeterminate(DOM::Document const&); bool command_insert_unordered_list_state(DOM::Document const&); bool command_italic_action(DOM::Document&, String const&); +bool command_justify_center_action(DOM::Document&, String const&); +bool command_justify_center_indeterminate(DOM::Document const&); +bool command_justify_center_state(DOM::Document const&); +String command_justify_center_value(DOM::Document const&); +bool command_justify_full_action(DOM::Document&, String const&); +bool command_justify_full_indeterminate(DOM::Document const&); +bool command_justify_full_state(DOM::Document const&); +String command_justify_full_value(DOM::Document const&); +bool command_justify_left_action(DOM::Document&, String const&); +bool command_justify_left_indeterminate(DOM::Document const&); +bool command_justify_left_state(DOM::Document const&); +String command_justify_left_value(DOM::Document const&); +bool command_justify_right_action(DOM::Document&, String const&); +bool command_justify_right_indeterminate(DOM::Document const&); +bool command_justify_right_state(DOM::Document const&); +String command_justify_right_value(DOM::Document const&); bool command_remove_format_action(DOM::Document&, String const&); bool command_strikethrough_action(DOM::Document&, String const&); bool command_style_with_css_action(DOM::Document&, String const&); diff --git a/Libraries/LibWeb/Editing/Internal/Algorithms.cpp b/Libraries/LibWeb/Editing/Internal/Algorithms.cpp index a60d7e6337c..7da93a98bb1 100644 --- a/Libraries/LibWeb/Editing/Internal/Algorithms.cpp +++ b/Libraries/LibWeb/Editing/Internal/Algorithms.cpp @@ -55,6 +55,54 @@ GC::Ptr active_range(DOM::Document const& document) return selection->range(); } +// https://w3c.github.io/editing/docs/execCommand/#alignment-value +JustifyAlignment alignment_value_of_node(GC::Ptr node) +{ + // 1. While node is neither null nor an Element, or it is an Element but its "display" property has resolved value + // "inline" or "none", set node to its parent. + auto is_display_inline_or_none = [](GC::Ref node) { + auto display = resolved_display(node); + if (!display.has_value()) + return false; + return (display.value().is_inline_outside() && display.value().is_flow_inside()) || display.value().is_none(); + }; + while ((node && !is(node.ptr())) || (is(node.ptr()) && is_display_inline_or_none(*node))) + node = node->parent(); + + // 2. If node is not an Element, return "left". + if (!is(node.ptr())) + return JustifyAlignment::Left; + GC::Ref element = static_cast(*node); + + // 3. If node's "text-align" property has resolved value "start", return "left" if the directionality of node is + // "ltr", "right" if it is "rtl". + auto text_align_value = resolved_keyword(*node, CSS::PropertyID::TextAlign); + if (!text_align_value.has_value()) + return JustifyAlignment::Left; + if (text_align_value.value() == CSS::Keyword::Start) + return element->directionality() == DOM::Element::Directionality::Ltr ? JustifyAlignment::Left : JustifyAlignment::Right; + + // 4. If node's "text-align" property has resolved value "end", return "right" if the directionality of node is + // "ltr", "left" if it is "rtl". + if (text_align_value.value() == CSS::Keyword::End) + return element->directionality() == DOM::Element::Directionality::Ltr ? JustifyAlignment::Right : JustifyAlignment::Left; + + // 5. If node's "text-align" property has resolved value "center", "justify", "left", or "right", return that value. + switch (text_align_value.value()) { + case CSS::Keyword::Center: + return JustifyAlignment::Center; + case CSS::Keyword::Justify: + return JustifyAlignment::Justify; + case CSS::Keyword::Left: + return JustifyAlignment::Left; + case CSS::Keyword::Right: + return JustifyAlignment::Right; + default: + // 6. Return "left". + return JustifyAlignment::Left; + } +} + // https://w3c.github.io/editing/docs/execCommand/#autolink void autolink(DOM::BoundaryPoint point) { @@ -2595,6 +2643,130 @@ bool is_whitespace_node(GC::Ref node) return false; } +// https://w3c.github.io/editing/docs/execCommand/#justify-the-selection +void justify_the_selection(DOM::Document& document, JustifyAlignment alignment) +{ + // 1. Block-extend the active range, and let new range be the result. + auto new_range = block_extend_a_range(*active_range(document)); + + // 2. Let element list be a list of all editable Elements contained in new range that either has an attribute in the + // HTML namespace whose local name is "align", or has a style attribute that sets "text-align", or is a center. + Vector> element_list; + new_range->for_each_contained([&element_list](GC::Ref node) { + if (!node->is_editable() || !is(*node)) + return IterationDecision::Continue; + + auto& element = static_cast(*node); + if (element.has_attribute_ns(Namespace::HTML, HTML::AttributeNames::align) + || property_in_style_attribute(element, CSS::PropertyID::TextAlign).has_value() + || element.local_name() == HTML::TagNames::center) + element_list.append(element); + + return IterationDecision::Continue; + }); + + // 3. For each element in element list: + for (auto element : element_list) { + // 1. If element has an attribute in the HTML namespace whose local name is "align", remove that attribute. + if (element->has_attribute_ns(Namespace::HTML, HTML::AttributeNames::align)) + element->remove_attribute_ns(Namespace::HTML, HTML::AttributeNames::align); + + // 2. Unset the CSS property "text-align" on element, if it's set by a style attribute. + auto* inline_style = element->style_for_bindings(); + MUST(inline_style->remove_property(CSS::PropertyID::TextAlign)); + + // 3. If element is a div or span or center with no attributes, remove it, preserving its descendants. + if (element->local_name().is_one_of(HTML::TagNames::div, HTML::TagNames::span, HTML::TagNames::center) + && !element->has_attributes()) + remove_node_preserving_its_descendants(element); + + // 4. If element is a center with one or more attributes, set the tag name of element to "div". + if (element->local_name() == HTML::TagNames::center && element->has_attributes()) + set_the_tag_name(element, HTML::TagNames::div); + } + + // 4. Block-extend the active range, and let new range be the result. + new_range = block_extend_a_range(*active_range(document)); + + // 5. Let node list be a list of nodes, initially empty. + Vector> node_list; + + // 6. For each node node contained in new range, append node to node list if the last member of node list (if any) + // is not an ancestor of node; node is editable; node is an allowed child of "div"; and node's alignment value is + // not alignment. + new_range->for_each_contained([&](GC::Ref node) { + if ((node_list.is_empty() || !node_list.last()->is_ancestor_of(node)) + && node->is_editable() + && is_allowed_child_of_node(node, HTML::TagNames::div) + && alignment_value_of_node(node) != alignment) + node_list.append(node); + return IterationDecision::Continue; + }); + + // 7. While node list is not empty: + while (!node_list.is_empty()) { + // 1. Let sublist be a list of nodes, initially empty. + Vector> sublist; + + // 2. Remove the first member of node list and append it to sublist. + sublist.append(node_list.take_first()); + + // 3. While node list is not empty, and the first member of node list is the nextSibling of the last member of + // sublist, remove the first member of node list and append it to sublist. + while (!node_list.is_empty() && node_list.first().ptr() == sublist.last()->next_sibling()) + sublist.append(node_list.take_first()); + + // 4. Wrap sublist. Sibling criteria returns true for any div that has one or both of the following two + // attributes and no other attributes, and false otherwise: + // * An align attribute whose value is an ASCII case-insensitive match for alignment. + // * A style attribute which sets exactly one CSS property (including unrecognized or invalid attributes), + // which is "text-align", which is set to alignment. + // + // New parent instructions are to call createElement("div") on the context object, then set its CSS property + // "text-align" to alignment and return the result. + auto alignment_keyword = string_from_keyword([&alignment] { + switch (alignment) { + case JustifyAlignment::Center: + return CSS::Keyword::Center; + case JustifyAlignment::Justify: + return CSS::Keyword::Justify; + case JustifyAlignment::Left: + return CSS::Keyword::Left; + case JustifyAlignment::Right: + return CSS::Keyword::Right; + } + VERIFY_NOT_REACHED(); + }()); + + wrap( + sublist, + [&](GC::Ref sibling) { + if (!is(*sibling)) + return false; + GC::Ref element = static_cast(*sibling); + u8 number_of_matching_attributes = 0; + if (element->get_attribute_value(HTML::AttributeNames::align).equals_ignoring_ascii_case(alignment_keyword)) + ++number_of_matching_attributes; + if (element->has_attribute(HTML::AttributeNames::style) && element->inline_style() + && element->inline_style()->length() == 1) { + auto text_align = element->inline_style()->property(CSS::PropertyID::TextAlign); + if (text_align.has_value()) { + auto align_value = text_align.value().value->to_string(CSS::CSSStyleValue::SerializationMode::Normal); + if (align_value.equals_ignoring_ascii_case(alignment_keyword)) + ++number_of_matching_attributes; + } + } + return element->attribute_list_size() == number_of_matching_attributes; + }, + [&] { + auto div = MUST(DOM::create_element(document, HTML::TagNames::div, Namespace::HTML)); + auto inline_style = div->style_for_bindings(); + MUST(inline_style->set_property(CSS::PropertyID::TextAlign, alignment_keyword)); + return div; + }); + } +} + // https://w3c.github.io/editing/docs/execCommand/#last-equivalent-point DOM::BoundaryPoint last_equivalent_point(DOM::BoundaryPoint boundary_point) { @@ -4530,6 +4702,21 @@ bool is_heading(FlyString const& local_name) HTML::TagNames::h6); } +String justify_alignment_to_string(JustifyAlignment alignment) +{ + switch (alignment) { + case JustifyAlignment::Center: + return "center"_string; + case JustifyAlignment::Justify: + return "justify"_string; + case JustifyAlignment::Left: + return "left"_string; + case JustifyAlignment::Right: + return "right"_string; + } + VERIFY_NOT_REACHED(); +} + Array named_font_sizes() { return { "x-small"sv, "small"sv, "medium"sv, "large"sv, "x-large"sv, "xx-large"sv, "xxx-large"sv }; diff --git a/Libraries/LibWeb/Editing/Internal/Algorithms.h b/Libraries/LibWeb/Editing/Internal/Algorithms.h index d81f7aa1235..6dbae5e2794 100644 --- a/Libraries/LibWeb/Editing/Internal/Algorithms.h +++ b/Libraries/LibWeb/Editing/Internal/Algorithms.h @@ -36,12 +36,21 @@ enum class SelectionsListState : u8 { None, }; +// https://w3c.github.io/editing/docs/execCommand/#justify-the-selection +enum class JustifyAlignment : u8 { + Center, + Justify, + Left, + Right, +}; + using Selection::Selection; // Below algorithms are specified here: // https://w3c.github.io/editing/docs/execCommand/#assorted-common-algorithms GC::Ptr active_range(DOM::Document const&); +JustifyAlignment alignment_value_of_node(GC::Ptr); void autolink(DOM::BoundaryPoint); GC::Ref block_extend_a_range(GC::Ref); GC::Ptr block_node_of_node(GC::Ref); @@ -85,6 +94,7 @@ bool is_simple_modifiable_element(GC::Ref); bool is_single_line_container(GC::Ref); bool is_visible_node(GC::Ref); bool is_whitespace_node(GC::Ref); +void justify_the_selection(DOM::Document&, JustifyAlignment); DOM::BoundaryPoint last_equivalent_point(DOM::BoundaryPoint); String legacy_font_size(int); void move_node_preserving_ranges(GC::Ref, GC::Ref new_parent, u32 new_index); @@ -121,6 +131,7 @@ CSSPixels font_size_to_pixel_size(StringView); void for_each_node_effectively_contained_in_range(GC::Ptr, Function)>); bool has_visible_children(GC::Ref); bool is_heading(FlyString const&); +String justify_alignment_to_string(JustifyAlignment); Array named_font_sizes(); Optional> property_in_style_attribute(GC::Ref, CSS::PropertyID); Optional resolved_display(GC::Ref); diff --git a/Tests/LibWeb/Text/expected/Editing/execCommand-justifyCenter.txt b/Tests/LibWeb/Text/expected/Editing/execCommand-justifyCenter.txt new file mode 100644 index 00000000000..8762e2c469b --- /dev/null +++ b/Tests/LibWeb/Text/expected/Editing/execCommand-justifyCenter.txt @@ -0,0 +1,2 @@ +
foobar
+
foobar
diff --git a/Tests/LibWeb/Text/expected/Editing/execCommand-justifyFull.txt b/Tests/LibWeb/Text/expected/Editing/execCommand-justifyFull.txt new file mode 100644 index 00000000000..05e9b728eb1 --- /dev/null +++ b/Tests/LibWeb/Text/expected/Editing/execCommand-justifyFull.txt @@ -0,0 +1,2 @@ +
foobar
+
foobar
diff --git a/Tests/LibWeb/Text/expected/Editing/execCommand-justifyLeft.txt b/Tests/LibWeb/Text/expected/Editing/execCommand-justifyLeft.txt new file mode 100644 index 00000000000..a6be3a30012 --- /dev/null +++ b/Tests/LibWeb/Text/expected/Editing/execCommand-justifyLeft.txt @@ -0,0 +1,2 @@ +foobar +
foobar
diff --git a/Tests/LibWeb/Text/expected/Editing/execCommand-justifyRight.txt b/Tests/LibWeb/Text/expected/Editing/execCommand-justifyRight.txt new file mode 100644 index 00000000000..7a7d3a795ab --- /dev/null +++ b/Tests/LibWeb/Text/expected/Editing/execCommand-justifyRight.txt @@ -0,0 +1,2 @@ +
foobar
+
foobar
diff --git a/Tests/LibWeb/Text/input/Editing/execCommand-justifyCenter.html b/Tests/LibWeb/Text/input/Editing/execCommand-justifyCenter.html new file mode 100644 index 00000000000..fd94a4357b8 --- /dev/null +++ b/Tests/LibWeb/Text/input/Editing/execCommand-justifyCenter.html @@ -0,0 +1,21 @@ + +
foobar
+
foobar
+ diff --git a/Tests/LibWeb/Text/input/Editing/execCommand-justifyFull.html b/Tests/LibWeb/Text/input/Editing/execCommand-justifyFull.html new file mode 100644 index 00000000000..eb893d94671 --- /dev/null +++ b/Tests/LibWeb/Text/input/Editing/execCommand-justifyFull.html @@ -0,0 +1,21 @@ + +
foobar
+
foobar
+ diff --git a/Tests/LibWeb/Text/input/Editing/execCommand-justifyLeft.html b/Tests/LibWeb/Text/input/Editing/execCommand-justifyLeft.html new file mode 100644 index 00000000000..92b9275d003 --- /dev/null +++ b/Tests/LibWeb/Text/input/Editing/execCommand-justifyLeft.html @@ -0,0 +1,21 @@ + +
foobar
+
foobar
+ diff --git a/Tests/LibWeb/Text/input/Editing/execCommand-justifyRight.html b/Tests/LibWeb/Text/input/Editing/execCommand-justifyRight.html new file mode 100644 index 00000000000..06fd2535eb6 --- /dev/null +++ b/Tests/LibWeb/Text/input/Editing/execCommand-justifyRight.html @@ -0,0 +1,21 @@ + +
foobar
+
foobar
+