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