Skip to main content

script/dom/execcommand/commands/
indent.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 js::context::JSContext;
8use script_bindings::codegen::GenericBindings::NodeBinding::NodeMethods;
9use script_bindings::inheritance::Castable;
10
11use crate::dom::bindings::root::DomRoot;
12use crate::dom::document::Document;
13use crate::dom::element::Element;
14use crate::dom::execcommand::contenteditable::node::{
15    NodeOrString, is_allowed_child, wrap_node_list,
16};
17use crate::dom::html::htmllielement::HTMLLIElement;
18use crate::dom::html::htmlolistelement::HTMLOListElement;
19use crate::dom::html::htmlulistelement::HTMLUListElement;
20use crate::dom::node::Node;
21use crate::dom::selection::Selection;
22
23/// <https://w3c.github.io/editing/docs/execCommand/#indent>
24pub(crate) fn indent(cx: &mut JSContext, context_object: &Document, node_list: Vec<DomRoot<Node>>) {
25    // Step 1. If node list is empty, do nothing and abort these steps.
26    if node_list.is_empty() {
27        return;
28    }
29
30    // Step 2. Let first node be the first member of node list.
31    let first_node = node_list
32        .first()
33        .expect("Must have a first node by now.")
34        .clone();
35
36    // Step 3. If first node's parent is an ol or ul:
37    if let Some(parent) = first_node.GetParentElement() &&
38        (parent.is::<HTMLOListElement>() || parent.is::<HTMLUListElement>())
39    {
40        // Step 3.1. Let tag be the local name of the parent of first node.
41        let tag = parent.local_name();
42
43        // Step 3.2. Wrap node list,
44        //           with sibling criteria returning true for an HTML element with local name tag and false otherwise,
45        //           and new parent instructions returning the result of calling createElement(tag) on the ownerDocument of first node.
46        wrap_node_list(
47            cx,
48            node_list,
49            |sibling| {
50                sibling
51                    .downcast::<Element>()
52                    .is_some_and(|sibling| sibling.local_name() == tag)
53            },
54            |cx| {
55                Some(DomRoot::upcast(
56                    first_node.owner_doc().create_element(cx, tag),
57                ))
58            },
59        );
60
61        // Step 3.3. Abort these steps.
62        return;
63    }
64
65    // Step 4. Wrap node list,
66    //         with sibling criteria returning true for a simple indentation element and false otherwise,
67    //         and new parent instructions returning the result of calling createElement("blockquote") on the ownerDocument of first node.
68    //         Let new parent be the result.
69    let new_parent = wrap_node_list(
70        cx,
71        node_list,
72        |sibling| {
73            sibling
74                .downcast::<Element>()
75                .is_some_and(|sibling| sibling.is_simple_indentation_element())
76        },
77        |cx| {
78            Some(DomRoot::upcast(
79                first_node.owner_doc().create_element(cx, "blockquote"),
80            ))
81        },
82    );
83
84    // Step 5. Fix disallowed ancestors of new parent.
85    if let Some(new_parent) = new_parent {
86        new_parent.fix_disallowed_ancestors(cx, context_object);
87    }
88}
89
90/// <https://w3c.github.io/editing/docs/execCommand/#the-indent-command>
91pub(crate) fn execute_indent_command(
92    cx: &mut JSContext,
93    document: &Document,
94    selection: &Selection,
95) -> bool {
96    let mut active_range = selection
97        .active_range(cx)
98        .expect("Must always have an active range.");
99    // Step 1. Let items be a list of all lis that are inclusive ancestors of the active range's start and/or end node.
100    let items: HashSet<DomRoot<Node>> = active_range
101        .start_container()
102        .ancestors()
103        .chain(active_range.end_container().ancestors())
104        .filter(|ancestor| ancestor.is::<HTMLLIElement>())
105        .collect();
106
107    // Step 2. For each item in items, normalize sublists of item.
108    for item in items {
109        item.normalize_sublists(cx);
110    }
111
112    // Normalizing sublists probably messes up the range
113    active_range = selection
114        .active_range(cx)
115        .expect("Must always have an active range.");
116
117    // Step 3. Block-extend the active range, and let new range be the result.
118    let new_range = active_range.block_extend(cx, document);
119
120    // Step 4. Let node list be a list of nodes, initially empty.
121    let mut node_list: Vec<DomRoot<Node>> = vec![];
122
123    // Step 5. For each node node contained in new range,
124    //         if node is editable and is an allowed child of "div" or "ol"
125    //         and if the last member of node list (if any) is not an ancestor of node,
126    //         append node to node list.
127    for node in new_range.contained_nodes(cx.no_gc()) {
128        if node.is_editable() &&
129            (is_allowed_child(
130                NodeOrString::Node(node.clone()),
131                NodeOrString::String("div".to_owned()),
132            ) || is_allowed_child(
133                NodeOrString::Node(node.clone()),
134                NodeOrString::String("ol".to_owned()),
135            )) &&
136            node_list
137                .last()
138                .is_none_or(|last| !last.is_ancestor_of(&node))
139        {
140            node_list.push(node.as_rooted());
141        }
142    }
143
144    // Step 6. If the first visible member of node list is an li whose parent is an ol or ul:
145    if let Some(first_visible_member) = node_list.iter().find(|node| node.is_visible(cx.no_gc())) &&
146        first_visible_member.is::<HTMLLIElement>() &&
147        let Some(parent) = first_visible_member.GetParentNode() &&
148        (parent.is::<HTMLOListElement>() || parent.is::<HTMLUListElement>())
149    {
150        // Step 6.1. Let sibling be node list's first visible member's previousSibling.
151        let mut sibling = first_visible_member.GetPreviousSibling();
152
153        // Step 6.2. While sibling is invisible, set sibling to its previousSibling.
154        while let Some(ref some_sibling) = sibling &&
155            some_sibling.is_invisible(cx.no_gc())
156        {
157            sibling = some_sibling.GetPreviousSibling();
158        }
159
160        // Step 6.3. If sibling is an li, normalize sublists of sibling.
161        if let Some(sibling) = sibling &&
162            sibling.is::<HTMLLIElement>()
163        {
164            sibling.normalize_sublists(cx);
165        }
166    }
167
168    // Step 7. While node list is not empty:
169    let mut node_list_iter = node_list.iter().peekable();
170    while node_list_iter.peek().is_some() {
171        // Step 7.1. Let sublist be a list of nodes, initially empty.
172        let mut sublist: Vec<DomRoot<Node>> = vec![];
173
174        // Step 7.2. Remove the first member of node list and append it to sublist.
175        sublist.push(
176            node_list_iter
177                .next()
178                .expect("Must always have a next item")
179                .clone(),
180        );
181
182        // Step 7.3. While the first member of node list is the nextSibling of the last member of
183        //           sublist, remove the first member of node list and append it to sublist.
184        while node_list_iter.peek().is_some_and(|node| {
185            sublist
186                .last()
187                .expect("Must always have last element here.")
188                .GetNextSibling()
189                .is_some_and(|next_sibling| &&next_sibling == node)
190        }) {
191            sublist.push(
192                node_list_iter
193                    .next()
194                    .expect("Must always have a next item")
195                    .clone(),
196            );
197        }
198
199        // Step 7.4. Indent sublist.
200        indent(cx, document, sublist);
201    }
202
203    // Step 8. Return true.
204    // Note: This isn't in the spec (yet), see https://github.com/w3c/editing/pull/547
205    true
206}