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(cx.no_gc())
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(cx.no_gc(), &start_container, start_offset);
277    let _ = new_line_range.SetEnd(cx.no_gc(), &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            cx.no_gc(),
289            &start.GetParentNode().expect("Must always have a parent"),
290            start.index(),
291        );
292    }
293    // Step 16. While new line range's start offset is the length of its start node
294    // and its start node is not a prohibited paragraph child,
295    // set its start to (parent of start node, 1 + index of start node).
296    while new_line_range.start_offset() == new_line_range.start_container().len() &&
297        !new_line_range
298            .start_container()
299            .is_prohibited_paragraph_child()
300    {
301        let start = new_line_range.start_container();
302        let _ = new_line_range.SetStart(
303            cx.no_gc(),
304            &start.GetParentNode().expect("Must always have a parent"),
305            1 + start.index(),
306        );
307    }
308    // Step 17. Let end of line be true if new line range contains either nothing or a single br, and false otherwise.
309    let end_of_line =
310        new_line_range
311            .contained_children(cx.no_gc())
312            .is_ok_and(|contained_children| {
313                let contained_children = contained_children.contained_children;
314                contained_children.is_empty() ||
315                    (contained_children.len() == 1 &&
316                        contained_children[0].is::<HTMLBRElement>())
317            });
318    // Step 18. If the local name of container is "h1", "h2", "h3", "h4", "h5", or "h6",
319    // and end of line is true, let new container name be the default single-line container name.
320    let container_as_element = container
321        .downcast::<Element>()
322        .expect("Must always be an element");
323    let container_name = container_as_element.local_name();
324    let new_container_name = if end_of_line &&
325        matches!(
326            *container_name,
327            local_name!("h1") |
328                local_name!("h2") |
329                local_name!("h3") |
330                local_name!("h4") |
331                local_name!("h5") |
332                local_name!("h6")
333        ) {
334        document
335            .default_single_line_container_name()
336            .str()
337            .to_owned()
338    } else
339    // Step 19. Otherwise, if the local name of container is "dt" and end of line is true, let new container name be "dd".
340    if end_of_line && container_name == &local_name!("dt") {
341        "dd".to_owned()
342    } else
343    // Step 20. Otherwise, if the local name of container is "dd" and end of line is true, let new container name be "dt".
344    if end_of_line && container_name == &local_name!("dd") {
345        "dt".to_owned()
346    } else {
347        // Step 21. Otherwise, let new container name be the local name of container.
348        container_name.to_string()
349    };
350    // Step 22. Let new container be the result of calling createElement(new container name) on the context object.
351    let new_container = document.create_element(cx, &new_container_name);
352    // Step 23. Copy all attributes of container to new container.
353    container_as_element.copy_all_attributes_to_other_element(cx, &new_container);
354    // Step 24. If new container has an id attribute, unset it.
355    new_container.remove_attribute_by_name(cx, &local_name!("id"));
356    // Step 25. Insert new container into the parent of container immediately after container.
357    let new_container_node = DomRoot::upcast(new_container);
358    if container
359        .GetParentNode()
360        .expect("Must always have a parent")
361        .InsertBefore(
362            cx,
363            &new_container_node,
364            container.GetNextSibling().as_deref(),
365        )
366        .is_err()
367    {
368        unreachable!("Must always be able to insert");
369    }
370    // Step 26. Let contained nodes be all nodes contained in new line range.
371    let contained_nodes: Vec<DomRoot<Node>> = new_line_range
372        .contained_nodes(cx.no_gc())
373        .map(|node| node.as_rooted())
374        .collect();
375    // Step 27. Let frag be the result of calling extractContents() on new line range.
376    let Ok(frag) = new_line_range.ExtractContents(cx) else {
377        unreachable!("Must always be able to extract");
378    };
379    let frag_as_node = frag.upcast::<Node>();
380    // Step 28. Unset the id attribute (if any) of each Element descendant of frag
381    // that is not in contained nodes.
382    for descendant in frag_as_node.traverse_preorder(ShadowIncluding::No) {
383        if !contained_nodes.contains(&descendant) &&
384            let Some(descendant) = descendant.downcast::<Element>()
385        {
386            descendant.remove_attribute_by_name(cx, &local_name!("id"));
387        }
388    }
389    // Step 29. Call appendChild(frag) on new container.
390    if new_container_node.AppendChild(cx, frag_as_node).is_err() {
391        unreachable!("Must always be able to append");
392    }
393    // Step 30. While container's lastChild is a prohibited paragraph child,
394    // set container to its lastChild.
395    loop {
396        let Some(last_child) = container.children().last() else {
397            break;
398        };
399        if !last_child.is_prohibited_paragraph_child() {
400            break;
401        }
402        container = last_child;
403    }
404    // Step 31. While new container's lastChild is a prohibited paragraph child,
405    // set new container to its lastChild.
406    let mut new_container_node = new_container_node;
407    loop {
408        let Some(last_child) = new_container_node.children().last() else {
409            break;
410        };
411        if !last_child.is_prohibited_paragraph_child() {
412            break;
413        }
414        new_container_node = last_child;
415    }
416    // Step 32. If container has no visible children,
417    // call createElement("br") on the context object,
418    // and append the result as the last child of container.
419    if container
420        .children()
421        .all(|child| child.is_invisible(cx.no_gc()))
422    {
423        let br = document.create_element(cx, "br");
424        if container.AppendChild(cx, br.upcast()).is_err() {
425            unreachable!("Must always be able to append");
426        }
427    }
428    // Step 33. If new container has no visible children,
429    // call createElement("br") on the context object,
430    // and append the result as the last child of new container.
431    if new_container_node
432        .children()
433        .all(|child| child.is_invisible(cx.no_gc()))
434    {
435        let br = document.create_element(cx, "br");
436        if new_container_node.AppendChild(cx, br.upcast()).is_err() {
437            unreachable!("Must always be able to append");
438        }
439    }
440    // Step 34. Call collapse(new container, 0) on the context object's selection.
441    let _ = selection.Collapse(cx, Some(&new_container_node), 0);
442    // Step 35. Return true.
443    true
444}