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}