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 "ApplyBlockElementCommand.h"
29
30#include "HTMLElement.h"
31#include "HTMLNames.h"
32#include "Text.h"
33#include "TextIterator.h"
34#include "VisiblePosition.h"
35#include "htmlediting.h"
36#include "visible_units.h"
37
38namespace WebCore {
39
40using namespace HTMLNames;
41
42ApplyBlockElementCommand::ApplyBlockElementCommand(Document* document, const QualifiedName& tagName, const AtomicString& className, const AtomicString& inlineStyle)
43    : CompositeEditCommand(document)
44    , m_tagName(tagName)
45    , m_className(className)
46    , m_inlineStyle(inlineStyle)
47{
48}
49
50ApplyBlockElementCommand::ApplyBlockElementCommand(Document* document, const QualifiedName& tagName)
51    : CompositeEditCommand(document)
52    , m_tagName(tagName)
53{
54}
55
56void ApplyBlockElementCommand::doApply()
57{
58    if (!endingSelection().isNonOrphanedCaretOrRange())
59        return;
60
61    if (!endingSelection().rootEditableElement())
62        return;
63
64    VisiblePosition visibleEnd = endingSelection().visibleEnd();
65    VisiblePosition visibleStart = endingSelection().visibleStart();
66    // When a selection ends at the start of a paragraph, we rarely paint
67    // the selection gap before that paragraph, because there often is no gap.
68    // In a case like this, it's not obvious to the user that the selection
69    // ends "inside" that paragraph, so it would be confusing if Indent/Outdent
70    // operated on that paragraph.
71    // FIXME: We paint the gap before some paragraphs that are indented with left
72    // margin/padding, but not others.  We should make the gap painting more consistent and
73    // then use a left margin/padding rule here.
74    if (visibleEnd != visibleStart && isStartOfParagraph(visibleEnd))
75        setEndingSelection(VisibleSelection(visibleStart, visibleEnd.previous(CannotCrossEditingBoundary)));
76
77    VisibleSelection selection = selectionForParagraphIteration(endingSelection());
78    VisiblePosition startOfSelection = selection.visibleStart();
79    VisiblePosition endOfSelection = selection.visibleEnd();
80    ASSERT(!startOfSelection.isNull());
81    ASSERT(!endOfSelection.isNull());
82    int startIndex = indexForVisiblePosition(startOfSelection);
83    int endIndex = indexForVisiblePosition(endOfSelection);
84
85    formatSelection(startOfSelection, endOfSelection);
86
87    updateLayout();
88
89    RefPtr<Range> startRange = TextIterator::rangeFromLocationAndLength(document()->documentElement(), startIndex, 0, true);
90    RefPtr<Range> endRange = TextIterator::rangeFromLocationAndLength(document()->documentElement(), endIndex, 0, true);
91    if (startRange && endRange)
92        setEndingSelection(VisibleSelection(startRange->startPosition(), endRange->startPosition(), DOWNSTREAM));
93}
94
95void ApplyBlockElementCommand::formatSelection(const VisiblePosition& startOfSelection, const VisiblePosition& endOfSelection)
96{
97    // Special case empty unsplittable elements because there's nothing to split
98    // and there's nothing to move.
99    Position start = startOfSelection.deepEquivalent().downstream();
100    if (isAtUnsplittableElement(start)) {
101        RefPtr<Element> blockquote = createBlockElement();
102        insertNodeAt(blockquote, start);
103        RefPtr<Element> placeholder = createBreakElement(document());
104        appendNode(placeholder, blockquote);
105        setEndingSelection(VisibleSelection(positionBeforeNode(placeholder.get()), DOWNSTREAM));
106        return;
107    }
108
109    RefPtr<Element> blockquoteForNextIndent;
110    VisiblePosition endOfCurrentParagraph = endOfParagraph(startOfSelection);
111    VisiblePosition endAfterSelection = endOfParagraph(endOfParagraph(endOfSelection).next());
112    m_endOfLastParagraph = endOfParagraph(endOfSelection).deepEquivalent();
113
114    bool atEnd = false;
115    Position end;
116    while (endOfCurrentParagraph != endAfterSelection && !atEnd) {
117        if (endOfCurrentParagraph.deepEquivalent() == m_endOfLastParagraph)
118            atEnd = true;
119
120        rangeForParagraphSplittingTextNodesIfNeeded(endOfCurrentParagraph, start, end);
121        endOfCurrentParagraph = end;
122
123        Position afterEnd = end.next();
124        Node* enclosingCell = enclosingNodeOfType(start, &isTableCell);
125        VisiblePosition endOfNextParagraph = endOfNextParagrahSplittingTextNodesIfNeeded(endOfCurrentParagraph, start, end);
126
127        formatRange(start, end, m_endOfLastParagraph, blockquoteForNextIndent);
128
129        // Don't put the next paragraph in the blockquote we just created for this paragraph unless
130        // the next paragraph is in the same cell.
131        if (enclosingCell && enclosingCell != enclosingNodeOfType(endOfNextParagraph.deepEquivalent(), &isTableCell))
132            blockquoteForNextIndent = 0;
133
134        // indentIntoBlockquote could move more than one paragraph if the paragraph
135        // is in a list item or a table. As a result, endAfterSelection could refer to a position
136        // no longer in the document.
137        if (endAfterSelection.isNotNull() && !endAfterSelection.deepEquivalent().anchorNode()->inDocument())
138            break;
139        // Sanity check: Make sure our moveParagraph calls didn't remove endOfNextParagraph.deepEquivalent().deprecatedNode()
140        // If somehow we did, return to prevent crashes.
141        if (endOfNextParagraph.isNotNull() && !endOfNextParagraph.deepEquivalent().anchorNode()->inDocument()) {
142            ASSERT_NOT_REACHED();
143            return;
144        }
145        endOfCurrentParagraph = endOfNextParagraph;
146    }
147}
148
149static bool isNewLineAtPosition(const Position& position)
150{
151    if (position.anchorType() != Position::PositionIsOffsetInAnchor)
152        return false;
153
154    Node* textNode = position.containerNode();
155    int offset = position.offsetInContainerNode();
156    if (!textNode || !textNode->isTextNode() || offset < 0 || offset >= textNode->maxCharacterOffset())
157        return false;
158
159    ExceptionCode ec = 0;
160    String textAtPosition = static_cast<Text*>(textNode)->substringData(offset, 1, ec);
161    if (ec)
162        return false;
163
164    return textAtPosition[0] == '\n';
165}
166
167static RenderStyle* renderStyleOfEnclosingTextNode(const Position& position)
168{
169    if (position.anchorType() != Position::PositionIsOffsetInAnchor
170        || !position.containerNode()
171        || !position.containerNode()->isTextNode()
172        || !position.containerNode()->renderer())
173        return 0;
174    return position.containerNode()->renderer()->style();
175}
176
177void ApplyBlockElementCommand::rangeForParagraphSplittingTextNodesIfNeeded(const VisiblePosition& endOfCurrentParagraph, Position& start, Position& end)
178{
179    start = startOfParagraph(endOfCurrentParagraph).deepEquivalent();
180    end = endOfCurrentParagraph.deepEquivalent();
181
182    RenderStyle* startStyle = renderStyleOfEnclosingTextNode(start);
183    bool isStartAndEndOnSameNode = false;
184    if (startStyle) {
185        isStartAndEndOnSameNode = renderStyleOfEnclosingTextNode(end) && start.deprecatedNode() == end.deprecatedNode();
186        bool isStartAndEndOfLastParagraphOnSameNode = renderStyleOfEnclosingTextNode(m_endOfLastParagraph) && start.deprecatedNode() == m_endOfLastParagraph.deprecatedNode();
187
188        // Avoid obtanining the start of next paragraph for start
189        if (startStyle->preserveNewline() && isNewLineAtPosition(start) && !isNewLineAtPosition(start.previous()) && start.offsetInContainerNode() > 0)
190            start = startOfParagraph(end.previous()).deepEquivalent();
191
192        // If start is in the middle of a text node, split.
193        if (!startStyle->collapseWhiteSpace() && start.offsetInContainerNode() > 0) {
194            int startOffset = start.offsetInContainerNode();
195            splitTextNode(static_cast<Text*>(start.deprecatedNode()), startOffset);
196            start = firstPositionInOrBeforeNode(start.deprecatedNode());
197            if (isStartAndEndOnSameNode) {
198                ASSERT(end.offsetInContainerNode() >= startOffset);
199                end = Position(end.deprecatedNode(), end.offsetInContainerNode() - startOffset, Position::PositionIsOffsetInAnchor);
200            }
201            if (isStartAndEndOfLastParagraphOnSameNode) {
202                ASSERT(m_endOfLastParagraph.offsetInContainerNode() >= startOffset);
203                m_endOfLastParagraph = Position(m_endOfLastParagraph.deprecatedNode(), m_endOfLastParagraph.offsetInContainerNode() - startOffset,
204                    Position::PositionIsOffsetInAnchor);
205            }
206        }
207    }
208
209    RenderStyle* endStyle = renderStyleOfEnclosingTextNode(end);
210    if (endStyle) {
211        bool isEndAndEndOfLastParagraphOnSameNode = renderStyleOfEnclosingTextNode(m_endOfLastParagraph) && end.deprecatedNode() == m_endOfLastParagraph.deprecatedNode();
212        // Include \n at the end of line if we're at an empty paragraph
213        if (endStyle->preserveNewline() && start == end
214            && end.offsetInContainerNode() < end.containerNode()->maxCharacterOffset()) {
215            int endOffset = end.offsetInContainerNode();
216            if (!isNewLineAtPosition(end.previous()) && isNewLineAtPosition(end))
217                end = Position(end.deprecatedNode(), endOffset + 1, Position::PositionIsOffsetInAnchor);
218            if (isEndAndEndOfLastParagraphOnSameNode && end.offsetInContainerNode() >= m_endOfLastParagraph.offsetInContainerNode())
219                m_endOfLastParagraph = end;
220        }
221
222        // If end is in the middle of a text node, split.
223        if (!endStyle->collapseWhiteSpace() && end.offsetInContainerNode()
224            && end.offsetInContainerNode() < end.containerNode()->maxCharacterOffset()) {
225            splitTextNode(static_cast<Text*>(end.deprecatedNode()), end.offsetInContainerNode());
226            if (isStartAndEndOnSameNode)
227                start = firstPositionInOrBeforeNode(end.deprecatedNode()->previousSibling());
228            if (isEndAndEndOfLastParagraphOnSameNode) {
229                if (m_endOfLastParagraph.offsetInContainerNode() == end.offsetInContainerNode())
230                    m_endOfLastParagraph = lastPositionInNode(end.deprecatedNode()->previousSibling());
231                else
232                    m_endOfLastParagraph = Position(end.deprecatedNode(), m_endOfLastParagraph.offsetInContainerNode() - end.offsetInContainerNode(),
233                                                    Position::PositionIsOffsetInAnchor);
234            }
235            end = lastPositionInNode(end.deprecatedNode()->previousSibling());
236        }
237    }
238}
239
240VisiblePosition ApplyBlockElementCommand::endOfNextParagrahSplittingTextNodesIfNeeded(VisiblePosition& endOfCurrentParagraph, Position& start, Position& end)
241{
242    VisiblePosition endOfNextParagraph = endOfParagraph(endOfCurrentParagraph.next());
243    Position position = endOfNextParagraph.deepEquivalent();
244    RenderStyle* style = renderStyleOfEnclosingTextNode(position);
245    if (!style)
246        return endOfNextParagraph;
247
248    RefPtr<Node> containerNode = position.containerNode();
249    if (!style->preserveNewline() || !position.offsetInContainerNode()
250        || !isNewLineAtPosition(Position(containerNode.get(), 0, Position::PositionIsOffsetInAnchor)))
251        return endOfNextParagraph;
252
253    // \n at the beginning of the text node immediately following the current paragraph is trimmed by moveParagraphWithClones.
254    // If endOfNextParagraph was pointing at this same text node, endOfNextParagraph will be shifted by one paragraph.
255    // Avoid this by splitting "\n"
256    splitTextNode(static_cast<Text*>(containerNode.get()), 1);
257
258    if (start.anchorType() == Position::PositionIsOffsetInAnchor && containerNode.get() == start.containerNode()) {
259        ASSERT(start.offsetInContainerNode() < position.offsetInContainerNode());
260        start = Position(containerNode->previousSibling(), start.offsetInContainerNode(), Position::PositionIsOffsetInAnchor);
261    }
262    if (end.anchorType() == Position::PositionIsOffsetInAnchor && containerNode.get() == end.containerNode()) {
263        ASSERT(end.offsetInContainerNode() < position.offsetInContainerNode());
264        end = Position(containerNode->previousSibling(), end.offsetInContainerNode(), Position::PositionIsOffsetInAnchor);
265    }
266    if (m_endOfLastParagraph.anchorType() == Position::PositionIsOffsetInAnchor && containerNode.get() == m_endOfLastParagraph.containerNode()) {
267        if (m_endOfLastParagraph.offsetInContainerNode() < position.offsetInContainerNode())
268            m_endOfLastParagraph = Position(containerNode->previousSibling(), m_endOfLastParagraph.offsetInContainerNode(), Position::PositionIsOffsetInAnchor);
269        else
270            m_endOfLastParagraph = Position(containerNode, m_endOfLastParagraph.offsetInContainerNode() - 1, Position::PositionIsOffsetInAnchor);
271    }
272
273    return Position(containerNode.get(), position.offsetInContainerNode() - 1, Position::PositionIsOffsetInAnchor);
274}
275
276PassRefPtr<Element> ApplyBlockElementCommand::createBlockElement() const
277{
278    RefPtr<Element> element = createHTMLElement(document(), m_tagName);
279    if (m_className.length())
280        element->setAttribute(classAttr, m_className);
281    if (m_inlineStyle.length())
282        element->setAttribute(styleAttr, m_inlineStyle);
283    return element.release();
284}
285
286}
287