Skip to main content

script/dom/execcommand/contenteditable/
node.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::cmp::Ordering;
6use std::ops::Deref;
7
8use cssparser::color::OPAQUE;
9use html5ever::local_name;
10use js::context::{JSContext, NoGC};
11use script_bindings::codegen::GenericBindings::RangeBinding::RangeMethods;
12use script_bindings::inheritance::Castable;
13use style::attr::parse_legacy_color;
14use style::values::specified::box_::DisplayOutside;
15
16use crate::dom::Document;
17use crate::dom::abstractrange::bp_position;
18use crate::dom::bindings::codegen::Bindings::CharacterDataBinding::CharacterDataMethods;
19use crate::dom::bindings::codegen::Bindings::DocumentBinding::DocumentMethods;
20use crate::dom::bindings::codegen::Bindings::HTMLAnchorElementBinding::HTMLAnchorElementMethods;
21use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods;
22use crate::dom::bindings::error::Fallible;
23use crate::dom::bindings::inheritance::NodeTypeId;
24use crate::dom::bindings::root::{DomRoot, DomSlice, UnrootedDom};
25use crate::dom::bindings::str::DOMString;
26use crate::dom::characterdata::CharacterData;
27use crate::dom::element::{AdjacentPosition, Element};
28use crate::dom::execcommand::basecommand::{CommandName, CssPropertyName};
29use crate::dom::execcommand::commands::forecolor::serialize_to_simple_color;
30use crate::dom::html::htmlanchorelement::HTMLAnchorElement;
31use crate::dom::html::htmlbrelement::HTMLBRElement;
32use crate::dom::html::htmlelement::HTMLElement;
33use crate::dom::html::htmlimageelement::HTMLImageElement;
34use crate::dom::html::htmllielement::HTMLLIElement;
35use crate::dom::html::htmlolistelement::HTMLOListElement;
36use crate::dom::html::htmlulistelement::HTMLUListElement;
37use crate::dom::iterators::ShadowIncluding;
38use crate::dom::node::{Node, NodeTraits};
39use crate::dom::text::Text;
40
41pub(crate) enum NodeOrString<'a> {
42    String(String),
43    Node(UnrootedDom<'a, Node>),
44}
45
46impl<'a> NodeOrString<'a> {
47    pub(crate) fn from_node(node: &Node, no_gc: &'a NoGC) -> NodeOrString<'a> {
48        NodeOrString::Node(UnrootedDom::from_ref(node, no_gc))
49    }
50
51    fn as_node(&self) -> Option<UnrootedDom<'a, Node>> {
52        match self {
53            NodeOrString::String(_) => None,
54            NodeOrString::Node(node) => Some(node.clone()),
55        }
56    }
57}
58
59// This is a macro instead of a method on `NodeOrString` since `UnrootedDom::downcast`
60// would result in `element` being a temporary referenced value. Therefore, it wouldn't
61// be possible to return its localname as `&str`. Instead, we need to compare immediately
62// when we have the element referenced, which is why we have to duplicate the `matches!`.
63macro_rules! node_or_string_matches_name(
64    ( $node_or_string:ident, $pattern:pat $(if $guard:expr)? $(,)? ) => (
65        match $node_or_string {
66            NodeOrString::String(ref str_) => matches!(str_.as_ref(), $pattern),
67            NodeOrString::Node(ref node) => UnrootedDom::downcast::<Element>(node.clone())
68                .map(|element| matches!(element.local_name().as_ref(), $pattern))
69                .unwrap_or_default(),
70        }
71    );
72);
73
74macro_rules! node_matches_local_name(
75    ( $node:ident, $pattern:pat $(if $guard:expr)? $(,)? ) => (
76        $node.downcast::<Element>().is_some_and(|element|
77            matches!(
78                *element.local_name(),
79                $pattern
80            )
81        )
82    );
83);
84
85pub(crate) use node_matches_local_name;
86
87/// <https://w3c.github.io/editing/docs/execCommand/#prohibited-paragraph-child-name>
88const PROHIBITED_PARAGRAPH_CHILD_NAMES: [&str; 47] = [
89    "address",
90    "article",
91    "aside",
92    "blockquote",
93    "caption",
94    "center",
95    "col",
96    "colgroup",
97    "dd",
98    "details",
99    "dir",
100    "div",
101    "dl",
102    "dt",
103    "fieldset",
104    "figcaption",
105    "figure",
106    "footer",
107    "form",
108    "h1",
109    "h2",
110    "h3",
111    "h4",
112    "h5",
113    "h6",
114    "header",
115    "hgroup",
116    "hr",
117    "li",
118    "listing",
119    "menu",
120    "nav",
121    "ol",
122    "p",
123    "plaintext",
124    "pre",
125    "section",
126    "summary",
127    "table",
128    "tbody",
129    "td",
130    "tfoot",
131    "th",
132    "thead",
133    "tr",
134    "ul",
135    "xmp",
136];
137/// <https://w3c.github.io/editing/docs/execCommand/#name-of-an-element-with-inline-contents>
138const NAME_OF_AN_ELEMENT_WITH_INLINE_CONTENTS: [&str; 43] = [
139    "a", "abbr", "b", "bdi", "bdo", "cite", "code", "dfn", "em", "h1", "h2", "h3", "h4", "h5",
140    "h6", "i", "kbd", "mark", "p", "pre", "q", "rp", "rt", "ruby", "s", "samp", "small", "span",
141    "strong", "sub", "sup", "u", "var", "acronym", "listing", "strike", "xmp", "big", "blink",
142    "font", "marquee", "nobr", "tt",
143];
144
145/// <https://w3c.github.io/editing/docs/execCommand/#element-with-inline-contents>
146fn is_element_with_inline_contents(element: &Node) -> bool {
147    // > An element with inline contents is an HTML element whose local name is a name of an element with inline contents.
148    let Some(html_element) = element.downcast::<HTMLElement>() else {
149        return false;
150    };
151    NAME_OF_AN_ELEMENT_WITH_INLINE_CONTENTS.contains(&html_element.local_name())
152}
153
154/// <https://w3c.github.io/editing/docs/execCommand/#preserving-ranges>
155pub(crate) fn move_preserving_ranges<Move>(cx: &mut JSContext, node: &Node, mut move_: Move)
156where
157    Move: FnMut(&mut JSContext) -> Fallible<DomRoot<Node>>,
158{
159    // Step 1. Let node be the moved node, old parent and old index be the old parent
160    // (which may be null) and index, and new parent and new index be the new parent and index.
161    let old_parent = node.GetParentNode();
162    let old_index = node.index();
163
164    let selection = node
165        .owner_document()
166        .GetSelection(cx)
167        .expect("Must always have a selection");
168    let active_range = selection
169        .active_range(cx)
170        .expect("Must always have an active range");
171
172    // Selection is currently implemented as a live range (not great!), which means that
173    // we need to record the old boundary point (container and offset) before actually
174    // performing the move operation and then apply the editing spec's range update
175    // algorithm to this original boundary point.
176    let start_container = active_range.start_container();
177    let start_offset = active_range.start_offset();
178    let end_container = active_range.end_container();
179    let end_offset = active_range.end_offset();
180
181    let should_adjust_start = !node.is_inclusive_ancestor_of(&start_container);
182    let should_adjust_end = !node.is_inclusive_ancestor_of(&end_container);
183
184    if move_(cx).is_err() {
185        unreachable!("Must always be able to move");
186    }
187
188    let new_parent = node.GetParentNode().expect("Must always have a new parent");
189    let new_index = node.index();
190
191    let adjust_boundary_point = |mut container, mut offset, should_adjust: bool| {
192        // Step 2. If a boundary point's node is the same as or a descendant of node, leave it
193        // unchanged, so it moves to the new location.
194        //
195        // From the spec:
196        // > This is actually implicit, but I state it anyway for completeness.
197        //
198        // However, this does not seem to be implicit for text nodes that are partially/fully
199        // selected. In these cases, we shouldn't update the offsets to the new parent, but
200        // instead retain them on the original text node. Therefore, if that's the case,
201        // update them here and immediately return.
202        if !should_adjust {
203            return (container, offset);
204        }
205
206        // Step 3. If a boundary point's node is new parent and its offset is greater than new
207        // index, add one to its offset.
208        if container == new_parent && offset > new_index {
209            offset += 1;
210        }
211
212        if let Some(old_parent) = old_parent.as_ref() {
213            // Step 4. If a boundary point's node is old parent and its offset is old
214            // index or old index + 1, set its node to new parent and add new index −
215            // old index to its offset.
216            if container == *old_parent && (offset == old_index || offset == old_index + 1) {
217                container = new_parent.clone();
218                offset += new_index;
219                offset -= old_index;
220            }
221
222            // Step 5. If a boundary point's node is old parent and its offset is
223            // greater than old index + 1, subtract one from its offset.
224            if container == *old_parent && (offset > old_index + 1) {
225                offset -= 1;
226            }
227        }
228
229        (container, offset)
230    };
231
232    let (new_start_container, new_start_offset) =
233        adjust_boundary_point(start_container, start_offset, should_adjust_start);
234    let (new_end_container, new_end_offset) =
235        adjust_boundary_point(end_container, end_offset, should_adjust_end);
236
237    let _ = active_range.SetStart(cx.no_gc(), &new_start_container, new_start_offset);
238    let _ = active_range.SetEnd(cx.no_gc(), &new_end_container, new_end_offset);
239}
240
241/// <https://w3c.github.io/editing/docs/execCommand/#allowed-child>
242pub(crate) fn is_allowed_child(child: NodeOrString, parent: NodeOrString) -> bool {
243    // Step 1. If parent is "colgroup", "table", "tbody", "tfoot", "thead", "tr",
244    // or an HTML element with local name equal to one of those,
245    // and child is a Text node whose data does not consist solely of space characters, return false.
246    if node_or_string_matches_name!(
247        parent,
248        "colgroup" | "table" | "tbody" | "tfoot" | "thead" | "tr"
249    ) && child.as_node().is_some_and(|node| {
250        // Note: cannot use `.and_then` here, since `downcast` would outlive its reference
251        node.downcast::<Text>()
252            .is_some_and(|text| !text.data().bytes().all(|byte| byte == b' '))
253    }) {
254        return false;
255    }
256    // Step 2. If parent is "script", "style", "plaintext", or "xmp",
257    // or an HTML element with local name equal to one of those, and child is not a Text node, return false.
258    if node_or_string_matches_name!(parent, "script" | "style" | "plaintext" | "xmp") &&
259        child.as_node().is_none_or(|node| !node.is::<Text>())
260    {
261        return false;
262    }
263    // Step 3. If child is a document, DocumentFragment, or DocumentType, return false.
264    if let NodeOrString::Node(ref node) = child &&
265        matches!(
266            node.type_id(),
267            NodeTypeId::Document(_) | NodeTypeId::DocumentFragment(_) | NodeTypeId::DocumentType
268        )
269    {
270        return false;
271    }
272    // Step 4. If child is an HTML element, set child to the local name of child.
273    let child_name = match child {
274        NodeOrString::String(str_) => str_,
275        NodeOrString::Node(node) => match node.downcast::<HTMLElement>() {
276            // Step 5. If child is not a string, return true.
277            None => return true,
278            Some(html_element) => html_element.local_name().to_owned(),
279        },
280    };
281    let child = child_name.as_str();
282    let parent_name = match parent {
283        NodeOrString::String(str_) => str_,
284        NodeOrString::Node(parent) => {
285            // Step 6. If parent is an HTML element:
286            if let Some(parent_element) = parent.downcast::<HTMLElement>() {
287                // Step 6.1. If child is "a", and parent or some ancestor of parent is an a, return false.
288                if child == "a" &&
289                    parent
290                        .inclusive_ancestors(ShadowIncluding::No)
291                        .any(|node| node.is::<HTMLAnchorElement>())
292                {
293                    return false;
294                }
295                // Step 6.2. If child is a prohibited paragraph child name and parent or some ancestor of parent
296                // is an element with inline contents, return false.
297                if PROHIBITED_PARAGRAPH_CHILD_NAMES.contains(&child) &&
298                    parent
299                        .inclusive_ancestors(ShadowIncluding::No)
300                        .any(|node| is_element_with_inline_contents(&node))
301                {
302                    return false;
303                }
304                // Step 6.3. If child is "h1", "h2", "h3", "h4", "h5", or "h6",
305                // and parent or some ancestor of parent is an HTML element with local name
306                // "h1", "h2", "h3", "h4", "h5", or "h6", return false.
307                if matches!(child, "h1" | "h2" | "h3" | "h4" | "h5" | "h6") &&
308                    parent.inclusive_ancestors(ShadowIncluding::No).any(|node| {
309                        node.downcast::<HTMLElement>().is_some_and(|html_element| {
310                            matches!(
311                                html_element.local_name(),
312                                "h1" | "h2" | "h3" | "h4" | "h5" | "h6"
313                            )
314                        })
315                    })
316                {
317                    return false;
318                }
319                // Step 6.4. Let parent be the local name of parent.
320                parent_element.local_name().to_owned()
321            } else {
322                // Step 7. If parent is an Element or DocumentFragment, return true.
323                // Step 8. If parent is not a string, return false.
324                return matches!(
325                    parent.type_id(),
326                    NodeTypeId::DocumentFragment(_) | NodeTypeId::Element(_)
327                );
328            }
329        },
330    };
331    let parent = parent_name.as_str();
332    // Step 9. If parent is on the left-hand side of an entry on the following list,
333    // then return true if child is listed on the right-hand side of that entry, and false otherwise.
334    match parent {
335        "colgroup" => return child == "col",
336        "table" => {
337            return matches!(
338                child,
339                "caption" | "col" | "colgroup" | "tbody" | "td" | "tfoot" | "th" | "thead" | "tr"
340            );
341        },
342        "tbody" | "tfoot" | "thead" => return matches!(child, "td" | "th" | "tr"),
343        "tr" => return matches!(child, "td" | "th"),
344        "dl" => return matches!(child, "dt" | "dd"),
345        "dir" | "ol" | "ul" => return matches!(child, "dir" | "li" | "ol" | "ul"),
346        "hgroup" => return matches!(child, "h1" | "h2" | "h3" | "h4" | "h5" | "h6"),
347        _ => {},
348    };
349    // Step 10. If child is "body", "caption", "col", "colgroup", "frame", "frameset", "head",
350    // "html", "tbody", "td", "tfoot", "th", "thead", or "tr", return false.
351    if matches!(
352        child,
353        "body" |
354            "caption" |
355            "col" |
356            "colgroup" |
357            "frame" |
358            "frameset" |
359            "head" |
360            "html" |
361            "tbody" |
362            "td" |
363            "tfoot" |
364            "th" |
365            "thead" |
366            "tr"
367    ) {
368        return false;
369    }
370    // Step 11. If child is "dd" or "dt" and parent is not "dl", return false.
371    if matches!(child, "dd" | "dt") && parent != "dl" {
372        return false;
373    }
374    // Step 12. If child is "li" and parent is not "ol" or "ul", return false.
375    if child == "li" && !matches!(parent, "ol" | "ul") {
376        return false;
377    }
378    // Step 13. If parent is on the left-hand side of an entry on the following list
379    // and child is listed on the right-hand side of that entry, return false.
380    if match parent {
381        "a" => child == "a",
382        "dd" | "dt" => matches!(child, "dd" | "dt"),
383        "h1" | "h2" | "h3" | "h4" | "h5" | "h6" => {
384            matches!(child, "h1" | "h2" | "h3" | "h4" | "h5" | "h6")
385        },
386        "li" => child == "li",
387        "nobr" => child == "nobr",
388        "td" | "th" => {
389            matches!(
390                child,
391                "caption" | "col" | "colgroup" | "tbody" | "td" | "tfoot" | "th" | "thead" | "tr"
392            )
393        },
394        _ if NAME_OF_AN_ELEMENT_WITH_INLINE_CONTENTS.contains(&parent) => {
395            PROHIBITED_PARAGRAPH_CHILD_NAMES.contains(&child)
396        },
397        _ => false,
398    } {
399        return false;
400    }
401    // Step 14. Return true.
402    true
403}
404
405/// <https://w3c.github.io/editing/docs/execCommand/#split-the-parent>
406pub(crate) fn split_the_parent<'a>(cx: &mut JSContext, node_list: &'a [&'a Node]) {
407    assert!(!node_list.is_empty());
408    // Step 1. Let original parent be the parent of the first member of node list.
409    let Some(original_parent) = node_list.first().and_then(|first| first.GetParentNode()) else {
410        return;
411    };
412    let context_object = original_parent.owner_document();
413    // Step 2. If original parent is not editable or its parent is null, do nothing and abort these steps.
414    if !original_parent.is_editable() {
415        return;
416    }
417    let Some(parent_of_original_parent) = original_parent.GetParentNode() else {
418        return;
419    };
420    // Step 3. If the first child of original parent is in node list, remove extraneous line breaks before original parent.
421    if original_parent
422        .children()
423        .next()
424        .is_some_and(|first_child| node_list.contains(&first_child.deref()))
425    {
426        original_parent.remove_extraneous_line_breaks_before(cx);
427    }
428    // Step 4. If the first child of original parent is in node list, and original parent follows a line break,
429    // set follows line break to true. Otherwise, set follows line break to false.
430    let first_child_is_in_node_list = original_parent
431        .children()
432        .next()
433        .is_some_and(|first_child| node_list.contains(&first_child.deref()));
434    let follows_line_break =
435        first_child_is_in_node_list && original_parent.follows_a_line_break(cx.no_gc());
436    // Step 5. If the last child of original parent is in node list, and original parent precedes a line break,
437    // set precedes line break to true. Otherwise, set precedes line break to false.
438    let last_child_is_in_node_list = original_parent
439        .children()
440        .last()
441        .is_some_and(|last_child| node_list.contains(&last_child.deref()));
442    let precedes_line_break =
443        last_child_is_in_node_list && original_parent.precedes_a_line_break(cx.no_gc());
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(cx.no_gc())
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(cx.no_gc())
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(cx.no_gc())
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(cx.no_gc()) && !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(cx.no_gc()) {
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(cx.no_gc()) {
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(cx))
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                let _ = range.SetStart(cx.no_gc(), &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                let _ = range.SetEnd(cx.no_gc(), &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(cx.no_gc(), 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(cx.no_gc()))
701                .is_some_and(|child| child.is_inline_node()) &&
702            node_list
703                .iter()
704                .find(|node| node.is_visible(cx.no_gc()))
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(cx.no_gc()))
731                .is_some_and(|child| child.is_inline_node()) &&
732            node_list
733                .iter()
734                .rev()
735                .find(|node| node.is_visible(cx.no_gc()))
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::from_node(self, cx.no_gc()),
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(cx.no_gc()) {
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::from_node(self, cx.no_gc()),
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, no_gc: &NoGC) -> 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(no_gc))
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(no_gc) {
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, no_gc: &NoGC) -> bool {
1564        // > Something is invisible if it is a node that is not visible.
1565        !self.is_visible(no_gc)
1566    }
1567
1568    /// <https://w3c.github.io/editing/docs/execCommand/#formattable-node>
1569    pub(crate) fn is_formattable(&self, no_gc: &NoGC) -> 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(no_gc) &&
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, no_gc: &NoGC, 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_unrooted(no_gc)
1598            .nth(offset - 1)
1599            .is_some_and(|child| {
1600                child.is_visible(no_gc) && (child.is_block_node() || child.is::<HTMLBRElement>())
1601            })
1602    }
1603
1604    /// <https://w3c.github.io/editing/docs/execCommand/#block-end-point>
1605    pub(crate) fn is_block_end_point(&self, offset: u32, no_gc: &NoGC) -> bool {
1606        // > A boundary point (node, offset) is a block end point if either node's parent is null and offset is node's length;
1607        if self.GetParentNode().is_none() && offset == self.len() {
1608            return true;
1609        }
1610        // > or node has a child with index offset, and that child is a visible block node.
1611        self.children()
1612            .nth(offset as usize)
1613            .is_some_and(|child| child.is_visible(no_gc) && child.is_block_node())
1614    }
1615
1616    /// <https://w3c.github.io/editing/docs/execCommand/#block-boundary-point>
1617    pub(crate) fn is_block_boundary_point(&self, no_gc: &NoGC, offset: u32) -> bool {
1618        // > A boundary point is a block boundary point if it is either a block start point or a block end point.
1619        self.is_block_start_point(no_gc, offset as usize) || self.is_block_end_point(offset, no_gc)
1620    }
1621
1622    pub(crate) fn is_no_allowed_child_in_same_editing_host(&self, no_gc: &NoGC) -> bool {
1623        // > If node is not an allowed child of any of its ancestors in the same editing host
1624        let Some(editing_host) = self.editing_host_of() else {
1625            return false;
1626        };
1627        let self_unrooted = UnrootedDom::from_ref(self, no_gc);
1628        self.ancestors_unrooted(no_gc)
1629            .take_while(|ancestor| ancestor.editing_host_of().as_ref() == Some(&editing_host))
1630            .all(|ancestor| {
1631                !is_allowed_child(
1632                    NodeOrString::Node(self_unrooted.clone()),
1633                    NodeOrString::Node(ancestor),
1634                )
1635            })
1636    }
1637
1638    /// <https://w3c.github.io/editing/docs/execCommand/#prohibited-paragraph-child>
1639    pub(crate) fn is_prohibited_paragraph_child(&self) -> bool {
1640        // > A prohibited paragraph child is an HTML element whose local name is a prohibited paragraph child name.
1641        let Some(node_as_element) = self.downcast::<HTMLElement>() else {
1642            return false;
1643        };
1644        PROHIBITED_PARAGRAPH_CHILD_NAMES.contains(&node_as_element.local_name())
1645    }
1646
1647    /// <https://w3c.github.io/editing/docs/execCommand/#fix-disallowed-ancestors>
1648    pub(crate) fn fix_disallowed_ancestors(&self, cx: &mut JSContext, context_object: &Document) {
1649        // Step 1. If node is not editable, abort these steps.
1650        if !self.is_editable() {
1651            return;
1652        }
1653        // Step 2. If node is not an allowed child of any of its ancestors in the same editing host:
1654        if self.is_no_allowed_child_in_same_editing_host(cx.no_gc()) {
1655            // Step 2.1. If node is a dd or dt, wrap the one-node list consisting of node,
1656            // with sibling criteria returning true for any dl with no attributes and false otherwise,
1657            // and new parent instructions returning
1658            // the result of calling createElement("dl") on the context object.
1659            // Then abort these steps.
1660            if node_matches_local_name!(self, local_name!("dd") | local_name!("dt")) {
1661                wrap_node_list(
1662                    cx,
1663                    vec![DomRoot::from_ref(self)],
1664                    |sibling| {
1665                        sibling
1666                            .downcast::<Element>()
1667                            .is_some_and(|sibling_element| {
1668                                let attrs = sibling_element.attrs().borrow();
1669                                sibling_element.local_name() == &local_name!("dl") &&
1670                                    attrs.is_empty()
1671                            })
1672                    },
1673                    |cx| Some(DomRoot::upcast(context_object.create_element(cx, "dl"))),
1674                );
1675                return;
1676            }
1677            // Step 2.2. If "p" is not an allowed child of the editing host of node,
1678            // abort these steps.
1679            if let Some(editing_host) = self.editing_host_of() &&
1680                !is_allowed_child(
1681                    NodeOrString::String("p".to_owned()),
1682                    NodeOrString::from_node(&editing_host, cx.no_gc()),
1683                )
1684            {
1685                return;
1686            }
1687            // Step 2.3. If node is not a prohibited paragraph child, abort these steps.
1688            if !self.is_prohibited_paragraph_child() {
1689                return;
1690            }
1691            // Step 2.4. Set the tag name of node to the default single-line container name,
1692            // and let node be the result.
1693            let node = self
1694                .downcast::<Element>()
1695                .expect("Must always be an element")
1696                .set_the_tag_name(
1697                    cx,
1698                    context_object.default_single_line_container_name().str(),
1699                );
1700            // Step 2.5. Fix disallowed ancestors of node.
1701            //
1702            // NOTE: We should only do this if we actually changed node. If node didn't change
1703            // (for example it was already the correct tag), then we would infinitely recurse
1704            // here. Therefore, we should check if we changed the node and only then do it
1705            // again.
1706            if *node != *self {
1707                node.fix_disallowed_ancestors(cx, context_object);
1708            }
1709            // Step 2.6. Let children be node's children.
1710            let children = node.children().collect::<Vec<DomRoot<Node>>>();
1711            // Step 2.7. For each child in children, if child is a prohibited paragraph child:
1712            for child in children {
1713                if child.is_prohibited_paragraph_child() {
1714                    // Step 2.7.1. Record the values of the one-node list consisting of child,
1715                    // and let values be the result.
1716                    let values = record_the_values(vec![child.clone()]);
1717                    // Step 2.7.2. Split the parent of the one-node list consisting of child.
1718                    split_the_parent(cx, &[&child]);
1719                    // Step 2.7.3. Restore the values from values.
1720                    restore_the_values(cx, values);
1721                }
1722            }
1723            // Step 2.8. Abort these steps.
1724            return;
1725        }
1726        // Step 3. Record the values of the one-node list consisting of node, and let values be the result.
1727        let values = record_the_values(vec![DomRoot::from_ref(self)]);
1728        // Step 4. While node is not an allowed child of its parent,
1729        // split the parent of the one-node list consisting of node.
1730        loop {
1731            let Some(parent) = self.GetParentNode() else {
1732                break;
1733            };
1734            if is_allowed_child(
1735                NodeOrString::from_node(self, cx.no_gc()),
1736                NodeOrString::from_node(&parent, cx.no_gc()),
1737            ) {
1738                break;
1739            }
1740            split_the_parent(cx, &[self]);
1741        }
1742        // Step 5. Restore the values from values.
1743        restore_the_values(cx, values);
1744    }
1745
1746    /// <https://w3c.github.io/editing/docs/execCommand/#normalize-sublists>
1747    pub(crate) fn normalize_sublists(&self, cx: &mut JSContext) {
1748        let item_element = self
1749            .downcast::<Element>()
1750            .expect("item should be an element.");
1751
1752        // Step 1. If item is not an li or it is not editable or its parent is not editable, abort these steps.
1753        if !self.is::<HTMLLIElement>() ||
1754            !self.is_editable() ||
1755            !self
1756                .GetParentElement()
1757                .is_some_and(|parent| parent.upcast::<Node>().is_editable())
1758        {
1759            return;
1760        }
1761
1762        // Step 2. Let new item be null.
1763        let mut new_item: Option<DomRoot<Element>> = None;
1764
1765        // Step 3. While item has an ol or ul child:
1766        while self
1767            .child_elements()
1768            .any(|child| child.is::<HTMLOListElement>() || child.is::<HTMLUListElement>())
1769        {
1770            // Step 3.1. Let child be the last child of item.
1771            let child = self.GetLastChild().expect("Must have a last child here.");
1772
1773            // Step 3.2. If child is an ol or ul, or new item is null and child is a Text node whose data consists of zero of more space characters:
1774            if child.is::<HTMLOListElement>() ||
1775                child.is::<HTMLUListElement>() ||
1776                (new_item.is_none() &&
1777                    child
1778                        .downcast::<Text>()
1779                        .is_some_and(|text| text.data().bytes().all(|byte| byte == b' ')))
1780            {
1781                // Step 3.2.1. Set new item to null.
1782                new_item = None;
1783
1784                // Step 3.2.2. Insert child into the parent of item immediately following item, preserving ranges.
1785                move_preserving_ranges(cx, &child, |cx| {
1786                    item_element
1787                        .insert_adjacent(cx, AdjacentPosition::AfterEnd, &child)
1788                        .map(|elem| elem.expect("Should have inserted"))
1789                });
1790
1791                continue;
1792            }
1793            // Step 3.3. Otherwise:
1794            // Step 3.3.1. If new item is null,
1795            //             let new item be the result of calling createElement("li") on the ownerDocument of item,
1796            //             then insert new item into the parent of item immediately after item.
1797            if new_item.is_none() {
1798                new_item = Some(self.owner_document().create_element(cx, "li"));
1799                item_element
1800                    .insert_adjacent(cx, AdjacentPosition::AfterEnd, &child)
1801                    .expect("Insertion should always work here.");
1802            }
1803
1804            // Step 3.3.2. Insert child into new item as its first child, preserving ranges.
1805            move_preserving_ranges(cx, &child, |cx| {
1806                new_item
1807                    .as_ref()
1808                    .expect("Must have new item here")
1809                    .downcast::<Element>()
1810                    .expect("New item must be able to support children")
1811                    .insert_adjacent(cx, AdjacentPosition::AfterBegin, &child)
1812                    .map(|elem| elem.expect("Should have inserted"))
1813            });
1814        }
1815    }
1816
1817    /// <https://w3c.github.io/editing/docs/execCommand/#collapsed-block-prop>
1818    pub(crate) fn is_collapsed_block_prop(&self, no_gc: &NoGC) -> bool {
1819        // > A collapsed block prop is either a collapsed line break that is not an extraneous line break,
1820
1821        // TODO: Check for collapsed line break
1822        if self
1823            .downcast::<HTMLBRElement>()
1824            .is_some_and(|br| !br.is_extraneous_line_break())
1825        {
1826            return true;
1827        }
1828        // > or an Element that is an inline node and whose children are all either invisible or collapsed block props
1829        if !self.is::<Element>() {
1830            return false;
1831        };
1832        if !self.is_inline_node() {
1833            return false;
1834        }
1835        let mut at_least_one_collapsed_block_prop = false;
1836        for child in self.children_unrooted(no_gc) {
1837            if child.is_collapsed_block_prop(no_gc) {
1838                at_least_one_collapsed_block_prop = true;
1839                continue;
1840            }
1841            if child.is_invisible(no_gc) {
1842                continue;
1843            }
1844
1845            return false;
1846        }
1847        // > and that has at least one child that is a collapsed block prop.
1848        at_least_one_collapsed_block_prop
1849    }
1850
1851    /// <https://w3c.github.io/editing/docs/execCommand/#follows-a-line-break>
1852    fn follows_a_line_break(&self, no_gc: &NoGC) -> bool {
1853        // Step 1. Let offset be zero.
1854        let mut offset = 0;
1855        // Step 2. While (node, offset) is not a block boundary point:
1856        let mut node = DomRoot::from_ref(self);
1857        while !node.is_block_boundary_point(no_gc, offset) {
1858            // Step 2.2. If offset is zero or node has no children, set offset to node's index, then set node to its parent.
1859            if offset == 0 || node.children_count() == 0 {
1860                offset = node.index();
1861                node = node.GetParentNode().expect("Must always have a parent");
1862                continue;
1863            }
1864            // Step 2.1. If node has a visible child with index offset minus one, return false.
1865            let child = node.children().nth(offset as usize - 1);
1866            let Some(child) = child else {
1867                return false;
1868            };
1869            if child.is_visible(no_gc) {
1870                return false;
1871            }
1872            // Step 2.3. Otherwise, set node to its child with index offset minus one, then set offset to node's length.
1873            node = child;
1874            offset = node.len();
1875        }
1876        // Step 3. Return true.
1877        true
1878    }
1879
1880    /// <https://w3c.github.io/editing/docs/execCommand/#precedes-a-line-break>
1881    fn precedes_a_line_break(&self, no_gc: &NoGC) -> bool {
1882        let mut node = DomRoot::from_ref(self);
1883        // Step 1. Let offset be node's length.
1884        let mut offset = node.len();
1885        // Step 2. While (node, offset) is not a block boundary point:
1886        while !node.is_block_boundary_point(no_gc, offset) {
1887            // Step 2.1. If node has a visible child with index offset, return false.
1888            if node
1889                .children()
1890                .nth(offset as usize)
1891                .is_some_and(|child| child.is_visible(no_gc))
1892            {
1893                return false;
1894            }
1895            // 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.
1896            if offset == node.len() || node.children_count() == 0 {
1897                offset = 1 + node.index();
1898                node = node.GetParentNode().expect("Must always have a parent");
1899                continue;
1900            }
1901            // Step 2.3. Otherwise, set node to its child with index offset and set offset to zero.
1902            let child = node.children().nth(offset as usize);
1903            node = match child {
1904                None => return false,
1905                Some(child) => child,
1906            };
1907            offset = 0;
1908        }
1909        // Step 3. Return true.
1910        true
1911    }
1912
1913    /// <https://w3c.github.io/editing/docs/execCommand/#canonical-space-sequence>
1914    fn canonical_space_sequence(
1915        n: usize,
1916        non_breaking_start: bool,
1917        non_breaking_end: bool,
1918    ) -> String {
1919        let mut n = n;
1920        // Step 1. If n is zero, return the empty string.
1921        if n == 0 {
1922            return String::new();
1923        }
1924        // Step 2. If n is one and both non-breaking start and non-breaking end are false, return a single space (U+0020).
1925        if n == 1 {
1926            if !non_breaking_start && !non_breaking_end {
1927                return "\u{0020}".to_owned();
1928            }
1929            // Step 3. If n is one, return a single non-breaking space (U+00A0).
1930            return "\u{00A0}".to_owned();
1931        }
1932        // Step 4. Let buffer be the empty string.
1933        let mut buffer = String::new();
1934        // Step 5. If non-breaking start is true, let repeated pair be U+00A0 U+0020. Otherwise, let it be U+0020 U+00A0.
1935        let repeated_pair = if non_breaking_start {
1936            "\u{00A0}\u{0020}"
1937        } else {
1938            "\u{0020}\u{00A0}"
1939        };
1940        // Step 6. While n is greater than three, append repeated pair to buffer and subtract two from n.
1941        while n > 3 {
1942            buffer.push_str(repeated_pair);
1943            n -= 2;
1944        }
1945        // Step 7. If n is three, append a three-code unit string to buffer depending on non-breaking start and non-breaking end:
1946        if n == 3 {
1947            buffer.push_str(match (non_breaking_start, non_breaking_end) {
1948                (false, false) => "\u{0020}\u{00A0}\u{0020}",
1949                (true, false) => "\u{00A0}\u{00A0}\u{0020}",
1950                (false, true) => "\u{0020}\u{00A0}\u{00A0}",
1951                (true, true) => "\u{00A0}\u{0020}\u{00A0}",
1952            });
1953        } else {
1954            // Step 8. Otherwise, append a two-code unit string to buffer depending on non-breaking start and non-breaking end:
1955            buffer.push_str(match (non_breaking_start, non_breaking_end) {
1956                (false, false) | (true, false) => "\u{00A0}\u{0020}",
1957                (false, true) => "\u{0020}\u{00A0}",
1958                (true, true) => "\u{00A0}\u{00A0}",
1959            });
1960        }
1961        // Step 9. Return buffer.
1962        buffer
1963    }
1964
1965    /// <https://w3c.github.io/editing/docs/execCommand/#canonicalize-whitespace>
1966    pub(crate) fn canonicalize_whitespace(
1967        &self,
1968        cx: &mut JSContext,
1969        offset: u32,
1970        fix_collapsed_space: bool,
1971    ) {
1972        // Step 1. If node is neither editable nor an editing host, abort these steps.
1973        if !self.is_editable_or_editing_host() {
1974            return;
1975        }
1976        // Step 2. Let start node equal node and let start offset equal offset.
1977        let mut start_node = DomRoot::from_ref(self);
1978        let mut start_offset = offset;
1979        // Step 3. Repeat the following steps:
1980        loop {
1981            // Step 3.1. If start node has a child in the same editing host with index start offset minus one,
1982            // set start node to that child, then set start offset to start node's length.
1983            if start_offset > 0 {
1984                let child = start_node.children().nth(start_offset as usize - 1);
1985                if let Some(child) = child &&
1986                    start_node.same_editing_host(&child)
1987                {
1988                    start_node = child;
1989                    start_offset = start_node.len();
1990                    continue;
1991                };
1992            }
1993            // Step 3.2. Otherwise, if start offset is zero and start node does not follow a line break
1994            // and start node's parent is in the same editing host, set start offset to start node's index,
1995            // then set start node to its parent.
1996            if start_offset == 0 &&
1997                !start_node.follows_a_line_break(cx.no_gc()) &&
1998                let Some(parent) = start_node.GetParentNode() &&
1999                parent.same_editing_host(&start_node)
2000            {
2001                start_offset = start_node.index();
2002                start_node = parent;
2003            }
2004            // Step 3.3. Otherwise, if start node is a Text node and its parent's resolved
2005            // value for "white-space" is neither "pre" nor "pre-wrap" and start offset is not zero
2006            // and the (start offset − 1)st code unit of start node's data is a space (0x0020) or
2007            // non-breaking space (0x00A0), subtract one from start offset.
2008            if start_offset != 0 &&
2009                start_node.downcast::<Text>().is_some_and(|text| {
2010                    text.has_whitespace_and_has_parent_with_whitespace_preserve(
2011                        start_offset - 1,
2012                        &[&'\u{0020}', &'\u{00A0}'],
2013                    )
2014                })
2015            {
2016                start_offset -= 1;
2017            }
2018            // Step 3.4. Otherwise, break from this loop.
2019            break;
2020        }
2021        // Step 4. Let end node equal start node and end offset equal start offset.
2022        let mut end_node = start_node.clone();
2023        let mut end_offset = start_offset;
2024        // Step 5. Let length equal zero.
2025        let mut length = 0;
2026        // Step 6. Let collapse spaces be true if start offset is zero and start node follows a line break, otherwise false.
2027        let mut collapse_spaces = start_offset == 0 && start_node.follows_a_line_break(cx.no_gc());
2028        // Step 7. Repeat the following steps:
2029        loop {
2030            // Step 7.1. If end node has a child in the same editing host with index end offset,
2031            // set end node to that child, then set end offset to zero.
2032            if let Some(child) = end_node.children().nth(end_offset as usize) &&
2033                child.same_editing_host(&end_node)
2034            {
2035                end_node = child;
2036                end_offset = 0;
2037                continue;
2038            }
2039            // Step 7.2. Otherwise, if end offset is end node's length
2040            // and end node does not precede a line break
2041            // and end node's parent is in the same editing host,
2042            // set end offset to one plus end node's index, then set end node to its parent.
2043            if end_offset == end_node.len() &&
2044                !end_node.precedes_a_line_break(cx.no_gc()) &&
2045                let Some(parent) = end_node.GetParentNode() &&
2046                parent.same_editing_host(&end_node)
2047            {
2048                end_offset = 1 + end_node.index();
2049                end_node = parent;
2050                continue;
2051            }
2052            // Step 7.3. Otherwise, if end node is a Text node and its parent's resolved value for "white-space"
2053            // is neither "pre" nor "pre-wrap"
2054            // and end offset is not end node's length and the end offsetth code unit of end node's data
2055            // is a space (0x0020) or non-breaking space (0x00A0):
2056            if let Some(text) = end_node.downcast::<Text>() &&
2057                text.has_whitespace_and_has_parent_with_whitespace_preserve(
2058                    end_offset,
2059                    &[&'\u{0020}', &'\u{00A0}'],
2060                )
2061            {
2062                // Step 7.3.1. If fix collapsed space is true, and collapse spaces is true,
2063                // and the end offsetth code unit of end node's data is a space (0x0020):
2064                // call deleteData(end offset, 1) on end node, then continue this loop from the beginning.
2065                let has_space_at_offset = text
2066                    .data()
2067                    .chars()
2068                    .nth(end_offset as usize)
2069                    .is_some_and(|c| c == '\u{0020}');
2070                if fix_collapsed_space && collapse_spaces && has_space_at_offset {
2071                    if text
2072                        .upcast::<CharacterData>()
2073                        .DeleteData(cx, end_offset, 1)
2074                        .is_err()
2075                    {
2076                        unreachable!("Invalid deletion for character at end offset");
2077                    }
2078                    continue;
2079                }
2080                // Step 7.3.2. Set collapse spaces to true if the end offsetth code unit of
2081                // end node's data is a space (0x0020), false otherwise.
2082                collapse_spaces = text
2083                    .data()
2084                    .chars()
2085                    .nth(end_offset as usize)
2086                    .is_some_and(|c| c == '\u{0020}');
2087                // Step 7.3.3. Add one to end offset.
2088                end_offset += 1;
2089                // Step 7.3.4. Add one to length.
2090                length += 1;
2091                continue;
2092            }
2093            // Step 7.4. Otherwise, break from this loop.
2094            break;
2095        }
2096        // Step 8. If fix collapsed space is true, then while (start node, start offset)
2097        // is before (end node, end offset):
2098        if fix_collapsed_space {
2099            while bp_position(cx.no_gc(), &start_node, start_offset, &end_node, end_offset) ==
2100                Ordering::Less
2101            {
2102                // Step 8.1. If end node has a child in the same editing host with index end offset − 1,
2103                // set end node to that child, then set end offset to end node's length.
2104                if end_offset > 0 &&
2105                    let Some(child) = end_node.children().nth(end_offset as usize - 1) &&
2106                    child.same_editing_host(&end_node)
2107                {
2108                    end_node = child;
2109                    end_offset = end_node.len();
2110                    continue;
2111                }
2112                // Step 8.2. Otherwise, if end offset is zero and end node's parent is in the same editing host,
2113                // set end offset to end node's index, then set end node to its parent.
2114                if let Some(parent) = end_node.GetParentNode() &&
2115                    end_offset == 0 &&
2116                    parent.same_editing_host(&end_node)
2117                {
2118                    end_offset = end_node.index();
2119                    end_node = parent;
2120                    continue;
2121                }
2122                // Step 8.3. Otherwise, if end node is a Text node and its parent's resolved value for "white-space"
2123                // is neither "pre" nor "pre-wrap"
2124                // and end offset is end node's length and the last code unit of end node's data
2125                // is a space (0x0020) and end node precedes a line break:
2126                if let Some(text) = end_node.downcast::<Text>() &&
2127                    text.has_whitespace_and_has_parent_with_whitespace_preserve(
2128                        text.data().len() as u32,
2129                        &[&'\u{0020}'],
2130                    ) &&
2131                    end_node.precedes_a_line_break(cx.no_gc())
2132                {
2133                    // Step 8.3.1. Subtract one from end offset.
2134                    end_offset -= 1;
2135                    // Step 8.3.2. Subtract one from length.
2136                    length -= 1;
2137                    // Step 8.3.3. Call deleteData(end offset, 1) on end node.
2138                    if text
2139                        .upcast::<CharacterData>()
2140                        .DeleteData(cx, end_offset, 1)
2141                        .is_err()
2142                    {
2143                        unreachable!("Invalid deletion for character at end offset");
2144                    }
2145                    continue;
2146                }
2147                // Step 8.4. Otherwise, break from this loop.
2148                break;
2149            }
2150        }
2151        // Step 9. Let replacement whitespace be the canonical space sequence of length length.
2152        // non-breaking start is true if start offset is zero and start node follows a line break, and false otherwise.
2153        // non-breaking end is true if end offset is end node's length and end node precedes a line break, and false otherwise.
2154        let replacement_whitespace = Node::canonical_space_sequence(
2155            length,
2156            start_offset == 0 && start_node.follows_a_line_break(cx.no_gc()),
2157            end_offset == end_node.len() && end_node.precedes_a_line_break(cx.no_gc()),
2158        );
2159        let mut replacement_whitespace_chars = replacement_whitespace.chars();
2160        // Step 10. While (start node, start offset) is before (end node, end offset):
2161        while bp_position(cx.no_gc(), &start_node, start_offset, &end_node, end_offset) ==
2162            Ordering::Less
2163        {
2164            // 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.
2165            if let Some(child) = start_node.children().nth(start_offset as usize) {
2166                start_node = child;
2167                start_offset = 0;
2168                continue;
2169            }
2170            // Step 10.2. Otherwise, if start node is not a Text node or if start offset is start node's length,
2171            // set start offset to one plus start node's index, then set start node to its parent.
2172            let start_node_as_text = start_node.downcast::<Text>();
2173            if start_node_as_text.is_none() || start_offset == start_node.len() {
2174                start_offset = 1 + start_node.index();
2175                start_node = start_node
2176                    .GetParentNode()
2177                    .expect("Must always have a parent");
2178                continue;
2179            }
2180            let start_node_as_text =
2181                start_node_as_text.expect("Already verified none in previous statement");
2182            // Step 10.3. Otherwise:
2183            // Step 10.3.1. Remove the first code unit from replacement whitespace, and let element be that code unit.
2184            if let Some(element) = replacement_whitespace_chars.next() {
2185                // Step 10.3.2. If element is not the same as the start offsetth code unit of start node's data:
2186                if start_node_as_text.data().chars().nth(start_offset as usize) != Some(element) {
2187                    let character_data = start_node_as_text.upcast::<CharacterData>();
2188                    // Step 10.3.2.1. Call insertData(start offset, element) on start node.
2189                    if character_data
2190                        .InsertData(cx, start_offset, element.to_string().into())
2191                        .is_err()
2192                    {
2193                        unreachable!("Invalid insertion for character at start offset");
2194                    }
2195                    // Step 10.3.2.2. Call deleteData(start offset + 1, 1) on start node.
2196                    if character_data.DeleteData(cx, start_offset + 1, 1).is_err() {
2197                        unreachable!("Invalid deletion for character at start offset + 1");
2198                    }
2199                }
2200            }
2201            // Step 10.3.3. Add one to start offset.
2202            start_offset += 1;
2203        }
2204    }
2205
2206    /// <https://w3c.github.io/editing/docs/execCommand/#remove-extraneous-line-breaks-before>
2207    fn remove_extraneous_line_breaks_before(&self, cx: &mut JSContext) {
2208        let parent = self.GetParentNode();
2209        // Step 1. Let ref be the previousSibling of node.
2210        let Some(mut ref_) = self.GetPreviousSibling() else {
2211            // Step 2. If ref is null, abort these steps.
2212            return;
2213        };
2214        // Step 3. While ref has children, set ref to its lastChild.
2215        while let Some(last_child) = ref_.children().last() {
2216            ref_ = last_child;
2217        }
2218        // Step 4. While ref is invisible but not an extraneous line break,
2219        // and ref does not equal node's parent, set ref to the node before it in tree order.
2220        loop {
2221            if ref_.is_invisible(cx.no_gc()) &&
2222                ref_.downcast::<HTMLBRElement>()
2223                    .is_none_or(|br| !br.is_extraneous_line_break()) &&
2224                let Some(parent) = parent.as_ref() &&
2225                ref_ != *parent
2226            {
2227                ref_ = match ref_.preceding_nodes(parent).nth(0) {
2228                    None => break,
2229                    Some(node) => node,
2230                };
2231                continue;
2232            }
2233            break;
2234        }
2235        // Step 5. If ref is an editable extraneous line break, remove it from its parent.
2236        if ref_.is_editable() &&
2237            ref_.downcast::<HTMLBRElement>()
2238                .is_some_and(|br| br.is_extraneous_line_break())
2239        {
2240            assert!(ref_.has_parent());
2241            ref_.remove_self(cx);
2242        }
2243    }
2244
2245    /// <https://w3c.github.io/editing/docs/execCommand/#remove-extraneous-line-breaks-at-the-end-of>
2246    pub(crate) fn remove_extraneous_line_breaks_at_the_end_of(&self, cx: &mut JSContext) {
2247        // Step 1. Let ref be node.
2248        let mut ref_ = DomRoot::from_ref(self);
2249        // Step 2. While ref has children, set ref to its lastChild.
2250        while let Some(last_child) = ref_.children().last() {
2251            ref_ = last_child;
2252        }
2253        // Step 3. While ref is invisible but not an extraneous line break, and ref does not equal node,
2254        // set ref to the node before it in tree order.
2255        loop {
2256            if ref_.is_invisible(cx.no_gc()) &&
2257                *ref_ != *self &&
2258                ref_.downcast::<HTMLBRElement>()
2259                    .is_none_or(|br| !br.is_extraneous_line_break()) &&
2260                let Some(parent_of_ref) = ref_.GetParentNode()
2261            {
2262                ref_ = match ref_.preceding_nodes(&parent_of_ref).nth(0) {
2263                    None => break,
2264                    Some(node) => node,
2265                };
2266                continue;
2267            }
2268            break;
2269        }
2270        // Step 4. If ref is an editable extraneous line break:
2271        if ref_.is_editable() &&
2272            ref_.downcast::<HTMLBRElement>()
2273                .is_some_and(|br| br.is_extraneous_line_break())
2274        {
2275            // Step 4.1. While ref's parent is editable and invisible, set ref to its parent.
2276            loop {
2277                if let Some(parent) = ref_.GetParentNode() &&
2278                    parent.is_editable() &&
2279                    parent.is_invisible(cx.no_gc())
2280                {
2281                    ref_ = parent;
2282                    continue;
2283                }
2284                break;
2285            }
2286            // Step 4.2. Remove ref from its parent.
2287            assert!(ref_.has_parent());
2288            ref_.remove_self(cx);
2289        }
2290    }
2291
2292    /// <https://w3c.github.io/editing/docs/execCommand/#remove-extraneous-line-breaks-from>
2293    fn remove_extraneous_line_breaks_from(&self, cx: &mut JSContext) {
2294        // > To remove extraneous line breaks from a node, first remove extraneous line breaks before it,
2295        // > then remove extraneous line breaks at the end of it.
2296        self.remove_extraneous_line_breaks_before(cx);
2297        self.remove_extraneous_line_breaks_at_the_end_of(cx);
2298    }
2299
2300    /// <https://w3c.github.io/editing/docs/execCommand/#preserving-its-descendants>
2301    pub(crate) fn remove_preserving_its_descendants(&self, cx: &mut JSContext) {
2302        // > To remove a node node while preserving its descendants,
2303        // > split the parent of node's children if it has any.
2304        // > If it has no children, instead remove it from its parent.
2305        if self.children_count() == 0 {
2306            assert!(self.has_parent());
2307            self.remove_self(cx);
2308        } else {
2309            rooted_vec!(let children <- self.children().map(|child| DomRoot::as_traced(&child)));
2310            split_the_parent(cx, children.r());
2311        }
2312    }
2313
2314    /// <https://w3c.github.io/editing/docs/execCommand/#effective-command-value>
2315    pub(crate) fn effective_command_value(&self, command: &CommandName) -> Option<DOMString> {
2316        // Step 1. If neither node nor its parent is an Element, return null.
2317        // Step 2. If node is not an Element, return the effective command value of its parent for command.
2318        let Some(element) = self.downcast::<Element>() else {
2319            return self
2320                .GetParentElement()
2321                .and_then(|parent| parent.upcast::<Node>().effective_command_value(command));
2322        };
2323        match command {
2324            // Step 3. If command is "createLink" or "unlink":
2325            CommandName::CreateLink | CommandName::Unlink => {
2326                // Step 3.1. While node is not null, and is not an a element that has an href attribute, set node to its parent.
2327                let mut current_node = Some(DomRoot::from_ref(self));
2328                while let Some(node) = current_node {
2329                    if let Some(anchor_value) =
2330                        node.downcast::<HTMLAnchorElement>().and_then(|anchor| {
2331                            anchor
2332                                .upcast::<Element>()
2333                                .get_attribute_string_value(&local_name!("href"))
2334                        })
2335                    {
2336                        // Step 3.3. Return the value of node's href attribute.
2337                        return Some(anchor_value.into());
2338                    }
2339                    current_node = node.GetParentNode();
2340                }
2341                // Step 3.2. If node is null, return null.
2342                None
2343            },
2344            // Step 4. If command is "backColor" or "hiliteColor":
2345            CommandName::BackColor | CommandName::HiliteColor => {
2346                // Step 4.1. While the resolved value of "background-color" on node is any fully transparent value,
2347                // and node's parent is an Element, set node to its parent.
2348                let mut current_element = Some(DomRoot::from_ref(element));
2349                while let Some(element) = current_element {
2350                    if let Some(background_color) =
2351                        CssPropertyName::BackgroundColor.resolved_value_for_node(&element)
2352                    {
2353                        // Step 4.2. Return the resolved value of "background-color" for node.
2354                        return Some(background_color);
2355                    }
2356                    current_element = element.upcast::<Node>().GetParentElement();
2357                }
2358                Some(DOMString::from_static("rgba(0, 0, 0, 0)"))
2359            },
2360            // Step 5. If command is "subscript" or "superscript":
2361            CommandName::Subscript | CommandName::Superscript => {
2362                // Step 5.1. Let affected by subscript and affected by superscript be two boolean variables,
2363                // both initially false.
2364                let mut affected_by_subscript = false;
2365                let mut affected_by_superscript = false;
2366                // Step 5.2. While node is an inline node:
2367                let mut current_node = Some(DomRoot::from_ref(self));
2368                while let Some(node) = current_node {
2369                    if !node.is_inline_node() {
2370                        break;
2371                    }
2372                    if let Some(element) = node.downcast::<Element>() {
2373                        // Step 5.2.1. If node is a sub, set affected by subscript to true.
2374                        if *element.local_name() == local_name!("sub") {
2375                            affected_by_subscript = true;
2376                        } else if *element.local_name() == local_name!("sup") {
2377                            // Step 5.2.2. Otherwise, if node is a sup, set affected by superscript to true.
2378                            affected_by_superscript = true;
2379                        }
2380                    }
2381                    // Step 5.2.3. Set node to its parent.
2382                    current_node = node.GetParentNode();
2383                }
2384                Some(match (affected_by_subscript, affected_by_superscript) {
2385                    // Step 5.3. If affected by subscript and affected by superscript are both true,
2386                    // return the string "mixed".
2387                    (true, true) => DOMString::from_static("mixed"),
2388                    // Step 5.4. If affected by subscript is true, return "subscript".
2389                    (true, false) => DOMString::from_static("subscript"),
2390                    // Step 5.5. If affected by superscript is true, return "superscript".
2391                    (false, true) => DOMString::from_static("superscript"),
2392                    // Step 5.6. Return null.
2393                    (false, false) => return None,
2394                })
2395            },
2396            // Step 6. If command is "strikethrough",
2397            // and the "text-decoration" property of node or any of its ancestors has resolved value containing "line-through",
2398            // return "line-through". Otherwise, return null.
2399            CommandName::Strikethrough => self
2400                .inclusive_ancestors(ShadowIncluding::No)
2401                .any(|node| {
2402                    node.downcast::<Element>()
2403                        .and_then(|element| {
2404                            CssPropertyName::TextDecorationLine.resolved_value_for_node(element)
2405                        })
2406                        .is_some_and(|property| property.contains("line-through"))
2407                })
2408                .then_some(DOMString::from_static("line-through")),
2409            // Step 7. If command is "underline",
2410            // and the "text-decoration" property of node or any of its ancestors has resolved value containing "underline",
2411            // return "underline". Otherwise, return null.
2412            CommandName::Underline => self
2413                .inclusive_ancestors(ShadowIncluding::No)
2414                .any(|node| {
2415                    node.downcast::<Element>()
2416                        .and_then(|element| {
2417                            CssPropertyName::TextDecorationLine.resolved_value_for_node(element)
2418                        })
2419                        .is_some_and(|property| property.contains("underline"))
2420                })
2421                .then_some(DOMString::from_static("underline")),
2422            // Step 8. Return the resolved value for node of the relevant CSS property for command.
2423            _ => command.resolved_value_for_node(element),
2424        }
2425    }
2426}