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;
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;
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) -> Option<DomRoot<Node>> {
72 if self.collapsed() {
73 return None;
74 }
75
76 self.ancestor_for_effectively_contained()
77 .traverse_preorder(ShadowIncluding::No)
78 .find(|child| child.is_formattable() && self.is_effectively_contained_node(child))
79 }
80
81 pub(crate) fn for_each_effectively_contained_child<Callback: FnMut(&Node)>(
82 &self,
83 mut callback: Callback,
84 ) {
85 if self.collapsed() {
86 return;
87 }
88
89 // Make sure to keep track of the tree nodes before, since `callback` might modify
90 // the underyling tree and then the iterator would prematurely stop.
91 let children = self
92 .ancestor_for_effectively_contained()
93 .traverse_preorder(ShadowIncluding::No)
94 .collect::<Vec<DomRoot<Node>>>();
95
96 for child in children {
97 if self.is_effectively_contained_node(&child) {
98 callback(&child);
99 }
100 }
101 }
102
103 /// <https://w3c.github.io/editing/docs/execCommand/#block-extend>
104 pub(crate) fn block_extend(&self, cx: &mut JSContext, document: &Document) -> DomRoot<Range> {
105 // Step 1. Let start node, start offset, end node,
106 // and end offset be the start and end nodes and offsets of range.
107 let mut start_node = self.start_container();
108 let mut start_offset = self.start_offset();
109 let mut end_node = self.end_container();
110 let mut end_offset = self.end_offset();
111 // Step 2. If some inclusive ancestor of start node is an li,
112 // set start offset to the index of the last such li in tree order, and set start node to that li's parent.
113 if let Some(li_ancestor) = start_node
114 .inclusive_ancestors_unrooted(cx.no_gc(), ShadowIncluding::No)
115 .find(|ancestor| ancestor.is::<HTMLLIElement>())
116 {
117 start_offset = li_ancestor.index();
118 start_node = li_ancestor
119 .GetParentNode()
120 .expect("Must always have a parent");
121 }
122 // Step 3. If (start node, start offset) is not a block start point, repeat the following steps:
123 if !start_node.is_block_start_point(start_offset as usize) {
124 loop {
125 // Step 3.1. If start offset is zero, set it to start node's index, then set start node to its parent.
126 if start_offset == 0 {
127 start_offset = start_node.index();
128 start_node = start_node
129 .GetParentNode()
130 .expect("Must always have a parent");
131 } else {
132 // Step 3.2. Otherwise, subtract one from start offset.
133 start_offset -= 1;
134 }
135 // Step 3.3. If (start node, start offset) is a block boundary point, break from this loop.
136 if start_node.is_block_boundary_point(start_offset) {
137 break;
138 }
139 }
140 }
141 // Step 4. While start offset is zero and start node's parent is not null,
142 // set start offset to start node's index, then set start node to its parent.
143 while start_offset == 0 &&
144 let Some(parent) = start_node.GetParentNode()
145 {
146 start_offset = start_node.index();
147 start_node = parent;
148 }
149 // Step 5. If some inclusive ancestor of end node is an li,
150 // set end offset to one plus the index of the last such li in tree order,
151 // and set end node to that li's parent.
152 if let Some(li_ancestor) = end_node
153 .inclusive_ancestors_unrooted(cx.no_gc(), ShadowIncluding::No)
154 .find(|ancestor| ancestor.is::<HTMLLIElement>())
155 {
156 end_offset = 1 + li_ancestor.index();
157 end_node = li_ancestor
158 .GetParentNode()
159 .expect("Must always have a parent");
160 }
161 // Step 6. If (end node, end offset) is not a block end point, repeat the following steps:
162 if !end_node.is_block_end_point(end_offset) {
163 loop {
164 // 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.
165 if end_offset == end_node.len() {
166 end_offset = 1 + end_node.index();
167 end_node = end_node.GetParentNode().expect("Must always have a parent");
168 } else {
169 // Step 6.2. Otherwise, add one to end offset.
170 end_offset += 1;
171 }
172 // Step 6.3. If (end node, end offset) is a block boundary point, break from this loop.
173 if end_node.is_block_boundary_point(end_offset) {
174 break;
175 }
176 }
177 }
178 // Step 7. While end offset is end node's length and end node's parent is not null,
179 // set end offset to one plus end node's index, then set end node to its parent.
180 while end_offset == end_node.len() &&
181 let Some(parent) = end_node.GetParentNode()
182 {
183 end_offset = 1 + end_node.index();
184 end_node = parent;
185 }
186 // Step 8. Let new range be a new range whose start and end nodes and offsets are start node,
187 // start offset, end node, and end offset.
188 let new_range = document.CreateRange(cx);
189 new_range.set_start(&start_node, start_offset);
190 new_range.set_end(&end_node, end_offset);
191 // Step 9. Return new range.
192 new_range
193 }
194
195 /// <https://w3c.github.io/editing/docs/execCommand/#record-current-states-and-values>
196 pub(crate) fn record_current_states_and_values(
197 &self,
198 cx: &mut JSContext,
199 ) -> Vec<RecordedStateOfCommand> {
200 // Step 1. Let overrides be a list of (string, string or boolean) ordered pairs, initially empty.
201 //
202 // We return the vec in one go for the relevant values
203
204 // Step 2. Let node be the first formattable node effectively contained in the active range,
205 // or null if there is none.
206 let Some(node) = self.first_formattable_contained_node() else {
207 // Step 3. If node is null, return overrides.
208 return vec![];
209 };
210 // Step 8. Return overrides.
211 let document = node.owner_doc();
212 vec![
213 // Step 4. Add ("createLink", node's effective command value for "createLink") to overrides.
214 RecordedStateOfCommand::for_command_node(CommandName::CreateLink, &node),
215 // Step 5. For each command in the list
216 // "bold", "italic", "strikethrough", "subscript", "superscript", "underline", in order:
217 // if node's effective command value for command is one of its inline command activated values,
218 // add (command, true) to overrides, and otherwise add (command, false) to overrides.
219 RecordedStateOfCommand::for_command_node_with_inline_activated_values(
220 CommandName::Bold,
221 &node,
222 ),
223 RecordedStateOfCommand::for_command_node_with_inline_activated_values(
224 CommandName::Italic,
225 &node,
226 ),
227 RecordedStateOfCommand::for_command_node_with_inline_activated_values(
228 CommandName::Strikethrough,
229 &node,
230 ),
231 RecordedStateOfCommand::for_command_node_with_inline_activated_values(
232 CommandName::Subscript,
233 &node,
234 ),
235 RecordedStateOfCommand::for_command_node_with_inline_activated_values(
236 CommandName::Superscript,
237 &node,
238 ),
239 RecordedStateOfCommand::for_command_node_with_inline_activated_values(
240 CommandName::Underline,
241 &node,
242 ),
243 // Step 6. For each command in the list "fontName", "foreColor", "hiliteColor", in order:
244 // add (command, command's value) to overrides.
245 RecordedStateOfCommand::for_command_node_with_value(
246 cx,
247 CommandName::FontName,
248 &document,
249 ),
250 RecordedStateOfCommand::for_command_node_with_value(
251 cx,
252 CommandName::ForeColor,
253 &document,
254 ),
255 RecordedStateOfCommand::for_command_node_with_value(
256 cx,
257 CommandName::HiliteColor,
258 &document,
259 ),
260 // Step 7. Add ("fontSize", node's effective command value for "fontSize") to overrides.
261 RecordedStateOfCommand::for_command_node(CommandName::FontSize, &node),
262 ]
263 }
264
265 /// <https://w3c.github.io/editing/docs/execCommand/#restore-states-and-values>
266 pub(crate) fn restore_states_and_values(
267 &self,
268 cx: &mut JSContext,
269 selection: &Selection,
270 context_object: &Document,
271 overrides: Vec<RecordedStateOfCommand>,
272 ) {
273 // Step 1. Let node be the first formattable node effectively contained in the active range,
274 // or null if there is none.
275 let mut first_formattable_contained_node = self.first_formattable_contained_node();
276 for override_state in overrides {
277 // Step 2. If node is not null, then for each (command, override) pair in overrides, in order:
278 if let Some(ref node) = first_formattable_contained_node {
279 match override_state.value {
280 // Step 2.1. If override is a boolean, and queryCommandState(command)
281 // returns something different from override, take the action for command,
282 // with value equal to the empty string.
283 BoolOrOptionalString::Bool(bool_)
284 if override_state
285 .command
286 .current_state(cx, context_object)
287 .is_some_and(|value| value != bool_) =>
288 {
289 override_state
290 .command
291 .execute(cx, context_object, selection, "".into());
292 },
293 BoolOrOptionalString::OptionalString(optional_string) => {
294 match override_state.command {
295 // Step 2.3. Otherwise, if override is a string; and command is "createLink";
296 // and either there is a value override for "createLink" that is not equal to override,
297 // or there is no value override for "createLink" and node's effective command value
298 // for "createLink" is not equal to override: take the action for "createLink", with value equal to override.
299 CommandName::CreateLink => {
300 let value_override =
301 context_object.value_override(&CommandName::CreateLink);
302 if value_override != optional_string {
303 CommandName::CreateLink.execute(
304 cx,
305 context_object,
306 selection,
307 optional_string.unwrap_or_default(),
308 );
309 }
310 },
311 // Step 2.4. Otherwise, if override is a string; and command is "fontSize";
312 // and either there is a value override for "fontSize" that is not equal to override,
313 // or there is no value override for "fontSize" and node's effective command value for "fontSize"
314 // is not loosely equivalent to override:
315 CommandName::FontSize => {
316 let value_override =
317 context_object.value_override(&CommandName::FontSize);
318 if value_override != optional_string ||
319 (value_override.is_none() &&
320 !CommandName::FontSize.are_loosely_equivalent_values(
321 node.effective_command_value(&CommandName::FontSize)
322 .as_ref(),
323 optional_string.as_ref(),
324 ))
325 {
326 // Step 2.5. Convert override to an integer number of pixels,
327 // and set override to the legacy font size for the result.
328 let pixels = optional_string
329 .and_then(|value| value.parse::<i32>().ok())
330 .map(|value| {
331 legacy_font_size_for(value as f32, context_object)
332 })
333 .unwrap_or("7".into());
334 // Step 2.6. Take the action for "fontSize", with value equal to override.
335 CommandName::FontSize.execute(
336 cx,
337 context_object,
338 selection,
339 pixels,
340 );
341 }
342 },
343 // Step 2.2. Otherwise, if override is a string, and command is neither "createLink" nor "fontSize",
344 // and queryCommandValue(command) returns something not equivalent to override,
345 // take the action for command, with value equal to override.
346 command
347 if command.current_value(cx, context_object) != optional_string =>
348 {
349 command.execute(
350 cx,
351 context_object,
352 selection,
353 optional_string.unwrap_or_default(),
354 );
355 },
356 // Step 2.5. Otherwise, continue this loop from the beginning.
357 _ => {
358 continue;
359 },
360 }
361 },
362 // Step 2.5. Otherwise, continue this loop from the beginning.
363 _ => {
364 continue;
365 },
366 }
367 // Step 2.6. Set node to the first formattable node effectively contained in the active range, if there is one.
368 first_formattable_contained_node = self.first_formattable_contained_node();
369 } else {
370 // Step 3. Otherwise, for each (command, override) pair in overrides, in order:
371 // Step 3.1. If override is a boolean, set the state override for command to override.
372 match override_state.value {
373 BoolOrOptionalString::Bool(bool_) => {
374 context_object.set_state_override(override_state.command, Some(bool_))
375 },
376 // Step 3.2. If override is a string, set the value override for command to override.
377 BoolOrOptionalString::OptionalString(optional_string) => {
378 context_object.set_value_override(override_state.command, optional_string)
379 },
380 }
381 }
382 }
383 }
384}