Skip to main content

script/dom/execcommand/commands/
forwarddelete.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 js::context::JSContext;
6use script_bindings::codegen::GenericBindings::NodeBinding::NodeMethods;
7use script_bindings::codegen::GenericBindings::SelectionBinding::SelectionMethods;
8use script_bindings::inheritance::Castable;
9
10use crate::dom::document::Document;
11use crate::dom::html::htmlbrelement::HTMLBRElement;
12use crate::dom::html::htmlhrelement::HTMLHRElement;
13use crate::dom::html::htmlimageelement::HTMLImageElement;
14use crate::dom::html::htmltableelement::HTMLTableElement;
15use crate::dom::selection::Selection;
16use crate::dom::text::Text;
17
18/// <https://w3c.github.io/editing/docs/execCommand/#the-forwarddelete-command>
19pub(crate) fn execute_forward_delete_command(
20    cx: &mut JSContext,
21    document: &Document,
22    selection: &Selection,
23) -> bool {
24    // Step 1. If the active range is not collapsed, delete the selection and return true.
25    let active_range = selection
26        .active_range()
27        .expect("Must always have an active range");
28    if !active_range.collapsed() {
29        selection.delete_the_selection(
30            cx,
31            document,
32            Default::default(),
33            Default::default(),
34            Default::default(),
35        );
36        return true;
37    }
38
39    // Step 2. Canonicalize whitespace at the active range's start.
40    active_range
41        .start_container()
42        .canonicalize_whitespace(cx, active_range.start_offset(), true);
43
44    // Step 3. Let node and offset be the active range's start node and offset.
45    let mut node = active_range.start_container();
46    let mut offset = active_range.start_offset();
47
48    // Step 4. Repeat the following steps:
49    loop {
50        // Step 4.1. If offset is the length of node and node's nextSibling is an editable invisible node,
51        //           remove node's nextSibling from its parent.
52        if offset == node.len() &&
53            let Some(sibling) = node.GetNextSibling() &&
54            sibling.is_editable() &&
55            sibling.is_invisible(cx.no_gc())
56        {
57            sibling.remove_self(cx);
58            continue;
59        }
60
61        // Step 4.2. Otherwise, if node has a child with index offset and that child is an editable invisible node,
62        //           remove that child from node.
63        if let Some(child) = node.children().nth(offset as usize) &&
64            child.is_editable() &&
65            child.is_invisible(cx.no_gc())
66        {
67            child.remove_self(cx);
68            continue;
69        }
70
71        // Step 4.3. Otherwise, if offset is the length of node and node is an inline node, or if node is invisible,
72        //           set offset to one plus the index of node, then set node to its parent.
73        if (offset == node.len() && node.is_inline_node()) || node.is_invisible(cx.no_gc()) {
74            offset = 1 + node.index();
75            node = node
76                .GetParentNode()
77                .expect("Node should always have a parent.");
78            continue;
79        }
80
81        // Step 4.4. Otherwise, if node has a child with index offset and that child is neither a block node nor a br
82        //           nor an img nor a collapsed block prop, set node to that child, then set offset to zero.
83        if let Some(child) = node.children().nth(offset as usize) &&
84            !(child.is_block_node() ||
85                child.is::<HTMLBRElement>() ||
86                child.is::<HTMLImageElement>() ||
87                child.is_collapsed_block_prop(cx.no_gc()))
88        {
89            node = child;
90            offset = 0;
91            continue;
92        }
93
94        // Step 4.5. Otherwise, break from this loop.
95        break;
96    }
97
98    // Step 5. If node is a Text node and offset is not node's length:
99    if node.is::<Text>() && offset != node.len() {
100        // Step 5.1. Let end offset be offset plus one.
101        let end_offset = offset + 1;
102
103        // Step 5.2. While end offset is not node's length and the end offsetth code unit of node's data has
104        //           general category M when interpreted as a Unicode code point, add one to end offset.
105        // TODO: Do the whole thing about diacritics here.
106        //       Also figure out if the spec note on this is right and we should defer to "grapheme cluster boundaries, using UAX#29 or something".
107
108        // Step 5.3. Call collapse(node, offset) on the context object's selection.
109        if selection.Collapse(cx, Some(&node), offset).is_err() {
110            unreachable!("Must always be able to collapse the selection");
111        }
112
113        // Step 5.4. Call extend(node, end offset) on the context object's selection.
114        if selection.Extend(cx, &node, end_offset).is_err() {
115            unreachable!("Must always be able to extend the selection");
116        }
117
118        // Step 5.5. Delete the selection.
119        selection.delete_the_selection(
120            cx,
121            document,
122            Default::default(),
123            Default::default(),
124            Default::default(),
125        );
126
127        // Step 5.6. Return true.
128        return true;
129    }
130
131    // Step 6. If node is an inline node, return true.
132    if node.is_inline_node() {
133        return true;
134    }
135
136    // Step 7. If node has a child with index offset and that child is a br or hr or img, but is not a collapsed block prop:
137    if let Some(child) = node.children().nth(offset as usize) &&
138        (child.is::<HTMLBRElement>() ||
139            child.is::<HTMLHRElement>() ||
140            child.is::<HTMLImageElement>()) &&
141        !child.is_collapsed_block_prop(cx.no_gc())
142    {
143        // Step 7.1. Call collapse(node, offset) on the context object's selection.
144        if selection.Collapse(cx, Some(&node), offset).is_err() {
145            unreachable!("Must always be able to collapse the selection");
146        }
147
148        // Step 7.2. Call extend(node, offset + 1) on the context object's selection.
149        if selection.Extend(cx, &node, offset + 1).is_err() {
150            unreachable!("Must always be able to extend the selection");
151        }
152
153        // Step 7.3. Delete the selection.
154        selection.delete_the_selection(
155            cx,
156            document,
157            Default::default(),
158            Default::default(),
159            Default::default(),
160        );
161
162        // Step 7.4. Return true.
163        return true;
164    }
165
166    // Step 8. Let end node equal node and let end offset equal offset.
167    let mut end_node = node.clone();
168    let mut end_offset = offset;
169
170    // Step 9. If end node has a child with index end offset, and that child is a collapsed block prop, add one to end offset.
171    if let Some(child) = end_node.children().nth(end_offset as usize) &&
172        child.is_collapsed_block_prop(cx.no_gc())
173    {
174        end_offset += 1;
175    }
176
177    // Step 10. Repeat the following steps:
178    loop {
179        // Step 10.1. If end offset is the length of end node, set end offset to one plus the index of end node and then set end node to its parent.
180        if end_offset == end_node.len() {
181            // The spec doesn't account for there not being a parent, but we need to just bail in case there is none,
182            // because we can't exactly walk out of the tree.
183            if let Some(parent) = end_node.GetParentNode() {
184                end_offset = 1 + end_node.index();
185                end_node = parent;
186            } else {
187                break;
188            }
189            continue;
190        }
191
192        // Step 10.2. Otherwise, if end node has an editable invisible child with index end offset, remove it from end node.
193        if let Some(child) = end_node.children().nth(end_offset as usize) &&
194            child.is_editable() &&
195            child.is_invisible(cx.no_gc())
196        {
197            child.remove_self(cx);
198            continue;
199        }
200
201        // Step 10.3. Otherwise, break from this loop.
202        break;
203    }
204
205    // Step 11. If the child of end node with index end offset minus one is a table, return true.
206    if end_offset > 0 &&
207        let Some(child) = end_node.children().nth((end_offset - 1) as usize) &&
208        child.is::<HTMLTableElement>()
209    {
210        return true;
211    }
212
213    // Step 12. If the child of end node with index end offset is a table:
214    if let Some(child) = end_node.children().nth(end_offset as usize) &&
215        child.is::<HTMLTableElement>()
216    {
217        // Step 12.1. Call collapse(end node, end offset) on the context object's selection.
218        if selection.Collapse(cx, Some(&end_node), end_offset).is_err() {
219            unreachable!("Must always be able to collapse the selection");
220        }
221
222        // Step 12.2. Call extend(end node, end offset + 1) on the context object's selection.
223        if selection.Extend(cx, &end_node, end_offset + 1).is_err() {
224            unreachable!("Must always be able to extend the selection");
225        }
226
227        // Step 12.3. Return true.
228        return true;
229    }
230
231    // Step 13. If offset is the length of node, and the child of end node with index end offset is an hr or br:
232    if offset == node.len() &&
233        let Some(child) = end_node.children().nth(end_offset as usize) &&
234        (child.is::<HTMLHRElement>() || child.is::<HTMLBRElement>())
235    {
236        // Step 13.1. Call collapse(end node, end offset) on the context object's selection.
237        if selection.Collapse(cx, Some(&end_node), end_offset).is_err() {
238            unreachable!("Must always be able to collapse the selection");
239        }
240
241        // Step 13.2. Call extend(end node, end offset + 1) on the context object's selection.
242        if selection.Extend(cx, &end_node, end_offset + 1).is_err() {
243            unreachable!("Must always be able to extend the selection");
244        }
245
246        // Step 13.3. Delete the selection.
247        selection.delete_the_selection(
248            cx,
249            document,
250            Default::default(),
251            Default::default(),
252            Default::default(),
253        );
254
255        // Step 13.4. Call collapse(node, offset) on the selection.
256        if selection.Collapse(cx, Some(&node), offset).is_err() {
257            unreachable!("Must always be able to collapse the selection");
258        }
259
260        // Step 13.5. Return true.
261        return true;
262    }
263
264    // Step 14. While end node has a child with index end offset:
265    while let Some(child) = end_node.children().nth(end_offset as usize) {
266        // Step 14.1. If end node's child with index end offset is editable and invisible, remove it from end node.
267        if child.is_editable() && child.is_invisible(cx.no_gc()) {
268            child.remove_self(cx);
269            continue;
270        }
271
272        // Step 14.2. Otherwise, set end node to its child with index end offset and set end offset to zero.
273        end_node = child;
274        end_offset = 0;
275    }
276
277    // Step 15. Call collapse(node, offset) on the context object's selection.
278    if selection.Collapse(cx, Some(&node), offset).is_err() {
279        unreachable!("Must always be able to collapse the selection");
280    }
281
282    // Step 16. Call extend(end node, end offset) on the context object's selection.
283    if selection.Extend(cx, &end_node, end_offset).is_err() {
284        unreachable!("Must always be able to extend the selection");
285    }
286
287    // Step 17. Delete the selection.
288    selection.delete_the_selection(
289        cx,
290        document,
291        Default::default(),
292        Default::default(),
293        Default::default(),
294    );
295
296    // This isn't in the spec, but it probably should be.
297    true
298}