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