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;
6
7use script_bindings::inheritance::Castable;
8use style::computed_values::white_space_collapse::T as WhiteSpaceCollapse;
9use style::values::specified::box_::DisplayOutside;
10
11use crate::dom::abstractrange::bp_position;
12use crate::dom::bindings::cell::Ref;
13use crate::dom::bindings::codegen::Bindings::CharacterDataBinding::CharacterDataMethods;
14use crate::dom::bindings::codegen::Bindings::DocumentBinding::DocumentMethods;
15use crate::dom::bindings::codegen::Bindings::HTMLElementBinding::HTMLElementMethods;
16use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods;
17use crate::dom::bindings::codegen::Bindings::RangeBinding::RangeMethods;
18use crate::dom::bindings::codegen::Bindings::SelectionBinding::SelectionMethods;
19use crate::dom::bindings::inheritance::{ElementTypeId, HTMLElementTypeId, NodeTypeId};
20use crate::dom::bindings::root::DomRoot;
21use crate::dom::characterdata::CharacterData;
22use crate::dom::element::Element;
23use crate::dom::html::htmlbrelement::HTMLBRElement;
24use crate::dom::html::htmlelement::HTMLElement;
25use crate::dom::html::htmlimageelement::HTMLImageElement;
26use crate::dom::html::htmllielement::HTMLLIElement;
27use crate::dom::node::{Node, NodeTraits, ShadowIncluding};
28use crate::dom::range::Range;
29use crate::dom::selection::Selection;
30use crate::dom::text::Text;
31use crate::script_runtime::CanGc;
32
33impl Text {
34 /// <https://dom.spec.whatwg.org/#concept-cd-data>
35 fn data(&self) -> Ref<'_, String> {
36 self.upcast::<CharacterData>().data()
37 }
38
39 /// <https://w3c.github.io/editing/docs/execCommand/#whitespace-node>
40 fn is_whitespace_node(&self) -> bool {
41 // > A whitespace node is either a Text node whose data is the empty string;
42 let data = self.data();
43 if data.is_empty() {
44 return true;
45 }
46 // > or a Text node whose data consists only of one or more tabs (0x0009), line feeds (0x000A),
47 // > carriage returns (0x000D), and/or spaces (0x0020),
48 // > and whose parent is an Element whose resolved value for "white-space" is "normal" or "nowrap";
49 let Some(parent) = self.upcast::<Node>().GetParentElement() else {
50 return false;
51 };
52 // TODO: Optimize the below to only do a traversal once and in the match handle the expected collapse value
53 let Some(style) = parent.style() else {
54 return false;
55 };
56 let white_space_collapse = style.get_inherited_text().white_space_collapse;
57 if data
58 .bytes()
59 .all(|byte| matches!(byte, b'\t' | b'\n' | b'\r' | b' ')) &&
60 // Note that for "normal" and "nowrap", the longhand "white-space-collapse: collapse" applies
61 // https://www.w3.org/TR/css-text-4/#white-space-property
62 white_space_collapse == WhiteSpaceCollapse::Collapse
63 {
64 return true;
65 }
66 // > or a Text node whose data consists only of one or more tabs (0x0009), carriage returns (0x000D),
67 // > and/or spaces (0x0020), and whose parent is an Element whose resolved value for "white-space" is "pre-line".
68 data.bytes()
69 .all(|byte| matches!(byte, b'\t' | b'\r' | b' ')) &&
70 // Note that for "pre-line", the longhand "white-space-collapse: preserve-breaks" applies
71 // https://www.w3.org/TR/css-text-4/#white-space-property
72 white_space_collapse == WhiteSpaceCollapse::PreserveBreaks
73 }
74
75 /// <https://w3c.github.io/editing/docs/execCommand/#collapsed-whitespace-node>
76 fn is_collapsed_whitespace_node(&self) -> bool {
77 // Step 1. If node is not a whitespace node, return false.
78 if !self.is_whitespace_node() {
79 return false;
80 }
81 // Step 2. If node's data is the empty string, return true.
82 if self.data().is_empty() {
83 return true;
84 }
85 // Step 3. Let ancestor be node's parent.
86 let node = self.upcast::<Node>();
87 let Some(ancestor) = node.GetParentNode() else {
88 // Step 4. If ancestor is null, return true.
89 return true;
90 };
91 let mut resolved_ancestor = ancestor.clone();
92 for parent in ancestor.ancestors() {
93 // Step 5. If the "display" property of some ancestor of node has resolved value "none", return true.
94 if parent.is_display_none() {
95 return true;
96 }
97 // Step 6. While ancestor is not a block node and its parent is not null, set ancestor to its parent.
98 //
99 // Note that the spec is written as "while not". Since this is the end-condition, we need to invert
100 // the condition to decide when to stop.
101 if parent.is_block_node() {
102 break;
103 }
104 resolved_ancestor = parent;
105 }
106 // Step 7. Let reference be node.
107 // Step 8. While reference is a descendant of ancestor:
108 // Step 8.1. Let reference be the node before it in tree order.
109 for reference in node.preceding_nodes(&resolved_ancestor) {
110 // Step 8.2. If reference is a block node or a br, return true.
111 if reference.is_block_node() || reference.is::<HTMLBRElement>() {
112 return true;
113 }
114 // Step 8.3. If reference is a Text node that is not a whitespace node, or is an img, break from this loop.
115 if reference
116 .downcast::<Text>()
117 .is_some_and(|text| !text.is_whitespace_node()) ||
118 reference.is::<HTMLImageElement>()
119 {
120 break;
121 }
122 }
123 // Step 9. Let reference be node.
124 // Step 10. While reference is a descendant of ancestor:
125 // Step 10.1. Let reference be the node after it in tree order, or null if there is no such node.
126 for reference in node.following_nodes(&resolved_ancestor) {
127 // Step 10.2. If reference is a block node or a br, return true.
128 if reference.is_block_node() || reference.is::<HTMLBRElement>() {
129 return true;
130 }
131 // Step 10.3. If reference is a Text node that is not a whitespace node, or is an img, break from this loop.
132 if reference
133 .downcast::<Text>()
134 .is_some_and(|text| !text.is_whitespace_node()) ||
135 reference.is::<HTMLImageElement>()
136 {
137 break;
138 }
139 }
140 // Step 11. Return false.
141 false
142 }
143
144 /// Part of <https://w3c.github.io/editing/docs/execCommand/#canonicalize-whitespace>
145 /// and deduplicated here, since we need to do this for both start and end nodes
146 fn has_whitespace_and_has_parent_with_whitespace_preserve(
147 &self,
148 offset: u32,
149 space_characters: &'static [&'static char],
150 ) -> bool {
151 // if node is a Text node and its parent's resolved value for "white-space" is neither "pre" nor "pre-wrap"
152 // and start offset is not zero and the (start offset − 1)st code unit of start node's data is a space (0x0020) or
153 // non-breaking space (0x00A0)
154 let has_preserve_space = self
155 .upcast::<Node>()
156 .GetParentNode()
157 .and_then(|parent| parent.style())
158 .is_some_and(|style| {
159 // Note that for "pre" and "pre-wrap", the longhand "white-space-collapse: preserve" applies
160 // https://www.w3.org/TR/css-text-4/#white-space-property
161 style.get_inherited_text().white_space_collapse != WhiteSpaceCollapse::Preserve
162 });
163 let has_space_character = self
164 .data()
165 .chars()
166 .nth(offset as usize)
167 .is_some_and(|c| space_characters.contains(&&c));
168 has_preserve_space && has_space_character
169 }
170}
171
172impl HTMLBRElement {
173 /// <https://w3c.github.io/editing/docs/execCommand/#extraneous-line-break>
174 fn is_extraneous_line_break(&self) -> bool {
175 let node = self.upcast::<Node>();
176 // > An extraneous line break is a br that has no visual effect, in that removing it from the DOM would not change layout,
177 // except that a br that is the sole child of an li is not extraneous.
178 if node
179 .GetParentNode()
180 .filter(|parent| parent.is::<HTMLLIElement>())
181 .is_some_and(|li| li.children_count() == 1)
182 {
183 return false;
184 }
185 // TODO: Figure out what this actually makes it have no visual effect
186 !node.is_block_node()
187 }
188}
189
190impl Node {
191 /// <https://w3c.github.io/editing/docs/execCommand/#in-the-same-editing-host>
192 fn same_editing_host(&self, other: &Node) -> bool {
193 // > 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.
194 self.editing_host_of()
195 .is_some_and(|editing_host| other.editing_host_of() == Some(editing_host))
196 }
197
198 /// <https://w3c.github.io/editing/docs/execCommand/#block-node>
199 fn is_block_node(&self) -> bool {
200 // > A block node is either an Element whose "display" property does not have resolved value "inline" or "inline-block" or "inline-table" or "none",
201 if self.downcast::<Element>().is_some_and(|el| {
202 !el.style()
203 .is_none_or(|style| style.get_box().display.outside() == DisplayOutside::Inline)
204 }) {
205 return true;
206 }
207 // > or a document, or a DocumentFragment.
208 matches!(
209 self.type_id(),
210 NodeTypeId::Document(_) | NodeTypeId::DocumentFragment(_)
211 )
212 }
213
214 /// <https://w3c.github.io/editing/docs/execCommand/#visible>
215 fn is_visible(&self) -> bool {
216 for parent in self.inclusive_ancestors(ShadowIncluding::Yes) {
217 // > excluding any node with an inclusive ancestor Element whose "display" property has resolved value "none".
218 if parent.is_display_none() {
219 return false;
220 }
221 }
222 // > Something is visible if it is a node that either is a block node,
223 if self.is_block_node() {
224 return true;
225 }
226 // > or a Text node that is not a collapsed whitespace node,
227 if self
228 .downcast::<Text>()
229 .is_some_and(|text| !text.is_collapsed_whitespace_node())
230 {
231 return true;
232 }
233 // > or an img, or a br that is not an extraneous line break, or any node with a visible descendant;
234 if self.is::<HTMLImageElement>() {
235 return true;
236 }
237 if self
238 .downcast::<HTMLBRElement>()
239 .is_some_and(|br| !br.is_extraneous_line_break())
240 {
241 return true;
242 }
243 for child in self.children() {
244 if child.is_visible() {
245 return true;
246 }
247 }
248 false
249 }
250
251 /// <https://w3c.github.io/editing/docs/execCommand/#block-start-point>
252 fn is_block_start_point(&self, offset: usize) -> bool {
253 // > A boundary point (node, offset) is a block start point if either node's parent is null and offset is zero;
254 if offset == 0 {
255 return self.GetParentNode().is_none();
256 }
257 // > or node has a child with index offset − 1, and that child is either a visible block node or a visible br.
258 self.children().nth(offset - 1).is_some_and(|child| {
259 child.is_visible() && (child.is_block_node() || child.is::<HTMLBRElement>())
260 })
261 }
262
263 /// <https://w3c.github.io/editing/docs/execCommand/#block-end-point>
264 fn is_block_end_point(&self, offset: u32) -> bool {
265 // > A boundary point (node, offset) is a block end point if either node's parent is null and offset is node's length;
266 if self.GetParentNode().is_none() && offset == self.len() {
267 return true;
268 }
269 // > or node has a child with index offset, and that child is a visible block node.
270 self.children()
271 .nth(offset as usize)
272 .is_some_and(|child| child.is_visible() && child.is_block_node())
273 }
274
275 /// <https://w3c.github.io/editing/docs/execCommand/#block-boundary-point>
276 fn is_block_boundary_point(&self, offset: u32) -> bool {
277 // > A boundary point is a block boundary point if it is either a block start point or a block end point.
278 self.is_block_start_point(offset as usize) || self.is_block_end_point(offset)
279 }
280
281 /// <https://w3c.github.io/editing/docs/execCommand/#follows-a-line-break>
282 fn follows_a_line_break(&self) -> bool {
283 // Step 1. Let offset be zero.
284 let mut offset = 0;
285 // Step 2. While (node, offset) is not a block boundary point:
286 let mut node = DomRoot::from_ref(self);
287 while !node.is_block_boundary_point(offset) {
288 // Step 2.2. If offset is zero or node has no children, set offset to node's index, then set node to its parent.
289 if offset == 0 || node.children_count() == 0 {
290 offset = node.index();
291 node = match node.GetParentNode() {
292 None => return false,
293 Some(node) => node,
294 };
295 continue;
296 }
297 // Step 2.1. If node has a visible child with index offset minus one, return false.
298 let child = node.children().nth(offset as usize - 1);
299 let Some(child) = child else {
300 return false;
301 };
302 if child.is_visible() {
303 return false;
304 }
305 // Step 2.3. Otherwise, set node to its child with index offset minus one, then set offset to node's length.
306 node = child;
307 offset = node.len();
308 }
309 // Step 3. Return true.
310 true
311 }
312
313 /// <https://w3c.github.io/editing/docs/execCommand/#precedes-a-line-break>
314 fn precedes_a_line_break(&self) -> bool {
315 let mut node = DomRoot::from_ref(self);
316 // Step 1. Let offset be node's length.
317 let mut offset = node.len();
318 // Step 2. While (node, offset) is not a block boundary point:
319 while !node.is_block_boundary_point(offset) {
320 // Step 2.1. If node has a visible child with index offset, return false.
321 if node
322 .children()
323 .nth(offset as usize)
324 .is_some_and(|child| child.is_visible())
325 {
326 return false;
327 }
328 // 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.
329 if offset == node.len() || node.children_count() == 0 {
330 offset = 1 + node.index();
331 node = match node.GetParentNode() {
332 None => return false,
333 Some(node) => node,
334 };
335 continue;
336 }
337 // Step 2.3. Otherwise, set node to its child with index offset and set offset to zero.
338 let child = node.children().nth(offset as usize);
339 node = match child {
340 None => return false,
341 Some(child) => child,
342 };
343 offset = 0;
344 }
345 // Step 3. Return true.
346 true
347 }
348
349 /// <https://w3c.github.io/editing/docs/execCommand/#canonical-space-sequence>
350 fn canonical_space_sequence(
351 n: usize,
352 non_breaking_start: bool,
353 non_breaking_end: bool,
354 ) -> String {
355 let mut n = n;
356 // Step 1. If n is zero, return the empty string.
357 if n == 0 {
358 return String::new();
359 }
360 // Step 2. If n is one and both non-breaking start and non-breaking end are false, return a single space (U+0020).
361 if n == 1 {
362 if !non_breaking_start && !non_breaking_end {
363 return "\u{0020}".to_owned();
364 }
365 // Step 3. If n is one, return a single non-breaking space (U+00A0).
366 return "\u{00A0}".to_owned();
367 }
368 // Step 4. Let buffer be the empty string.
369 let mut buffer = String::new();
370 // Step 5. If non-breaking start is true, let repeated pair be U+00A0 U+0020. Otherwise, let it be U+0020 U+00A0.
371 let repeated_pair = if non_breaking_start {
372 "\u{00A0}\u{0020}"
373 } else {
374 "\u{0020}\u{00A0}"
375 };
376 // Step 6. While n is greater than three, append repeated pair to buffer and subtract two from n.
377 while n > 3 {
378 buffer.push_str(repeated_pair);
379 n -= 2;
380 }
381 // Step 7. If n is three, append a three-code unit string to buffer depending on non-breaking start and non-breaking end:
382 if n == 3 {
383 buffer.push_str(match (non_breaking_start, non_breaking_end) {
384 (false, false) => "\u{0020}\u{00A0}\u{0020}",
385 (true, false) => "\u{00A0}\u{00A0}\u{0020}",
386 (false, true) => "\u{0020}\u{00A0}\u{00A0}",
387 (true, true) => "\u{00A0}\u{0020}\u{00A0}",
388 });
389 } else {
390 // Step 8. Otherwise, append a two-code unit string to buffer depending on non-breaking start and non-breaking end:
391 buffer.push_str(match (non_breaking_start, non_breaking_end) {
392 (false, false) | (true, false) => "\u{00A0}\u{0020}",
393 (false, true) => "\u{0020}\u{00A0}",
394 (true, true) => "\u{00A0}\u{00A0}",
395 });
396 }
397 // Step 9. Return buffer.
398 buffer
399 }
400
401 /// <https://w3c.github.io/editing/docs/execCommand/#canonicalize-whitespace>
402 fn canonicalize_whitespace(&self, offset: u32, fix_collapsed_space: bool) {
403 // Step 1. If node is neither editable nor an editing host, abort these steps.
404 if !self.is_editable_or_editing_host() {
405 return;
406 }
407 // Step 2. Let start node equal node and let start offset equal offset.
408 let mut start_node = DomRoot::from_ref(self);
409 let mut start_offset = offset;
410 // Step 3. Repeat the following steps:
411 loop {
412 // Step 3.1. If start node has a child in the same editing host with index start offset minus one,
413 // set start node to that child, then set start offset to start node's length.
414 if start_offset > 0 {
415 let child = start_node.children().nth(start_offset as usize - 1);
416 if let Some(child) = child {
417 if start_node.same_editing_host(&child) {
418 start_node = child;
419 start_offset = start_node.len();
420 continue;
421 }
422 };
423 }
424 // Step 3.2. Otherwise, if start offset is zero and start node does not follow a line break
425 // and start node's parent is in the same editing host, set start offset to start node's index,
426 // then set start node to its parent.
427 if start_offset == 0 && !start_node.follows_a_line_break() {
428 if let Some(parent) = start_node.GetParentNode() {
429 if parent.same_editing_host(&start_node) {
430 start_offset = start_node.index();
431 start_node = parent;
432 }
433 }
434 }
435 // Step 3.3. Otherwise, if start node is a Text node and its parent's resolved
436 // value for "white-space" is neither "pre" nor "pre-wrap" and start offset is not zero
437 // and the (start offset − 1)st code unit of start node's data is a space (0x0020) or
438 // non-breaking space (0x00A0), subtract one from start offset.
439 if start_offset != 0 &&
440 start_node.downcast::<Text>().is_some_and(|text| {
441 text.has_whitespace_and_has_parent_with_whitespace_preserve(
442 start_offset - 1,
443 &[&'\u{0020}', &'\u{00A0}'],
444 )
445 })
446 {
447 start_offset -= 1;
448 }
449 // Step 3.4. Otherwise, break from this loop.
450 break;
451 }
452 // Step 4. Let end node equal start node and end offset equal start offset.
453 let mut end_node = start_node.clone();
454 let mut end_offset = start_offset;
455 // Step 5. Let length equal zero.
456 let mut length = 0;
457 // Step 6. Let collapse spaces be true if start offset is zero and start node follows a line break, otherwise false.
458 let mut collapse_spaces = start_offset == 0 && start_node.follows_a_line_break();
459 // Step 7. Repeat the following steps:
460 loop {
461 // Step 7.1. If end node has a child in the same editing host with index end offset,
462 // set end node to that child, then set end offset to zero.
463 if let Some(child) = end_node.children().nth(end_offset as usize) {
464 if child.same_editing_host(&end_node) {
465 end_node = child;
466 end_offset = 0;
467 continue;
468 }
469 }
470 // Step 7.2. Otherwise, if end offset is end node's length
471 // and end node does not precede a line break
472 // and end node's parent is in the same editing host,
473 // set end offset to one plus end node's index, then set end node to its parent.
474 if end_offset == end_node.len() && !end_node.precedes_a_line_break() {
475 if let Some(parent) = end_node.GetParentNode() {
476 if parent.same_editing_host(&end_node) {
477 end_offset = 1 + end_node.index();
478 end_node = parent;
479 }
480 }
481 continue;
482 }
483 // Step 7.3. Otherwise, if end node is a Text node and its parent's resolved value for "white-space"
484 // is neither "pre" nor "pre-wrap"
485 // and end offset is not end node's length and the end offsetth code unit of end node's data
486 // is a space (0x0020) or non-breaking space (0x00A0):
487 if let Some(text) = end_node.downcast::<Text>() {
488 if text.has_whitespace_and_has_parent_with_whitespace_preserve(
489 end_offset,
490 &[&'\u{0020}', &'\u{00A0}'],
491 ) {
492 // Step 7.3.1. If fix collapsed space is true, and collapse spaces is true,
493 // and the end offsetth code unit of end node's data is a space (0x0020):
494 // call deleteData(end offset, 1) on end node, then continue this loop from the beginning.
495 let has_space_at_offset = text
496 .data()
497 .chars()
498 .nth(end_offset as usize)
499 .is_some_and(|c| c == '\u{0020}');
500 if fix_collapsed_space && collapse_spaces && has_space_at_offset {
501 if text
502 .upcast::<CharacterData>()
503 .DeleteData(end_offset, 1)
504 .is_err()
505 {
506 unreachable!("Invalid deletion for character at end offset");
507 }
508 continue;
509 }
510 // Step 7.3.2. Set collapse spaces to true if the end offsetth code unit of
511 // end node's data is a space (0x0020), false otherwise.
512 collapse_spaces = has_space_at_offset;
513 // Step 7.3.3. Add one to end offset.
514 end_offset += 1;
515 // Step 7.3.4. Add one to length.
516 length += 1;
517 continue;
518 }
519 }
520 // Step 7.4. Otherwise, break from this loop.
521 break;
522 }
523 // Step 8. If fix collapsed space is true, then while (start node, start offset)
524 // is before (end node, end offset):
525 if fix_collapsed_space {
526 while bp_position(&start_node, start_offset, &end_node, end_offset) ==
527 Some(Ordering::Less)
528 {
529 // Step 8.1. If end node has a child in the same editing host with index end offset − 1,
530 // set end node to that child, then set end offset to end node's length.
531 if end_offset > 0 {
532 if let Some(child) = end_node.children().nth(end_offset as usize - 1) {
533 if child.same_editing_host(&end_node) {
534 end_node = child;
535 end_offset = end_node.len();
536 continue;
537 }
538 }
539 }
540 // Step 8.2. Otherwise, if end offset is zero and end node's parent is in the same editing host,
541 // set end offset to end node's index, then set end node to its parent.
542 if let Some(parent) = end_node.GetParentNode() {
543 if end_offset == 0 && parent.same_editing_host(&end_node) {
544 end_offset = end_node.index();
545 end_node = parent;
546 continue;
547 }
548 }
549 // Step 8.3. Otherwise, if end node is a Text node and its parent's resolved value for "white-space"
550 // is neither "pre" nor "pre-wrap"
551 // and end offset is end node's length and the last code unit of end node's data
552 // is a space (0x0020) and end node precedes a line break:
553 if let Some(text) = end_node.downcast::<Text>() {
554 if text.has_whitespace_and_has_parent_with_whitespace_preserve(
555 text.data().len() as u32,
556 &[&'\u{0020}'],
557 ) && end_node.precedes_a_line_break()
558 {
559 // Step 8.3.1. Subtract one from end offset.
560 end_offset -= 1;
561 // Step 8.3.2. Subtract one from length.
562 length -= 1;
563 // Step 8.3.3. Call deleteData(end offset, 1) on end node.
564 if text
565 .upcast::<CharacterData>()
566 .DeleteData(end_offset, 1)
567 .is_err()
568 {
569 unreachable!("Invalid deletion for character at end offset");
570 }
571 continue;
572 }
573 }
574 // Step 8.4. Otherwise, break from this loop.
575 break;
576 }
577 }
578 // Step 9. Let replacement whitespace be the canonical space sequence of length length.
579 // non-breaking start is true if start offset is zero and start node follows a line break, and false otherwise.
580 // non-breaking end is true if end offset is end node's length and end node precedes a line break, and false otherwise.
581 let replacement_whitespace = Node::canonical_space_sequence(
582 length,
583 start_offset == 0 && start_node.follows_a_line_break(),
584 end_offset == end_node.len() && end_node.precedes_a_line_break(),
585 );
586 let mut replacement_whitespace_chars = replacement_whitespace.chars();
587 // Step 10. While (start node, start offset) is before (end node, end offset):
588 while bp_position(&start_node, start_offset, &end_node, end_offset) == Some(Ordering::Less)
589 {
590 // 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.
591 if let Some(child) = start_node.children().nth(start_offset as usize) {
592 start_node = child;
593 start_offset = 0;
594 continue;
595 }
596 // Step 10.2. Otherwise, if start node is not a Text node or if start offset is start node's length,
597 // set start offset to one plus start node's index, then set start node to its parent.
598 let start_node_as_text = start_node.downcast::<Text>();
599 if start_node_as_text.is_none() || start_offset == start_node.len() {
600 start_offset = 1 + start_node.index();
601 start_node = match start_node.GetParentNode() {
602 None => break,
603 Some(node) => node,
604 };
605 continue;
606 }
607 let start_node_as_text =
608 start_node_as_text.expect("Already verified none in previous statement");
609 // Step 10.3. Otherwise:
610 // Step 10.3.1. Remove the first code unit from replacement whitespace, and let element be that code unit.
611 if let Some(element) = replacement_whitespace_chars.next() {
612 // Step 10.3.2. If element is not the same as the start offsetth code unit of start node's data:
613 if start_node_as_text.data().chars().nth(start_offset as usize) != Some(element) {
614 let character_data = start_node_as_text.upcast::<CharacterData>();
615 // Step 10.3.2.1. Call insertData(start offset, element) on start node.
616 if character_data
617 .InsertData(start_offset, element.to_string().into())
618 .is_err()
619 {
620 unreachable!("Invalid insertion for character at start offset");
621 }
622 // Step 10.3.2.2. Call deleteData(start offset + 1, 1) on start node.
623 if character_data.DeleteData(start_offset + 1, 1).is_err() {
624 unreachable!("Invalid deletion for character at start offset + 1");
625 }
626 }
627 }
628 // Step 10.3.3. Add one to start offset.
629 start_offset += 1;
630 }
631 }
632}
633
634pub(crate) trait ContentEditableRange {
635 fn handle_focus_state_for_contenteditable(&self, can_gc: CanGc);
636}
637
638impl ContentEditableRange for HTMLElement {
639 /// There is no specification for this implementation. Instead, it is
640 /// reverse-engineered based on the WPT test
641 /// /selection/contenteditable/initial-selection-on-focus.tentative.html
642 fn handle_focus_state_for_contenteditable(&self, can_gc: CanGc) {
643 if !self.is_editing_host() {
644 return;
645 }
646 let document = self.owner_document();
647 let Some(selection) = document.GetSelection(can_gc) else {
648 return;
649 };
650 let range = self
651 .upcast::<Element>()
652 .ensure_contenteditable_selection_range(&document, can_gc);
653 // If the current range is already associated with this contenteditable
654 // element, then we shouldn't do anything. This is important when focus
655 // is lost and regained, but selection was changed beforehand. In that
656 // case, we should maintain the selection as it were, by not creating
657 // a new range.
658 if selection
659 .active_range()
660 .is_some_and(|active| active == range)
661 {
662 return;
663 }
664 let node = self.upcast::<Node>();
665 let mut selected_node = DomRoot::from_ref(node);
666 let mut previous_eligible_node = DomRoot::from_ref(node);
667 let mut previous_node = DomRoot::from_ref(node);
668 let mut selected_offset = 0;
669 for child in node.traverse_preorder(ShadowIncluding::Yes) {
670 if let Some(text) = child.downcast::<Text>() {
671 // Note that to consider it whitespace, it needs to take more
672 // into account than simply "it has a non-whitespace" character.
673 // Therefore, we need to first check if it is not a whitespace
674 // node and only then can we find what the relevant character is.
675 if !text.is_whitespace_node() {
676 // A node with "white-space: pre" set must select its first
677 // character, regardless if that's a whitespace character or not.
678 let is_pre_formatted_text_node = child
679 .GetParentElement()
680 .and_then(|parent| parent.style())
681 .is_some_and(|style| {
682 style.get_inherited_text().white_space_collapse ==
683 WhiteSpaceCollapse::Preserve
684 });
685 if !is_pre_formatted_text_node {
686 // If it isn't pre-formatted, then we should instead select the
687 // first non-whitespace character.
688 selected_offset = text
689 .data()
690 .find(|c: char| !c.is_whitespace())
691 .unwrap_or_default() as u32;
692 }
693 selected_node = child;
694 break;
695 }
696 }
697 // For <input>, <textarea>, <hr> and <br> elements, we should select the previous
698 // node, regardless if it was a block node or not
699 if matches!(
700 child.type_id(),
701 NodeTypeId::Element(ElementTypeId::HTMLElement(
702 HTMLElementTypeId::HTMLInputElement,
703 )) | NodeTypeId::Element(ElementTypeId::HTMLElement(
704 HTMLElementTypeId::HTMLTextAreaElement,
705 )) | NodeTypeId::Element(ElementTypeId::HTMLElement(
706 HTMLElementTypeId::HTMLHRElement,
707 )) | NodeTypeId::Element(ElementTypeId::HTMLElement(
708 HTMLElementTypeId::HTMLBRElement,
709 ))
710 ) {
711 selected_node = previous_node;
712 break;
713 }
714 // When we encounter a non-contenteditable element, we should select the previous
715 // eligible node
716 if child
717 .downcast::<HTMLElement>()
718 .is_some_and(|el| el.ContentEditable().str() == "false")
719 {
720 selected_node = previous_eligible_node;
721 break;
722 }
723 // We can only select block nodes as eligible nodes for the case of non-conenteditable
724 // nodes
725 if child.is_block_node() {
726 previous_eligible_node = child.clone();
727 }
728 previous_node = child;
729 }
730 range.set_start(&selected_node, selected_offset);
731 range.set_end(&selected_node, selected_offset);
732 selection.AddRange(&range);
733 }
734}
735
736pub(crate) trait SelectionExecCommandSupport {
737 fn delete_the_selection(&self, active_range: &Range);
738}
739
740impl SelectionExecCommandSupport for Selection {
741 /// <https://w3c.github.io/editing/docs/execCommand/#delete-the-selection>
742 fn delete_the_selection(&self, active_range: &Range) {
743 // Step 1. If the active range is null, abort these steps and do nothing.
744 //
745 // Always passed in as argument
746
747 // Step 2. Canonicalize whitespace at the active range's start.
748 active_range
749 .start_container()
750 .canonicalize_whitespace(active_range.start_offset(), true);
751
752 // Step 3. Canonicalize whitespace at the active range's end.
753 active_range
754 .end_container()
755 .canonicalize_whitespace(active_range.end_offset(), true);
756
757 // Step 4. Let (start node, start offset) be the last equivalent point for the active range's start.
758 // TODO
759
760 // Step 5. Let (end node, end offset) be the first equivalent point for the active range's end.
761 // TODO
762
763 // Step 6. If (end node, end offset) is not after (start node, start offset):
764 // TODO
765
766 // Step 7. If start node is a Text node and start offset is 0, set start offset to the index of start node,
767 // then set start node to its parent.
768 // TODO
769
770 // 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,
771 // then set end node to its parent.
772 // TODO
773
774 // Step 9. Call collapse(start node, start offset) on the context object's selection.
775 // TODO
776
777 // Step 10. Call extend(end node, end offset) on the context object's selection.
778 // TODO
779
780 // Step 11.
781 //
782 // This step does not exist in the spec
783
784 // Step 12. Let start block be the active range's start node.
785 // TODO
786
787 // Step 13. While start block's parent is in the same editing host and start block is an inline node, set start block to its parent.
788 // TODO
789
790 // Step 14. If start block is neither a block node nor an editing host, or "span" is not an allowed child of start block,
791 // or start block is a td or th, set start block to null.
792 // TODO
793
794 // Step 15. Let end block be the active range's end node.
795 // TODO
796
797 // 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.
798 // TODO
799
800 // Step 17. If end block is neither a block node nor an editing host, or "span" is not an allowed child of end block,
801 // or end block is a td or th, set end block to null.
802 // TODO
803
804 // Step 18.
805 //
806 // This step does not exist in the spec
807
808 // Step 19. Record current states and values, and let overrides be the result.
809 // TODO
810
811 // Step 20.
812 //
813 // This step does not exist in the spec
814
815 // Step 21. If start node and end node are the same, and start node is an editable Text node:
816 //
817 // As per the spec:
818 // > NOTE: This whole piece of the algorithm is based on deleteContents() in DOM Range, copy-pasted and then adjusted to fit.
819 let _ = active_range.DeleteContents();
820
821 // Step 22. If start node is an editable Text node, call deleteData() on it, with start offset as
822 // the first argument and (length of start node − start offset) as the second argument.
823 // TODO
824
825 // Step 23. Let node list be a list of nodes, initially empty.
826 // TODO
827
828 // Step 24. For each node contained in the active range, append node to node list if the
829 // last member of node list (if any) is not an ancestor of node; node is editable;
830 // and node is not a thead, tbody, tfoot, tr, th, or td.
831 // TODO
832
833 // Step 25. For each node in node list:
834 // TODO
835
836 // Step 26. If end node is an editable Text node, call deleteData(0, end offset) on it.
837 // TODO
838
839 // Step 27. Canonicalize whitespace at the active range's start, with fix collapsed space false.
840 // TODO
841
842 // Step 28. Canonicalize whitespace at the active range's end, with fix collapsed space false.
843 // TODO
844
845 // Step 29.
846 //
847 // This step does not exist in the spec
848
849 // Step 30. If block merging is false, or start block or end block is null, or start block is not
850 // in the same editing host as end block, or start block and end block are the same:
851 // TODO
852
853 // Step 31. If start block has one child, which is a collapsed block prop, remove its child from it.
854 // TODO
855
856 // Step 32. If start block is an ancestor of end block:
857 // TODO
858
859 // Step 33. Otherwise, if start block is a descendant of end block:
860 // TODO
861
862 // Step 34. Otherwise:
863 // TODO
864
865 // Step 35.
866 //
867 // This step does not exist in the spec
868
869 // Step 36. Let ancestor be start block.
870 // TODO
871
872 // Step 37. While ancestor has an inclusive ancestor ol in the same editing host whose nextSibling is
873 // also an ol in the same editing host, or an inclusive ancestor ul in the same editing host whose nextSibling
874 // is also a ul in the same editing host:
875 // TODO
876
877 // Step 38. Restore the values from values.
878 // TODO
879
880 // Step 39. If start block has no children, call createElement("br") on the context object and
881 // append the result as the last child of start block.
882 // TODO
883
884 // Step 40. Remove extraneous line breaks at the end of start block.
885 // TODO
886
887 // Step 41. Restore states and values from overrides.
888 // TODO
889 }
890}