@@ -615,6 +615,260 @@ bool command_fore_color_action(DOM::Document& document, String const& value)
615
615
return true ;
616
616
}
617
617
618
+ // https://w3c.github.io/editing/docs/execCommand/#the-formatblock-command
619
+ bool command_format_block_action (DOM::Document& document, String const & value)
620
+ {
621
+ // 1. If value begins with a "<" character and ends with a ">" character, remove the first and last characters from
622
+ // it.
623
+ auto resulting_value = value;
624
+ if (resulting_value.starts_with_bytes (" <" sv) && resulting_value.ends_with_bytes (" >" sv))
625
+ resulting_value = MUST (resulting_value.substring_from_byte_offset (1 , resulting_value.bytes_as_string_view ().length () - 2 ));
626
+
627
+ // 2. Let value be converted to ASCII lowercase.
628
+ resulting_value = resulting_value.to_ascii_lowercase ();
629
+
630
+ // 3. If value is not a formattable block name, return false.
631
+ if (!is_formattable_block_name (resulting_value))
632
+ return false ;
633
+
634
+ // 4. Block-extend the active range, and let new range be the result.
635
+ auto new_range = block_extend_a_range (*active_range (document));
636
+
637
+ // 5. Let node list be an empty list of nodes.
638
+ Vector<GC::Ref<DOM::Node>> node_list;
639
+
640
+ // 6. For each node node contained in new range, append node to node list if it is editable, the last member of
641
+ // original node list (if any) is not an ancestor of node, node is either a non-list single-line container or an
642
+ // allowed child of "p" or a dd or dt, and node is not the ancestor of a prohibited paragraph child.
643
+ auto is_ancestor_of_prohibited_paragraph_child = [](GC::Ref<DOM::Node> node) {
644
+ bool result = false ;
645
+ node->for_each_in_subtree ([&result](GC::Ref<DOM::Node> descendant) {
646
+ if (is_prohibited_paragraph_child (descendant)) {
647
+ result = true ;
648
+ return TraversalDecision::Break;
649
+ }
650
+ return TraversalDecision::Continue;
651
+ });
652
+ return result;
653
+ };
654
+ new_range->for_each_contained ([&](GC::Ref<DOM::Node> node) {
655
+ if (node->is_editable ()
656
+ && (node_list.is_empty () || !node_list.last ()->is_ancestor_of (node))
657
+ && (is_non_list_single_line_container (node) || is_allowed_child_of_node (node, HTML::TagNames::p)
658
+ || (is<DOM::Element>(*node)
659
+ && static_cast <DOM::Element&>(*node).local_name ().is_one_of (HTML::TagNames::dd, HTML::TagNames::dt)))
660
+ && !is_ancestor_of_prohibited_paragraph_child (node)) {
661
+ node_list.append (node);
662
+ }
663
+ return IterationDecision::Continue;
664
+ });
665
+
666
+ // 7. Record the values of node list, and let values be the result.
667
+ auto values = record_the_values_of_nodes (node_list);
668
+
669
+ // 8. For each node in node list, while node is the descendant of an editable HTML element in the same editing host,
670
+ // whose local name is a formattable block name, and which is not the ancestor of a prohibited paragraph child,
671
+ // split the parent of the one-node list consisting of node.
672
+ for (auto node : node_list) {
673
+ while (true ) {
674
+ bool is_matching_descendant = false ;
675
+ node->for_each_ancestor ([&](GC::Ref<DOM::Node> ancestor) {
676
+ if (ancestor->is_editable () && is<HTML::HTMLElement>(*ancestor) && is_in_same_editing_host (node, ancestor)
677
+ && is_formattable_block_name (static_cast <DOM::Element&>(*ancestor).local_name ())
678
+ && !is_ancestor_of_prohibited_paragraph_child (ancestor)) {
679
+ is_matching_descendant = true ;
680
+ return IterationDecision::Break;
681
+ }
682
+ return IterationDecision::Continue;
683
+ });
684
+ if (!is_matching_descendant)
685
+ break ;
686
+
687
+ split_the_parent_of_nodes ({ node });
688
+ }
689
+ }
690
+
691
+ // 9. Restore the values from values.
692
+ restore_the_values_of_nodes (values);
693
+
694
+ // 10. While node list is not empty:
695
+ while (!node_list.is_empty ()) {
696
+ Vector<GC::Ref<DOM::Node>> sublist;
697
+
698
+ // 1. If the first member of node list is a single-line container:
699
+ if (is_single_line_container (node_list.first ())) {
700
+ // AD-HOC: The spec makes note of single-line containers without children, and how they should probably
701
+ // disappear given that Firefox and Opera did this at the time. We're going to follow their lead and
702
+ // remove the node if it has no children.
703
+ if (!node_list.first ()->has_children ()) {
704
+ node_list.take_first ()->remove ();
705
+ continue ;
706
+ }
707
+
708
+ // 1. Let sublist be the children of the first member of node list.
709
+ node_list.first ()->for_each_child ([&sublist](GC::Ref<DOM::Node> child) {
710
+ sublist.append (child);
711
+ return IterationDecision::Continue;
712
+ });
713
+
714
+ // 2. Record the values of sublist, and let values be the result.
715
+ auto values = record_the_values_of_nodes (sublist);
716
+
717
+ // 3. Remove the first member of node list from its parent, preserving its descendants.
718
+ remove_node_preserving_its_descendants (node_list.first ());
719
+
720
+ // 4. Restore the values from values.
721
+ restore_the_values_of_nodes (values);
722
+
723
+ // 5. Remove the first member from node list.
724
+ node_list.take_first ();
725
+ }
726
+
727
+ // 2. Otherwise:
728
+ else {
729
+ // 1. Let sublist be an empty list of nodes.
730
+ // 2. Remove the first member of node list and append it to sublist.
731
+ sublist.append (node_list.take_first ());
732
+
733
+ // 3. While node list is not empty, and the first member of node list is the nextSibling of the last member
734
+ // of sublist, and the first member of node list is not a single-line container, and the last member of
735
+ // sublist is not a br, remove the first member of node list and append it to sublist.
736
+ while (!node_list.is_empty () && node_list.first ().ptr () == sublist.last ()->next_sibling ()
737
+ && !is_single_line_container (node_list.first ()) && !is<HTML::HTMLBRElement>(*sublist.last ()))
738
+ sublist.append (node_list.take_first ());
739
+ }
740
+
741
+ // 3. Wrap sublist. If value is "div" or "p", sibling criteria returns false; otherwise it returns true for an
742
+ // HTML element with local name value and no attributes, and false otherwise. New parent instructions return
743
+ // the result of running createElement(value) on the context object. Then fix disallowed ancestors of the
744
+ // result.
745
+ auto result = wrap (
746
+ sublist,
747
+ [&](GC::Ref<DOM::Node> sibling) {
748
+ if (resulting_value.is_one_of (" div" sv, " p" sv))
749
+ return false ;
750
+ return is<HTML::HTMLElement>(*sibling)
751
+ && static_cast <DOM::Element&>(*sibling).local_name () == resulting_value
752
+ && !static_cast <DOM::Element&>(*sibling).has_attributes ();
753
+ },
754
+ [&] { return MUST (DOM::create_element (document, resulting_value, Namespace::HTML)); });
755
+ if (result)
756
+ fix_disallowed_ancestors_of_node (*result);
757
+ }
758
+
759
+ // 11. Return true.
760
+ return true ;
761
+ }
762
+
763
+ // https://w3c.github.io/editing/docs/execCommand/#the-formatblock-command
764
+ bool command_format_block_indeterminate (DOM::Document const & document)
765
+ {
766
+ // 1. If the active range is null, return the empty string.
767
+ // AD-HOC: We're returning false instead. See https://github.com/w3c/editing/issues/474
768
+ auto range = active_range (document);
769
+ if (!range)
770
+ return false ;
771
+
772
+ // 2. Block-extend the active range, and let new range be the result.
773
+ auto new_range = block_extend_a_range (*range);
774
+
775
+ // 3. Let node list be all visible editable nodes that are contained in new range and have no children.
776
+ Vector<GC::Ref<DOM::Node>> node_list;
777
+ new_range->for_each_contained ([&](GC::Ref<DOM::Node> node) {
778
+ if (is_visible_node (node) && node->is_editable () && !node->has_children ())
779
+ node_list.append (node);
780
+ return IterationDecision::Continue;
781
+ });
782
+
783
+ // 4. If node list is empty, return false.
784
+ if (node_list.is_empty ())
785
+ return false ;
786
+
787
+ // 5. Let type be null.
788
+ Optional<FlyString const &> type;
789
+
790
+ // 6. For each node in node list:
791
+ for (auto node : node_list) {
792
+ // 1. While node's parent is editable and in the same editing host as node, and node is not an HTML element
793
+ // whose local name is a formattable block name, set node to its parent.
794
+ while (node->parent () && node->parent ()->is_editable () && is_in_same_editing_host (node, *node->parent ())
795
+ && !(is<HTML::HTMLElement>(*node) && is_formattable_block_name (static_cast <DOM::Element&>(*node).local_name ())))
796
+ node = *node->parent ();
797
+
798
+ // 2. Let current type be the empty string.
799
+ FlyString current_type;
800
+
801
+ // 3. If node is an editable HTML element whose local name is a formattable block name, and node is not the
802
+ // ancestor of a prohibited paragraph child, set current type to node's local name.
803
+ if (node->is_editable () && is<HTML::HTMLElement>(*node)
804
+ && is_formattable_block_name (static_cast <DOM::Element&>(*node).local_name ()))
805
+ current_type = static_cast <DOM::Element&>(*node).local_name ();
806
+
807
+ // 4. If type is null, set type to current type.
808
+ if (!type.has_value ()) {
809
+ type = current_type;
810
+ }
811
+
812
+ // 5. Otherwise, if type does not equal current type, return true.
813
+ else if (type.value () != current_type) {
814
+ return true ;
815
+ }
816
+ }
817
+
818
+ // 7. Return false.
819
+ return false ;
820
+ }
821
+
822
+ // https://w3c.github.io/editing/docs/execCommand/#the-formatblock-command
823
+ String command_format_block_value (DOM::Document const & document)
824
+ {
825
+ // 1. If the active range is null, return the empty string.
826
+ auto range = active_range (document);
827
+ if (!range)
828
+ return {};
829
+
830
+ // 2. Block-extend the active range, and let new range be the result.
831
+ auto new_range = block_extend_a_range (*range);
832
+
833
+ // 3. Let node be the first visible editable node that is contained in new range and has no children. If there is no
834
+ // such node, return the empty string.
835
+ GC::Ptr<DOM::Node> node;
836
+ new_range->for_each_contained ([&](GC::Ref<DOM::Node> contained_node) {
837
+ if (is_visible_node (contained_node) && contained_node->is_editable () && !contained_node->has_children ()) {
838
+ node = contained_node;
839
+ return IterationDecision::Break;
840
+ }
841
+ return IterationDecision::Continue;
842
+ });
843
+ if (!node)
844
+ return {};
845
+
846
+ // 4. While node's parent is editable and in the same editing host as node, and node is not an HTML element whose
847
+ // local name is a formattable block name, set node to its parent.
848
+ while (node->parent () && node->parent ()->is_editable () && is_in_same_editing_host (*node, *node->parent ())
849
+ && !(is<HTML::HTMLElement>(*node) && is_formattable_block_name (static_cast <DOM::Element&>(*node).local_name ())))
850
+ node = node->parent ();
851
+
852
+ // 5. If node is an editable HTML element whose local name is a formattable block name, and node is not the ancestor
853
+ // of a prohibited paragraph child, return node's local name, converted to ASCII lowercase.
854
+ if (node->is_editable () && is<HTML::HTMLElement>(*node)
855
+ && is_formattable_block_name (static_cast <DOM::Element&>(*node).local_name ())) {
856
+ bool is_ancestor_of_prohibited_paragraph_child = false ;
857
+ node->for_each_in_subtree ([&is_ancestor_of_prohibited_paragraph_child](GC::Ref<DOM::Node> descendant) {
858
+ if (is_prohibited_paragraph_child (descendant)) {
859
+ is_ancestor_of_prohibited_paragraph_child = true ;
860
+ return TraversalDecision::Break;
861
+ }
862
+ return TraversalDecision::Continue;
863
+ });
864
+ if (!is_ancestor_of_prohibited_paragraph_child)
865
+ return static_cast <DOM::Element&>(*node).local_name ().to_string ().to_ascii_lowercase ();
866
+ }
867
+
868
+ // 6. Return the empty string.
869
+ return {};
870
+ }
871
+
618
872
// https://w3c.github.io/editing/docs/execCommand/#the-forwarddelete-command
619
873
bool command_forward_delete_action (DOM::Document& document, String const &)
620
874
{
@@ -1524,6 +1778,14 @@ static Array const commands {
1524
1778
.action = command_fore_color_action,
1525
1779
.relevant_css_property = CSS::PropertyID::Color,
1526
1780
},
1781
+ // https://w3c.github.io/editing/docs/execCommand/#the-formatblock-command
1782
+ CommandDefinition {
1783
+ .command = CommandNames::formatBlock,
1784
+ .action = command_format_block_action,
1785
+ .indeterminate = command_format_block_indeterminate,
1786
+ .value = command_format_block_value,
1787
+ .preserves_overrides = true ,
1788
+ },
1527
1789
// https://w3c.github.io/editing/docs/execCommand/#the-forwarddelete-command
1528
1790
CommandDefinition {
1529
1791
.command = CommandNames::forwardDelete,
0 commit comments