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