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