script/dom/execcommand/contenteditable.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 html5ever::local_name;
9use script_bindings::inheritance::Castable;
10use style::attr::AttrValue;
11use style::computed_values::white_space_collapse::T as WhiteSpaceCollapse;
12use style::properties::PropertyDeclarationId;
13use style::properties::generated::{LonghandId, PropertyDeclaration};
14use style::values::specified::box_::DisplayOutside;
15use style::values::specified::text::TextDecorationLine;
16
17use crate::dom::abstractrange::bp_position;
18use crate::dom::bindings::cell::Ref;
19use crate::dom::bindings::codegen::Bindings::CharacterDataBinding::CharacterDataMethods;
20use crate::dom::bindings::codegen::Bindings::DocumentBinding::{
21 DocumentMethods, ElementCreationOptions,
22};
23use crate::dom::bindings::codegen::Bindings::HTMLElementBinding::HTMLElementMethods;
24use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods;
25use crate::dom::bindings::codegen::Bindings::RangeBinding::RangeMethods;
26use crate::dom::bindings::codegen::Bindings::SelectionBinding::SelectionMethods;
27use crate::dom::bindings::codegen::Bindings::TextBinding::TextMethods;
28use crate::dom::bindings::codegen::UnionTypes::StringOrElementCreationOptions;
29use crate::dom::bindings::error::Fallible;
30use crate::dom::bindings::inheritance::{ElementTypeId, HTMLElementTypeId, NodeTypeId};
31use crate::dom::bindings::root::{DomRoot, DomSlice};
32use crate::dom::bindings::str::DOMString;
33use crate::dom::characterdata::CharacterData;
34use crate::dom::document::Document;
35use crate::dom::element::Element;
36use crate::dom::execcommand::basecommand::{CommandName, CssPropertyName};
37use crate::dom::execcommand::commands::fontsize::{font_size_to_css_font, legacy_font_size_for};
38use crate::dom::html::htmlanchorelement::HTMLAnchorElement;
39use crate::dom::html::htmlbrelement::HTMLBRElement;
40use crate::dom::html::htmlelement::HTMLElement;
41use crate::dom::html::htmlfontelement::HTMLFontElement;
42use crate::dom::html::htmlimageelement::HTMLImageElement;
43use crate::dom::html::htmllielement::HTMLLIElement;
44use crate::dom::html::htmltablecellelement::HTMLTableCellElement;
45use crate::dom::html::htmltablerowelement::HTMLTableRowElement;
46use crate::dom::html::htmltablesectionelement::HTMLTableSectionElement;
47use crate::dom::node::{Node, NodeTraits, ShadowIncluding};
48use crate::dom::range::Range;
49use crate::dom::selection::Selection;
50use crate::dom::text::Text;
51use crate::script_runtime::CanGc;
52
53impl Text {
54 /// <https://dom.spec.whatwg.org/#concept-cd-data>
55 fn data(&self) -> Ref<'_, String> {
56 self.upcast::<CharacterData>().data()
57 }
58
59 /// <https://w3c.github.io/editing/docs/execCommand/#whitespace-node>
60 fn is_whitespace_node(&self) -> bool {
61 // > A whitespace node is either a Text node whose data is the empty string;
62 let data = self.data();
63 if data.is_empty() {
64 return true;
65 }
66 // > or a Text node whose data consists only of one or more tabs (0x0009), line feeds (0x000A),
67 // > carriage returns (0x000D), and/or spaces (0x0020),
68 // > and whose parent is an Element whose resolved value for "white-space" is "normal" or "nowrap";
69 let Some(parent) = self.upcast::<Node>().GetParentElement() else {
70 return false;
71 };
72 // TODO: Optimize the below to only do a traversal once and in the match handle the expected collapse value
73 let Some(style) = parent.style() else {
74 return false;
75 };
76 let white_space_collapse = style.get_inherited_text().white_space_collapse;
77 if data
78 .bytes()
79 .all(|byte| matches!(byte, b'\t' | b'\n' | b'\r' | b' ')) &&
80 // Note that for "normal" and "nowrap", the longhand "white-space-collapse: collapse" applies
81 // https://www.w3.org/TR/css-text-4/#white-space-property
82 white_space_collapse == WhiteSpaceCollapse::Collapse
83 {
84 return true;
85 }
86 // > or a Text node whose data consists only of one or more tabs (0x0009), carriage returns (0x000D),
87 // > and/or spaces (0x0020), and whose parent is an Element whose resolved value for "white-space" is "pre-line".
88 data.bytes()
89 .all(|byte| matches!(byte, b'\t' | b'\r' | b' ')) &&
90 // Note that for "pre-line", the longhand "white-space-collapse: preserve-breaks" applies
91 // https://www.w3.org/TR/css-text-4/#white-space-property
92 white_space_collapse == WhiteSpaceCollapse::PreserveBreaks
93 }
94
95 /// <https://w3c.github.io/editing/docs/execCommand/#collapsed-whitespace-node>
96 fn is_collapsed_whitespace_node(&self) -> bool {
97 // Step 1. If node is not a whitespace node, return false.
98 if !self.is_whitespace_node() {
99 return false;
100 }
101 // Step 2. If node's data is the empty string, return true.
102 if self.data().is_empty() {
103 return true;
104 }
105 // Step 3. Let ancestor be node's parent.
106 let node = self.upcast::<Node>();
107 let Some(ancestor) = node.GetParentNode() else {
108 // Step 4. If ancestor is null, return true.
109 return true;
110 };
111 let mut resolved_ancestor = ancestor.clone();
112 for parent in ancestor.ancestors() {
113 // Step 5. If the "display" property of some ancestor of node has resolved value "none", return true.
114 if parent
115 .downcast::<Element>()
116 .is_some_and(Element::is_display_none)
117 {
118 return true;
119 }
120 // Step 6. While ancestor is not a block node and its parent is not null, set ancestor to its parent.
121 //
122 // Note that the spec is written as "while not". Since this is the end-condition, we need to invert
123 // the condition to decide when to stop.
124 if parent.is_block_node() {
125 break;
126 }
127 resolved_ancestor = parent;
128 }
129 // Step 7. Let reference be node.
130 // Step 8. While reference is a descendant of ancestor:
131 // Step 8.1. Let reference be the node before it in tree order.
132 for reference in node.preceding_nodes(&resolved_ancestor) {
133 // Step 8.2. If reference is a block node or a br, return true.
134 if reference.is_block_node() || reference.is::<HTMLBRElement>() {
135 return true;
136 }
137 // Step 8.3. If reference is a Text node that is not a whitespace node, or is an img, break from this loop.
138 if reference
139 .downcast::<Text>()
140 .is_some_and(|text| !text.is_whitespace_node()) ||
141 reference.is::<HTMLImageElement>()
142 {
143 break;
144 }
145 }
146 // Step 9. Let reference be node.
147 // Step 10. While reference is a descendant of ancestor:
148 // Step 10.1. Let reference be the node after it in tree order, or null if there is no such node.
149 for reference in node.following_nodes(&resolved_ancestor) {
150 // Step 10.2. If reference is a block node or a br, return true.
151 if reference.is_block_node() || reference.is::<HTMLBRElement>() {
152 return true;
153 }
154 // Step 10.3. If reference is a Text node that is not a whitespace node, or is an img, break from this loop.
155 if reference
156 .downcast::<Text>()
157 .is_some_and(|text| !text.is_whitespace_node()) ||
158 reference.is::<HTMLImageElement>()
159 {
160 break;
161 }
162 }
163 // Step 11. Return false.
164 false
165 }
166
167 /// Part of <https://w3c.github.io/editing/docs/execCommand/#canonicalize-whitespace>
168 /// and deduplicated here, since we need to do this for both start and end nodes
169 fn has_whitespace_and_has_parent_with_whitespace_preserve(
170 &self,
171 offset: u32,
172 space_characters: &'static [&'static char],
173 ) -> bool {
174 // if node is a Text node and its parent's resolved value for "white-space" is neither "pre" nor "pre-wrap"
175 // and start offset is not zero and the (start offset − 1)st code unit of start node's data is a space (0x0020) or
176 // non-breaking space (0x00A0)
177 let has_preserve_space = self
178 .upcast::<Node>()
179 .GetParentNode()
180 .and_then(|parent_node| parent_node.downcast::<Element>().and_then(Element::style))
181 .is_some_and(|style| {
182 // Note that for "pre" and "pre-wrap", the longhand "white-space-collapse: preserve" applies
183 // https://www.w3.org/TR/css-text-4/#white-space-property
184 style.get_inherited_text().white_space_collapse != WhiteSpaceCollapse::Preserve
185 });
186 let has_space_character = self
187 .data()
188 .chars()
189 .nth(offset as usize)
190 .is_some_and(|c| space_characters.contains(&&c));
191 has_preserve_space && has_space_character
192 }
193}
194
195impl HTMLBRElement {
196 /// <https://w3c.github.io/editing/docs/execCommand/#extraneous-line-break>
197 fn is_extraneous_line_break(&self) -> bool {
198 let node = self.upcast::<Node>();
199 // > An extraneous line break is a br that has no visual effect, in that removing it from the DOM would not change layout,
200 // except that a br that is the sole child of an li is not extraneous.
201 if node
202 .GetParentNode()
203 .filter(|parent| parent.is::<HTMLLIElement>())
204 .is_some_and(|li| li.children_count() == 1)
205 {
206 return false;
207 }
208 // TODO: Figure out what this actually makes it have no visual effect
209 !node.is_block_node()
210 }
211}
212
213impl Document {
214 pub(crate) fn create_element(
215 &self,
216 cx: &mut js::context::JSContext,
217 name: &str,
218 ) -> DomRoot<Element> {
219 let element_options =
220 StringOrElementCreationOptions::ElementCreationOptions(ElementCreationOptions {
221 is: None,
222 });
223 self.CreateElement(cx, name.into(), element_options)
224 .expect("Must always be able to create element")
225 }
226}
227
228impl HTMLElement {
229 fn local_name(&self) -> &str {
230 self.upcast::<Element>().local_name()
231 }
232
233 /// <https://w3c.github.io/editing/docs/execCommand/#clear-the-value>
234 fn clear_the_value(&self, cx: &mut js::context::JSContext, command: &CommandName) {
235 // Step 1. Let command be the current command.
236 //
237 // Passed in as argument
238
239 let node = self.upcast::<Node>();
240 let element = self.upcast::<Element>();
241
242 // Step 2. If element is not editable, return the empty list.
243 if !node.is_editable() {
244 return;
245 }
246 // Step 3. If element's specified command value for command is null,
247 // return the empty list.
248 if element.specified_command_value(command).is_none() {
249 return;
250 }
251 // Step 4. If element is a simple modifiable element:
252 if element.is_simple_modifiable_element() {
253 // Step 4.1. Let children be the children of element.
254 // Step 4.2. For each child in children, insert child into element's parent immediately before element, preserving ranges.
255 let element_parent = node.GetParentNode().expect("Must always have a parent");
256 for child in node.children() {
257 if element_parent.InsertBefore(cx, &child, Some(node)).is_err() {
258 unreachable!("Must always be able to insert");
259 }
260 }
261 // Step 4.3. Remove element from its parent.
262 node.remove_self(cx);
263 // Step 4.4. Return children.
264 return;
265 }
266 match command {
267 // Step 5. If command is "strikethrough", and element has a style attribute
268 // that sets "text-decoration" to some value containing "line-through",
269 // delete "line-through" from the value.
270 CommandName::Strikethrough => {
271 let property = CssPropertyName::TextDecorationLine;
272 if property.value_for_element(cx, self) == "line-through" {
273 // TODO: Only remove line-through
274 property.remove_from_element(cx, self);
275 }
276 },
277 // Step 6. If command is "underline", and element has a style attribute that
278 // sets "text-decoration" to some value containing "underline", delete "underline" from the value.
279 CommandName::Underline => {
280 let property = CssPropertyName::TextDecorationLine;
281 if property.value_for_element(cx, self) == "underline" {
282 // TODO: Only remove underline
283 property.remove_from_element(cx, self);
284 }
285 },
286 _ => {},
287 }
288 // Step 7. If the relevant CSS property for command is not null,
289 // unset that property of element.
290 if let Some(property) = command.relevant_css_property() {
291 property.remove_from_element(cx, self);
292 }
293 // Step 8. If element is a font element:
294 if self.is::<HTMLFontElement>() {
295 match command {
296 // Step 8.1. If command is "foreColor", unset element's color attribute, if set.
297 CommandName::ForeColor => {
298 element.remove_attribute_by_name(&local_name!("color"), CanGc::from_cx(cx));
299 },
300 // Step 8.2. If command is "fontName", unset element's face attribute, if set.
301 CommandName::FontName => {
302 element.remove_attribute_by_name(&local_name!("face"), CanGc::from_cx(cx));
303 },
304 // Step 8.3. If command is "fontSize", unset element's size attribute, if set.
305 CommandName::FontSize => {
306 element.remove_attribute_by_name(&local_name!("size"), CanGc::from_cx(cx));
307 },
308 _ => {},
309 }
310 }
311 // Step 9. If element is an a element and command is "createLink" or "unlink",
312 // unset the href property of element.
313 if self.is::<HTMLAnchorElement>() &&
314 matches!(command, CommandName::CreateLink | CommandName::Unlink)
315 {
316 element.remove_attribute_by_name(&local_name!("href"), CanGc::from_cx(cx));
317 }
318 // Step 10. If element's specified command value for command is null,
319 // return the empty list.
320 if element.specified_command_value(command).is_none() {
321 // TODO
322 }
323 // Step 11. Set the tag name of element to "span",
324 // and return the one-node list consisting of the result.
325 // TODO
326 }
327}
328
329impl Element {
330 fn resolved_display_value(&self) -> Option<DisplayOutside> {
331 self.style().map(|style| style.get_box().display.outside())
332 }
333
334 /// <https://w3c.github.io/editing/docs/execCommand/#specified-command-value>
335 pub(crate) fn specified_command_value(&self, command: &CommandName) -> Option<DOMString> {
336 match command {
337 // Step 1. If command is "backColor" or "hiliteColor" and the Element's display property does not have resolved value "inline", return null.
338 CommandName::BackColor | CommandName::HiliteColor => {
339 // TODO
340 },
341 // Step 2. If command is "createLink" or "unlink":
342 CommandName::CreateLink | CommandName::Unlink => {
343 // TODO
344 },
345 // Step 3. If command is "subscript" or "superscript":
346 CommandName::Subscript | CommandName::Superscript => {
347 // TODO
348 },
349 CommandName::Strikethrough => {
350 // Step 4. If command is "strikethrough", and element has a style attribute set, and that attribute sets "text-decoration":
351 // TODO
352 // Step 5. If command is "strikethrough" and element is an s or strike element, return "line-through".
353 // TODO
354 },
355 CommandName::Underline => {
356 // Step 6. If command is "underline", and element has a style attribute set, and that attribute sets "text-decoration":
357 // TODO
358 // Step 7. If command is "underline" and element is a u element, return "underline".
359 // TODO
360 },
361 _ => {},
362 };
363 // Step 8. Let property be the relevant CSS property for command.
364 // Step 9. If property is null, return null.
365 let property = command.relevant_css_property()?;
366 // Step 10. If element has a style attribute set, and that attribute has the effect of setting property,
367 // return the value that it sets property to.
368 if let Some(value) = property.value_set_for_style(self) {
369 return Some(value);
370 }
371 // Step 11. If element is a font element that has an attribute whose effect is to create a presentational hint for property,
372 // return the value that the hint sets property to. (For a size of 7, this will be the non-CSS value "xxx-large".)
373 if self.is::<HTMLFontElement>() {
374 if let Some(font_size) = self.get_attribute(&local_name!("size")) {
375 if let AttrValue::UInt(_, value) = *font_size.value() {
376 return Some(font_size_to_css_font(&value).into());
377 }
378 }
379 }
380
381 // Step 12. If element is in the following list, and property is equal to the CSS property name listed for it,
382 // return the string listed for it.
383 let element_name = self.local_name();
384 match property {
385 CssPropertyName::FontWeight
386 if element_name == &local_name!("b") || element_name == &local_name!("strong") =>
387 {
388 Some("bold".into())
389 },
390 CssPropertyName::FontStyle
391 if element_name == &local_name!("i") || element_name == &local_name!("em") =>
392 {
393 Some("italic".into())
394 },
395 // Step 13. Return null.
396 _ => None,
397 }
398 }
399
400 /// <https://w3c.github.io/editing/docs/execCommand/#simple-modifiable-element>
401 fn is_simple_modifiable_element(&self) -> bool {
402 let attrs = self.attrs();
403 let attr_count = attrs.len();
404 let type_id = self.upcast::<Node>().type_id();
405
406 if matches!(
407 type_id,
408 NodeTypeId::Element(ElementTypeId::HTMLElement(
409 HTMLElementTypeId::HTMLAnchorElement,
410 )) | NodeTypeId::Element(ElementTypeId::HTMLElement(
411 HTMLElementTypeId::HTMLFontElement,
412 )) | NodeTypeId::Element(ElementTypeId::HTMLElement(
413 HTMLElementTypeId::HTMLSpanElement,
414 ))
415 ) {
416 // > It is an a, b, em, font, i, s, span, strike, strong, sub, sup, or u element with no attributes.
417 //
418 // TODO: All elements that are HTMLElement rather than a specific one
419 if attr_count == 0 {
420 return true;
421 }
422
423 // > It is an a, b, em, font, i, s, span, strike, strong, sub, sup, or u element
424 // > with exactly one attribute, which is style,
425 // > which sets no CSS properties (including invalid or unrecognized properties).
426 if attr_count == 1 &&
427 self.attrs().first().expect("Size is 1").local_name() == &local_name!("style")
428 {
429 let style_attribute = self.style_attribute().borrow();
430 if style_attribute.as_ref().is_some_and(|declarations| {
431 let document = self.owner_document();
432 let shared_lock = document.style_shared_lock();
433 let read_lock = shared_lock.read();
434 let style = declarations.read_with(&read_lock);
435
436 style.is_empty()
437 }) {
438 return true;
439 }
440 }
441 }
442
443 if attr_count != 1 {
444 return false;
445 }
446
447 let only_attribute = attrs.first().expect("Size is 1").local_name();
448
449 // > It is an a element with exactly one attribute, which is href.
450 if matches!(
451 type_id,
452 NodeTypeId::Element(ElementTypeId::HTMLElement(
453 HTMLElementTypeId::HTMLAnchorElement,
454 ))
455 ) {
456 return only_attribute == &local_name!("href");
457 }
458
459 // > It is a font element with exactly one attribute, which is either color, face, or size.
460 if matches!(
461 type_id,
462 NodeTypeId::Element(ElementTypeId::HTMLElement(
463 HTMLElementTypeId::HTMLFontElement,
464 ))
465 ) {
466 return only_attribute == &local_name!("color") ||
467 only_attribute == &local_name!("face") ||
468 only_attribute == &local_name!("size");
469 }
470
471 // > It is a b or strong element with exactly one attribute, which is style,
472 // > and the style attribute sets exactly one CSS property
473 // > (including invalid or unrecognized properties), which is "font-weight".
474 // TODO
475
476 // > It is an i or em element with exactly one attribute, which is style,
477 // > and the style attribute sets exactly one CSS property (including invalid or unrecognized properties),
478 // > which is "font-style".
479 // TODO
480
481 // > It is an a, font, or span element with exactly one attribute, which is style,
482 // > and the style attribute sets exactly one CSS property (including invalid or unrecognized properties),
483 // > and that property is not "text-decoration".
484 if matches!(
485 type_id,
486 NodeTypeId::Element(ElementTypeId::HTMLElement(
487 HTMLElementTypeId::HTMLAnchorElement,
488 )) | NodeTypeId::Element(ElementTypeId::HTMLElement(
489 HTMLElementTypeId::HTMLFontElement,
490 )) | NodeTypeId::Element(ElementTypeId::HTMLElement(
491 HTMLElementTypeId::HTMLSpanElement,
492 ))
493 ) {
494 if only_attribute != &local_name!("style") {
495 return false;
496 }
497 let style_attribute = self.style_attribute().borrow();
498 let Some(declarations) = style_attribute.as_ref() else {
499 return false;
500 };
501 let document = self.owner_document();
502 let shared_lock = document.style_shared_lock();
503 let read_lock = shared_lock.read();
504 let style = declarations.read_with(&read_lock);
505
506 if style.len() == 1 {
507 if let Some((text_decoration, _)) = style.get(PropertyDeclarationId::Longhand(
508 LonghandId::TextDecorationLine,
509 )) {
510 // > It is an a, font, s, span, strike, or u element with exactly one attribute,
511 // > which is style, and the style attribute sets exactly one CSS property
512 // > (including invalid or unrecognized properties), which is "text-decoration",
513 // > which is set to "line-through" or "underline" or "overline" or "none".
514 //
515 // TODO: Also the other element types
516 return matches!(
517 text_decoration,
518 PropertyDeclaration::TextDecorationLine(
519 TextDecorationLine::LINE_THROUGH |
520 TextDecorationLine::UNDERLINE |
521 TextDecorationLine::OVERLINE |
522 TextDecorationLine::NONE
523 )
524 );
525 } else {
526 // > It is an a, font, or span element with exactly one attribute, which is style,
527 // > and the style attribute sets exactly one CSS property (including invalid or unrecognized properties),
528 // > and that property is not "text-decoration".
529 return true;
530 }
531 }
532 }
533
534 false
535 }
536}
537
538pub(crate) enum NodeOrString {
539 String(String),
540 Node(DomRoot<Node>),
541}
542
543impl NodeOrString {
544 fn name(&self) -> &str {
545 match self {
546 NodeOrString::String(str_) => str_,
547 NodeOrString::Node(node) => node
548 .downcast::<Element>()
549 .map(|element| element.local_name().as_ref())
550 .unwrap_or_default(),
551 }
552 }
553
554 fn as_node(&self) -> Option<DomRoot<Node>> {
555 match self {
556 NodeOrString::String(_) => None,
557 NodeOrString::Node(node) => Some(node.clone()),
558 }
559 }
560}
561
562/// <https://w3c.github.io/editing/docs/execCommand/#prohibited-paragraph-child-name>
563const PROHIBITED_PARAGRAPH_CHILD_NAMES: [&str; 47] = [
564 "address",
565 "article",
566 "aside",
567 "blockquote",
568 "caption",
569 "center",
570 "col",
571 "colgroup",
572 "dd",
573 "details",
574 "dir",
575 "div",
576 "dl",
577 "dt",
578 "fieldset",
579 "figcaption",
580 "figure",
581 "footer",
582 "form",
583 "h1",
584 "h2",
585 "h3",
586 "h4",
587 "h5",
588 "h6",
589 "header",
590 "hgroup",
591 "hr",
592 "li",
593 "listing",
594 "menu",
595 "nav",
596 "ol",
597 "p",
598 "plaintext",
599 "pre",
600 "section",
601 "summary",
602 "table",
603 "tbody",
604 "td",
605 "tfoot",
606 "th",
607 "thead",
608 "tr",
609 "ul",
610 "xmp",
611];
612/// <https://w3c.github.io/editing/docs/execCommand/#name-of-an-element-with-inline-contents>
613const NAME_OF_AN_ELEMENT_WITH_INLINE_CONTENTS: [&str; 43] = [
614 "a", "abbr", "b", "bdi", "bdo", "cite", "code", "dfn", "em", "h1", "h2", "h3", "h4", "h5",
615 "h6", "i", "kbd", "mark", "p", "pre", "q", "rp", "rt", "ruby", "s", "samp", "small", "span",
616 "strong", "sub", "sup", "u", "var", "acronym", "listing", "strike", "xmp", "big", "blink",
617 "font", "marquee", "nobr", "tt",
618];
619
620/// <https://w3c.github.io/editing/docs/execCommand/#element-with-inline-contents>
621fn is_element_with_inline_contents(element: &Node) -> bool {
622 // > An element with inline contents is an HTML element whose local name is a name of an element with inline contents.
623 let Some(html_element) = element.downcast::<HTMLElement>() else {
624 return false;
625 };
626 NAME_OF_AN_ELEMENT_WITH_INLINE_CONTENTS.contains(&html_element.local_name())
627}
628
629/// <https://w3c.github.io/editing/docs/execCommand/#preserving-ranges>
630fn move_preserving_ranges<Move>(cx: &mut js::context::JSContext, node: &Node, mut move_: Move)
631where
632 Move: FnMut(&mut js::context::JSContext) -> Fallible<DomRoot<Node>>,
633{
634 // Step 1. Let node be the moved node, old parent and old index be the old parent
635 // (which may be null) and index, and new parent and new index be the new parent and index.
636 let old_parent = node.GetParentNode();
637 let old_index = node.index();
638
639 if move_(cx).is_err() {
640 unreachable!("Must always be able to move");
641 }
642
643 let Some(selection) = node.owner_document().GetSelection(CanGc::from_cx(cx)) else {
644 return;
645 };
646 let Some(active_range) = selection.active_range() else {
647 return;
648 };
649
650 let new_parent = node.GetParentNode().expect("Must always have a new parent");
651 let new_index = node.index();
652
653 let mut start_node = active_range.start_container();
654 let mut start_offset = active_range.start_offset();
655 let mut end_node = active_range.end_container();
656 let mut end_offset = active_range.end_offset();
657
658 // Step 2. If a boundary point's node is the same as or a descendant of node, leave it unchanged, so it moves to the new location.
659 //
660 // From the spec:
661 // > This is actually implicit, but I state it anyway for completeness.
662
663 // Step 3. If a boundary point's node is new parent and its offset is greater than new index, add one to its offset.
664 if start_node == new_parent && start_offset > new_index {
665 start_offset += 1;
666 }
667 if end_node == new_parent && end_offset > new_index {
668 end_offset += 1;
669 }
670
671 if let Some(old_parent) = old_parent {
672 // Step 4. If a boundary point's node is old parent and its offset is old index or old index + 1,
673 // set its node to new parent and add new index − old index to its offset.
674 if start_node == old_parent && (start_offset == old_index || start_offset == old_index + 1)
675 {
676 start_node = new_parent.clone();
677 start_offset += new_index;
678 start_offset -= old_index;
679 }
680 if end_node == old_parent && (end_offset == old_index || end_offset == old_index + 1) {
681 end_node = new_parent;
682 end_offset += new_index;
683 end_offset -= old_index;
684 }
685
686 // Step 5. If a boundary point's node is old parent and its offset is greater than old index + 1,
687 // subtract one from its offset.
688 if start_node == old_parent && (start_offset > old_index + 1) {
689 start_offset -= 1;
690 }
691 if end_node == old_parent && (end_offset > old_index + 1) {
692 end_offset -= 1;
693 }
694 }
695
696 active_range.set_start(&start_node, start_offset);
697 active_range.set_end(&end_node, end_offset);
698}
699
700/// <https://w3c.github.io/editing/docs/execCommand/#allowed-child>
701fn is_allowed_child(child: NodeOrString, parent: NodeOrString) -> bool {
702 // Step 1. If parent is "colgroup", "table", "tbody", "tfoot", "thead", "tr",
703 // or an HTML element with local name equal to one of those,
704 // and child is a Text node whose data does not consist solely of space characters, return false.
705 if matches!(
706 parent.name(),
707 "colgroup" | "table" | "tbody" | "tfoot" | "thead" | "tr"
708 ) && child.as_node().is_some_and(|node| {
709 // Note: cannot use `.and_then` here, since `downcast` would outlive its reference
710 node.downcast::<Text>()
711 .is_some_and(|text| !text.data().bytes().all(|byte| byte == b' '))
712 }) {
713 return false;
714 }
715 // Step 2. If parent is "script", "style", "plaintext", or "xmp",
716 // or an HTML element with local name equal to one of those, and child is not a Text node, return false.
717 if matches!(parent.name(), "script" | "style" | "plaintext" | "xmp") &&
718 child.as_node().is_none_or(|node| !node.is::<Text>())
719 {
720 return false;
721 }
722 // Step 3. If child is a document, DocumentFragment, or DocumentType, return false.
723 if let NodeOrString::Node(ref node) = child {
724 if matches!(
725 node.type_id(),
726 NodeTypeId::Document(_) | NodeTypeId::DocumentFragment(_) | NodeTypeId::DocumentType
727 ) {
728 return false;
729 }
730 }
731 // Step 4. If child is an HTML element, set child to the local name of child.
732 let child_name = match child {
733 NodeOrString::String(str_) => str_,
734 NodeOrString::Node(node) => match node.downcast::<HTMLElement>() {
735 // Step 5. If child is not a string, return true.
736 None => return true,
737 Some(html_element) => html_element.local_name().to_owned(),
738 },
739 };
740 let child = child_name.as_str();
741 let parent_name = match parent {
742 NodeOrString::String(str_) => str_,
743 NodeOrString::Node(parent) => {
744 // Step 6. If parent is an HTML element:
745 if let Some(parent_element) = parent.downcast::<HTMLElement>() {
746 // Step 6.1. If child is "a", and parent or some ancestor of parent is an a, return false.
747 if child == "a" &&
748 parent
749 .inclusive_ancestors(ShadowIncluding::No)
750 .any(|node| node.is::<HTMLAnchorElement>())
751 {
752 return false;
753 }
754 // Step 6.2. If child is a prohibited paragraph child name and parent or some ancestor of parent
755 // is an element with inline contents, return false.
756 if PROHIBITED_PARAGRAPH_CHILD_NAMES.contains(&child) &&
757 parent
758 .inclusive_ancestors(ShadowIncluding::No)
759 .any(|node| is_element_with_inline_contents(&node))
760 {
761 return false;
762 }
763 // Step 6.3. If child is "h1", "h2", "h3", "h4", "h5", or "h6",
764 // and parent or some ancestor of parent is an HTML element with local name
765 // "h1", "h2", "h3", "h4", "h5", or "h6", return false.
766 if matches!(child, "h1" | "h2" | "h3" | "h4" | "h5" | "h6") &&
767 parent.inclusive_ancestors(ShadowIncluding::No).any(|node| {
768 node.downcast::<HTMLElement>().is_some_and(|html_element| {
769 matches!(
770 html_element.local_name(),
771 "h1" | "h2" | "h3" | "h4" | "h5" | "h6"
772 )
773 })
774 })
775 {
776 return false;
777 }
778 // Step 6.4. Let parent be the local name of parent.
779 parent_element.local_name().to_owned()
780 } else {
781 // Step 7. If parent is an Element or DocumentFragment, return true.
782 // Step 8. If parent is not a string, return false.
783 return matches!(
784 parent.type_id(),
785 NodeTypeId::DocumentFragment(_) | NodeTypeId::Element(_)
786 );
787 }
788 },
789 };
790 let parent = parent_name.as_str();
791 // Step 9. If parent is on the left-hand side of an entry on the following list,
792 // then return true if child is listed on the right-hand side of that entry, and false otherwise.
793 match parent {
794 "colgroup" => return child == "col",
795 "table" => {
796 return matches!(
797 child,
798 "caption" | "col" | "colgroup" | "tbody" | "td" | "tfoot" | "th" | "thead" | "tr"
799 );
800 },
801 "tbody" | "tfoot" | "thead" => return matches!(child, "td" | "th" | "tr"),
802 "tr" => return matches!(child, "td" | "th"),
803 "dl" => return matches!(child, "dt" | "dd"),
804 "dir" | "ol" | "ul" => return matches!(child, "dir" | "li" | "ol" | "ul"),
805 "hgroup" => return matches!(child, "h1" | "h2" | "h3" | "h4" | "h5" | "h6"),
806 _ => {},
807 };
808 // Step 10. If child is "body", "caption", "col", "colgroup", "frame", "frameset", "head",
809 // "html", "tbody", "td", "tfoot", "th", "thead", or "tr", return false.
810 if matches!(
811 child,
812 "body" |
813 "caption" |
814 "col" |
815 "colgroup" |
816 "frame" |
817 "frameset" |
818 "head" |
819 "html" |
820 "tbody" |
821 "td" |
822 "tfoot" |
823 "th" |
824 "thead" |
825 "tr"
826 ) {
827 return false;
828 }
829 // Step 11. If child is "dd" or "dt" and parent is not "dl", return false.
830 if matches!(child, "dd" | "dt") && parent != "dl" {
831 return false;
832 }
833 // Step 12. If child is "li" and parent is not "ol" or "ul", return false.
834 if child == "li" && !matches!(parent, "ol" | "ul") {
835 return false;
836 }
837 // Step 13. If parent is on the left-hand side of an entry on the following list
838 // and child is listed on the right-hand side of that entry, return false.
839 if match parent {
840 "a" => child == "a",
841 "dd" | "dt" => matches!(child, "dd" | "dt"),
842 "h1" | "h2" | "h3" | "h4" | "h5" | "h6" => {
843 matches!(child, "h1" | "h2" | "h3" | "h4" | "h5" | "h6")
844 },
845 "li" => child == "li",
846 "nobr" => child == "nobr",
847 "td" | "th" => {
848 matches!(
849 child,
850 "caption" | "col" | "colgroup" | "tbody" | "td" | "tfoot" | "th" | "thead" | "tr"
851 )
852 },
853 _ if NAME_OF_AN_ELEMENT_WITH_INLINE_CONTENTS.contains(&parent) => {
854 PROHIBITED_PARAGRAPH_CHILD_NAMES.contains(&child)
855 },
856 _ => false,
857 } {
858 return false;
859 }
860 // Step 14. Return true.
861 true
862}
863
864/// <https://w3c.github.io/editing/docs/execCommand/#split-the-parent>
865pub(crate) fn split_the_parent<'a>(cx: &mut js::context::JSContext, node_list: &'a [&'a Node]) {
866 assert!(!node_list.is_empty());
867 // Step 1. Let original parent be the parent of the first member of node list.
868 let Some(original_parent) = node_list.first().and_then(|first| first.GetParentNode()) else {
869 return;
870 };
871 let context_object = original_parent.owner_document();
872 // Step 2. If original parent is not editable or its parent is null, do nothing and abort these steps.
873 if !original_parent.is_editable() {
874 return;
875 }
876 let Some(parent_of_original_parent) = original_parent.GetParentNode() else {
877 return;
878 };
879 // Step 3. If the first child of original parent is in node list, remove extraneous line breaks before original parent.
880 if original_parent
881 .children()
882 .next()
883 .is_some_and(|first_child| node_list.contains(&first_child.deref()))
884 {
885 original_parent.remove_extraneous_line_breaks_before(cx);
886 }
887 // Step 4. If the first child of original parent is in node list, and original parent follows a line break,
888 // set follows line break to true. Otherwise, set follows line break to false.
889 let first_child_is_in_node_list = original_parent
890 .children()
891 .next()
892 .is_some_and(|first_child| node_list.contains(&first_child.deref()));
893 let follows_line_break = first_child_is_in_node_list && original_parent.follows_a_line_break();
894 // Step 5. If the last child of original parent is in node list, and original parent precedes a line break,
895 // set precedes line break to true. Otherwise, set precedes line break to false.
896 let last_child_is_in_node_list = original_parent
897 .children()
898 .last()
899 .is_some_and(|last_child| node_list.contains(&last_child.deref()));
900 let precedes_line_break = last_child_is_in_node_list && original_parent.precedes_a_line_break();
901 // Step 6. If the first child of original parent is not in node list, but its last child is:
902 if !first_child_is_in_node_list && last_child_is_in_node_list {
903 // Step 6.1. For each node in node list, in reverse order,
904 // insert node into the parent of original parent immediately after original parent, preserving ranges.
905 let next_of_original_parent = original_parent.GetNextSibling();
906 for node in node_list.iter().rev() {
907 move_preserving_ranges(cx, node, |cx| {
908 parent_of_original_parent.InsertBefore(cx, node, next_of_original_parent.as_deref())
909 });
910 }
911 // Step 6.2. If precedes line break is true, and the last member of node list does not precede a line break,
912 // call createElement("br") on the context object and insert the result immediately after the last member of node list.
913 if precedes_line_break {
914 if let Some(last) = node_list.last() {
915 if !last.precedes_a_line_break() {
916 let br = context_object.create_element(cx, "br");
917 if last
918 .GetParentNode()
919 .expect("Must always have a parent")
920 .InsertBefore(cx, br.upcast(), last.GetNextSibling().as_deref())
921 .is_err()
922 {
923 unreachable!("Must always be able to append");
924 }
925 }
926 }
927 }
928 // Step 6.3. Remove extraneous line breaks at the end of original parent.
929 original_parent.remove_extraneous_line_breaks_at_the_end_of(cx);
930 // Step 6.4. Abort these steps.
931 return;
932 }
933 // Step 7. If the first child of original parent is not in node list:
934 if first_child_is_in_node_list {
935 // Step 7.1. Let cloned parent be the result of calling cloneNode(false) on original parent.
936 let Ok(cloned_parent) = original_parent.CloneNode(cx, false) else {
937 unreachable!("Must always be able to clone node");
938 };
939 // Step 7.2. If original parent has an id attribute, unset it.
940 if let Some(element) = original_parent.downcast::<Element>() {
941 element.remove_attribute_by_name(&local_name!("id"), CanGc::from_cx(cx));
942 }
943 // Step 7.3. Insert cloned parent into the parent of original parent immediately before original parent.
944 if parent_of_original_parent
945 .InsertBefore(cx, &cloned_parent, Some(&original_parent))
946 .is_err()
947 {
948 unreachable!("Must always have a parent");
949 }
950 // Step 7.4. While the previousSibling of the first member of node list is not null,
951 // append the first child of original parent as the last child of cloned parent, preserving ranges.
952 loop {
953 if node_list
954 .first()
955 .and_then(|first| first.GetPreviousSibling())
956 .is_some()
957 {
958 if let Some(first_of_original) = original_parent.children().next() {
959 move_preserving_ranges(cx, &first_of_original, |cx| {
960 cloned_parent.AppendChild(cx, &first_of_original)
961 });
962 continue;
963 }
964 }
965 break;
966 }
967 }
968 // Step 8. For each node in node list, insert node into the parent of original parent immediately before original parent, preserving ranges.
969 for node in node_list.iter() {
970 move_preserving_ranges(cx, node, |cx| {
971 parent_of_original_parent.InsertBefore(cx, node, Some(&original_parent))
972 });
973 }
974 // Step 9. If follows line break is true, and the first member of node list does not follow a line break,
975 // call createElement("br") on the context object and insert the result immediately before the first member of node list.
976 if follows_line_break {
977 if let Some(first) = node_list.first() {
978 if !first.follows_a_line_break() {
979 let br = context_object.create_element(cx, "br");
980 if first
981 .GetParentNode()
982 .expect("Must always have a parent")
983 .InsertBefore(cx, br.upcast(), Some(first))
984 .is_err()
985 {
986 unreachable!("Must always be able to insert");
987 }
988 }
989 }
990 }
991 // Step 10. If the last member of node list is an inline node other than a br,
992 // and the first child of original parent is a br, and original parent is not an inline node,
993 // remove the first child of original parent from original parent.
994 if node_list
995 .last()
996 .is_some_and(|last| last.is_inline_node() && !last.is::<HTMLBRElement>()) &&
997 !original_parent.is_inline_node()
998 {
999 if let Some(first_of_original) = original_parent.children().next() {
1000 if first_of_original.is::<HTMLBRElement>() {
1001 assert!(first_of_original.has_parent());
1002 first_of_original.remove_self(cx);
1003 }
1004 }
1005 }
1006 // Step 11. If original parent has no children:
1007 if original_parent.children_count() == 0 {
1008 // Step 11.1. Remove original parent from its parent.
1009 assert!(original_parent.has_parent());
1010 original_parent.remove_self(cx);
1011 // Step 11.2. If precedes line break is true, and the last member of node list does not precede a line break,
1012 // call createElement("br") on the context object and insert the result immediately after the last member of node list.
1013 if precedes_line_break {
1014 if let Some(last) = node_list.last() {
1015 if !last.precedes_a_line_break() {
1016 let br = context_object.create_element(cx, "br");
1017 if last
1018 .GetParentNode()
1019 .expect("Must always have a parent")
1020 .InsertBefore(cx, br.upcast(), last.GetNextSibling().as_deref())
1021 .is_err()
1022 {
1023 unreachable!("Must always be able to insert");
1024 }
1025 }
1026 }
1027 }
1028 } else {
1029 // Step 12. Otherwise, remove extraneous line breaks before original parent.
1030 original_parent.remove_extraneous_line_breaks_before(cx);
1031 }
1032 // Step 13. If node list's last member's nextSibling is null, but its parent is not null,
1033 // remove extraneous line breaks at the end of node list's last member's parent.
1034 if let Some(last) = node_list.last() {
1035 if last.GetNextSibling().is_none() {
1036 if let Some(parent_of_last) = last.GetParentNode() {
1037 parent_of_last.remove_extraneous_line_breaks_at_the_end_of(cx);
1038 }
1039 }
1040 }
1041}
1042
1043/// <https://w3c.github.io/editing/docs/execCommand/#wrap>
1044fn wrap_node_list<SiblingCriteria, NewParentInstructions>(
1045 cx: &mut js::context::JSContext,
1046 node_list: Vec<DomRoot<Node>>,
1047 sibling_criteria: SiblingCriteria,
1048 new_parent_instructions: NewParentInstructions,
1049) -> Option<DomRoot<Node>>
1050where
1051 SiblingCriteria: Fn(&Node) -> bool,
1052 NewParentInstructions: Fn() -> Option<DomRoot<Node>>,
1053{
1054 // Step 1. If every member of node list is invisible,
1055 // and none is a br, return null and abort these steps.
1056 if node_list
1057 .iter()
1058 .all(|node| node.is_invisible() && !node.is::<HTMLBRElement>())
1059 {
1060 return None;
1061 }
1062 // Step 2. If node list's first member's parent is null, return null and abort these steps.
1063 node_list.first().and_then(|first| first.GetParentNode())?;
1064 // Step 3. If node list's last member is an inline node that's not a br,
1065 // and node list's last member's nextSibling is a br, append that br to node list.
1066 let mut node_list = node_list;
1067 if let Some(last) = node_list.last() {
1068 if last.is_inline_node() && !last.is::<HTMLBRElement>() {
1069 if let Some(next_of_last) = last.GetNextSibling() {
1070 if next_of_last.is::<HTMLBRElement>() {
1071 node_list.push(next_of_last);
1072 }
1073 }
1074 }
1075 }
1076 // Step 4. While node list's first member's previousSibling is invisible, prepend it to node list.
1077 while let Some(previous_of_first) = node_list.first().and_then(|last| last.GetPreviousSibling())
1078 {
1079 if previous_of_first.is_invisible() {
1080 node_list.insert(0, previous_of_first);
1081 continue;
1082 }
1083 break;
1084 }
1085 // Step 5. While node list's last member's nextSibling is invisible, append it to node list.
1086 while let Some(next_of_last) = node_list.last().and_then(|last| last.GetNextSibling()) {
1087 if next_of_last.is_invisible() {
1088 node_list.push(next_of_last);
1089 continue;
1090 }
1091 break;
1092 }
1093 // Step 6. If the previousSibling of the first member of node list is editable
1094 // and running sibling criteria on it returns true,
1095 // let new parent be the previousSibling of the first member of node list.
1096 let new_parent = node_list
1097 .first()
1098 .and_then(|first| first.GetPreviousSibling())
1099 .filter(|previous_of_first| {
1100 previous_of_first.is_editable() && sibling_criteria(previous_of_first)
1101 });
1102 // Step 7. Otherwise, if the nextSibling of the last member of node list is editable
1103 // and running sibling criteria on it returns true,
1104 // let new parent be the nextSibling of the last member of node list.
1105 let new_parent = new_parent.or_else(|| {
1106 node_list
1107 .last()
1108 .and_then(|first| first.GetNextSibling())
1109 .filter(|next_of_last| next_of_last.is_editable() && sibling_criteria(next_of_last))
1110 });
1111 // Step 8. Otherwise, run new parent instructions, and let new parent be the result.
1112 // Step 9. If new parent is null, abort these steps and return null.
1113 let new_parent = new_parent.or_else(new_parent_instructions)?;
1114 // Step 11. Let original parent be the parent of the first member of node list.
1115 let first_in_node_list = node_list
1116 .first()
1117 .expect("Must always have at least one node");
1118 let original_parent = first_in_node_list
1119 .GetParentNode()
1120 .expect("First node must have a parent");
1121 // Step 10. If new parent's parent is null:
1122 if new_parent.GetParentNode().is_none() {
1123 // Step 10.1. Insert new parent into the parent of the first member
1124 // of node list immediately before the first member of node list.
1125 if original_parent
1126 .InsertBefore(cx, &new_parent, Some(first_in_node_list))
1127 .is_err()
1128 {
1129 unreachable!("Must always be able to insert");
1130 }
1131 // Step 10.2. If any range has a boundary point with node equal
1132 // to the parent of new parent and offset equal to the index of new parent,
1133 // add one to that boundary point's offset.
1134 if let Some(range) = first_in_node_list
1135 .owner_document()
1136 .GetSelection(CanGc::from_cx(cx))
1137 .and_then(|selection| selection.active_range())
1138 {
1139 let parent_of_new_parent = new_parent.GetParentNode().expect("Must have a parent");
1140 let start_container = range.start_container();
1141 let start_offset = range.start_offset();
1142
1143 if start_container == parent_of_new_parent && start_offset == new_parent.index() {
1144 range.set_start(&start_container, start_offset + 1);
1145 }
1146
1147 let end_container = range.end_container();
1148 let end_offset = range.end_offset();
1149
1150 if end_container == parent_of_new_parent && end_offset == new_parent.index() {
1151 range.set_start(&end_container, end_offset + 1);
1152 }
1153 }
1154 }
1155 // Step 12. If new parent is before the first member of node list in tree order:
1156 if new_parent.is_before(first_in_node_list) {
1157 // Step 12.1. If new parent is not an inline node, but the last visible child of new parent
1158 // and the first visible member of node list are both inline nodes,
1159 // and the last child of new parent is not a br,
1160 // call createElement("br") on the ownerDocument of new parent
1161 // and append the result as the last child of new parent.
1162 // TODO
1163 // Step 12.2. For each node in node list, append node as the last child of new parent, preserving ranges.
1164 for node in node_list {
1165 move_preserving_ranges(cx, &node, |cx| new_parent.AppendChild(cx, &node));
1166 }
1167 } else {
1168 // Step 13. Otherwise:
1169 // Step 13.1. If new parent is not an inline node, but the first visible child of new parent
1170 // and the last visible member of node list are both inline nodes,
1171 // and the last member of node list is not a br,
1172 // call createElement("br") on the ownerDocument of new parent
1173 // and insert the result as the first child of new parent.
1174 // TODO
1175 // Step 13.2. For each node in node list, in reverse order,
1176 // insert node as the first child of new parent, preserving ranges.
1177 let mut before = new_parent.GetFirstChild();
1178 for node in node_list.iter().rev() {
1179 move_preserving_ranges(cx, node, |cx| {
1180 new_parent.InsertBefore(cx, node, before.as_deref())
1181 });
1182 before = Some(DomRoot::from_ref(node));
1183 }
1184 }
1185 // Step 14. If original parent is editable and has no children, remove it from its parent.
1186 if original_parent.is_editable() && original_parent.children_count() == 0 {
1187 original_parent.remove_self(cx);
1188 }
1189 // Step 15. If new parent's nextSibling is editable and running sibling criteria on it returns true:
1190 if let Some(next_of_new_parent) = new_parent.GetNextSibling() {
1191 if next_of_new_parent.is_editable() && sibling_criteria(&next_of_new_parent) {
1192 // Step 15.1. If new parent is not an inline node,
1193 // but new parent's last child and new parent's nextSibling's first child are both inline nodes,
1194 // and new parent's last child is not a br, call createElement("br") on the ownerDocument
1195 // of new parent and append the result as the last child of new parent.
1196 if !new_parent.is_inline_node() {
1197 if let Some(last_child_of_new_parent) = new_parent.children().last() {
1198 if last_child_of_new_parent.is_inline_node() &&
1199 !last_child_of_new_parent.is::<HTMLBRElement>() &&
1200 next_of_new_parent
1201 .children()
1202 .next()
1203 .is_some_and(|first| first.is_inline_node())
1204 {
1205 let new_br_element = new_parent.owner_document().create_element(cx, "br");
1206 if new_parent.AppendChild(cx, new_br_element.upcast()).is_err() {
1207 unreachable!("Must always be able to append");
1208 }
1209 }
1210 }
1211 }
1212 // Step 15.2. While new parent's nextSibling has children,
1213 // append its first child as the last child of new parent, preserving ranges.
1214 while let Some(first_of_next) = next_of_new_parent.children().next() {
1215 move_preserving_ranges(cx, &first_of_next, |cx| {
1216 new_parent.AppendChild(cx, &first_of_next)
1217 });
1218 }
1219 // Step 15.3. Remove new parent's nextSibling from its parent.
1220 next_of_new_parent.remove_self(cx);
1221 }
1222 }
1223 // Step 16. Remove extraneous line breaks from new parent.
1224 new_parent.remove_extraneous_line_breaks_from(cx);
1225 // Step 17. Return new parent.
1226 Some(new_parent)
1227}
1228
1229impl Node {
1230 /// <https://w3c.github.io/editing/docs/execCommand/#push-down-values>
1231 fn push_down_values(
1232 &self,
1233 cx: &mut js::context::JSContext,
1234 command: &CommandName,
1235 new_value: Option<DOMString>,
1236 ) {
1237 // Step 1. Let command be the current command.
1238 //
1239 // Passed in as argument
1240
1241 // Step 4. Let current ancestor be node's parent.
1242 let mut current_ancestor = self.GetParentElement();
1243 // Step 2. If node's parent is not an Element, abort this algorithm.
1244 if current_ancestor.is_none() {
1245 return;
1246 };
1247 // Step 3. If the effective command value of command is loosely equivalent to new value on node,
1248 // abort this algorithm.
1249 if command.are_loosely_equivalent_values(
1250 self.effective_command_value(command).as_ref(),
1251 new_value.as_ref(),
1252 ) {
1253 return;
1254 }
1255 // Step 5. Let ancestor list be a list of nodes, initially empty.
1256 rooted_vec!(let mut ancestor_list);
1257 // Step 6. While current ancestor is an editable Element and
1258 // the effective command value of command is not loosely equivalent to new value on it,
1259 // append current ancestor to ancestor list, then set current ancestor to its parent.
1260 while let Some(ancestor) = current_ancestor {
1261 let ancestor_node = ancestor.upcast::<Node>();
1262 if ancestor_node.is_editable() &&
1263 !command.are_loosely_equivalent_values(
1264 ancestor_node.effective_command_value(command).as_ref(),
1265 new_value.as_ref(),
1266 )
1267 {
1268 ancestor_list.push(ancestor.clone());
1269 current_ancestor = ancestor_node.GetParentElement();
1270 continue;
1271 }
1272 break;
1273 }
1274 let Some(last_ancestor) = ancestor_list.last() else {
1275 // Step 7. If ancestor list is empty, abort this algorithm.
1276 return;
1277 };
1278 // Step 8. Let propagated value be the specified command value of command on the last member of ancestor list.
1279 let mut propagated_value = last_ancestor.specified_command_value(command);
1280 // Step 9. If propagated value is null and is not equal to new value, abort this algorithm.
1281 if propagated_value.is_none() && new_value.is_some() {
1282 return;
1283 }
1284 // Step 10. If the effective command value of command is not loosely equivalent to new value on the parent
1285 // of the last member of ancestor list, and new value is not null, abort this algorithm.
1286 if new_value.is_some() &&
1287 !last_ancestor
1288 .upcast::<Node>()
1289 .GetParentNode()
1290 .is_some_and(|last_ancestor| {
1291 command.are_loosely_equivalent_values(
1292 last_ancestor.effective_command_value(command).as_ref(),
1293 new_value.as_ref(),
1294 )
1295 })
1296 {
1297 return;
1298 }
1299 // Step 11. While ancestor list is not empty:
1300 let mut ancestor_list_iter = ancestor_list.iter().rev().peekable();
1301 while let Some(current_ancestor) = ancestor_list_iter.next() {
1302 let current_ancestor_node = current_ancestor.upcast::<Node>();
1303 // Step 11.1. Let current ancestor be the last member of ancestor list.
1304 // Step 11.2. Remove the last member from ancestor list.
1305 //
1306 // Both of these steps done by iterating and reversing the iterator
1307
1308 // Step 11.3. If the specified command value of current ancestor for command is not null, set propagated value to that value.
1309 let command_value = current_ancestor.specified_command_value(command);
1310 let has_command_value = command_value.is_some();
1311 propagated_value = command_value.or(propagated_value);
1312 // Step 11.4. Let children be the children of current ancestor.
1313 let children = current_ancestor_node.children();
1314 // Step 11.5. If the specified command value of current ancestor for command is not null, clear the value of current ancestor.
1315 if has_command_value {
1316 if let Some(html_element) = current_ancestor.downcast::<HTMLElement>() {
1317 html_element.clear_the_value(cx, command);
1318 }
1319 }
1320 // Step 11.6. For every child in children:
1321 for child in children {
1322 // Step 11.6.1. If child is node, continue with the next child.
1323 if *child == *self {
1324 continue;
1325 }
1326 // Step 11.6.2. If child is an Element whose specified command value for command is neither null
1327 // nor equivalent to propagated value, continue with the next child.
1328 if let Some(child_element) = child.downcast::<Element>() {
1329 let specified_value = child_element.specified_command_value(command);
1330 if specified_value.is_some() &&
1331 !command.are_equivalent_values(
1332 specified_value.as_ref(),
1333 propagated_value.as_ref(),
1334 )
1335 {
1336 continue;
1337 }
1338 }
1339
1340 // Step 11.6.3. If child is the last member of ancestor list, continue with the next child.
1341 //
1342 // Since we had to remove the last member in step 11.2, if we now peek at the next possible
1343 // value, we essentially have the "last member after removal"
1344 if ancestor_list_iter
1345 .peek()
1346 .is_some_and(|ancestor| *ancestor.upcast::<Node>() == *child)
1347 {
1348 continue;
1349 }
1350 // step 11.6.4. Force the value of child, with command as in this algorithm and new value equal to propagated value.
1351 child.force_the_value(cx, command, propagated_value.as_ref());
1352 }
1353 }
1354 }
1355
1356 /// <https://w3c.github.io/editing/docs/execCommand/#force-the-value>
1357 pub(crate) fn force_the_value(
1358 &self,
1359 cx: &mut js::context::JSContext,
1360 command: &CommandName,
1361 new_value: Option<&DOMString>,
1362 ) {
1363 // Step 1. Let command be the current command.
1364 //
1365 // That's command
1366
1367 // Step 2. If node's parent is null, abort this algorithm.
1368 if self.GetParentNode().is_none() {
1369 return;
1370 }
1371 // Step 3. If new value is null, abort this algorithm.
1372 let Some(new_value) = new_value else {
1373 return;
1374 };
1375 // Step 4. If node is an allowed child of "span":
1376 if is_allowed_child(
1377 NodeOrString::Node(DomRoot::from_ref(self)),
1378 NodeOrString::String("span".to_owned()),
1379 ) {
1380 // Step 4.1. Reorder modifiable descendants of node's previousSibling.
1381 // TODO
1382 // Step 4.2. Reorder modifiable descendants of node's nextSibling.
1383 // TODO
1384 // Step 4.3. Wrap the one-node list consisting of node,
1385 // with sibling criteria returning true for a simple modifiable element whose
1386 // specified command value is equivalent to new value and whose effective command value
1387 // is loosely equivalent to new value and false otherwise,
1388 // and with new parent instructions returning null.
1389 wrap_node_list(
1390 cx,
1391 vec![DomRoot::from_ref(self)],
1392 |sibling| {
1393 sibling
1394 .downcast::<Element>()
1395 .is_some_and(|sibling_element| {
1396 sibling_element.is_simple_modifiable_element() &&
1397 command.are_equivalent_values(
1398 sibling_element.specified_command_value(command).as_ref(),
1399 Some(new_value),
1400 ) &&
1401 command.are_loosely_equivalent_values(
1402 sibling.effective_command_value(command).as_ref(),
1403 Some(new_value),
1404 )
1405 })
1406 },
1407 || None,
1408 );
1409 }
1410 // Step 5. If node is invisible, abort this algorithm.
1411 if self.is_invisible() {
1412 return;
1413 }
1414 // Step 6. If the effective command value of command is loosely equivalent to new value on node, abort this algorithm.
1415 if command.are_loosely_equivalent_values(
1416 self.effective_command_value(command).as_ref(),
1417 Some(new_value),
1418 ) {
1419 return;
1420 }
1421 // Step 7. If node is not an allowed child of "span":
1422 if !is_allowed_child(
1423 NodeOrString::Node(DomRoot::from_ref(self)),
1424 NodeOrString::String("span".to_owned()),
1425 ) {
1426 for child in self.children() {
1427 // Step 7.1. Let children be all children of node, omitting any that are Elements whose
1428 // specified command value for command is neither null nor equivalent to new value.
1429 if let Some(child_element) = child.downcast::<Element>() {
1430 let specified_value = child_element.specified_command_value(command);
1431 if specified_value.is_some() &&
1432 !command.are_equivalent_values(specified_value.as_ref(), Some(new_value))
1433 {
1434 continue;
1435 }
1436 }
1437 // Step 7.2. Force the value of each node in children,
1438 // with command and new value as in this invocation of the algorithm.
1439 child.force_the_value(cx, command, Some(new_value));
1440 }
1441 // Step 7.3. Abort this algorithm.
1442 return;
1443 }
1444 // Step 8. If the effective command value of command is loosely equivalent to new value on node, abort this algorithm.
1445 if command.are_loosely_equivalent_values(
1446 self.effective_command_value(command).as_ref(),
1447 Some(new_value),
1448 ) {
1449 return;
1450 }
1451 // Step 9. Let new parent be null.
1452 let mut new_parent = None;
1453 let document = self.owner_document();
1454 let css_styling_flag = document.css_styling_flag();
1455 // Step 10. If the CSS styling flag is false:
1456 // TODO
1457 match command {
1458 // Step 11. If command is "createLink" or "unlink":
1459 // TODO
1460 // Step 12. If command is "fontSize"; and new value is one of
1461 // "x-small", "small", "medium", "large", "x-large", "xx-large", or "xxx-large";
1462 // and either the CSS styling flag is false, or new value is "xxx-large":
1463 // let new parent be the result of calling createElement("font") on the ownerDocument of node,
1464 // then set the size attribute of new parent to the number from the following table based on new value:
1465 CommandName::FontSize => {
1466 if !css_styling_flag || new_value == "xxx-large" {
1467 let size = match &*new_value.str() {
1468 "x-small" => 1,
1469 "small" => 2,
1470 "medium" => 3,
1471 "large" => 4,
1472 "x-large" => 5,
1473 "xx-large" => 6,
1474 "xxx-large" => 7,
1475 _ => 0,
1476 };
1477
1478 if size > 0 {
1479 let new_font_element = document.create_element(cx, "font");
1480 new_font_element.set_int_attribute(
1481 &local_name!("size"),
1482 size,
1483 CanGc::from_cx(cx),
1484 );
1485 new_parent = Some(new_font_element);
1486 }
1487 }
1488 },
1489 CommandName::Subscript | CommandName::Superscript => {
1490 // Step 13. If command is "subscript" or "superscript" and new value is "subscript",
1491 // let new parent be the result of calling createElement("sub") on the ownerDocument of node.
1492 if new_value == "subscript" {
1493 new_parent = Some(document.create_element(cx, "sub"));
1494 }
1495 // Step 14. If command is "subscript" or "superscript" and new value is "superscript",
1496 // let new parent be the result of calling createElement("sup") on the ownerDocument of node.
1497 if new_value == "superscript" {
1498 new_parent = Some(document.create_element(cx, "sup"));
1499 }
1500 },
1501 _ => {},
1502 }
1503 // Step 15. If new parent is null, let new parent be the result of calling createElement("span") on the ownerDocument of node.
1504 let new_parent = new_parent.unwrap_or_else(|| document.create_element(cx, "span"));
1505 // Step 16. Insert new parent in node's parent before node.
1506 if self
1507 .GetParentNode()
1508 .expect("Must always have a parent")
1509 .InsertBefore(cx, new_parent.upcast(), Some(self))
1510 .is_err()
1511 {
1512 unreachable!("Must always be able to insert");
1513 }
1514 // Step 17. If the effective command value of command for new parent is not loosely equivalent to new value,
1515 // and the relevant CSS property for command is not null,
1516 // set that CSS property of new parent to new value (if the new value would be valid).
1517 if !command.are_loosely_equivalent_values(
1518 new_parent
1519 .upcast::<Node>()
1520 .effective_command_value(command)
1521 .as_ref(),
1522 Some(new_value),
1523 ) {
1524 if let Some(css_property) = command.relevant_css_property() {
1525 css_property.set_for_element(
1526 cx,
1527 new_parent
1528 .downcast::<HTMLElement>()
1529 .expect("Must always create a HTML element"),
1530 new_value.clone(),
1531 );
1532 }
1533 }
1534 // Step 18. If command is "strikethrough", and new value is "line-through",
1535 // and the effective command value of "strikethrough" for new parent is not "line-through",
1536 // set the "text-decoration" property of new parent to "line-through".
1537 // TODO
1538 // Step 19. If command is "underline", and new value is "underline",
1539 // and the effective command value of "underline" for new parent is not "underline",
1540 // set the "text-decoration" property of new parent to "underline".
1541 // TODO
1542 // Step 20. Append node to new parent as its last child, preserving ranges.
1543 let new_parent = new_parent.upcast::<Node>();
1544 move_preserving_ranges(cx, self, |cx| new_parent.AppendChild(cx, self));
1545 // Step 21. If node is an Element and the effective command value of command for node is not loosely equivalent to new value:
1546 if self.is::<Element>() &&
1547 !command.are_loosely_equivalent_values(
1548 self.effective_command_value(command).as_ref(),
1549 Some(new_value),
1550 )
1551 {
1552 // Step 21.1. Insert node into the parent of new parent before new parent, preserving ranges.
1553 let parent_of_new_parent = new_parent.GetParentNode().expect("Must have a parent");
1554 move_preserving_ranges(cx, self, |cx| {
1555 parent_of_new_parent.InsertBefore(cx, self, Some(new_parent))
1556 });
1557 // Step 21.2. Remove new parent from its parent.
1558 new_parent.remove_self(cx);
1559 // Step 21.3. Let children be all children of node,
1560 // omitting any that are Elements whose specified command value for command is neither null nor equivalent to new value.
1561 for child in self.children() {
1562 if child.downcast::<Element>().is_some_and(|child_element| {
1563 let specified_command_value = child_element.specified_command_value(command);
1564 specified_command_value.is_some() &&
1565 !command.are_equivalent_values(
1566 specified_command_value.as_ref(),
1567 Some(new_value),
1568 )
1569 }) {
1570 continue;
1571 }
1572 // Step 21.4. Force the value of each node in children,
1573 // with command and new value as in this invocation of the algorithm.
1574 child.force_the_value(cx, command, Some(new_value));
1575 }
1576 }
1577 }
1578}
1579
1580pub(crate) trait NodeExecCommandSupport {
1581 fn same_editing_host(&self, other: &Node) -> bool;
1582 fn is_block_node(&self) -> bool;
1583 fn is_inline_node(&self) -> bool;
1584 fn block_node_of(&self) -> Option<DomRoot<Node>>;
1585 fn is_visible(&self) -> bool;
1586 fn is_invisible(&self) -> bool;
1587 fn is_formattable(&self) -> bool;
1588 fn is_block_start_point(&self, offset: usize) -> bool;
1589 fn is_block_end_point(&self, offset: u32) -> bool;
1590 fn is_block_boundary_point(&self, offset: u32) -> bool;
1591 fn is_collapsed_block_prop(&self) -> bool;
1592 fn follows_a_line_break(&self) -> bool;
1593 fn precedes_a_line_break(&self) -> bool;
1594 fn canonical_space_sequence(
1595 n: usize,
1596 non_breaking_start: bool,
1597 non_breaking_end: bool,
1598 ) -> String;
1599 fn canonicalize_whitespace(&self, offset: u32, fix_collapsed_space: bool);
1600 fn remove_extraneous_line_breaks_before(&self, cx: &mut js::context::JSContext);
1601 fn remove_extraneous_line_breaks_at_the_end_of(&self, cx: &mut js::context::JSContext);
1602 fn remove_extraneous_line_breaks_from(&self, cx: &mut js::context::JSContext);
1603 fn remove_preserving_its_descendants(&self, cx: &mut js::context::JSContext);
1604 fn effective_command_value(&self, command: &CommandName) -> Option<DOMString>;
1605}
1606
1607impl NodeExecCommandSupport for Node {
1608 /// <https://w3c.github.io/editing/docs/execCommand/#in-the-same-editing-host>
1609 fn same_editing_host(&self, other: &Node) -> bool {
1610 // > 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.
1611 self.editing_host_of()
1612 .is_some_and(|editing_host| other.editing_host_of() == Some(editing_host))
1613 }
1614
1615 /// <https://w3c.github.io/editing/docs/execCommand/#block-node>
1616 fn is_block_node(&self) -> bool {
1617 // > A block node is either an Element whose "display" property does not have resolved value "inline" or "inline-block" or "inline-table" or "none",
1618 if self
1619 .downcast::<Element>()
1620 .and_then(Element::resolved_display_value)
1621 .is_some_and(|display| {
1622 display != DisplayOutside::Inline && display != DisplayOutside::None
1623 })
1624 {
1625 return true;
1626 }
1627 // > or a document, or a DocumentFragment.
1628 matches!(
1629 self.type_id(),
1630 NodeTypeId::Document(_) | NodeTypeId::DocumentFragment(_)
1631 )
1632 }
1633
1634 /// <https://w3c.github.io/editing/docs/execCommand/#inline-node>
1635 fn is_inline_node(&self) -> bool {
1636 // > An inline node is a node that is not a block node.
1637 !self.is_block_node()
1638 }
1639
1640 /// <https://w3c.github.io/editing/docs/execCommand/#block-node-of>
1641 fn block_node_of(&self) -> Option<DomRoot<Node>> {
1642 let mut node = DomRoot::from_ref(self);
1643
1644 loop {
1645 // Step 1. While node is an inline node, set node to its parent.
1646 if node.is_inline_node() {
1647 node = node.GetParentNode()?;
1648 continue;
1649 }
1650 // Step 2. Return node.
1651 return Some(node);
1652 }
1653 }
1654
1655 /// <https://w3c.github.io/editing/docs/execCommand/#visible>
1656 fn is_visible(&self) -> bool {
1657 for parent in self.inclusive_ancestors(ShadowIncluding::No) {
1658 // > excluding any node with an inclusive ancestor Element whose "display" property has resolved value "none".
1659 if parent
1660 .downcast::<Element>()
1661 .and_then(Element::resolved_display_value)
1662 .is_some_and(|display| display == DisplayOutside::None)
1663 {
1664 return false;
1665 }
1666 }
1667 // > Something is visible if it is a node that either is a block node,
1668 if self.is_block_node() {
1669 return true;
1670 }
1671 // > or a Text node that is not a collapsed whitespace node,
1672 if self
1673 .downcast::<Text>()
1674 .is_some_and(|text| !text.is_collapsed_whitespace_node())
1675 {
1676 return true;
1677 }
1678 // > or an img, or a br that is not an extraneous line break, or any node with a visible descendant;
1679 if self.is::<HTMLImageElement>() {
1680 return true;
1681 }
1682 if self
1683 .downcast::<HTMLBRElement>()
1684 .is_some_and(|br| !br.is_extraneous_line_break())
1685 {
1686 return true;
1687 }
1688 for child in self.children() {
1689 if child.is_visible() {
1690 return true;
1691 }
1692 }
1693 false
1694 }
1695
1696 /// <https://w3c.github.io/editing/docs/execCommand/#invisible>
1697 fn is_invisible(&self) -> bool {
1698 // > Something is invisible if it is a node that is not visible.
1699 !self.is_visible()
1700 }
1701
1702 /// <https://w3c.github.io/editing/docs/execCommand/#formattable-node>
1703 fn is_formattable(&self) -> bool {
1704 // > A formattable node is an editable visible node that is either a Text node, an img, or a br.
1705 self.is_editable() &&
1706 self.is_visible() &&
1707 (self.is::<Text>() || self.is::<HTMLImageElement>() || self.is::<HTMLBRElement>())
1708 }
1709
1710 /// <https://w3c.github.io/editing/docs/execCommand/#block-start-point>
1711 fn is_block_start_point(&self, offset: usize) -> bool {
1712 // > A boundary point (node, offset) is a block start point if either node's parent is null and offset is zero;
1713 if offset == 0 {
1714 return self.GetParentNode().is_none();
1715 }
1716 // > or node has a child with index offset − 1, and that child is either a visible block node or a visible br.
1717 self.children().nth(offset - 1).is_some_and(|child| {
1718 child.is_visible() && (child.is_block_node() || child.is::<HTMLBRElement>())
1719 })
1720 }
1721
1722 /// <https://w3c.github.io/editing/docs/execCommand/#block-end-point>
1723 fn is_block_end_point(&self, offset: u32) -> bool {
1724 // > A boundary point (node, offset) is a block end point if either node's parent is null and offset is node's length;
1725 if self.GetParentNode().is_none() && offset == self.len() {
1726 return true;
1727 }
1728 // > or node has a child with index offset, and that child is a visible block node.
1729 self.children()
1730 .nth(offset as usize)
1731 .is_some_and(|child| child.is_visible() && child.is_block_node())
1732 }
1733
1734 /// <https://w3c.github.io/editing/docs/execCommand/#block-boundary-point>
1735 fn is_block_boundary_point(&self, offset: u32) -> bool {
1736 // > A boundary point is a block boundary point if it is either a block start point or a block end point.
1737 self.is_block_start_point(offset as usize) || self.is_block_end_point(offset)
1738 }
1739
1740 /// <https://w3c.github.io/editing/docs/execCommand/#collapsed-block-prop>
1741 fn is_collapsed_block_prop(&self) -> bool {
1742 // > A collapsed block prop is either a collapsed line break that is not an extraneous line break,
1743
1744 // TODO: Check for collapsed line break
1745 if self
1746 .downcast::<HTMLBRElement>()
1747 .is_some_and(|br| !br.is_extraneous_line_break())
1748 {
1749 return true;
1750 }
1751 // > or an Element that is an inline node and whose children are all either invisible or collapsed block props
1752 if !self.is::<Element>() {
1753 return false;
1754 };
1755 if !self.is_inline_node() {
1756 return false;
1757 }
1758 let mut at_least_one_collapsed_block_prop = false;
1759 for child in self.children() {
1760 if child.is_collapsed_block_prop() {
1761 at_least_one_collapsed_block_prop = true;
1762 continue;
1763 }
1764 if child.is_invisible() {
1765 continue;
1766 }
1767
1768 return false;
1769 }
1770 // > and that has at least one child that is a collapsed block prop.
1771 at_least_one_collapsed_block_prop
1772 }
1773
1774 /// <https://w3c.github.io/editing/docs/execCommand/#follows-a-line-break>
1775 fn follows_a_line_break(&self) -> bool {
1776 // Step 1. Let offset be zero.
1777 let mut offset = 0;
1778 // Step 2. While (node, offset) is not a block boundary point:
1779 let mut node = DomRoot::from_ref(self);
1780 while !node.is_block_boundary_point(offset) {
1781 // Step 2.2. If offset is zero or node has no children, set offset to node's index, then set node to its parent.
1782 if offset == 0 || node.children_count() == 0 {
1783 offset = node.index();
1784 node = node.GetParentNode().expect("Must always have a parent");
1785 continue;
1786 }
1787 // Step 2.1. If node has a visible child with index offset minus one, return false.
1788 let child = node.children().nth(offset as usize - 1);
1789 let Some(child) = child else {
1790 return false;
1791 };
1792 if child.is_visible() {
1793 return false;
1794 }
1795 // Step 2.3. Otherwise, set node to its child with index offset minus one, then set offset to node's length.
1796 node = child;
1797 offset = node.len();
1798 }
1799 // Step 3. Return true.
1800 true
1801 }
1802
1803 /// <https://w3c.github.io/editing/docs/execCommand/#precedes-a-line-break>
1804 fn precedes_a_line_break(&self) -> bool {
1805 let mut node = DomRoot::from_ref(self);
1806 // Step 1. Let offset be node's length.
1807 let mut offset = node.len();
1808 // Step 2. While (node, offset) is not a block boundary point:
1809 while !node.is_block_boundary_point(offset) {
1810 // Step 2.1. If node has a visible child with index offset, return false.
1811 if node
1812 .children()
1813 .nth(offset as usize)
1814 .is_some_and(|child| child.is_visible())
1815 {
1816 return false;
1817 }
1818 // 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.
1819 if offset == node.len() || node.children_count() == 0 {
1820 offset = 1 + node.index();
1821 node = node.GetParentNode().expect("Must always have a parent");
1822 continue;
1823 }
1824 // Step 2.3. Otherwise, set node to its child with index offset and set offset to zero.
1825 let child = node.children().nth(offset as usize);
1826 node = match child {
1827 None => return false,
1828 Some(child) => child,
1829 };
1830 offset = 0;
1831 }
1832 // Step 3. Return true.
1833 true
1834 }
1835
1836 /// <https://w3c.github.io/editing/docs/execCommand/#canonical-space-sequence>
1837 fn canonical_space_sequence(
1838 n: usize,
1839 non_breaking_start: bool,
1840 non_breaking_end: bool,
1841 ) -> String {
1842 let mut n = n;
1843 // Step 1. If n is zero, return the empty string.
1844 if n == 0 {
1845 return String::new();
1846 }
1847 // Step 2. If n is one and both non-breaking start and non-breaking end are false, return a single space (U+0020).
1848 if n == 1 {
1849 if !non_breaking_start && !non_breaking_end {
1850 return "\u{0020}".to_owned();
1851 }
1852 // Step 3. If n is one, return a single non-breaking space (U+00A0).
1853 return "\u{00A0}".to_owned();
1854 }
1855 // Step 4. Let buffer be the empty string.
1856 let mut buffer = String::new();
1857 // Step 5. If non-breaking start is true, let repeated pair be U+00A0 U+0020. Otherwise, let it be U+0020 U+00A0.
1858 let repeated_pair = if non_breaking_start {
1859 "\u{00A0}\u{0020}"
1860 } else {
1861 "\u{0020}\u{00A0}"
1862 };
1863 // Step 6. While n is greater than three, append repeated pair to buffer and subtract two from n.
1864 while n > 3 {
1865 buffer.push_str(repeated_pair);
1866 n -= 2;
1867 }
1868 // Step 7. If n is three, append a three-code unit string to buffer depending on non-breaking start and non-breaking end:
1869 if n == 3 {
1870 buffer.push_str(match (non_breaking_start, non_breaking_end) {
1871 (false, false) => "\u{0020}\u{00A0}\u{0020}",
1872 (true, false) => "\u{00A0}\u{00A0}\u{0020}",
1873 (false, true) => "\u{0020}\u{00A0}\u{00A0}",
1874 (true, true) => "\u{00A0}\u{0020}\u{00A0}",
1875 });
1876 } else {
1877 // Step 8. Otherwise, append a two-code unit string to buffer depending on non-breaking start and non-breaking end:
1878 buffer.push_str(match (non_breaking_start, non_breaking_end) {
1879 (false, false) | (true, false) => "\u{00A0}\u{0020}",
1880 (false, true) => "\u{0020}\u{00A0}",
1881 (true, true) => "\u{00A0}\u{00A0}",
1882 });
1883 }
1884 // Step 9. Return buffer.
1885 buffer
1886 }
1887
1888 /// <https://w3c.github.io/editing/docs/execCommand/#canonicalize-whitespace>
1889 fn canonicalize_whitespace(&self, offset: u32, fix_collapsed_space: bool) {
1890 // Step 1. If node is neither editable nor an editing host, abort these steps.
1891 if !self.is_editable_or_editing_host() {
1892 return;
1893 }
1894 // Step 2. Let start node equal node and let start offset equal offset.
1895 let mut start_node = DomRoot::from_ref(self);
1896 let mut start_offset = offset;
1897 // Step 3. Repeat the following steps:
1898 loop {
1899 // Step 3.1. If start node has a child in the same editing host with index start offset minus one,
1900 // set start node to that child, then set start offset to start node's length.
1901 if start_offset > 0 {
1902 let child = start_node.children().nth(start_offset as usize - 1);
1903 if let Some(child) = child {
1904 if start_node.same_editing_host(&child) {
1905 start_node = child;
1906 start_offset = start_node.len();
1907 continue;
1908 }
1909 };
1910 }
1911 // Step 3.2. Otherwise, if start offset is zero and start node does not follow a line break
1912 // and start node's parent is in the same editing host, set start offset to start node's index,
1913 // then set start node to its parent.
1914 if start_offset == 0 && !start_node.follows_a_line_break() {
1915 if let Some(parent) = start_node.GetParentNode() {
1916 if parent.same_editing_host(&start_node) {
1917 start_offset = start_node.index();
1918 start_node = parent;
1919 }
1920 }
1921 }
1922 // Step 3.3. Otherwise, if start node is a Text node and its parent's resolved
1923 // value for "white-space" is neither "pre" nor "pre-wrap" and start offset is not zero
1924 // and the (start offset − 1)st code unit of start node's data is a space (0x0020) or
1925 // non-breaking space (0x00A0), subtract one from start offset.
1926 if start_offset != 0 &&
1927 start_node.downcast::<Text>().is_some_and(|text| {
1928 text.has_whitespace_and_has_parent_with_whitespace_preserve(
1929 start_offset - 1,
1930 &[&'\u{0020}', &'\u{00A0}'],
1931 )
1932 })
1933 {
1934 start_offset -= 1;
1935 }
1936 // Step 3.4. Otherwise, break from this loop.
1937 break;
1938 }
1939 // Step 4. Let end node equal start node and end offset equal start offset.
1940 let mut end_node = start_node.clone();
1941 let mut end_offset = start_offset;
1942 // Step 5. Let length equal zero.
1943 let mut length = 0;
1944 // Step 6. Let collapse spaces be true if start offset is zero and start node follows a line break, otherwise false.
1945 let mut collapse_spaces = start_offset == 0 && start_node.follows_a_line_break();
1946 // Step 7. Repeat the following steps:
1947 loop {
1948 // Step 7.1. If end node has a child in the same editing host with index end offset,
1949 // set end node to that child, then set end offset to zero.
1950 if let Some(child) = end_node.children().nth(end_offset as usize) {
1951 if child.same_editing_host(&end_node) {
1952 end_node = child;
1953 end_offset = 0;
1954 continue;
1955 }
1956 }
1957 // Step 7.2. Otherwise, if end offset is end node's length
1958 // and end node does not precede a line break
1959 // and end node's parent is in the same editing host,
1960 // set end offset to one plus end node's index, then set end node to its parent.
1961 if end_offset == end_node.len() && !end_node.precedes_a_line_break() {
1962 if let Some(parent) = end_node.GetParentNode() {
1963 if parent.same_editing_host(&end_node) {
1964 end_offset = 1 + end_node.index();
1965 end_node = parent;
1966 }
1967 }
1968 continue;
1969 }
1970 // Step 7.3. Otherwise, if end node is a Text node and its parent's resolved value for "white-space"
1971 // is neither "pre" nor "pre-wrap"
1972 // and end offset is not end node's length and the end offsetth code unit of end node's data
1973 // is a space (0x0020) or non-breaking space (0x00A0):
1974 if let Some(text) = end_node.downcast::<Text>() {
1975 if text.has_whitespace_and_has_parent_with_whitespace_preserve(
1976 end_offset,
1977 &[&'\u{0020}', &'\u{00A0}'],
1978 ) {
1979 // Step 7.3.1. If fix collapsed space is true, and collapse spaces is true,
1980 // and the end offsetth code unit of end node's data is a space (0x0020):
1981 // call deleteData(end offset, 1) on end node, then continue this loop from the beginning.
1982 let has_space_at_offset = text
1983 .data()
1984 .chars()
1985 .nth(end_offset as usize)
1986 .is_some_and(|c| c == '\u{0020}');
1987 if fix_collapsed_space && collapse_spaces && has_space_at_offset {
1988 if text
1989 .upcast::<CharacterData>()
1990 .DeleteData(end_offset, 1)
1991 .is_err()
1992 {
1993 unreachable!("Invalid deletion for character at end offset");
1994 }
1995 continue;
1996 }
1997 // Step 7.3.2. Set collapse spaces to true if the end offsetth code unit of
1998 // end node's data is a space (0x0020), false otherwise.
1999 collapse_spaces = text
2000 .data()
2001 .chars()
2002 .nth(end_offset as usize)
2003 .is_some_and(|c| c == '\u{0020}');
2004 // Step 7.3.3. Add one to end offset.
2005 end_offset += 1;
2006 // Step 7.3.4. Add one to length.
2007 length += 1;
2008 continue;
2009 }
2010 }
2011 // Step 7.4. Otherwise, break from this loop.
2012 break;
2013 }
2014 // Step 8. If fix collapsed space is true, then while (start node, start offset)
2015 // is before (end node, end offset):
2016 if fix_collapsed_space {
2017 while bp_position(&start_node, start_offset, &end_node, end_offset) ==
2018 Some(Ordering::Less)
2019 {
2020 // Step 8.1. If end node has a child in the same editing host with index end offset − 1,
2021 // set end node to that child, then set end offset to end node's length.
2022 if end_offset > 0 {
2023 if let Some(child) = end_node.children().nth(end_offset as usize - 1) {
2024 if child.same_editing_host(&end_node) {
2025 end_node = child;
2026 end_offset = end_node.len();
2027 continue;
2028 }
2029 }
2030 }
2031 // Step 8.2. Otherwise, if end offset is zero and end node's parent is in the same editing host,
2032 // set end offset to end node's index, then set end node to its parent.
2033 if let Some(parent) = end_node.GetParentNode() {
2034 if end_offset == 0 && parent.same_editing_host(&end_node) {
2035 end_offset = end_node.index();
2036 end_node = parent;
2037 continue;
2038 }
2039 }
2040 // Step 8.3. Otherwise, if end node is a Text node and its parent's resolved value for "white-space"
2041 // is neither "pre" nor "pre-wrap"
2042 // and end offset is end node's length and the last code unit of end node's data
2043 // is a space (0x0020) and end node precedes a line break:
2044 if let Some(text) = end_node.downcast::<Text>() {
2045 if text.has_whitespace_and_has_parent_with_whitespace_preserve(
2046 text.data().len() as u32,
2047 &[&'\u{0020}'],
2048 ) && end_node.precedes_a_line_break()
2049 {
2050 // Step 8.3.1. Subtract one from end offset.
2051 end_offset -= 1;
2052 // Step 8.3.2. Subtract one from length.
2053 length -= 1;
2054 // Step 8.3.3. Call deleteData(end offset, 1) on end node.
2055 if text
2056 .upcast::<CharacterData>()
2057 .DeleteData(end_offset, 1)
2058 .is_err()
2059 {
2060 unreachable!("Invalid deletion for character at end offset");
2061 }
2062 continue;
2063 }
2064 }
2065 // Step 8.4. Otherwise, break from this loop.
2066 break;
2067 }
2068 }
2069 // Step 9. Let replacement whitespace be the canonical space sequence of length length.
2070 // non-breaking start is true if start offset is zero and start node follows a line break, and false otherwise.
2071 // non-breaking end is true if end offset is end node's length and end node precedes a line break, and false otherwise.
2072 let replacement_whitespace = Node::canonical_space_sequence(
2073 length,
2074 start_offset == 0 && start_node.follows_a_line_break(),
2075 end_offset == end_node.len() && end_node.precedes_a_line_break(),
2076 );
2077 let mut replacement_whitespace_chars = replacement_whitespace.chars();
2078 // Step 10. While (start node, start offset) is before (end node, end offset):
2079 while bp_position(&start_node, start_offset, &end_node, end_offset) == Some(Ordering::Less)
2080 {
2081 // 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.
2082 if let Some(child) = start_node.children().nth(start_offset as usize) {
2083 start_node = child;
2084 start_offset = 0;
2085 continue;
2086 }
2087 // Step 10.2. Otherwise, if start node is not a Text node or if start offset is start node's length,
2088 // set start offset to one plus start node's index, then set start node to its parent.
2089 let start_node_as_text = start_node.downcast::<Text>();
2090 if start_node_as_text.is_none() || start_offset == start_node.len() {
2091 start_offset = 1 + start_node.index();
2092 start_node = start_node
2093 .GetParentNode()
2094 .expect("Must always have a parent");
2095 continue;
2096 }
2097 let start_node_as_text =
2098 start_node_as_text.expect("Already verified none in previous statement");
2099 // Step 10.3. Otherwise:
2100 // Step 10.3.1. Remove the first code unit from replacement whitespace, and let element be that code unit.
2101 if let Some(element) = replacement_whitespace_chars.next() {
2102 // Step 10.3.2. If element is not the same as the start offsetth code unit of start node's data:
2103 if start_node_as_text.data().chars().nth(start_offset as usize) != Some(element) {
2104 let character_data = start_node_as_text.upcast::<CharacterData>();
2105 // Step 10.3.2.1. Call insertData(start offset, element) on start node.
2106 if character_data
2107 .InsertData(start_offset, element.to_string().into())
2108 .is_err()
2109 {
2110 unreachable!("Invalid insertion for character at start offset");
2111 }
2112 // Step 10.3.2.2. Call deleteData(start offset + 1, 1) on start node.
2113 if character_data.DeleteData(start_offset + 1, 1).is_err() {
2114 unreachable!("Invalid deletion for character at start offset + 1");
2115 }
2116 }
2117 }
2118 // Step 10.3.3. Add one to start offset.
2119 start_offset += 1;
2120 }
2121 }
2122
2123 /// <https://w3c.github.io/editing/docs/execCommand/#remove-extraneous-line-breaks-before>
2124 fn remove_extraneous_line_breaks_before(&self, cx: &mut js::context::JSContext) {
2125 let parent = self.GetParentNode();
2126 // Step 1. Let ref be the previousSibling of node.
2127 let Some(mut ref_) = self.GetPreviousSibling() else {
2128 // Step 2. If ref is null, abort these steps.
2129 return;
2130 };
2131 // Step 3. While ref has children, set ref to its lastChild.
2132 while let Some(last_child) = ref_.children().last() {
2133 ref_ = last_child;
2134 }
2135 // Step 4. While ref is invisible but not an extraneous line break,
2136 // and ref does not equal node's parent, set ref to the node before it in tree order.
2137 loop {
2138 if ref_.is_invisible() &&
2139 ref_.downcast::<HTMLBRElement>()
2140 .is_none_or(|br| !br.is_extraneous_line_break())
2141 {
2142 if let Some(parent) = parent.as_ref() {
2143 if ref_ != *parent {
2144 ref_ = match ref_.preceding_nodes(parent).nth(0) {
2145 None => break,
2146 Some(node) => node,
2147 };
2148 continue;
2149 }
2150 }
2151 }
2152 break;
2153 }
2154 // Step 5. If ref is an editable extraneous line break, remove it from its parent.
2155 if ref_.is_editable() &&
2156 ref_.downcast::<HTMLBRElement>()
2157 .is_some_and(|br| br.is_extraneous_line_break())
2158 {
2159 assert!(ref_.has_parent());
2160 ref_.remove_self(cx);
2161 }
2162 }
2163
2164 /// <https://w3c.github.io/editing/docs/execCommand/#remove-extraneous-line-breaks-at-the-end-of>
2165 fn remove_extraneous_line_breaks_at_the_end_of(&self, cx: &mut js::context::JSContext) {
2166 // Step 1. Let ref be node.
2167 let mut ref_ = DomRoot::from_ref(self);
2168 // Step 2. While ref has children, set ref to its lastChild.
2169 while let Some(last_child) = ref_.children().last() {
2170 ref_ = last_child;
2171 }
2172 // Step 3. While ref is invisible but not an extraneous line break, and ref does not equal node,
2173 // set ref to the node before it in tree order.
2174 loop {
2175 if ref_.is_invisible() &&
2176 *ref_ != *self &&
2177 ref_.downcast::<HTMLBRElement>()
2178 .is_none_or(|br| !br.is_extraneous_line_break())
2179 {
2180 if let Some(parent_of_ref) = ref_.GetParentNode() {
2181 ref_ = match ref_.preceding_nodes(&parent_of_ref).nth(0) {
2182 None => break,
2183 Some(node) => node,
2184 };
2185 continue;
2186 }
2187 }
2188 break;
2189 }
2190 // Step 4. If ref is an editable extraneous line break:
2191 if ref_.is_editable() &&
2192 ref_.downcast::<HTMLBRElement>()
2193 .is_some_and(|br| br.is_extraneous_line_break())
2194 {
2195 // Step 4.1. While ref's parent is editable and invisible, set ref to its parent.
2196 loop {
2197 if let Some(parent) = ref_.GetParentNode() {
2198 if parent.is_editable() && parent.is_invisible() {
2199 ref_ = parent;
2200 continue;
2201 }
2202 }
2203 break;
2204 }
2205 // Step 4.2. Remove ref from its parent.
2206 assert!(ref_.has_parent());
2207 ref_.remove_self(cx);
2208 }
2209 }
2210
2211 /// <https://w3c.github.io/editing/docs/execCommand/#remove-extraneous-line-breaks-from>
2212 fn remove_extraneous_line_breaks_from(&self, cx: &mut js::context::JSContext) {
2213 // > To remove extraneous line breaks from a node, first remove extraneous line breaks before it,
2214 // > then remove extraneous line breaks at the end of it.
2215 self.remove_extraneous_line_breaks_before(cx);
2216 self.remove_extraneous_line_breaks_at_the_end_of(cx);
2217 }
2218
2219 /// <https://w3c.github.io/editing/docs/execCommand/#preserving-its-descendants>
2220 fn remove_preserving_its_descendants(&self, cx: &mut js::context::JSContext) {
2221 // > To remove a node node while preserving its descendants,
2222 // > split the parent of node's children if it has any.
2223 // > If it has no children, instead remove it from its parent.
2224 if self.children_count() == 0 {
2225 assert!(self.has_parent());
2226 self.remove_self(cx);
2227 } else {
2228 rooted_vec!(let children <- self.children().map(|child| DomRoot::as_traced(&child)));
2229 split_the_parent(cx, children.r());
2230 }
2231 }
2232
2233 /// <https://w3c.github.io/editing/docs/execCommand/#effective-command-value>
2234 fn effective_command_value(&self, command: &CommandName) -> Option<DOMString> {
2235 // Step 1. If neither node nor its parent is an Element, return null.
2236 // Step 2. If node is not an Element, return the effective command value of its parent for command.
2237 let Some(element) = self.downcast::<Element>() else {
2238 return self
2239 .GetParentElement()
2240 .and_then(|parent| parent.upcast::<Node>().effective_command_value(command));
2241 };
2242 match command {
2243 // Step 3. If command is "createLink" or "unlink":
2244 CommandName::CreateLink | CommandName::Unlink => {
2245 // Step 3.1. While node is not null, and is not an a element that has an href attribute, set node to its parent.
2246 let mut current_node = Some(DomRoot::from_ref(self));
2247 while let Some(node) = current_node {
2248 if let Some(anchor_value) =
2249 node.downcast::<HTMLAnchorElement>().and_then(|anchor| {
2250 anchor
2251 .upcast::<Element>()
2252 .get_attribute(&local_name!("href"))
2253 })
2254 {
2255 // Step 3.3. Return the value of node's href attribute.
2256 return Some(DOMString::from(&**anchor_value.value()));
2257 }
2258 current_node = node.GetParentNode();
2259 }
2260 // Step 3.2. If node is null, return null.
2261 None
2262 },
2263 // Step 4. If command is "backColor" or "hiliteColor":
2264 CommandName::BackColor | CommandName::HiliteColor => {
2265 // Step 4.1. While the resolved value of "background-color" on node is any fully transparent value,
2266 // and node's parent is an Element, set node to its parent.
2267 // TODO
2268 // Step 4.2. Return the resolved value of "background-color" for node.
2269 // TODO
2270 None
2271 },
2272 // Step 5. If command is "subscript" or "superscript":
2273 CommandName::Subscript | CommandName::Superscript => {
2274 // Step 5.1. Let affected by subscript and affected by superscript be two boolean variables,
2275 // both initially false.
2276 let mut affected_by_subscript = false;
2277 let mut affected_by_superscript = false;
2278 // Step 5.2. While node is an inline node:
2279 let mut current_node = Some(DomRoot::from_ref(self));
2280 while let Some(node) = current_node {
2281 if !node.is_inline_node() {
2282 break;
2283 }
2284 // Step 5.2.1. If node is a sub, set affected by subscript to true.
2285 if *element.local_name() == local_name!("sub") {
2286 affected_by_subscript = true;
2287 } else if *element.local_name() == local_name!("sup") {
2288 // Step 5.2.2. Otherwise, if node is a sup, set affected by superscript to true.
2289 affected_by_superscript = true;
2290 }
2291 // Step 5.2.3. Set node to its parent.
2292 current_node = node.GetParentNode();
2293 }
2294 Some(match (affected_by_subscript, affected_by_superscript) {
2295 // Step 5.3. If affected by subscript and affected by superscript are both true,
2296 // return the string "mixed".
2297 (true, true) => "mixed".into(),
2298 // Step 5.4. If affected by subscript is true, return "subscript".
2299 (true, false) => "subscript".into(),
2300 // Step 5.5. If affected by superscript is true, return "superscript".
2301 (false, true) => "superscript".into(),
2302 // Step 5.6. Return null.
2303 (false, false) => return None,
2304 })
2305 },
2306 // Step 6. If command is "strikethrough",
2307 // and the "text-decoration" property of node or any of its ancestors has resolved value containing "line-through",
2308 // return "line-through". Otherwise, return null.
2309 // TODO
2310 CommandName::Strikethrough => None,
2311 // Step 7. If command is "underline",
2312 // and the "text-decoration" property of node or any of its ancestors has resolved value containing "underline",
2313 // return "underline". Otherwise, return null.
2314 // TODO
2315 CommandName::Underline => None,
2316 // Step 8. Return the resolved value for node of the relevant CSS property for command.
2317 _ => command.resolved_value_for_node(element),
2318 }
2319 }
2320}
2321
2322pub(crate) trait ContentEditableRange {
2323 fn handle_focus_state_for_contenteditable(&self, can_gc: CanGc);
2324}
2325
2326impl ContentEditableRange for HTMLElement {
2327 /// There is no specification for this implementation. Instead, it is
2328 /// reverse-engineered based on the WPT test
2329 /// /selection/contenteditable/initial-selection-on-focus.tentative.html
2330 fn handle_focus_state_for_contenteditable(&self, can_gc: CanGc) {
2331 if !self.is_editing_host() {
2332 return;
2333 }
2334 let document = self.owner_document();
2335 let Some(selection) = document.GetSelection(can_gc) else {
2336 return;
2337 };
2338 let range = self
2339 .upcast::<Element>()
2340 .ensure_contenteditable_selection_range(&document, can_gc);
2341 // If the current range is already associated with this contenteditable
2342 // element, then we shouldn't do anything. This is important when focus
2343 // is lost and regained, but selection was changed beforehand. In that
2344 // case, we should maintain the selection as it were, by not creating
2345 // a new range.
2346 if selection
2347 .active_range()
2348 .is_some_and(|active| active == range)
2349 {
2350 return;
2351 }
2352 let node = self.upcast::<Node>();
2353 let mut selected_node = DomRoot::from_ref(node);
2354 let mut previous_eligible_node = DomRoot::from_ref(node);
2355 let mut previous_node = DomRoot::from_ref(node);
2356 let mut selected_offset = 0;
2357 for child in node.traverse_preorder(ShadowIncluding::Yes) {
2358 if let Some(text) = child.downcast::<Text>() {
2359 // Note that to consider it whitespace, it needs to take more
2360 // into account than simply "it has a non-whitespace" character.
2361 // Therefore, we need to first check if it is not a whitespace
2362 // node and only then can we find what the relevant character is.
2363 if !text.is_whitespace_node() {
2364 // A node with "white-space: pre" set must select its first
2365 // character, regardless if that's a whitespace character or not.
2366 let is_pre_formatted_text_node = child
2367 .GetParentElement()
2368 .and_then(|parent| parent.style())
2369 .is_some_and(|style| {
2370 style.get_inherited_text().white_space_collapse ==
2371 WhiteSpaceCollapse::Preserve
2372 });
2373 if !is_pre_formatted_text_node {
2374 // If it isn't pre-formatted, then we should instead select the
2375 // first non-whitespace character.
2376 selected_offset = text
2377 .data()
2378 .find(|c: char| !c.is_whitespace())
2379 .unwrap_or_default() as u32;
2380 }
2381 selected_node = child;
2382 break;
2383 }
2384 }
2385 // For <input>, <textarea>, <hr> and <br> elements, we should select the previous
2386 // node, regardless if it was a block node or not
2387 if matches!(
2388 child.type_id(),
2389 NodeTypeId::Element(ElementTypeId::HTMLElement(
2390 HTMLElementTypeId::HTMLInputElement,
2391 )) | NodeTypeId::Element(ElementTypeId::HTMLElement(
2392 HTMLElementTypeId::HTMLTextAreaElement,
2393 )) | NodeTypeId::Element(ElementTypeId::HTMLElement(
2394 HTMLElementTypeId::HTMLHRElement,
2395 )) | NodeTypeId::Element(ElementTypeId::HTMLElement(
2396 HTMLElementTypeId::HTMLBRElement,
2397 ))
2398 ) {
2399 selected_node = previous_node;
2400 break;
2401 }
2402 // When we encounter a non-contenteditable element, we should select the previous
2403 // eligible node
2404 if child
2405 .downcast::<HTMLElement>()
2406 .is_some_and(|el| el.ContentEditable().str() == "false")
2407 {
2408 selected_node = previous_eligible_node;
2409 break;
2410 }
2411 // We can only select block nodes as eligible nodes for the case of non-conenteditable
2412 // nodes
2413 if child.is_block_node() {
2414 previous_eligible_node = child.clone();
2415 }
2416 previous_node = child;
2417 }
2418 range.set_start(&selected_node, selected_offset);
2419 range.set_end(&selected_node, selected_offset);
2420 selection.AddRange(&range);
2421 }
2422}
2423
2424trait EquivalentPoint {
2425 fn previous_equivalent_point(&self) -> Option<(DomRoot<Node>, u32)>;
2426 fn next_equivalent_point(&self) -> Option<(DomRoot<Node>, u32)>;
2427 fn first_equivalent_point(self) -> (DomRoot<Node>, u32);
2428 fn last_equivalent_point(self) -> (DomRoot<Node>, u32);
2429}
2430
2431impl EquivalentPoint for (DomRoot<Node>, u32) {
2432 /// <https://w3c.github.io/editing/docs/execCommand/#previous-equivalent-point>
2433 fn previous_equivalent_point(&self) -> Option<(DomRoot<Node>, u32)> {
2434 let (node, offset) = self;
2435 // Step 1. If node's length is zero, return null.
2436 let len = node.len();
2437 if len == 0 {
2438 return None;
2439 }
2440 // Step 2. If offset is 0, and node's parent is not null, and node is an inline node,
2441 // return (node's parent, node's index).
2442 if *offset == 0 && node.is_inline_node() {
2443 if let Some(parent) = node.GetParentNode() {
2444 return Some((parent, node.index()));
2445 }
2446 }
2447 // Step 3. If node has a child with index offset − 1, and that child's length is not zero,
2448 // and that child is an inline node, return (that child, that child's length).
2449 if *offset > 0 {
2450 if let Some(child) = node.children().nth(*offset as usize - 1) {
2451 if !child.is_empty() && child.is_inline_node() {
2452 let len = child.len();
2453 return Some((child, len));
2454 }
2455 }
2456 }
2457
2458 // Step 4. Return null.
2459 None
2460 }
2461
2462 /// <https://w3c.github.io/editing/docs/execCommand/#next-equivalent-point>
2463 fn next_equivalent_point(&self) -> Option<(DomRoot<Node>, u32)> {
2464 let (node, offset) = self;
2465 // Step 1. If node's length is zero, return null.
2466 let len = node.len();
2467 if len == 0 {
2468 return None;
2469 }
2470
2471 // Step 2.
2472 //
2473 // This step does not exist in the spec
2474
2475 // Step 3. If offset is node's length, and node's parent is not null, and node is an inline node,
2476 // return (node's parent, 1 + node's index).
2477 if *offset == len && node.is_inline_node() {
2478 if let Some(parent) = node.GetParentNode() {
2479 return Some((parent, node.index() + 1));
2480 }
2481 }
2482
2483 // Step 4.
2484 //
2485 // This step does not exist in the spec
2486
2487 // Step 5. If node has a child with index offset, and that child's length is not zero,
2488 // and that child is an inline node, return (that child, 0).
2489 if let Some(child) = node.children().nth(*offset as usize) {
2490 if !child.is_empty() && child.is_inline_node() {
2491 return Some((child, 0));
2492 }
2493 }
2494
2495 // Step 6.
2496 //
2497 // This step does not exist in the spec
2498
2499 // Step 7. Return null.
2500 None
2501 }
2502
2503 /// <https://w3c.github.io/editing/docs/execCommand/#first-equivalent-point>
2504 fn first_equivalent_point(self) -> (DomRoot<Node>, u32) {
2505 let mut previous_equivalent_point = self;
2506 // Step 1. While (node, offset)'s previous equivalent point is not null, set (node, offset) to its previous equivalent point.
2507 loop {
2508 if let Some(next) = previous_equivalent_point.previous_equivalent_point() {
2509 previous_equivalent_point = next;
2510 } else {
2511 // Step 2. Return (node, offset).
2512 return previous_equivalent_point;
2513 }
2514 }
2515 }
2516
2517 /// <https://w3c.github.io/editing/docs/execCommand/#last-equivalent-point>
2518 fn last_equivalent_point(self) -> (DomRoot<Node>, u32) {
2519 let mut next_equivalent_point = self;
2520 // Step 1. While (node, offset)'s next equivalent point is not null, set (node, offset) to its next equivalent point.
2521 loop {
2522 if let Some(next) = next_equivalent_point.next_equivalent_point() {
2523 next_equivalent_point = next;
2524 } else {
2525 // Step 2. Return (node, offset).
2526 return next_equivalent_point;
2527 }
2528 }
2529 }
2530}
2531
2532enum BoolOrOptionalString {
2533 Bool(bool),
2534 OptionalString(Option<DOMString>),
2535}
2536
2537impl From<Option<DOMString>> for BoolOrOptionalString {
2538 fn from(optional_string: Option<DOMString>) -> Self {
2539 Self::OptionalString(optional_string)
2540 }
2541}
2542
2543impl From<bool> for BoolOrOptionalString {
2544 fn from(bool_: bool) -> Self {
2545 Self::Bool(bool_)
2546 }
2547}
2548
2549struct RecordedStateOfNode {
2550 command: CommandName,
2551 value: BoolOrOptionalString,
2552}
2553
2554impl RecordedStateOfNode {
2555 fn for_command_node(command: CommandName, node: &Node) -> Self {
2556 let value = node.effective_command_value(&command).into();
2557 Self { command, value }
2558 }
2559}
2560
2561impl Range {
2562 /// <https://w3c.github.io/editing/docs/execCommand/#effectively-contained>
2563 fn is_effectively_contained_node(&self, node: &Node) -> bool {
2564 // > A node node is effectively contained in a range range if range is not collapsed,
2565 if self.collapsed() {
2566 return false;
2567 }
2568 // > and at least one of the following holds:
2569 // > node is range's start node, it is a Text node, and its length is different from range's start offset.
2570 let start_container = self.start_container();
2571 if *start_container == *node && node.is::<Text>() && node.len() != self.start_offset() {
2572 return true;
2573 }
2574 // > node is range's end node, it is a Text node, and range's end offset is not 0.
2575 let end_container = self.end_container();
2576 if *end_container == *node && node.is::<Text>() && self.end_offset() != 0 {
2577 return true;
2578 }
2579 // > node is contained in range.
2580 if self.contains(node) {
2581 return true;
2582 }
2583 // > node has at least one child; and all its children are effectively contained in range;
2584 node.children_count() > 0 && node.children().all(|child| self.is_effectively_contained_node(&child))
2585 // > and either range's start node is not a descendant of node or is not a Text node or range's start offset is zero;
2586 && (!node.is_ancestor_of(&start_container) || !start_container.is::<Text>() || self.start_offset() == 0)
2587 // > and either range's end node is not a descendant of node or is not a Text node or range's end offset is its end node's length.
2588 && (!node.is_ancestor_of(&end_container) || !end_container.is::<Text>() || self.end_offset() == end_container.len())
2589 }
2590
2591 pub(crate) fn first_formattable_contained_node(&self) -> Option<DomRoot<Node>> {
2592 if self.collapsed() {
2593 return None;
2594 }
2595
2596 self.CommonAncestorContainer()
2597 .traverse_preorder(ShadowIncluding::No)
2598 .find(|child| child.is_formattable() && self.is_effectively_contained_node(child))
2599 }
2600
2601 fn for_each_effectively_contained_child<Callback: FnMut(&Node)>(&self, mut callback: Callback) {
2602 if self.collapsed() {
2603 return;
2604 }
2605
2606 for child in self
2607 .CommonAncestorContainer()
2608 .traverse_preorder(ShadowIncluding::No)
2609 {
2610 if self.is_effectively_contained_node(&child) {
2611 callback(&child);
2612 }
2613 }
2614 }
2615
2616 /// <https://w3c.github.io/editing/docs/execCommand/#record-current-states-and-values>
2617 fn record_current_states_and_values(&self) -> Vec<RecordedStateOfNode> {
2618 // Step 1. Let overrides be a list of (string, string or boolean) ordered pairs, initially empty.
2619 //
2620 // We return the vec in one go for the relevant values
2621
2622 // Step 2. Let node be the first formattable node effectively contained in the active range,
2623 // or null if there is none.
2624 let Some(node) = self.first_formattable_contained_node() else {
2625 // Step 3. If node is null, return overrides.
2626 return vec![];
2627 };
2628 // Step 8. Return overrides.
2629 vec![
2630 // Step 4. Add ("createLink", node's effective command value for "createLink") to overrides.
2631 RecordedStateOfNode::for_command_node(CommandName::CreateLink, &node),
2632 // Step 5. For each command in the list
2633 // "bold", "italic", "strikethrough", "subscript", "superscript", "underline", in order:
2634 // if node's effective command value for command is one of its inline command activated values,
2635 // add (command, true) to overrides, and otherwise add (command, false) to overrides.
2636 // TODO
2637
2638 // Step 6. For each command in the list "fontName", "foreColor", "hiliteColor", in order:
2639 // add (command, command's value) to overrides.
2640 // TODO
2641
2642 // Step 7. Add ("fontSize", node's effective command value for "fontSize") to overrides.
2643 RecordedStateOfNode::for_command_node(CommandName::FontSize, &node),
2644 ]
2645 }
2646
2647 /// <https://w3c.github.io/editing/docs/execCommand/#restore-states-and-values>
2648 fn restore_states_and_values(
2649 &self,
2650 cx: &mut js::context::JSContext,
2651 selection: &Selection,
2652 context_object: &Document,
2653 overrides: Vec<RecordedStateOfNode>,
2654 ) {
2655 // Step 1. Let node be the first formattable node effectively contained in the active range,
2656 // or null if there is none.
2657 let mut first_formattable_contained_node = self.first_formattable_contained_node();
2658 for override_state in overrides {
2659 // Step 2. If node is not null, then for each (command, override) pair in overrides, in order:
2660 if let Some(ref node) = first_formattable_contained_node {
2661 match override_state.value {
2662 // Step 2.1. If override is a boolean, and queryCommandState(command)
2663 // returns something different from override, take the action for command,
2664 // with value equal to the empty string.
2665 BoolOrOptionalString::Bool(bool_)
2666 if override_state
2667 .command
2668 .current_state(context_object)
2669 .is_some_and(|value| value != bool_) =>
2670 {
2671 override_state
2672 .command
2673 .execute(cx, context_object, selection, "".into());
2674 },
2675 BoolOrOptionalString::OptionalString(optional_string) => {
2676 match override_state.command {
2677 // Step 2.3. Otherwise, if override is a string; and command is "createLink";
2678 // and either there is a value override for "createLink" that is not equal to override,
2679 // or there is no value override for "createLink" and node's effective command value
2680 // for "createLink" is not equal to override: take the action for "createLink", with value equal to override.
2681 CommandName::CreateLink => {
2682 let value_override =
2683 context_object.value_override(&CommandName::CreateLink);
2684 if value_override != optional_string {
2685 CommandName::CreateLink.execute(
2686 cx,
2687 context_object,
2688 selection,
2689 optional_string.unwrap_or_default(),
2690 );
2691 }
2692 },
2693 // Step 2.4. Otherwise, if override is a string; and command is "fontSize";
2694 // and either there is a value override for "fontSize" that is not equal to override,
2695 // or there is no value override for "fontSize" and node's effective command value for "fontSize"
2696 // is not loosely equivalent to override:
2697 CommandName::FontSize => {
2698 let value_override =
2699 context_object.value_override(&CommandName::FontSize);
2700 if value_override != optional_string ||
2701 (value_override.is_none() &&
2702 !CommandName::FontSize.are_loosely_equivalent_values(
2703 node.effective_command_value(&CommandName::FontSize)
2704 .as_ref(),
2705 optional_string.as_ref(),
2706 ))
2707 {
2708 // Step 2.5. Convert override to an integer number of pixels,
2709 // and set override to the legacy font size for the result.
2710 let pixels = optional_string
2711 .and_then(|value| value.parse::<i32>().ok())
2712 .map(|value| {
2713 legacy_font_size_for(value as f32, context_object)
2714 })
2715 .unwrap_or("7".into());
2716 // Step 2.6. Take the action for "fontSize", with value equal to override.
2717 CommandName::FontSize.execute(
2718 cx,
2719 context_object,
2720 selection,
2721 pixels,
2722 );
2723 }
2724 },
2725 // Step 2.2. Otherwise, if override is a string, and command is neither "createLink" nor "fontSize",
2726 // and queryCommandValue(command) returns something not equivalent to override,
2727 // take the action for command, with value equal to override.
2728 command
2729 if command.current_value(cx, context_object) != optional_string =>
2730 {
2731 command.execute(
2732 cx,
2733 context_object,
2734 selection,
2735 optional_string.unwrap_or_default(),
2736 );
2737 },
2738 // Step 2.5. Otherwise, continue this loop from the beginning.
2739 _ => {
2740 continue;
2741 },
2742 }
2743 },
2744 // Step 2.5. Otherwise, continue this loop from the beginning.
2745 _ => {
2746 continue;
2747 },
2748 }
2749 // Step 2.6. Set node to the first formattable node effectively contained in the active range, if there is one.
2750 first_formattable_contained_node = self.first_formattable_contained_node();
2751 } else {
2752 // Step 3. Otherwise, for each (command, override) pair in overrides, in order:
2753 // Step 3.1. If override is a boolean, set the state override for command to override.
2754 match override_state.value {
2755 BoolOrOptionalString::Bool(bool_) => {
2756 context_object.set_state_override(override_state.command, Some(bool_))
2757 },
2758 // Step 3.2. If override is a string, set the value override for command to override.
2759 BoolOrOptionalString::OptionalString(optional_string) => {
2760 context_object.set_value_override(override_state.command, optional_string)
2761 },
2762 }
2763 }
2764 }
2765 }
2766}
2767
2768#[derive(Default, PartialEq)]
2769pub(crate) enum SelectionDeletionBlockMerging {
2770 #[default]
2771 Merge,
2772 Skip,
2773}
2774
2775#[derive(Default, PartialEq)]
2776pub(crate) enum SelectionDeletionStripWrappers {
2777 #[default]
2778 Strip,
2779}
2780
2781#[derive(Default, PartialEq)]
2782pub(crate) enum SelectionDeleteDirection {
2783 #[default]
2784 Forward,
2785 Backward,
2786}
2787
2788pub(crate) trait SelectionExecCommandSupport {
2789 fn delete_the_selection(
2790 &self,
2791 cx: &mut js::context::JSContext,
2792 context_object: &Document,
2793 block_merging: SelectionDeletionBlockMerging,
2794 strip_wrappers: SelectionDeletionStripWrappers,
2795 direction: SelectionDeleteDirection,
2796 );
2797 fn set_the_selection_value(
2798 &self,
2799 cx: &mut js::context::JSContext,
2800 new_value: Option<DOMString>,
2801 command: CommandName,
2802 context_object: &Document,
2803 );
2804}
2805
2806impl SelectionExecCommandSupport for Selection {
2807 /// <https://w3c.github.io/editing/docs/execCommand/#delete-the-selection>
2808 fn delete_the_selection(
2809 &self,
2810 cx: &mut js::context::JSContext,
2811 context_object: &Document,
2812 block_merging: SelectionDeletionBlockMerging,
2813 strip_wrappers: SelectionDeletionStripWrappers,
2814 direction: SelectionDeleteDirection,
2815 ) {
2816 // Step 1. If the active range is null, abort these steps and do nothing.
2817 let Some(active_range) = self.active_range() else {
2818 return;
2819 };
2820
2821 // Step 2. Canonicalize whitespace at the active range's start.
2822 active_range
2823 .start_container()
2824 .canonicalize_whitespace(active_range.start_offset(), true);
2825
2826 // Step 3. Canonicalize whitespace at the active range's end.
2827 active_range
2828 .end_container()
2829 .canonicalize_whitespace(active_range.end_offset(), true);
2830
2831 // Step 4. Let (start node, start offset) be the last equivalent point for the active range's start.
2832 let (mut start_node, mut start_offset) =
2833 (active_range.start_container(), active_range.start_offset()).last_equivalent_point();
2834
2835 // Step 5. Let (end node, end offset) be the first equivalent point for the active range's end.
2836 let (mut end_node, mut end_offset) =
2837 (active_range.end_container(), active_range.end_offset()).first_equivalent_point();
2838
2839 // Step 6. If (end node, end offset) is not after (start node, start offset):
2840 if bp_position(&end_node, end_offset, &start_node, start_offset) != Some(Ordering::Greater)
2841 {
2842 // Step 6.1. If direction is "forward", call collapseToStart() on the context object's selection.
2843 if direction == SelectionDeleteDirection::Forward {
2844 if self.CollapseToStart(CanGc::from_cx(cx)).is_err() {
2845 unreachable!("Should be able to collapse to start");
2846 }
2847 } else {
2848 // Step 6.2. Otherwise, call collapseToEnd() on the context object's selection.
2849 if self.CollapseToEnd(CanGc::from_cx(cx)).is_err() {
2850 unreachable!("Should be able to collapse to end");
2851 }
2852 }
2853 // Step 6.3. Abort these steps.
2854 return;
2855 }
2856
2857 // Step 7. If start node is a Text node and start offset is 0, set start offset to the index of start node,
2858 // then set start node to its parent.
2859 if start_node.is::<Text>() && start_offset == 0 {
2860 start_offset = start_node.index();
2861 start_node = start_node
2862 .GetParentNode()
2863 .expect("Must always have a parent");
2864 }
2865
2866 // Step 8. If end node is a Text node and end offset is its length, set end offset to one plus the index of end node,
2867 // then set end node to its parent.
2868 if end_node.is::<Text>() && end_offset == end_node.len() {
2869 end_offset = end_node.index() + 1;
2870 end_node = end_node.GetParentNode().expect("Must always have a parent");
2871 }
2872
2873 // Step 9. Call collapse(start node, start offset) on the context object's selection.
2874 if self
2875 .Collapse(Some(&start_node), start_offset, CanGc::from_cx(cx))
2876 .is_err()
2877 {
2878 unreachable!("Must always be able to collapse");
2879 }
2880
2881 // Step 10. Call extend(end node, end offset) on the context object's selection.
2882 if self
2883 .Extend(&end_node, end_offset, CanGc::from_cx(cx))
2884 .is_err()
2885 {
2886 unreachable!("Must always be able to extend");
2887 }
2888
2889 // Step 11.
2890 //
2891 // This step does not exist in the spec
2892
2893 // Step 12. Let start block be the active range's start node.
2894 let Some(active_range) = self.active_range() else {
2895 return;
2896 };
2897 let mut start_block = active_range.start_container();
2898
2899 // Step 13. While start block's parent is in the same editing host and start block is an inline node,
2900 // set start block to its parent.
2901 loop {
2902 if start_block.is_inline_node() {
2903 if let Some(parent) = start_block.GetParentNode() {
2904 if parent.same_editing_host(&start_node) {
2905 start_block = parent;
2906 continue;
2907 }
2908 }
2909 }
2910 break;
2911 }
2912
2913 // Step 14. If start block is neither a block node nor an editing host,
2914 // or "span" is not an allowed child of start block,
2915 // or start block is a td or th, set start block to null.
2916 let start_block = if (!start_block.is_block_node() && !start_block.is_editing_host()) ||
2917 !is_allowed_child(
2918 NodeOrString::String("span".to_owned()),
2919 NodeOrString::Node(start_block.clone()),
2920 ) ||
2921 start_block.is::<HTMLTableCellElement>()
2922 {
2923 None
2924 } else {
2925 Some(start_block)
2926 };
2927
2928 // Step 15. Let end block be the active range's end node.
2929 let mut end_block = active_range.end_container();
2930
2931 // Step 16. While end block's parent is in the same editing host and end block is an inline node, set end block to its parent.
2932 loop {
2933 if end_block.is_inline_node() {
2934 if let Some(parent) = end_block.GetParentNode() {
2935 if parent.same_editing_host(&end_block) {
2936 end_block = parent;
2937 continue;
2938 }
2939 }
2940 }
2941 break;
2942 }
2943
2944 // Step 17. If end block is neither a block node nor an editing host, or "span" is not an allowed child of end block,
2945 // or end block is a td or th, set end block to null.
2946 let end_block = if (!end_block.is_block_node() && !end_block.is_editing_host()) ||
2947 !is_allowed_child(
2948 NodeOrString::String("span".to_owned()),
2949 NodeOrString::Node(end_block.clone()),
2950 ) ||
2951 end_block.is::<HTMLTableCellElement>()
2952 {
2953 None
2954 } else {
2955 Some(end_block)
2956 };
2957
2958 // Step 18.
2959 //
2960 // This step does not exist in the spec
2961
2962 // Step 19. Record current states and values, and let overrides be the result.
2963 let overrides = active_range.record_current_states_and_values();
2964
2965 // Step 20.
2966 //
2967 // This step does not exist in the spec
2968
2969 // Step 21. If start node and end node are the same, and start node is an editable Text node:
2970 if start_node == end_node && start_node.is_editable() {
2971 if let Some(start_text) = start_node.downcast::<Text>() {
2972 // Step 21.1. Call deleteData(start offset, end offset − start offset) on start node.
2973 if start_text
2974 .upcast::<CharacterData>()
2975 .DeleteData(start_offset, end_offset - start_offset)
2976 .is_err()
2977 {
2978 unreachable!("Must always be able to delete");
2979 }
2980 // Step 21.2. Canonicalize whitespace at (start node, start offset), with fix collapsed space false.
2981 start_node.canonicalize_whitespace(start_offset, false);
2982 // Step 21.3. If direction is "forward", call collapseToStart() on the context object's selection.
2983 if direction == SelectionDeleteDirection::Forward {
2984 if self.CollapseToStart(CanGc::from_cx(cx)).is_err() {
2985 unreachable!("Should be able to collapse to start");
2986 }
2987 } else {
2988 // Step 21.4. Otherwise, call collapseToEnd() on the context object's selection.
2989 if self.CollapseToEnd(CanGc::from_cx(cx)).is_err() {
2990 unreachable!("Should be able to collapse to end");
2991 }
2992 }
2993 // Step 21.5. Restore states and values from overrides.
2994 active_range.restore_states_and_values(cx, self, context_object, overrides);
2995
2996 // Step 21.6. Abort these steps.
2997 return;
2998 }
2999 }
3000
3001 // Step 22. If start node is an editable Text node, call deleteData() on it, with start offset as
3002 // the first argument and (length of start node − start offset) as the second argument.
3003 if start_node.is_editable() {
3004 if let Some(start_text) = start_node.downcast::<Text>() {
3005 if start_text
3006 .upcast::<CharacterData>()
3007 .DeleteData(start_offset, start_node.len() - start_offset)
3008 .is_err()
3009 {
3010 unreachable!("Must always be able to delete");
3011 }
3012 }
3013 }
3014
3015 // Step 23. Let node list be a list of nodes, initially empty.
3016 rooted_vec!(let mut node_list);
3017
3018 // Step 24. For each node contained in the active range, append node to node list if the
3019 // last member of node list (if any) is not an ancestor of node; node is editable;
3020 // and node is not a thead, tbody, tfoot, tr, th, or td.
3021 let Ok(contained_children) = active_range.contained_children() else {
3022 unreachable!("Must always have contained children");
3023 };
3024 for node in contained_children.contained_children {
3025 // This type is only used to tell the compiler how to handle the type of `node_list.last()`.
3026 // It is not allowed to add a `& DomRoot<Node>` annotation, as test-tidy disallows that.
3027 // However, if we omit the type, the compiler doesn't know what it is, since we also
3028 // aren't allowed to add a type annotation to `node_list` itself, as that is handled
3029 // by the `rooted_vec` macro. Lastly, we also can't make it `&Node`, since then the compiler
3030 // thinks that the contents of the `RootedVec` is `Node`, whereas it is should be
3031 // `RootedVec<DomRoot<Node>>`. The type alias here doesn't upset test-tidy,
3032 // while also providing the necessary information to the compiler to work.
3033 type DomRootNode = DomRoot<Node>;
3034 if node.is_editable() &&
3035 !(node.is::<HTMLTableSectionElement>() ||
3036 node.is::<HTMLTableRowElement>() ||
3037 node.is::<HTMLTableCellElement>()) &&
3038 node_list
3039 .last()
3040 .is_none_or(|last: &DomRootNode| !last.is_ancestor_of(&node))
3041 {
3042 node_list.push(node);
3043 }
3044 }
3045
3046 // Step 25. For each node in node list:
3047 for node in node_list.iter() {
3048 // Step 25.1. Let parent be the parent of node.
3049 let parent = node.GetParentNode().expect("Must always have a parent");
3050 // Step 25.2. Remove node from parent.
3051 assert!(node.has_parent());
3052 node.remove_self(cx);
3053 // Step 25.3. If the block node of parent has no visible children, and parent is editable or an editing host,
3054 // call createElement("br") on the context object and append the result as the last child of parent.
3055 if parent
3056 .block_node_of()
3057 .is_some_and(|block_node| block_node.children().all(|child| child.is_invisible())) &&
3058 parent.is_editable_or_editing_host()
3059 {
3060 let br = context_object.create_element(cx, "br");
3061 if parent.AppendChild(cx, br.upcast()).is_err() {
3062 unreachable!("Must always be able to append");
3063 }
3064 }
3065 // Step 25.4. If strip wrappers is true or parent is not an inclusive ancestor of start node,
3066 // while parent is an editable inline node with length 0, let grandparent be the parent of parent,
3067 // then remove parent from grandparent, then set parent to grandparent.
3068 if strip_wrappers == SelectionDeletionStripWrappers::Strip ||
3069 !parent.is_inclusive_ancestor_of(&start_node)
3070 {
3071 let mut parent = parent;
3072 loop {
3073 if parent.is_editable() && parent.is_inline_node() && parent.is_empty() {
3074 let grand_parent =
3075 parent.GetParentNode().expect("Must always have a parent");
3076 assert!(parent.has_parent());
3077 parent.remove_self(cx);
3078 parent = grand_parent;
3079 continue;
3080 }
3081 break;
3082 }
3083 }
3084 }
3085
3086 // Step 26. If end node is an editable Text node, call deleteData(0, end offset) on it.
3087 if end_node.is_editable() {
3088 if let Some(end_text) = end_node.downcast::<Text>() {
3089 if end_text
3090 .upcast::<CharacterData>()
3091 .DeleteData(0, end_offset)
3092 .is_err()
3093 {
3094 unreachable!("Must always be able to delete");
3095 }
3096 }
3097 }
3098
3099 // Step 27. Canonicalize whitespace at the active range's start, with fix collapsed space false.
3100 active_range
3101 .start_container()
3102 .canonicalize_whitespace(active_range.start_offset(), false);
3103
3104 // Step 28. Canonicalize whitespace at the active range's end, with fix collapsed space false.
3105 active_range
3106 .end_container()
3107 .canonicalize_whitespace(active_range.end_offset(), false);
3108
3109 // Step 29.
3110 //
3111 // This step does not exist in the spec
3112
3113 // Step 30. If block merging is false, or start block or end block is null, or start block is not
3114 // in the same editing host as end block, or start block and end block are the same:
3115 if block_merging == SelectionDeletionBlockMerging::Skip ||
3116 start_block.as_ref().zip(end_block.as_ref()).is_none_or(
3117 |(start_block, end_block)| {
3118 start_block == end_block || !start_block.same_editing_host(end_block)
3119 },
3120 )
3121 {
3122 // Step 30.1. If direction is "forward", call collapseToStart() on the context object's selection.
3123 if direction == SelectionDeleteDirection::Forward {
3124 if self.CollapseToStart(CanGc::from_cx(cx)).is_err() {
3125 unreachable!("Should be able to collapse to start");
3126 }
3127 } else {
3128 // Step 30.2. Otherwise, call collapseToEnd() on the context object's selection.
3129 if self.CollapseToEnd(CanGc::from_cx(cx)).is_err() {
3130 unreachable!("Should be able to collapse to end");
3131 }
3132 }
3133 // Step 30.3. Restore states and values from overrides.
3134 active_range.restore_states_and_values(cx, self, context_object, overrides);
3135
3136 // Step 30.4. Abort these steps.
3137 return;
3138 }
3139 let start_block = start_block.expect("Already checked for None in previous statement");
3140 let end_block = end_block.expect("Already checked for None in previous statement");
3141
3142 // Step 31. If start block has one child, which is a collapsed block prop, remove its child from it.
3143 if start_block.children_count() == 1 {
3144 let Some(child) = start_block.children().nth(0) else {
3145 unreachable!("Must always have a single child");
3146 };
3147 if child.is_collapsed_block_prop() {
3148 assert!(child.has_parent());
3149 child.remove_self(cx);
3150 }
3151 }
3152
3153 // Step 32. If start block is an ancestor of end block:
3154 if start_block.is_ancestor_of(&end_block) {
3155 // Step 32.1. Let reference node be end block.
3156 let mut reference_node = end_block.clone();
3157 // Step 32.2. While reference node is not a child of start block, set reference node to its parent.
3158 loop {
3159 if start_block.children().all(|child| child != reference_node) {
3160 reference_node = reference_node
3161 .GetParentNode()
3162 .expect("Must always have a parent, at least start_block");
3163 continue;
3164 }
3165 break;
3166 }
3167 // Step 32.3. Call collapse() on the context object's selection,
3168 // with first argument start block and second argument the index of reference node.
3169 if self
3170 .Collapse(
3171 Some(&start_block),
3172 reference_node.index(),
3173 CanGc::from_cx(cx),
3174 )
3175 .is_err()
3176 {
3177 unreachable!("Must always be able to collapse");
3178 }
3179 // Step 32.4. If end block has no children:
3180 if end_block.children_count() == 0 {
3181 let mut end_block = end_block;
3182 // Step 32.4.1. While end block is editable and is the only child of its parent and is not a child of start block,
3183 // let parent equal end block, then remove end block from parent, then set end block to parent.
3184 loop {
3185 if end_block.is_editable() &&
3186 start_block.children().all(|child| child != end_block)
3187 {
3188 if let Some(parent) = end_block.GetParentNode() {
3189 if parent.children_count() == 1 {
3190 assert!(end_block.has_parent());
3191 end_block.remove_self(cx);
3192 end_block = parent;
3193 continue;
3194 }
3195 }
3196 }
3197 break;
3198 }
3199 // Step 32.4.2. If end block is editable and is not an inline node,
3200 // and its previousSibling and nextSibling are both inline nodes,
3201 // call createElement("br") on the context object and insert it into end block's parent immediately after end block.
3202 if end_block.is_editable() &&
3203 !end_block.is_inline_node() &&
3204 end_block
3205 .GetPreviousSibling()
3206 .is_some_and(|previous| previous.is_inline_node())
3207 {
3208 if let Some(next_of_end_block) = end_block.GetNextSibling() {
3209 if next_of_end_block.is_inline_node() {
3210 let br = context_object.create_element(cx, "br");
3211 let parent = end_block
3212 .GetParentNode()
3213 .expect("Must always have a parent");
3214 if parent
3215 .InsertBefore(cx, br.upcast(), Some(&next_of_end_block))
3216 .is_err()
3217 {
3218 unreachable!("Must always be able to insert into parent");
3219 }
3220 }
3221 }
3222 }
3223 // Step 32.4.3. If end block is editable, remove it from its parent.
3224 if end_block.is_editable() {
3225 assert!(end_block.has_parent());
3226 end_block.remove_self(cx);
3227 }
3228 // Step 32.4.4. Restore states and values from overrides.
3229 active_range.restore_states_and_values(cx, self, context_object, overrides);
3230
3231 // Step 32.4.5. Abort these steps.
3232 return;
3233 }
3234 let first_child = end_block
3235 .children()
3236 .nth(0)
3237 .expect("Already checked at least 1 child in previous statement");
3238 // Step 32.5. If end block's firstChild is not an inline node,
3239 // restore states and values from record, then abort these steps.
3240 if !first_child.is_inline_node() {
3241 // TODO: Restore state
3242 return;
3243 }
3244 // Step 32.6. Let children be a list of nodes, initially empty.
3245 rooted_vec!(let mut children);
3246 // Step 32.7. Append the first child of end block to children.
3247 children.push(first_child.as_traced());
3248 // Step 32.8. While children's last member is not a br,
3249 // and children's last member's nextSibling is an inline node,
3250 // append children's last member's nextSibling to children.
3251 loop {
3252 let Some(last) = children.last() else {
3253 break;
3254 };
3255 if last.is::<HTMLBRElement>() {
3256 break;
3257 }
3258 let Some(next) = last.GetNextSibling() else {
3259 break;
3260 };
3261 if next.is_inline_node() {
3262 children.push(next.as_traced());
3263 continue;
3264 }
3265 break;
3266 }
3267 // Step 32.9. Record the values of children, and let values be the result.
3268 // TODO
3269
3270 // Step 32.10. While children's first member's parent is not start block,
3271 // split the parent of children.
3272 loop {
3273 if children
3274 .first()
3275 .and_then(|child| child.GetParentNode())
3276 .is_some_and(|parent_of_child| parent_of_child != start_block)
3277 {
3278 split_the_parent(cx, children.r());
3279 continue;
3280 }
3281 break;
3282 }
3283 // Step 32.11. If children's first member's previousSibling is an editable br,
3284 // remove that br from its parent.
3285 if let Some(first) = children.first() {
3286 if let Some(previous_of_first) = first.GetPreviousSibling() {
3287 if previous_of_first.is_editable() && previous_of_first.is::<HTMLBRElement>() {
3288 assert!(previous_of_first.has_parent());
3289 previous_of_first.remove_self(cx);
3290 }
3291 }
3292 }
3293 // Step 33. Otherwise, if start block is a descendant of end block:
3294 } else if end_block.is_ancestor_of(&start_block) {
3295 // Step 33.1. Call collapse() on the context object's selection,
3296 // with first argument start block and second argument start block's length.
3297 if self
3298 .Collapse(Some(&start_block), start_block.len(), CanGc::from_cx(cx))
3299 .is_err()
3300 {
3301 unreachable!("Must always be able to collapse");
3302 }
3303 // Step 33.2. Let reference node be start block.
3304 let mut reference_node = start_block.clone();
3305 // Step 33.3. While reference node is not a child of end block, set reference node to its parent.
3306 loop {
3307 if end_block.children().all(|child| child != reference_node) {
3308 if let Some(parent) = reference_node.GetParentNode() {
3309 reference_node = parent;
3310 continue;
3311 }
3312 }
3313 break;
3314 }
3315 // Step 33.4. If reference node's nextSibling is an inline node and start block's lastChild is a br,
3316 // remove start block's lastChild from it.
3317 if reference_node
3318 .GetNextSibling()
3319 .is_some_and(|next| next.is_inline_node())
3320 {
3321 if let Some(last) = start_block.children().last() {
3322 if last.is::<HTMLBRElement>() {
3323 assert!(last.has_parent());
3324 last.remove_self(cx);
3325 }
3326 }
3327 }
3328 // Step 33.5. Let nodes to move be a list of nodes, initially empty.
3329 rooted_vec!(let mut nodes_to_move);
3330 // Step 33.6. If reference node's nextSibling is neither null nor a block node,
3331 // append it to nodes to move.
3332 if let Some(next) = reference_node.GetNextSibling() {
3333 if !next.is_block_node() {
3334 nodes_to_move.push(next);
3335 }
3336 }
3337 // Step 33.7. While nodes to move is nonempty and its last member isn't a br
3338 // and its last member's nextSibling is neither null nor a block node,
3339 // append its last member's nextSibling to nodes to move.
3340 loop {
3341 if let Some(last) = nodes_to_move.last() {
3342 if !last.is::<HTMLBRElement>() {
3343 if let Some(next_of_last) = last.GetNextSibling() {
3344 if !next_of_last.is_block_node() {
3345 nodes_to_move.push(next_of_last);
3346 continue;
3347 }
3348 }
3349 }
3350 }
3351 break;
3352 }
3353 // Step 33.8. Record the values of nodes to move, and let values be the result.
3354 // TODO
3355
3356 // Step 33.9. For each node in nodes to move,
3357 // append node as the last child of start block, preserving ranges.
3358 for node in nodes_to_move.iter() {
3359 move_preserving_ranges(cx, node, |cx| start_block.AppendChild(cx, node));
3360 }
3361 // Step 34. Otherwise:
3362 } else {
3363 // Step 34.1. Call collapse() on the context object's selection,
3364 // with first argument start block and second argument start block's length.
3365 if self
3366 .Collapse(Some(&start_block), start_block.len(), CanGc::from_cx(cx))
3367 .is_err()
3368 {
3369 unreachable!("Must always be able to collapse");
3370 }
3371 // Step 34.2. If end block's firstChild is an inline node and start block's lastChild is a br,
3372 // remove start block's lastChild from it.
3373 if end_block
3374 .children()
3375 .nth(0)
3376 .is_some_and(|next| next.is_inline_node())
3377 {
3378 if let Some(last) = start_block.children().last() {
3379 if last.is::<HTMLBRElement>() {
3380 assert!(last.has_parent());
3381 last.remove_self(cx);
3382 }
3383 }
3384 }
3385 // Step 34.3. Record the values of end block's children, and let values be the result.
3386 // TODO
3387
3388 // Step 34.4. While end block has children,
3389 // append the first child of end block to start block, preserving ranges.
3390 loop {
3391 if let Some(first_child) = end_block.children().nth(0) {
3392 move_preserving_ranges(cx, &first_child, |cx| {
3393 start_block.AppendChild(cx, &first_child)
3394 });
3395 continue;
3396 }
3397 break;
3398 }
3399 // Step 34.5. While end block has no children,
3400 // let parent be the parent of end block, then remove end block from parent,
3401 // then set end block to parent.
3402 let mut end_block = end_block;
3403 loop {
3404 if end_block.children_count() == 0 {
3405 if let Some(parent) = end_block.GetParentNode() {
3406 assert!(end_block.has_parent());
3407 end_block.remove_self(cx);
3408 end_block = parent;
3409 continue;
3410 }
3411 }
3412 break;
3413 }
3414 }
3415
3416 // Step 35.
3417 //
3418 // This step does not exist in the spec
3419
3420 // Step 36. Let ancestor be start block.
3421 // TODO
3422
3423 // Step 37. While ancestor has an inclusive ancestor ol in the same editing host whose nextSibling is
3424 // also an ol in the same editing host, or an inclusive ancestor ul in the same editing host whose nextSibling
3425 // is also a ul in the same editing host:
3426 // TODO
3427
3428 // Step 38. Restore the values from values.
3429 // TODO
3430
3431 // Step 39. If start block has no children, call createElement("br") on the context object and
3432 // append the result as the last child of start block.
3433 if start_block.children_count() == 0 {
3434 let br = context_object.create_element(cx, "br");
3435 if start_block.AppendChild(cx, br.upcast()).is_err() {
3436 unreachable!("Must always be able to append");
3437 }
3438 }
3439
3440 // Step 40. Remove extraneous line breaks at the end of start block.
3441 start_block.remove_extraneous_line_breaks_at_the_end_of(cx);
3442
3443 // Step 41. Restore states and values from overrides.
3444 active_range.restore_states_and_values(cx, self, context_object, overrides);
3445 }
3446
3447 /// <https://w3c.github.io/editing/docs/execCommand/#set-the-selection%27s-value>
3448 fn set_the_selection_value(
3449 &self,
3450 cx: &mut js::context::JSContext,
3451 new_value: Option<DOMString>,
3452 command: CommandName,
3453 context_object: &Document,
3454 ) {
3455 let active_range = self
3456 .active_range()
3457 .expect("Must always have an active range");
3458
3459 // Step 1. Let command be the current command.
3460 //
3461 // Passed as argument
3462
3463 // Step 2. If there is no formattable node effectively contained in the active range:
3464 if active_range.first_formattable_contained_node().is_none() {
3465 // Step 2.1. If command has inline command activated values, set the state override to true if new value is among them and false if it's not.
3466 // TODO
3467
3468 // Step 2.2. If command is "subscript", unset the state override for "superscript".
3469 if command == CommandName::Subscript {
3470 context_object.set_state_override(CommandName::Superscript, None);
3471 }
3472 // Step 2.3. If command is "superscript", unset the state override for "subscript".
3473 if command == CommandName::Superscript {
3474 context_object.set_state_override(CommandName::Subscript, None);
3475 }
3476 // Step 2.4. If new value is null, unset the value override (if any).
3477 // Step 2.5. Otherwise, if command is "createLink" or it has a value specified, set the value override to new value.
3478 context_object.set_value_override(command, new_value);
3479 // Step 2.6. Abort these steps.
3480 return;
3481 }
3482 // Step 3. If the active range's start node is an editable Text node,
3483 // and its start offset is neither zero nor its start node's length,
3484 // call splitText() on the active range's start node,
3485 // with argument equal to the active range's start offset.
3486 // Then set the active range's start node to the result, and its start offset to zero.
3487 let start_node = active_range.start_container();
3488 let start_offset = active_range.start_offset();
3489 if start_node.is_editable() && start_offset != 0 && start_offset != start_node.len() {
3490 if let Some(start_text) = start_node.downcast::<Text>() {
3491 let Ok(start_text) = start_text.SplitText(cx, start_offset) else {
3492 unreachable!("Must always be able to split");
3493 };
3494 active_range.set_start(start_text.upcast(), 0);
3495 }
3496 }
3497 // Step 4. If the active range's end node is an editable Text node,
3498 // and its end offset is neither zero nor its end node's length,
3499 // call splitText() on the active range's end node,
3500 // with argument equal to the active range's end offset.
3501 let end_node = active_range.end_container();
3502 let end_offset = active_range.end_offset();
3503 if end_node.is_editable() && end_offset != 0 && end_offset != end_node.len() {
3504 if let Some(end_text) = end_node.downcast::<Text>() {
3505 if end_text.SplitText(cx, end_offset).is_err() {
3506 unreachable!("Must always be able to split");
3507 };
3508 }
3509 }
3510 // Step 5. Let element list be all editable Elements effectively contained in the active range.
3511 // Step 6. For each element in element list, clear the value of element.
3512 active_range.for_each_effectively_contained_child(|child| {
3513 if child.is_editable() {
3514 if let Some(element_child) = child.downcast::<HTMLElement>() {
3515 element_child.clear_the_value(cx, &command);
3516 }
3517 }
3518 });
3519 // Step 7. Let node list be all editable nodes effectively contained in the active range.
3520 // Step 8. For each node in node list:
3521 active_range.for_each_effectively_contained_child(|child| {
3522 if child.is_editable() {
3523 // Step 8.1. Push down values on node.
3524 child.push_down_values(cx, &command, new_value.clone());
3525 // Step 8.2. If node is an allowed child of "span", force the value of node.
3526 if is_allowed_child(
3527 NodeOrString::Node(DomRoot::from_ref(child)),
3528 NodeOrString::String("span".to_owned()),
3529 ) {
3530 child.force_the_value(cx, &command, new_value.as_ref());
3531 }
3532 }
3533 });
3534 }
3535}