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