Files
servo/components/script/dom/execcommand/contenteditable.rs
Gae24 4ef22ed227 script: Pass &mut JSContext to VirtualMethods::cloning_steps and Node::clone (#43130)
Continuation of #43108, two new `temp_cx()` calls were required:
- inside `maybe_clone_an_option_into_selectedcontent` since it's part of
a markup5ever trait
- inside `serialize_and_cache_subtree` replacing a `CanGc::note()` call,
propagating it inside reflow code will require even more effort.

Testing: No behaviour change, a successful build is enough.
Part of #40600

---------

Signed-off-by: Gae24 <96017547+Gae24@users.noreply.github.com>
2026-03-10 17:05:34 +00:00

2246 lines
96 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::cmp::Ordering;
use std::ops::Deref;
use html5ever::local_name;
use script_bindings::inheritance::Castable;
use style::computed_values::white_space_collapse::T as WhiteSpaceCollapse;
use style::values::specified::box_::DisplayOutside;
use crate::dom::abstractrange::bp_position;
use crate::dom::bindings::cell::Ref;
use crate::dom::bindings::codegen::Bindings::CharacterDataBinding::CharacterDataMethods;
use crate::dom::bindings::codegen::Bindings::DocumentBinding::{
DocumentMethods, ElementCreationOptions,
};
use crate::dom::bindings::codegen::Bindings::HTMLElementBinding::HTMLElementMethods;
use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods;
use crate::dom::bindings::codegen::Bindings::SelectionBinding::SelectionMethods;
use crate::dom::bindings::codegen::UnionTypes::StringOrElementCreationOptions;
use crate::dom::bindings::inheritance::{ElementTypeId, HTMLElementTypeId, NodeTypeId};
use crate::dom::bindings::root::{DomRoot, DomSlice};
use crate::dom::characterdata::CharacterData;
use crate::dom::document::Document;
use crate::dom::element::Element;
use crate::dom::html::htmlanchorelement::HTMLAnchorElement;
use crate::dom::html::htmlbrelement::HTMLBRElement;
use crate::dom::html::htmlelement::HTMLElement;
use crate::dom::html::htmlimageelement::HTMLImageElement;
use crate::dom::html::htmllielement::HTMLLIElement;
use crate::dom::html::htmltablecellelement::HTMLTableCellElement;
use crate::dom::html::htmltablerowelement::HTMLTableRowElement;
use crate::dom::html::htmltablesectionelement::HTMLTableSectionElement;
use crate::dom::node::{Node, NodeTraits, ShadowIncluding};
use crate::dom::selection::Selection;
use crate::dom::text::Text;
use crate::script_runtime::CanGc;
impl Text {
/// <https://dom.spec.whatwg.org/#concept-cd-data>
fn data(&self) -> Ref<'_, String> {
self.upcast::<CharacterData>().data()
}
/// <https://w3c.github.io/editing/docs/execCommand/#whitespace-node>
fn is_whitespace_node(&self) -> bool {
// > A whitespace node is either a Text node whose data is the empty string;
let data = self.data();
if data.is_empty() {
return true;
}
// > or a Text node whose data consists only of one or more tabs (0x0009), line feeds (0x000A),
// > carriage returns (0x000D), and/or spaces (0x0020),
// > and whose parent is an Element whose resolved value for "white-space" is "normal" or "nowrap";
let Some(parent) = self.upcast::<Node>().GetParentElement() else {
return false;
};
// TODO: Optimize the below to only do a traversal once and in the match handle the expected collapse value
let Some(style) = parent.style() else {
return false;
};
let white_space_collapse = style.get_inherited_text().white_space_collapse;
if data
.bytes()
.all(|byte| matches!(byte, b'\t' | b'\n' | b'\r' | b' ')) &&
// Note that for "normal" and "nowrap", the longhand "white-space-collapse: collapse" applies
// https://www.w3.org/TR/css-text-4/#white-space-property
white_space_collapse == WhiteSpaceCollapse::Collapse
{
return true;
}
// > or a Text node whose data consists only of one or more tabs (0x0009), carriage returns (0x000D),
// > and/or spaces (0x0020), and whose parent is an Element whose resolved value for "white-space" is "pre-line".
data.bytes()
.all(|byte| matches!(byte, b'\t' | b'\r' | b' ')) &&
// Note that for "pre-line", the longhand "white-space-collapse: preserve-breaks" applies
// https://www.w3.org/TR/css-text-4/#white-space-property
white_space_collapse == WhiteSpaceCollapse::PreserveBreaks
}
/// <https://w3c.github.io/editing/docs/execCommand/#collapsed-whitespace-node>
fn is_collapsed_whitespace_node(&self) -> bool {
// Step 1. If node is not a whitespace node, return false.
if !self.is_whitespace_node() {
return false;
}
// Step 2. If node's data is the empty string, return true.
if self.data().is_empty() {
return true;
}
// Step 3. Let ancestor be node's parent.
let node = self.upcast::<Node>();
let Some(ancestor) = node.GetParentNode() else {
// Step 4. If ancestor is null, return true.
return true;
};
let mut resolved_ancestor = ancestor.clone();
for parent in ancestor.ancestors() {
// Step 5. If the "display" property of some ancestor of node has resolved value "none", return true.
if parent.is_display_none() {
return true;
}
// Step 6. While ancestor is not a block node and its parent is not null, set ancestor to its parent.
//
// Note that the spec is written as "while not". Since this is the end-condition, we need to invert
// the condition to decide when to stop.
if parent.is_block_node() {
break;
}
resolved_ancestor = parent;
}
// Step 7. Let reference be node.
// Step 8. While reference is a descendant of ancestor:
// Step 8.1. Let reference be the node before it in tree order.
for reference in node.preceding_nodes(&resolved_ancestor) {
// Step 8.2. If reference is a block node or a br, return true.
if reference.is_block_node() || reference.is::<HTMLBRElement>() {
return true;
}
// Step 8.3. If reference is a Text node that is not a whitespace node, or is an img, break from this loop.
if reference
.downcast::<Text>()
.is_some_and(|text| !text.is_whitespace_node()) ||
reference.is::<HTMLImageElement>()
{
break;
}
}
// Step 9. Let reference be node.
// Step 10. While reference is a descendant of ancestor:
// Step 10.1. Let reference be the node after it in tree order, or null if there is no such node.
for reference in node.following_nodes(&resolved_ancestor) {
// Step 10.2. If reference is a block node or a br, return true.
if reference.is_block_node() || reference.is::<HTMLBRElement>() {
return true;
}
// Step 10.3. If reference is a Text node that is not a whitespace node, or is an img, break from this loop.
if reference
.downcast::<Text>()
.is_some_and(|text| !text.is_whitespace_node()) ||
reference.is::<HTMLImageElement>()
{
break;
}
}
// Step 11. Return false.
false
}
/// Part of <https://w3c.github.io/editing/docs/execCommand/#canonicalize-whitespace>
/// and deduplicated here, since we need to do this for both start and end nodes
fn has_whitespace_and_has_parent_with_whitespace_preserve(
&self,
offset: u32,
space_characters: &'static [&'static char],
) -> bool {
// if node is a Text node and its parent's resolved value for "white-space" is neither "pre" nor "pre-wrap"
// and start offset is not zero and the (start offset 1)st code unit of start node's data is a space (0x0020) or
// non-breaking space (0x00A0)
let has_preserve_space = self
.upcast::<Node>()
.GetParentNode()
.and_then(|parent| parent.style())
.is_some_and(|style| {
// Note that for "pre" and "pre-wrap", the longhand "white-space-collapse: preserve" applies
// https://www.w3.org/TR/css-text-4/#white-space-property
style.get_inherited_text().white_space_collapse != WhiteSpaceCollapse::Preserve
});
let has_space_character = self
.data()
.chars()
.nth(offset as usize)
.is_some_and(|c| space_characters.contains(&&c));
has_preserve_space && has_space_character
}
}
impl HTMLBRElement {
/// <https://w3c.github.io/editing/docs/execCommand/#extraneous-line-break>
fn is_extraneous_line_break(&self) -> bool {
let node = self.upcast::<Node>();
// > An extraneous line break is a br that has no visual effect, in that removing it from the DOM would not change layout,
// except that a br that is the sole child of an li is not extraneous.
if node
.GetParentNode()
.filter(|parent| parent.is::<HTMLLIElement>())
.is_some_and(|li| li.children_count() == 1)
{
return false;
}
// TODO: Figure out what this actually makes it have no visual effect
!node.is_block_node()
}
}
impl Document {
fn create_br_element(&self, cx: &mut js::context::JSContext) -> DomRoot<Element> {
let element_options =
StringOrElementCreationOptions::ElementCreationOptions(ElementCreationOptions {
is: None,
});
match self.CreateElement(cx, "br".into(), element_options) {
Err(_) => unreachable!("Must always be able to create br"),
Ok(br) => br,
}
}
}
impl HTMLElement {
fn local_name(&self) -> &str {
self.upcast::<Element>().local_name()
}
}
pub(crate) enum NodeOrString {
String(String),
Node(DomRoot<Node>),
}
impl NodeOrString {
fn name(&self) -> &str {
match self {
NodeOrString::String(str_) => str_,
NodeOrString::Node(node) => node
.downcast::<Element>()
.map(|element| element.local_name().as_ref())
.unwrap_or_default(),
}
}
fn as_node(&self) -> Option<DomRoot<Node>> {
match self {
NodeOrString::String(_) => None,
NodeOrString::Node(node) => Some(node.clone()),
}
}
}
/// <https://w3c.github.io/editing/docs/execCommand/#prohibited-paragraph-child-name>
const PROHIBITED_PARAGRAPH_CHILD_NAMES: [&str; 47] = [
"address",
"article",
"aside",
"blockquote",
"caption",
"center",
"col",
"colgroup",
"dd",
"details",
"dir",
"div",
"dl",
"dt",
"fieldset",
"figcaption",
"figure",
"footer",
"form",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"header",
"hgroup",
"hr",
"li",
"listing",
"menu",
"nav",
"ol",
"p",
"plaintext",
"pre",
"section",
"summary",
"table",
"tbody",
"td",
"tfoot",
"th",
"thead",
"tr",
"ul",
"xmp",
];
/// <https://w3c.github.io/editing/docs/execCommand/#name-of-an-element-with-inline-contents>
const NAME_OF_AN_ELEMENT_WITH_INLINE_CONTENTS: [&str; 43] = [
"a", "abbr", "b", "bdi", "bdo", "cite", "code", "dfn", "em", "h1", "h2", "h3", "h4", "h5",
"h6", "i", "kbd", "mark", "p", "pre", "q", "rp", "rt", "ruby", "s", "samp", "small", "span",
"strong", "sub", "sup", "u", "var", "acronym", "listing", "strike", "xmp", "big", "blink",
"font", "marquee", "nobr", "tt",
];
/// <https://w3c.github.io/editing/docs/execCommand/#element-with-inline-contents>
fn is_element_with_inline_contents(element: &Node) -> bool {
// > An element with inline contents is an HTML element whose local name is a name of an element with inline contents.
let Some(html_element) = element.downcast::<HTMLElement>() else {
return false;
};
NAME_OF_AN_ELEMENT_WITH_INLINE_CONTENTS.contains(&html_element.local_name())
}
/// <https://w3c.github.io/editing/docs/execCommand/#allowed-child>
fn is_allowed_child(child: NodeOrString, parent: NodeOrString) -> bool {
// Step 1. If parent is "colgroup", "table", "tbody", "tfoot", "thead", "tr",
// or an HTML element with local name equal to one of those,
// and child is a Text node whose data does not consist solely of space characters, return false.
if matches!(
parent.name(),
"colgroup" | "table" | "tbody" | "tfoot" | "thead" | "tr"
) && child.as_node().is_some_and(|node| {
// Note: cannot use `.and_then` here, since `downcast` would outlive its reference
node.downcast::<Text>()
.is_some_and(|text| !text.data().bytes().all(|byte| byte == b' '))
}) {
return false;
}
// Step 2. If parent is "script", "style", "plaintext", or "xmp",
// or an HTML element with local name equal to one of those, and child is not a Text node, return false.
if matches!(parent.name(), "script" | "style" | "plaintext" | "xmp") &&
child.as_node().is_none_or(|node| !node.is::<Text>())
{
return false;
}
// Step 3. If child is a document, DocumentFragment, or DocumentType, return false.
if let NodeOrString::Node(ref node) = child {
if matches!(
node.type_id(),
NodeTypeId::Document(_) | NodeTypeId::DocumentFragment(_) | NodeTypeId::DocumentType
) {
return false;
}
}
// Step 4. If child is an HTML element, set child to the local name of child.
let child_name = match child {
NodeOrString::String(str_) => str_,
NodeOrString::Node(node) => match node.downcast::<HTMLElement>() {
// Step 5. If child is not a string, return true.
None => return true,
Some(html_element) => html_element.local_name().to_owned(),
},
};
let child = child_name.as_str();
let parent_name = match parent {
NodeOrString::String(str_) => str_,
NodeOrString::Node(parent) => {
// Step 6. If parent is an HTML element:
if let Some(parent_element) = parent.downcast::<HTMLElement>() {
// Step 6.1. If child is "a", and parent or some ancestor of parent is an a, return false.
if child == "a" &&
parent
.inclusive_ancestors(ShadowIncluding::No)
.any(|node| node.is::<HTMLAnchorElement>())
{
return false;
}
// Step 6.2. If child is a prohibited paragraph child name and parent or some ancestor of parent
// is an element with inline contents, return false.
if PROHIBITED_PARAGRAPH_CHILD_NAMES.contains(&child) &&
parent
.inclusive_ancestors(ShadowIncluding::No)
.any(|node| is_element_with_inline_contents(&node))
{
return false;
}
// Step 6.3. If child is "h1", "h2", "h3", "h4", "h5", or "h6",
// and parent or some ancestor of parent is an HTML element with local name
// "h1", "h2", "h3", "h4", "h5", or "h6", return false.
if matches!(child, "h1" | "h2" | "h3" | "h4" | "h5" | "h6") &&
parent.inclusive_ancestors(ShadowIncluding::No).any(|node| {
node.downcast::<HTMLElement>().is_some_and(|html_element| {
matches!(
html_element.local_name(),
"h1" | "h2" | "h3" | "h4" | "h5" | "h6"
)
})
})
{
return false;
}
// Step 6.4. Let parent be the local name of parent.
parent_element.local_name().to_owned()
} else {
// Step 7. If parent is an Element or DocumentFragment, return true.
// Step 8. If parent is not a string, return false.
return matches!(
parent.type_id(),
NodeTypeId::DocumentFragment(_) | NodeTypeId::Element(_)
);
}
},
};
let parent = parent_name.as_str();
// Step 9. If parent is on the left-hand side of an entry on the following list,
// then return true if child is listed on the right-hand side of that entry, and false otherwise.
match parent {
"colgroup" => return child == "col",
"table" => {
return matches!(
child,
"caption" | "col" | "colgroup" | "tbody" | "td" | "tfoot" | "th" | "thead" | "tr"
);
},
"tbody" | "tfoot" | "thead" => return matches!(child, "td" | "th" | "tr"),
"tr" => return matches!(child, "td" | "th"),
"dl" => return matches!(child, "dt" | "dd"),
"dir" | "ol" | "ul" => return matches!(child, "dir" | "li" | "ol" | "ul"),
"hgroup" => return matches!(child, "h1" | "h2" | "h3" | "h4" | "h5" | "h6"),
_ => {},
};
// Step 10. If child is "body", "caption", "col", "colgroup", "frame", "frameset", "head",
// "html", "tbody", "td", "tfoot", "th", "thead", or "tr", return false.
if matches!(
child,
"body" |
"caption" |
"col" |
"colgroup" |
"frame" |
"frameset" |
"head" |
"html" |
"tbody" |
"td" |
"tfoot" |
"th" |
"thead" |
"tr"
) {
return false;
}
// Step 11. If child is "dd" or "dt" and parent is not "dl", return false.
if matches!(child, "dd" | "dt") && parent != "dl" {
return false;
}
// Step 12. If child is "li" and parent is not "ol" or "ul", return false.
if child == "li" && !matches!(parent, "ol" | "ul") {
return false;
}
// Step 13. If parent is on the left-hand side of an entry on the following list
// and child is listed on the right-hand side of that entry, return false.
if match parent {
"a" => child == "a",
"dd" | "dt" => matches!(child, "dd" | "dt"),
"h1" | "h2" | "h3" | "h4" | "h5" | "h6" => {
matches!(child, "h1" | "h2" | "h3" | "h4" | "h5" | "h6")
},
"li" => child == "li",
"nobr" => child == "nobr",
"td" | "th" => {
matches!(
child,
"caption" | "col" | "colgroup" | "tbody" | "td" | "tfoot" | "th" | "thead" | "tr"
)
},
_ if NAME_OF_AN_ELEMENT_WITH_INLINE_CONTENTS.contains(&parent) => {
PROHIBITED_PARAGRAPH_CHILD_NAMES.contains(&child)
},
_ => false,
} {
return false;
}
// Step 14. Return true.
true
}
/// <https://w3c.github.io/editing/docs/execCommand/#split-the-parent>
fn split_the_parent<'a>(cx: &mut js::context::JSContext, node_list: &'a [&'a Node]) {
assert!(!node_list.is_empty());
// Step 1. Let original parent be the parent of the first member of node list.
let Some(original_parent) = node_list.first().and_then(|first| first.GetParentNode()) else {
return;
};
let context_object = original_parent.owner_document();
// Step 2. If original parent is not editable or its parent is null, do nothing and abort these steps.
if !original_parent.is_editable() {
return;
}
let Some(parent_of_original_parent) = original_parent.GetParentNode() else {
return;
};
// Step 3. If the first child of original parent is in node list, remove extraneous line breaks before original parent.
if original_parent
.children()
.next()
.is_some_and(|first_child| node_list.contains(&first_child.deref()))
{
original_parent.remove_extraneous_line_breaks_before(cx);
}
// Step 4. If the first child of original parent is in node list, and original parent follows a line break,
// set follows line break to true. Otherwise, set follows line break to false.
let first_child_is_in_node_list = original_parent
.children()
.next()
.is_some_and(|first_child| node_list.contains(&first_child.deref()));
let follows_line_break = first_child_is_in_node_list && original_parent.follows_a_line_break();
// Step 5. If the last child of original parent is in node list, and original parent precedes a line break,
// set precedes line break to true. Otherwise, set precedes line break to false.
let last_child_is_in_node_list = original_parent
.children()
.last()
.is_some_and(|last_child| node_list.contains(&last_child.deref()));
let precedes_line_break = last_child_is_in_node_list && original_parent.precedes_a_line_break();
// Step 6. If the first child of original parent is not in node list, but its last child is:
if !first_child_is_in_node_list && last_child_is_in_node_list {
// Step 6.1. For each node in node list, in reverse order,
// insert node into the parent of original parent immediately after original parent, preserving ranges.
for node in node_list.iter().rev() {
// TODO: Preserving ranges
if parent_of_original_parent
.InsertBefore(
node,
original_parent.GetNextSibling().as_deref(),
CanGc::from_cx(cx),
)
.is_err()
{
unreachable!("Must always have a parent");
}
}
// Step 6.2. If precedes line break is true, and the last member of node list does not precede a line break,
// call createElement("br") on the context object and insert the result immediately after the last member of node list.
if precedes_line_break {
if let Some(last) = node_list.last() {
if !last.precedes_a_line_break() {
let br = context_object.create_br_element(cx);
if last
.GetParentNode()
.expect("Must always have a parent")
.InsertBefore(
br.upcast(),
last.GetNextSibling().as_deref(),
CanGc::from_cx(cx),
)
.is_err()
{
unreachable!("Must always be able to append");
}
}
}
}
// Step 6.3. Remove extraneous line breaks at the end of original parent.
original_parent.remove_extraneous_line_breaks_at_the_end_of(cx);
// Step 6.4. Abort these steps.
return;
}
// Step 7. If the first child of original parent is not in node list:
if first_child_is_in_node_list {
// Step 7.1. Let cloned parent be the result of calling cloneNode(false) on original parent.
let Ok(cloned_parent) = original_parent.CloneNode(cx, false) else {
unreachable!("Must always be able to clone node");
};
// Step 7.2. If original parent has an id attribute, unset it.
if let Some(element) = original_parent.downcast::<Element>() {
element.remove_attribute_by_name(&local_name!("id"), CanGc::from_cx(cx));
}
// Step 7.3. Insert cloned parent into the parent of original parent immediately before original parent.
if parent_of_original_parent
.InsertBefore(&cloned_parent, Some(&original_parent), CanGc::from_cx(cx))
.is_err()
{
unreachable!("Must always have a parent");
}
// Step 7.4. While the previousSibling of the first member of node list is not null,
// append the first child of original parent as the last child of cloned parent, preserving ranges.
loop {
if node_list
.first()
.and_then(|first| first.GetPreviousSibling())
.is_some()
{
if let Some(first_of_original) = original_parent.children().next() {
// TODO: Preserving ranges
if cloned_parent
.AppendChild(&first_of_original, CanGc::from_cx(cx))
.is_err()
{
unreachable!("Must always have a parent");
}
continue;
}
}
break;
}
}
// Step 8. For each node in node list, insert node into the parent of original parent immediately before original parent, preserving ranges.
for node in node_list.iter() {
// TODO: Preserving ranges
if parent_of_original_parent
.InsertBefore(node, Some(&original_parent), CanGc::from_cx(cx))
.is_err()
{
unreachable!("Must always have a parent");
}
}
// Step 9. If follows line break is true, and the first member of node list does not follow a line break,
// call createElement("br") on the context object and insert the result immediately before the first member of node list.
if follows_line_break {
if let Some(first) = node_list.first() {
if !first.follows_a_line_break() {
let br = context_object.create_br_element(cx);
if first
.GetParentNode()
.expect("Must always have a parent")
.InsertBefore(br.upcast(), Some(first), CanGc::from_cx(cx))
.is_err()
{
unreachable!("Must always be able to insert");
}
}
}
}
// Step 10. If the last member of node list is an inline node other than a br,
// and the first child of original parent is a br, and original parent is not an inline node,
// remove the first child of original parent from original parent.
if node_list
.last()
.is_some_and(|last| last.is_inline_node() && !last.is::<HTMLBRElement>()) &&
!original_parent.is_inline_node()
{
if let Some(first_of_original) = original_parent.children().next() {
if first_of_original.is::<HTMLBRElement>() &&
original_parent
.RemoveChild(&first_of_original, CanGc::from_cx(cx))
.is_err()
{
unreachable!("Must always have a parent");
}
}
}
// Step 11. If original parent has no children:
if original_parent.children_count() == 0 {
// Step 11.1. Remove original parent from its parent.
if parent_of_original_parent
.RemoveChild(&original_parent, CanGc::from_cx(cx))
.is_err()
{
unreachable!("Must always have a parent");
}
// Step 11.2. If precedes line break is true, and the last member of node list does not precede a line break,
// call createElement("br") on the context object and insert the result immediately after the last member of node list.
if precedes_line_break {
if let Some(last) = node_list.last() {
if !last.precedes_a_line_break() {
let br = context_object.create_br_element(cx);
if last
.GetParentNode()
.expect("Must always have a parent")
.InsertBefore(
br.upcast(),
last.GetNextSibling().as_deref(),
CanGc::from_cx(cx),
)
.is_err()
{
unreachable!("Must always be able to insert");
}
}
}
}
} else {
// Step 12. Otherwise, remove extraneous line breaks before original parent.
original_parent.remove_extraneous_line_breaks_before(cx);
}
// Step 13. If node list's last member's nextSibling is null, but its parent is not null,
// remove extraneous line breaks at the end of node list's last member's parent.
if let Some(last) = node_list.last() {
if last.GetNextSibling().is_none() {
if let Some(parent_of_last) = last.GetParentNode() {
parent_of_last.remove_extraneous_line_breaks_at_the_end_of(cx);
}
}
}
}
pub(crate) trait NodeExecCommandSupport {
fn same_editing_host(&self, other: &Node) -> bool;
fn is_block_node(&self) -> bool;
fn is_inline_node(&self) -> bool;
fn block_node_of(&self) -> Option<DomRoot<Node>>;
fn is_visible(&self) -> bool;
fn is_invisible(&self) -> bool;
fn is_block_start_point(&self, offset: usize) -> bool;
fn is_block_end_point(&self, offset: u32) -> bool;
fn is_block_boundary_point(&self, offset: u32) -> bool;
fn is_collapsed_block_prop(&self) -> bool;
fn follows_a_line_break(&self) -> bool;
fn precedes_a_line_break(&self) -> bool;
fn canonical_space_sequence(
n: usize,
non_breaking_start: bool,
non_breaking_end: bool,
) -> String;
fn canonicalize_whitespace(&self, offset: u32, fix_collapsed_space: bool);
fn remove_extraneous_line_breaks_before(&self, cx: &mut js::context::JSContext);
fn remove_extraneous_line_breaks_at_the_end_of(&self, cx: &mut js::context::JSContext);
fn remove_preserving_its_descendants(&self, cx: &mut js::context::JSContext, parent: &Node);
}
impl NodeExecCommandSupport for Node {
/// <https://w3c.github.io/editing/docs/execCommand/#in-the-same-editing-host>
fn same_editing_host(&self, other: &Node) -> bool {
// > 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.
self.editing_host_of()
.is_some_and(|editing_host| other.editing_host_of() == Some(editing_host))
}
/// <https://w3c.github.io/editing/docs/execCommand/#block-node>
fn is_block_node(&self) -> bool {
// > A block node is either an Element whose "display" property does not have resolved value "inline" or "inline-block" or "inline-table" or "none",
if self.downcast::<Element>().is_some_and(|el| {
!el.style()
.is_none_or(|style| style.get_box().display.outside() == DisplayOutside::Inline)
}) {
return true;
}
// > or a document, or a DocumentFragment.
matches!(
self.type_id(),
NodeTypeId::Document(_) | NodeTypeId::DocumentFragment(_)
)
}
/// <https://w3c.github.io/editing/docs/execCommand/#inline-node>
fn is_inline_node(&self) -> bool {
// > An inline node is a node that is not a block node.
!self.is_block_node()
}
/// <https://w3c.github.io/editing/docs/execCommand/#block-node-of>
fn block_node_of(&self) -> Option<DomRoot<Node>> {
let mut node = DomRoot::from_ref(self);
loop {
// Step 1. While node is an inline node, set node to its parent.
if node.is_inline_node() {
node = node.GetParentNode()?;
continue;
}
// Step 2. Return node.
return Some(node);
}
}
/// <https://w3c.github.io/editing/docs/execCommand/#visible>
fn is_visible(&self) -> bool {
for parent in self.inclusive_ancestors(ShadowIncluding::Yes) {
// > excluding any node with an inclusive ancestor Element whose "display" property has resolved value "none".
if parent.is_display_none() {
return false;
}
}
// > Something is visible if it is a node that either is a block node,
if self.is_block_node() {
return true;
}
// > or a Text node that is not a collapsed whitespace node,
if self
.downcast::<Text>()
.is_some_and(|text| !text.is_collapsed_whitespace_node())
{
return true;
}
// > or an img, or a br that is not an extraneous line break, or any node with a visible descendant;
if self.is::<HTMLImageElement>() {
return true;
}
if self
.downcast::<HTMLBRElement>()
.is_some_and(|br| !br.is_extraneous_line_break())
{
return true;
}
for child in self.children() {
if child.is_visible() {
return true;
}
}
false
}
/// <https://w3c.github.io/editing/docs/execCommand/#invisible>
fn is_invisible(&self) -> bool {
// > Something is invisible if it is a node that is not visible.
!self.is_visible()
}
/// <https://w3c.github.io/editing/docs/execCommand/#block-start-point>
fn is_block_start_point(&self, offset: usize) -> bool {
// > A boundary point (node, offset) is a block start point if either node's parent is null and offset is zero;
if offset == 0 {
return self.GetParentNode().is_none();
}
// > or node has a child with index offset 1, and that child is either a visible block node or a visible br.
self.children().nth(offset - 1).is_some_and(|child| {
child.is_visible() && (child.is_block_node() || child.is::<HTMLBRElement>())
})
}
/// <https://w3c.github.io/editing/docs/execCommand/#block-end-point>
fn is_block_end_point(&self, offset: u32) -> bool {
// > A boundary point (node, offset) is a block end point if either node's parent is null and offset is node's length;
if self.GetParentNode().is_none() && offset == self.len() {
return true;
}
// > or node has a child with index offset, and that child is a visible block node.
self.children()
.nth(offset as usize)
.is_some_and(|child| child.is_visible() && child.is_block_node())
}
/// <https://w3c.github.io/editing/docs/execCommand/#block-boundary-point>
fn is_block_boundary_point(&self, offset: u32) -> bool {
// > A boundary point is a block boundary point if it is either a block start point or a block end point.
self.is_block_start_point(offset as usize) || self.is_block_end_point(offset)
}
/// <https://w3c.github.io/editing/docs/execCommand/#collapsed-block-prop>
fn is_collapsed_block_prop(&self) -> bool {
// > A collapsed block prop is either a collapsed line break that is not an extraneous line break,
// TODO: Check for collapsed line break
if self
.downcast::<HTMLBRElement>()
.is_some_and(|br| !br.is_extraneous_line_break())
{
return true;
}
// > or an Element that is an inline node and whose children are all either invisible or collapsed block props
if !self.is::<Element>() {
return false;
};
if !self.is_inline_node() {
return false;
}
let mut at_least_one_collapsed_block_prop = false;
for child in self.children() {
if child.is_collapsed_block_prop() {
at_least_one_collapsed_block_prop = true;
continue;
}
if child.is_invisible() {
continue;
}
return false;
}
// > and that has at least one child that is a collapsed block prop.
at_least_one_collapsed_block_prop
}
/// <https://w3c.github.io/editing/docs/execCommand/#follows-a-line-break>
fn follows_a_line_break(&self) -> bool {
// Step 1. Let offset be zero.
let mut offset = 0;
// Step 2. While (node, offset) is not a block boundary point:
let mut node = DomRoot::from_ref(self);
while !node.is_block_boundary_point(offset) {
// Step 2.2. If offset is zero or node has no children, set offset to node's index, then set node to its parent.
if offset == 0 || node.children_count() == 0 {
offset = node.index();
node = match node.GetParentNode() {
None => return false,
Some(node) => node,
};
continue;
}
// Step 2.1. If node has a visible child with index offset minus one, return false.
let child = node.children().nth(offset as usize - 1);
let Some(child) = child else {
return false;
};
if child.is_visible() {
return false;
}
// Step 2.3. Otherwise, set node to its child with index offset minus one, then set offset to node's length.
node = child;
offset = node.len();
}
// Step 3. Return true.
true
}
/// <https://w3c.github.io/editing/docs/execCommand/#precedes-a-line-break>
fn precedes_a_line_break(&self) -> bool {
let mut node = DomRoot::from_ref(self);
// Step 1. Let offset be node's length.
let mut offset = node.len();
// Step 2. While (node, offset) is not a block boundary point:
while !node.is_block_boundary_point(offset) {
// Step 2.1. If node has a visible child with index offset, return false.
if node
.children()
.nth(offset as usize)
.is_some_and(|child| child.is_visible())
{
return false;
}
// 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.
if offset == node.len() || node.children_count() == 0 {
offset = 1 + node.index();
node = match node.GetParentNode() {
None => return false,
Some(node) => node,
};
continue;
}
// Step 2.3. Otherwise, set node to its child with index offset and set offset to zero.
let child = node.children().nth(offset as usize);
node = match child {
None => return false,
Some(child) => child,
};
offset = 0;
}
// Step 3. Return true.
true
}
/// <https://w3c.github.io/editing/docs/execCommand/#canonical-space-sequence>
fn canonical_space_sequence(
n: usize,
non_breaking_start: bool,
non_breaking_end: bool,
) -> String {
let mut n = n;
// Step 1. If n is zero, return the empty string.
if n == 0 {
return String::new();
}
// Step 2. If n is one and both non-breaking start and non-breaking end are false, return a single space (U+0020).
if n == 1 {
if !non_breaking_start && !non_breaking_end {
return "\u{0020}".to_owned();
}
// Step 3. If n is one, return a single non-breaking space (U+00A0).
return "\u{00A0}".to_owned();
}
// Step 4. Let buffer be the empty string.
let mut buffer = String::new();
// Step 5. If non-breaking start is true, let repeated pair be U+00A0 U+0020. Otherwise, let it be U+0020 U+00A0.
let repeated_pair = if non_breaking_start {
"\u{00A0}\u{0020}"
} else {
"\u{0020}\u{00A0}"
};
// Step 6. While n is greater than three, append repeated pair to buffer and subtract two from n.
while n > 3 {
buffer.push_str(repeated_pair);
n -= 2;
}
// Step 7. If n is three, append a three-code unit string to buffer depending on non-breaking start and non-breaking end:
if n == 3 {
buffer.push_str(match (non_breaking_start, non_breaking_end) {
(false, false) => "\u{0020}\u{00A0}\u{0020}",
(true, false) => "\u{00A0}\u{00A0}\u{0020}",
(false, true) => "\u{0020}\u{00A0}\u{00A0}",
(true, true) => "\u{00A0}\u{0020}\u{00A0}",
});
} else {
// Step 8. Otherwise, append a two-code unit string to buffer depending on non-breaking start and non-breaking end:
buffer.push_str(match (non_breaking_start, non_breaking_end) {
(false, false) | (true, false) => "\u{00A0}\u{0020}",
(false, true) => "\u{0020}\u{00A0}",
(true, true) => "\u{00A0}\u{00A0}",
});
}
// Step 9. Return buffer.
buffer
}
/// <https://w3c.github.io/editing/docs/execCommand/#canonicalize-whitespace>
fn canonicalize_whitespace(&self, offset: u32, fix_collapsed_space: bool) {
// Step 1. If node is neither editable nor an editing host, abort these steps.
if !self.is_editable_or_editing_host() {
return;
}
// Step 2. Let start node equal node and let start offset equal offset.
let mut start_node = DomRoot::from_ref(self);
let mut start_offset = offset;
// Step 3. Repeat the following steps:
loop {
// Step 3.1. If start node has a child in the same editing host with index start offset minus one,
// set start node to that child, then set start offset to start node's length.
if start_offset > 0 {
let child = start_node.children().nth(start_offset as usize - 1);
if let Some(child) = child {
if start_node.same_editing_host(&child) {
start_node = child;
start_offset = start_node.len();
continue;
}
};
}
// Step 3.2. Otherwise, if start offset is zero and start node does not follow a line break
// and start node's parent is in the same editing host, set start offset to start node's index,
// then set start node to its parent.
if start_offset == 0 && !start_node.follows_a_line_break() {
if let Some(parent) = start_node.GetParentNode() {
if parent.same_editing_host(&start_node) {
start_offset = start_node.index();
start_node = parent;
}
}
}
// Step 3.3. Otherwise, if start node is a Text node and its parent's resolved
// value for "white-space" is neither "pre" nor "pre-wrap" and start offset is not zero
// and the (start offset 1)st code unit of start node's data is a space (0x0020) or
// non-breaking space (0x00A0), subtract one from start offset.
if start_offset != 0 &&
start_node.downcast::<Text>().is_some_and(|text| {
text.has_whitespace_and_has_parent_with_whitespace_preserve(
start_offset - 1,
&[&'\u{0020}', &'\u{00A0}'],
)
})
{
start_offset -= 1;
}
// Step 3.4. Otherwise, break from this loop.
break;
}
// Step 4. Let end node equal start node and end offset equal start offset.
let mut end_node = start_node.clone();
let mut end_offset = start_offset;
// Step 5. Let length equal zero.
let mut length = 0;
// Step 6. Let collapse spaces be true if start offset is zero and start node follows a line break, otherwise false.
let mut collapse_spaces = start_offset == 0 && start_node.follows_a_line_break();
// Step 7. Repeat the following steps:
loop {
// Step 7.1. If end node has a child in the same editing host with index end offset,
// set end node to that child, then set end offset to zero.
if let Some(child) = end_node.children().nth(end_offset as usize) {
if child.same_editing_host(&end_node) {
end_node = child;
end_offset = 0;
continue;
}
}
// Step 7.2. Otherwise, if end offset is end node's length
// and end node does not precede a line break
// and end node's parent is in the same editing host,
// set end offset to one plus end node's index, then set end node to its parent.
if end_offset == end_node.len() && !end_node.precedes_a_line_break() {
if let Some(parent) = end_node.GetParentNode() {
if parent.same_editing_host(&end_node) {
end_offset = 1 + end_node.index();
end_node = parent;
}
}
continue;
}
// Step 7.3. Otherwise, if end node is a Text node and its parent's resolved value for "white-space"
// is neither "pre" nor "pre-wrap"
// and end offset is not end node's length and the end offsetth code unit of end node's data
// is a space (0x0020) or non-breaking space (0x00A0):
if let Some(text) = end_node.downcast::<Text>() {
if text.has_whitespace_and_has_parent_with_whitespace_preserve(
end_offset,
&[&'\u{0020}', &'\u{00A0}'],
) {
// Step 7.3.1. If fix collapsed space is true, and collapse spaces is true,
// and the end offsetth code unit of end node's data is a space (0x0020):
// call deleteData(end offset, 1) on end node, then continue this loop from the beginning.
let has_space_at_offset = text
.data()
.chars()
.nth(end_offset as usize)
.is_some_and(|c| c == '\u{0020}');
if fix_collapsed_space && collapse_spaces && has_space_at_offset {
if text
.upcast::<CharacterData>()
.DeleteData(end_offset, 1)
.is_err()
{
unreachable!("Invalid deletion for character at end offset");
}
continue;
}
// Step 7.3.2. Set collapse spaces to true if the end offsetth code unit of
// end node's data is a space (0x0020), false otherwise.
collapse_spaces = has_space_at_offset;
// Step 7.3.3. Add one to end offset.
end_offset += 1;
// Step 7.3.4. Add one to length.
length += 1;
continue;
}
}
// Step 7.4. Otherwise, break from this loop.
break;
}
// Step 8. If fix collapsed space is true, then while (start node, start offset)
// is before (end node, end offset):
if fix_collapsed_space {
while bp_position(&start_node, start_offset, &end_node, end_offset) ==
Some(Ordering::Less)
{
// Step 8.1. If end node has a child in the same editing host with index end offset 1,
// set end node to that child, then set end offset to end node's length.
if end_offset > 0 {
if let Some(child) = end_node.children().nth(end_offset as usize - 1) {
if child.same_editing_host(&end_node) {
end_node = child;
end_offset = end_node.len();
continue;
}
}
}
// Step 8.2. Otherwise, if end offset is zero and end node's parent is in the same editing host,
// set end offset to end node's index, then set end node to its parent.
if let Some(parent) = end_node.GetParentNode() {
if end_offset == 0 && parent.same_editing_host(&end_node) {
end_offset = end_node.index();
end_node = parent;
continue;
}
}
// Step 8.3. Otherwise, if end node is a Text node and its parent's resolved value for "white-space"
// is neither "pre" nor "pre-wrap"
// and end offset is end node's length and the last code unit of end node's data
// is a space (0x0020) and end node precedes a line break:
if let Some(text) = end_node.downcast::<Text>() {
if text.has_whitespace_and_has_parent_with_whitespace_preserve(
text.data().len() as u32,
&[&'\u{0020}'],
) && end_node.precedes_a_line_break()
{
// Step 8.3.1. Subtract one from end offset.
end_offset -= 1;
// Step 8.3.2. Subtract one from length.
length -= 1;
// Step 8.3.3. Call deleteData(end offset, 1) on end node.
if text
.upcast::<CharacterData>()
.DeleteData(end_offset, 1)
.is_err()
{
unreachable!("Invalid deletion for character at end offset");
}
continue;
}
}
// Step 8.4. Otherwise, break from this loop.
break;
}
}
// Step 9. Let replacement whitespace be the canonical space sequence of length length.
// non-breaking start is true if start offset is zero and start node follows a line break, and false otherwise.
// non-breaking end is true if end offset is end node's length and end node precedes a line break, and false otherwise.
let replacement_whitespace = Node::canonical_space_sequence(
length,
start_offset == 0 && start_node.follows_a_line_break(),
end_offset == end_node.len() && end_node.precedes_a_line_break(),
);
let mut replacement_whitespace_chars = replacement_whitespace.chars();
// Step 10. While (start node, start offset) is before (end node, end offset):
while bp_position(&start_node, start_offset, &end_node, end_offset) == Some(Ordering::Less)
{
// 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.
if let Some(child) = start_node.children().nth(start_offset as usize) {
start_node = child;
start_offset = 0;
continue;
}
// Step 10.2. Otherwise, if start node is not a Text node or if start offset is start node's length,
// set start offset to one plus start node's index, then set start node to its parent.
let start_node_as_text = start_node.downcast::<Text>();
if start_node_as_text.is_none() || start_offset == start_node.len() {
start_offset = 1 + start_node.index();
start_node = match start_node.GetParentNode() {
None => break,
Some(node) => node,
};
continue;
}
let start_node_as_text =
start_node_as_text.expect("Already verified none in previous statement");
// Step 10.3. Otherwise:
// Step 10.3.1. Remove the first code unit from replacement whitespace, and let element be that code unit.
if let Some(element) = replacement_whitespace_chars.next() {
// Step 10.3.2. If element is not the same as the start offsetth code unit of start node's data:
if start_node_as_text.data().chars().nth(start_offset as usize) != Some(element) {
let character_data = start_node_as_text.upcast::<CharacterData>();
// Step 10.3.2.1. Call insertData(start offset, element) on start node.
if character_data
.InsertData(start_offset, element.to_string().into())
.is_err()
{
unreachable!("Invalid insertion for character at start offset");
}
// Step 10.3.2.2. Call deleteData(start offset + 1, 1) on start node.
if character_data.DeleteData(start_offset + 1, 1).is_err() {
unreachable!("Invalid deletion for character at start offset + 1");
}
}
}
// Step 10.3.3. Add one to start offset.
start_offset += 1;
}
}
/// <https://w3c.github.io/editing/docs/execCommand/#remove-extraneous-line-breaks-before>
fn remove_extraneous_line_breaks_before(&self, cx: &mut js::context::JSContext) {
let parent = self.GetParentNode();
// Step 1. Let ref be the previousSibling of node.
let Some(mut ref_) = self.GetPreviousSibling() else {
// Step 2. If ref is null, abort these steps.
return;
};
// Step 3. While ref has children, set ref to its lastChild.
while let Some(last_child) = ref_.children().last() {
ref_ = last_child;
}
// Step 4. While ref is invisible but not an extraneous line break,
// and ref does not equal node's parent, set ref to the node before it in tree order.
loop {
if ref_.is_invisible() &&
ref_.downcast::<HTMLBRElement>()
.is_none_or(|br| !br.is_extraneous_line_break())
{
if let Some(parent) = parent.as_ref() {
if ref_ != *parent {
ref_ = match ref_.preceding_nodes(parent).nth(0) {
None => break,
Some(node) => node,
};
continue;
}
}
}
break;
}
// Step 5. If ref is an editable extraneous line break, remove it from its parent.
if ref_.is_editable() &&
ref_.downcast::<HTMLBRElement>()
.is_some_and(|br| br.is_extraneous_line_break())
{
ref_.remove_self(CanGc::from_cx(cx));
}
}
/// <https://w3c.github.io/editing/docs/execCommand/#remove-extraneous-line-breaks-at-the-end-of>
fn remove_extraneous_line_breaks_at_the_end_of(&self, cx: &mut js::context::JSContext) {
// Step 1. Let ref be node.
let mut ref_ = DomRoot::from_ref(self);
// Step 2. While ref has children, set ref to its lastChild.
while let Some(last_child) = ref_.children().last() {
ref_ = last_child;
}
// Step 3. While ref is invisible but not an extraneous line break, and ref does not equal node,
// set ref to the node before it in tree order.
loop {
if ref_.is_invisible() &&
*ref_ != *self &&
ref_.downcast::<HTMLBRElement>()
.is_none_or(|br| !br.is_extraneous_line_break())
{
if let Some(parent_of_ref) = ref_.GetParentNode() {
ref_ = match ref_.preceding_nodes(&parent_of_ref).nth(0) {
None => break,
Some(node) => node,
};
continue;
}
}
break;
}
// Step 4. If ref is an editable extraneous line break:
if ref_.is_editable() &&
ref_.downcast::<HTMLBRElement>()
.is_some_and(|br| br.is_extraneous_line_break())
{
// Step 4.1. While ref's parent is editable and invisible, set ref to its parent.
loop {
if let Some(parent) = ref_.GetParentNode() {
if parent.is_editable() && parent.is_invisible() {
ref_ = parent;
continue;
}
}
break;
}
// Step 4.2. Remove ref from its parent.
if ref_
.GetParentNode()
.expect("Must always have a parent")
.RemoveChild(&ref_, CanGc::from_cx(cx))
.is_err()
{
unreachable!("Must always have a parent");
}
}
}
/// <https://w3c.github.io/editing/docs/execCommand/#preserving-its-descendants>
fn remove_preserving_its_descendants(&self, cx: &mut js::context::JSContext, parent: &Node) {
// > To remove a node node while preserving its descendants,
// > split the parent of node's children if it has any.
// > If it has no children, instead remove it from its parent.
if self.children_count() == 0 {
if parent.RemoveChild(self, CanGc::from_cx(cx)).is_err() {
unreachable!("Must always have a parent to be able to remove from");
}
} else {
rooted_vec!(let children <- self.children().map(|child| DomRoot::as_traced(&child)));
split_the_parent(cx, children.r());
}
}
}
pub(crate) trait ContentEditableRange {
fn handle_focus_state_for_contenteditable(&self, can_gc: CanGc);
}
impl ContentEditableRange for HTMLElement {
/// There is no specification for this implementation. Instead, it is
/// reverse-engineered based on the WPT test
/// /selection/contenteditable/initial-selection-on-focus.tentative.html
fn handle_focus_state_for_contenteditable(&self, can_gc: CanGc) {
if !self.is_editing_host() {
return;
}
let document = self.owner_document();
let Some(selection) = document.GetSelection(can_gc) else {
return;
};
let range = self
.upcast::<Element>()
.ensure_contenteditable_selection_range(&document, can_gc);
// If the current range is already associated with this contenteditable
// element, then we shouldn't do anything. This is important when focus
// is lost and regained, but selection was changed beforehand. In that
// case, we should maintain the selection as it were, by not creating
// a new range.
if selection
.active_range()
.is_some_and(|active| active == range)
{
return;
}
let node = self.upcast::<Node>();
let mut selected_node = DomRoot::from_ref(node);
let mut previous_eligible_node = DomRoot::from_ref(node);
let mut previous_node = DomRoot::from_ref(node);
let mut selected_offset = 0;
for child in node.traverse_preorder(ShadowIncluding::Yes) {
if let Some(text) = child.downcast::<Text>() {
// Note that to consider it whitespace, it needs to take more
// into account than simply "it has a non-whitespace" character.
// Therefore, we need to first check if it is not a whitespace
// node and only then can we find what the relevant character is.
if !text.is_whitespace_node() {
// A node with "white-space: pre" set must select its first
// character, regardless if that's a whitespace character or not.
let is_pre_formatted_text_node = child
.GetParentElement()
.and_then(|parent| parent.style())
.is_some_and(|style| {
style.get_inherited_text().white_space_collapse ==
WhiteSpaceCollapse::Preserve
});
if !is_pre_formatted_text_node {
// If it isn't pre-formatted, then we should instead select the
// first non-whitespace character.
selected_offset = text
.data()
.find(|c: char| !c.is_whitespace())
.unwrap_or_default() as u32;
}
selected_node = child;
break;
}
}
// For <input>, <textarea>, <hr> and <br> elements, we should select the previous
// node, regardless if it was a block node or not
if matches!(
child.type_id(),
NodeTypeId::Element(ElementTypeId::HTMLElement(
HTMLElementTypeId::HTMLInputElement,
)) | NodeTypeId::Element(ElementTypeId::HTMLElement(
HTMLElementTypeId::HTMLTextAreaElement,
)) | NodeTypeId::Element(ElementTypeId::HTMLElement(
HTMLElementTypeId::HTMLHRElement,
)) | NodeTypeId::Element(ElementTypeId::HTMLElement(
HTMLElementTypeId::HTMLBRElement,
))
) {
selected_node = previous_node;
break;
}
// When we encounter a non-contenteditable element, we should select the previous
// eligible node
if child
.downcast::<HTMLElement>()
.is_some_and(|el| el.ContentEditable().str() == "false")
{
selected_node = previous_eligible_node;
break;
}
// We can only select block nodes as eligible nodes for the case of non-conenteditable
// nodes
if child.is_block_node() {
previous_eligible_node = child.clone();
}
previous_node = child;
}
range.set_start(&selected_node, selected_offset);
range.set_end(&selected_node, selected_offset);
selection.AddRange(&range);
}
}
trait EquivalentPoint {
fn previous_equivalent_point(&self) -> Option<(DomRoot<Node>, u32)>;
fn next_equivalent_point(&self) -> Option<(DomRoot<Node>, u32)>;
fn first_equivalent_point(self) -> (DomRoot<Node>, u32);
fn last_equivalent_point(self) -> (DomRoot<Node>, u32);
}
impl EquivalentPoint for (DomRoot<Node>, u32) {
/// <https://w3c.github.io/editing/docs/execCommand/#previous-equivalent-point>
fn previous_equivalent_point(&self) -> Option<(DomRoot<Node>, u32)> {
let (node, offset) = self;
// Step 1. If node's length is zero, return null.
let len = node.len();
if len == 0 {
return None;
}
// Step 2. If offset is 0, and node's parent is not null, and node is an inline node,
// return (node's parent, node's index).
if *offset == 0 && node.is_inline_node() {
if let Some(parent) = node.GetParentNode() {
return Some((parent, node.index()));
}
}
// Step 3. If node has a child with index offset 1, and that child's length is not zero,
// and that child is an inline node, return (that child, that child's length).
if *offset > 0 {
if let Some(child) = node.children().nth(*offset as usize - 1) {
if !child.is_empty() && child.is_inline_node() {
let len = child.len();
return Some((child, len));
}
}
}
// Step 4. Return null.
None
}
/// <https://w3c.github.io/editing/docs/execCommand/#next-equivalent-point>
fn next_equivalent_point(&self) -> Option<(DomRoot<Node>, u32)> {
let (node, offset) = self;
// Step 1. If node's length is zero, return null.
let len = node.len();
if len == 0 {
return None;
}
// Step 2.
//
// This step does not exist in the spec
// Step 3. If offset is node's length, and node's parent is not null, and node is an inline node,
// return (node's parent, 1 + node's index).
if *offset == len && node.is_inline_node() {
if let Some(parent) = node.GetParentNode() {
return Some((parent, node.index() + 1));
}
}
// Step 4.
//
// This step does not exist in the spec
// Step 5. If node has a child with index offset, and that child's length is not zero,
// and that child is an inline node, return (that child, 0).
if let Some(child) = node.children().nth(*offset as usize) {
if !child.is_empty() && child.is_inline_node() {
return Some((child, 0));
}
}
// Step 6.
//
// This step does not exist in the spec
// Step 7. Return null.
None
}
/// <https://w3c.github.io/editing/docs/execCommand/#first-equivalent-point>
fn first_equivalent_point(self) -> (DomRoot<Node>, u32) {
let mut previous_equivalent_point = self;
// Step 1. While (node, offset)'s previous equivalent point is not null, set (node, offset) to its previous equivalent point.
loop {
if let Some(next) = previous_equivalent_point.previous_equivalent_point() {
previous_equivalent_point = next;
} else {
// Step 2. Return (node, offset).
return previous_equivalent_point;
}
}
}
/// <https://w3c.github.io/editing/docs/execCommand/#last-equivalent-point>
fn last_equivalent_point(self) -> (DomRoot<Node>, u32) {
let mut next_equivalent_point = self;
// Step 1. While (node, offset)'s next equivalent point is not null, set (node, offset) to its next equivalent point.
loop {
if let Some(next) = next_equivalent_point.next_equivalent_point() {
next_equivalent_point = next;
} else {
// Step 2. Return (node, offset).
return next_equivalent_point;
}
}
}
}
#[derive(Default, PartialEq)]
pub(crate) enum SelectionDeletionBlockMerging {
#[default]
Merge,
Skip,
}
#[derive(Default, PartialEq)]
pub(crate) enum SelectionDeletionStripWrappers {
#[default]
Strip,
}
#[derive(Default, PartialEq)]
pub(crate) enum SelectionDeleteDirection {
#[default]
Forward,
}
pub(crate) trait SelectionExecCommandSupport {
fn delete_the_selection(
&self,
cx: &mut js::context::JSContext,
context_object: &Document,
block_merging: SelectionDeletionBlockMerging,
strip_wrappers: SelectionDeletionStripWrappers,
direction: SelectionDeleteDirection,
);
}
impl SelectionExecCommandSupport for Selection {
/// <https://w3c.github.io/editing/docs/execCommand/#delete-the-selection>
fn delete_the_selection(
&self,
cx: &mut js::context::JSContext,
context_object: &Document,
block_merging: SelectionDeletionBlockMerging,
strip_wrappers: SelectionDeletionStripWrappers,
direction: SelectionDeleteDirection,
) {
// Step 1. If the active range is null, abort these steps and do nothing.
let Some(active_range) = self.active_range() else {
return;
};
// Step 2. Canonicalize whitespace at the active range's start.
active_range
.start_container()
.canonicalize_whitespace(active_range.start_offset(), true);
// Step 3. Canonicalize whitespace at the active range's end.
active_range
.end_container()
.canonicalize_whitespace(active_range.end_offset(), true);
// Step 4. Let (start node, start offset) be the last equivalent point for the active range's start.
let (mut start_node, mut start_offset) =
(active_range.start_container(), active_range.start_offset()).last_equivalent_point();
// Step 5. Let (end node, end offset) be the first equivalent point for the active range's end.
let (mut end_node, mut end_offset) =
(active_range.end_container(), active_range.end_offset()).first_equivalent_point();
// Step 6. If (end node, end offset) is not after (start node, start offset):
if bp_position(&end_node, end_offset, &start_node, start_offset) != Some(Ordering::Greater)
{
// Step 6.1. If direction is "forward", call collapseToStart() on the context object's selection.
if direction == SelectionDeleteDirection::Forward {
if self.CollapseToStart(CanGc::from_cx(cx)).is_err() {
unreachable!("Should be able to collapse to start");
}
} else {
// Step 6.2. Otherwise, call collapseToEnd() on the context object's selection.
if self.CollapseToEnd(CanGc::from_cx(cx)).is_err() {
unreachable!("Should be able to collapse to end");
}
}
// Step 6.3. Abort these steps.
return;
}
// Step 7. If start node is a Text node and start offset is 0, set start offset to the index of start node,
// then set start node to its parent.
if start_node.is::<Text>() && start_offset == 0 {
start_offset = start_node.index();
start_node = match start_node.GetParentNode() {
None => return,
Some(node) => node,
};
}
// 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,
// then set end node to its parent.
if end_node.is::<Text>() && end_offset == end_node.len() {
end_offset = end_node.index() + 1;
end_node = match end_node.GetParentNode() {
None => return,
Some(node) => node,
};
}
// Step 9. Call collapse(start node, start offset) on the context object's selection.
if self
.Collapse(Some(&start_node), start_offset, CanGc::from_cx(cx))
.is_err()
{
unreachable!("Must always be able to collapse");
}
// Step 10. Call extend(end node, end offset) on the context object's selection.
if self
.Extend(&end_node, end_offset, CanGc::from_cx(cx))
.is_err()
{
unreachable!("Must always be able to extend");
}
// Step 11.
//
// This step does not exist in the spec
// Step 12. Let start block be the active range's start node.
let Some(active_range) = self.active_range() else {
return;
};
let mut start_block = active_range.start_container();
// Step 13. While start block's parent is in the same editing host and start block is an inline node,
// set start block to its parent.
loop {
if start_block.is_inline_node() {
if let Some(parent) = start_block.GetParentNode() {
if parent.same_editing_host(&start_node) {
start_block = parent;
continue;
}
}
}
break;
}
// Step 14. If start block is neither a block node nor an editing host,
// or "span" is not an allowed child of start block,
// or start block is a td or th, set start block to null.
let start_block = if (!start_block.is_block_node() && !start_block.is_editing_host()) ||
!is_allowed_child(
NodeOrString::String("span".to_owned()),
NodeOrString::Node(start_block.clone()),
) ||
start_block.is::<HTMLTableCellElement>()
{
None
} else {
Some(start_block)
};
// Step 15. Let end block be the active range's end node.
let mut end_block = active_range.end_container();
// 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.
loop {
if end_block.is_inline_node() {
if let Some(parent) = end_block.GetParentNode() {
if parent.same_editing_host(&end_block) {
end_block = parent;
continue;
}
}
}
break;
}
// Step 17. If end block is neither a block node nor an editing host, or "span" is not an allowed child of end block,
// or end block is a td or th, set end block to null.
let end_block = if (!end_block.is_block_node() && !end_block.is_editing_host()) ||
!is_allowed_child(
NodeOrString::String("span".to_owned()),
NodeOrString::Node(end_block.clone()),
) ||
end_block.is::<HTMLTableCellElement>()
{
None
} else {
Some(end_block)
};
// Step 18.
//
// This step does not exist in the spec
// Step 19. Record current states and values, and let overrides be the result.
// TODO
// Step 20.
//
// This step does not exist in the spec
// Step 21. If start node and end node are the same, and start node is an editable Text node:
if start_node == end_node && start_node.is_editable() {
if let Some(start_text) = start_node.downcast::<Text>() {
// Step 21.1. Call deleteData(start offset, end offset start offset) on start node.
if start_text
.upcast::<CharacterData>()
.DeleteData(start_offset, end_offset - start_offset)
.is_err()
{
unreachable!("Must always be able to delete");
}
// Step 21.2. Canonicalize whitespace at (start node, start offset), with fix collapsed space false.
start_node.canonicalize_whitespace(start_offset, false);
// Step 21.3. If direction is "forward", call collapseToStart() on the context object's selection.
if direction == SelectionDeleteDirection::Forward {
if self.CollapseToStart(CanGc::from_cx(cx)).is_err() {
unreachable!("Should be able to collapse to start");
}
} else {
// Step 21.4. Otherwise, call collapseToEnd() on the context object's selection.
if self.CollapseToEnd(CanGc::from_cx(cx)).is_err() {
unreachable!("Should be able to collapse to end");
}
}
// Step 21.5. Restore states and values from overrides.
//
// TODO
// Step 21.6. Abort these steps.
return;
}
}
// Step 22. If start node is an editable Text node, call deleteData() on it, with start offset as
// the first argument and (length of start node start offset) as the second argument.
if start_node.is_editable() {
if let Some(start_text) = start_node.downcast::<Text>() {
if start_text
.upcast::<CharacterData>()
.DeleteData(start_offset, start_node.len() - start_offset)
.is_err()
{
unreachable!("Must always be able to delete");
}
}
}
// Step 23. Let node list be a list of nodes, initially empty.
rooted_vec!(let mut node_list);
// Step 24. For each node contained in the active range, append node to node list if the
// last member of node list (if any) is not an ancestor of node; node is editable;
// and node is not a thead, tbody, tfoot, tr, th, or td.
let Ok(contained_children) = active_range.contained_children() else {
unreachable!("Must always have contained children");
};
for node in contained_children.contained_children {
// This type is only used to tell the compiler how to handle the type of `node_list.last()`.
// It is not allowed to add a `& DomRoot<Node>` annotation, as test-tidy disallows that.
// However, if we omit the type, the compiler doesn't know what it is, since we also
// aren't allowed to add a type annotation to `node_list` itself, as that is handled
// by the `rooted_vec` macro. Lastly, we also can't make it `&Node`, since then the compiler
// thinks that the contents of the `RootedVec` is `Node`, whereas it is should be
// `RootedVec<DomRoot<Node>>`. The type alias here doesn't upset test-tidy,
// while also providing the necessary information to the compiler to work.
type DomRootNode = DomRoot<Node>;
if node.is_editable() &&
!(node.is::<HTMLTableSectionElement>() ||
node.is::<HTMLTableRowElement>() ||
node.is::<HTMLTableCellElement>()) &&
node_list
.last()
.is_none_or(|last: &DomRootNode| !last.is_ancestor_of(&node))
{
node_list.push(node);
}
}
// Step 25. For each node in node list:
for node in node_list.iter() {
// Step 25.1. Let parent be the parent of node.
let parent = match node.GetParentNode() {
None => continue,
Some(node) => node,
};
// Step 25.2. Remove node from parent.
if parent.RemoveChild(node, CanGc::from_cx(cx)).is_err() {
unreachable!("Must always have a parent");
}
// Step 25.3. If the block node of parent has no visible children, and parent is editable or an editing host,
// call createElement("br") on the context object and append the result as the last child of parent.
if parent
.block_node_of()
.is_some_and(|block_node| block_node.children().all(|child| child.is_invisible())) &&
parent.is_editable_or_editing_host()
{
let br = context_object.create_br_element(cx);
if parent.AppendChild(br.upcast(), CanGc::from_cx(cx)).is_err() {
unreachable!("Must always be able to append");
}
}
// Step 25.4. If strip wrappers is true or parent is not an inclusive ancestor of start node,
// while parent is an editable inline node with length 0, let grandparent be the parent of parent,
// then remove parent from grandparent, then set parent to grandparent.
if strip_wrappers == SelectionDeletionStripWrappers::Strip ||
!parent.is_inclusive_ancestor_of(&start_node)
{
let mut parent = parent;
loop {
if parent.is_editable() && parent.is_inline_node() && parent.is_empty() {
let grand_parent = match parent.GetParentNode() {
None => break,
Some(node) => node,
};
if grand_parent
.RemoveChild(&parent, CanGc::from_cx(cx))
.is_err()
{
unreachable!("Must always have a grand parent");
}
parent = grand_parent;
continue;
}
break;
}
}
}
// Step 26. If end node is an editable Text node, call deleteData(0, end offset) on it.
if end_node.is_editable() {
if let Some(end_text) = end_node.downcast::<Text>() {
if end_text
.upcast::<CharacterData>()
.DeleteData(0, end_offset)
.is_err()
{
unreachable!("Must always be able to delete");
}
}
}
// Step 27. Canonicalize whitespace at the active range's start, with fix collapsed space false.
active_range
.start_container()
.canonicalize_whitespace(active_range.start_offset(), false);
// Step 28. Canonicalize whitespace at the active range's end, with fix collapsed space false.
active_range
.end_container()
.canonicalize_whitespace(active_range.end_offset(), false);
// Step 29.
//
// This step does not exist in the spec
// Step 30. If block merging is false, or start block or end block is null, or start block is not
// in the same editing host as end block, or start block and end block are the same:
if block_merging == SelectionDeletionBlockMerging::Skip ||
start_block.as_ref().zip(end_block.as_ref()).is_none_or(
|(start_block, end_block)| {
start_block == end_block || !start_block.same_editing_host(end_block)
},
)
{
// Step 30.1. If direction is "forward", call collapseToStart() on the context object's selection.
if direction == SelectionDeleteDirection::Forward {
if self.CollapseToStart(CanGc::from_cx(cx)).is_err() {
unreachable!("Should be able to collapse to start");
}
} else {
// Step 30.2. Otherwise, call collapseToEnd() on the context object's selection.
if self.CollapseToEnd(CanGc::from_cx(cx)).is_err() {
unreachable!("Should be able to collapse to end");
}
}
// Step 30.3. Restore states and values from overrides.
//
// TODO
// Step 30.4. Abort these steps.
return;
}
let start_block = start_block.expect("Already checked for None in previous statement");
let end_block = end_block.expect("Already checked for None in previous statement");
// Step 31. If start block has one child, which is a collapsed block prop, remove its child from it.
if start_block.children_count() == 1 {
let Some(child) = start_block.children().nth(0) else {
unreachable!("Must always have a single child");
};
if child.is_collapsed_block_prop() &&
start_block.RemoveChild(&child, CanGc::from_cx(cx)).is_err()
{
unreachable!("Must always be able to remove child");
}
}
// Step 32. If start block is an ancestor of end block:
if start_block.is_ancestor_of(&end_block) {
// Step 32.1. Let reference node be end block.
let mut reference_node = end_block.clone();
// Step 32.2. While reference node is not a child of start block, set reference node to its parent.
loop {
if start_block.children().all(|child| child != reference_node) {
reference_node = reference_node
.GetParentNode()
.expect("Must always have a parent, at least start_block");
continue;
}
break;
}
// Step 32.3. Call collapse() on the context object's selection,
// with first argument start block and second argument the index of reference node.
if self
.Collapse(
Some(&start_block),
reference_node.index(),
CanGc::from_cx(cx),
)
.is_err()
{
unreachable!("Must always be able to collapse");
}
// Step 32.4. If end block has no children:
if end_block.children_count() == 0 {
let mut end_block = end_block;
// 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,
// let parent equal end block, then remove end block from parent, then set end block to parent.
loop {
if end_block.is_editable() &&
start_block.children().all(|child| child != end_block)
{
if let Some(parent) = end_block.GetParentNode() {
if parent.children_count() == 1 {
if parent.RemoveChild(&end_block, CanGc::from_cx(cx)).is_err() {
unreachable!("Must always have a parent");
}
end_block = parent;
continue;
}
}
}
break;
}
// Step 32.4.2. If end block is editable and is not an inline node,
// and its previousSibling and nextSibling are both inline nodes,
// call createElement("br") on the context object and insert it into end block's parent immediately after end block.
if end_block.is_editable() &&
!end_block.is_inline_node() &&
end_block
.GetPreviousSibling()
.is_some_and(|previous| previous.is_inline_node())
{
if let Some(next_of_end_block) = end_block.GetNextSibling() {
if next_of_end_block.is_inline_node() {
let br = context_object.create_br_element(cx);
let parent = end_block
.GetParentNode()
.expect("Must always have a parent");
if parent
.InsertBefore(
br.upcast(),
Some(&next_of_end_block),
CanGc::from_cx(cx),
)
.is_err()
{
unreachable!("Must always be able to insert into parent");
}
}
}
}
// Step 32.4.3. If end block is editable, remove it from its parent.
if end_block.is_editable() &&
end_block
.GetParentNode()
.expect("Must always have a parent")
.RemoveChild(&end_block, CanGc::from_cx(cx))
.is_err()
{
unreachable!("Must always have a parent to remove child");
}
// Step 32.4.4. Restore states and values from overrides.
//
// TODO
// Step 32.4.5. Abort these steps.
return;
}
let first_child = end_block
.children()
.nth(0)
.expect("Already checked at least 1 child in previous statement");
// Step 32.5. If end block's firstChild is not an inline node,
// restore states and values from record, then abort these steps.
if !first_child.is_inline_node() {
// TODO: Restore state
return;
}
// Step 32.6. Let children be a list of nodes, initially empty.
rooted_vec!(let mut children);
// Step 32.7. Append the first child of end block to children.
children.push(first_child.as_traced());
// Step 32.8. While children's last member is not a br,
// and children's last member's nextSibling is an inline node,
// append children's last member's nextSibling to children.
loop {
let Some(last) = children.last() else {
break;
};
if last.is::<HTMLBRElement>() {
break;
}
let Some(next) = last.GetNextSibling() else {
break;
};
if next.is_inline_node() {
children.push(next.as_traced());
continue;
}
break;
}
// Step 32.9. Record the values of children, and let values be the result.
// TODO
// Step 32.10. While children's first member's parent is not start block,
// split the parent of children.
loop {
if children
.first()
.and_then(|child| child.GetParentNode())
.is_some_and(|parent_of_child| parent_of_child != start_block)
{
split_the_parent(cx, children.r());
continue;
}
break;
}
// Step 32.11. If children's first member's previousSibling is an editable br,
// remove that br from its parent.
if let Some(first) = children.first() {
if let Some(previous_of_first) = first.GetPreviousSibling() {
if previous_of_first.is_editable() && previous_of_first.is::<HTMLBRElement>() {
if let Some(parent) = previous_of_first.GetParentNode() {
if parent
.RemoveChild(&previous_of_first, CanGc::from_cx(cx))
.is_err()
{
unreachable!("Must always have a parent");
}
}
}
}
}
// Step 33. Otherwise, if start block is a descendant of end block:
} else if end_block.is_ancestor_of(&start_block) {
// Step 33.1. Call collapse() on the context object's selection,
// with first argument start block and second argument start block's length.
if self
.Collapse(Some(&start_block), start_block.len(), CanGc::from_cx(cx))
.is_err()
{
unreachable!("Must always be able to collapse");
}
// Step 33.2. Let reference node be start block.
let mut reference_node = start_block.clone();
// Step 33.3. While reference node is not a child of end block, set reference node to its parent.
loop {
if end_block.children().all(|child| child != reference_node) {
if let Some(parent) = reference_node.GetParentNode() {
reference_node = parent;
continue;
}
}
break;
}
// Step 33.4. If reference node's nextSibling is an inline node and start block's lastChild is a br,
// remove start block's lastChild from it.
if reference_node
.GetNextSibling()
.is_some_and(|next| next.is_inline_node())
{
if let Some(last) = start_block.children().last() {
if last.is::<HTMLBRElement>() &&
start_block.RemoveChild(&last, CanGc::from_cx(cx)).is_err()
{
unreachable!("Must always have a parent");
}
}
}
// Step 33.5. Let nodes to move be a list of nodes, initially empty.
rooted_vec!(let mut nodes_to_move);
// Step 33.6. If reference node's nextSibling is neither null nor a block node,
// append it to nodes to move.
if let Some(next) = reference_node.GetNextSibling() {
if !next.is_block_node() {
nodes_to_move.push(next);
}
}
// Step 33.7. While nodes to move is nonempty and its last member isn't a br
// and its last member's nextSibling is neither null nor a block node,
// append its last member's nextSibling to nodes to move.
loop {
if let Some(last) = nodes_to_move.last() {
if !last.is::<HTMLBRElement>() {
if let Some(next_of_last) = last.GetNextSibling() {
if !next_of_last.is_block_node() {
nodes_to_move.push(next_of_last);
continue;
}
}
}
}
break;
}
// Step 33.8. Record the values of nodes to move, and let values be the result.
// TODO
// Step 33.9. For each node in nodes to move,
// append node as the last child of start block, preserving ranges.
for node in nodes_to_move.iter() {
// TODO: Preserve ranges
if start_block.AppendChild(node, CanGc::from_cx(cx)).is_err() {
unreachable!("Must always be able to append");
}
}
// Step 34. Otherwise:
} else {
// Step 34.1. Call collapse() on the context object's selection,
// with first argument start block and second argument start block's length.
if self
.Collapse(Some(&start_block), start_block.len(), CanGc::from_cx(cx))
.is_err()
{
unreachable!("Must always be able to collapse");
}
// Step 34.2. If end block's firstChild is an inline node and start block's lastChild is a br,
// remove start block's lastChild from it.
if end_block
.children()
.nth(0)
.is_some_and(|next| next.is_inline_node())
{
if let Some(last) = start_block.children().last() {
if last.is::<HTMLBRElement>() &&
start_block.RemoveChild(&last, CanGc::from_cx(cx)).is_err()
{
unreachable!("Must always have a parent");
}
}
}
// Step 34.3. Record the values of end block's children, and let values be the result.
// TODO
// Step 34.4. While end block has children,
// append the first child of end block to start block, preserving ranges.
loop {
if let Some(first_child) = end_block.children().nth(0) {
// TODO: Preserve ranges
if start_block
.AppendChild(&first_child, CanGc::from_cx(cx))
.is_err()
{
unreachable!("Must always be able to append");
}
continue;
}
break;
}
// Step 34.5. While end block has no children,
// let parent be the parent of end block, then remove end block from parent,
// then set end block to parent.
let mut end_block = end_block;
loop {
if end_block.children_count() == 0 {
if let Some(parent) = end_block.GetParentNode() {
if parent.RemoveChild(&end_block, CanGc::from_cx(cx)).is_err() {
unreachable!("Must always have a parent");
}
end_block = parent;
continue;
}
}
break;
}
}
// Step 35.
//
// This step does not exist in the spec
// Step 36. Let ancestor be start block.
// TODO
// Step 37. While ancestor has an inclusive ancestor ol in the same editing host whose nextSibling is
// also an ol in the same editing host, or an inclusive ancestor ul in the same editing host whose nextSibling
// is also a ul in the same editing host:
// TODO
// Step 38. Restore the values from values.
// TODO
// Step 39. If start block has no children, call createElement("br") on the context object and
// append the result as the last child of start block.
if start_block.children_count() == 0 {
let br = context_object.create_br_element(cx);
if start_block
.AppendChild(br.upcast(), CanGc::from_cx(cx))
.is_err()
{
unreachable!("Must always be able to append");
}
}
// Step 40. Remove extraneous line breaks at the end of start block.
start_block.remove_extraneous_line_breaks_at_the_end_of(cx);
// Step 41. Restore states and values from overrides.
// TODO
}
}