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