| 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 | |