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
38 namespace WebCore {
39
40 using namespace HTMLNames;
41
ApplyBlockElementCommand(Document * document,const QualifiedName & tagName,const AtomicString & className,const AtomicString & inlineStyle)42 ApplyBlockElementCommand::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
ApplyBlockElementCommand(Document * document,const QualifiedName & tagName)50 ApplyBlockElementCommand::ApplyBlockElementCommand(Document* document, const QualifiedName& tagName)
51 : CompositeEditCommand(document)
52 , m_tagName(tagName)
53 {
54 }
55
doApply()56 void 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
formatSelection(const VisiblePosition & startOfSelection,const VisiblePosition & endOfSelection)95 void 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
isNewLineAtPosition(const Position & position)149 static 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
renderStyleOfEnclosingTextNode(const Position & position)167 static 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
rangeForParagraphSplittingTextNodesIfNeeded(const VisiblePosition & endOfCurrentParagraph,Position & start,Position & end)177 void 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
endOfNextParagrahSplittingTextNodesIfNeeded(VisiblePosition & endOfCurrentParagraph,Position & start,Position & end)240 VisiblePosition 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
createBlockElement() const276 PassRefPtr<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