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