Skip to main content

script/dom/execcommand/commands/
outdent.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 std::collections::HashSet;
6
7use html5ever::local_name;
8use js::context::JSContext;
9use script_bindings::codegen::GenericBindings::CSSStyleDeclarationBinding::CSSStyleDeclarationMethods;
10use script_bindings::codegen::GenericBindings::NodeBinding::NodeMethods;
11use script_bindings::inheritance::Castable;
12use script_bindings::str::DOMString;
13
14use crate::dom::bindings::codegen::Bindings::HTMLElementBinding::HTMLElementMethods;
15use crate::dom::bindings::root::{DomRoot, DomSlice};
16use crate::dom::document::Document;
17use crate::dom::element::Element;
18use crate::dom::execcommand::commands::indent::indent;
19use crate::dom::execcommand::contenteditable::node::{
20    record_the_values, restore_the_values, split_the_parent,
21};
22use crate::dom::html::htmlbrelement::HTMLBRElement;
23use crate::dom::html::htmlelement::HTMLElement;
24use crate::dom::html::htmllielement::HTMLLIElement;
25use crate::dom::html::htmlolistelement::HTMLOListElement;
26use crate::dom::html::htmlulistelement::HTMLUListElement;
27use crate::dom::iterators::ShadowIncluding;
28use crate::dom::node::Node;
29use crate::dom::selection::Selection;
30
31/// <https://w3c.github.io/editing/docs/execCommand/#outdent>
32pub(crate) fn outdent(cx: &mut JSContext, context_object: &Document, node: &Node) {
33    // Step 1. If node is not editable, abort these steps.
34    if !node.is_editable() {
35        return;
36    }
37
38    // Step 2. If node is a simple indentation element, remove node, preserving its descendants.
39    //         Then abort these steps.
40    if node
41        .downcast::<Element>()
42        .is_some_and(|element| element.is_simple_indentation_element())
43    {
44        node.remove_preserving_its_descendants(cx);
45        return;
46    }
47
48    // Step 3. If node is an indentation element:
49    if let Some(node_as_element) = node.downcast::<Element>() &&
50        node_as_element.is_indentation_element()
51    {
52        // Step 3.1. Unset the dir attribute of node, if any.
53        node_as_element.remove_attribute_by_name(cx, &local_name!("dir"));
54
55        // Step 3.2. Unset the margin, padding, and border CSS properties of node.
56        if let Some(node_as_html_element) = node_as_element.downcast::<HTMLElement>() {
57            let _ = node_as_html_element
58                .Style(cx)
59                .RemoveProperty(cx, DOMString::from_static("margin"));
60            let _ = node_as_html_element
61                .Style(cx)
62                .RemoveProperty(cx, DOMString::from_static("padding"));
63            let _ = node_as_html_element
64                .Style(cx)
65                .RemoveProperty(cx, DOMString::from_static("border"));
66        }
67
68        // Step 3.3. Set the tag name of node to "div".
69        node_as_element.set_the_tag_name(cx, "div");
70
71        // Step 3.4. Abort these steps.
72        return;
73    }
74
75    // Step 4. Let current ancestor be node's parent.
76    let mut current_ancestor = node.GetParentNode();
77
78    // Step 5. Let ancestor list be a list of nodes, initially empty.
79    let mut ancestor_list: Vec<DomRoot<Node>> = vec![];
80
81    // Step 6. While current ancestor is an editable Element that is neither a simple indentation
82    //         element nor an ol nor a ul, append current ancestor to ancestor list and then set
83    //         current ancestor to its parent.
84    while let Some(ancestor) = current_ancestor.clone() &&
85        ancestor.is_editable() &&
86        ancestor
87            .downcast::<Element>()
88            .is_some_and(|element| !element.is_simple_indentation_element()) &&
89        !ancestor.is::<HTMLOListElement>() &&
90        !ancestor.is::<HTMLUListElement>()
91    {
92        current_ancestor = ancestor.GetParentNode();
93        ancestor_list.push(ancestor);
94    }
95
96    // Step 7. If current ancestor is not an editable simple indentation element:
97    if !current_ancestor.as_ref().is_some_and(|ancestor| {
98        ancestor.is_editable() &&
99            ancestor
100                .downcast::<Element>()
101                .is_some_and(|element| element.is_simple_indentation_element())
102    }) {
103        // Step 7.1. Let current ancestor be node's parent.
104        current_ancestor = node.GetParentNode();
105
106        // Step 7.2. Let ancestor list be the empty list.
107        ancestor_list.clear();
108
109        // Step 7.3. While current ancestor is an editable Element that is neither an indentation
110        //           element nor an ol nor a ul, append current ancestor to ancestor list and then
111        //           set current ancestor to its parent.
112        while let Some(ancestor) = current_ancestor.clone() &&
113            ancestor.is_editable() &&
114            ancestor
115                .downcast::<Element>()
116                .is_some_and(|element| !element.is_indentation_element()) &&
117            !ancestor.is::<HTMLOListElement>() &&
118            !ancestor.is::<HTMLUListElement>()
119        {
120            current_ancestor = ancestor.GetParentNode();
121            ancestor_list.push(ancestor);
122        }
123    }
124
125    // Step 8. If node is an ol or ul and current ancestor is not an editable indentation element:
126    if let Some(node_as_element) = node.downcast::<Element>() &&
127        (node.is::<HTMLOListElement>() || node.is::<HTMLUListElement>()) &&
128        !current_ancestor.as_ref().is_some_and(|ancestor| {
129            ancestor.is_editable() &&
130                ancestor
131                    .downcast::<Element>()
132                    .is_some_and(|element| element.is_indentation_element())
133        })
134    {
135        // Step 8.1. Unset the reversed, start, and type attributes of node, if any are set.
136        node_as_element.remove_attribute_by_name(cx, &local_name!("reversed"));
137        node_as_element.remove_attribute_by_name(cx, &local_name!("start"));
138        node_as_element.remove_attribute_by_name(cx, &local_name!("type"));
139
140        // Step 8.2. Let children be the children of node.
141        let children = node.children();
142
143        // Step 8.3. If node has attributes, and its parent is not an ol or ul, set the tag name of node to "div".
144        if !node_as_element.attrs().borrow().is_empty() &&
145            !node.GetParentElement().is_some_and(|parent| {
146                parent.is::<HTMLOListElement>() || parent.is::<HTMLUListElement>()
147            })
148        {
149            node_as_element.set_the_tag_name(cx, "div");
150        }
151        // Step 8.4. Otherwise:
152        else {
153            // Step 8.4.1. Record the values of node's children, and let values be the result.
154            let values = record_the_values(node.children().collect());
155
156            // Step 8.4.2. Remove node, preserving its descendants.
157            node.remove_preserving_its_descendants(cx);
158
159            // Step 8.4.3. Restore the values from values.
160            restore_the_values(cx, values);
161        }
162
163        // Step 8.5. Fix disallowed ancestors of each member of children.
164        for child in children {
165            child.fix_disallowed_ancestors(cx, context_object);
166        }
167
168        // Step 8.6. Abort these steps.
169        return;
170    }
171
172    // Step 9. If current ancestor is not an editable indentation element, abort these steps.
173    if !current_ancestor.as_ref().is_some_and(|ancestor| {
174        ancestor.is_editable() &&
175            ancestor
176                .downcast::<Element>()
177                .is_some_and(|element| element.is_indentation_element())
178    }) {
179        return;
180    }
181
182    // Step 10. Append current ancestor to ancestor list.
183    ancestor_list.push(
184        current_ancestor
185            .clone()
186            .expect("Should have an ancestor here."),
187    );
188
189    // Step 11. Let original ancestor be current ancestor.
190    let original_ancestor = current_ancestor;
191
192    // Step 12. While ancestor list is not empty:
193    while !ancestor_list.is_empty() {
194        // Step 12.1. Let current ancestor be the last member of ancestor list.
195        // Step 12.2. Remove the last member from ancestor list.
196        let current_ancestor_ref = ancestor_list.pop();
197
198        // Step 12.3. Let target be the child of current ancestor that is equal to either node or
199        //            the last member of ancestor list.
200        let target = current_ancestor_ref
201            .expect("Must always have an ancestor here.")
202            .children()
203            .find(|child| **child == *node || Some(child) == ancestor_list.last())
204            .expect("Must be able to find a target here.");
205
206        // Step 12.4. If target is an inline node that is not a br, and its nextSibling is a br,
207        //            remove target's nextSibling from its parent.
208        if target.is_inline_node() &&
209            !target.is::<HTMLBRElement>() &&
210            let Some(next_br_sibling) = target.GetNextSibling() &&
211            next_br_sibling.is::<HTMLBRElement>()
212        {
213            next_br_sibling.remove_self(cx);
214        }
215
216        // Step 12.5. Let preceding siblings be the precedings siblings of target, and let
217        //            following siblings be the followings siblings of target.
218        let preceding_siblings = target.preceding_siblings().collect();
219        let following_siblings = target.following_siblings().collect();
220
221        // Step 12.6. Indent preceding siblings.
222        indent(cx, context_object, preceding_siblings);
223
224        // Step 12.7. Indent following siblings.
225        indent(cx, context_object, following_siblings);
226    }
227
228    // Step 13. Outdent original ancestor.
229    outdent(
230        cx,
231        context_object,
232        &original_ancestor.expect("Should have an ancestor here."),
233    );
234}
235
236/// <https://w3c.github.io/editing/docs/execCommand/#the-outdent-command>
237pub(crate) fn execute_outdent_command(
238    cx: &mut JSContext,
239    document: &Document,
240    selection: &Selection,
241) -> bool {
242    let mut active_range = selection
243        .active_range(cx)
244        .expect("Must always have an active range.");
245    // Step 1. Let items be a list of all lis that are inclusive ancestors of the active range's start and/or end node.
246    let items: HashSet<DomRoot<Node>> = active_range
247        .start_container()
248        .ancestors()
249        .chain(active_range.end_container().ancestors())
250        .filter(|ancestor| ancestor.is::<HTMLLIElement>())
251        .collect();
252
253    // Step 2. For each item in items, normalize sublists of item.
254    for item in items {
255        item.normalize_sublists(cx);
256    }
257
258    // Normalizing sublists probably messes up the range
259    active_range = selection
260        .active_range(cx)
261        .expect("Must always have an active range.");
262
263    // Step 3. Block-extend the active range, and let new range be the result.
264    let new_range = active_range.block_extend(cx, document);
265
266    // Step 4. Let node list be a list of nodes, initially empty.
267    rooted_vec!(let mut node_list);
268
269    // Step 5. For each node node contained in new range,
270    //         append node to node list if the last member of node list (if any)
271    //         is not an ancestor of node; node is editable;
272    //         and either node has no editable descendants, or is an ol or ul,
273    //         or is an li whose parent is an ol or ul.
274    for node in new_range.contained_nodes(cx.no_gc()) {
275        // This type is only used to tell the compiler how to handle the type of `node_list.last()`.
276        // It is not allowed to add a `& DomRoot<Node>` annotation, as test-tidy disallows that.
277        // However, if we omit the type, the compiler doesn't know what it is, since we also
278        // aren't allowed to add a type annotation to `node_list` itself, as that is handled
279        // by the `rooted_vec` macro. Lastly, we also can't make it `&Node`, since then the compiler
280        // thinks that the contents of the `RootedVec` is `Node`, whereas it is should be
281        // `RootedVec<DomRoot<Node>>`. The type alias here doesn't upset test-tidy,
282        // while also providing the necessary information to the compiler to work.
283        type DomRootNode = DomRoot<Node>;
284        if !node_list
285            .last()
286            .is_some_and(|last: &DomRootNode| last.is_ancestor_of(&node)) &&
287            node.is_editable() &&
288            (!node
289                .traverse_preorder(ShadowIncluding::No)
290                .any(|node| node.is_editable()) ||
291                node.is::<HTMLOListElement>() ||
292                node.is::<HTMLUListElement>() ||
293                (node.is::<HTMLLIElement>() &&
294                    node.GetParentElement().is_some_and(|parent| {
295                        parent.is::<HTMLOListElement>() || parent.is::<HTMLUListElement>()
296                    })))
297        {
298            node_list.push(node.as_rooted());
299        }
300    }
301
302    // Step 6. While node list is not empty:
303    let mut node_list_iter = node_list.iter().peekable();
304    while let Some(first_node) = node_list_iter.next() {
305        // Step 6.1. While the first member of node list is an ol or ul or is not the child of an
306        //           ol or ul, outdent it and remove it from node list.
307        // Note: This loop is combined with the outer one here so as to not require modifying the
308        //       node list.
309        if (first_node.is::<HTMLOListElement>() || first_node.is::<HTMLUListElement>()) ||
310            !first_node.GetParentElement().is_some_and(|parent| {
311                parent.is::<HTMLOListElement>() || parent.is::<HTMLUListElement>()
312            })
313        {
314            outdent(cx, document, first_node);
315            // We just walk the iterator instead of continuously removing things from node list.
316            continue;
317        }
318
319        // Step 6.2. If node list is empty, break from these substeps.
320        // The fact that we are here means that node list is not empty, as we still have a
321        // first_node.
322
323        // Step 6.3. Let sublist be a list of nodes, initially empty.
324        rooted_vec!(let mut sublist);
325
326        // Step 6.4. Remove the first member of node list and append it to sublist.
327        sublist.push(first_node.as_traced());
328
329        // Step 6.5. While the first member of node list is the nextSibling of the last member of
330        //           sublist, and the first member of node list is not an ol or ul, remove the
331        //           first member of node list and append it to sublist.
332        while let Some(node) = node_list_iter.next_if(|node| {
333            sublist
334                .last()
335                .expect("Must always have last element here.")
336                .GetNextSibling()
337                .is_some_and(|next_sibling| next_sibling == **node) &&
338                !node.is::<HTMLOListElement>() &&
339                !node.is::<HTMLUListElement>()
340        }) {
341            sublist.push(node.as_traced());
342        }
343
344        // Step 6.6. Record the values of sublist, and let values be the result.
345        let values = record_the_values(sublist.iter().map(|node| node.as_rooted()).collect());
346
347        // Step 6.7. Split the parent of sublist.
348        split_the_parent(cx, sublist.r());
349
350        // Step 6.8. Fix disallowed ancestors of each member of sublist.
351        for node in sublist.iter() {
352            node.fix_disallowed_ancestors(cx, document);
353        }
354
355        // Step 6.9. Restore the values from values.
356        restore_the_values(cx, values);
357    }
358
359    // Step 7. Return true.
360    true
361}