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