1/*
2 * Copyright (C) 2006, 2008 Apple Inc. All rights reserved.
3 * Copyright (C) 2010 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 THE COPYRIGHT HOLDERS AND CONTRIBUTORS
15 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
16 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
17 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
18 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
19 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
20 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
21 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
22 * THEORY 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/ApplyBlockElementCommand.h"
29
30#include "bindings/core/v8/ExceptionState.h"
31#include "core/HTMLNames.h"
32#include "core/dom/NodeRenderStyle.h"
33#include "core/dom/Text.h"
34#include "core/editing/VisiblePosition.h"
35#include "core/editing/VisibleUnits.h"
36#include "core/editing/htmlediting.h"
37#include "core/html/HTMLBRElement.h"
38#include "core/html/HTMLElement.h"
39#include "core/rendering/RenderObject.h"
40#include "core/rendering/style/RenderStyle.h"
41
42namespace blink {
43
44using namespace HTMLNames;
45
46ApplyBlockElementCommand::ApplyBlockElementCommand(Document& document, const QualifiedName& tagName, const AtomicString& inlineStyle)
47    : CompositeEditCommand(document)
48    , m_tagName(tagName)
49    , m_inlineStyle(inlineStyle)
50{
51}
52
53ApplyBlockElementCommand::ApplyBlockElementCommand(Document& document, const QualifiedName& tagName)
54    : CompositeEditCommand(document)
55    , m_tagName(tagName)
56{
57}
58
59void ApplyBlockElementCommand::doApply()
60{
61    if (!endingSelection().rootEditableElement())
62        return;
63
64    VisiblePosition visibleEnd = endingSelection().visibleEnd();
65    VisiblePosition visibleStart = endingSelection().visibleStart();
66    if (visibleStart.isNull() || visibleStart.isOrphan() || visibleEnd.isNull() || visibleEnd.isOrphan())
67        return;
68
69    // When a selection ends at the start of a paragraph, we rarely paint
70    // the selection gap before that paragraph, because there often is no gap.
71    // In a case like this, it's not obvious to the user that the selection
72    // ends "inside" that paragraph, so it would be confusing if Indent/Outdent
73    // operated on that paragraph.
74    // FIXME: We paint the gap before some paragraphs that are indented with left
75    // margin/padding, but not others.  We should make the gap painting more consistent and
76    // then use a left margin/padding rule here.
77    if (visibleEnd != visibleStart && isStartOfParagraph(visibleEnd)) {
78        VisibleSelection newSelection(visibleStart, visibleEnd.previous(CannotCrossEditingBoundary), endingSelection().isDirectional());
79        if (newSelection.isNone())
80            return;
81        setEndingSelection(newSelection);
82    }
83
84    VisibleSelection selection = selectionForParagraphIteration(endingSelection());
85    VisiblePosition startOfSelection = selection.visibleStart();
86    VisiblePosition endOfSelection = selection.visibleEnd();
87    ASSERT(!startOfSelection.isNull());
88    ASSERT(!endOfSelection.isNull());
89    RefPtrWillBeRawPtr<ContainerNode> startScope = nullptr;
90    int startIndex = indexForVisiblePosition(startOfSelection, startScope);
91    RefPtrWillBeRawPtr<ContainerNode> endScope = nullptr;
92    int endIndex = indexForVisiblePosition(endOfSelection, endScope);
93
94    formatSelection(startOfSelection, endOfSelection);
95
96    document().updateLayoutIgnorePendingStylesheets();
97
98    ASSERT(startScope == endScope);
99    ASSERT(startIndex >= 0);
100    ASSERT(startIndex <= endIndex);
101    if (startScope == endScope && startIndex >= 0 && startIndex <= endIndex) {
102        VisiblePosition start(visiblePositionForIndex(startIndex, startScope.get()));
103        VisiblePosition end(visiblePositionForIndex(endIndex, endScope.get()));
104        if (start.isNotNull() && end.isNotNull())
105            setEndingSelection(VisibleSelection(start, end, endingSelection().isDirectional()));
106    }
107}
108
109void ApplyBlockElementCommand::formatSelection(const VisiblePosition& startOfSelection, const VisiblePosition& endOfSelection)
110{
111    // Special case empty unsplittable elements because there's nothing to split
112    // and there's nothing to move.
113    Position start = startOfSelection.deepEquivalent().downstream();
114    if (isAtUnsplittableElement(start)) {
115        RefPtrWillBeRawPtr<HTMLElement> blockquote = createBlockElement();
116        insertNodeAt(blockquote, start);
117        RefPtrWillBeRawPtr<HTMLBRElement> placeholder = createBreakElement(document());
118        appendNode(placeholder, blockquote);
119        setEndingSelection(VisibleSelection(positionBeforeNode(placeholder.get()), DOWNSTREAM, endingSelection().isDirectional()));
120        return;
121    }
122
123    RefPtrWillBeRawPtr<HTMLElement> blockquoteForNextIndent = nullptr;
124    VisiblePosition endOfCurrentParagraph = endOfParagraph(startOfSelection);
125    VisiblePosition endOfLastParagraph = endOfParagraph(endOfSelection);
126    VisiblePosition endAfterSelection = endOfParagraph(endOfLastParagraph.next());
127    m_endOfLastParagraph = endOfLastParagraph.deepEquivalent();
128
129    bool atEnd = false;
130    Position end;
131    while (endOfCurrentParagraph != endAfterSelection && !atEnd) {
132        if (endOfCurrentParagraph.deepEquivalent() == m_endOfLastParagraph)
133            atEnd = true;
134
135        rangeForParagraphSplittingTextNodesIfNeeded(endOfCurrentParagraph, start, end);
136        endOfCurrentParagraph = VisiblePosition(end);
137
138        Node* enclosingCell = enclosingNodeOfType(start, &isTableCell);
139        VisiblePosition endOfNextParagraph = endOfNextParagrahSplittingTextNodesIfNeeded(endOfCurrentParagraph, start, end);
140
141        formatRange(start, end, m_endOfLastParagraph, blockquoteForNextIndent);
142
143        // Don't put the next paragraph in the blockquote we just created for this paragraph unless
144        // the next paragraph is in the same cell.
145        if (enclosingCell && enclosingCell != enclosingNodeOfType(endOfNextParagraph.deepEquivalent(), &isTableCell))
146            blockquoteForNextIndent = nullptr;
147
148        // indentIntoBlockquote could move more than one paragraph if the paragraph
149        // is in a list item or a table. As a result, endAfterSelection could refer to a position
150        // no longer in the document.
151        if (endAfterSelection.isNotNull() && !endAfterSelection.deepEquivalent().inDocument())
152            break;
153        // Sanity check: Make sure our moveParagraph calls didn't remove endOfNextParagraph.deepEquivalent().deprecatedNode()
154        // If somehow, e.g. mutation event handler, we did, return to prevent crashes.
155        if (endOfNextParagraph.isNotNull() && !endOfNextParagraph.deepEquivalent().inDocument())
156            return;
157        endOfCurrentParagraph = endOfNextParagraph;
158    }
159}
160
161static bool isNewLineAtPosition(const Position& position)
162{
163    Node* textNode = position.containerNode();
164    int offset = position.offsetInContainerNode();
165    if (!textNode || !textNode->isTextNode() || offset < 0 || offset >= textNode->maxCharacterOffset())
166        return false;
167
168    TrackExceptionState exceptionState;
169    String textAtPosition = toText(textNode)->substringData(offset, 1, exceptionState);
170    if (exceptionState.hadException())
171        return false;
172
173    return textAtPosition[0] == '\n';
174}
175
176static RenderStyle* renderStyleOfEnclosingTextNode(const Position& position)
177{
178    if (position.anchorType() != Position::PositionIsOffsetInAnchor || !position.containerNode() || !position.containerNode()->isTextNode())
179        return 0;
180    return position.containerNode()->renderStyle();
181}
182
183void ApplyBlockElementCommand::rangeForParagraphSplittingTextNodesIfNeeded(const VisiblePosition& endOfCurrentParagraph, Position& start, Position& end)
184{
185    start = startOfParagraph(endOfCurrentParagraph).deepEquivalent();
186    end = endOfCurrentParagraph.deepEquivalent();
187
188    document().updateRenderTreeIfNeeded();
189
190    bool isStartAndEndOnSameNode = false;
191    if (RenderStyle* startStyle = renderStyleOfEnclosingTextNode(start)) {
192        isStartAndEndOnSameNode = renderStyleOfEnclosingTextNode(end) && start.containerNode() == end.containerNode();
193        bool isStartAndEndOfLastParagraphOnSameNode = renderStyleOfEnclosingTextNode(m_endOfLastParagraph) && start.containerNode() == m_endOfLastParagraph.containerNode();
194
195        // Avoid obtanining the start of next paragraph for start
196        if (startStyle->preserveNewline() && isNewLineAtPosition(start) && !isNewLineAtPosition(start.previous()) && start.offsetInContainerNode() > 0)
197            start = startOfParagraph(VisiblePosition(end.previous())).deepEquivalent();
198
199        // If start is in the middle of a text node, split.
200        if (!startStyle->collapseWhiteSpace() && start.offsetInContainerNode() > 0) {
201            int startOffset = start.offsetInContainerNode();
202            Text* startText = start.containerText();
203            splitTextNode(startText, startOffset);
204            start = firstPositionInNode(startText);
205            if (isStartAndEndOnSameNode) {
206                ASSERT(end.offsetInContainerNode() >= startOffset);
207                end = Position(startText, end.offsetInContainerNode() - startOffset);
208            }
209            if (isStartAndEndOfLastParagraphOnSameNode) {
210                ASSERT(m_endOfLastParagraph.offsetInContainerNode() >= startOffset);
211                m_endOfLastParagraph = Position(startText, m_endOfLastParagraph.offsetInContainerNode() - startOffset);
212            }
213        }
214    }
215
216    document().updateRenderTreeIfNeeded();
217
218    if (RenderStyle* endStyle = renderStyleOfEnclosingTextNode(end)) {
219        bool isEndAndEndOfLastParagraphOnSameNode = renderStyleOfEnclosingTextNode(m_endOfLastParagraph) && end.deprecatedNode() == m_endOfLastParagraph.deprecatedNode();
220        // Include \n at the end of line if we're at an empty paragraph
221        if (endStyle->preserveNewline() && start == end && end.offsetInContainerNode() < end.containerNode()->maxCharacterOffset()) {
222            int endOffset = end.offsetInContainerNode();
223            if (!isNewLineAtPosition(end.previous()) && isNewLineAtPosition(end))
224                end = Position(end.containerText(), endOffset + 1);
225            if (isEndAndEndOfLastParagraphOnSameNode && end.offsetInContainerNode() >= m_endOfLastParagraph.offsetInContainerNode())
226                m_endOfLastParagraph = end;
227        }
228
229        // If end is in the middle of a text node, split.
230        if (!endStyle->collapseWhiteSpace() && end.offsetInContainerNode() && end.offsetInContainerNode() < end.containerNode()->maxCharacterOffset()) {
231            RefPtrWillBeRawPtr<Text> endContainer = end.containerText();
232            splitTextNode(endContainer, end.offsetInContainerNode());
233            if (isStartAndEndOnSameNode)
234                start = firstPositionInOrBeforeNode(endContainer->previousSibling());
235            if (isEndAndEndOfLastParagraphOnSameNode) {
236                if (m_endOfLastParagraph.offsetInContainerNode() == end.offsetInContainerNode())
237                    m_endOfLastParagraph = lastPositionInOrAfterNode(endContainer->previousSibling());
238                else
239                    m_endOfLastParagraph = Position(endContainer, m_endOfLastParagraph.offsetInContainerNode() - end.offsetInContainerNode());
240            }
241            end = lastPositionInNode(endContainer->previousSibling());
242        }
243    }
244}
245
246VisiblePosition ApplyBlockElementCommand::endOfNextParagrahSplittingTextNodesIfNeeded(VisiblePosition& endOfCurrentParagraph, Position& start, Position& end)
247{
248    VisiblePosition endOfNextParagraph = endOfParagraph(endOfCurrentParagraph.next());
249    Position position = endOfNextParagraph.deepEquivalent();
250    RenderStyle* style = renderStyleOfEnclosingTextNode(position);
251    if (!style)
252        return endOfNextParagraph;
253
254    RefPtrWillBeRawPtr<Text> text = position.containerText();
255    if (!style->preserveNewline() || !position.offsetInContainerNode() || !isNewLineAtPosition(firstPositionInNode(text.get())))
256        return endOfNextParagraph;
257
258    // \n at the beginning of the text node immediately following the current paragraph is trimmed by moveParagraphWithClones.
259    // If endOfNextParagraph was pointing at this same text node, endOfNextParagraph will be shifted by one paragraph.
260    // Avoid this by splitting "\n"
261    splitTextNode(text, 1);
262
263    if (text == start.containerNode() && text->previousSibling() && text->previousSibling()->isTextNode()) {
264        ASSERT(start.offsetInContainerNode() < position.offsetInContainerNode());
265        start = Position(toText(text->previousSibling()), start.offsetInContainerNode());
266    }
267    if (text == end.containerNode() && text->previousSibling() && text->previousSibling()->isTextNode()) {
268        ASSERT(end.offsetInContainerNode() < position.offsetInContainerNode());
269        end = Position(toText(text->previousSibling()), end.offsetInContainerNode());
270    }
271    if (text == m_endOfLastParagraph.containerNode()) {
272        if (m_endOfLastParagraph.offsetInContainerNode() < position.offsetInContainerNode()) {
273            // We can only fix endOfLastParagraph if the previous node was still text and hasn't been modified by script.
274            if (text->previousSibling()->isTextNode()
275                && static_cast<unsigned>(m_endOfLastParagraph.offsetInContainerNode()) <= toText(text->previousSibling())->length())
276                m_endOfLastParagraph = Position(toText(text->previousSibling()), m_endOfLastParagraph.offsetInContainerNode());
277        } else
278            m_endOfLastParagraph = Position(text.get(), m_endOfLastParagraph.offsetInContainerNode() - 1);
279    }
280
281    return VisiblePosition(Position(text.get(), position.offsetInContainerNode() - 1));
282}
283
284PassRefPtrWillBeRawPtr<HTMLElement> ApplyBlockElementCommand::createBlockElement() const
285{
286    RefPtrWillBeRawPtr<HTMLElement> element = createHTMLElement(document(), m_tagName);
287    if (m_inlineStyle.length())
288        element->setAttribute(styleAttr, m_inlineStyle);
289    return element.release();
290}
291
292void ApplyBlockElementCommand::trace(Visitor* visitor)
293{
294    visitor->trace(m_endOfLastParagraph);
295    CompositeEditCommand::trace(visitor);
296}
297
298}
299