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