script/dom/execcommand/contenteditable.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;
6use std::ops::Deref;
7
8use html5ever::local_name;
9use script_bindings::inheritance::Castable;
10use style::computed_values::white_space_collapse::T as WhiteSpaceCollapse;
11use style::values::specified::box_::DisplayOutside;
12
13use crate::dom::abstractrange::bp_position;
14use crate::dom::bindings::cell::Ref;
15use crate::dom::bindings::codegen::Bindings::CharacterDataBinding::CharacterDataMethods;
16use crate::dom::bindings::codegen::Bindings::DocumentBinding::{
17 DocumentMethods, ElementCreationOptions,
18};
19use crate::dom::bindings::codegen::Bindings::HTMLElementBinding::HTMLElementMethods;
20use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods;
21use crate::dom::bindings::codegen::Bindings::SelectionBinding::SelectionMethods;
22use crate::dom::bindings::codegen::UnionTypes::StringOrElementCreationOptions;
23use crate::dom::bindings::inheritance::{ElementTypeId, HTMLElementTypeId, NodeTypeId};
24use crate::dom::bindings::root::{DomRoot, DomSlice};
25use crate::dom::bindings::str::DOMString;
26use crate::dom::characterdata::CharacterData;
27use crate::dom::document::Document;
28use crate::dom::element::Element;
29use crate::dom::execcommand::basecommand::CommandName;
30use crate::dom::html::htmlanchorelement::HTMLAnchorElement;
31use crate::dom::html::htmlbrelement::HTMLBRElement;
32use crate::dom::html::htmlelement::HTMLElement;
33use crate::dom::html::htmlimageelement::HTMLImageElement;
34use crate::dom::html::htmllielement::HTMLLIElement;
35use crate::dom::html::htmltablecellelement::HTMLTableCellElement;
36use crate::dom::html::htmltablerowelement::HTMLTableRowElement;
37use crate::dom::html::htmltablesectionelement::HTMLTableSectionElement;
38use crate::dom::node::{Node, NodeTraits, ShadowIncluding};
39use crate::dom::range::Range;
40use crate::dom::selection::Selection;
41use crate::dom::text::Text;
42use crate::script_runtime::CanGc;
43
44impl Text {
45 /// <https://dom.spec.whatwg.org/#concept-cd-data>
46 fn data(&self) -> Ref<'_, String> {
47 self.upcast::<CharacterData>().data()
48 }
49
50 /// <https://w3c.github.io/editing/docs/execCommand/#whitespace-node>
51 fn is_whitespace_node(&self) -> bool {
52 // > A whitespace node is either a Text node whose data is the empty string;
53 let data = self.data();
54 if data.is_empty() {
55 return true;
56 }
57 // > or a Text node whose data consists only of one or more tabs (0x0009), line feeds (0x000A),
58 // > carriage returns (0x000D), and/or spaces (0x0020),
59 // > and whose parent is an Element whose resolved value for "white-space" is "normal" or "nowrap";
60 let Some(parent) = self.upcast::<Node>().GetParentElement() else {
61 return false;
62 };
63 // TODO: Optimize the below to only do a traversal once and in the match handle the expected collapse value
64 let Some(style) = parent.style() else {
65 return false;
66 };
67 let white_space_collapse = style.get_inherited_text().white_space_collapse;
68 if data
69 .bytes()
70 .all(|byte| matches!(byte, b'\t' | b'\n' | b'\r' | b' ')) &&
71 // Note that for "normal" and "nowrap", the longhand "white-space-collapse: collapse" applies
72 // https://www.w3.org/TR/css-text-4/#white-space-property
73 white_space_collapse == WhiteSpaceCollapse::Collapse
74 {
75 return true;
76 }
77 // > or a Text node whose data consists only of one or more tabs (0x0009), carriage returns (0x000D),
78 // > and/or spaces (0x0020), and whose parent is an Element whose resolved value for "white-space" is "pre-line".
79 data.bytes()
80 .all(|byte| matches!(byte, b'\t' | b'\r' | b' ')) &&
81 // Note that for "pre-line", the longhand "white-space-collapse: preserve-breaks" applies
82 // https://www.w3.org/TR/css-text-4/#white-space-property
83 white_space_collapse == WhiteSpaceCollapse::PreserveBreaks
84 }
85
86 /// <https://w3c.github.io/editing/docs/execCommand/#collapsed-whitespace-node>
87 fn is_collapsed_whitespace_node(&self) -> bool {
88 // Step 1. If node is not a whitespace node, return false.
89 if !self.is_whitespace_node() {
90 return false;
91 }
92 // Step 2. If node's data is the empty string, return true.
93 if self.data().is_empty() {
94 return true;
95 }
96 // Step 3. Let ancestor be node's parent.
97 let node = self.upcast::<Node>();
98 let Some(ancestor) = node.GetParentNode() else {
99 // Step 4. If ancestor is null, return true.
100 return true;
101 };
102 let mut resolved_ancestor = ancestor.clone();
103 for parent in ancestor.ancestors() {
104 // Step 5. If the "display" property of some ancestor of node has resolved value "none", return true.
105 if parent.is_display_none() {
106 return true;
107 }
108 // Step 6. While ancestor is not a block node and its parent is not null, set ancestor to its parent.
109 //
110 // Note that the spec is written as "while not". Since this is the end-condition, we need to invert
111 // the condition to decide when to stop.
112 if parent.is_block_node() {
113 break;
114 }
115 resolved_ancestor = parent;
116 }
117 // Step 7. Let reference be node.
118 // Step 8. While reference is a descendant of ancestor:
119 // Step 8.1. Let reference be the node before it in tree order.
120 for reference in node.preceding_nodes(&resolved_ancestor) {
121 // Step 8.2. If reference is a block node or a br, return true.
122 if reference.is_block_node() || reference.is::<HTMLBRElement>() {
123 return true;
124 }
125 // Step 8.3. If reference is a Text node that is not a whitespace node, or is an img, break from this loop.
126 if reference
127 .downcast::<Text>()
128 .is_some_and(|text| !text.is_whitespace_node()) ||
129 reference.is::<HTMLImageElement>()
130 {
131 break;
132 }
133 }
134 // Step 9. Let reference be node.
135 // Step 10. While reference is a descendant of ancestor:
136 // Step 10.1. Let reference be the node after it in tree order, or null if there is no such node.
137 for reference in node.following_nodes(&resolved_ancestor) {
138 // Step 10.2. If reference is a block node or a br, return true.
139 if reference.is_block_node() || reference.is::<HTMLBRElement>() {
140 return true;
141 }
142 // Step 10.3. If reference is a Text node that is not a whitespace node, or is an img, break from this loop.
143 if reference
144 .downcast::<Text>()
145 .is_some_and(|text| !text.is_whitespace_node()) ||
146 reference.is::<HTMLImageElement>()
147 {
148 break;
149 }
150 }
151 // Step 11. Return false.
152 false
153 }
154
155 /// Part of <https://w3c.github.io/editing/docs/execCommand/#canonicalize-whitespace>
156 /// and deduplicated here, since we need to do this for both start and end nodes
157 fn has_whitespace_and_has_parent_with_whitespace_preserve(
158 &self,
159 offset: u32,
160 space_characters: &'static [&'static char],
161 ) -> bool {
162 // if node is a Text node and its parent's resolved value for "white-space" is neither "pre" nor "pre-wrap"
163 // and start offset is not zero and the (start offset − 1)st code unit of start node's data is a space (0x0020) or
164 // non-breaking space (0x00A0)
165 let has_preserve_space = self
166 .upcast::<Node>()
167 .GetParentNode()
168 .and_then(|parent| parent.style())
169 .is_some_and(|style| {
170 // Note that for "pre" and "pre-wrap", the longhand "white-space-collapse: preserve" applies
171 // https://www.w3.org/TR/css-text-4/#white-space-property
172 style.get_inherited_text().white_space_collapse != WhiteSpaceCollapse::Preserve
173 });
174 let has_space_character = self
175 .data()
176 .chars()
177 .nth(offset as usize)
178 .is_some_and(|c| space_characters.contains(&&c));
179 has_preserve_space && has_space_character
180 }
181}
182
183impl HTMLBRElement {
184 /// <https://w3c.github.io/editing/docs/execCommand/#extraneous-line-break>
185 fn is_extraneous_line_break(&self) -> bool {
186 let node = self.upcast::<Node>();
187 // > An extraneous line break is a br that has no visual effect, in that removing it from the DOM would not change layout,
188 // except that a br that is the sole child of an li is not extraneous.
189 if node
190 .GetParentNode()
191 .filter(|parent| parent.is::<HTMLLIElement>())
192 .is_some_and(|li| li.children_count() == 1)
193 {
194 return false;
195 }
196 // TODO: Figure out what this actually makes it have no visual effect
197 !node.is_block_node()
198 }
199}
200
201impl Document {
202 fn create_br_element(&self, cx: &mut js::context::JSContext) -> DomRoot<Element> {
203 let element_options =
204 StringOrElementCreationOptions::ElementCreationOptions(ElementCreationOptions {
205 is: None,
206 });
207 match self.CreateElement(cx, "br".into(), element_options) {
208 Err(_) => unreachable!("Must always be able to create br"),
209 Ok(br) => br,
210 }
211 }
212}
213
214impl HTMLElement {
215 fn local_name(&self) -> &str {
216 self.upcast::<Element>().local_name()
217 }
218}
219
220pub(crate) enum NodeOrString {
221 String(String),
222 Node(DomRoot<Node>),
223}
224
225impl NodeOrString {
226 fn name(&self) -> &str {
227 match self {
228 NodeOrString::String(str_) => str_,
229 NodeOrString::Node(node) => node
230 .downcast::<Element>()
231 .map(|element| element.local_name().as_ref())
232 .unwrap_or_default(),
233 }
234 }
235
236 fn as_node(&self) -> Option<DomRoot<Node>> {
237 match self {
238 NodeOrString::String(_) => None,
239 NodeOrString::Node(node) => Some(node.clone()),
240 }
241 }
242}
243
244/// <https://w3c.github.io/editing/docs/execCommand/#prohibited-paragraph-child-name>
245const PROHIBITED_PARAGRAPH_CHILD_NAMES: [&str; 47] = [
246 "address",
247 "article",
248 "aside",
249 "blockquote",
250 "caption",
251 "center",
252 "col",
253 "colgroup",
254 "dd",
255 "details",
256 "dir",
257 "div",
258 "dl",
259 "dt",
260 "fieldset",
261 "figcaption",
262 "figure",
263 "footer",
264 "form",
265 "h1",
266 "h2",
267 "h3",
268 "h4",
269 "h5",
270 "h6",
271 "header",
272 "hgroup",
273 "hr",
274 "li",
275 "listing",
276 "menu",
277 "nav",
278 "ol",
279 "p",
280 "plaintext",
281 "pre",
282 "section",
283 "summary",
284 "table",
285 "tbody",
286 "td",
287 "tfoot",
288 "th",
289 "thead",
290 "tr",
291 "ul",
292 "xmp",
293];
294/// <https://w3c.github.io/editing/docs/execCommand/#name-of-an-element-with-inline-contents>
295const NAME_OF_AN_ELEMENT_WITH_INLINE_CONTENTS: [&str; 43] = [
296 "a", "abbr", "b", "bdi", "bdo", "cite", "code", "dfn", "em", "h1", "h2", "h3", "h4", "h5",
297 "h6", "i", "kbd", "mark", "p", "pre", "q", "rp", "rt", "ruby", "s", "samp", "small", "span",
298 "strong", "sub", "sup", "u", "var", "acronym", "listing", "strike", "xmp", "big", "blink",
299 "font", "marquee", "nobr", "tt",
300];
301
302/// <https://w3c.github.io/editing/docs/execCommand/#element-with-inline-contents>
303fn is_element_with_inline_contents(element: &Node) -> bool {
304 // > An element with inline contents is an HTML element whose local name is a name of an element with inline contents.
305 let Some(html_element) = element.downcast::<HTMLElement>() else {
306 return false;
307 };
308 NAME_OF_AN_ELEMENT_WITH_INLINE_CONTENTS.contains(&html_element.local_name())
309}
310
311/// <https://w3c.github.io/editing/docs/execCommand/#allowed-child>
312fn is_allowed_child(child: NodeOrString, parent: NodeOrString) -> bool {
313 // Step 1. If parent is "colgroup", "table", "tbody", "tfoot", "thead", "tr",
314 // or an HTML element with local name equal to one of those,
315 // and child is a Text node whose data does not consist solely of space characters, return false.
316 if matches!(
317 parent.name(),
318 "colgroup" | "table" | "tbody" | "tfoot" | "thead" | "tr"
319 ) && child.as_node().is_some_and(|node| {
320 // Note: cannot use `.and_then` here, since `downcast` would outlive its reference
321 node.downcast::<Text>()
322 .is_some_and(|text| !text.data().bytes().all(|byte| byte == b' '))
323 }) {
324 return false;
325 }
326 // Step 2. If parent is "script", "style", "plaintext", or "xmp",
327 // or an HTML element with local name equal to one of those, and child is not a Text node, return false.
328 if matches!(parent.name(), "script" | "style" | "plaintext" | "xmp") &&
329 child.as_node().is_none_or(|node| !node.is::<Text>())
330 {
331 return false;
332 }
333 // Step 3. If child is a document, DocumentFragment, or DocumentType, return false.
334 if let NodeOrString::Node(ref node) = child {
335 if matches!(
336 node.type_id(),
337 NodeTypeId::Document(_) | NodeTypeId::DocumentFragment(_) | NodeTypeId::DocumentType
338 ) {
339 return false;
340 }
341 }
342 // Step 4. If child is an HTML element, set child to the local name of child.
343 let child_name = match child {
344 NodeOrString::String(str_) => str_,
345 NodeOrString::Node(node) => match node.downcast::<HTMLElement>() {
346 // Step 5. If child is not a string, return true.
347 None => return true,
348 Some(html_element) => html_element.local_name().to_owned(),
349 },
350 };
351 let child = child_name.as_str();
352 let parent_name = match parent {
353 NodeOrString::String(str_) => str_,
354 NodeOrString::Node(parent) => {
355 // Step 6. If parent is an HTML element:
356 if let Some(parent_element) = parent.downcast::<HTMLElement>() {
357 // Step 6.1. If child is "a", and parent or some ancestor of parent is an a, return false.
358 if child == "a" &&
359 parent
360 .inclusive_ancestors(ShadowIncluding::No)
361 .any(|node| node.is::<HTMLAnchorElement>())
362 {
363 return false;
364 }
365 // Step 6.2. If child is a prohibited paragraph child name and parent or some ancestor of parent
366 // is an element with inline contents, return false.
367 if PROHIBITED_PARAGRAPH_CHILD_NAMES.contains(&child) &&
368 parent
369 .inclusive_ancestors(ShadowIncluding::No)
370 .any(|node| is_element_with_inline_contents(&node))
371 {
372 return false;
373 }
374 // Step 6.3. If child is "h1", "h2", "h3", "h4", "h5", or "h6",
375 // and parent or some ancestor of parent is an HTML element with local name
376 // "h1", "h2", "h3", "h4", "h5", or "h6", return false.
377 if matches!(child, "h1" | "h2" | "h3" | "h4" | "h5" | "h6") &&
378 parent.inclusive_ancestors(ShadowIncluding::No).any(|node| {
379 node.downcast::<HTMLElement>().is_some_and(|html_element| {
380 matches!(
381 html_element.local_name(),
382 "h1" | "h2" | "h3" | "h4" | "h5" | "h6"
383 )
384 })
385 })
386 {
387 return false;
388 }
389 // Step 6.4. Let parent be the local name of parent.
390 parent_element.local_name().to_owned()
391 } else {
392 // Step 7. If parent is an Element or DocumentFragment, return true.
393 // Step 8. If parent is not a string, return false.
394 return matches!(
395 parent.type_id(),
396 NodeTypeId::DocumentFragment(_) | NodeTypeId::Element(_)
397 );
398 }
399 },
400 };
401 let parent = parent_name.as_str();
402 // Step 9. If parent is on the left-hand side of an entry on the following list,
403 // then return true if child is listed on the right-hand side of that entry, and false otherwise.
404 match parent {
405 "colgroup" => return child == "col",
406 "table" => {
407 return matches!(
408 child,
409 "caption" | "col" | "colgroup" | "tbody" | "td" | "tfoot" | "th" | "thead" | "tr"
410 );
411 },
412 "tbody" | "tfoot" | "thead" => return matches!(child, "td" | "th" | "tr"),
413 "tr" => return matches!(child, "td" | "th"),
414 "dl" => return matches!(child, "dt" | "dd"),
415 "dir" | "ol" | "ul" => return matches!(child, "dir" | "li" | "ol" | "ul"),
416 "hgroup" => return matches!(child, "h1" | "h2" | "h3" | "h4" | "h5" | "h6"),
417 _ => {},
418 };
419 // Step 10. If child is "body", "caption", "col", "colgroup", "frame", "frameset", "head",
420 // "html", "tbody", "td", "tfoot", "th", "thead", or "tr", return false.
421 if matches!(
422 child,
423 "body" |
424 "caption" |
425 "col" |
426 "colgroup" |
427 "frame" |
428 "frameset" |
429 "head" |
430 "html" |
431 "tbody" |
432 "td" |
433 "tfoot" |
434 "th" |
435 "thead" |
436 "tr"
437 ) {
438 return false;
439 }
440 // Step 11. If child is "dd" or "dt" and parent is not "dl", return false.
441 if matches!(child, "dd" | "dt") && parent != "dl" {
442 return false;
443 }
444 // Step 12. If child is "li" and parent is not "ol" or "ul", return false.
445 if child == "li" && !matches!(parent, "ol" | "ul") {
446 return false;
447 }
448 // Step 13. If parent is on the left-hand side of an entry on the following list
449 // and child is listed on the right-hand side of that entry, return false.
450 if match parent {
451 "a" => child == "a",
452 "dd" | "dt" => matches!(child, "dd" | "dt"),
453 "h1" | "h2" | "h3" | "h4" | "h5" | "h6" => {
454 matches!(child, "h1" | "h2" | "h3" | "h4" | "h5" | "h6")
455 },
456 "li" => child == "li",
457 "nobr" => child == "nobr",
458 "td" | "th" => {
459 matches!(
460 child,
461 "caption" | "col" | "colgroup" | "tbody" | "td" | "tfoot" | "th" | "thead" | "tr"
462 )
463 },
464 _ if NAME_OF_AN_ELEMENT_WITH_INLINE_CONTENTS.contains(&parent) => {
465 PROHIBITED_PARAGRAPH_CHILD_NAMES.contains(&child)
466 },
467 _ => false,
468 } {
469 return false;
470 }
471 // Step 14. Return true.
472 true
473}
474
475/// <https://w3c.github.io/editing/docs/execCommand/#split-the-parent>
476pub(crate) fn split_the_parent<'a>(cx: &mut js::context::JSContext, node_list: &'a [&'a Node]) {
477 assert!(!node_list.is_empty());
478 // Step 1. Let original parent be the parent of the first member of node list.
479 let Some(original_parent) = node_list.first().and_then(|first| first.GetParentNode()) else {
480 return;
481 };
482 let context_object = original_parent.owner_document();
483 // Step 2. If original parent is not editable or its parent is null, do nothing and abort these steps.
484 if !original_parent.is_editable() {
485 return;
486 }
487 let Some(parent_of_original_parent) = original_parent.GetParentNode() else {
488 return;
489 };
490 // Step 3. If the first child of original parent is in node list, remove extraneous line breaks before original parent.
491 if original_parent
492 .children()
493 .next()
494 .is_some_and(|first_child| node_list.contains(&first_child.deref()))
495 {
496 original_parent.remove_extraneous_line_breaks_before(cx);
497 }
498 // Step 4. If the first child of original parent is in node list, and original parent follows a line break,
499 // set follows line break to true. Otherwise, set follows line break to false.
500 let first_child_is_in_node_list = original_parent
501 .children()
502 .next()
503 .is_some_and(|first_child| node_list.contains(&first_child.deref()));
504 let follows_line_break = first_child_is_in_node_list && original_parent.follows_a_line_break();
505 // Step 5. If the last child of original parent is in node list, and original parent precedes a line break,
506 // set precedes line break to true. Otherwise, set precedes line break to false.
507 let last_child_is_in_node_list = original_parent
508 .children()
509 .last()
510 .is_some_and(|last_child| node_list.contains(&last_child.deref()));
511 let precedes_line_break = last_child_is_in_node_list && original_parent.precedes_a_line_break();
512 // Step 6. If the first child of original parent is not in node list, but its last child is:
513 if !first_child_is_in_node_list && last_child_is_in_node_list {
514 // Step 6.1. For each node in node list, in reverse order,
515 // insert node into the parent of original parent immediately after original parent, preserving ranges.
516 for node in node_list.iter().rev() {
517 // TODO: Preserving ranges
518 if parent_of_original_parent
519 .InsertBefore(cx, node, original_parent.GetNextSibling().as_deref())
520 .is_err()
521 {
522 unreachable!("Must always have a parent");
523 }
524 }
525 // Step 6.2. If precedes line break is true, and the last member of node list does not precede a line break,
526 // call createElement("br") on the context object and insert the result immediately after the last member of node list.
527 if precedes_line_break {
528 if let Some(last) = node_list.last() {
529 if !last.precedes_a_line_break() {
530 let br = context_object.create_br_element(cx);
531 if last
532 .GetParentNode()
533 .expect("Must always have a parent")
534 .InsertBefore(cx, br.upcast(), last.GetNextSibling().as_deref())
535 .is_err()
536 {
537 unreachable!("Must always be able to append");
538 }
539 }
540 }
541 }
542 // Step 6.3. Remove extraneous line breaks at the end of original parent.
543 original_parent.remove_extraneous_line_breaks_at_the_end_of(cx);
544 // Step 6.4. Abort these steps.
545 return;
546 }
547 // Step 7. If the first child of original parent is not in node list:
548 if first_child_is_in_node_list {
549 // Step 7.1. Let cloned parent be the result of calling cloneNode(false) on original parent.
550 let Ok(cloned_parent) = original_parent.CloneNode(cx, false) else {
551 unreachable!("Must always be able to clone node");
552 };
553 // Step 7.2. If original parent has an id attribute, unset it.
554 if let Some(element) = original_parent.downcast::<Element>() {
555 element.remove_attribute_by_name(&local_name!("id"), CanGc::from_cx(cx));
556 }
557 // Step 7.3. Insert cloned parent into the parent of original parent immediately before original parent.
558 if parent_of_original_parent
559 .InsertBefore(cx, &cloned_parent, Some(&original_parent))
560 .is_err()
561 {
562 unreachable!("Must always have a parent");
563 }
564 // Step 7.4. While the previousSibling of the first member of node list is not null,
565 // append the first child of original parent as the last child of cloned parent, preserving ranges.
566 loop {
567 if node_list
568 .first()
569 .and_then(|first| first.GetPreviousSibling())
570 .is_some()
571 {
572 if let Some(first_of_original) = original_parent.children().next() {
573 // TODO: Preserving ranges
574 if cloned_parent.AppendChild(cx, &first_of_original).is_err() {
575 unreachable!("Must always have a parent");
576 }
577 continue;
578 }
579 }
580 break;
581 }
582 }
583 // Step 8. For each node in node list, insert node into the parent of original parent immediately before original parent, preserving ranges.
584 for node in node_list.iter() {
585 // TODO: Preserving ranges
586 if parent_of_original_parent
587 .InsertBefore(cx, node, Some(&original_parent))
588 .is_err()
589 {
590 unreachable!("Must always have a parent");
591 }
592 }
593 // Step 9. If follows line break is true, and the first member of node list does not follow a line break,
594 // call createElement("br") on the context object and insert the result immediately before the first member of node list.
595 if follows_line_break {
596 if let Some(first) = node_list.first() {
597 if !first.follows_a_line_break() {
598 let br = context_object.create_br_element(cx);
599 if first
600 .GetParentNode()
601 .expect("Must always have a parent")
602 .InsertBefore(cx, br.upcast(), Some(first))
603 .is_err()
604 {
605 unreachable!("Must always be able to insert");
606 }
607 }
608 }
609 }
610 // Step 10. If the last member of node list is an inline node other than a br,
611 // and the first child of original parent is a br, and original parent is not an inline node,
612 // remove the first child of original parent from original parent.
613 if node_list
614 .last()
615 .is_some_and(|last| last.is_inline_node() && !last.is::<HTMLBRElement>()) &&
616 !original_parent.is_inline_node()
617 {
618 if let Some(first_of_original) = original_parent.children().next() {
619 if first_of_original.is::<HTMLBRElement>() {
620 assert!(first_of_original.has_parent());
621 first_of_original.remove_self(cx);
622 }
623 }
624 }
625 // Step 11. If original parent has no children:
626 if original_parent.children_count() == 0 {
627 // Step 11.1. Remove original parent from its parent.
628 assert!(original_parent.has_parent());
629 original_parent.remove_self(cx);
630 // Step 11.2. If precedes line break is true, and the last member of node list does not precede a line break,
631 // call createElement("br") on the context object and insert the result immediately after the last member of node list.
632 if precedes_line_break {
633 if let Some(last) = node_list.last() {
634 if !last.precedes_a_line_break() {
635 let br = context_object.create_br_element(cx);
636 if last
637 .GetParentNode()
638 .expect("Must always have a parent")
639 .InsertBefore(cx, br.upcast(), last.GetNextSibling().as_deref())
640 .is_err()
641 {
642 unreachable!("Must always be able to insert");
643 }
644 }
645 }
646 }
647 } else {
648 // Step 12. Otherwise, remove extraneous line breaks before original parent.
649 original_parent.remove_extraneous_line_breaks_before(cx);
650 }
651 // Step 13. If node list's last member's nextSibling is null, but its parent is not null,
652 // remove extraneous line breaks at the end of node list's last member's parent.
653 if let Some(last) = node_list.last() {
654 if last.GetNextSibling().is_none() {
655 if let Some(parent_of_last) = last.GetParentNode() {
656 parent_of_last.remove_extraneous_line_breaks_at_the_end_of(cx);
657 }
658 }
659 }
660}
661
662impl Node {
663 fn resolved_display_value(&self) -> Option<DisplayOutside> {
664 self.style().map(|style| style.get_box().display.outside())
665 }
666}
667
668pub(crate) trait NodeExecCommandSupport {
669 fn same_editing_host(&self, other: &Node) -> bool;
670 fn is_block_node(&self) -> bool;
671 fn is_inline_node(&self) -> bool;
672 fn block_node_of(&self) -> Option<DomRoot<Node>>;
673 fn is_visible(&self) -> bool;
674 fn is_invisible(&self) -> bool;
675 fn is_formattable(&self) -> bool;
676 fn is_block_start_point(&self, offset: usize) -> bool;
677 fn is_block_end_point(&self, offset: u32) -> bool;
678 fn is_block_boundary_point(&self, offset: u32) -> bool;
679 fn is_collapsed_block_prop(&self) -> bool;
680 fn follows_a_line_break(&self) -> bool;
681 fn precedes_a_line_break(&self) -> bool;
682 fn canonical_space_sequence(
683 n: usize,
684 non_breaking_start: bool,
685 non_breaking_end: bool,
686 ) -> String;
687 fn canonicalize_whitespace(&self, offset: u32, fix_collapsed_space: bool);
688 fn remove_extraneous_line_breaks_before(&self, cx: &mut js::context::JSContext);
689 fn remove_extraneous_line_breaks_at_the_end_of(&self, cx: &mut js::context::JSContext);
690 fn remove_preserving_its_descendants(&self, cx: &mut js::context::JSContext);
691 fn effective_command_value(&self, command_name: CommandName) -> Option<DOMString>;
692}
693
694impl NodeExecCommandSupport for Node {
695 /// <https://w3c.github.io/editing/docs/execCommand/#in-the-same-editing-host>
696 fn same_editing_host(&self, other: &Node) -> bool {
697 // > Two nodes are in the same editing host if the editing host of the first is non-null and the same as the editing host of the second.
698 self.editing_host_of()
699 .is_some_and(|editing_host| other.editing_host_of() == Some(editing_host))
700 }
701
702 /// <https://w3c.github.io/editing/docs/execCommand/#block-node>
703 fn is_block_node(&self) -> bool {
704 // > A block node is either an Element whose "display" property does not have resolved value "inline" or "inline-block" or "inline-table" or "none",
705 if self.is::<Element>() &&
706 self.resolved_display_value().is_some_and(|display| {
707 display != DisplayOutside::Inline && display != DisplayOutside::None
708 })
709 {
710 return true;
711 }
712 // > or a document, or a DocumentFragment.
713 matches!(
714 self.type_id(),
715 NodeTypeId::Document(_) | NodeTypeId::DocumentFragment(_)
716 )
717 }
718
719 /// <https://w3c.github.io/editing/docs/execCommand/#inline-node>
720 fn is_inline_node(&self) -> bool {
721 // > An inline node is a node that is not a block node.
722 !self.is_block_node()
723 }
724
725 /// <https://w3c.github.io/editing/docs/execCommand/#block-node-of>
726 fn block_node_of(&self) -> Option<DomRoot<Node>> {
727 let mut node = DomRoot::from_ref(self);
728
729 loop {
730 // Step 1. While node is an inline node, set node to its parent.
731 if node.is_inline_node() {
732 node = node.GetParentNode()?;
733 continue;
734 }
735 // Step 2. Return node.
736 return Some(node);
737 }
738 }
739
740 /// <https://w3c.github.io/editing/docs/execCommand/#visible>
741 fn is_visible(&self) -> bool {
742 for parent in self.inclusive_ancestors(ShadowIncluding::No) {
743 // > excluding any node with an inclusive ancestor Element whose "display" property has resolved value "none".
744 if parent.is::<Element>() &&
745 parent
746 .resolved_display_value()
747 .is_some_and(|display| display == DisplayOutside::None)
748 {
749 return false;
750 }
751 }
752 // > Something is visible if it is a node that either is a block node,
753 if self.is_block_node() {
754 return true;
755 }
756 // > or a Text node that is not a collapsed whitespace node,
757 if self
758 .downcast::<Text>()
759 .is_some_and(|text| !text.is_collapsed_whitespace_node())
760 {
761 return true;
762 }
763 // > or an img, or a br that is not an extraneous line break, or any node with a visible descendant;
764 if self.is::<HTMLImageElement>() {
765 return true;
766 }
767 if self
768 .downcast::<HTMLBRElement>()
769 .is_some_and(|br| !br.is_extraneous_line_break())
770 {
771 return true;
772 }
773 for child in self.children() {
774 if child.is_visible() {
775 return true;
776 }
777 }
778 false
779 }
780
781 /// <https://w3c.github.io/editing/docs/execCommand/#invisible>
782 fn is_invisible(&self) -> bool {
783 // > Something is invisible if it is a node that is not visible.
784 !self.is_visible()
785 }
786
787 /// <https://w3c.github.io/editing/docs/execCommand/#formattable-node>
788 fn is_formattable(&self) -> bool {
789 // > A formattable node is an editable visible node that is either a Text node, an img, or a br.
790 self.is_editable() &&
791 self.is_visible() &&
792 (self.is::<Text>() || self.is::<HTMLImageElement>() || self.is::<HTMLBRElement>())
793 }
794
795 /// <https://w3c.github.io/editing/docs/execCommand/#block-start-point>
796 fn is_block_start_point(&self, offset: usize) -> bool {
797 // > A boundary point (node, offset) is a block start point if either node's parent is null and offset is zero;
798 if offset == 0 {
799 return self.GetParentNode().is_none();
800 }
801 // > or node has a child with index offset − 1, and that child is either a visible block node or a visible br.
802 self.children().nth(offset - 1).is_some_and(|child| {
803 child.is_visible() && (child.is_block_node() || child.is::<HTMLBRElement>())
804 })
805 }
806
807 /// <https://w3c.github.io/editing/docs/execCommand/#block-end-point>
808 fn is_block_end_point(&self, offset: u32) -> bool {
809 // > A boundary point (node, offset) is a block end point if either node's parent is null and offset is node's length;
810 if self.GetParentNode().is_none() && offset == self.len() {
811 return true;
812 }
813 // > or node has a child with index offset, and that child is a visible block node.
814 self.children()
815 .nth(offset as usize)
816 .is_some_and(|child| child.is_visible() && child.is_block_node())
817 }
818
819 /// <https://w3c.github.io/editing/docs/execCommand/#block-boundary-point>
820 fn is_block_boundary_point(&self, offset: u32) -> bool {
821 // > A boundary point is a block boundary point if it is either a block start point or a block end point.
822 self.is_block_start_point(offset as usize) || self.is_block_end_point(offset)
823 }
824
825 /// <https://w3c.github.io/editing/docs/execCommand/#collapsed-block-prop>
826 fn is_collapsed_block_prop(&self) -> bool {
827 // > A collapsed block prop is either a collapsed line break that is not an extraneous line break,
828
829 // TODO: Check for collapsed line break
830 if self
831 .downcast::<HTMLBRElement>()
832 .is_some_and(|br| !br.is_extraneous_line_break())
833 {
834 return true;
835 }
836 // > or an Element that is an inline node and whose children are all either invisible or collapsed block props
837 if !self.is::<Element>() {
838 return false;
839 };
840 if !self.is_inline_node() {
841 return false;
842 }
843 let mut at_least_one_collapsed_block_prop = false;
844 for child in self.children() {
845 if child.is_collapsed_block_prop() {
846 at_least_one_collapsed_block_prop = true;
847 continue;
848 }
849 if child.is_invisible() {
850 continue;
851 }
852
853 return false;
854 }
855 // > and that has at least one child that is a collapsed block prop.
856 at_least_one_collapsed_block_prop
857 }
858
859 /// <https://w3c.github.io/editing/docs/execCommand/#follows-a-line-break>
860 fn follows_a_line_break(&self) -> bool {
861 // Step 1. Let offset be zero.
862 let mut offset = 0;
863 // Step 2. While (node, offset) is not a block boundary point:
864 let mut node = DomRoot::from_ref(self);
865 while !node.is_block_boundary_point(offset) {
866 // Step 2.2. If offset is zero or node has no children, set offset to node's index, then set node to its parent.
867 if offset == 0 || node.children_count() == 0 {
868 offset = node.index();
869 node = node.GetParentNode().expect("Must always have a parent");
870 continue;
871 }
872 // Step 2.1. If node has a visible child with index offset minus one, return false.
873 let child = node.children().nth(offset as usize - 1);
874 let Some(child) = child else {
875 return false;
876 };
877 if child.is_visible() {
878 return false;
879 }
880 // Step 2.3. Otherwise, set node to its child with index offset minus one, then set offset to node's length.
881 node = child;
882 offset = node.len();
883 }
884 // Step 3. Return true.
885 true
886 }
887
888 /// <https://w3c.github.io/editing/docs/execCommand/#precedes-a-line-break>
889 fn precedes_a_line_break(&self) -> bool {
890 let mut node = DomRoot::from_ref(self);
891 // Step 1. Let offset be node's length.
892 let mut offset = node.len();
893 // Step 2. While (node, offset) is not a block boundary point:
894 while !node.is_block_boundary_point(offset) {
895 // Step 2.1. If node has a visible child with index offset, return false.
896 if node
897 .children()
898 .nth(offset as usize)
899 .is_some_and(|child| child.is_visible())
900 {
901 return false;
902 }
903 // Step 2.2. If offset is node's length or node has no children, set offset to one plus node's index, then set node to its parent.
904 if offset == node.len() || node.children_count() == 0 {
905 offset = 1 + node.index();
906 node = node.GetParentNode().expect("Must always have a parent");
907 continue;
908 }
909 // Step 2.3. Otherwise, set node to its child with index offset and set offset to zero.
910 let child = node.children().nth(offset as usize);
911 node = match child {
912 None => return false,
913 Some(child) => child,
914 };
915 offset = 0;
916 }
917 // Step 3. Return true.
918 true
919 }
920
921 /// <https://w3c.github.io/editing/docs/execCommand/#canonical-space-sequence>
922 fn canonical_space_sequence(
923 n: usize,
924 non_breaking_start: bool,
925 non_breaking_end: bool,
926 ) -> String {
927 let mut n = n;
928 // Step 1. If n is zero, return the empty string.
929 if n == 0 {
930 return String::new();
931 }
932 // Step 2. If n is one and both non-breaking start and non-breaking end are false, return a single space (U+0020).
933 if n == 1 {
934 if !non_breaking_start && !non_breaking_end {
935 return "\u{0020}".to_owned();
936 }
937 // Step 3. If n is one, return a single non-breaking space (U+00A0).
938 return "\u{00A0}".to_owned();
939 }
940 // Step 4. Let buffer be the empty string.
941 let mut buffer = String::new();
942 // Step 5. If non-breaking start is true, let repeated pair be U+00A0 U+0020. Otherwise, let it be U+0020 U+00A0.
943 let repeated_pair = if non_breaking_start {
944 "\u{00A0}\u{0020}"
945 } else {
946 "\u{0020}\u{00A0}"
947 };
948 // Step 6. While n is greater than three, append repeated pair to buffer and subtract two from n.
949 while n > 3 {
950 buffer.push_str(repeated_pair);
951 n -= 2;
952 }
953 // Step 7. If n is three, append a three-code unit string to buffer depending on non-breaking start and non-breaking end:
954 if n == 3 {
955 buffer.push_str(match (non_breaking_start, non_breaking_end) {
956 (false, false) => "\u{0020}\u{00A0}\u{0020}",
957 (true, false) => "\u{00A0}\u{00A0}\u{0020}",
958 (false, true) => "\u{0020}\u{00A0}\u{00A0}",
959 (true, true) => "\u{00A0}\u{0020}\u{00A0}",
960 });
961 } else {
962 // Step 8. Otherwise, append a two-code unit string to buffer depending on non-breaking start and non-breaking end:
963 buffer.push_str(match (non_breaking_start, non_breaking_end) {
964 (false, false) | (true, false) => "\u{00A0}\u{0020}",
965 (false, true) => "\u{0020}\u{00A0}",
966 (true, true) => "\u{00A0}\u{00A0}",
967 });
968 }
969 // Step 9. Return buffer.
970 buffer
971 }
972
973 /// <https://w3c.github.io/editing/docs/execCommand/#canonicalize-whitespace>
974 fn canonicalize_whitespace(&self, offset: u32, fix_collapsed_space: bool) {
975 // Step 1. If node is neither editable nor an editing host, abort these steps.
976 if !self.is_editable_or_editing_host() {
977 return;
978 }
979 // Step 2. Let start node equal node and let start offset equal offset.
980 let mut start_node = DomRoot::from_ref(self);
981 let mut start_offset = offset;
982 // Step 3. Repeat the following steps:
983 loop {
984 // Step 3.1. If start node has a child in the same editing host with index start offset minus one,
985 // set start node to that child, then set start offset to start node's length.
986 if start_offset > 0 {
987 let child = start_node.children().nth(start_offset as usize - 1);
988 if let Some(child) = child {
989 if start_node.same_editing_host(&child) {
990 start_node = child;
991 start_offset = start_node.len();
992 continue;
993 }
994 };
995 }
996 // Step 3.2. Otherwise, if start offset is zero and start node does not follow a line break
997 // and start node's parent is in the same editing host, set start offset to start node's index,
998 // then set start node to its parent.
999 if start_offset == 0 && !start_node.follows_a_line_break() {
1000 if let Some(parent) = start_node.GetParentNode() {
1001 if parent.same_editing_host(&start_node) {
1002 start_offset = start_node.index();
1003 start_node = parent;
1004 }
1005 }
1006 }
1007 // Step 3.3. Otherwise, if start node is a Text node and its parent's resolved
1008 // value for "white-space" is neither "pre" nor "pre-wrap" and start offset is not zero
1009 // and the (start offset − 1)st code unit of start node's data is a space (0x0020) or
1010 // non-breaking space (0x00A0), subtract one from start offset.
1011 if start_offset != 0 &&
1012 start_node.downcast::<Text>().is_some_and(|text| {
1013 text.has_whitespace_and_has_parent_with_whitespace_preserve(
1014 start_offset - 1,
1015 &[&'\u{0020}', &'\u{00A0}'],
1016 )
1017 })
1018 {
1019 start_offset -= 1;
1020 }
1021 // Step 3.4. Otherwise, break from this loop.
1022 break;
1023 }
1024 // Step 4. Let end node equal start node and end offset equal start offset.
1025 let mut end_node = start_node.clone();
1026 let mut end_offset = start_offset;
1027 // Step 5. Let length equal zero.
1028 let mut length = 0;
1029 // Step 6. Let collapse spaces be true if start offset is zero and start node follows a line break, otherwise false.
1030 let mut collapse_spaces = start_offset == 0 && start_node.follows_a_line_break();
1031 // Step 7. Repeat the following steps:
1032 loop {
1033 // Step 7.1. If end node has a child in the same editing host with index end offset,
1034 // set end node to that child, then set end offset to zero.
1035 if let Some(child) = end_node.children().nth(end_offset as usize) {
1036 if child.same_editing_host(&end_node) {
1037 end_node = child;
1038 end_offset = 0;
1039 continue;
1040 }
1041 }
1042 // Step 7.2. Otherwise, if end offset is end node's length
1043 // and end node does not precede a line break
1044 // and end node's parent is in the same editing host,
1045 // set end offset to one plus end node's index, then set end node to its parent.
1046 if end_offset == end_node.len() && !end_node.precedes_a_line_break() {
1047 if let Some(parent) = end_node.GetParentNode() {
1048 if parent.same_editing_host(&end_node) {
1049 end_offset = 1 + end_node.index();
1050 end_node = parent;
1051 }
1052 }
1053 continue;
1054 }
1055 // Step 7.3. Otherwise, if end node is a Text node and its parent's resolved value for "white-space"
1056 // is neither "pre" nor "pre-wrap"
1057 // and end offset is not end node's length and the end offsetth code unit of end node's data
1058 // is a space (0x0020) or non-breaking space (0x00A0):
1059 if let Some(text) = end_node.downcast::<Text>() {
1060 if text.has_whitespace_and_has_parent_with_whitespace_preserve(
1061 end_offset,
1062 &[&'\u{0020}', &'\u{00A0}'],
1063 ) {
1064 // Step 7.3.1. If fix collapsed space is true, and collapse spaces is true,
1065 // and the end offsetth code unit of end node's data is a space (0x0020):
1066 // call deleteData(end offset, 1) on end node, then continue this loop from the beginning.
1067 let has_space_at_offset = text
1068 .data()
1069 .chars()
1070 .nth(end_offset as usize)
1071 .is_some_and(|c| c == '\u{0020}');
1072 if fix_collapsed_space && collapse_spaces && has_space_at_offset {
1073 if text
1074 .upcast::<CharacterData>()
1075 .DeleteData(end_offset, 1)
1076 .is_err()
1077 {
1078 unreachable!("Invalid deletion for character at end offset");
1079 }
1080 continue;
1081 }
1082 // Step 7.3.2. Set collapse spaces to true if the end offsetth code unit of
1083 // end node's data is a space (0x0020), false otherwise.
1084 collapse_spaces = text
1085 .data()
1086 .chars()
1087 .nth(end_offset as usize)
1088 .is_some_and(|c| c == '\u{0020}');
1089 // Step 7.3.3. Add one to end offset.
1090 end_offset += 1;
1091 // Step 7.3.4. Add one to length.
1092 length += 1;
1093 continue;
1094 }
1095 }
1096 // Step 7.4. Otherwise, break from this loop.
1097 break;
1098 }
1099 // Step 8. If fix collapsed space is true, then while (start node, start offset)
1100 // is before (end node, end offset):
1101 if fix_collapsed_space {
1102 while bp_position(&start_node, start_offset, &end_node, end_offset) ==
1103 Some(Ordering::Less)
1104 {
1105 // Step 8.1. If end node has a child in the same editing host with index end offset − 1,
1106 // set end node to that child, then set end offset to end node's length.
1107 if end_offset > 0 {
1108 if let Some(child) = end_node.children().nth(end_offset as usize - 1) {
1109 if child.same_editing_host(&end_node) {
1110 end_node = child;
1111 end_offset = end_node.len();
1112 continue;
1113 }
1114 }
1115 }
1116 // Step 8.2. Otherwise, if end offset is zero and end node's parent is in the same editing host,
1117 // set end offset to end node's index, then set end node to its parent.
1118 if let Some(parent) = end_node.GetParentNode() {
1119 if end_offset == 0 && parent.same_editing_host(&end_node) {
1120 end_offset = end_node.index();
1121 end_node = parent;
1122 continue;
1123 }
1124 }
1125 // Step 8.3. Otherwise, if end node is a Text node and its parent's resolved value for "white-space"
1126 // is neither "pre" nor "pre-wrap"
1127 // and end offset is end node's length and the last code unit of end node's data
1128 // is a space (0x0020) and end node precedes a line break:
1129 if let Some(text) = end_node.downcast::<Text>() {
1130 if text.has_whitespace_and_has_parent_with_whitespace_preserve(
1131 text.data().len() as u32,
1132 &[&'\u{0020}'],
1133 ) && end_node.precedes_a_line_break()
1134 {
1135 // Step 8.3.1. Subtract one from end offset.
1136 end_offset -= 1;
1137 // Step 8.3.2. Subtract one from length.
1138 length -= 1;
1139 // Step 8.3.3. Call deleteData(end offset, 1) on end node.
1140 if text
1141 .upcast::<CharacterData>()
1142 .DeleteData(end_offset, 1)
1143 .is_err()
1144 {
1145 unreachable!("Invalid deletion for character at end offset");
1146 }
1147 continue;
1148 }
1149 }
1150 // Step 8.4. Otherwise, break from this loop.
1151 break;
1152 }
1153 }
1154 // Step 9. Let replacement whitespace be the canonical space sequence of length length.
1155 // non-breaking start is true if start offset is zero and start node follows a line break, and false otherwise.
1156 // non-breaking end is true if end offset is end node's length and end node precedes a line break, and false otherwise.
1157 let replacement_whitespace = Node::canonical_space_sequence(
1158 length,
1159 start_offset == 0 && start_node.follows_a_line_break(),
1160 end_offset == end_node.len() && end_node.precedes_a_line_break(),
1161 );
1162 let mut replacement_whitespace_chars = replacement_whitespace.chars();
1163 // Step 10. While (start node, start offset) is before (end node, end offset):
1164 while bp_position(&start_node, start_offset, &end_node, end_offset) == Some(Ordering::Less)
1165 {
1166 // Step 10.1. If start node has a child with index start offset, set start node to that child, then set start offset to zero.
1167 if let Some(child) = start_node.children().nth(start_offset as usize) {
1168 start_node = child;
1169 start_offset = 0;
1170 continue;
1171 }
1172 // Step 10.2. Otherwise, if start node is not a Text node or if start offset is start node's length,
1173 // set start offset to one plus start node's index, then set start node to its parent.
1174 let start_node_as_text = start_node.downcast::<Text>();
1175 if start_node_as_text.is_none() || start_offset == start_node.len() {
1176 start_offset = 1 + start_node.index();
1177 start_node = start_node
1178 .GetParentNode()
1179 .expect("Must always have a parent");
1180 continue;
1181 }
1182 let start_node_as_text =
1183 start_node_as_text.expect("Already verified none in previous statement");
1184 // Step 10.3. Otherwise:
1185 // Step 10.3.1. Remove the first code unit from replacement whitespace, and let element be that code unit.
1186 if let Some(element) = replacement_whitespace_chars.next() {
1187 // Step 10.3.2. If element is not the same as the start offsetth code unit of start node's data:
1188 if start_node_as_text.data().chars().nth(start_offset as usize) != Some(element) {
1189 let character_data = start_node_as_text.upcast::<CharacterData>();
1190 // Step 10.3.2.1. Call insertData(start offset, element) on start node.
1191 if character_data
1192 .InsertData(start_offset, element.to_string().into())
1193 .is_err()
1194 {
1195 unreachable!("Invalid insertion for character at start offset");
1196 }
1197 // Step 10.3.2.2. Call deleteData(start offset + 1, 1) on start node.
1198 if character_data.DeleteData(start_offset + 1, 1).is_err() {
1199 unreachable!("Invalid deletion for character at start offset + 1");
1200 }
1201 }
1202 }
1203 // Step 10.3.3. Add one to start offset.
1204 start_offset += 1;
1205 }
1206 }
1207
1208 /// <https://w3c.github.io/editing/docs/execCommand/#remove-extraneous-line-breaks-before>
1209 fn remove_extraneous_line_breaks_before(&self, cx: &mut js::context::JSContext) {
1210 let parent = self.GetParentNode();
1211 // Step 1. Let ref be the previousSibling of node.
1212 let Some(mut ref_) = self.GetPreviousSibling() else {
1213 // Step 2. If ref is null, abort these steps.
1214 return;
1215 };
1216 // Step 3. While ref has children, set ref to its lastChild.
1217 while let Some(last_child) = ref_.children().last() {
1218 ref_ = last_child;
1219 }
1220 // Step 4. While ref is invisible but not an extraneous line break,
1221 // and ref does not equal node's parent, set ref to the node before it in tree order.
1222 loop {
1223 if ref_.is_invisible() &&
1224 ref_.downcast::<HTMLBRElement>()
1225 .is_none_or(|br| !br.is_extraneous_line_break())
1226 {
1227 if let Some(parent) = parent.as_ref() {
1228 if ref_ != *parent {
1229 ref_ = match ref_.preceding_nodes(parent).nth(0) {
1230 None => break,
1231 Some(node) => node,
1232 };
1233 continue;
1234 }
1235 }
1236 }
1237 break;
1238 }
1239 // Step 5. If ref is an editable extraneous line break, remove it from its parent.
1240 if ref_.is_editable() &&
1241 ref_.downcast::<HTMLBRElement>()
1242 .is_some_and(|br| br.is_extraneous_line_break())
1243 {
1244 assert!(ref_.has_parent());
1245 ref_.remove_self(cx);
1246 }
1247 }
1248
1249 /// <https://w3c.github.io/editing/docs/execCommand/#remove-extraneous-line-breaks-at-the-end-of>
1250 fn remove_extraneous_line_breaks_at_the_end_of(&self, cx: &mut js::context::JSContext) {
1251 // Step 1. Let ref be node.
1252 let mut ref_ = DomRoot::from_ref(self);
1253 // Step 2. While ref has children, set ref to its lastChild.
1254 while let Some(last_child) = ref_.children().last() {
1255 ref_ = last_child;
1256 }
1257 // Step 3. While ref is invisible but not an extraneous line break, and ref does not equal node,
1258 // set ref to the node before it in tree order.
1259 loop {
1260 if ref_.is_invisible() &&
1261 *ref_ != *self &&
1262 ref_.downcast::<HTMLBRElement>()
1263 .is_none_or(|br| !br.is_extraneous_line_break())
1264 {
1265 if let Some(parent_of_ref) = ref_.GetParentNode() {
1266 ref_ = match ref_.preceding_nodes(&parent_of_ref).nth(0) {
1267 None => break,
1268 Some(node) => node,
1269 };
1270 continue;
1271 }
1272 }
1273 break;
1274 }
1275 // Step 4. If ref is an editable extraneous line break:
1276 if ref_.is_editable() &&
1277 ref_.downcast::<HTMLBRElement>()
1278 .is_some_and(|br| br.is_extraneous_line_break())
1279 {
1280 // Step 4.1. While ref's parent is editable and invisible, set ref to its parent.
1281 loop {
1282 if let Some(parent) = ref_.GetParentNode() {
1283 if parent.is_editable() && parent.is_invisible() {
1284 ref_ = parent;
1285 continue;
1286 }
1287 }
1288 break;
1289 }
1290 // Step 4.2. Remove ref from its parent.
1291 assert!(ref_.has_parent());
1292 ref_.remove_self(cx);
1293 }
1294 }
1295
1296 /// <https://w3c.github.io/editing/docs/execCommand/#preserving-its-descendants>
1297 fn remove_preserving_its_descendants(&self, cx: &mut js::context::JSContext) {
1298 // > To remove a node node while preserving its descendants,
1299 // > split the parent of node's children if it has any.
1300 // > If it has no children, instead remove it from its parent.
1301 if self.children_count() == 0 {
1302 assert!(self.has_parent());
1303 self.remove_self(cx);
1304 } else {
1305 rooted_vec!(let children <- self.children().map(|child| DomRoot::as_traced(&child)));
1306 split_the_parent(cx, children.r());
1307 }
1308 }
1309
1310 /// <https://w3c.github.io/editing/docs/execCommand/#effective-command-value>
1311 fn effective_command_value(&self, command_name: CommandName) -> Option<DOMString> {
1312 // Step 1. If neither node nor its parent is an Element, return null.
1313 // Step 2. If node is not an Element, return the effective command value of its parent for command.
1314 if !self.is::<Element>() {
1315 return self.GetParentElement().and_then(|parent| {
1316 parent
1317 .upcast::<Node>()
1318 .effective_command_value(command_name)
1319 });
1320 }
1321 match command_name {
1322 // Step 3. If command is "createLink" or "unlink":
1323 CommandName::CreateLink | CommandName::Unlink => {
1324 // Step 3.1. While node is not null, and is not an a element that has an href attribute, set node to its parent.
1325 let mut current_node = Some(DomRoot::from_ref(self));
1326 while let Some(node) = current_node {
1327 if let Some(anchor_value) =
1328 node.downcast::<HTMLAnchorElement>().and_then(|anchor| {
1329 anchor
1330 .upcast::<Element>()
1331 .get_attribute(&local_name!("href"))
1332 })
1333 {
1334 // Step 3.3. Return the value of node's href attribute.
1335 return Some(DOMString::from(&**anchor_value.value()));
1336 }
1337 current_node = node.GetParentNode();
1338 }
1339 // Step 3.2. If node is null, return null.
1340 None
1341 },
1342 // Step 4. If command is "backColor" or "hiliteColor":
1343 CommandName::BackColor | CommandName::HiliteColor => {
1344 // Step 4.1. While the resolved value of "background-color" on node is any fully transparent value,
1345 // and node's parent is an Element, set node to its parent.
1346 // TODO
1347 // Step 4.2. Return the resolved value of "background-color" for node.
1348 // TODO
1349 None
1350 },
1351 // Step 5. If command is "subscript" or "superscript":
1352 CommandName::Subscript | CommandName::Superscript => {
1353 // Step 5.1. Let affected by subscript and affected by superscript be two boolean variables,
1354 // both initially false.
1355 let mut affected_by_subscript = false;
1356 let mut affected_by_superscript = false;
1357 // Step 5.2. While node is an inline node:
1358 let mut current_node = Some(DomRoot::from_ref(self));
1359 while let Some(node) = current_node {
1360 if !node.is_inline_node() {
1361 break;
1362 }
1363 if let Some(element) = node.downcast::<Element>() {
1364 // Step 5.2.1. If node is a sub, set affected by subscript to true.
1365 if *element.local_name() == local_name!("sub") {
1366 affected_by_subscript = true;
1367 } else if *element.local_name() == local_name!("sup") {
1368 // Step 5.2.2. Otherwise, if node is a sup, set affected by superscript to true.
1369 affected_by_superscript = true;
1370 }
1371 }
1372 // Step 5.2.3. Set node to its parent.
1373 current_node = node.GetParentNode();
1374 }
1375 Some(match (affected_by_subscript, affected_by_superscript) {
1376 // Step 5.3. If affected by subscript and affected by superscript are both true,
1377 // return the string "mixed".
1378 (true, true) => "mixed".into(),
1379 // Step 5.4. If affected by subscript is true, return "subscript".
1380 (true, false) => "subscript".into(),
1381 // Step 5.5. If affected by superscript is true, return "superscript".
1382 (false, true) => "superscript".into(),
1383 // Step 5.6. Return null.
1384 (false, false) => return None,
1385 })
1386 },
1387 // Step 6. If command is "strikethrough",
1388 // and the "text-decoration" property of node or any of its ancestors has resolved value containing "line-through",
1389 // return "line-through". Otherwise, return null.
1390 // TODO
1391 CommandName::Strikethrough => None,
1392 // Step 7. If command is "underline",
1393 // and the "text-decoration" property of node or any of its ancestors has resolved value containing "underline",
1394 // return "underline". Otherwise, return null.
1395 // TODO
1396 CommandName::Underline => None,
1397 // Step 8. Return the resolved value for node of the relevant CSS property for command.
1398 // TODO
1399 _ => None,
1400 }
1401 }
1402}
1403
1404pub(crate) trait ContentEditableRange {
1405 fn handle_focus_state_for_contenteditable(&self, can_gc: CanGc);
1406}
1407
1408impl ContentEditableRange for HTMLElement {
1409 /// There is no specification for this implementation. Instead, it is
1410 /// reverse-engineered based on the WPT test
1411 /// /selection/contenteditable/initial-selection-on-focus.tentative.html
1412 fn handle_focus_state_for_contenteditable(&self, can_gc: CanGc) {
1413 if !self.is_editing_host() {
1414 return;
1415 }
1416 let document = self.owner_document();
1417 let Some(selection) = document.GetSelection(can_gc) else {
1418 return;
1419 };
1420 let range = self
1421 .upcast::<Element>()
1422 .ensure_contenteditable_selection_range(&document, can_gc);
1423 // If the current range is already associated with this contenteditable
1424 // element, then we shouldn't do anything. This is important when focus
1425 // is lost and regained, but selection was changed beforehand. In that
1426 // case, we should maintain the selection as it were, by not creating
1427 // a new range.
1428 if selection
1429 .active_range()
1430 .is_some_and(|active| active == range)
1431 {
1432 return;
1433 }
1434 let node = self.upcast::<Node>();
1435 let mut selected_node = DomRoot::from_ref(node);
1436 let mut previous_eligible_node = DomRoot::from_ref(node);
1437 let mut previous_node = DomRoot::from_ref(node);
1438 let mut selected_offset = 0;
1439 for child in node.traverse_preorder(ShadowIncluding::Yes) {
1440 if let Some(text) = child.downcast::<Text>() {
1441 // Note that to consider it whitespace, it needs to take more
1442 // into account than simply "it has a non-whitespace" character.
1443 // Therefore, we need to first check if it is not a whitespace
1444 // node and only then can we find what the relevant character is.
1445 if !text.is_whitespace_node() {
1446 // A node with "white-space: pre" set must select its first
1447 // character, regardless if that's a whitespace character or not.
1448 let is_pre_formatted_text_node = child
1449 .GetParentElement()
1450 .and_then(|parent| parent.style())
1451 .is_some_and(|style| {
1452 style.get_inherited_text().white_space_collapse ==
1453 WhiteSpaceCollapse::Preserve
1454 });
1455 if !is_pre_formatted_text_node {
1456 // If it isn't pre-formatted, then we should instead select the
1457 // first non-whitespace character.
1458 selected_offset = text
1459 .data()
1460 .find(|c: char| !c.is_whitespace())
1461 .unwrap_or_default() as u32;
1462 }
1463 selected_node = child;
1464 break;
1465 }
1466 }
1467 // For <input>, <textarea>, <hr> and <br> elements, we should select the previous
1468 // node, regardless if it was a block node or not
1469 if matches!(
1470 child.type_id(),
1471 NodeTypeId::Element(ElementTypeId::HTMLElement(
1472 HTMLElementTypeId::HTMLInputElement,
1473 )) | NodeTypeId::Element(ElementTypeId::HTMLElement(
1474 HTMLElementTypeId::HTMLTextAreaElement,
1475 )) | NodeTypeId::Element(ElementTypeId::HTMLElement(
1476 HTMLElementTypeId::HTMLHRElement,
1477 )) | NodeTypeId::Element(ElementTypeId::HTMLElement(
1478 HTMLElementTypeId::HTMLBRElement,
1479 ))
1480 ) {
1481 selected_node = previous_node;
1482 break;
1483 }
1484 // When we encounter a non-contenteditable element, we should select the previous
1485 // eligible node
1486 if child
1487 .downcast::<HTMLElement>()
1488 .is_some_and(|el| el.ContentEditable().str() == "false")
1489 {
1490 selected_node = previous_eligible_node;
1491 break;
1492 }
1493 // We can only select block nodes as eligible nodes for the case of non-conenteditable
1494 // nodes
1495 if child.is_block_node() {
1496 previous_eligible_node = child.clone();
1497 }
1498 previous_node = child;
1499 }
1500 range.set_start(&selected_node, selected_offset);
1501 range.set_end(&selected_node, selected_offset);
1502 selection.AddRange(&range);
1503 }
1504}
1505
1506trait EquivalentPoint {
1507 fn previous_equivalent_point(&self) -> Option<(DomRoot<Node>, u32)>;
1508 fn next_equivalent_point(&self) -> Option<(DomRoot<Node>, u32)>;
1509 fn first_equivalent_point(self) -> (DomRoot<Node>, u32);
1510 fn last_equivalent_point(self) -> (DomRoot<Node>, u32);
1511}
1512
1513impl EquivalentPoint for (DomRoot<Node>, u32) {
1514 /// <https://w3c.github.io/editing/docs/execCommand/#previous-equivalent-point>
1515 fn previous_equivalent_point(&self) -> Option<(DomRoot<Node>, u32)> {
1516 let (node, offset) = self;
1517 // Step 1. If node's length is zero, return null.
1518 let len = node.len();
1519 if len == 0 {
1520 return None;
1521 }
1522 // Step 2. If offset is 0, and node's parent is not null, and node is an inline node,
1523 // return (node's parent, node's index).
1524 if *offset == 0 && node.is_inline_node() {
1525 if let Some(parent) = node.GetParentNode() {
1526 return Some((parent, node.index()));
1527 }
1528 }
1529 // Step 3. If node has a child with index offset − 1, and that child's length is not zero,
1530 // and that child is an inline node, return (that child, that child's length).
1531 if *offset > 0 {
1532 if let Some(child) = node.children().nth(*offset as usize - 1) {
1533 if !child.is_empty() && child.is_inline_node() {
1534 let len = child.len();
1535 return Some((child, len));
1536 }
1537 }
1538 }
1539
1540 // Step 4. Return null.
1541 None
1542 }
1543
1544 /// <https://w3c.github.io/editing/docs/execCommand/#next-equivalent-point>
1545 fn next_equivalent_point(&self) -> Option<(DomRoot<Node>, u32)> {
1546 let (node, offset) = self;
1547 // Step 1. If node's length is zero, return null.
1548 let len = node.len();
1549 if len == 0 {
1550 return None;
1551 }
1552
1553 // Step 2.
1554 //
1555 // This step does not exist in the spec
1556
1557 // Step 3. If offset is node's length, and node's parent is not null, and node is an inline node,
1558 // return (node's parent, 1 + node's index).
1559 if *offset == len && node.is_inline_node() {
1560 if let Some(parent) = node.GetParentNode() {
1561 return Some((parent, node.index() + 1));
1562 }
1563 }
1564
1565 // Step 4.
1566 //
1567 // This step does not exist in the spec
1568
1569 // Step 5. If node has a child with index offset, and that child's length is not zero,
1570 // and that child is an inline node, return (that child, 0).
1571 if let Some(child) = node.children().nth(*offset as usize) {
1572 if !child.is_empty() && child.is_inline_node() {
1573 return Some((child, 0));
1574 }
1575 }
1576
1577 // Step 6.
1578 //
1579 // This step does not exist in the spec
1580
1581 // Step 7. Return null.
1582 None
1583 }
1584
1585 /// <https://w3c.github.io/editing/docs/execCommand/#first-equivalent-point>
1586 fn first_equivalent_point(self) -> (DomRoot<Node>, u32) {
1587 let mut previous_equivalent_point = self;
1588 // Step 1. While (node, offset)'s previous equivalent point is not null, set (node, offset) to its previous equivalent point.
1589 loop {
1590 if let Some(next) = previous_equivalent_point.previous_equivalent_point() {
1591 previous_equivalent_point = next;
1592 } else {
1593 // Step 2. Return (node, offset).
1594 return previous_equivalent_point;
1595 }
1596 }
1597 }
1598
1599 /// <https://w3c.github.io/editing/docs/execCommand/#last-equivalent-point>
1600 fn last_equivalent_point(self) -> (DomRoot<Node>, u32) {
1601 let mut next_equivalent_point = self;
1602 // Step 1. While (node, offset)'s next equivalent point is not null, set (node, offset) to its next equivalent point.
1603 loop {
1604 if let Some(next) = next_equivalent_point.next_equivalent_point() {
1605 next_equivalent_point = next;
1606 } else {
1607 // Step 2. Return (node, offset).
1608 return next_equivalent_point;
1609 }
1610 }
1611 }
1612}
1613
1614enum BoolOrOptionalString {
1615 Bool(bool),
1616 OptionalString(Option<DOMString>),
1617}
1618
1619impl From<Option<DOMString>> for BoolOrOptionalString {
1620 fn from(optional_string: Option<DOMString>) -> Self {
1621 Self::OptionalString(optional_string)
1622 }
1623}
1624
1625impl From<bool> for BoolOrOptionalString {
1626 fn from(bool_: bool) -> Self {
1627 Self::Bool(bool_)
1628 }
1629}
1630
1631struct RecordedStateOfNode {
1632 name: CommandName,
1633 value: BoolOrOptionalString,
1634}
1635
1636impl Range {
1637 /// <https://w3c.github.io/editing/docs/execCommand/#effectively-contained>
1638 fn is_effectively_contained_node(&self, node: &Node) -> bool {
1639 assert!(!self.collapsed());
1640 // > A node node is effectively contained in a range range if range is not collapsed,
1641 // > and at least one of the following holds:
1642 // > node is range's start node, it is a Text node, and its length is different from range's start offset.
1643 let start_container = self.start_container();
1644 if *start_container == *node && node.is::<Text>() && node.len() != self.start_offset() {
1645 return true;
1646 }
1647 // > node is range's end node, it is a Text node, and range's end offset is not 0.
1648 let end_container = self.end_container();
1649 if *end_container == *node && node.is::<Text>() && self.end_offset() != 0 {
1650 return true;
1651 }
1652 // > node is contained in range.
1653 //
1654 // Already checked by caller
1655
1656 // > node has at least one child; and all its children are effectively contained in range;
1657 node.children_count() > 0 && node.children().all(|child| self.is_effectively_contained_node(&child))
1658 // > and either range's start node is not a descendant of node or is not a Text node or range's start offset is zero;
1659 && (!node.is_ancestor_of(&start_container) || !start_container.is::<Text>() || self.start_offset() == 0)
1660 // > and either range's end node is not a descendant of node or is not a Text node or range's end offset is its end node's length.
1661 && (!node.is_ancestor_of(&end_container) || !end_container.is::<Text>() || self.end_offset() == end_container.len())
1662 }
1663
1664 fn first_formattable_contained_node(&self) -> Option<DomRoot<Node>> {
1665 if self.collapsed() {
1666 return None;
1667 }
1668 let Ok(contained_children) = self.contained_children() else {
1669 unreachable!("Must always be able to obtain contained children");
1670 };
1671 contained_children
1672 .first_partially_contained_child
1673 .as_ref()
1674 .filter(|child| child.is_formattable() && self.is_effectively_contained_node(child))
1675 .or_else(|| {
1676 // We don't call `is_effectively_contained_node` here, since nodes are considered
1677 // effectively contained if they are in `contained_children`
1678 contained_children
1679 .contained_children
1680 .iter()
1681 .find(|child| child.is_formattable())
1682 })
1683 .or_else(|| {
1684 contained_children
1685 .last_partially_contained_child
1686 .as_ref()
1687 .filter(|child| {
1688 child.is_formattable() && self.is_effectively_contained_node(child)
1689 })
1690 })
1691 .cloned()
1692 }
1693
1694 /// <https://w3c.github.io/editing/docs/execCommand/#record-current-states-and-values>
1695 fn record_current_states_and_values(&self) -> Vec<RecordedStateOfNode> {
1696 // Step 1. Let overrides be a list of (string, string or boolean) ordered pairs, initially empty.
1697 //
1698 // We return the vec in one go for the relevant values
1699
1700 // Step 2. Let node be the first formattable node effectively contained in the active range,
1701 // or null if there is none.
1702 let Some(node) = self.first_formattable_contained_node() else {
1703 // Step 3. If node is null, return overrides.
1704 return vec![];
1705 };
1706 // Step 8. Return overrides.
1707 vec![
1708 // Step 4. Add ("createLink", node's effective command value for "createLink") to overrides.
1709 RecordedStateOfNode {
1710 name: CommandName::CreateLink,
1711 value: node.effective_command_value(CommandName::CreateLink).into(),
1712 },
1713 // Step 5. For each command in the list
1714 // "bold", "italic", "strikethrough", "subscript", "superscript", "underline", in order:
1715 // if node's effective command value for command is one of its inline command activated values,
1716 // add (command, true) to overrides, and otherwise add (command, false) to overrides.
1717 // TODO
1718
1719 // Step 6. For each command in the list "fontName", "foreColor", "hiliteColor", in order:
1720 // add (command, command's value) to overrides.
1721 // TODO
1722
1723 // Step 7. Add ("fontSize", node's effective command value for "fontSize") to overrides.
1724 RecordedStateOfNode {
1725 name: CommandName::FontSize,
1726 value: node.effective_command_value(CommandName::FontSize).into(),
1727 },
1728 ]
1729 }
1730
1731 /// <https://w3c.github.io/editing/docs/execCommand/#restore-states-and-values>
1732 fn restore_states_and_values(
1733 &self,
1734 context_object: &Document,
1735 overrides: Vec<RecordedStateOfNode>,
1736 ) {
1737 // Step 1. Let node be the first formattable node effectively contained in the active range,
1738 // or null if there is none.
1739 let Some(_node) = self.first_formattable_contained_node() else {
1740 // Step 3. Otherwise, for each (command, override) pair in overrides, in order:
1741 for override_state in overrides {
1742 // Step 3.1. If override is a boolean, set the state override for command to override.
1743 match override_state.value {
1744 BoolOrOptionalString::Bool(bool_) => {
1745 context_object.set_state_override(override_state.name, bool_)
1746 },
1747 // Step 3.2. If override is a string, set the value override for command to override.
1748 BoolOrOptionalString::OptionalString(optional_string) => {
1749 context_object.set_value_override(override_state.name, optional_string)
1750 },
1751 }
1752 }
1753 return;
1754 };
1755 // Step 2. If node is not null, then for each (command, override) pair in overrides, in order:
1756 // TODO
1757
1758 // Step 2.1. If override is a boolean, and queryCommandState(command)
1759 // returns something different from override, take the action for command,
1760 // with value equal to the empty string.
1761 // TODO
1762
1763 // Step 2.2. Otherwise, if override is a string, and command is neither "createLink" nor "fontSize",
1764 // and queryCommandValue(command) returns something not equivalent to override,
1765 // take the action for command, with value equal to override.
1766 // TODO
1767
1768 // Step 2.3. Otherwise, if override is a string; and command is "createLink";
1769 // and either there is a value override for "createLink" that is not equal to override,
1770 // or there is no value override for "createLink" and node's effective command value
1771 // for "createLink" is not equal to override: take the action for "createLink", with value equal to override.
1772 // TODO
1773
1774 // Step 2.4. Otherwise, if override is a string; and command is "fontSize";
1775 // and either there is a value override for "fontSize" that is not equal to override,
1776 // or there is no value override for "fontSize" and node's effective command value for "fontSize"
1777 // is not loosely equivalent to override:
1778 // TODO
1779
1780 // Step 2.5. Otherwise, continue this loop from the beginning.
1781 // TODO
1782
1783 // Step 2.6. Set node to the first formattable node effectively contained in the active range, if there is one.
1784 // TODO
1785 }
1786}
1787
1788#[derive(Default, PartialEq)]
1789pub(crate) enum SelectionDeletionBlockMerging {
1790 #[default]
1791 Merge,
1792 Skip,
1793}
1794
1795#[derive(Default, PartialEq)]
1796pub(crate) enum SelectionDeletionStripWrappers {
1797 #[default]
1798 Strip,
1799}
1800
1801#[derive(Default, PartialEq)]
1802pub(crate) enum SelectionDeleteDirection {
1803 #[default]
1804 Forward,
1805 Backward,
1806}
1807
1808pub(crate) trait SelectionExecCommandSupport {
1809 fn delete_the_selection(
1810 &self,
1811 cx: &mut js::context::JSContext,
1812 context_object: &Document,
1813 block_merging: SelectionDeletionBlockMerging,
1814 strip_wrappers: SelectionDeletionStripWrappers,
1815 direction: SelectionDeleteDirection,
1816 );
1817}
1818
1819impl SelectionExecCommandSupport for Selection {
1820 /// <https://w3c.github.io/editing/docs/execCommand/#delete-the-selection>
1821 fn delete_the_selection(
1822 &self,
1823 cx: &mut js::context::JSContext,
1824 context_object: &Document,
1825 block_merging: SelectionDeletionBlockMerging,
1826 strip_wrappers: SelectionDeletionStripWrappers,
1827 direction: SelectionDeleteDirection,
1828 ) {
1829 // Step 1. If the active range is null, abort these steps and do nothing.
1830 let Some(active_range) = self.active_range() else {
1831 return;
1832 };
1833
1834 // Step 2. Canonicalize whitespace at the active range's start.
1835 active_range
1836 .start_container()
1837 .canonicalize_whitespace(active_range.start_offset(), true);
1838
1839 // Step 3. Canonicalize whitespace at the active range's end.
1840 active_range
1841 .end_container()
1842 .canonicalize_whitespace(active_range.end_offset(), true);
1843
1844 // Step 4. Let (start node, start offset) be the last equivalent point for the active range's start.
1845 let (mut start_node, mut start_offset) =
1846 (active_range.start_container(), active_range.start_offset()).last_equivalent_point();
1847
1848 // Step 5. Let (end node, end offset) be the first equivalent point for the active range's end.
1849 let (mut end_node, mut end_offset) =
1850 (active_range.end_container(), active_range.end_offset()).first_equivalent_point();
1851
1852 // Step 6. If (end node, end offset) is not after (start node, start offset):
1853 if bp_position(&end_node, end_offset, &start_node, start_offset) != Some(Ordering::Greater)
1854 {
1855 // Step 6.1. If direction is "forward", call collapseToStart() on the context object's selection.
1856 if direction == SelectionDeleteDirection::Forward {
1857 if self.CollapseToStart(CanGc::from_cx(cx)).is_err() {
1858 unreachable!("Should be able to collapse to start");
1859 }
1860 } else {
1861 // Step 6.2. Otherwise, call collapseToEnd() on the context object's selection.
1862 if self.CollapseToEnd(CanGc::from_cx(cx)).is_err() {
1863 unreachable!("Should be able to collapse to end");
1864 }
1865 }
1866 // Step 6.3. Abort these steps.
1867 return;
1868 }
1869
1870 // Step 7. If start node is a Text node and start offset is 0, set start offset to the index of start node,
1871 // then set start node to its parent.
1872 if start_node.is::<Text>() && start_offset == 0 {
1873 start_offset = start_node.index();
1874 start_node = start_node
1875 .GetParentNode()
1876 .expect("Must always have a parent");
1877 }
1878
1879 // 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,
1880 // then set end node to its parent.
1881 if end_node.is::<Text>() && end_offset == end_node.len() {
1882 end_offset = end_node.index() + 1;
1883 end_node = end_node.GetParentNode().expect("Must always have a parent");
1884 }
1885
1886 // Step 9. Call collapse(start node, start offset) on the context object's selection.
1887 if self
1888 .Collapse(Some(&start_node), start_offset, CanGc::from_cx(cx))
1889 .is_err()
1890 {
1891 unreachable!("Must always be able to collapse");
1892 }
1893
1894 // Step 10. Call extend(end node, end offset) on the context object's selection.
1895 if self
1896 .Extend(&end_node, end_offset, CanGc::from_cx(cx))
1897 .is_err()
1898 {
1899 unreachable!("Must always be able to extend");
1900 }
1901
1902 // Step 11.
1903 //
1904 // This step does not exist in the spec
1905
1906 // Step 12. Let start block be the active range's start node.
1907 let Some(active_range) = self.active_range() else {
1908 return;
1909 };
1910 let mut start_block = active_range.start_container();
1911
1912 // Step 13. While start block's parent is in the same editing host and start block is an inline node,
1913 // set start block to its parent.
1914 loop {
1915 if start_block.is_inline_node() {
1916 if let Some(parent) = start_block.GetParentNode() {
1917 if parent.same_editing_host(&start_node) {
1918 start_block = parent;
1919 continue;
1920 }
1921 }
1922 }
1923 break;
1924 }
1925
1926 // Step 14. If start block is neither a block node nor an editing host,
1927 // or "span" is not an allowed child of start block,
1928 // or start block is a td or th, set start block to null.
1929 let start_block = if (!start_block.is_block_node() && !start_block.is_editing_host()) ||
1930 !is_allowed_child(
1931 NodeOrString::String("span".to_owned()),
1932 NodeOrString::Node(start_block.clone()),
1933 ) ||
1934 start_block.is::<HTMLTableCellElement>()
1935 {
1936 None
1937 } else {
1938 Some(start_block)
1939 };
1940
1941 // Step 15. Let end block be the active range's end node.
1942 let mut end_block = active_range.end_container();
1943
1944 // 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.
1945 loop {
1946 if end_block.is_inline_node() {
1947 if let Some(parent) = end_block.GetParentNode() {
1948 if parent.same_editing_host(&end_block) {
1949 end_block = parent;
1950 continue;
1951 }
1952 }
1953 }
1954 break;
1955 }
1956
1957 // Step 17. If end block is neither a block node nor an editing host, or "span" is not an allowed child of end block,
1958 // or end block is a td or th, set end block to null.
1959 let end_block = if (!end_block.is_block_node() && !end_block.is_editing_host()) ||
1960 !is_allowed_child(
1961 NodeOrString::String("span".to_owned()),
1962 NodeOrString::Node(end_block.clone()),
1963 ) ||
1964 end_block.is::<HTMLTableCellElement>()
1965 {
1966 None
1967 } else {
1968 Some(end_block)
1969 };
1970
1971 // Step 18.
1972 //
1973 // This step does not exist in the spec
1974
1975 // Step 19. Record current states and values, and let overrides be the result.
1976 let overrides = active_range.record_current_states_and_values();
1977
1978 // Step 20.
1979 //
1980 // This step does not exist in the spec
1981
1982 // Step 21. If start node and end node are the same, and start node is an editable Text node:
1983 if start_node == end_node && start_node.is_editable() {
1984 if let Some(start_text) = start_node.downcast::<Text>() {
1985 // Step 21.1. Call deleteData(start offset, end offset − start offset) on start node.
1986 if start_text
1987 .upcast::<CharacterData>()
1988 .DeleteData(start_offset, end_offset - start_offset)
1989 .is_err()
1990 {
1991 unreachable!("Must always be able to delete");
1992 }
1993 // Step 21.2. Canonicalize whitespace at (start node, start offset), with fix collapsed space false.
1994 start_node.canonicalize_whitespace(start_offset, false);
1995 // Step 21.3. If direction is "forward", call collapseToStart() on the context object's selection.
1996 if direction == SelectionDeleteDirection::Forward {
1997 if self.CollapseToStart(CanGc::from_cx(cx)).is_err() {
1998 unreachable!("Should be able to collapse to start");
1999 }
2000 } else {
2001 // Step 21.4. Otherwise, call collapseToEnd() on the context object's selection.
2002 if self.CollapseToEnd(CanGc::from_cx(cx)).is_err() {
2003 unreachable!("Should be able to collapse to end");
2004 }
2005 }
2006 // Step 21.5. Restore states and values from overrides.
2007 active_range.restore_states_and_values(context_object, overrides);
2008
2009 // Step 21.6. Abort these steps.
2010 return;
2011 }
2012 }
2013
2014 // Step 22. If start node is an editable Text node, call deleteData() on it, with start offset as
2015 // the first argument and (length of start node − start offset) as the second argument.
2016 if start_node.is_editable() {
2017 if let Some(start_text) = start_node.downcast::<Text>() {
2018 if start_text
2019 .upcast::<CharacterData>()
2020 .DeleteData(start_offset, start_node.len() - start_offset)
2021 .is_err()
2022 {
2023 unreachable!("Must always be able to delete");
2024 }
2025 }
2026 }
2027
2028 // Step 23. Let node list be a list of nodes, initially empty.
2029 rooted_vec!(let mut node_list);
2030
2031 // Step 24. For each node contained in the active range, append node to node list if the
2032 // last member of node list (if any) is not an ancestor of node; node is editable;
2033 // and node is not a thead, tbody, tfoot, tr, th, or td.
2034 let Ok(contained_children) = active_range.contained_children() else {
2035 unreachable!("Must always have contained children");
2036 };
2037 for node in contained_children.contained_children {
2038 // This type is only used to tell the compiler how to handle the type of `node_list.last()`.
2039 // It is not allowed to add a `& DomRoot<Node>` annotation, as test-tidy disallows that.
2040 // However, if we omit the type, the compiler doesn't know what it is, since we also
2041 // aren't allowed to add a type annotation to `node_list` itself, as that is handled
2042 // by the `rooted_vec` macro. Lastly, we also can't make it `&Node`, since then the compiler
2043 // thinks that the contents of the `RootedVec` is `Node`, whereas it is should be
2044 // `RootedVec<DomRoot<Node>>`. The type alias here doesn't upset test-tidy,
2045 // while also providing the necessary information to the compiler to work.
2046 type DomRootNode = DomRoot<Node>;
2047 if node.is_editable() &&
2048 !(node.is::<HTMLTableSectionElement>() ||
2049 node.is::<HTMLTableRowElement>() ||
2050 node.is::<HTMLTableCellElement>()) &&
2051 node_list
2052 .last()
2053 .is_none_or(|last: &DomRootNode| !last.is_ancestor_of(&node))
2054 {
2055 node_list.push(node);
2056 }
2057 }
2058
2059 // Step 25. For each node in node list:
2060 for node in node_list.iter() {
2061 // Step 25.1. Let parent be the parent of node.
2062 let parent = node.GetParentNode().expect("Must always have a parent");
2063 // Step 25.2. Remove node from parent.
2064 assert!(node.has_parent());
2065 node.remove_self(cx);
2066 // Step 25.3. If the block node of parent has no visible children, and parent is editable or an editing host,
2067 // call createElement("br") on the context object and append the result as the last child of parent.
2068 if parent
2069 .block_node_of()
2070 .is_some_and(|block_node| block_node.children().all(|child| child.is_invisible())) &&
2071 parent.is_editable_or_editing_host()
2072 {
2073 let br = context_object.create_br_element(cx);
2074 if parent.AppendChild(cx, br.upcast()).is_err() {
2075 unreachable!("Must always be able to append");
2076 }
2077 }
2078 // Step 25.4. If strip wrappers is true or parent is not an inclusive ancestor of start node,
2079 // while parent is an editable inline node with length 0, let grandparent be the parent of parent,
2080 // then remove parent from grandparent, then set parent to grandparent.
2081 if strip_wrappers == SelectionDeletionStripWrappers::Strip ||
2082 !parent.is_inclusive_ancestor_of(&start_node)
2083 {
2084 let mut parent = parent;
2085 loop {
2086 if parent.is_editable() && parent.is_inline_node() && parent.is_empty() {
2087 let grand_parent =
2088 parent.GetParentNode().expect("Must always have a parent");
2089 assert!(parent.has_parent());
2090 parent.remove_self(cx);
2091 parent = grand_parent;
2092 continue;
2093 }
2094 break;
2095 }
2096 }
2097 }
2098
2099 // Step 26. If end node is an editable Text node, call deleteData(0, end offset) on it.
2100 if end_node.is_editable() {
2101 if let Some(end_text) = end_node.downcast::<Text>() {
2102 if end_text
2103 .upcast::<CharacterData>()
2104 .DeleteData(0, end_offset)
2105 .is_err()
2106 {
2107 unreachable!("Must always be able to delete");
2108 }
2109 }
2110 }
2111
2112 // Step 27. Canonicalize whitespace at the active range's start, with fix collapsed space false.
2113 active_range
2114 .start_container()
2115 .canonicalize_whitespace(active_range.start_offset(), false);
2116
2117 // Step 28. Canonicalize whitespace at the active range's end, with fix collapsed space false.
2118 active_range
2119 .end_container()
2120 .canonicalize_whitespace(active_range.end_offset(), false);
2121
2122 // Step 29.
2123 //
2124 // This step does not exist in the spec
2125
2126 // Step 30. If block merging is false, or start block or end block is null, or start block is not
2127 // in the same editing host as end block, or start block and end block are the same:
2128 if block_merging == SelectionDeletionBlockMerging::Skip ||
2129 start_block.as_ref().zip(end_block.as_ref()).is_none_or(
2130 |(start_block, end_block)| {
2131 start_block == end_block || !start_block.same_editing_host(end_block)
2132 },
2133 )
2134 {
2135 // Step 30.1. If direction is "forward", call collapseToStart() on the context object's selection.
2136 if direction == SelectionDeleteDirection::Forward {
2137 if self.CollapseToStart(CanGc::from_cx(cx)).is_err() {
2138 unreachable!("Should be able to collapse to start");
2139 }
2140 } else {
2141 // Step 30.2. Otherwise, call collapseToEnd() on the context object's selection.
2142 if self.CollapseToEnd(CanGc::from_cx(cx)).is_err() {
2143 unreachable!("Should be able to collapse to end");
2144 }
2145 }
2146 // Step 30.3. Restore states and values from overrides.
2147 active_range.restore_states_and_values(context_object, overrides);
2148
2149 // Step 30.4. Abort these steps.
2150 return;
2151 }
2152 let start_block = start_block.expect("Already checked for None in previous statement");
2153 let end_block = end_block.expect("Already checked for None in previous statement");
2154
2155 // Step 31. If start block has one child, which is a collapsed block prop, remove its child from it.
2156 if start_block.children_count() == 1 {
2157 let Some(child) = start_block.children().nth(0) else {
2158 unreachable!("Must always have a single child");
2159 };
2160 if child.is_collapsed_block_prop() {
2161 assert!(child.has_parent());
2162 child.remove_self(cx);
2163 }
2164 }
2165
2166 // Step 32. If start block is an ancestor of end block:
2167 if start_block.is_ancestor_of(&end_block) {
2168 // Step 32.1. Let reference node be end block.
2169 let mut reference_node = end_block.clone();
2170 // Step 32.2. While reference node is not a child of start block, set reference node to its parent.
2171 loop {
2172 if start_block.children().all(|child| child != reference_node) {
2173 reference_node = reference_node
2174 .GetParentNode()
2175 .expect("Must always have a parent, at least start_block");
2176 continue;
2177 }
2178 break;
2179 }
2180 // Step 32.3. Call collapse() on the context object's selection,
2181 // with first argument start block and second argument the index of reference node.
2182 if self
2183 .Collapse(
2184 Some(&start_block),
2185 reference_node.index(),
2186 CanGc::from_cx(cx),
2187 )
2188 .is_err()
2189 {
2190 unreachable!("Must always be able to collapse");
2191 }
2192 // Step 32.4. If end block has no children:
2193 if end_block.children_count() == 0 {
2194 let mut end_block = end_block;
2195 // 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,
2196 // let parent equal end block, then remove end block from parent, then set end block to parent.
2197 loop {
2198 if end_block.is_editable() &&
2199 start_block.children().all(|child| child != end_block)
2200 {
2201 if let Some(parent) = end_block.GetParentNode() {
2202 if parent.children_count() == 1 {
2203 assert!(end_block.has_parent());
2204 end_block.remove_self(cx);
2205 end_block = parent;
2206 continue;
2207 }
2208 }
2209 }
2210 break;
2211 }
2212 // Step 32.4.2. If end block is editable and is not an inline node,
2213 // and its previousSibling and nextSibling are both inline nodes,
2214 // call createElement("br") on the context object and insert it into end block's parent immediately after end block.
2215 if end_block.is_editable() &&
2216 !end_block.is_inline_node() &&
2217 end_block
2218 .GetPreviousSibling()
2219 .is_some_and(|previous| previous.is_inline_node())
2220 {
2221 if let Some(next_of_end_block) = end_block.GetNextSibling() {
2222 if next_of_end_block.is_inline_node() {
2223 let br = context_object.create_br_element(cx);
2224 let parent = end_block
2225 .GetParentNode()
2226 .expect("Must always have a parent");
2227 if parent
2228 .InsertBefore(cx, br.upcast(), Some(&next_of_end_block))
2229 .is_err()
2230 {
2231 unreachable!("Must always be able to insert into parent");
2232 }
2233 }
2234 }
2235 }
2236 // Step 32.4.3. If end block is editable, remove it from its parent.
2237 if end_block.is_editable() {
2238 assert!(end_block.has_parent());
2239 end_block.remove_self(cx);
2240 }
2241 // Step 32.4.4. Restore states and values from overrides.
2242 active_range.restore_states_and_values(context_object, overrides);
2243
2244 // Step 32.4.5. Abort these steps.
2245 return;
2246 }
2247 let first_child = end_block
2248 .children()
2249 .nth(0)
2250 .expect("Already checked at least 1 child in previous statement");
2251 // Step 32.5. If end block's firstChild is not an inline node,
2252 // restore states and values from record, then abort these steps.
2253 if !first_child.is_inline_node() {
2254 // TODO: Restore state
2255 return;
2256 }
2257 // Step 32.6. Let children be a list of nodes, initially empty.
2258 rooted_vec!(let mut children);
2259 // Step 32.7. Append the first child of end block to children.
2260 children.push(first_child.as_traced());
2261 // Step 32.8. While children's last member is not a br,
2262 // and children's last member's nextSibling is an inline node,
2263 // append children's last member's nextSibling to children.
2264 loop {
2265 let Some(last) = children.last() else {
2266 break;
2267 };
2268 if last.is::<HTMLBRElement>() {
2269 break;
2270 }
2271 let Some(next) = last.GetNextSibling() else {
2272 break;
2273 };
2274 if next.is_inline_node() {
2275 children.push(next.as_traced());
2276 continue;
2277 }
2278 break;
2279 }
2280 // Step 32.9. Record the values of children, and let values be the result.
2281 // TODO
2282
2283 // Step 32.10. While children's first member's parent is not start block,
2284 // split the parent of children.
2285 loop {
2286 if children
2287 .first()
2288 .and_then(|child| child.GetParentNode())
2289 .is_some_and(|parent_of_child| parent_of_child != start_block)
2290 {
2291 split_the_parent(cx, children.r());
2292 continue;
2293 }
2294 break;
2295 }
2296 // Step 32.11. If children's first member's previousSibling is an editable br,
2297 // remove that br from its parent.
2298 if let Some(first) = children.first() {
2299 if let Some(previous_of_first) = first.GetPreviousSibling() {
2300 if previous_of_first.is_editable() && previous_of_first.is::<HTMLBRElement>() {
2301 assert!(previous_of_first.has_parent());
2302 previous_of_first.remove_self(cx);
2303 }
2304 }
2305 }
2306 // Step 33. Otherwise, if start block is a descendant of end block:
2307 } else if end_block.is_ancestor_of(&start_block) {
2308 // Step 33.1. Call collapse() on the context object's selection,
2309 // with first argument start block and second argument start block's length.
2310 if self
2311 .Collapse(Some(&start_block), start_block.len(), CanGc::from_cx(cx))
2312 .is_err()
2313 {
2314 unreachable!("Must always be able to collapse");
2315 }
2316 // Step 33.2. Let reference node be start block.
2317 let mut reference_node = start_block.clone();
2318 // Step 33.3. While reference node is not a child of end block, set reference node to its parent.
2319 loop {
2320 if end_block.children().all(|child| child != reference_node) {
2321 if let Some(parent) = reference_node.GetParentNode() {
2322 reference_node = parent;
2323 continue;
2324 }
2325 }
2326 break;
2327 }
2328 // Step 33.4. If reference node's nextSibling is an inline node and start block's lastChild is a br,
2329 // remove start block's lastChild from it.
2330 if reference_node
2331 .GetNextSibling()
2332 .is_some_and(|next| next.is_inline_node())
2333 {
2334 if let Some(last) = start_block.children().last() {
2335 if last.is::<HTMLBRElement>() {
2336 assert!(last.has_parent());
2337 last.remove_self(cx);
2338 }
2339 }
2340 }
2341 // Step 33.5. Let nodes to move be a list of nodes, initially empty.
2342 rooted_vec!(let mut nodes_to_move);
2343 // Step 33.6. If reference node's nextSibling is neither null nor a block node,
2344 // append it to nodes to move.
2345 if let Some(next) = reference_node.GetNextSibling() {
2346 if !next.is_block_node() {
2347 nodes_to_move.push(next);
2348 }
2349 }
2350 // Step 33.7. While nodes to move is nonempty and its last member isn't a br
2351 // and its last member's nextSibling is neither null nor a block node,
2352 // append its last member's nextSibling to nodes to move.
2353 loop {
2354 if let Some(last) = nodes_to_move.last() {
2355 if !last.is::<HTMLBRElement>() {
2356 if let Some(next_of_last) = last.GetNextSibling() {
2357 if !next_of_last.is_block_node() {
2358 nodes_to_move.push(next_of_last);
2359 continue;
2360 }
2361 }
2362 }
2363 }
2364 break;
2365 }
2366 // Step 33.8. Record the values of nodes to move, and let values be the result.
2367 // TODO
2368
2369 // Step 33.9. For each node in nodes to move,
2370 // append node as the last child of start block, preserving ranges.
2371 for node in nodes_to_move.iter() {
2372 // TODO: Preserve ranges
2373 if start_block.AppendChild(cx, node).is_err() {
2374 unreachable!("Must always be able to append");
2375 }
2376 }
2377 // Step 34. Otherwise:
2378 } else {
2379 // Step 34.1. Call collapse() on the context object's selection,
2380 // with first argument start block and second argument start block's length.
2381 if self
2382 .Collapse(Some(&start_block), start_block.len(), CanGc::from_cx(cx))
2383 .is_err()
2384 {
2385 unreachable!("Must always be able to collapse");
2386 }
2387 // Step 34.2. If end block's firstChild is an inline node and start block's lastChild is a br,
2388 // remove start block's lastChild from it.
2389 if end_block
2390 .children()
2391 .nth(0)
2392 .is_some_and(|next| next.is_inline_node())
2393 {
2394 if let Some(last) = start_block.children().last() {
2395 if last.is::<HTMLBRElement>() {
2396 assert!(last.has_parent());
2397 last.remove_self(cx);
2398 }
2399 }
2400 }
2401 // Step 34.3. Record the values of end block's children, and let values be the result.
2402 // TODO
2403
2404 // Step 34.4. While end block has children,
2405 // append the first child of end block to start block, preserving ranges.
2406 loop {
2407 if let Some(first_child) = end_block.children().nth(0) {
2408 // TODO: Preserve ranges
2409 if start_block.AppendChild(cx, &first_child).is_err() {
2410 unreachable!("Must always be able to append");
2411 }
2412 continue;
2413 }
2414 break;
2415 }
2416 // Step 34.5. While end block has no children,
2417 // let parent be the parent of end block, then remove end block from parent,
2418 // then set end block to parent.
2419 let mut end_block = end_block;
2420 loop {
2421 if end_block.children_count() == 0 {
2422 if let Some(parent) = end_block.GetParentNode() {
2423 assert!(end_block.has_parent());
2424 end_block.remove_self(cx);
2425 end_block = parent;
2426 continue;
2427 }
2428 }
2429 break;
2430 }
2431 }
2432
2433 // Step 35.
2434 //
2435 // This step does not exist in the spec
2436
2437 // Step 36. Let ancestor be start block.
2438 // TODO
2439
2440 // Step 37. While ancestor has an inclusive ancestor ol in the same editing host whose nextSibling is
2441 // also an ol in the same editing host, or an inclusive ancestor ul in the same editing host whose nextSibling
2442 // is also a ul in the same editing host:
2443 // TODO
2444
2445 // Step 38. Restore the values from values.
2446 // TODO
2447
2448 // Step 39. If start block has no children, call createElement("br") on the context object and
2449 // append the result as the last child of start block.
2450 if start_block.children_count() == 0 {
2451 let br = context_object.create_br_element(cx);
2452 if start_block.AppendChild(cx, br.upcast()).is_err() {
2453 unreachable!("Must always be able to append");
2454 }
2455 }
2456
2457 // Step 40. Remove extraneous line breaks at the end of start block.
2458 start_block.remove_extraneous_line_breaks_at_the_end_of(cx);
2459
2460 // Step 41. Restore states and values from overrides.
2461 active_range.restore_states_and_values(context_object, overrides);
2462 }
2463}