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