script/dom/execcommand/contenteditable/node.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 js::context::JSContext;
10use script_bindings::inheritance::Castable;
11use style::values::specified::box_::DisplayOutside;
12
13use crate::dom::abstractrange::bp_position;
14use crate::dom::bindings::codegen::Bindings::CharacterDataBinding::CharacterDataMethods;
15use crate::dom::bindings::codegen::Bindings::DocumentBinding::DocumentMethods;
16use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods;
17use crate::dom::bindings::error::Fallible;
18use crate::dom::bindings::inheritance::NodeTypeId;
19use crate::dom::bindings::root::{DomRoot, DomSlice};
20use crate::dom::bindings::str::DOMString;
21use crate::dom::characterdata::CharacterData;
22use crate::dom::element::Element;
23use crate::dom::execcommand::basecommand::{CommandName, CssPropertyName};
24use crate::dom::html::htmlanchorelement::HTMLAnchorElement;
25use crate::dom::html::htmlbrelement::HTMLBRElement;
26use crate::dom::html::htmlelement::HTMLElement;
27use crate::dom::html::htmlimageelement::HTMLImageElement;
28use crate::dom::html::htmllielement::HTMLLIElement;
29use crate::dom::node::{Node, NodeTraits, ShadowIncluding};
30use crate::dom::text::Text;
31use crate::script_runtime::CanGc;
32
33pub(crate) enum NodeOrString {
34 String(String),
35 Node(DomRoot<Node>),
36}
37
38impl NodeOrString {
39 fn name(&self) -> &str {
40 match self {
41 NodeOrString::String(str_) => str_,
42 NodeOrString::Node(node) => node
43 .downcast::<Element>()
44 .map(|element| element.local_name().as_ref())
45 .unwrap_or_default(),
46 }
47 }
48
49 fn as_node(&self) -> Option<DomRoot<Node>> {
50 match self {
51 NodeOrString::String(_) => None,
52 NodeOrString::Node(node) => Some(node.clone()),
53 }
54 }
55}
56
57/// <https://w3c.github.io/editing/docs/execCommand/#prohibited-paragraph-child-name>
58const PROHIBITED_PARAGRAPH_CHILD_NAMES: [&str; 47] = [
59 "address",
60 "article",
61 "aside",
62 "blockquote",
63 "caption",
64 "center",
65 "col",
66 "colgroup",
67 "dd",
68 "details",
69 "dir",
70 "div",
71 "dl",
72 "dt",
73 "fieldset",
74 "figcaption",
75 "figure",
76 "footer",
77 "form",
78 "h1",
79 "h2",
80 "h3",
81 "h4",
82 "h5",
83 "h6",
84 "header",
85 "hgroup",
86 "hr",
87 "li",
88 "listing",
89 "menu",
90 "nav",
91 "ol",
92 "p",
93 "plaintext",
94 "pre",
95 "section",
96 "summary",
97 "table",
98 "tbody",
99 "td",
100 "tfoot",
101 "th",
102 "thead",
103 "tr",
104 "ul",
105 "xmp",
106];
107/// <https://w3c.github.io/editing/docs/execCommand/#name-of-an-element-with-inline-contents>
108const NAME_OF_AN_ELEMENT_WITH_INLINE_CONTENTS: [&str; 43] = [
109 "a", "abbr", "b", "bdi", "bdo", "cite", "code", "dfn", "em", "h1", "h2", "h3", "h4", "h5",
110 "h6", "i", "kbd", "mark", "p", "pre", "q", "rp", "rt", "ruby", "s", "samp", "small", "span",
111 "strong", "sub", "sup", "u", "var", "acronym", "listing", "strike", "xmp", "big", "blink",
112 "font", "marquee", "nobr", "tt",
113];
114
115/// <https://w3c.github.io/editing/docs/execCommand/#element-with-inline-contents>
116fn is_element_with_inline_contents(element: &Node) -> bool {
117 // > An element with inline contents is an HTML element whose local name is a name of an element with inline contents.
118 let Some(html_element) = element.downcast::<HTMLElement>() else {
119 return false;
120 };
121 NAME_OF_AN_ELEMENT_WITH_INLINE_CONTENTS.contains(&html_element.local_name())
122}
123
124/// <https://w3c.github.io/editing/docs/execCommand/#preserving-ranges>
125pub(crate) fn move_preserving_ranges<Move>(cx: &mut JSContext, node: &Node, mut move_: Move)
126where
127 Move: FnMut(&mut JSContext) -> Fallible<DomRoot<Node>>,
128{
129 // Step 1. Let node be the moved node, old parent and old index be the old parent
130 // (which may be null) and index, and new parent and new index be the new parent and index.
131 let old_parent = node.GetParentNode();
132 let old_index = node.index();
133
134 if move_(cx).is_err() {
135 unreachable!("Must always be able to move");
136 }
137
138 let Some(selection) = node.owner_document().GetSelection(cx) else {
139 return;
140 };
141 let Some(active_range) = selection.active_range() else {
142 return;
143 };
144
145 let new_parent = node.GetParentNode().expect("Must always have a new parent");
146 let new_index = node.index();
147
148 let mut start_node = active_range.start_container();
149 let mut start_offset = active_range.start_offset();
150 let mut end_node = active_range.end_container();
151 let mut end_offset = active_range.end_offset();
152
153 // Step 2. If a boundary point's node is the same as or a descendant of node, leave it unchanged, so it moves to the new location.
154 //
155 // From the spec:
156 // > This is actually implicit, but I state it anyway for completeness.
157
158 // Step 3. If a boundary point's node is new parent and its offset is greater than new index, add one to its offset.
159 if start_node == new_parent && start_offset > new_index {
160 start_offset += 1;
161 }
162 if end_node == new_parent && end_offset > new_index {
163 end_offset += 1;
164 }
165
166 if let Some(old_parent) = old_parent {
167 // Step 4. If a boundary point's node is old parent and its offset is old index or old index + 1,
168 // set its node to new parent and add new index − old index to its offset.
169 if start_node == old_parent && (start_offset == old_index || start_offset == old_index + 1)
170 {
171 start_node = new_parent.clone();
172 start_offset += new_index;
173 start_offset -= old_index;
174 }
175 if end_node == old_parent && (end_offset == old_index || end_offset == old_index + 1) {
176 end_node = new_parent;
177 end_offset += new_index;
178 end_offset -= old_index;
179 }
180
181 // Step 5. If a boundary point's node is old parent and its offset is greater than old index + 1,
182 // subtract one from its offset.
183 if start_node == old_parent && (start_offset > old_index + 1) {
184 start_offset -= 1;
185 }
186 if end_node == old_parent && (end_offset > old_index + 1) {
187 end_offset -= 1;
188 }
189 }
190
191 active_range.set_start(&start_node, start_offset);
192 active_range.set_end(&end_node, end_offset);
193}
194
195/// <https://w3c.github.io/editing/docs/execCommand/#allowed-child>
196pub(crate) fn is_allowed_child(child: NodeOrString, parent: NodeOrString) -> bool {
197 // Step 1. If parent is "colgroup", "table", "tbody", "tfoot", "thead", "tr",
198 // or an HTML element with local name equal to one of those,
199 // and child is a Text node whose data does not consist solely of space characters, return false.
200 if matches!(
201 parent.name(),
202 "colgroup" | "table" | "tbody" | "tfoot" | "thead" | "tr"
203 ) && child.as_node().is_some_and(|node| {
204 // Note: cannot use `.and_then` here, since `downcast` would outlive its reference
205 node.downcast::<Text>()
206 .is_some_and(|text| !text.data().bytes().all(|byte| byte == b' '))
207 }) {
208 return false;
209 }
210 // Step 2. If parent is "script", "style", "plaintext", or "xmp",
211 // or an HTML element with local name equal to one of those, and child is not a Text node, return false.
212 if matches!(parent.name(), "script" | "style" | "plaintext" | "xmp") &&
213 child.as_node().is_none_or(|node| !node.is::<Text>())
214 {
215 return false;
216 }
217 // Step 3. If child is a document, DocumentFragment, or DocumentType, return false.
218 if let NodeOrString::Node(ref node) = child {
219 if matches!(
220 node.type_id(),
221 NodeTypeId::Document(_) | NodeTypeId::DocumentFragment(_) | NodeTypeId::DocumentType
222 ) {
223 return false;
224 }
225 }
226 // Step 4. If child is an HTML element, set child to the local name of child.
227 let child_name = match child {
228 NodeOrString::String(str_) => str_,
229 NodeOrString::Node(node) => match node.downcast::<HTMLElement>() {
230 // Step 5. If child is not a string, return true.
231 None => return true,
232 Some(html_element) => html_element.local_name().to_owned(),
233 },
234 };
235 let child = child_name.as_str();
236 let parent_name = match parent {
237 NodeOrString::String(str_) => str_,
238 NodeOrString::Node(parent) => {
239 // Step 6. If parent is an HTML element:
240 if let Some(parent_element) = parent.downcast::<HTMLElement>() {
241 // Step 6.1. If child is "a", and parent or some ancestor of parent is an a, return false.
242 if child == "a" &&
243 parent
244 .inclusive_ancestors(ShadowIncluding::No)
245 .any(|node| node.is::<HTMLAnchorElement>())
246 {
247 return false;
248 }
249 // Step 6.2. If child is a prohibited paragraph child name and parent or some ancestor of parent
250 // is an element with inline contents, return false.
251 if PROHIBITED_PARAGRAPH_CHILD_NAMES.contains(&child) &&
252 parent
253 .inclusive_ancestors(ShadowIncluding::No)
254 .any(|node| is_element_with_inline_contents(&node))
255 {
256 return false;
257 }
258 // Step 6.3. If child is "h1", "h2", "h3", "h4", "h5", or "h6",
259 // and parent or some ancestor of parent is an HTML element with local name
260 // "h1", "h2", "h3", "h4", "h5", or "h6", return false.
261 if matches!(child, "h1" | "h2" | "h3" | "h4" | "h5" | "h6") &&
262 parent.inclusive_ancestors(ShadowIncluding::No).any(|node| {
263 node.downcast::<HTMLElement>().is_some_and(|html_element| {
264 matches!(
265 html_element.local_name(),
266 "h1" | "h2" | "h3" | "h4" | "h5" | "h6"
267 )
268 })
269 })
270 {
271 return false;
272 }
273 // Step 6.4. Let parent be the local name of parent.
274 parent_element.local_name().to_owned()
275 } else {
276 // Step 7. If parent is an Element or DocumentFragment, return true.
277 // Step 8. If parent is not a string, return false.
278 return matches!(
279 parent.type_id(),
280 NodeTypeId::DocumentFragment(_) | NodeTypeId::Element(_)
281 );
282 }
283 },
284 };
285 let parent = parent_name.as_str();
286 // Step 9. If parent is on the left-hand side of an entry on the following list,
287 // then return true if child is listed on the right-hand side of that entry, and false otherwise.
288 match parent {
289 "colgroup" => return child == "col",
290 "table" => {
291 return matches!(
292 child,
293 "caption" | "col" | "colgroup" | "tbody" | "td" | "tfoot" | "th" | "thead" | "tr"
294 );
295 },
296 "tbody" | "tfoot" | "thead" => return matches!(child, "td" | "th" | "tr"),
297 "tr" => return matches!(child, "td" | "th"),
298 "dl" => return matches!(child, "dt" | "dd"),
299 "dir" | "ol" | "ul" => return matches!(child, "dir" | "li" | "ol" | "ul"),
300 "hgroup" => return matches!(child, "h1" | "h2" | "h3" | "h4" | "h5" | "h6"),
301 _ => {},
302 };
303 // Step 10. If child is "body", "caption", "col", "colgroup", "frame", "frameset", "head",
304 // "html", "tbody", "td", "tfoot", "th", "thead", or "tr", return false.
305 if matches!(
306 child,
307 "body" |
308 "caption" |
309 "col" |
310 "colgroup" |
311 "frame" |
312 "frameset" |
313 "head" |
314 "html" |
315 "tbody" |
316 "td" |
317 "tfoot" |
318 "th" |
319 "thead" |
320 "tr"
321 ) {
322 return false;
323 }
324 // Step 11. If child is "dd" or "dt" and parent is not "dl", return false.
325 if matches!(child, "dd" | "dt") && parent != "dl" {
326 return false;
327 }
328 // Step 12. If child is "li" and parent is not "ol" or "ul", return false.
329 if child == "li" && !matches!(parent, "ol" | "ul") {
330 return false;
331 }
332 // Step 13. If parent is on the left-hand side of an entry on the following list
333 // and child is listed on the right-hand side of that entry, return false.
334 if match parent {
335 "a" => child == "a",
336 "dd" | "dt" => matches!(child, "dd" | "dt"),
337 "h1" | "h2" | "h3" | "h4" | "h5" | "h6" => {
338 matches!(child, "h1" | "h2" | "h3" | "h4" | "h5" | "h6")
339 },
340 "li" => child == "li",
341 "nobr" => child == "nobr",
342 "td" | "th" => {
343 matches!(
344 child,
345 "caption" | "col" | "colgroup" | "tbody" | "td" | "tfoot" | "th" | "thead" | "tr"
346 )
347 },
348 _ if NAME_OF_AN_ELEMENT_WITH_INLINE_CONTENTS.contains(&parent) => {
349 PROHIBITED_PARAGRAPH_CHILD_NAMES.contains(&child)
350 },
351 _ => false,
352 } {
353 return false;
354 }
355 // Step 14. Return true.
356 true
357}
358
359/// <https://w3c.github.io/editing/docs/execCommand/#split-the-parent>
360pub(crate) fn split_the_parent<'a>(cx: &mut JSContext, node_list: &'a [&'a Node]) {
361 assert!(!node_list.is_empty());
362 // Step 1. Let original parent be the parent of the first member of node list.
363 let Some(original_parent) = node_list.first().and_then(|first| first.GetParentNode()) else {
364 return;
365 };
366 let context_object = original_parent.owner_document();
367 // Step 2. If original parent is not editable or its parent is null, do nothing and abort these steps.
368 if !original_parent.is_editable() {
369 return;
370 }
371 let Some(parent_of_original_parent) = original_parent.GetParentNode() else {
372 return;
373 };
374 // Step 3. If the first child of original parent is in node list, remove extraneous line breaks before original parent.
375 if original_parent
376 .children()
377 .next()
378 .is_some_and(|first_child| node_list.contains(&first_child.deref()))
379 {
380 original_parent.remove_extraneous_line_breaks_before(cx);
381 }
382 // Step 4. If the first child of original parent is in node list, and original parent follows a line break,
383 // set follows line break to true. Otherwise, set follows line break to false.
384 let first_child_is_in_node_list = original_parent
385 .children()
386 .next()
387 .is_some_and(|first_child| node_list.contains(&first_child.deref()));
388 let follows_line_break = first_child_is_in_node_list && original_parent.follows_a_line_break();
389 // Step 5. If the last child of original parent is in node list, and original parent precedes a line break,
390 // set precedes line break to true. Otherwise, set precedes line break to false.
391 let last_child_is_in_node_list = original_parent
392 .children()
393 .last()
394 .is_some_and(|last_child| node_list.contains(&last_child.deref()));
395 let precedes_line_break = last_child_is_in_node_list && original_parent.precedes_a_line_break();
396 // Step 6. If the first child of original parent is not in node list, but its last child is:
397 if !first_child_is_in_node_list && last_child_is_in_node_list {
398 // Step 6.1. For each node in node list, in reverse order,
399 // insert node into the parent of original parent immediately after original parent, preserving ranges.
400 let next_of_original_parent = original_parent.GetNextSibling();
401 for node in node_list.iter().rev() {
402 move_preserving_ranges(cx, node, |cx| {
403 parent_of_original_parent.InsertBefore(cx, node, next_of_original_parent.as_deref())
404 });
405 }
406 // Step 6.2. If precedes line break is true, and the last member of node list does not precede a line break,
407 // call createElement("br") on the context object and insert the result immediately after the last member of node list.
408 if precedes_line_break {
409 if let Some(last) = node_list.last() {
410 if !last.precedes_a_line_break() {
411 let br = context_object.create_element(cx, "br");
412 if last
413 .GetParentNode()
414 .expect("Must always have a parent")
415 .InsertBefore(cx, br.upcast(), last.GetNextSibling().as_deref())
416 .is_err()
417 {
418 unreachable!("Must always be able to append");
419 }
420 }
421 }
422 }
423 // Step 6.3. Remove extraneous line breaks at the end of original parent.
424 original_parent.remove_extraneous_line_breaks_at_the_end_of(cx);
425 // Step 6.4. Abort these steps.
426 return;
427 }
428 // Step 7. If the first child of original parent is not in node list:
429 if first_child_is_in_node_list {
430 // Step 7.1. Let cloned parent be the result of calling cloneNode(false) on original parent.
431 let Ok(cloned_parent) = original_parent.CloneNode(cx, false) else {
432 unreachable!("Must always be able to clone node");
433 };
434 // Step 7.2. If original parent has an id attribute, unset it.
435 if let Some(element) = original_parent.downcast::<Element>() {
436 element.remove_attribute_by_name(&local_name!("id"), CanGc::from_cx(cx));
437 }
438 // Step 7.3. Insert cloned parent into the parent of original parent immediately before original parent.
439 if parent_of_original_parent
440 .InsertBefore(cx, &cloned_parent, Some(&original_parent))
441 .is_err()
442 {
443 unreachable!("Must always have a parent");
444 }
445 // Step 7.4. While the previousSibling of the first member of node list is not null,
446 // append the first child of original parent as the last child of cloned parent, preserving ranges.
447 loop {
448 if node_list
449 .first()
450 .and_then(|first| first.GetPreviousSibling())
451 .is_some()
452 {
453 if let Some(first_of_original) = original_parent.children().next() {
454 move_preserving_ranges(cx, &first_of_original, |cx| {
455 cloned_parent.AppendChild(cx, &first_of_original)
456 });
457 continue;
458 }
459 }
460 break;
461 }
462 }
463 // Step 8. For each node in node list, insert node into the parent of original parent immediately before original parent, preserving ranges.
464 for node in node_list.iter() {
465 move_preserving_ranges(cx, node, |cx| {
466 parent_of_original_parent.InsertBefore(cx, node, Some(&original_parent))
467 });
468 }
469 // Step 9. If follows line break is true, and the first member of node list does not follow a line break,
470 // call createElement("br") on the context object and insert the result immediately before the first member of node list.
471 if follows_line_break {
472 if let Some(first) = node_list.first() {
473 if !first.follows_a_line_break() {
474 let br = context_object.create_element(cx, "br");
475 if first
476 .GetParentNode()
477 .expect("Must always have a parent")
478 .InsertBefore(cx, br.upcast(), Some(first))
479 .is_err()
480 {
481 unreachable!("Must always be able to insert");
482 }
483 }
484 }
485 }
486 // Step 10. If the last member of node list is an inline node other than a br,
487 // and the first child of original parent is a br, and original parent is not an inline node,
488 // remove the first child of original parent from original parent.
489 if node_list
490 .last()
491 .is_some_and(|last| last.is_inline_node() && !last.is::<HTMLBRElement>()) &&
492 !original_parent.is_inline_node()
493 {
494 if let Some(first_of_original) = original_parent.children().next() {
495 if first_of_original.is::<HTMLBRElement>() {
496 assert!(first_of_original.has_parent());
497 first_of_original.remove_self(cx);
498 }
499 }
500 }
501 // Step 11. If original parent has no children:
502 if original_parent.children_count() == 0 {
503 // Step 11.1. Remove original parent from its parent.
504 assert!(original_parent.has_parent());
505 original_parent.remove_self(cx);
506 // Step 11.2. If precedes line break is true, and the last member of node list does not precede a line break,
507 // call createElement("br") on the context object and insert the result immediately after the last member of node list.
508 if precedes_line_break {
509 if let Some(last) = node_list.last() {
510 if !last.precedes_a_line_break() {
511 let br = context_object.create_element(cx, "br");
512 if last
513 .GetParentNode()
514 .expect("Must always have a parent")
515 .InsertBefore(cx, br.upcast(), last.GetNextSibling().as_deref())
516 .is_err()
517 {
518 unreachable!("Must always be able to insert");
519 }
520 }
521 }
522 }
523 } else {
524 // Step 12. Otherwise, remove extraneous line breaks before original parent.
525 original_parent.remove_extraneous_line_breaks_before(cx);
526 }
527 // Step 13. If node list's last member's nextSibling is null, but its parent is not null,
528 // remove extraneous line breaks at the end of node list's last member's parent.
529 if let Some(last) = node_list.last() {
530 if last.GetNextSibling().is_none() {
531 if let Some(parent_of_last) = last.GetParentNode() {
532 parent_of_last.remove_extraneous_line_breaks_at_the_end_of(cx);
533 }
534 }
535 }
536}
537
538/// <https://w3c.github.io/editing/docs/execCommand/#wrap>
539fn wrap_node_list<SiblingCriteria, NewParentInstructions>(
540 cx: &mut JSContext,
541 node_list: Vec<DomRoot<Node>>,
542 sibling_criteria: SiblingCriteria,
543 new_parent_instructions: NewParentInstructions,
544) -> Option<DomRoot<Node>>
545where
546 SiblingCriteria: Fn(&Node) -> bool,
547 NewParentInstructions: Fn() -> Option<DomRoot<Node>>,
548{
549 // Step 1. If every member of node list is invisible,
550 // and none is a br, return null and abort these steps.
551 if node_list
552 .iter()
553 .all(|node| node.is_invisible() && !node.is::<HTMLBRElement>())
554 {
555 return None;
556 }
557 // Step 2. If node list's first member's parent is null, return null and abort these steps.
558 node_list.first().and_then(|first| first.GetParentNode())?;
559 // Step 3. If node list's last member is an inline node that's not a br,
560 // and node list's last member's nextSibling is a br, append that br to node list.
561 let mut node_list = node_list;
562 if let Some(last) = node_list.last() {
563 if last.is_inline_node() && !last.is::<HTMLBRElement>() {
564 if let Some(next_of_last) = last.GetNextSibling() {
565 if next_of_last.is::<HTMLBRElement>() {
566 node_list.push(next_of_last);
567 }
568 }
569 }
570 }
571 // Step 4. While node list's first member's previousSibling is invisible, prepend it to node list.
572 while let Some(previous_of_first) = node_list.first().and_then(|last| last.GetPreviousSibling())
573 {
574 if previous_of_first.is_invisible() {
575 node_list.insert(0, previous_of_first);
576 continue;
577 }
578 break;
579 }
580 // Step 5. While node list's last member's nextSibling is invisible, append it to node list.
581 while let Some(next_of_last) = node_list.last().and_then(|last| last.GetNextSibling()) {
582 if next_of_last.is_invisible() {
583 node_list.push(next_of_last);
584 continue;
585 }
586 break;
587 }
588 // Step 6. If the previousSibling of the first member of node list is editable
589 // and running sibling criteria on it returns true,
590 // let new parent be the previousSibling of the first member of node list.
591 let new_parent = node_list
592 .first()
593 .and_then(|first| first.GetPreviousSibling())
594 .filter(|previous_of_first| {
595 previous_of_first.is_editable() && sibling_criteria(previous_of_first)
596 });
597 // Step 7. Otherwise, if the nextSibling of the last member of node list is editable
598 // and running sibling criteria on it returns true,
599 // let new parent be the nextSibling of the last member of node list.
600 let new_parent = new_parent.or_else(|| {
601 node_list
602 .last()
603 .and_then(|last| last.GetNextSibling())
604 .filter(|next_of_last| next_of_last.is_editable() && sibling_criteria(next_of_last))
605 });
606 // Step 8. Otherwise, run new parent instructions, and let new parent be the result.
607 // Step 9. If new parent is null, abort these steps and return null.
608 let new_parent = new_parent.or_else(new_parent_instructions)?;
609 // Step 11. Let original parent be the parent of the first member of node list.
610 let first_in_node_list = node_list
611 .first()
612 .expect("Must always have at least one node");
613 let original_parent = first_in_node_list
614 .GetParentNode()
615 .expect("First node must have a parent");
616 // Step 10. If new parent's parent is null:
617 if new_parent.GetParentNode().is_none() {
618 // Step 10.1. Insert new parent into the parent of the first member
619 // of node list immediately before the first member of node list.
620 if original_parent
621 .InsertBefore(cx, &new_parent, Some(first_in_node_list))
622 .is_err()
623 {
624 unreachable!("Must always be able to insert");
625 }
626 // Step 10.2. If any range has a boundary point with node equal
627 // to the parent of new parent and offset equal to the index of new parent,
628 // add one to that boundary point's offset.
629 if let Some(range) = first_in_node_list
630 .owner_document()
631 .GetSelection(cx)
632 .and_then(|selection| selection.active_range())
633 {
634 let parent_of_new_parent = new_parent.GetParentNode().expect("Must have a parent");
635 let start_container = range.start_container();
636 let start_offset = range.start_offset();
637
638 if start_container == parent_of_new_parent && start_offset == new_parent.index() {
639 range.set_start(&start_container, start_offset + 1);
640 }
641
642 let end_container = range.end_container();
643 let end_offset = range.end_offset();
644
645 if end_container == parent_of_new_parent && end_offset == new_parent.index() {
646 range.set_end(&end_container, end_offset + 1);
647 }
648 }
649 }
650 // Step 12. If new parent is before the first member of node list in tree order:
651 if new_parent.is_before(first_in_node_list) {
652 // Step 12.1. If new parent is not an inline node, but the last visible child of new parent
653 // and the first visible member of node list are both inline nodes,
654 // and the last child of new parent is not a br,
655 // call createElement("br") on the ownerDocument of new parent
656 // and append the result as the last child of new parent.
657 if !new_parent.is_inline_node() &&
658 new_parent
659 .rev_children()
660 .find(|child| child.is_visible())
661 .is_some_and(|child| child.is_inline_node()) &&
662 node_list
663 .iter()
664 .find(|node| node.is_visible())
665 .is_some_and(|node| node.is_inline_node()) &&
666 new_parent
667 .children_unrooted(cx.no_gc())
668 .last()
669 .is_none_or(|last_child| !last_child.is::<HTMLBRElement>())
670 {
671 let new_br_element = new_parent.owner_document().create_element(cx, "br");
672 if new_parent.AppendChild(cx, new_br_element.upcast()).is_err() {
673 unreachable!("Must always be able to append");
674 }
675 }
676 // Step 12.2. For each node in node list, append node as the last child of new parent, preserving ranges.
677 for node in node_list {
678 move_preserving_ranges(cx, &node, |cx| new_parent.AppendChild(cx, &node));
679 }
680 } else {
681 // Step 13. Otherwise:
682 // Step 13.1. If new parent is not an inline node, but the first visible child of new parent
683 // and the last visible member of node list are both inline nodes,
684 // and the last member of node list is not a br,
685 // call createElement("br") on the ownerDocument of new parent
686 // and insert the result as the first child of new parent.
687 if !new_parent.is_inline_node() &&
688 new_parent
689 .children_unrooted(cx.no_gc())
690 .find(|child| child.is_visible())
691 .is_some_and(|child| child.is_inline_node()) &&
692 node_list
693 .iter()
694 .rev()
695 .find(|node| node.is_visible())
696 .is_some_and(|node| node.is_inline_node()) &&
697 node_list
698 .last()
699 .is_none_or(|last_child| !last_child.is::<HTMLBRElement>())
700 {
701 let new_br_element = new_parent.owner_document().create_element(cx, "br");
702 if new_parent
703 .InsertBefore(
704 cx,
705 new_br_element.upcast(),
706 new_parent.GetFirstChild().as_deref(),
707 )
708 .is_err()
709 {
710 unreachable!("Must always be able to append");
711 }
712 }
713 // Step 13.2. For each node in node list, in reverse order,
714 // insert node as the first child of new parent, preserving ranges.
715 let mut before = new_parent.GetFirstChild();
716 for node in node_list.iter().rev() {
717 move_preserving_ranges(cx, node, |cx| {
718 new_parent.InsertBefore(cx, node, before.as_deref())
719 });
720 before = Some(DomRoot::from_ref(node));
721 }
722 }
723 // Step 14. If original parent is editable and has no children, remove it from its parent.
724 if original_parent.is_editable() && original_parent.children_count() == 0 {
725 original_parent.remove_self(cx);
726 }
727 // Step 15. If new parent's nextSibling is editable and running sibling criteria on it returns true:
728 if let Some(next_of_new_parent) = new_parent.GetNextSibling() {
729 if next_of_new_parent.is_editable() && sibling_criteria(&next_of_new_parent) {
730 // Step 15.1. If new parent is not an inline node,
731 // but new parent's last child and new parent's nextSibling's first child are both inline nodes,
732 // and new parent's last child is not a br, call createElement("br") on the ownerDocument
733 // of new parent and append the result as the last child of new parent.
734 if !new_parent.is_inline_node() {
735 let child = new_parent
736 .children_unrooted(cx.no_gc())
737 .last()
738 .map(|node| node.as_rooted());
739 if let Some(last_child_of_new_parent) = child {
740 if last_child_of_new_parent.is_inline_node() &&
741 !last_child_of_new_parent.is::<HTMLBRElement>() &&
742 next_of_new_parent
743 .children()
744 .next()
745 .is_some_and(|first| first.is_inline_node())
746 {
747 let new_br_element = new_parent.owner_document().create_element(cx, "br");
748 if new_parent.AppendChild(cx, new_br_element.upcast()).is_err() {
749 unreachable!("Must always be able to append");
750 }
751 }
752 }
753 }
754 // Step 15.2. While new parent's nextSibling has children,
755 // append its first child as the last child of new parent, preserving ranges.
756 for child in next_of_new_parent.children() {
757 move_preserving_ranges(cx, &child, |cx| new_parent.AppendChild(cx, &child));
758 }
759 // Step 15.3. Remove new parent's nextSibling from its parent.
760 next_of_new_parent.remove_self(cx);
761 }
762 }
763 // Step 16. Remove extraneous line breaks from new parent.
764 new_parent.remove_extraneous_line_breaks_from(cx);
765 // Step 17. Return new parent.
766 Some(new_parent)
767}
768
769pub(crate) struct RecordedValueAndCommandOfNode {
770 node: DomRoot<Node>,
771 command: CommandName,
772 specified_command_value: Option<DOMString>,
773}
774
775/// <https://w3c.github.io/editing/docs/execCommand/#record-the-values>
776pub(crate) fn record_the_values(
777 node_list: Vec<DomRoot<Node>>,
778) -> Vec<RecordedValueAndCommandOfNode> {
779 // Step 1. Let values be a list of (node, command, specified command value) triples, initially empty.
780 let mut values = vec![];
781 // Step 2. For each node in node list,
782 // for each command in the list "subscript", "bold", "fontName", "fontSize", "foreColor",
783 // "hiliteColor", "italic", "strikethrough", and "underline" in that order:
784 for node in node_list {
785 for command in vec![
786 CommandName::Subscript,
787 CommandName::Bold,
788 CommandName::FontName,
789 CommandName::FontSize,
790 CommandName::ForeColor,
791 CommandName::HiliteColor,
792 CommandName::Italic,
793 CommandName::Strikethrough,
794 CommandName::Underline,
795 ] {
796 // Step 2.1. Let ancestor equal node.
797 let mut ancestor =
798 if let Some(node_element) = DomRoot::downcast::<Element>(node.clone()) {
799 Some(node_element)
800 } else {
801 // Step 2.2. If ancestor is not an Element, set it to its parent.
802 node.GetParentElement()
803 };
804 // Step 2.3. While ancestor is an Element and its specified command value for command is null, set it to its parent.
805 while let Some(ref ancestor_element) = ancestor {
806 if ancestor_element.specified_command_value(&command).is_none() {
807 ancestor = ancestor_element.upcast::<Node>().GetParentElement();
808 continue;
809 }
810 break;
811 }
812 // Step 2.4. If ancestor is an Element,
813 // add (node, command, ancestor's specified command value for command) to values.
814 // Otherwise add (node, command, null) to values.
815 let specified_command_value =
816 ancestor.and_then(|ancestor| ancestor.specified_command_value(&command));
817 values.push(RecordedValueAndCommandOfNode {
818 node: node.clone(),
819 command,
820 specified_command_value,
821 });
822 }
823 }
824 // Step 3. Return values.
825 values
826}
827
828/// <https://w3c.github.io/editing/docs/execCommand/#restore-the-values>
829pub(crate) fn restore_the_values(cx: &mut JSContext, values: Vec<RecordedValueAndCommandOfNode>) {
830 // Step 1. For each (node, command, value) triple in values:
831 for triple in values {
832 let RecordedValueAndCommandOfNode {
833 node,
834 command,
835 specified_command_value,
836 } = triple;
837 // Step 1.1. Let ancestor equal node.
838 let mut ancestor = if let Some(node_element) = DomRoot::downcast::<Element>(node.clone()) {
839 Some(node_element)
840 } else {
841 // Step 1.2. If ancestor is not an Element, set it to its parent.
842 node.GetParentElement()
843 };
844 // Step 1.3. While ancestor is an Element and its specified command value for command is null, set it to its parent.
845 while let Some(ref ancestor_element) = ancestor {
846 if ancestor_element.specified_command_value(&command).is_none() {
847 ancestor = ancestor_element.upcast::<Node>().GetParentElement();
848 continue;
849 }
850 break;
851 }
852 // Step 1.4. If value is null and ancestor is an Element,
853 // push down values on node for command, with new value null.
854 if specified_command_value.is_none() && ancestor.is_some() {
855 node.push_down_values(cx, &command, None);
856 } else {
857 // Step 1.5. Otherwise, if ancestor is an Element and its specified command value for command is not equivalent to value,
858 // or if ancestor is not an Element and value is not null, force the value of command to value on node.
859 if match (ancestor, specified_command_value.as_ref()) {
860 (Some(ancestor), value) => !command.are_equivalent_values(
861 ancestor.specified_command_value(&command).as_ref(),
862 value,
863 ),
864 (None, Some(_)) => true,
865 _ => false,
866 } {
867 node.force_the_value(cx, &command, specified_command_value.as_ref());
868 }
869 }
870 }
871}
872
873impl HTMLBRElement {
874 /// <https://w3c.github.io/editing/docs/execCommand/#extraneous-line-break>
875 fn is_extraneous_line_break(&self) -> bool {
876 let node = self.upcast::<Node>();
877 // > An extraneous line break is a br that has no visual effect, in that removing it from the DOM would not change layout,
878 // except that a br that is the sole child of an li is not extraneous.
879 if node
880 .GetParentNode()
881 .filter(|parent| parent.is::<HTMLLIElement>())
882 .is_some_and(|li| li.children_count() == 1)
883 {
884 return false;
885 }
886 // TODO: Figure out what this actually makes it have no visual effect
887 !node.is_block_node()
888 }
889}
890
891impl Node {
892 /// <https://w3c.github.io/editing/docs/execCommand/#push-down-values>
893 pub(crate) fn push_down_values(
894 &self,
895 cx: &mut JSContext,
896 command: &CommandName,
897 new_value: Option<DOMString>,
898 ) {
899 // Step 1. Let command be the current command.
900 //
901 // Passed in as argument
902
903 // Step 4. Let current ancestor be node's parent.
904 let mut current_ancestor = self.GetParentElement();
905 // Step 2. If node's parent is not an Element, abort this algorithm.
906 if current_ancestor.is_none() {
907 return;
908 };
909 // Step 3. If the effective command value of command is loosely equivalent to new value on node,
910 // abort this algorithm.
911 if command.are_loosely_equivalent_values(
912 self.effective_command_value(command).as_ref(),
913 new_value.as_ref(),
914 ) {
915 return;
916 }
917 // Step 5. Let ancestor list be a list of nodes, initially empty.
918 rooted_vec!(let mut ancestor_list);
919 // Step 6. While current ancestor is an editable Element and
920 // the effective command value of command is not loosely equivalent to new value on it,
921 // append current ancestor to ancestor list, then set current ancestor to its parent.
922 while let Some(ancestor) = current_ancestor {
923 let ancestor_node = ancestor.upcast::<Node>();
924 if ancestor_node.is_editable() &&
925 !command.are_loosely_equivalent_values(
926 ancestor_node.effective_command_value(command).as_ref(),
927 new_value.as_ref(),
928 )
929 {
930 ancestor_list.push(ancestor.clone());
931 current_ancestor = ancestor_node.GetParentElement();
932 continue;
933 }
934 break;
935 }
936 let Some(last_ancestor) = ancestor_list.last() else {
937 // Step 7. If ancestor list is empty, abort this algorithm.
938 return;
939 };
940 // Step 8. Let propagated value be the specified command value of command on the last member of ancestor list.
941 let mut propagated_value = last_ancestor.specified_command_value(command);
942 // Step 9. If propagated value is null and is not equal to new value, abort this algorithm.
943 if propagated_value.is_none() && new_value.is_some() {
944 return;
945 }
946 // Step 10. If the effective command value of command is not loosely equivalent to new value on the parent
947 // of the last member of ancestor list, and new value is not null, abort this algorithm.
948 if new_value.is_some() &&
949 !last_ancestor
950 .upcast::<Node>()
951 .GetParentNode()
952 .is_some_and(|last_ancestor_parent| {
953 command.are_loosely_equivalent_values(
954 last_ancestor_parent
955 .effective_command_value(command)
956 .as_ref(),
957 new_value.as_ref(),
958 )
959 })
960 {
961 return;
962 }
963 // Step 11. While ancestor list is not empty:
964 let mut ancestor_list_iter = ancestor_list.iter().rev().peekable();
965 while let Some(current_ancestor) = ancestor_list_iter.next() {
966 let current_ancestor_node = current_ancestor.upcast::<Node>();
967 // Step 11.1. Let current ancestor be the last member of ancestor list.
968 // Step 11.2. Remove the last member from ancestor list.
969 //
970 // Both of these steps done by iterating and reversing the iterator
971
972 // Step 11.3. If the specified command value of current ancestor for command is not null, set propagated value to that value.
973 let command_value = current_ancestor.specified_command_value(command);
974 let has_command_value = command_value.is_some();
975 propagated_value = command_value.or(propagated_value);
976 // Step 11.4. Let children be the children of current ancestor.
977 let children = current_ancestor_node
978 .children()
979 .collect::<Vec<DomRoot<Node>>>();
980 // Step 11.5. If the specified command value of current ancestor for command is not null, clear the value of current ancestor.
981 if has_command_value {
982 if let Some(html_element) = current_ancestor.downcast::<HTMLElement>() {
983 html_element.clear_the_value(cx, command);
984 }
985 }
986 // Step 11.6. For every child in children:
987 for child in children {
988 // Step 11.6.1. If child is node, continue with the next child.
989 if *child == *self {
990 continue;
991 }
992 // Step 11.6.2. If child is an Element whose specified command value for command is neither null
993 // nor equivalent to propagated value, continue with the next child.
994 if let Some(child_element) = child.downcast::<Element>() {
995 let specified_value = child_element.specified_command_value(command);
996 if specified_value.is_some() &&
997 !command.are_equivalent_values(
998 specified_value.as_ref(),
999 propagated_value.as_ref(),
1000 )
1001 {
1002 continue;
1003 }
1004 }
1005
1006 // Step 11.6.3. If child is the last member of ancestor list, continue with the next child.
1007 //
1008 // Since we had to remove the last member in step 11.2, if we now peek at the next possible
1009 // value, we essentially have the "last member after removal"
1010 if ancestor_list_iter
1011 .peek()
1012 .is_some_and(|ancestor| *ancestor.upcast::<Node>() == *child)
1013 {
1014 continue;
1015 }
1016 // step 11.6.4. Force the value of child, with command as in this algorithm and new value equal to propagated value.
1017 child.force_the_value(cx, command, propagated_value.as_ref());
1018 }
1019 }
1020 }
1021
1022 /// <https://w3c.github.io/editing/docs/execCommand/#reorder-modifiable-descendants>
1023 fn reorder_modifiable_descendants(
1024 &self,
1025 cx: &mut JSContext,
1026 command: &CommandName,
1027 new_value: &DOMString,
1028 ) {
1029 // Step 1. Let candidate equal node.
1030 let mut candidate = DomRoot::from_ref(self);
1031 // Step 2. While candidate is a modifiable element, and candidate has exactly one child,
1032 // and that child is also a modifiable element,
1033 // and candidate is not a simple modifiable element or candidate's specified command value
1034 // for command is not equivalent to new value, set candidate to its child.
1035 loop {
1036 if let Some(candidate_element) = candidate.downcast::<Element>() {
1037 if candidate_element.is_modifiable_element() &&
1038 candidate.children_count() == 1 &&
1039 (!candidate_element.is_simple_modifiable_element() ||
1040 !command.are_equivalent_values(
1041 candidate_element.specified_command_value(command).as_ref(),
1042 Some(new_value),
1043 ))
1044 {
1045 let child = candidate.children().next().expect("Has one child");
1046
1047 if let Some(child_element) = child.downcast::<Element>() {
1048 if child_element.is_modifiable_element() {
1049 candidate = child;
1050 continue;
1051 }
1052 }
1053 }
1054 }
1055 break;
1056 }
1057 // Step 3. If candidate is node, or is not a simple modifiable element,
1058 // or its specified command value is not equivalent to new value,
1059 // or its effective command value is not loosely equivalent to new value, abort these steps.
1060 if *candidate == *self ||
1061 !command.are_loosely_equivalent_values(
1062 candidate.effective_command_value(command).as_ref(),
1063 Some(new_value),
1064 )
1065 {
1066 return;
1067 }
1068 if let Some(candidate) = candidate.downcast::<Element>() {
1069 if !candidate.is_simple_modifiable_element() ||
1070 !command.are_equivalent_values(
1071 candidate.specified_command_value(command).as_ref(),
1072 Some(new_value),
1073 )
1074 {
1075 return;
1076 }
1077 }
1078 // Step 4. While candidate has children,
1079 // insert the first child of candidate into candidate's parent immediately before candidate, preserving ranges.
1080 let parent_of_candidate = candidate
1081 .GetParentNode()
1082 .expect("Must always have a parent");
1083 for child in candidate.children() {
1084 move_preserving_ranges(cx, &child, |cx| {
1085 parent_of_candidate.InsertBefore(cx, &child, Some(&candidate))
1086 });
1087 }
1088 // Step 5. Insert candidate into node's parent immediately after node.
1089 let parent_of_node = self.GetParentNode().expect("Must always have a parent");
1090 if parent_of_node
1091 .InsertBefore(cx, &candidate, self.GetNextSibling().as_deref())
1092 .is_err()
1093 {
1094 unreachable!("Must always be able to insert");
1095 }
1096 // Step 6. Append the node as the last child of candidate, preserving ranges.
1097 move_preserving_ranges(cx, self, |cx| candidate.AppendChild(cx, self));
1098 }
1099
1100 /// <https://w3c.github.io/editing/docs/execCommand/#force-the-value>
1101 pub(crate) fn force_the_value(
1102 &self,
1103 cx: &mut JSContext,
1104 command: &CommandName,
1105 new_value: Option<&DOMString>,
1106 ) {
1107 // Step 1. Let command be the current command.
1108 //
1109 // That's command
1110
1111 // Step 2. If node's parent is null, abort this algorithm.
1112 if self.GetParentNode().is_none() {
1113 return;
1114 }
1115 // Step 3. If new value is null, abort this algorithm.
1116 let Some(new_value) = new_value else {
1117 return;
1118 };
1119 // Step 4. If node is an allowed child of "span":
1120 if is_allowed_child(
1121 NodeOrString::Node(DomRoot::from_ref(self)),
1122 NodeOrString::String("span".to_owned()),
1123 ) {
1124 // Step 4.1. Reorder modifiable descendants of node's previousSibling.
1125 if let Some(previous) = self.GetPreviousSibling() {
1126 previous.reorder_modifiable_descendants(cx, command, new_value);
1127 }
1128 // Step 4.2. Reorder modifiable descendants of node's nextSibling.
1129 if let Some(next) = self.GetNextSibling() {
1130 next.reorder_modifiable_descendants(cx, command, new_value);
1131 }
1132 // Step 4.3. Wrap the one-node list consisting of node,
1133 // with sibling criteria returning true for a simple modifiable element whose
1134 // specified command value is equivalent to new value and whose effective command value
1135 // is loosely equivalent to new value and false otherwise,
1136 // and with new parent instructions returning null.
1137 wrap_node_list(
1138 cx,
1139 vec![DomRoot::from_ref(self)],
1140 |sibling| {
1141 sibling
1142 .downcast::<Element>()
1143 .is_some_and(|sibling_element| {
1144 sibling_element.is_simple_modifiable_element() &&
1145 command.are_equivalent_values(
1146 sibling_element.specified_command_value(command).as_ref(),
1147 Some(new_value),
1148 ) &&
1149 command.are_loosely_equivalent_values(
1150 sibling.effective_command_value(command).as_ref(),
1151 Some(new_value),
1152 )
1153 })
1154 },
1155 || None,
1156 );
1157 }
1158 // Step 5. If node is invisible, abort this algorithm.
1159 if self.is_invisible() {
1160 return;
1161 }
1162 // Step 6. If the effective command value of command is loosely equivalent to new value on node, abort this algorithm.
1163 if command.are_loosely_equivalent_values(
1164 self.effective_command_value(command).as_ref(),
1165 Some(new_value),
1166 ) {
1167 return;
1168 }
1169 // Step 7. If node is not an allowed child of "span":
1170 if !is_allowed_child(
1171 NodeOrString::Node(DomRoot::from_ref(self)),
1172 NodeOrString::String("span".to_owned()),
1173 ) {
1174 // Step 7.1. Let children be all children of node, omitting any that are Elements whose
1175 // specified command value for command is neither null nor equivalent to new value.
1176 let children = self
1177 .children()
1178 .filter(|child| {
1179 !child.downcast::<Element>().is_some_and(|child_element| {
1180 let specified_value = child_element.specified_command_value(command);
1181 specified_value.is_some() &&
1182 !command
1183 .are_equivalent_values(specified_value.as_ref(), Some(new_value))
1184 })
1185 })
1186 .collect::<Vec<DomRoot<Node>>>();
1187 // Step 7.2. Force the value of each node in children,
1188 // with command and new value as in this invocation of the algorithm.
1189 for child in children {
1190 child.force_the_value(cx, command, Some(new_value));
1191 }
1192 // Step 7.3. Abort this algorithm.
1193 return;
1194 }
1195 // Step 8. If the effective command value of command is loosely equivalent to new value on node, abort this algorithm.
1196 if command.are_loosely_equivalent_values(
1197 self.effective_command_value(command).as_ref(),
1198 Some(new_value),
1199 ) {
1200 return;
1201 }
1202 // Step 9. Let new parent be null.
1203 let mut new_parent = None;
1204 let document = self.owner_document();
1205 let css_styling_flag = document.css_styling_flag();
1206 // Step 10. If the CSS styling flag is false:
1207 if !css_styling_flag {
1208 match command {
1209 // Step 10.1. If command is "bold" and new value is "bold",
1210 // let new parent be the result of calling createElement("b") on the ownerDocument of node.
1211 CommandName::Bold => {
1212 new_parent = Some(document.create_element(cx, "b"));
1213 },
1214 // Step 10.2. If command is "italic" and new value is "italic",
1215 // let new parent be the result of calling createElement("i") on the ownerDocument of node.
1216 CommandName::Italic => {
1217 new_parent = Some(document.create_element(cx, "i"));
1218 },
1219 // Step 10.3. If command is "strikethrough" and new value is "line-through",
1220 // let new parent be the result of calling createElement("s") on the ownerDocument of node.
1221 CommandName::Strikethrough => {
1222 new_parent = Some(document.create_element(cx, "s"));
1223 },
1224 // Step 10.4. If command is "underline" and new value is "underline",
1225 // let new parent be the result of calling createElement("u") on the ownerDocument of node.
1226 CommandName::Underline => {
1227 new_parent = Some(document.create_element(cx, "u"));
1228 },
1229 // Step 10.5. If command is "foreColor", and new value is fully opaque with
1230 // red, green, and blue components in the range 0 to 255:
1231 CommandName::ForeColor => {
1232 // TODO
1233 },
1234 // Step 10.6. If command is "fontName",
1235 // let new parent be the result of calling createElement("font") on the ownerDocument of node,
1236 // then set the face attribute of new parent to new value.
1237 CommandName::FontName => {
1238 let new_font_element = document.create_element(cx, "font");
1239 new_font_element.set_string_attribute(
1240 &local_name!("face"),
1241 new_value.clone(),
1242 CanGc::from_cx(cx),
1243 );
1244 new_parent = Some(new_font_element);
1245 },
1246 _ => {},
1247 }
1248 }
1249
1250 match command {
1251 // Step 11. If command is "createLink" or "unlink":
1252 // TODO
1253 // Step 12. If command is "fontSize"; and new value is one of
1254 // "x-small", "small", "medium", "large", "x-large", "xx-large", or "xxx-large";
1255 // and either the CSS styling flag is false, or new value is "xxx-large":
1256 // let new parent be the result of calling createElement("font") on the ownerDocument of node,
1257 // then set the size attribute of new parent to the number from the following table based on new value:
1258 CommandName::FontSize => {
1259 if !css_styling_flag || new_value == "xxx-large" {
1260 let size = match &*new_value.str() {
1261 "x-small" => 1,
1262 "small" => 2,
1263 "medium" => 3,
1264 "large" => 4,
1265 "x-large" => 5,
1266 "xx-large" => 6,
1267 "xxx-large" => 7,
1268 _ => 0,
1269 };
1270
1271 if size > 0 {
1272 let new_font_element = document.create_element(cx, "font");
1273 new_font_element.set_int_attribute(
1274 &local_name!("size"),
1275 size,
1276 CanGc::from_cx(cx),
1277 );
1278 new_parent = Some(new_font_element);
1279 }
1280 }
1281 },
1282 CommandName::Subscript | CommandName::Superscript => {
1283 // Step 13. If command is "subscript" or "superscript" and new value is "subscript",
1284 // let new parent be the result of calling createElement("sub") on the ownerDocument of node.
1285 if new_value == "subscript" {
1286 new_parent = Some(document.create_element(cx, "sub"));
1287 }
1288 // Step 14. If command is "subscript" or "superscript" and new value is "superscript",
1289 // let new parent be the result of calling createElement("sup") on the ownerDocument of node.
1290 if new_value == "superscript" {
1291 new_parent = Some(document.create_element(cx, "sup"));
1292 }
1293 },
1294 _ => {},
1295 }
1296 // Step 15. If new parent is null, let new parent be the result of calling createElement("span") on the ownerDocument of node.
1297 let new_parent = new_parent.unwrap_or_else(|| document.create_element(cx, "span"));
1298 let new_parent_html_element = new_parent
1299 .downcast::<HTMLElement>()
1300 .expect("Must always create a HTML element");
1301 // Step 16. Insert new parent in node's parent before node.
1302 if self
1303 .GetParentNode()
1304 .expect("Must always have a parent")
1305 .InsertBefore(cx, new_parent.upcast(), Some(self))
1306 .is_err()
1307 {
1308 unreachable!("Must always be able to insert");
1309 }
1310 // Step 17. If the effective command value of command for new parent is not loosely equivalent to new value,
1311 // and the relevant CSS property for command is not null,
1312 // set that CSS property of new parent to new value (if the new value would be valid).
1313 if !command.are_loosely_equivalent_values(
1314 new_parent
1315 .upcast::<Node>()
1316 .effective_command_value(command)
1317 .as_ref(),
1318 Some(new_value),
1319 ) {
1320 if let Some(css_property) = command.relevant_css_property() {
1321 css_property.set_for_element(cx, new_parent_html_element, new_value.clone());
1322 }
1323 }
1324 match command {
1325 // Step 18. If command is "strikethrough", and new value is "line-through",
1326 // and the effective command value of "strikethrough" for new parent is not "line-through",
1327 // set the "text-decoration" property of new parent to "line-through".
1328 CommandName::Strikethrough => {
1329 if new_value == "line-through" &&
1330 new_parent
1331 .upcast::<Node>()
1332 .effective_command_value(&CommandName::Strikethrough)
1333 .is_none_or(|value| value != "line-through")
1334 {
1335 CssPropertyName::TextDecoration.set_for_element(
1336 cx,
1337 new_parent_html_element,
1338 new_value.clone(),
1339 );
1340 }
1341 },
1342 // Step 19. If command is "underline", and new value is "underline",
1343 // and the effective command value of "underline" for new parent is not "underline",
1344 // set the "text-decoration" property of new parent to "underline".
1345 CommandName::Underline => {
1346 if new_value == "underline" &&
1347 new_parent
1348 .upcast::<Node>()
1349 .effective_command_value(&CommandName::Underline)
1350 .is_none_or(|value| value != "underline")
1351 {
1352 CssPropertyName::TextDecoration.set_for_element(
1353 cx,
1354 new_parent_html_element,
1355 new_value.clone(),
1356 );
1357 }
1358 },
1359 _ => {},
1360 }
1361 // Step 20. Append node to new parent as its last child, preserving ranges.
1362 let new_parent = new_parent.upcast::<Node>();
1363 move_preserving_ranges(cx, self, |cx| new_parent.AppendChild(cx, self));
1364 // Step 21. If node is an Element and the effective command value of command for node is not loosely equivalent to new value:
1365 if self.is::<Element>() &&
1366 !command.are_loosely_equivalent_values(
1367 self.effective_command_value(command).as_ref(),
1368 Some(new_value),
1369 )
1370 {
1371 // Step 21.1. Insert node into the parent of new parent before new parent, preserving ranges.
1372 let parent_of_new_parent = new_parent.GetParentNode().expect("Must have a parent");
1373 move_preserving_ranges(cx, self, |cx| {
1374 parent_of_new_parent.InsertBefore(cx, self, Some(new_parent))
1375 });
1376 // Step 21.2. Remove new parent from its parent.
1377 new_parent.remove_self(cx);
1378 // Step 21.3. Let children be all children of node,
1379 // omitting any that are Elements whose specified command value for command is neither null nor equivalent to new value.
1380 let children = self
1381 .children()
1382 .filter(|child| {
1383 !child.downcast::<Element>().is_some_and(|child_element| {
1384 let specified_command_value =
1385 child_element.specified_command_value(command);
1386 specified_command_value.is_some() &&
1387 !command.are_equivalent_values(
1388 specified_command_value.as_ref(),
1389 Some(new_value),
1390 )
1391 })
1392 })
1393 .collect::<Vec<DomRoot<Node>>>();
1394 // Step 21.4. Force the value of each node in children,
1395 // with command and new value as in this invocation of the algorithm.
1396 for child in children {
1397 child.force_the_value(cx, command, Some(new_value));
1398 }
1399 }
1400 }
1401
1402 /// <https://w3c.github.io/editing/docs/execCommand/#in-the-same-editing-host>
1403 pub(crate) fn same_editing_host(&self, other: &Node) -> bool {
1404 // > 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.
1405 self.editing_host_of()
1406 .is_some_and(|editing_host| other.editing_host_of() == Some(editing_host))
1407 }
1408
1409 /// <https://w3c.github.io/editing/docs/execCommand/#block-node>
1410 pub(crate) fn is_block_node(&self) -> bool {
1411 // > A block node is either an Element whose "display" property does not have resolved value "inline" or "inline-block" or "inline-table" or "none",
1412 if self
1413 .downcast::<Element>()
1414 .and_then(Element::resolved_display_value)
1415 .is_some_and(|display| {
1416 display != DisplayOutside::Inline && display != DisplayOutside::None
1417 })
1418 {
1419 return true;
1420 }
1421 // > or a document, or a DocumentFragment.
1422 matches!(
1423 self.type_id(),
1424 NodeTypeId::Document(_) | NodeTypeId::DocumentFragment(_)
1425 )
1426 }
1427
1428 /// <https://w3c.github.io/editing/docs/execCommand/#inline-node>
1429 pub(crate) fn is_inline_node(&self) -> bool {
1430 // > An inline node is a node that is not a block node.
1431 !self.is_block_node()
1432 }
1433
1434 /// <https://w3c.github.io/editing/docs/execCommand/#block-node-of>
1435 pub(crate) fn block_node_of(&self) -> Option<DomRoot<Node>> {
1436 let mut node = DomRoot::from_ref(self);
1437
1438 loop {
1439 // Step 1. While node is an inline node, set node to its parent.
1440 if node.is_inline_node() {
1441 node = node.GetParentNode()?;
1442 continue;
1443 }
1444 // Step 2. Return node.
1445 return Some(node);
1446 }
1447 }
1448
1449 /// <https://w3c.github.io/editing/docs/execCommand/#visible>
1450 pub(crate) fn is_visible(&self) -> bool {
1451 for parent in self.inclusive_ancestors(ShadowIncluding::No) {
1452 // > excluding any node with an inclusive ancestor Element whose "display" property has resolved value "none".
1453 if parent
1454 .downcast::<Element>()
1455 .and_then(Element::resolved_display_value)
1456 .is_some_and(|display| display == DisplayOutside::None)
1457 {
1458 return false;
1459 }
1460 }
1461 // > Something is visible if it is a node that either is a block node,
1462 if self.is_block_node() {
1463 return true;
1464 }
1465 // > or a Text node that is not a collapsed whitespace node,
1466 if self
1467 .downcast::<Text>()
1468 .is_some_and(|text| !text.is_collapsed_whitespace_node())
1469 {
1470 return true;
1471 }
1472 // > or an img, or a br that is not an extraneous line break, or any node with a visible descendant;
1473 if self.is::<HTMLImageElement>() {
1474 return true;
1475 }
1476 if self
1477 .downcast::<HTMLBRElement>()
1478 .is_some_and(|br| !br.is_extraneous_line_break())
1479 {
1480 return true;
1481 }
1482 for child in self.children() {
1483 if child.is_visible() {
1484 return true;
1485 }
1486 }
1487 false
1488 }
1489
1490 /// <https://w3c.github.io/editing/docs/execCommand/#invisible>
1491 pub(crate) fn is_invisible(&self) -> bool {
1492 // > Something is invisible if it is a node that is not visible.
1493 !self.is_visible()
1494 }
1495
1496 /// <https://w3c.github.io/editing/docs/execCommand/#formattable-node>
1497 pub(crate) fn is_formattable(&self) -> bool {
1498 // > A formattable node is an editable visible node that is either a Text node, an img, or a br.
1499 self.is_editable() &&
1500 self.is_visible() &&
1501 (self.is::<Text>() || self.is::<HTMLImageElement>() || self.is::<HTMLBRElement>())
1502 }
1503
1504 /// <https://w3c.github.io/editing/docs/execCommand/#block-start-point>
1505 fn is_block_start_point(&self, offset: usize) -> bool {
1506 // > A boundary point (node, offset) is a block start point if either node's parent is null and offset is zero;
1507 if offset == 0 {
1508 return self.GetParentNode().is_none();
1509 }
1510 // > or node has a child with index offset − 1, and that child is either a visible block node or a visible br.
1511 self.children().nth(offset - 1).is_some_and(|child| {
1512 child.is_visible() && (child.is_block_node() || child.is::<HTMLBRElement>())
1513 })
1514 }
1515
1516 /// <https://w3c.github.io/editing/docs/execCommand/#block-end-point>
1517 fn is_block_end_point(&self, offset: u32) -> bool {
1518 // > A boundary point (node, offset) is a block end point if either node's parent is null and offset is node's length;
1519 if self.GetParentNode().is_none() && offset == self.len() {
1520 return true;
1521 }
1522 // > or node has a child with index offset, and that child is a visible block node.
1523 self.children()
1524 .nth(offset as usize)
1525 .is_some_and(|child| child.is_visible() && child.is_block_node())
1526 }
1527
1528 /// <https://w3c.github.io/editing/docs/execCommand/#block-boundary-point>
1529 fn is_block_boundary_point(&self, offset: u32) -> bool {
1530 // > A boundary point is a block boundary point if it is either a block start point or a block end point.
1531 self.is_block_start_point(offset as usize) || self.is_block_end_point(offset)
1532 }
1533
1534 /// <https://w3c.github.io/editing/docs/execCommand/#collapsed-block-prop>
1535 pub(crate) fn is_collapsed_block_prop(&self) -> bool {
1536 // > A collapsed block prop is either a collapsed line break that is not an extraneous line break,
1537
1538 // TODO: Check for collapsed line break
1539 if self
1540 .downcast::<HTMLBRElement>()
1541 .is_some_and(|br| !br.is_extraneous_line_break())
1542 {
1543 return true;
1544 }
1545 // > or an Element that is an inline node and whose children are all either invisible or collapsed block props
1546 if !self.is::<Element>() {
1547 return false;
1548 };
1549 if !self.is_inline_node() {
1550 return false;
1551 }
1552 let mut at_least_one_collapsed_block_prop = false;
1553 for child in self.children() {
1554 if child.is_collapsed_block_prop() {
1555 at_least_one_collapsed_block_prop = true;
1556 continue;
1557 }
1558 if child.is_invisible() {
1559 continue;
1560 }
1561
1562 return false;
1563 }
1564 // > and that has at least one child that is a collapsed block prop.
1565 at_least_one_collapsed_block_prop
1566 }
1567
1568 /// <https://w3c.github.io/editing/docs/execCommand/#follows-a-line-break>
1569 fn follows_a_line_break(&self) -> bool {
1570 // Step 1. Let offset be zero.
1571 let mut offset = 0;
1572 // Step 2. While (node, offset) is not a block boundary point:
1573 let mut node = DomRoot::from_ref(self);
1574 while !node.is_block_boundary_point(offset) {
1575 // Step 2.2. If offset is zero or node has no children, set offset to node's index, then set node to its parent.
1576 if offset == 0 || node.children_count() == 0 {
1577 offset = node.index();
1578 node = node.GetParentNode().expect("Must always have a parent");
1579 continue;
1580 }
1581 // Step 2.1. If node has a visible child with index offset minus one, return false.
1582 let child = node.children().nth(offset as usize - 1);
1583 let Some(child) = child else {
1584 return false;
1585 };
1586 if child.is_visible() {
1587 return false;
1588 }
1589 // Step 2.3. Otherwise, set node to its child with index offset minus one, then set offset to node's length.
1590 node = child;
1591 offset = node.len();
1592 }
1593 // Step 3. Return true.
1594 true
1595 }
1596
1597 /// <https://w3c.github.io/editing/docs/execCommand/#precedes-a-line-break>
1598 fn precedes_a_line_break(&self) -> bool {
1599 let mut node = DomRoot::from_ref(self);
1600 // Step 1. Let offset be node's length.
1601 let mut offset = node.len();
1602 // Step 2. While (node, offset) is not a block boundary point:
1603 while !node.is_block_boundary_point(offset) {
1604 // Step 2.1. If node has a visible child with index offset, return false.
1605 if node
1606 .children()
1607 .nth(offset as usize)
1608 .is_some_and(|child| child.is_visible())
1609 {
1610 return false;
1611 }
1612 // 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.
1613 if offset == node.len() || node.children_count() == 0 {
1614 offset = 1 + node.index();
1615 node = node.GetParentNode().expect("Must always have a parent");
1616 continue;
1617 }
1618 // Step 2.3. Otherwise, set node to its child with index offset and set offset to zero.
1619 let child = node.children().nth(offset as usize);
1620 node = match child {
1621 None => return false,
1622 Some(child) => child,
1623 };
1624 offset = 0;
1625 }
1626 // Step 3. Return true.
1627 true
1628 }
1629
1630 /// <https://w3c.github.io/editing/docs/execCommand/#canonical-space-sequence>
1631 fn canonical_space_sequence(
1632 n: usize,
1633 non_breaking_start: bool,
1634 non_breaking_end: bool,
1635 ) -> String {
1636 let mut n = n;
1637 // Step 1. If n is zero, return the empty string.
1638 if n == 0 {
1639 return String::new();
1640 }
1641 // Step 2. If n is one and both non-breaking start and non-breaking end are false, return a single space (U+0020).
1642 if n == 1 {
1643 if !non_breaking_start && !non_breaking_end {
1644 return "\u{0020}".to_owned();
1645 }
1646 // Step 3. If n is one, return a single non-breaking space (U+00A0).
1647 return "\u{00A0}".to_owned();
1648 }
1649 // Step 4. Let buffer be the empty string.
1650 let mut buffer = String::new();
1651 // Step 5. If non-breaking start is true, let repeated pair be U+00A0 U+0020. Otherwise, let it be U+0020 U+00A0.
1652 let repeated_pair = if non_breaking_start {
1653 "\u{00A0}\u{0020}"
1654 } else {
1655 "\u{0020}\u{00A0}"
1656 };
1657 // Step 6. While n is greater than three, append repeated pair to buffer and subtract two from n.
1658 while n > 3 {
1659 buffer.push_str(repeated_pair);
1660 n -= 2;
1661 }
1662 // Step 7. If n is three, append a three-code unit string to buffer depending on non-breaking start and non-breaking end:
1663 if n == 3 {
1664 buffer.push_str(match (non_breaking_start, non_breaking_end) {
1665 (false, false) => "\u{0020}\u{00A0}\u{0020}",
1666 (true, false) => "\u{00A0}\u{00A0}\u{0020}",
1667 (false, true) => "\u{0020}\u{00A0}\u{00A0}",
1668 (true, true) => "\u{00A0}\u{0020}\u{00A0}",
1669 });
1670 } else {
1671 // Step 8. Otherwise, append a two-code unit string to buffer depending on non-breaking start and non-breaking end:
1672 buffer.push_str(match (non_breaking_start, non_breaking_end) {
1673 (false, false) | (true, false) => "\u{00A0}\u{0020}",
1674 (false, true) => "\u{0020}\u{00A0}",
1675 (true, true) => "\u{00A0}\u{00A0}",
1676 });
1677 }
1678 // Step 9. Return buffer.
1679 buffer
1680 }
1681
1682 /// <https://w3c.github.io/editing/docs/execCommand/#canonicalize-whitespace>
1683 pub(crate) fn canonicalize_whitespace(&self, offset: u32, fix_collapsed_space: bool) {
1684 // Step 1. If node is neither editable nor an editing host, abort these steps.
1685 if !self.is_editable_or_editing_host() {
1686 return;
1687 }
1688 // Step 2. Let start node equal node and let start offset equal offset.
1689 let mut start_node = DomRoot::from_ref(self);
1690 let mut start_offset = offset;
1691 // Step 3. Repeat the following steps:
1692 loop {
1693 // Step 3.1. If start node has a child in the same editing host with index start offset minus one,
1694 // set start node to that child, then set start offset to start node's length.
1695 if start_offset > 0 {
1696 let child = start_node.children().nth(start_offset as usize - 1);
1697 if let Some(child) = child {
1698 if start_node.same_editing_host(&child) {
1699 start_node = child;
1700 start_offset = start_node.len();
1701 continue;
1702 }
1703 };
1704 }
1705 // Step 3.2. Otherwise, if start offset is zero and start node does not follow a line break
1706 // and start node's parent is in the same editing host, set start offset to start node's index,
1707 // then set start node to its parent.
1708 if start_offset == 0 && !start_node.follows_a_line_break() {
1709 if let Some(parent) = start_node.GetParentNode() {
1710 if parent.same_editing_host(&start_node) {
1711 start_offset = start_node.index();
1712 start_node = parent;
1713 }
1714 }
1715 }
1716 // Step 3.3. Otherwise, if start node is a Text node and its parent's resolved
1717 // value for "white-space" is neither "pre" nor "pre-wrap" and start offset is not zero
1718 // and the (start offset − 1)st code unit of start node's data is a space (0x0020) or
1719 // non-breaking space (0x00A0), subtract one from start offset.
1720 if start_offset != 0 &&
1721 start_node.downcast::<Text>().is_some_and(|text| {
1722 text.has_whitespace_and_has_parent_with_whitespace_preserve(
1723 start_offset - 1,
1724 &[&'\u{0020}', &'\u{00A0}'],
1725 )
1726 })
1727 {
1728 start_offset -= 1;
1729 }
1730 // Step 3.4. Otherwise, break from this loop.
1731 break;
1732 }
1733 // Step 4. Let end node equal start node and end offset equal start offset.
1734 let mut end_node = start_node.clone();
1735 let mut end_offset = start_offset;
1736 // Step 5. Let length equal zero.
1737 let mut length = 0;
1738 // Step 6. Let collapse spaces be true if start offset is zero and start node follows a line break, otherwise false.
1739 let mut collapse_spaces = start_offset == 0 && start_node.follows_a_line_break();
1740 // Step 7. Repeat the following steps:
1741 loop {
1742 // Step 7.1. If end node has a child in the same editing host with index end offset,
1743 // set end node to that child, then set end offset to zero.
1744 if let Some(child) = end_node.children().nth(end_offset as usize) {
1745 if child.same_editing_host(&end_node) {
1746 end_node = child;
1747 end_offset = 0;
1748 continue;
1749 }
1750 }
1751 // Step 7.2. Otherwise, if end offset is end node's length
1752 // and end node does not precede a line break
1753 // and end node's parent is in the same editing host,
1754 // set end offset to one plus end node's index, then set end node to its parent.
1755 if end_offset == end_node.len() && !end_node.precedes_a_line_break() {
1756 if let Some(parent) = end_node.GetParentNode() {
1757 if parent.same_editing_host(&end_node) {
1758 end_offset = 1 + end_node.index();
1759 end_node = parent;
1760 }
1761 }
1762 continue;
1763 }
1764 // Step 7.3. Otherwise, if end node is a Text node and its parent's resolved value for "white-space"
1765 // is neither "pre" nor "pre-wrap"
1766 // and end offset is not end node's length and the end offsetth code unit of end node's data
1767 // is a space (0x0020) or non-breaking space (0x00A0):
1768 if let Some(text) = end_node.downcast::<Text>() {
1769 if text.has_whitespace_and_has_parent_with_whitespace_preserve(
1770 end_offset,
1771 &[&'\u{0020}', &'\u{00A0}'],
1772 ) {
1773 // Step 7.3.1. If fix collapsed space is true, and collapse spaces is true,
1774 // and the end offsetth code unit of end node's data is a space (0x0020):
1775 // call deleteData(end offset, 1) on end node, then continue this loop from the beginning.
1776 let has_space_at_offset = text
1777 .data()
1778 .chars()
1779 .nth(end_offset as usize)
1780 .is_some_and(|c| c == '\u{0020}');
1781 if fix_collapsed_space && collapse_spaces && has_space_at_offset {
1782 if text
1783 .upcast::<CharacterData>()
1784 .DeleteData(end_offset, 1)
1785 .is_err()
1786 {
1787 unreachable!("Invalid deletion for character at end offset");
1788 }
1789 continue;
1790 }
1791 // Step 7.3.2. Set collapse spaces to true if the end offsetth code unit of
1792 // end node's data is a space (0x0020), false otherwise.
1793 collapse_spaces = text
1794 .data()
1795 .chars()
1796 .nth(end_offset as usize)
1797 .is_some_and(|c| c == '\u{0020}');
1798 // Step 7.3.3. Add one to end offset.
1799 end_offset += 1;
1800 // Step 7.3.4. Add one to length.
1801 length += 1;
1802 continue;
1803 }
1804 }
1805 // Step 7.4. Otherwise, break from this loop.
1806 break;
1807 }
1808 // Step 8. If fix collapsed space is true, then while (start node, start offset)
1809 // is before (end node, end offset):
1810 if fix_collapsed_space {
1811 while bp_position(&start_node, start_offset, &end_node, end_offset) ==
1812 Some(Ordering::Less)
1813 {
1814 // Step 8.1. If end node has a child in the same editing host with index end offset − 1,
1815 // set end node to that child, then set end offset to end node's length.
1816 if end_offset > 0 {
1817 if let Some(child) = end_node.children().nth(end_offset as usize - 1) {
1818 if child.same_editing_host(&end_node) {
1819 end_node = child;
1820 end_offset = end_node.len();
1821 continue;
1822 }
1823 }
1824 }
1825 // Step 8.2. Otherwise, if end offset is zero and end node's parent is in the same editing host,
1826 // set end offset to end node's index, then set end node to its parent.
1827 if let Some(parent) = end_node.GetParentNode() {
1828 if end_offset == 0 && parent.same_editing_host(&end_node) {
1829 end_offset = end_node.index();
1830 end_node = parent;
1831 continue;
1832 }
1833 }
1834 // Step 8.3. Otherwise, if end node is a Text node and its parent's resolved value for "white-space"
1835 // is neither "pre" nor "pre-wrap"
1836 // and end offset is end node's length and the last code unit of end node's data
1837 // is a space (0x0020) and end node precedes a line break:
1838 if let Some(text) = end_node.downcast::<Text>() {
1839 if text.has_whitespace_and_has_parent_with_whitespace_preserve(
1840 text.data().len() as u32,
1841 &[&'\u{0020}'],
1842 ) && end_node.precedes_a_line_break()
1843 {
1844 // Step 8.3.1. Subtract one from end offset.
1845 end_offset -= 1;
1846 // Step 8.3.2. Subtract one from length.
1847 length -= 1;
1848 // Step 8.3.3. Call deleteData(end offset, 1) on end node.
1849 if text
1850 .upcast::<CharacterData>()
1851 .DeleteData(end_offset, 1)
1852 .is_err()
1853 {
1854 unreachable!("Invalid deletion for character at end offset");
1855 }
1856 continue;
1857 }
1858 }
1859 // Step 8.4. Otherwise, break from this loop.
1860 break;
1861 }
1862 }
1863 // Step 9. Let replacement whitespace be the canonical space sequence of length length.
1864 // non-breaking start is true if start offset is zero and start node follows a line break, and false otherwise.
1865 // non-breaking end is true if end offset is end node's length and end node precedes a line break, and false otherwise.
1866 let replacement_whitespace = Node::canonical_space_sequence(
1867 length,
1868 start_offset == 0 && start_node.follows_a_line_break(),
1869 end_offset == end_node.len() && end_node.precedes_a_line_break(),
1870 );
1871 let mut replacement_whitespace_chars = replacement_whitespace.chars();
1872 // Step 10. While (start node, start offset) is before (end node, end offset):
1873 while bp_position(&start_node, start_offset, &end_node, end_offset) == Some(Ordering::Less)
1874 {
1875 // 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.
1876 if let Some(child) = start_node.children().nth(start_offset as usize) {
1877 start_node = child;
1878 start_offset = 0;
1879 continue;
1880 }
1881 // Step 10.2. Otherwise, if start node is not a Text node or if start offset is start node's length,
1882 // set start offset to one plus start node's index, then set start node to its parent.
1883 let start_node_as_text = start_node.downcast::<Text>();
1884 if start_node_as_text.is_none() || start_offset == start_node.len() {
1885 start_offset = 1 + start_node.index();
1886 start_node = start_node
1887 .GetParentNode()
1888 .expect("Must always have a parent");
1889 continue;
1890 }
1891 let start_node_as_text =
1892 start_node_as_text.expect("Already verified none in previous statement");
1893 // Step 10.3. Otherwise:
1894 // Step 10.3.1. Remove the first code unit from replacement whitespace, and let element be that code unit.
1895 if let Some(element) = replacement_whitespace_chars.next() {
1896 // Step 10.3.2. If element is not the same as the start offsetth code unit of start node's data:
1897 if start_node_as_text.data().chars().nth(start_offset as usize) != Some(element) {
1898 let character_data = start_node_as_text.upcast::<CharacterData>();
1899 // Step 10.3.2.1. Call insertData(start offset, element) on start node.
1900 if character_data
1901 .InsertData(start_offset, element.to_string().into())
1902 .is_err()
1903 {
1904 unreachable!("Invalid insertion for character at start offset");
1905 }
1906 // Step 10.3.2.2. Call deleteData(start offset + 1, 1) on start node.
1907 if character_data.DeleteData(start_offset + 1, 1).is_err() {
1908 unreachable!("Invalid deletion for character at start offset + 1");
1909 }
1910 }
1911 }
1912 // Step 10.3.3. Add one to start offset.
1913 start_offset += 1;
1914 }
1915 }
1916
1917 /// <https://w3c.github.io/editing/docs/execCommand/#remove-extraneous-line-breaks-before>
1918 fn remove_extraneous_line_breaks_before(&self, cx: &mut JSContext) {
1919 let parent = self.GetParentNode();
1920 // Step 1. Let ref be the previousSibling of node.
1921 let Some(mut ref_) = self.GetPreviousSibling() else {
1922 // Step 2. If ref is null, abort these steps.
1923 return;
1924 };
1925 // Step 3. While ref has children, set ref to its lastChild.
1926 while let Some(last_child) = ref_.children().last() {
1927 ref_ = last_child;
1928 }
1929 // Step 4. While ref is invisible but not an extraneous line break,
1930 // and ref does not equal node's parent, set ref to the node before it in tree order.
1931 loop {
1932 if ref_.is_invisible() &&
1933 ref_.downcast::<HTMLBRElement>()
1934 .is_none_or(|br| !br.is_extraneous_line_break())
1935 {
1936 if let Some(parent) = parent.as_ref() {
1937 if ref_ != *parent {
1938 ref_ = match ref_.preceding_nodes(parent).nth(0) {
1939 None => break,
1940 Some(node) => node,
1941 };
1942 continue;
1943 }
1944 }
1945 }
1946 break;
1947 }
1948 // Step 5. If ref is an editable extraneous line break, remove it from its parent.
1949 if ref_.is_editable() &&
1950 ref_.downcast::<HTMLBRElement>()
1951 .is_some_and(|br| br.is_extraneous_line_break())
1952 {
1953 assert!(ref_.has_parent());
1954 ref_.remove_self(cx);
1955 }
1956 }
1957
1958 /// <https://w3c.github.io/editing/docs/execCommand/#remove-extraneous-line-breaks-at-the-end-of>
1959 pub(crate) fn remove_extraneous_line_breaks_at_the_end_of(&self, cx: &mut JSContext) {
1960 // Step 1. Let ref be node.
1961 let mut ref_ = DomRoot::from_ref(self);
1962 // Step 2. While ref has children, set ref to its lastChild.
1963 while let Some(last_child) = ref_.children().last() {
1964 ref_ = last_child;
1965 }
1966 // Step 3. While ref is invisible but not an extraneous line break, and ref does not equal node,
1967 // set ref to the node before it in tree order.
1968 loop {
1969 if ref_.is_invisible() &&
1970 *ref_ != *self &&
1971 ref_.downcast::<HTMLBRElement>()
1972 .is_none_or(|br| !br.is_extraneous_line_break())
1973 {
1974 if let Some(parent_of_ref) = ref_.GetParentNode() {
1975 ref_ = match ref_.preceding_nodes(&parent_of_ref).nth(0) {
1976 None => break,
1977 Some(node) => node,
1978 };
1979 continue;
1980 }
1981 }
1982 break;
1983 }
1984 // Step 4. If ref is an editable extraneous line break:
1985 if ref_.is_editable() &&
1986 ref_.downcast::<HTMLBRElement>()
1987 .is_some_and(|br| br.is_extraneous_line_break())
1988 {
1989 // Step 4.1. While ref's parent is editable and invisible, set ref to its parent.
1990 loop {
1991 if let Some(parent) = ref_.GetParentNode() {
1992 if parent.is_editable() && parent.is_invisible() {
1993 ref_ = parent;
1994 continue;
1995 }
1996 }
1997 break;
1998 }
1999 // Step 4.2. Remove ref from its parent.
2000 assert!(ref_.has_parent());
2001 ref_.remove_self(cx);
2002 }
2003 }
2004
2005 /// <https://w3c.github.io/editing/docs/execCommand/#remove-extraneous-line-breaks-from>
2006 fn remove_extraneous_line_breaks_from(&self, cx: &mut JSContext) {
2007 // > To remove extraneous line breaks from a node, first remove extraneous line breaks before it,
2008 // > then remove extraneous line breaks at the end of it.
2009 self.remove_extraneous_line_breaks_before(cx);
2010 self.remove_extraneous_line_breaks_at_the_end_of(cx);
2011 }
2012
2013 /// <https://w3c.github.io/editing/docs/execCommand/#preserving-its-descendants>
2014 pub(crate) fn remove_preserving_its_descendants(&self, cx: &mut JSContext) {
2015 // > To remove a node node while preserving its descendants,
2016 // > split the parent of node's children if it has any.
2017 // > If it has no children, instead remove it from its parent.
2018 if self.children_count() == 0 {
2019 assert!(self.has_parent());
2020 self.remove_self(cx);
2021 } else {
2022 rooted_vec!(let children <- self.children().map(|child| DomRoot::as_traced(&child)));
2023 split_the_parent(cx, children.r());
2024 }
2025 }
2026
2027 /// <https://w3c.github.io/editing/docs/execCommand/#effective-command-value>
2028 pub(crate) fn effective_command_value(&self, command: &CommandName) -> Option<DOMString> {
2029 // Step 1. If neither node nor its parent is an Element, return null.
2030 // Step 2. If node is not an Element, return the effective command value of its parent for command.
2031 let Some(element) = self.downcast::<Element>() else {
2032 return self
2033 .GetParentElement()
2034 .and_then(|parent| parent.upcast::<Node>().effective_command_value(command));
2035 };
2036 match command {
2037 // Step 3. If command is "createLink" or "unlink":
2038 CommandName::CreateLink | CommandName::Unlink => {
2039 // Step 3.1. While node is not null, and is not an a element that has an href attribute, set node to its parent.
2040 let mut current_node = Some(DomRoot::from_ref(self));
2041 while let Some(node) = current_node {
2042 if let Some(anchor_value) =
2043 node.downcast::<HTMLAnchorElement>().and_then(|anchor| {
2044 anchor
2045 .upcast::<Element>()
2046 .get_attribute(&local_name!("href"))
2047 })
2048 {
2049 // Step 3.3. Return the value of node's href attribute.
2050 return Some(DOMString::from(&**anchor_value.value()));
2051 }
2052 current_node = node.GetParentNode();
2053 }
2054 // Step 3.2. If node is null, return null.
2055 None
2056 },
2057 // Step 4. If command is "backColor" or "hiliteColor":
2058 CommandName::BackColor | CommandName::HiliteColor => {
2059 // Step 4.1. While the resolved value of "background-color" on node is any fully transparent value,
2060 // and node's parent is an Element, set node to its parent.
2061 // TODO
2062 // Step 4.2. Return the resolved value of "background-color" for node.
2063 // TODO
2064 None
2065 },
2066 // Step 5. If command is "subscript" or "superscript":
2067 CommandName::Subscript | CommandName::Superscript => {
2068 // Step 5.1. Let affected by subscript and affected by superscript be two boolean variables,
2069 // both initially false.
2070 let mut affected_by_subscript = false;
2071 let mut affected_by_superscript = false;
2072 // Step 5.2. While node is an inline node:
2073 let mut current_node = Some(DomRoot::from_ref(self));
2074 while let Some(node) = current_node {
2075 if !node.is_inline_node() {
2076 break;
2077 }
2078 // Step 5.2.1. If node is a sub, set affected by subscript to true.
2079 if *element.local_name() == local_name!("sub") {
2080 affected_by_subscript = true;
2081 } else if *element.local_name() == local_name!("sup") {
2082 // Step 5.2.2. Otherwise, if node is a sup, set affected by superscript to true.
2083 affected_by_superscript = true;
2084 }
2085 // Step 5.2.3. Set node to its parent.
2086 current_node = node.GetParentNode();
2087 }
2088 Some(match (affected_by_subscript, affected_by_superscript) {
2089 // Step 5.3. If affected by subscript and affected by superscript are both true,
2090 // return the string "mixed".
2091 (true, true) => "mixed".into(),
2092 // Step 5.4. If affected by subscript is true, return "subscript".
2093 (true, false) => "subscript".into(),
2094 // Step 5.5. If affected by superscript is true, return "superscript".
2095 (false, true) => "superscript".into(),
2096 // Step 5.6. Return null.
2097 (false, false) => return None,
2098 })
2099 },
2100 // Step 6. If command is "strikethrough",
2101 // and the "text-decoration" property of node or any of its ancestors has resolved value containing "line-through",
2102 // return "line-through". Otherwise, return null.
2103 CommandName::Strikethrough => Some("line-through".into()).filter(|_| {
2104 self.inclusive_ancestors(ShadowIncluding::No).any(|node| {
2105 node.downcast::<Element>()
2106 .and_then(|element| {
2107 CssPropertyName::TextDecorationLine.resolved_value_for_node(element)
2108 })
2109 .is_some_and(|property| property.contains("line-through"))
2110 })
2111 }),
2112 // Step 7. If command is "underline",
2113 // and the "text-decoration" property of node or any of its ancestors has resolved value containing "underline",
2114 // return "underline". Otherwise, return null.
2115 CommandName::Underline => Some("underline".into()).filter(|_| {
2116 self.inclusive_ancestors(ShadowIncluding::No).any(|node| {
2117 node.downcast::<Element>()
2118 .and_then(|element| {
2119 CssPropertyName::TextDecorationLine.resolved_value_for_node(element)
2120 })
2121 .is_some_and(|property| property.contains("underline"))
2122 })
2123 }),
2124 // Step 8. Return the resolved value for node of the relevant CSS property for command.
2125 _ => command.resolved_value_for_node(element),
2126 }
2127 }
2128}