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 | |
38 | namespace WebCore { |
39 | |
40 | using namespace HTMLNames; |
41 | |
42 | ApplyBlockElementCommand::ApplyBlockElementCommand(Document& document, const QualifiedName& tagName, const AtomicString& inlineStyle) |
43 | : CompositeEditCommand(document) |
44 | , m_tagName(tagName) |
45 | , m_inlineStyle(inlineStyle) |
46 | { |
47 | } |
48 | |
49 | ApplyBlockElementCommand::ApplyBlockElementCommand(Document& document, const QualifiedName& tagName) |
50 | : CompositeEditCommand(document) |
51 | , m_tagName(tagName) |
52 | { |
53 | } |
54 | |
55 | void 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 | |
110 | void 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 | |
172 | static 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 | |
181 | const 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 | |
197 | void 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 | |
257 | VisiblePosition 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 | |
295 | Ref<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 | |