Skip to main content

script/dom/execcommand/commands/
insertparagraph.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 html5ever::local_name;
6use js::context::JSContext;
7use script_bindings::codegen::GenericBindings::SelectionBinding::SelectionMethods;
8use script_bindings::inheritance::Castable;
9
10use crate::dom::Node;
11use crate::dom::bindings::codegen::Bindings::DocumentBinding::DocumentMethods;
12use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods;
13use crate::dom::bindings::codegen::Bindings::RangeBinding::RangeMethods;
14use crate::dom::bindings::codegen::Bindings::TextBinding::TextMethods;
15use crate::dom::bindings::root::DomRoot;
16use crate::dom::comment::Comment;
17use crate::dom::document::Document;
18use crate::dom::element::Element;
19use crate::dom::execcommand::contenteditable::node::{
20    NodeOrString, is_allowed_child, node_matches_local_name, split_the_parent, wrap_node_list,
21};
22use crate::dom::html::htmlbrelement::HTMLBRElement;
23use crate::dom::iterators::ShadowIncluding;
24use crate::dom::selection::Selection;
25use crate::dom::text::Text;
26
27/// <https://w3c.github.io/editing/docs/execCommand/#the-insertparagraph-command>
28pub(crate) fn execute_insert_paragraph_command(
29    cx: &mut JSContext,
30    document: &Document,
31    selection: &Selection,
32) -> bool {
33    // Step 1. Delete the selection.
34    selection.delete_the_selection(
35        cx,
36        document,
37        Default::default(),
38        Default::default(),
39        Default::default(),
40    );
41    // Step 3. Let node and offset be the active range's start node and offset.
42    let (mut node, mut offset) = selection.start_boundary(cx);
43    // Step 2. If the active range's start node is neither editable
44    // nor an editing host, return true.
45    if !node.is_editable_or_editing_host() {
46        return true;
47    }
48    // Step 4. If node is a Text node, and offset is neither 0 nor the length of node,
49    // call splitText(offset) on node.
50    if offset != 0 &&
51        offset != node.len() &&
52        let Some(text_node) = node.downcast::<Text>() &&
53        text_node.SplitText(cx, offset).is_err()
54    {
55        unreachable!("Must always be able to split");
56    }
57    // Step 5. If node is a Text node and offset is its length,
58    // set offset to one plus the index of node, then set node to its parent.
59    if node.is::<Text>() && offset == node.len() {
60        offset = 1 + node.index();
61        node = node.GetParentNode().expect("Must always have a parent");
62    }
63    // Step 6. If node is a Text or Comment node, set offset to the index of node,
64    // then set node to its parent.
65    if node.is::<Text>() || node.is::<Comment>() {
66        offset = node.index();
67        node = node.GetParentNode().expect("Must always have a parent");
68    }
69    // Step 7. Call collapse(node, offset) on the context object's selection.
70    let _ = selection.Collapse(cx, Some(&node), offset);
71    // Step 8. Let container equal node.
72    let mut container = node.clone();
73    // Step 9. While container is not a single-line container,
74    // and container's parent is editable and in the same editing host as node,
75    // set container to its parent.
76    while !container.is_single_line_container() &&
77        let Some(parent) = container.GetParentNode() &&
78        parent.is_editable() &&
79        parent.same_editing_host(&node)
80    {
81        container = parent;
82    }
83    // Step 10. If container is an editable single-line container in the same editing host as node,
84    // and its local name is "p" or "div":
85    if container.is_editable() &&
86        container.is_single_line_container() &&
87        container.same_editing_host(&node) &&
88        node_matches_local_name!(container, local_name!("p") | local_name!("div"))
89    {
90        // Step 10.1. Let outer container equal container.
91        let mut outer_container = container.clone();
92        // Step 10.2. While outer container is not a dd or dt or li,
93        // and outer container's parent is editable, set outer container to its parent.
94        while !node_matches_local_name!(
95            outer_container,
96            local_name!("dd") | local_name!("dt") | local_name!("li")
97        ) && let Some(parent) = outer_container.GetParentNode() &&
98            parent.is_editable()
99        {
100            outer_container = parent;
101        }
102        // Step 10.3. If outer container is a dd or dt or li, set container to outer container.
103        if node_matches_local_name!(
104            outer_container,
105            local_name!("dd") | local_name!("dt") | local_name!("li")
106        ) {
107            container = outer_container;
108        }
109    }
110    // Step 11. If container is not editable or not in the same editing host as node or is not a single-line container:
111    if !container.is_editable() ||
112        !container.same_editing_host(&node) ||
113        !container.is_single_line_container()
114    {
115        // Step 11.1. Let tag be the default single-line container name.
116        let tag = document.default_single_line_container_name();
117        // Step 11.2. Block-extend the active range, and let new range be the result.
118        let new_range = selection.expect_active_range(cx).block_extend(cx, document);
119        // Step 11.4. Append to node list the first node in tree order that is contained in new range and is an allowed child of "p", if any.
120        let mut node_list = if let Some(eligible_node) = new_range
121            .contained_children()
122            .ok()
123            .and_then(|contained_children| {
124                contained_children
125                    .contained_children
126                    .into_iter()
127                    .find(|node| {
128                        is_allowed_child(
129                            NodeOrString::from_node(node, cx.no_gc()),
130                            NodeOrString::String("p".to_owned()),
131                        )
132                    })
133            }) {
134            vec![eligible_node]
135        } else {
136            // Step 11.3. Let node list be a list of nodes, initially empty.
137            // Step 11.5. If node list is empty:
138            // Step 11.5.1. If tag is not an allowed child of the active range's start node, return true.
139            if !is_allowed_child(
140                NodeOrString::String(tag.str().to_owned()),
141                NodeOrString::from_node(
142                    &selection.expect_active_range(cx).start_container(),
143                    cx.no_gc(),
144                ),
145            ) {
146                return true;
147            }
148            // Step 11.5.2. Set container to the result of calling createElement(tag) on the context object.
149            let container = document.create_element(cx, tag.str());
150            let container = container.upcast::<Node>();
151            // Step 11.5.3. Call insertNode(container) on the active range.
152            if selection
153                .expect_active_range(cx)
154                .InsertNode(cx, container)
155                .is_err()
156            {
157                unreachable!("Must always be able to insert");
158            }
159            // Step 11.5.4. Call createElement("br") on the context object,
160            // and append the result as the last child of container.
161            let br = document.create_element(cx, "br");
162            if container.AppendChild(cx, br.upcast()).is_err() {
163                unreachable!("Must always be able to append");
164            }
165            // Step 11.5.5. Call collapse(container, 0) on the context object's selection.
166            let _ = selection.Collapse(cx, Some(container), 0);
167            // Step 11.5.6. Return true.
168            return true;
169        };
170        // Step 11.6. While the nextSibling of the last member of node list is not null
171        // and is an allowed child of "p", append it to node list.
172        while let Some(next_of_last) = node_list
173            .iter()
174            .last()
175            .and_then(|node| node.GetNextSibling())
176            .filter(|next_of_last| {
177                is_allowed_child(
178                    NodeOrString::from_node(next_of_last, cx.no_gc()),
179                    NodeOrString::String("p".to_owned()),
180                )
181            })
182        {
183            node_list.push(next_of_last);
184        }
185        // Step 11.7. Wrap node list, with sibling criteria returning false
186        // and new parent instructions returning the result of calling createElement(tag) on the context object.
187        // Set container to the result.
188        container = wrap_node_list(
189            cx,
190            node_list,
191            |_| false,
192            |cx| Some(DomRoot::upcast(document.create_element(cx, tag.str()))),
193        )
194        .expect("Must always be able to wrap");
195    }
196    // Step 12. If container's local name is "address", "listing", or "pre":
197    if node_matches_local_name!(
198        container,
199        local_name!("address") | local_name!("listing") | local_name!("pre")
200    ) {
201        // Step 12.1. Let br be the result of calling createElement("br") on the context object.
202        let br = document.create_element(cx, "br");
203        // Step 12.2. Call insertNode(br) on the active range.
204        if selection
205            .expect_active_range(cx)
206            .InsertNode(cx, br.upcast())
207            .is_err()
208        {
209            unreachable!("Must always be able to insert");
210        }
211        // Step 12.3. Call collapse(node, offset + 1) on the context object's selection.
212        let _ = selection.Collapse(cx, Some(&node), offset + 1);
213        // Step 12.4. If br is the last descendant of container,
214        // let br be the result of calling createElement("br") on the context object,
215        // then call insertNode(br) on the active range.
216        if container
217            .children()
218            .last()
219            .is_some_and(|child| *child == *br.upcast())
220        {
221            let br = document.create_element(cx, "br");
222            if selection
223                .expect_active_range(cx)
224                .InsertNode(cx, br.upcast())
225                .is_err()
226            {
227                unreachable!("Must always be able to insert");
228            }
229        }
230        // Step 12.5. Return true.
231        return true;
232    }
233    // Step 13. If container's local name is "li", "dt", or "dd";
234    // and either it has no children or it has a single child and that child is a br:
235    if node_matches_local_name!(
236        container,
237        local_name!("li") | local_name!("dt") | local_name!("dd")
238    ) && (container.children_count() == 0 ||
239        (container.children_count() == 1 &&
240            container
241                .children()
242                .next()
243                .expect("has one child")
244                .is::<HTMLBRElement>()))
245    {
246        // Step 13.1. Split the parent of the one-node list consisting of container.
247        split_the_parent(cx, &[&container]);
248        // Step 13.2. If container has no children,
249        // call createElement("br") on the context object and append the result as the last child of container.
250        if container.children_count() == 0 {
251            let br = document.create_element(cx, "br");
252            if container.AppendChild(cx, br.upcast()).is_err() {
253                unreachable!("Must always be able to append");
254            }
255        }
256        // Step 13.3. If container is a dd or dt,
257        // and it is not an allowed child of any of its ancestors in the same editing host,
258        // set the tag name of container to the default single-line container name and let container be the result.
259        if node_matches_local_name!(container, local_name!("dd") | local_name!("dt")) &&
260            container.is_no_allowed_child_in_same_editing_host(cx.no_gc())
261        {
262            container = container
263                .downcast::<Element>()
264                .expect("Must always be an element")
265                .set_the_tag_name(cx, document.default_single_line_container_name().str());
266        }
267        // Step 13.4. Fix disallowed ancestors of container.
268        container.fix_disallowed_ancestors(cx, document);
269        // Step 13.5. Return true.
270        return true;
271    }
272    // Step 14. Let new line range be a new range whose start is the same as the active range's,
273    // and whose end is (container, length of container).
274    let new_line_range = document.CreateRange(cx);
275    let (start_container, start_offset) = selection.start_boundary(cx);
276    let _ = new_line_range.SetStart(&start_container, start_offset);
277    let _ = new_line_range.SetEnd(&container, container.len());
278    // Step 15. While new line range's start offset is zero and its start node
279    // is not a prohibited paragraph child,
280    // set its start to (parent of start node, index of start node).
281    while new_line_range.start_offset() == 0 &&
282        !new_line_range
283            .start_container()
284            .is_prohibited_paragraph_child()
285    {
286        let start = new_line_range.start_container();
287        let _ = new_line_range.SetStart(
288            &start.GetParentNode().expect("Must always have a parent"),
289            start.index(),
290        );
291    }
292    // Step 16. While new line range's start offset is the length of its start node
293    // and its start node is not a prohibited paragraph child,
294    // set its start to (parent of start node, 1 + index of start node).
295    while new_line_range.start_offset() == new_line_range.start_container().len() &&
296        !new_line_range
297            .start_container()
298            .is_prohibited_paragraph_child()
299    {
300        let start = new_line_range.start_container();
301        let _ = new_line_range.SetStart(
302            &start.GetParentNode().expect("Must always have a parent"),
303            1 + start.index(),
304        );
305    }
306    // Step 17. Let end of line be true if new line range contains either nothing or a single br, and false otherwise.
307    let end_of_line = new_line_range
308        .contained_children()
309        .is_ok_and(|contained_children| {
310            let contained_children = contained_children.contained_children;
311            contained_children.is_empty() ||
312                (contained_children.len() == 1 && contained_children[0].is::<HTMLBRElement>())
313        });
314    // Step 18. If the local name of container is "h1", "h2", "h3", "h4", "h5", or "h6",
315    // and end of line is true, let new container name be the default single-line container name.
316    let container_as_element = container
317        .downcast::<Element>()
318        .expect("Must always be an element");
319    let container_name = container_as_element.local_name();
320    let new_container_name = if end_of_line &&
321        matches!(
322            *container_name,
323            local_name!("h1") |
324                local_name!("h2") |
325                local_name!("h3") |
326                local_name!("h4") |
327                local_name!("h5") |
328                local_name!("h6")
329        ) {
330        document
331            .default_single_line_container_name()
332            .str()
333            .to_owned()
334    } else
335    // Step 19. Otherwise, if the local name of container is "dt" and end of line is true, let new container name be "dd".
336    if end_of_line && container_name == &local_name!("dt") {
337        "dd".to_owned()
338    } else
339    // Step 20. Otherwise, if the local name of container is "dd" and end of line is true, let new container name be "dt".
340    if end_of_line && container_name == &local_name!("dd") {
341        "dt".to_owned()
342    } else {
343        // Step 21. Otherwise, let new container name be the local name of container.
344        container_name.to_string()
345    };
346    // Step 22. Let new container be the result of calling createElement(new container name) on the context object.
347    let new_container = document.create_element(cx, &new_container_name);
348    // Step 23. Copy all attributes of container to new container.
349    container_as_element.copy_all_attributes_to_other_element(cx, &new_container);
350    // Step 24. If new container has an id attribute, unset it.
351    new_container.remove_attribute_by_name(cx, &local_name!("id"));
352    // Step 25. Insert new container into the parent of container immediately after container.
353    let new_container_node = DomRoot::upcast(new_container);
354    if container
355        .GetParentNode()
356        .expect("Must always have a parent")
357        .InsertBefore(
358            cx,
359            &new_container_node,
360            container.GetNextSibling().as_deref(),
361        )
362        .is_err()
363    {
364        unreachable!("Must always be able to insert");
365    }
366    // Step 26. Let contained nodes be all nodes contained in new line range.
367    let contained_nodes: Vec<DomRoot<Node>> = new_line_range
368        .contained_nodes(cx.no_gc())
369        .map(|node| node.as_rooted())
370        .collect();
371    // Step 27. Let frag be the result of calling extractContents() on new line range.
372    let Ok(frag) = new_line_range.ExtractContents(cx) else {
373        unreachable!("Must always be able to extract");
374    };
375    let frag_as_node = frag.upcast::<Node>();
376    // Step 28. Unset the id attribute (if any) of each Element descendant of frag
377    // that is not in contained nodes.
378    for descendant in frag_as_node.traverse_preorder(ShadowIncluding::No) {
379        if !contained_nodes.contains(&descendant) &&
380            let Some(descendant) = descendant.downcast::<Element>()
381        {
382            descendant.remove_attribute_by_name(cx, &local_name!("id"));
383        }
384    }
385    // Step 29. Call appendChild(frag) on new container.
386    if new_container_node.AppendChild(cx, frag_as_node).is_err() {
387        unreachable!("Must always be able to append");
388    }
389    // Step 30. While container's lastChild is a prohibited paragraph child,
390    // set container to its lastChild.
391    loop {
392        let Some(last_child) = container.children().last() else {
393            break;
394        };
395        if !last_child.is_prohibited_paragraph_child() {
396            break;
397        }
398        container = last_child;
399    }
400    // Step 31. While new container's lastChild is a prohibited paragraph child,
401    // set new container to its lastChild.
402    let mut new_container_node = new_container_node;
403    loop {
404        let Some(last_child) = new_container_node.children().last() else {
405            break;
406        };
407        if !last_child.is_prohibited_paragraph_child() {
408            break;
409        }
410        new_container_node = last_child;
411    }
412    // Step 32. If container has no visible children,
413    // call createElement("br") on the context object,
414    // and append the result as the last child of container.
415    if container
416        .children()
417        .all(|child| child.is_invisible(cx.no_gc()))
418    {
419        let br = document.create_element(cx, "br");
420        if container.AppendChild(cx, br.upcast()).is_err() {
421            unreachable!("Must always be able to append");
422        }
423    }
424    // Step 33. If new container has no visible children,
425    // call createElement("br") on the context object,
426    // and append the result as the last child of new container.
427    if new_container_node
428        .children()
429        .all(|child| child.is_invisible(cx.no_gc()))
430    {
431        let br = document.create_element(cx, "br");
432        if new_container_node.AppendChild(cx, br.upcast()).is_err() {
433            unreachable!("Must always be able to append");
434        }
435    }
436    // Step 34. Call collapse(new container, 0) on the context object's selection.
437    let _ = selection.Collapse(cx, Some(&new_container_node), 0);
438    // Step 35. Return true.
439    true
440}