OLD | NEW |
| (Empty) |
1 /* | |
2 * Copyright (C) 2005, 2006, 2008 Apple Inc. All rights reserved. | |
3 * Copyright (C) 2009, 2010, 2011 Google Inc. All rights reserved. | |
4 * | |
5 * Redistribution and use in source and binary forms, with or without | |
6 * modification, are permitted provided that the following conditions | |
7 * are met: | |
8 * 1. Redistributions of source code must retain the above copyright | |
9 * notice, this list of conditions and the following disclaimer. | |
10 * 2. Redistributions in binary form must reproduce the above copyright | |
11 * notice, this list of conditions and the following disclaimer in the | |
12 * documentation and/or other materials provided with the distribution. | |
13 * | |
14 * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY | |
15 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |
16 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR | |
17 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR | |
18 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, | |
19 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, | |
20 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR | |
21 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY | |
22 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
23 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
24 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
25 */ | |
26 | |
27 #include "config.h" | |
28 #include "core/editing/ReplaceSelectionCommand.h" | |
29 | |
30 #include "bindings/core/v8/ExceptionStatePlaceholder.h" | |
31 #include "core/CSSPropertyNames.h" | |
32 #include "core/HTMLNames.h" | |
33 #include "core/InputTypeNames.h" | |
34 #include "core/css/CSSStyleDeclaration.h" | |
35 #include "core/css/StylePropertySet.h" | |
36 #include "core/dom/Document.h" | |
37 #include "core/dom/DocumentFragment.h" | |
38 #include "core/dom/Element.h" | |
39 #include "core/dom/Text.h" | |
40 #include "core/editing/ApplyStyleCommand.h" | |
41 #include "core/editing/BreakBlockquoteCommand.h" | |
42 #include "core/editing/EditingUtilities.h" | |
43 #include "core/editing/FrameSelection.h" | |
44 #include "core/editing/SimplifyMarkupCommand.h" | |
45 #include "core/editing/SmartReplace.h" | |
46 #include "core/editing/VisibleUnits.h" | |
47 #include "core/editing/iterators/TextIterator.h" | |
48 #include "core/editing/serializers/HTMLInterchange.h" | |
49 #include "core/editing/serializers/Serialization.h" | |
50 #include "core/events/BeforeTextInsertedEvent.h" | |
51 #include "core/frame/LocalFrame.h" | |
52 #include "core/frame/UseCounter.h" | |
53 #include "core/html/HTMLBRElement.h" | |
54 #include "core/html/HTMLElement.h" | |
55 #include "core/html/HTMLInputElement.h" | |
56 #include "core/html/HTMLLIElement.h" | |
57 #include "core/html/HTMLQuoteElement.h" | |
58 #include "core/html/HTMLSelectElement.h" | |
59 #include "core/html/HTMLSpanElement.h" | |
60 #include "core/layout/LayoutObject.h" | |
61 #include "core/layout/LayoutText.h" | |
62 #include "wtf/StdLibExtras.h" | |
63 #include "wtf/Vector.h" | |
64 | |
65 namespace blink { | |
66 | |
67 using namespace HTMLNames; | |
68 | |
69 enum EFragmentType { EmptyFragment, SingleTextNodeFragment, TreeFragment }; | |
70 | |
71 // --- ReplacementFragment helper class | |
72 | |
73 class ReplacementFragment final { | |
74 WTF_MAKE_NONCOPYABLE(ReplacementFragment); | |
75 STACK_ALLOCATED(); | |
76 public: | |
77 ReplacementFragment(Document*, DocumentFragment*, const VisibleSelection&); | |
78 | |
79 Node* firstChild() const; | |
80 Node* lastChild() const; | |
81 | |
82 bool isEmpty() const; | |
83 | |
84 bool hasInterchangeNewlineAtStart() const { return m_hasInterchangeNewlineAt
Start; } | |
85 bool hasInterchangeNewlineAtEnd() const { return m_hasInterchangeNewlineAtEn
d; } | |
86 | |
87 void removeNode(PassRefPtrWillBeRawPtr<Node>); | |
88 void removeNodePreservingChildren(PassRefPtrWillBeRawPtr<ContainerNode>); | |
89 | |
90 private: | |
91 PassRefPtrWillBeRawPtr<HTMLElement> insertFragmentForTestRendering(Element*
rootEditableElement); | |
92 void removeUnrenderedNodes(ContainerNode*); | |
93 void restoreAndRemoveTestRenderingNodesToFragment(Element*); | |
94 void removeInterchangeNodes(ContainerNode*); | |
95 | |
96 void insertNodeBefore(PassRefPtrWillBeRawPtr<Node>, Node* refNode); | |
97 | |
98 RefPtrWillBeMember<Document> m_document; | |
99 RefPtrWillBeMember<DocumentFragment> m_fragment; | |
100 bool m_hasInterchangeNewlineAtStart; | |
101 bool m_hasInterchangeNewlineAtEnd; | |
102 }; | |
103 | |
104 static bool isInterchangeHTMLBRElement(const Node* node) | |
105 { | |
106 DEFINE_STATIC_LOCAL(String, interchangeNewlineClassString, (AppleInterchange
Newline)); | |
107 if (!isHTMLBRElement(node) || toHTMLBRElement(node)->getAttribute(classAttr)
!= interchangeNewlineClassString) | |
108 return false; | |
109 UseCounter::count(node->document(), UseCounter::EditingAppleInterchangeNewli
ne); | |
110 return true; | |
111 } | |
112 | |
113 static bool isHTMLInterchangeConvertedSpaceSpan(const Node* node) | |
114 { | |
115 DEFINE_STATIC_LOCAL(String, convertedSpaceSpanClassString, (AppleConvertedSp
ace)); | |
116 if (!node->isHTMLElement() || toHTMLElement(node)->getAttribute(classAttr) !
= convertedSpaceSpanClassString) | |
117 return false; | |
118 UseCounter::count(node->document(), UseCounter::EditingAppleConvertedSpace); | |
119 return true; | |
120 } | |
121 | |
122 static Position positionAvoidingPrecedingNodes(Position pos) | |
123 { | |
124 // If we're already on a break, it's probably a placeholder and we shouldn't
change our position. | |
125 if (editingIgnoresContent(pos.anchorNode())) | |
126 return pos; | |
127 | |
128 // We also stop when changing block flow elements because even though the vi
sual position is the | |
129 // same. E.g., | |
130 // <div>foo^</div>^ | |
131 // The two positions above are the same visual position, but we want to stay
in the same block. | |
132 Element* enclosingBlockElement = enclosingBlock(pos.computeContainerNode()); | |
133 for (Position nextPosition = pos; nextPosition.computeContainerNode() != enc
losingBlockElement; pos = nextPosition) { | |
134 if (lineBreakExistsAtPosition(pos)) | |
135 break; | |
136 | |
137 if (pos.computeContainerNode()->nonShadowBoundaryParentNode()) | |
138 nextPosition = positionInParentAfterNode(*pos.computeContainerNode()
); | |
139 | |
140 if (nextPosition == pos | |
141 || enclosingBlock(nextPosition.computeContainerNode()) != enclosingB
lockElement | |
142 || VisiblePosition(pos).deepEquivalent() != VisiblePosition(nextPosi
tion).deepEquivalent()) | |
143 break; | |
144 } | |
145 return pos; | |
146 } | |
147 | |
148 ReplacementFragment::ReplacementFragment(Document* document, DocumentFragment* f
ragment, const VisibleSelection& selection) | |
149 : m_document(document) | |
150 , m_fragment(fragment) | |
151 , m_hasInterchangeNewlineAtStart(false) | |
152 , m_hasInterchangeNewlineAtEnd(false) | |
153 { | |
154 if (!m_document) | |
155 return; | |
156 if (!m_fragment || !m_fragment->hasChildren()) | |
157 return; | |
158 | |
159 RefPtrWillBeRawPtr<Element> editableRoot = selection.rootEditableElement(); | |
160 ASSERT(editableRoot); | |
161 if (!editableRoot) | |
162 return; | |
163 | |
164 Element* shadowAncestorElement; | |
165 if (editableRoot->isInShadowTree()) | |
166 shadowAncestorElement = editableRoot->shadowHost(); | |
167 else | |
168 shadowAncestorElement = editableRoot.get(); | |
169 | |
170 if (!editableRoot->getAttributeEventListener(EventTypeNames::webkitBeforeTex
tInserted) | |
171 // FIXME: Remove these checks once textareas and textfields actually reg
ister an event handler. | |
172 && !(shadowAncestorElement && shadowAncestorElement->layoutObject() && s
hadowAncestorElement->layoutObject()->isTextControl()) | |
173 && editableRoot->layoutObjectIsRichlyEditable()) { | |
174 removeInterchangeNodes(m_fragment.get()); | |
175 return; | |
176 } | |
177 | |
178 RefPtrWillBeRawPtr<HTMLElement> holder = insertFragmentForTestRendering(edit
ableRoot.get()); | |
179 if (!holder) { | |
180 removeInterchangeNodes(m_fragment.get()); | |
181 return; | |
182 } | |
183 | |
184 const EphemeralRange range = VisibleSelection::selectionFromContentsOfNode(h
older.get()).toNormalizedEphemeralRange(); | |
185 String text = plainText(range, static_cast<TextIteratorBehavior>(TextIterato
rEmitsOriginalText | TextIteratorIgnoresStyleVisibility)); | |
186 | |
187 removeInterchangeNodes(holder.get()); | |
188 removeUnrenderedNodes(holder.get()); | |
189 restoreAndRemoveTestRenderingNodesToFragment(holder.get()); | |
190 | |
191 // Give the root a chance to change the text. | |
192 RefPtrWillBeRawPtr<BeforeTextInsertedEvent> evt = BeforeTextInsertedEvent::c
reate(text); | |
193 editableRoot->dispatchEvent(evt); | |
194 if (text != evt->text() || !editableRoot->layoutObjectIsRichlyEditable()) { | |
195 restoreAndRemoveTestRenderingNodesToFragment(holder.get()); | |
196 | |
197 m_fragment = createFragmentFromText(selection.toNormalizedEphemeralRange
(), evt->text()); | |
198 if (!m_fragment->hasChildren()) | |
199 return; | |
200 | |
201 holder = insertFragmentForTestRendering(editableRoot.get()); | |
202 removeInterchangeNodes(holder.get()); | |
203 removeUnrenderedNodes(holder.get()); | |
204 restoreAndRemoveTestRenderingNodesToFragment(holder.get()); | |
205 } | |
206 } | |
207 | |
208 bool ReplacementFragment::isEmpty() const | |
209 { | |
210 return (!m_fragment || !m_fragment->hasChildren()) && !m_hasInterchangeNewli
neAtStart && !m_hasInterchangeNewlineAtEnd; | |
211 } | |
212 | |
213 Node* ReplacementFragment::firstChild() const | |
214 { | |
215 return m_fragment ? m_fragment->firstChild() : 0; | |
216 } | |
217 | |
218 Node* ReplacementFragment::lastChild() const | |
219 { | |
220 return m_fragment ? m_fragment->lastChild() : 0; | |
221 } | |
222 | |
223 void ReplacementFragment::removeNodePreservingChildren(PassRefPtrWillBeRawPtr<Co
ntainerNode> node) | |
224 { | |
225 if (!node) | |
226 return; | |
227 | |
228 while (RefPtrWillBeRawPtr<Node> n = node->firstChild()) { | |
229 removeNode(n); | |
230 insertNodeBefore(n.release(), node.get()); | |
231 } | |
232 removeNode(node); | |
233 } | |
234 | |
235 void ReplacementFragment::removeNode(PassRefPtrWillBeRawPtr<Node> node) | |
236 { | |
237 if (!node) | |
238 return; | |
239 | |
240 ContainerNode* parent = node->nonShadowBoundaryParentNode(); | |
241 if (!parent) | |
242 return; | |
243 | |
244 parent->removeChild(node.get()); | |
245 } | |
246 | |
247 void ReplacementFragment::insertNodeBefore(PassRefPtrWillBeRawPtr<Node> node, No
de* refNode) | |
248 { | |
249 if (!node || !refNode) | |
250 return; | |
251 | |
252 ContainerNode* parent = refNode->nonShadowBoundaryParentNode(); | |
253 if (!parent) | |
254 return; | |
255 | |
256 parent->insertBefore(node, refNode); | |
257 } | |
258 | |
259 PassRefPtrWillBeRawPtr<HTMLElement> ReplacementFragment::insertFragmentForTestRe
ndering(Element* rootEditableElement) | |
260 { | |
261 ASSERT(m_document); | |
262 RefPtrWillBeRawPtr<HTMLElement> holder = createDefaultParagraphElement(*m_do
cument.get()); | |
263 | |
264 holder->appendChild(m_fragment); | |
265 rootEditableElement->appendChild(holder.get()); | |
266 m_document->updateLayoutIgnorePendingStylesheets(); | |
267 | |
268 return holder.release(); | |
269 } | |
270 | |
271 void ReplacementFragment::restoreAndRemoveTestRenderingNodesToFragment(Element*
holder) | |
272 { | |
273 if (!holder) | |
274 return; | |
275 | |
276 while (RefPtrWillBeRawPtr<Node> node = holder->firstChild()) { | |
277 holder->removeChild(node.get()); | |
278 m_fragment->appendChild(node.get()); | |
279 } | |
280 | |
281 removeNode(holder); | |
282 } | |
283 | |
284 void ReplacementFragment::removeUnrenderedNodes(ContainerNode* holder) | |
285 { | |
286 WillBeHeapVector<RefPtrWillBeMember<Node>> unrendered; | |
287 | |
288 for (Node& node : NodeTraversal::descendantsOf(*holder)) { | |
289 if (!isNodeRendered(node) && !isTableStructureNode(&node)) | |
290 unrendered.append(&node); | |
291 } | |
292 | |
293 for (auto& node : unrendered) | |
294 removeNode(node); | |
295 } | |
296 | |
297 void ReplacementFragment::removeInterchangeNodes(ContainerNode* container) | |
298 { | |
299 m_hasInterchangeNewlineAtStart = false; | |
300 m_hasInterchangeNewlineAtEnd = false; | |
301 | |
302 // Interchange newlines at the "start" of the incoming fragment must be | |
303 // either the first node in the fragment or the first leaf in the fragment. | |
304 Node* node = container->firstChild(); | |
305 while (node) { | |
306 if (isInterchangeHTMLBRElement(node)) { | |
307 m_hasInterchangeNewlineAtStart = true; | |
308 removeNode(node); | |
309 break; | |
310 } | |
311 node = node->firstChild(); | |
312 } | |
313 if (!container->hasChildren()) | |
314 return; | |
315 // Interchange newlines at the "end" of the incoming fragment must be | |
316 // either the last node in the fragment or the last leaf in the fragment. | |
317 node = container->lastChild(); | |
318 while (node) { | |
319 if (isInterchangeHTMLBRElement(node)) { | |
320 m_hasInterchangeNewlineAtEnd = true; | |
321 removeNode(node); | |
322 break; | |
323 } | |
324 node = node->lastChild(); | |
325 } | |
326 | |
327 node = container->firstChild(); | |
328 while (node) { | |
329 RefPtrWillBeRawPtr<Node> next = NodeTraversal::next(*node); | |
330 if (isHTMLInterchangeConvertedSpaceSpan(node)) { | |
331 HTMLElement& element = toHTMLElement(*node); | |
332 next = NodeTraversal::nextSkippingChildren(element); | |
333 removeNodePreservingChildren(&element); | |
334 } | |
335 node = next.get(); | |
336 } | |
337 } | |
338 | |
339 inline void ReplaceSelectionCommand::InsertedNodes::respondToNodeInsertion(Node&
node) | |
340 { | |
341 if (!m_firstNodeInserted) | |
342 m_firstNodeInserted = &node; | |
343 | |
344 m_lastNodeInserted = &node; | |
345 } | |
346 | |
347 inline void ReplaceSelectionCommand::InsertedNodes::willRemoveNodePreservingChil
dren(Node& node) | |
348 { | |
349 if (m_firstNodeInserted.get() == node) | |
350 m_firstNodeInserted = NodeTraversal::next(node); | |
351 if (m_lastNodeInserted.get() == node) | |
352 m_lastNodeInserted = node.lastChild() ? node.lastChild() : NodeTraversal
::nextSkippingChildren(node); | |
353 } | |
354 | |
355 inline void ReplaceSelectionCommand::InsertedNodes::willRemoveNode(Node& node) | |
356 { | |
357 if (m_firstNodeInserted.get() == node && m_lastNodeInserted.get() == node) { | |
358 m_firstNodeInserted = nullptr; | |
359 m_lastNodeInserted = nullptr; | |
360 } else if (m_firstNodeInserted.get() == node) { | |
361 m_firstNodeInserted = NodeTraversal::nextSkippingChildren(*m_firstNodeIn
serted); | |
362 } else if (m_lastNodeInserted.get() == node) { | |
363 m_lastNodeInserted = NodeTraversal::previousSkippingChildren(*m_lastNode
Inserted); | |
364 } | |
365 } | |
366 | |
367 inline void ReplaceSelectionCommand::InsertedNodes::didReplaceNode(Node& node, N
ode& newNode) | |
368 { | |
369 if (m_firstNodeInserted.get() == node) | |
370 m_firstNodeInserted = &newNode; | |
371 if (m_lastNodeInserted.get() == node) | |
372 m_lastNodeInserted = &newNode; | |
373 } | |
374 | |
375 ReplaceSelectionCommand::ReplaceSelectionCommand(Document& document, PassRefPtrW
illBeRawPtr<DocumentFragment> fragment, CommandOptions options, EditAction editA
ction) | |
376 : CompositeEditCommand(document) | |
377 , m_selectReplacement(options & SelectReplacement) | |
378 , m_smartReplace(options & SmartReplace) | |
379 , m_matchStyle(options & MatchStyle) | |
380 , m_documentFragment(fragment) | |
381 , m_preventNesting(options & PreventNesting) | |
382 , m_movingParagraph(options & MovingParagraph) | |
383 , m_editAction(editAction) | |
384 , m_sanitizeFragment(options & SanitizeFragment) | |
385 , m_shouldMergeEnd(false) | |
386 { | |
387 } | |
388 | |
389 static bool hasMatchingQuoteLevel(VisiblePosition endOfExistingContent, VisibleP
osition endOfInsertedContent) | |
390 { | |
391 Position existing = endOfExistingContent.deepEquivalent(); | |
392 Position inserted = endOfInsertedContent.deepEquivalent(); | |
393 bool isInsideMailBlockquote = enclosingNodeOfType(inserted, isMailHTMLBlockq
uoteElement, CanCrossEditingBoundary); | |
394 return isInsideMailBlockquote && (numEnclosingMailBlockquotes(existing) == n
umEnclosingMailBlockquotes(inserted)); | |
395 } | |
396 | |
397 bool ReplaceSelectionCommand::shouldMergeStart(bool selectionStartWasStartOfPara
graph, bool fragmentHasInterchangeNewlineAtStart, bool selectionStartWasInsideMa
ilBlockquote) | |
398 { | |
399 if (m_movingParagraph) | |
400 return false; | |
401 | |
402 VisiblePosition startOfInsertedContent(positionAtStartOfInsertedContent()); | |
403 VisiblePosition prev = startOfInsertedContent.previous(CannotCrossEditingBou
ndary); | |
404 if (prev.isNull()) | |
405 return false; | |
406 | |
407 // When we have matching quote levels, its ok to merge more frequently. | |
408 // For a successful merge, we still need to make sure that the inserted cont
ent starts with the beginning of a paragraph. | |
409 // And we should only merge here if the selection start was inside a mail bl
ockquote. This prevents against removing a | |
410 // blockquote from newly pasted quoted content that was pasted into an unquo
ted position. If that unquoted position happens | |
411 // to be right after another blockquote, we don't want to merge and risk str
ipping a valid block (and newline) from the pasted content. | |
412 if (isStartOfParagraph(startOfInsertedContent) && selectionStartWasInsideMai
lBlockquote && hasMatchingQuoteLevel(prev, positionAtEndOfInsertedContent())) | |
413 return true; | |
414 | |
415 return !selectionStartWasStartOfParagraph | |
416 && !fragmentHasInterchangeNewlineAtStart | |
417 && isStartOfParagraph(startOfInsertedContent) | |
418 && !isHTMLBRElement(*startOfInsertedContent.deepEquivalent().anchorNode(
)) | |
419 && shouldMerge(startOfInsertedContent, prev); | |
420 } | |
421 | |
422 bool ReplaceSelectionCommand::shouldMergeEnd(bool selectionEndWasEndOfParagraph) | |
423 { | |
424 VisiblePosition endOfInsertedContent(positionAtEndOfInsertedContent()); | |
425 VisiblePosition next = endOfInsertedContent.next(CannotCrossEditingBoundary)
; | |
426 if (next.isNull()) | |
427 return false; | |
428 | |
429 return !selectionEndWasEndOfParagraph | |
430 && isEndOfParagraph(endOfInsertedContent) | |
431 && !isHTMLBRElement(*endOfInsertedContent.deepEquivalent().anchorNode()) | |
432 && shouldMerge(endOfInsertedContent, next); | |
433 } | |
434 | |
435 static bool isMailPasteAsQuotationHTMLBlockQuoteElement(const Node* node) | |
436 { | |
437 if (!node || !node->isHTMLElement()) | |
438 return false; | |
439 const HTMLElement& element = toHTMLElement(*node); | |
440 if (!element.hasTagName(blockquoteTag) || element.getAttribute(classAttr) !=
ApplePasteAsQuotation) | |
441 return false; | |
442 UseCounter::count(node->document(), UseCounter::EditingApplePasteAsQuotation
); | |
443 return true; | |
444 } | |
445 | |
446 static bool isHTMLHeaderElement(const Node* a) | |
447 { | |
448 if (!a || !a->isHTMLElement()) | |
449 return false; | |
450 | |
451 const HTMLElement& element = toHTMLElement(*a); | |
452 return element.hasTagName(h1Tag) | |
453 || element.hasTagName(h2Tag) | |
454 || element.hasTagName(h3Tag) | |
455 || element.hasTagName(h4Tag) | |
456 || element.hasTagName(h5Tag) | |
457 || element.hasTagName(h6Tag); | |
458 } | |
459 | |
460 static bool haveSameTagName(Element* a, Element* b) | |
461 { | |
462 return a && b && a->tagName() == b->tagName(); | |
463 } | |
464 | |
465 bool ReplaceSelectionCommand::shouldMerge(const VisiblePosition& source, const V
isiblePosition& destination) | |
466 { | |
467 if (source.isNull() || destination.isNull()) | |
468 return false; | |
469 | |
470 Node* sourceNode = source.deepEquivalent().anchorNode(); | |
471 Node* destinationNode = destination.deepEquivalent().anchorNode(); | |
472 Element* sourceBlock = enclosingBlock(sourceNode); | |
473 Element* destinationBlock = enclosingBlock(destinationNode); | |
474 return !enclosingNodeOfType(source.deepEquivalent(), &isMailPasteAsQuotation
HTMLBlockQuoteElement) | |
475 && sourceBlock && (!sourceBlock->hasTagName(blockquoteTag) || isMailHTML
BlockquoteElement(sourceBlock)) | |
476 && enclosingListChild(sourceBlock) == enclosingListChild(destinationNode
) | |
477 && enclosingTableCell(source.deepEquivalent()) == enclosingTableCell(des
tination.deepEquivalent()) | |
478 && (!isHTMLHeaderElement(sourceBlock) || haveSameTagName(sourceBlock, de
stinationBlock)) | |
479 // Don't merge to or from a position before or after a block because it
would | |
480 // be a no-op and cause infinite recursion. | |
481 && !isBlock(sourceNode) && !isBlock(destinationNode); | |
482 } | |
483 | |
484 // Style rules that match just inserted elements could change their appearance,
like | |
485 // a div inserted into a document with div { display:inline; }. | |
486 void ReplaceSelectionCommand::removeRedundantStylesAndKeepStyleSpanInline(Insert
edNodes& insertedNodes) | |
487 { | |
488 RefPtrWillBeRawPtr<Node> pastEndNode = insertedNodes.pastLastLeaf(); | |
489 RefPtrWillBeRawPtr<Node> next = nullptr; | |
490 for (RefPtrWillBeRawPtr<Node> node = insertedNodes.firstNodeInserted(); node
&& node != pastEndNode; node = next) { | |
491 // FIXME: <rdar://problem/5371536> Style rules that match pasted content
can change it's appearance | |
492 | |
493 next = NodeTraversal::next(*node); | |
494 if (!node->isStyledElement()) | |
495 continue; | |
496 | |
497 Element* element = toElement(node); | |
498 | |
499 const StylePropertySet* inlineStyle = element->inlineStyle(); | |
500 RefPtrWillBeRawPtr<EditingStyle> newInlineStyle = EditingStyle::create(i
nlineStyle); | |
501 if (inlineStyle) { | |
502 if (element->isHTMLElement()) { | |
503 Vector<QualifiedName> attributes; | |
504 HTMLElement* htmlElement = toHTMLElement(element); | |
505 ASSERT(htmlElement); | |
506 | |
507 if (newInlineStyle->conflictsWithImplicitStyleOfElement(htmlElem
ent)) { | |
508 // e.g. <b style="font-weight: normal;"> is converted to <sp
an style="font-weight: normal;"> | |
509 element = replaceElementWithSpanPreservingChildrenAndAttribu
tes(htmlElement); | |
510 inlineStyle = element->inlineStyle(); | |
511 insertedNodes.didReplaceNode(*htmlElement, *element); | |
512 } else if (newInlineStyle->extractConflictingImplicitStyleOfAttr
ibutes(htmlElement, EditingStyle::PreserveWritingDirection, 0, attributes, | |
513 EditingStyle::DoNotExtractMatchingStyle)) { | |
514 // e.g. <font size="3" style="font-size: 20px;"> is converte
d to <font style="font-size: 20px;"> | |
515 for (size_t i = 0; i < attributes.size(); i++) | |
516 removeElementAttribute(htmlElement, attributes[i]); | |
517 } | |
518 } | |
519 | |
520 ContainerNode* context = element->parentNode(); | |
521 | |
522 // If Mail wraps the fragment with a Paste as Quotation blockquote,
or if you're pasting into a quoted region, | |
523 // styles from blockquoteNode are allowed to override those from the
source document, see <rdar://problem/4930986> and <rdar://problem/5089327>. | |
524 HTMLQuoteElement* blockquoteElement = !context || isMailPasteAsQuota
tionHTMLBlockQuoteElement(context) ? | |
525 toHTMLQuoteElement(context) : | |
526 toHTMLQuoteElement(enclosingNodeOfType(firstPositionInNode(conte
xt), isMailHTMLBlockquoteElement, CanCrossEditingBoundary)); | |
527 if (blockquoteElement) | |
528 newInlineStyle->removeStyleFromRulesAndContext(element, document
().documentElement()); | |
529 | |
530 newInlineStyle->removeStyleFromRulesAndContext(element, context); | |
531 } | |
532 | |
533 if (!inlineStyle || newInlineStyle->isEmpty()) { | |
534 if (isStyleSpanOrSpanWithOnlyStyleAttribute(element) || isEmptyFontT
ag(element, AllowNonEmptyStyleAttribute)) { | |
535 insertedNodes.willRemoveNodePreservingChildren(*element); | |
536 removeNodePreservingChildren(element); | |
537 continue; | |
538 } | |
539 removeElementAttribute(element, styleAttr); | |
540 } else if (newInlineStyle->style()->propertyCount() != inlineStyle->prop
ertyCount()) { | |
541 setNodeAttribute(element, styleAttr, AtomicString(newInlineStyle->st
yle()->asText())); | |
542 } | |
543 | |
544 // FIXME: Tolerate differences in id, class, and style attributes. | |
545 if (element->parentNode() && isNonTableCellHTMLBlockElement(element) &&
areIdenticalElements(element, element->parentNode()) | |
546 && VisiblePosition(firstPositionInNode(element->parentNode())).deepE
quivalent() == VisiblePosition(firstPositionInNode(element)).deepEquivalent() | |
547 && VisiblePosition(lastPositionInNode(element->parentNode())).deepEq
uivalent() == VisiblePosition(lastPositionInNode(element)).deepEquivalent()) { | |
548 insertedNodes.willRemoveNodePreservingChildren(*element); | |
549 removeNodePreservingChildren(element); | |
550 continue; | |
551 } | |
552 | |
553 if (element->parentNode() && element->parentNode()->layoutObjectIsRichly
Editable()) | |
554 removeElementAttribute(element, contenteditableAttr); | |
555 | |
556 // WebKit used to not add display: inline and float: none on copy. | |
557 // Keep this code around for backward compatibility | |
558 if (isLegacyAppleHTMLSpanElement(element)) { | |
559 if (!element->hasChildren()) { | |
560 insertedNodes.willRemoveNodePreservingChildren(*element); | |
561 removeNodePreservingChildren(element); | |
562 continue; | |
563 } | |
564 // There are other styles that style rules can give to style spans, | |
565 // but these are the two important ones because they'll prevent | |
566 // inserted content from appearing in the right paragraph. | |
567 // FIXME: Hyatt is concerned that selectively using display:inline w
ill give inconsistent | |
568 // results. We already know one issue because td elements ignore the
ir display property | |
569 // in quirks mode (which Mail.app is always in). We should look for
an alternative. | |
570 | |
571 // Mutate using the CSSOM wrapper so we get the same event behavior
as a script. | |
572 if (isBlock(element)) | |
573 element->style()->setPropertyInternal(CSSPropertyDisplay, "inlin
e", false, IGNORE_EXCEPTION); | |
574 if (element->layoutObject() && element->layoutObject()->style()->isF
loating()) | |
575 element->style()->setPropertyInternal(CSSPropertyFloat, "none",
false, IGNORE_EXCEPTION); | |
576 } | |
577 } | |
578 } | |
579 | |
580 static bool isProhibitedParagraphChild(const AtomicString& name) | |
581 { | |
582 // https://dvcs.w3.org/hg/editing/raw-file/57abe6d3cb60/editing.html#prohibi
ted-paragraph-child | |
583 DEFINE_STATIC_LOCAL(HashSet<AtomicString>, elements, ()); | |
584 if (elements.isEmpty()) { | |
585 elements.add(addressTag.localName()); | |
586 elements.add(articleTag.localName()); | |
587 elements.add(asideTag.localName()); | |
588 elements.add(blockquoteTag.localName()); | |
589 elements.add(captionTag.localName()); | |
590 elements.add(centerTag.localName()); | |
591 elements.add(colTag.localName()); | |
592 elements.add(colgroupTag.localName()); | |
593 elements.add(ddTag.localName()); | |
594 elements.add(detailsTag.localName()); | |
595 elements.add(dirTag.localName()); | |
596 elements.add(divTag.localName()); | |
597 elements.add(dlTag.localName()); | |
598 elements.add(dtTag.localName()); | |
599 elements.add(fieldsetTag.localName()); | |
600 elements.add(figcaptionTag.localName()); | |
601 elements.add(figureTag.localName()); | |
602 elements.add(footerTag.localName()); | |
603 elements.add(formTag.localName()); | |
604 elements.add(h1Tag.localName()); | |
605 elements.add(h2Tag.localName()); | |
606 elements.add(h3Tag.localName()); | |
607 elements.add(h4Tag.localName()); | |
608 elements.add(h5Tag.localName()); | |
609 elements.add(h6Tag.localName()); | |
610 elements.add(headerTag.localName()); | |
611 elements.add(hgroupTag.localName()); | |
612 elements.add(hrTag.localName()); | |
613 elements.add(liTag.localName()); | |
614 elements.add(listingTag.localName()); | |
615 elements.add(mainTag.localName()); // Missing in the specification. | |
616 elements.add(menuTag.localName()); | |
617 elements.add(navTag.localName()); | |
618 elements.add(olTag.localName()); | |
619 elements.add(pTag.localName()); | |
620 elements.add(plaintextTag.localName()); | |
621 elements.add(preTag.localName()); | |
622 elements.add(sectionTag.localName()); | |
623 elements.add(summaryTag.localName()); | |
624 elements.add(tableTag.localName()); | |
625 elements.add(tbodyTag.localName()); | |
626 elements.add(tdTag.localName()); | |
627 elements.add(tfootTag.localName()); | |
628 elements.add(thTag.localName()); | |
629 elements.add(theadTag.localName()); | |
630 elements.add(trTag.localName()); | |
631 elements.add(ulTag.localName()); | |
632 elements.add(xmpTag.localName()); | |
633 } | |
634 return elements.contains(name); | |
635 } | |
636 | |
637 void ReplaceSelectionCommand::makeInsertedContentRoundTrippableWithHTMLTreeBuild
er(const InsertedNodes& insertedNodes) | |
638 { | |
639 RefPtrWillBeRawPtr<Node> pastEndNode = insertedNodes.pastLastLeaf(); | |
640 RefPtrWillBeRawPtr<Node> next = nullptr; | |
641 for (RefPtrWillBeRawPtr<Node> node = insertedNodes.firstNodeInserted(); node
&& node != pastEndNode; node = next) { | |
642 next = NodeTraversal::next(*node); | |
643 | |
644 if (!node->isHTMLElement()) | |
645 continue; | |
646 | |
647 HTMLElement& element = toHTMLElement(*node); | |
648 if (isProhibitedParagraphChild(element.localName())) { | |
649 if (HTMLElement* paragraphElement = toHTMLElement(enclosingElementWi
thTag(positionInParentBeforeNode(element), pTag))) | |
650 moveElementOutOfAncestor(&element, paragraphElement); | |
651 } | |
652 | |
653 if (isHTMLHeaderElement(&element)) { | |
654 if (HTMLElement* headerElement = toHTMLElement(highestEnclosingNodeO
fType(positionInParentBeforeNode(element), isHTMLHeaderElement))) | |
655 moveElementOutOfAncestor(&element, headerElement); | |
656 } | |
657 } | |
658 } | |
659 | |
660 void ReplaceSelectionCommand::moveElementOutOfAncestor(PassRefPtrWillBeRawPtr<El
ement> prpElement, PassRefPtrWillBeRawPtr<ContainerNode> prpAncestor) | |
661 { | |
662 RefPtrWillBeRawPtr<Element> element = prpElement; | |
663 RefPtrWillBeRawPtr<ContainerNode> ancestor = prpAncestor; | |
664 | |
665 if (!ancestor->parentNode()->hasEditableStyle()) | |
666 return; | |
667 | |
668 VisiblePosition positionAtEndOfNode(lastPositionInOrAfterNode(element.get())
); | |
669 VisiblePosition lastPositionInParagraph(lastPositionInNode(ancestor.get())); | |
670 if (positionAtEndOfNode.deepEquivalent() == lastPositionInParagraph.deepEqui
valent()) { | |
671 removeNode(element); | |
672 if (ancestor->nextSibling()) | |
673 insertNodeBefore(element, ancestor->nextSibling()); | |
674 else | |
675 appendNode(element, ancestor->parentNode()); | |
676 } else { | |
677 RefPtrWillBeRawPtr<Node> nodeToSplitTo = splitTreeToNode(element.get(),
ancestor.get(), true); | |
678 removeNode(element); | |
679 insertNodeBefore(element, nodeToSplitTo); | |
680 } | |
681 if (!ancestor->hasChildren()) | |
682 removeNode(ancestor.release()); | |
683 } | |
684 | |
685 static inline bool nodeHasVisibleLayoutText(Text& text) | |
686 { | |
687 return text.layoutObject() && text.layoutObject()->resolvedTextLength() > 0; | |
688 } | |
689 | |
690 void ReplaceSelectionCommand::removeUnrenderedTextNodesAtEnds(InsertedNodes& ins
ertedNodes) | |
691 { | |
692 document().updateLayoutIgnorePendingStylesheets(); | |
693 | |
694 Node* lastLeafInserted = insertedNodes.lastLeafInserted(); | |
695 if (lastLeafInserted && lastLeafInserted->isTextNode() && !nodeHasVisibleLay
outText(toText(*lastLeafInserted)) | |
696 && !enclosingElementWithTag(firstPositionInOrBeforeNode(lastLeafInserted
), selectTag) | |
697 && !enclosingElementWithTag(firstPositionInOrBeforeNode(lastLeafInserted
), scriptTag)) { | |
698 insertedNodes.willRemoveNode(*lastLeafInserted); | |
699 removeNode(lastLeafInserted); | |
700 } | |
701 | |
702 // We don't have to make sure that firstNodeInserted isn't inside a select o
r script element, because | |
703 // it is a top level node in the fragment and the user can't insert into tho
se elements. | |
704 Node* firstNodeInserted = insertedNodes.firstNodeInserted(); | |
705 if (firstNodeInserted && firstNodeInserted->isTextNode() && !nodeHasVisibleL
ayoutText(toText(*firstNodeInserted))) { | |
706 insertedNodes.willRemoveNode(*firstNodeInserted); | |
707 removeNode(firstNodeInserted); | |
708 } | |
709 } | |
710 | |
711 VisiblePosition ReplaceSelectionCommand::positionAtEndOfInsertedContent() const | |
712 { | |
713 // FIXME: Why is this hack here? What's special about <select> tags? | |
714 HTMLSelectElement* enclosingSelect = toHTMLSelectElement(enclosingElementWit
hTag(m_endOfInsertedContent, selectTag)); | |
715 return VisiblePosition(enclosingSelect ? lastPositionInOrAfterNode(enclosing
Select) : m_endOfInsertedContent); | |
716 } | |
717 | |
718 VisiblePosition ReplaceSelectionCommand::positionAtStartOfInsertedContent() cons
t | |
719 { | |
720 return VisiblePosition(m_startOfInsertedContent); | |
721 } | |
722 | |
723 static void removeHeadContents(ReplacementFragment& fragment) | |
724 { | |
725 Node* next = nullptr; | |
726 for (Node* node = fragment.firstChild(); node; node = next) { | |
727 if (isHTMLBaseElement(*node) | |
728 || isHTMLLinkElement(*node) | |
729 || isHTMLMetaElement(*node) | |
730 || isHTMLStyleElement(*node) | |
731 || isHTMLTitleElement(*node)) { | |
732 next = NodeTraversal::nextSkippingChildren(*node); | |
733 fragment.removeNode(node); | |
734 } else { | |
735 next = NodeTraversal::next(*node); | |
736 } | |
737 } | |
738 } | |
739 | |
740 // Remove style spans before insertion if they are unnecessary. It's faster bec
ause we'll | |
741 // avoid doing a layout. | |
742 static bool handleStyleSpansBeforeInsertion(ReplacementFragment& fragment, const
Position& insertionPos) | |
743 { | |
744 Node* topNode = fragment.firstChild(); | |
745 | |
746 // Handling the case where we are doing Paste as Quotation or pasting into q
uoted content is more complicated (see handleStyleSpans) | |
747 // and doesn't receive the optimization. | |
748 if (isMailPasteAsQuotationHTMLBlockQuoteElement(topNode) || enclosingNodeOfT
ype(firstPositionInOrBeforeNode(topNode), isMailHTMLBlockquoteElement, CanCrossE
ditingBoundary)) | |
749 return false; | |
750 | |
751 // Either there are no style spans in the fragment or a WebKit client has ad
ded content to the fragment | |
752 // before inserting it. Look for and handle style spans after insertion. | |
753 if (!isLegacyAppleHTMLSpanElement(topNode)) | |
754 return false; | |
755 | |
756 HTMLSpanElement* wrappingStyleSpan = toHTMLSpanElement(topNode); | |
757 RefPtrWillBeRawPtr<EditingStyle> styleAtInsertionPos = EditingStyle::create(
insertionPos.parentAnchoredEquivalent()); | |
758 String styleText = styleAtInsertionPos->style()->asText(); | |
759 | |
760 // FIXME: This string comparison is a naive way of comparing two styles. | |
761 // We should be taking the diff and check that the diff is empty. | |
762 if (styleText != wrappingStyleSpan->getAttribute(styleAttr)) | |
763 return false; | |
764 | |
765 fragment.removeNodePreservingChildren(wrappingStyleSpan); | |
766 return true; | |
767 } | |
768 | |
769 // At copy time, WebKit wraps copied content in a span that contains the source
document's | |
770 // default styles. If the copied Range inherits any other styles from its ances
tors, we put | |
771 // those styles on a second span. | |
772 // This function removes redundant styles from those spans, and removes the span
s if all their | |
773 // styles are redundant. | |
774 // We should remove the Apple-style-span class when we're done, see <rdar://prob
lem/5685600>. | |
775 // We should remove styles from spans that are overridden by all of their childr
en, either here | |
776 // or at copy time. | |
777 void ReplaceSelectionCommand::handleStyleSpans(InsertedNodes& insertedNodes) | |
778 { | |
779 HTMLSpanElement* wrappingStyleSpan = nullptr; | |
780 // The style span that contains the source document's default style should b
e at | |
781 // the top of the fragment, but Mail sometimes adds a wrapper (for Paste As
Quotation), | |
782 // so search for the top level style span instead of assuming it's at the to
p. | |
783 | |
784 for (Node& node : NodeTraversal::startsAt(insertedNodes.firstNodeInserted())
) { | |
785 if (isLegacyAppleHTMLSpanElement(&node)) { | |
786 wrappingStyleSpan = toHTMLSpanElement(&node); | |
787 break; | |
788 } | |
789 } | |
790 | |
791 // There might not be any style spans if we're pasting from another applicat
ion or if | |
792 // we are here because of a document.execCommand("InsertHTML", ...) call. | |
793 if (!wrappingStyleSpan) | |
794 return; | |
795 | |
796 RefPtrWillBeRawPtr<EditingStyle> style = EditingStyle::create(wrappingStyleS
pan->inlineStyle()); | |
797 ContainerNode* context = wrappingStyleSpan->parentNode(); | |
798 | |
799 // If Mail wraps the fragment with a Paste as Quotation blockquote, or if yo
u're pasting into a quoted region, | |
800 // styles from blockquoteElement are allowed to override those from the sour
ce document, see <rdar://problem/4930986> and <rdar://problem/5089327>. | |
801 HTMLQuoteElement* blockquoteElement = isMailPasteAsQuotationHTMLBlockQuoteEl
ement(context) ? | |
802 toHTMLQuoteElement(context) : | |
803 toHTMLQuoteElement(enclosingNodeOfType(firstPositionInNode(context), isM
ailHTMLBlockquoteElement, CanCrossEditingBoundary)); | |
804 if (blockquoteElement) | |
805 context = document().documentElement(); | |
806 | |
807 // This operation requires that only editing styles to be removed from sourc
eDocumentStyle. | |
808 style->prepareToApplyAt(firstPositionInNode(context)); | |
809 | |
810 // Remove block properties in the span's style. This prevents properties tha
t probably have no effect | |
811 // currently from affecting blocks later if the style is cloned for a new bl
ock element during a future | |
812 // editing operation. | |
813 // FIXME: They *can* have an effect currently if blocks beneath the style sp
an aren't individually marked | |
814 // with block styles by the editing engine used to style them. WebKit doesn
't do this, but others might. | |
815 style->removeBlockProperties(); | |
816 | |
817 if (style->isEmpty() || !wrappingStyleSpan->hasChildren()) { | |
818 insertedNodes.willRemoveNodePreservingChildren(*wrappingStyleSpan); | |
819 removeNodePreservingChildren(wrappingStyleSpan); | |
820 } else { | |
821 setNodeAttribute(wrappingStyleSpan, styleAttr, AtomicString(style->style
()->asText())); | |
822 } | |
823 } | |
824 | |
825 void ReplaceSelectionCommand::mergeEndIfNeeded() | |
826 { | |
827 if (!m_shouldMergeEnd) | |
828 return; | |
829 | |
830 VisiblePosition startOfInsertedContent(positionAtStartOfInsertedContent()); | |
831 VisiblePosition endOfInsertedContent(positionAtEndOfInsertedContent()); | |
832 | |
833 // Bail to avoid infinite recursion. | |
834 if (m_movingParagraph) { | |
835 ASSERT_NOT_REACHED(); | |
836 return; | |
837 } | |
838 | |
839 // Merging two paragraphs will destroy the moved one's block styles. Always
move the end of inserted forward | |
840 // to preserve the block style of the paragraph already in the document, unl
ess the paragraph to move would | |
841 // include the what was the start of the selection that was pasted into, so
that we preserve that paragraph's | |
842 // block styles. | |
843 bool mergeForward = !(inSameParagraph(startOfInsertedContent, endOfInsertedC
ontent) && !isStartOfParagraph(startOfInsertedContent)); | |
844 | |
845 VisiblePosition destination = mergeForward ? endOfInsertedContent.next() : e
ndOfInsertedContent; | |
846 VisiblePosition startOfParagraphToMove = mergeForward ? startOfParagraph(end
OfInsertedContent) : endOfInsertedContent.next(); | |
847 | |
848 // Merging forward could result in deleting the destination anchor node. | |
849 // To avoid this, we add a placeholder node before the start of the paragrap
h. | |
850 if (endOfParagraph(startOfParagraphToMove).deepEquivalent() == destination.d
eepEquivalent()) { | |
851 RefPtrWillBeRawPtr<HTMLBRElement> placeholder = createBreakElement(docum
ent()); | |
852 insertNodeBefore(placeholder, startOfParagraphToMove.deepEquivalent().an
chorNode()); | |
853 destination = VisiblePosition(positionBeforeNode(placeholder.get())); | |
854 } | |
855 | |
856 moveParagraph(startOfParagraphToMove, endOfParagraph(startOfParagraphToMove)
, destination); | |
857 | |
858 // Merging forward will remove m_endOfInsertedContent from the document. | |
859 if (mergeForward) { | |
860 if (m_startOfInsertedContent.isOrphan()) | |
861 m_startOfInsertedContent = endingSelection().visibleStart().deepEqui
valent(); | |
862 m_endOfInsertedContent = endingSelection().visibleEnd().deepEquivalent()
; | |
863 // If we merged text nodes, m_endOfInsertedContent could be null. If | |
864 // this is the case, we use m_startOfInsertedContent. | |
865 if (m_endOfInsertedContent.isNull()) | |
866 m_endOfInsertedContent = m_startOfInsertedContent; | |
867 } | |
868 } | |
869 | |
870 static Node* enclosingInline(Node* node) | |
871 { | |
872 while (ContainerNode* parent = node->parentNode()) { | |
873 if (isBlockFlowElement(*parent) || isHTMLBodyElement(*parent)) | |
874 return node; | |
875 // Stop if any previous sibling is a block. | |
876 for (Node* sibling = node->previousSibling(); sibling; sibling = sibling
->previousSibling()) { | |
877 if (isBlockFlowElement(*sibling)) | |
878 return node; | |
879 } | |
880 node = parent; | |
881 } | |
882 return node; | |
883 } | |
884 | |
885 static bool isInlineHTMLElementWithStyle(const Node* node) | |
886 { | |
887 // We don't want to skip over any block elements. | |
888 if (isBlock(node)) | |
889 return false; | |
890 | |
891 if (!node->isHTMLElement()) | |
892 return false; | |
893 | |
894 // We can skip over elements whose class attribute is | |
895 // one of our internal classes. | |
896 const HTMLElement* element = toHTMLElement(node); | |
897 const AtomicString& classAttributeValue = element->getAttribute(classAttr); | |
898 if (classAttributeValue == AppleTabSpanClass) { | |
899 UseCounter::count(element->document(), UseCounter::EditingAppleTabSpanCl
ass); | |
900 return true; | |
901 } | |
902 if (classAttributeValue == AppleConvertedSpace) { | |
903 UseCounter::count(element->document(), UseCounter::EditingAppleConverted
Space); | |
904 return true; | |
905 } | |
906 if (classAttributeValue == ApplePasteAsQuotation) { | |
907 UseCounter::count(element->document(), UseCounter::EditingApplePasteAsQu
otation); | |
908 return true; | |
909 } | |
910 | |
911 return EditingStyle::elementIsStyledSpanOrHTMLEquivalent(element); | |
912 } | |
913 | |
914 static inline HTMLElement* elementToSplitToAvoidPastingIntoInlineElementsWithSty
le(const Position& insertionPos) | |
915 { | |
916 Element* containingBlock = enclosingBlock(insertionPos.computeContainerNode(
)); | |
917 return toHTMLElement(highestEnclosingNodeOfType(insertionPos, isInlineHTMLEl
ementWithStyle, CannotCrossEditingBoundary, containingBlock)); | |
918 } | |
919 | |
920 void ReplaceSelectionCommand::doApply() | |
921 { | |
922 VisibleSelection selection = endingSelection(); | |
923 ASSERT(selection.isCaretOrRange()); | |
924 ASSERT(selection.start().anchorNode()); | |
925 if (!selection.isNonOrphanedCaretOrRange() || !selection.start().anchorNode(
)) | |
926 return; | |
927 | |
928 if (!selection.rootEditableElement()) | |
929 return; | |
930 | |
931 ReplacementFragment fragment(&document(), m_documentFragment.get(), selectio
n); | |
932 if (performTrivialReplace(fragment)) | |
933 return; | |
934 | |
935 // We can skip matching the style if the selection is plain text. | |
936 if ((selection.start().anchorNode()->layoutObject() && selection.start().anc
horNode()->layoutObject()->style()->userModify() == READ_WRITE_PLAINTEXT_ONLY) | |
937 && (selection.end().anchorNode()->layoutObject() && selection.end().anch
orNode()->layoutObject()->style()->userModify() == READ_WRITE_PLAINTEXT_ONLY)) | |
938 m_matchStyle = false; | |
939 | |
940 if (m_matchStyle) { | |
941 m_insertionStyle = EditingStyle::create(selection.start()); | |
942 m_insertionStyle->mergeTypingStyle(&document()); | |
943 } | |
944 | |
945 VisiblePosition visibleStart = selection.visibleStart(); | |
946 VisiblePosition visibleEnd = selection.visibleEnd(); | |
947 | |
948 bool selectionEndWasEndOfParagraph = isEndOfParagraph(visibleEnd); | |
949 bool selectionStartWasStartOfParagraph = isStartOfParagraph(visibleStart); | |
950 | |
951 Element* enclosingBlockOfVisibleStart = enclosingBlock(visibleStart.deepEqui
valent().anchorNode()); | |
952 | |
953 Position insertionPos = selection.start(); | |
954 bool startIsInsideMailBlockquote = enclosingNodeOfType(insertionPos, isMailH
TMLBlockquoteElement, CanCrossEditingBoundary); | |
955 bool selectionIsPlainText = !selection.isContentRichlyEditable(); | |
956 Element* currentRoot = selection.rootEditableElement(); | |
957 | |
958 if ((selectionStartWasStartOfParagraph && selectionEndWasEndOfParagraph && !
startIsInsideMailBlockquote) | |
959 || enclosingBlockOfVisibleStart == currentRoot | |
960 || isListItem(enclosingBlockOfVisibleStart) | |
961 || selectionIsPlainText) { | |
962 m_preventNesting = false; | |
963 } | |
964 | |
965 if (selection.isRange()) { | |
966 // When the end of the selection being pasted into is at the end of a pa
ragraph, and that selection | |
967 // spans multiple blocks, not merging may leave an empty line. | |
968 // When the start of the selection being pasted into is at the start of
a block, not merging | |
969 // will leave hanging block(s). | |
970 // Merge blocks if the start of the selection was in a Mail blockquote,
since we handle | |
971 // that case specially to prevent nesting. | |
972 bool mergeBlocksAfterDelete = startIsInsideMailBlockquote || isEndOfPara
graph(visibleEnd) || isStartOfBlock(visibleStart); | |
973 // FIXME: We should only expand to include fully selected special elemen
ts if we are copying a | |
974 // selection and pasting it on top of itself. | |
975 deleteSelection(false, mergeBlocksAfterDelete, false); | |
976 visibleStart = endingSelection().visibleStart(); | |
977 if (fragment.hasInterchangeNewlineAtStart()) { | |
978 if (isEndOfParagraph(visibleStart) && !isStartOfParagraph(visibleSta
rt)) { | |
979 if (!isEndOfEditableOrNonEditableContent(visibleStart)) | |
980 setEndingSelection(visibleStart.next()); | |
981 } else { | |
982 insertParagraphSeparator(); | |
983 } | |
984 } | |
985 insertionPos = endingSelection().start(); | |
986 } else { | |
987 ASSERT(selection.isCaret()); | |
988 if (fragment.hasInterchangeNewlineAtStart()) { | |
989 VisiblePosition next = visibleStart.next(CannotCrossEditingBoundary)
; | |
990 if (isEndOfParagraph(visibleStart) && !isStartOfParagraph(visibleSta
rt) && next.isNotNull()) { | |
991 setEndingSelection(next); | |
992 } else { | |
993 insertParagraphSeparator(); | |
994 visibleStart = endingSelection().visibleStart(); | |
995 } | |
996 } | |
997 // We split the current paragraph in two to avoid nesting the blocks fro
m the fragment inside the current block. | |
998 // For example paste <div>foo</div><div>bar</div><div>baz</div> into <di
v>x^x</div>, where ^ is the caret. | |
999 // As long as the div styles are the same, visually you'd expect: <div>
xbar</div><div>bar</div><div>bazx</div>, | |
1000 // not <div>xbar<div>bar</div><div>bazx</div></div>. | |
1001 // Don't do this if the selection started in a Mail blockquote. | |
1002 if (m_preventNesting && !startIsInsideMailBlockquote && !isEndOfParagrap
h(visibleStart) && !isStartOfParagraph(visibleStart)) { | |
1003 insertParagraphSeparator(); | |
1004 setEndingSelection(endingSelection().visibleStart().previous()); | |
1005 } | |
1006 insertionPos = endingSelection().start(); | |
1007 } | |
1008 | |
1009 // We don't want any of the pasted content to end up nested in a Mail blockq
uote, so first break | |
1010 // out of any surrounding Mail blockquotes. Unless we're inserting in a tabl
e, in which case | |
1011 // breaking the blockquote will prevent the content from actually being inse
rted in the table. | |
1012 if (startIsInsideMailBlockquote && m_preventNesting && !(enclosingNodeOfType
(insertionPos, &isTableStructureNode))) { | |
1013 applyCommandToComposite(BreakBlockquoteCommand::create(document())); | |
1014 // This will leave a br between the split. | |
1015 Node* br = endingSelection().start().anchorNode(); | |
1016 ASSERT(isHTMLBRElement(br)); | |
1017 // Insert content between the two blockquotes, but remove the br (since
it was just a placeholder). | |
1018 insertionPos = positionInParentBeforeNode(*br); | |
1019 removeNode(br); | |
1020 } | |
1021 | |
1022 // Inserting content could cause whitespace to collapse, e.g. inserting <div
>foo</div> into hello^ world. | |
1023 prepareWhitespaceAtPositionForSplit(insertionPos); | |
1024 | |
1025 // If the downstream node has been removed there's no point in continuing. | |
1026 if (!insertionPos.downstream().anchorNode()) | |
1027 return; | |
1028 | |
1029 // NOTE: This would be an incorrect usage of downstream() if downstream() we
re changed to mean the last position after | |
1030 // p that maps to the same visible position as p (since in the case where a
br is at the end of a block and collapsed | |
1031 // away, there are positions after the br which map to the same visible posi
tion as [br, 0]). | |
1032 HTMLBRElement* endBR = isHTMLBRElement(*insertionPos.downstream().anchorNode
()) ? toHTMLBRElement(insertionPos.downstream().anchorNode()) : 0; | |
1033 VisiblePosition originalVisPosBeforeEndBR; | |
1034 if (endBR) | |
1035 originalVisPosBeforeEndBR = VisiblePosition(positionBeforeNode(endBR), D
OWNSTREAM).previous(); | |
1036 | |
1037 RefPtrWillBeRawPtr<Element> enclosingBlockOfInsertionPos = enclosingBlock(in
sertionPos.anchorNode()); | |
1038 | |
1039 // Adjust insertionPos to prevent nesting. | |
1040 // If the start was in a Mail blockquote, we will have already handled adjus
ting insertionPos above. | |
1041 if (m_preventNesting && enclosingBlockOfInsertionPos && !isTableCell(enclosi
ngBlockOfInsertionPos.get()) && !startIsInsideMailBlockquote) { | |
1042 ASSERT(enclosingBlockOfInsertionPos != currentRoot); | |
1043 VisiblePosition visibleInsertionPos(insertionPos); | |
1044 if (isEndOfBlock(visibleInsertionPos) && !(isStartOfBlock(visibleInserti
onPos) && fragment.hasInterchangeNewlineAtEnd())) | |
1045 insertionPos = positionInParentAfterNode(*enclosingBlockOfInsertionP
os); | |
1046 else if (isStartOfBlock(visibleInsertionPos)) | |
1047 insertionPos = positionInParentBeforeNode(*enclosingBlockOfInsertion
Pos); | |
1048 } | |
1049 | |
1050 // Paste at start or end of link goes outside of link. | |
1051 insertionPos = positionAvoidingSpecialElementBoundary(insertionPos); | |
1052 | |
1053 // FIXME: Can this wait until after the operation has been performed? There
doesn't seem to be | |
1054 // any work performed after this that queries or uses the typing style. | |
1055 if (LocalFrame* frame = document().frame()) | |
1056 frame->selection().clearTypingStyle(); | |
1057 | |
1058 removeHeadContents(fragment); | |
1059 | |
1060 // We don't want the destination to end up inside nodes that weren't selecte
d. To avoid that, we move the | |
1061 // position forward without changing the visible position so we're still at
the same visible location, but | |
1062 // outside of preceding tags. | |
1063 insertionPos = positionAvoidingPrecedingNodes(insertionPos); | |
1064 | |
1065 // Paste into run of tabs splits the tab span. | |
1066 insertionPos = positionOutsideTabSpan(insertionPos); | |
1067 | |
1068 bool handledStyleSpans = handleStyleSpansBeforeInsertion(fragment, insertion
Pos); | |
1069 | |
1070 // We're finished if there is nothing to add. | |
1071 if (fragment.isEmpty() || !fragment.firstChild()) | |
1072 return; | |
1073 | |
1074 // If we are not trying to match the destination style we prefer a position | |
1075 // that is outside inline elements that provide style. | |
1076 // This way we can produce a less verbose markup. | |
1077 // We can skip this optimization for fragments not wrapped in one of | |
1078 // our style spans and for positions inside list items | |
1079 // since insertAsListItems already does the right thing. | |
1080 if (!m_matchStyle && !enclosingList(insertionPos.computeContainerNode())) { | |
1081 if (insertionPos.computeContainerNode()->isTextNode() && insertionPos.of
fsetInContainerNode() && !insertionPos.atLastEditingPositionForNode()) { | |
1082 splitTextNode(toText(insertionPos.computeContainerNode()), insertion
Pos.offsetInContainerNode()); | |
1083 insertionPos = firstPositionInNode(insertionPos.computeContainerNode
()); | |
1084 } | |
1085 | |
1086 if (RefPtrWillBeRawPtr<HTMLElement> elementToSplitTo = elementToSplitToA
voidPastingIntoInlineElementsWithStyle(insertionPos)) { | |
1087 if (insertionPos.computeContainerNode() != elementToSplitTo->parentN
ode()) { | |
1088 Node* splitStart = insertionPos.computeNodeAfterPosition(); | |
1089 if (!splitStart) | |
1090 splitStart = insertionPos.computeContainerNode(); | |
1091 RefPtrWillBeRawPtr<Node> nodeToSplitTo = splitTreeToNode(splitSt
art, elementToSplitTo->parentNode()).get(); | |
1092 insertionPos = positionInParentBeforeNode(*nodeToSplitTo); | |
1093 } | |
1094 } | |
1095 } | |
1096 | |
1097 // FIXME: When pasting rich content we're often prevented from heading down
the fast path by style spans. Try | |
1098 // again here if they've been removed. | |
1099 | |
1100 // 1) Insert the content. | |
1101 // 2) Remove redundant styles and style tags, this inner <b> for example: <b
>foo <b>bar</b> baz</b>. | |
1102 // 3) Merge the start of the added content with the content before the posit
ion being pasted into. | |
1103 // 4) Do one of the following: a) expand the last br if the fragment ends wi
th one and it collapsed, | |
1104 // b) merge the last paragraph of the incoming fragment with the paragraph t
hat contained the | |
1105 // end of the selection that was pasted into, or c) handle an interchange ne
wline at the end of the | |
1106 // incoming fragment. | |
1107 // 5) Add spaces for smart replace. | |
1108 // 6) Select the replacement if requested, and match style if requested. | |
1109 | |
1110 InsertedNodes insertedNodes; | |
1111 RefPtrWillBeRawPtr<Node> refNode = fragment.firstChild(); | |
1112 ASSERT(refNode); | |
1113 RefPtrWillBeRawPtr<Node> node = refNode->nextSibling(); | |
1114 | |
1115 fragment.removeNode(refNode); | |
1116 | |
1117 Element* blockStart = enclosingBlock(insertionPos.anchorNode()); | |
1118 if ((isHTMLListElement(refNode.get()) || (isLegacyAppleHTMLSpanElement(refNo
de.get()) && isHTMLListElement(refNode->firstChild()))) | |
1119 && blockStart && blockStart->layoutObject()->isListItem()) { | |
1120 refNode = insertAsListItems(toHTMLElement(refNode), blockStart, insertio
nPos, insertedNodes); | |
1121 } else { | |
1122 insertNodeAt(refNode, insertionPos); | |
1123 insertedNodes.respondToNodeInsertion(*refNode); | |
1124 } | |
1125 | |
1126 // Mutation events (bug 22634) may have already removed the inserted content | |
1127 if (!refNode->inDocument()) | |
1128 return; | |
1129 | |
1130 bool plainTextFragment = isPlainTextMarkup(refNode.get()); | |
1131 | |
1132 while (node) { | |
1133 RefPtrWillBeRawPtr<Node> next = node->nextSibling(); | |
1134 fragment.removeNode(node.get()); | |
1135 insertNodeAfter(node, refNode); | |
1136 insertedNodes.respondToNodeInsertion(*node); | |
1137 | |
1138 // Mutation events (bug 22634) may have already removed the inserted con
tent | |
1139 if (!node->inDocument()) | |
1140 return; | |
1141 | |
1142 refNode = node; | |
1143 if (node && plainTextFragment) | |
1144 plainTextFragment = isPlainTextMarkup(node.get()); | |
1145 node = next; | |
1146 } | |
1147 | |
1148 removeUnrenderedTextNodesAtEnds(insertedNodes); | |
1149 | |
1150 if (!handledStyleSpans) | |
1151 handleStyleSpans(insertedNodes); | |
1152 | |
1153 // Mutation events (bug 20161) may have already removed the inserted content | |
1154 if (!insertedNodes.firstNodeInserted() || !insertedNodes.firstNodeInserted()
->inDocument()) | |
1155 return; | |
1156 | |
1157 // Scripts specified in javascript protocol may remove |enclosingBlockOfInse
rtionPos| | |
1158 // during insertion, e.g. <iframe src="javascript:..."> | |
1159 if (enclosingBlockOfInsertionPos && !enclosingBlockOfInsertionPos->inDocumen
t()) | |
1160 enclosingBlockOfInsertionPos = nullptr; | |
1161 | |
1162 VisiblePosition startOfInsertedContent(firstPositionInOrBeforeNode(insertedN
odes.firstNodeInserted())); | |
1163 | |
1164 // We inserted before the enclosingBlockOfInsertionPos to prevent nesting, a
nd the content before the enclosingBlockOfInsertionPos wasn't in its own block a
nd | |
1165 // didn't have a br after it, so the inserted content ended up in the same p
aragraph. | |
1166 if (!startOfInsertedContent.isNull() && enclosingBlockOfInsertionPos && inse
rtionPos.anchorNode() == enclosingBlockOfInsertionPos->parentNode() && (unsigned
)insertionPos.computeEditingOffset() < enclosingBlockOfInsertionPos->nodeIndex()
&& !isStartOfParagraph(startOfInsertedContent)) | |
1167 insertNodeAt(createBreakElement(document()).get(), startOfInsertedConten
t.deepEquivalent()); | |
1168 | |
1169 if (endBR && (plainTextFragment || (shouldRemoveEndBR(endBR, originalVisPosB
eforeEndBR) && !(fragment.hasInterchangeNewlineAtEnd() && selectionIsPlainText))
)) { | |
1170 RefPtrWillBeRawPtr<ContainerNode> parent = endBR->parentNode(); | |
1171 insertedNodes.willRemoveNode(*endBR); | |
1172 removeNode(endBR); | |
1173 if (Node* nodeToRemove = highestNodeToRemoveInPruning(parent.get())) { | |
1174 insertedNodes.willRemoveNode(*nodeToRemove); | |
1175 removeNode(nodeToRemove); | |
1176 } | |
1177 } | |
1178 | |
1179 makeInsertedContentRoundTrippableWithHTMLTreeBuilder(insertedNodes); | |
1180 | |
1181 removeRedundantStylesAndKeepStyleSpanInline(insertedNodes); | |
1182 | |
1183 if (m_sanitizeFragment) | |
1184 applyCommandToComposite(SimplifyMarkupCommand::create(document(), insert
edNodes.firstNodeInserted(), insertedNodes.pastLastLeaf())); | |
1185 | |
1186 // Setup m_startOfInsertedContent and m_endOfInsertedContent. This should be
the last two lines of code that access insertedNodes. | |
1187 m_startOfInsertedContent = firstPositionInOrBeforeNode(insertedNodes.firstNo
deInserted()); | |
1188 m_endOfInsertedContent = lastPositionInOrAfterNode(insertedNodes.lastLeafIns
erted()); | |
1189 | |
1190 // Determine whether or not we should merge the end of inserted content with
what's after it before we do | |
1191 // the start merge so that the start merge doesn't effect our decision. | |
1192 m_shouldMergeEnd = shouldMergeEnd(selectionEndWasEndOfParagraph); | |
1193 | |
1194 if (shouldMergeStart(selectionStartWasStartOfParagraph, fragment.hasIntercha
ngeNewlineAtStart(), startIsInsideMailBlockquote)) { | |
1195 VisiblePosition startOfParagraphToMove = positionAtStartOfInsertedConten
t(); | |
1196 VisiblePosition destination = startOfParagraphToMove.previous(); | |
1197 // We need to handle the case where we need to merge the end | |
1198 // but our destination node is inside an inline that is the last in the
block. | |
1199 // We insert a placeholder before the newly inserted content to avoid be
ing merged into the inline. | |
1200 Node* destinationNode = destination.deepEquivalent().anchorNode(); | |
1201 if (m_shouldMergeEnd && destinationNode != enclosingInline(destinationNo
de) && enclosingInline(destinationNode)->nextSibling()) | |
1202 insertNodeBefore(createBreakElement(document()), refNode.get()); | |
1203 | |
1204 // Merging the the first paragraph of inserted content with the content
that came | |
1205 // before the selection that was pasted into would also move content aft
er | |
1206 // the selection that was pasted into if: only one paragraph was being p
asted, | |
1207 // and it was not wrapped in a block, the selection that was pasted into
ended | |
1208 // at the end of a block and the next paragraph didn't start at the star
t of a block. | |
1209 // Insert a line break just after the inserted content to separate it fr
om what | |
1210 // comes after and prevent that from happening. | |
1211 VisiblePosition endOfInsertedContent = positionAtEndOfInsertedContent(); | |
1212 if (startOfParagraph(endOfInsertedContent).deepEquivalent() == startOfPa
ragraphToMove.deepEquivalent()) { | |
1213 insertNodeAt(createBreakElement(document()).get(), endOfInsertedCont
ent.deepEquivalent()); | |
1214 // Mutation events (bug 22634) triggered by inserting the <br> might
have removed the content we're about to move | |
1215 if (!startOfParagraphToMove.deepEquivalent().inDocument()) | |
1216 return; | |
1217 } | |
1218 | |
1219 // FIXME: Maintain positions for the start and end of inserted content i
nstead of keeping nodes. The nodes are | |
1220 // only ever used to create positions where inserted content starts/ends
. | |
1221 moveParagraph(startOfParagraphToMove, endOfParagraph(startOfParagraphToM
ove), destination); | |
1222 m_startOfInsertedContent = endingSelection().visibleStart().deepEquivale
nt().downstream(); | |
1223 if (m_endOfInsertedContent.isOrphan()) | |
1224 m_endOfInsertedContent = endingSelection().visibleEnd().deepEquivale
nt().upstream(); | |
1225 } | |
1226 | |
1227 Position lastPositionToSelect; | |
1228 if (fragment.hasInterchangeNewlineAtEnd()) { | |
1229 VisiblePosition endOfInsertedContent = positionAtEndOfInsertedContent(); | |
1230 VisiblePosition next = endOfInsertedContent.next(CannotCrossEditingBound
ary); | |
1231 | |
1232 if (selectionEndWasEndOfParagraph || !isEndOfParagraph(endOfInsertedCont
ent) || next.isNull()) { | |
1233 if (!isStartOfParagraph(endOfInsertedContent)) { | |
1234 setEndingSelection(endOfInsertedContent); | |
1235 Element* enclosingBlockElement = enclosingBlock(endOfInsertedCon
tent.deepEquivalent().anchorNode()); | |
1236 if (isListItem(enclosingBlockElement)) { | |
1237 RefPtrWillBeRawPtr<HTMLLIElement> newListItem = createListIt
emElement(document()); | |
1238 insertNodeAfter(newListItem, enclosingBlockElement); | |
1239 setEndingSelection(VisiblePosition(firstPositionInNode(newLi
stItem.get()))); | |
1240 } else { | |
1241 // Use a default paragraph element (a plain div) for the emp
ty paragraph, using the last paragraph | |
1242 // block's style seems to annoy users. | |
1243 insertParagraphSeparator(true, !startIsInsideMailBlockquote
&& highestEnclosingNodeOfType(endOfInsertedContent.deepEquivalent(), | |
1244 isMailHTMLBlockquoteElement, CannotCrossEditingBoundary,
insertedNodes.firstNodeInserted()->parentNode())); | |
1245 } | |
1246 | |
1247 // Select up to the paragraph separator that was added. | |
1248 lastPositionToSelect = endingSelection().visibleStart().deepEqui
valent(); | |
1249 updateNodesInserted(lastPositionToSelect.anchorNode()); | |
1250 } | |
1251 } else { | |
1252 // Select up to the beginning of the next paragraph. | |
1253 lastPositionToSelect = next.deepEquivalent().downstream(); | |
1254 } | |
1255 } else { | |
1256 mergeEndIfNeeded(); | |
1257 } | |
1258 | |
1259 if (HTMLQuoteElement* mailBlockquote = toHTMLQuoteElement(enclosingNodeOfTyp
e(positionAtStartOfInsertedContent().deepEquivalent(), isMailPasteAsQuotationHTM
LBlockQuoteElement))) | |
1260 removeElementAttribute(mailBlockquote, classAttr); | |
1261 | |
1262 if (shouldPerformSmartReplace()) | |
1263 addSpacesForSmartReplace(); | |
1264 | |
1265 // If we are dealing with a fragment created from plain text | |
1266 // no style matching is necessary. | |
1267 if (plainTextFragment) | |
1268 m_matchStyle = false; | |
1269 | |
1270 completeHTMLReplacement(lastPositionToSelect); | |
1271 } | |
1272 | |
1273 bool ReplaceSelectionCommand::shouldRemoveEndBR(HTMLBRElement* endBR, const Visi
blePosition& originalVisPosBeforeEndBR) | |
1274 { | |
1275 if (!endBR || !endBR->inDocument()) | |
1276 return false; | |
1277 | |
1278 VisiblePosition visiblePos(positionBeforeNode(endBR)); | |
1279 | |
1280 // Don't remove the br if nothing was inserted. | |
1281 if (visiblePos.previous().deepEquivalent() == originalVisPosBeforeEndBR.deep
Equivalent()) | |
1282 return false; | |
1283 | |
1284 // Remove the br if it is collapsed away and so is unnecessary. | |
1285 if (!document().inNoQuirksMode() && isEndOfBlock(visiblePos) && !isStartOfPa
ragraph(visiblePos)) | |
1286 return true; | |
1287 | |
1288 // A br that was originally holding a line open should be displaced by inser
ted content or turned into a line break. | |
1289 // A br that was originally acting as a line break should still be acting as
a line break, not as a placeholder. | |
1290 return isStartOfParagraph(visiblePos) && isEndOfParagraph(visiblePos); | |
1291 } | |
1292 | |
1293 bool ReplaceSelectionCommand::shouldPerformSmartReplace() const | |
1294 { | |
1295 if (!m_smartReplace) | |
1296 return false; | |
1297 | |
1298 HTMLTextFormControlElement* textControl = enclosingTextFormControl(positionA
tStartOfInsertedContent().deepEquivalent()); | |
1299 if (isHTMLInputElement(textControl) && toHTMLInputElement(textControl)->type
() == InputTypeNames::password) | |
1300 return false; // Disable smart replace for password fields. | |
1301 | |
1302 return true; | |
1303 } | |
1304 | |
1305 static bool isCharacterSmartReplaceExemptConsideringNonBreakingSpace(UChar32 cha
racter, bool previousCharacter) | |
1306 { | |
1307 return isCharacterSmartReplaceExempt(character == noBreakSpaceCharacter ? '
' : character, previousCharacter); | |
1308 } | |
1309 | |
1310 void ReplaceSelectionCommand::addSpacesForSmartReplace() | |
1311 { | |
1312 VisiblePosition startOfInsertedContent = positionAtStartOfInsertedContent(); | |
1313 VisiblePosition endOfInsertedContent = positionAtEndOfInsertedContent(); | |
1314 | |
1315 Position endUpstream = endOfInsertedContent.deepEquivalent().upstream(); | |
1316 Node* endNode = endUpstream.computeNodeBeforePosition(); | |
1317 int endOffset = endNode && endNode->isTextNode() ? toText(endNode)->length()
: 0; | |
1318 if (endUpstream.isOffsetInAnchor()) { | |
1319 endNode = endUpstream.computeContainerNode(); | |
1320 endOffset = endUpstream.offsetInContainerNode(); | |
1321 } | |
1322 | |
1323 bool needsTrailingSpace = !isEndOfParagraph(endOfInsertedContent) && !isChar
acterSmartReplaceExemptConsideringNonBreakingSpace(endOfInsertedContent.characte
rAfter(), false); | |
1324 if (needsTrailingSpace && endNode) { | |
1325 bool collapseWhiteSpace = !endNode->layoutObject() || endNode->layoutObj
ect()->style()->collapseWhiteSpace(); | |
1326 if (endNode->isTextNode()) { | |
1327 insertTextIntoNode(toText(endNode), endOffset, collapseWhiteSpace ?
nonBreakingSpaceString() : " "); | |
1328 if (m_endOfInsertedContent.computeContainerNode() == endNode) | |
1329 m_endOfInsertedContent = Position(endNode, m_endOfInsertedConten
t.offsetInContainerNode() + 1); | |
1330 } else { | |
1331 RefPtrWillBeRawPtr<Text> node = document().createEditingTextNode(col
lapseWhiteSpace ? nonBreakingSpaceString() : " "); | |
1332 insertNodeAfter(node, endNode); | |
1333 updateNodesInserted(node.get()); | |
1334 } | |
1335 } | |
1336 | |
1337 document().updateLayout(); | |
1338 | |
1339 Position startDownstream = startOfInsertedContent.deepEquivalent().downstrea
m(); | |
1340 Node* startNode = startDownstream.computeNodeAfterPosition(); | |
1341 unsigned startOffset = 0; | |
1342 if (startDownstream.isOffsetInAnchor()) { | |
1343 startNode = startDownstream.computeContainerNode(); | |
1344 startOffset = startDownstream.offsetInContainerNode(); | |
1345 } | |
1346 | |
1347 bool needsLeadingSpace = !isStartOfParagraph(startOfInsertedContent) && !isC
haracterSmartReplaceExemptConsideringNonBreakingSpace(startOfInsertedContent.pre
vious().characterAfter(), true); | |
1348 if (needsLeadingSpace && startNode) { | |
1349 bool collapseWhiteSpace = !startNode->layoutObject() || startNode->layou
tObject()->style()->collapseWhiteSpace(); | |
1350 if (startNode->isTextNode()) { | |
1351 insertTextIntoNode(toText(startNode), startOffset, collapseWhiteSpac
e ? nonBreakingSpaceString() : " "); | |
1352 if (m_endOfInsertedContent.computeContainerNode() == startNode && m_
endOfInsertedContent.offsetInContainerNode()) | |
1353 m_endOfInsertedContent = Position(startNode, m_endOfInsertedCont
ent.offsetInContainerNode() + 1); | |
1354 } else { | |
1355 RefPtrWillBeRawPtr<Text> node = document().createEditingTextNode(col
lapseWhiteSpace ? nonBreakingSpaceString() : " "); | |
1356 // Don't updateNodesInserted. Doing so would set m_endOfInsertedCont
ent to be the node containing the leading space, | |
1357 // but m_endOfInsertedContent is supposed to mark the end of pasted
content. | |
1358 insertNodeBefore(node, startNode); | |
1359 m_startOfInsertedContent = firstPositionInNode(node.get()); | |
1360 } | |
1361 } | |
1362 } | |
1363 | |
1364 void ReplaceSelectionCommand::completeHTMLReplacement(const Position &lastPositi
onToSelect) | |
1365 { | |
1366 Position start = positionAtStartOfInsertedContent().deepEquivalent(); | |
1367 Position end = positionAtEndOfInsertedContent().deepEquivalent(); | |
1368 | |
1369 // Mutation events may have deleted start or end | |
1370 if (start.isNotNull() && !start.isOrphan() && end.isNotNull() && !end.isOrph
an()) { | |
1371 // FIXME (11475): Remove this and require that the creator of the fragme
nt to use nbsps. | |
1372 rebalanceWhitespaceAt(start); | |
1373 rebalanceWhitespaceAt(end); | |
1374 | |
1375 if (m_matchStyle) { | |
1376 ASSERT(m_insertionStyle); | |
1377 applyStyle(m_insertionStyle.get(), start, end); | |
1378 } | |
1379 | |
1380 if (lastPositionToSelect.isNotNull()) | |
1381 end = lastPositionToSelect; | |
1382 | |
1383 mergeTextNodesAroundPosition(start, end); | |
1384 } else if (lastPositionToSelect.isNotNull()) { | |
1385 start = end = lastPositionToSelect; | |
1386 } else { | |
1387 return; | |
1388 } | |
1389 | |
1390 if (m_selectReplacement) | |
1391 setEndingSelection(VisibleSelection(start, end, SEL_DEFAULT_AFFINITY, en
dingSelection().isDirectional())); | |
1392 else | |
1393 setEndingSelection(VisibleSelection(end, SEL_DEFAULT_AFFINITY, endingSel
ection().isDirectional())); | |
1394 } | |
1395 | |
1396 void ReplaceSelectionCommand::mergeTextNodesAroundPosition(Position& position, P
osition& positionOnlyToBeUpdated) | |
1397 { | |
1398 bool positionIsOffsetInAnchor = position.isOffsetInAnchor(); | |
1399 bool positionOnlyToBeUpdatedIsOffsetInAnchor = positionOnlyToBeUpdated.isOff
setInAnchor(); | |
1400 RefPtrWillBeRawPtr<Text> text = nullptr; | |
1401 if (positionIsOffsetInAnchor && position.computeContainerNode() && position.
computeContainerNode()->isTextNode()) { | |
1402 text = toText(position.computeContainerNode()); | |
1403 } else { | |
1404 Node* before = position.computeNodeBeforePosition(); | |
1405 if (before && before->isTextNode()) { | |
1406 text = toText(before); | |
1407 } else { | |
1408 Node* after = position.computeNodeAfterPosition(); | |
1409 if (after && after->isTextNode()) | |
1410 text = toText(after); | |
1411 } | |
1412 } | |
1413 if (!text) | |
1414 return; | |
1415 | |
1416 if (text->previousSibling() && text->previousSibling()->isTextNode()) { | |
1417 RefPtrWillBeRawPtr<Text> previous = toText(text->previousSibling()); | |
1418 insertTextIntoNode(text, 0, previous->data()); | |
1419 | |
1420 if (positionIsOffsetInAnchor) | |
1421 position = Position(position.computeContainerNode(), previous->lengt
h() + position.offsetInContainerNode()); | |
1422 else | |
1423 updatePositionForNodeRemoval(position, *previous); | |
1424 | |
1425 if (positionOnlyToBeUpdatedIsOffsetInAnchor) { | |
1426 if (positionOnlyToBeUpdated.computeContainerNode() == text) | |
1427 positionOnlyToBeUpdated = Position(text, previous->length() + po
sitionOnlyToBeUpdated.offsetInContainerNode()); | |
1428 else if (positionOnlyToBeUpdated.computeContainerNode() == previous) | |
1429 positionOnlyToBeUpdated = Position(text, positionOnlyToBeUpdated
.offsetInContainerNode()); | |
1430 } else { | |
1431 updatePositionForNodeRemoval(positionOnlyToBeUpdated, *previous); | |
1432 } | |
1433 | |
1434 removeNode(previous); | |
1435 } | |
1436 if (text->nextSibling() && text->nextSibling()->isTextNode()) { | |
1437 RefPtrWillBeRawPtr<Text> next = toText(text->nextSibling()); | |
1438 unsigned originalLength = text->length(); | |
1439 insertTextIntoNode(text, originalLength, next->data()); | |
1440 | |
1441 if (!positionIsOffsetInAnchor) | |
1442 updatePositionForNodeRemoval(position, *next); | |
1443 | |
1444 if (positionOnlyToBeUpdatedIsOffsetInAnchor && positionOnlyToBeUpdated.c
omputeContainerNode() == next) | |
1445 positionOnlyToBeUpdated = Position(text, originalLength + positionOn
lyToBeUpdated.offsetInContainerNode()); | |
1446 else | |
1447 updatePositionForNodeRemoval(positionOnlyToBeUpdated, *next); | |
1448 | |
1449 removeNode(next); | |
1450 } | |
1451 } | |
1452 | |
1453 EditAction ReplaceSelectionCommand::editingAction() const | |
1454 { | |
1455 return m_editAction; | |
1456 } | |
1457 | |
1458 // If the user is inserting a list into an existing list, instead of nesting the
list, | |
1459 // we put the list items into the existing list. | |
1460 Node* ReplaceSelectionCommand::insertAsListItems(PassRefPtrWillBeRawPtr<HTMLElem
ent> prpListElement, Element* insertionBlock, const Position& insertPos, Inserte
dNodes& insertedNodes) | |
1461 { | |
1462 RefPtrWillBeRawPtr<HTMLElement> listElement = prpListElement; | |
1463 | |
1464 while (listElement->hasOneChild() && isHTMLListElement(listElement->firstChi
ld())) | |
1465 listElement = toHTMLElement(listElement->firstChild()); | |
1466 | |
1467 bool isStart = isStartOfParagraph(VisiblePosition(insertPos)); | |
1468 bool isEnd = isEndOfParagraph(VisiblePosition(insertPos)); | |
1469 bool isMiddle = !isStart && !isEnd; | |
1470 Node* lastNode = insertionBlock; | |
1471 | |
1472 // If we're in the middle of a list item, we should split it into two separa
te | |
1473 // list items and insert these nodes between them. | |
1474 if (isMiddle) { | |
1475 int textNodeOffset = insertPos.offsetInContainerNode(); | |
1476 if (insertPos.anchorNode()->isTextNode() && textNodeOffset > 0) | |
1477 splitTextNode(toText(insertPos.anchorNode()), textNodeOffset); | |
1478 splitTreeToNode(insertPos.anchorNode(), lastNode, true); | |
1479 } | |
1480 | |
1481 while (RefPtrWillBeRawPtr<Node> listItem = listElement->firstChild()) { | |
1482 listElement->removeChild(listItem.get(), ASSERT_NO_EXCEPTION); | |
1483 if (isStart || isMiddle) { | |
1484 insertNodeBefore(listItem, lastNode); | |
1485 insertedNodes.respondToNodeInsertion(*listItem); | |
1486 } else if (isEnd) { | |
1487 insertNodeAfter(listItem, lastNode); | |
1488 insertedNodes.respondToNodeInsertion(*listItem); | |
1489 lastNode = listItem.get(); | |
1490 } else { | |
1491 ASSERT_NOT_REACHED(); | |
1492 } | |
1493 } | |
1494 if (isStart || isMiddle) { | |
1495 if (Node* node = lastNode->previousSibling()) | |
1496 return node; | |
1497 } | |
1498 return lastNode; | |
1499 } | |
1500 | |
1501 void ReplaceSelectionCommand::updateNodesInserted(Node *node) | |
1502 { | |
1503 if (!node) | |
1504 return; | |
1505 | |
1506 if (m_startOfInsertedContent.isNull()) | |
1507 m_startOfInsertedContent = firstPositionInOrBeforeNode(node); | |
1508 | |
1509 m_endOfInsertedContent = lastPositionInOrAfterNode(&NodeTraversal::lastWithi
nOrSelf(*node)); | |
1510 } | |
1511 | |
1512 // During simple pastes, where we're just pasting a text node into a run of text
, we insert the text node | |
1513 // directly into the text node that holds the selection. This is much faster th
an the generalized code in | |
1514 // ReplaceSelectionCommand, and works around <https://bugs.webkit.org/show_bug.c
gi?id=6148> since we don't | |
1515 // split text nodes. | |
1516 bool ReplaceSelectionCommand::performTrivialReplace(const ReplacementFragment& f
ragment) | |
1517 { | |
1518 if (!fragment.firstChild() || fragment.firstChild() != fragment.lastChild()
|| !fragment.firstChild()->isTextNode()) | |
1519 return false; | |
1520 | |
1521 // FIXME: Would be nice to handle smart replace in the fast path. | |
1522 if (m_smartReplace || fragment.hasInterchangeNewlineAtStart() || fragment.ha
sInterchangeNewlineAtEnd()) | |
1523 return false; | |
1524 | |
1525 // e.g. when "bar" is inserted after "foo" in <div><u>foo</u></div>, "bar" s
hould not be underlined. | |
1526 if (elementToSplitToAvoidPastingIntoInlineElementsWithStyle(endingSelection(
).start())) | |
1527 return false; | |
1528 | |
1529 RefPtrWillBeRawPtr<Node> nodeAfterInsertionPos = endingSelection().end().dow
nstream().anchorNode(); | |
1530 Text* textNode = toText(fragment.firstChild()); | |
1531 // Our fragment creation code handles tabs, spaces, and newlines, so we don'
t have to worry about those here. | |
1532 | |
1533 Position start = endingSelection().start(); | |
1534 Position end = replaceSelectedTextInNode(textNode->data()); | |
1535 if (end.isNull()) | |
1536 return false; | |
1537 | |
1538 if (nodeAfterInsertionPos && nodeAfterInsertionPos->parentNode() && isHTMLBR
Element(*nodeAfterInsertionPos) | |
1539 && shouldRemoveEndBR(toHTMLBRElement(nodeAfterInsertionPos.get()), Visib
lePosition(positionBeforeNode(nodeAfterInsertionPos.get())))) | |
1540 removeNodeAndPruneAncestors(nodeAfterInsertionPos.get()); | |
1541 | |
1542 VisibleSelection selectionAfterReplace(m_selectReplacement ? start : end, en
d); | |
1543 | |
1544 setEndingSelection(selectionAfterReplace); | |
1545 | |
1546 return true; | |
1547 } | |
1548 | |
1549 DEFINE_TRACE(ReplaceSelectionCommand) | |
1550 { | |
1551 visitor->trace(m_startOfInsertedContent); | |
1552 visitor->trace(m_endOfInsertedContent); | |
1553 visitor->trace(m_insertionStyle); | |
1554 visitor->trace(m_documentFragment); | |
1555 CompositeEditCommand::trace(visitor); | |
1556 } | |
1557 | |
1558 } // namespace blink | |
OLD | NEW |