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::NodeTraits;
12use crate::dom::bindings::root::DomRoot;
13use crate::dom::document::Document;
14use crate::dom::element::{AdjacentPosition, Element};
15use crate::dom::execcommand::contenteditable::node::{
16    NodeOrString, is_allowed_child, move_preserving_ranges, wrap_node_list,
17};
18use crate::dom::html::htmllielement::HTMLLIElement;
19use crate::dom::html::htmlolistelement::HTMLOListElement;
20use crate::dom::html::htmlulistelement::HTMLUListElement;
21use crate::dom::node::Node;
22use crate::dom::selection::Selection;
23use crate::dom::text::Text;
24
25// <https://w3c.github.io/editing/docs/execCommand/#indent>
26pub(crate) fn indent(cx: &mut JSContext, document: &Document, node_list: Vec<DomRoot<Node>>) {
27    // Step 1. If node list is empty, do nothing and abort these steps.
28    if node_list.is_empty() {
29        return;
30    }
31
32    // Step 2. Let first node be the first member of node list.
33    let first_node = node_list
34        .first()
35        .expect("Must have a first node by now.")
36        .clone();
37
38    // Step 3. If first node's parent is an ol or ul:
39    if let Some(parent) = first_node.GetParentElement() &&
40        (parent.is::<HTMLOListElement>() || parent.is::<HTMLUListElement>())
41    {
42        // Step 3.1. Let tag be the local name of the parent of first node.
43        let tag = parent.local_name();
44
45        // Step 3.2. Wrap node list,
46        //           with sibling criteria returning true for an HTML element with local name tag and false otherwise,
47        //           and new parent instructions returning the result of calling createElement(tag) on the ownerDocument of first node.
48        wrap_node_list(
49            cx,
50            node_list,
51            |sibling| {
52                sibling
53                    .downcast::<Element>()
54                    .is_some_and(|sibling| sibling.local_name() == tag)
55            },
56            |cx| {
57                Some(DomRoot::upcast(
58                    first_node.owner_doc().create_element(cx, tag),
59                ))
60            },
61        );
62
63        // Step 3.3. Abort these steps.
64        return;
65    }
66
67    // Step 4. Wrap node list,
68    //         with sibling criteria returning true for a simple indentation element and false otherwise,
69    //         and new parent instructions returning the result of calling createElement("blockquote") on the ownerDocument of first node.
70    //         Let new parent be the result.
71    let new_parent = wrap_node_list(
72        cx,
73        node_list,
74        |sibling| {
75            sibling
76                .downcast::<Element>()
77                .is_some_and(|sibling| sibling.is_simple_indentation_element())
78        },
79        |cx| {
80            Some(DomRoot::upcast(
81                first_node.owner_doc().create_element(cx, "blockquote"),
82            ))
83        },
84    );
85
86    // Step 5. Fix disallowed ancestors of new parent.
87    if let Some(new_parent) = new_parent {
88        new_parent.fix_disallowed_ancestors(cx, document);
89    }
90}
91
92/// <https://w3c.github.io/editing/docs/execCommand/#normalize-sublists>
93pub(crate) fn normalize_sublists(cx: &mut JSContext, item: DomRoot<Node>) {
94    let item_element = item
95        .downcast::<Element>()
96        .expect("item should be an element");
97
98    // Step 1. If item is not an li or it is not editable or its parent is not editable, abort these steps.
99    if !item.is::<HTMLLIElement>() ||
100        !item.is_editable() ||
101        !item
102            .GetParentElement()
103            .is_some_and(|parent| parent.upcast::<Node>().is_editable())
104    {
105        return;
106    }
107
108    // Step 2. Let new item be null.
109    let mut new_item: Option<DomRoot<Element>> = None;
110
111    // Step 3. While item has an ol or ul child:
112    while item
113        .child_elements()
114        .any(|child| child.is::<HTMLOListElement>() || child.is::<HTMLUListElement>())
115    {
116        // Step 3.1. Let child be the last child of item.
117        let child = item.GetLastChild().expect("Must have a last child here.");
118
119        // Step 3.2. If child is an ol or ul, or new item is null and child is a Text node whose data consists of zero of more space characters:
120        if child.is::<HTMLOListElement>() ||
121            child.is::<HTMLUListElement>() ||
122            (new_item.is_none() &&
123                child
124                    .downcast::<Text>()
125                    .is_some_and(|text| text.data().bytes().all(|byte| byte == b' ')))
126        {
127            // Step 3.2.1. Set new item to null.
128            new_item = None;
129
130            // Step 3.2.2. Insert child into the parent of item immediately following item, preserving ranges.
131            move_preserving_ranges(cx, &child, |cx| {
132                item_element
133                    .insert_adjacent(cx, AdjacentPosition::AfterEnd, &child)
134                    .map(|elem| elem.expect("Should have inserted"))
135            });
136
137            continue;
138        }
139        // Step 3.3. Otherwise:
140        // Step 3.3.1. If new item is null,
141        //             let new item be the result of calling createElement("li") on the ownerDocument of item,
142        //             then insert new item into the parent of item immediately after item.
143        if new_item.is_none() {
144            new_item = Some(item.owner_document().create_element(cx, "li"));
145            item_element
146                .insert_adjacent(cx, AdjacentPosition::AfterEnd, &child)
147                .expect("Insertion should always work here.");
148        }
149
150        // Step 3.3.2. Insert child into new item as its first child, preserving ranges.
151        move_preserving_ranges(cx, &child, |cx| {
152            new_item
153                .as_ref()
154                .expect("Must have new item here")
155                .downcast::<Element>()
156                .expect("New item must be able to support children")
157                .insert_adjacent(cx, AdjacentPosition::AfterBegin, &child)
158                .map(|elem| elem.expect("Should have inserted"))
159        });
160    }
161}
162
163/// <https://w3c.github.io/editing/docs/execCommand/#the-indent-command>
164pub(crate) fn execute_indent_command(
165    cx: &mut JSContext,
166    document: &Document,
167    selection: &Selection,
168) -> bool {
169    let mut active_range = selection
170        .active_range(cx)
171        .expect("Must always have an active range.");
172    // Step 1. Let items be a list of all lis that are inclusive ancestors of the active range's start and/or end node.
173    let items: HashSet<DomRoot<Node>> = active_range
174        .start_container()
175        .ancestors()
176        .chain(active_range.end_container().ancestors())
177        .filter(|ancestor| ancestor.is::<HTMLLIElement>())
178        .collect();
179
180    // Step 2. For each item in items, normalize sublists of item.
181    for item in items {
182        normalize_sublists(cx, item);
183    }
184
185    // Normalizing sublists probably messes up the range
186    active_range = selection
187        .active_range(cx)
188        .expect("Must always have an active range.");
189
190    // Step 3. Block-extend the active range, and let new range be the result.
191    let new_range = active_range.block_extend(cx, document);
192
193    // Step 4. Let node list be a list of nodes, initially empty.
194    let mut node_list: Vec<DomRoot<Node>> = vec![];
195
196    // Step 5. For each node node contained in new range,
197    //         if node is editable and is an allowed child of "div" or "ol"
198    //         and if the last member of node list (if any) is not an ancestor of node,
199    //         append node to node list.
200    for node in new_range.contained_nodes(cx.no_gc()) {
201        if node.is_editable() &&
202            (is_allowed_child(
203                NodeOrString::Node(node.clone()),
204                NodeOrString::String("div".to_owned()),
205            ) || is_allowed_child(
206                NodeOrString::Node(node.clone()),
207                NodeOrString::String("ol".to_owned()),
208            )) &&
209            node_list
210                .last()
211                .is_none_or(|last| !last.is_ancestor_of(&node))
212        {
213            node_list.push(node.as_rooted());
214        }
215    }
216
217    // Step 6. If the first visible member of node list is an li whose parent is an ol or ul:
218    if let Some(first_visible_member) = node_list.iter().find(|node| node.is_visible(cx.no_gc())) &&
219        first_visible_member.is::<HTMLLIElement>() &&
220        let Some(parent) = first_visible_member.GetParentNode() &&
221        (parent.is::<HTMLOListElement>() || parent.is::<HTMLUListElement>())
222    {
223        // Step 6.1. Let sibling be node list's first visible member's previousSibling.
224        let mut sibling = first_visible_member.GetPreviousSibling();
225
226        // Step 6.2. While sibling is invisible, set sibling to its previousSibling.
227        while let Some(ref some_sibling) = sibling &&
228            some_sibling.is_invisible(cx.no_gc())
229        {
230            sibling = some_sibling.GetPreviousSibling();
231        }
232
233        // Step 6.3. If sibling is an li, normalize sublists of sibling.
234        if let Some(sibling) = sibling &&
235            sibling.is::<HTMLLIElement>()
236        {
237            normalize_sublists(cx, sibling);
238        }
239    }
240
241    // Step 7. While node list is not empty:
242    let mut node_list_iter = node_list.iter().peekable();
243    while node_list_iter.peek().is_some() {
244        // Step 7.1. Let sublist be a list of nodes, initially empty.
245        let mut sublist: Vec<DomRoot<Node>> = vec![];
246
247        // Step 7.2. Remove the first member of node list and append it to sublist.
248        sublist.push(
249            node_list_iter
250                .next()
251                .expect("Must always have a next item")
252                .clone(),
253        );
254
255        // Step 7.3. While the first member of node list is the nextSibling of the last member of
256        //           sublist, remove the first member of node list and append it to sublist.
257        while node_list_iter.peek().is_some_and(|node| {
258            sublist
259                .last()
260                .expect("Must always have last element here.")
261                .GetNextSibling()
262                .is_some_and(|next_sibling| &&next_sibling == node)
263        }) {
264            sublist.push(
265                node_list_iter
266                    .next()
267                    .expect("Must always have a next item")
268                    .clone(),
269            );
270        }
271
272        // Step 7.4. Indent sublist.
273        indent(cx, document, sublist);
274    }
275
276    // Step 8. Return true.
277    // Note: This isn't in the spec (yet), see https://github.com/w3c/editing/pull/547
278    true
279}