/* * Copyright (C) 2006, 2008 Apple Inc. All rights reserved. * Copyright (C) 2010 Google Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "config.h" #include "ApplyBlockElementCommand.h" #include "Editing.h" #include "HTMLBRElement.h" #include "HTMLNames.h" #include "RenderElement.h" #include "RenderStyle.h" #include "Text.h" #include "VisibleUnits.h" namespace WebCore { using namespace HTMLNames; ApplyBlockElementCommand::ApplyBlockElementCommand(Document& document, const QualifiedName& tagName, const AtomString& inlineStyle) : CompositeEditCommand(document) , m_tagName(tagName) , m_inlineStyle(inlineStyle) { } ApplyBlockElementCommand::ApplyBlockElementCommand(Document& document, const QualifiedName& tagName) : CompositeEditCommand(document) , m_tagName(tagName) { } void ApplyBlockElementCommand::doApply() { if (!endingSelection().rootEditableElement()) return; VisiblePosition visibleEnd = endingSelection().visibleEnd(); VisiblePosition visibleStart = endingSelection().visibleStart(); if (visibleStart.isNull() || visibleStart.isOrphan() || visibleEnd.isNull() || visibleEnd.isOrphan()) return; // When a selection ends at the start of a paragraph, we rarely paint // the selection gap before that paragraph, because there often is no gap. // In a case like this, it's not obvious to the user that the selection // ends "inside" that paragraph, so it would be confusing if Indent/Outdent // operated on that paragraph. // FIXME: We paint the gap before some paragraphs that are indented with left // margin/padding, but not others. We should make the gap painting more consistent and // then use a left margin/padding rule here. if (visibleEnd != visibleStart && isStartOfParagraph(visibleEnd)) { VisibleSelection newSelection(visibleStart, visibleEnd.previous(CannotCrossEditingBoundary), endingSelection().isDirectional()); if (newSelection.isNone()) return; setEndingSelection(newSelection); } VisibleSelection selection = selectionForParagraphIteration(endingSelection()); VisiblePosition startOfSelection = selection.visibleStart(); VisiblePosition endOfSelection = selection.visibleEnd(); ASSERT(!startOfSelection.isNull()); ASSERT(!endOfSelection.isNull()); RefPtr startScope; int startIndex = indexForVisiblePosition(startOfSelection, startScope); RefPtr endScope; int endIndex = indexForVisiblePosition(endOfSelection, endScope); formatSelection(startOfSelection, endOfSelection); document().updateLayoutIgnorePendingStylesheets(); ASSERT(startScope == endScope); ASSERT(startIndex >= 0); ASSERT(startIndex <= endIndex); if (startScope == endScope && startIndex >= 0 && startIndex <= endIndex) { VisiblePosition start(visiblePositionForIndex(startIndex, startScope.get())); VisiblePosition end(visiblePositionForIndex(endIndex, endScope.get())); // Work around the fact indexForVisiblePosition can return a larger index due to TextIterator // using an extra newline to represent a large margin. // FIXME: Add a new TextIteratorBehavior to suppress it. if (start.isNotNull() && end.isNull()) end = lastPositionInNode(endScope.get()); if (start.isNotNull() && end.isNotNull()) setEndingSelection(VisibleSelection(start, end, endingSelection().isDirectional())); } } void ApplyBlockElementCommand::formatSelection(const VisiblePosition& startOfSelection, const VisiblePosition& endOfSelection) { // Special case empty unsplittable elements because there's nothing to split // and there's nothing to move. Position start = startOfSelection.deepEquivalent().downstream(); if (isAtUnsplittableElement(start) && startOfParagraph(start) == endOfParagraph(endOfSelection)) { auto blockquote = createBlockElement(); insertNodeAt(blockquote.copyRef(), start); auto placeholder = HTMLBRElement::create(document()); appendNode(placeholder.copyRef(), WTFMove(blockquote)); setEndingSelection(VisibleSelection(positionBeforeNode(placeholder.ptr()), Affinity::Downstream, endingSelection().isDirectional())); return; } RefPtr blockquoteForNextIndent; VisiblePosition endOfCurrentParagraph = endOfParagraph(startOfSelection); VisiblePosition endAfterSelection = endOfParagraph(endOfParagraph(endOfSelection).next()); m_endOfLastParagraph = endOfParagraph(endOfSelection).deepEquivalent(); bool atEnd = false; Position end; while (endOfCurrentParagraph != endAfterSelection && !atEnd) { if (endOfCurrentParagraph.deepEquivalent() == m_endOfLastParagraph) atEnd = true; rangeForParagraphSplittingTextNodesIfNeeded(endOfCurrentParagraph, start, end); if (start.isNull() || end.isNull()) break; endOfCurrentParagraph = end; // FIXME: endOfParagraph can errornously return a position at the beginning of a block element // when the position passed into endOfParagraph is at the beginning of a block. // Work around this bug here because too much of the existing code depends on the current behavior of endOfParagraph. if (start == end && startOfBlock(start) != endOfBlock(start) && !isEndOfBlock(end) && start == startOfParagraph(endOfBlock(start))) { endOfCurrentParagraph = endOfBlock(end); end = endOfCurrentParagraph.deepEquivalent(); } Position afterEnd = end.next(); RefPtr enclosingCell = enclosingNodeOfType(start, &isTableCell); VisiblePosition endOfNextParagraph = endOfNextParagraphSplittingTextNodesIfNeeded(endOfCurrentParagraph, start, end); formatRange(start, end, m_endOfLastParagraph, blockquoteForNextIndent); // Don't put the next paragraph in the blockquote we just created for this paragraph unless // the next paragraph is in the same cell. if (enclosingCell && enclosingCell != enclosingNodeOfType(endOfNextParagraph.deepEquivalent(), &isTableCell)) blockquoteForNextIndent = nullptr; // indentIntoBlockquote could move more than one paragraph if the paragraph // is in a list item or a table. As a result, endAfterSelection could refer to a position // no longer in the document. if (endAfterSelection.isNotNull() && !endAfterSelection.deepEquivalent().anchorNode()->isConnected()) break; // Sanity check: Make sure our moveParagraph calls didn't remove endOfNextParagraph.deepEquivalent().deprecatedNode() // If somehow we did, return to prevent crashes. if (endOfNextParagraph.isNotNull() && !endOfNextParagraph.deepEquivalent().anchorNode()->isConnected()) { ASSERT_NOT_REACHED(); return; } endOfCurrentParagraph = endOfNextParagraph; } } static bool isNewLineAtPosition(const Position& position) { RefPtr textNode = position.containerNode(); unsigned offset = position.offsetInContainerNode(); if (!is(textNode) || offset >= downcast(*textNode).length()) return false; return downcast(*textNode).data()[offset] == '\n'; } const RenderStyle* ApplyBlockElementCommand::renderStyleOfEnclosingTextNode(const Position& position) { if (position.anchorType() != Position::PositionIsOffsetInAnchor || !position.containerNode() || !position.containerNode()->isTextNode()) return nullptr; document().updateStyleIfNeeded(); RenderObject* renderer = position.containerNode()->renderer(); if (!renderer) return nullptr; return &renderer->style(); } void ApplyBlockElementCommand::rangeForParagraphSplittingTextNodesIfNeeded(const VisiblePosition& endOfCurrentParagraph, Position& start, Position& end) { start = startOfParagraph(endOfCurrentParagraph).deepEquivalent(); end = endOfCurrentParagraph.deepEquivalent(); bool isStartAndEndOnSameNode = false; if (auto* startStyle = renderStyleOfEnclosingTextNode(start)) { isStartAndEndOnSameNode = renderStyleOfEnclosingTextNode(end) && start.containerNode() == end.containerNode(); bool isStartAndEndOfLastParagraphOnSameNode = renderStyleOfEnclosingTextNode(m_endOfLastParagraph) && start.containerNode() == m_endOfLastParagraph.containerNode(); bool preservesNewLine = startStyle->preserveNewline(); bool collapsesWhiteSpace = startStyle->collapseWhiteSpace(); startStyle = nullptr; // Avoid obtanining the start of next paragraph for start if (preservesNewLine && isNewLineAtPosition(start) && !isNewLineAtPosition(start.previous()) && start.offsetInContainerNode() > 0) start = startOfParagraph(end.previous()).deepEquivalent(); // If start is in the middle of a text node, split. if (!collapsesWhiteSpace && start.offsetInContainerNode() > 0) { int startOffset = start.offsetInContainerNode(); RefPtr startText = start.containerText(); ASSERT(startText); splitTextNode(*startText, startOffset); start = firstPositionInNode(startText.get()); if (isStartAndEndOnSameNode) { ASSERT(end.offsetInContainerNode() >= startOffset); end = Position(startText.get(), end.offsetInContainerNode() - startOffset); } if (isStartAndEndOfLastParagraphOnSameNode) { ASSERT(m_endOfLastParagraph.offsetInContainerNode() >= startOffset); m_endOfLastParagraph = Position(startText.get(), m_endOfLastParagraph.offsetInContainerNode() - startOffset); } } } if (auto* endStyle = renderStyleOfEnclosingTextNode(end)) { bool isEndAndEndOfLastParagraphOnSameNode = renderStyleOfEnclosingTextNode(m_endOfLastParagraph) && end.deprecatedNode() == m_endOfLastParagraph.deprecatedNode(); // Include \n at the end of line if we're at an empty paragraph unsigned endOffset = end.offsetInContainerNode(); bool preservesNewLine = endStyle->preserveNewline(); bool collapseWhiteSpace = endStyle->collapseWhiteSpace(); auto userModify = endStyle->userModify(); endStyle = nullptr; if (preservesNewLine && start == end && endOffset < end.containerNode()->length()) { if (!isNewLineAtPosition(end.previous()) && isNewLineAtPosition(end)) end = Position(end.containerText(), ++endOffset); if (isEndAndEndOfLastParagraphOnSameNode && end.offsetInContainerNode() >= m_endOfLastParagraph.offsetInContainerNode()) m_endOfLastParagraph = end; } // If end is in the middle of a text node and the text node is editable, split. if (userModify != UserModify::ReadOnly && !collapseWhiteSpace && endOffset && endOffset < end.containerNode()->length()) { RefPtr endContainer = end.containerText(); splitTextNode(*endContainer, endOffset); if (is(endContainer) && !endContainer->previousSibling()) { start = { }; end = { }; return; } if (isStartAndEndOnSameNode) start = firstPositionInOrBeforeNode(endContainer->previousSibling()); if (isEndAndEndOfLastParagraphOnSameNode) { if (static_cast(m_endOfLastParagraph.offsetInContainerNode()) == endOffset) m_endOfLastParagraph = lastPositionInOrAfterNode(endContainer->previousSibling()); else m_endOfLastParagraph = Position(endContainer.get(), m_endOfLastParagraph.offsetInContainerNode() - endOffset); } end = lastPositionInNode(endContainer->previousSibling()); } } } VisiblePosition ApplyBlockElementCommand::endOfNextParagraphSplittingTextNodesIfNeeded(VisiblePosition& endOfCurrentParagraph, Position& start, Position& end) { VisiblePosition endOfNextParagraph = endOfParagraph(endOfCurrentParagraph.next()); Position position = endOfNextParagraph.deepEquivalent(); auto* style = renderStyleOfEnclosingTextNode(position); if (!style) return endOfNextParagraph; bool preserveNewLine = style->preserveNewline(); style = nullptr; RefPtr text = position.containerText(); if (!preserveNewLine || !position.offsetInContainerNode() || !isNewLineAtPosition(firstPositionInNode(text.get()))) return endOfNextParagraph; // \n at the beginning of the text node immediately following the current paragraph is trimmed by moveParagraphWithClones. // If endOfNextParagraph was pointing at this same text node, endOfNextParagraph will be shifted by one paragraph. // Avoid this by splitting "\n" splitTextNode(*text, 1); auto previousSiblingOfText = makeRefPtr(text->previousSibling()); if (text == start.containerNode() && previousSiblingOfText && is(previousSiblingOfText)) { ASSERT(start.offsetInContainerNode() < position.offsetInContainerNode()); start = Position(downcast(text->previousSibling()), start.offsetInContainerNode()); } if (text == end.containerNode() && previousSiblingOfText && is(previousSiblingOfText)) { ASSERT(end.offsetInContainerNode() < position.offsetInContainerNode()); end = Position(downcast(text->previousSibling()), end.offsetInContainerNode()); } if (text == m_endOfLastParagraph.containerNode()) { if (m_endOfLastParagraph.offsetInContainerNode() < position.offsetInContainerNode()) { // We can only fix endOfLastParagraph if the previous node was still text and hasn't been modified by script. if (previousSiblingOfText && is(previousSiblingOfText) && static_cast(m_endOfLastParagraph.offsetInContainerNode()) <= downcast(text->previousSibling())->length()) m_endOfLastParagraph = Position(downcast(text->previousSibling()), m_endOfLastParagraph.offsetInContainerNode()); } else m_endOfLastParagraph = Position(text.get(), m_endOfLastParagraph.offsetInContainerNode() - 1); } return Position(text.get(), position.offsetInContainerNode() - 1); } Ref ApplyBlockElementCommand::createBlockElement() { auto element = createHTMLElement(document(), m_tagName); if (m_inlineStyle.length()) element->setAttribute(styleAttr, m_inlineStyle); return element; } }