1/*
2 * Copyright (C) 2005 Apple Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
14 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
17 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26#include "config.h"
27#include "InsertTextCommand.h"
28
29#include "Document.h"
30#include "Editing.h"
31#include "Editor.h"
32#include "Frame.h"
33#include "HTMLElement.h"
34#include "HTMLInterchange.h"
35#include "Text.h"
36#include "VisibleUnits.h"
37
38namespace WebCore {
39
40InsertTextCommand::InsertTextCommand(Document& document, const String& text, bool selectInsertedText, RebalanceType rebalanceType, EditAction editingAction)
41 : CompositeEditCommand(document, editingAction)
42 , m_text(text)
43 , m_selectInsertedText(selectInsertedText)
44 , m_rebalanceType(rebalanceType)
45{
46}
47
48InsertTextCommand::InsertTextCommand(Document& document, const String& text, Ref<TextInsertionMarkerSupplier>&& markerSupplier, EditAction editingAction)
49 : CompositeEditCommand(document, editingAction)
50 , m_text(text)
51 , m_selectInsertedText(false)
52 , m_rebalanceType(RebalanceLeadingAndTrailingWhitespaces)
53 , m_markerSupplier(WTFMove(markerSupplier))
54{
55}
56
57Position InsertTextCommand::positionInsideTextNode(const Position& p)
58{
59 Position pos = p;
60 if (isTabSpanTextNode(pos.anchorNode())) {
61 auto textNode = document().createEditingTextNode(emptyString());
62 auto* textNodePtr = textNode.ptr();
63 insertNodeAtTabSpanPosition(WTFMove(textNode), pos);
64 return firstPositionInNode(textNodePtr);
65 }
66
67 // Prepare for text input by looking at the specified position.
68 // It may be necessary to insert a text node to receive characters.
69 if (!pos.containerNode()->isTextNode()) {
70 auto textNode = document().createEditingTextNode(emptyString());
71 auto* textNodePtr = textNode.ptr();
72 insertNodeAt(WTFMove(textNode), pos);
73 return firstPositionInNode(textNodePtr);
74 }
75
76 return pos;
77}
78
79void InsertTextCommand::setEndingSelectionWithoutValidation(const Position& startPosition, const Position& endPosition)
80{
81 // We could have inserted a part of composed character sequence,
82 // so we are basically treating ending selection as a range to avoid validation.
83 // <http://bugs.webkit.org/show_bug.cgi?id=15781>
84 VisibleSelection forcedEndingSelection;
85 forcedEndingSelection.setWithoutValidation(startPosition, endPosition);
86 forcedEndingSelection.setIsDirectional(endingSelection().isDirectional());
87 setEndingSelection(forcedEndingSelection);
88}
89
90// This avoids the expense of a full fledged delete operation, and avoids a layout that typically results
91// from text removal.
92bool InsertTextCommand::performTrivialReplace(const String& text, bool selectInsertedText)
93{
94 if (!endingSelection().isRange())
95 return false;
96
97 if (text.contains('\t') || text.contains(' ') || text.contains('\n'))
98 return false;
99
100 Position start = endingSelection().start();
101 Position endPosition = replaceSelectedTextInNode(text);
102 if (endPosition.isNull())
103 return false;
104
105 setEndingSelectionWithoutValidation(start, endPosition);
106 if (!selectInsertedText)
107 setEndingSelection(VisibleSelection(endingSelection().visibleEnd(), endingSelection().isDirectional()));
108
109 return true;
110}
111
112bool InsertTextCommand::performOverwrite(const String& text, bool selectInsertedText)
113{
114 Position start = endingSelection().start();
115 RefPtr<Text> textNode = start.containerText();
116 if (!textNode)
117 return false;
118
119 unsigned count = std::min(text.length(), textNode->length() - start.offsetInContainerNode());
120 if (!count)
121 return false;
122
123 replaceTextInNode(*textNode, start.offsetInContainerNode(), count, text);
124
125 Position endPosition = Position(textNode.get(), start.offsetInContainerNode() + text.length());
126 setEndingSelectionWithoutValidation(start, endPosition);
127 if (!selectInsertedText)
128 setEndingSelection(VisibleSelection(endingSelection().visibleEnd(), endingSelection().isDirectional()));
129
130 return true;
131}
132
133void InsertTextCommand::doApply()
134{
135 ASSERT(m_text.find('\n') == notFound);
136
137 if (endingSelection().isNoneOrOrphaned())
138 return;
139
140 // Delete the current selection.
141 // FIXME: This delete operation blows away the typing style.
142 if (endingSelection().isRange()) {
143 if (performTrivialReplace(m_text, m_selectInsertedText))
144 return;
145 deleteSelection(false, true, true, false, false);
146 // deleteSelection eventually makes a new endingSelection out of a Position. If that Position doesn't have
147 // a renderer (e.g. it is on a <frameset> in the DOM), the VisibleSelection cannot be canonicalized to
148 // anything other than NoSelection. The rest of this function requires a real endingSelection, so bail out.
149 if (endingSelection().isNone())
150 return;
151 } else if (frame().editor().isOverwriteModeEnabled()) {
152 if (performOverwrite(m_text, m_selectInsertedText))
153 return;
154 }
155
156 Position startPosition(endingSelection().start());
157
158 Position placeholder;
159 // We want to remove preserved newlines and brs that will collapse (and thus become unnecessary) when content
160 // is inserted just before them.
161 // FIXME: We shouldn't really have to do this, but removing placeholders is a workaround for 9661.
162 // If the caret is just before a placeholder, downstream will normalize the caret to it.
163 Position downstream(startPosition.downstream());
164 if (lineBreakExistsAtPosition(downstream)) {
165 // FIXME: This doesn't handle placeholders at the end of anonymous blocks.
166 VisiblePosition caret(startPosition);
167 if (isEndOfBlock(caret) && isStartOfParagraph(caret))
168 placeholder = downstream;
169 // Don't remove the placeholder yet, otherwise the block we're inserting into would collapse before
170 // we get a chance to insert into it. We check for a placeholder now, though, because doing so requires
171 // the creation of a VisiblePosition, and if we did that post-insertion it would force a layout.
172 }
173
174 // Insert the character at the leftmost candidate.
175 startPosition = startPosition.upstream();
176
177 // It is possible for the node that contains startPosition to contain only unrendered whitespace,
178 // and so deleteInsignificantText could remove it. Save the position before the node in case that happens.
179 Position positionBeforeStartNode(positionInParentBeforeNode(startPosition.containerNode()));
180 deleteInsignificantText(startPosition.upstream(), startPosition.downstream());
181 if (!startPosition.anchorNode()->isConnected())
182 startPosition = positionBeforeStartNode;
183 if (!startPosition.isCandidate())
184 startPosition = startPosition.downstream();
185
186 startPosition = positionAvoidingSpecialElementBoundary(startPosition);
187
188 Position endPosition;
189
190 if (m_text == "\t") {
191 endPosition = insertTab(startPosition);
192 startPosition = endPosition.previous();
193 if (placeholder.isNotNull())
194 removePlaceholderAt(placeholder);
195 } else {
196 // Make sure the document is set up to receive m_text
197 startPosition = positionInsideTextNode(startPosition);
198 ASSERT(startPosition.anchorType() == Position::PositionIsOffsetInAnchor);
199 ASSERT(startPosition.containerNode());
200 ASSERT(startPosition.containerNode()->isTextNode());
201 if (placeholder.isNotNull())
202 removePlaceholderAt(placeholder);
203 RefPtr<Text> textNode = startPosition.containerText();
204 const unsigned offset = startPosition.offsetInContainerNode();
205
206 insertTextIntoNode(*textNode, offset, m_text);
207 endPosition = Position(textNode.get(), offset + m_text.length());
208 if (m_markerSupplier)
209 m_markerSupplier->addMarkersToTextNode(*textNode, offset, m_text);
210
211 if (m_rebalanceType == RebalanceLeadingAndTrailingWhitespaces) {
212 // The insertion may require adjusting adjacent whitespace, if it is present.
213 rebalanceWhitespaceAt(endPosition);
214 // Rebalancing on both sides isn't necessary if we've inserted only spaces.
215 if (!shouldRebalanceLeadingWhitespaceFor(m_text))
216 rebalanceWhitespaceAt(startPosition);
217 } else {
218 ASSERT(m_rebalanceType == RebalanceAllWhitespaces);
219 if (canRebalance(startPosition) && canRebalance(endPosition))
220 rebalanceWhitespaceOnTextSubstring(*textNode, startPosition.offsetInContainerNode(), endPosition.offsetInContainerNode());
221 }
222 }
223
224 setEndingSelectionWithoutValidation(startPosition, endPosition);
225
226 // Handle the case where there is a typing style.
227 if (RefPtr<EditingStyle> typingStyle = frame().selection().typingStyle()) {
228 typingStyle->prepareToApplyAt(endPosition, EditingStyle::PreserveWritingDirection);
229 if (!typingStyle->isEmpty())
230 applyStyle(typingStyle.get());
231 }
232
233 if (!m_selectInsertedText)
234 setEndingSelection(VisibleSelection(endingSelection().end(), endingSelection().affinity(), endingSelection().isDirectional()));
235}
236
237Position InsertTextCommand::insertTab(const Position& pos)
238{
239 Position insertPos = VisiblePosition(pos, DOWNSTREAM).deepEquivalent();
240 if (insertPos.isNull())
241 return pos;
242
243 Node* node = insertPos.containerNode();
244 unsigned int offset = node->isTextNode() ? insertPos.offsetInContainerNode() : 0;
245
246 // keep tabs coalesced in tab span
247 if (isTabSpanTextNode(node)) {
248 Ref<Text> textNode = downcast<Text>(*node);
249 insertTextIntoNode(textNode, offset, "\t");
250 return Position(textNode.ptr(), offset + 1);
251 }
252
253 // create new tab span
254 auto spanNode = createTabSpanElement(document());
255 auto* spanNodePtr = spanNode.ptr();
256
257 // place it
258 if (!is<Text>(*node))
259 insertNodeAt(WTFMove(spanNode), insertPos);
260 else {
261 Ref<Text> textNode = downcast<Text>(*node);
262 if (offset >= textNode->length())
263 insertNodeAfter(WTFMove(spanNode), textNode);
264 else {
265 // split node to make room for the span
266 // NOTE: splitTextNode uses textNode for the
267 // second node in the split, so we need to
268 // insert the span before it.
269 if (offset > 0)
270 splitTextNode(textNode, offset);
271 insertNodeBefore(WTFMove(spanNode), textNode);
272 }
273 }
274
275 // return the position following the new tab
276 return lastPositionInNode(spanNodePtr);
277}
278
279}
280