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