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