script/dom/execcommand/contenteditable/selection.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 js::context::JSContext;
8use script_bindings::inheritance::Castable;
9
10use crate::dom::abstractrange::bp_position;
11use crate::dom::bindings::codegen::Bindings::CharacterDataBinding::CharacterDataMethods;
12use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods;
13use crate::dom::bindings::codegen::Bindings::SelectionBinding::SelectionMethods;
14use crate::dom::bindings::codegen::Bindings::TextBinding::TextMethods;
15use crate::dom::bindings::root::{DomRoot, DomSlice};
16use crate::dom::bindings::str::DOMString;
17use crate::dom::characterdata::CharacterData;
18use crate::dom::document::Document;
19use crate::dom::execcommand::basecommand::CommandName;
20use crate::dom::execcommand::contenteditable::node::{
21 NodeOrString, is_allowed_child, move_preserving_ranges, record_the_values, restore_the_values,
22 split_the_parent,
23};
24use crate::dom::html::htmlbrelement::HTMLBRElement;
25use crate::dom::html::htmlelement::HTMLElement;
26use crate::dom::html::htmltablecellelement::HTMLTableCellElement;
27use crate::dom::html::htmltablerowelement::HTMLTableRowElement;
28use crate::dom::html::htmltablesectionelement::HTMLTableSectionElement;
29use crate::dom::node::Node;
30use crate::dom::selection::Selection;
31use crate::dom::text::Text;
32
33#[derive(Default, PartialEq)]
34pub(crate) enum SelectionDeletionBlockMerging {
35 #[default]
36 Merge,
37 Skip,
38}
39
40#[derive(Default, PartialEq)]
41pub(crate) enum SelectionDeletionStripWrappers {
42 #[default]
43 Strip,
44}
45
46#[derive(Default, PartialEq)]
47pub(crate) enum SelectionDeleteDirection {
48 #[default]
49 Forward,
50 Backward,
51}
52
53trait EquivalentPoint {
54 fn previous_equivalent_point(&self) -> Option<(DomRoot<Node>, u32)>;
55 fn next_equivalent_point(&self) -> Option<(DomRoot<Node>, u32)>;
56 fn first_equivalent_point(self) -> (DomRoot<Node>, u32);
57 fn last_equivalent_point(self) -> (DomRoot<Node>, u32);
58}
59
60impl EquivalentPoint for (DomRoot<Node>, u32) {
61 /// <https://w3c.github.io/editing/docs/execCommand/#previous-equivalent-point>
62 fn previous_equivalent_point(&self) -> Option<(DomRoot<Node>, u32)> {
63 let (node, offset) = self;
64 // Step 1. If node's length is zero, return null.
65 let len = node.len();
66 if len == 0 {
67 return None;
68 }
69 // Step 2. If offset is 0, and node's parent is not null, and node is an inline node,
70 // return (node's parent, node's index).
71 if *offset == 0 && node.is_inline_node() {
72 if let Some(parent) = node.GetParentNode() {
73 return Some((parent, node.index()));
74 }
75 }
76 // Step 3. If node has a child with index offset − 1, and that child's length is not zero,
77 // and that child is an inline node, return (that child, that child's length).
78 if *offset > 0 {
79 if let Some(child) = node.children().nth(*offset as usize - 1) {
80 if !child.is_empty() && child.is_inline_node() {
81 let len = child.len();
82 return Some((child, len));
83 }
84 }
85 }
86
87 // Step 4. Return null.
88 None
89 }
90
91 /// <https://w3c.github.io/editing/docs/execCommand/#next-equivalent-point>
92 fn next_equivalent_point(&self) -> Option<(DomRoot<Node>, u32)> {
93 let (node, offset) = self;
94 // Step 1. If node's length is zero, return null.
95 let len = node.len();
96 if len == 0 {
97 return None;
98 }
99
100 // Step 2.
101 //
102 // This step does not exist in the spec
103
104 // Step 3. If offset is node's length, and node's parent is not null, and node is an inline node,
105 // return (node's parent, 1 + node's index).
106 if *offset == len && node.is_inline_node() {
107 if let Some(parent) = node.GetParentNode() {
108 return Some((parent, node.index() + 1));
109 }
110 }
111
112 // Step 4.
113 //
114 // This step does not exist in the spec
115
116 // Step 5. If node has a child with index offset, and that child's length is not zero,
117 // and that child is an inline node, return (that child, 0).
118 if let Some(child) = node.children().nth(*offset as usize) {
119 if !child.is_empty() && child.is_inline_node() {
120 return Some((child, 0));
121 }
122 }
123
124 // Step 6.
125 //
126 // This step does not exist in the spec
127
128 // Step 7. Return null.
129 None
130 }
131
132 /// <https://w3c.github.io/editing/docs/execCommand/#first-equivalent-point>
133 fn first_equivalent_point(self) -> (DomRoot<Node>, u32) {
134 let mut previous_equivalent_point = self;
135 // Step 1. While (node, offset)'s previous equivalent point is not null, set (node, offset) to its previous equivalent point.
136 loop {
137 if let Some(next) = previous_equivalent_point.previous_equivalent_point() {
138 previous_equivalent_point = next;
139 } else {
140 // Step 2. Return (node, offset).
141 return previous_equivalent_point;
142 }
143 }
144 }
145
146 /// <https://w3c.github.io/editing/docs/execCommand/#last-equivalent-point>
147 fn last_equivalent_point(self) -> (DomRoot<Node>, u32) {
148 let mut next_equivalent_point = self;
149 // Step 1. While (node, offset)'s next equivalent point is not null, set (node, offset) to its next equivalent point.
150 loop {
151 if let Some(next) = next_equivalent_point.next_equivalent_point() {
152 next_equivalent_point = next;
153 } else {
154 // Step 2. Return (node, offset).
155 return next_equivalent_point;
156 }
157 }
158 }
159}
160
161impl Selection {
162 /// <https://w3c.github.io/editing/docs/execCommand/#delete-the-selection>
163 pub(crate) fn delete_the_selection(
164 &self,
165 cx: &mut JSContext,
166 context_object: &Document,
167 block_merging: SelectionDeletionBlockMerging,
168 strip_wrappers: SelectionDeletionStripWrappers,
169 direction: SelectionDeleteDirection,
170 ) {
171 // Step 1. If the active range is null, abort these steps and do nothing.
172 let Some(active_range) = self.active_range() else {
173 return;
174 };
175
176 // Step 2. Canonicalize whitespace at the active range's start.
177 active_range
178 .start_container()
179 .canonicalize_whitespace(active_range.start_offset(), true);
180
181 // Step 3. Canonicalize whitespace at the active range's end.
182 active_range
183 .end_container()
184 .canonicalize_whitespace(active_range.end_offset(), true);
185
186 // Step 4. Let (start node, start offset) be the last equivalent point for the active range's start.
187 let (mut start_node, mut start_offset) =
188 (active_range.start_container(), active_range.start_offset()).last_equivalent_point();
189
190 // Step 5. Let (end node, end offset) be the first equivalent point for the active range's end.
191 let (mut end_node, mut end_offset) =
192 (active_range.end_container(), active_range.end_offset()).first_equivalent_point();
193
194 // Step 6. If (end node, end offset) is not after (start node, start offset):
195 if bp_position(&end_node, end_offset, &start_node, start_offset) != Some(Ordering::Greater)
196 {
197 // Step 6.1. If direction is "forward", call collapseToStart() on the context object's selection.
198 if direction == SelectionDeleteDirection::Forward {
199 if self.CollapseToStart(cx).is_err() {
200 unreachable!("Should be able to collapse to start");
201 }
202 } else {
203 // Step 6.2. Otherwise, call collapseToEnd() on the context object's selection.
204 if self.CollapseToEnd(cx).is_err() {
205 unreachable!("Should be able to collapse to end");
206 }
207 }
208 // Step 6.3. Abort these steps.
209 return;
210 }
211
212 // Step 7. If start node is a Text node and start offset is 0, set start offset to the index of start node,
213 // then set start node to its parent.
214 if start_node.is::<Text>() && start_offset == 0 {
215 start_offset = start_node.index();
216 start_node = start_node
217 .GetParentNode()
218 .expect("Must always have a parent");
219 }
220
221 // 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,
222 // then set end node to its parent.
223 if end_node.is::<Text>() && end_offset == end_node.len() {
224 end_offset = end_node.index() + 1;
225 end_node = end_node.GetParentNode().expect("Must always have a parent");
226 }
227
228 // Step 9. Call collapse(start node, start offset) on the context object's selection.
229 if self.Collapse(cx, Some(&start_node), start_offset).is_err() {
230 unreachable!("Must always be able to collapse");
231 }
232
233 // Step 10. Call extend(end node, end offset) on the context object's selection.
234 if self.Extend(cx, &end_node, end_offset).is_err() {
235 unreachable!("Must always be able to extend");
236 }
237
238 // Step 11.
239 //
240 // This step does not exist in the spec
241
242 // Step 12. Let start block be the active range's start node.
243 let Some(active_range) = self.active_range() else {
244 return;
245 };
246 let mut start_block = active_range.start_container();
247
248 // Step 13. While start block's parent is in the same editing host and start block is an inline node,
249 // set start block to its parent.
250 loop {
251 if start_block.is_inline_node() {
252 if let Some(parent) = start_block.GetParentNode() {
253 if parent.same_editing_host(&start_node) {
254 start_block = parent;
255 continue;
256 }
257 }
258 }
259 break;
260 }
261
262 // Step 14. If start block is neither a block node nor an editing host,
263 // or "span" is not an allowed child of start block,
264 // or start block is a td or th, set start block to null.
265 let start_block = if (!start_block.is_block_node() && !start_block.is_editing_host()) ||
266 !is_allowed_child(
267 NodeOrString::String("span".to_owned()),
268 NodeOrString::Node(start_block.clone()),
269 ) ||
270 start_block.is::<HTMLTableCellElement>()
271 {
272 None
273 } else {
274 Some(start_block)
275 };
276
277 // Step 15. Let end block be the active range's end node.
278 let mut end_block = active_range.end_container();
279
280 // 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.
281 loop {
282 if end_block.is_inline_node() {
283 if let Some(parent) = end_block.GetParentNode() {
284 if parent.same_editing_host(&end_block) {
285 end_block = parent;
286 continue;
287 }
288 }
289 }
290 break;
291 }
292
293 // Step 17. If end block is neither a block node nor an editing host, or "span" is not an allowed child of end block,
294 // or end block is a td or th, set end block to null.
295 let end_block = if (!end_block.is_block_node() && !end_block.is_editing_host()) ||
296 !is_allowed_child(
297 NodeOrString::String("span".to_owned()),
298 NodeOrString::Node(end_block.clone()),
299 ) ||
300 end_block.is::<HTMLTableCellElement>()
301 {
302 None
303 } else {
304 Some(end_block)
305 };
306
307 // Step 18.
308 //
309 // This step does not exist in the spec
310
311 // Step 19. Record current states and values, and let overrides be the result.
312 let overrides = active_range.record_current_states_and_values();
313
314 // Step 20.
315 //
316 // This step does not exist in the spec
317
318 // Step 21. If start node and end node are the same, and start node is an editable Text node:
319 if start_node == end_node && start_node.is_editable() {
320 if let Some(start_text) = start_node.downcast::<Text>() {
321 // Step 21.1. Call deleteData(start offset, end offset − start offset) on start node.
322 if start_text
323 .upcast::<CharacterData>()
324 .DeleteData(start_offset, end_offset - start_offset)
325 .is_err()
326 {
327 unreachable!("Must always be able to delete");
328 }
329 // Step 21.2. Canonicalize whitespace at (start node, start offset), with fix collapsed space false.
330 start_node.canonicalize_whitespace(start_offset, false);
331 // Step 21.3. If direction is "forward", call collapseToStart() on the context object's selection.
332 if direction == SelectionDeleteDirection::Forward {
333 if self.CollapseToStart(cx).is_err() {
334 unreachable!("Should be able to collapse to start");
335 }
336 } else {
337 // Step 21.4. Otherwise, call collapseToEnd() on the context object's selection.
338 if self.CollapseToEnd(cx).is_err() {
339 unreachable!("Should be able to collapse to end");
340 }
341 }
342 // Step 21.5. Restore states and values from overrides.
343 active_range.restore_states_and_values(cx, self, context_object, overrides);
344
345 // Step 21.6. Abort these steps.
346 return;
347 }
348 }
349
350 // Step 22. If start node is an editable Text node, call deleteData() on it, with start offset as
351 // the first argument and (length of start node − start offset) as the second argument.
352 if start_node.is_editable() {
353 if let Some(start_text) = start_node.downcast::<Text>() {
354 if start_text
355 .upcast::<CharacterData>()
356 .DeleteData(start_offset, start_node.len() - start_offset)
357 .is_err()
358 {
359 unreachable!("Must always be able to delete");
360 }
361 }
362 }
363
364 // Step 23. Let node list be a list of nodes, initially empty.
365 rooted_vec!(let mut node_list);
366
367 // Step 24. For each node contained in the active range, append node to node list if the
368 // last member of node list (if any) is not an ancestor of node; node is editable;
369 // and node is not a thead, tbody, tfoot, tr, th, or td.
370 let Ok(contained_children) = active_range.contained_children() else {
371 unreachable!("Must always have contained children");
372 };
373 for node in contained_children.contained_children {
374 // This type is only used to tell the compiler how to handle the type of `node_list.last()`.
375 // It is not allowed to add a `& DomRoot<Node>` annotation, as test-tidy disallows that.
376 // However, if we omit the type, the compiler doesn't know what it is, since we also
377 // aren't allowed to add a type annotation to `node_list` itself, as that is handled
378 // by the `rooted_vec` macro. Lastly, we also can't make it `&Node`, since then the compiler
379 // thinks that the contents of the `RootedVec` is `Node`, whereas it is should be
380 // `RootedVec<DomRoot<Node>>`. The type alias here doesn't upset test-tidy,
381 // while also providing the necessary information to the compiler to work.
382 type DomRootNode = DomRoot<Node>;
383 if node.is_editable() &&
384 !(node.is::<HTMLTableSectionElement>() ||
385 node.is::<HTMLTableRowElement>() ||
386 node.is::<HTMLTableCellElement>()) &&
387 node_list
388 .last()
389 .is_none_or(|last: &DomRootNode| !last.is_ancestor_of(&node))
390 {
391 node_list.push(node);
392 }
393 }
394
395 // Step 25. For each node in node list:
396 for node in node_list.iter() {
397 // Step 25.1. Let parent be the parent of node.
398 let parent = node.GetParentNode().expect("Must always have a parent");
399 // Step 25.2. Remove node from parent.
400 assert!(node.has_parent());
401 node.remove_self(cx);
402 // Step 25.3. If the block node of parent has no visible children, and parent is editable or an editing host,
403 // call createElement("br") on the context object and append the result as the last child of parent.
404 if parent.block_node_of().is_some_and(|block_node| {
405 block_node
406 .children_unrooted(cx.no_gc())
407 .all(|child| child.is_invisible())
408 }) && parent.is_editable_or_editing_host()
409 {
410 let br = context_object.create_element(cx, "br");
411 if parent.AppendChild(cx, br.upcast()).is_err() {
412 unreachable!("Must always be able to append");
413 }
414 }
415 // Step 25.4. If strip wrappers is true or parent is not an inclusive ancestor of start node,
416 // while parent is an editable inline node with length 0, let grandparent be the parent of parent,
417 // then remove parent from grandparent, then set parent to grandparent.
418 if strip_wrappers == SelectionDeletionStripWrappers::Strip ||
419 !parent.is_inclusive_ancestor_of(&start_node)
420 {
421 let mut parent = parent;
422 loop {
423 if parent.is_editable() && parent.is_inline_node() && parent.is_empty() {
424 let grand_parent =
425 parent.GetParentNode().expect("Must always have a parent");
426 assert!(parent.has_parent());
427 parent.remove_self(cx);
428 parent = grand_parent;
429 continue;
430 }
431 break;
432 }
433 }
434 }
435
436 // Step 26. If end node is an editable Text node, call deleteData(0, end offset) on it.
437 if end_node.is_editable() {
438 if let Some(end_text) = end_node.downcast::<Text>() {
439 if end_text
440 .upcast::<CharacterData>()
441 .DeleteData(0, end_offset)
442 .is_err()
443 {
444 unreachable!("Must always be able to delete");
445 }
446 }
447 }
448
449 // Step 27. Canonicalize whitespace at the active range's start, with fix collapsed space false.
450 active_range
451 .start_container()
452 .canonicalize_whitespace(active_range.start_offset(), false);
453
454 // Step 28. Canonicalize whitespace at the active range's end, with fix collapsed space false.
455 active_range
456 .end_container()
457 .canonicalize_whitespace(active_range.end_offset(), false);
458
459 // Step 29.
460 //
461 // This step does not exist in the spec
462
463 // Step 30. If block merging is false, or start block or end block is null, or start block is not
464 // in the same editing host as end block, or start block and end block are the same:
465 if block_merging == SelectionDeletionBlockMerging::Skip ||
466 start_block.as_ref().zip(end_block.as_ref()).is_none_or(
467 |(start_block, end_block)| {
468 start_block == end_block || !start_block.same_editing_host(end_block)
469 },
470 )
471 {
472 // Step 30.1. If direction is "forward", call collapseToStart() on the context object's selection.
473 if direction == SelectionDeleteDirection::Forward {
474 if self.CollapseToStart(cx).is_err() {
475 unreachable!("Should be able to collapse to start");
476 }
477 } else {
478 // Step 30.2. Otherwise, call collapseToEnd() on the context object's selection.
479 if self.CollapseToEnd(cx).is_err() {
480 unreachable!("Should be able to collapse to end");
481 }
482 }
483 // Step 30.3. Restore states and values from overrides.
484 active_range.restore_states_and_values(cx, self, context_object, overrides);
485
486 // Step 30.4. Abort these steps.
487 return;
488 }
489 let start_block = start_block.expect("Already checked for None in previous statement");
490 let end_block = end_block.expect("Already checked for None in previous statement");
491
492 // Step 31. If start block has one child, which is a collapsed block prop, remove its child from it.
493 if start_block.children_count() == 1 {
494 let Some(child) = start_block.children().nth(0) else {
495 unreachable!("Must always have a single child");
496 };
497 if child.is_collapsed_block_prop() {
498 assert!(child.has_parent());
499 child.remove_self(cx);
500 }
501 }
502
503 // Step 32. If start block is an ancestor of end block:
504 let values = if start_block.is_ancestor_of(&end_block) {
505 // Step 32.1. Let reference node be end block.
506 let mut reference_node = end_block.clone();
507 // Step 32.2. While reference node is not a child of start block, set reference node to its parent.
508 loop {
509 if start_block
510 .children_unrooted(cx.no_gc())
511 .all(|child| child != &reference_node)
512 {
513 reference_node = reference_node
514 .GetParentNode()
515 .expect("Must always have a parent, at least start_block");
516 continue;
517 }
518 break;
519 }
520 // Step 32.3. Call collapse() on the context object's selection,
521 // with first argument start block and second argument the index of reference node.
522 if self
523 .Collapse(cx, Some(&start_block), reference_node.index())
524 .is_err()
525 {
526 unreachable!("Must always be able to collapse");
527 }
528 // Step 32.4. If end block has no children:
529 if end_block.children_count() == 0 {
530 let mut end_block = end_block;
531 // Step 32.4.1. While end block is editable and is the only child of its parent and is not a child of start block,
532 // let parent equal end block, then remove end block from parent, then set end block to parent.
533 loop {
534 if end_block.is_editable() &&
535 start_block.children().all(|child| child != end_block)
536 {
537 if let Some(parent) = end_block.GetParentNode() {
538 if parent.children_count() == 1 {
539 assert!(end_block.has_parent());
540 end_block.remove_self(cx);
541 end_block = parent;
542 continue;
543 }
544 }
545 }
546 break;
547 }
548 // Step 32.4.2. If end block is editable and is not an inline node,
549 // and its previousSibling and nextSibling are both inline nodes,
550 // call createElement("br") on the context object and insert it into end block's parent immediately after end block.
551 if end_block.is_editable() &&
552 !end_block.is_inline_node() &&
553 end_block
554 .GetPreviousSibling()
555 .is_some_and(|previous| previous.is_inline_node())
556 {
557 if let Some(next_of_end_block) = end_block.GetNextSibling() {
558 if next_of_end_block.is_inline_node() {
559 let br = context_object.create_element(cx, "br");
560 let parent = end_block
561 .GetParentNode()
562 .expect("Must always have a parent");
563 if parent
564 .InsertBefore(cx, br.upcast(), Some(&next_of_end_block))
565 .is_err()
566 {
567 unreachable!("Must always be able to insert into parent");
568 }
569 }
570 }
571 }
572 // Step 32.4.3. If end block is editable, remove it from its parent.
573 if end_block.is_editable() {
574 assert!(end_block.has_parent());
575 end_block.remove_self(cx);
576 }
577 // Step 32.4.4. Restore states and values from overrides.
578 active_range.restore_states_and_values(cx, self, context_object, overrides);
579
580 // Step 32.4.5. Abort these steps.
581 return;
582 }
583 let first_child = end_block
584 .children()
585 .nth(0)
586 .expect("Already checked at least 1 child in previous statement");
587 // Step 32.5. If end block's firstChild is not an inline node,
588 // restore states and values from record, then abort these steps.
589 if !first_child.is_inline_node() {
590 active_range.restore_states_and_values(cx, self, context_object, overrides);
591 return;
592 }
593 // Step 32.6. Let children be a list of nodes, initially empty.
594 rooted_vec!(let mut children);
595 // Step 32.7. Append the first child of end block to children.
596 children.push(first_child.as_traced());
597 // Step 32.8. While children's last member is not a br,
598 // and children's last member's nextSibling is an inline node,
599 // append children's last member's nextSibling to children.
600 loop {
601 let Some(last) = children.last() else {
602 break;
603 };
604 if last.is::<HTMLBRElement>() {
605 break;
606 }
607 let Some(next) = last.GetNextSibling() else {
608 break;
609 };
610 if next.is_inline_node() {
611 children.push(next.as_traced());
612 continue;
613 }
614 break;
615 }
616 // Step 32.9. Record the values of children, and let values be the result.
617 let values = record_the_values(children.iter().map(|dom| dom.as_rooted()).collect());
618
619 // Step 32.10. While children's first member's parent is not start block,
620 // split the parent of children.
621 loop {
622 if children
623 .first()
624 .and_then(|child| child.GetParentNode())
625 .is_some_and(|parent_of_child| parent_of_child != start_block)
626 {
627 split_the_parent(cx, children.r());
628 continue;
629 }
630 break;
631 }
632 // Step 32.11. If children's first member's previousSibling is an editable br,
633 // remove that br from its parent.
634 if let Some(first) = children.first() {
635 if let Some(previous_of_first) = first.GetPreviousSibling() {
636 if previous_of_first.is_editable() && previous_of_first.is::<HTMLBRElement>() {
637 assert!(previous_of_first.has_parent());
638 previous_of_first.remove_self(cx);
639 }
640 }
641 }
642
643 values
644 // Step 33. Otherwise, if start block is a descendant of end block:
645 } else if end_block.is_ancestor_of(&start_block) {
646 // Step 33.1. Call collapse() on the context object's selection,
647 // with first argument start block and second argument start block's length.
648 if self
649 .Collapse(cx, Some(&start_block), start_block.len())
650 .is_err()
651 {
652 unreachable!("Must always be able to collapse");
653 }
654 // Step 33.2. Let reference node be start block.
655 let mut reference_node = start_block.clone();
656 // Step 33.3. While reference node is not a child of end block, set reference node to its parent.
657 loop {
658 if end_block.children().all(|child| child != reference_node) {
659 if let Some(parent) = reference_node.GetParentNode() {
660 reference_node = parent;
661 continue;
662 }
663 }
664 break;
665 }
666 // Step 33.4. If reference node's nextSibling is an inline node and start block's lastChild is a br,
667 // remove start block's lastChild from it.
668 if reference_node
669 .GetNextSibling()
670 .is_some_and(|next| next.is_inline_node())
671 {
672 if let Some(last) = start_block.children().last() {
673 if last.is::<HTMLBRElement>() {
674 assert!(last.has_parent());
675 last.remove_self(cx);
676 }
677 }
678 }
679 // Step 33.5. Let nodes to move be a list of nodes, initially empty.
680 rooted_vec!(let mut nodes_to_move);
681 // Step 33.6. If reference node's nextSibling is neither null nor a block node,
682 // append it to nodes to move.
683 if let Some(next) = reference_node.GetNextSibling() {
684 if !next.is_block_node() {
685 nodes_to_move.push(next);
686 }
687 }
688 // Step 33.7. While nodes to move is nonempty and its last member isn't a br
689 // and its last member's nextSibling is neither null nor a block node,
690 // append its last member's nextSibling to nodes to move.
691 loop {
692 if let Some(last) = nodes_to_move.last() {
693 if !last.is::<HTMLBRElement>() {
694 if let Some(next_of_last) = last.GetNextSibling() {
695 if !next_of_last.is_block_node() {
696 nodes_to_move.push(next_of_last);
697 continue;
698 }
699 }
700 }
701 }
702 break;
703 }
704 // Step 33.8. Record the values of nodes to move, and let values be the result.
705 let values = record_the_values(nodes_to_move.iter().cloned().collect());
706
707 // Step 33.9. For each node in nodes to move,
708 // append node as the last child of start block, preserving ranges.
709 for node in nodes_to_move.iter() {
710 move_preserving_ranges(cx, node, |cx| start_block.AppendChild(cx, node));
711 }
712
713 values
714 // Step 34. Otherwise:
715 } else {
716 // Step 34.1. Call collapse() on the context object's selection,
717 // with first argument start block and second argument start block's length.
718 if self
719 .Collapse(cx, Some(&start_block), start_block.len())
720 .is_err()
721 {
722 unreachable!("Must always be able to collapse");
723 }
724 // Step 34.2. If end block's firstChild is an inline node and start block's lastChild is a br,
725 // remove start block's lastChild from it.
726 if end_block
727 .children()
728 .nth(0)
729 .is_some_and(|next| next.is_inline_node())
730 {
731 if let Some(last) = start_block.children().last() {
732 if last.is::<HTMLBRElement>() {
733 assert!(last.has_parent());
734 last.remove_self(cx);
735 }
736 }
737 }
738 // Step 34.3. Record the values of end block's children, and let values be the result.
739 let values = record_the_values(end_block.children().collect());
740
741 // Step 34.4. While end block has children,
742 // append the first child of end block to start block, preserving ranges.
743 loop {
744 if let Some(first_child) = end_block.children().nth(0) {
745 move_preserving_ranges(cx, &first_child, |cx| {
746 start_block.AppendChild(cx, &first_child)
747 });
748 continue;
749 }
750 break;
751 }
752 // Step 34.5. While end block has no children,
753 // let parent be the parent of end block, then remove end block from parent,
754 // then set end block to parent.
755 let mut end_block = end_block;
756 loop {
757 if end_block.children_count() == 0 {
758 if let Some(parent) = end_block.GetParentNode() {
759 assert!(end_block.has_parent());
760 end_block.remove_self(cx);
761 end_block = parent;
762 continue;
763 }
764 }
765 break;
766 }
767
768 values
769 };
770
771 // Step 35.
772 //
773 // This step does not exist in the spec
774
775 // Step 36. Let ancestor be start block.
776 // TODO
777
778 // Step 37. While ancestor has an inclusive ancestor ol in the same editing host whose nextSibling is
779 // also an ol in the same editing host, or an inclusive ancestor ul in the same editing host whose nextSibling
780 // is also a ul in the same editing host:
781 // TODO
782
783 // Step 38. Restore the values from values.
784 restore_the_values(cx, values);
785
786 // Step 39. If start block has no children, call createElement("br") on the context object and
787 // append the result as the last child of start block.
788 if start_block.children_count() == 0 {
789 let br = context_object.create_element(cx, "br");
790 if start_block.AppendChild(cx, br.upcast()).is_err() {
791 unreachable!("Must always be able to append");
792 }
793 }
794
795 // Step 40. Remove extraneous line breaks at the end of start block.
796 start_block.remove_extraneous_line_breaks_at_the_end_of(cx);
797
798 // Step 41. Restore states and values from overrides.
799 active_range.restore_states_and_values(cx, self, context_object, overrides);
800 }
801
802 /// <https://w3c.github.io/editing/docs/execCommand/#set-the-selection%27s-value>
803 pub(crate) fn set_the_selection_value(
804 &self,
805 cx: &mut JSContext,
806 new_value: Option<DOMString>,
807 command: CommandName,
808 context_object: &Document,
809 ) {
810 let active_range = self
811 .active_range()
812 .expect("Must always have an active range");
813
814 // Step 1. Let command be the current command.
815 //
816 // Passed as argument
817
818 // Step 2. If there is no formattable node effectively contained in the active range:
819 if active_range.first_formattable_contained_node().is_none() {
820 // Step 2.1. If command has inline command activated values, set the state override to true if new value is among them and false if it's not.
821 let inline_command_activated_values = command.inline_command_activated_values();
822 if !inline_command_activated_values.is_empty() {
823 context_object.set_state_override(
824 command,
825 Some(new_value.as_ref().is_some_and(|new_value| {
826 inline_command_activated_values.contains(&new_value.str().as_ref())
827 })),
828 );
829 }
830 // Step 2.2. If command is "subscript", unset the state override for "superscript".
831 if command == CommandName::Subscript {
832 context_object.set_state_override(CommandName::Superscript, None);
833 }
834 // Step 2.3. If command is "superscript", unset the state override for "subscript".
835 if command == CommandName::Superscript {
836 context_object.set_state_override(CommandName::Subscript, None);
837 }
838 // Step 2.4. If new value is null, unset the value override (if any).
839 // Step 2.5. Otherwise, if command is "createLink" or it has a value specified, set the value override to new value.
840 context_object.set_value_override(command, new_value);
841 // Step 2.6. Abort these steps.
842 return;
843 }
844 // Step 3. If the active range's start node is an editable Text node,
845 // and its start offset is neither zero nor its start node's length,
846 // call splitText() on the active range's start node,
847 // with argument equal to the active range's start offset.
848 // Then set the active range's start node to the result, and its start offset to zero.
849 let start_node = active_range.start_container();
850 let start_offset = active_range.start_offset();
851 if start_node.is_editable() && start_offset != 0 && start_offset != start_node.len() {
852 if let Some(start_text) = start_node.downcast::<Text>() {
853 let Ok(start_text) = start_text.SplitText(cx, start_offset) else {
854 unreachable!("Must always be able to split");
855 };
856 active_range.set_start(start_text.upcast(), 0);
857 }
858 }
859 // Step 4. If the active range's end node is an editable Text node,
860 // and its end offset is neither zero nor its end node's length,
861 // call splitText() on the active range's end node,
862 // with argument equal to the active range's end offset.
863 let end_node = active_range.end_container();
864 let end_offset = active_range.end_offset();
865 if end_node.is_editable() && end_offset != 0 && end_offset != end_node.len() {
866 if let Some(end_text) = end_node.downcast::<Text>() {
867 if end_text.SplitText(cx, end_offset).is_err() {
868 unreachable!("Must always be able to split");
869 };
870 }
871 }
872 // Step 5. Let element list be all editable Elements effectively contained in the active range.
873 // Step 6. For each element in element list, clear the value of element.
874 active_range.for_each_effectively_contained_child(|child| {
875 if child.is_editable() {
876 if let Some(element_child) = child.downcast::<HTMLElement>() {
877 element_child.clear_the_value(cx, &command);
878 }
879 }
880 });
881 // Step 7. Let node list be all editable nodes effectively contained in the active range.
882 // Step 8. For each node in node list:
883 active_range.for_each_effectively_contained_child(|child| {
884 if child.is_editable() {
885 // Step 8.1. Push down values on node.
886 child.push_down_values(cx, &command, new_value.clone());
887 // Step 8.2. If node is an allowed child of "span", force the value of node.
888 if is_allowed_child(
889 NodeOrString::Node(DomRoot::from_ref(child)),
890 NodeOrString::String("span".to_owned()),
891 ) {
892 child.force_the_value(cx, &command, new_value.as_ref());
893 }
894 }
895 });
896 }
897}