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