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::RangeBinding::RangeMethods;
9use crate::dom::bindings::root::DomRoot;
10use crate::dom::bindings::str::DOMString;
11use crate::dom::document::Document;
12use crate::dom::execcommand::basecommand::CommandName;
13use crate::dom::execcommand::commands::fontsize::legacy_font_size_for;
14use crate::dom::node::{Node, ShadowIncluding};
15use crate::dom::range::Range;
16use crate::dom::selection::Selection;
17use crate::dom::text::Text;
18
19enum BoolOrOptionalString {
20 Bool(bool),
21 OptionalString(Option<DOMString>),
22}
23
24impl From<Option<DOMString>> for BoolOrOptionalString {
25 fn from(optional_string: Option<DOMString>) -> Self {
26 Self::OptionalString(optional_string)
27 }
28}
29
30impl From<bool> for BoolOrOptionalString {
31 fn from(bool_: bool) -> Self {
32 Self::Bool(bool_)
33 }
34}
35
36pub(crate) struct RecordedStateOfNode {
37 command: CommandName,
38 value: BoolOrOptionalString,
39}
40
41impl RecordedStateOfNode {
42 fn for_command_node(command: CommandName, node: &Node) -> Self {
43 let value = node.effective_command_value(&command).into();
44 Self { command, value }
45 }
46
47 fn for_command_node_with_inline_activated_values(command: CommandName, node: &Node) -> Self {
48 let effective_command_value = node.effective_command_value(&command);
49 let value = effective_command_value
50 .is_some_and(|effective_command_value| {
51 command
52 .inline_command_activated_values()
53 .contains(&effective_command_value.str().as_ref())
54 })
55 .into();
56 Self { command, value }
57 }
58}
59
60impl Range {
61 /// <https://w3c.github.io/editing/docs/execCommand/#effectively-contained>
62 fn is_effectively_contained_node(&self, node: &Node) -> bool {
63 // > A node node is effectively contained in a range range if range is not collapsed,
64 if self.collapsed() {
65 return false;
66 }
67 // > and at least one of the following holds:
68 // > node is range's start node, it is a Text node, and its length is different from range's start offset.
69 let start_container = self.start_container();
70 if *start_container == *node && node.is::<Text>() && node.len() != self.start_offset() {
71 return true;
72 }
73 // > node is range's end node, it is a Text node, and range's end offset is not 0.
74 let end_container = self.end_container();
75 if *end_container == *node && node.is::<Text>() && self.end_offset() != 0 {
76 return true;
77 }
78 // > node is contained in range.
79 if self.contains(node) {
80 return true;
81 }
82 // > node has at least one child; and all its children are effectively contained in range;
83 node.children_count() > 0 && node.children().all(|child| self.is_effectively_contained_node(&child))
84 // > 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;
85 && (!node.is_ancestor_of(&start_container) || !start_container.is::<Text>() || self.start_offset() == 0)
86 // > 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.
87 && (!node.is_ancestor_of(&end_container) || !end_container.is::<Text>() || self.end_offset() == end_container.len())
88 }
89
90 pub(crate) fn first_formattable_contained_node(&self) -> Option<DomRoot<Node>> {
91 if self.collapsed() {
92 return None;
93 }
94
95 self.CommonAncestorContainer()
96 .traverse_preorder(ShadowIncluding::No)
97 .find(|child| child.is_formattable() && self.is_effectively_contained_node(child))
98 }
99
100 pub(crate) fn for_each_effectively_contained_child<Callback: FnMut(&Node)>(
101 &self,
102 mut callback: Callback,
103 ) {
104 if self.collapsed() {
105 return;
106 }
107
108 // Make sure to keep track of the tree nodes before, since `callback` might modify
109 // the underyling tree and then the iterator would prematurely stop.
110 let children = self
111 .CommonAncestorContainer()
112 .traverse_preorder(ShadowIncluding::No)
113 .collect::<Vec<DomRoot<Node>>>();
114
115 for child in children {
116 if self.is_effectively_contained_node(&child) {
117 callback(&child);
118 }
119 }
120 }
121
122 /// <https://w3c.github.io/editing/docs/execCommand/#record-current-states-and-values>
123 pub(crate) fn record_current_states_and_values(&self) -> Vec<RecordedStateOfNode> {
124 // Step 1. Let overrides be a list of (string, string or boolean) ordered pairs, initially empty.
125 //
126 // We return the vec in one go for the relevant values
127
128 // Step 2. Let node be the first formattable node effectively contained in the active range,
129 // or null if there is none.
130 let Some(node) = self.first_formattable_contained_node() else {
131 // Step 3. If node is null, return overrides.
132 return vec![];
133 };
134 // Step 8. Return overrides.
135 vec![
136 // Step 4. Add ("createLink", node's effective command value for "createLink") to overrides.
137 RecordedStateOfNode::for_command_node(CommandName::CreateLink, &node),
138 // Step 5. For each command in the list
139 // "bold", "italic", "strikethrough", "subscript", "superscript", "underline", in order:
140 // if node's effective command value for command is one of its inline command activated values,
141 // add (command, true) to overrides, and otherwise add (command, false) to overrides.
142 RecordedStateOfNode::for_command_node_with_inline_activated_values(
143 CommandName::Bold,
144 &node,
145 ),
146 RecordedStateOfNode::for_command_node_with_inline_activated_values(
147 CommandName::Italic,
148 &node,
149 ),
150 RecordedStateOfNode::for_command_node_with_inline_activated_values(
151 CommandName::Strikethrough,
152 &node,
153 ),
154 RecordedStateOfNode::for_command_node_with_inline_activated_values(
155 CommandName::Subscript,
156 &node,
157 ),
158 RecordedStateOfNode::for_command_node_with_inline_activated_values(
159 CommandName::Superscript,
160 &node,
161 ),
162 RecordedStateOfNode::for_command_node_with_inline_activated_values(
163 CommandName::Underline,
164 &node,
165 ),
166 // Step 6. For each command in the list "fontName", "foreColor", "hiliteColor", in order:
167 // add (command, command's value) to overrides.
168 // TODO
169
170 // Step 7. Add ("fontSize", node's effective command value for "fontSize") to overrides.
171 RecordedStateOfNode::for_command_node(CommandName::FontSize, &node),
172 ]
173 }
174
175 /// <https://w3c.github.io/editing/docs/execCommand/#restore-states-and-values>
176 pub(crate) fn restore_states_and_values(
177 &self,
178 cx: &mut JSContext,
179 selection: &Selection,
180 context_object: &Document,
181 overrides: Vec<RecordedStateOfNode>,
182 ) {
183 // Step 1. Let node be the first formattable node effectively contained in the active range,
184 // or null if there is none.
185 let mut first_formattable_contained_node = self.first_formattable_contained_node();
186 for override_state in overrides {
187 // Step 2. If node is not null, then for each (command, override) pair in overrides, in order:
188 if let Some(ref node) = first_formattable_contained_node {
189 match override_state.value {
190 // Step 2.1. If override is a boolean, and queryCommandState(command)
191 // returns something different from override, take the action for command,
192 // with value equal to the empty string.
193 BoolOrOptionalString::Bool(bool_)
194 if override_state
195 .command
196 .current_state(cx, context_object)
197 .is_some_and(|value| value != bool_) =>
198 {
199 override_state
200 .command
201 .execute(cx, context_object, selection, "".into());
202 },
203 BoolOrOptionalString::OptionalString(optional_string) => {
204 match override_state.command {
205 // Step 2.3. Otherwise, if override is a string; and command is "createLink";
206 // and either there is a value override for "createLink" that is not equal to override,
207 // or there is no value override for "createLink" and node's effective command value
208 // for "createLink" is not equal to override: take the action for "createLink", with value equal to override.
209 CommandName::CreateLink => {
210 let value_override =
211 context_object.value_override(&CommandName::CreateLink);
212 if value_override != optional_string {
213 CommandName::CreateLink.execute(
214 cx,
215 context_object,
216 selection,
217 optional_string.unwrap_or_default(),
218 );
219 }
220 },
221 // Step 2.4. Otherwise, if override is a string; and command is "fontSize";
222 // and either there is a value override for "fontSize" that is not equal to override,
223 // or there is no value override for "fontSize" and node's effective command value for "fontSize"
224 // is not loosely equivalent to override:
225 CommandName::FontSize => {
226 let value_override =
227 context_object.value_override(&CommandName::FontSize);
228 if value_override != optional_string ||
229 (value_override.is_none() &&
230 !CommandName::FontSize.are_loosely_equivalent_values(
231 node.effective_command_value(&CommandName::FontSize)
232 .as_ref(),
233 optional_string.as_ref(),
234 ))
235 {
236 // Step 2.5. Convert override to an integer number of pixels,
237 // and set override to the legacy font size for the result.
238 let pixels = optional_string
239 .and_then(|value| value.parse::<i32>().ok())
240 .map(|value| {
241 legacy_font_size_for(value as f32, context_object)
242 })
243 .unwrap_or("7".into());
244 // Step 2.6. Take the action for "fontSize", with value equal to override.
245 CommandName::FontSize.execute(
246 cx,
247 context_object,
248 selection,
249 pixels,
250 );
251 }
252 },
253 // Step 2.2. Otherwise, if override is a string, and command is neither "createLink" nor "fontSize",
254 // and queryCommandValue(command) returns something not equivalent to override,
255 // take the action for command, with value equal to override.
256 command
257 if command.current_value(cx, context_object) != optional_string =>
258 {
259 command.execute(
260 cx,
261 context_object,
262 selection,
263 optional_string.unwrap_or_default(),
264 );
265 },
266 // Step 2.5. Otherwise, continue this loop from the beginning.
267 _ => {
268 continue;
269 },
270 }
271 },
272 // Step 2.5. Otherwise, continue this loop from the beginning.
273 _ => {
274 continue;
275 },
276 }
277 // Step 2.6. Set node to the first formattable node effectively contained in the active range, if there is one.
278 first_formattable_contained_node = self.first_formattable_contained_node();
279 } else {
280 // Step 3. Otherwise, for each (command, override) pair in overrides, in order:
281 // Step 3.1. If override is a boolean, set the state override for command to override.
282 match override_state.value {
283 BoolOrOptionalString::Bool(bool_) => {
284 context_object.set_state_override(override_state.command, Some(bool_))
285 },
286 // Step 3.2. If override is a string, set the value override for command to override.
287 BoolOrOptionalString::OptionalString(optional_string) => {
288 context_object.set_value_override(override_state.command, optional_string)
289 },
290 }
291 }
292 }
293 }
294}