1 /*
2 * Copyright (C) 2005, 2006 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 "InsertParagraphSeparatorCommand.h"
28
29 #include "CSSComputedStyleDeclaration.h"
30 #include "CSSMutableStyleDeclaration.h"
31 #include "CSSPropertyNames.h"
32 #include "Document.h"
33 #include "HTMLElement.h"
34 #include "HTMLNames.h"
35 #include "InsertLineBreakCommand.h"
36 #include "Logging.h"
37 #include "RenderObject.h"
38 #include "Text.h"
39 #include "htmlediting.h"
40 #include "visible_units.h"
41 #include "ApplyStyleCommand.h"
42
43 namespace WebCore {
44
45 using namespace HTMLNames;
46
InsertParagraphSeparatorCommand(Document * document,bool mustUseDefaultParagraphElement)47 InsertParagraphSeparatorCommand::InsertParagraphSeparatorCommand(Document *document, bool mustUseDefaultParagraphElement)
48 : CompositeEditCommand(document)
49 , m_mustUseDefaultParagraphElement(mustUseDefaultParagraphElement)
50 {
51 }
52
preservesTypingStyle() const53 bool InsertParagraphSeparatorCommand::preservesTypingStyle() const
54 {
55 return true;
56 }
57
calculateStyleBeforeInsertion(const Position & pos)58 void InsertParagraphSeparatorCommand::calculateStyleBeforeInsertion(const Position &pos)
59 {
60 // It is only important to set a style to apply later if we're at the boundaries of
61 // a paragraph. Otherwise, content that is moved as part of the work of the command
62 // will lend their styles to the new paragraph without any extra work needed.
63 VisiblePosition visiblePos(pos, VP_DEFAULT_AFFINITY);
64 if (!isStartOfParagraph(visiblePos) && !isEndOfParagraph(visiblePos))
65 return;
66
67 m_style = editingStyleAtPosition(pos, IncludeTypingStyle);
68 }
69
applyStyleAfterInsertion(Node * originalEnclosingBlock)70 void InsertParagraphSeparatorCommand::applyStyleAfterInsertion(Node* originalEnclosingBlock)
71 {
72 // Not only do we break out of header tags, but we also do not preserve the typing style,
73 // in order to match other browsers.
74 if (originalEnclosingBlock->hasTagName(h1Tag) ||
75 originalEnclosingBlock->hasTagName(h2Tag) ||
76 originalEnclosingBlock->hasTagName(h3Tag) ||
77 originalEnclosingBlock->hasTagName(h4Tag) ||
78 originalEnclosingBlock->hasTagName(h5Tag))
79 return;
80
81 if (!m_style)
82 return;
83
84 prepareEditingStyleToApplyAt(m_style.get(), endingSelection().start());
85
86 if (m_style->length() > 0)
87 applyStyle(m_style.get());
88 }
89
shouldUseDefaultParagraphElement(Node * enclosingBlock) const90 bool InsertParagraphSeparatorCommand::shouldUseDefaultParagraphElement(Node* enclosingBlock) const
91 {
92 if (m_mustUseDefaultParagraphElement)
93 return true;
94
95 // Assumes that if there was a range selection, it was already deleted.
96 if (!isEndOfBlock(endingSelection().visibleStart()))
97 return false;
98
99 return enclosingBlock->hasTagName(h1Tag) ||
100 enclosingBlock->hasTagName(h2Tag) ||
101 enclosingBlock->hasTagName(h3Tag) ||
102 enclosingBlock->hasTagName(h4Tag) ||
103 enclosingBlock->hasTagName(h5Tag);
104 }
105
doApply()106 void InsertParagraphSeparatorCommand::doApply()
107 {
108 bool splitText = false;
109 if (endingSelection().isNone())
110 return;
111
112 Position insertionPosition = endingSelection().start();
113
114 EAffinity affinity = endingSelection().affinity();
115
116 // Delete the current selection.
117 if (endingSelection().isRange()) {
118 calculateStyleBeforeInsertion(insertionPosition);
119 deleteSelection(false, true);
120 insertionPosition = endingSelection().start();
121 affinity = endingSelection().affinity();
122 }
123
124 // FIXME: The rangeCompliantEquivalent conversion needs to be moved into enclosingBlock.
125 Node* startBlockNode = enclosingBlock(rangeCompliantEquivalent(insertionPosition).node());
126 Position canonicalPos = VisiblePosition(insertionPosition).deepEquivalent();
127 Element* startBlock = static_cast<Element*>(startBlockNode);
128 if (!startBlockNode
129 || !startBlockNode->isElementNode()
130 || !startBlock->parentNode()
131 || isTableCell(startBlock)
132 || startBlock->hasTagName(formTag)
133 || (canonicalPos.node()->renderer() && canonicalPos.node()->renderer()->isTable())
134 || canonicalPos.node()->hasTagName(hrTag)) {
135 applyCommandToComposite(InsertLineBreakCommand::create(document()));
136 return;
137 }
138
139 // Use the leftmost candidate.
140 insertionPosition = insertionPosition.upstream();
141 if (!insertionPosition.isCandidate())
142 insertionPosition = insertionPosition.downstream();
143
144 // Adjust the insertion position after the delete
145 insertionPosition = positionAvoidingSpecialElementBoundary(insertionPosition);
146 VisiblePosition visiblePos(insertionPosition, affinity);
147 calculateStyleBeforeInsertion(insertionPosition);
148
149 //---------------------------------------------------------------------
150 // Handle special case of typing return on an empty list item
151 if (breakOutOfEmptyListItem())
152 return;
153
154 //---------------------------------------------------------------------
155 // Prepare for more general cases.
156
157 bool isFirstInBlock = isStartOfBlock(visiblePos);
158 bool isLastInBlock = isEndOfBlock(visiblePos);
159 bool nestNewBlock = false;
160
161 // Create block to be inserted.
162 RefPtr<Element> blockToInsert;
163 if (startBlock == startBlock->rootEditableElement()) {
164 blockToInsert = createDefaultParagraphElement(document());
165 nestNewBlock = true;
166 } else if (shouldUseDefaultParagraphElement(startBlock))
167 blockToInsert = createDefaultParagraphElement(document());
168 else
169 blockToInsert = startBlock->cloneElementWithoutChildren();
170
171 //---------------------------------------------------------------------
172 // Handle case when position is in the last visible position in its block,
173 // including when the block is empty.
174 if (isLastInBlock) {
175 bool shouldApplyStyleAfterInsertion = true;
176 if (nestNewBlock) {
177 if (isFirstInBlock && !lineBreakExistsAtVisiblePosition(visiblePos)) {
178 // The block is empty. Create an empty block to
179 // represent the paragraph that we're leaving.
180 RefPtr<Element> extraBlock = createDefaultParagraphElement(document());
181 appendNode(extraBlock, startBlock);
182 appendBlockPlaceholder(extraBlock);
183 }
184 appendNode(blockToInsert, startBlock);
185 } else {
186 // We can get here if we pasted a copied portion of a blockquote with a newline at the end and are trying to paste it
187 // into an unquoted area. We then don't want the newline within the blockquote or else it will also be quoted.
188 if (Node* highestBlockquote = highestEnclosingNodeOfType(canonicalPos, &isMailBlockquote)) {
189 startBlock = static_cast<Element*>(highestBlockquote);
190 // When inserting the newline after the blockquote, we don't want to apply the original style after the insertion
191 shouldApplyStyleAfterInsertion = false;
192 }
193 insertNodeAfter(blockToInsert, startBlock);
194 }
195
196 appendBlockPlaceholder(blockToInsert);
197 setEndingSelection(VisibleSelection(Position(blockToInsert.get(), 0), DOWNSTREAM));
198 if (shouldApplyStyleAfterInsertion)
199 applyStyleAfterInsertion(startBlock);
200 return;
201 }
202
203 //---------------------------------------------------------------------
204 // Handle case when position is in the first visible position in its block, and
205 // similar case where previous position is in another, presumeably nested, block.
206 if (isFirstInBlock || !inSameBlock(visiblePos, visiblePos.previous())) {
207 Node *refNode;
208 if (isFirstInBlock && !nestNewBlock)
209 refNode = startBlock;
210 else if (insertionPosition.node() == startBlock && nestNewBlock) {
211 refNode = startBlock->childNode(insertionPosition.deprecatedEditingOffset());
212 ASSERT(refNode); // must be true or we'd be in the end of block case
213 } else
214 refNode = insertionPosition.node();
215
216 // find ending selection position easily before inserting the paragraph
217 insertionPosition = insertionPosition.downstream();
218
219 insertNodeBefore(blockToInsert, refNode);
220 appendBlockPlaceholder(blockToInsert.get());
221 setEndingSelection(VisibleSelection(Position(blockToInsert.get(), 0), DOWNSTREAM));
222 applyStyleAfterInsertion(startBlock);
223 setEndingSelection(VisibleSelection(insertionPosition, DOWNSTREAM));
224 return;
225 }
226
227 //---------------------------------------------------------------------
228 // Handle the (more complicated) general case,
229
230 // All of the content in the current block after visiblePos is
231 // about to be wrapped in a new paragraph element. Add a br before
232 // it if visiblePos is at the start of a paragraph so that the
233 // content will move down a line.
234 if (isStartOfParagraph(visiblePos)) {
235 RefPtr<Element> br = createBreakElement(document());
236 insertNodeAt(br.get(), insertionPosition);
237 insertionPosition = positionAfterNode(br.get());
238 }
239
240 // Move downstream. Typing style code will take care of carrying along the
241 // style of the upstream position.
242 insertionPosition = insertionPosition.downstream();
243
244 // At this point, the insertionPosition's node could be a container, and we want to make sure we include
245 // all of the correct nodes when building the ancestor list. So this needs to be the deepest representation of the position
246 // before we walk the DOM tree.
247 insertionPosition = VisiblePosition(insertionPosition).deepEquivalent();
248
249 // Build up list of ancestors in between the start node and the start block.
250 Vector<Element*> ancestors;
251 if (insertionPosition.node() != startBlock) {
252 for (Element* n = insertionPosition.node()->parentElement(); n && n != startBlock; n = n->parentElement())
253 ancestors.append(n);
254 }
255
256 // Make sure we do not cause a rendered space to become unrendered.
257 // FIXME: We need the affinity for pos, but pos.downstream() does not give it
258 Position leadingWhitespace = insertionPosition.leadingWhitespacePosition(VP_DEFAULT_AFFINITY);
259 // FIXME: leadingWhitespacePosition is returning the position before preserved newlines for positions
260 // after the preserved newline, causing the newline to be turned into a nbsp.
261 if (leadingWhitespace.isNotNull()) {
262 Text* textNode = static_cast<Text*>(leadingWhitespace.node());
263 ASSERT(!textNode->renderer() || textNode->renderer()->style()->collapseWhiteSpace());
264 replaceTextInNode(textNode, leadingWhitespace.deprecatedEditingOffset(), 1, nonBreakingSpaceString());
265 }
266
267 // Split at pos if in the middle of a text node.
268 if (insertionPosition.node()->isTextNode()) {
269 Text* textNode = static_cast<Text*>(insertionPosition.node());
270 bool atEnd = (unsigned)insertionPosition.deprecatedEditingOffset() >= textNode->length();
271 if (insertionPosition.deprecatedEditingOffset() > 0 && !atEnd) {
272 splitTextNode(textNode, insertionPosition.deprecatedEditingOffset());
273 insertionPosition.moveToOffset(0);
274 visiblePos = VisiblePosition(insertionPosition);
275 splitText = true;
276 }
277 }
278
279 // Put the added block in the tree.
280 if (nestNewBlock)
281 appendNode(blockToInsert.get(), startBlock);
282 else
283 insertNodeAfter(blockToInsert.get(), startBlock);
284
285 updateLayout();
286
287 // Make clones of ancestors in between the start node and the start block.
288 RefPtr<Element> parent = blockToInsert;
289 for (size_t i = ancestors.size(); i != 0; --i) {
290 RefPtr<Element> child = ancestors[i - 1]->cloneElementWithoutChildren();
291 appendNode(child, parent);
292 parent = child.release();
293 }
294
295 // If the paragraph separator was inserted at the end of a paragraph, an empty line must be
296 // created. All of the nodes, starting at visiblePos, are about to be added to the new paragraph
297 // element. If the first node to be inserted won't be one that will hold an empty line open, add a br.
298 if (isEndOfParagraph(visiblePos) && !lineBreakExistsAtVisiblePosition(visiblePos))
299 appendNode(createBreakElement(document()).get(), blockToInsert.get());
300
301 // Move the start node and the siblings of the start node.
302 if (insertionPosition.node() != startBlock) {
303 Node* n = insertionPosition.node();
304 if (insertionPosition.deprecatedEditingOffset() >= caretMaxOffset(n))
305 n = n->nextSibling();
306
307 while (n && n != blockToInsert) {
308 Node *next = n->nextSibling();
309 removeNode(n);
310 appendNode(n, parent.get());
311 n = next;
312 }
313 }
314
315 // Move everything after the start node.
316 if (!ancestors.isEmpty()) {
317 Element* leftParent = ancestors.first();
318 while (leftParent && leftParent != startBlock) {
319 parent = parent->parentElement();
320 if (!parent)
321 break;
322 Node* n = leftParent->nextSibling();
323 while (n && n != blockToInsert) {
324 Node* next = n->nextSibling();
325 removeNode(n);
326 appendNode(n, parent.get());
327 n = next;
328 }
329 leftParent = leftParent->parentElement();
330 }
331 }
332
333 // Handle whitespace that occurs after the split
334 if (splitText) {
335 updateLayout();
336 insertionPosition = Position(insertionPosition.node(), 0);
337 if (!insertionPosition.isRenderedCharacter()) {
338 // Clear out all whitespace and insert one non-breaking space
339 ASSERT(insertionPosition.node()->isTextNode());
340 ASSERT(!insertionPosition.node()->renderer() || insertionPosition.node()->renderer()->style()->collapseWhiteSpace());
341 deleteInsignificantTextDownstream(insertionPosition);
342 insertTextIntoNode(static_cast<Text*>(insertionPosition.node()), 0, nonBreakingSpaceString());
343 }
344 }
345
346 setEndingSelection(VisibleSelection(Position(blockToInsert.get(), 0), DOWNSTREAM));
347 applyStyleAfterInsertion(startBlock);
348 }
349
350 } // namespace WebCore
351