diff --git a/Libraries/LibWeb/Editing/Commands.h b/Libraries/LibWeb/Editing/Commands.h index cd4ea394ea6..7ad268aaea5 100644 --- a/Libraries/LibWeb/Editing/Commands.h +++ b/Libraries/LibWeb/Editing/Commands.h @@ -10,12 +10,17 @@ namespace Web::Editing { +// https://w3c.github.io/editing/docs/execCommand/#properties-of-commands struct CommandDefinition { FlyString const& command; Function action {}; Function indeterminate {}; Function state {}; Function value {}; + Optional relevant_css_property {}; + + // https://w3c.github.io/editing/docs/execCommand/#inline-command-activated-values + Vector inline_activated_values {}; }; Optional find_command_definition(FlyString const&); diff --git a/Libraries/LibWeb/Editing/Internal/Algorithms.cpp b/Libraries/LibWeb/Editing/Internal/Algorithms.cpp index 30107ea62ae..f9bf6b3ea6b 100644 --- a/Libraries/LibWeb/Editing/Internal/Algorithms.cpp +++ b/Libraries/LibWeb/Editing/Internal/Algorithms.cpp @@ -5,6 +5,7 @@ */ #include +#include #include #include #include @@ -16,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -901,6 +903,148 @@ GC::Ptr editing_host_of_node(GC::Ref node) return {}; } +// https://w3c.github.io/editing/docs/execCommand/#effective-command-value +Optional effective_command_value(GC::Ptr node, FlyString const& command) +{ + VERIFY(node); + + // 1. If neither node nor its parent is an Element, return null. + // 2. If node is not an Element, return the effective command value of its parent for command. + if (!is(*node)) { + if (!node->parent() || !is(*node->parent())) + return {}; + return effective_command_value(node->parent(), command); + } + + // 3. If command is "createLink" or "unlink": + auto node_as_element = [&] -> GC::Ref { return static_cast(*node); }; + if (command.is_one_of(CommandNames::createLink, CommandNames::unlink)) { + // 1. While node is not null, and is not an a element that has an href attribute, set node to its parent. + while (node && !(is(*node) && node_as_element()->has_attribute(HTML::AttributeNames::href))) + node = node->parent(); + + // 2. If node is null, return null. + if (!node) + return {}; + + // 3. Return the value of node's href attribute. + return node_as_element()->get_attribute_value(HTML::AttributeNames::href); + } + + // 4. If command is "backColor" or "hiliteColor": + if (command.is_one_of(CommandNames::backColor, CommandNames::hiliteColor)) { + // 1. While the resolved value of "background-color" on node is any fully transparent value, and node's parent + // is an Element, set node to its parent. + auto resolved_background_color = [&] { return resolved_value(*node, CSS::PropertyID::BackgroundColor); }; + auto resolved_background_alpha = [&] { + auto background_color = resolved_background_color(); + if (!background_color.has_value()) + return NumericLimits::max(); + VERIFY(is(node->layout_node())); + return background_color.value()->to_color(*static_cast(node->layout_node())).alpha(); + }; + while (resolved_background_alpha() == 0 && node->parent() && is(*node->parent())) + node = node->parent(); + + // 2. Return the resolved value of "background-color" for node. + auto resolved_value = resolved_background_color(); + if (!resolved_value.has_value()) + return {}; + return resolved_value.value()->to_string(CSS::CSSStyleValue::SerializationMode::ResolvedValue); + } + + // 5. If command is "subscript" or "superscript": + if (command.is_one_of(CommandNames::subscript, CommandNames::superscript)) { + // 1. Let affected by subscript and affected by superscript be two boolean variables, both initially false. + bool affected_by_subscript = false; + bool affected_by_superscript = false; + + // 2. While node is an inline node: + while (node && is_inline_node(*node)) { + // 1. If node is a sub, set affected by subscript to true. + if (is(*node) && node_as_element()->local_name() == HTML::TagNames::sub) { + affected_by_subscript = true; + } + + // 2. Otherwise, if node is a sup, set affected by superscript to true. + else if (is(*node) && node_as_element()->local_name() == HTML::TagNames::sup) { + affected_by_superscript = true; + } + + // 3. Set node to its parent. + node = node->parent(); + } + + // 3. If affected by subscript and affected by superscript are both true, return the string "mixed". + if (affected_by_subscript && affected_by_superscript) + return "mixed"_string; + + // 4. If affected by subscript is true, return "subscript". + if (affected_by_subscript) + return "subscript"_string; + + // 5. If affected by superscript is true, return "superscript". + if (affected_by_superscript) + return "superscript"_string; + + // 6. Return null. + return {}; + } + + // 6. If command is "strikethrough", and the "text-decoration" property of node or any of its ancestors has resolved + // value containing "line-through", return "line-through". Otherwise, return null. + if (command == CommandNames::strikethrough) { + auto inclusive_ancestor = node; + do { + auto text_decoration_line = resolved_value(*node, CSS::PropertyID::TextDecorationLine); + if (text_decoration_line.has_value() && text_decoration_line.value()->is_value_list()) { + auto const& line_value_list = text_decoration_line.value()->as_value_list(); + auto has_line_through = line_value_list.values().find_first_index_if([](CSS::CSSStyleValue const& value) { + return value.is_keyword() && value.as_keyword().keyword() == CSS::Keyword::LineThrough; + }); + if (has_line_through.has_value()) + return "line-through"_string; + } + inclusive_ancestor = inclusive_ancestor->parent(); + } while (inclusive_ancestor); + + return {}; + } + + // 7. If command is "underline", and the "text-decoration" property of node or any of its ancestors has resolved + // value containing "underline", return "underline". Otherwise, return null. + if (command == CommandNames::underline) { + auto inclusive_ancestor = node; + do { + auto text_decoration_line = resolved_value(*node, CSS::PropertyID::TextDecorationLine); + if (text_decoration_line.has_value() && text_decoration_line.value()->is_value_list()) { + auto const& line_value_list = text_decoration_line.value()->as_value_list(); + auto has_line_through = line_value_list.values().find_first_index_if([](CSS::CSSStyleValue const& value) { + return value.is_keyword() && value.as_keyword().keyword() == CSS::Keyword::Underline; + }); + if (has_line_through.has_value()) + return "line-through"_string; + } + inclusive_ancestor = inclusive_ancestor->parent(); + } while (inclusive_ancestor); + + return {}; + } + + // 8. Return the resolved value for node of the relevant CSS property for command. + auto optional_command_definition = find_command_definition(command); + // FIXME: change this to VERIFY(command_definition.has_value()) once all command definitions are in place. + if (!optional_command_definition.has_value()) + return {}; + auto const& command_definition = optional_command_definition.release_value(); + VERIFY(command_definition.relevant_css_property.has_value()); + + auto optional_value = resolved_value(*node, command_definition.relevant_css_property.value()); + if (!optional_value.has_value()) + return {}; + return optional_value.value()->to_string(CSS::CSSStyleValue::SerializationMode::ResolvedValue); +} + // https://w3c.github.io/editing/docs/execCommand/#first-equivalent-point BoundaryPoint first_equivalent_point(BoundaryPoint boundary_point) { @@ -1421,6 +1565,48 @@ bool is_collapsed_whitespace_node(GC::Ref node) return false; } +// https://w3c.github.io/editing/docs/execCommand/#effectively-contained +bool is_effectively_contained_in_range(GC::Ref node, GC::Ref range) +{ + // A node node is effectively contained in a range range if range is not collapsed, and at least one of the + // following holds: + if (range->collapsed()) + return false; + + // * node is contained in range. + if (range->contains_node(node)) + return true; + + // * node is range's start node, it is a Text node, and its length is different from range's start offset. + if (node == range->start_container() && is(*node) && node->length() != range->start_offset()) + return true; + + // * node is range's end node, it is a Text node, and range's end offset is not 0. + if (node == range->end_container() && is(*node) && range->end_offset() != 0) + return true; + + // * node has at least one child; and all its children are effectively contained in range; + if (!node->has_children()) + return false; + for (auto* child = node->first_child(); child; child = child->next_sibling()) { + if (!is_effectively_contained_in_range(*child, range)) + return false; + } + + // and either range's start node is not a descendant of node or is not a Text node or range's start offset is zero; + auto start_node = range->start_container(); + if (start_node->is_descendant_of(node) && is(*start_node) && range->start_offset() != 0) + return false; + + // and either range's end node is not a descendant of node or is not a Text node or range's end offset is its end + // node's length. + auto end_node = range->end_container(); + if (end_node->is_descendant_of(node) && is(*end_node) && range->end_offset() != end_node->length()) + return false; + + return true; +} + // https://w3c.github.io/editing/docs/execCommand/#element-with-inline-contents bool is_element_with_inline_contents(GC::Ref node) { @@ -1447,6 +1633,13 @@ bool is_extraneous_line_break(GC::Ref node) return false; } +// https://w3c.github.io/editing/docs/execCommand/#formattable-node +bool is_formattable_node(GC::Ref node) +{ + // A formattable node is an editable visible node that is either a Text node, an img, or a br. + return is(*node) || is(*node) || is(*node); +} + // https://w3c.github.io/editing/docs/execCommand/#in-the-same-editing-host bool is_in_same_editing_host(GC::Ref node_a, GC::Ref node_b) { @@ -1873,25 +2066,55 @@ Optional previous_equivalent_point(BoundaryPoint boundary_point) } // https://w3c.github.io/editing/docs/execCommand/#record-current-states-and-values -Vector record_current_states_and_values(GC::Ref) +Vector record_current_states_and_values(GC::Ref active_range) { // 1. Let overrides be a list of (string, string or boolean) ordered pairs, initially empty. Vector overrides; - // FIXME: 2. Let node be the first formattable node effectively contained in the active range, or null if there is none. + // 2. Let node be the first formattable node effectively contained in the active range, or null if there is none. + GC::Ptr node; + auto common_ancestor = active_range->common_ancestor_container(); + common_ancestor->for_each_in_subtree([&](GC::Ref descendant) { + if (is_formattable_node(descendant) && is_effectively_contained_in_range(descendant, active_range)) { + node = descendant; + return TraversalDecision::Break; + } + return TraversalDecision::Continue; + }); - // FIXME: 3. If node is null, return overrides. + // 3. If node is null, return overrides. + if (!node) + return overrides; - // FIXME: 4. Add ("createLink", node's effective command value for "createLink") to overrides. + // 4. Add ("createLink", node's effective command value for "createLink") to overrides. + auto effective_value = effective_command_value(node, CommandNames::createLink); + if (effective_value.has_value()) + overrides.empend(CommandNames::createLink, effective_value.release_value()); - // FIXME: 5. For each command in the list "bold", "italic", "strikethrough", "subscript", "superscript", "underline", in + // 5. For each command in the list "bold", "italic", "strikethrough", "subscript", "superscript", "underline", in // order: if node's effective command value for command is one of its inline command activated values, add // (command, true) to overrides, and otherwise add (command, false) to overrides. + for (auto const& command : { CommandNames::bold, CommandNames::italic, CommandNames::strikethrough, + CommandNames::subscript, CommandNames::superscript, CommandNames::underline }) { + auto command_definition = find_command_definition(command); + // FIXME: change this to VERIFY(command_definition.has_value()) once all command definitions are in place. + if (!command_definition.has_value()) + continue; - // FIXME: 6. For each command in the list "fontName", "foreColor", "hiliteColor", in order: add (command, command's value) + effective_value = effective_command_value(node, command); + auto& inline_activated_values = command_definition.value().inline_activated_values; + overrides.empend(command, effective_value.has_value() && inline_activated_values.contains_slow(*effective_value)); + } + + // 6. For each command in the list "fontName", "foreColor", "hiliteColor", in order: add (command, command's value) // to overrides. + for (auto const& command : { CommandNames::fontName, CommandNames::foreColor, CommandNames::hiliteColor }) + overrides.empend(command, node->document().query_command_value(command)); - // FIXME: 7. Add ("fontSize", node's effective command value for "fontSize") to overrides. + // 7. Add ("fontSize", node's effective command value for "fontSize") to overrides. + effective_value = effective_command_value(node, CommandNames::fontSize); + if (effective_value.has_value()) + overrides.empend(CommandNames::fontSize, effective_value.release_value()); // 8. Return overrides. return overrides; @@ -2124,14 +2347,14 @@ Optional specified_command_value(GC::Ref element, FlyStrin { // 1. If command is "backColor" or "hiliteColor" and the Element's display property does not have resolved value // "inline", return null. - if (command == CommandNames::backColor || command == CommandNames::hiliteColor) { + if (command.is_one_of(CommandNames::backColor, CommandNames::hiliteColor)) { auto display = resolved_display(element); if (!display.has_value() || !display->is_inline_outside() || !display->is_flow_inside()) return {}; } // 2. If command is "createLink" or "unlink": - if (command == CommandNames::createLink || command == CommandNames::unlink) { + if (command.is_one_of(CommandNames::createLink, CommandNames::unlink)) { // 1. If element is an a element and has an href attribute, return the value of that attribute. auto href_attribute = element->get_attribute(HTML::AttributeNames::href); if (href_attribute.has_value()) @@ -2142,7 +2365,7 @@ Optional specified_command_value(GC::Ref element, FlyStrin } // 3. If command is "subscript" or "superscript": - if (command == CommandNames::subscript || command == CommandNames::superscript) { + if (command.is_one_of(CommandNames::subscript, CommandNames::superscript)) { // 1. If element is a sup, return "superscript". if (element->local_name() == HTML::TagNames::sup) return "superscript"_string; @@ -2164,7 +2387,7 @@ Optional specified_command_value(GC::Ref element, FlyStrin } // 5. If command is "strikethrough" and element is an s or strike element, return "line-through". - if (command == CommandNames::strikethrough && (element->local_name() == HTML::TagNames::s || element->local_name() == HTML::TagNames::strike)) + if (command == CommandNames::strikethrough && element->local_name().is_one_of(HTML::TagNames::s, HTML::TagNames::strike)) return "line-through"_string; // FIXME: 6. If command is "underline", and element has a style attribute set, and that attribute sets "text-decoration": @@ -2179,9 +2402,16 @@ Optional specified_command_value(GC::Ref element, FlyStrin if (command == CommandNames::underline && element->local_name() == HTML::TagNames::u) return "underline"_string; - // FIXME: 8. Let property be the relevant CSS property for command. + // 8. Let property be the relevant CSS property for command. + auto command_definition = find_command_definition(command); + // FIXME: change this to VERIFY(command_definition.has_value()) once all command definitions are in place. + if (!command_definition.has_value()) + return {}; + auto property = command_definition.value().relevant_css_property; - // FIXME: 9. If property is null, return null. + // 9. If property is null, return null. + if (!property.has_value()) + return {}; // FIXME: 10. If element has a style attribute set, and that attribute has the effect of setting property, return the value // that it sets property to. @@ -2190,10 +2420,14 @@ Optional specified_command_value(GC::Ref element, FlyStrin // property, return the value that the hint sets property to. (For a size of 7, this will be the non-CSS value // "xxx-large".) - // FIXME: 12. If element is in the following list, and property is equal to the CSS property name listed for it, return the + // 12. If element is in the following list, and property is equal to the CSS property name listed for it, return the // string listed for it. // * b, strong: font-weight: "bold" // * i, em: font-style: "italic" + if (element->local_name().is_one_of(HTML::TagNames::b, HTML::TagNames::strong) && *property == CSS::PropertyID::FontWeight) + return "bold"_string; + if (element->local_name().is_one_of(HTML::TagNames::i, HTML::TagNames::em) && *property == CSS::PropertyID::FontStyle) + return "italic"_string; // 13. Return null. return {}; diff --git a/Libraries/LibWeb/Editing/Internal/Algorithms.h b/Libraries/LibWeb/Editing/Internal/Algorithms.h index 6bc28a7bfce..f3fec5d8321 100644 --- a/Libraries/LibWeb/Editing/Internal/Algorithms.h +++ b/Libraries/LibWeb/Editing/Internal/Algorithms.h @@ -15,13 +15,13 @@ namespace Web::Editing { // https://w3c.github.io/editing/docs/execCommand/#record-the-values struct RecordedNodeValue { GC::Ref node; - FlyString const& command; + FlyString command; Optional specified_command_value; }; // https://w3c.github.io/editing/docs/execCommand/#record-current-states-and-values struct RecordedOverride { - FlyString const& command; + FlyString command; Variant value; }; @@ -44,6 +44,7 @@ void canonicalize_whitespace(GC::Ref, u32 offset, bool fix_collapsed_ 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); +Optional effective_command_value(GC::Ptr, FlyString const& command); BoundaryPoint first_equivalent_point(BoundaryPoint); void fix_disallowed_ancestors_of_node(GC::Ref); bool follows_a_line_break(GC::Ref); @@ -55,8 +56,10 @@ bool is_block_start_point(GC::Ref, u32 offset); bool is_collapsed_block_prop(GC::Ref); bool is_collapsed_line_break(GC::Ref); bool is_collapsed_whitespace_node(GC::Ref); +bool is_effectively_contained_in_range(GC::Ref, GC::Ref); 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_inline_node(GC::Ref); bool is_invisible_node(GC::Ref);