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