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