Skip to main content

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