script/dom/execcommand/contenteditable/range.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 js::context::{JSContext, NoGC};
6use script_bindings::inheritance::Castable;
7
8use crate::dom::bindings::codegen::Bindings::DocumentBinding::DocumentMethods;
9use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods;
10use crate::dom::bindings::codegen::Bindings::RangeBinding::RangeMethods;
11use crate::dom::bindings::root::{DomRoot, UnrootedDom};
12use crate::dom::document::Document;
13use crate::dom::execcommand::basecommand::{
14 BoolOrOptionalString, CommandName, RecordedStateOfCommand,
15};
16use crate::dom::execcommand::commands::fontsize::legacy_font_size_for;
17use crate::dom::html::htmllielement::HTMLLIElement;
18use crate::dom::iterators::ShadowIncluding;
19use crate::dom::node::Node;
20use crate::dom::range::Range;
21use crate::dom::selection::Selection;
22use crate::dom::text::Text;
23
24impl Range {
25 /// <https://w3c.github.io/editing/docs/execCommand/#effectively-contained>
26 fn is_effectively_contained_node(&self, node: &Node) -> bool {
27 // > A node node is effectively contained in a range range if range is not collapsed,
28 if self.collapsed() {
29 return false;
30 }
31 // > and at least one of the following holds:
32 // > node is range's start node, it is a Text node, and its length is different from range's start offset.
33 let start_container = self.start_container();
34 if *start_container == *node && node.is::<Text>() && node.len() != self.start_offset() {
35 return true;
36 }
37 // > node is range's end node, it is a Text node, and range's end offset is not 0.
38 let end_container = self.end_container();
39 if *end_container == *node && node.is::<Text>() && self.end_offset() != 0 {
40 return true;
41 }
42 // > node is contained in range.
43 if self.contains(node) {
44 return true;
45 }
46 // > node has at least one child; and all its children are effectively contained in range;
47 node.children_count() > 0 && node.children().all(|child| self.is_effectively_contained_node(&child))
48 // > 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;
49 && (!node.is_ancestor_of(&start_container) || !start_container.is::<Text>() || self.start_offset() == 0)
50 // > 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.
51 && (!node.is_ancestor_of(&end_container) || !end_container.is::<Text>() || self.end_offset() == end_container.len())
52 }
53
54 /// The definition of "effectively contained" contains the recursion of
55 /// ancestors of a single fully selected text node. That is to say, that
56 /// if the selection is a fully selected text node <div>[foobar]</div>,
57 /// then the div would also be considered effectively contained. As such,
58 /// we can't use the common ancestor container, since that would be the
59 /// text node only.
60 ///
61 /// Instead, we traverse all the way up to the editing host, which we know
62 /// is sufficient to know to include all contained nodes. That way, we also
63 /// would traverse ancestors such as the parent div.
64 fn ancestor_for_effectively_contained(&self) -> DomRoot<Node> {
65 let ancestor_container = self.CommonAncestorContainer();
66 ancestor_container
67 .editing_host_of()
68 .unwrap_or(ancestor_container)
69 }
70
71 pub(crate) fn first_formattable_contained_node(&self, no_gc: &NoGC) -> Option<DomRoot<Node>> {
72 if self.collapsed() {
73 return None;
74 }
75
76 self.ancestor_for_effectively_contained()
77 .traverse_preorder_non_rooting(no_gc, ShadowIncluding::No)
78 .find(|child| child.is_formattable(no_gc) && self.is_effectively_contained_node(child))
79 .map(|node| node.as_rooted())
80 }
81
82 pub(crate) fn for_each_effectively_contained_child<Callback: FnMut(&Node)>(
83 &self,
84 mut callback: Callback,
85 ) {
86 if self.collapsed() {
87 return;
88 }
89
90 // Make sure to keep track of the tree nodes before, since `callback` might modify
91 // the underyling tree and then the iterator would prematurely stop.
92 let children = self
93 .ancestor_for_effectively_contained()
94 .traverse_preorder(ShadowIncluding::No)
95 .collect::<Vec<DomRoot<Node>>>();
96
97 for child in children {
98 if self.is_effectively_contained_node(&child) {
99 callback(&child);
100 }
101 }
102 }
103
104 pub(crate) fn contained_nodes<'a>(
105 &self,
106 no_gc: &'a NoGC,
107 ) -> impl Iterator<Item = UnrootedDom<'a, Node>> {
108 self.CommonAncestorContainer()
109 .traverse_preorder_non_rooting(no_gc, ShadowIncluding::No)
110 .filter(|node| self.contains(node))
111 }
112
113 /// <https://w3c.github.io/editing/docs/execCommand/#block-extend>
114 pub(crate) fn block_extend(&self, cx: &mut JSContext, document: &Document) -> DomRoot<Range> {
115 // Step 1. Let start node, start offset, end node,
116 // and end offset be the start and end nodes and offsets of range.
117 let mut start_node = self.start_container();
118 let mut start_offset = self.start_offset();
119 let mut end_node = self.end_container();
120 let mut end_offset = self.end_offset();
121 // Step 2. If some inclusive ancestor of start node is an li,
122 // set start offset to the index of the last such li in tree order, and set start node to that li's parent.
123 if let Some(li_ancestor) = start_node
124 .inclusive_ancestors_unrooted(cx.no_gc(), ShadowIncluding::No)
125 .find(|ancestor| ancestor.is::<HTMLLIElement>())
126 {
127 start_offset = li_ancestor.index();
128 start_node = li_ancestor
129 .GetParentNode()
130 .expect("Must always have a parent");
131 }
132 // Step 3. If (start node, start offset) is not a block start point, repeat the following steps:
133 if !start_node.is_block_start_point(cx.no_gc(), start_offset as usize) {
134 loop {
135 // Step 3.1. If start offset is zero, set it to start node's index, then set start node to its parent.
136 if start_offset == 0 {
137 start_offset = start_node.index();
138 start_node = start_node
139 .GetParentNode()
140 .expect("Must always have a parent");
141 } else {
142 // Step 3.2. Otherwise, subtract one from start offset.
143 start_offset -= 1;
144 }
145 // Step 3.3. If (start node, start offset) is a block boundary point, break from this loop.
146 if start_node.is_block_boundary_point(cx.no_gc(), start_offset) {
147 break;
148 }
149 }
150 }
151 // Step 4. While start offset is zero and start node's parent is not null,
152 // set start offset to start node's index, then set start node to its parent.
153 while start_offset == 0 &&
154 let Some(parent) = start_node.GetParentNode()
155 {
156 start_offset = start_node.index();
157 start_node = parent;
158 }
159 // Step 5. If some inclusive ancestor of end node is an li,
160 // set end offset to one plus the index of the last such li in tree order,
161 // and set end node to that li's parent.
162 if let Some(li_ancestor) = end_node
163 .inclusive_ancestors_unrooted(cx.no_gc(), ShadowIncluding::No)
164 .find(|ancestor| ancestor.is::<HTMLLIElement>())
165 {
166 end_offset = 1 + li_ancestor.index();
167 end_node = li_ancestor
168 .GetParentNode()
169 .expect("Must always have a parent");
170 }
171 // Step 6. If (end node, end offset) is not a block end point, repeat the following steps:
172 if !end_node.is_block_end_point(end_offset, cx.no_gc()) {
173 loop {
174 // Step 6.1. If end offset is end node's length, set it to one plus end node's index, then set end node to its parent.
175 if end_offset == end_node.len() {
176 end_offset = 1 + end_node.index();
177 end_node = end_node.GetParentNode().expect("Must always have a parent");
178 } else {
179 // Step 6.2. Otherwise, add one to end offset.
180 end_offset += 1;
181 }
182 // Step 6.3. If (end node, end offset) is a block boundary point, break from this loop.
183 if end_node.is_block_boundary_point(cx.no_gc(), end_offset) {
184 break;
185 }
186 }
187 }
188 // Step 7. While end offset is end node's length and end node's parent is not null,
189 // set end offset to one plus end node's index, then set end node to its parent.
190 while end_offset == end_node.len() &&
191 let Some(parent) = end_node.GetParentNode()
192 {
193 end_offset = 1 + end_node.index();
194 end_node = parent;
195 }
196 // Step 8. Let new range be a new range whose start and end nodes and offsets are start node,
197 // start offset, end node, and end offset.
198 let new_range = document.CreateRange(cx);
199 let _ = new_range.SetStart(&start_node, start_offset);
200 let _ = new_range.SetEnd(&end_node, end_offset);
201 // Step 9. Return new range.
202 new_range
203 }
204
205 /// <https://w3c.github.io/editing/docs/execCommand/#record-current-states-and-values>
206 pub(crate) fn record_current_states_and_values(
207 &self,
208 cx: &mut JSContext,
209 ) -> Vec<RecordedStateOfCommand> {
210 // Step 1. Let overrides be a list of (string, string or boolean) ordered pairs, initially empty.
211 //
212 // We return the vec in one go for the relevant values
213
214 // Step 2. Let node be the first formattable node effectively contained in the active range,
215 // or null if there is none.
216 let Some(node) = self.first_formattable_contained_node(cx.no_gc()) else {
217 // Step 3. If node is null, return overrides.
218 return vec![];
219 };
220 // Step 8. Return overrides.
221 let document = node.owner_doc();
222 vec![
223 // Step 4. Add ("createLink", node's effective command value for "createLink") to overrides.
224 RecordedStateOfCommand::for_command_node(CommandName::CreateLink, &node),
225 // Step 5. For each command in the list
226 // "bold", "italic", "strikethrough", "subscript", "superscript", "underline", in order:
227 // if node's effective command value for command is one of its inline command activated values,
228 // add (command, true) to overrides, and otherwise add (command, false) to overrides.
229 RecordedStateOfCommand::for_command_node_with_inline_activated_values(
230 CommandName::Bold,
231 &node,
232 ),
233 RecordedStateOfCommand::for_command_node_with_inline_activated_values(
234 CommandName::Italic,
235 &node,
236 ),
237 RecordedStateOfCommand::for_command_node_with_inline_activated_values(
238 CommandName::Strikethrough,
239 &node,
240 ),
241 RecordedStateOfCommand::for_command_node_with_inline_activated_values(
242 CommandName::Subscript,
243 &node,
244 ),
245 RecordedStateOfCommand::for_command_node_with_inline_activated_values(
246 CommandName::Superscript,
247 &node,
248 ),
249 RecordedStateOfCommand::for_command_node_with_inline_activated_values(
250 CommandName::Underline,
251 &node,
252 ),
253 // Step 6. For each command in the list "fontName", "foreColor", "hiliteColor", in order:
254 // add (command, command's value) to overrides.
255 RecordedStateOfCommand::for_command_node_with_value(
256 cx,
257 CommandName::FontName,
258 &document,
259 ),
260 RecordedStateOfCommand::for_command_node_with_value(
261 cx,
262 CommandName::ForeColor,
263 &document,
264 ),
265 RecordedStateOfCommand::for_command_node_with_value(
266 cx,
267 CommandName::HiliteColor,
268 &document,
269 ),
270 // Step 7. Add ("fontSize", node's effective command value for "fontSize") to overrides.
271 RecordedStateOfCommand::for_command_node(CommandName::FontSize, &node),
272 ]
273 }
274
275 /// <https://w3c.github.io/editing/docs/execCommand/#restore-states-and-values>
276 pub(crate) fn restore_states_and_values(
277 &self,
278 cx: &mut JSContext,
279 selection: &Selection,
280 context_object: &Document,
281 overrides: Vec<RecordedStateOfCommand>,
282 ) {
283 // Step 1. Let node be the first formattable node effectively contained in the active range,
284 // or null if there is none.
285 let mut first_formattable_contained_node =
286 self.first_formattable_contained_node(cx.no_gc());
287 for override_state in overrides {
288 // Step 2. If node is not null, then for each (command, override) pair in overrides, in order:
289 if let Some(ref node) = first_formattable_contained_node {
290 match override_state.value {
291 // Step 2.1. If override is a boolean, and queryCommandState(command)
292 // returns something different from override, take the action for command,
293 // with value equal to the empty string.
294 BoolOrOptionalString::Bool(bool_)
295 if override_state
296 .command
297 .current_state(cx, context_object)
298 .is_some_and(|value| value != bool_) =>
299 {
300 override_state
301 .command
302 .execute(cx, context_object, selection, "".into());
303 },
304 BoolOrOptionalString::OptionalString(optional_string) => {
305 match override_state.command {
306 // Step 2.3. Otherwise, if override is a string; and command is "createLink";
307 // and either there is a value override for "createLink" that is not equal to override,
308 // or there is no value override for "createLink" and node's effective command value
309 // for "createLink" is not equal to override: take the action for "createLink", with value equal to override.
310 CommandName::CreateLink => {
311 let value_override =
312 context_object.value_override(&CommandName::CreateLink);
313 if value_override != optional_string {
314 CommandName::CreateLink.execute(
315 cx,
316 context_object,
317 selection,
318 optional_string.unwrap_or_default(),
319 );
320 }
321 },
322 // Step 2.4. Otherwise, if override is a string; and command is "fontSize";
323 // and either there is a value override for "fontSize" that is not equal to override,
324 // or there is no value override for "fontSize" and node's effective command value for "fontSize"
325 // is not loosely equivalent to override:
326 CommandName::FontSize => {
327 let value_override =
328 context_object.value_override(&CommandName::FontSize);
329 if value_override != optional_string ||
330 (value_override.is_none() &&
331 !CommandName::FontSize.are_loosely_equivalent_values(
332 node.effective_command_value(&CommandName::FontSize)
333 .as_ref(),
334 optional_string.as_ref(),
335 ))
336 {
337 // Step 2.5. Convert override to an integer number of pixels,
338 // and set override to the legacy font size for the result.
339 let pixels = optional_string
340 .and_then(|value| value.parse::<i32>().ok())
341 .map(|value| {
342 legacy_font_size_for(value as f32, context_object)
343 })
344 .unwrap_or("7".into());
345 // Step 2.6. Take the action for "fontSize", with value equal to override.
346 CommandName::FontSize.execute(
347 cx,
348 context_object,
349 selection,
350 pixels,
351 );
352 }
353 },
354 // Step 2.2. Otherwise, if override is a string, and command is neither "createLink" nor "fontSize",
355 // and queryCommandValue(command) returns something not equivalent to override,
356 // take the action for command, with value equal to override.
357 command
358 if command.current_value(cx, context_object) != optional_string =>
359 {
360 command.execute(
361 cx,
362 context_object,
363 selection,
364 optional_string.unwrap_or_default(),
365 );
366 },
367 // Step 2.5. Otherwise, continue this loop from the beginning.
368 _ => {
369 continue;
370 },
371 }
372 },
373 // Step 2.5. Otherwise, continue this loop from the beginning.
374 _ => {
375 continue;
376 },
377 }
378 // Step 2.6. Set node to the first formattable node effectively contained in the active range, if there is one.
379 first_formattable_contained_node =
380 self.first_formattable_contained_node(cx.no_gc());
381 } else {
382 // Step 3. Otherwise, for each (command, override) pair in overrides, in order:
383 // Step 3.1. If override is a boolean, set the state override for command to override.
384 match override_state.value {
385 BoolOrOptionalString::Bool(bool_) => {
386 context_object.set_state_override(override_state.command, Some(bool_))
387 },
388 // Step 3.2. If override is a string, set the value override for command to override.
389 BoolOrOptionalString::OptionalString(optional_string) => {
390 context_object.set_value_override(override_state.command, optional_string)
391 },
392 }
393 }
394 }
395 }
396}