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(cx)
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    // NOTE: In theory, the 'spec' wants us to do insertText for every UTF-16 code unit. We do it for every UTF-8
48    //       character instead, as that lets us avoid having to deal with inbetween states where we should have
49    //       inserted one half of a surrogate pair, which would temporarily make the text in the node invalid UTF-8.
50    //       This shouldn't cause any issues since none of the per-character handling cares about half a surrogate pair
51    //       and the events that a MutationObserver sees aren't per-character in other browsers.
52    if value.str().chars().nth(1).is_some() {
53        // Step 3.1. For each code unit el in value, take the action for the insertText command, with value equal to el.
54        for el in value.str().chars() {
55            execute_insert_text_command(cx, document, selection, DOMString::from(el.to_string()));
56        }
57
58        // Step 3.2. Return true.
59        return true;
60    }
61
62    // Step 4. If value is the empty string, return true.
63    if value.is_empty() {
64        return true;
65    }
66
67    // Step 5. If value is a newline (U+000A), take the action for the insertParagraph command and return true.
68    if value == "\n" {
69        execute_insert_paragraph_command(cx, document, selection);
70        return true;
71    }
72
73    // Step 6. Let node and offset be the active range's start node and offset.
74    let mut node = active_range.start_container();
75    let mut offset = active_range.start_offset();
76
77    // 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.
78    if offset > 0 &&
79        let Some(child) = node.children().nth((offset - 1) as usize) &&
80        child.is::<Text>()
81    {
82        node = child;
83        offset = node.len();
84    }
85
86    // 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.
87    if let Some(child) = node.children().nth((offset) as usize) &&
88        child.is::<Text>()
89    {
90        node = child;
91        offset = 0;
92    }
93
94    // Step 9. Record current overrides, and let overrides be the result.
95    let overrides = CommandName::record_current_overrides(document);
96
97    // Step 10. Call collapse(node, offset) on the context object's selection.
98    if selection.Collapse(cx, Some(&node), offset).is_err() {
99        unreachable!("Must always be able to collapse the selection");
100    }
101
102    // Step 11. Canonicalize whitespace at (node, offset).
103    node.canonicalize_whitespace(cx, offset, Default::default());
104
105    // Step 12. Let (node, offset) be the active range's start.
106    active_range = selection
107        .active_range(cx)
108        .expect("Must always have an active range.");
109    node = active_range.start_container();
110    offset = active_range.start_offset();
111
112    // Step 13. If node is a Text node:
113    if let Some(node_as_text) = node.downcast::<Text>() {
114        // Step 13.1. Call insertData(offset, value) on node.
115        if node_as_text
116            .upcast::<CharacterData>()
117            .InsertData(cx, offset, value.clone())
118            .is_err()
119        {
120            unreachable!("Must always be able to insert");
121        }
122
123        // Step 13.2. Call collapse(node, offset) on the context object's selection.
124        if selection.Collapse(cx, Some(&node), offset).is_err() {
125            unreachable!("Must always be able to collapse the selection.");
126        }
127
128        // Step 13.3. Call extend(node, offset + 1) on the context object's selection.
129        // Note: We're doing this per UTF-8 character instead of per UTF-16 code unit.
130        if selection
131            .Extend(cx, &node, offset + (value.len_utf16().0 as u32))
132            .is_err()
133        {
134            unreachable!("Must always be able to extend the selection");
135        }
136
137        active_range = selection
138            .active_range(cx)
139            .expect("Must always have an active range.");
140    }
141    // Step 14. Otherwise:
142    else {
143        // Step 14.1. If node has only one child, which is a collapsed line break, remove its child from it.
144        // TODO: Implement this.
145
146        // Step 14.2. Let text be the result of calling createTextNode(value) on the context object.
147        let text = document.CreateTextNode(cx, value.clone());
148        let text = text.upcast::<Node>();
149
150        // Step 14.3. Call insertNode(text) on the active range.
151        if active_range.InsertNode(cx, text).is_err() {
152            unreachable!("Must always be able to insert");
153        }
154
155        // Step 14.4. Call collapse(text, 0) on the context object's selection.
156        if selection.Collapse(cx, Some(text), 0).is_err() {
157            unreachable!("Must always be able to collapse the selection");
158        }
159
160        // Step 14.5. Call extend(text, 1) on the context object's selection.
161        // Note: We're doing this per UTF-8 character instead of per UTF-16 code unit.
162        if selection
163            .Extend(cx, text, value.len_utf16().0 as u32)
164            .is_err()
165        {
166            unreachable!("Must always be able to extend the selection");
167        }
168
169        active_range = selection
170            .active_range(cx)
171            .expect("Must always have an active range.");
172    }
173
174    // Step 15. Restore states and values from overrides.
175    active_range.restore_states_and_values(cx, selection, document, overrides);
176
177    // Step 16. Canonicalize whitespace at the active range's start, with fix collapsed space false.
178    active_range
179        .start_container()
180        .canonicalize_whitespace(cx, active_range.start_offset(), false);
181
182    // Step 17. Canonicalize whitespace at the active range's end, with fix collapsed space false.
183    active_range
184        .end_container()
185        .canonicalize_whitespace(cx, active_range.end_offset(), false);
186
187    // Step 18. If value is a space character, autolink the active range's start.
188    if value == " " {
189        // TODO: Implement autolink.
190    }
191
192    // Step 19. Call collapseToEnd() on the context object's selection.
193    if selection.CollapseToEnd(cx).is_err() {
194        unreachable!("Must always be able to CollapseToEnd here.");
195    }
196
197    // Step 20. Return true.
198    true
199}