1/*
2 * Copyright (C) 2005 Apple Computer, 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 COMPUTER, 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 COMPUTER, 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 "core/editing/InsertTextCommand.h"
28
29#include "core/dom/Document.h"
30#include "core/dom/Element.h"
31#include "core/dom/Text.h"
32#include "core/editing/Editor.h"
33#include "core/editing/VisibleUnits.h"
34#include "core/editing/htmlediting.h"
35#include "core/frame/Frame.h"
36
37namespace WebCore {
38
39InsertTextCommand::InsertTextCommand(Document& document, const String& text, bool selectInsertedText, RebalanceType rebalanceType)
40    : CompositeEditCommand(document)
41    , m_text(text)
42    , m_selectInsertedText(selectInsertedText)
43    , m_rebalanceType(rebalanceType)
44{
45}
46
47InsertTextCommand::InsertTextCommand(Document& document, const String& text, PassRefPtr<TextInsertionMarkerSupplier> markerSupplier)
48    : CompositeEditCommand(document)
49    , m_text(text)
50    , m_selectInsertedText(false)
51    , m_rebalanceType(RebalanceLeadingAndTrailingWhitespaces)
52    , m_markerSupplier(markerSupplier)
53{
54}
55
56Position InsertTextCommand::positionInsideTextNode(const Position& p)
57{
58    Position pos = p;
59    if (isTabSpanTextNode(pos.anchorNode())) {
60        RefPtr<Node> textNode = document().createEditingTextNode("");
61        insertNodeAtTabSpanPosition(textNode.get(), pos);
62        return firstPositionInNode(textNode.get());
63    }
64
65    // Prepare for text input by looking at the specified position.
66    // It may be necessary to insert a text node to receive characters.
67    if (!pos.containerNode()->isTextNode()) {
68        RefPtr<Node> textNode = document().createEditingTextNode("");
69        insertNodeAt(textNode.get(), pos);
70        return firstPositionInNode(textNode.get());
71    }
72
73    return pos;
74}
75
76void InsertTextCommand::setEndingSelectionWithoutValidation(const Position& startPosition, const Position& endPosition)
77{
78    // We could have inserted a part of composed character sequence,
79    // so we are basically treating ending selection as a range to avoid validation.
80    // <http://bugs.webkit.org/show_bug.cgi?id=15781>
81    VisibleSelection forcedEndingSelection;
82    forcedEndingSelection.setWithoutValidation(startPosition, endPosition);
83    forcedEndingSelection.setIsDirectional(endingSelection().isDirectional());
84    setEndingSelection(forcedEndingSelection);
85}
86
87// This avoids the expense of a full fledged delete operation, and avoids a layout that typically results
88// from text removal.
89bool InsertTextCommand::performTrivialReplace(const String& text, bool selectInsertedText)
90{
91    if (!endingSelection().isRange())
92        return false;
93
94    if (text.contains('\t') || text.contains(' ') || text.contains('\n'))
95        return false;
96
97    Position start = endingSelection().start();
98    Position endPosition = replaceSelectedTextInNode(text);
99    if (endPosition.isNull())
100        return false;
101
102    setEndingSelectionWithoutValidation(start, endPosition);
103    if (!selectInsertedText)
104        setEndingSelection(VisibleSelection(endingSelection().visibleEnd(), endingSelection().isDirectional()));
105
106    return true;
107}
108
109bool InsertTextCommand::performOverwrite(const String& text, bool selectInsertedText)
110{
111    Position start = endingSelection().start();
112    RefPtr<Text> textNode = start.containerText();
113    if (!textNode)
114        return false;
115
116    unsigned count = std::min(text.length(), textNode->length() - start.offsetInContainerNode());
117    if (!count)
118        return false;
119
120    replaceTextInNode(textNode, start.offsetInContainerNode(), count, text);
121
122    Position endPosition = Position(textNode.release(), start.offsetInContainerNode() + text.length());
123    setEndingSelectionWithoutValidation(start, endPosition);
124    if (!selectInsertedText)
125        setEndingSelection(VisibleSelection(endingSelection().visibleEnd(), endingSelection().isDirectional()));
126
127    return true;
128}
129
130void InsertTextCommand::doApply()
131{
132    ASSERT(m_text.find('\n') == kNotFound);
133
134    if (!endingSelection().isNonOrphanedCaretOrRange())
135        return;
136
137    // Delete the current selection.
138    // FIXME: This delete operation blows away the typing style.
139    if (endingSelection().isRange()) {
140        if (performTrivialReplace(m_text, m_selectInsertedText))
141            return;
142        deleteSelection(false, true, true, false, false);
143        // deleteSelection eventually makes a new endingSelection out of a Position. If that Position doesn't have
144        // a renderer (e.g. it is on a <frameset> in the DOM), the VisibleSelection cannot be canonicalized to
145        // anything other than NoSelection. The rest of this function requires a real endingSelection, so bail out.
146        if (endingSelection().isNone())
147            return;
148    } else if (document().frame()->editor().isOverwriteModeEnabled()) {
149        if (performOverwrite(m_text, m_selectInsertedText))
150            return;
151    }
152
153    Position startPosition(endingSelection().start());
154
155    Position placeholder;
156    // We want to remove preserved newlines and brs that will collapse (and thus become unnecessary) when content
157    // is inserted just before them.
158    // FIXME: We shouldn't really have to do this, but removing placeholders is a workaround for 9661.
159    // If the caret is just before a placeholder, downstream will normalize the caret to it.
160    Position downstream(startPosition.downstream());
161    if (lineBreakExistsAtPosition(downstream)) {
162        // FIXME: This doesn't handle placeholders at the end of anonymous blocks.
163        VisiblePosition caret(startPosition);
164        if (isEndOfBlock(caret) && isStartOfParagraph(caret))
165            placeholder = downstream;
166        // Don't remove the placeholder yet, otherwise the block we're inserting into would collapse before
167        // we get a chance to insert into it.  We check for a placeholder now, though, because doing so requires
168        // the creation of a VisiblePosition, and if we did that post-insertion it would force a layout.
169    }
170
171    // Insert the character at the leftmost candidate.
172    startPosition = startPosition.upstream();
173
174    // It is possible for the node that contains startPosition to contain only unrendered whitespace,
175    // and so deleteInsignificantText could remove it.  Save the position before the node in case that happens.
176    Position positionBeforeStartNode(positionInParentBeforeNode(startPosition.containerNode()));
177    deleteInsignificantText(startPosition, startPosition.downstream());
178    if (!startPosition.inDocument())
179        startPosition = positionBeforeStartNode;
180    if (!startPosition.isCandidate())
181        startPosition = startPosition.downstream();
182
183    startPosition = positionAvoidingSpecialElementBoundary(startPosition);
184
185    Position endPosition;
186
187    if (m_text == "\t") {
188        endPosition = insertTab(startPosition);
189        startPosition = endPosition.previous();
190        if (placeholder.isNotNull())
191            removePlaceholderAt(placeholder);
192    } else {
193        // Make sure the document is set up to receive m_text
194        startPosition = positionInsideTextNode(startPosition);
195        ASSERT(startPosition.anchorType() == Position::PositionIsOffsetInAnchor);
196        ASSERT(startPosition.containerNode());
197        ASSERT(startPosition.containerNode()->isTextNode());
198        if (placeholder.isNotNull())
199            removePlaceholderAt(placeholder);
200        RefPtr<Text> textNode = startPosition.containerText();
201        const unsigned offset = startPosition.offsetInContainerNode();
202
203        insertTextIntoNode(textNode, offset, m_text);
204        endPosition = Position(textNode, offset + m_text.length());
205        if (m_markerSupplier)
206            m_markerSupplier->addMarkersToTextNode(textNode.get(), offset, m_text);
207
208        if (m_rebalanceType == RebalanceLeadingAndTrailingWhitespaces) {
209            // The insertion may require adjusting adjacent whitespace, if it is present.
210            rebalanceWhitespaceAt(endPosition);
211            // Rebalancing on both sides isn't necessary if we've inserted only spaces.
212            if (!shouldRebalanceLeadingWhitespaceFor(m_text))
213                rebalanceWhitespaceAt(startPosition);
214        } else {
215            ASSERT(m_rebalanceType == RebalanceAllWhitespaces);
216            if (canRebalance(startPosition) && canRebalance(endPosition))
217                rebalanceWhitespaceOnTextSubstring(textNode, startPosition.offsetInContainerNode(), endPosition.offsetInContainerNode());
218        }
219    }
220
221    setEndingSelectionWithoutValidation(startPosition, endPosition);
222
223    // Handle the case where there is a typing style.
224    if (RefPtr<EditingStyle> typingStyle = document().frame()->selection().typingStyle()) {
225        typingStyle->prepareToApplyAt(endPosition, EditingStyle::PreserveWritingDirection);
226        if (!typingStyle->isEmpty())
227            applyStyle(typingStyle.get());
228    }
229
230    if (!m_selectInsertedText)
231        setEndingSelection(VisibleSelection(endingSelection().end(), endingSelection().affinity(), endingSelection().isDirectional()));
232}
233
234Position InsertTextCommand::insertTab(const Position& pos)
235{
236    Position insertPos = VisiblePosition(pos, DOWNSTREAM).deepEquivalent();
237
238    Node* node = insertPos.containerNode();
239    unsigned int offset = node->isTextNode() ? insertPos.offsetInContainerNode() : 0;
240
241    // keep tabs coalesced in tab span
242    if (isTabSpanTextNode(node)) {
243        RefPtr<Text> textNode = toText(node);
244        insertTextIntoNode(textNode, offset, "\t");
245        return Position(textNode.release(), offset + 1);
246    }
247
248    // create new tab span
249    RefPtr<Element> spanNode = createTabSpanElement(document());
250
251    // place it
252    if (!node->isTextNode()) {
253        insertNodeAt(spanNode.get(), insertPos);
254    } else {
255        RefPtr<Text> textNode = toText(node);
256        if (offset >= textNode->length())
257            insertNodeAfter(spanNode, textNode.release());
258        else {
259            // split node to make room for the span
260            // NOTE: splitTextNode uses textNode for the
261            // second node in the split, so we need to
262            // insert the span before it.
263            if (offset > 0)
264                splitTextNode(textNode, offset);
265            insertNodeBefore(spanNode, textNode.release());
266        }
267    }
268
269    // return the position following the new tab
270    return lastPositionInNode(spanNode.get());
271}
272
273}
274