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