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}