script/dom/execcommand/
contenteditable.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;
6
7use script_bindings::inheritance::Castable;
8use style::computed_values::white_space_collapse::T as WhiteSpaceCollapse;
9use style::values::specified::box_::DisplayOutside;
10
11use crate::dom::abstractrange::bp_position;
12use crate::dom::bindings::cell::Ref;
13use crate::dom::bindings::codegen::Bindings::CharacterDataBinding::CharacterDataMethods;
14use crate::dom::bindings::codegen::Bindings::DocumentBinding::DocumentMethods;
15use crate::dom::bindings::codegen::Bindings::HTMLElementBinding::HTMLElementMethods;
16use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods;
17use crate::dom::bindings::codegen::Bindings::RangeBinding::RangeMethods;
18use crate::dom::bindings::codegen::Bindings::SelectionBinding::SelectionMethods;
19use crate::dom::bindings::inheritance::{ElementTypeId, HTMLElementTypeId, NodeTypeId};
20use crate::dom::bindings::root::DomRoot;
21use crate::dom::characterdata::CharacterData;
22use crate::dom::element::Element;
23use crate::dom::html::htmlbrelement::HTMLBRElement;
24use crate::dom::html::htmlelement::HTMLElement;
25use crate::dom::html::htmlimageelement::HTMLImageElement;
26use crate::dom::html::htmllielement::HTMLLIElement;
27use crate::dom::node::{Node, NodeTraits, ShadowIncluding};
28use crate::dom::range::Range;
29use crate::dom::selection::Selection;
30use crate::dom::text::Text;
31use crate::script_runtime::CanGc;
32
33impl Text {
34    /// <https://dom.spec.whatwg.org/#concept-cd-data>
35    fn data(&self) -> Ref<'_, String> {
36        self.upcast::<CharacterData>().data()
37    }
38
39    /// <https://w3c.github.io/editing/docs/execCommand/#whitespace-node>
40    fn is_whitespace_node(&self) -> bool {
41        // > A whitespace node is either a Text node whose data is the empty string;
42        let data = self.data();
43        if data.is_empty() {
44            return true;
45        }
46        // > or a Text node whose data consists only of one or more tabs (0x0009), line feeds (0x000A),
47        // > carriage returns (0x000D), and/or spaces (0x0020),
48        // > and whose parent is an Element whose resolved value for "white-space" is "normal" or "nowrap";
49        let Some(parent) = self.upcast::<Node>().GetParentElement() else {
50            return false;
51        };
52        // TODO: Optimize the below to only do a traversal once and in the match handle the expected collapse value
53        let Some(style) = parent.style() else {
54            return false;
55        };
56        let white_space_collapse = style.get_inherited_text().white_space_collapse;
57        if data
58            .bytes()
59            .all(|byte| matches!(byte, b'\t' | b'\n' | b'\r' | b' ')) &&
60            // Note that for "normal" and "nowrap", the longhand "white-space-collapse: collapse" applies
61            // https://www.w3.org/TR/css-text-4/#white-space-property
62            white_space_collapse == WhiteSpaceCollapse::Collapse
63        {
64            return true;
65        }
66        // > or a Text node whose data consists only of one or more tabs (0x0009), carriage returns (0x000D),
67        // > and/or spaces (0x0020), and whose parent is an Element whose resolved value for "white-space" is "pre-line".
68        data.bytes()
69            .all(|byte| matches!(byte, b'\t' | b'\r' | b' ')) &&
70            // Note that for "pre-line", the longhand "white-space-collapse: preserve-breaks" applies
71            // https://www.w3.org/TR/css-text-4/#white-space-property
72            white_space_collapse == WhiteSpaceCollapse::PreserveBreaks
73    }
74
75    /// <https://w3c.github.io/editing/docs/execCommand/#collapsed-whitespace-node>
76    fn is_collapsed_whitespace_node(&self) -> bool {
77        // Step 1. If node is not a whitespace node, return false.
78        if !self.is_whitespace_node() {
79            return false;
80        }
81        // Step 2. If node's data is the empty string, return true.
82        if self.data().is_empty() {
83            return true;
84        }
85        // Step 3. Let ancestor be node's parent.
86        let node = self.upcast::<Node>();
87        let Some(ancestor) = node.GetParentNode() else {
88            // Step 4. If ancestor is null, return true.
89            return true;
90        };
91        let mut resolved_ancestor = ancestor.clone();
92        for parent in ancestor.ancestors() {
93            // Step 5. If the "display" property of some ancestor of node has resolved value "none", return true.
94            if parent.is_display_none() {
95                return true;
96            }
97            // Step 6. While ancestor is not a block node and its parent is not null, set ancestor to its parent.
98            //
99            // Note that the spec is written as "while not". Since this is the end-condition, we need to invert
100            // the condition to decide when to stop.
101            if parent.is_block_node() {
102                break;
103            }
104            resolved_ancestor = parent;
105        }
106        // Step 7. Let reference be node.
107        // Step 8. While reference is a descendant of ancestor:
108        // Step 8.1. Let reference be the node before it in tree order.
109        for reference in node.preceding_nodes(&resolved_ancestor) {
110            // Step 8.2. If reference is a block node or a br, return true.
111            if reference.is_block_node() || reference.is::<HTMLBRElement>() {
112                return true;
113            }
114            // Step 8.3. If reference is a Text node that is not a whitespace node, or is an img, break from this loop.
115            if reference
116                .downcast::<Text>()
117                .is_some_and(|text| !text.is_whitespace_node()) ||
118                reference.is::<HTMLImageElement>()
119            {
120                break;
121            }
122        }
123        // Step 9. Let reference be node.
124        // Step 10. While reference is a descendant of ancestor:
125        // Step 10.1. Let reference be the node after it in tree order, or null if there is no such node.
126        for reference in node.following_nodes(&resolved_ancestor) {
127            // Step 10.2. If reference is a block node or a br, return true.
128            if reference.is_block_node() || reference.is::<HTMLBRElement>() {
129                return true;
130            }
131            // Step 10.3. If reference is a Text node that is not a whitespace node, or is an img, break from this loop.
132            if reference
133                .downcast::<Text>()
134                .is_some_and(|text| !text.is_whitespace_node()) ||
135                reference.is::<HTMLImageElement>()
136            {
137                break;
138            }
139        }
140        // Step 11. Return false.
141        false
142    }
143
144    /// Part of <https://w3c.github.io/editing/docs/execCommand/#canonicalize-whitespace>
145    /// and deduplicated here, since we need to do this for both start and end nodes
146    fn has_whitespace_and_has_parent_with_whitespace_preserve(
147        &self,
148        offset: u32,
149        space_characters: &'static [&'static char],
150    ) -> bool {
151        // if node is a Text node and its parent's resolved value for "white-space" is neither "pre" nor "pre-wrap"
152        // and start offset is not zero and the (start offset − 1)st code unit of start node's data is a space (0x0020) or
153        // non-breaking space (0x00A0)
154        let has_preserve_space = self
155            .upcast::<Node>()
156            .GetParentNode()
157            .and_then(|parent| parent.style())
158            .is_some_and(|style| {
159                // Note that for "pre" and "pre-wrap", the longhand "white-space-collapse: preserve" applies
160                // https://www.w3.org/TR/css-text-4/#white-space-property
161                style.get_inherited_text().white_space_collapse != WhiteSpaceCollapse::Preserve
162            });
163        let has_space_character = self
164            .data()
165            .chars()
166            .nth(offset as usize)
167            .is_some_and(|c| space_characters.contains(&&c));
168        has_preserve_space && has_space_character
169    }
170}
171
172impl HTMLBRElement {
173    /// <https://w3c.github.io/editing/docs/execCommand/#extraneous-line-break>
174    fn is_extraneous_line_break(&self) -> bool {
175        let node = self.upcast::<Node>();
176        // > An extraneous line break is a br that has no visual effect, in that removing it from the DOM would not change layout,
177        // except that a br that is the sole child of an li is not extraneous.
178        if node
179            .GetParentNode()
180            .filter(|parent| parent.is::<HTMLLIElement>())
181            .is_some_and(|li| li.children_count() == 1)
182        {
183            return false;
184        }
185        // TODO: Figure out what this actually makes it have no visual effect
186        !node.is_block_node()
187    }
188}
189
190impl Node {
191    /// <https://w3c.github.io/editing/docs/execCommand/#in-the-same-editing-host>
192    fn same_editing_host(&self, other: &Node) -> bool {
193        // > 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.
194        self.editing_host_of()
195            .is_some_and(|editing_host| other.editing_host_of() == Some(editing_host))
196    }
197
198    /// <https://w3c.github.io/editing/docs/execCommand/#block-node>
199    fn is_block_node(&self) -> bool {
200        // > A block node is either an Element whose "display" property does not have resolved value "inline" or "inline-block" or "inline-table" or "none",
201        if self.downcast::<Element>().is_some_and(|el| {
202            !el.style()
203                .is_none_or(|style| style.get_box().display.outside() == DisplayOutside::Inline)
204        }) {
205            return true;
206        }
207        // > or a document, or a DocumentFragment.
208        matches!(
209            self.type_id(),
210            NodeTypeId::Document(_) | NodeTypeId::DocumentFragment(_)
211        )
212    }
213
214    /// <https://w3c.github.io/editing/docs/execCommand/#visible>
215    fn is_visible(&self) -> bool {
216        for parent in self.inclusive_ancestors(ShadowIncluding::Yes) {
217            // > excluding any node with an inclusive ancestor Element whose "display" property has resolved value "none".
218            if parent.is_display_none() {
219                return false;
220            }
221        }
222        // > Something is visible if it is a node that either is a block node,
223        if self.is_block_node() {
224            return true;
225        }
226        // > or a Text node that is not a collapsed whitespace node,
227        if self
228            .downcast::<Text>()
229            .is_some_and(|text| !text.is_collapsed_whitespace_node())
230        {
231            return true;
232        }
233        // > or an img, or a br that is not an extraneous line break, or any node with a visible descendant;
234        if self.is::<HTMLImageElement>() {
235            return true;
236        }
237        if self
238            .downcast::<HTMLBRElement>()
239            .is_some_and(|br| !br.is_extraneous_line_break())
240        {
241            return true;
242        }
243        for child in self.children() {
244            if child.is_visible() {
245                return true;
246            }
247        }
248        false
249    }
250
251    /// <https://w3c.github.io/editing/docs/execCommand/#block-start-point>
252    fn is_block_start_point(&self, offset: usize) -> bool {
253        // > A boundary point (node, offset) is a block start point if either node's parent is null and offset is zero;
254        if offset == 0 {
255            return self.GetParentNode().is_none();
256        }
257        // > or node has a child with index offset − 1, and that child is either a visible block node or a visible br.
258        self.children().nth(offset - 1).is_some_and(|child| {
259            child.is_visible() && (child.is_block_node() || child.is::<HTMLBRElement>())
260        })
261    }
262
263    /// <https://w3c.github.io/editing/docs/execCommand/#block-end-point>
264    fn is_block_end_point(&self, offset: u32) -> bool {
265        // > A boundary point (node, offset) is a block end point if either node's parent is null and offset is node's length;
266        if self.GetParentNode().is_none() && offset == self.len() {
267            return true;
268        }
269        // > or node has a child with index offset, and that child is a visible block node.
270        self.children()
271            .nth(offset as usize)
272            .is_some_and(|child| child.is_visible() && child.is_block_node())
273    }
274
275    /// <https://w3c.github.io/editing/docs/execCommand/#block-boundary-point>
276    fn is_block_boundary_point(&self, offset: u32) -> bool {
277        // > A boundary point is a block boundary point if it is either a block start point or a block end point.
278        self.is_block_start_point(offset as usize) || self.is_block_end_point(offset)
279    }
280
281    /// <https://w3c.github.io/editing/docs/execCommand/#follows-a-line-break>
282    fn follows_a_line_break(&self) -> bool {
283        // Step 1. Let offset be zero.
284        let mut offset = 0;
285        // Step 2. While (node, offset) is not a block boundary point:
286        let mut node = DomRoot::from_ref(self);
287        while !node.is_block_boundary_point(offset) {
288            // Step 2.2. If offset is zero or node has no children, set offset to node's index, then set node to its parent.
289            if offset == 0 || node.children_count() == 0 {
290                offset = node.index();
291                node = match node.GetParentNode() {
292                    None => return false,
293                    Some(node) => node,
294                };
295                continue;
296            }
297            // Step 2.1. If node has a visible child with index offset minus one, return false.
298            let child = node.children().nth(offset as usize - 1);
299            let Some(child) = child else {
300                return false;
301            };
302            if child.is_visible() {
303                return false;
304            }
305            // Step 2.3. Otherwise, set node to its child with index offset minus one, then set offset to node's length.
306            node = child;
307            offset = node.len();
308        }
309        // Step 3. Return true.
310        true
311    }
312
313    /// <https://w3c.github.io/editing/docs/execCommand/#precedes-a-line-break>
314    fn precedes_a_line_break(&self) -> bool {
315        let mut node = DomRoot::from_ref(self);
316        // Step 1. Let offset be node's length.
317        let mut offset = node.len();
318        // Step 2. While (node, offset) is not a block boundary point:
319        while !node.is_block_boundary_point(offset) {
320            // Step 2.1. If node has a visible child with index offset, return false.
321            if node
322                .children()
323                .nth(offset as usize)
324                .is_some_and(|child| child.is_visible())
325            {
326                return false;
327            }
328            // 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.
329            if offset == node.len() || node.children_count() == 0 {
330                offset = 1 + node.index();
331                node = match node.GetParentNode() {
332                    None => return false,
333                    Some(node) => node,
334                };
335                continue;
336            }
337            // Step 2.3. Otherwise, set node to its child with index offset and set offset to zero.
338            let child = node.children().nth(offset as usize);
339            node = match child {
340                None => return false,
341                Some(child) => child,
342            };
343            offset = 0;
344        }
345        // Step 3. Return true.
346        true
347    }
348
349    /// <https://w3c.github.io/editing/docs/execCommand/#canonical-space-sequence>
350    fn canonical_space_sequence(
351        n: usize,
352        non_breaking_start: bool,
353        non_breaking_end: bool,
354    ) -> String {
355        let mut n = n;
356        // Step 1. If n is zero, return the empty string.
357        if n == 0 {
358            return String::new();
359        }
360        // Step 2. If n is one and both non-breaking start and non-breaking end are false, return a single space (U+0020).
361        if n == 1 {
362            if !non_breaking_start && !non_breaking_end {
363                return "\u{0020}".to_owned();
364            }
365            // Step 3. If n is one, return a single non-breaking space (U+00A0).
366            return "\u{00A0}".to_owned();
367        }
368        // Step 4. Let buffer be the empty string.
369        let mut buffer = String::new();
370        // Step 5. If non-breaking start is true, let repeated pair be U+00A0 U+0020. Otherwise, let it be U+0020 U+00A0.
371        let repeated_pair = if non_breaking_start {
372            "\u{00A0}\u{0020}"
373        } else {
374            "\u{0020}\u{00A0}"
375        };
376        // Step 6. While n is greater than three, append repeated pair to buffer and subtract two from n.
377        while n > 3 {
378            buffer.push_str(repeated_pair);
379            n -= 2;
380        }
381        // Step 7. If n is three, append a three-code unit string to buffer depending on non-breaking start and non-breaking end:
382        if n == 3 {
383            buffer.push_str(match (non_breaking_start, non_breaking_end) {
384                (false, false) => "\u{0020}\u{00A0}\u{0020}",
385                (true, false) => "\u{00A0}\u{00A0}\u{0020}",
386                (false, true) => "\u{0020}\u{00A0}\u{00A0}",
387                (true, true) => "\u{00A0}\u{0020}\u{00A0}",
388            });
389        } else {
390            // Step 8. Otherwise, append a two-code unit string to buffer depending on non-breaking start and non-breaking end:
391            buffer.push_str(match (non_breaking_start, non_breaking_end) {
392                (false, false) | (true, false) => "\u{00A0}\u{0020}",
393                (false, true) => "\u{0020}\u{00A0}",
394                (true, true) => "\u{00A0}\u{00A0}",
395            });
396        }
397        // Step 9. Return buffer.
398        buffer
399    }
400
401    /// <https://w3c.github.io/editing/docs/execCommand/#canonicalize-whitespace>
402    fn canonicalize_whitespace(&self, offset: u32, fix_collapsed_space: bool) {
403        // Step 1. If node is neither editable nor an editing host, abort these steps.
404        if !self.is_editable_or_editing_host() {
405            return;
406        }
407        // Step 2. Let start node equal node and let start offset equal offset.
408        let mut start_node = DomRoot::from_ref(self);
409        let mut start_offset = offset;
410        // Step 3. Repeat the following steps:
411        loop {
412            // Step 3.1. If start node has a child in the same editing host with index start offset minus one,
413            // set start node to that child, then set start offset to start node's length.
414            if start_offset > 0 {
415                let child = start_node.children().nth(start_offset as usize - 1);
416                if let Some(child) = child {
417                    if start_node.same_editing_host(&child) {
418                        start_node = child;
419                        start_offset = start_node.len();
420                        continue;
421                    }
422                };
423            }
424            // Step 3.2. Otherwise, if start offset is zero and start node does not follow a line break
425            // and start node's parent is in the same editing host, set start offset to start node's index,
426            // then set start node to its parent.
427            if start_offset == 0 && !start_node.follows_a_line_break() {
428                if let Some(parent) = start_node.GetParentNode() {
429                    if parent.same_editing_host(&start_node) {
430                        start_offset = start_node.index();
431                        start_node = parent;
432                    }
433                }
434            }
435            // Step 3.3. Otherwise, if start node is a Text node and its parent's resolved
436            // value for "white-space" is neither "pre" nor "pre-wrap" and start offset is not zero
437            // and the (start offset − 1)st code unit of start node's data is a space (0x0020) or
438            // non-breaking space (0x00A0), subtract one from start offset.
439            if start_offset != 0 &&
440                start_node.downcast::<Text>().is_some_and(|text| {
441                    text.has_whitespace_and_has_parent_with_whitespace_preserve(
442                        start_offset - 1,
443                        &[&'\u{0020}', &'\u{00A0}'],
444                    )
445                })
446            {
447                start_offset -= 1;
448            }
449            // Step 3.4. Otherwise, break from this loop.
450            break;
451        }
452        // Step 4. Let end node equal start node and end offset equal start offset.
453        let mut end_node = start_node.clone();
454        let mut end_offset = start_offset;
455        // Step 5. Let length equal zero.
456        let mut length = 0;
457        // Step 6. Let collapse spaces be true if start offset is zero and start node follows a line break, otherwise false.
458        let mut collapse_spaces = start_offset == 0 && start_node.follows_a_line_break();
459        // Step 7. Repeat the following steps:
460        loop {
461            // Step 7.1. If end node has a child in the same editing host with index end offset,
462            // set end node to that child, then set end offset to zero.
463            if let Some(child) = end_node.children().nth(end_offset as usize) {
464                if child.same_editing_host(&end_node) {
465                    end_node = child;
466                    end_offset = 0;
467                    continue;
468                }
469            }
470            // Step 7.2. Otherwise, if end offset is end node's length
471            // and end node does not precede a line break
472            // and end node's parent is in the same editing host,
473            // set end offset to one plus end node's index, then set end node to its parent.
474            if end_offset == end_node.len() && !end_node.precedes_a_line_break() {
475                if let Some(parent) = end_node.GetParentNode() {
476                    if parent.same_editing_host(&end_node) {
477                        end_offset = 1 + end_node.index();
478                        end_node = parent;
479                    }
480                }
481                continue;
482            }
483            // Step 7.3. Otherwise, if end node is a Text node and its parent's resolved value for "white-space"
484            // is neither "pre" nor "pre-wrap"
485            // and end offset is not end node's length and the end offsetth code unit of end node's data
486            // is a space (0x0020) or non-breaking space (0x00A0):
487            if let Some(text) = end_node.downcast::<Text>() {
488                if text.has_whitespace_and_has_parent_with_whitespace_preserve(
489                    end_offset,
490                    &[&'\u{0020}', &'\u{00A0}'],
491                ) {
492                    // Step 7.3.1. If fix collapsed space is true, and collapse spaces is true,
493                    // and the end offsetth code unit of end node's data is a space (0x0020):
494                    // call deleteData(end offset, 1) on end node, then continue this loop from the beginning.
495                    let has_space_at_offset = text
496                        .data()
497                        .chars()
498                        .nth(end_offset as usize)
499                        .is_some_and(|c| c == '\u{0020}');
500                    if fix_collapsed_space && collapse_spaces && has_space_at_offset {
501                        if text
502                            .upcast::<CharacterData>()
503                            .DeleteData(end_offset, 1)
504                            .is_err()
505                        {
506                            unreachable!("Invalid deletion for character at end offset");
507                        }
508                        continue;
509                    }
510                    // Step 7.3.2. Set collapse spaces to true if the end offsetth code unit of
511                    // end node's data is a space (0x0020), false otherwise.
512                    collapse_spaces = has_space_at_offset;
513                    // Step 7.3.3. Add one to end offset.
514                    end_offset += 1;
515                    // Step 7.3.4. Add one to length.
516                    length += 1;
517                    continue;
518                }
519            }
520            // Step 7.4. Otherwise, break from this loop.
521            break;
522        }
523        // Step 8. If fix collapsed space is true, then while (start node, start offset)
524        // is before (end node, end offset):
525        if fix_collapsed_space {
526            while bp_position(&start_node, start_offset, &end_node, end_offset) ==
527                Some(Ordering::Less)
528            {
529                // Step 8.1. If end node has a child in the same editing host with index end offset − 1,
530                // set end node to that child, then set end offset to end node's length.
531                if end_offset > 0 {
532                    if let Some(child) = end_node.children().nth(end_offset as usize - 1) {
533                        if child.same_editing_host(&end_node) {
534                            end_node = child;
535                            end_offset = end_node.len();
536                            continue;
537                        }
538                    }
539                }
540                // Step 8.2. Otherwise, if end offset is zero and end node's parent is in the same editing host,
541                // set end offset to end node's index, then set end node to its parent.
542                if let Some(parent) = end_node.GetParentNode() {
543                    if end_offset == 0 && parent.same_editing_host(&end_node) {
544                        end_offset = end_node.index();
545                        end_node = parent;
546                        continue;
547                    }
548                }
549                // Step 8.3. Otherwise, if end node is a Text node and its parent's resolved value for "white-space"
550                // is neither "pre" nor "pre-wrap"
551                // and end offset is end node's length and the last code unit of end node's data
552                // is a space (0x0020) and end node precedes a line break:
553                if let Some(text) = end_node.downcast::<Text>() {
554                    if text.has_whitespace_and_has_parent_with_whitespace_preserve(
555                        text.data().len() as u32,
556                        &[&'\u{0020}'],
557                    ) && end_node.precedes_a_line_break()
558                    {
559                        // Step 8.3.1. Subtract one from end offset.
560                        end_offset -= 1;
561                        // Step 8.3.2. Subtract one from length.
562                        length -= 1;
563                        // Step 8.3.3. Call deleteData(end offset, 1) on end node.
564                        if text
565                            .upcast::<CharacterData>()
566                            .DeleteData(end_offset, 1)
567                            .is_err()
568                        {
569                            unreachable!("Invalid deletion for character at end offset");
570                        }
571                        continue;
572                    }
573                }
574                // Step 8.4. Otherwise, break from this loop.
575                break;
576            }
577        }
578        // Step 9. Let replacement whitespace be the canonical space sequence of length length.
579        // non-breaking start is true if start offset is zero and start node follows a line break, and false otherwise.
580        // non-breaking end is true if end offset is end node's length and end node precedes a line break, and false otherwise.
581        let replacement_whitespace = Node::canonical_space_sequence(
582            length,
583            start_offset == 0 && start_node.follows_a_line_break(),
584            end_offset == end_node.len() && end_node.precedes_a_line_break(),
585        );
586        let mut replacement_whitespace_chars = replacement_whitespace.chars();
587        // Step 10. While (start node, start offset) is before (end node, end offset):
588        while bp_position(&start_node, start_offset, &end_node, end_offset) == Some(Ordering::Less)
589        {
590            // 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.
591            if let Some(child) = start_node.children().nth(start_offset as usize) {
592                start_node = child;
593                start_offset = 0;
594                continue;
595            }
596            // Step 10.2. Otherwise, if start node is not a Text node or if start offset is start node's length,
597            // set start offset to one plus start node's index, then set start node to its parent.
598            let start_node_as_text = start_node.downcast::<Text>();
599            if start_node_as_text.is_none() || start_offset == start_node.len() {
600                start_offset = 1 + start_node.index();
601                start_node = match start_node.GetParentNode() {
602                    None => break,
603                    Some(node) => node,
604                };
605                continue;
606            }
607            let start_node_as_text =
608                start_node_as_text.expect("Already verified none in previous statement");
609            // Step 10.3. Otherwise:
610            // Step 10.3.1. Remove the first code unit from replacement whitespace, and let element be that code unit.
611            if let Some(element) = replacement_whitespace_chars.next() {
612                // Step 10.3.2. If element is not the same as the start offsetth code unit of start node's data:
613                if start_node_as_text.data().chars().nth(start_offset as usize) != Some(element) {
614                    let character_data = start_node_as_text.upcast::<CharacterData>();
615                    // Step 10.3.2.1. Call insertData(start offset, element) on start node.
616                    if character_data
617                        .InsertData(start_offset, element.to_string().into())
618                        .is_err()
619                    {
620                        unreachable!("Invalid insertion for character at start offset");
621                    }
622                    // Step 10.3.2.2. Call deleteData(start offset + 1, 1) on start node.
623                    if character_data.DeleteData(start_offset + 1, 1).is_err() {
624                        unreachable!("Invalid deletion for character at start offset + 1");
625                    }
626                }
627            }
628            // Step 10.3.3. Add one to start offset.
629            start_offset += 1;
630        }
631    }
632}
633
634pub(crate) trait ContentEditableRange {
635    fn handle_focus_state_for_contenteditable(&self, can_gc: CanGc);
636}
637
638impl ContentEditableRange for HTMLElement {
639    /// There is no specification for this implementation. Instead, it is
640    /// reverse-engineered based on the WPT test
641    /// /selection/contenteditable/initial-selection-on-focus.tentative.html
642    fn handle_focus_state_for_contenteditable(&self, can_gc: CanGc) {
643        if !self.is_editing_host() {
644            return;
645        }
646        let document = self.owner_document();
647        let Some(selection) = document.GetSelection(can_gc) else {
648            return;
649        };
650        let range = self
651            .upcast::<Element>()
652            .ensure_contenteditable_selection_range(&document, can_gc);
653        // If the current range is already associated with this contenteditable
654        // element, then we shouldn't do anything. This is important when focus
655        // is lost and regained, but selection was changed beforehand. In that
656        // case, we should maintain the selection as it were, by not creating
657        // a new range.
658        if selection
659            .active_range()
660            .is_some_and(|active| active == range)
661        {
662            return;
663        }
664        let node = self.upcast::<Node>();
665        let mut selected_node = DomRoot::from_ref(node);
666        let mut previous_eligible_node = DomRoot::from_ref(node);
667        let mut previous_node = DomRoot::from_ref(node);
668        let mut selected_offset = 0;
669        for child in node.traverse_preorder(ShadowIncluding::Yes) {
670            if let Some(text) = child.downcast::<Text>() {
671                // Note that to consider it whitespace, it needs to take more
672                // into account than simply "it has a non-whitespace" character.
673                // Therefore, we need to first check if it is not a whitespace
674                // node and only then can we find what the relevant character is.
675                if !text.is_whitespace_node() {
676                    // A node with "white-space: pre" set must select its first
677                    // character, regardless if that's a whitespace character or not.
678                    let is_pre_formatted_text_node = child
679                        .GetParentElement()
680                        .and_then(|parent| parent.style())
681                        .is_some_and(|style| {
682                            style.get_inherited_text().white_space_collapse ==
683                                WhiteSpaceCollapse::Preserve
684                        });
685                    if !is_pre_formatted_text_node {
686                        // If it isn't pre-formatted, then we should instead select the
687                        // first non-whitespace character.
688                        selected_offset = text
689                            .data()
690                            .find(|c: char| !c.is_whitespace())
691                            .unwrap_or_default() as u32;
692                    }
693                    selected_node = child;
694                    break;
695                }
696            }
697            // For <input>, <textarea>, <hr> and <br> elements, we should select the previous
698            // node, regardless if it was a block node or not
699            if matches!(
700                child.type_id(),
701                NodeTypeId::Element(ElementTypeId::HTMLElement(
702                    HTMLElementTypeId::HTMLInputElement,
703                )) | NodeTypeId::Element(ElementTypeId::HTMLElement(
704                    HTMLElementTypeId::HTMLTextAreaElement,
705                )) | NodeTypeId::Element(ElementTypeId::HTMLElement(
706                    HTMLElementTypeId::HTMLHRElement,
707                )) | NodeTypeId::Element(ElementTypeId::HTMLElement(
708                    HTMLElementTypeId::HTMLBRElement,
709                ))
710            ) {
711                selected_node = previous_node;
712                break;
713            }
714            // When we encounter a non-contenteditable element, we should select the previous
715            // eligible node
716            if child
717                .downcast::<HTMLElement>()
718                .is_some_and(|el| el.ContentEditable().str() == "false")
719            {
720                selected_node = previous_eligible_node;
721                break;
722            }
723            // We can only select block nodes as eligible nodes for the case of non-conenteditable
724            // nodes
725            if child.is_block_node() {
726                previous_eligible_node = child.clone();
727            }
728            previous_node = child;
729        }
730        range.set_start(&selected_node, selected_offset);
731        range.set_end(&selected_node, selected_offset);
732        selection.AddRange(&range);
733    }
734}
735
736pub(crate) trait SelectionExecCommandSupport {
737    fn delete_the_selection(&self, active_range: &Range);
738}
739
740impl SelectionExecCommandSupport for Selection {
741    /// <https://w3c.github.io/editing/docs/execCommand/#delete-the-selection>
742    fn delete_the_selection(&self, active_range: &Range) {
743        // Step 1. If the active range is null, abort these steps and do nothing.
744        //
745        // Always passed in as argument
746
747        // Step 2. Canonicalize whitespace at the active range's start.
748        active_range
749            .start_container()
750            .canonicalize_whitespace(active_range.start_offset(), true);
751
752        // Step 3. Canonicalize whitespace at the active range's end.
753        active_range
754            .end_container()
755            .canonicalize_whitespace(active_range.end_offset(), true);
756
757        // Step 4. Let (start node, start offset) be the last equivalent point for the active range's start.
758        // TODO
759
760        // Step 5. Let (end node, end offset) be the first equivalent point for the active range's end.
761        // TODO
762
763        // Step 6. If (end node, end offset) is not after (start node, start offset):
764        // TODO
765
766        // Step 7. If start node is a Text node and start offset is 0, set start offset to the index of start node,
767        // then set start node to its parent.
768        // TODO
769
770        // Step 8. If end node is a Text node and end offset is its length, set end offset to one plus the index of end node,
771        // then set end node to its parent.
772        // TODO
773
774        // Step 9. Call collapse(start node, start offset) on the context object's selection.
775        // TODO
776
777        // Step 10. Call extend(end node, end offset) on the context object's selection.
778        // TODO
779
780        // Step 11.
781        //
782        // This step does not exist in the spec
783
784        // Step 12. Let start block be the active range's start node.
785        // TODO
786
787        // Step 13. While start block's parent is in the same editing host and start block is an inline node, set start block to its parent.
788        // TODO
789
790        // Step 14. If start block is neither a block node nor an editing host, or "span" is not an allowed child of start block,
791        // or start block is a td or th, set start block to null.
792        // TODO
793
794        // Step 15. Let end block be the active range's end node.
795        // TODO
796
797        // Step 16. While end block's parent is in the same editing host and end block is an inline node, set end block to its parent.
798        // TODO
799
800        // Step 17. If end block is neither a block node nor an editing host, or "span" is not an allowed child of end block,
801        // or end block is a td or th, set end block to null.
802        // TODO
803
804        // Step 18.
805        //
806        // This step does not exist in the spec
807
808        // Step 19. Record current states and values, and let overrides be the result.
809        // TODO
810
811        // Step 20.
812        //
813        // This step does not exist in the spec
814
815        // Step 21. If start node and end node are the same, and start node is an editable Text node:
816        //
817        // As per the spec:
818        // > NOTE: This whole piece of the algorithm is based on deleteContents() in DOM Range, copy-pasted and then adjusted to fit.
819        let _ = active_range.DeleteContents();
820
821        // Step 22. If start node is an editable Text node, call deleteData() on it, with start offset as
822        // the first argument and (length of start node − start offset) as the second argument.
823        // TODO
824
825        // Step 23. Let node list be a list of nodes, initially empty.
826        // TODO
827
828        // Step 24. For each node contained in the active range, append node to node list if the
829        // last member of node list (if any) is not an ancestor of node; node is editable;
830        // and node is not a thead, tbody, tfoot, tr, th, or td.
831        // TODO
832
833        // Step 25. For each node in node list:
834        // TODO
835
836        // Step 26. If end node is an editable Text node, call deleteData(0, end offset) on it.
837        // TODO
838
839        // Step 27. Canonicalize whitespace at the active range's start, with fix collapsed space false.
840        // TODO
841
842        // Step 28. Canonicalize whitespace at the active range's end, with fix collapsed space false.
843        // TODO
844
845        // Step 29.
846        //
847        // This step does not exist in the spec
848
849        // Step 30. If block merging is false, or start block or end block is null, or start block is not
850        // in the same editing host as end block, or start block and end block are the same:
851        // TODO
852
853        // Step 31. If start block has one child, which is a collapsed block prop, remove its child from it.
854        // TODO
855
856        // Step 32. If start block is an ancestor of end block:
857        // TODO
858
859        // Step 33. Otherwise, if start block is a descendant of end block:
860        // TODO
861
862        // Step 34. Otherwise:
863        // TODO
864
865        // Step 35.
866        //
867        // This step does not exist in the spec
868
869        // Step 36. Let ancestor be start block.
870        // TODO
871
872        // Step 37. While ancestor has an inclusive ancestor ol in the same editing host whose nextSibling is
873        // also an ol in the same editing host, or an inclusive ancestor ul in the same editing host whose nextSibling
874        // is also a ul in the same editing host:
875        // TODO
876
877        // Step 38. Restore the values from values.
878        // TODO
879
880        // Step 39. If start block has no children, call createElement("br") on the context object and
881        // append the result as the last child of start block.
882        // TODO
883
884        // Step 40. Remove extraneous line breaks at the end of start block.
885        // TODO
886
887        // Step 41. Restore states and values from overrides.
888        // TODO
889    }
890}