diff --git a/Libraries/LibWeb/DOM/Element.h b/Libraries/LibWeb/DOM/Element.h index 278971ad4ed..8535e8ca368 100644 --- a/Libraries/LibWeb/DOM/Element.h +++ b/Libraries/LibWeb/DOM/Element.h @@ -211,6 +211,7 @@ public: void reset_animated_css_properties(); + GC::Ptr inline_style() { return m_inline_style; } GC::Ptr inline_style() const { return m_inline_style; } CSS::CSSStyleDeclaration* style_for_bindings(); diff --git a/Libraries/LibWeb/Editing/Internal/Algorithms.cpp b/Libraries/LibWeb/Editing/Internal/Algorithms.cpp index b692816870a..29a94e55b81 100644 --- a/Libraries/LibWeb/Editing/Internal/Algorithms.cpp +++ b/Libraries/LibWeb/Editing/Internal/Algorithms.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -471,6 +472,118 @@ void canonicalize_whitespace(DOM::BoundaryPoint boundary, bool fix_collapsed_spa } } +// https://w3c.github.io/editing/docs/execCommand/#clear-the-value +Vector> clear_the_value(FlyString const& command, GC::Ref element) +{ + // 1. Let command be the current command. + + // 2. If element is not editable, return the empty list. + if (!element->is_editable()) + return {}; + + // 3. If element's specified command value for command is null, return the empty list. + if (!specified_command_value(element, command).has_value()) + return {}; + + // 4. If element is a simple modifiable element: + if (is_simple_modifiable_element(element)) { + // 1. Let children be the children of element. + Vector> children; + element->for_each_child([&children](DOM::Node& child) { + children.append(child); + return IterationDecision::Continue; + }); + + // 2. For each child in children, insert child into element's parent immediately before element, preserving + // ranges. + auto element_index = element->index(); + for (auto child : children) + move_node_preserving_ranges(child, *element->parent(), element_index++); + + // 3. Remove element from its parent. + element->remove(); + + // 4. Return children. + return children; + } + + // 5. If command is "strikethrough", and element has a style attribute that sets "text-decoration" to some value + // containing "line-through", delete "line-through" from the value. + auto remove_text_decoration_value = [&element](CSS::Keyword keyword_to_delete) { + auto inline_style = element->inline_style(); + if (!inline_style) + return; + + auto style_property = inline_style->property(CSS::PropertyID::TextDecoration); + if (!style_property.has_value()) + return; + + auto style_value = style_property.value().value; + VERIFY(style_value->is_value_list()); + auto const& value_list = style_value->as_value_list(); + auto& old_values = value_list.values(); + + auto new_values = old_values; + auto was_removed = new_values.remove_all_matching([&](CSS::ValueComparingNonnullRefPtr const& value) { + return value->is_keyword() && value->as_keyword().keyword() == keyword_to_delete; + }); + if (!was_removed) + return; + if (new_values.is_empty()) { + MUST(inline_style->remove_property(string_from_property_id(CSS::PropertyID::TextDecoration))); + return; + } + + auto new_style_value = CSS::StyleValueList::create(move(new_values), value_list.separator()); + MUST(inline_style->set_property( + string_from_property_id(CSS::PropertyID::TextDecoration), + new_style_value->to_string(CSS::CSSStyleValue::SerializationMode::Normal), + {})); + }; + if (command == CommandNames::strikethrough) + remove_text_decoration_value(CSS::Keyword::LineThrough); + + // 6. If command is "underline", and element has a style attribute that sets "text-decoration" to some value + // containing "underline", delete "underline" from the value. + if (command == CommandNames::underline) + remove_text_decoration_value(CSS::Keyword::Underline); + + // 7. If the relevant CSS property for command is not null, unset that property of element. + auto command_definition = find_command_definition(command); + // FIXME: remove command_definition.has_value() as soon as all commands are implemented. + if (command_definition.has_value() && command_definition.value().relevant_css_property.has_value()) { + auto property_to_remove = command_definition.value().relevant_css_property.value(); + if (auto inline_style = element->inline_style()) + MUST(inline_style->remove_property(string_from_property_id(property_to_remove))); + } + + // 8. If element is a font element: + if (is(*element)) { + // 1. If command is "foreColor", unset element's color attribute, if set. + if (command == CommandNames::foreColor) + element->remove_attribute(HTML::AttributeNames::color); + + // 2. If command is "fontName", unset element's face attribute, if set. + if (command == CommandNames::fontName) + element->remove_attribute(HTML::AttributeNames::face); + + // 3. If command is "fontSize", unset element's size attribute, if set. + if (command == CommandNames::fontSize) + element->remove_attribute(HTML::AttributeNames::size); + } + + // 9. If element is an a element and command is "createLink" or "unlink", unset the href property of element. + if (is(*element) && command.is_one_of(CommandNames::createLink, CommandNames::unlink)) + element->remove_attribute(HTML::AttributeNames::href); + + // 10. If element's specified command value for command is null, return the empty list. + if (!specified_command_value(element, command).has_value()) + return {}; + + // 11. Set the tag name of element to "span", and return the one-node list consisting of the result. + return { set_the_tag_name(element, HTML::TagNames::span) }; +} + // https://w3c.github.io/editing/docs/execCommand/#delete-the-selection void delete_the_selection(Selection& selection, bool block_merging, bool strip_wrappers, Selection::Direction direction) { @@ -1175,6 +1288,248 @@ bool follows_a_line_break(GC::Ref node) return true; } +// https://w3c.github.io/editing/docs/execCommand/#force-the-value +void force_the_value(GC::Ref node, FlyString const& command, Optional new_value) +{ + // 1. Let command be the current command. + + // 2. If node's parent is null, abort this algorithm. + if (!node->parent()) + return; + + // 3. If new value is null, abort this algorithm. + if (!new_value.has_value()) + return; + + // 4. If node is an allowed child of "span": + if (is_allowed_child_of_node(node, HTML::TagNames::span)) { + // 1. Reorder modifiable descendants of node's previousSibling. + if (node->previous_sibling()) + reorder_modifiable_descendants(*node->previous_sibling(), command, new_value); + + // 2. Reorder modifiable descendants of node's nextSibling. + if (node->next_sibling()) + reorder_modifiable_descendants(*node->next_sibling(), command, new_value); + + // 3. Wrap the one-node list consisting of node, with sibling criteria returning true for a simple modifiable + // element whose specified command value is equivalent to new value and whose effective command value is + // loosely equivalent to new value and false otherwise, and with new parent instructions returning null. + wrap( + { node }, + [&](GC::Ref sibling) { + return is_simple_modifiable_element(sibling) + && specified_command_value(static_cast(*sibling), command) == new_value + && values_are_loosely_equivalent(command, effective_command_value(sibling, command), new_value); + }, + [] -> GC::Ptr { return {}; }); + } + + // 5. If node is invisible, abort this algorithm. + if (is_invisible_node(node)) + return; + + // 6. If the effective command value of command is loosely equivalent to new value on node, abort this algorithm. + if (values_are_loosely_equivalent(command, effective_command_value(node, command), new_value)) + return; + + // 7. If node is not an allowed child of "span": + if (!is_allowed_child_of_node(node, HTML::TagNames::span)) { + // 1. Let children be all children of node, omitting any that are Elements whose specified command value for + // command is neither null nor equivalent to new value. + Vector> children; + node->for_each_child([&](GC::Ref child) { + if (is(*child)) { + auto const& child_specified_value = specified_command_value(static_cast(*child), command); + if (child_specified_value.has_value() && !values_are_equivalent(command, child_specified_value.value(), new_value)) + return IterationDecision::Continue; + } + + children.append(child); + return IterationDecision::Continue; + }); + + // 2. Force the value of each node in children, with command and new value as in this invocation of the + // algorithm. + for (auto child : children) + force_the_value(child, command, new_value); + + // 3. Abort this algorithm. + return; + } + + // 8. If the effective command value of command is loosely equivalent to new value on node, abort this algorithm. + if (values_are_loosely_equivalent(command, effective_command_value(node, command), new_value)) + return; + + // 9. Let new parent be null. + GC::Ptr new_parent; + + // 10. If the CSS styling flag is false: + auto& document = node->document(); + if (!document.css_styling_flag()) { + // 1. If command is "bold" and new value is "bold", let new parent be the result of calling createElement("b") + // on the ownerDocument of node. + if (command == CommandNames::bold && new_value == "bold"sv) + new_parent = MUST(DOM::create_element(document, HTML::TagNames::b, Namespace::HTML)); + + // 2. If command is "italic" and new value is "italic", let new parent be the result of calling + // createElement("i") on the ownerDocument of node. + if (command == CommandNames::italic && new_value == "italic"sv) + new_parent = MUST(DOM::create_element(document, HTML::TagNames::i, Namespace::HTML)); + + // 3. If command is "strikethrough" and new value is "line-through", let new parent be the result of calling + // createElement("s") on the ownerDocument of node. + if (command == CommandNames::strikethrough && new_value == "line-through"sv) + new_parent = MUST(DOM::create_element(document, HTML::TagNames::s, Namespace::HTML)); + + // 4. If command is "underline" and new value is "underline", let new parent be the result of calling + // createElement("u") on the ownerDocument of node. + if (command == CommandNames::underline && new_value == "underline"sv) + new_parent = MUST(DOM::create_element(document, HTML::TagNames::u, Namespace::HTML)); + + // 5. If command is "foreColor", and new value is fully opaque with red, green, and blue components in the + // range 0 to 255: + if (command == CommandNames::foreColor) { + auto new_value_color = Color::from_string(new_value.value()); + if (new_value_color->alpha() == NumericLimits::max()) { + // 1. Let new parent be the result of calling createElement("font") on the ownerDocument of node. + new_parent = MUST(DOM::create_element(document, HTML::TagNames::font, Namespace::HTML)); + + // 2. Set the color attribute of new parent to the result of applying the rules for serializing simple color + // values to new value (interpreted as a simple color). + MUST(new_parent->set_attribute(HTML::AttributeNames::color, new_value_color->to_string_without_alpha())); + } + } + + // 6. If command is "fontName", let new parent be the result of calling createElement("font") on the + // ownerDocument of node, then set the face attribute of new parent to new value. + if (command == CommandNames::fontName) { + new_parent = MUST(DOM::create_element(document, HTML::TagNames::font, Namespace::HTML)); + MUST(new_parent->set_attribute(HTML::AttributeNames::face, new_value.value())); + } + } + + // 11. If command is "createLink" or "unlink": + if (command.is_one_of(CommandNames::createLink, CommandNames::unlink)) { + // 1. Let new parent be the result of calling createElement("a") on the ownerDocument of node. + new_parent = MUST(DOM::create_element(document, HTML::TagNames::a, Namespace::HTML)); + + // 2. Set the href attribute of new parent to new value. + MUST(new_parent->set_attribute(HTML::AttributeNames::href, new_value.value())); + + // 3. Let ancestor be node's parent. + GC::Ptr ancestor = node->parent(); + + // 4. While ancestor is not null: + while (ancestor) { + // 1. If ancestor is an a, set the tag name of ancestor to "span", and let ancestor be the result. + if (is(*ancestor)) + ancestor = set_the_tag_name(static_cast(*ancestor), HTML::TagNames::span); + + // 2. Set ancestor to its parent. + ancestor = ancestor->parent(); + } + } + + // 12. If command is "fontSize"; and new value is one of "x-small", "small", "medium", "large", "x-large", + // "xx-large", or "xxx-large"; and either the CSS styling flag is false, or new value is "xxx-large": + auto const& font_sizes = named_font_sizes(); + if (command == CommandNames::fontSize && font_sizes.contains_slow(new_value.value()) + && (!document.css_styling_flag() || new_value == "xxx-large"sv)) { + // let new parent be the result of calling createElement("font") on the ownerDocument of node, + new_parent = MUST(DOM::create_element(document, HTML::TagNames::font, Namespace::HTML)); + + // then set the size attribute of new parent to the number from the following table based on new value: + // * x-small: 1 + // * small: 2 + // * normal: 3 + // * large: 4 + // * x-large: 5 + // * xx-large: 6 + // * xxx-large: 7 + auto size = font_sizes.first_index_of(new_value.value()).value() + 1; + MUST(new_parent->set_attribute(HTML::AttributeNames::size, String::number(size))); + } + + // 13. If command is "subscript" or "superscript" and new value is "subscript", let new parent be the result of + // calling createElement("sub") on the ownerDocument of node. + if (command.is_one_of(CommandNames::subscript, CommandNames::superscript) && new_value == "subscript"sv) + new_parent = MUST(DOM::create_element(document, HTML::TagNames::sub, Namespace::HTML)); + + // 14. If command is "subscript" or "superscript" and new value is "superscript", let new parent be the result of + // calling createElement("sup") on the ownerDocument of node. + if (command.is_one_of(CommandNames::subscript, CommandNames::superscript) && new_value == "superscript"sv) + new_parent = MUST(DOM::create_element(document, HTML::TagNames::sup, Namespace::HTML)); + + // 15. If new parent is null, let new parent be the result of calling createElement("span") on the ownerDocument of + // node. + if (!new_parent) + new_parent = MUST(DOM::create_element(document, HTML::TagNames::span, Namespace::HTML)); + + // 16. Insert new parent in node's parent before node. + node->parent()->insert_before(*new_parent, node); + + // 17. If the effective command value of command for new parent is not loosely equivalent to new value, and the + // relevant CSS property for command is not null, set that CSS property of new parent to new value (if the new + // value would be valid). + if (!values_are_loosely_equivalent(command, effective_command_value(new_parent, command), new_value)) { + auto const& command_definition = find_command_definition(command); + if (command_definition.has_value() && command_definition.value().relevant_css_property.has_value()) { + auto inline_style = new_parent->style_for_bindings(); + MUST(inline_style->set_property(command_definition.value().relevant_css_property.value(), new_value.value())); + } + } + + // 18. If command is "strikethrough", and new value is "line-through", and the effective command value of + // "strikethrough" for new parent is not "line-through", set the "text-decoration" property of new parent to + // "line-through". + if (command == CommandNames::strikethrough && new_value == "line-through"sv + && effective_command_value(new_parent, command) != "line-through"sv) { + auto inline_style = new_parent->style_for_bindings(); + MUST(inline_style->set_property(CSS::PropertyID::TextDecoration, "line-through"sv)); + } + + // 19. If command is "underline", and new value is "underline", and the effective command value of "underline" for + // new parent is not "underline", set the "text-decoration" property of new parent to "underline". + if (command == CommandNames::underline && new_value == "underline"sv + && effective_command_value(new_parent, command) != "underline"sv) { + auto inline_style = new_parent->style_for_bindings(); + MUST(inline_style->set_property(CSS::PropertyID::TextDecoration, "underline"sv)); + } + + // 20. Append node to new parent as its last child, preserving ranges. + move_node_preserving_ranges(node, *new_parent, new_parent->child_count()); + + // 21. If node is an Element and the effective command value of command for node is not loosely equivalent to new + // value: + if (is(*node) && !values_are_loosely_equivalent(command, effective_command_value(node, command), new_value)) { + // 1. Insert node into the parent of new parent before new parent, preserving ranges. + move_node_preserving_ranges(node, *new_parent->parent(), new_parent->index()); + + // 2. Remove new parent from its parent. + new_parent->remove(); + + // 3. Let children be all children of node, omitting any that are Elements whose specified command value for + // command is neither null nor equivalent to new value. + Vector> children; + node->for_each_child([&](GC::Ref child) { + if (is(*child)) { + auto child_value = specified_command_value(static_cast(*child), command); + if (child_value.has_value() && !values_are_equivalent(command, child_value.value(), new_value)) + return IterationDecision::Continue; + } + + children.append(child); + return IterationDecision::Continue; + }); + + // 4. Force the value of each node in children, with command and new value as in this invocation of the + // algorithm. + for (auto child : children) + force_the_value(child, command, new_value); + } +} + // https://w3c.github.io/editing/docs/execCommand/#allowed-child bool is_allowed_child_of_node(Variant, FlyString> child, Variant, FlyString> parent) { @@ -1664,6 +2019,40 @@ bool is_invisible_node(GC::Ref node) return !is_visible_node(node); } +// https://w3c.github.io/editing/docs/execCommand/#modifiable-element +bool is_modifiable_element(GC::Ref node) +{ + // NOTE: All conditions below expect an HTML element. + if (!is(*node)) + return false; + auto const& html_element = static_cast(*node); + + // A modifiable element is a b, em, i, s, span, strike, strong, sub, sup, or u element with no attributes except + // possibly style; + auto has_no_attributes_except = [&](auto exclusions) { + auto attribute_count = 0; + html_element.for_each_attribute([&](DOM::Attr const& attribute) { + if (!exclusions.contains_slow(attribute.local_name())) + ++attribute_count; + }); + return attribute_count == 0; + }; + if (html_element.local_name().is_one_of(HTML::TagNames::b, HTML::TagNames::em, HTML::TagNames::i, + HTML::TagNames::s, HTML::TagNames::span, HTML::TagNames::strike, HTML::TagNames::strong, + HTML::TagNames::sub, HTML::TagNames::sup, HTML::TagNames::u)) + return has_no_attributes_except(Array { HTML::AttributeNames::style }); + + // or a font element with no attributes except possibly style, color, face, and/or size; + if (is(html_element)) { + return has_no_attributes_except(Array { HTML::AttributeNames::style, HTML::AttributeNames::color, + HTML::AttributeNames::face, HTML::AttributeNames::size }); + } + + // or an a element with no attributes except possibly style and/or href. + return is(html_element) + && has_no_attributes_except(Array { HTML::AttributeNames::style, HTML::AttributeNames::href }); +} + // https://w3c.github.io/editing/docs/execCommand/#name-of-an-element-with-inline-contents bool is_name_of_an_element_with_inline_contents(FlyString const& local_name) { @@ -1794,6 +2183,88 @@ bool is_prohibited_paragraph_child_name(FlyString const& local_name) HTML::TagNames::xmp); } +// https://w3c.github.io/editing/docs/execCommand/#simple-modifiable-element +bool is_simple_modifiable_element(GC::Ref node) +{ + // A simple modifiable element is an HTML element for which at least one of the following holds: + if (!is(*node)) + return false; + auto const& html_element = static_cast(*node); + auto const inline_style = html_element.inline_style(); + + // * It is an a, b, em, font, i, s, span, strike, strong, sub, sup, or u element with no attributes. + // * It is an a, b, em, font, i, s, span, strike, strong, sub, sup, or u element with exactly one attribute, which + // is style, which sets no CSS properties (including invalid or unrecognized properties). + auto attribute_count = html_element.attribute_list_size(); + if (html_element.local_name().is_one_of(HTML::TagNames::a, HTML::TagNames::b, HTML::TagNames::em, + HTML::TagNames::font, HTML::TagNames::i, HTML::TagNames::s, HTML::TagNames::span, HTML::TagNames::strike, + HTML::TagNames::strong, HTML::TagNames::sub, HTML::TagNames::sup, HTML::TagNames::u)) { + if (attribute_count == 0) + return true; + + if (attribute_count == 1 && html_element.has_attribute(HTML::AttributeNames::style) + && (!inline_style || inline_style->length() == 0)) + return true; + } + + // NOTE: All conditions below require exactly one attribute on the element + if (attribute_count != 1) + return false; + + // * It is an a element with exactly one attribute, which is href. + if (is(html_element) + && html_element.get_attribute(HTML::AttributeNames::href).has_value()) + return true; + + // * It is a font element with exactly one attribute, which is either color, face, or size. + if (is(html_element)) { + if (html_element.has_attribute(HTML::AttributeNames::color) + || html_element.has_attribute(HTML::AttributeNames::face) + || html_element.has_attribute(HTML::AttributeNames::size)) + return true; + } + + // NOTE: All conditions below require exactly one attribute which is style, that sets one CSS property. + if (!html_element.has_attribute(HTML::AttributeNames::style) || !inline_style || (inline_style->length() != 1)) + return false; + + // * It is a b or strong element with exactly one attribute, which is style, and the style attribute sets exactly + // one CSS property (including invalid or unrecognized properties), which is "font-weight". + if (html_element.local_name().is_one_of(HTML::TagNames::b, HTML::TagNames::strong) + && inline_style->property(CSS::PropertyID::FontWeight).has_value()) + return true; + + // * It is an i or em element with exactly one attribute, which is style, and the style attribute sets exactly one + // CSS property (including invalid or unrecognized properties), which is "font-style". + if (html_element.local_name().is_one_of(HTML::TagNames::i, HTML::TagNames::em) + && inline_style->property(CSS::PropertyID::FontStyle).has_value()) + return true; + + // * It is an a, font, or span element with exactly one attribute, which is style, and the style attribute sets + // exactly one CSS property (including invalid or unrecognized properties), and that property is not + // "text-decoration". + if (html_element.local_name().is_one_of(HTML::TagNames::a, HTML::TagNames::font, HTML::TagNames::span) + && !inline_style->property(CSS::PropertyID::TextDecoration).has_value()) + return true; + + // * It is an a, font, s, span, strike, or u element with exactly one attribute, which is style, and the style + // attribute sets exactly one CSS property (including invalid or unrecognized properties), which is + // "text-decoration", which is set to "line-through" or "underline" or "overline" or "none". + if (html_element.local_name().is_one_of(HTML::TagNames::a, HTML::TagNames::font, HTML::TagNames::s, + HTML::TagNames::span, HTML::TagNames::strike, HTML::TagNames::u) + && inline_style->property(CSS::PropertyID::TextDecoration).has_value()) { + auto text_decoration = inline_style->text_decoration(); + if (first_is_one_of(text_decoration, + string_from_keyword(CSS::Keyword::LineThrough), + string_from_keyword(CSS::Keyword::Underline), + string_from_keyword(CSS::Keyword::Overline), + string_from_keyword(CSS::Keyword::None))) + return true; + } + + return false; +} + // https://w3c.github.io/editing/docs/execCommand/#single-line-container bool is_single_line_container(GC::Ref node) { @@ -2133,6 +2604,96 @@ Optional previous_equivalent_point(DOM::BoundaryPoint bounda return {}; } +// https://w3c.github.io/editing/docs/execCommand/#push-down-values +void push_down_values(FlyString const& command, GC::Ref node, Optional new_value) +{ + // 1. Let command be the current command. + + // 2. If node's parent is not an Element, abort this algorithm. + if (!is(node->parent())) + return; + + // 3. If the effective command value of command is loosely equivalent to new value on node, abort this algorithm. + if (values_are_loosely_equivalent(command, effective_command_value(node, command), new_value)) + return; + + // 4. Let current ancestor be node's parent. + auto current_ancestor = GC::Ptr { node->parent() }; + + // 5. Let ancestor list be a list of nodes, initially empty. + Vector> ancestor_list; + + // 6. While current ancestor is an editable Element and the effective command value of command is not loosely + // equivalent to new value on it, append current ancestor to ancestor list, then set current ancestor to its + // parent. + while (is(current_ancestor.ptr()) && current_ancestor->is_editable() + && !values_are_loosely_equivalent(command, effective_command_value(current_ancestor, command), new_value)) { + ancestor_list.append(*current_ancestor); + current_ancestor = current_ancestor->parent(); + } + + // 7. If ancestor list is empty, abort this algorithm. + if (ancestor_list.is_empty()) + return; + + // 8. Let propagated value be the specified command value of command on the last member of ancestor list. + auto propagated_value = specified_command_value(static_cast(*ancestor_list.last()), command); + + // 9. If propagated value is null and is not equal to new value, abort this algorithm. + if (!propagated_value.has_value() && new_value.has_value()) + return; + + // 10. If the effective command value of command is not loosely equivalent to new value on the parent of the last + // member of ancestor list, and new value is not null, abort this algorithm. + if (new_value.has_value() && ancestor_list.last()->parent() + && !values_are_loosely_equivalent(command, effective_command_value(ancestor_list.last()->parent(), command), new_value)) + return; + + // 11. 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. If the specified command value of current ancestor for command is not null, set propagated value to that + // value. + // NOTE: Step 6 above guarantees that current_ancestor is an Element. + auto command_value = specified_command_value(static_cast(*current_ancestor), command); + if (command_value.has_value()) + propagated_value = command_value.value(); + + // 4. Let children be the children of current ancestor. + auto children = current_ancestor->children_as_vector(); + + // 5. If the specified command value of current ancestor for command is not null, clear the value of current + // ancestor. + if (command_value.has_value()) + clear_the_value(command, static_cast(*current_ancestor)); + + // 6. For every child in children: + for (auto const& child : children) { + // 1. If child is node, continue with the next child. + if (child.ptr() == node.ptr()) + continue; + + // 2. If child is an Element whose specified command value for command is neither null nor equivalent to + // propagated value, continue with the next child. + if (is(*child)) { + auto child_command_value = specified_command_value(static_cast(*child), command); + if (child_command_value.has_value() && child_command_value != propagated_value) + continue; + } + + // 3. If child is the last member of ancestor list, continue with the next child. + if (!ancestor_list.is_empty() && child.ptr() == ancestor_list.last().ptr()) + continue; + + // 4. Force the value of child, with command as in this algorithm and new value equal to propagated value. + force_the_value(*child, command, propagated_value); + } + } +} + // https://w3c.github.io/editing/docs/execCommand/#record-current-overrides Vector record_current_overrides(DOM::Document const& document) { @@ -2333,6 +2894,45 @@ void remove_node_preserving_its_descendants(GC::Ref node) node->remove(); } +// https://w3c.github.io/editing/docs/execCommand/#reorder-modifiable-descendants +void reorder_modifiable_descendants(GC::Ref node, FlyString const& command, Optional new_value) +{ + // 1. Let candidate equal node. + GC::Ptr candidate = node; + + // 2. While candidate is a modifiable element, and candidate has exactly one child, and that child is also a + // modifiable element, and candidate is not a simple modifiable element or candidate's specified command value + // for command is not equivalent to new value, set candidate to its child. + while (is_modifiable_element(*candidate) && candidate->child_count() == 1 + && is_modifiable_element(*candidate->first_child()) + && (!is_simple_modifiable_element(*candidate) + || specified_command_value(static_cast(*candidate), command) != new_value)) { + candidate = candidate->first_child(); + } + + // 3. If candidate is node, or is not a simple modifiable element, or its specified command value is not equivalent + // to new value, or its effective command value is not loosely equivalent to new value, abort these steps. + if (candidate == node + || !is_simple_modifiable_element(*candidate) + || specified_command_value(static_cast(*candidate), command) != new_value + || !values_are_loosely_equivalent(CommandNames::createLink, effective_command_value(candidate, command), new_value)) + return; + + // 4. While candidate has children, insert the first child of candidate into candidate's parent immediately before + // candidate, preserving ranges. + while (candidate->has_children()) + move_node_preserving_ranges(*candidate->first_child(), *candidate->parent(), candidate->index()); + + // 5. Insert candidate into node's parent immediately after node. + if (node->next_sibling()) + node->parent()->insert_before(*candidate, node->next_sibling()); + else + MUST(node->parent()->append_child(*candidate)); + + // 6. Append the node as the last child of candidate, preserving ranges. + move_node_preserving_ranges(node, *candidate, candidate->child_count()); +} + // https://w3c.github.io/editing/docs/execCommand/#restore-states-and-values void restore_states_and_values(DOM::Document& document, Vector const& overrides) { @@ -2424,23 +3024,33 @@ void restore_the_values_of_nodes(Vector const& values) { // 1. For each (node, command, value) triple in values: for (auto& recorded_node_value : values) { + auto node = recorded_node_value.node; + auto const& command = recorded_node_value.command; + auto value = recorded_node_value.specified_command_value; + // 1. Let ancestor equal node. - GC::Ptr ancestor = recorded_node_value.node; + GC::Ptr ancestor = node; // 2. If ancestor is not an Element, set it to its parent. if (!is(*ancestor)) ancestor = ancestor->parent(); // 3. While ancestor is an Element and its specified command value for command is null, set it to its parent. - auto const& command = recorded_node_value.command; while (is(ancestor.ptr()) && !specified_command_value(static_cast(*ancestor), command).has_value()) ancestor = ancestor->parent(); - // FIXME: 4. If value is null and ancestor is an Element, push down values on node for command, with new value null. + // 4. If value is null and ancestor is an Element, push down values on node for command, with new value null. + if (!value.has_value() && is(ancestor.ptr())) { + push_down_values(command, node, {}); + } - // FIXME: 5. Otherwise, if ancestor is an Element and its specified command value for command is not equivalent to + // 5. Otherwise, if ancestor is an Element and its specified command value for command is not equivalent to // value, or if ancestor is not an Element and value is not null, force the value of command to value on // node. + else if ((is(ancestor.ptr()) && specified_command_value(static_cast(*ancestor), command) != value) + || (!is(ancestor.ptr()) && value.has_value())) { + force_the_value(node, command, value); + } } } @@ -3040,6 +3650,11 @@ bool is_heading(FlyString const& local_name) HTML::TagNames::h6); } +Array named_font_sizes() +{ + return { "x-small"sv, "small"sv, "medium"sv, "large"sv, "x-large"sv, "xx-large"sv, "xxx-large"sv }; +} + Optional> property_in_style_attribute(GC::Ref element, CSS::PropertyID property_id) { auto inline_style = element->inline_style(); diff --git a/Libraries/LibWeb/Editing/Internal/Algorithms.h b/Libraries/LibWeb/Editing/Internal/Algorithms.h index c69f33aea94..4e465a4c21b 100644 --- a/Libraries/LibWeb/Editing/Internal/Algorithms.h +++ b/Libraries/LibWeb/Editing/Internal/Algorithms.h @@ -36,6 +36,7 @@ GC::Ref block_extend_a_range(GC::Ref); GC::Ptr block_node_of_node(GC::Ref); String canonical_space_sequence(u32 length, bool non_breaking_start, bool non_breaking_end); void canonicalize_whitespace(DOM::BoundaryPoint, bool fix_collapsed_space = true); +Vector> clear_the_value(FlyString const&, GC::Ref); void delete_the_selection(Selection&, bool block_merging = true, bool strip_wrappers = true, Selection::Direction direction = Selection::Direction::Forwards); GC::Ptr editing_host_of_node(GC::Ref); @@ -43,6 +44,7 @@ Optional effective_command_value(GC::Ptr, FlyString const& co 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); 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); @@ -58,10 +60,12 @@ bool is_formattable_node(GC::Ref); bool is_in_same_editing_host(GC::Ref, GC::Ref); bool is_inline_node(GC::Ref); bool is_invisible_node(GC::Ref); +bool is_modifiable_element(GC::Ref); 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_modifiable_element(GC::Ref); bool is_single_line_container(GC::Ref); bool is_visible_node(GC::Ref); bool is_whitespace_node(GC::Ref); @@ -72,6 +76,7 @@ Optional next_equivalent_point(DOM::BoundaryPoint); void normalize_sublists_in_node(GC::Ref); bool precedes_a_line_break(GC::Ref); Optional previous_equivalent_point(DOM::BoundaryPoint); +void push_down_values(FlyString const&, GC::Ref, Optional); Vector record_current_overrides(DOM::Document const&); Vector record_current_states_and_values(DOM::Document const&); Vector record_the_values_of_nodes(Vector> const&); @@ -79,6 +84,7 @@ void remove_extraneous_line_breaks_at_the_end_of_node(GC::Ref); void remove_extraneous_line_breaks_before_node(GC::Ref); void remove_extraneous_line_breaks_from_a_node(GC::Ref); void remove_node_preserving_its_descendants(GC::Ref); +void reorder_modifiable_descendants(GC::Ref, FlyString const&, Optional); void restore_states_and_values(DOM::Document&, Vector const&); void restore_the_values_of_nodes(Vector const&); GC::Ref set_the_tag_name(GC::Ref, FlyString const&); @@ -95,6 +101,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&); +Array named_font_sizes(); Optional> property_in_style_attribute(GC::Ref, CSS::PropertyID); Optional resolved_display(GC::Ref); Optional resolved_keyword(GC::Ref, CSS::PropertyID);