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