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;
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}