Skip to main content

script/dom/execcommand/commands/
inserttext.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 js::context::JSContext;
6use script_bindings::codegen::GenericBindings::CharacterDataBinding::CharacterDataMethods;
7use script_bindings::codegen::GenericBindings::DocumentBinding::DocumentMethods;
8use script_bindings::codegen::GenericBindings::SelectionBinding::SelectionMethods;
9use script_bindings::inheritance::Castable;
10
11use crate::dom::Node;
12use crate::dom::bindings::codegen::Bindings::RangeBinding::RangeMethods;
13use crate::dom::bindings::str::DOMString;
14use crate::dom::characterdata::CharacterData;
15use crate::dom::document::Document;
16use crate::dom::execcommand::basecommand::CommandName;
17use crate::dom::execcommand::commands::insertparagraph::execute_insert_paragraph_command;
18use crate::dom::execcommand::contenteditable::selection::SelectionDeletionStripWrappers;
19use crate::dom::selection::Selection;
20use crate::dom::text::Text;
21
22/// <https://w3c.github.io/editing/docs/execCommand/#the-inserttext-command>
23pub(crate) fn execute_insert_text_command(
24    cx: &mut JSContext,
25    document: &Document,
26    selection: &Selection,
27    value: DOMString,
28) -> bool {
29    // Step 1. Delete the selection, with strip wrappers false.
30    selection.delete_the_selection(
31        cx,
32        document,
33        Default::default(),
34        SelectionDeletionStripWrappers::NoStrip,
35        Default::default(),
36    );
37
38    // Step 2. If the active range's start node is neither editable nor an editing host, return true.
39    let mut active_range = selection
40        .active_range()
41        .expect("Must always have an active range.");
42    if !active_range.start_container().is_editable_or_editing_host() {
43        return true;
44    }
45
46    // Step 3. If value's length is greater than one:
47    if value.str().chars().nth(1).is_some() {
48        // Step 3.1. For each code unit el in value, take the action for the insertText command, with value equal to el.
49        for el in value.str().chars() {
50            execute_insert_text_command(cx, document, selection, DOMString::from(el.to_string()));
51        }
52
53        // Step 3.2. Return true.
54        return true;
55    }
56
57    // Step 4. If value is the empty string, return true.
58    if value.is_empty() {
59        return true;
60    }
61
62    // Step 5. If value is a newline (U+000A), take the action for the insertParagraph command and return true.
63    if value == "\n" {
64        execute_insert_paragraph_command(cx, document, selection);
65        return true;
66    }
67
68    // Step 6. Let node and offset be the active range's start node and offset.
69    let mut node = active_range.start_container();
70    let mut offset = active_range.start_offset();
71
72    // Step 7. If node has a child whose index is offset − 1, and that child is a Text node, set node to that child, then set offset to node's length.
73    if offset > 0 &&
74        let Some(child) = node.children().nth((offset - 1) as usize) &&
75        child.is::<Text>()
76    {
77        node = child;
78        offset = node.len();
79    }
80
81    // Step 8. If node has a child whose index is offset, and that child is a Text node, set node to that child, then set offset to zero.
82    if let Some(child) = node.children().nth((offset) as usize) &&
83        child.is::<Text>()
84    {
85        node = child;
86        offset = 0;
87    }
88
89    // Step 9. Record current overrides, and let overrides be the result.
90    let overrides = CommandName::record_current_overrides(document);
91
92    // Step 10. Call collapse(node, offset) on the context object's selection.
93    if selection.Collapse(cx, Some(&node), offset).is_err() {
94        unreachable!("Must always be able to collapse the selection");
95    }
96
97    // Step 11. Canonicalize whitespace at (node, offset).
98    node.canonicalize_whitespace(cx, offset, Default::default());
99
100    // Step 12. Let (node, offset) be the active range's start.
101    active_range = selection
102        .active_range()
103        .expect("Must always have an active range.");
104    node = active_range.start_container();
105    offset = active_range.start_offset();
106
107    // Step 13. If node is a Text node:
108    if let Some(node_as_text) = node.downcast::<Text>() {
109        // Step 13.1. Call insertData(offset, value) on node.
110        if node_as_text
111            .upcast::<CharacterData>()
112            .InsertData(cx, offset, value.clone())
113            .is_err()
114        {
115            unreachable!("Must always be able to insert");
116        }
117
118        // Step 13.2. Call collapse(node, offset) on the context object's selection.
119        if selection.Collapse(cx, Some(&node), offset).is_err() {
120            unreachable!("Must always be able to collapse the selection.");
121        }
122
123        // Step 13.3. Call extend(node, offset + 1) on the context object's selection.
124        if selection.Extend(cx, &node, offset + 1).is_err() {
125            unreachable!("Must always be able to extend the selection");
126        }
127
128        active_range = selection
129            .active_range()
130            .expect("Must always have an active range.");
131    }
132    // Step 14. Otherwise:
133    else {
134        // Step 14.1. If node has only one child, which is a collapsed line break, remove its child from it.
135        // TODO: Implement this.
136
137        // Step 14.2. Let text be the result of calling createTextNode(value) on the context object.
138        let text = document.CreateTextNode(cx, value.clone());
139        let text = text.upcast::<Node>();
140
141        // Step 14.3. Call insertNode(text) on the active range.
142        if active_range.InsertNode(cx, text).is_err() {
143            unreachable!("Must always be able to insert");
144        }
145
146        // Step 14.4. Call collapse(text, 0) on the context object's selection.
147        if selection.Collapse(cx, Some(text), 0).is_err() {
148            unreachable!("Must always be able to collapse the selection");
149        }
150
151        // Step 14.5. Call extend(text, 1) on the context object's selection.
152        if selection.Extend(cx, text, 1).is_err() {
153            unreachable!("Must always be able to extend the selection");
154        }
155
156        active_range = selection
157            .active_range()
158            .expect("Must always have an active range.");
159    }
160
161    // Step 15. Restore states and values from overrides.
162    active_range.restore_states_and_values(cx, selection, document, overrides);
163
164    // Step 16. Canonicalize whitespace at the active range's start, with fix collapsed space false.
165    active_range
166        .start_container()
167        .canonicalize_whitespace(cx, active_range.start_offset(), false);
168
169    // Step 17. Canonicalize whitespace at the active range's end, with fix collapsed space false.
170    active_range
171        .end_container()
172        .canonicalize_whitespace(cx, active_range.end_offset(), false);
173
174    // Step 18. If value is a space character, autolink the active range's start.
175    if value == " " {
176        // TODO: Implement autolink.
177    }
178
179    // Step 19. Call collapseToEnd() on the context object's selection.
180    if selection.CollapseToEnd(cx).is_err() {
181        unreachable!("Must always be able to CollapseToEnd here.");
182    }
183
184    // Step 20. Return true.
185    true
186}