diff options
Diffstat (limited to 'Source/WebCore/editing/ApplyBlockElementCommand.cpp')
-rw-r--r-- | Source/WebCore/editing/ApplyBlockElementCommand.cpp | 286 |
1 files changed, 286 insertions, 0 deletions
diff --git a/Source/WebCore/editing/ApplyBlockElementCommand.cpp b/Source/WebCore/editing/ApplyBlockElementCommand.cpp new file mode 100644 index 0000000..285650d --- /dev/null +++ b/Source/WebCore/editing/ApplyBlockElementCommand.cpp @@ -0,0 +1,286 @@ +/* + * 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 "HTMLElement.h" +#include "HTMLNames.h" +#include "Text.h" +#include "TextIterator.h" +#include "VisiblePosition.h" +#include "htmlediting.h" +#include "visible_units.h" + +namespace WebCore { + +using namespace HTMLNames; + +ApplyBlockElementCommand::ApplyBlockElementCommand(Document* document, const QualifiedName& tagName, const AtomicString& className, const AtomicString& inlineStyle) + : CompositeEditCommand(document) + , m_tagName(tagName) + , m_className(className) + , m_inlineStyle(inlineStyle) +{ +} + +ApplyBlockElementCommand::ApplyBlockElementCommand(Document* document, const QualifiedName& tagName) + : CompositeEditCommand(document) + , m_tagName(tagName) +{ +} + +void ApplyBlockElementCommand::doApply() +{ + if (!endingSelection().isNonOrphanedCaretOrRange()) + return; + + if (!endingSelection().rootEditableElement()) + return; + + VisiblePosition visibleEnd = endingSelection().visibleEnd(); + VisiblePosition visibleStart = endingSelection().visibleStart(); + // 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)) + setEndingSelection(VisibleSelection(visibleStart, visibleEnd.previous(true))); + + VisibleSelection selection = selectionForParagraphIteration(endingSelection()); + VisiblePosition startOfSelection = selection.visibleStart(); + VisiblePosition endOfSelection = selection.visibleEnd(); + ASSERT(!startOfSelection.isNull()); + ASSERT(!endOfSelection.isNull()); + int startIndex = indexForVisiblePosition(startOfSelection); + int endIndex = indexForVisiblePosition(endOfSelection); + + formatSelection(startOfSelection, endOfSelection); + + updateLayout(); + + RefPtr<Range> startRange = TextIterator::rangeFromLocationAndLength(document()->documentElement(), startIndex, 0, true); + RefPtr<Range> endRange = TextIterator::rangeFromLocationAndLength(document()->documentElement(), endIndex, 0, true); + if (startRange && endRange) + setEndingSelection(VisibleSelection(startRange->startPosition(), endRange->startPosition(), DOWNSTREAM)); +} + +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)) { + RefPtr<Element> blockquote = createBlockElement(); + insertNodeAt(blockquote, start); + RefPtr<Element> placeholder = createBreakElement(document()); + appendNode(placeholder, blockquote); + setEndingSelection(VisibleSelection(Position(placeholder.get(), 0), DOWNSTREAM)); + return; + } + + RefPtr<Element> 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); + endOfCurrentParagraph = end; + + Position afterEnd = end.next(); + Node* enclosingCell = enclosingNodeOfType(start, &isTableCell); + VisiblePosition endOfNextParagraph = endOfNextParagrahSplittingTextNodesIfNeeded(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 = 0; + + // 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().node()->inDocument()) + break; + // Sanity check: Make sure our moveParagraph calls didn't remove endOfNextParagraph.deepEquivalent().node() + // If somehow we did, return to prevent crashes. + if (endOfNextParagraph.isNotNull() && !endOfNextParagraph.deepEquivalent().node()->inDocument()) { + ASSERT_NOT_REACHED(); + return; + } + endOfCurrentParagraph = endOfNextParagraph; + } +} + +static bool isNewLineAtPosition(const Position& position) +{ + if (position.anchorType() != Position::PositionIsOffsetInAnchor) + return false; + + Node* textNode = position.containerNode(); + int offset = position.offsetInContainerNode(); + if (!textNode || !textNode->isTextNode() || offset < 0 || offset >= textNode->maxCharacterOffset()) + return false; + + ExceptionCode ec = 0; + String textAtPosition = static_cast<Text*>(textNode)->substringData(offset, 1, ec); + if (ec) + return false; + + return textAtPosition[0] == '\n'; +} + +static RenderStyle* renderStyleOfEnclosingTextNode(const Position& position) +{ + if (position.anchorType() != Position::PositionIsOffsetInAnchor + || !position.containerNode() + || !position.containerNode()->isTextNode() + || !position.containerNode()->renderer()) + return 0; + return position.containerNode()->renderer()->style(); +} + +void ApplyBlockElementCommand::rangeForParagraphSplittingTextNodesIfNeeded(const VisiblePosition& endOfCurrentParagraph, Position& start, Position& end) +{ + start = startOfParagraph(endOfCurrentParagraph).deepEquivalent(); + end = endOfCurrentParagraph.deepEquivalent(); + + RenderStyle* startStyle = renderStyleOfEnclosingTextNode(start); + bool isStartAndEndOnSameNode = false; + if (startStyle) { + isStartAndEndOnSameNode = renderStyleOfEnclosingTextNode(end) && start.node() == end.node(); + bool isStartAndEndOfLastParagraphOnSameNode = renderStyleOfEnclosingTextNode(m_endOfLastParagraph) && start.node() == m_endOfLastParagraph.node(); + + // Avoid obtanining the start of next paragraph for start + if (startStyle->preserveNewline() && 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 (!startStyle->collapseWhiteSpace() && start.offsetInContainerNode() > 0) { + int startOffset = start.offsetInContainerNode(); + splitTextNode(static_cast<Text*>(start.node()), startOffset); + start = positionBeforeNode(start.node()); + if (isStartAndEndOnSameNode) { + ASSERT(end.offsetInContainerNode() >= startOffset); + end = Position(end.node(), end.offsetInContainerNode() - startOffset, Position::PositionIsOffsetInAnchor); + } + if (isStartAndEndOfLastParagraphOnSameNode) { + ASSERT(m_endOfLastParagraph.offsetInContainerNode() >= startOffset); + m_endOfLastParagraph = Position(m_endOfLastParagraph.node(), m_endOfLastParagraph.offsetInContainerNode() - startOffset, + Position::PositionIsOffsetInAnchor); + } + } + } + + RenderStyle* endStyle = renderStyleOfEnclosingTextNode(end); + if (endStyle) { + bool isEndAndEndOfLastParagraphOnSameNode = renderStyleOfEnclosingTextNode(m_endOfLastParagraph) && end.node() == m_endOfLastParagraph.node(); + // Include \n at the end of line if we're at an empty paragraph + if (endStyle->preserveNewline() && start == end + && end.offsetInContainerNode() < end.containerNode()->maxCharacterOffset()) { + int endOffset = end.offsetInContainerNode(); + if (!isNewLineAtPosition(end.previous()) && isNewLineAtPosition(end)) + end = Position(end.node(), endOffset + 1, Position::PositionIsOffsetInAnchor); + if (isEndAndEndOfLastParagraphOnSameNode && end.offsetInContainerNode() >= m_endOfLastParagraph.offsetInContainerNode()) + m_endOfLastParagraph = end; + } + + // If end is in the middle of a text node, split. + if (!endStyle->collapseWhiteSpace() && end.offsetInContainerNode() + && end.offsetInContainerNode() < end.containerNode()->maxCharacterOffset()) { + splitTextNode(static_cast<Text*>(end.node()), end.offsetInContainerNode()); + if (isStartAndEndOnSameNode) + start = positionBeforeNode(end.node()->previousSibling()); + if (isEndAndEndOfLastParagraphOnSameNode) { + if (m_endOfLastParagraph.offsetInContainerNode() == end.offsetInContainerNode()) + m_endOfLastParagraph = lastPositionInNode(end.node()->previousSibling()); + else + m_endOfLastParagraph = Position(end.node(), m_endOfLastParagraph.offsetInContainerNode() - end.offsetInContainerNode(), + Position::PositionIsOffsetInAnchor); + } + end = lastPositionInNode(end.node()->previousSibling()); + } + } +} + +VisiblePosition ApplyBlockElementCommand::endOfNextParagrahSplittingTextNodesIfNeeded(VisiblePosition& endOfCurrentParagraph, Position& start, Position& end) +{ + VisiblePosition endOfNextParagraph = endOfParagraph(endOfCurrentParagraph.next()); + Position position = endOfNextParagraph.deepEquivalent(); + RenderStyle* style = renderStyleOfEnclosingTextNode(position); + if (!style) + return endOfNextParagraph; + + RefPtr<Node> containerNode = position.containerNode(); + if (!style->preserveNewline() || !position.offsetInContainerNode() + || !isNewLineAtPosition(Position(containerNode.get(), 0, Position::PositionIsOffsetInAnchor))) + 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(static_cast<Text*>(containerNode.get()), 1); + + if (start.anchorType() == Position::PositionIsOffsetInAnchor && containerNode.get() == start.containerNode()) { + ASSERT(start.offsetInContainerNode() < position.offsetInContainerNode()); + start = Position(containerNode->previousSibling(), start.offsetInContainerNode(), Position::PositionIsOffsetInAnchor); + } + if (end.anchorType() == Position::PositionIsOffsetInAnchor && containerNode.get() == end.containerNode()) { + ASSERT(end.offsetInContainerNode() < position.offsetInContainerNode()); + end = Position(containerNode->previousSibling(), end.offsetInContainerNode(), Position::PositionIsOffsetInAnchor); + } + if (m_endOfLastParagraph.anchorType() == Position::PositionIsOffsetInAnchor && containerNode.get() == m_endOfLastParagraph.containerNode()) { + if (m_endOfLastParagraph.offsetInContainerNode() < position.offsetInContainerNode()) + m_endOfLastParagraph = Position(containerNode->previousSibling(), m_endOfLastParagraph.offsetInContainerNode(), Position::PositionIsOffsetInAnchor); + else + m_endOfLastParagraph = Position(containerNode, m_endOfLastParagraph.offsetInContainerNode() - 1, Position::PositionIsOffsetInAnchor); + } + + return Position(containerNode.get(), position.offsetInContainerNode() - 1, Position::PositionIsOffsetInAnchor); +} + +PassRefPtr<Element> ApplyBlockElementCommand::createBlockElement() const +{ + RefPtr<Element> element = createHTMLElement(document(), m_tagName); + if (m_className.length()) + element->setAttribute(classAttr, m_className); + if (m_inlineStyle.length()) + element->setAttribute(styleAttr, m_inlineStyle); + return element.release(); +} + +} |