Skip to main content

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;
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(ShadowIncluding::No)
78            .find(|child| child.is_formattable(no_gc) && 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(cx.no_gc(), 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(cx.no_gc(), 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, cx.no_gc()) {
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(cx.no_gc(), 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(cx.no_gc()) 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 =
276            self.first_formattable_contained_node(cx.no_gc());
277        for override_state in overrides {
278            // Step 2. If node is not null, then for each (command, override) pair in overrides, in order:
279            if let Some(ref node) = first_formattable_contained_node {
280                match override_state.value {
281                    // Step 2.1. If override is a boolean, and queryCommandState(command)
282                    // returns something different from override, take the action for command,
283                    // with value equal to the empty string.
284                    BoolOrOptionalString::Bool(bool_)
285                        if override_state
286                            .command
287                            .current_state(cx, context_object)
288                            .is_some_and(|value| value != bool_) =>
289                    {
290                        override_state
291                            .command
292                            .execute(cx, context_object, selection, "".into());
293                    },
294                    BoolOrOptionalString::OptionalString(optional_string) => {
295                        match override_state.command {
296                            // Step 2.3. Otherwise, if override is a string; and command is "createLink";
297                            // and either there is a value override for "createLink" that is not equal to override,
298                            // or there is no value override for "createLink" and node's effective command value
299                            // for "createLink" is not equal to override: take the action for "createLink", with value equal to override.
300                            CommandName::CreateLink => {
301                                let value_override =
302                                    context_object.value_override(&CommandName::CreateLink);
303                                if value_override != optional_string {
304                                    CommandName::CreateLink.execute(
305                                        cx,
306                                        context_object,
307                                        selection,
308                                        optional_string.unwrap_or_default(),
309                                    );
310                                }
311                            },
312                            // Step 2.4. Otherwise, if override is a string; and command is "fontSize";
313                            // and either there is a value override for "fontSize" that is not equal to override,
314                            // or there is no value override for "fontSize" and node's effective command value for "fontSize"
315                            // is not loosely equivalent to override:
316                            CommandName::FontSize => {
317                                let value_override =
318                                    context_object.value_override(&CommandName::FontSize);
319                                if value_override != optional_string ||
320                                    (value_override.is_none() &&
321                                        !CommandName::FontSize.are_loosely_equivalent_values(
322                                            node.effective_command_value(&CommandName::FontSize)
323                                                .as_ref(),
324                                            optional_string.as_ref(),
325                                        ))
326                                {
327                                    // Step 2.5. Convert override to an integer number of pixels,
328                                    // and set override to the legacy font size for the result.
329                                    let pixels = optional_string
330                                        .and_then(|value| value.parse::<i32>().ok())
331                                        .map(|value| {
332                                            legacy_font_size_for(value as f32, context_object)
333                                        })
334                                        .unwrap_or("7".into());
335                                    // Step 2.6. Take the action for "fontSize", with value equal to override.
336                                    CommandName::FontSize.execute(
337                                        cx,
338                                        context_object,
339                                        selection,
340                                        pixels,
341                                    );
342                                }
343                            },
344                            // Step 2.2. Otherwise, if override is a string, and command is neither "createLink" nor "fontSize",
345                            // and queryCommandValue(command) returns something not equivalent to override,
346                            // take the action for command, with value equal to override.
347                            command
348                                if command.current_value(cx, context_object) != optional_string =>
349                            {
350                                command.execute(
351                                    cx,
352                                    context_object,
353                                    selection,
354                                    optional_string.unwrap_or_default(),
355                                );
356                            },
357                            // Step 2.5. Otherwise, continue this loop from the beginning.
358                            _ => {
359                                continue;
360                            },
361                        }
362                    },
363                    // Step 2.5. Otherwise, continue this loop from the beginning.
364                    _ => {
365                        continue;
366                    },
367                }
368                // Step 2.6. Set node to the first formattable node effectively contained in the active range, if there is one.
369                first_formattable_contained_node =
370                    self.first_formattable_contained_node(cx.no_gc());
371            } else {
372                // Step 3. Otherwise, for each (command, override) pair in overrides, in order:
373                // Step 3.1. If override is a boolean, set the state override for command to override.
374                match override_state.value {
375                    BoolOrOptionalString::Bool(bool_) => {
376                        context_object.set_state_override(override_state.command, Some(bool_))
377                    },
378                    // Step 3.2. If override is a string, set the value override for command to override.
379                    BoolOrOptionalString::OptionalString(optional_string) => {
380                        context_object.set_value_override(override_state.command, optional_string)
381                    },
382                }
383            }
384        }
385    }
386}