Skip to main content

script/dom/execcommand/commands/
delete.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 html5ever::local_name;
6use script_bindings::codegen::GenericBindings::SelectionBinding::SelectionMethods;
7use script_bindings::inheritance::Castable;
8
9use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods;
10use crate::dom::document::Document;
11use crate::dom::element::Element;
12use crate::dom::execcommand::contenteditable::node::{
13    node_matches_local_name, record_the_values, restore_the_values, split_the_parent,
14};
15use crate::dom::execcommand::contenteditable::selection::SelectionDeleteDirection;
16use crate::dom::html::htmlanchorelement::HTMLAnchorElement;
17use crate::dom::html::htmlbrelement::HTMLBRElement;
18use crate::dom::html::htmlhrelement::HTMLHRElement;
19use crate::dom::html::htmlimageelement::HTMLImageElement;
20use crate::dom::html::htmllielement::HTMLLIElement;
21use crate::dom::html::htmltableelement::HTMLTableElement;
22use crate::dom::selection::Selection;
23use crate::dom::text::Text;
24
25/// <https://w3c.github.io/editing/docs/execCommand/#the-delete-command>
26pub(crate) fn execute_delete_command(
27    cx: &mut js::context::JSContext,
28    document: &Document,
29    selection: &Selection,
30) -> bool {
31    let active_range = selection
32        .active_range(cx)
33        .expect("Must always have an active range");
34    // Step 1. If the active range is not collapsed, delete the selection and return true.
35    if !active_range.collapsed() {
36        selection.delete_the_selection(
37            cx,
38            document,
39            Default::default(),
40            Default::default(),
41            Default::default(),
42        );
43        return true;
44    }
45
46    // Step 2. Canonicalize whitespace at the active range's start.
47    active_range
48        .start_container()
49        .canonicalize_whitespace(cx, active_range.start_offset(), true);
50
51    // Step 3. Let node and offset be the active range's start node and offset.
52    let mut node = active_range.start_container();
53    let mut offset = active_range.start_offset();
54
55    // Step 4. Repeat the following steps:
56    loop {
57        // Step 4.1. If offset is zero and node's previousSibling is an editable invisible node,
58        // remove node's previousSibling from its parent.
59        if offset == 0 &&
60            let Some(sibling) = node.GetPreviousSibling() &&
61            sibling.is_editable() &&
62            sibling.is_invisible(cx.no_gc())
63        {
64            sibling.remove_self(cx);
65            continue;
66        }
67        // Step 4.2. Otherwise, if node has a child with index offset − 1 and that child is an editable invisible node,
68        // remove that child from node, then subtract one from offset.
69        if offset > 0 {
70            let child = node
71                .children_unrooted(cx.no_gc())
72                .nth(offset as usize - 1)
73                .map(|node| node.as_rooted());
74            if let Some(child) = child &&
75                child.is_editable() &&
76                child.is_invisible(cx.no_gc())
77            {
78                child.remove_self(cx);
79                offset -= 1;
80                continue;
81            }
82        }
83        // Step 4.3. Otherwise, if offset is zero and node is an inline node, or if node is an invisible node,
84        // set offset to the index of node, then set node to its parent.
85        if (offset == 0 && node.is_inline_node()) || node.is_invisible(cx.no_gc()) {
86            offset = node.index();
87            node = node.GetParentNode().expect("Must always have a parent");
88            continue;
89        }
90        if offset > 0 {
91            let child = node
92                .children_unrooted(cx.no_gc())
93                .nth(offset as usize - 1)
94                .map(|node| node.as_rooted());
95            if let Some(child) = child {
96                // Step 4.4. Otherwise, if node has a child with index offset − 1 and that child is an editable a,
97                // remove that child from node, preserving its descendants. Then return true.
98                if child.is_editable() && child.is::<HTMLAnchorElement>() {
99                    child.remove_preserving_its_descendants(cx);
100                    return true;
101                }
102                // Step 4.5. Otherwise, if node has a child with index offset − 1 and that child is not a block node or a br or an img,
103                // set node to that child, then set offset to the length of node.
104                if !(child.is_block_node() ||
105                    child.is::<HTMLBRElement>() ||
106                    child.is::<HTMLImageElement>())
107                {
108                    node = child;
109                    offset = node.len();
110                    continue;
111                }
112            }
113        }
114        // Step 4.6. Otherwise, break from this loop.
115        break;
116    }
117
118    // Step 5. If node is a Text node and offset is not zero, or if node is
119    // a block node that has a child with index offset − 1 and that child is a br or hr or img:
120    if (node.is::<Text>() && offset != 0) ||
121        (offset > 0 &&
122            node.is_block_node() &&
123            node.children_unrooted(cx.no_gc())
124                .nth(offset as usize - 1)
125                .is_some_and(|child| {
126                    child.is::<HTMLBRElement>() ||
127                        child.is::<HTMLHRElement>() ||
128                        child.is::<HTMLImageElement>()
129                }))
130    {
131        // Step 5.1. Call collapse(node, offset) on the context object's selection.
132        let _ = selection.Collapse(cx, Some(&node), offset);
133        // Step 5.2. Call extend(node, offset − 1) on the context object's selection.
134        let _ = selection.Extend(cx, &node, offset - 1);
135        // Step 5.3. Delete the selection.
136        selection.delete_the_selection(
137            cx,
138            document,
139            Default::default(),
140            Default::default(),
141            Default::default(),
142        );
143        // Step 5.4. Return true.
144        return true;
145    }
146
147    // Step 6. If node is an inline node, return true.
148    if node.is_inline_node() {
149        return true;
150    }
151
152    // Step 7. If node is an li or dt or dd and is the first child of its parent, and offset is zero:
153    if node_matches_local_name!(
154        node,
155        local_name!("li") | local_name!("dt") | local_name!("dd")
156    ) && node
157        .GetParentNode()
158        .and_then(|parent| parent.children_unrooted(cx.no_gc()).next())
159        .is_some_and(|first| **first == *node) &&
160        offset == 0
161    {
162        // Step 7.1. Let items be a list of all lis that are ancestors of node.
163        let items = node
164            .ancestors()
165            .filter(|ancestor| ancestor.is::<HTMLLIElement>());
166        // Step 7.2. Normalize sublists of each item in items.
167        for item in items {
168            item.normalize_sublists(cx);
169        }
170        // Step 7.3. Record the values of the one-node list consisting of node, and let values be the result.
171        let values = record_the_values(vec![node.clone()]);
172        // Step 7.4. Split the parent of the one-node list consisting of node.
173        split_the_parent(cx, &[&node]);
174        // Step 7.5. Restore the values from values.
175        restore_the_values(cx, values);
176        // Step 7.6. If node is a dd or dt, and it is not an allowed child of
177        // any of its ancestors in the same editing host,
178        // set the tag name of node to the default single-line container name
179        // and let node be the result.
180        if node_matches_local_name!(node, local_name!("dd") | local_name!("dt")) &&
181            node.is_no_allowed_child_in_same_editing_host(cx.no_gc())
182        {
183            node = node
184                .downcast::<Element>()
185                .expect("Must always be an element")
186                .set_the_tag_name(cx, document.default_single_line_container_name().str());
187        }
188        // Step 7.7. Fix disallowed ancestors of node.
189        node.fix_disallowed_ancestors(cx, document);
190        // Step 7.8. Return true.
191        return true;
192    }
193
194    // Step 8. Let start node equal node and let start offset equal offset.
195    let mut start_node = node.clone();
196    let mut start_offset = offset;
197
198    // Step 9. Repeat the following steps:
199    loop {
200        // Step 9.1. If start offset is zero,
201        // set start offset to the index of start node and then set start node to its parent.
202        if start_offset == 0 {
203            // NOTE: This is not in the spec, but required in case we are traversing out of
204            // an editing host and end up at the root node. Since below we start deleting
205            // backwards and stop at the editing host, it's fine to stop at the root node
206            // as well.
207            let Some(parent) = start_node.GetParentNode() else {
208                break;
209            };
210            start_offset = start_node.index();
211            start_node = parent;
212            continue;
213        }
214        // Step 9.2. Otherwise, if start node has an editable invisible child with index start offset minus one,
215        // remove it from start node and subtract one from start offset.
216        assert!(
217            start_offset > 0,
218            "Must always have a start_offset greater than one"
219        );
220        let child = start_node
221            .children_unrooted(cx.no_gc())
222            .nth(start_offset as usize - 1)
223            .map(|node| node.as_rooted());
224        if let Some(child) = child &&
225            child.is_editable() &&
226            child.is_invisible(cx.no_gc())
227        {
228            child.remove_self(cx);
229            start_offset -= 1;
230            continue;
231        }
232        // Step 9.3. Otherwise, break from this loop.
233        break;
234    }
235
236    // Step 10. If offset is zero, and node has an editable inclusive ancestor in the same editing host that's an indentation element:
237    // TODO
238
239    // Step 11. If the child of start node with index start offset is a table, return true.
240    if start_node
241        .children_unrooted(cx.no_gc())
242        .nth(start_offset as usize)
243        .is_some_and(|child| child.is::<HTMLTableElement>())
244    {
245        return true;
246    }
247
248    // Step 12. If start node has a child with index start offset − 1, and that child is a table:
249    if start_offset > 0 &&
250        start_node
251            .children_unrooted(cx.no_gc())
252            .nth((start_offset - 1) as usize)
253            .is_some_and(|child| child.is::<HTMLTableElement>())
254    {
255        // Step 12.1. Call collapse(start node, start offset − 1) on the context object's selection.
256        let _ = selection.Collapse(cx, Some(&start_node), start_offset - 1);
257
258        // Step 12.2. Call extend(start node, start offset) on the context object's selection.
259        let _ = selection.Extend(cx, &start_node, start_offset);
260
261        // Step 12.3. Return true.
262        return true;
263    }
264
265    // Step 13. If offset is zero; and either the child of start node with index start offset
266    // minus one is an hr, or the child is a br whose previousSibling is either a br or not an inline node:
267    if offset == 0 &&
268        (start_offset > 0 &&
269            start_node
270                .children_unrooted(cx.no_gc())
271                .nth(start_offset as usize - 1)
272                .is_some_and(|child| {
273                    child.is::<HTMLHRElement>() ||
274                        (child.is::<HTMLBRElement>() &&
275                            child.GetPreviousSibling().is_some_and(|previous| {
276                                previous.is::<HTMLBRElement>() || !previous.is_inline_node()
277                            }))
278                }))
279    {
280        // Step 13.1. Call collapse(start node, start offset − 1) on the context object's selection.
281        let _ = selection.Collapse(cx, Some(&start_node), start_offset - 1);
282        // Step 13.2. Call extend(start node, start offset) on the context object's selection.
283        let _ = selection.Extend(cx, &start_node, start_offset);
284        // Step 13.3. Delete the selection.
285        selection.delete_the_selection(
286            cx,
287            document,
288            Default::default(),
289            Default::default(),
290            Default::default(),
291        );
292        // Step 13.4. Call collapse(node, offset) on the selection.
293        let _ = selection.Collapse(cx, Some(&node), offset);
294        // Step 13.5. Return true.
295        return true;
296    }
297
298    // Step 14. If the child of start node with index start offset is an li or dt or dd, and
299    // that child's firstChild is an inline node, and start offset is not zero:
300    // TODO
301
302    // Step 15. If start node's child with index start offset is an li or dt or dd, and
303    // that child's previousSibling is also an li or dt or dd:
304    // TODO
305
306    // Step 16. While start node has a child with index start offset minus one:
307    loop {
308        if start_offset == 0 {
309            break;
310        }
311        let child = start_node
312            .children_unrooted(cx.no_gc())
313            .nth(start_offset as usize - 1)
314            .map(|node| node.as_rooted());
315        let Some(child) = child else {
316            break;
317        };
318        // Step 16.1. If start node's child with index start offset minus one
319        // is editable and invisible, remove it from start node, then subtract one from start offset.
320        if child.is_editable() && child.is_invisible(cx.no_gc()) {
321            child.remove_self(cx);
322            start_offset -= 1;
323        } else {
324            // Step 16.2. Otherwise, set start node to its child with index start offset minus one,
325            // then set start offset to the length of start node.
326            start_node = child;
327            start_offset = start_node.len();
328        }
329    }
330
331    // Step 17. Call collapse(start node, start offset) on the context object's selection.
332    let _ = selection.Collapse(cx, Some(&start_node), start_offset);
333
334    // Step 18. Call extend(node, offset) on the context object's selection.
335    let _ = selection.Extend(cx, &node, offset);
336
337    // Step 19. Delete the selection, with direction "backward".
338    selection.delete_the_selection(
339        cx,
340        document,
341        Default::default(),
342        Default::default(),
343        SelectionDeleteDirection::Backward,
344    );
345
346    // Step 20. Return true.
347    true
348}