script/dom/execcommand/
contenteditable.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::cmp::Ordering;
6use std::ops::Deref;
7
8use html5ever::local_name;
9use script_bindings::inheritance::Castable;
10use style::computed_values::white_space_collapse::T as WhiteSpaceCollapse;
11use style::values::specified::box_::DisplayOutside;
12
13use crate::dom::abstractrange::bp_position;
14use crate::dom::bindings::cell::Ref;
15use crate::dom::bindings::codegen::Bindings::CharacterDataBinding::CharacterDataMethods;
16use crate::dom::bindings::codegen::Bindings::DocumentBinding::{
17    DocumentMethods, ElementCreationOptions,
18};
19use crate::dom::bindings::codegen::Bindings::HTMLElementBinding::HTMLElementMethods;
20use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods;
21use crate::dom::bindings::codegen::Bindings::SelectionBinding::SelectionMethods;
22use crate::dom::bindings::codegen::UnionTypes::StringOrElementCreationOptions;
23use crate::dom::bindings::inheritance::{ElementTypeId, HTMLElementTypeId, NodeTypeId};
24use crate::dom::bindings::root::{DomRoot, DomSlice};
25use crate::dom::bindings::str::DOMString;
26use crate::dom::characterdata::CharacterData;
27use crate::dom::document::Document;
28use crate::dom::element::Element;
29use crate::dom::execcommand::basecommand::CommandName;
30use crate::dom::html::htmlanchorelement::HTMLAnchorElement;
31use crate::dom::html::htmlbrelement::HTMLBRElement;
32use crate::dom::html::htmlelement::HTMLElement;
33use crate::dom::html::htmlimageelement::HTMLImageElement;
34use crate::dom::html::htmllielement::HTMLLIElement;
35use crate::dom::html::htmltablecellelement::HTMLTableCellElement;
36use crate::dom::html::htmltablerowelement::HTMLTableRowElement;
37use crate::dom::html::htmltablesectionelement::HTMLTableSectionElement;
38use crate::dom::node::{Node, NodeTraits, ShadowIncluding};
39use crate::dom::range::Range;
40use crate::dom::selection::Selection;
41use crate::dom::text::Text;
42use crate::script_runtime::CanGc;
43
44impl Text {
45    /// <https://dom.spec.whatwg.org/#concept-cd-data>
46    fn data(&self) -> Ref<'_, String> {
47        self.upcast::<CharacterData>().data()
48    }
49
50    /// <https://w3c.github.io/editing/docs/execCommand/#whitespace-node>
51    fn is_whitespace_node(&self) -> bool {
52        // > A whitespace node is either a Text node whose data is the empty string;
53        let data = self.data();
54        if data.is_empty() {
55            return true;
56        }
57        // > or a Text node whose data consists only of one or more tabs (0x0009), line feeds (0x000A),
58        // > carriage returns (0x000D), and/or spaces (0x0020),
59        // > and whose parent is an Element whose resolved value for "white-space" is "normal" or "nowrap";
60        let Some(parent) = self.upcast::<Node>().GetParentElement() else {
61            return false;
62        };
63        // TODO: Optimize the below to only do a traversal once and in the match handle the expected collapse value
64        let Some(style) = parent.style() else {
65            return false;
66        };
67        let white_space_collapse = style.get_inherited_text().white_space_collapse;
68        if data
69            .bytes()
70            .all(|byte| matches!(byte, b'\t' | b'\n' | b'\r' | b' ')) &&
71            // Note that for "normal" and "nowrap", the longhand "white-space-collapse: collapse" applies
72            // https://www.w3.org/TR/css-text-4/#white-space-property
73            white_space_collapse == WhiteSpaceCollapse::Collapse
74        {
75            return true;
76        }
77        // > or a Text node whose data consists only of one or more tabs (0x0009), carriage returns (0x000D),
78        // > and/or spaces (0x0020), and whose parent is an Element whose resolved value for "white-space" is "pre-line".
79        data.bytes()
80            .all(|byte| matches!(byte, b'\t' | b'\r' | b' ')) &&
81            // Note that for "pre-line", the longhand "white-space-collapse: preserve-breaks" applies
82            // https://www.w3.org/TR/css-text-4/#white-space-property
83            white_space_collapse == WhiteSpaceCollapse::PreserveBreaks
84    }
85
86    /// <https://w3c.github.io/editing/docs/execCommand/#collapsed-whitespace-node>
87    fn is_collapsed_whitespace_node(&self) -> bool {
88        // Step 1. If node is not a whitespace node, return false.
89        if !self.is_whitespace_node() {
90            return false;
91        }
92        // Step 2. If node's data is the empty string, return true.
93        if self.data().is_empty() {
94            return true;
95        }
96        // Step 3. Let ancestor be node's parent.
97        let node = self.upcast::<Node>();
98        let Some(ancestor) = node.GetParentNode() else {
99            // Step 4. If ancestor is null, return true.
100            return true;
101        };
102        let mut resolved_ancestor = ancestor.clone();
103        for parent in ancestor.ancestors() {
104            // Step 5. If the "display" property of some ancestor of node has resolved value "none", return true.
105            if parent.is_display_none() {
106                return true;
107            }
108            // Step 6. While ancestor is not a block node and its parent is not null, set ancestor to its parent.
109            //
110            // Note that the spec is written as "while not". Since this is the end-condition, we need to invert
111            // the condition to decide when to stop.
112            if parent.is_block_node() {
113                break;
114            }
115            resolved_ancestor = parent;
116        }
117        // Step 7. Let reference be node.
118        // Step 8. While reference is a descendant of ancestor:
119        // Step 8.1. Let reference be the node before it in tree order.
120        for reference in node.preceding_nodes(&resolved_ancestor) {
121            // Step 8.2. If reference is a block node or a br, return true.
122            if reference.is_block_node() || reference.is::<HTMLBRElement>() {
123                return true;
124            }
125            // Step 8.3. If reference is a Text node that is not a whitespace node, or is an img, break from this loop.
126            if reference
127                .downcast::<Text>()
128                .is_some_and(|text| !text.is_whitespace_node()) ||
129                reference.is::<HTMLImageElement>()
130            {
131                break;
132            }
133        }
134        // Step 9. Let reference be node.
135        // Step 10. While reference is a descendant of ancestor:
136        // Step 10.1. Let reference be the node after it in tree order, or null if there is no such node.
137        for reference in node.following_nodes(&resolved_ancestor) {
138            // Step 10.2. If reference is a block node or a br, return true.
139            if reference.is_block_node() || reference.is::<HTMLBRElement>() {
140                return true;
141            }
142            // Step 10.3. If reference is a Text node that is not a whitespace node, or is an img, break from this loop.
143            if reference
144                .downcast::<Text>()
145                .is_some_and(|text| !text.is_whitespace_node()) ||
146                reference.is::<HTMLImageElement>()
147            {
148                break;
149            }
150        }
151        // Step 11. Return false.
152        false
153    }
154
155    /// Part of <https://w3c.github.io/editing/docs/execCommand/#canonicalize-whitespace>
156    /// and deduplicated here, since we need to do this for both start and end nodes
157    fn has_whitespace_and_has_parent_with_whitespace_preserve(
158        &self,
159        offset: u32,
160        space_characters: &'static [&'static char],
161    ) -> bool {
162        // if node is a Text node and its parent's resolved value for "white-space" is neither "pre" nor "pre-wrap"
163        // and start offset is not zero and the (start offset − 1)st code unit of start node's data is a space (0x0020) or
164        // non-breaking space (0x00A0)
165        let has_preserve_space = self
166            .upcast::<Node>()
167            .GetParentNode()
168            .and_then(|parent| parent.style())
169            .is_some_and(|style| {
170                // Note that for "pre" and "pre-wrap", the longhand "white-space-collapse: preserve" applies
171                // https://www.w3.org/TR/css-text-4/#white-space-property
172                style.get_inherited_text().white_space_collapse != WhiteSpaceCollapse::Preserve
173            });
174        let has_space_character = self
175            .data()
176            .chars()
177            .nth(offset as usize)
178            .is_some_and(|c| space_characters.contains(&&c));
179        has_preserve_space && has_space_character
180    }
181}
182
183impl HTMLBRElement {
184    /// <https://w3c.github.io/editing/docs/execCommand/#extraneous-line-break>
185    fn is_extraneous_line_break(&self) -> bool {
186        let node = self.upcast::<Node>();
187        // > An extraneous line break is a br that has no visual effect, in that removing it from the DOM would not change layout,
188        // except that a br that is the sole child of an li is not extraneous.
189        if node
190            .GetParentNode()
191            .filter(|parent| parent.is::<HTMLLIElement>())
192            .is_some_and(|li| li.children_count() == 1)
193        {
194            return false;
195        }
196        // TODO: Figure out what this actually makes it have no visual effect
197        !node.is_block_node()
198    }
199}
200
201impl Document {
202    fn create_br_element(&self, cx: &mut js::context::JSContext) -> DomRoot<Element> {
203        let element_options =
204            StringOrElementCreationOptions::ElementCreationOptions(ElementCreationOptions {
205                is: None,
206            });
207        match self.CreateElement(cx, "br".into(), element_options) {
208            Err(_) => unreachable!("Must always be able to create br"),
209            Ok(br) => br,
210        }
211    }
212}
213
214impl HTMLElement {
215    fn local_name(&self) -> &str {
216        self.upcast::<Element>().local_name()
217    }
218}
219
220pub(crate) enum NodeOrString {
221    String(String),
222    Node(DomRoot<Node>),
223}
224
225impl NodeOrString {
226    fn name(&self) -> &str {
227        match self {
228            NodeOrString::String(str_) => str_,
229            NodeOrString::Node(node) => node
230                .downcast::<Element>()
231                .map(|element| element.local_name().as_ref())
232                .unwrap_or_default(),
233        }
234    }
235
236    fn as_node(&self) -> Option<DomRoot<Node>> {
237        match self {
238            NodeOrString::String(_) => None,
239            NodeOrString::Node(node) => Some(node.clone()),
240        }
241    }
242}
243
244/// <https://w3c.github.io/editing/docs/execCommand/#prohibited-paragraph-child-name>
245const PROHIBITED_PARAGRAPH_CHILD_NAMES: [&str; 47] = [
246    "address",
247    "article",
248    "aside",
249    "blockquote",
250    "caption",
251    "center",
252    "col",
253    "colgroup",
254    "dd",
255    "details",
256    "dir",
257    "div",
258    "dl",
259    "dt",
260    "fieldset",
261    "figcaption",
262    "figure",
263    "footer",
264    "form",
265    "h1",
266    "h2",
267    "h3",
268    "h4",
269    "h5",
270    "h6",
271    "header",
272    "hgroup",
273    "hr",
274    "li",
275    "listing",
276    "menu",
277    "nav",
278    "ol",
279    "p",
280    "plaintext",
281    "pre",
282    "section",
283    "summary",
284    "table",
285    "tbody",
286    "td",
287    "tfoot",
288    "th",
289    "thead",
290    "tr",
291    "ul",
292    "xmp",
293];
294/// <https://w3c.github.io/editing/docs/execCommand/#name-of-an-element-with-inline-contents>
295const NAME_OF_AN_ELEMENT_WITH_INLINE_CONTENTS: [&str; 43] = [
296    "a", "abbr", "b", "bdi", "bdo", "cite", "code", "dfn", "em", "h1", "h2", "h3", "h4", "h5",
297    "h6", "i", "kbd", "mark", "p", "pre", "q", "rp", "rt", "ruby", "s", "samp", "small", "span",
298    "strong", "sub", "sup", "u", "var", "acronym", "listing", "strike", "xmp", "big", "blink",
299    "font", "marquee", "nobr", "tt",
300];
301
302/// <https://w3c.github.io/editing/docs/execCommand/#element-with-inline-contents>
303fn is_element_with_inline_contents(element: &Node) -> bool {
304    // > An element with inline contents is an HTML element whose local name is a name of an element with inline contents.
305    let Some(html_element) = element.downcast::<HTMLElement>() else {
306        return false;
307    };
308    NAME_OF_AN_ELEMENT_WITH_INLINE_CONTENTS.contains(&html_element.local_name())
309}
310
311/// <https://w3c.github.io/editing/docs/execCommand/#allowed-child>
312fn is_allowed_child(child: NodeOrString, parent: NodeOrString) -> bool {
313    // Step 1. If parent is "colgroup", "table", "tbody", "tfoot", "thead", "tr",
314    // or an HTML element with local name equal to one of those,
315    // and child is a Text node whose data does not consist solely of space characters, return false.
316    if matches!(
317        parent.name(),
318        "colgroup" | "table" | "tbody" | "tfoot" | "thead" | "tr"
319    ) && child.as_node().is_some_and(|node| {
320        // Note: cannot use `.and_then` here, since `downcast` would outlive its reference
321        node.downcast::<Text>()
322            .is_some_and(|text| !text.data().bytes().all(|byte| byte == b' '))
323    }) {
324        return false;
325    }
326    // Step 2. If parent is "script", "style", "plaintext", or "xmp",
327    // or an HTML element with local name equal to one of those, and child is not a Text node, return false.
328    if matches!(parent.name(), "script" | "style" | "plaintext" | "xmp") &&
329        child.as_node().is_none_or(|node| !node.is::<Text>())
330    {
331        return false;
332    }
333    // Step 3. If child is a document, DocumentFragment, or DocumentType, return false.
334    if let NodeOrString::Node(ref node) = child {
335        if matches!(
336            node.type_id(),
337            NodeTypeId::Document(_) | NodeTypeId::DocumentFragment(_) | NodeTypeId::DocumentType
338        ) {
339            return false;
340        }
341    }
342    // Step 4. If child is an HTML element, set child to the local name of child.
343    let child_name = match child {
344        NodeOrString::String(str_) => str_,
345        NodeOrString::Node(node) => match node.downcast::<HTMLElement>() {
346            // Step 5. If child is not a string, return true.
347            None => return true,
348            Some(html_element) => html_element.local_name().to_owned(),
349        },
350    };
351    let child = child_name.as_str();
352    let parent_name = match parent {
353        NodeOrString::String(str_) => str_,
354        NodeOrString::Node(parent) => {
355            // Step 6. If parent is an HTML element:
356            if let Some(parent_element) = parent.downcast::<HTMLElement>() {
357                // Step 6.1. If child is "a", and parent or some ancestor of parent is an a, return false.
358                if child == "a" &&
359                    parent
360                        .inclusive_ancestors(ShadowIncluding::No)
361                        .any(|node| node.is::<HTMLAnchorElement>())
362                {
363                    return false;
364                }
365                // Step 6.2. If child is a prohibited paragraph child name and parent or some ancestor of parent
366                // is an element with inline contents, return false.
367                if PROHIBITED_PARAGRAPH_CHILD_NAMES.contains(&child) &&
368                    parent
369                        .inclusive_ancestors(ShadowIncluding::No)
370                        .any(|node| is_element_with_inline_contents(&node))
371                {
372                    return false;
373                }
374                // Step 6.3. If child is "h1", "h2", "h3", "h4", "h5", or "h6",
375                // and parent or some ancestor of parent is an HTML element with local name
376                // "h1", "h2", "h3", "h4", "h5", or "h6", return false.
377                if matches!(child, "h1" | "h2" | "h3" | "h4" | "h5" | "h6") &&
378                    parent.inclusive_ancestors(ShadowIncluding::No).any(|node| {
379                        node.downcast::<HTMLElement>().is_some_and(|html_element| {
380                            matches!(
381                                html_element.local_name(),
382                                "h1" | "h2" | "h3" | "h4" | "h5" | "h6"
383                            )
384                        })
385                    })
386                {
387                    return false;
388                }
389                // Step 6.4. Let parent be the local name of parent.
390                parent_element.local_name().to_owned()
391            } else {
392                // Step 7. If parent is an Element or DocumentFragment, return true.
393                // Step 8. If parent is not a string, return false.
394                return matches!(
395                    parent.type_id(),
396                    NodeTypeId::DocumentFragment(_) | NodeTypeId::Element(_)
397                );
398            }
399        },
400    };
401    let parent = parent_name.as_str();
402    // Step 9. If parent is on the left-hand side of an entry on the following list,
403    // then return true if child is listed on the right-hand side of that entry, and false otherwise.
404    match parent {
405        "colgroup" => return child == "col",
406        "table" => {
407            return matches!(
408                child,
409                "caption" | "col" | "colgroup" | "tbody" | "td" | "tfoot" | "th" | "thead" | "tr"
410            );
411        },
412        "tbody" | "tfoot" | "thead" => return matches!(child, "td" | "th" | "tr"),
413        "tr" => return matches!(child, "td" | "th"),
414        "dl" => return matches!(child, "dt" | "dd"),
415        "dir" | "ol" | "ul" => return matches!(child, "dir" | "li" | "ol" | "ul"),
416        "hgroup" => return matches!(child, "h1" | "h2" | "h3" | "h4" | "h5" | "h6"),
417        _ => {},
418    };
419    // Step 10. If child is "body", "caption", "col", "colgroup", "frame", "frameset", "head",
420    // "html", "tbody", "td", "tfoot", "th", "thead", or "tr", return false.
421    if matches!(
422        child,
423        "body" |
424            "caption" |
425            "col" |
426            "colgroup" |
427            "frame" |
428            "frameset" |
429            "head" |
430            "html" |
431            "tbody" |
432            "td" |
433            "tfoot" |
434            "th" |
435            "thead" |
436            "tr"
437    ) {
438        return false;
439    }
440    // Step 11. If child is "dd" or "dt" and parent is not "dl", return false.
441    if matches!(child, "dd" | "dt") && parent != "dl" {
442        return false;
443    }
444    // Step 12. If child is "li" and parent is not "ol" or "ul", return false.
445    if child == "li" && !matches!(parent, "ol" | "ul") {
446        return false;
447    }
448    // Step 13. If parent is on the left-hand side of an entry on the following list
449    // and child is listed on the right-hand side of that entry, return false.
450    if match parent {
451        "a" => child == "a",
452        "dd" | "dt" => matches!(child, "dd" | "dt"),
453        "h1" | "h2" | "h3" | "h4" | "h5" | "h6" => {
454            matches!(child, "h1" | "h2" | "h3" | "h4" | "h5" | "h6")
455        },
456        "li" => child == "li",
457        "nobr" => child == "nobr",
458        "td" | "th" => {
459            matches!(
460                child,
461                "caption" | "col" | "colgroup" | "tbody" | "td" | "tfoot" | "th" | "thead" | "tr"
462            )
463        },
464        _ if NAME_OF_AN_ELEMENT_WITH_INLINE_CONTENTS.contains(&parent) => {
465            PROHIBITED_PARAGRAPH_CHILD_NAMES.contains(&child)
466        },
467        _ => false,
468    } {
469        return false;
470    }
471    // Step 14. Return true.
472    true
473}
474
475/// <https://w3c.github.io/editing/docs/execCommand/#split-the-parent>
476pub(crate) fn split_the_parent<'a>(cx: &mut js::context::JSContext, node_list: &'a [&'a Node]) {
477    assert!(!node_list.is_empty());
478    // Step 1. Let original parent be the parent of the first member of node list.
479    let Some(original_parent) = node_list.first().and_then(|first| first.GetParentNode()) else {
480        return;
481    };
482    let context_object = original_parent.owner_document();
483    // Step 2. If original parent is not editable or its parent is null, do nothing and abort these steps.
484    if !original_parent.is_editable() {
485        return;
486    }
487    let Some(parent_of_original_parent) = original_parent.GetParentNode() else {
488        return;
489    };
490    // Step 3. If the first child of original parent is in node list, remove extraneous line breaks before original parent.
491    if original_parent
492        .children()
493        .next()
494        .is_some_and(|first_child| node_list.contains(&first_child.deref()))
495    {
496        original_parent.remove_extraneous_line_breaks_before(cx);
497    }
498    // Step 4. If the first child of original parent is in node list, and original parent follows a line break,
499    // set follows line break to true. Otherwise, set follows line break to false.
500    let first_child_is_in_node_list = original_parent
501        .children()
502        .next()
503        .is_some_and(|first_child| node_list.contains(&first_child.deref()));
504    let follows_line_break = first_child_is_in_node_list && original_parent.follows_a_line_break();
505    // Step 5. If the last child of original parent is in node list, and original parent precedes a line break,
506    // set precedes line break to true. Otherwise, set precedes line break to false.
507    let last_child_is_in_node_list = original_parent
508        .children()
509        .last()
510        .is_some_and(|last_child| node_list.contains(&last_child.deref()));
511    let precedes_line_break = last_child_is_in_node_list && original_parent.precedes_a_line_break();
512    // Step 6. If the first child of original parent is not in node list, but its last child is:
513    if !first_child_is_in_node_list && last_child_is_in_node_list {
514        // Step 6.1. For each node in node list, in reverse order,
515        // insert node into the parent of original parent immediately after original parent, preserving ranges.
516        for node in node_list.iter().rev() {
517            // TODO: Preserving ranges
518            if parent_of_original_parent
519                .InsertBefore(cx, node, original_parent.GetNextSibling().as_deref())
520                .is_err()
521            {
522                unreachable!("Must always have a parent");
523            }
524        }
525        // Step 6.2. If precedes line break is true, and the last member of node list does not precede a line break,
526        // call createElement("br") on the context object and insert the result immediately after the last member of node list.
527        if precedes_line_break {
528            if let Some(last) = node_list.last() {
529                if !last.precedes_a_line_break() {
530                    let br = context_object.create_br_element(cx);
531                    if last
532                        .GetParentNode()
533                        .expect("Must always have a parent")
534                        .InsertBefore(cx, br.upcast(), last.GetNextSibling().as_deref())
535                        .is_err()
536                    {
537                        unreachable!("Must always be able to append");
538                    }
539                }
540            }
541        }
542        // Step 6.3. Remove extraneous line breaks at the end of original parent.
543        original_parent.remove_extraneous_line_breaks_at_the_end_of(cx);
544        // Step 6.4. Abort these steps.
545        return;
546    }
547    // Step 7. If the first child of original parent is not in node list:
548    if first_child_is_in_node_list {
549        // Step 7.1. Let cloned parent be the result of calling cloneNode(false) on original parent.
550        let Ok(cloned_parent) = original_parent.CloneNode(cx, false) else {
551            unreachable!("Must always be able to clone node");
552        };
553        // Step 7.2. If original parent has an id attribute, unset it.
554        if let Some(element) = original_parent.downcast::<Element>() {
555            element.remove_attribute_by_name(&local_name!("id"), CanGc::from_cx(cx));
556        }
557        // Step 7.3. Insert cloned parent into the parent of original parent immediately before original parent.
558        if parent_of_original_parent
559            .InsertBefore(cx, &cloned_parent, Some(&original_parent))
560            .is_err()
561        {
562            unreachable!("Must always have a parent");
563        }
564        // Step 7.4. While the previousSibling of the first member of node list is not null,
565        // append the first child of original parent as the last child of cloned parent, preserving ranges.
566        loop {
567            if node_list
568                .first()
569                .and_then(|first| first.GetPreviousSibling())
570                .is_some()
571            {
572                if let Some(first_of_original) = original_parent.children().next() {
573                    // TODO: Preserving ranges
574                    if cloned_parent.AppendChild(cx, &first_of_original).is_err() {
575                        unreachable!("Must always have a parent");
576                    }
577                    continue;
578                }
579            }
580            break;
581        }
582    }
583    // Step 8. For each node in node list, insert node into the parent of original parent immediately before original parent, preserving ranges.
584    for node in node_list.iter() {
585        // TODO: Preserving ranges
586        if parent_of_original_parent
587            .InsertBefore(cx, node, Some(&original_parent))
588            .is_err()
589        {
590            unreachable!("Must always have a parent");
591        }
592    }
593    // Step 9. If follows line break is true, and the first member of node list does not follow a line break,
594    // call createElement("br") on the context object and insert the result immediately before the first member of node list.
595    if follows_line_break {
596        if let Some(first) = node_list.first() {
597            if !first.follows_a_line_break() {
598                let br = context_object.create_br_element(cx);
599                if first
600                    .GetParentNode()
601                    .expect("Must always have a parent")
602                    .InsertBefore(cx, br.upcast(), Some(first))
603                    .is_err()
604                {
605                    unreachable!("Must always be able to insert");
606                }
607            }
608        }
609    }
610    // Step 10. If the last member of node list is an inline node other than a br,
611    // and the first child of original parent is a br, and original parent is not an inline node,
612    // remove the first child of original parent from original parent.
613    if node_list
614        .last()
615        .is_some_and(|last| last.is_inline_node() && !last.is::<HTMLBRElement>()) &&
616        !original_parent.is_inline_node()
617    {
618        if let Some(first_of_original) = original_parent.children().next() {
619            if first_of_original.is::<HTMLBRElement>() {
620                assert!(first_of_original.has_parent());
621                first_of_original.remove_self(cx);
622            }
623        }
624    }
625    // Step 11. If original parent has no children:
626    if original_parent.children_count() == 0 {
627        // Step 11.1. Remove original parent from its parent.
628        assert!(original_parent.has_parent());
629        original_parent.remove_self(cx);
630        // Step 11.2. If precedes line break is true, and the last member of node list does not precede a line break,
631        // call createElement("br") on the context object and insert the result immediately after the last member of node list.
632        if precedes_line_break {
633            if let Some(last) = node_list.last() {
634                if !last.precedes_a_line_break() {
635                    let br = context_object.create_br_element(cx);
636                    if last
637                        .GetParentNode()
638                        .expect("Must always have a parent")
639                        .InsertBefore(cx, br.upcast(), last.GetNextSibling().as_deref())
640                        .is_err()
641                    {
642                        unreachable!("Must always be able to insert");
643                    }
644                }
645            }
646        }
647    } else {
648        // Step 12. Otherwise, remove extraneous line breaks before original parent.
649        original_parent.remove_extraneous_line_breaks_before(cx);
650    }
651    // Step 13. If node list's last member's nextSibling is null, but its parent is not null,
652    // remove extraneous line breaks at the end of node list's last member's parent.
653    if let Some(last) = node_list.last() {
654        if last.GetNextSibling().is_none() {
655            if let Some(parent_of_last) = last.GetParentNode() {
656                parent_of_last.remove_extraneous_line_breaks_at_the_end_of(cx);
657            }
658        }
659    }
660}
661
662impl Node {
663    fn resolved_display_value(&self) -> Option<DisplayOutside> {
664        self.style().map(|style| style.get_box().display.outside())
665    }
666}
667
668pub(crate) trait NodeExecCommandSupport {
669    fn same_editing_host(&self, other: &Node) -> bool;
670    fn is_block_node(&self) -> bool;
671    fn is_inline_node(&self) -> bool;
672    fn block_node_of(&self) -> Option<DomRoot<Node>>;
673    fn is_visible(&self) -> bool;
674    fn is_invisible(&self) -> bool;
675    fn is_formattable(&self) -> bool;
676    fn is_block_start_point(&self, offset: usize) -> bool;
677    fn is_block_end_point(&self, offset: u32) -> bool;
678    fn is_block_boundary_point(&self, offset: u32) -> bool;
679    fn is_collapsed_block_prop(&self) -> bool;
680    fn follows_a_line_break(&self) -> bool;
681    fn precedes_a_line_break(&self) -> bool;
682    fn canonical_space_sequence(
683        n: usize,
684        non_breaking_start: bool,
685        non_breaking_end: bool,
686    ) -> String;
687    fn canonicalize_whitespace(&self, offset: u32, fix_collapsed_space: bool);
688    fn remove_extraneous_line_breaks_before(&self, cx: &mut js::context::JSContext);
689    fn remove_extraneous_line_breaks_at_the_end_of(&self, cx: &mut js::context::JSContext);
690    fn remove_preserving_its_descendants(&self, cx: &mut js::context::JSContext);
691    fn effective_command_value(&self, command_name: CommandName) -> Option<DOMString>;
692}
693
694impl NodeExecCommandSupport for Node {
695    /// <https://w3c.github.io/editing/docs/execCommand/#in-the-same-editing-host>
696    fn same_editing_host(&self, other: &Node) -> bool {
697        // > Two nodes are in the same editing host if the editing host of the first is non-null and the same as the editing host of the second.
698        self.editing_host_of()
699            .is_some_and(|editing_host| other.editing_host_of() == Some(editing_host))
700    }
701
702    /// <https://w3c.github.io/editing/docs/execCommand/#block-node>
703    fn is_block_node(&self) -> bool {
704        // > A block node is either an Element whose "display" property does not have resolved value "inline" or "inline-block" or "inline-table" or "none",
705        if self.is::<Element>() &&
706            self.resolved_display_value().is_some_and(|display| {
707                display != DisplayOutside::Inline && display != DisplayOutside::None
708            })
709        {
710            return true;
711        }
712        // > or a document, or a DocumentFragment.
713        matches!(
714            self.type_id(),
715            NodeTypeId::Document(_) | NodeTypeId::DocumentFragment(_)
716        )
717    }
718
719    /// <https://w3c.github.io/editing/docs/execCommand/#inline-node>
720    fn is_inline_node(&self) -> bool {
721        // > An inline node is a node that is not a block node.
722        !self.is_block_node()
723    }
724
725    /// <https://w3c.github.io/editing/docs/execCommand/#block-node-of>
726    fn block_node_of(&self) -> Option<DomRoot<Node>> {
727        let mut node = DomRoot::from_ref(self);
728
729        loop {
730            // Step 1. While node is an inline node, set node to its parent.
731            if node.is_inline_node() {
732                node = node.GetParentNode()?;
733                continue;
734            }
735            // Step 2. Return node.
736            return Some(node);
737        }
738    }
739
740    /// <https://w3c.github.io/editing/docs/execCommand/#visible>
741    fn is_visible(&self) -> bool {
742        for parent in self.inclusive_ancestors(ShadowIncluding::No) {
743            // > excluding any node with an inclusive ancestor Element whose "display" property has resolved value "none".
744            if parent.is::<Element>() &&
745                parent
746                    .resolved_display_value()
747                    .is_some_and(|display| display == DisplayOutside::None)
748            {
749                return false;
750            }
751        }
752        // > Something is visible if it is a node that either is a block node,
753        if self.is_block_node() {
754            return true;
755        }
756        // > or a Text node that is not a collapsed whitespace node,
757        if self
758            .downcast::<Text>()
759            .is_some_and(|text| !text.is_collapsed_whitespace_node())
760        {
761            return true;
762        }
763        // > or an img, or a br that is not an extraneous line break, or any node with a visible descendant;
764        if self.is::<HTMLImageElement>() {
765            return true;
766        }
767        if self
768            .downcast::<HTMLBRElement>()
769            .is_some_and(|br| !br.is_extraneous_line_break())
770        {
771            return true;
772        }
773        for child in self.children() {
774            if child.is_visible() {
775                return true;
776            }
777        }
778        false
779    }
780
781    /// <https://w3c.github.io/editing/docs/execCommand/#invisible>
782    fn is_invisible(&self) -> bool {
783        // > Something is invisible if it is a node that is not visible.
784        !self.is_visible()
785    }
786
787    /// <https://w3c.github.io/editing/docs/execCommand/#formattable-node>
788    fn is_formattable(&self) -> bool {
789        // > A formattable node is an editable visible node that is either a Text node, an img, or a br.
790        self.is_editable() &&
791            self.is_visible() &&
792            (self.is::<Text>() || self.is::<HTMLImageElement>() || self.is::<HTMLBRElement>())
793    }
794
795    /// <https://w3c.github.io/editing/docs/execCommand/#block-start-point>
796    fn is_block_start_point(&self, offset: usize) -> bool {
797        // > A boundary point (node, offset) is a block start point if either node's parent is null and offset is zero;
798        if offset == 0 {
799            return self.GetParentNode().is_none();
800        }
801        // > or node has a child with index offset − 1, and that child is either a visible block node or a visible br.
802        self.children().nth(offset - 1).is_some_and(|child| {
803            child.is_visible() && (child.is_block_node() || child.is::<HTMLBRElement>())
804        })
805    }
806
807    /// <https://w3c.github.io/editing/docs/execCommand/#block-end-point>
808    fn is_block_end_point(&self, offset: u32) -> bool {
809        // > A boundary point (node, offset) is a block end point if either node's parent is null and offset is node's length;
810        if self.GetParentNode().is_none() && offset == self.len() {
811            return true;
812        }
813        // > or node has a child with index offset, and that child is a visible block node.
814        self.children()
815            .nth(offset as usize)
816            .is_some_and(|child| child.is_visible() && child.is_block_node())
817    }
818
819    /// <https://w3c.github.io/editing/docs/execCommand/#block-boundary-point>
820    fn is_block_boundary_point(&self, offset: u32) -> bool {
821        // > A boundary point is a block boundary point if it is either a block start point or a block end point.
822        self.is_block_start_point(offset as usize) || self.is_block_end_point(offset)
823    }
824
825    /// <https://w3c.github.io/editing/docs/execCommand/#collapsed-block-prop>
826    fn is_collapsed_block_prop(&self) -> bool {
827        // > A collapsed block prop is either a collapsed line break that is not an extraneous line break,
828
829        // TODO: Check for collapsed line break
830        if self
831            .downcast::<HTMLBRElement>()
832            .is_some_and(|br| !br.is_extraneous_line_break())
833        {
834            return true;
835        }
836        // > or an Element that is an inline node and whose children are all either invisible or collapsed block props
837        if !self.is::<Element>() {
838            return false;
839        };
840        if !self.is_inline_node() {
841            return false;
842        }
843        let mut at_least_one_collapsed_block_prop = false;
844        for child in self.children() {
845            if child.is_collapsed_block_prop() {
846                at_least_one_collapsed_block_prop = true;
847                continue;
848            }
849            if child.is_invisible() {
850                continue;
851            }
852
853            return false;
854        }
855        // > and that has at least one child that is a collapsed block prop.
856        at_least_one_collapsed_block_prop
857    }
858
859    /// <https://w3c.github.io/editing/docs/execCommand/#follows-a-line-break>
860    fn follows_a_line_break(&self) -> bool {
861        // Step 1. Let offset be zero.
862        let mut offset = 0;
863        // Step 2. While (node, offset) is not a block boundary point:
864        let mut node = DomRoot::from_ref(self);
865        while !node.is_block_boundary_point(offset) {
866            // Step 2.2. If offset is zero or node has no children, set offset to node's index, then set node to its parent.
867            if offset == 0 || node.children_count() == 0 {
868                offset = node.index();
869                node = node.GetParentNode().expect("Must always have a parent");
870                continue;
871            }
872            // Step 2.1. If node has a visible child with index offset minus one, return false.
873            let child = node.children().nth(offset as usize - 1);
874            let Some(child) = child else {
875                return false;
876            };
877            if child.is_visible() {
878                return false;
879            }
880            // Step 2.3. Otherwise, set node to its child with index offset minus one, then set offset to node's length.
881            node = child;
882            offset = node.len();
883        }
884        // Step 3. Return true.
885        true
886    }
887
888    /// <https://w3c.github.io/editing/docs/execCommand/#precedes-a-line-break>
889    fn precedes_a_line_break(&self) -> bool {
890        let mut node = DomRoot::from_ref(self);
891        // Step 1. Let offset be node's length.
892        let mut offset = node.len();
893        // Step 2. While (node, offset) is not a block boundary point:
894        while !node.is_block_boundary_point(offset) {
895            // Step 2.1. If node has a visible child with index offset, return false.
896            if node
897                .children()
898                .nth(offset as usize)
899                .is_some_and(|child| child.is_visible())
900            {
901                return false;
902            }
903            // Step 2.2. If offset is node's length or node has no children, set offset to one plus node's index, then set node to its parent.
904            if offset == node.len() || node.children_count() == 0 {
905                offset = 1 + node.index();
906                node = node.GetParentNode().expect("Must always have a parent");
907                continue;
908            }
909            // Step 2.3. Otherwise, set node to its child with index offset and set offset to zero.
910            let child = node.children().nth(offset as usize);
911            node = match child {
912                None => return false,
913                Some(child) => child,
914            };
915            offset = 0;
916        }
917        // Step 3. Return true.
918        true
919    }
920
921    /// <https://w3c.github.io/editing/docs/execCommand/#canonical-space-sequence>
922    fn canonical_space_sequence(
923        n: usize,
924        non_breaking_start: bool,
925        non_breaking_end: bool,
926    ) -> String {
927        let mut n = n;
928        // Step 1. If n is zero, return the empty string.
929        if n == 0 {
930            return String::new();
931        }
932        // Step 2. If n is one and both non-breaking start and non-breaking end are false, return a single space (U+0020).
933        if n == 1 {
934            if !non_breaking_start && !non_breaking_end {
935                return "\u{0020}".to_owned();
936            }
937            // Step 3. If n is one, return a single non-breaking space (U+00A0).
938            return "\u{00A0}".to_owned();
939        }
940        // Step 4. Let buffer be the empty string.
941        let mut buffer = String::new();
942        // Step 5. If non-breaking start is true, let repeated pair be U+00A0 U+0020. Otherwise, let it be U+0020 U+00A0.
943        let repeated_pair = if non_breaking_start {
944            "\u{00A0}\u{0020}"
945        } else {
946            "\u{0020}\u{00A0}"
947        };
948        // Step 6. While n is greater than three, append repeated pair to buffer and subtract two from n.
949        while n > 3 {
950            buffer.push_str(repeated_pair);
951            n -= 2;
952        }
953        // Step 7. If n is three, append a three-code unit string to buffer depending on non-breaking start and non-breaking end:
954        if n == 3 {
955            buffer.push_str(match (non_breaking_start, non_breaking_end) {
956                (false, false) => "\u{0020}\u{00A0}\u{0020}",
957                (true, false) => "\u{00A0}\u{00A0}\u{0020}",
958                (false, true) => "\u{0020}\u{00A0}\u{00A0}",
959                (true, true) => "\u{00A0}\u{0020}\u{00A0}",
960            });
961        } else {
962            // Step 8. Otherwise, append a two-code unit string to buffer depending on non-breaking start and non-breaking end:
963            buffer.push_str(match (non_breaking_start, non_breaking_end) {
964                (false, false) | (true, false) => "\u{00A0}\u{0020}",
965                (false, true) => "\u{0020}\u{00A0}",
966                (true, true) => "\u{00A0}\u{00A0}",
967            });
968        }
969        // Step 9. Return buffer.
970        buffer
971    }
972
973    /// <https://w3c.github.io/editing/docs/execCommand/#canonicalize-whitespace>
974    fn canonicalize_whitespace(&self, offset: u32, fix_collapsed_space: bool) {
975        // Step 1. If node is neither editable nor an editing host, abort these steps.
976        if !self.is_editable_or_editing_host() {
977            return;
978        }
979        // Step 2. Let start node equal node and let start offset equal offset.
980        let mut start_node = DomRoot::from_ref(self);
981        let mut start_offset = offset;
982        // Step 3. Repeat the following steps:
983        loop {
984            // Step 3.1. If start node has a child in the same editing host with index start offset minus one,
985            // set start node to that child, then set start offset to start node's length.
986            if start_offset > 0 {
987                let child = start_node.children().nth(start_offset as usize - 1);
988                if let Some(child) = child {
989                    if start_node.same_editing_host(&child) {
990                        start_node = child;
991                        start_offset = start_node.len();
992                        continue;
993                    }
994                };
995            }
996            // Step 3.2. Otherwise, if start offset is zero and start node does not follow a line break
997            // and start node's parent is in the same editing host, set start offset to start node's index,
998            // then set start node to its parent.
999            if start_offset == 0 && !start_node.follows_a_line_break() {
1000                if let Some(parent) = start_node.GetParentNode() {
1001                    if parent.same_editing_host(&start_node) {
1002                        start_offset = start_node.index();
1003                        start_node = parent;
1004                    }
1005                }
1006            }
1007            // Step 3.3. Otherwise, if start node is a Text node and its parent's resolved
1008            // value for "white-space" is neither "pre" nor "pre-wrap" and start offset is not zero
1009            // and the (start offset − 1)st code unit of start node's data is a space (0x0020) or
1010            // non-breaking space (0x00A0), subtract one from start offset.
1011            if start_offset != 0 &&
1012                start_node.downcast::<Text>().is_some_and(|text| {
1013                    text.has_whitespace_and_has_parent_with_whitespace_preserve(
1014                        start_offset - 1,
1015                        &[&'\u{0020}', &'\u{00A0}'],
1016                    )
1017                })
1018            {
1019                start_offset -= 1;
1020            }
1021            // Step 3.4. Otherwise, break from this loop.
1022            break;
1023        }
1024        // Step 4. Let end node equal start node and end offset equal start offset.
1025        let mut end_node = start_node.clone();
1026        let mut end_offset = start_offset;
1027        // Step 5. Let length equal zero.
1028        let mut length = 0;
1029        // Step 6. Let collapse spaces be true if start offset is zero and start node follows a line break, otherwise false.
1030        let mut collapse_spaces = start_offset == 0 && start_node.follows_a_line_break();
1031        // Step 7. Repeat the following steps:
1032        loop {
1033            // Step 7.1. If end node has a child in the same editing host with index end offset,
1034            // set end node to that child, then set end offset to zero.
1035            if let Some(child) = end_node.children().nth(end_offset as usize) {
1036                if child.same_editing_host(&end_node) {
1037                    end_node = child;
1038                    end_offset = 0;
1039                    continue;
1040                }
1041            }
1042            // Step 7.2. Otherwise, if end offset is end node's length
1043            // and end node does not precede a line break
1044            // and end node's parent is in the same editing host,
1045            // set end offset to one plus end node's index, then set end node to its parent.
1046            if end_offset == end_node.len() && !end_node.precedes_a_line_break() {
1047                if let Some(parent) = end_node.GetParentNode() {
1048                    if parent.same_editing_host(&end_node) {
1049                        end_offset = 1 + end_node.index();
1050                        end_node = parent;
1051                    }
1052                }
1053                continue;
1054            }
1055            // Step 7.3. Otherwise, if end node is a Text node and its parent's resolved value for "white-space"
1056            // is neither "pre" nor "pre-wrap"
1057            // and end offset is not end node's length and the end offsetth code unit of end node's data
1058            // is a space (0x0020) or non-breaking space (0x00A0):
1059            if let Some(text) = end_node.downcast::<Text>() {
1060                if text.has_whitespace_and_has_parent_with_whitespace_preserve(
1061                    end_offset,
1062                    &[&'\u{0020}', &'\u{00A0}'],
1063                ) {
1064                    // Step 7.3.1. If fix collapsed space is true, and collapse spaces is true,
1065                    // and the end offsetth code unit of end node's data is a space (0x0020):
1066                    // call deleteData(end offset, 1) on end node, then continue this loop from the beginning.
1067                    let has_space_at_offset = text
1068                        .data()
1069                        .chars()
1070                        .nth(end_offset as usize)
1071                        .is_some_and(|c| c == '\u{0020}');
1072                    if fix_collapsed_space && collapse_spaces && has_space_at_offset {
1073                        if text
1074                            .upcast::<CharacterData>()
1075                            .DeleteData(end_offset, 1)
1076                            .is_err()
1077                        {
1078                            unreachable!("Invalid deletion for character at end offset");
1079                        }
1080                        continue;
1081                    }
1082                    // Step 7.3.2. Set collapse spaces to true if the end offsetth code unit of
1083                    // end node's data is a space (0x0020), false otherwise.
1084                    collapse_spaces = text
1085                        .data()
1086                        .chars()
1087                        .nth(end_offset as usize)
1088                        .is_some_and(|c| c == '\u{0020}');
1089                    // Step 7.3.3. Add one to end offset.
1090                    end_offset += 1;
1091                    // Step 7.3.4. Add one to length.
1092                    length += 1;
1093                    continue;
1094                }
1095            }
1096            // Step 7.4. Otherwise, break from this loop.
1097            break;
1098        }
1099        // Step 8. If fix collapsed space is true, then while (start node, start offset)
1100        // is before (end node, end offset):
1101        if fix_collapsed_space {
1102            while bp_position(&start_node, start_offset, &end_node, end_offset) ==
1103                Some(Ordering::Less)
1104            {
1105                // Step 8.1. If end node has a child in the same editing host with index end offset − 1,
1106                // set end node to that child, then set end offset to end node's length.
1107                if end_offset > 0 {
1108                    if let Some(child) = end_node.children().nth(end_offset as usize - 1) {
1109                        if child.same_editing_host(&end_node) {
1110                            end_node = child;
1111                            end_offset = end_node.len();
1112                            continue;
1113                        }
1114                    }
1115                }
1116                // Step 8.2. Otherwise, if end offset is zero and end node's parent is in the same editing host,
1117                // set end offset to end node's index, then set end node to its parent.
1118                if let Some(parent) = end_node.GetParentNode() {
1119                    if end_offset == 0 && parent.same_editing_host(&end_node) {
1120                        end_offset = end_node.index();
1121                        end_node = parent;
1122                        continue;
1123                    }
1124                }
1125                // Step 8.3. Otherwise, if end node is a Text node and its parent's resolved value for "white-space"
1126                // is neither "pre" nor "pre-wrap"
1127                // and end offset is end node's length and the last code unit of end node's data
1128                // is a space (0x0020) and end node precedes a line break:
1129                if let Some(text) = end_node.downcast::<Text>() {
1130                    if text.has_whitespace_and_has_parent_with_whitespace_preserve(
1131                        text.data().len() as u32,
1132                        &[&'\u{0020}'],
1133                    ) && end_node.precedes_a_line_break()
1134                    {
1135                        // Step 8.3.1. Subtract one from end offset.
1136                        end_offset -= 1;
1137                        // Step 8.3.2. Subtract one from length.
1138                        length -= 1;
1139                        // Step 8.3.3. Call deleteData(end offset, 1) on end node.
1140                        if text
1141                            .upcast::<CharacterData>()
1142                            .DeleteData(end_offset, 1)
1143                            .is_err()
1144                        {
1145                            unreachable!("Invalid deletion for character at end offset");
1146                        }
1147                        continue;
1148                    }
1149                }
1150                // Step 8.4. Otherwise, break from this loop.
1151                break;
1152            }
1153        }
1154        // Step 9. Let replacement whitespace be the canonical space sequence of length length.
1155        // non-breaking start is true if start offset is zero and start node follows a line break, and false otherwise.
1156        // non-breaking end is true if end offset is end node's length and end node precedes a line break, and false otherwise.
1157        let replacement_whitespace = Node::canonical_space_sequence(
1158            length,
1159            start_offset == 0 && start_node.follows_a_line_break(),
1160            end_offset == end_node.len() && end_node.precedes_a_line_break(),
1161        );
1162        let mut replacement_whitespace_chars = replacement_whitespace.chars();
1163        // Step 10. While (start node, start offset) is before (end node, end offset):
1164        while bp_position(&start_node, start_offset, &end_node, end_offset) == Some(Ordering::Less)
1165        {
1166            // Step 10.1. If start node has a child with index start offset, set start node to that child, then set start offset to zero.
1167            if let Some(child) = start_node.children().nth(start_offset as usize) {
1168                start_node = child;
1169                start_offset = 0;
1170                continue;
1171            }
1172            // Step 10.2. Otherwise, if start node is not a Text node or if start offset is start node's length,
1173            // set start offset to one plus start node's index, then set start node to its parent.
1174            let start_node_as_text = start_node.downcast::<Text>();
1175            if start_node_as_text.is_none() || start_offset == start_node.len() {
1176                start_offset = 1 + start_node.index();
1177                start_node = start_node
1178                    .GetParentNode()
1179                    .expect("Must always have a parent");
1180                continue;
1181            }
1182            let start_node_as_text =
1183                start_node_as_text.expect("Already verified none in previous statement");
1184            // Step 10.3. Otherwise:
1185            // Step 10.3.1. Remove the first code unit from replacement whitespace, and let element be that code unit.
1186            if let Some(element) = replacement_whitespace_chars.next() {
1187                // Step 10.3.2. If element is not the same as the start offsetth code unit of start node's data:
1188                if start_node_as_text.data().chars().nth(start_offset as usize) != Some(element) {
1189                    let character_data = start_node_as_text.upcast::<CharacterData>();
1190                    // Step 10.3.2.1. Call insertData(start offset, element) on start node.
1191                    if character_data
1192                        .InsertData(start_offset, element.to_string().into())
1193                        .is_err()
1194                    {
1195                        unreachable!("Invalid insertion for character at start offset");
1196                    }
1197                    // Step 10.3.2.2. Call deleteData(start offset + 1, 1) on start node.
1198                    if character_data.DeleteData(start_offset + 1, 1).is_err() {
1199                        unreachable!("Invalid deletion for character at start offset + 1");
1200                    }
1201                }
1202            }
1203            // Step 10.3.3. Add one to start offset.
1204            start_offset += 1;
1205        }
1206    }
1207
1208    /// <https://w3c.github.io/editing/docs/execCommand/#remove-extraneous-line-breaks-before>
1209    fn remove_extraneous_line_breaks_before(&self, cx: &mut js::context::JSContext) {
1210        let parent = self.GetParentNode();
1211        // Step 1. Let ref be the previousSibling of node.
1212        let Some(mut ref_) = self.GetPreviousSibling() else {
1213            // Step 2. If ref is null, abort these steps.
1214            return;
1215        };
1216        // Step 3. While ref has children, set ref to its lastChild.
1217        while let Some(last_child) = ref_.children().last() {
1218            ref_ = last_child;
1219        }
1220        // Step 4. While ref is invisible but not an extraneous line break,
1221        // and ref does not equal node's parent, set ref to the node before it in tree order.
1222        loop {
1223            if ref_.is_invisible() &&
1224                ref_.downcast::<HTMLBRElement>()
1225                    .is_none_or(|br| !br.is_extraneous_line_break())
1226            {
1227                if let Some(parent) = parent.as_ref() {
1228                    if ref_ != *parent {
1229                        ref_ = match ref_.preceding_nodes(parent).nth(0) {
1230                            None => break,
1231                            Some(node) => node,
1232                        };
1233                        continue;
1234                    }
1235                }
1236            }
1237            break;
1238        }
1239        // Step 5. If ref is an editable extraneous line break, remove it from its parent.
1240        if ref_.is_editable() &&
1241            ref_.downcast::<HTMLBRElement>()
1242                .is_some_and(|br| br.is_extraneous_line_break())
1243        {
1244            assert!(ref_.has_parent());
1245            ref_.remove_self(cx);
1246        }
1247    }
1248
1249    /// <https://w3c.github.io/editing/docs/execCommand/#remove-extraneous-line-breaks-at-the-end-of>
1250    fn remove_extraneous_line_breaks_at_the_end_of(&self, cx: &mut js::context::JSContext) {
1251        // Step 1. Let ref be node.
1252        let mut ref_ = DomRoot::from_ref(self);
1253        // Step 2. While ref has children, set ref to its lastChild.
1254        while let Some(last_child) = ref_.children().last() {
1255            ref_ = last_child;
1256        }
1257        // Step 3. While ref is invisible but not an extraneous line break, and ref does not equal node,
1258        // set ref to the node before it in tree order.
1259        loop {
1260            if ref_.is_invisible() &&
1261                *ref_ != *self &&
1262                ref_.downcast::<HTMLBRElement>()
1263                    .is_none_or(|br| !br.is_extraneous_line_break())
1264            {
1265                if let Some(parent_of_ref) = ref_.GetParentNode() {
1266                    ref_ = match ref_.preceding_nodes(&parent_of_ref).nth(0) {
1267                        None => break,
1268                        Some(node) => node,
1269                    };
1270                    continue;
1271                }
1272            }
1273            break;
1274        }
1275        // Step 4. If ref is an editable extraneous line break:
1276        if ref_.is_editable() &&
1277            ref_.downcast::<HTMLBRElement>()
1278                .is_some_and(|br| br.is_extraneous_line_break())
1279        {
1280            // Step 4.1. While ref's parent is editable and invisible, set ref to its parent.
1281            loop {
1282                if let Some(parent) = ref_.GetParentNode() {
1283                    if parent.is_editable() && parent.is_invisible() {
1284                        ref_ = parent;
1285                        continue;
1286                    }
1287                }
1288                break;
1289            }
1290            // Step 4.2. Remove ref from its parent.
1291            assert!(ref_.has_parent());
1292            ref_.remove_self(cx);
1293        }
1294    }
1295
1296    /// <https://w3c.github.io/editing/docs/execCommand/#preserving-its-descendants>
1297    fn remove_preserving_its_descendants(&self, cx: &mut js::context::JSContext) {
1298        // > To remove a node node while preserving its descendants,
1299        // > split the parent of node's children if it has any.
1300        // > If it has no children, instead remove it from its parent.
1301        if self.children_count() == 0 {
1302            assert!(self.has_parent());
1303            self.remove_self(cx);
1304        } else {
1305            rooted_vec!(let children <- self.children().map(|child| DomRoot::as_traced(&child)));
1306            split_the_parent(cx, children.r());
1307        }
1308    }
1309
1310    /// <https://w3c.github.io/editing/docs/execCommand/#effective-command-value>
1311    fn effective_command_value(&self, command_name: CommandName) -> Option<DOMString> {
1312        // Step 1. If neither node nor its parent is an Element, return null.
1313        // Step 2. If node is not an Element, return the effective command value of its parent for command.
1314        if !self.is::<Element>() {
1315            return self.GetParentElement().and_then(|parent| {
1316                parent
1317                    .upcast::<Node>()
1318                    .effective_command_value(command_name)
1319            });
1320        }
1321        match command_name {
1322            // Step 3. If command is "createLink" or "unlink":
1323            CommandName::CreateLink | CommandName::Unlink => {
1324                // Step 3.1. While node is not null, and is not an a element that has an href attribute, set node to its parent.
1325                let mut current_node = Some(DomRoot::from_ref(self));
1326                while let Some(node) = current_node {
1327                    if let Some(anchor_value) =
1328                        node.downcast::<HTMLAnchorElement>().and_then(|anchor| {
1329                            anchor
1330                                .upcast::<Element>()
1331                                .get_attribute(&local_name!("href"))
1332                        })
1333                    {
1334                        // Step 3.3. Return the value of node's href attribute.
1335                        return Some(DOMString::from(&**anchor_value.value()));
1336                    }
1337                    current_node = node.GetParentNode();
1338                }
1339                // Step 3.2. If node is null, return null.
1340                None
1341            },
1342            // Step 4. If command is "backColor" or "hiliteColor":
1343            CommandName::BackColor | CommandName::HiliteColor => {
1344                // Step 4.1. While the resolved value of "background-color" on node is any fully transparent value,
1345                // and node's parent is an Element, set node to its parent.
1346                // TODO
1347                // Step 4.2. Return the resolved value of "background-color" for node.
1348                // TODO
1349                None
1350            },
1351            // Step 5. If command is "subscript" or "superscript":
1352            CommandName::Subscript | CommandName::Superscript => {
1353                // Step 5.1. Let affected by subscript and affected by superscript be two boolean variables,
1354                // both initially false.
1355                let mut affected_by_subscript = false;
1356                let mut affected_by_superscript = false;
1357                // Step 5.2. While node is an inline node:
1358                let mut current_node = Some(DomRoot::from_ref(self));
1359                while let Some(node) = current_node {
1360                    if !node.is_inline_node() {
1361                        break;
1362                    }
1363                    if let Some(element) = node.downcast::<Element>() {
1364                        // Step 5.2.1. If node is a sub, set affected by subscript to true.
1365                        if *element.local_name() == local_name!("sub") {
1366                            affected_by_subscript = true;
1367                        } else if *element.local_name() == local_name!("sup") {
1368                            // Step 5.2.2. Otherwise, if node is a sup, set affected by superscript to true.
1369                            affected_by_superscript = true;
1370                        }
1371                    }
1372                    // Step 5.2.3. Set node to its parent.
1373                    current_node = node.GetParentNode();
1374                }
1375                Some(match (affected_by_subscript, affected_by_superscript) {
1376                    // Step 5.3. If affected by subscript and affected by superscript are both true,
1377                    // return the string "mixed".
1378                    (true, true) => "mixed".into(),
1379                    // Step 5.4. If affected by subscript is true, return "subscript".
1380                    (true, false) => "subscript".into(),
1381                    // Step 5.5. If affected by superscript is true, return "superscript".
1382                    (false, true) => "superscript".into(),
1383                    // Step 5.6. Return null.
1384                    (false, false) => return None,
1385                })
1386            },
1387            // Step 6. If command is "strikethrough",
1388            // and the "text-decoration" property of node or any of its ancestors has resolved value containing "line-through",
1389            // return "line-through". Otherwise, return null.
1390            // TODO
1391            CommandName::Strikethrough => None,
1392            // Step 7. If command is "underline",
1393            // and the "text-decoration" property of node or any of its ancestors has resolved value containing "underline",
1394            // return "underline". Otherwise, return null.
1395            // TODO
1396            CommandName::Underline => None,
1397            // Step 8. Return the resolved value for node of the relevant CSS property for command.
1398            // TODO
1399            _ => None,
1400        }
1401    }
1402}
1403
1404pub(crate) trait ContentEditableRange {
1405    fn handle_focus_state_for_contenteditable(&self, can_gc: CanGc);
1406}
1407
1408impl ContentEditableRange for HTMLElement {
1409    /// There is no specification for this implementation. Instead, it is
1410    /// reverse-engineered based on the WPT test
1411    /// /selection/contenteditable/initial-selection-on-focus.tentative.html
1412    fn handle_focus_state_for_contenteditable(&self, can_gc: CanGc) {
1413        if !self.is_editing_host() {
1414            return;
1415        }
1416        let document = self.owner_document();
1417        let Some(selection) = document.GetSelection(can_gc) else {
1418            return;
1419        };
1420        let range = self
1421            .upcast::<Element>()
1422            .ensure_contenteditable_selection_range(&document, can_gc);
1423        // If the current range is already associated with this contenteditable
1424        // element, then we shouldn't do anything. This is important when focus
1425        // is lost and regained, but selection was changed beforehand. In that
1426        // case, we should maintain the selection as it were, by not creating
1427        // a new range.
1428        if selection
1429            .active_range()
1430            .is_some_and(|active| active == range)
1431        {
1432            return;
1433        }
1434        let node = self.upcast::<Node>();
1435        let mut selected_node = DomRoot::from_ref(node);
1436        let mut previous_eligible_node = DomRoot::from_ref(node);
1437        let mut previous_node = DomRoot::from_ref(node);
1438        let mut selected_offset = 0;
1439        for child in node.traverse_preorder(ShadowIncluding::Yes) {
1440            if let Some(text) = child.downcast::<Text>() {
1441                // Note that to consider it whitespace, it needs to take more
1442                // into account than simply "it has a non-whitespace" character.
1443                // Therefore, we need to first check if it is not a whitespace
1444                // node and only then can we find what the relevant character is.
1445                if !text.is_whitespace_node() {
1446                    // A node with "white-space: pre" set must select its first
1447                    // character, regardless if that's a whitespace character or not.
1448                    let is_pre_formatted_text_node = child
1449                        .GetParentElement()
1450                        .and_then(|parent| parent.style())
1451                        .is_some_and(|style| {
1452                            style.get_inherited_text().white_space_collapse ==
1453                                WhiteSpaceCollapse::Preserve
1454                        });
1455                    if !is_pre_formatted_text_node {
1456                        // If it isn't pre-formatted, then we should instead select the
1457                        // first non-whitespace character.
1458                        selected_offset = text
1459                            .data()
1460                            .find(|c: char| !c.is_whitespace())
1461                            .unwrap_or_default() as u32;
1462                    }
1463                    selected_node = child;
1464                    break;
1465                }
1466            }
1467            // For <input>, <textarea>, <hr> and <br> elements, we should select the previous
1468            // node, regardless if it was a block node or not
1469            if matches!(
1470                child.type_id(),
1471                NodeTypeId::Element(ElementTypeId::HTMLElement(
1472                    HTMLElementTypeId::HTMLInputElement,
1473                )) | NodeTypeId::Element(ElementTypeId::HTMLElement(
1474                    HTMLElementTypeId::HTMLTextAreaElement,
1475                )) | NodeTypeId::Element(ElementTypeId::HTMLElement(
1476                    HTMLElementTypeId::HTMLHRElement,
1477                )) | NodeTypeId::Element(ElementTypeId::HTMLElement(
1478                    HTMLElementTypeId::HTMLBRElement,
1479                ))
1480            ) {
1481                selected_node = previous_node;
1482                break;
1483            }
1484            // When we encounter a non-contenteditable element, we should select the previous
1485            // eligible node
1486            if child
1487                .downcast::<HTMLElement>()
1488                .is_some_and(|el| el.ContentEditable().str() == "false")
1489            {
1490                selected_node = previous_eligible_node;
1491                break;
1492            }
1493            // We can only select block nodes as eligible nodes for the case of non-conenteditable
1494            // nodes
1495            if child.is_block_node() {
1496                previous_eligible_node = child.clone();
1497            }
1498            previous_node = child;
1499        }
1500        range.set_start(&selected_node, selected_offset);
1501        range.set_end(&selected_node, selected_offset);
1502        selection.AddRange(&range);
1503    }
1504}
1505
1506trait EquivalentPoint {
1507    fn previous_equivalent_point(&self) -> Option<(DomRoot<Node>, u32)>;
1508    fn next_equivalent_point(&self) -> Option<(DomRoot<Node>, u32)>;
1509    fn first_equivalent_point(self) -> (DomRoot<Node>, u32);
1510    fn last_equivalent_point(self) -> (DomRoot<Node>, u32);
1511}
1512
1513impl EquivalentPoint for (DomRoot<Node>, u32) {
1514    /// <https://w3c.github.io/editing/docs/execCommand/#previous-equivalent-point>
1515    fn previous_equivalent_point(&self) -> Option<(DomRoot<Node>, u32)> {
1516        let (node, offset) = self;
1517        // Step 1. If node's length is zero, return null.
1518        let len = node.len();
1519        if len == 0 {
1520            return None;
1521        }
1522        // Step 2. If offset is 0, and node's parent is not null, and node is an inline node,
1523        // return (node's parent, node's index).
1524        if *offset == 0 && node.is_inline_node() {
1525            if let Some(parent) = node.GetParentNode() {
1526                return Some((parent, node.index()));
1527            }
1528        }
1529        // Step 3. If node has a child with index offset − 1, and that child's length is not zero,
1530        // and that child is an inline node, return (that child, that child's length).
1531        if *offset > 0 {
1532            if let Some(child) = node.children().nth(*offset as usize - 1) {
1533                if !child.is_empty() && child.is_inline_node() {
1534                    let len = child.len();
1535                    return Some((child, len));
1536                }
1537            }
1538        }
1539
1540        // Step 4. Return null.
1541        None
1542    }
1543
1544    /// <https://w3c.github.io/editing/docs/execCommand/#next-equivalent-point>
1545    fn next_equivalent_point(&self) -> Option<(DomRoot<Node>, u32)> {
1546        let (node, offset) = self;
1547        // Step 1. If node's length is zero, return null.
1548        let len = node.len();
1549        if len == 0 {
1550            return None;
1551        }
1552
1553        // Step 2.
1554        //
1555        // This step does not exist in the spec
1556
1557        // Step 3. If offset is node's length, and node's parent is not null, and node is an inline node,
1558        // return (node's parent, 1 + node's index).
1559        if *offset == len && node.is_inline_node() {
1560            if let Some(parent) = node.GetParentNode() {
1561                return Some((parent, node.index() + 1));
1562            }
1563        }
1564
1565        // Step 4.
1566        //
1567        // This step does not exist in the spec
1568
1569        // Step 5. If node has a child with index offset, and that child's length is not zero,
1570        // and that child is an inline node, return (that child, 0).
1571        if let Some(child) = node.children().nth(*offset as usize) {
1572            if !child.is_empty() && child.is_inline_node() {
1573                return Some((child, 0));
1574            }
1575        }
1576
1577        // Step 6.
1578        //
1579        // This step does not exist in the spec
1580
1581        // Step 7. Return null.
1582        None
1583    }
1584
1585    /// <https://w3c.github.io/editing/docs/execCommand/#first-equivalent-point>
1586    fn first_equivalent_point(self) -> (DomRoot<Node>, u32) {
1587        let mut previous_equivalent_point = self;
1588        // Step 1. While (node, offset)'s previous equivalent point is not null, set (node, offset) to its previous equivalent point.
1589        loop {
1590            if let Some(next) = previous_equivalent_point.previous_equivalent_point() {
1591                previous_equivalent_point = next;
1592            } else {
1593                // Step 2. Return (node, offset).
1594                return previous_equivalent_point;
1595            }
1596        }
1597    }
1598
1599    /// <https://w3c.github.io/editing/docs/execCommand/#last-equivalent-point>
1600    fn last_equivalent_point(self) -> (DomRoot<Node>, u32) {
1601        let mut next_equivalent_point = self;
1602        // Step 1. While (node, offset)'s next equivalent point is not null, set (node, offset) to its next equivalent point.
1603        loop {
1604            if let Some(next) = next_equivalent_point.next_equivalent_point() {
1605                next_equivalent_point = next;
1606            } else {
1607                // Step 2. Return (node, offset).
1608                return next_equivalent_point;
1609            }
1610        }
1611    }
1612}
1613
1614enum BoolOrOptionalString {
1615    Bool(bool),
1616    OptionalString(Option<DOMString>),
1617}
1618
1619impl From<Option<DOMString>> for BoolOrOptionalString {
1620    fn from(optional_string: Option<DOMString>) -> Self {
1621        Self::OptionalString(optional_string)
1622    }
1623}
1624
1625impl From<bool> for BoolOrOptionalString {
1626    fn from(bool_: bool) -> Self {
1627        Self::Bool(bool_)
1628    }
1629}
1630
1631struct RecordedStateOfNode {
1632    name: CommandName,
1633    value: BoolOrOptionalString,
1634}
1635
1636impl Range {
1637    /// <https://w3c.github.io/editing/docs/execCommand/#effectively-contained>
1638    fn is_effectively_contained_node(&self, node: &Node) -> bool {
1639        assert!(!self.collapsed());
1640        // > A node node is effectively contained in a range range if range is not collapsed,
1641        // > and at least one of the following holds:
1642        // > node is range's start node, it is a Text node, and its length is different from range's start offset.
1643        let start_container = self.start_container();
1644        if *start_container == *node && node.is::<Text>() && node.len() != self.start_offset() {
1645            return true;
1646        }
1647        // > node is range's end node, it is a Text node, and range's end offset is not 0.
1648        let end_container = self.end_container();
1649        if *end_container == *node && node.is::<Text>() && self.end_offset() != 0 {
1650            return true;
1651        }
1652        // > node is contained in range.
1653        //
1654        // Already checked by caller
1655
1656        // > node has at least one child; and all its children are effectively contained in range;
1657        node.children_count() > 0 && node.children().all(|child| self.is_effectively_contained_node(&child))
1658        // > 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;
1659        && (!node.is_ancestor_of(&start_container) || !start_container.is::<Text>() || self.start_offset() == 0)
1660        // > 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.
1661        && (!node.is_ancestor_of(&end_container) || !end_container.is::<Text>() || self.end_offset() == end_container.len())
1662    }
1663
1664    fn first_formattable_contained_node(&self) -> Option<DomRoot<Node>> {
1665        if self.collapsed() {
1666            return None;
1667        }
1668        let Ok(contained_children) = self.contained_children() else {
1669            unreachable!("Must always be able to obtain contained children");
1670        };
1671        contained_children
1672            .first_partially_contained_child
1673            .as_ref()
1674            .filter(|child| child.is_formattable() && self.is_effectively_contained_node(child))
1675            .or_else(|| {
1676                // We don't call `is_effectively_contained_node` here, since nodes are considered
1677                // effectively contained if they are in `contained_children`
1678                contained_children
1679                    .contained_children
1680                    .iter()
1681                    .find(|child| child.is_formattable())
1682            })
1683            .or_else(|| {
1684                contained_children
1685                    .last_partially_contained_child
1686                    .as_ref()
1687                    .filter(|child| {
1688                        child.is_formattable() && self.is_effectively_contained_node(child)
1689                    })
1690            })
1691            .cloned()
1692    }
1693
1694    /// <https://w3c.github.io/editing/docs/execCommand/#record-current-states-and-values>
1695    fn record_current_states_and_values(&self) -> Vec<RecordedStateOfNode> {
1696        // Step 1. Let overrides be a list of (string, string or boolean) ordered pairs, initially empty.
1697        //
1698        // We return the vec in one go for the relevant values
1699
1700        // Step 2. Let node be the first formattable node effectively contained in the active range,
1701        // or null if there is none.
1702        let Some(node) = self.first_formattable_contained_node() else {
1703            // Step 3. If node is null, return overrides.
1704            return vec![];
1705        };
1706        // Step 8. Return overrides.
1707        vec![
1708            // Step 4. Add ("createLink", node's effective command value for "createLink") to overrides.
1709            RecordedStateOfNode {
1710                name: CommandName::CreateLink,
1711                value: node.effective_command_value(CommandName::CreateLink).into(),
1712            },
1713            // Step 5. For each command in the list
1714            // "bold", "italic", "strikethrough", "subscript", "superscript", "underline", in order:
1715            // if node's effective command value for command is one of its inline command activated values,
1716            // add (command, true) to overrides, and otherwise add (command, false) to overrides.
1717            // TODO
1718
1719            // Step 6. For each command in the list "fontName", "foreColor", "hiliteColor", in order:
1720            // add (command, command's value) to overrides.
1721            // TODO
1722
1723            // Step 7. Add ("fontSize", node's effective command value for "fontSize") to overrides.
1724            RecordedStateOfNode {
1725                name: CommandName::FontSize,
1726                value: node.effective_command_value(CommandName::FontSize).into(),
1727            },
1728        ]
1729    }
1730
1731    /// <https://w3c.github.io/editing/docs/execCommand/#restore-states-and-values>
1732    fn restore_states_and_values(
1733        &self,
1734        context_object: &Document,
1735        overrides: Vec<RecordedStateOfNode>,
1736    ) {
1737        // Step 1. Let node be the first formattable node effectively contained in the active range,
1738        // or null if there is none.
1739        let Some(_node) = self.first_formattable_contained_node() else {
1740            // Step 3. Otherwise, for each (command, override) pair in overrides, in order:
1741            for override_state in overrides {
1742                // Step 3.1. If override is a boolean, set the state override for command to override.
1743                match override_state.value {
1744                    BoolOrOptionalString::Bool(bool_) => {
1745                        context_object.set_state_override(override_state.name, bool_)
1746                    },
1747                    // Step 3.2. If override is a string, set the value override for command to override.
1748                    BoolOrOptionalString::OptionalString(optional_string) => {
1749                        context_object.set_value_override(override_state.name, optional_string)
1750                    },
1751                }
1752            }
1753            return;
1754        };
1755        // Step 2. If node is not null, then for each (command, override) pair in overrides, in order:
1756        // TODO
1757
1758        // Step 2.1. If override is a boolean, and queryCommandState(command)
1759        // returns something different from override, take the action for command,
1760        // with value equal to the empty string.
1761        // TODO
1762
1763        // Step 2.2. Otherwise, if override is a string, and command is neither "createLink" nor "fontSize",
1764        // and queryCommandValue(command) returns something not equivalent to override,
1765        // take the action for command, with value equal to override.
1766        // TODO
1767
1768        // Step 2.3. Otherwise, if override is a string; and command is "createLink";
1769        // and either there is a value override for "createLink" that is not equal to override,
1770        // or there is no value override for "createLink" and node's effective command value
1771        // for "createLink" is not equal to override: take the action for "createLink", with value equal to override.
1772        // TODO
1773
1774        // Step 2.4. Otherwise, if override is a string; and command is "fontSize";
1775        // and either there is a value override for "fontSize" that is not equal to override,
1776        // or there is no value override for "fontSize" and node's effective command value for "fontSize"
1777        // is not loosely equivalent to override:
1778        // TODO
1779
1780        // Step 2.5. Otherwise, continue this loop from the beginning.
1781        // TODO
1782
1783        // Step 2.6. Set node to the first formattable node effectively contained in the active range, if there is one.
1784        // TODO
1785    }
1786}
1787
1788#[derive(Default, PartialEq)]
1789pub(crate) enum SelectionDeletionBlockMerging {
1790    #[default]
1791    Merge,
1792    Skip,
1793}
1794
1795#[derive(Default, PartialEq)]
1796pub(crate) enum SelectionDeletionStripWrappers {
1797    #[default]
1798    Strip,
1799}
1800
1801#[derive(Default, PartialEq)]
1802pub(crate) enum SelectionDeleteDirection {
1803    #[default]
1804    Forward,
1805    Backward,
1806}
1807
1808pub(crate) trait SelectionExecCommandSupport {
1809    fn delete_the_selection(
1810        &self,
1811        cx: &mut js::context::JSContext,
1812        context_object: &Document,
1813        block_merging: SelectionDeletionBlockMerging,
1814        strip_wrappers: SelectionDeletionStripWrappers,
1815        direction: SelectionDeleteDirection,
1816    );
1817}
1818
1819impl SelectionExecCommandSupport for Selection {
1820    /// <https://w3c.github.io/editing/docs/execCommand/#delete-the-selection>
1821    fn delete_the_selection(
1822        &self,
1823        cx: &mut js::context::JSContext,
1824        context_object: &Document,
1825        block_merging: SelectionDeletionBlockMerging,
1826        strip_wrappers: SelectionDeletionStripWrappers,
1827        direction: SelectionDeleteDirection,
1828    ) {
1829        // Step 1. If the active range is null, abort these steps and do nothing.
1830        let Some(active_range) = self.active_range() else {
1831            return;
1832        };
1833
1834        // Step 2. Canonicalize whitespace at the active range's start.
1835        active_range
1836            .start_container()
1837            .canonicalize_whitespace(active_range.start_offset(), true);
1838
1839        // Step 3. Canonicalize whitespace at the active range's end.
1840        active_range
1841            .end_container()
1842            .canonicalize_whitespace(active_range.end_offset(), true);
1843
1844        // Step 4. Let (start node, start offset) be the last equivalent point for the active range's start.
1845        let (mut start_node, mut start_offset) =
1846            (active_range.start_container(), active_range.start_offset()).last_equivalent_point();
1847
1848        // Step 5. Let (end node, end offset) be the first equivalent point for the active range's end.
1849        let (mut end_node, mut end_offset) =
1850            (active_range.end_container(), active_range.end_offset()).first_equivalent_point();
1851
1852        // Step 6. If (end node, end offset) is not after (start node, start offset):
1853        if bp_position(&end_node, end_offset, &start_node, start_offset) != Some(Ordering::Greater)
1854        {
1855            // Step 6.1. If direction is "forward", call collapseToStart() on the context object's selection.
1856            if direction == SelectionDeleteDirection::Forward {
1857                if self.CollapseToStart(CanGc::from_cx(cx)).is_err() {
1858                    unreachable!("Should be able to collapse to start");
1859                }
1860            } else {
1861                // Step 6.2. Otherwise, call collapseToEnd() on the context object's selection.
1862                if self.CollapseToEnd(CanGc::from_cx(cx)).is_err() {
1863                    unreachable!("Should be able to collapse to end");
1864                }
1865            }
1866            // Step 6.3. Abort these steps.
1867            return;
1868        }
1869
1870        // Step 7. If start node is a Text node and start offset is 0, set start offset to the index of start node,
1871        // then set start node to its parent.
1872        if start_node.is::<Text>() && start_offset == 0 {
1873            start_offset = start_node.index();
1874            start_node = start_node
1875                .GetParentNode()
1876                .expect("Must always have a parent");
1877        }
1878
1879        // Step 8. If end node is a Text node and end offset is its length, set end offset to one plus the index of end node,
1880        // then set end node to its parent.
1881        if end_node.is::<Text>() && end_offset == end_node.len() {
1882            end_offset = end_node.index() + 1;
1883            end_node = end_node.GetParentNode().expect("Must always have a parent");
1884        }
1885
1886        // Step 9. Call collapse(start node, start offset) on the context object's selection.
1887        if self
1888            .Collapse(Some(&start_node), start_offset, CanGc::from_cx(cx))
1889            .is_err()
1890        {
1891            unreachable!("Must always be able to collapse");
1892        }
1893
1894        // Step 10. Call extend(end node, end offset) on the context object's selection.
1895        if self
1896            .Extend(&end_node, end_offset, CanGc::from_cx(cx))
1897            .is_err()
1898        {
1899            unreachable!("Must always be able to extend");
1900        }
1901
1902        // Step 11.
1903        //
1904        // This step does not exist in the spec
1905
1906        // Step 12. Let start block be the active range's start node.
1907        let Some(active_range) = self.active_range() else {
1908            return;
1909        };
1910        let mut start_block = active_range.start_container();
1911
1912        // Step 13. While start block's parent is in the same editing host and start block is an inline node,
1913        // set start block to its parent.
1914        loop {
1915            if start_block.is_inline_node() {
1916                if let Some(parent) = start_block.GetParentNode() {
1917                    if parent.same_editing_host(&start_node) {
1918                        start_block = parent;
1919                        continue;
1920                    }
1921                }
1922            }
1923            break;
1924        }
1925
1926        // Step 14. If start block is neither a block node nor an editing host,
1927        // or "span" is not an allowed child of start block,
1928        // or start block is a td or th, set start block to null.
1929        let start_block = if (!start_block.is_block_node() && !start_block.is_editing_host()) ||
1930            !is_allowed_child(
1931                NodeOrString::String("span".to_owned()),
1932                NodeOrString::Node(start_block.clone()),
1933            ) ||
1934            start_block.is::<HTMLTableCellElement>()
1935        {
1936            None
1937        } else {
1938            Some(start_block)
1939        };
1940
1941        // Step 15. Let end block be the active range's end node.
1942        let mut end_block = active_range.end_container();
1943
1944        // Step 16. While end block's parent is in the same editing host and end block is an inline node, set end block to its parent.
1945        loop {
1946            if end_block.is_inline_node() {
1947                if let Some(parent) = end_block.GetParentNode() {
1948                    if parent.same_editing_host(&end_block) {
1949                        end_block = parent;
1950                        continue;
1951                    }
1952                }
1953            }
1954            break;
1955        }
1956
1957        // Step 17. If end block is neither a block node nor an editing host, or "span" is not an allowed child of end block,
1958        // or end block is a td or th, set end block to null.
1959        let end_block = if (!end_block.is_block_node() && !end_block.is_editing_host()) ||
1960            !is_allowed_child(
1961                NodeOrString::String("span".to_owned()),
1962                NodeOrString::Node(end_block.clone()),
1963            ) ||
1964            end_block.is::<HTMLTableCellElement>()
1965        {
1966            None
1967        } else {
1968            Some(end_block)
1969        };
1970
1971        // Step 18.
1972        //
1973        // This step does not exist in the spec
1974
1975        // Step 19. Record current states and values, and let overrides be the result.
1976        let overrides = active_range.record_current_states_and_values();
1977
1978        // Step 20.
1979        //
1980        // This step does not exist in the spec
1981
1982        // Step 21. If start node and end node are the same, and start node is an editable Text node:
1983        if start_node == end_node && start_node.is_editable() {
1984            if let Some(start_text) = start_node.downcast::<Text>() {
1985                // Step 21.1. Call deleteData(start offset, end offset − start offset) on start node.
1986                if start_text
1987                    .upcast::<CharacterData>()
1988                    .DeleteData(start_offset, end_offset - start_offset)
1989                    .is_err()
1990                {
1991                    unreachable!("Must always be able to delete");
1992                }
1993                // Step 21.2. Canonicalize whitespace at (start node, start offset), with fix collapsed space false.
1994                start_node.canonicalize_whitespace(start_offset, false);
1995                // Step 21.3. If direction is "forward", call collapseToStart() on the context object's selection.
1996                if direction == SelectionDeleteDirection::Forward {
1997                    if self.CollapseToStart(CanGc::from_cx(cx)).is_err() {
1998                        unreachable!("Should be able to collapse to start");
1999                    }
2000                } else {
2001                    // Step 21.4. Otherwise, call collapseToEnd() on the context object's selection.
2002                    if self.CollapseToEnd(CanGc::from_cx(cx)).is_err() {
2003                        unreachable!("Should be able to collapse to end");
2004                    }
2005                }
2006                // Step 21.5. Restore states and values from overrides.
2007                active_range.restore_states_and_values(context_object, overrides);
2008
2009                // Step 21.6. Abort these steps.
2010                return;
2011            }
2012        }
2013
2014        // Step 22. If start node is an editable Text node, call deleteData() on it, with start offset as
2015        // the first argument and (length of start node − start offset) as the second argument.
2016        if start_node.is_editable() {
2017            if let Some(start_text) = start_node.downcast::<Text>() {
2018                if start_text
2019                    .upcast::<CharacterData>()
2020                    .DeleteData(start_offset, start_node.len() - start_offset)
2021                    .is_err()
2022                {
2023                    unreachable!("Must always be able to delete");
2024                }
2025            }
2026        }
2027
2028        // Step 23. Let node list be a list of nodes, initially empty.
2029        rooted_vec!(let mut node_list);
2030
2031        // Step 24. For each node contained in the active range, append node to node list if the
2032        // last member of node list (if any) is not an ancestor of node; node is editable;
2033        // and node is not a thead, tbody, tfoot, tr, th, or td.
2034        let Ok(contained_children) = active_range.contained_children() else {
2035            unreachable!("Must always have contained children");
2036        };
2037        for node in contained_children.contained_children {
2038            // This type is only used to tell the compiler how to handle the type of `node_list.last()`.
2039            // It is not allowed to add a `& DomRoot<Node>` annotation, as test-tidy disallows that.
2040            // However, if we omit the type, the compiler doesn't know what it is, since we also
2041            // aren't allowed to add a type annotation to `node_list` itself, as that is handled
2042            // by the `rooted_vec` macro. Lastly, we also can't make it `&Node`, since then the compiler
2043            // thinks that the contents of the `RootedVec` is `Node`, whereas it is should be
2044            // `RootedVec<DomRoot<Node>>`. The type alias here doesn't upset test-tidy,
2045            // while also providing the necessary information to the compiler to work.
2046            type DomRootNode = DomRoot<Node>;
2047            if node.is_editable() &&
2048                !(node.is::<HTMLTableSectionElement>() ||
2049                    node.is::<HTMLTableRowElement>() ||
2050                    node.is::<HTMLTableCellElement>()) &&
2051                node_list
2052                    .last()
2053                    .is_none_or(|last: &DomRootNode| !last.is_ancestor_of(&node))
2054            {
2055                node_list.push(node);
2056            }
2057        }
2058
2059        // Step 25. For each node in node list:
2060        for node in node_list.iter() {
2061            // Step 25.1. Let parent be the parent of node.
2062            let parent = node.GetParentNode().expect("Must always have a parent");
2063            // Step 25.2. Remove node from parent.
2064            assert!(node.has_parent());
2065            node.remove_self(cx);
2066            // Step 25.3. If the block node of parent has no visible children, and parent is editable or an editing host,
2067            // call createElement("br") on the context object and append the result as the last child of parent.
2068            if parent
2069                .block_node_of()
2070                .is_some_and(|block_node| block_node.children().all(|child| child.is_invisible())) &&
2071                parent.is_editable_or_editing_host()
2072            {
2073                let br = context_object.create_br_element(cx);
2074                if parent.AppendChild(cx, br.upcast()).is_err() {
2075                    unreachable!("Must always be able to append");
2076                }
2077            }
2078            // Step 25.4. If strip wrappers is true or parent is not an inclusive ancestor of start node,
2079            // while parent is an editable inline node with length 0, let grandparent be the parent of parent,
2080            // then remove parent from grandparent, then set parent to grandparent.
2081            if strip_wrappers == SelectionDeletionStripWrappers::Strip ||
2082                !parent.is_inclusive_ancestor_of(&start_node)
2083            {
2084                let mut parent = parent;
2085                loop {
2086                    if parent.is_editable() && parent.is_inline_node() && parent.is_empty() {
2087                        let grand_parent =
2088                            parent.GetParentNode().expect("Must always have a parent");
2089                        assert!(parent.has_parent());
2090                        parent.remove_self(cx);
2091                        parent = grand_parent;
2092                        continue;
2093                    }
2094                    break;
2095                }
2096            }
2097        }
2098
2099        // Step 26. If end node is an editable Text node, call deleteData(0, end offset) on it.
2100        if end_node.is_editable() {
2101            if let Some(end_text) = end_node.downcast::<Text>() {
2102                if end_text
2103                    .upcast::<CharacterData>()
2104                    .DeleteData(0, end_offset)
2105                    .is_err()
2106                {
2107                    unreachable!("Must always be able to delete");
2108                }
2109            }
2110        }
2111
2112        // Step 27. Canonicalize whitespace at the active range's start, with fix collapsed space false.
2113        active_range
2114            .start_container()
2115            .canonicalize_whitespace(active_range.start_offset(), false);
2116
2117        // Step 28. Canonicalize whitespace at the active range's end, with fix collapsed space false.
2118        active_range
2119            .end_container()
2120            .canonicalize_whitespace(active_range.end_offset(), false);
2121
2122        // Step 29.
2123        //
2124        // This step does not exist in the spec
2125
2126        // Step 30. If block merging is false, or start block or end block is null, or start block is not
2127        // in the same editing host as end block, or start block and end block are the same:
2128        if block_merging == SelectionDeletionBlockMerging::Skip ||
2129            start_block.as_ref().zip(end_block.as_ref()).is_none_or(
2130                |(start_block, end_block)| {
2131                    start_block == end_block || !start_block.same_editing_host(end_block)
2132                },
2133            )
2134        {
2135            // Step 30.1. If direction is "forward", call collapseToStart() on the context object's selection.
2136            if direction == SelectionDeleteDirection::Forward {
2137                if self.CollapseToStart(CanGc::from_cx(cx)).is_err() {
2138                    unreachable!("Should be able to collapse to start");
2139                }
2140            } else {
2141                // Step 30.2. Otherwise, call collapseToEnd() on the context object's selection.
2142                if self.CollapseToEnd(CanGc::from_cx(cx)).is_err() {
2143                    unreachable!("Should be able to collapse to end");
2144                }
2145            }
2146            // Step 30.3. Restore states and values from overrides.
2147            active_range.restore_states_and_values(context_object, overrides);
2148
2149            // Step 30.4. Abort these steps.
2150            return;
2151        }
2152        let start_block = start_block.expect("Already checked for None in previous statement");
2153        let end_block = end_block.expect("Already checked for None in previous statement");
2154
2155        // Step 31. If start block has one child, which is a collapsed block prop, remove its child from it.
2156        if start_block.children_count() == 1 {
2157            let Some(child) = start_block.children().nth(0) else {
2158                unreachable!("Must always have a single child");
2159            };
2160            if child.is_collapsed_block_prop() {
2161                assert!(child.has_parent());
2162                child.remove_self(cx);
2163            }
2164        }
2165
2166        // Step 32. If start block is an ancestor of end block:
2167        if start_block.is_ancestor_of(&end_block) {
2168            // Step 32.1. Let reference node be end block.
2169            let mut reference_node = end_block.clone();
2170            // Step 32.2. While reference node is not a child of start block, set reference node to its parent.
2171            loop {
2172                if start_block.children().all(|child| child != reference_node) {
2173                    reference_node = reference_node
2174                        .GetParentNode()
2175                        .expect("Must always have a parent, at least start_block");
2176                    continue;
2177                }
2178                break;
2179            }
2180            // Step 32.3. Call collapse() on the context object's selection,
2181            // with first argument start block and second argument the index of reference node.
2182            if self
2183                .Collapse(
2184                    Some(&start_block),
2185                    reference_node.index(),
2186                    CanGc::from_cx(cx),
2187                )
2188                .is_err()
2189            {
2190                unreachable!("Must always be able to collapse");
2191            }
2192            // Step 32.4. If end block has no children:
2193            if end_block.children_count() == 0 {
2194                let mut end_block = end_block;
2195                // Step 32.4.1. While end block is editable and is the only child of its parent and is not a child of start block,
2196                // let parent equal end block, then remove end block from parent, then set end block to parent.
2197                loop {
2198                    if end_block.is_editable() &&
2199                        start_block.children().all(|child| child != end_block)
2200                    {
2201                        if let Some(parent) = end_block.GetParentNode() {
2202                            if parent.children_count() == 1 {
2203                                assert!(end_block.has_parent());
2204                                end_block.remove_self(cx);
2205                                end_block = parent;
2206                                continue;
2207                            }
2208                        }
2209                    }
2210                    break;
2211                }
2212                // Step 32.4.2. If end block is editable and is not an inline node,
2213                // and its previousSibling and nextSibling are both inline nodes,
2214                // call createElement("br") on the context object and insert it into end block's parent immediately after end block.
2215                if end_block.is_editable() &&
2216                    !end_block.is_inline_node() &&
2217                    end_block
2218                        .GetPreviousSibling()
2219                        .is_some_and(|previous| previous.is_inline_node())
2220                {
2221                    if let Some(next_of_end_block) = end_block.GetNextSibling() {
2222                        if next_of_end_block.is_inline_node() {
2223                            let br = context_object.create_br_element(cx);
2224                            let parent = end_block
2225                                .GetParentNode()
2226                                .expect("Must always have a parent");
2227                            if parent
2228                                .InsertBefore(cx, br.upcast(), Some(&next_of_end_block))
2229                                .is_err()
2230                            {
2231                                unreachable!("Must always be able to insert into parent");
2232                            }
2233                        }
2234                    }
2235                }
2236                // Step 32.4.3. If end block is editable, remove it from its parent.
2237                if end_block.is_editable() {
2238                    assert!(end_block.has_parent());
2239                    end_block.remove_self(cx);
2240                }
2241                // Step 32.4.4. Restore states and values from overrides.
2242                active_range.restore_states_and_values(context_object, overrides);
2243
2244                // Step 32.4.5. Abort these steps.
2245                return;
2246            }
2247            let first_child = end_block
2248                .children()
2249                .nth(0)
2250                .expect("Already checked at least 1 child in previous statement");
2251            // Step 32.5. If end block's firstChild is not an inline node,
2252            // restore states and values from record, then abort these steps.
2253            if !first_child.is_inline_node() {
2254                // TODO: Restore state
2255                return;
2256            }
2257            // Step 32.6. Let children be a list of nodes, initially empty.
2258            rooted_vec!(let mut children);
2259            // Step 32.7. Append the first child of end block to children.
2260            children.push(first_child.as_traced());
2261            // Step 32.8. While children's last member is not a br,
2262            // and children's last member's nextSibling is an inline node,
2263            // append children's last member's nextSibling to children.
2264            loop {
2265                let Some(last) = children.last() else {
2266                    break;
2267                };
2268                if last.is::<HTMLBRElement>() {
2269                    break;
2270                }
2271                let Some(next) = last.GetNextSibling() else {
2272                    break;
2273                };
2274                if next.is_inline_node() {
2275                    children.push(next.as_traced());
2276                    continue;
2277                }
2278                break;
2279            }
2280            // Step 32.9. Record the values of children, and let values be the result.
2281            // TODO
2282
2283            // Step 32.10. While children's first member's parent is not start block,
2284            // split the parent of children.
2285            loop {
2286                if children
2287                    .first()
2288                    .and_then(|child| child.GetParentNode())
2289                    .is_some_and(|parent_of_child| parent_of_child != start_block)
2290                {
2291                    split_the_parent(cx, children.r());
2292                    continue;
2293                }
2294                break;
2295            }
2296            // Step 32.11. If children's first member's previousSibling is an editable br,
2297            // remove that br from its parent.
2298            if let Some(first) = children.first() {
2299                if let Some(previous_of_first) = first.GetPreviousSibling() {
2300                    if previous_of_first.is_editable() && previous_of_first.is::<HTMLBRElement>() {
2301                        assert!(previous_of_first.has_parent());
2302                        previous_of_first.remove_self(cx);
2303                    }
2304                }
2305            }
2306        // Step 33. Otherwise, if start block is a descendant of end block:
2307        } else if end_block.is_ancestor_of(&start_block) {
2308            // Step 33.1. Call collapse() on the context object's selection,
2309            // with first argument start block and second argument start block's length.
2310            if self
2311                .Collapse(Some(&start_block), start_block.len(), CanGc::from_cx(cx))
2312                .is_err()
2313            {
2314                unreachable!("Must always be able to collapse");
2315            }
2316            // Step 33.2. Let reference node be start block.
2317            let mut reference_node = start_block.clone();
2318            // Step 33.3. While reference node is not a child of end block, set reference node to its parent.
2319            loop {
2320                if end_block.children().all(|child| child != reference_node) {
2321                    if let Some(parent) = reference_node.GetParentNode() {
2322                        reference_node = parent;
2323                        continue;
2324                    }
2325                }
2326                break;
2327            }
2328            // Step 33.4. If reference node's nextSibling is an inline node and start block's lastChild is a br,
2329            // remove start block's lastChild from it.
2330            if reference_node
2331                .GetNextSibling()
2332                .is_some_and(|next| next.is_inline_node())
2333            {
2334                if let Some(last) = start_block.children().last() {
2335                    if last.is::<HTMLBRElement>() {
2336                        assert!(last.has_parent());
2337                        last.remove_self(cx);
2338                    }
2339                }
2340            }
2341            // Step 33.5. Let nodes to move be a list of nodes, initially empty.
2342            rooted_vec!(let mut nodes_to_move);
2343            // Step 33.6. If reference node's nextSibling is neither null nor a block node,
2344            // append it to nodes to move.
2345            if let Some(next) = reference_node.GetNextSibling() {
2346                if !next.is_block_node() {
2347                    nodes_to_move.push(next);
2348                }
2349            }
2350            // Step 33.7. While nodes to move is nonempty and its last member isn't a br
2351            // and its last member's nextSibling is neither null nor a block node,
2352            // append its last member's nextSibling to nodes to move.
2353            loop {
2354                if let Some(last) = nodes_to_move.last() {
2355                    if !last.is::<HTMLBRElement>() {
2356                        if let Some(next_of_last) = last.GetNextSibling() {
2357                            if !next_of_last.is_block_node() {
2358                                nodes_to_move.push(next_of_last);
2359                                continue;
2360                            }
2361                        }
2362                    }
2363                }
2364                break;
2365            }
2366            // Step 33.8. Record the values of nodes to move, and let values be the result.
2367            // TODO
2368
2369            // Step 33.9. For each node in nodes to move,
2370            // append node as the last child of start block, preserving ranges.
2371            for node in nodes_to_move.iter() {
2372                // TODO: Preserve ranges
2373                if start_block.AppendChild(cx, node).is_err() {
2374                    unreachable!("Must always be able to append");
2375                }
2376            }
2377        // Step 34. Otherwise:
2378        } else {
2379            // Step 34.1. Call collapse() on the context object's selection,
2380            // with first argument start block and second argument start block's length.
2381            if self
2382                .Collapse(Some(&start_block), start_block.len(), CanGc::from_cx(cx))
2383                .is_err()
2384            {
2385                unreachable!("Must always be able to collapse");
2386            }
2387            // Step 34.2. If end block's firstChild is an inline node and start block's lastChild is a br,
2388            // remove start block's lastChild from it.
2389            if end_block
2390                .children()
2391                .nth(0)
2392                .is_some_and(|next| next.is_inline_node())
2393            {
2394                if let Some(last) = start_block.children().last() {
2395                    if last.is::<HTMLBRElement>() {
2396                        assert!(last.has_parent());
2397                        last.remove_self(cx);
2398                    }
2399                }
2400            }
2401            // Step 34.3. Record the values of end block's children, and let values be the result.
2402            // TODO
2403
2404            // Step 34.4. While end block has children,
2405            // append the first child of end block to start block, preserving ranges.
2406            loop {
2407                if let Some(first_child) = end_block.children().nth(0) {
2408                    // TODO: Preserve ranges
2409                    if start_block.AppendChild(cx, &first_child).is_err() {
2410                        unreachable!("Must always be able to append");
2411                    }
2412                    continue;
2413                }
2414                break;
2415            }
2416            // Step 34.5. While end block has no children,
2417            // let parent be the parent of end block, then remove end block from parent,
2418            // then set end block to parent.
2419            let mut end_block = end_block;
2420            loop {
2421                if end_block.children_count() == 0 {
2422                    if let Some(parent) = end_block.GetParentNode() {
2423                        assert!(end_block.has_parent());
2424                        end_block.remove_self(cx);
2425                        end_block = parent;
2426                        continue;
2427                    }
2428                }
2429                break;
2430            }
2431        }
2432
2433        // Step 35.
2434        //
2435        // This step does not exist in the spec
2436
2437        // Step 36. Let ancestor be start block.
2438        // TODO
2439
2440        // Step 37. While ancestor has an inclusive ancestor ol in the same editing host whose nextSibling is
2441        // also an ol in the same editing host, or an inclusive ancestor ul in the same editing host whose nextSibling
2442        // is also a ul in the same editing host:
2443        // TODO
2444
2445        // Step 38. Restore the values from values.
2446        // TODO
2447
2448        // Step 39. If start block has no children, call createElement("br") on the context object and
2449        // append the result as the last child of start block.
2450        if start_block.children_count() == 0 {
2451            let br = context_object.create_br_element(cx);
2452            if start_block.AppendChild(cx, br.upcast()).is_err() {
2453                unreachable!("Must always be able to append");
2454            }
2455        }
2456
2457        // Step 40. Remove extraneous line breaks at the end of start block.
2458        start_block.remove_extraneous_line_breaks_at_the_end_of(cx);
2459
2460        // Step 41. Restore states and values from overrides.
2461        active_range.restore_states_and_values(context_object, overrides);
2462    }
2463}