diff options
author | Steve Block <steveblock@google.com> | 2011-05-13 06:44:40 -0700 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2011-05-13 06:44:40 -0700 |
commit | 08014c20784f3db5df3a89b73cce46037b77eb59 (patch) | |
tree | 47749210d31e19e6e2f64036fa8fae2ad693476f /Source/WebCore/editing | |
parent | 860220379e56aeb66424861ad602b07ee22b4055 (diff) | |
parent | 4c3661f7918f8b3f139f824efb7855bedccb4c94 (diff) | |
download | external_webkit-08014c20784f3db5df3a89b73cce46037b77eb59.zip external_webkit-08014c20784f3db5df3a89b73cce46037b77eb59.tar.gz external_webkit-08014c20784f3db5df3a89b73cce46037b77eb59.tar.bz2 |
Merge changes Ide388898,Ic49f367c,I1158a808,Iacb6ca5d,I2100dd3a,I5c1abe54,Ib0ef9902,I31dbc523,I570314b3
* changes:
Merge WebKit at r75315: Update WebKit version
Merge WebKit at r75315: Add FrameLoaderClient PageCache stubs
Merge WebKit at r75315: Stub out AXObjectCache::remove()
Merge WebKit at r75315: Fix ImageBuffer
Merge WebKit at r75315: Fix PluginData::initPlugins()
Merge WebKit at r75315: Fix conflicts
Merge WebKit at r75315: Fix Makefiles
Merge WebKit at r75315: Move Android-specific WebCore files to Source
Merge WebKit at r75315: Initial merge by git.
Diffstat (limited to 'Source/WebCore/editing')
126 files changed, 32501 insertions, 0 deletions
diff --git a/Source/WebCore/editing/AppendNodeCommand.cpp b/Source/WebCore/editing/AppendNodeCommand.cpp new file mode 100644 index 0000000..58f7fa6 --- /dev/null +++ b/Source/WebCore/editing/AppendNodeCommand.cpp @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2005, 2006, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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 "AppendNodeCommand.h" + +#include "AXObjectCache.h" +#include "htmlediting.h" + +namespace WebCore { + +AppendNodeCommand::AppendNodeCommand(PassRefPtr<Element> parent, PassRefPtr<Node> node) + : SimpleEditCommand(parent->document()) + , m_parent(parent) + , m_node(node) +{ + ASSERT(m_parent); + ASSERT(m_node); + ASSERT(!m_node->parentNode()); + + ASSERT(m_parent->isContentEditable() || !m_parent->attached()); +} + +static void sendAXTextChangedIgnoringLineBreaks(Node* node, AXObjectCache::AXTextChange textChange) +{ + String nodeValue = node->nodeValue(); + unsigned len = nodeValue.length(); + // Don't consider linebreaks in this command + if (nodeValue == "\n") + return; + + node->document()->axObjectCache()->nodeTextChangeNotification(node->renderer(), textChange, 0, len); +} + +void AppendNodeCommand::doApply() +{ + if (!m_parent->isContentEditable() && m_parent->attached()) + return; + + ExceptionCode ec; + m_parent->appendChild(m_node.get(), ec); + + if (AXObjectCache::accessibilityEnabled()) + sendAXTextChangedIgnoringLineBreaks(m_node.get(), AXObjectCache::AXTextInserted); +} + +void AppendNodeCommand::doUnapply() +{ + if (!m_parent->isContentEditable()) + return; + + // Need to notify this before actually deleting the text + if (AXObjectCache::accessibilityEnabled()) + sendAXTextChangedIgnoringLineBreaks(m_node.get(), AXObjectCache::AXTextDeleted); + + ExceptionCode ec; + m_node->remove(ec); +} + +} // namespace WebCore diff --git a/Source/WebCore/editing/AppendNodeCommand.h b/Source/WebCore/editing/AppendNodeCommand.h new file mode 100644 index 0000000..5ffb881 --- /dev/null +++ b/Source/WebCore/editing/AppendNodeCommand.h @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2005, 2006, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef AppendNodeCommand_h +#define AppendNodeCommand_h + +#include "EditCommand.h" + +namespace WebCore { + +class AppendNodeCommand : public SimpleEditCommand { +public: + static PassRefPtr<AppendNodeCommand> create(PassRefPtr<Element> parent, PassRefPtr<Node> node) + { + return adoptRef(new AppendNodeCommand(parent, node)); + } + +private: + AppendNodeCommand(PassRefPtr<Element> parent, PassRefPtr<Node> node); + + virtual void doApply(); + virtual void doUnapply(); + + RefPtr<Element> m_parent; + RefPtr<Node> m_node; +}; + +} // namespace WebCore + +#endif // AppendNodeCommand_h 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(); +} + +} diff --git a/Source/WebCore/editing/ApplyBlockElementCommand.h b/Source/WebCore/editing/ApplyBlockElementCommand.h new file mode 100644 index 0000000..535f499 --- /dev/null +++ b/Source/WebCore/editing/ApplyBlockElementCommand.h @@ -0,0 +1,61 @@ +/* + * 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: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * 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. + */ + +#ifndef ApplyBlockElementCommand_h +#define ApplyBlockElementCommand_h + +#include "CompositeEditCommand.h" + +namespace WebCore { + +class ApplyBlockElementCommand : public CompositeEditCommand { +protected: + ApplyBlockElementCommand(Document*, const QualifiedName& tagName, const AtomicString& className, const AtomicString& inlineStyle); + ApplyBlockElementCommand(Document*, const QualifiedName& tagName); + + virtual void formatSelection(const VisiblePosition& startOfSelection, const VisiblePosition& endOfSelection); + PassRefPtr<Element> createBlockElement() const; + const QualifiedName tagName() const { return m_tagName; } + +private: + virtual void doApply(); + virtual void formatRange(const Position& start, const Position& end, const Position& endOfSelection, RefPtr<Element>&) = 0; + void rangeForParagraphSplittingTextNodesIfNeeded(const VisiblePosition&, Position&, Position&); + VisiblePosition endOfNextParagrahSplittingTextNodesIfNeeded(VisiblePosition&, Position&, Position&); + + QualifiedName m_tagName; + AtomicString m_className; + AtomicString m_inlineStyle; + Position m_endOfLastParagraph; +}; + +} + +#endif diff --git a/Source/WebCore/editing/ApplyStyleCommand.cpp b/Source/WebCore/editing/ApplyStyleCommand.cpp new file mode 100644 index 0000000..71b6a27 --- /dev/null +++ b/Source/WebCore/editing/ApplyStyleCommand.cpp @@ -0,0 +1,1931 @@ +/* + * Copyright (C) 2005, 2006, 2008, 2009 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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 "ApplyStyleCommand.h" + +#include "CSSComputedStyleDeclaration.h" +#include "CSSMutableStyleDeclaration.h" +#include "CSSParser.h" +#include "CSSProperty.h" +#include "CSSPropertyNames.h" +#include "CSSStyleSelector.h" +#include "CSSValueKeywords.h" +#include "Document.h" +#include "EditingStyle.h" +#include "Editor.h" +#include "Frame.h" +#include "HTMLFontElement.h" +#include "HTMLInterchange.h" +#include "HTMLNames.h" +#include "NodeList.h" +#include "Range.h" +#include "RenderObject.h" +#include "Text.h" +#include "TextIterator.h" +#include "htmlediting.h" +#include "visible_units.h" +#include <wtf/StdLibExtras.h> + +namespace WebCore { + +using namespace HTMLNames; + +static RGBA32 getRGBAFontColor(CSSStyleDeclaration* style) +{ + RefPtr<CSSValue> colorValue = style->getPropertyCSSValue(CSSPropertyColor); + if (!colorValue) + return Color::transparent; + + ASSERT(colorValue->isPrimitiveValue()); + + CSSPrimitiveValue* primitiveColor = static_cast<CSSPrimitiveValue*>(colorValue.get()); + RGBA32 rgba = 0; + if (primitiveColor->primitiveType() != CSSPrimitiveValue::CSS_RGBCOLOR) { + CSSParser::parseColor(rgba, colorValue->cssText()); + // Need to take care of named color such as green and black + // This code should be removed after https://bugs.webkit.org/show_bug.cgi?id=28282 is fixed. + } else + rgba = primitiveColor->getRGBA32Value(); + + return rgba; +} + +class StyleChange { +public: + explicit StyleChange(CSSStyleDeclaration*, const Position&); + + String cssStyle() const { return m_cssStyle; } + bool applyBold() const { return m_applyBold; } + bool applyItalic() const { return m_applyItalic; } + bool applyUnderline() const { return m_applyUnderline; } + bool applyLineThrough() const { return m_applyLineThrough; } + bool applySubscript() const { return m_applySubscript; } + bool applySuperscript() const { return m_applySuperscript; } + bool applyFontColor() const { return m_applyFontColor.length() > 0; } + bool applyFontFace() const { return m_applyFontFace.length() > 0; } + bool applyFontSize() const { return m_applyFontSize.length() > 0; } + + String fontColor() { return m_applyFontColor; } + String fontFace() { return m_applyFontFace; } + String fontSize() { return m_applyFontSize; } + + bool operator==(const StyleChange& other) + { + return m_cssStyle == other.m_cssStyle + && m_applyBold == other.m_applyBold + && m_applyItalic == other.m_applyItalic + && m_applyUnderline == other.m_applyUnderline + && m_applyLineThrough == other.m_applyLineThrough + && m_applySubscript == other.m_applySubscript + && m_applySuperscript == other.m_applySuperscript + && m_applyFontColor == other.m_applyFontColor + && m_applyFontFace == other.m_applyFontFace + && m_applyFontSize == other.m_applyFontSize; + } + bool operator!=(const StyleChange& other) + { + return !(*this == other); + } +private: + void init(PassRefPtr<CSSStyleDeclaration>, const Position&); + void reconcileTextDecorationProperties(CSSMutableStyleDeclaration*); + void extractTextStyles(Document*, CSSMutableStyleDeclaration*, bool shouldUseFixedFontDefautlSize); + + String m_cssStyle; + bool m_applyBold; + bool m_applyItalic; + bool m_applyUnderline; + bool m_applyLineThrough; + bool m_applySubscript; + bool m_applySuperscript; + String m_applyFontColor; + String m_applyFontFace; + String m_applyFontSize; +}; + + +StyleChange::StyleChange(CSSStyleDeclaration* style, const Position& position) + : m_applyBold(false) + , m_applyItalic(false) + , m_applyUnderline(false) + , m_applyLineThrough(false) + , m_applySubscript(false) + , m_applySuperscript(false) +{ + init(style, position); +} + +void StyleChange::init(PassRefPtr<CSSStyleDeclaration> style, const Position& position) +{ + Document* document = position.node() ? position.node()->document() : 0; + if (!document || !document->frame()) + return; + + RefPtr<CSSComputedStyleDeclaration> computedStyle = position.computedStyle(); + RefPtr<CSSMutableStyleDeclaration> mutableStyle = getPropertiesNotIn(style.get(), computedStyle.get()); + + reconcileTextDecorationProperties(mutableStyle.get()); + if (!document->frame()->editor()->shouldStyleWithCSS()) + extractTextStyles(document, mutableStyle.get(), computedStyle->useFixedFontDefaultSize()); + + // Changing the whitespace style in a tab span would collapse the tab into a space. + if (isTabSpanTextNode(position.node()) || isTabSpanNode((position.node()))) + mutableStyle->removeProperty(CSSPropertyWhiteSpace); + + // If unicode-bidi is present in mutableStyle and direction is not, then add direction to mutableStyle. + // FIXME: Shouldn't this be done in getPropertiesNotIn? + if (mutableStyle->getPropertyCSSValue(CSSPropertyUnicodeBidi) && !style->getPropertyCSSValue(CSSPropertyDirection)) + mutableStyle->setProperty(CSSPropertyDirection, style->getPropertyValue(CSSPropertyDirection)); + + // Save the result for later + m_cssStyle = mutableStyle->cssText().stripWhiteSpace(); +} + +void StyleChange::reconcileTextDecorationProperties(CSSMutableStyleDeclaration* style) +{ + RefPtr<CSSValue> textDecorationsInEffect = style->getPropertyCSSValue(CSSPropertyWebkitTextDecorationsInEffect); + RefPtr<CSSValue> textDecoration = style->getPropertyCSSValue(CSSPropertyTextDecoration); + // We shouldn't have both text-decoration and -webkit-text-decorations-in-effect because that wouldn't make sense. + ASSERT(!textDecorationsInEffect || !textDecoration); + if (textDecorationsInEffect) { + style->setProperty(CSSPropertyTextDecoration, textDecorationsInEffect->cssText()); + style->removeProperty(CSSPropertyWebkitTextDecorationsInEffect); + textDecoration = textDecorationsInEffect; + } + + // If text-decoration is set to "none", remove the property because we don't want to add redundant "text-decoration: none". + if (textDecoration && !textDecoration->isValueList()) + style->removeProperty(CSSPropertyTextDecoration); +} + +static int getIdentifierValue(CSSStyleDeclaration* style, int propertyID) +{ + if (!style) + return 0; + + RefPtr<CSSValue> value = style->getPropertyCSSValue(propertyID); + if (!value || !value->isPrimitiveValue()) + return 0; + + return static_cast<CSSPrimitiveValue*>(value.get())->getIdent(); +} + +static void setTextDecorationProperty(CSSMutableStyleDeclaration* style, const CSSValueList* newTextDecoration, int propertyID) +{ + if (newTextDecoration->length()) + style->setProperty(propertyID, newTextDecoration->cssText(), style->getPropertyPriority(propertyID)); + else { + // text-decoration: none is redundant since it does not remove any text decorations. + ASSERT(!style->getPropertyPriority(propertyID)); + style->removeProperty(propertyID); + } +} + +void StyleChange::extractTextStyles(Document* document, CSSMutableStyleDeclaration* style, bool shouldUseFixedFontDefautlSize) +{ + ASSERT(style); + + if (getIdentifierValue(style, CSSPropertyFontWeight) == CSSValueBold) { + style->removeProperty(CSSPropertyFontWeight); + m_applyBold = true; + } + + int fontStyle = getIdentifierValue(style, CSSPropertyFontStyle); + if (fontStyle == CSSValueItalic || fontStyle == CSSValueOblique) { + style->removeProperty(CSSPropertyFontStyle); + m_applyItalic = true; + } + + // Assuming reconcileTextDecorationProperties has been called, there should not be -webkit-text-decorations-in-effect + // Furthermore, text-decoration: none has been trimmed so that text-decoration property is always a CSSValueList. + RefPtr<CSSValue> textDecoration = style->getPropertyCSSValue(CSSPropertyTextDecoration); + if (textDecoration && textDecoration->isValueList()) { + DEFINE_STATIC_LOCAL(RefPtr<CSSPrimitiveValue>, underline, (CSSPrimitiveValue::createIdentifier(CSSValueUnderline))); + DEFINE_STATIC_LOCAL(RefPtr<CSSPrimitiveValue>, lineThrough, (CSSPrimitiveValue::createIdentifier(CSSValueLineThrough))); + + RefPtr<CSSValueList> newTextDecoration = static_cast<CSSValueList*>(textDecoration.get())->copy(); + if (newTextDecoration->removeAll(underline.get())) + m_applyUnderline = true; + if (newTextDecoration->removeAll(lineThrough.get())) + m_applyLineThrough = true; + + // If trimTextDecorations, delete underline and line-through + setTextDecorationProperty(style, newTextDecoration.get(), CSSPropertyTextDecoration); + } + + int verticalAlign = getIdentifierValue(style, CSSPropertyVerticalAlign); + switch (verticalAlign) { + case CSSValueSub: + style->removeProperty(CSSPropertyVerticalAlign); + m_applySubscript = true; + break; + case CSSValueSuper: + style->removeProperty(CSSPropertyVerticalAlign); + m_applySuperscript = true; + break; + } + + if (style->getPropertyCSSValue(CSSPropertyColor)) { + m_applyFontColor = Color(getRGBAFontColor(style)).name(); + style->removeProperty(CSSPropertyColor); + } + + m_applyFontFace = style->getPropertyValue(CSSPropertyFontFamily); + style->removeProperty(CSSPropertyFontFamily); + + if (RefPtr<CSSValue> fontSize = style->getPropertyCSSValue(CSSPropertyFontSize)) { + if (!fontSize->isPrimitiveValue()) + style->removeProperty(CSSPropertyFontSize); // Can't make sense of the number. Put no font size. + else { + CSSPrimitiveValue* value = static_cast<CSSPrimitiveValue*>(fontSize.get()); + if (value->primitiveType() >= CSSPrimitiveValue::CSS_PX && value->primitiveType() <= CSSPrimitiveValue::CSS_PC) { + int pixelFontSize = value->getFloatValue(CSSPrimitiveValue::CSS_PX); + int legacyFontSize = CSSStyleSelector::legacyFontSize(document, pixelFontSize, shouldUseFixedFontDefautlSize); + // Use legacy font size only if pixel value matches exactly to that of legacy font size. + if (CSSStyleSelector::fontSizeForKeyword(document, legacyFontSize - 1 + CSSValueXSmall, shouldUseFixedFontDefautlSize) == pixelFontSize) { + m_applyFontSize = String::number(legacyFontSize); + style->removeProperty(CSSPropertyFontSize); + } + } else if (CSSValueXSmall <= value->getIdent() && value->getIdent() <= CSSValueWebkitXxxLarge) { + m_applyFontSize = String::number(value->getIdent() - CSSValueXSmall + 1); + style->removeProperty(CSSPropertyFontSize); + } + } + } +} + +static String& styleSpanClassString() +{ + DEFINE_STATIC_LOCAL(String, styleSpanClassString, ((AppleStyleSpanClass))); + return styleSpanClassString; +} + +bool isStyleSpan(const Node *node) +{ + if (!node || !node->isHTMLElement()) + return false; + + const HTMLElement* elem = static_cast<const HTMLElement*>(node); + return elem->hasLocalName(spanAttr) && elem->getAttribute(classAttr) == styleSpanClassString(); +} + +static bool isUnstyledStyleSpan(const Node* node) +{ + if (!node || !node->isHTMLElement() || !node->hasTagName(spanTag)) + return false; + + const HTMLElement* elem = static_cast<const HTMLElement*>(node); + CSSMutableStyleDeclaration* inlineStyleDecl = elem->inlineStyleDecl(); + return (!inlineStyleDecl || inlineStyleDecl->isEmpty()) && elem->getAttribute(classAttr) == styleSpanClassString(); +} + +static bool isSpanWithoutAttributesOrUnstyleStyleSpan(const Node* node) +{ + if (!node || !node->isHTMLElement() || !node->hasTagName(spanTag)) + return false; + + const HTMLElement* elem = static_cast<const HTMLElement*>(node); + NamedNodeMap* attributes = elem->attributes(true); // readonly + if (attributes->isEmpty()) + return true; + + return isUnstyledStyleSpan(node); +} + +static bool isEmptyFontTag(const Node *node) +{ + if (!node || !node->hasTagName(fontTag)) + return false; + + const Element *elem = static_cast<const Element *>(node); + NamedNodeMap *map = elem->attributes(true); // true for read-only + if (!map) + return true; + return map->isEmpty() || (map->length() == 1 && elem->getAttribute(classAttr) == styleSpanClassString()); +} + +static PassRefPtr<Element> createFontElement(Document* document) +{ + RefPtr<Element> fontNode = createHTMLElement(document, fontTag); + fontNode->setAttribute(classAttr, styleSpanClassString()); + return fontNode.release(); +} + +PassRefPtr<HTMLElement> createStyleSpanElement(Document* document) +{ + RefPtr<HTMLElement> styleElement = createHTMLElement(document, spanTag); + styleElement->setAttribute(classAttr, styleSpanClassString()); + return styleElement.release(); +} + +static void diffTextDecorations(CSSMutableStyleDeclaration* style, int propertID, CSSValue* refTextDecoration) +{ + RefPtr<CSSValue> textDecoration = style->getPropertyCSSValue(propertID); + if (!textDecoration || !textDecoration->isValueList() || !refTextDecoration || !refTextDecoration->isValueList()) + return; + + RefPtr<CSSValueList> newTextDecoration = static_cast<CSSValueList*>(textDecoration.get())->copy(); + CSSValueList* valuesInRefTextDecoration = static_cast<CSSValueList*>(refTextDecoration); + + for (size_t i = 0; i < valuesInRefTextDecoration->length(); i++) + newTextDecoration->removeAll(valuesInRefTextDecoration->item(i)); + + setTextDecorationProperty(style, newTextDecoration.get(), propertID); +} + +static bool fontWeightIsBold(CSSStyleDeclaration* style) +{ + ASSERT(style); + RefPtr<CSSValue> fontWeight = style->getPropertyCSSValue(CSSPropertyFontWeight); + + if (!fontWeight) + return false; + if (!fontWeight->isPrimitiveValue()) + return false; + + // Because b tag can only bold text, there are only two states in plain html: bold and not bold. + // Collapse all other values to either one of these two states for editing purposes. + switch (static_cast<CSSPrimitiveValue*>(fontWeight.get())->getIdent()) { + case CSSValue100: + case CSSValue200: + case CSSValue300: + case CSSValue400: + case CSSValue500: + case CSSValueNormal: + return false; + case CSSValueBold: + case CSSValue600: + case CSSValue700: + case CSSValue800: + case CSSValue900: + return true; + } + + ASSERT_NOT_REACHED(); // For CSSValueBolder and CSSValueLighter + return false; // Make compiler happy +} + +static int getTextAlignment(CSSStyleDeclaration* style) +{ + int textAlign = getIdentifierValue(style, CSSPropertyTextAlign); + switch (textAlign) { + case CSSValueCenter: + case CSSValueWebkitCenter: + return CSSValueCenter; + case CSSValueJustify: + return CSSValueJustify; + case CSSValueLeft: + case CSSValueWebkitLeft: + return CSSValueLeft; + case CSSValueRight: + case CSSValueWebkitRight: + return CSSValueRight; + } + return CSSValueInvalid; +} + +RefPtr<CSSMutableStyleDeclaration> getPropertiesNotIn(CSSStyleDeclaration* styleWithRedundantProperties, CSSStyleDeclaration* baseStyle) +{ + ASSERT(styleWithRedundantProperties); + ASSERT(baseStyle); + RefPtr<CSSMutableStyleDeclaration> result = styleWithRedundantProperties->copy(); + baseStyle->diff(result.get()); + + RefPtr<CSSValue> baseTextDecorationsInEffect = baseStyle->getPropertyCSSValue(CSSPropertyWebkitTextDecorationsInEffect); + diffTextDecorations(result.get(), CSSPropertyTextDecoration, baseTextDecorationsInEffect.get()); + diffTextDecorations(result.get(), CSSPropertyWebkitTextDecorationsInEffect, baseTextDecorationsInEffect.get()); + + if (fontWeightIsBold(result.get()) == fontWeightIsBold(baseStyle)) + result->removeProperty(CSSPropertyFontWeight); + + if (getRGBAFontColor(result.get()) == getRGBAFontColor(baseStyle)) + result->removeProperty(CSSPropertyColor); + + if (getTextAlignment(result.get()) == getTextAlignment(baseStyle)) + result->removeProperty(CSSPropertyTextAlign); + + return result; +} + +ApplyStyleCommand::ApplyStyleCommand(Document* document, const EditingStyle* style, EditAction editingAction, EPropertyLevel propertyLevel) + : CompositeEditCommand(document) + , m_style(style->copy()) + , m_editingAction(editingAction) + , m_propertyLevel(propertyLevel) + , m_start(endingSelection().start().downstream()) + , m_end(endingSelection().end().upstream()) + , m_useEndingSelection(true) + , m_styledInlineElement(0) + , m_removeOnly(false) + , m_isInlineElementToRemoveFunction(0) +{ +} + +ApplyStyleCommand::ApplyStyleCommand(Document* document, const EditingStyle* style, const Position& start, const Position& end, EditAction editingAction, EPropertyLevel propertyLevel) + : CompositeEditCommand(document) + , m_style(style->copy()) + , m_editingAction(editingAction) + , m_propertyLevel(propertyLevel) + , m_start(start) + , m_end(end) + , m_useEndingSelection(false) + , m_styledInlineElement(0) + , m_removeOnly(false) + , m_isInlineElementToRemoveFunction(0) +{ +} + +ApplyStyleCommand::ApplyStyleCommand(PassRefPtr<Element> element, bool removeOnly, EditAction editingAction) + : CompositeEditCommand(element->document()) + , m_style(EditingStyle::create()) + , m_editingAction(editingAction) + , m_propertyLevel(PropertyDefault) + , m_start(endingSelection().start().downstream()) + , m_end(endingSelection().end().upstream()) + , m_useEndingSelection(true) + , m_styledInlineElement(element) + , m_removeOnly(removeOnly) + , m_isInlineElementToRemoveFunction(0) +{ +} + +ApplyStyleCommand::ApplyStyleCommand(Document* document, const EditingStyle* style, IsInlineElementToRemoveFunction isInlineElementToRemoveFunction, EditAction editingAction) + : CompositeEditCommand(document) + , m_style(style->copy()) + , m_editingAction(editingAction) + , m_propertyLevel(PropertyDefault) + , m_start(endingSelection().start().downstream()) + , m_end(endingSelection().end().upstream()) + , m_useEndingSelection(true) + , m_styledInlineElement(0) + , m_removeOnly(true) + , m_isInlineElementToRemoveFunction(isInlineElementToRemoveFunction) +{ +} + +void ApplyStyleCommand::updateStartEnd(const Position& newStart, const Position& newEnd) +{ + ASSERT(comparePositions(newEnd, newStart) >= 0); + + if (!m_useEndingSelection && (newStart != m_start || newEnd != m_end)) + m_useEndingSelection = true; + + setEndingSelection(VisibleSelection(newStart, newEnd, VP_DEFAULT_AFFINITY)); + m_start = newStart; + m_end = newEnd; +} + +Position ApplyStyleCommand::startPosition() +{ + if (m_useEndingSelection) + return endingSelection().start(); + + return m_start; +} + +Position ApplyStyleCommand::endPosition() +{ + if (m_useEndingSelection) + return endingSelection().end(); + + return m_end; +} + +void ApplyStyleCommand::doApply() +{ + switch (m_propertyLevel) { + case PropertyDefault: { + // Apply the block-centric properties of the style. + RefPtr<EditingStyle> blockStyle = m_style->extractAndRemoveBlockProperties(); + if (!blockStyle->isEmpty()) + applyBlockStyle(blockStyle->style()); + // Apply any remaining styles to the inline elements. + if (!m_style->isEmpty() || m_styledInlineElement || m_isInlineElementToRemoveFunction) { + RefPtr<CSSMutableStyleDeclaration> style = m_style->style() ? m_style->style() : CSSMutableStyleDeclaration::create(); + applyRelativeFontStyleChange(m_style.get()); + applyInlineStyle(style.get()); + } + break; + } + case ForceBlockProperties: + // Force all properties to be applied as block styles. + applyBlockStyle(m_style->style()); + break; + } +} + +EditAction ApplyStyleCommand::editingAction() const +{ + return m_editingAction; +} + +void ApplyStyleCommand::applyBlockStyle(CSSMutableStyleDeclaration *style) +{ + // update document layout once before removing styles + // so that we avoid the expense of updating before each and every call + // to check a computed style + updateLayout(); + + // get positions we want to use for applying style + Position start = startPosition(); + Position end = endPosition(); + if (comparePositions(end, start) < 0) { + Position swap = start; + start = end; + end = swap; + } + + VisiblePosition visibleStart(start); + VisiblePosition visibleEnd(end); + + if (visibleStart.isNull() || visibleStart.isOrphan() || visibleEnd.isNull() || visibleEnd.isOrphan()) + return; + + // Save and restore the selection endpoints using their indices in the document, since + // addBlockStyleIfNeeded may moveParagraphs, which can remove these endpoints. + // Calculate start and end indices from the start of the tree that they're in. + Node* scope = highestAncestor(visibleStart.deepEquivalent().node()); + Position rangeStart(scope, 0); + RefPtr<Range> startRange = Range::create(document(), rangeStart, rangeCompliantEquivalent(visibleStart.deepEquivalent())); + RefPtr<Range> endRange = Range::create(document(), rangeStart, rangeCompliantEquivalent(visibleEnd.deepEquivalent())); + int startIndex = TextIterator::rangeLength(startRange.get(), true); + int endIndex = TextIterator::rangeLength(endRange.get(), true); + + VisiblePosition paragraphStart(startOfParagraph(visibleStart)); + VisiblePosition nextParagraphStart(endOfParagraph(paragraphStart).next()); + VisiblePosition beyondEnd(endOfParagraph(visibleEnd).next()); + while (paragraphStart.isNotNull() && paragraphStart != beyondEnd) { + StyleChange styleChange(style, paragraphStart.deepEquivalent()); + if (styleChange.cssStyle().length() || m_removeOnly) { + RefPtr<Node> block = enclosingBlock(paragraphStart.deepEquivalent().node()); + if (!m_removeOnly) { + RefPtr<Node> newBlock = moveParagraphContentsToNewBlockIfNecessary(paragraphStart.deepEquivalent()); + if (newBlock) + block = newBlock; + } + ASSERT(block->isHTMLElement()); + if (block->isHTMLElement()) { + removeCSSStyle(style, static_cast<HTMLElement*>(block.get())); + if (!m_removeOnly) + addBlockStyle(styleChange, static_cast<HTMLElement*>(block.get())); + } + + if (nextParagraphStart.isOrphan()) + nextParagraphStart = endOfParagraph(paragraphStart).next(); + } + + paragraphStart = nextParagraphStart; + nextParagraphStart = endOfParagraph(paragraphStart).next(); + } + + startRange = TextIterator::rangeFromLocationAndLength(static_cast<Element*>(scope), startIndex, 0, true); + endRange = TextIterator::rangeFromLocationAndLength(static_cast<Element*>(scope), endIndex, 0, true); + if (startRange && endRange) + updateStartEnd(startRange->startPosition(), endRange->startPosition()); +} + +void ApplyStyleCommand::applyRelativeFontStyleChange(EditingStyle* style) +{ + static const float MinimumFontSize = 0.1f; + + if (!style || !style->hasFontSizeDelta()) + return; + + Position start = startPosition(); + Position end = endPosition(); + if (comparePositions(end, start) < 0) { + Position swap = start; + start = end; + end = swap; + } + + // Join up any adjacent text nodes. + if (start.node()->isTextNode()) { + joinChildTextNodes(start.node()->parentNode(), start, end); + start = startPosition(); + end = endPosition(); + } + if (end.node()->isTextNode() && start.node()->parentNode() != end.node()->parentNode()) { + joinChildTextNodes(end.node()->parentNode(), start, end); + start = startPosition(); + end = endPosition(); + } + + // Split the start text nodes if needed to apply style. + if (isValidCaretPositionInTextNode(start)) { + splitTextAtStart(start, end); + start = startPosition(); + end = endPosition(); + } + + if (isValidCaretPositionInTextNode(end)) { + splitTextAtEnd(start, end); + start = startPosition(); + end = endPosition(); + } + + // Calculate loop end point. + // If the end node is before the start node (can only happen if the end node is + // an ancestor of the start node), we gather nodes up to the next sibling of the end node + Node *beyondEnd; + if (start.node()->isDescendantOf(end.node())) + beyondEnd = end.node()->traverseNextSibling(); + else + beyondEnd = end.node()->traverseNextNode(); + + start = start.upstream(); // Move upstream to ensure we do not add redundant spans. + Node *startNode = start.node(); + if (startNode->isTextNode() && start.deprecatedEditingOffset() >= caretMaxOffset(startNode)) // Move out of text node if range does not include its characters. + startNode = startNode->traverseNextNode(); + + // Store away font size before making any changes to the document. + // This ensures that changes to one node won't effect another. + HashMap<Node*, float> startingFontSizes; + for (Node *node = startNode; node != beyondEnd; node = node->traverseNextNode()) + startingFontSizes.set(node, computedFontSize(node)); + + // These spans were added by us. If empty after font size changes, they can be removed. + Vector<RefPtr<HTMLElement> > unstyledSpans; + + Node* lastStyledNode = 0; + for (Node* node = startNode; node != beyondEnd; node = node->traverseNextNode()) { + RefPtr<HTMLElement> element; + if (node->isHTMLElement()) { + // Only work on fully selected nodes. + if (!nodeFullySelected(node, start, end)) + continue; + element = static_cast<HTMLElement*>(node); + } else if (node->isTextNode() && node->renderer() && node->parentNode() != lastStyledNode) { + // Last styled node was not parent node of this text node, but we wish to style this + // text node. To make this possible, add a style span to surround this text node. + RefPtr<HTMLElement> span = createStyleSpanElement(document()); + surroundNodeRangeWithElement(node, node, span.get()); + element = span.release(); + } else { + // Only handle HTML elements and text nodes. + continue; + } + lastStyledNode = node; + + CSSMutableStyleDeclaration* inlineStyleDecl = element->getInlineStyleDecl(); + float currentFontSize = computedFontSize(node); + float desiredFontSize = max(MinimumFontSize, startingFontSizes.get(node) + style->fontSizeDelta()); + RefPtr<CSSValue> value = inlineStyleDecl->getPropertyCSSValue(CSSPropertyFontSize); + if (value) { + inlineStyleDecl->removeProperty(CSSPropertyFontSize, true); + currentFontSize = computedFontSize(node); + } + if (currentFontSize != desiredFontSize) { + inlineStyleDecl->setProperty(CSSPropertyFontSize, String::number(desiredFontSize) + "px", false, false); + setNodeAttribute(element.get(), styleAttr, inlineStyleDecl->cssText()); + } + if (inlineStyleDecl->isEmpty()) { + removeNodeAttribute(element.get(), styleAttr); + // FIXME: should this be isSpanWithoutAttributesOrUnstyleStyleSpan? Need a test. + if (isUnstyledStyleSpan(element.get())) + unstyledSpans.append(element.release()); + } + } + + size_t size = unstyledSpans.size(); + for (size_t i = 0; i < size; ++i) + removeNodePreservingChildren(unstyledSpans[i].get()); +} + +static Node* dummySpanAncestorForNode(const Node* node) +{ + while (node && !isStyleSpan(node)) + node = node->parentNode(); + + return node ? node->parentNode() : 0; +} + +void ApplyStyleCommand::cleanupUnstyledAppleStyleSpans(Node* dummySpanAncestor) +{ + if (!dummySpanAncestor) + return; + + // Dummy spans are created when text node is split, so that style information + // can be propagated, which can result in more splitting. If a dummy span gets + // cloned/split, the new node is always a sibling of it. Therefore, we scan + // all the children of the dummy's parent + Node* next; + for (Node* node = dummySpanAncestor->firstChild(); node; node = next) { + next = node->nextSibling(); + if (isUnstyledStyleSpan(node)) + removeNodePreservingChildren(node); + node = next; + } +} + +HTMLElement* ApplyStyleCommand::splitAncestorsWithUnicodeBidi(Node* node, bool before, int allowedDirection) +{ + // We are allowed to leave the highest ancestor with unicode-bidi unsplit if it is unicode-bidi: embed and direction: allowedDirection. + // In that case, we return the unsplit ancestor. Otherwise, we return 0. + Node* block = enclosingBlock(node); + if (!block) + return 0; + + Node* highestAncestorWithUnicodeBidi = 0; + Node* nextHighestAncestorWithUnicodeBidi = 0; + int highestAncestorUnicodeBidi = 0; + for (Node* n = node->parentNode(); n != block; n = n->parentNode()) { + int unicodeBidi = getIdentifierValue(computedStyle(n).get(), CSSPropertyUnicodeBidi); + if (unicodeBidi && unicodeBidi != CSSValueNormal) { + highestAncestorUnicodeBidi = unicodeBidi; + nextHighestAncestorWithUnicodeBidi = highestAncestorWithUnicodeBidi; + highestAncestorWithUnicodeBidi = n; + } + } + + if (!highestAncestorWithUnicodeBidi) + return 0; + + HTMLElement* unsplitAncestor = 0; + + if (allowedDirection && highestAncestorUnicodeBidi != CSSValueBidiOverride + && getIdentifierValue(computedStyle(highestAncestorWithUnicodeBidi).get(), CSSPropertyDirection) == allowedDirection + && highestAncestorWithUnicodeBidi->isHTMLElement()) { + if (!nextHighestAncestorWithUnicodeBidi) + return static_cast<HTMLElement*>(highestAncestorWithUnicodeBidi); + + unsplitAncestor = static_cast<HTMLElement*>(highestAncestorWithUnicodeBidi); + highestAncestorWithUnicodeBidi = nextHighestAncestorWithUnicodeBidi; + } + + // Split every ancestor through highest ancestor with embedding. + Node* n = node; + while (true) { + Element* parent = static_cast<Element*>(n->parentNode()); + if (before ? n->previousSibling() : n->nextSibling()) + splitElement(parent, before ? n : n->nextSibling()); + if (parent == highestAncestorWithUnicodeBidi) + break; + n = n->parentNode(); + } + return unsplitAncestor; +} + +void ApplyStyleCommand::removeEmbeddingUpToEnclosingBlock(Node* node, Node* unsplitAncestor) +{ + Node* block = enclosingBlock(node); + if (!block) + return; + + Node* parent = 0; + for (Node* n = node->parentNode(); n != block && n != unsplitAncestor; n = parent) { + parent = n->parentNode(); + if (!n->isStyledElement()) + continue; + + StyledElement* element = static_cast<StyledElement*>(n); + int unicodeBidi = getIdentifierValue(computedStyle(element).get(), CSSPropertyUnicodeBidi); + if (!unicodeBidi || unicodeBidi == CSSValueNormal) + continue; + + // FIXME: This code should really consider the mapped attribute 'dir', the inline style declaration, + // and all matching style rules in order to determine how to best set the unicode-bidi property to 'normal'. + // For now, it assumes that if the 'dir' attribute is present, then removing it will suffice, and + // otherwise it sets the property in the inline style declaration. + if (element->hasAttribute(dirAttr)) { + // FIXME: If this is a BDO element, we should probably just remove it if it has no + // other attributes, like we (should) do with B and I elements. + removeNodeAttribute(element, dirAttr); + } else { + RefPtr<CSSMutableStyleDeclaration> inlineStyle = element->getInlineStyleDecl()->copy(); + inlineStyle->setProperty(CSSPropertyUnicodeBidi, CSSValueNormal); + inlineStyle->removeProperty(CSSPropertyDirection); + setNodeAttribute(element, styleAttr, inlineStyle->cssText()); + // FIXME: should this be isSpanWithoutAttributesOrUnstyleStyleSpan? Need a test. + if (isUnstyledStyleSpan(element)) + removeNodePreservingChildren(element); + } + } +} + +static Node* highestEmbeddingAncestor(Node* startNode, Node* enclosingNode) +{ + for (Node* n = startNode; n && n != enclosingNode; n = n->parentNode()) { + if (n->isHTMLElement() && getIdentifierValue(computedStyle(n).get(), CSSPropertyUnicodeBidi) == CSSValueEmbed) + return n; + } + + return 0; +} + +void ApplyStyleCommand::applyInlineStyle(CSSMutableStyleDeclaration *style) +{ + Node* startDummySpanAncestor = 0; + Node* endDummySpanAncestor = 0; + + // update document layout once before removing styles + // so that we avoid the expense of updating before each and every call + // to check a computed style + updateLayout(); + + // adjust to the positions we want to use for applying style + Position start = startPosition(); + Position end = endPosition(); + if (comparePositions(end, start) < 0) { + Position swap = start; + start = end; + end = swap; + } + + // split the start node and containing element if the selection starts inside of it + bool splitStart = isValidCaretPositionInTextNode(start); + if (splitStart) { + if (shouldSplitTextElement(start.node()->parentElement(), style)) + splitTextElementAtStart(start, end); + else + splitTextAtStart(start, end); + start = startPosition(); + end = endPosition(); + startDummySpanAncestor = dummySpanAncestorForNode(start.node()); + } + + // split the end node and containing element if the selection ends inside of it + bool splitEnd = isValidCaretPositionInTextNode(end); + if (splitEnd) { + if (shouldSplitTextElement(end.node()->parentElement(), style)) + splitTextElementAtEnd(start, end); + else + splitTextAtEnd(start, end); + start = startPosition(); + end = endPosition(); + endDummySpanAncestor = dummySpanAncestorForNode(end.node()); + } + + // Remove style from the selection. + // Use the upstream position of the start for removing style. + // This will ensure we remove all traces of the relevant styles from the selection + // and prevent us from adding redundant ones, as described in: + // <rdar://problem/3724344> Bolding and unbolding creates extraneous tags + Position removeStart = start.upstream(); + int unicodeBidi = getIdentifierValue(style, CSSPropertyUnicodeBidi); + int direction = 0; + RefPtr<CSSMutableStyleDeclaration> styleWithoutEmbedding; + if (unicodeBidi) { + // Leave alone an ancestor that provides the desired single level embedding, if there is one. + if (unicodeBidi == CSSValueEmbed) + direction = getIdentifierValue(style, CSSPropertyDirection); + HTMLElement* startUnsplitAncestor = splitAncestorsWithUnicodeBidi(start.node(), true, direction); + HTMLElement* endUnsplitAncestor = splitAncestorsWithUnicodeBidi(end.node(), false, direction); + removeEmbeddingUpToEnclosingBlock(start.node(), startUnsplitAncestor); + removeEmbeddingUpToEnclosingBlock(end.node(), endUnsplitAncestor); + + // Avoid removing the dir attribute and the unicode-bidi and direction properties from the unsplit ancestors. + Position embeddingRemoveStart = removeStart; + if (startUnsplitAncestor && nodeFullySelected(startUnsplitAncestor, removeStart, end)) + embeddingRemoveStart = positionInParentAfterNode(startUnsplitAncestor); + + Position embeddingRemoveEnd = end; + if (endUnsplitAncestor && nodeFullySelected(endUnsplitAncestor, removeStart, end)) + embeddingRemoveEnd = positionInParentBeforeNode(endUnsplitAncestor).downstream(); + + if (embeddingRemoveEnd != removeStart || embeddingRemoveEnd != end) { + RefPtr<CSSMutableStyleDeclaration> embeddingStyle = CSSMutableStyleDeclaration::create(); + embeddingStyle->setProperty(CSSPropertyUnicodeBidi, CSSValueEmbed); + embeddingStyle->setProperty(CSSPropertyDirection, direction); + if (comparePositions(embeddingRemoveStart, embeddingRemoveEnd) <= 0) + removeInlineStyle(embeddingStyle, embeddingRemoveStart, embeddingRemoveEnd); + styleWithoutEmbedding = style->copy(); + styleWithoutEmbedding->removeProperty(CSSPropertyUnicodeBidi); + styleWithoutEmbedding->removeProperty(CSSPropertyDirection); + } + } + + removeInlineStyle(styleWithoutEmbedding ? styleWithoutEmbedding.get() : style, removeStart, end); + start = startPosition(); + end = endPosition(); + if (start.isNull() || start.isOrphan() || end.isNull() || end.isOrphan()) + return; + + if (splitStart) { + if (mergeStartWithPreviousIfIdentical(start, end)) { + start = startPosition(); + end = endPosition(); + } + } + + if (splitEnd) { + mergeEndWithNextIfIdentical(start, end); + start = startPosition(); + end = endPosition(); + } + + // update document layout once before running the rest of the function + // so that we avoid the expense of updating before each and every call + // to check a computed style + updateLayout(); + + RefPtr<CSSMutableStyleDeclaration> styleToApply = style; + if (unicodeBidi) { + // Avoid applying the unicode-bidi and direction properties beneath ancestors that already have them. + Node* embeddingStartNode = highestEmbeddingAncestor(start.node(), enclosingBlock(start.node())); + Node* embeddingEndNode = highestEmbeddingAncestor(end.node(), enclosingBlock(end.node())); + + if (embeddingStartNode || embeddingEndNode) { + Position embeddingApplyStart = embeddingStartNode ? positionInParentAfterNode(embeddingStartNode) : start; + Position embeddingApplyEnd = embeddingEndNode ? positionInParentBeforeNode(embeddingEndNode) : end; + ASSERT(embeddingApplyStart.isNotNull() && embeddingApplyEnd.isNotNull()); + + RefPtr<CSSMutableStyleDeclaration> embeddingStyle = CSSMutableStyleDeclaration::create(); + embeddingStyle->setProperty(CSSPropertyUnicodeBidi, CSSValueEmbed); + embeddingStyle->setProperty(CSSPropertyDirection, direction); + fixRangeAndApplyInlineStyle(embeddingStyle.get(), embeddingApplyStart, embeddingApplyEnd); + + if (styleWithoutEmbedding) + styleToApply = styleWithoutEmbedding; + else { + styleToApply = style->copy(); + styleToApply->removeProperty(CSSPropertyUnicodeBidi); + styleToApply->removeProperty(CSSPropertyDirection); + } + } + } + + fixRangeAndApplyInlineStyle(styleToApply.get(), start, end); + + // Remove dummy style spans created by splitting text elements. + cleanupUnstyledAppleStyleSpans(startDummySpanAncestor); + if (endDummySpanAncestor != startDummySpanAncestor) + cleanupUnstyledAppleStyleSpans(endDummySpanAncestor); +} + +void ApplyStyleCommand::fixRangeAndApplyInlineStyle(CSSMutableStyleDeclaration* style, const Position& start, const Position& end) +{ + Node* startNode = start.node(); + + if (start.deprecatedEditingOffset() >= caretMaxOffset(start.node())) { + startNode = startNode->traverseNextNode(); + if (!startNode || comparePositions(end, Position(startNode, 0)) < 0) + return; + } + + Node* pastEndNode = end.node(); + if (end.deprecatedEditingOffset() >= caretMaxOffset(end.node())) + pastEndNode = end.node()->traverseNextSibling(); + + // FIXME: Callers should perform this operation on a Range that includes the br + // if they want style applied to the empty line. + if (start == end && start.node()->hasTagName(brTag)) + pastEndNode = start.node()->traverseNextNode(); + + // Start from the highest fully selected ancestor so that we can modify the fully selected node. + // e.g. When applying font-size: large on <font color="blue">hello</font>, we need to include the font element in our run + // to generate <font color="blue" size="4">hello</font> instead of <font color="blue"><font size="4">hello</font></font> + RefPtr<Range> range = Range::create(startNode->document(), start, end); + Element* editableRoot = startNode->rootEditableElement(); + if (startNode != editableRoot) { + while (editableRoot && startNode->parentNode() != editableRoot && isNodeVisiblyContainedWithin(startNode->parentNode(), range.get())) + startNode = startNode->parentNode(); + } + + applyInlineStyleToNodeRange(style, startNode, pastEndNode); +} + +static bool containsNonEditableRegion(Node* node) +{ + if (!node->isContentEditable()) + return true; + + Node* sibling = node->traverseNextSibling(); + for (Node* descendent = node->firstChild(); descendent && descendent != sibling; descendent = descendent->traverseNextNode()) { + if (!descendent->isContentEditable()) + return true; + } + + return false; +} + +void ApplyStyleCommand::applyInlineStyleToNodeRange(CSSMutableStyleDeclaration* style, Node* node, Node* pastEndNode) +{ + if (m_removeOnly) + return; + + for (RefPtr<Node> next; node && node != pastEndNode; node = next.get()) { + next = node->traverseNextNode(); + + if (!node->renderer() || !node->isContentEditable()) + continue; + + if (!node->isContentRichlyEditable() && node->isHTMLElement()) { + // This is a plaintext-only region. Only proceed if it's fully selected. + // pastEndNode is the node after the last fully selected node, so if it's inside node then + // node isn't fully selected. + if (pastEndNode && pastEndNode->isDescendantOf(node)) + break; + // Add to this element's inline style and skip over its contents. + HTMLElement* element = static_cast<HTMLElement*>(node); + RefPtr<CSSMutableStyleDeclaration> inlineStyle = element->getInlineStyleDecl()->copy(); + inlineStyle->merge(style); + setNodeAttribute(element, styleAttr, inlineStyle->cssText()); + next = node->traverseNextSibling(); + continue; + } + + if (isBlock(node)) + continue; + + if (node->childNodeCount()) { + if (node->contains(pastEndNode) || containsNonEditableRegion(node) || !node->parentNode()->isContentEditable()) + continue; + if (editingIgnoresContent(node)) { + next = node->traverseNextSibling(); + continue; + } + } + + RefPtr<Node> runStart = node; + RefPtr<Node> runEnd = node; + Node* sibling = node->nextSibling(); + while (sibling && sibling != pastEndNode && !sibling->contains(pastEndNode) + && (!isBlock(sibling) || sibling->hasTagName(brTag)) + && !containsNonEditableRegion(sibling)) { + runEnd = sibling; + sibling = runEnd->nextSibling(); + } + next = runEnd->traverseNextSibling(); + + if (!removeStyleFromRunBeforeApplyingStyle(style, runStart, runEnd)) + continue; + addInlineStyleIfNeeded(style, runStart.get(), runEnd.get(), AddStyledElement); + } +} + +bool ApplyStyleCommand::isStyledInlineElementToRemove(Element* element) const +{ + return (m_styledInlineElement && element->hasTagName(m_styledInlineElement->tagQName())) + || (m_isInlineElementToRemoveFunction && m_isInlineElementToRemoveFunction(element)); +} + +bool ApplyStyleCommand::removeStyleFromRunBeforeApplyingStyle(CSSMutableStyleDeclaration* style, RefPtr<Node>& runStart, RefPtr<Node>& runEnd) +{ + ASSERT(runStart && runEnd && runStart->parentNode() == runEnd->parentNode()); + RefPtr<Node> pastEndNode = runEnd->traverseNextSibling(); + bool needToApplyStyle = false; + for (Node* node = runStart.get(); node && node != pastEndNode.get(); node = node->traverseNextNode()) { + if (node->childNodeCount()) + continue; + // We don't consider m_isInlineElementToRemoveFunction here because we never apply style when m_isInlineElementToRemoveFunction is specified + if (getPropertiesNotIn(style, computedStyle(node).get())->length() + || (m_styledInlineElement && !enclosingNodeWithTag(positionBeforeNode(node), m_styledInlineElement->tagQName()))) { + needToApplyStyle = true; + break; + } + } + if (!needToApplyStyle) + return false; + + RefPtr<Node> next = runStart; + for (RefPtr<Node> node = next; node && node->inDocument() && node != pastEndNode; node = next) { + next = node->traverseNextNode(); + if (!node->isHTMLElement()) + continue; + + RefPtr<Node> previousSibling = node->previousSibling(); + RefPtr<Node> nextSibling = node->nextSibling(); + RefPtr<ContainerNode> parent = node->parentNode(); + removeInlineStyleFromElement(style, static_cast<HTMLElement*>(node.get()), RemoveAlways); + if (!node->inDocument()) { + // FIXME: We might need to update the start and the end of current selection here but need a test. + if (runStart == node) + runStart = previousSibling ? previousSibling->nextSibling() : parent->firstChild(); + if (runEnd == node) + runEnd = nextSibling ? nextSibling->previousSibling() : parent->lastChild(); + } + } + + return true; +} + +bool ApplyStyleCommand::removeInlineStyleFromElement(CSSMutableStyleDeclaration* style, PassRefPtr<HTMLElement> element, InlineStyleRemovalMode mode, CSSMutableStyleDeclaration* extractedStyle) +{ + ASSERT(style); + ASSERT(element); + + if (!element->parentNode() || !element->parentNode()->isContentEditable()) + return false; + + if (isStyledInlineElementToRemove(element.get())) { + if (mode == RemoveNone) + return true; + ASSERT(extractedStyle); + if (element->inlineStyleDecl()) + extractedStyle->merge(element->inlineStyleDecl()); + removeNodePreservingChildren(element); + return true; + } + + bool removed = false; + if (removeImplicitlyStyledElement(style, element.get(), mode, extractedStyle)) + removed = true; + + if (!element->inDocument()) + return removed; + + // If the node was converted to a span, the span may still contain relevant + // styles which must be removed (e.g. <b style='font-weight: bold'>) + if (removeCSSStyle(style, element.get(), mode, extractedStyle)) + removed = true; + + return removed; +} + +enum EPushDownType { ShouldBePushedDown, ShouldNotBePushedDown }; +struct HTMLEquivalent { + int propertyID; + bool isValueList; + int primitiveId; + const QualifiedName* element; + const QualifiedName* attribute; + PassRefPtr<CSSValue> (*attributeToCSSValue)(int propertyID, const String&); + EPushDownType pushDownType; +}; + +static PassRefPtr<CSSValue> stringToCSSValue(int propertyID, const String& value) +{ + RefPtr<CSSMutableStyleDeclaration> dummyStyle; + dummyStyle = CSSMutableStyleDeclaration::create(); + dummyStyle->setProperty(propertyID, value); + return dummyStyle->getPropertyCSSValue(propertyID); +} + +static PassRefPtr<CSSValue> fontSizeToCSSValue(int propertyID, const String& value) +{ + UNUSED_PARAM(propertyID); + ASSERT(propertyID == CSSPropertyFontSize); + int size; + if (!HTMLFontElement::cssValueFromFontSizeNumber(value, size)) + return 0; + return CSSPrimitiveValue::createIdentifier(size); +} + +static const HTMLEquivalent HTMLEquivalents[] = { + { CSSPropertyFontWeight, false, CSSValueBold, &bTag, 0, 0, ShouldBePushedDown }, + { CSSPropertyFontWeight, false, CSSValueBold, &strongTag, 0, 0, ShouldBePushedDown }, + { CSSPropertyVerticalAlign, false, CSSValueSub, &subTag, 0, 0, ShouldBePushedDown }, + { CSSPropertyVerticalAlign, false, CSSValueSuper, &supTag, 0, 0, ShouldBePushedDown }, + { CSSPropertyFontStyle, false, CSSValueItalic, &iTag, 0, 0, ShouldBePushedDown }, + { CSSPropertyFontStyle, false, CSSValueItalic, &emTag, 0, 0, ShouldBePushedDown }, + + // text-decorations should be CSSValueList + { CSSPropertyTextDecoration, true, CSSValueUnderline, &uTag, 0, 0, ShouldBePushedDown }, + { CSSPropertyTextDecoration, true, CSSValueLineThrough, &sTag, 0, 0, ShouldBePushedDown }, + { CSSPropertyTextDecoration, true, CSSValueLineThrough, &strikeTag, 0, 0, ShouldBePushedDown }, + { CSSPropertyWebkitTextDecorationsInEffect, true, CSSValueUnderline, &uTag, 0, 0, ShouldBePushedDown }, + { CSSPropertyWebkitTextDecorationsInEffect, true, CSSValueLineThrough, &sTag, 0, 0, ShouldBePushedDown }, + { CSSPropertyWebkitTextDecorationsInEffect, true, CSSValueLineThrough, &strikeTag, 0, 0, ShouldBePushedDown }, + + // FIXME: font attributes should only be removed if values were different + { CSSPropertyColor, false, CSSValueInvalid, &fontTag, &colorAttr, stringToCSSValue, ShouldBePushedDown }, + { CSSPropertyFontFamily, false, CSSValueInvalid, &fontTag, &faceAttr, stringToCSSValue, ShouldBePushedDown }, + { CSSPropertyFontSize, false, CSSValueInvalid, &fontTag, &sizeAttr, fontSizeToCSSValue, ShouldBePushedDown }, + + // unicode-bidi and direction are pushed down separately so don't push down with other styles. + { CSSPropertyDirection, false, CSSValueInvalid, 0, &dirAttr, stringToCSSValue, ShouldNotBePushedDown }, + { CSSPropertyUnicodeBidi, false, CSSValueInvalid, 0, &dirAttr, stringToCSSValue, ShouldNotBePushedDown }, +}; + +bool ApplyStyleCommand::removeImplicitlyStyledElement(CSSMutableStyleDeclaration* style, HTMLElement* element, InlineStyleRemovalMode mode, CSSMutableStyleDeclaration* extractedStyle) +{ + // Current implementation does not support stylePushedDown when mode == RemoveNone because of early exit. + ASSERT(!extractedStyle || mode != RemoveNone); + bool removed = false; + for (size_t i = 0; i < WTF_ARRAY_LENGTH(HTMLEquivalents); ++i) { + const HTMLEquivalent& equivalent = HTMLEquivalents[i]; + ASSERT(equivalent.element || equivalent.attribute); + if ((extractedStyle && equivalent.pushDownType == ShouldNotBePushedDown) + || (equivalent.element && !element->hasTagName(*equivalent.element)) + || (equivalent.attribute && !element->hasAttribute(*equivalent.attribute))) + continue; + + RefPtr<CSSValue> styleValue = style->getPropertyCSSValue(equivalent.propertyID); + if (!styleValue) + continue; + RefPtr<CSSValue> mapValue; + if (equivalent.attribute) + mapValue = equivalent.attributeToCSSValue(equivalent.propertyID, element->getAttribute(*equivalent.attribute)); + else + mapValue = CSSPrimitiveValue::createIdentifier(equivalent.primitiveId).get(); + + if (mode != RemoveAlways) { + if (equivalent.isValueList && styleValue->isValueList() && static_cast<CSSValueList*>(styleValue.get())->hasValue(mapValue.get())) + continue; // If CSS value assumes CSSValueList, then only skip if the value was present in style to apply. + else if (mapValue && styleValue->cssText() == mapValue->cssText()) + continue; // If CSS value is primitive, then skip if they are equal. + } + + if (extractedStyle && mapValue) + extractedStyle->setProperty(equivalent.propertyID, mapValue->cssText()); + + if (mode == RemoveNone) + return true; + + removed = true; + if (!equivalent.attribute) { + replaceWithSpanOrRemoveIfWithoutAttributes(element); + break; + } + removeNodeAttribute(element, *equivalent.attribute); + if (isEmptyFontTag(element) || isSpanWithoutAttributesOrUnstyleStyleSpan(element)) + removeNodePreservingChildren(element); + } + return removed; +} + +void ApplyStyleCommand::replaceWithSpanOrRemoveIfWithoutAttributes(HTMLElement*& elem) +{ + bool removeNode = false; + + // Similar to isSpanWithoutAttributesOrUnstyleStyleSpan, but does not look for Apple-style-span. + NamedNodeMap* attributes = elem->attributes(true); // readonly + if (!attributes || attributes->isEmpty()) + removeNode = true; + else if (attributes->length() == 1 && elem->hasAttribute(styleAttr)) { + // Remove the element even if it has just style='' (this might be redundantly checked later too) + CSSMutableStyleDeclaration* inlineStyleDecl = elem->inlineStyleDecl(); + if (!inlineStyleDecl || inlineStyleDecl->isEmpty()) + removeNode = true; + } + + if (removeNode) + removeNodePreservingChildren(elem); + else { + HTMLElement* newSpanElement = replaceElementWithSpanPreservingChildrenAndAttributes(elem); + ASSERT(newSpanElement && newSpanElement->inDocument()); + elem = newSpanElement; + } +} + +bool ApplyStyleCommand::removeCSSStyle(CSSMutableStyleDeclaration* style, HTMLElement* element, InlineStyleRemovalMode mode, CSSMutableStyleDeclaration* extractedStyle) +{ + ASSERT(style); + ASSERT(element); + + CSSMutableStyleDeclaration* decl = element->inlineStyleDecl(); + if (!decl) + return false; + + bool removed = false; + CSSMutableStyleDeclaration::const_iterator end = style->end(); + for (CSSMutableStyleDeclaration::const_iterator it = style->begin(); it != end; ++it) { + CSSPropertyID propertyID = static_cast<CSSPropertyID>(it->id()); + RefPtr<CSSValue> value = decl->getPropertyCSSValue(propertyID); + if (value && (propertyID != CSSPropertyWhiteSpace || !isTabSpanNode(element))) { + removed = true; + if (mode == RemoveNone) + return true; + + ExceptionCode ec = 0; + if (extractedStyle) + extractedStyle->setProperty(propertyID, value->cssText(), decl->getPropertyPriority(propertyID), ec); + removeCSSProperty(element, propertyID); + + if (propertyID == CSSPropertyUnicodeBidi && !decl->getPropertyValue(CSSPropertyDirection).isEmpty()) { + if (extractedStyle) + extractedStyle->setProperty(CSSPropertyDirection, decl->getPropertyValue(CSSPropertyDirection), decl->getPropertyPriority(CSSPropertyDirection), ec); + removeCSSProperty(element, CSSPropertyDirection); + } + } + } + + if (mode == RemoveNone) + return removed; + + // No need to serialize <foo style=""> if we just removed the last css property + if (decl->isEmpty()) + removeNodeAttribute(element, styleAttr); + + if (isSpanWithoutAttributesOrUnstyleStyleSpan(element)) + removeNodePreservingChildren(element); + + return removed; +} + +HTMLElement* ApplyStyleCommand::highestAncestorWithConflictingInlineStyle(CSSMutableStyleDeclaration* style, Node* node) +{ + if (!node) + return 0; + + HTMLElement* result = 0; + Node* unsplittableElement = unsplittableElementForPosition(Position(node, 0)); + + for (Node *n = node; n; n = n->parentNode()) { + if (n->isHTMLElement() && shouldRemoveInlineStyleFromElement(style, static_cast<HTMLElement*>(n))) + result = static_cast<HTMLElement*>(n); + // Should stop at the editable root (cannot cross editing boundary) and + // also stop at the unsplittable element to be consistent with other UAs + if (n == unsplittableElement) + break; + } + + return result; +} + +void ApplyStyleCommand::applyInlineStyleToPushDown(Node* node, CSSMutableStyleDeclaration* style) +{ + ASSERT(node); + + if (!style || !style->length() || !node->renderer()) + return; + + RefPtr<CSSMutableStyleDeclaration> newInlineStyle = style; + if (node->isHTMLElement()) { + HTMLElement* element = static_cast<HTMLElement*>(node); + CSSMutableStyleDeclaration* existingInlineStyle = element->inlineStyleDecl(); + + // Avoid overriding existing styles of node + if (existingInlineStyle) { + newInlineStyle = existingInlineStyle->copy(); + CSSMutableStyleDeclaration::const_iterator end = style->end(); + for (CSSMutableStyleDeclaration::const_iterator it = style->begin(); it != end; ++it) { + ExceptionCode ec; + if (!existingInlineStyle->getPropertyCSSValue(it->id())) + newInlineStyle->setProperty(it->id(), it->value()->cssText(), it->isImportant(), ec); + + // text-decorations adds up + if (it->id() == CSSPropertyTextDecoration && it->value()->isValueList()) { + RefPtr<CSSValue> textDecoration = newInlineStyle->getPropertyCSSValue(CSSPropertyTextDecoration); + if (textDecoration && textDecoration->isValueList()) { + CSSValueList* textDecorationOfInlineStyle = static_cast<CSSValueList*>(textDecoration.get()); + CSSValueList* textDecorationOfStyleApplied = static_cast<CSSValueList*>(it->value()); + + DEFINE_STATIC_LOCAL(RefPtr<CSSPrimitiveValue>, underline, (CSSPrimitiveValue::createIdentifier(CSSValueUnderline))); + DEFINE_STATIC_LOCAL(RefPtr<CSSPrimitiveValue>, lineThrough, (CSSPrimitiveValue::createIdentifier(CSSValueLineThrough))); + + if (textDecorationOfStyleApplied->hasValue(underline.get()) && !textDecorationOfInlineStyle->hasValue(underline.get())) + textDecorationOfInlineStyle->append(underline.get()); + + if (textDecorationOfStyleApplied->hasValue(lineThrough.get()) && !textDecorationOfInlineStyle->hasValue(lineThrough.get())) + textDecorationOfInlineStyle->append(lineThrough.get()); + } + } + } + } + } + + // Since addInlineStyleIfNeeded can't add styles to block-flow render objects, add style attribute instead. + // FIXME: applyInlineStyleToRange should be used here instead. + if ((node->renderer()->isBlockFlow() || node->childNodeCount()) && node->isHTMLElement()) { + setNodeAttribute(static_cast<HTMLElement*>(node), styleAttr, newInlineStyle->cssText()); + return; + } + + if (node->renderer()->isText() && static_cast<RenderText*>(node->renderer())->isAllCollapsibleWhitespace()) + return; + + // We can't wrap node with the styled element here because new styled element will never be removed if we did. + // If we modified the child pointer in pushDownInlineStyleAroundNode to point to new style element + // then we fall into an infinite loop where we keep removing and adding styled element wrapping node. + addInlineStyleIfNeeded(newInlineStyle.get(), node, node, DoNotAddStyledElement); +} + +void ApplyStyleCommand::pushDownInlineStyleAroundNode(CSSMutableStyleDeclaration* style, Node* targetNode) +{ + HTMLElement* highestAncestor = highestAncestorWithConflictingInlineStyle(style, targetNode); + if (!highestAncestor) + return; + + // The outer loop is traversing the tree vertically from highestAncestor to targetNode + Node* current = highestAncestor; + // Along the way, styled elements that contain targetNode are removed and accumulated into elementsToPushDown. + // Each child of the removed element, exclusing ancestors of targetNode, is then wrapped by clones of elements in elementsToPushDown. + Vector<RefPtr<Element> > elementsToPushDown; + while (current != targetNode) { + ASSERT(current); + ASSERT(current->isHTMLElement()); + ASSERT(current->contains(targetNode)); + Node* child = current->firstChild(); + Node* lastChild = current->lastChild(); + RefPtr<StyledElement> styledElement; + if (current->isStyledElement() && isStyledInlineElementToRemove(static_cast<Element*>(current))) { + styledElement = static_cast<StyledElement*>(current); + elementsToPushDown.append(styledElement); + } + RefPtr<CSSMutableStyleDeclaration> styleToPushDown = CSSMutableStyleDeclaration::create(); + removeInlineStyleFromElement(style, static_cast<HTMLElement*>(current), RemoveIfNeeded, styleToPushDown.get()); + + // The inner loop will go through children on each level + // FIXME: we should aggregate inline child elements together so that we don't wrap each child separately. + while (child) { + Node* nextChild = child->nextSibling(); + + if (!child->contains(targetNode) && elementsToPushDown.size()) { + for (size_t i = 0; i < elementsToPushDown.size(); i++) { + RefPtr<Element> wrapper = elementsToPushDown[i]->cloneElementWithoutChildren(); + ExceptionCode ec = 0; + wrapper->removeAttribute(styleAttr, ec); + ASSERT(!ec); + surroundNodeRangeWithElement(child, child, wrapper); + } + } + + // Apply text decoration to all nodes containing targetNode and their siblings but NOT to targetNode + // But if we've removed styledElement then go ahead and always apply the style. + if (child != targetNode || styledElement) + applyInlineStyleToPushDown(child, styleToPushDown.get()); + + // We found the next node for the outer loop (contains targetNode) + // When reached targetNode, stop the outer loop upon the completion of the current inner loop + if (child == targetNode || child->contains(targetNode)) + current = child; + + if (child == lastChild || child->contains(lastChild)) + break; + child = nextChild; + } + } +} + +void ApplyStyleCommand::removeInlineStyle(PassRefPtr<CSSMutableStyleDeclaration> style, const Position &start, const Position &end) +{ + ASSERT(start.isNotNull()); + ASSERT(end.isNotNull()); + ASSERT(start.node()->inDocument()); + ASSERT(end.node()->inDocument()); + ASSERT(comparePositions(start, end) <= 0); + + RefPtr<CSSValue> textDecorationSpecialProperty = style->getPropertyCSSValue(CSSPropertyWebkitTextDecorationsInEffect); + if (textDecorationSpecialProperty) { + style = style->copy(); + style->setProperty(CSSPropertyTextDecoration, textDecorationSpecialProperty->cssText(), style->getPropertyPriority(CSSPropertyWebkitTextDecorationsInEffect)); + } + + Position pushDownStart = start.downstream(); + // If the pushDownStart is at the end of a text node, then this node is not fully selected. + // Move it to the next deep quivalent position to avoid removing the style from this node. + // e.g. if pushDownStart was at Position("hello", 5) in <b>hello<div>world</div></b>, we want Position("world", 0) instead. + Node* pushDownStartContainer = pushDownStart.containerNode(); + if (pushDownStartContainer && pushDownStartContainer->isTextNode() + && pushDownStart.computeOffsetInContainerNode() == pushDownStartContainer->maxCharacterOffset()) + pushDownStart = nextVisuallyDistinctCandidate(pushDownStart); + Position pushDownEnd = end.upstream(); + pushDownInlineStyleAroundNode(style.get(), pushDownStart.node()); + pushDownInlineStyleAroundNode(style.get(), pushDownEnd.node()); + + // The s and e variables store the positions used to set the ending selection after style removal + // takes place. This will help callers to recognize when either the start node or the end node + // are removed from the document during the work of this function. + // If pushDownInlineStyleAroundNode has pruned start.node() or end.node(), + // use pushDownStart or pushDownEnd instead, which pushDownInlineStyleAroundNode won't prune. + Position s = start.isNull() || start.isOrphan() ? pushDownStart : start; + Position e = end.isNull() || end.isOrphan() ? pushDownEnd : end; + + Node* node = start.node(); + while (node) { + RefPtr<Node> next = node->traverseNextNode(); + if (node->isHTMLElement() && nodeFullySelected(node, start, end)) { + RefPtr<HTMLElement> elem = static_cast<HTMLElement*>(node); + RefPtr<Node> prev = elem->traversePreviousNodePostOrder(); + RefPtr<Node> next = elem->traverseNextNode(); + RefPtr<CSSMutableStyleDeclaration> styleToPushDown; + PassRefPtr<Node> childNode = 0; + if (isStyledInlineElementToRemove(elem.get())) { + styleToPushDown = CSSMutableStyleDeclaration::create(); + childNode = elem->firstChild(); + } + + removeInlineStyleFromElement(style.get(), elem.get(), RemoveIfNeeded, styleToPushDown.get()); + if (!elem->inDocument()) { + if (s.node() == elem) { + // Since elem must have been fully selected, and it is at the start + // of the selection, it is clear we can set the new s offset to 0. + ASSERT(s.deprecatedEditingOffset() <= caretMinOffset(s.node())); + s = Position(next, 0); + } + if (e.node() == elem) { + // Since elem must have been fully selected, and it is at the end + // of the selection, it is clear we can set the new e offset to + // the max range offset of prev. + ASSERT(e.deprecatedEditingOffset() >= lastOffsetForEditing(e.node())); + e = Position(prev, lastOffsetForEditing(prev.get())); + } + } + + if (styleToPushDown) { + for (; childNode; childNode = childNode->nextSibling()) + applyInlineStyleToPushDown(childNode.get(), styleToPushDown.get()); + } + } + if (node == end.node()) + break; + node = next.get(); + } + + updateStartEnd(s, e); +} + +bool ApplyStyleCommand::nodeFullySelected(Node *node, const Position &start, const Position &end) const +{ + ASSERT(node); + ASSERT(node->isElementNode()); + + Position pos = Position(node, node->childNodeCount()).upstream(); + return comparePositions(Position(node, 0), start) >= 0 && comparePositions(pos, end) <= 0; +} + +bool ApplyStyleCommand::nodeFullyUnselected(Node *node, const Position &start, const Position &end) const +{ + ASSERT(node); + ASSERT(node->isElementNode()); + + Position pos = Position(node, node->childNodeCount()).upstream(); + bool isFullyBeforeStart = comparePositions(pos, start) < 0; + bool isFullyAfterEnd = comparePositions(Position(node, 0), end) > 0; + + return isFullyBeforeStart || isFullyAfterEnd; +} + +void ApplyStyleCommand::splitTextAtStart(const Position& start, const Position& end) +{ + int endOffsetAdjustment = start.node() == end.node() ? start.deprecatedEditingOffset() : 0; + Text* text = static_cast<Text*>(start.node()); + splitTextNode(text, start.deprecatedEditingOffset()); + updateStartEnd(Position(start.node(), 0), Position(end.node(), end.deprecatedEditingOffset() - endOffsetAdjustment)); +} + +void ApplyStyleCommand::splitTextAtEnd(const Position& start, const Position& end) +{ + Text* text = static_cast<Text *>(end.node()); + splitTextNode(text, end.deprecatedEditingOffset()); + + Node* prevNode = text->previousSibling(); + ASSERT(prevNode); + Node* startNode = start.node() == end.node() ? prevNode : start.node(); + ASSERT(startNode); + updateStartEnd(Position(startNode, start.deprecatedEditingOffset()), Position(prevNode, caretMaxOffset(prevNode))); +} + +void ApplyStyleCommand::splitTextElementAtStart(const Position& start, const Position& end) +{ + int endOffsetAdjustment = start.node() == end.node() ? start.deprecatedEditingOffset() : 0; + Text* text = static_cast<Text*>(start.node()); + splitTextNodeContainingElement(text, start.deprecatedEditingOffset()); + updateStartEnd(Position(start.node()->parentNode(), start.node()->nodeIndex()), Position(end.node(), end.deprecatedEditingOffset() - endOffsetAdjustment)); +} + +void ApplyStyleCommand::splitTextElementAtEnd(const Position& start, const Position& end) +{ + Text* text = static_cast<Text*>(end.node()); + splitTextNodeContainingElement(text, end.deprecatedEditingOffset()); + + Node* prevNode = text->parentNode()->previousSibling()->lastChild(); + ASSERT(prevNode); + Node* startNode = start.node() == end.node() ? prevNode : start.node(); + ASSERT(startNode); + updateStartEnd(Position(startNode, start.deprecatedEditingOffset()), Position(prevNode->parentNode(), prevNode->nodeIndex() + 1)); +} + +bool ApplyStyleCommand::shouldSplitTextElement(Element* element, CSSMutableStyleDeclaration* style) +{ + if (!element || !element->isHTMLElement()) + return false; + + return shouldRemoveInlineStyleFromElement(style, static_cast<HTMLElement*>(element)); +} + +bool ApplyStyleCommand::isValidCaretPositionInTextNode(const Position& position) +{ + Node* node = position.node(); + if (!node->isTextNode()) + return false; + int offsetInText = position.deprecatedEditingOffset(); + return (offsetInText > caretMinOffset(node) && offsetInText < caretMaxOffset(node)); +} + +static bool areIdenticalElements(Node *first, Node *second) +{ + // check that tag name and all attribute names and values are identical + + if (!first->isElementNode()) + return false; + + if (!second->isElementNode()) + return false; + + Element *firstElement = static_cast<Element *>(first); + Element *secondElement = static_cast<Element *>(second); + + if (!firstElement->tagQName().matches(secondElement->tagQName())) + return false; + + NamedNodeMap *firstMap = firstElement->attributes(); + NamedNodeMap *secondMap = secondElement->attributes(); + + unsigned firstLength = firstMap->length(); + + if (firstLength != secondMap->length()) + return false; + + for (unsigned i = 0; i < firstLength; i++) { + Attribute *attribute = firstMap->attributeItem(i); + Attribute *secondAttribute = secondMap->getAttributeItem(attribute->name()); + + if (!secondAttribute || attribute->value() != secondAttribute->value()) + return false; + } + + return true; +} + +bool ApplyStyleCommand::mergeStartWithPreviousIfIdentical(const Position &start, const Position &end) +{ + Node *startNode = start.node(); + int startOffset = start.deprecatedEditingOffset(); + + if (isAtomicNode(start.node())) { + if (start.deprecatedEditingOffset() != 0) + return false; + + // note: prior siblings could be unrendered elements. it's silly to miss the + // merge opportunity just for that. + if (start.node()->previousSibling()) + return false; + + startNode = start.node()->parentNode(); + startOffset = 0; + } + + if (!startNode->isElementNode()) + return false; + + if (startOffset != 0) + return false; + + Node *previousSibling = startNode->previousSibling(); + + if (previousSibling && areIdenticalElements(startNode, previousSibling)) { + Element *previousElement = static_cast<Element *>(previousSibling); + Element *element = static_cast<Element *>(startNode); + Node *startChild = element->firstChild(); + ASSERT(startChild); + mergeIdenticalElements(previousElement, element); + + int startOffsetAdjustment = startChild->nodeIndex(); + int endOffsetAdjustment = startNode == end.node() ? startOffsetAdjustment : 0; + updateStartEnd(Position(startNode, startOffsetAdjustment), Position(end.node(), end.deprecatedEditingOffset() + endOffsetAdjustment)); + return true; + } + + return false; +} + +bool ApplyStyleCommand::mergeEndWithNextIfIdentical(const Position &start, const Position &end) +{ + Node *endNode = end.node(); + int endOffset = end.deprecatedEditingOffset(); + + if (isAtomicNode(endNode)) { + if (endOffset < caretMaxOffset(endNode)) + return false; + + unsigned parentLastOffset = end.node()->parentNode()->childNodes()->length() - 1; + if (end.node()->nextSibling()) + return false; + + endNode = end.node()->parentNode(); + endOffset = parentLastOffset; + } + + if (!endNode->isElementNode() || endNode->hasTagName(brTag)) + return false; + + Node *nextSibling = endNode->nextSibling(); + + if (nextSibling && areIdenticalElements(endNode, nextSibling)) { + Element *nextElement = static_cast<Element *>(nextSibling); + Element *element = static_cast<Element *>(endNode); + Node *nextChild = nextElement->firstChild(); + + mergeIdenticalElements(element, nextElement); + + Node *startNode = start.node() == endNode ? nextElement : start.node(); + ASSERT(startNode); + + int endOffset = nextChild ? nextChild->nodeIndex() : nextElement->childNodes()->length(); + updateStartEnd(Position(startNode, start.deprecatedEditingOffset()), Position(nextElement, endOffset)); + return true; + } + + return false; +} + +void ApplyStyleCommand::surroundNodeRangeWithElement(PassRefPtr<Node> passedStartNode, PassRefPtr<Node> endNode, PassRefPtr<Element> elementToInsert) +{ + ASSERT(passedStartNode); + ASSERT(endNode); + ASSERT(elementToInsert); + RefPtr<Node> startNode = passedStartNode; + RefPtr<Element> element = elementToInsert; + + insertNodeBefore(element, startNode); + + RefPtr<Node> node = startNode; + while (node) { + RefPtr<Node> next = node->nextSibling(); + removeNode(node); + appendNode(node, element); + if (node == endNode) + break; + node = next; + } + + RefPtr<Node> nextSibling = element->nextSibling(); + RefPtr<Node> previousSibling = element->previousSibling(); + if (nextSibling && nextSibling->isElementNode() && nextSibling->isContentEditable() + && areIdenticalElements(element.get(), static_cast<Element*>(nextSibling.get()))) + mergeIdenticalElements(element.get(), static_cast<Element*>(nextSibling.get())); + + if (previousSibling && previousSibling->isElementNode() && previousSibling->isContentEditable()) { + Node* mergedElement = previousSibling->nextSibling(); + if (mergedElement->isElementNode() && mergedElement->isContentEditable() + && areIdenticalElements(static_cast<Element*>(previousSibling.get()), static_cast<Element*>(mergedElement))) + mergeIdenticalElements(static_cast<Element*>(previousSibling.get()), static_cast<Element*>(mergedElement)); + } + + // FIXME: We should probably call updateStartEnd if the start or end was in the node + // range so that the endingSelection() is canonicalized. See the comments at the end of + // VisibleSelection::validate(). +} + +void ApplyStyleCommand::addBlockStyle(const StyleChange& styleChange, HTMLElement* block) +{ + // Do not check for legacy styles here. Those styles, like <B> and <I>, only apply for + // inline content. + if (!block) + return; + + String cssText = styleChange.cssStyle(); + CSSMutableStyleDeclaration* decl = block->inlineStyleDecl(); + if (decl) + cssText += decl->cssText(); + setNodeAttribute(block, styleAttr, cssText); +} + +void ApplyStyleCommand::addInlineStyleIfNeeded(CSSMutableStyleDeclaration *style, PassRefPtr<Node> passedStart, PassRefPtr<Node> passedEnd, EAddStyledElement addStyledElement) +{ + if (!passedStart || !passedEnd || !passedStart->inDocument() || !passedEnd->inDocument()) + return; + RefPtr<Node> startNode = passedStart; + RefPtr<Node> endNode = passedEnd; + + // It's okay to obtain the style at the startNode because we've removed all relevant styles from the current run. + RefPtr<HTMLElement> dummyElement; + Position positionForStyleComparison; + if (!startNode->isElementNode()) { + dummyElement = createStyleSpanElement(document()); + insertNodeAt(dummyElement, positionBeforeNode(startNode.get())); + positionForStyleComparison = positionBeforeNode(dummyElement.get()); + } else + positionForStyleComparison = firstPositionInNode(startNode.get()); + + StyleChange styleChange(style, positionForStyleComparison); + + if (dummyElement) + removeNode(dummyElement); + + // Find appropriate font and span elements top-down. + HTMLElement* fontContainer = 0; + HTMLElement* styleContainer = 0; + for (Node* container = startNode.get(); container && startNode == endNode; container = container->firstChild()) { + if (container->isHTMLElement() && container->hasTagName(fontTag)) + fontContainer = static_cast<HTMLElement*>(container); + bool styleContainerIsNotSpan = !styleContainer || !styleContainer->hasTagName(spanTag); + if (container->isHTMLElement() && (container->hasTagName(spanTag) || (styleContainerIsNotSpan && container->childNodeCount()))) + styleContainer = static_cast<HTMLElement*>(container); + if (!container->firstChild()) + break; + startNode = container->firstChild(); + endNode = container->lastChild(); + } + + // Font tags need to go outside of CSS so that CSS font sizes override leagcy font sizes. + if (styleChange.applyFontColor() || styleChange.applyFontFace() || styleChange.applyFontSize()) { + if (fontContainer) { + if (styleChange.applyFontColor()) + setNodeAttribute(fontContainer, colorAttr, styleChange.fontColor()); + if (styleChange.applyFontFace()) + setNodeAttribute(fontContainer, faceAttr, styleChange.fontFace()); + if (styleChange.applyFontSize()) + setNodeAttribute(fontContainer, sizeAttr, styleChange.fontSize()); + } else { + RefPtr<Element> fontElement = createFontElement(document()); + if (styleChange.applyFontColor()) + fontElement->setAttribute(colorAttr, styleChange.fontColor()); + if (styleChange.applyFontFace()) + fontElement->setAttribute(faceAttr, styleChange.fontFace()); + if (styleChange.applyFontSize()) + fontElement->setAttribute(sizeAttr, styleChange.fontSize()); + surroundNodeRangeWithElement(startNode, endNode, fontElement.get()); + } + } + + if (styleChange.cssStyle().length()) { + if (styleContainer) { + CSSMutableStyleDeclaration* existingStyle = static_cast<HTMLElement*>(styleContainer)->inlineStyleDecl(); + if (existingStyle) + setNodeAttribute(styleContainer, styleAttr, existingStyle->cssText() + styleChange.cssStyle()); + else + setNodeAttribute(styleContainer, styleAttr, styleChange.cssStyle()); + } else { + RefPtr<Element> styleElement = createStyleSpanElement(document()); + styleElement->setAttribute(styleAttr, styleChange.cssStyle()); + surroundNodeRangeWithElement(startNode, endNode, styleElement.release()); + } + } + + if (styleChange.applyBold()) + surroundNodeRangeWithElement(startNode, endNode, createHTMLElement(document(), bTag)); + + if (styleChange.applyItalic()) + surroundNodeRangeWithElement(startNode, endNode, createHTMLElement(document(), iTag)); + + if (styleChange.applyUnderline()) + surroundNodeRangeWithElement(startNode, endNode, createHTMLElement(document(), uTag)); + + if (styleChange.applyLineThrough()) + surroundNodeRangeWithElement(startNode, endNode, createHTMLElement(document(), sTag)); + + if (styleChange.applySubscript()) + surroundNodeRangeWithElement(startNode, endNode, createHTMLElement(document(), subTag)); + else if (styleChange.applySuperscript()) + surroundNodeRangeWithElement(startNode, endNode, createHTMLElement(document(), supTag)); + + if (m_styledInlineElement && addStyledElement == AddStyledElement) + surroundNodeRangeWithElement(startNode, endNode, m_styledInlineElement->cloneElementWithoutChildren()); +} + +float ApplyStyleCommand::computedFontSize(const Node *node) +{ + if (!node) + return 0; + + Position pos(const_cast<Node *>(node), 0); + RefPtr<CSSComputedStyleDeclaration> computedStyle = pos.computedStyle(); + if (!computedStyle) + return 0; + + RefPtr<CSSPrimitiveValue> value = static_pointer_cast<CSSPrimitiveValue>(computedStyle->getPropertyCSSValue(CSSPropertyFontSize)); + if (!value) + return 0; + + return value->getFloatValue(CSSPrimitiveValue::CSS_PX); +} + +void ApplyStyleCommand::joinChildTextNodes(Node *node, const Position &start, const Position &end) +{ + if (!node) + return; + + Position newStart = start; + Position newEnd = end; + + Node *child = node->firstChild(); + while (child) { + Node *next = child->nextSibling(); + if (child->isTextNode() && next && next->isTextNode()) { + Text *childText = static_cast<Text *>(child); + Text *nextText = static_cast<Text *>(next); + if (next == start.node()) + newStart = Position(childText, childText->length() + start.deprecatedEditingOffset()); + if (next == end.node()) + newEnd = Position(childText, childText->length() + end.deprecatedEditingOffset()); + String textToMove = nextText->data(); + insertTextIntoNode(childText, childText->length(), textToMove); + removeNode(next); + // don't move child node pointer. it may want to merge with more text nodes. + } + else { + child = child->nextSibling(); + } + } + + updateStartEnd(newStart, newEnd); +} + +} diff --git a/Source/WebCore/editing/ApplyStyleCommand.h b/Source/WebCore/editing/ApplyStyleCommand.h new file mode 100644 index 0000000..5f369ba --- /dev/null +++ b/Source/WebCore/editing/ApplyStyleCommand.h @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2005, 2006, 2008, 2009 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef ApplyStyleCommand_h +#define ApplyStyleCommand_h + +#include "CompositeEditCommand.h" +#include "HTMLElement.h" + +namespace WebCore { + +class CSSPrimitiveValue; +class EditingStyle; +class HTMLElement; +class StyleChange; + +enum ShouldIncludeTypingStyle { + IncludeTypingStyle, + IgnoreTypingStyle +}; + +class ApplyStyleCommand : public CompositeEditCommand { +public: + enum EPropertyLevel { PropertyDefault, ForceBlockProperties }; + enum InlineStyleRemovalMode { RemoveIfNeeded, RemoveAlways, RemoveNone }; + enum EAddStyledElement { AddStyledElement, DoNotAddStyledElement }; + typedef bool (*IsInlineElementToRemoveFunction)(const Element*); + + static PassRefPtr<ApplyStyleCommand> create(Document* document, const EditingStyle* style, EditAction action = EditActionChangeAttributes, EPropertyLevel level = PropertyDefault) + { + return adoptRef(new ApplyStyleCommand(document, style, action, level)); + } + static PassRefPtr<ApplyStyleCommand> create(Document* document, const EditingStyle* style, const Position& start, const Position& end, EditAction action = EditActionChangeAttributes, EPropertyLevel level = PropertyDefault) + { + return adoptRef(new ApplyStyleCommand(document, style, start, end, action, level)); + } + static PassRefPtr<ApplyStyleCommand> create(PassRefPtr<Element> element, bool removeOnly = false, EditAction action = EditActionChangeAttributes) + { + return adoptRef(new ApplyStyleCommand(element, removeOnly, action)); + } + static PassRefPtr<ApplyStyleCommand> create(Document* document, const EditingStyle* style, IsInlineElementToRemoveFunction isInlineElementToRemoveFunction, EditAction action = EditActionChangeAttributes) + { + return adoptRef(new ApplyStyleCommand(document, style, isInlineElementToRemoveFunction, action)); + } + +private: + ApplyStyleCommand(Document*, const EditingStyle*, EditAction, EPropertyLevel); + ApplyStyleCommand(Document*, const EditingStyle*, const Position& start, const Position& end, EditAction, EPropertyLevel); + ApplyStyleCommand(PassRefPtr<Element>, bool removeOnly, EditAction); + ApplyStyleCommand(Document*, const EditingStyle*, bool (*isInlineElementToRemove)(const Element*), EditAction); + + virtual void doApply(); + virtual EditAction editingAction() const; + + // style-removal helpers + bool isStyledInlineElementToRemove(Element*) const; + bool removeStyleFromRunBeforeApplyingStyle(CSSMutableStyleDeclaration* style, RefPtr<Node>& runStart, RefPtr<Node>& runEnd); + bool removeInlineStyleFromElement(CSSMutableStyleDeclaration*, PassRefPtr<HTMLElement>, InlineStyleRemovalMode = RemoveIfNeeded, CSSMutableStyleDeclaration* extractedStyle = 0); + inline bool shouldRemoveInlineStyleFromElement(CSSMutableStyleDeclaration* style, HTMLElement* element) {return removeInlineStyleFromElement(style, element, RemoveNone);} + bool removeImplicitlyStyledElement(CSSMutableStyleDeclaration*, HTMLElement*, InlineStyleRemovalMode, CSSMutableStyleDeclaration* extractedStyle); + void replaceWithSpanOrRemoveIfWithoutAttributes(HTMLElement*&); + bool removeCSSStyle(CSSMutableStyleDeclaration*, HTMLElement*, InlineStyleRemovalMode = RemoveIfNeeded, CSSMutableStyleDeclaration* extractedStyle = 0); + HTMLElement* highestAncestorWithConflictingInlineStyle(CSSMutableStyleDeclaration*, Node*); + void applyInlineStyleToPushDown(Node*, CSSMutableStyleDeclaration *style); + void pushDownInlineStyleAroundNode(CSSMutableStyleDeclaration*, Node*); + void removeInlineStyle(PassRefPtr<CSSMutableStyleDeclaration>, const Position& start, const Position& end); + bool nodeFullySelected(Node*, const Position& start, const Position& end) const; + bool nodeFullyUnselected(Node*, const Position& start, const Position& end) const; + + // style-application helpers + void applyBlockStyle(CSSMutableStyleDeclaration*); + void applyRelativeFontStyleChange(EditingStyle*); + void applyInlineStyle(CSSMutableStyleDeclaration*); + void fixRangeAndApplyInlineStyle(CSSMutableStyleDeclaration*, const Position& start, const Position& end); + void applyInlineStyleToNodeRange(CSSMutableStyleDeclaration*, Node* startNode, Node* pastEndNode); + void addBlockStyle(const StyleChange&, HTMLElement*); + void addInlineStyleIfNeeded(CSSMutableStyleDeclaration*, PassRefPtr<Node> start, PassRefPtr<Node> end, EAddStyledElement addStyledElement = AddStyledElement); + void splitTextAtStart(const Position& start, const Position& end); + void splitTextAtEnd(const Position& start, const Position& end); + void splitTextElementAtStart(const Position& start, const Position& end); + void splitTextElementAtEnd(const Position& start, const Position& end); + bool shouldSplitTextElement(Element* elem, CSSMutableStyleDeclaration*); + bool isValidCaretPositionInTextNode(const Position& position); + bool mergeStartWithPreviousIfIdentical(const Position& start, const Position& end); + bool mergeEndWithNextIfIdentical(const Position& start, const Position& end); + void cleanupUnstyledAppleStyleSpans(Node* dummySpanAncestor); + + void surroundNodeRangeWithElement(PassRefPtr<Node> start, PassRefPtr<Node> end, PassRefPtr<Element>); + float computedFontSize(const Node*); + void joinChildTextNodes(Node*, const Position& start, const Position& end); + + HTMLElement* splitAncestorsWithUnicodeBidi(Node*, bool before, int allowedDirection); + void removeEmbeddingUpToEnclosingBlock(Node* node, Node* unsplitAncestor); + + void updateStartEnd(const Position& newStart, const Position& newEnd); + Position startPosition(); + Position endPosition(); + + RefPtr<EditingStyle> m_style; + EditAction m_editingAction; + EPropertyLevel m_propertyLevel; + Position m_start; + Position m_end; + bool m_useEndingSelection; + RefPtr<Element> m_styledInlineElement; + bool m_removeOnly; + IsInlineElementToRemoveFunction m_isInlineElementToRemoveFunction; +}; + +bool isStyleSpan(const Node*); +PassRefPtr<HTMLElement> createStyleSpanElement(Document*); +RefPtr<CSSMutableStyleDeclaration> getPropertiesNotIn(CSSStyleDeclaration* styleWithRedundantProperties, CSSStyleDeclaration* baseStyle); + +} // namespace WebCore + +#endif diff --git a/Source/WebCore/editing/BreakBlockquoteCommand.cpp b/Source/WebCore/editing/BreakBlockquoteCommand.cpp new file mode 100644 index 0000000..63956e5 --- /dev/null +++ b/Source/WebCore/editing/BreakBlockquoteCommand.cpp @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2005 Apple Computer, 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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 "BreakBlockquoteCommand.h" + +#include "HTMLElement.h" +#include "HTMLNames.h" +#include "RenderListItem.h" +#include "Text.h" +#include "VisiblePosition.h" +#include "htmlediting.h" + +namespace WebCore { + +using namespace HTMLNames; + +BreakBlockquoteCommand::BreakBlockquoteCommand(Document *document) + : CompositeEditCommand(document) +{ +} + +void BreakBlockquoteCommand::doApply() +{ + if (endingSelection().isNone()) + return; + + // Delete the current selection. + if (endingSelection().isRange()) + deleteSelection(false, false); + + // This is a scenario that should never happen, but we want to + // make sure we don't dereference a null pointer below. + + ASSERT(!endingSelection().isNone()); + + if (endingSelection().isNone()) + return; + + VisiblePosition visiblePos = endingSelection().visibleStart(); + + // pos is a position equivalent to the caret. We use downstream() so that pos will + // be in the first node that we need to move (there are a few exceptions to this, see below). + Position pos = endingSelection().start().downstream(); + + // Find the top-most blockquote from the start. + Element* topBlockquote = 0; + for (ContainerNode* node = pos.node()->parentNode(); node; node = node->parentNode()) { + if (isMailBlockquote(node)) + topBlockquote = static_cast<Element*>(node); + } + if (!topBlockquote || !topBlockquote->parentNode()) + return; + + RefPtr<Element> breakNode = createBreakElement(document()); + + bool isLastVisPosInNode = isLastVisiblePositionInNode(visiblePos, topBlockquote); + + // If the position is at the beginning of the top quoted content, we don't need to break the quote. + // Instead, insert the break before the blockquote, unless the position is as the end of the the quoted content. + if (isFirstVisiblePositionInNode(visiblePos, topBlockquote) && !isLastVisPosInNode) { + insertNodeBefore(breakNode.get(), topBlockquote); + setEndingSelection(VisibleSelection(Position(breakNode.get(), 0), DOWNSTREAM)); + rebalanceWhitespace(); + return; + } + + // Insert a break after the top blockquote. + insertNodeAfter(breakNode.get(), topBlockquote); + + // If we're inserting the break at the end of the quoted content, we don't need to break the quote. + if (isLastVisPosInNode) { + setEndingSelection(VisibleSelection(Position(breakNode.get(), 0), DOWNSTREAM)); + rebalanceWhitespace(); + return; + } + + // Don't move a line break just after the caret. Doing so would create an extra, empty paragraph + // in the new blockquote. + if (lineBreakExistsAtVisiblePosition(visiblePos)) + pos = pos.next(); + + // Adjust the position so we don't split at the beginning of a quote. + while (isFirstVisiblePositionInNode(VisiblePosition(pos), nearestMailBlockquote(pos.node()))) + pos = pos.previous(); + + // startNode is the first node that we need to move to the new blockquote. + Node* startNode = pos.node(); + + // Split at pos if in the middle of a text node. + if (startNode->isTextNode()) { + Text* textNode = static_cast<Text*>(startNode); + if ((unsigned)pos.deprecatedEditingOffset() >= textNode->length()) { + startNode = startNode->traverseNextNode(); + ASSERT(startNode); + } else if (pos.deprecatedEditingOffset() > 0) + splitTextNode(textNode, pos.deprecatedEditingOffset()); + } else if (pos.deprecatedEditingOffset() > 0) { + Node* childAtOffset = startNode->childNode(pos.deprecatedEditingOffset()); + startNode = childAtOffset ? childAtOffset : startNode->traverseNextNode(); + ASSERT(startNode); + } + + // If there's nothing inside topBlockquote to move, we're finished. + if (!startNode->isDescendantOf(topBlockquote)) { + setEndingSelection(VisibleSelection(VisiblePosition(Position(startNode, 0)))); + return; + } + + // Build up list of ancestors in between the start node and the top blockquote. + Vector<Element*> ancestors; + for (Element* node = startNode->parentElement(); node && node != topBlockquote; node = node->parentElement()) + ancestors.append(node); + + // Insert a clone of the top blockquote after the break. + RefPtr<Element> clonedBlockquote = topBlockquote->cloneElementWithoutChildren(); + insertNodeAfter(clonedBlockquote.get(), breakNode.get()); + + // Clone startNode's ancestors into the cloned blockquote. + // On exiting this loop, clonedAncestor is the lowest ancestor + // that was cloned (i.e. the clone of either ancestors.last() + // or clonedBlockquote if ancestors is empty). + RefPtr<Element> clonedAncestor = clonedBlockquote; + for (size_t i = ancestors.size(); i != 0; --i) { + RefPtr<Element> clonedChild = ancestors[i - 1]->cloneElementWithoutChildren(); + // Preserve list item numbering in cloned lists. + if (clonedChild->isElementNode() && clonedChild->hasTagName(olTag)) { + Node* listChildNode = i > 1 ? ancestors[i - 2] : startNode; + // The first child of the cloned list might not be a list item element, + // find the first one so that we know where to start numbering. + while (listChildNode && !listChildNode->hasTagName(liTag)) + listChildNode = listChildNode->nextSibling(); + if (listChildNode && listChildNode->renderer()) + setNodeAttribute(static_cast<Element*>(clonedChild.get()), startAttr, String::number(toRenderListItem(listChildNode->renderer())->value())); + } + + appendNode(clonedChild.get(), clonedAncestor.get()); + clonedAncestor = clonedChild; + } + + // Move the startNode and its siblings. + Node *moveNode = startNode; + while (moveNode) { + Node *next = moveNode->nextSibling(); + removeNode(moveNode); + appendNode(moveNode, clonedAncestor.get()); + moveNode = next; + } + + if (!ancestors.isEmpty()) { + // Split the tree up the ancestor chain until the topBlockquote + // Throughout this loop, clonedParent is the clone of ancestor's parent. + // This is so we can clone ancestor's siblings and place the clones + // into the clone corresponding to the ancestor's parent. + Element* ancestor; + Element* clonedParent; + for (ancestor = ancestors.first(), clonedParent = clonedAncestor->parentElement(); + ancestor && ancestor != topBlockquote; + ancestor = ancestor->parentElement(), clonedParent = clonedParent->parentElement()) { + moveNode = ancestor->nextSibling(); + while (moveNode) { + Node *next = moveNode->nextSibling(); + removeNode(moveNode); + appendNode(moveNode, clonedParent); + moveNode = next; + } + } + + // If the startNode's original parent is now empty, remove it + Node* originalParent = ancestors.first(); + if (!originalParent->hasChildNodes()) + removeNode(originalParent); + } + + // Make sure the cloned block quote renders. + addBlockPlaceholderIfNeeded(clonedBlockquote.get()); + + // Put the selection right before the break. + setEndingSelection(VisibleSelection(Position(breakNode.get(), 0), DOWNSTREAM)); + rebalanceWhitespace(); +} + +} // namespace WebCore diff --git a/Source/WebCore/editing/BreakBlockquoteCommand.h b/Source/WebCore/editing/BreakBlockquoteCommand.h new file mode 100644 index 0000000..885e5d6 --- /dev/null +++ b/Source/WebCore/editing/BreakBlockquoteCommand.h @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2005, 2006, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef BreakBlockquoteCommand_h +#define BreakBlockquoteCommand_h + +#include "CompositeEditCommand.h" + +namespace WebCore { + +class BreakBlockquoteCommand : public CompositeEditCommand { +public: + static PassRefPtr<BreakBlockquoteCommand> create(Document* document) + { + return adoptRef(new BreakBlockquoteCommand(document)); + } + +private: + BreakBlockquoteCommand(Document*); + virtual void doApply(); +}; + +} // namespace WebCore + +#endif diff --git a/Source/WebCore/editing/CompositeEditCommand.cpp b/Source/WebCore/editing/CompositeEditCommand.cpp new file mode 100644 index 0000000..748777d --- /dev/null +++ b/Source/WebCore/editing/CompositeEditCommand.cpp @@ -0,0 +1,1212 @@ +/* + * Copyright (C) 2005, 2006, 2007, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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 "CompositeEditCommand.h" + +#include "AppendNodeCommand.h" +#include "ApplyStyleCommand.h" +#include "CharacterNames.h" +#include "DeleteFromTextNodeCommand.h" +#include "DeleteSelectionCommand.h" +#include "Document.h" +#include "DocumentFragment.h" +#include "EditorInsertAction.h" +#include "Frame.h" +#include "HTMLElement.h" +#include "HTMLNames.h" +#include "InlineTextBox.h" +#include "InsertIntoTextNodeCommand.h" +#include "InsertLineBreakCommand.h" +#include "InsertNodeBeforeCommand.h" +#include "InsertParagraphSeparatorCommand.h" +#include "InsertTextCommand.h" +#include "JoinTextNodesCommand.h" +#include "MergeIdenticalElementsCommand.h" +#include "Range.h" +#include "RemoveCSSPropertyCommand.h" +#include "RemoveNodeCommand.h" +#include "RemoveNodePreservingChildrenCommand.h" +#include "ReplaceNodeWithSpanCommand.h" +#include "ReplaceSelectionCommand.h" +#include "RenderBlock.h" +#include "RenderText.h" +#include "SetNodeAttributeCommand.h" +#include "SplitElementCommand.h" +#include "SplitTextNodeCommand.h" +#include "SplitTextNodeContainingElementCommand.h" +#include "Text.h" +#include "TextIterator.h" +#include "WrapContentsInDummySpanCommand.h" +#include "htmlediting.h" +#include "markup.h" +#include "visible_units.h" + +using namespace std; + +namespace WebCore { + +using namespace HTMLNames; + +CompositeEditCommand::CompositeEditCommand(Document *document) + : EditCommand(document) +{ +} + +CompositeEditCommand::~CompositeEditCommand() +{ +} + +void CompositeEditCommand::doUnapply() +{ + size_t size = m_commands.size(); + for (size_t i = size; i != 0; --i) + m_commands[i - 1]->unapply(); +} + +void CompositeEditCommand::doReapply() +{ + size_t size = m_commands.size(); + for (size_t i = 0; i != size; ++i) + m_commands[i]->reapply(); +} + +// +// sugary-sweet convenience functions to help create and apply edit commands in composite commands +// +void CompositeEditCommand::applyCommandToComposite(PassRefPtr<EditCommand> cmd) +{ + cmd->setParent(this); + cmd->apply(); + m_commands.append(cmd); +} + +void CompositeEditCommand::applyStyle(const EditingStyle* style, EditAction editingAction) +{ + applyCommandToComposite(ApplyStyleCommand::create(document(), style, editingAction)); +} + +void CompositeEditCommand::applyStyle(const EditingStyle* style, const Position& start, const Position& end, EditAction editingAction) +{ + applyCommandToComposite(ApplyStyleCommand::create(document(), style, start, end, editingAction)); +} + +void CompositeEditCommand::applyStyledElement(PassRefPtr<Element> element) +{ + applyCommandToComposite(ApplyStyleCommand::create(element, false)); +} + +void CompositeEditCommand::removeStyledElement(PassRefPtr<Element> element) +{ + applyCommandToComposite(ApplyStyleCommand::create(element, true)); +} + +void CompositeEditCommand::insertParagraphSeparator(bool useDefaultParagraphElement) +{ + applyCommandToComposite(InsertParagraphSeparatorCommand::create(document(), useDefaultParagraphElement)); +} + +void CompositeEditCommand::insertLineBreak() +{ + applyCommandToComposite(InsertLineBreakCommand::create(document())); +} + +void CompositeEditCommand::insertNodeBefore(PassRefPtr<Node> insertChild, PassRefPtr<Node> refChild) +{ + ASSERT(!refChild->hasTagName(bodyTag)); + applyCommandToComposite(InsertNodeBeforeCommand::create(insertChild, refChild)); +} + +void CompositeEditCommand::insertNodeAfter(PassRefPtr<Node> insertChild, PassRefPtr<Node> refChild) +{ + ASSERT(insertChild); + ASSERT(refChild); + ASSERT(!refChild->hasTagName(bodyTag)); + Element* parent = refChild->parentElement(); + ASSERT(parent); + if (parent->lastChild() == refChild) + appendNode(insertChild, parent); + else { + ASSERT(refChild->nextSibling()); + insertNodeBefore(insertChild, refChild->nextSibling()); + } +} + +void CompositeEditCommand::insertNodeAt(PassRefPtr<Node> insertChild, const Position& editingPosition) +{ + ASSERT(isEditablePosition(editingPosition)); + // For editing positions like [table, 0], insert before the table, + // likewise for replaced elements, brs, etc. + Position p = rangeCompliantEquivalent(editingPosition); + Node* refChild = p.node(); + int offset = p.deprecatedEditingOffset(); + + if (canHaveChildrenForEditing(refChild)) { + Node* child = refChild->firstChild(); + for (int i = 0; child && i < offset; i++) + child = child->nextSibling(); + if (child) + insertNodeBefore(insertChild, child); + else + appendNode(insertChild, static_cast<Element*>(refChild)); + } else if (caretMinOffset(refChild) >= offset) + insertNodeBefore(insertChild, refChild); + else if (refChild->isTextNode() && caretMaxOffset(refChild) > offset) { + splitTextNode(static_cast<Text *>(refChild), offset); + + // Mutation events (bug 22634) from the text node insertion may have removed the refChild + if (!refChild->inDocument()) + return; + insertNodeBefore(insertChild, refChild); + } else + insertNodeAfter(insertChild, refChild); +} + +void CompositeEditCommand::appendNode(PassRefPtr<Node> node, PassRefPtr<Element> parent) +{ + ASSERT(canHaveChildrenForEditing(parent.get())); + applyCommandToComposite(AppendNodeCommand::create(parent, node)); +} + +void CompositeEditCommand::removeChildrenInRange(PassRefPtr<Node> node, unsigned from, unsigned to) +{ + Vector<RefPtr<Node> > children; + Node* child = node->childNode(from); + for (unsigned i = from; child && i < to; i++, child = child->nextSibling()) + children.append(child); + + size_t size = children.size(); + for (size_t i = 0; i < size; ++i) + removeNode(children[i].release()); +} + +void CompositeEditCommand::removeNode(PassRefPtr<Node> node) +{ + if (!node || !node->parentNode()) + return; + applyCommandToComposite(RemoveNodeCommand::create(node)); +} + +void CompositeEditCommand::removeNodePreservingChildren(PassRefPtr<Node> node) +{ + applyCommandToComposite(RemoveNodePreservingChildrenCommand::create(node)); +} + +void CompositeEditCommand::removeNodeAndPruneAncestors(PassRefPtr<Node> node) +{ + RefPtr<ContainerNode> parent = node->parentNode(); + removeNode(node); + prune(parent.release()); +} + +HTMLElement* CompositeEditCommand::replaceElementWithSpanPreservingChildrenAndAttributes(PassRefPtr<HTMLElement> node) +{ + // It would also be possible to implement all of ReplaceNodeWithSpanCommand + // as a series of existing smaller edit commands. Someone who wanted to + // reduce the number of edit commands could do so here. + RefPtr<ReplaceNodeWithSpanCommand> command = ReplaceNodeWithSpanCommand::create(node); + applyCommandToComposite(command); + // Returning a raw pointer here is OK because the command is retained by + // applyCommandToComposite (thus retaining the span), and the span is also + // in the DOM tree, and thus alive whie it has a parent. + ASSERT(command->spanElement()->inDocument()); + return command->spanElement(); +} + +static bool hasARenderedDescendant(Node* node) +{ + Node* n = node->firstChild(); + while (n) { + if (n->renderer()) + return true; + n = n->traverseNextNode(node); + } + return false; +} + +void CompositeEditCommand::prune(PassRefPtr<Node> node) +{ + while (node) { + // If you change this rule you may have to add an updateLayout() here. + RenderObject* renderer = node->renderer(); + if (renderer && (!renderer->canHaveChildren() || hasARenderedDescendant(node.get()) || node->rootEditableElement() == node)) + return; + + RefPtr<ContainerNode> next = node->parentNode(); + removeNode(node); + node = next; + } +} + +void CompositeEditCommand::splitTextNode(PassRefPtr<Text> node, unsigned offset) +{ + applyCommandToComposite(SplitTextNodeCommand::create(node, offset)); +} + +void CompositeEditCommand::splitElement(PassRefPtr<Element> element, PassRefPtr<Node> atChild) +{ + applyCommandToComposite(SplitElementCommand::create(element, atChild)); +} + +void CompositeEditCommand::mergeIdenticalElements(PassRefPtr<Element> prpFirst, PassRefPtr<Element> prpSecond) +{ + RefPtr<Element> first = prpFirst; + RefPtr<Element> second = prpSecond; + ASSERT(!first->isDescendantOf(second.get()) && second != first); + if (first->nextSibling() != second) { + removeNode(second); + insertNodeAfter(second, first); + } + applyCommandToComposite(MergeIdenticalElementsCommand::create(first, second)); +} + +void CompositeEditCommand::wrapContentsInDummySpan(PassRefPtr<Element> element) +{ + applyCommandToComposite(WrapContentsInDummySpanCommand::create(element)); +} + +void CompositeEditCommand::splitTextNodeContainingElement(PassRefPtr<Text> text, unsigned offset) +{ + applyCommandToComposite(SplitTextNodeContainingElementCommand::create(text, offset)); +} + +void CompositeEditCommand::joinTextNodes(PassRefPtr<Text> text1, PassRefPtr<Text> text2) +{ + applyCommandToComposite(JoinTextNodesCommand::create(text1, text2)); +} + +void CompositeEditCommand::inputText(const String& text, bool selectInsertedText) +{ + unsigned offset = 0; + unsigned length = text.length(); + RefPtr<Range> startRange = Range::create(document(), Position(document()->documentElement(), 0), endingSelection().start()); + unsigned startIndex = TextIterator::rangeLength(startRange.get()); + size_t newline; + do { + newline = text.find('\n', offset); + if (newline != offset) { + RefPtr<InsertTextCommand> command = InsertTextCommand::create(document()); + applyCommandToComposite(command); + int substringLength = newline == notFound ? length - offset : newline - offset; + command->input(text.substring(offset, substringLength), false); + } + if (newline != notFound) + insertLineBreak(); + + offset = newline + 1; + } while (newline != notFound && offset != length); + + if (selectInsertedText) { + RefPtr<Range> selectedRange = TextIterator::rangeFromLocationAndLength(document()->documentElement(), startIndex, length); + setEndingSelection(VisibleSelection(selectedRange.get())); + } +} + +void CompositeEditCommand::insertTextIntoNode(PassRefPtr<Text> node, unsigned offset, const String& text) +{ + applyCommandToComposite(InsertIntoTextNodeCommand::create(node, offset, text)); +} + +void CompositeEditCommand::deleteTextFromNode(PassRefPtr<Text> node, unsigned offset, unsigned count) +{ + applyCommandToComposite(DeleteFromTextNodeCommand::create(node, offset, count)); +} + +void CompositeEditCommand::replaceTextInNode(PassRefPtr<Text> node, unsigned offset, unsigned count, const String& replacementText) +{ + applyCommandToComposite(DeleteFromTextNodeCommand::create(node.get(), offset, count)); + applyCommandToComposite(InsertIntoTextNodeCommand::create(node, offset, replacementText)); +} + +Position CompositeEditCommand::positionOutsideTabSpan(const Position& pos) +{ + if (!isTabSpanTextNode(pos.node())) + return pos; + + Node* tabSpan = tabSpanNode(pos.node()); + + if (pos.deprecatedEditingOffset() <= caretMinOffset(pos.node())) + return positionInParentBeforeNode(tabSpan); + + if (pos.deprecatedEditingOffset() >= caretMaxOffset(pos.node())) + return positionInParentAfterNode(tabSpan); + + splitTextNodeContainingElement(static_cast<Text *>(pos.node()), pos.deprecatedEditingOffset()); + return positionInParentBeforeNode(tabSpan); +} + +void CompositeEditCommand::insertNodeAtTabSpanPosition(PassRefPtr<Node> node, const Position& pos) +{ + // insert node before, after, or at split of tab span + insertNodeAt(node, positionOutsideTabSpan(pos)); +} + +void CompositeEditCommand::deleteSelection(bool smartDelete, bool mergeBlocksAfterDelete, bool replace, bool expandForSpecialElements) +{ + if (endingSelection().isRange()) + applyCommandToComposite(DeleteSelectionCommand::create(document(), smartDelete, mergeBlocksAfterDelete, replace, expandForSpecialElements)); +} + +void CompositeEditCommand::deleteSelection(const VisibleSelection &selection, bool smartDelete, bool mergeBlocksAfterDelete, bool replace, bool expandForSpecialElements) +{ + if (selection.isRange()) + applyCommandToComposite(DeleteSelectionCommand::create(selection, smartDelete, mergeBlocksAfterDelete, replace, expandForSpecialElements)); +} + +void CompositeEditCommand::removeCSSProperty(PassRefPtr<StyledElement> element, CSSPropertyID property) +{ + applyCommandToComposite(RemoveCSSPropertyCommand::create(document(), element, property)); +} + +void CompositeEditCommand::removeNodeAttribute(PassRefPtr<Element> element, const QualifiedName& attribute) +{ + setNodeAttribute(element, attribute, AtomicString()); +} + +void CompositeEditCommand::setNodeAttribute(PassRefPtr<Element> element, const QualifiedName& attribute, const AtomicString& value) +{ + applyCommandToComposite(SetNodeAttributeCommand::create(element, attribute, value)); +} + +static inline bool isWhitespace(UChar c) +{ + return c == noBreakSpace || c == ' ' || c == '\n' || c == '\t'; +} + +// FIXME: Doesn't go into text nodes that contribute adjacent text (siblings, cousins, etc). +void CompositeEditCommand::rebalanceWhitespaceAt(const Position& position) +{ + Node* node = position.node(); + if (!node || !node->isTextNode()) + return; + Text* textNode = static_cast<Text*>(node); + + if (textNode->length() == 0) + return; + RenderObject* renderer = textNode->renderer(); + if (renderer && !renderer->style()->collapseWhiteSpace()) + return; + + String text = textNode->data(); + ASSERT(!text.isEmpty()); + + int offset = position.deprecatedEditingOffset(); + // If neither text[offset] nor text[offset - 1] are some form of whitespace, do nothing. + if (!isWhitespace(text[offset])) { + offset--; + if (offset < 0 || !isWhitespace(text[offset])) + return; + } + + // Set upstream and downstream to define the extent of the whitespace surrounding text[offset]. + int upstream = offset; + while (upstream > 0 && isWhitespace(text[upstream - 1])) + upstream--; + + int downstream = offset; + while ((unsigned)downstream + 1 < text.length() && isWhitespace(text[downstream + 1])) + downstream++; + + int length = downstream - upstream + 1; + ASSERT(length > 0); + + VisiblePosition visibleUpstreamPos(Position(position.node(), upstream)); + VisiblePosition visibleDownstreamPos(Position(position.node(), downstream + 1)); + + String string = text.substring(upstream, length); + String rebalancedString = stringWithRebalancedWhitespace(string, + // FIXME: Because of the problem mentioned at the top of this function, we must also use nbsps at the start/end of the string because + // this function doesn't get all surrounding whitespace, just the whitespace in the current text node. + isStartOfParagraph(visibleUpstreamPos) || upstream == 0, + isEndOfParagraph(visibleDownstreamPos) || (unsigned)downstream == text.length() - 1); + + if (string != rebalancedString) + replaceTextInNode(textNode, upstream, length, rebalancedString); +} + +void CompositeEditCommand::prepareWhitespaceAtPositionForSplit(Position& position) +{ + Node* node = position.node(); + if (!node || !node->isTextNode()) + return; + Text* textNode = static_cast<Text*>(node); + + if (textNode->length() == 0) + return; + RenderObject* renderer = textNode->renderer(); + if (renderer && !renderer->style()->collapseWhiteSpace()) + return; + + // Delete collapsed whitespace so that inserting nbsps doesn't uncollapse it. + Position upstreamPos = position.upstream(); + deleteInsignificantText(position.upstream(), position.downstream()); + position = upstreamPos.downstream(); + + VisiblePosition visiblePos(position); + VisiblePosition previousVisiblePos(visiblePos.previous()); + Position previous(previousVisiblePos.deepEquivalent()); + + if (isCollapsibleWhitespace(previousVisiblePos.characterAfter()) && previous.node()->isTextNode() && !previous.node()->hasTagName(brTag)) + replaceTextInNode(static_cast<Text*>(previous.node()), previous.deprecatedEditingOffset(), 1, nonBreakingSpaceString()); + if (isCollapsibleWhitespace(visiblePos.characterAfter()) && position.node()->isTextNode() && !position.node()->hasTagName(brTag)) + replaceTextInNode(static_cast<Text*>(position.node()), position.deprecatedEditingOffset(), 1, nonBreakingSpaceString()); +} + +void CompositeEditCommand::rebalanceWhitespace() +{ + VisibleSelection selection = endingSelection(); + if (selection.isNone()) + return; + + rebalanceWhitespaceAt(selection.start()); + if (selection.isRange()) + rebalanceWhitespaceAt(selection.end()); +} + +void CompositeEditCommand::deleteInsignificantText(PassRefPtr<Text> textNode, unsigned start, unsigned end) +{ + if (!textNode || start >= end) + return; + + RenderText* textRenderer = toRenderText(textNode->renderer()); + if (!textRenderer) + return; + + Vector<InlineTextBox*> sortedTextBoxes; + size_t sortedTextBoxesPosition = 0; + + for (InlineTextBox* textBox = textRenderer->firstTextBox(); textBox; textBox = textBox->nextTextBox()) + sortedTextBoxes.append(textBox); + + // If there is mixed directionality text, the boxes can be out of order, + // (like Arabic with embedded LTR), so sort them first. + if (textRenderer->containsReversedText()) + std::sort(sortedTextBoxes.begin(), sortedTextBoxes.end(), InlineTextBox::compareByStart); + InlineTextBox* box = sortedTextBoxes.isEmpty() ? 0 : sortedTextBoxes[sortedTextBoxesPosition]; + + if (!box) { + // whole text node is empty + removeNode(textNode); + return; + } + + unsigned length = textNode->length(); + if (start >= length || end > length) + return; + + unsigned removed = 0; + InlineTextBox* prevBox = 0; + String str; + + // This loop structure works to process all gaps preceding a box, + // and also will look at the gap after the last box. + while (prevBox || box) { + unsigned gapStart = prevBox ? prevBox->start() + prevBox->len() : 0; + if (end < gapStart) + // No more chance for any intersections + break; + + unsigned gapEnd = box ? box->start() : length; + bool indicesIntersect = start <= gapEnd && end >= gapStart; + int gapLen = gapEnd - gapStart; + if (indicesIntersect && gapLen > 0) { + gapStart = max(gapStart, start); + gapEnd = min(gapEnd, end); + if (str.isNull()) + str = textNode->data().substring(start, end - start); + // remove text in the gap + str.remove(gapStart - start - removed, gapLen); + removed += gapLen; + } + + prevBox = box; + if (box) { + if (++sortedTextBoxesPosition < sortedTextBoxes.size()) + box = sortedTextBoxes[sortedTextBoxesPosition]; + else + box = 0; + } + } + + if (!str.isNull()) { + // Replace the text between start and end with our pruned version. + if (!str.isEmpty()) + replaceTextInNode(textNode, start, end - start, str); + else { + // Assert that we are not going to delete all of the text in the node. + // If we were, that should have been done above with the call to + // removeNode and return. + ASSERT(start > 0 || end - start < textNode->length()); + deleteTextFromNode(textNode, start, end - start); + } + } +} + +void CompositeEditCommand::deleteInsignificantText(const Position& start, const Position& end) +{ + if (start.isNull() || end.isNull()) + return; + + if (comparePositions(start, end) >= 0) + return; + + Node* next; + for (Node* node = start.node(); node; node = next) { + next = node->traverseNextNode(); + if (node->isTextNode()) { + Text* textNode = static_cast<Text*>(node); + int startOffset = node == start.node() ? start.deprecatedEditingOffset() : 0; + int endOffset = node == end.node() ? end.deprecatedEditingOffset() : static_cast<int>(textNode->length()); + deleteInsignificantText(textNode, startOffset, endOffset); + } + if (node == end.node()) + break; + } +} + +void CompositeEditCommand::deleteInsignificantTextDownstream(const Position& pos) +{ + Position end = VisiblePosition(pos, VP_DEFAULT_AFFINITY).next().deepEquivalent().downstream(); + deleteInsignificantText(pos, end); +} + +PassRefPtr<Node> CompositeEditCommand::appendBlockPlaceholder(PassRefPtr<Element> container) +{ + if (!container) + return 0; + + // Should assert isBlockFlow || isInlineFlow when deletion improves. See 4244964. + ASSERT(container->renderer()); + + RefPtr<Node> placeholder = createBlockPlaceholderElement(document()); + appendNode(placeholder, container); + return placeholder.release(); +} + +PassRefPtr<Node> CompositeEditCommand::insertBlockPlaceholder(const Position& pos) +{ + if (pos.isNull()) + return 0; + + // Should assert isBlockFlow || isInlineFlow when deletion improves. See 4244964. + ASSERT(pos.node()->renderer()); + + RefPtr<Node> placeholder = createBlockPlaceholderElement(document()); + insertNodeAt(placeholder, pos); + return placeholder.release(); +} + +PassRefPtr<Node> CompositeEditCommand::addBlockPlaceholderIfNeeded(Element* container) +{ + if (!container) + return 0; + + updateLayout(); + + RenderObject* renderer = container->renderer(); + if (!renderer || !renderer->isBlockFlow()) + return 0; + + // append the placeholder to make sure it follows + // any unrendered blocks + RenderBlock* block = toRenderBlock(renderer); + if (block->height() == 0 || (block->isListItem() && block->isEmpty())) + return appendBlockPlaceholder(container); + + return 0; +} + +// Assumes that the position is at a placeholder and does the removal without much checking. +void CompositeEditCommand::removePlaceholderAt(const Position& p) +{ + ASSERT(lineBreakExistsAtPosition(p)); + + // We are certain that the position is at a line break, but it may be a br or a preserved newline. + if (p.anchorNode()->hasTagName(brTag)) { + removeNode(p.anchorNode()); + return; + } + + deleteTextFromNode(static_cast<Text*>(p.anchorNode()), p.offsetInContainerNode(), 1); +} + +PassRefPtr<Node> CompositeEditCommand::insertNewDefaultParagraphElementAt(const Position& position) +{ + RefPtr<Element> paragraphElement = createDefaultParagraphElement(document()); + ExceptionCode ec; + paragraphElement->appendChild(createBreakElement(document()), ec); + insertNodeAt(paragraphElement, position); + return paragraphElement.release(); +} + +// If the paragraph is not entirely within it's own block, create one and move the paragraph into +// it, and return that block. Otherwise return 0. +PassRefPtr<Node> CompositeEditCommand::moveParagraphContentsToNewBlockIfNecessary(const Position& pos) +{ + if (pos.isNull()) + return 0; + + updateLayout(); + + // It's strange that this function is responsible for verifying that pos has not been invalidated + // by an earlier call to this function. The caller, applyBlockStyle, should do this. + VisiblePosition visiblePos(pos, VP_DEFAULT_AFFINITY); + VisiblePosition visibleParagraphStart(startOfParagraph(visiblePos)); + VisiblePosition visibleParagraphEnd = endOfParagraph(visiblePos); + VisiblePosition next = visibleParagraphEnd.next(); + VisiblePosition visibleEnd = next.isNotNull() ? next : visibleParagraphEnd; + + Position upstreamStart = visibleParagraphStart.deepEquivalent().upstream(); + Position upstreamEnd = visibleEnd.deepEquivalent().upstream(); + + // If there are no VisiblePositions in the same block as pos then + // upstreamStart will be outside the paragraph + if (comparePositions(pos, upstreamStart) < 0) + return 0; + + // Perform some checks to see if we need to perform work in this function. + if (isBlock(upstreamStart.node())) { + // If the block is the root editable element, always move content to a new block, + // since it is illegal to modify attributes on the root editable element for editing. + if (upstreamStart.node() == editableRootForPosition(upstreamStart)) { + // If the block is the root editable element and it contains no visible content, create a new + // block but don't try and move content into it, since there's nothing for moveParagraphs to move. + if (!Position::hasRenderedNonAnonymousDescendantsWithHeight(upstreamStart.node()->renderer())) + return insertNewDefaultParagraphElementAt(upstreamStart); + } else if (isBlock(upstreamEnd.node())) { + if (!upstreamEnd.node()->isDescendantOf(upstreamStart.node())) { + // If the paragraph end is a descendant of paragraph start, then we need to run + // the rest of this function. If not, we can bail here. + return 0; + } + } + else if (enclosingBlock(upstreamEnd.node()) != upstreamStart.node()) { + // The visibleEnd. It must be an ancestor of the paragraph start. + // We can bail as we have a full block to work with. + ASSERT(upstreamStart.node()->isDescendantOf(enclosingBlock(upstreamEnd.node()))); + return 0; + } + else if (isEndOfDocument(visibleEnd)) { + // At the end of the document. We can bail here as well. + return 0; + } + } + + RefPtr<Node> newBlock = insertNewDefaultParagraphElementAt(upstreamStart); + + bool endWasBr = visibleParagraphEnd.deepEquivalent().node()->hasTagName(brTag); + + moveParagraphs(visibleParagraphStart, visibleParagraphEnd, VisiblePosition(Position(newBlock.get(), 0))); + + if (newBlock->lastChild() && newBlock->lastChild()->hasTagName(brTag) && !endWasBr) + removeNode(newBlock->lastChild()); + + return newBlock.release(); +} + +void CompositeEditCommand::pushAnchorElementDown(Node* anchorNode) +{ + if (!anchorNode) + return; + + ASSERT(anchorNode->isLink()); + + setEndingSelection(VisibleSelection::selectionFromContentsOfNode(anchorNode)); + applyStyledElement(static_cast<Element*>(anchorNode)); + // Clones of anchorNode have been pushed down, now remove it. + if (anchorNode->inDocument()) + removeNodePreservingChildren(anchorNode); +} + +// Clone the paragraph between start and end under blockElement, +// preserving the hierarchy up to outerNode. + +void CompositeEditCommand::cloneParagraphUnderNewElement(Position& start, Position& end, Node* outerNode, Element* blockElement) +{ + // First we clone the outerNode + + RefPtr<Node> topNode = outerNode->cloneNode(isTableElement(outerNode)); + appendNode(topNode, blockElement); + RefPtr<Node> lastNode = topNode; + + if (start.node() != outerNode) { + Vector<RefPtr<Node> > ancestors; + + // Insert each node from innerNode to outerNode (excluded) in a list. + for (Node* n = start.node(); n && n != outerNode; n = n->parentNode()) + ancestors.append(n); + + // Clone every node between start.node() and outerBlock. + + for (size_t i = ancestors.size(); i != 0; --i) { + Node* item = ancestors[i - 1].get(); + RefPtr<Node> child = item->cloneNode(isTableElement(item)); + appendNode(child, static_cast<Element *>(lastNode.get())); + lastNode = child.release(); + } + } + + // Handle the case of paragraphs with more than one node, + // cloning all the siblings until end.node() is reached. + + if (start.node() != end.node() && !start.node()->isDescendantOf(end.node())) { + // If end is not a descendant of outerNode we need to + // find the first common ancestor and adjust the insertion + // point accordingly. + while (!end.node()->isDescendantOf(outerNode)) { + outerNode = outerNode->parentNode(); + topNode = topNode->parentNode(); + } + + for (Node* n = start.node()->traverseNextSibling(outerNode); n; n = n->traverseNextSibling(outerNode)) { + if (n->parentNode() != start.node()->parentNode()) + lastNode = topNode->lastChild(); + + RefPtr<Node> clonedNode = n->cloneNode(true); + insertNodeAfter(clonedNode, lastNode); + lastNode = clonedNode.release(); + if (n == end.node() || end.node()->isDescendantOf(n)) + break; + } + } +} + + +// There are bugs in deletion when it removes a fully selected table/list. +// It expands and removes the entire table/list, but will let content +// before and after the table/list collapse onto one line. +// Deleting a paragraph will leave a placeholder. Remove it (and prune +// empty or unrendered parents). + +void CompositeEditCommand::cleanupAfterDeletion() +{ + VisiblePosition caretAfterDelete = endingSelection().visibleStart(); + if (isStartOfParagraph(caretAfterDelete) && isEndOfParagraph(caretAfterDelete)) { + // Note: We want the rightmost candidate. + Position position = caretAfterDelete.deepEquivalent().downstream(); + Node* node = position.node(); + // Normally deletion will leave a br as a placeholder. + if (node->hasTagName(brTag)) + removeNodeAndPruneAncestors(node); + // If the selection to move was empty and in an empty block that + // doesn't require a placeholder to prop itself open (like a bordered + // div or an li), remove it during the move (the list removal code + // expects this behavior). + else if (isBlock(node)) + removeNodeAndPruneAncestors(node); + else if (lineBreakExistsAtPosition(position)) { + // There is a preserved '\n' at caretAfterDelete. + // We can safely assume this is a text node. + Text* textNode = static_cast<Text*>(node); + if (textNode->length() == 1) + removeNodeAndPruneAncestors(node); + else + deleteTextFromNode(textNode, position.deprecatedEditingOffset(), 1); + } + } +} + +// This is a version of moveParagraph that preserves style by keeping the original markup +// It is currently used only by IndentOutdentCommand but it is meant to be used in the +// future by several other commands such as InsertList and the align commands. +// The blockElement parameter is the element to move the paragraph to, +// outerNode is the top element of the paragraph hierarchy. + +void CompositeEditCommand::moveParagraphWithClones(const VisiblePosition& startOfParagraphToMove, const VisiblePosition& endOfParagraphToMove, Element* blockElement, Node* outerNode) +{ + ASSERT(outerNode); + ASSERT(blockElement); + + VisiblePosition beforeParagraph = startOfParagraphToMove.previous(); + VisiblePosition afterParagraph(endOfParagraphToMove.next()); + + // We upstream() the end and downstream() the start so that we don't include collapsed whitespace in the move. + // When we paste a fragment, spaces after the end and before the start are treated as though they were rendered. + Position start = startOfParagraphToMove.deepEquivalent().downstream(); + Position end = endOfParagraphToMove.deepEquivalent().upstream(); + + cloneParagraphUnderNewElement(start, end, outerNode, blockElement); + + setEndingSelection(VisibleSelection(start, end, DOWNSTREAM)); + deleteSelection(false, false, false, false); + + // There are bugs in deletion when it removes a fully selected table/list. + // It expands and removes the entire table/list, but will let content + // before and after the table/list collapse onto one line. + + cleanupAfterDeletion(); + + // Add a br if pruning an empty block level element caused a collapse. For example: + // foo^ + // <div>bar</div> + // baz + // Imagine moving 'bar' to ^. 'bar' will be deleted and its div pruned. That would + // cause 'baz' to collapse onto the line with 'foobar' unless we insert a br. + // Must recononicalize these two VisiblePositions after the pruning above. + beforeParagraph = VisiblePosition(beforeParagraph.deepEquivalent()); + afterParagraph = VisiblePosition(afterParagraph.deepEquivalent()); + + if (beforeParagraph.isNotNull() && !isTableElement(beforeParagraph.deepEquivalent().node()) + && ((!isEndOfParagraph(beforeParagraph) && !isStartOfParagraph(beforeParagraph)) || beforeParagraph == afterParagraph)) { + // FIXME: Trim text between beforeParagraph and afterParagraph if they aren't equal. + insertNodeAt(createBreakElement(document()), beforeParagraph.deepEquivalent()); + } +} + + +// This moves a paragraph preserving its style. +void CompositeEditCommand::moveParagraph(const VisiblePosition& startOfParagraphToMove, const VisiblePosition& endOfParagraphToMove, const VisiblePosition& destination, bool preserveSelection, bool preserveStyle) +{ + ASSERT(isStartOfParagraph(startOfParagraphToMove)); + ASSERT(isEndOfParagraph(endOfParagraphToMove)); + moveParagraphs(startOfParagraphToMove, endOfParagraphToMove, destination, preserveSelection, preserveStyle); +} + +void CompositeEditCommand::moveParagraphs(const VisiblePosition& startOfParagraphToMove, const VisiblePosition& endOfParagraphToMove, const VisiblePosition& destination, bool preserveSelection, bool preserveStyle) +{ + if (startOfParagraphToMove == destination) + return; + + int startIndex = -1; + int endIndex = -1; + int destinationIndex = -1; + if (preserveSelection && !endingSelection().isNone()) { + VisiblePosition visibleStart = endingSelection().visibleStart(); + VisiblePosition visibleEnd = endingSelection().visibleEnd(); + + bool startAfterParagraph = comparePositions(visibleStart, endOfParagraphToMove) > 0; + bool endBeforeParagraph = comparePositions(visibleEnd, startOfParagraphToMove) < 0; + + if (!startAfterParagraph && !endBeforeParagraph) { + bool startInParagraph = comparePositions(visibleStart, startOfParagraphToMove) >= 0; + bool endInParagraph = comparePositions(visibleEnd, endOfParagraphToMove) <= 0; + + startIndex = 0; + if (startInParagraph) { + RefPtr<Range> startRange = Range::create(document(), rangeCompliantEquivalent(startOfParagraphToMove.deepEquivalent()), rangeCompliantEquivalent(visibleStart.deepEquivalent())); + startIndex = TextIterator::rangeLength(startRange.get(), true); + } + + endIndex = 0; + if (endInParagraph) { + RefPtr<Range> endRange = Range::create(document(), rangeCompliantEquivalent(startOfParagraphToMove.deepEquivalent()), rangeCompliantEquivalent(visibleEnd.deepEquivalent())); + endIndex = TextIterator::rangeLength(endRange.get(), true); + } + } + } + + VisiblePosition beforeParagraph = startOfParagraphToMove.previous(); + VisiblePosition afterParagraph(endOfParagraphToMove.next()); + + // We upstream() the end and downstream() the start so that we don't include collapsed whitespace in the move. + // When we paste a fragment, spaces after the end and before the start are treated as though they were rendered. + Position start = startOfParagraphToMove.deepEquivalent().downstream(); + Position end = endOfParagraphToMove.deepEquivalent().upstream(); + + // start and end can't be used directly to create a Range; they are "editing positions" + Position startRangeCompliant = rangeCompliantEquivalent(start); + Position endRangeCompliant = rangeCompliantEquivalent(end); + RefPtr<Range> range = Range::create(document(), startRangeCompliant.node(), startRangeCompliant.deprecatedEditingOffset(), endRangeCompliant.node(), endRangeCompliant.deprecatedEditingOffset()); + + // FIXME: This is an inefficient way to preserve style on nodes in the paragraph to move. It + // shouldn't matter though, since moved paragraphs will usually be quite small. + RefPtr<DocumentFragment> fragment; + // This used to use a ternary for initialization, but that confused some versions of GCC, see bug 37912 + if (startOfParagraphToMove != endOfParagraphToMove) + fragment = createFragmentFromMarkup(document(), createMarkup(range.get(), 0, DoNotAnnotateForInterchange, true), ""); + + // A non-empty paragraph's style is moved when we copy and move it. We don't move + // anything if we're given an empty paragraph, but an empty paragraph can have style + // too, <div><b><br></b></div> for example. Save it so that we can preserve it later. + RefPtr<EditingStyle> styleInEmptyParagraph; + if (startOfParagraphToMove == endOfParagraphToMove && preserveStyle) { + styleInEmptyParagraph = editingStyleIncludingTypingStyle(startOfParagraphToMove.deepEquivalent()); + // The moved paragraph should assume the block style of the destination. + styleInEmptyParagraph->removeBlockProperties(); + } + + // FIXME (5098931): We should add a new insert action "WebViewInsertActionMoved" and call shouldInsertFragment here. + + setEndingSelection(VisibleSelection(start, end, DOWNSTREAM)); + document()->frame()->editor()->clearMisspellingsAndBadGrammar(endingSelection()); + deleteSelection(false, false, false, false); + + ASSERT(destination.deepEquivalent().node()->inDocument()); + + cleanupAfterDeletion(); + ASSERT(destination.deepEquivalent().node()->inDocument()); + + // Add a br if pruning an empty block level element caused a collapse. For example: + // foo^ + // <div>bar</div> + // baz + // Imagine moving 'bar' to ^. 'bar' will be deleted and its div pruned. That would + // cause 'baz' to collapse onto the line with 'foobar' unless we insert a br. + // Must recononicalize these two VisiblePositions after the pruning above. + beforeParagraph = VisiblePosition(beforeParagraph.deepEquivalent()); + afterParagraph = VisiblePosition(afterParagraph.deepEquivalent()); + if (beforeParagraph.isNotNull() && (!isEndOfParagraph(beforeParagraph) || beforeParagraph == afterParagraph)) { + // FIXME: Trim text between beforeParagraph and afterParagraph if they aren't equal. + insertNodeAt(createBreakElement(document()), beforeParagraph.deepEquivalent()); + // Need an updateLayout here in case inserting the br has split a text node. + updateLayout(); + } + + RefPtr<Range> startToDestinationRange(Range::create(document(), Position(document(), 0), rangeCompliantEquivalent(destination.deepEquivalent()))); + destinationIndex = TextIterator::rangeLength(startToDestinationRange.get(), true); + + setEndingSelection(destination); + ASSERT(endingSelection().isCaretOrRange()); + applyCommandToComposite(ReplaceSelectionCommand::create(document(), fragment, true, false, !preserveStyle, false, true)); + + document()->frame()->editor()->markMisspellingsAndBadGrammar(endingSelection()); + + // If the selection is in an empty paragraph, restore styles from the old empty paragraph to the new empty paragraph. + bool selectionIsEmptyParagraph = endingSelection().isCaret() && isStartOfParagraph(endingSelection().visibleStart()) && isEndOfParagraph(endingSelection().visibleStart()); + if (styleInEmptyParagraph && selectionIsEmptyParagraph) + applyStyle(styleInEmptyParagraph.get()); + + if (preserveSelection && startIndex != -1) { + // Fragment creation (using createMarkup) incorrectly uses regular + // spaces instead of nbsps for some spaces that were rendered (11475), which + // causes spaces to be collapsed during the move operation. This results + // in a call to rangeFromLocationAndLength with a location past the end + // of the document (which will return null). + RefPtr<Range> start = TextIterator::rangeFromLocationAndLength(document()->documentElement(), destinationIndex + startIndex, 0, true); + RefPtr<Range> end = TextIterator::rangeFromLocationAndLength(document()->documentElement(), destinationIndex + endIndex, 0, true); + if (start && end) + setEndingSelection(VisibleSelection(start->startPosition(), end->startPosition(), DOWNSTREAM)); + } +} + +// FIXME: Send an appropriate shouldDeleteRange call. +bool CompositeEditCommand::breakOutOfEmptyListItem() +{ + Node* emptyListItem = enclosingEmptyListItem(endingSelection().visibleStart()); + if (!emptyListItem) + return false; + + RefPtr<EditingStyle> style = editingStyleIncludingTypingStyle(endingSelection().start()); + + ContainerNode* listNode = emptyListItem->parentNode(); + // FIXME: Can't we do something better when the immediate parent wasn't a list node? + if (!listNode + || (!listNode->hasTagName(ulTag) && !listNode->hasTagName(olTag)) + || !listNode->isContentEditable() + || listNode == emptyListItem->rootEditableElement()) + return false; + + RefPtr<Element> newBlock = 0; + if (ContainerNode* blockEnclosingList = listNode->parentNode()) { + if (blockEnclosingList->hasTagName(liTag)) { // listNode is inside another list item + if (visiblePositionAfterNode(blockEnclosingList) == visiblePositionAfterNode(listNode)) { + // If listNode appears at the end of the outer list item, then move listNode outside of this list item + // e.g. <ul><li>hello <ul><li><br></li></ul> </li></ul> should become <ul><li>hello</li> <ul><li><br></li></ul> </ul> after this section + // If listNode does NOT appear at the end, then we should consider it as a regular paragraph. + // e.g. <ul><li> <ul><li><br></li></ul> hello</li></ul> should become <ul><li> <div><br></div> hello</li></ul> at the end + splitElement(static_cast<Element*>(blockEnclosingList), listNode); + removeNodePreservingChildren(listNode->parentNode()); + newBlock = createListItemElement(document()); + } + // If listNode does NOT appear at the end of the outer list item, then behave as if in a regular paragraph. + } else if (blockEnclosingList->hasTagName(olTag) || blockEnclosingList->hasTagName(ulTag)) + newBlock = createListItemElement(document()); + } + if (!newBlock) + newBlock = createDefaultParagraphElement(document()); + + if (emptyListItem->renderer()->nextSibling()) { + // If emptyListItem follows another list item, split the list node. + if (emptyListItem->renderer()->previousSibling()) + splitElement(static_cast<Element*>(listNode), emptyListItem); + + // If emptyListItem is followed by other list item, then insert newBlock before the list node. + // Because we have splitted the element, emptyListItem is the first element in the list node. + // i.e. insert newBlock before ul or ol whose first element is emptyListItem + insertNodeBefore(newBlock, listNode); + removeNode(emptyListItem); + } else { + // When emptyListItem does not follow any list item, insert newBlock after the enclosing list node. + // Remove the enclosing node if emptyListItem is the only child; otherwise just remove emptyListItem. + insertNodeAfter(newBlock, listNode); + removeNode(emptyListItem->renderer()->previousSibling() ? emptyListItem : listNode); + } + + appendBlockPlaceholder(newBlock); + setEndingSelection(VisibleSelection(Position(newBlock.get(), 0), DOWNSTREAM)); + + style->prepareToApplyAt(endingSelection().start()); + if (!style->isEmpty()) + applyStyle(style.get()); + + return true; +} + +// If the caret is in an empty quoted paragraph, and either there is nothing before that +// paragraph, or what is before is unquoted, and the user presses delete, unquote that paragraph. +bool CompositeEditCommand::breakOutOfEmptyMailBlockquotedParagraph() +{ + if (!endingSelection().isCaret()) + return false; + + VisiblePosition caret(endingSelection().visibleStart()); + Node* highestBlockquote = highestEnclosingNodeOfType(caret.deepEquivalent(), &isMailBlockquote); + if (!highestBlockquote) + return false; + + if (!isStartOfParagraph(caret) || !isEndOfParagraph(caret)) + return false; + + VisiblePosition previous(caret.previous(true)); + // Only move forward if there's nothing before the caret, or if there's unquoted content before it. + if (enclosingNodeOfType(previous.deepEquivalent(), &isMailBlockquote)) + return false; + + RefPtr<Node> br = createBreakElement(document()); + // We want to replace this quoted paragraph with an unquoted one, so insert a br + // to hold the caret before the highest blockquote. + insertNodeBefore(br, highestBlockquote); + VisiblePosition atBR(Position(br.get(), 0)); + // If the br we inserted collapsed, for example foo<br><blockquote>...</blockquote>, insert + // a second one. + if (!isStartOfParagraph(atBR)) + insertNodeBefore(createBreakElement(document()), br); + setEndingSelection(VisibleSelection(atBR)); + + // If this is an empty paragraph there must be a line break here. + if (!lineBreakExistsAtVisiblePosition(caret)) + return false; + + Position caretPos(caret.deepEquivalent()); + // A line break is either a br or a preserved newline. + ASSERT(caretPos.node()->hasTagName(brTag) || (caretPos.node()->isTextNode() && caretPos.node()->renderer()->style()->preserveNewline())); + + if (caretPos.node()->hasTagName(brTag)) { + Position beforeBR(positionInParentBeforeNode(caretPos.node())); + removeNode(caretPos.node()); + prune(beforeBR.node()); + } else { + ASSERT(caretPos.deprecatedEditingOffset() == 0); + Text* textNode = static_cast<Text*>(caretPos.node()); + ContainerNode* parentNode = textNode->parentNode(); + // The preserved newline must be the first thing in the node, since otherwise the previous + // paragraph would be quoted, and we verified that it wasn't above. + deleteTextFromNode(textNode, 0, 1); + prune(parentNode); + } + + return true; +} + +// Operations use this function to avoid inserting content into an anchor when at the start or the end of +// that anchor, as in NSTextView. +// FIXME: This is only an approximation of NSTextViews insertion behavior, which varies depending on how +// the caret was made. +Position CompositeEditCommand::positionAvoidingSpecialElementBoundary(const Position& original) +{ + if (original.isNull()) + return original; + + VisiblePosition visiblePos(original); + Node* enclosingAnchor = enclosingAnchorElement(original); + Position result = original; + + if (!enclosingAnchor) + return result; + + // Don't avoid block level anchors, because that would insert content into the wrong paragraph. + if (enclosingAnchor && !isBlock(enclosingAnchor)) { + VisiblePosition firstInAnchor(firstDeepEditingPositionForNode(enclosingAnchor)); + VisiblePosition lastInAnchor(lastDeepEditingPositionForNode(enclosingAnchor)); + // If visually just after the anchor, insert *inside* the anchor unless it's the last + // VisiblePosition in the document, to match NSTextView. + if (visiblePos == lastInAnchor) { + // Make sure anchors are pushed down before avoiding them so that we don't + // also avoid structural elements like lists and blocks (5142012). + if (original.node() != enclosingAnchor && original.node()->parentNode() != enclosingAnchor) { + pushAnchorElementDown(enclosingAnchor); + enclosingAnchor = enclosingAnchorElement(original); + if (!enclosingAnchor) + return original; + } + // Don't insert outside an anchor if doing so would skip over a line break. It would + // probably be safe to move the line break so that we could still avoid the anchor here. + Position downstream(visiblePos.deepEquivalent().downstream()); + if (lineBreakExistsAtVisiblePosition(visiblePos) && downstream.node()->isDescendantOf(enclosingAnchor)) + return original; + + result = positionInParentAfterNode(enclosingAnchor); + } + // If visually just before an anchor, insert *outside* the anchor unless it's the first + // VisiblePosition in a paragraph, to match NSTextView. + if (visiblePos == firstInAnchor) { + // Make sure anchors are pushed down before avoiding them so that we don't + // also avoid structural elements like lists and blocks (5142012). + if (original.node() != enclosingAnchor && original.node()->parentNode() != enclosingAnchor) { + pushAnchorElementDown(enclosingAnchor); + enclosingAnchor = enclosingAnchorElement(original); + } + if (!enclosingAnchor) + return original; + + result = positionInParentBeforeNode(enclosingAnchor); + } + } + + if (result.isNull() || !editableRootForPosition(result)) + result = original; + + return result; +} + +// Splits the tree parent by parent until we reach the specified ancestor. We use VisiblePositions +// to determine if the split is necessary. Returns the last split node. +PassRefPtr<Node> CompositeEditCommand::splitTreeToNode(Node* start, Node* end, bool splitAncestor) +{ + ASSERT(start != end); + + RefPtr<Node> node; + for (node = start; node && node->parentNode() != end; node = node->parentNode()) { + if (!node->parentNode()->isElementNode()) + break; + VisiblePosition positionInParent(Position(node->parentNode(), 0), DOWNSTREAM); + VisiblePosition positionInNode(Position(node, 0), DOWNSTREAM); + if (positionInParent != positionInNode) + applyCommandToComposite(SplitElementCommand::create(static_cast<Element*>(node->parentNode()), node)); + } + if (splitAncestor) { + splitElement(static_cast<Element*>(end), node); + return node->parentNode(); + } + return node.release(); +} + +PassRefPtr<Element> createBlockPlaceholderElement(Document* document) +{ + RefPtr<Element> breakNode = document->createElement(brTag, false); + return breakNode.release(); +} + +} // namespace WebCore diff --git a/Source/WebCore/editing/CompositeEditCommand.h b/Source/WebCore/editing/CompositeEditCommand.h new file mode 100644 index 0000000..6db4eb1 --- /dev/null +++ b/Source/WebCore/editing/CompositeEditCommand.h @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2005, 2006, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef CompositeEditCommand_h +#define CompositeEditCommand_h + +#include "EditCommand.h" +#include "CSSPropertyNames.h" +#include <wtf/Vector.h> + +namespace WebCore { + +class CSSStyleDeclaration; +class EditingStyle; +class HTMLElement; +class StyledElement; +class Text; + +class CompositeEditCommand : public EditCommand { +public: + virtual ~CompositeEditCommand(); + + bool isFirstCommand(EditCommand* command) { return !m_commands.isEmpty() && m_commands.first() == command; } + +protected: + explicit CompositeEditCommand(Document*); + + // + // sugary-sweet convenience functions to help create and apply edit commands in composite commands + // + void appendNode(PassRefPtr<Node>, PassRefPtr<Element> parent); + void applyCommandToComposite(PassRefPtr<EditCommand>); + void applyStyle(const EditingStyle*, EditAction = EditActionChangeAttributes); + void applyStyle(const EditingStyle*, const Position& start, const Position& end, EditAction = EditActionChangeAttributes); + void applyStyledElement(PassRefPtr<Element>); + void removeStyledElement(PassRefPtr<Element>); + void deleteSelection(bool smartDelete = false, bool mergeBlocksAfterDelete = true, bool replace = false, bool expandForSpecialElements = true); + void deleteSelection(const VisibleSelection&, bool smartDelete = false, bool mergeBlocksAfterDelete = true, bool replace = false, bool expandForSpecialElements = true); + virtual void deleteTextFromNode(PassRefPtr<Text>, unsigned offset, unsigned count); + void inputText(const String&, bool selectInsertedText = false); + void insertNodeAfter(PassRefPtr<Node>, PassRefPtr<Node> refChild); + void insertNodeAt(PassRefPtr<Node>, const Position&); + void insertNodeAtTabSpanPosition(PassRefPtr<Node>, const Position&); + void insertNodeBefore(PassRefPtr<Node>, PassRefPtr<Node> refChild); + void insertParagraphSeparator(bool useDefaultParagraphElement = false); + void insertLineBreak(); + void insertTextIntoNode(PassRefPtr<Text>, unsigned offset, const String& text); + void joinTextNodes(PassRefPtr<Text>, PassRefPtr<Text>); + void mergeIdenticalElements(PassRefPtr<Element>, PassRefPtr<Element>); + void rebalanceWhitespace(); + void rebalanceWhitespaceAt(const Position&); + void prepareWhitespaceAtPositionForSplit(Position&); + void removeCSSProperty(PassRefPtr<StyledElement>, CSSPropertyID); + void removeNodeAttribute(PassRefPtr<Element>, const QualifiedName& attribute); + void removeChildrenInRange(PassRefPtr<Node>, unsigned from, unsigned to); + virtual void removeNode(PassRefPtr<Node>); + HTMLElement* replaceElementWithSpanPreservingChildrenAndAttributes(PassRefPtr<HTMLElement>); + void removeNodePreservingChildren(PassRefPtr<Node>); + void removeNodeAndPruneAncestors(PassRefPtr<Node>); + void prune(PassRefPtr<Node>); + void replaceTextInNode(PassRefPtr<Text>, unsigned offset, unsigned count, const String& replacementText); + Position positionOutsideTabSpan(const Position&); + void setNodeAttribute(PassRefPtr<Element>, const QualifiedName& attribute, const AtomicString& value); + void splitElement(PassRefPtr<Element>, PassRefPtr<Node> atChild); + void splitTextNode(PassRefPtr<Text>, unsigned offset); + void splitTextNodeContainingElement(PassRefPtr<Text>, unsigned offset); + void wrapContentsInDummySpan(PassRefPtr<Element>); + + void deleteInsignificantText(PassRefPtr<Text>, unsigned start, unsigned end); + void deleteInsignificantText(const Position& start, const Position& end); + void deleteInsignificantTextDownstream(const Position&); + + PassRefPtr<Node> appendBlockPlaceholder(PassRefPtr<Element>); + PassRefPtr<Node> insertBlockPlaceholder(const Position&); + PassRefPtr<Node> addBlockPlaceholderIfNeeded(Element*); + void removePlaceholderAt(const Position&); + + PassRefPtr<Node> insertNewDefaultParagraphElementAt(const Position&); + + PassRefPtr<Node> moveParagraphContentsToNewBlockIfNecessary(const Position&); + + void pushAnchorElementDown(Node*); + + void moveParagraph(const VisiblePosition&, const VisiblePosition&, const VisiblePosition&, bool preserveSelection = false, bool preserveStyle = true); + void moveParagraphs(const VisiblePosition&, const VisiblePosition&, const VisiblePosition&, bool preserveSelection = false, bool preserveStyle = true); + void moveParagraphWithClones(const VisiblePosition& startOfParagraphToMove, const VisiblePosition& endOfParagraphToMove, Element* blockElement, Node* outerNode); + void cloneParagraphUnderNewElement(Position& start, Position& end, Node* outerNode, Element* blockElement); + void cleanupAfterDeletion(); + + bool breakOutOfEmptyListItem(); + bool breakOutOfEmptyMailBlockquotedParagraph(); + + Position positionAvoidingSpecialElementBoundary(const Position&); + + PassRefPtr<Node> splitTreeToNode(Node*, Node*, bool splitAncestor = false); + + Vector<RefPtr<EditCommand> > m_commands; + +private: + virtual void doUnapply(); + virtual void doReapply(); +}; + +} // namespace WebCore + +#endif // CompositeEditCommand_h diff --git a/Source/WebCore/editing/CorrectionPanelInfo.h b/Source/WebCore/editing/CorrectionPanelInfo.h new file mode 100644 index 0000000..76099e1 --- /dev/null +++ b/Source/WebCore/editing/CorrectionPanelInfo.h @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2010 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef CorrectionPanelInfo_h +#define CorrectionPanelInfo_h + +#if PLATFORM(MAC) && !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD) && !defined(BUILDING_ON_SNOW_LEOPARD) +// Some platforms provide UI for suggesting autocorrection. +#define SUPPORT_AUTOCORRECTION_PANEL 1 +// Some platforms use spelling and autocorrection markers to provide visual cue. +// On such platform, if word with marker is edited, we need to remove the marker. +#define REMOVE_MARKERS_UPON_EDITING 1 +#else +#define SUPPORT_AUTOCORRECTION_PANEL 0 +#define REMOVE_MARKERS_UPON_EDITING 0 +#endif // #if PLATFORM(MAC) && !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD) && !defined(BUILDING_ON_SNOW_LEOPARD) + +#include "Range.h" + +namespace WebCore { + +struct CorrectionPanelInfo { + enum PanelType { + PanelTypeCorrection = 0, + PanelTypeReversion, + PanelTypeSpellingSuggestions + }; + + RefPtr<Range> rangeToBeReplaced; + String replacedString; + String replacementString; + PanelType panelType; + bool isActive; +}; + +enum ReasonForDismissingCorrectionPanel { + ReasonForDismissingCorrectionPanelCancelled = 0, + ReasonForDismissingCorrectionPanelIgnored, + ReasonForDismissingCorrectionPanelAccepted +}; +} // namespace WebCore + +#endif // CorrectionPanelInfo_h diff --git a/Source/WebCore/editing/CreateLinkCommand.cpp b/Source/WebCore/editing/CreateLinkCommand.cpp new file mode 100644 index 0000000..fe7af4a --- /dev/null +++ b/Source/WebCore/editing/CreateLinkCommand.cpp @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2006 Apple Computer, 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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 "CreateLinkCommand.h" +#include "htmlediting.h" +#include "Text.h" + +#include "HTMLAnchorElement.h" + +namespace WebCore { + +CreateLinkCommand::CreateLinkCommand(Document* document, const String& url) + : CompositeEditCommand(document) +{ + m_url = url; +} + +void CreateLinkCommand::doApply() +{ + if (endingSelection().isNone()) + return; + + RefPtr<HTMLAnchorElement> anchorElement = HTMLAnchorElement::create(document()); + anchorElement->setHref(m_url); + + if (endingSelection().isRange()) + applyStyledElement(anchorElement.get()); + else { + insertNodeAt(anchorElement.get(), endingSelection().start()); + RefPtr<Text> textNode = Text::create(document(), m_url); + appendNode(textNode.get(), anchorElement.get()); + setEndingSelection(VisibleSelection(positionInParentBeforeNode(anchorElement.get()), positionInParentAfterNode(anchorElement.get()), DOWNSTREAM)); + } +} + +} diff --git a/Source/WebCore/editing/CreateLinkCommand.h b/Source/WebCore/editing/CreateLinkCommand.h new file mode 100644 index 0000000..ba5fe6f --- /dev/null +++ b/Source/WebCore/editing/CreateLinkCommand.h @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2006, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef CreateLinkCommand_h +#define CreateLinkCommand_h + +#include "CompositeEditCommand.h" + +namespace WebCore { + +class CreateLinkCommand : public CompositeEditCommand { +public: + static PassRefPtr<CreateLinkCommand> create(Document* document, const String& linkURL) + { + return adoptRef(new CreateLinkCommand(document, linkURL)); + } + +private: + CreateLinkCommand(Document*, const String& linkURL); + + virtual void doApply(); + virtual EditAction editingAction() const { return EditActionCreateLink; } + + String m_url; +}; + +} // namespace WebCore + +#endif // CreateLinkCommand_h diff --git a/Source/WebCore/editing/DeleteButton.cpp b/Source/WebCore/editing/DeleteButton.cpp new file mode 100644 index 0000000..7f2fec0 --- /dev/null +++ b/Source/WebCore/editing/DeleteButton.cpp @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2006, 2010 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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 "DeleteButton.h" + +#include "DeleteButtonController.h" +#include "Document.h" +#include "Editor.h" +#include "Event.h" +#include "EventNames.h" +#include "Frame.h" +#include "HTMLNames.h" + +namespace WebCore { + +using namespace HTMLNames; + +inline DeleteButton::DeleteButton(Document* document) + : HTMLImageElement(imgTag, document) +{ +} + +PassRefPtr<DeleteButton> DeleteButton::create(Document* document) +{ + return adoptRef(new DeleteButton(document)); +} + +void DeleteButton::defaultEventHandler(Event* event) +{ + if (event->type() == eventNames().clickEvent) { + document()->frame()->editor()->deleteButtonController()->deleteTarget(); + event->setDefaultHandled(); + return; + } + + HTMLImageElement::defaultEventHandler(event); +} + +} // namespace diff --git a/Source/WebCore/editing/DeleteButton.h b/Source/WebCore/editing/DeleteButton.h new file mode 100644 index 0000000..af6c1f4 --- /dev/null +++ b/Source/WebCore/editing/DeleteButton.h @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2006, 2010 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef DeleteButton_h +#define DeleteButton_h + +#include "HTMLImageElement.h" + +namespace WebCore { + +class DeleteButton : public HTMLImageElement { +public: + static PassRefPtr<DeleteButton> create(Document*); + +private: + DeleteButton(Document*); + + virtual void defaultEventHandler(Event*); +}; + +} // namespace + +#endif diff --git a/Source/WebCore/editing/DeleteButtonController.cpp b/Source/WebCore/editing/DeleteButtonController.cpp new file mode 100644 index 0000000..028edc8 --- /dev/null +++ b/Source/WebCore/editing/DeleteButtonController.cpp @@ -0,0 +1,358 @@ +/* + * Copyright (C) 2006, 2008, 2009 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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 "DeleteButtonController.h" + +#include "CachedImage.h" +#include "CSSMutableStyleDeclaration.h" +#include "CSSPrimitiveValue.h" +#include "CSSPropertyNames.h" +#include "CSSValueKeywords.h" +#include "DeleteButton.h" +#include "Document.h" +#include "Editor.h" +#include "Frame.h" +#include "htmlediting.h" +#include "HTMLDivElement.h" +#include "HTMLNames.h" +#include "Image.h" +#include "Node.h" +#include "Range.h" +#include "RemoveNodeCommand.h" +#include "RenderBox.h" +#include "SelectionController.h" + +namespace WebCore { + +using namespace HTMLNames; + +const char* const DeleteButtonController::containerElementIdentifier = "WebKit-Editing-Delete-Container"; +const char* const DeleteButtonController::buttonElementIdentifier = "WebKit-Editing-Delete-Button"; +const char* const DeleteButtonController::outlineElementIdentifier = "WebKit-Editing-Delete-Outline"; + +DeleteButtonController::DeleteButtonController(Frame* frame) + : m_frame(frame) + , m_wasStaticPositioned(false) + , m_wasAutoZIndex(false) + , m_disableStack(0) +{ +} + +static bool isDeletableElement(const Node* node) +{ + if (!node || !node->isHTMLElement() || !node->inDocument() || !node->isContentEditable()) + return false; + + // In general we want to only draw the UI around object of a certain area, but we still keep the min width/height to + // make sure we don't end up with very thin or very short elements getting the UI. + const int minimumArea = 2500; + const int minimumWidth = 48; + const int minimumHeight = 16; + const unsigned minimumVisibleBorders = 1; + + RenderObject* renderer = node->renderer(); + if (!renderer || !renderer->isBox()) + return false; + + // Disallow the body element since it isn't practical to delete, and the deletion UI would be clipped. + if (node->hasTagName(bodyTag)) + return false; + + // Disallow elements with any overflow clip, since the deletion UI would be clipped as well. <rdar://problem/6840161> + if (renderer->hasOverflowClip()) + return false; + + // Disallow Mail blockquotes since the deletion UI would get in the way of editing for these. + if (isMailBlockquote(node)) + return false; + + RenderBox* box = toRenderBox(renderer); + IntRect borderBoundingBox = box->borderBoundingBox(); + if (borderBoundingBox.width() < minimumWidth || borderBoundingBox.height() < minimumHeight) + return false; + + if ((borderBoundingBox.width() * borderBoundingBox.height()) < minimumArea) + return false; + + if (renderer->isTable()) + return true; + + if (node->hasTagName(ulTag) || node->hasTagName(olTag) || node->hasTagName(iframeTag)) + return true; + + if (renderer->isPositioned()) + return true; + + if (renderer->isRenderBlock() && !renderer->isTableCell()) { + RenderStyle* style = renderer->style(); + if (!style) + return false; + + // Allow blocks that have background images + if (style->hasBackgroundImage() && style->backgroundImage()->canRender(1.0f)) + return true; + + // Allow blocks with a minimum number of non-transparent borders + unsigned visibleBorders = style->borderTop().isVisible() + style->borderBottom().isVisible() + style->borderLeft().isVisible() + style->borderRight().isVisible(); + if (visibleBorders >= minimumVisibleBorders) + return true; + + // Allow blocks that have a different background from it's parent + ContainerNode* parentNode = node->parentNode(); + if (!parentNode) + return false; + + RenderObject* parentRenderer = parentNode->renderer(); + if (!parentRenderer) + return false; + + RenderStyle* parentStyle = parentRenderer->style(); + if (!parentStyle) + return false; + + if (renderer->hasBackground() && (!parentRenderer->hasBackground() || style->visitedDependentColor(CSSPropertyBackgroundColor) != parentStyle->visitedDependentColor(CSSPropertyBackgroundColor))) + return true; + } + + return false; +} + +static HTMLElement* enclosingDeletableElement(const VisibleSelection& selection) +{ + if (!selection.isContentEditable()) + return 0; + + RefPtr<Range> range = selection.toNormalizedRange(); + if (!range) + return 0; + + ExceptionCode ec = 0; + Node* container = range->commonAncestorContainer(ec); + ASSERT(container); + ASSERT(ec == 0); + + // The enclosingNodeOfType function only works on nodes that are editable + // (which is strange, given its name). + if (!container->isContentEditable()) + return 0; + + Node* element = enclosingNodeOfType(Position(container, 0), &isDeletableElement); + if (!element) + return 0; + + ASSERT(element->isHTMLElement()); + return static_cast<HTMLElement*>(element); +} + +void DeleteButtonController::respondToChangedSelection(const VisibleSelection& oldSelection) +{ + if (!enabled()) + return; + + HTMLElement* oldElement = enclosingDeletableElement(oldSelection); + HTMLElement* newElement = enclosingDeletableElement(m_frame->selection()->selection()); + if (oldElement == newElement) + return; + + // If the base is inside a deletable element, give the element a delete widget. + if (newElement) + show(newElement); + else + hide(); +} + +void DeleteButtonController::createDeletionUI() +{ + RefPtr<HTMLDivElement> container = HTMLDivElement::create(m_target->document()); + container->setIdAttribute(containerElementIdentifier); + + CSSMutableStyleDeclaration* style = container->getInlineStyleDecl(); + style->setProperty(CSSPropertyWebkitUserDrag, CSSValueNone); + style->setProperty(CSSPropertyWebkitUserSelect, CSSValueNone); + style->setProperty(CSSPropertyWebkitUserModify, CSSValueNone); + style->setProperty(CSSPropertyVisibility, CSSValueHidden); + style->setProperty(CSSPropertyPosition, CSSValueAbsolute); + style->setProperty(CSSPropertyCursor, CSSValueDefault); + style->setProperty(CSSPropertyTop, "0"); + style->setProperty(CSSPropertyRight, "0"); + style->setProperty(CSSPropertyBottom, "0"); + style->setProperty(CSSPropertyLeft, "0"); + + RefPtr<HTMLDivElement> outline = HTMLDivElement::create(m_target->document()); + outline->setIdAttribute(outlineElementIdentifier); + + const int borderWidth = 4; + const int borderRadius = 6; + + style = outline->getInlineStyleDecl(); + style->setProperty(CSSPropertyPosition, CSSValueAbsolute); + style->setProperty(CSSPropertyZIndex, String::number(-1000000)); + style->setProperty(CSSPropertyTop, String::number(-borderWidth - m_target->renderBox()->borderTop()) + "px"); + style->setProperty(CSSPropertyRight, String::number(-borderWidth - m_target->renderBox()->borderRight()) + "px"); + style->setProperty(CSSPropertyBottom, String::number(-borderWidth - m_target->renderBox()->borderBottom()) + "px"); + style->setProperty(CSSPropertyLeft, String::number(-borderWidth - m_target->renderBox()->borderLeft()) + "px"); + style->setProperty(CSSPropertyBorder, String::number(borderWidth) + "px solid rgba(0, 0, 0, 0.6)"); + style->setProperty(CSSPropertyWebkitBorderRadius, String::number(borderRadius) + "px"); + style->setProperty(CSSPropertyVisibility, CSSValueVisible); + + ExceptionCode ec = 0; + container->appendChild(outline.get(), ec); + ASSERT(ec == 0); + if (ec) + return; + + RefPtr<DeleteButton> button = DeleteButton::create(m_target->document()); + button->setIdAttribute(buttonElementIdentifier); + + const int buttonWidth = 30; + const int buttonHeight = 30; + const int buttonBottomShadowOffset = 2; + + style = button->getInlineStyleDecl(); + style->setProperty(CSSPropertyPosition, CSSValueAbsolute); + style->setProperty(CSSPropertyZIndex, String::number(1000000)); + style->setProperty(CSSPropertyTop, String::number((-buttonHeight / 2) - m_target->renderBox()->borderTop() - (borderWidth / 2) + buttonBottomShadowOffset) + "px"); + style->setProperty(CSSPropertyLeft, String::number((-buttonWidth / 2) - m_target->renderBox()->borderLeft() - (borderWidth / 2)) + "px"); + style->setProperty(CSSPropertyWidth, String::number(buttonWidth) + "px"); + style->setProperty(CSSPropertyHeight, String::number(buttonHeight) + "px"); + style->setProperty(CSSPropertyVisibility, CSSValueVisible); + + RefPtr<Image> buttonImage = Image::loadPlatformResource("deleteButton"); + if (buttonImage->isNull()) + return; + + button->setCachedImage(new CachedImage(buttonImage.get())); + + container->appendChild(button.get(), ec); + ASSERT(ec == 0); + if (ec) + return; + + m_containerElement = container.release(); + m_outlineElement = outline.release(); + m_buttonElement = button.release(); +} + +void DeleteButtonController::show(HTMLElement* element) +{ + hide(); + + if (!enabled() || !element || !element->inDocument() || !isDeletableElement(element)) + return; + + if (!m_frame->editor()->shouldShowDeleteInterface(static_cast<HTMLElement*>(element))) + return; + + // we rely on the renderer having current information, so we should update the layout if needed + m_frame->document()->updateLayoutIgnorePendingStylesheets(); + + m_target = element; + + if (!m_containerElement) { + createDeletionUI(); + if (!m_containerElement) { + hide(); + return; + } + } + + ExceptionCode ec = 0; + m_target->appendChild(m_containerElement.get(), ec); + ASSERT(ec == 0); + if (ec) { + hide(); + return; + } + + if (m_target->renderer()->style()->position() == StaticPosition) { + m_target->getInlineStyleDecl()->setProperty(CSSPropertyPosition, CSSValueRelative); + m_wasStaticPositioned = true; + } + + if (m_target->renderer()->style()->hasAutoZIndex()) { + m_target->getInlineStyleDecl()->setProperty(CSSPropertyZIndex, "0"); + m_wasAutoZIndex = true; + } +} + +void DeleteButtonController::hide() +{ + m_outlineElement = 0; + m_buttonElement = 0; + + ExceptionCode ec = 0; + if (m_containerElement && m_containerElement->parentNode()) + m_containerElement->parentNode()->removeChild(m_containerElement.get(), ec); + + if (m_target) { + if (m_wasStaticPositioned) + m_target->getInlineStyleDecl()->setProperty(CSSPropertyPosition, CSSValueStatic); + if (m_wasAutoZIndex) + m_target->getInlineStyleDecl()->setProperty(CSSPropertyZIndex, CSSValueAuto); + } + + m_wasStaticPositioned = false; + m_wasAutoZIndex = false; +} + +void DeleteButtonController::enable() +{ + ASSERT(m_disableStack > 0); + if (m_disableStack > 0) + m_disableStack--; + if (enabled()) { + // Determining if the element is deletable currently depends on style + // because whether something is editable depends on style, so we need + // to recalculate style before calling enclosingDeletableElement. + m_frame->document()->updateStyleIfNeeded(); + show(enclosingDeletableElement(m_frame->selection()->selection())); + } +} + +void DeleteButtonController::disable() +{ + if (enabled()) + hide(); + m_disableStack++; +} + +void DeleteButtonController::deleteTarget() +{ + if (!enabled() || !m_target) + return; + + RefPtr<Node> element = m_target; + hide(); + + // Because the deletion UI only appears when the selection is entirely + // within the target, we unconditionally update the selection to be + // a caret where the target had been. + Position pos = positionInParentBeforeNode(element.get()); + applyCommand(RemoveNodeCommand::create(element.release())); + m_frame->selection()->setSelection(VisiblePosition(pos)); +} + +} // namespace WebCore diff --git a/Source/WebCore/editing/DeleteButtonController.h b/Source/WebCore/editing/DeleteButtonController.h new file mode 100644 index 0000000..1286c07 --- /dev/null +++ b/Source/WebCore/editing/DeleteButtonController.h @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2006, 2007 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef DeleteButtonController_h +#define DeleteButtonController_h + +#include "DeleteButton.h" + +namespace WebCore { + +class DeleteButton; +class Frame; +class HTMLElement; +class RenderObject; +class VisibleSelection; + +class DeleteButtonController : public Noncopyable { +public: + DeleteButtonController(Frame*); + + static const char* const containerElementIdentifier; + + HTMLElement* target() const { return m_target.get(); } + HTMLElement* containerElement() const { return m_containerElement.get(); } + + void respondToChangedSelection(const VisibleSelection& oldSelection); + + void show(HTMLElement*); + void hide(); + + bool enabled() const { return (m_disableStack == 0); } + void enable(); + void disable(); + + void deleteTarget(); + +private: + static const char* const buttonElementIdentifier; + static const char* const outlineElementIdentifier; + + void createDeletionUI(); + + Frame* m_frame; + RefPtr<HTMLElement> m_target; + RefPtr<HTMLElement> m_containerElement; + RefPtr<HTMLElement> m_outlineElement; + RefPtr<DeleteButton> m_buttonElement; + bool m_wasStaticPositioned; + bool m_wasAutoZIndex; + unsigned m_disableStack; +}; + +} // namespace WebCore + +#endif diff --git a/Source/WebCore/editing/DeleteFromTextNodeCommand.cpp b/Source/WebCore/editing/DeleteFromTextNodeCommand.cpp new file mode 100644 index 0000000..fe572e1 --- /dev/null +++ b/Source/WebCore/editing/DeleteFromTextNodeCommand.cpp @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2005, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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 "DeleteFromTextNodeCommand.h" + +#include "AXObjectCache.h" +#include "Text.h" + +namespace WebCore { + +DeleteFromTextNodeCommand::DeleteFromTextNodeCommand(PassRefPtr<Text> node, unsigned offset, unsigned count) + : SimpleEditCommand(node->document()) + , m_node(node) + , m_offset(offset) + , m_count(count) +{ + ASSERT(m_node); + ASSERT(m_offset <= m_node->length()); + ASSERT(m_offset + m_count <= m_node->length()); +} + +void DeleteFromTextNodeCommand::doApply() +{ + ASSERT(m_node); + + if (!m_node->isContentEditable()) + return; + + ExceptionCode ec = 0; + m_text = m_node->substringData(m_offset, m_count, ec); + if (ec) + return; + + // Need to notify this before actually deleting the text + if (AXObjectCache::accessibilityEnabled()) + document()->axObjectCache()->nodeTextChangeNotification(m_node->renderer(), AXObjectCache::AXTextDeleted, m_offset, m_count); + + m_node->deleteData(m_offset, m_count, ec); +} + +void DeleteFromTextNodeCommand::doUnapply() +{ + ASSERT(m_node); + + if (!m_node->isContentEditable()) + return; + + ExceptionCode ec; + m_node->insertData(m_offset, m_text, ec); + + if (AXObjectCache::accessibilityEnabled()) + document()->axObjectCache()->nodeTextChangeNotification(m_node->renderer(), AXObjectCache::AXTextInserted, m_offset, m_count); +} + +} // namespace WebCore diff --git a/Source/WebCore/editing/DeleteFromTextNodeCommand.h b/Source/WebCore/editing/DeleteFromTextNodeCommand.h new file mode 100644 index 0000000..0d145f5 --- /dev/null +++ b/Source/WebCore/editing/DeleteFromTextNodeCommand.h @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2005, 2006, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef DeleteFromTextNodeCommand_h +#define DeleteFromTextNodeCommand_h + +#include "EditCommand.h" + +namespace WebCore { + +class Text; + +class DeleteFromTextNodeCommand : public SimpleEditCommand { +public: + static PassRefPtr<DeleteFromTextNodeCommand> create(PassRefPtr<Text> node, unsigned offset, unsigned count) + { + return adoptRef(new DeleteFromTextNodeCommand(node, offset, count)); + } + +private: + DeleteFromTextNodeCommand(PassRefPtr<Text>, unsigned offset, unsigned count); + + virtual void doApply(); + virtual void doUnapply(); + + RefPtr<Text> m_node; + unsigned m_offset; + unsigned m_count; + String m_text; +}; + +} // namespace WebCore + +#endif // DeleteFromTextNodeCommand_h diff --git a/Source/WebCore/editing/DeleteSelectionCommand.cpp b/Source/WebCore/editing/DeleteSelectionCommand.cpp new file mode 100644 index 0000000..24c1968 --- /dev/null +++ b/Source/WebCore/editing/DeleteSelectionCommand.cpp @@ -0,0 +1,811 @@ +/* + * Copyright (C) 2005 Apple Computer, 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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 "DeleteSelectionCommand.h" + +#include "Document.h" +#include "DocumentFragment.h" +#include "EditingBoundary.h" +#include "Editor.h" +#include "EditorClient.h" +#include "Element.h" +#include "Frame.h" +#include "htmlediting.h" +#include "HTMLInputElement.h" +#include "HTMLNames.h" +#include "RenderTableCell.h" +#include "Text.h" +#include "visible_units.h" + +namespace WebCore { + +using namespace HTMLNames; + +static bool isTableRow(const Node* node) +{ + return node && node->hasTagName(trTag); +} + +static bool isTableCellEmpty(Node* cell) +{ + ASSERT(isTableCell(cell)); + return VisiblePosition(firstDeepEditingPositionForNode(cell)) == VisiblePosition(lastDeepEditingPositionForNode(cell)); +} + +static bool isTableRowEmpty(Node* row) +{ + if (!isTableRow(row)) + return false; + + for (Node* child = row->firstChild(); child; child = child->nextSibling()) + if (isTableCell(child) && !isTableCellEmpty(child)) + return false; + + return true; +} + +DeleteSelectionCommand::DeleteSelectionCommand(Document *document, bool smartDelete, bool mergeBlocksAfterDelete, bool replace, bool expandForSpecialElements) + : CompositeEditCommand(document), + m_hasSelectionToDelete(false), + m_smartDelete(smartDelete), + m_mergeBlocksAfterDelete(mergeBlocksAfterDelete), + m_replace(replace), + m_expandForSpecialElements(expandForSpecialElements), + m_pruneStartBlockIfNecessary(false), + m_startsAtEmptyLine(false), + m_startBlock(0), + m_endBlock(0), + m_typingStyle(0), + m_deleteIntoBlockquoteStyle(0) +{ +} + +DeleteSelectionCommand::DeleteSelectionCommand(const VisibleSelection& selection, bool smartDelete, bool mergeBlocksAfterDelete, bool replace, bool expandForSpecialElements) + : CompositeEditCommand(selection.start().node()->document()), + m_hasSelectionToDelete(true), + m_smartDelete(smartDelete), + m_mergeBlocksAfterDelete(mergeBlocksAfterDelete), + m_replace(replace), + m_expandForSpecialElements(expandForSpecialElements), + m_pruneStartBlockIfNecessary(false), + m_startsAtEmptyLine(false), + m_selectionToDelete(selection), + m_startBlock(0), + m_endBlock(0), + m_typingStyle(0), + m_deleteIntoBlockquoteStyle(0) +{ +} + +void DeleteSelectionCommand::initializeStartEnd(Position& start, Position& end) +{ + Node* startSpecialContainer = 0; + Node* endSpecialContainer = 0; + + start = m_selectionToDelete.start(); + end = m_selectionToDelete.end(); + + // For HRs, we'll get a position at (HR,1) when hitting delete from the beginning of the previous line, or (HR,0) when forward deleting, + // but in these cases, we want to delete it, so manually expand the selection + if (start.node()->hasTagName(hrTag)) + start = Position(start.node(), 0); + else if (end.node()->hasTagName(hrTag)) + end = Position(end.node(), 1); + + // FIXME: This is only used so that moveParagraphs can avoid the bugs in special element expansion. + if (!m_expandForSpecialElements) + return; + + while (1) { + startSpecialContainer = 0; + endSpecialContainer = 0; + + Position s = positionBeforeContainingSpecialElement(start, &startSpecialContainer); + Position e = positionAfterContainingSpecialElement(end, &endSpecialContainer); + + if (!startSpecialContainer && !endSpecialContainer) + break; + + if (VisiblePosition(start) != m_selectionToDelete.visibleStart() || VisiblePosition(end) != m_selectionToDelete.visibleEnd()) + break; + + // If we're going to expand to include the startSpecialContainer, it must be fully selected. + if (startSpecialContainer && !endSpecialContainer && comparePositions(positionInParentAfterNode(startSpecialContainer), end) > -1) + break; + + // If we're going to expand to include the endSpecialContainer, it must be fully selected. + if (endSpecialContainer && !startSpecialContainer && comparePositions(start, positionInParentBeforeNode(endSpecialContainer)) > -1) + break; + + if (startSpecialContainer && startSpecialContainer->isDescendantOf(endSpecialContainer)) + // Don't adjust the end yet, it is the end of a special element that contains the start + // special element (which may or may not be fully selected). + start = s; + else if (endSpecialContainer && endSpecialContainer->isDescendantOf(startSpecialContainer)) + // Don't adjust the start yet, it is the start of a special element that contains the end + // special element (which may or may not be fully selected). + end = e; + else { + start = s; + end = e; + } + } +} + +void DeleteSelectionCommand::setStartingSelectionOnSmartDelete(const Position& start, const Position& end) +{ + VisiblePosition newBase; + VisiblePosition newExtent; + if (startingSelection().isBaseFirst()) { + newBase = start; + newExtent = end; + } else { + newBase = end; + newExtent = start; + } + setStartingSelection(VisibleSelection(newBase, newExtent)); +} + +void DeleteSelectionCommand::initializePositionData() +{ + Position start, end; + initializeStartEnd(start, end); + + m_upstreamStart = start.upstream(); + m_downstreamStart = start.downstream(); + m_upstreamEnd = end.upstream(); + m_downstreamEnd = end.downstream(); + + m_startRoot = editableRootForPosition(start); + m_endRoot = editableRootForPosition(end); + + m_startTableRow = enclosingNodeOfType(start, &isTableRow); + m_endTableRow = enclosingNodeOfType(end, &isTableRow); + + // Don't move content out of a table cell. + // If the cell is non-editable, enclosingNodeOfType won't return it by default, so + // tell that function that we don't care if it returns non-editable nodes. + Node* startCell = enclosingNodeOfType(m_upstreamStart, &isTableCell, false); + Node* endCell = enclosingNodeOfType(m_downstreamEnd, &isTableCell, false); + // FIXME: This isn't right. A borderless table with two rows and a single column would appear as two paragraphs. + if (endCell && endCell != startCell) + m_mergeBlocksAfterDelete = false; + + // Usually the start and the end of the selection to delete are pulled together as a result of the deletion. + // Sometimes they aren't (like when no merge is requested), so we must choose one position to hold the caret + // and receive the placeholder after deletion. + VisiblePosition visibleEnd(m_downstreamEnd); + if (m_mergeBlocksAfterDelete && !isEndOfParagraph(visibleEnd)) + m_endingPosition = m_downstreamEnd; + else + m_endingPosition = m_downstreamStart; + + // We don't want to merge into a block if it will mean changing the quote level of content after deleting + // selections that contain a whole number paragraphs plus a line break, since it is unclear to most users + // that such a selection actually ends at the start of the next paragraph. This matches TextEdit behavior + // for indented paragraphs. + // Only apply this rule if the endingSelection is a range selection. If it is a caret, then other operations have created + // the selection we're deleting (like the process of creating a selection to delete during a backspace), and the user isn't in the situation described above. + if (numEnclosingMailBlockquotes(start) != numEnclosingMailBlockquotes(end) + && isStartOfParagraph(visibleEnd) && isStartOfParagraph(VisiblePosition(start)) + && endingSelection().isRange()) { + m_mergeBlocksAfterDelete = false; + m_pruneStartBlockIfNecessary = true; + } + + // Handle leading and trailing whitespace, as well as smart delete adjustments to the selection + m_leadingWhitespace = m_upstreamStart.leadingWhitespacePosition(m_selectionToDelete.affinity()); + m_trailingWhitespace = m_downstreamEnd.trailingWhitespacePosition(VP_DEFAULT_AFFINITY); + + if (m_smartDelete) { + + // skip smart delete if the selection to delete already starts or ends with whitespace + Position pos = VisiblePosition(m_upstreamStart, m_selectionToDelete.affinity()).deepEquivalent(); + bool skipSmartDelete = pos.trailingWhitespacePosition(VP_DEFAULT_AFFINITY, true).isNotNull(); + if (!skipSmartDelete) + skipSmartDelete = m_downstreamEnd.leadingWhitespacePosition(VP_DEFAULT_AFFINITY, true).isNotNull(); + + // extend selection upstream if there is whitespace there + bool hasLeadingWhitespaceBeforeAdjustment = m_upstreamStart.leadingWhitespacePosition(m_selectionToDelete.affinity(), true).isNotNull(); + if (!skipSmartDelete && hasLeadingWhitespaceBeforeAdjustment) { + VisiblePosition visiblePos = VisiblePosition(m_upstreamStart, VP_DEFAULT_AFFINITY).previous(); + pos = visiblePos.deepEquivalent(); + // Expand out one character upstream for smart delete and recalculate + // positions based on this change. + m_upstreamStart = pos.upstream(); + m_downstreamStart = pos.downstream(); + m_leadingWhitespace = m_upstreamStart.leadingWhitespacePosition(visiblePos.affinity()); + + setStartingSelectionOnSmartDelete(m_upstreamStart, m_upstreamEnd); + } + + // trailing whitespace is only considered for smart delete if there is no leading + // whitespace, as in the case where you double-click the first word of a paragraph. + if (!skipSmartDelete && !hasLeadingWhitespaceBeforeAdjustment && m_downstreamEnd.trailingWhitespacePosition(VP_DEFAULT_AFFINITY, true).isNotNull()) { + // Expand out one character downstream for smart delete and recalculate + // positions based on this change. + pos = VisiblePosition(m_downstreamEnd, VP_DEFAULT_AFFINITY).next().deepEquivalent(); + m_upstreamEnd = pos.upstream(); + m_downstreamEnd = pos.downstream(); + m_trailingWhitespace = m_downstreamEnd.trailingWhitespacePosition(VP_DEFAULT_AFFINITY); + + setStartingSelectionOnSmartDelete(m_downstreamStart, m_downstreamEnd); + } + } + + // We must pass the positions through rangeCompliantEquivalent, since some editing positions + // that appear inside their nodes aren't really inside them. [hr, 0] is one example. + // FIXME: rangeComplaintEquivalent should eventually be moved into enclosing element getters + // like the one below, since editing functions should obviously accept editing positions. + // FIXME: Passing false to enclosingNodeOfType tells it that it's OK to return a non-editable + // node. This was done to match existing behavior, but it seems wrong. + m_startBlock = enclosingNodeOfType(rangeCompliantEquivalent(m_downstreamStart), &isBlock, false); + m_endBlock = enclosingNodeOfType(rangeCompliantEquivalent(m_upstreamEnd), &isBlock, false); +} + +void DeleteSelectionCommand::saveTypingStyleState() +{ + // A common case is deleting characters that are all from the same text node. In + // that case, the style at the start of the selection before deletion will be the + // same as the style at the start of the selection after deletion (since those + // two positions will be identical). Therefore there is no need to save the + // typing style at the start of the selection, nor is there a reason to + // compute the style at the start of the selection after deletion (see the + // early return in calculateTypingStyleAfterDelete). + if (m_upstreamStart.node() == m_downstreamEnd.node() && m_upstreamStart.node()->isTextNode()) + return; + + // Figure out the typing style in effect before the delete is done. + m_typingStyle = EditingStyle::create(positionBeforeTabSpan(m_selectionToDelete.start())); + m_typingStyle->removeStyleAddedByNode(enclosingAnchorElement(m_selectionToDelete.start())); + + // If we're deleting into a Mail blockquote, save the style at end() instead of start() + // We'll use this later in computeTypingStyleAfterDelete if we end up outside of a Mail blockquote + if (nearestMailBlockquote(m_selectionToDelete.start().node())) + m_deleteIntoBlockquoteStyle = EditingStyle::create(m_selectionToDelete.end()); + else + m_deleteIntoBlockquoteStyle = 0; +} + +bool DeleteSelectionCommand::handleSpecialCaseBRDelete() +{ + // Check for special-case where the selection contains only a BR on a line by itself after another BR. + bool upstreamStartIsBR = m_upstreamStart.node()->hasTagName(brTag); + bool downstreamStartIsBR = m_downstreamStart.node()->hasTagName(brTag); + bool isBROnLineByItself = upstreamStartIsBR && downstreamStartIsBR && m_downstreamStart.node() == m_upstreamEnd.node(); + if (isBROnLineByItself) { + removeNode(m_downstreamStart.node()); + return true; + } + + // Not a special-case delete per se, but we can detect that the merging of content between blocks + // should not be done. + if (upstreamStartIsBR && downstreamStartIsBR) { + m_startsAtEmptyLine = true; + m_endingPosition = m_downstreamEnd; + } + + return false; +} + +static void updatePositionForNodeRemoval(Node* node, Position& position) +{ + if (position.isNull()) + return; + if (node->parentNode() == position.node() && node->nodeIndex() < (unsigned)position.deprecatedEditingOffset()) + position = Position(position.node(), position.deprecatedEditingOffset() - 1); + if (position.node() == node || position.node()->isDescendantOf(node)) + position = positionInParentBeforeNode(node); +} + +void DeleteSelectionCommand::removeNode(PassRefPtr<Node> node) +{ + if (!node) + return; + + if (m_startRoot != m_endRoot && !(node->isDescendantOf(m_startRoot.get()) && node->isDescendantOf(m_endRoot.get()))) { + // If a node is not in both the start and end editable roots, remove it only if its inside an editable region. + if (!node->parentNode()->isContentEditable()) { + // Don't remove non-editable atomic nodes. + if (!node->firstChild()) + return; + // Search this non-editable region for editable regions to empty. + RefPtr<Node> child = node->firstChild(); + while (child) { + RefPtr<Node> nextChild = child->nextSibling(); + removeNode(child.get()); + // Bail if nextChild is no longer node's child. + if (nextChild && nextChild->parentNode() != node) + return; + child = nextChild; + } + + // Don't remove editable regions that are inside non-editable ones, just clear them. + return; + } + } + + if (isTableStructureNode(node.get()) || node == node->rootEditableElement()) { + // Do not remove an element of table structure; remove its contents. + // Likewise for the root editable element. + Node* child = node->firstChild(); + while (child) { + Node* remove = child; + child = child->nextSibling(); + removeNode(remove); + } + + // make sure empty cell has some height + updateLayout(); + RenderObject *r = node->renderer(); + if (r && r->isTableCell() && toRenderTableCell(r)->contentHeight() <= 0) + insertBlockPlaceholder(Position(node, 0)); + return; + } + + if (node == m_startBlock && !isEndOfBlock(VisiblePosition(firstDeepEditingPositionForNode(m_startBlock.get())).previous())) + m_needPlaceholder = true; + else if (node == m_endBlock && !isStartOfBlock(VisiblePosition(lastDeepEditingPositionForNode(m_startBlock.get())).next())) + m_needPlaceholder = true; + + // FIXME: Update the endpoints of the range being deleted. + updatePositionForNodeRemoval(node.get(), m_endingPosition); + updatePositionForNodeRemoval(node.get(), m_leadingWhitespace); + updatePositionForNodeRemoval(node.get(), m_trailingWhitespace); + + CompositeEditCommand::removeNode(node); +} + +static void updatePositionForTextRemoval(Node* node, int offset, int count, Position& position) +{ + if (position.node() == node) { + if (position.deprecatedEditingOffset() > offset + count) + position = Position(position.node(), position.deprecatedEditingOffset() - count); + else if (position.deprecatedEditingOffset() > offset) + position = Position(position.node(), offset); + } +} + +void DeleteSelectionCommand::deleteTextFromNode(PassRefPtr<Text> node, unsigned offset, unsigned count) +{ + // FIXME: Update the endpoints of the range being deleted. + updatePositionForTextRemoval(node.get(), offset, count, m_endingPosition); + updatePositionForTextRemoval(node.get(), offset, count, m_leadingWhitespace); + updatePositionForTextRemoval(node.get(), offset, count, m_trailingWhitespace); + updatePositionForTextRemoval(node.get(), offset, count, m_downstreamEnd); + + CompositeEditCommand::deleteTextFromNode(node, offset, count); +} + +void DeleteSelectionCommand::handleGeneralDelete() +{ + int startOffset = m_upstreamStart.deprecatedEditingOffset(); + Node* startNode = m_upstreamStart.node(); + + // Never remove the start block unless it's a table, in which case we won't merge content in. + if (startNode == m_startBlock && startOffset == 0 && canHaveChildrenForEditing(startNode) && !startNode->hasTagName(tableTag)) { + startOffset = 0; + startNode = startNode->traverseNextNode(); + } + + if (startOffset >= caretMaxOffset(startNode) && startNode->isTextNode()) { + Text *text = static_cast<Text *>(startNode); + if (text->length() > (unsigned)caretMaxOffset(startNode)) + deleteTextFromNode(text, caretMaxOffset(startNode), text->length() - caretMaxOffset(startNode)); + } + + if (startOffset >= lastOffsetForEditing(startNode)) { + startNode = startNode->traverseNextSibling(); + startOffset = 0; + } + + // Done adjusting the start. See if we're all done. + if (!startNode) + return; + + if (startNode == m_downstreamEnd.node()) { + if (m_downstreamEnd.deprecatedEditingOffset() - startOffset > 0) { + if (startNode->isTextNode()) { + // in a text node that needs to be trimmed + Text* text = static_cast<Text*>(startNode); + deleteTextFromNode(text, startOffset, m_downstreamEnd.deprecatedEditingOffset() - startOffset); + } else { + removeChildrenInRange(startNode, startOffset, m_downstreamEnd.deprecatedEditingOffset()); + m_endingPosition = m_upstreamStart; + } + } + + // The selection to delete is all in one node. + if (!startNode->renderer() || (!startOffset && m_downstreamEnd.atLastEditingPositionForNode())) + removeNode(startNode); + } + else { + bool startNodeWasDescendantOfEndNode = m_upstreamStart.node()->isDescendantOf(m_downstreamEnd.node()); + // The selection to delete spans more than one node. + RefPtr<Node> node(startNode); + + if (startOffset > 0) { + if (startNode->isTextNode()) { + // in a text node that needs to be trimmed + Text *text = static_cast<Text *>(node.get()); + deleteTextFromNode(text, startOffset, text->length() - startOffset); + node = node->traverseNextNode(); + } else { + node = startNode->childNode(startOffset); + } + } else if (startNode == m_upstreamEnd.node() && startNode->isTextNode()) { + Text* text = static_cast<Text*>(m_upstreamEnd.node()); + deleteTextFromNode(text, 0, m_upstreamEnd.deprecatedEditingOffset()); + } + + // handle deleting all nodes that are completely selected + while (node && node != m_downstreamEnd.node()) { + if (comparePositions(Position(node.get(), 0), m_downstreamEnd) >= 0) { + // traverseNextSibling just blew past the end position, so stop deleting + node = 0; + } else if (!m_downstreamEnd.node()->isDescendantOf(node.get())) { + RefPtr<Node> nextNode = node->traverseNextSibling(); + // if we just removed a node from the end container, update end position so the + // check above will work + if (node->parentNode() == m_downstreamEnd.node()) { + ASSERT(node->nodeIndex() < (unsigned)m_downstreamEnd.deprecatedEditingOffset()); + m_downstreamEnd = Position(m_downstreamEnd.node(), m_downstreamEnd.deprecatedEditingOffset() - 1); + } + removeNode(node.get()); + node = nextNode.get(); + } else { + Node* n = node->lastDescendant(); + if (m_downstreamEnd.node() == n && m_downstreamEnd.deprecatedEditingOffset() >= caretMaxOffset(n)) { + removeNode(node.get()); + node = 0; + } else + node = node->traverseNextNode(); + } + } + + if (m_downstreamEnd.node() != startNode && !m_upstreamStart.node()->isDescendantOf(m_downstreamEnd.node()) && m_downstreamEnd.node()->inDocument() && m_downstreamEnd.deprecatedEditingOffset() >= caretMinOffset(m_downstreamEnd.node())) { + if (m_downstreamEnd.atLastEditingPositionForNode() && !canHaveChildrenForEditing(m_downstreamEnd.node())) { + // The node itself is fully selected, not just its contents. Delete it. + removeNode(m_downstreamEnd.node()); + } else { + if (m_downstreamEnd.node()->isTextNode()) { + // in a text node that needs to be trimmed + Text *text = static_cast<Text *>(m_downstreamEnd.node()); + if (m_downstreamEnd.deprecatedEditingOffset() > 0) { + deleteTextFromNode(text, 0, m_downstreamEnd.deprecatedEditingOffset()); + } + // Remove children of m_downstreamEnd.node() that come after m_upstreamStart. + // Don't try to remove children if m_upstreamStart was inside m_downstreamEnd.node() + // and m_upstreamStart has been removed from the document, because then we don't + // know how many children to remove. + // FIXME: Make m_upstreamStart a position we update as we remove content, then we can + // always know which children to remove. + } else if (!(startNodeWasDescendantOfEndNode && !m_upstreamStart.node()->inDocument())) { + int offset = 0; + if (m_upstreamStart.node()->isDescendantOf(m_downstreamEnd.node())) { + Node *n = m_upstreamStart.node(); + while (n && n->parentNode() != m_downstreamEnd.node()) + n = n->parentNode(); + if (n) + offset = n->nodeIndex() + 1; + } + removeChildrenInRange(m_downstreamEnd.node(), offset, m_downstreamEnd.deprecatedEditingOffset()); + m_downstreamEnd = Position(m_downstreamEnd.node(), offset); + } + } + } + } +} + +void DeleteSelectionCommand::fixupWhitespace() +{ + updateLayout(); + // FIXME: isRenderedCharacter should be removed, and we should use VisiblePosition::characterAfter and VisiblePosition::characterBefore + if (m_leadingWhitespace.isNotNull() && !m_leadingWhitespace.isRenderedCharacter() && m_leadingWhitespace.node()->isTextNode()) { + Text* textNode = static_cast<Text*>(m_leadingWhitespace.node()); + ASSERT(!textNode->renderer() || textNode->renderer()->style()->collapseWhiteSpace()); + replaceTextInNode(textNode, m_leadingWhitespace.deprecatedEditingOffset(), 1, nonBreakingSpaceString()); + } + if (m_trailingWhitespace.isNotNull() && !m_trailingWhitespace.isRenderedCharacter() && m_trailingWhitespace.node()->isTextNode()) { + Text* textNode = static_cast<Text*>(m_trailingWhitespace.node()); + ASSERT(!textNode->renderer() ||textNode->renderer()->style()->collapseWhiteSpace()); + replaceTextInNode(textNode, m_trailingWhitespace.deprecatedEditingOffset(), 1, nonBreakingSpaceString()); + } +} + +// If a selection starts in one block and ends in another, we have to merge to bring content before the +// start together with content after the end. +void DeleteSelectionCommand::mergeParagraphs() +{ + if (!m_mergeBlocksAfterDelete) { + if (m_pruneStartBlockIfNecessary) { + // We aren't going to merge into the start block, so remove it if it's empty. + prune(m_startBlock); + // Removing the start block during a deletion is usually an indication that we need + // a placeholder, but not in this case. + m_needPlaceholder = false; + } + return; + } + + // It shouldn't have been asked to both try and merge content into the start block and prune it. + ASSERT(!m_pruneStartBlockIfNecessary); + + // FIXME: Deletion should adjust selection endpoints as it removes nodes so that we never get into this state (4099839). + if (!m_downstreamEnd.node()->inDocument() || !m_upstreamStart.node()->inDocument()) + return; + + // FIXME: The deletion algorithm shouldn't let this happen. + if (comparePositions(m_upstreamStart, m_downstreamEnd) > 0) + return; + + // There's nothing to merge. + if (m_upstreamStart == m_downstreamEnd) + return; + + VisiblePosition startOfParagraphToMove(m_downstreamEnd); + VisiblePosition mergeDestination(m_upstreamStart); + + // m_downstreamEnd's block has been emptied out by deletion. There is no content inside of it to + // move, so just remove it. + Element* endBlock = static_cast<Element*>(enclosingBlock(m_downstreamEnd.node())); + if (!startOfParagraphToMove.deepEquivalent().node() || !endBlock->contains(startOfParagraphToMove.deepEquivalent().node())) { + removeNode(enclosingBlock(m_downstreamEnd.node())); + return; + } + + // We need to merge into m_upstreamStart's block, but it's been emptied out and collapsed by deletion. + if (!mergeDestination.deepEquivalent().node() || !mergeDestination.deepEquivalent().node()->isDescendantOf(m_upstreamStart.node()->enclosingBlockFlowElement()) || m_startsAtEmptyLine) { + insertNodeAt(createBreakElement(document()).get(), m_upstreamStart); + mergeDestination = VisiblePosition(m_upstreamStart); + } + + if (mergeDestination == startOfParagraphToMove) + return; + + VisiblePosition endOfParagraphToMove = endOfParagraph(startOfParagraphToMove); + + if (mergeDestination == endOfParagraphToMove) + return; + + // The rule for merging into an empty block is: only do so if its farther to the right. + // FIXME: Consider RTL. + if (!m_startsAtEmptyLine && isStartOfParagraph(mergeDestination) && startOfParagraphToMove.absoluteCaretBounds().x() > mergeDestination.absoluteCaretBounds().x()) { + if (mergeDestination.deepEquivalent().downstream().node()->hasTagName(brTag)) { + removeNodeAndPruneAncestors(mergeDestination.deepEquivalent().downstream().node()); + m_endingPosition = startOfParagraphToMove.deepEquivalent(); + return; + } + } + + // Block images, tables and horizontal rules cannot be made inline with content at mergeDestination. If there is + // any (!isStartOfParagraph(mergeDestination)), don't merge, just move the caret to just before the selection we deleted. + // See https://bugs.webkit.org/show_bug.cgi?id=25439 + if (isRenderedAsNonInlineTableImageOrHR(startOfParagraphToMove.deepEquivalent().node()) && !isStartOfParagraph(mergeDestination)) { + m_endingPosition = m_upstreamStart; + return; + } + + RefPtr<Range> range = Range::create(document(), rangeCompliantEquivalent(startOfParagraphToMove.deepEquivalent()), rangeCompliantEquivalent(endOfParagraphToMove.deepEquivalent())); + RefPtr<Range> rangeToBeReplaced = Range::create(document(), rangeCompliantEquivalent(mergeDestination.deepEquivalent()), rangeCompliantEquivalent(mergeDestination.deepEquivalent())); + if (!document()->frame()->editor()->client()->shouldMoveRangeAfterDelete(range.get(), rangeToBeReplaced.get())) + return; + + // moveParagraphs will insert placeholders if it removes blocks that would require their use, don't let block + // removals that it does cause the insertion of *another* placeholder. + bool needPlaceholder = m_needPlaceholder; + bool paragraphToMergeIsEmpty = (startOfParagraphToMove == endOfParagraphToMove); + moveParagraph(startOfParagraphToMove, endOfParagraphToMove, mergeDestination, false, !paragraphToMergeIsEmpty); + m_needPlaceholder = needPlaceholder; + // The endingPosition was likely clobbered by the move, so recompute it (moveParagraph selects the moved paragraph). + m_endingPosition = endingSelection().start(); +} + +void DeleteSelectionCommand::removePreviouslySelectedEmptyTableRows() +{ + if (m_endTableRow && m_endTableRow->inDocument() && m_endTableRow != m_startTableRow) { + Node* row = m_endTableRow->previousSibling(); + while (row && row != m_startTableRow) { + RefPtr<Node> previousRow = row->previousSibling(); + if (isTableRowEmpty(row)) + // Use a raw removeNode, instead of DeleteSelectionCommand's, because + // that won't remove rows, it only empties them in preparation for this function. + CompositeEditCommand::removeNode(row); + row = previousRow.get(); + } + } + + // Remove empty rows after the start row. + if (m_startTableRow && m_startTableRow->inDocument() && m_startTableRow != m_endTableRow) { + Node* row = m_startTableRow->nextSibling(); + while (row && row != m_endTableRow) { + RefPtr<Node> nextRow = row->nextSibling(); + if (isTableRowEmpty(row)) + CompositeEditCommand::removeNode(row); + row = nextRow.get(); + } + } + + if (m_endTableRow && m_endTableRow->inDocument() && m_endTableRow != m_startTableRow) + if (isTableRowEmpty(m_endTableRow.get())) { + // Don't remove m_endTableRow if it's where we're putting the ending selection. + if (!m_endingPosition.node()->isDescendantOf(m_endTableRow.get())) { + // FIXME: We probably shouldn't remove m_endTableRow unless it's fully selected, even if it is empty. + // We'll need to start adjusting the selection endpoints during deletion to know whether or not m_endTableRow + // was fully selected here. + CompositeEditCommand::removeNode(m_endTableRow.get()); + } + } +} + +void DeleteSelectionCommand::calculateTypingStyleAfterDelete() +{ + if (!m_typingStyle) + return; + + // Compute the difference between the style before the delete and the style now + // after the delete has been done. Set this style on the frame, so other editing + // commands being composed with this one will work, and also cache it on the command, + // so the Frame::appliedEditing can set it after the whole composite command + // has completed. + + // If we deleted into a blockquote, but are now no longer in a blockquote, use the alternate typing style + if (m_deleteIntoBlockquoteStyle && !nearestMailBlockquote(m_endingPosition.node())) + m_typingStyle = m_deleteIntoBlockquoteStyle; + m_deleteIntoBlockquoteStyle = 0; + + m_typingStyle->prepareToApplyAt(m_endingPosition); + if (m_typingStyle->isEmpty()) + m_typingStyle = 0; + VisiblePosition visibleEnd(m_endingPosition); + if (m_typingStyle && + isStartOfParagraph(visibleEnd) && + isEndOfParagraph(visibleEnd) && + lineBreakExistsAtVisiblePosition(visibleEnd)) { + // Apply style to the placeholder that is now holding open the empty paragraph. + // This makes sure that the paragraph has the right height, and that the paragraph + // takes on the right style and retains it even if you move the selection away and + // then move it back (which will clear typing style). + + setEndingSelection(visibleEnd); + applyStyle(m_typingStyle.get(), EditActionUnspecified); + // applyStyle can destroy the placeholder that was at m_endingPosition if it needs to + // move it, but it will set an endingSelection() at [movedPlaceholder, 0] if it does so. + m_endingPosition = endingSelection().start(); + m_typingStyle = 0; + } + // This is where we've deleted all traces of a style but not a whole paragraph (that's handled above). + // In this case if we start typing, the new characters should have the same style as the just deleted ones, + // but, if we change the selection, come back and start typing that style should be lost. Also see + // preserveTypingStyle() below. + document()->frame()->selection()->setTypingStyle(m_typingStyle); +} + +void DeleteSelectionCommand::clearTransientState() +{ + m_selectionToDelete = VisibleSelection(); + m_upstreamStart.clear(); + m_downstreamStart.clear(); + m_upstreamEnd.clear(); + m_downstreamEnd.clear(); + m_endingPosition.clear(); + m_leadingWhitespace.clear(); + m_trailingWhitespace.clear(); +} + +void DeleteSelectionCommand::doApply() +{ + // If selection has not been set to a custom selection when the command was created, + // use the current ending selection. + if (!m_hasSelectionToDelete) + m_selectionToDelete = endingSelection(); + + if (!m_selectionToDelete.isNonOrphanedRange()) + return; + + // If the deletion is occurring in a text field, and we're not deleting to replace the selection, then let the frame call across the bridge to notify the form delegate. + if (!m_replace) { + Node* startNode = m_selectionToDelete.start().node(); + Node* ancestorNode = startNode ? startNode->shadowAncestorNode() : 0; + if (ancestorNode && ancestorNode->hasTagName(inputTag) + && static_cast<HTMLInputElement*>(ancestorNode)->isTextField() + && ancestorNode->focused()) + document()->frame()->editor()->textWillBeDeletedInTextField(static_cast<Element*>(ancestorNode)); + } + + // save this to later make the selection with + EAffinity affinity = m_selectionToDelete.affinity(); + + Position downstreamEnd = m_selectionToDelete.end().downstream(); + m_needPlaceholder = isStartOfParagraph(m_selectionToDelete.visibleStart(), CanCrossEditingBoundary) + && isEndOfParagraph(m_selectionToDelete.visibleEnd(), CanCrossEditingBoundary) + && !lineBreakExistsAtVisiblePosition(m_selectionToDelete.visibleEnd()); + if (m_needPlaceholder) { + // Don't need a placeholder when deleting a selection that starts just before a table + // and ends inside it (we do need placeholders to hold open empty cells, but that's + // handled elsewhere). + if (Node* table = isLastPositionBeforeTable(m_selectionToDelete.visibleStart())) + if (m_selectionToDelete.end().node()->isDescendantOf(table)) + m_needPlaceholder = false; + } + + + // set up our state + initializePositionData(); + + // Delete any text that may hinder our ability to fixup whitespace after the delete + deleteInsignificantTextDownstream(m_trailingWhitespace); + + saveTypingStyleState(); + + // deleting just a BR is handled specially, at least because we do not + // want to replace it with a placeholder BR! + if (handleSpecialCaseBRDelete()) { + calculateTypingStyleAfterDelete(); + setEndingSelection(VisibleSelection(m_endingPosition, affinity)); + clearTransientState(); + rebalanceWhitespace(); + return; + } + + handleGeneralDelete(); + + fixupWhitespace(); + + mergeParagraphs(); + + removePreviouslySelectedEmptyTableRows(); + + RefPtr<Node> placeholder = m_needPlaceholder ? createBreakElement(document()).get() : 0; + + if (placeholder) + insertNodeAt(placeholder.get(), m_endingPosition); + + rebalanceWhitespaceAt(m_endingPosition); + + calculateTypingStyleAfterDelete(); + + setEndingSelection(VisibleSelection(m_endingPosition, affinity)); + clearTransientState(); +} + +EditAction DeleteSelectionCommand::editingAction() const +{ + // Note that DeleteSelectionCommand is also used when the user presses the Delete key, + // but in that case there's a TypingCommand that supplies the editingAction(), so + // the Undo menu correctly shows "Undo Typing" + return EditActionCut; +} + +// Normally deletion doesn't preserve the typing style that was present before it. For example, +// type a character, Bold, then delete the character and start typing. The Bold typing style shouldn't +// stick around. Deletion should preserve a typing style that *it* sets, however. +bool DeleteSelectionCommand::preservesTypingStyle() const +{ + return m_typingStyle; +} + +} // namespace WebCore diff --git a/Source/WebCore/editing/DeleteSelectionCommand.h b/Source/WebCore/editing/DeleteSelectionCommand.h new file mode 100644 index 0000000..7b46935 --- /dev/null +++ b/Source/WebCore/editing/DeleteSelectionCommand.h @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2005, 2006, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef DeleteSelectionCommand_h +#define DeleteSelectionCommand_h + +#include "CompositeEditCommand.h" + +namespace WebCore { + +class EditingStyle; + +class DeleteSelectionCommand : public CompositeEditCommand { +public: + static PassRefPtr<DeleteSelectionCommand> create(Document* document, bool smartDelete = false, bool mergeBlocksAfterDelete = true, bool replace = false, bool expandForSpecialElements = false) + { + return adoptRef(new DeleteSelectionCommand(document, smartDelete, mergeBlocksAfterDelete, replace, expandForSpecialElements)); + } + static PassRefPtr<DeleteSelectionCommand> create(const VisibleSelection& selection, bool smartDelete = false, bool mergeBlocksAfterDelete = true, bool replace = false, bool expandForSpecialElements = false) + { + return adoptRef(new DeleteSelectionCommand(selection, smartDelete, mergeBlocksAfterDelete, replace, expandForSpecialElements)); + } + +private: + DeleteSelectionCommand(Document*, bool smartDelete, bool mergeBlocksAfterDelete, bool replace, bool expandForSpecialElements); + DeleteSelectionCommand(const VisibleSelection&, bool smartDelete, bool mergeBlocksAfterDelete, bool replace, bool expandForSpecialElements); + + virtual void doApply(); + virtual EditAction editingAction() const; + + virtual bool preservesTypingStyle() const; + + void initializeStartEnd(Position&, Position&); + void setStartingSelectionOnSmartDelete(const Position&, const Position&); + void initializePositionData(); + void saveTypingStyleState(); + void insertPlaceholderForAncestorBlockContent(); + bool handleSpecialCaseBRDelete(); + void handleGeneralDelete(); + void fixupWhitespace(); + void mergeParagraphs(); + void removePreviouslySelectedEmptyTableRows(); + void calculateEndingPosition(); + void calculateTypingStyleAfterDelete(); + void clearTransientState(); + virtual void removeNode(PassRefPtr<Node>); + virtual void deleteTextFromNode(PassRefPtr<Text>, unsigned, unsigned); + + bool m_hasSelectionToDelete; + bool m_smartDelete; + bool m_mergeBlocksAfterDelete; + bool m_needPlaceholder; + bool m_replace; + bool m_expandForSpecialElements; + bool m_pruneStartBlockIfNecessary; + bool m_startsAtEmptyLine; + + // This data is transient and should be cleared at the end of the doApply function. + VisibleSelection m_selectionToDelete; + Position m_upstreamStart; + Position m_downstreamStart; + Position m_upstreamEnd; + Position m_downstreamEnd; + Position m_endingPosition; + Position m_leadingWhitespace; + Position m_trailingWhitespace; + RefPtr<Node> m_startBlock; + RefPtr<Node> m_endBlock; + RefPtr<EditingStyle> m_typingStyle; + RefPtr<EditingStyle> m_deleteIntoBlockquoteStyle; + RefPtr<Node> m_startRoot; + RefPtr<Node> m_endRoot; + RefPtr<Node> m_startTableRow; + RefPtr<Node> m_endTableRow; + RefPtr<Node> m_temporaryPlaceholder; +}; + +} // namespace WebCore + +#endif // DeleteSelectionCommand_h diff --git a/Source/WebCore/editing/EditAction.h b/Source/WebCore/editing/EditAction.h new file mode 100644 index 0000000..8046f3c --- /dev/null +++ b/Source/WebCore/editing/EditAction.h @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2004 Apple Computer, 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef EditAction_h +#define EditAction_h + +namespace WebCore { + typedef enum { + EditActionUnspecified, + EditActionSetColor, + EditActionSetBackgroundColor, + EditActionTurnOffKerning, + EditActionTightenKerning, + EditActionLoosenKerning, + EditActionUseStandardKerning, + EditActionTurnOffLigatures, + EditActionUseStandardLigatures, + EditActionUseAllLigatures, + EditActionRaiseBaseline, + EditActionLowerBaseline, + EditActionSetTraditionalCharacterShape, + EditActionSetFont, + EditActionChangeAttributes, + EditActionAlignLeft, + EditActionAlignRight, + EditActionCenter, + EditActionJustify, + EditActionSetWritingDirection, + EditActionSubscript, + EditActionSuperscript, + EditActionUnderline, + EditActionOutline, + EditActionUnscript, + EditActionDrag, + EditActionCut, + EditActionPaste, + EditActionPasteFont, + EditActionPasteRuler, + EditActionTyping, + EditActionCreateLink, + EditActionUnlink, + EditActionFormatBlock, + EditActionInsertList, + EditActionIndent, + EditActionOutdent + } EditAction; +} + +#endif diff --git a/Source/WebCore/editing/EditCommand.cpp b/Source/WebCore/editing/EditCommand.cpp new file mode 100644 index 0000000..1b4451d --- /dev/null +++ b/Source/WebCore/editing/EditCommand.cpp @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2005, 2006, 2007 Apple, 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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 "EditCommand.h" + +#include "CompositeEditCommand.h" +#include "DeleteButtonController.h" +#include "Document.h" +#include "Editor.h" +#include "Element.h" +#include "EventNames.h" +#include "Frame.h" +#include "ScopedEventQueue.h" +#include "SelectionController.h" +#include "VisiblePosition.h" +#include "htmlediting.h" + +namespace WebCore { + +EditCommand::EditCommand(Document* document) + : m_document(document) + , m_parent(0) +{ + ASSERT(m_document); + ASSERT(m_document->frame()); + setStartingSelection(avoidIntersectionWithNode(m_document->frame()->selection()->selection(), m_document->frame()->editor()->deleteButtonController()->containerElement())); + setEndingSelection(m_startingSelection); +} + +EditCommand::~EditCommand() +{ +} + +void EditCommand::apply() +{ + ASSERT(m_document); + ASSERT(m_document->frame()); + + Frame* frame = m_document->frame(); + + if (isTopLevelCommand()) { + if (!endingSelection().isContentRichlyEditable()) { + switch (editingAction()) { + case EditActionTyping: + case EditActionPaste: + case EditActionDrag: + case EditActionSetWritingDirection: + case EditActionCut: + case EditActionUnspecified: + break; + default: + ASSERT_NOT_REACHED(); + return; + } + } + } + + // Changes to the document may have been made since the last editing operation that + // require a layout, as in <rdar://problem/5658603>. Low level operations, like + // RemoveNodeCommand, don't require a layout because the high level operations that + // use them perform one if one is necessary (like for the creation of VisiblePositions). + if (isTopLevelCommand()) + updateLayout(); + + { + EventQueueScope scope; + DeleteButtonController* deleteButtonController = frame->editor()->deleteButtonController(); + deleteButtonController->disable(); + doApply(); + deleteButtonController->enable(); + } + + if (isTopLevelCommand()) { + // Only need to call appliedEditing for top-level commands, and TypingCommands do it on their + // own (see TypingCommand::typingAddedToOpenCommand). + if (!isTypingCommand()) + frame->editor()->appliedEditing(this); + } +} + +void EditCommand::unapply() +{ + ASSERT(m_document); + ASSERT(m_document->frame()); + + Frame* frame = m_document->frame(); + + // Changes to the document may have been made since the last editing operation that + // require a layout, as in <rdar://problem/5658603>. Low level operations, like + // RemoveNodeCommand, don't require a layout because the high level operations that + // use them perform one if one is necessary (like for the creation of VisiblePositions). + if (isTopLevelCommand()) + updateLayout(); + + DeleteButtonController* deleteButtonController = frame->editor()->deleteButtonController(); + deleteButtonController->disable(); + doUnapply(); + deleteButtonController->enable(); + + if (isTopLevelCommand()) + frame->editor()->unappliedEditing(this); +} + +void EditCommand::reapply() +{ + ASSERT(m_document); + ASSERT(m_document->frame()); + + Frame* frame = m_document->frame(); + + // Changes to the document may have been made since the last editing operation that + // require a layout, as in <rdar://problem/5658603>. Low level operations, like + // RemoveNodeCommand, don't require a layout because the high level operations that + // use them perform one if one is necessary (like for the creation of VisiblePositions). + if (isTopLevelCommand()) + updateLayout(); + + DeleteButtonController* deleteButtonController = frame->editor()->deleteButtonController(); + deleteButtonController->disable(); + doReapply(); + deleteButtonController->enable(); + + if (isTopLevelCommand()) + frame->editor()->reappliedEditing(this); +} + +void EditCommand::doReapply() +{ + doApply(); +} + +EditAction EditCommand::editingAction() const +{ + return EditActionUnspecified; +} + +void EditCommand::setStartingSelection(const VisibleSelection& s) +{ + Element* root = s.rootEditableElement(); + for (EditCommand* cmd = this; ; cmd = cmd->m_parent) { + cmd->m_startingSelection = s; + cmd->m_startingRootEditableElement = root; + if (!cmd->m_parent || cmd->m_parent->isFirstCommand(cmd)) + break; + } +} + +void EditCommand::setEndingSelection(const VisibleSelection &s) +{ + Element* root = s.rootEditableElement(); + for (EditCommand* cmd = this; cmd; cmd = cmd->m_parent) { + cmd->m_endingSelection = s; + cmd->m_endingRootEditableElement = root; + } +} + +bool EditCommand::preservesTypingStyle() const +{ + return false; +} + +bool EditCommand::isInsertTextCommand() const +{ + return false; +} + +bool EditCommand::isTypingCommand() const +{ + return false; +} + + +void EditCommand::updateLayout() const +{ + document()->updateLayoutIgnorePendingStylesheets(); +} + +void EditCommand::setParent(CompositeEditCommand* parent) +{ + ASSERT(parent); + ASSERT(!m_parent); + m_parent = parent; + m_startingSelection = parent->m_endingSelection; + m_endingSelection = parent->m_endingSelection; + m_startingRootEditableElement = parent->m_endingRootEditableElement; + m_endingRootEditableElement = parent->m_endingRootEditableElement; +} + +void applyCommand(PassRefPtr<EditCommand> command) +{ + command->apply(); +} + +} // namespace WebCore diff --git a/Source/WebCore/editing/EditCommand.h b/Source/WebCore/editing/EditCommand.h new file mode 100644 index 0000000..4826ec0 --- /dev/null +++ b/Source/WebCore/editing/EditCommand.h @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2005, 2006, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef EditCommand_h +#define EditCommand_h + +#include "EditAction.h" +#include "Element.h" +#include "VisibleSelection.h" + +namespace WebCore { + +class CompositeEditCommand; + +class EditCommand : public RefCounted<EditCommand> { +public: + virtual ~EditCommand(); + + void setParent(CompositeEditCommand*); + + void apply(); + void unapply(); + void reapply(); + + virtual EditAction editingAction() const; + + const VisibleSelection& startingSelection() const { return m_startingSelection; } + const VisibleSelection& endingSelection() const { return m_endingSelection; } + + Element* startingRootEditableElement() const { return m_startingRootEditableElement.get(); } + Element* endingRootEditableElement() const { return m_endingRootEditableElement.get(); } + + virtual bool isInsertTextCommand() const; + virtual bool isTypingCommand() const; + + virtual bool preservesTypingStyle() const; + + bool isTopLevelCommand() const { return !m_parent; } + +protected: + EditCommand(Document*); + + Document* document() const { return m_document.get(); } + + void setStartingSelection(const VisibleSelection&); + void setEndingSelection(const VisibleSelection&); + + void updateLayout() const; + +private: + virtual void doApply() = 0; + virtual void doUnapply() = 0; + virtual void doReapply(); // calls doApply() + + RefPtr<Document> m_document; + VisibleSelection m_startingSelection; + VisibleSelection m_endingSelection; + RefPtr<Element> m_startingRootEditableElement; + RefPtr<Element> m_endingRootEditableElement; + CompositeEditCommand* m_parent; + + friend void applyCommand(PassRefPtr<EditCommand>); +}; + +class SimpleEditCommand : public EditCommand { +protected: + SimpleEditCommand(Document* document) : EditCommand(document) { } +}; + +void applyCommand(PassRefPtr<EditCommand>); + +} // namespace WebCore + +#endif // EditCommand_h diff --git a/Source/WebCore/editing/EditingAllInOne.cpp b/Source/WebCore/editing/EditingAllInOne.cpp new file mode 100644 index 0000000..e4e0bbb --- /dev/null +++ b/Source/WebCore/editing/EditingAllInOne.cpp @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2010 Apple 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 APPLE INC. ``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 APPLE INC. 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. + */ + +// This all-in-one cpp file cuts down on template bloat to allow us to build our Windows release build. + +#include <AppendNodeCommand.cpp> +#include <ApplyBlockElementCommand.cpp> +#include <ApplyStyleCommand.cpp> +#include <BreakBlockquoteCommand.cpp> +#include <CompositeEditCommand.cpp> +#include <CreateLinkCommand.cpp> +#include <DeleteButton.cpp> +#include <DeleteButtonController.cpp> +#include <DeleteFromTextNodeCommand.cpp> +#include <DeleteSelectionCommand.cpp> +#include <EditCommand.cpp> +#include <EditingStyle.cpp> +#include <Editor.cpp> +#include <EditorCommand.cpp> +#include <FormatBlockCommand.cpp> +#include <HTMLInterchange.cpp> +#include <IndentOutdentCommand.cpp> +#include <InsertIntoTextNodeCommand.cpp> +#include <InsertLineBreakCommand.cpp> +#include <InsertListCommand.cpp> +#include <InsertNodeBeforeCommand.cpp> +#include <InsertParagraphSeparatorCommand.cpp> +#include <InsertTextCommand.cpp> +#include <JoinTextNodesCommand.cpp> +#include <MarkupAccumulator.cpp> +#include <MergeIdenticalElementsCommand.cpp> +#include <ModifySelectionListLevel.cpp> +#include <MoveSelectionCommand.cpp> +#include <RemoveCSSPropertyCommand.cpp> +#include <RemoveFormatCommand.cpp> +#include <RemoveNodeCommand.cpp> +#include <RemoveNodePreservingChildrenCommand.cpp> +#include <ReplaceNodeWithSpanCommand.cpp> +#include <ReplaceSelectionCommand.cpp> +#include <SelectionController.cpp> +#include <SetNodeAttributeCommand.cpp> +#include <SmartReplace.cpp> +#include <SmartReplaceCF.cpp> +#include <SpellChecker.cpp> +#include <SplitElementCommand.cpp> +#include <SplitTextNodeCommand.cpp> +#include <SplitTextNodeContainingElementCommand.cpp> +#include <TextCheckingHelper.cpp> +#include <TextIterator.cpp> +#include <TypingCommand.cpp> +#include <UnlinkCommand.cpp> +#include <VisiblePosition.cpp> +#include <VisibleSelection.cpp> +#include <WrapContentsInDummySpanCommand.cpp> +#include <htmlediting.cpp> +#include <markup.cpp> +#include <visible_units.cpp> diff --git a/Source/WebCore/editing/EditingBehavior.h b/Source/WebCore/editing/EditingBehavior.h new file mode 100644 index 0000000..a367c52 --- /dev/null +++ b/Source/WebCore/editing/EditingBehavior.h @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies) + * Copyright (C) 2010 Apple Inc. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef EditingBehavior_h +#define EditingBehavior_h + +#include "EditingBehaviorTypes.h" + +namespace WebCore { + +class EditingBehavior { + +public: + EditingBehavior(EditingBehaviorType type) + : m_type(type) + { + } + + // Individual functions for each case where we have more than one style of editing behavior. + // Create a new function for any platform difference so we can control it here. + + // When extending a selection beyond the top or bottom boundary of an editable area, + // maintain the horizontal position on Windows but extend it to the boundary of the editable + // content on Mac. + bool shouldMoveCaretToHorizontalBoundaryWhenPastTopOrBottom() const { return m_type != EditingWindowsBehavior; } + + // On Windows, selections should always be considered as directional, regardless if it is + // mouse-based or keyboard-based. + bool shouldConsiderSelectionAsDirectional() const { return m_type != EditingMacBehavior; } + + // On Mac, when revealing a selection (for example as a result of a Find operation on the Browser), + // content should be scrolled such that the selection gets certer aligned. + bool shouldCenterAlignWhenSelectionIsRevealed() const { return m_type == EditingMacBehavior; } + + // On Mac, style is considered present when present at the beginning of selection. On other platforms, + // style has to be present throughout the selection. + bool shouldToggleStyleBasedOnStartOfSelection() const { return m_type == EditingMacBehavior; } + + // Standard Mac behavior when extending to a boundary is grow the selection rather than leaving the base + // in place and moving the extent. Matches NSTextView. + bool shouldAlwaysGrowSelectionWhenExtendingToBoundary() const { return m_type == EditingMacBehavior; } + + // On Mac, when processing a contextual click, the object being clicked upon should be selected. + bool shouldSelectOnContextualMenuClick() const { return m_type == EditingMacBehavior; } + +private: + EditingBehaviorType m_type; +}; + +} // namespace WebCore + +#endif // EditingBehavior_h diff --git a/Source/WebCore/editing/EditingBehaviorTypes.h b/Source/WebCore/editing/EditingBehaviorTypes.h new file mode 100644 index 0000000..11345da --- /dev/null +++ b/Source/WebCore/editing/EditingBehaviorTypes.h @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies) + * Copyright (C) 2010 Apple Inc. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef EditingBehaviorTypes_h +#define EditingBehaviorTypes_h + +namespace WebCore { + +// There are multiple editing details that are different on Windows than Macintosh. +// We use a single switch for all of them. Some examples: +// +// 1) Clicking below the last line of an editable area puts the caret at the end +// of the last line on Mac, but in the middle of the last line on Windows. +// 2) Pushing the down arrow key on the last line puts the caret at the end of the +// last line on Mac, but does nothing on Windows. A similar case exists on the +// top line. +// +// This setting is intended to control these sorts of behaviors. There are some other +// behaviors with individual function calls on EditorClient (smart copy and paste and +// selecting the space after a double click) that could be combined with this if +// if possible in the future. +enum EditingBehaviorType { + EditingMacBehavior, + EditingWindowsBehavior, + EditingUnixBehavior +}; + +} // WebCore namespace + +#endif // EditingBehaviorTypes_h diff --git a/Source/WebCore/editing/EditingBoundary.h b/Source/WebCore/editing/EditingBoundary.h new file mode 100644 index 0000000..1cb0849 --- /dev/null +++ b/Source/WebCore/editing/EditingBoundary.h @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2004, 2006, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef EditingBoundary_h +#define EditingBoundary_h + +namespace WebCore { + +enum EditingBoundaryCrossingRule { + CanCrossEditingBoundary, + CannotCrossEditingBoundary +}; + +} + +#endif // EditingBoundary_h diff --git a/Source/WebCore/editing/EditingStyle.cpp b/Source/WebCore/editing/EditingStyle.cpp new file mode 100644 index 0000000..8caf4b6 --- /dev/null +++ b/Source/WebCore/editing/EditingStyle.cpp @@ -0,0 +1,341 @@ +/* + * Copyright (C) 2007, 2008, 2009 Apple Computer, Inc. + * 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 "EditingStyle.h" + +#include "ApplyStyleCommand.h" +#include "CSSComputedStyleDeclaration.h" +#include "CSSMutableStyleDeclaration.h" +#include "CSSValueKeywords.h" +#include "Frame.h" +#include "Node.h" +#include "Position.h" +#include "RenderStyle.h" +#include "SelectionController.h" + +namespace WebCore { + +// Editing style properties must be preserved during editing operation. +// e.g. when a user inserts a new paragraph, all properties listed here must be copied to the new paragraph. +// FIXME: The current editingStyleProperties contains all inheritableProperties but we may not need to preserve all inheritable properties +static const int editingStyleProperties[] = { + // CSS inheritable properties + CSSPropertyBorderCollapse, + CSSPropertyColor, + CSSPropertyFontFamily, + CSSPropertyFontSize, + CSSPropertyFontStyle, + CSSPropertyFontVariant, + CSSPropertyFontWeight, + CSSPropertyLetterSpacing, + CSSPropertyLineHeight, + CSSPropertyOrphans, + CSSPropertyTextAlign, + CSSPropertyTextIndent, + CSSPropertyTextTransform, + CSSPropertyWhiteSpace, + CSSPropertyWidows, + CSSPropertyWordSpacing, + CSSPropertyWebkitBorderHorizontalSpacing, + CSSPropertyWebkitBorderVerticalSpacing, + CSSPropertyWebkitTextDecorationsInEffect, + CSSPropertyWebkitTextFillColor, + CSSPropertyWebkitTextSizeAdjust, + CSSPropertyWebkitTextStrokeColor, + CSSPropertyWebkitTextStrokeWidth, +}; +size_t numEditingStyleProperties = WTF_ARRAY_LENGTH(editingStyleProperties); + +static PassRefPtr<CSSMutableStyleDeclaration> copyEditingProperties(CSSStyleDeclaration* style) +{ + return style->copyPropertiesInSet(editingStyleProperties, numEditingStyleProperties); +} + +static PassRefPtr<CSSMutableStyleDeclaration> editingStyleFromComputedStyle(PassRefPtr<CSSComputedStyleDeclaration> style) +{ + if (!style) + return CSSMutableStyleDeclaration::create(); + return copyEditingProperties(style.get()); +} + +float EditingStyle::NoFontDelta = 0.0f; + +EditingStyle::EditingStyle() + : m_shouldUseFixedDefaultFontSize(false) + , m_fontSizeDelta(NoFontDelta) +{ +} + +EditingStyle::EditingStyle(Node* node) + : m_shouldUseFixedDefaultFontSize(false) + , m_fontSizeDelta(NoFontDelta) +{ + init(node); +} + +EditingStyle::EditingStyle(const Position& position) + : m_shouldUseFixedDefaultFontSize(false) + , m_fontSizeDelta(NoFontDelta) +{ + init(position.node()); +} + +EditingStyle::EditingStyle(const CSSStyleDeclaration* style) + : m_mutableStyle(style->copy()) + , m_shouldUseFixedDefaultFontSize(false) + , m_fontSizeDelta(NoFontDelta) +{ + extractFontSizeDelta(); +} + +EditingStyle::~EditingStyle() +{ +} + +void EditingStyle::init(Node* node) +{ + RefPtr<CSSComputedStyleDeclaration> computedStyleAtPosition = computedStyle(node); + m_mutableStyle = editingStyleFromComputedStyle(computedStyleAtPosition); + + if (node && node->computedStyle()) { + RenderStyle* renderStyle = node->computedStyle(); + removeTextFillAndStrokeColorsIfNeeded(renderStyle); + replaceFontSizeByKeywordIfPossible(renderStyle, computedStyleAtPosition.get()); + } + + m_shouldUseFixedDefaultFontSize = computedStyleAtPosition->useFixedFontDefaultSize(); + extractFontSizeDelta(); +} + +void EditingStyle::removeTextFillAndStrokeColorsIfNeeded(RenderStyle* renderStyle) +{ + // If a node's text fill color is invalid, then its children use + // their font-color as their text fill color (they don't + // inherit it). Likewise for stroke color. + ExceptionCode ec = 0; + if (!renderStyle->textFillColor().isValid()) + m_mutableStyle->removeProperty(CSSPropertyWebkitTextFillColor, ec); + if (!renderStyle->textStrokeColor().isValid()) + m_mutableStyle->removeProperty(CSSPropertyWebkitTextStrokeColor, ec); + ASSERT(!ec); +} + +void EditingStyle::replaceFontSizeByKeywordIfPossible(RenderStyle* renderStyle, CSSComputedStyleDeclaration* computedStyle) +{ + ASSERT(renderStyle); + if (renderStyle->fontDescription().keywordSize()) + m_mutableStyle->setProperty(CSSPropertyFontSize, computedStyle->getFontSizeCSSValuePreferringKeyword()->cssText()); +} + +void EditingStyle::extractFontSizeDelta() +{ + if (m_mutableStyle->getPropertyCSSValue(CSSPropertyFontSize)) { + // Explicit font size overrides any delta. + m_mutableStyle->removeProperty(CSSPropertyWebkitFontSizeDelta); + return; + } + + // Get the adjustment amount out of the style. + RefPtr<CSSValue> value = m_mutableStyle->getPropertyCSSValue(CSSPropertyWebkitFontSizeDelta); + if (!value || value->cssValueType() != CSSValue::CSS_PRIMITIVE_VALUE) + return; + + CSSPrimitiveValue* primitiveValue = static_cast<CSSPrimitiveValue*>(value.get()); + + // Only PX handled now. If we handle more types in the future, perhaps + // a switch statement here would be more appropriate. + if (primitiveValue->primitiveType() != CSSPrimitiveValue::CSS_PX) + return; + + m_fontSizeDelta = primitiveValue->getFloatValue(); + m_mutableStyle->removeProperty(CSSPropertyWebkitFontSizeDelta); +} + +bool EditingStyle::isEmpty() const +{ + return (!m_mutableStyle || m_mutableStyle->isEmpty()) && m_fontSizeDelta == NoFontDelta; +} + +bool EditingStyle::textDirection(WritingDirection& writingDirection) const +{ + RefPtr<CSSValue> unicodeBidi = m_mutableStyle->getPropertyCSSValue(CSSPropertyUnicodeBidi); + if (!unicodeBidi) + return false; + + ASSERT(unicodeBidi->isPrimitiveValue()); + int unicodeBidiValue = static_cast<CSSPrimitiveValue*>(unicodeBidi.get())->getIdent(); + if (unicodeBidiValue == CSSValueEmbed) { + RefPtr<CSSValue> direction = m_mutableStyle->getPropertyCSSValue(CSSPropertyDirection); + ASSERT(!direction || direction->isPrimitiveValue()); + if (!direction) + return false; + + writingDirection = static_cast<CSSPrimitiveValue*>(direction.get())->getIdent() == CSSValueLtr ? LeftToRightWritingDirection : RightToLeftWritingDirection; + + return true; + } + + if (unicodeBidiValue == CSSValueNormal) { + writingDirection = NaturalWritingDirection; + return true; + } + + return false; +} + +void EditingStyle::setStyle(PassRefPtr<CSSMutableStyleDeclaration> style) +{ + m_mutableStyle = style; + // FIXME: We should be able to figure out whether or not font is fixed width for mutable style. + // We need to check font-family is monospace as in FontDescription but we don't want to duplicate code here. + m_shouldUseFixedDefaultFontSize = false; + extractFontSizeDelta(); +} + +void EditingStyle::overrideWithStyle(const CSSMutableStyleDeclaration* style) +{ + if (!style || !style->length()) + return; + if (!m_mutableStyle) + m_mutableStyle = CSSMutableStyleDeclaration::create(); + m_mutableStyle->merge(style); + extractFontSizeDelta(); +} + +void EditingStyle::clear() +{ + m_mutableStyle.clear(); + m_shouldUseFixedDefaultFontSize = false; + m_fontSizeDelta = NoFontDelta; +} + +PassRefPtr<EditingStyle> EditingStyle::copy() const +{ + RefPtr<EditingStyle> copy = EditingStyle::create(); + if (m_mutableStyle) + copy->m_mutableStyle = m_mutableStyle->copy(); + copy->m_shouldUseFixedDefaultFontSize = m_shouldUseFixedDefaultFontSize; + copy->m_fontSizeDelta = m_fontSizeDelta; + return copy; +} + +PassRefPtr<EditingStyle> EditingStyle::extractAndRemoveBlockProperties() +{ + RefPtr<EditingStyle> blockProperties = EditingStyle::create(); + if (!m_mutableStyle) + return blockProperties; + + blockProperties->m_mutableStyle = m_mutableStyle->copyBlockProperties(); + m_mutableStyle->removeBlockProperties(); + + return blockProperties; +} + +void EditingStyle::removeBlockProperties() +{ + if (!m_mutableStyle) + return; + + m_mutableStyle->removeBlockProperties(); +} + +void EditingStyle::removeStyleAddedByNode(Node* node) +{ + if (!node || !node->parentNode()) + return; + RefPtr<CSSMutableStyleDeclaration> parentStyle = editingStyleFromComputedStyle(computedStyle(node->parentNode())); + RefPtr<CSSMutableStyleDeclaration> nodeStyle = editingStyleFromComputedStyle(computedStyle(node)); + parentStyle->diff(nodeStyle.get()); + nodeStyle->diff(m_mutableStyle.get()); +} + +void EditingStyle::removeStyleConflictingWithStyleOfNode(Node* node) +{ + if (!node || !node->parentNode() || !m_mutableStyle) + return; + RefPtr<CSSMutableStyleDeclaration> parentStyle = editingStyleFromComputedStyle(computedStyle(node->parentNode())); + RefPtr<CSSMutableStyleDeclaration> nodeStyle = editingStyleFromComputedStyle(computedStyle(node)); + parentStyle->diff(nodeStyle.get()); + + CSSMutableStyleDeclaration::const_iterator end = nodeStyle->end(); + for (CSSMutableStyleDeclaration::const_iterator it = nodeStyle->begin(); it != end; ++it) + m_mutableStyle->removeProperty(it->id()); +} + +void EditingStyle::removeNonEditingProperties() +{ + if (m_mutableStyle) + m_mutableStyle = copyEditingProperties(m_mutableStyle.get()); +} + +void EditingStyle::prepareToApplyAt(const Position& position, ShouldPreserveWritingDirection shouldPreserveWritingDirection) +{ + if (!m_mutableStyle) + return; + + // ReplaceSelectionCommand::handleStyleSpans() requires that this function only removes the editing style. + // If this function was modified in the future to delete all redundant properties, then add a boolean value to indicate + // which one of editingStyleAtPosition or computedStyle is called. + RefPtr<EditingStyle> style = EditingStyle::create(position); + + RefPtr<CSSValue> unicodeBidi; + RefPtr<CSSValue> direction; + if (shouldPreserveWritingDirection == PreserveWritingDirection) { + unicodeBidi = m_mutableStyle->getPropertyCSSValue(CSSPropertyUnicodeBidi); + direction = m_mutableStyle->getPropertyCSSValue(CSSPropertyDirection); + } + + style->m_mutableStyle->diff(m_mutableStyle.get()); + + // if alpha value is zero, we don't add the background color. + RefPtr<CSSValue> backgroundColor = m_mutableStyle->getPropertyCSSValue(CSSPropertyBackgroundColor); + if (backgroundColor && backgroundColor->isPrimitiveValue() + && !alphaChannel(static_cast<CSSPrimitiveValue*>(backgroundColor.get())->getRGBA32Value())) { + ExceptionCode ec; + m_mutableStyle->removeProperty(CSSPropertyBackgroundColor, ec); + } + + if (unicodeBidi) { + ASSERT(unicodeBidi->isPrimitiveValue()); + m_mutableStyle->setProperty(CSSPropertyUnicodeBidi, static_cast<CSSPrimitiveValue*>(unicodeBidi.get())->getIdent()); + if (direction) { + ASSERT(direction->isPrimitiveValue()); + m_mutableStyle->setProperty(CSSPropertyDirection, static_cast<CSSPrimitiveValue*>(direction.get())->getIdent()); + } + } +} + +PassRefPtr<EditingStyle> editingStyleIncludingTypingStyle(const Position& position) +{ + RefPtr<EditingStyle> editingStyle = EditingStyle::create(position); + RefPtr<EditingStyle> typingStyle = position.node()->document()->frame()->selection()->typingStyle(); + if (typingStyle && typingStyle->style()) + editingStyle->style()->merge(copyEditingProperties(typingStyle->style()).get()); + return editingStyle; +} + +} diff --git a/Source/WebCore/editing/EditingStyle.h b/Source/WebCore/editing/EditingStyle.h new file mode 100644 index 0000000..a71b4ad --- /dev/null +++ b/Source/WebCore/editing/EditingStyle.h @@ -0,0 +1,111 @@ +/* + * 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: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * 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. + */ + +#ifndef EditingStyle_h +#define EditingStyle_h + +#include "WritingDirection.h" +#include <wtf/RefCounted.h> +#include <wtf/RefPtr.h> + +namespace WebCore { + +class CSSStyleDeclaration; +class CSSComputedStyleDeclaration; +class CSSMutableStyleDeclaration; +class Node; +class Position; +class RenderStyle; + +class EditingStyle : public RefCounted<EditingStyle> { +public: + + enum ShouldPreserveWritingDirection { PreserveWritingDirection, DoNotPreserveWritingDirection }; + static float NoFontDelta; + + static PassRefPtr<EditingStyle> create() + { + return adoptRef(new EditingStyle()); + } + + static PassRefPtr<EditingStyle> create(Node* node) + { + return adoptRef(new EditingStyle(node)); + } + + static PassRefPtr<EditingStyle> create(const Position& position) + { + return adoptRef(new EditingStyle(position)); + } + + static PassRefPtr<EditingStyle> create(const CSSStyleDeclaration* style) + { + return adoptRef(new EditingStyle(style)); + } + + ~EditingStyle(); + + CSSMutableStyleDeclaration* style() { return m_mutableStyle.get(); } + bool textDirection(WritingDirection&) const; + bool isEmpty() const; + void setStyle(PassRefPtr<CSSMutableStyleDeclaration>); + void overrideWithStyle(const CSSMutableStyleDeclaration*); + void clear(); + PassRefPtr<EditingStyle> copy() const; + PassRefPtr<EditingStyle> extractAndRemoveBlockProperties(); + void removeBlockProperties(); + void removeStyleAddedByNode(Node*); + void removeStyleConflictingWithStyleOfNode(Node*); + void removeNonEditingProperties(); + void prepareToApplyAt(const Position&, ShouldPreserveWritingDirection = DoNotPreserveWritingDirection); + + float fontSizeDelta() const { return m_fontSizeDelta; } + bool hasFontSizeDelta() const { return m_fontSizeDelta != NoFontDelta; } + +private: + EditingStyle(); + EditingStyle(Node*); + EditingStyle(const Position&); + EditingStyle(const CSSStyleDeclaration*); + void init(Node*); + void removeTextFillAndStrokeColorsIfNeeded(RenderStyle*); + void replaceFontSizeByKeywordIfPossible(RenderStyle*, CSSComputedStyleDeclaration*); + void extractFontSizeDelta(); + + RefPtr<CSSMutableStyleDeclaration> m_mutableStyle; + bool m_shouldUseFixedDefaultFontSize; + float m_fontSizeDelta; +}; + +PassRefPtr<EditingStyle> editingStyleIncludingTypingStyle(const Position&); + +} // namespace WebCore + +#endif // EditingStyle_h diff --git a/Source/WebCore/editing/Editor.cpp b/Source/WebCore/editing/Editor.cpp new file mode 100644 index 0000000..a24e7c6 --- /dev/null +++ b/Source/WebCore/editing/Editor.cpp @@ -0,0 +1,3525 @@ +/* + * Copyright (C) 2006, 2007, 2008 Apple Inc. All rights reserved. + * Copyright (C) 2008 Nokia Corporation and/or its subsidiary(-ies) + * + * 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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 "Editor.h" + +#include "AXObjectCache.h" +#include "ApplyStyleCommand.h" +#include "CSSComputedStyleDeclaration.h" +#include "CSSMutableStyleDeclaration.h" +#include "CSSProperty.h" +#include "CSSPropertyNames.h" +#include "CSSStyleSelector.h" +#include "CSSValueKeywords.h" +#include "CachedResourceLoader.h" +#include "CharacterNames.h" +#include "ClipboardEvent.h" +#include "CompositionEvent.h" +#include "CreateLinkCommand.h" +#include "DeleteButtonController.h" +#include "DeleteSelectionCommand.h" +#include "DocumentFragment.h" +#include "DocumentMarkerController.h" +#include "EditingText.h" +#include "EditorClient.h" +#include "EventHandler.h" +#include "EventNames.h" +#include "FocusController.h" +#include "Frame.h" +#include "FrameTree.h" +#include "FrameView.h" +#include "HTMLFrameOwnerElement.h" +#include "HTMLInputElement.h" +#include "HTMLTextAreaElement.h" +#include "HitTestResult.h" +#include "IndentOutdentCommand.h" +#include "InsertListCommand.h" +#include "KeyboardEvent.h" +#include "KillRing.h" +#include "ModifySelectionListLevel.h" +#include "NodeList.h" +#include "Page.h" +#include "Pasteboard.h" +#include "TextCheckingHelper.h" +#include "RemoveFormatCommand.h" +#include "RenderBlock.h" +#include "RenderPart.h" +#include "RenderTextControl.h" +#include "ReplaceSelectionCommand.h" +#include "Settings.h" +#include "Sound.h" +#include "SpellChecker.h" +#include "Text.h" +#include "TextEvent.h" +#include "TextIterator.h" +#include "TypingCommand.h" +#include "UserTypingGestureIndicator.h" +#include "htmlediting.h" +#include "markup.h" +#include "visible_units.h" +#include <wtf/UnusedParam.h> + +namespace WebCore { + +using namespace std; +using namespace HTMLNames; + +static inline bool isAmbiguousBoundaryCharacter(UChar character) +{ + // These are characters that can behave as word boundaries, but can appear within words. + // If they are just typed, i.e. if they are immediately followed by a caret, we want to delay text checking until the next character has been typed. + // FIXME: this is required until 6853027 is fixed and text checking can do this for us. + return character == '\'' || character == rightSingleQuotationMark || character == hebrewPunctuationGershayim; +} + +#if SUPPORT_AUTOCORRECTION_PANEL +static FloatRect boundingBoxForRange(Range* range) +{ + Vector<FloatQuad> textQuads; + range->getBorderAndTextQuads(textQuads); + FloatRect totalBoundingBox; + size_t size = textQuads.size(); + for (size_t i = 0; i< size; ++i) + totalBoundingBox.unite(textQuads[i].boundingBox()); + return totalBoundingBox; +} +#endif // SUPPORT_AUTOCORRECTION_PANEL + +static const Vector<DocumentMarker::MarkerType>& markerTypesForAutocorrection() +{ + DEFINE_STATIC_LOCAL(Vector<DocumentMarker::MarkerType>, markerTypesForAutoCorrection, ()); + if (markerTypesForAutoCorrection.isEmpty()) { + markerTypesForAutoCorrection.append(DocumentMarker::Replacement); + markerTypesForAutoCorrection.append(DocumentMarker::CorrectionIndicator); + } + return markerTypesForAutoCorrection; +} + +static const Vector<DocumentMarker::MarkerType>& markerTypesForReplacement() +{ + DEFINE_STATIC_LOCAL(Vector<DocumentMarker::MarkerType>, markerTypesForReplacement, ()); + if (markerTypesForReplacement.isEmpty()) + markerTypesForReplacement.append(DocumentMarker::Replacement); + return markerTypesForReplacement; +} + +// When an event handler has moved the selection outside of a text control +// we should use the target control's selection for this editing operation. +VisibleSelection Editor::selectionForCommand(Event* event) +{ + VisibleSelection selection = m_frame->selection()->selection(); + if (!event) + return selection; + // If the target is a text control, and the current selection is outside of its shadow tree, + // then use the saved selection for that text control. + Node* target = event->target()->toNode(); + Node* selectionStart = selection.start().node(); + if (target && (!selectionStart || target->shadowAncestorNode() != selectionStart->shadowAncestorNode())) { + RefPtr<Range> range; + if (target->hasTagName(inputTag) && static_cast<HTMLInputElement*>(target)->isTextField()) + range = static_cast<HTMLInputElement*>(target)->selection(); + else if (target->hasTagName(textareaTag)) + range = static_cast<HTMLTextAreaElement*>(target)->selection(); + + if (range) + return VisibleSelection(range.get()); + } + return selection; +} + +// Function considers Mac editing behavior a fallback when Page or Settings is not available. +EditingBehavior Editor::behavior() const +{ + if (!m_frame || !m_frame->settings()) + return EditingBehavior(EditingMacBehavior); + + return EditingBehavior(m_frame->settings()->editingBehaviorType()); +} + +EditorClient* Editor::client() const +{ + if (Page* page = m_frame->page()) + return page->editorClient(); + return 0; +} + +void Editor::handleKeyboardEvent(KeyboardEvent* event) +{ + if (EditorClient* c = client()) + c->handleKeyboardEvent(event); +} + +void Editor::handleInputMethodKeydown(KeyboardEvent* event) +{ + if (EditorClient* c = client()) + c->handleInputMethodKeydown(event); +} + +bool Editor::handleTextEvent(TextEvent* event) +{ + // Default event handling for Drag and Drop will be handled by DragController + // so we leave the event for it. + if (event->isDrop()) + return false; + + if (event->isPaste()) { + if (event->pastingFragment()) + replaceSelectionWithFragment(event->pastingFragment(), false, event->shouldSmartReplace(), event->shouldMatchStyle()); + else + replaceSelectionWithText(event->data(), false, event->shouldSmartReplace()); + return true; + } + + String data = event->data(); + if (data == "\n") { + if (event->isLineBreak()) + return insertLineBreak(); + return insertParagraphSeparator(); + } + + return insertTextWithoutSendingTextEvent(data, false, event); +} + +bool Editor::canEdit() const +{ + return m_frame->selection()->isContentEditable(); +} + +bool Editor::canEditRichly() const +{ + return m_frame->selection()->isContentRichlyEditable(); +} + +// WinIE uses onbeforecut and onbeforepaste to enables the cut and paste menu items. They +// also send onbeforecopy, apparently for symmetry, but it doesn't affect the menu items. +// We need to use onbeforecopy as a real menu enabler because we allow elements that are not +// normally selectable to implement copy/paste (like divs, or a document body). + +bool Editor::canDHTMLCut() +{ + return !m_frame->selection()->isInPasswordField() && !dispatchCPPEvent(eventNames().beforecutEvent, ClipboardNumb); +} + +bool Editor::canDHTMLCopy() +{ + return !m_frame->selection()->isInPasswordField() && !dispatchCPPEvent(eventNames().beforecopyEvent, ClipboardNumb); +} + +bool Editor::canDHTMLPaste() +{ + return !dispatchCPPEvent(eventNames().beforepasteEvent, ClipboardNumb); +} + +bool Editor::canCut() const +{ + return canCopy() && canDelete(); +} + +static HTMLImageElement* imageElementFromImageDocument(Document* document) +{ + if (!document) + return 0; + if (!document->isImageDocument()) + return 0; + + HTMLElement* body = document->body(); + if (!body) + return 0; + + Node* node = body->firstChild(); + if (!node) + return 0; + if (!node->hasTagName(imgTag)) + return 0; + return static_cast<HTMLImageElement*>(node); +} + +bool Editor::canCopy() const +{ + if (imageElementFromImageDocument(m_frame->document())) + return true; + SelectionController* selection = m_frame->selection(); + return selection->isRange() && !selection->isInPasswordField(); +} + +bool Editor::canPaste() const +{ + return canEdit(); +} + +bool Editor::canDelete() const +{ + SelectionController* selection = m_frame->selection(); + return selection->isRange() && selection->isContentEditable(); +} + +bool Editor::canDeleteRange(Range* range) const +{ + ExceptionCode ec = 0; + Node* startContainer = range->startContainer(ec); + Node* endContainer = range->endContainer(ec); + if (!startContainer || !endContainer) + return false; + + if (!startContainer->isContentEditable() || !endContainer->isContentEditable()) + return false; + + if (range->collapsed(ec)) { + VisiblePosition start(startContainer, range->startOffset(ec), DOWNSTREAM); + VisiblePosition previous = start.previous(); + // FIXME: We sometimes allow deletions at the start of editable roots, like when the caret is in an empty list item. + if (previous.isNull() || previous.deepEquivalent().node()->rootEditableElement() != startContainer->rootEditableElement()) + return false; + } + return true; +} + +bool Editor::smartInsertDeleteEnabled() +{ + return client() && client()->smartInsertDeleteEnabled(); +} + +bool Editor::canSmartCopyOrDelete() +{ + return client() && client()->smartInsertDeleteEnabled() && m_frame->selection()->granularity() == WordGranularity; +} + +bool Editor::isSelectTrailingWhitespaceEnabled() +{ + return client() && client()->isSelectTrailingWhitespaceEnabled(); +} + +bool Editor::deleteWithDirection(SelectionDirection direction, TextGranularity granularity, bool killRing, bool isTypingAction) +{ + if (!canEdit()) + return false; + + if (m_frame->selection()->isRange()) { + if (isTypingAction) { + TypingCommand::deleteKeyPressed(m_frame->document(), canSmartCopyOrDelete(), granularity); + revealSelectionAfterEditingOperation(); + } else { + if (killRing) + addToKillRing(selectedRange().get(), false); + deleteSelectionWithSmartDelete(canSmartCopyOrDelete()); + // Implicitly calls revealSelectionAfterEditingOperation(). + } + } else { + switch (direction) { + case DirectionForward: + case DirectionRight: + TypingCommand::forwardDeleteKeyPressed(m_frame->document(), canSmartCopyOrDelete(), granularity, killRing); + break; + case DirectionBackward: + case DirectionLeft: + TypingCommand::deleteKeyPressed(m_frame->document(), canSmartCopyOrDelete(), granularity, killRing); + break; + } + revealSelectionAfterEditingOperation(); + } + + // FIXME: We should to move this down into deleteKeyPressed. + // clear the "start new kill ring sequence" setting, because it was set to true + // when the selection was updated by deleting the range + if (killRing) + setStartNewKillRingSequence(false); + + return true; +} + +void Editor::deleteSelectionWithSmartDelete(bool smartDelete) +{ + if (m_frame->selection()->isNone()) + return; + + applyCommand(DeleteSelectionCommand::create(m_frame->document(), smartDelete)); +} + +void Editor::pasteAsPlainText(const String& pastingText, bool smartReplace) +{ + Node* target = findEventTargetFromSelection(); + if (!target) + return; + ExceptionCode ec = 0; + target->dispatchEvent(TextEvent::createForPlainTextPaste(m_frame->domWindow(), pastingText, smartReplace), ec); +} + +void Editor::pasteAsFragment(PassRefPtr<DocumentFragment> pastingFragment, bool smartReplace, bool matchStyle) +{ + Node* target = findEventTargetFromSelection(); + if (!target) + return; + ExceptionCode ec = 0; + target->dispatchEvent(TextEvent::createForFragmentPaste(m_frame->domWindow(), pastingFragment, smartReplace, matchStyle), ec); +} + +void Editor::pasteAsPlainTextBypassingDHTML() +{ + pasteAsPlainTextWithPasteboard(Pasteboard::generalPasteboard()); +} + +void Editor::pasteAsPlainTextWithPasteboard(Pasteboard* pasteboard) +{ + String text = pasteboard->plainText(m_frame); + if (client() && client()->shouldInsertText(text, selectedRange().get(), EditorInsertActionPasted)) + pasteAsPlainText(text, canSmartReplaceWithPasteboard(pasteboard)); +} + +#if !PLATFORM(MAC) +void Editor::pasteWithPasteboard(Pasteboard* pasteboard, bool allowPlainText) +{ + RefPtr<Range> range = selectedRange(); + bool chosePlainText; + RefPtr<DocumentFragment> fragment = pasteboard->documentFragment(m_frame, range, allowPlainText, chosePlainText); + if (fragment && shouldInsertFragment(fragment, range, EditorInsertActionPasted)) + pasteAsFragment(fragment, canSmartReplaceWithPasteboard(pasteboard), chosePlainText); +} +#endif + +bool Editor::canSmartReplaceWithPasteboard(Pasteboard* pasteboard) +{ + return client() && client()->smartInsertDeleteEnabled() && pasteboard->canSmartReplace(); +} + +bool Editor::shouldInsertFragment(PassRefPtr<DocumentFragment> fragment, PassRefPtr<Range> replacingDOMRange, EditorInsertAction givenAction) +{ + if (!client()) + return false; + + if (fragment) { + Node* child = fragment->firstChild(); + if (child && fragment->lastChild() == child && child->isCharacterDataNode()) + return client()->shouldInsertText(static_cast<CharacterData*>(child)->data(), replacingDOMRange.get(), givenAction); + } + + return client()->shouldInsertNode(fragment.get(), replacingDOMRange.get(), givenAction); +} + +void Editor::replaceSelectionWithFragment(PassRefPtr<DocumentFragment> fragment, bool selectReplacement, bool smartReplace, bool matchStyle) +{ + if (m_frame->selection()->isNone() || !fragment) + return; + + applyCommand(ReplaceSelectionCommand::create(m_frame->document(), fragment, selectReplacement, smartReplace, matchStyle)); + revealSelectionAfterEditingOperation(); + + Node* nodeToCheck = m_frame->selection()->rootEditableElement(); + if (m_spellChecker->canCheckAsynchronously(nodeToCheck)) + m_spellChecker->requestCheckingFor(nodeToCheck); +} + +void Editor::replaceSelectionWithText(const String& text, bool selectReplacement, bool smartReplace) +{ + replaceSelectionWithFragment(createFragmentFromText(selectedRange().get(), text), selectReplacement, smartReplace, true); +} + +PassRefPtr<Range> Editor::selectedRange() +{ + if (!m_frame) + return 0; + return m_frame->selection()->toNormalizedRange(); +} + +bool Editor::shouldDeleteRange(Range* range) const +{ + ExceptionCode ec; + if (!range || range->collapsed(ec)) + return false; + + if (!canDeleteRange(range)) + return false; + + return client() && client()->shouldDeleteRange(range); +} + +bool Editor::tryDHTMLCopy() +{ + if (m_frame->selection()->isInPasswordField()) + return false; + + if (canCopy()) + // Must be done before oncopy adds types and data to the pboard, + // also done for security, as it erases data from the last copy/paste. + Pasteboard::generalPasteboard()->clear(); + + return !dispatchCPPEvent(eventNames().copyEvent, ClipboardWritable); +} + +bool Editor::tryDHTMLCut() +{ + if (m_frame->selection()->isInPasswordField()) + return false; + + if (canCut()) + // Must be done before oncut adds types and data to the pboard, + // also done for security, as it erases data from the last copy/paste. + Pasteboard::generalPasteboard()->clear(); + + return !dispatchCPPEvent(eventNames().cutEvent, ClipboardWritable); +} + +bool Editor::tryDHTMLPaste() +{ + return !dispatchCPPEvent(eventNames().pasteEvent, ClipboardReadable); +} + +void Editor::writeSelectionToPasteboard(Pasteboard* pasteboard) +{ + pasteboard->writeSelection(selectedRange().get(), canSmartCopyOrDelete(), m_frame); +} + +bool Editor::shouldInsertText(const String& text, Range* range, EditorInsertAction action) const +{ + return client() && client()->shouldInsertText(text, range, action); +} + +bool Editor::shouldShowDeleteInterface(HTMLElement* element) const +{ + return client() && client()->shouldShowDeleteInterface(element); +} + +void Editor::respondToChangedSelection(const VisibleSelection& oldSelection) +{ +#if SUPPORT_AUTOCORRECTION_PANEL + VisibleSelection currentSelection(frame()->selection()->selection()); + if (currentSelection != oldSelection) { + stopCorrectionPanelTimer(); + dismissCorrectionPanel(ReasonForDismissingCorrectionPanelIgnored); + } +#endif // SUPPORT_AUTOCORRECTION_PANEL + + if (client()) + client()->respondToChangedSelection(); + m_deleteButtonController->respondToChangedSelection(oldSelection); + +#if SUPPORT_AUTOCORRECTION_PANEL + // When user moves caret to the end of autocorrected word and pauses, we show the panel + // containing the original pre-correction word so that user can quickly revert the + // undesired autocorrection. Here, we start correction panel timer once we confirm that + // the new caret position is at the end of a word. + if (!currentSelection.isCaret() || currentSelection == oldSelection) + return; + + VisiblePosition selectionPosition = currentSelection.start(); + VisiblePosition endPositionOfWord = endOfWord(selectionPosition, LeftWordIfOnBoundary); + if (selectionPosition != endPositionOfWord) + return; + + Position position = endPositionOfWord.deepEquivalent(); + if (position.anchorType() != Position::PositionIsOffsetInAnchor) + return; + + Node* node = position.containerNode(); + int endOffset = position.offsetInContainerNode(); + Vector<DocumentMarker> markers = node->document()->markers()->markersForNode(node); + size_t markerCount = markers.size(); + for (size_t i = 0; i < markerCount; ++i) { + const DocumentMarker& marker = markers[i]; + if (((marker.type == DocumentMarker::CorrectionIndicator && marker.description.length()) || marker.type == DocumentMarker::Spelling) && static_cast<int>(marker.endOffset) == endOffset) { + RefPtr<Range> wordRange = Range::create(frame()->document(), node, marker.startOffset, node, marker.endOffset); + String currentWord = plainText(wordRange.get()); + if (currentWord.length()) { + m_correctionPanelInfo.rangeToBeReplaced = wordRange; + m_correctionPanelInfo.replacedString = currentWord; + if (marker.type == DocumentMarker::Spelling) + startCorrectionPanelTimer(CorrectionPanelInfo::PanelTypeSpellingSuggestions); + else { + m_correctionPanelInfo.replacementString = marker.description; + startCorrectionPanelTimer(CorrectionPanelInfo::PanelTypeReversion); + } + } + break; + } + } +#endif // SUPPORT_AUTOCORRECTION_PANEL +} + +void Editor::respondToChangedContents(const VisibleSelection& endingSelection) +{ + if (AXObjectCache::accessibilityEnabled()) { + Node* node = endingSelection.start().node(); + if (node) + m_frame->document()->axObjectCache()->postNotification(node->renderer(), AXObjectCache::AXValueChanged, false); + } + +#if REMOVE_MARKERS_UPON_EDITING + removeSpellAndCorrectionMarkersFromWordsToBeEdited(true); +#endif + + if (client()) + client()->respondToChangedContents(); +} + +const SimpleFontData* Editor::fontForSelection(bool& hasMultipleFonts) const +{ +#if !PLATFORM(QT) + hasMultipleFonts = false; + + if (!m_frame->selection()->isRange()) { + Node* nodeToRemove; + RenderStyle* style = styleForSelectionStart(nodeToRemove); // sets nodeToRemove + + const SimpleFontData* result = 0; + if (style) + result = style->font().primaryFont(); + + if (nodeToRemove) { + ExceptionCode ec; + nodeToRemove->remove(ec); + ASSERT(!ec); + } + + return result; + } + + const SimpleFontData* font = 0; + + RefPtr<Range> range = m_frame->selection()->toNormalizedRange(); + Node* startNode = range->editingStartPosition().node(); + if (startNode) { + Node* pastEnd = range->pastLastNode(); + // In the loop below, n should eventually match pastEnd and not become nil, but we've seen at least one + // unreproducible case where this didn't happen, so check for nil also. + for (Node* n = startNode; n && n != pastEnd; n = n->traverseNextNode()) { + RenderObject* renderer = n->renderer(); + if (!renderer) + continue; + // FIXME: Are there any node types that have renderers, but that we should be skipping? + const SimpleFontData* f = renderer->style()->font().primaryFont(); + if (!font) + font = f; + else if (font != f) { + hasMultipleFonts = true; + break; + } + } + } + + return font; +#else + return 0; +#endif +} + +WritingDirection Editor::textDirectionForSelection(bool& hasNestedOrMultipleEmbeddings) const +{ + hasNestedOrMultipleEmbeddings = true; + + if (m_frame->selection()->isNone()) + return NaturalWritingDirection; + + Position position = m_frame->selection()->selection().start().downstream(); + + Node* node = position.node(); + if (!node) + return NaturalWritingDirection; + + Position end; + if (m_frame->selection()->isRange()) { + end = m_frame->selection()->selection().end().upstream(); + + Node* pastLast = Range::create(m_frame->document(), rangeCompliantEquivalent(position), rangeCompliantEquivalent(end))->pastLastNode(); + for (Node* n = node; n && n != pastLast; n = n->traverseNextNode()) { + if (!n->isStyledElement()) + continue; + + RefPtr<CSSComputedStyleDeclaration> style = computedStyle(n); + RefPtr<CSSValue> unicodeBidi = style->getPropertyCSSValue(CSSPropertyUnicodeBidi); + if (!unicodeBidi) + continue; + + ASSERT(unicodeBidi->isPrimitiveValue()); + int unicodeBidiValue = static_cast<CSSPrimitiveValue*>(unicodeBidi.get())->getIdent(); + if (unicodeBidiValue == CSSValueEmbed || unicodeBidiValue == CSSValueBidiOverride) + return NaturalWritingDirection; + } + } + + if (m_frame->selection()->isCaret()) { + RefPtr<EditingStyle> typingStyle = m_frame->selection()->typingStyle(); + WritingDirection direction; + if (typingStyle && typingStyle->textDirection(direction)) { + hasNestedOrMultipleEmbeddings = false; + return direction; + } + node = m_frame->selection()->selection().visibleStart().deepEquivalent().node(); + } + + // The selection is either a caret with no typing attributes or a range in which no embedding is added, so just use the start position + // to decide. + Node* block = enclosingBlock(node); + WritingDirection foundDirection = NaturalWritingDirection; + + for (; node != block; node = node->parentNode()) { + if (!node->isStyledElement()) + continue; + + RefPtr<CSSComputedStyleDeclaration> style = computedStyle(node); + RefPtr<CSSValue> unicodeBidi = style->getPropertyCSSValue(CSSPropertyUnicodeBidi); + if (!unicodeBidi) + continue; + + ASSERT(unicodeBidi->isPrimitiveValue()); + int unicodeBidiValue = static_cast<CSSPrimitiveValue*>(unicodeBidi.get())->getIdent(); + if (unicodeBidiValue == CSSValueNormal) + continue; + + if (unicodeBidiValue == CSSValueBidiOverride) + return NaturalWritingDirection; + + ASSERT(unicodeBidiValue == CSSValueEmbed); + RefPtr<CSSValue> direction = style->getPropertyCSSValue(CSSPropertyDirection); + if (!direction) + continue; + + ASSERT(direction->isPrimitiveValue()); + int directionValue = static_cast<CSSPrimitiveValue*>(direction.get())->getIdent(); + if (directionValue != CSSValueLtr && directionValue != CSSValueRtl) + continue; + + if (foundDirection != NaturalWritingDirection) + return NaturalWritingDirection; + + // In the range case, make sure that the embedding element persists until the end of the range. + if (m_frame->selection()->isRange() && !end.node()->isDescendantOf(node)) + return NaturalWritingDirection; + + foundDirection = directionValue == CSSValueLtr ? LeftToRightWritingDirection : RightToLeftWritingDirection; + } + hasNestedOrMultipleEmbeddings = false; + return foundDirection; +} + +bool Editor::hasBidiSelection() const +{ + if (m_frame->selection()->isNone()) + return false; + + Node* startNode; + if (m_frame->selection()->isRange()) { + startNode = m_frame->selection()->selection().start().downstream().node(); + Node* endNode = m_frame->selection()->selection().end().upstream().node(); + if (enclosingBlock(startNode) != enclosingBlock(endNode)) + return false; + } else + startNode = m_frame->selection()->selection().visibleStart().deepEquivalent().node(); + + RenderObject* renderer = startNode->renderer(); + while (renderer && !renderer->isRenderBlock()) + renderer = renderer->parent(); + + if (!renderer) + return false; + + RenderStyle* style = renderer->style(); + if (!style->isLeftToRightDirection()) + return true; + + return toRenderBlock(renderer)->containsNonZeroBidiLevel(); +} + +TriState Editor::selectionUnorderedListState() const +{ + if (m_frame->selection()->isCaret()) { + if (enclosingNodeWithTag(m_frame->selection()->selection().start(), ulTag)) + return TrueTriState; + } else if (m_frame->selection()->isRange()) { + Node* startNode = enclosingNodeWithTag(m_frame->selection()->selection().start(), ulTag); + Node* endNode = enclosingNodeWithTag(m_frame->selection()->selection().end(), ulTag); + if (startNode && endNode && startNode == endNode) + return TrueTriState; + } + + return FalseTriState; +} + +TriState Editor::selectionOrderedListState() const +{ + if (m_frame->selection()->isCaret()) { + if (enclosingNodeWithTag(m_frame->selection()->selection().start(), olTag)) + return TrueTriState; + } else if (m_frame->selection()->isRange()) { + Node* startNode = enclosingNodeWithTag(m_frame->selection()->selection().start(), olTag); + Node* endNode = enclosingNodeWithTag(m_frame->selection()->selection().end(), olTag); + if (startNode && endNode && startNode == endNode) + return TrueTriState; + } + + return FalseTriState; +} + +PassRefPtr<Node> Editor::insertOrderedList() +{ + if (!canEditRichly()) + return 0; + + RefPtr<Node> newList = InsertListCommand::insertList(m_frame->document(), InsertListCommand::OrderedList); + revealSelectionAfterEditingOperation(); + return newList; +} + +PassRefPtr<Node> Editor::insertUnorderedList() +{ + if (!canEditRichly()) + return 0; + + RefPtr<Node> newList = InsertListCommand::insertList(m_frame->document(), InsertListCommand::UnorderedList); + revealSelectionAfterEditingOperation(); + return newList; +} + +bool Editor::canIncreaseSelectionListLevel() +{ + return canEditRichly() && IncreaseSelectionListLevelCommand::canIncreaseSelectionListLevel(m_frame->document()); +} + +bool Editor::canDecreaseSelectionListLevel() +{ + return canEditRichly() && DecreaseSelectionListLevelCommand::canDecreaseSelectionListLevel(m_frame->document()); +} + +PassRefPtr<Node> Editor::increaseSelectionListLevel() +{ + if (!canEditRichly() || m_frame->selection()->isNone()) + return 0; + + RefPtr<Node> newList = IncreaseSelectionListLevelCommand::increaseSelectionListLevel(m_frame->document()); + revealSelectionAfterEditingOperation(); + return newList; +} + +PassRefPtr<Node> Editor::increaseSelectionListLevelOrdered() +{ + if (!canEditRichly() || m_frame->selection()->isNone()) + return 0; + + RefPtr<Node> newList = IncreaseSelectionListLevelCommand::increaseSelectionListLevelOrdered(m_frame->document()); + revealSelectionAfterEditingOperation(); + return newList.release(); +} + +PassRefPtr<Node> Editor::increaseSelectionListLevelUnordered() +{ + if (!canEditRichly() || m_frame->selection()->isNone()) + return 0; + + RefPtr<Node> newList = IncreaseSelectionListLevelCommand::increaseSelectionListLevelUnordered(m_frame->document()); + revealSelectionAfterEditingOperation(); + return newList.release(); +} + +void Editor::decreaseSelectionListLevel() +{ + if (!canEditRichly() || m_frame->selection()->isNone()) + return; + + DecreaseSelectionListLevelCommand::decreaseSelectionListLevel(m_frame->document()); + revealSelectionAfterEditingOperation(); +} + +void Editor::removeFormattingAndStyle() +{ + applyCommand(RemoveFormatCommand::create(m_frame->document())); +} + +void Editor::clearLastEditCommand() +{ + m_lastEditCommand.clear(); +} + +// Returns whether caller should continue with "the default processing", which is the same as +// the event handler NOT setting the return value to false +bool Editor::dispatchCPPEvent(const AtomicString &eventType, ClipboardAccessPolicy policy) +{ + Node* target = findEventTargetFromSelection(); + if (!target) + return true; + + RefPtr<Clipboard> clipboard = newGeneralClipboard(policy, m_frame); + + ExceptionCode ec = 0; + RefPtr<Event> evt = ClipboardEvent::create(eventType, true, true, clipboard); + target->dispatchEvent(evt, ec); + bool noDefaultProcessing = evt->defaultPrevented(); + + // invalidate clipboard here for security + clipboard->setAccessPolicy(ClipboardNumb); + + return !noDefaultProcessing; +} + +Node* Editor::findEventTargetFrom(const VisibleSelection& selection) const +{ + Node* target = selection.start().element(); + if (!target) + target = m_frame->document()->body(); + if (!target) + return 0; + return target->shadowAncestorNode(); + +} + +Node* Editor::findEventTargetFromSelection() const +{ + return findEventTargetFrom(m_frame->selection()->selection()); +} + +void Editor::applyStyle(CSSStyleDeclaration* style, EditAction editingAction) +{ + switch (m_frame->selection()->selectionType()) { + case VisibleSelection::NoSelection: + // do nothing + break; + case VisibleSelection::CaretSelection: + computeAndSetTypingStyle(style, editingAction); + break; + case VisibleSelection::RangeSelection: + if (style) + applyCommand(ApplyStyleCommand::create(m_frame->document(), EditingStyle::create(style).get(), editingAction)); + break; + } +} + +bool Editor::shouldApplyStyle(CSSStyleDeclaration* style, Range* range) +{ + return client()->shouldApplyStyle(style, range); +} + +void Editor::applyParagraphStyle(CSSStyleDeclaration* style, EditAction editingAction) +{ + switch (m_frame->selection()->selectionType()) { + case VisibleSelection::NoSelection: + // do nothing + break; + case VisibleSelection::CaretSelection: + case VisibleSelection::RangeSelection: + if (style) + applyCommand(ApplyStyleCommand::create(m_frame->document(), EditingStyle::create(style).get(), editingAction, ApplyStyleCommand::ForceBlockProperties)); + break; + } +} + +void Editor::applyStyleToSelection(CSSStyleDeclaration* style, EditAction editingAction) +{ + if (!style || !style->length() || !canEditRichly()) + return; + + if (client() && client()->shouldApplyStyle(style, m_frame->selection()->toNormalizedRange().get())) + applyStyle(style, editingAction); +} + +void Editor::applyParagraphStyleToSelection(CSSStyleDeclaration* style, EditAction editingAction) +{ + if (!style || !style->length() || !canEditRichly()) + return; + + if (client() && client()->shouldApplyStyle(style, m_frame->selection()->toNormalizedRange().get())) + applyParagraphStyle(style, editingAction); +} + +bool Editor::clientIsEditable() const +{ + return client() && client()->isEditable(); +} + +// CSS properties that only has a visual difference when applied to text. +static const int textOnlyProperties[] = { + CSSPropertyTextDecoration, + CSSPropertyWebkitTextDecorationsInEffect, + CSSPropertyFontStyle, + CSSPropertyFontWeight, + CSSPropertyColor, +}; + +static TriState triStateOfStyle(CSSStyleDeclaration* desiredStyle, CSSStyleDeclaration* styleToCompare, bool ignoreTextOnlyProperties = false) +{ + RefPtr<CSSMutableStyleDeclaration> diff = getPropertiesNotIn(desiredStyle, styleToCompare); + + if (ignoreTextOnlyProperties) + diff->removePropertiesInSet(textOnlyProperties, WTF_ARRAY_LENGTH(textOnlyProperties)); + + if (!diff->length()) + return TrueTriState; + if (diff->length() == desiredStyle->length()) + return FalseTriState; + return MixedTriState; +} + +bool Editor::selectionStartHasStyle(CSSStyleDeclaration* style) const +{ + bool shouldUseFixedFontDefaultSize; + RefPtr<CSSMutableStyleDeclaration> selectionStyle = selectionComputedStyle(shouldUseFixedFontDefaultSize); + if (!selectionStyle) + return false; + return triStateOfStyle(style, selectionStyle.get()) == TrueTriState; +} + +TriState Editor::selectionHasStyle(CSSStyleDeclaration* style) const +{ + TriState state = FalseTriState; + + if (!m_frame->selection()->isRange()) { + bool shouldUseFixedFontDefaultSize; + RefPtr<CSSMutableStyleDeclaration> selectionStyle = selectionComputedStyle(shouldUseFixedFontDefaultSize); + if (!selectionStyle) + return FalseTriState; + state = triStateOfStyle(style, selectionStyle.get()); + } else { + for (Node* node = m_frame->selection()->start().node(); node; node = node->traverseNextNode()) { + RefPtr<CSSComputedStyleDeclaration> nodeStyle = computedStyle(node); + if (nodeStyle) { + TriState nodeState = triStateOfStyle(style, nodeStyle.get(), !node->isTextNode()); + if (node == m_frame->selection()->start().node()) + state = nodeState; + else if (state != nodeState && node->isTextNode()) { + state = MixedTriState; + break; + } + } + if (node == m_frame->selection()->end().node()) + break; + } + } + + return state; +} + +static bool hasTransparentBackgroundColor(CSSStyleDeclaration* style) +{ + RefPtr<CSSValue> cssValue = style->getPropertyCSSValue(CSSPropertyBackgroundColor); + if (!cssValue) + return true; + + if (!cssValue->isPrimitiveValue()) + return false; + CSSPrimitiveValue* value = static_cast<CSSPrimitiveValue*>(cssValue.get()); + + if (value->primitiveType() == CSSPrimitiveValue::CSS_RGBCOLOR) + return !alphaChannel(value->getRGBA32Value()); + + return value->getIdent() == CSSValueTransparent; +} + +String Editor::selectionStartCSSPropertyValue(int propertyID) +{ + bool shouldUseFixedFontDefaultSize = false; + RefPtr<CSSMutableStyleDeclaration> selectionStyle = selectionComputedStyle(shouldUseFixedFontDefaultSize); + if (!selectionStyle) + return String(); + + String value = selectionStyle->getPropertyValue(propertyID); + + // If background color is transparent, traverse parent nodes until we hit a different value or document root + // Also, if the selection is a range, ignore the background color at the start of selection, + // and find the background color of the common ancestor. + if (propertyID == CSSPropertyBackgroundColor && (m_frame->selection()->isRange() || hasTransparentBackgroundColor(selectionStyle.get()))) { + RefPtr<Range> range(m_frame->selection()->toNormalizedRange()); + ExceptionCode ec = 0; + for (Node* ancestor = range->commonAncestorContainer(ec); ancestor; ancestor = ancestor->parentNode()) { + selectionStyle = computedStyle(ancestor)->copy(); + if (!hasTransparentBackgroundColor(selectionStyle.get())) { + value = selectionStyle->getPropertyValue(CSSPropertyBackgroundColor); + break; + } + } + } + + if (propertyID == CSSPropertyFontSize) { + RefPtr<CSSValue> cssValue = selectionStyle->getPropertyCSSValue(CSSPropertyFontSize); + ASSERT(cssValue->isPrimitiveValue()); + int fontPixelSize = static_cast<CSSPrimitiveValue*>(cssValue.get())->getIntValue(CSSPrimitiveValue::CSS_PX); + int size = CSSStyleSelector::legacyFontSize(m_frame->document(), fontPixelSize, shouldUseFixedFontDefaultSize); + value = String::number(size); + } + + return value; +} + +void Editor::indent() +{ + applyCommand(IndentOutdentCommand::create(m_frame->document(), IndentOutdentCommand::Indent)); +} + +void Editor::outdent() +{ + applyCommand(IndentOutdentCommand::create(m_frame->document(), IndentOutdentCommand::Outdent)); +} + +static void dispatchEditableContentChangedEvents(const EditCommand& command) +{ + Element* startRoot = command.startingRootEditableElement(); + Element* endRoot = command.endingRootEditableElement(); + ExceptionCode ec; + if (startRoot) + startRoot->dispatchEvent(Event::create(eventNames().webkitEditableContentChangedEvent, false, false), ec); + if (endRoot && endRoot != startRoot) + endRoot->dispatchEvent(Event::create(eventNames().webkitEditableContentChangedEvent, false, false), ec); +} + +void Editor::appliedEditing(PassRefPtr<EditCommand> cmd) +{ + // We may start reversion panel timer in respondToChangedSelection(). + // So we stop the timer for current panel before calling changeSelectionAfterCommand() later in this method. + stopCorrectionPanelTimer(); + m_frame->document()->updateLayout(); + + dispatchEditableContentChangedEvents(*cmd); + + VisibleSelection newSelection(cmd->endingSelection()); + // Don't clear the typing style with this selection change. We do those things elsewhere if necessary. + changeSelectionAfterCommand(newSelection, false, false); + + if (!cmd->preservesTypingStyle()) + m_frame->selection()->clearTypingStyle(); + + // Command will be equal to last edit command only in the case of typing + if (m_lastEditCommand.get() == cmd) + ASSERT(cmd->isTypingCommand()); + else { + // Only register a new undo command if the command passed in is + // different from the last command + m_lastEditCommand = cmd; + if (client()) + client()->registerCommandForUndo(m_lastEditCommand); + } + respondToChangedContents(newSelection); +} + +void Editor::unappliedEditing(PassRefPtr<EditCommand> cmd) +{ + m_frame->document()->updateLayout(); + + dispatchEditableContentChangedEvents(*cmd); + + VisibleSelection newSelection(cmd->startingSelection()); + changeSelectionAfterCommand(newSelection, true, true); + + m_lastEditCommand = 0; + if (client()) + client()->registerCommandForRedo(cmd); + respondToChangedContents(newSelection); +} + +void Editor::reappliedEditing(PassRefPtr<EditCommand> cmd) +{ + m_frame->document()->updateLayout(); + + dispatchEditableContentChangedEvents(*cmd); + + VisibleSelection newSelection(cmd->endingSelection()); + changeSelectionAfterCommand(newSelection, true, true); + + m_lastEditCommand = 0; + if (client()) + client()->registerCommandForUndo(cmd); + respondToChangedContents(newSelection); +} + +Editor::Editor(Frame* frame) + : m_frame(frame) + , m_deleteButtonController(adoptPtr(new DeleteButtonController(frame))) + , m_ignoreCompositionSelectionChange(false) + , m_shouldStartNewKillRingSequence(false) + // This is off by default, since most editors want this behavior (this matches IE but not FF). + , m_shouldStyleWithCSS(false) + , m_killRing(adoptPtr(new KillRing)) + , m_spellChecker(new SpellChecker(frame, frame->page() ? frame->page()->editorClient() : 0)) + , m_correctionPanelTimer(this, &Editor::correctionPanelTimerFired) + , m_areMarkedTextMatchesHighlighted(false) +{ +} + +Editor::~Editor() +{ +#if SUPPORT_AUTOCORRECTION_PANEL + dismissCorrectionPanel(ReasonForDismissingCorrectionPanelIgnored); +#endif +} + +void Editor::clear() +{ + m_compositionNode = 0; + m_customCompositionUnderlines.clear(); + m_shouldStyleWithCSS = false; +} + +bool Editor::insertText(const String& text, Event* triggeringEvent) +{ + return m_frame->eventHandler()->handleTextInputEvent(text, triggeringEvent); +} + +bool Editor::insertTextWithoutSendingTextEvent(const String& text, bool selectInsertedText, Event* triggeringEvent) +{ + if (text.isEmpty()) + return false; + + VisibleSelection selection = selectionForCommand(triggeringEvent); + if (!selection.isContentEditable()) + return false; + RefPtr<Range> range = selection.toNormalizedRange(); + + if (!shouldInsertText(text, range.get(), EditorInsertActionTyped)) + return true; + + // Get the selection to use for the event that triggered this insertText. + // If the event handler changed the selection, we may want to use a different selection + // that is contained in the event target. + selection = selectionForCommand(triggeringEvent); + if (selection.isContentEditable()) { + if (Node* selectionStart = selection.start().node()) { + RefPtr<Document> document = selectionStart->document(); + + // Insert the text + TypingCommand::insertText(document.get(), text, selection, selectInsertedText); + + // Reveal the current selection + if (Frame* editedFrame = document->frame()) + if (Page* page = editedFrame->page()) + page->focusController()->focusedOrMainFrame()->selection()->revealSelection(ScrollAlignment::alignToEdgeIfNeeded); + } + } + + return true; +} + +bool Editor::insertLineBreak() +{ + if (!canEdit()) + return false; + + if (!shouldInsertText("\n", m_frame->selection()->toNormalizedRange().get(), EditorInsertActionTyped)) + return true; + + TypingCommand::insertLineBreak(m_frame->document()); + revealSelectionAfterEditingOperation(); + return true; +} + +bool Editor::insertParagraphSeparator() +{ + if (!canEdit()) + return false; + + if (!canEditRichly()) + return insertLineBreak(); + + if (!shouldInsertText("\n", m_frame->selection()->toNormalizedRange().get(), EditorInsertActionTyped)) + return true; + + TypingCommand::insertParagraphSeparator(m_frame->document()); + revealSelectionAfterEditingOperation(); + return true; +} + +void Editor::cut() +{ + if (tryDHTMLCut()) + return; // DHTML did the whole operation + if (!canCut()) { + systemBeep(); + return; + } + RefPtr<Range> selection = selectedRange(); + if (shouldDeleteRange(selection.get())) { +#if REMOVE_MARKERS_UPON_EDITING + removeSpellAndCorrectionMarkersFromWordsToBeEdited(true); +#endif + if (isNodeInTextFormControl(m_frame->selection()->start().node())) + Pasteboard::generalPasteboard()->writePlainText(selectedText()); + else + Pasteboard::generalPasteboard()->writeSelection(selection.get(), canSmartCopyOrDelete(), m_frame); + didWriteSelectionToPasteboard(); + deleteSelectionWithSmartDelete(canSmartCopyOrDelete()); + } +} + +void Editor::copy() +{ + if (tryDHTMLCopy()) + return; // DHTML did the whole operation + if (!canCopy()) { + systemBeep(); + return; + } + + if (isNodeInTextFormControl(m_frame->selection()->start().node())) + Pasteboard::generalPasteboard()->writePlainText(selectedText()); + else { + Document* document = m_frame->document(); + if (HTMLImageElement* imageElement = imageElementFromImageDocument(document)) + Pasteboard::generalPasteboard()->writeImage(imageElement, document->url(), document->title()); + else + Pasteboard::generalPasteboard()->writeSelection(selectedRange().get(), canSmartCopyOrDelete(), m_frame); + } + + didWriteSelectionToPasteboard(); +} + +void Editor::paste() +{ + ASSERT(m_frame->document()); + if (tryDHTMLPaste()) + return; // DHTML did the whole operation + if (!canPaste()) + return; +#if REMOVE_MARKERS_UPON_EDITING + removeSpellAndCorrectionMarkersFromWordsToBeEdited(false); +#endif + CachedResourceLoader* loader = m_frame->document()->cachedResourceLoader(); + loader->setAllowStaleResources(true); + if (m_frame->selection()->isContentRichlyEditable()) + pasteWithPasteboard(Pasteboard::generalPasteboard(), true); + else + pasteAsPlainTextWithPasteboard(Pasteboard::generalPasteboard()); + loader->setAllowStaleResources(false); +} + +void Editor::pasteAsPlainText() +{ + if (tryDHTMLPaste()) + return; + if (!canPaste()) + return; +#if REMOVE_MARKERS_UPON_EDITING + removeSpellAndCorrectionMarkersFromWordsToBeEdited(false); +#endif + pasteAsPlainTextWithPasteboard(Pasteboard::generalPasteboard()); +} + +void Editor::performDelete() +{ + if (!canDelete()) { + systemBeep(); + return; + } + + addToKillRing(selectedRange().get(), false); + deleteSelectionWithSmartDelete(canSmartCopyOrDelete()); + + // clear the "start new kill ring sequence" setting, because it was set to true + // when the selection was updated by deleting the range + setStartNewKillRingSequence(false); +} + +void Editor::copyURL(const KURL& url, const String& title) +{ + Pasteboard::generalPasteboard()->writeURL(url, title, m_frame); +} + +void Editor::copyImage(const HitTestResult& result) +{ + KURL url = result.absoluteLinkURL(); + if (url.isEmpty()) + url = result.absoluteImageURL(); + + Pasteboard::generalPasteboard()->writeImage(result.innerNonSharedNode(), url, result.altDisplayString()); +} + +bool Editor::isContinuousSpellCheckingEnabled() +{ + return client() && client()->isContinuousSpellCheckingEnabled(); +} + +void Editor::toggleContinuousSpellChecking() +{ + if (client()) + client()->toggleContinuousSpellChecking(); +} + +bool Editor::isGrammarCheckingEnabled() +{ + return client() && client()->isGrammarCheckingEnabled(); +} + +void Editor::toggleGrammarChecking() +{ + if (client()) + client()->toggleGrammarChecking(); +} + +int Editor::spellCheckerDocumentTag() +{ + return client() ? client()->spellCheckerDocumentTag() : 0; +} + +#if PLATFORM(MAC) && !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD) + +void Editor::uppercaseWord() +{ + if (client()) + client()->uppercaseWord(); +} + +void Editor::lowercaseWord() +{ + if (client()) + client()->lowercaseWord(); +} + +void Editor::capitalizeWord() +{ + if (client()) + client()->capitalizeWord(); +} + +void Editor::showSubstitutionsPanel() +{ + if (!client()) { + LOG_ERROR("No NSSpellChecker"); + return; + } + + if (client()->substitutionsPanelIsShowing()) { + client()->showSubstitutionsPanel(false); + return; + } + client()->showSubstitutionsPanel(true); +} + +bool Editor::substitutionsPanelIsShowing() +{ + if (!client()) + return false; + return client()->substitutionsPanelIsShowing(); +} + +void Editor::toggleSmartInsertDelete() +{ + if (client()) + client()->toggleSmartInsertDelete(); +} + +bool Editor::isAutomaticQuoteSubstitutionEnabled() +{ + return client() && client()->isAutomaticQuoteSubstitutionEnabled(); +} + +void Editor::toggleAutomaticQuoteSubstitution() +{ + if (client()) + client()->toggleAutomaticQuoteSubstitution(); +} + +bool Editor::isAutomaticLinkDetectionEnabled() +{ + return client() && client()->isAutomaticLinkDetectionEnabled(); +} + +void Editor::toggleAutomaticLinkDetection() +{ + if (client()) + client()->toggleAutomaticLinkDetection(); +} + +bool Editor::isAutomaticDashSubstitutionEnabled() +{ + return client() && client()->isAutomaticDashSubstitutionEnabled(); +} + +void Editor::toggleAutomaticDashSubstitution() +{ + if (client()) + client()->toggleAutomaticDashSubstitution(); +} + +bool Editor::isAutomaticTextReplacementEnabled() +{ + return client() && client()->isAutomaticTextReplacementEnabled(); +} + +void Editor::toggleAutomaticTextReplacement() +{ + if (client()) + client()->toggleAutomaticTextReplacement(); +} + +bool Editor::isAutomaticSpellingCorrectionEnabled() +{ + return client() && client()->isAutomaticSpellingCorrectionEnabled(); +} + +void Editor::toggleAutomaticSpellingCorrection() +{ + if (client()) + client()->toggleAutomaticSpellingCorrection(); +} + +#endif + +bool Editor::shouldEndEditing(Range* range) +{ + return client() && client()->shouldEndEditing(range); +} + +bool Editor::shouldBeginEditing(Range* range) +{ + return client() && client()->shouldBeginEditing(range); +} + +void Editor::clearUndoRedoOperations() +{ + if (client()) + client()->clearUndoRedoOperations(); +} + +bool Editor::canUndo() +{ + return client() && client()->canUndo(); +} + +void Editor::undo() +{ + if (client()) + client()->undo(); +} + +bool Editor::canRedo() +{ + return client() && client()->canRedo(); +} + +void Editor::redo() +{ + if (client()) + client()->redo(); +} + +void Editor::didBeginEditing() +{ + if (client()) + client()->didBeginEditing(); +} + +void Editor::didEndEditing() +{ + if (client()) + client()->didEndEditing(); +} + +void Editor::didWriteSelectionToPasteboard() +{ + if (client()) + client()->didWriteSelectionToPasteboard(); +} + +void Editor::toggleBold() +{ + command("ToggleBold").execute(); +} + +void Editor::toggleUnderline() +{ + command("ToggleUnderline").execute(); +} + +void Editor::setBaseWritingDirection(WritingDirection direction) +{ + Node* focusedNode = frame()->document()->focusedNode(); + if (focusedNode && (focusedNode->hasTagName(textareaTag) || (focusedNode->hasTagName(inputTag) && static_cast<HTMLInputElement*>(focusedNode)->isTextField()))) { + if (direction == NaturalWritingDirection) + return; + static_cast<HTMLElement*>(focusedNode)->setAttribute(dirAttr, direction == LeftToRightWritingDirection ? "ltr" : "rtl"); + frame()->document()->updateStyleIfNeeded(); + return; + } + + RefPtr<CSSMutableStyleDeclaration> style = CSSMutableStyleDeclaration::create(); + style->setProperty(CSSPropertyDirection, direction == LeftToRightWritingDirection ? "ltr" : direction == RightToLeftWritingDirection ? "rtl" : "inherit", false); + applyParagraphStyleToSelection(style.get(), EditActionSetWritingDirection); +} + +void Editor::selectComposition() +{ + RefPtr<Range> range = compositionRange(); + if (!range) + return; + + // The composition can start inside a composed character sequence, so we have to override checks. + // See <http://bugs.webkit.org/show_bug.cgi?id=15781> + VisibleSelection selection; + selection.setWithoutValidation(range->startPosition(), range->endPosition()); + m_frame->selection()->setSelection(selection, false, false); +} + +void Editor::confirmComposition() +{ + if (!m_compositionNode) + return; + confirmComposition(m_compositionNode->data().substring(m_compositionStart, m_compositionEnd - m_compositionStart), false); +} + +void Editor::confirmCompositionWithoutDisturbingSelection() +{ + if (!m_compositionNode) + return; + confirmComposition(m_compositionNode->data().substring(m_compositionStart, m_compositionEnd - m_compositionStart), true); +} + +void Editor::confirmComposition(const String& text) +{ + confirmComposition(text, false); +} + +void Editor::confirmComposition(const String& text, bool preserveSelection) +{ + UserTypingGestureIndicator typingGestureIndicator(m_frame); + + setIgnoreCompositionSelectionChange(true); + + VisibleSelection oldSelection = m_frame->selection()->selection(); + + selectComposition(); + + if (m_frame->selection()->isNone()) { + setIgnoreCompositionSelectionChange(false); + return; + } + + // Dispatch a compositionend event to the focused node. + // We should send this event before sending a TextEvent as written in Section 6.2.2 and 6.2.3 of + // the DOM Event specification. + Node* target = m_frame->document()->focusedNode(); + if (target) { + RefPtr<CompositionEvent> event = CompositionEvent::create(eventNames().compositionendEvent, m_frame->domWindow(), text); + ExceptionCode ec = 0; + target->dispatchEvent(event, ec); + } + + // If text is empty, then delete the old composition here. If text is non-empty, InsertTextCommand::input + // will delete the old composition with an optimized replace operation. + if (text.isEmpty()) + TypingCommand::deleteSelection(m_frame->document(), false); + + m_compositionNode = 0; + m_customCompositionUnderlines.clear(); + + insertText(text, 0); + + if (preserveSelection) { + m_frame->selection()->setSelection(oldSelection, false, false); + // An open typing command that disagrees about current selection would cause issues with typing later on. + TypingCommand::closeTyping(m_lastEditCommand.get()); + } + + setIgnoreCompositionSelectionChange(false); +} + +void Editor::setComposition(const String& text, const Vector<CompositionUnderline>& underlines, unsigned selectionStart, unsigned selectionEnd) +{ + UserTypingGestureIndicator typingGestureIndicator(m_frame); + + setIgnoreCompositionSelectionChange(true); + + // Updates styles before setting selection for composition to prevent + // inserting the previous composition text into text nodes oddly. + // See https://bugs.webkit.org/show_bug.cgi?id=46868 + m_frame->document()->updateStyleIfNeeded(); + + selectComposition(); + + if (m_frame->selection()->isNone()) { + setIgnoreCompositionSelectionChange(false); + return; + } + + Node* target = m_frame->document()->focusedNode(); + if (target) { + // Dispatch an appropriate composition event to the focused node. + // We check the composition status and choose an appropriate composition event since this + // function is used for three purposes: + // 1. Starting a new composition. + // Send a compositionstart event when this function creates a new composition node, i.e. + // m_compositionNode == 0 && !text.isEmpty(). + // 2. Updating the existing composition node. + // Send a compositionupdate event when this function updates the existing composition + // node, i.e. m_compositionNode != 0 && !text.isEmpty(). + // 3. Canceling the ongoing composition. + // Send a compositionend event when function deletes the existing composition node, i.e. + // m_compositionNode != 0 && test.isEmpty(). + RefPtr<CompositionEvent> event; + if (!m_compositionNode) { + // We should send a compositionstart event only when the given text is not empty because this + // function doesn't create a composition node when the text is empty. + if (!text.isEmpty()) + event = CompositionEvent::create(eventNames().compositionstartEvent, m_frame->domWindow(), text); + } else { + if (!text.isEmpty()) + event = CompositionEvent::create(eventNames().compositionupdateEvent, m_frame->domWindow(), text); + else + event = CompositionEvent::create(eventNames().compositionendEvent, m_frame->domWindow(), text); + } + ExceptionCode ec = 0; + if (event.get()) + target->dispatchEvent(event, ec); + } + + // If text is empty, then delete the old composition here. If text is non-empty, InsertTextCommand::input + // will delete the old composition with an optimized replace operation. + if (text.isEmpty()) + TypingCommand::deleteSelection(m_frame->document(), false); + + m_compositionNode = 0; + m_customCompositionUnderlines.clear(); + + if (!text.isEmpty()) { + TypingCommand::insertText(m_frame->document(), text, true, true); + + // Find out what node has the composition now. + Position base = m_frame->selection()->base().downstream(); + Position extent = m_frame->selection()->extent(); + Node* baseNode = base.node(); + unsigned baseOffset = base.deprecatedEditingOffset(); + Node* extentNode = extent.node(); + unsigned extentOffset = extent.deprecatedEditingOffset(); + + if (baseNode && baseNode == extentNode && baseNode->isTextNode() && baseOffset + text.length() == extentOffset) { + m_compositionNode = static_cast<Text*>(baseNode); + m_compositionStart = baseOffset; + m_compositionEnd = extentOffset; + m_customCompositionUnderlines = underlines; + size_t numUnderlines = m_customCompositionUnderlines.size(); + for (size_t i = 0; i < numUnderlines; ++i) { + m_customCompositionUnderlines[i].startOffset += baseOffset; + m_customCompositionUnderlines[i].endOffset += baseOffset; + } + if (baseNode->renderer()) + baseNode->renderer()->repaint(); + + unsigned start = min(baseOffset + selectionStart, extentOffset); + unsigned end = min(max(start, baseOffset + selectionEnd), extentOffset); + RefPtr<Range> selectedRange = Range::create(baseNode->document(), baseNode, start, baseNode, end); + m_frame->selection()->setSelectedRange(selectedRange.get(), DOWNSTREAM, false); + } + } + + setIgnoreCompositionSelectionChange(false); +} + +void Editor::ignoreSpelling() +{ + if (!client()) + return; + + RefPtr<Range> selectedRange = frame()->selection()->toNormalizedRange(); + if (selectedRange) + frame()->document()->markers()->removeMarkers(selectedRange.get(), DocumentMarker::Spelling); + + String text = selectedText(); + ASSERT(text.length()); + client()->ignoreWordInSpellDocument(text); +} + +void Editor::learnSpelling() +{ + if (!client()) + return; + + // FIXME: We don't call this on the Mac, and it should remove misspelling markers around the + // learned word, see <rdar://problem/5396072>. + + String text = selectedText(); + ASSERT(text.length()); + client()->learnWord(text); +} + +void Editor::advanceToNextMisspelling(bool startBeforeSelection) +{ + ExceptionCode ec = 0; + + // The basic approach is to search in two phases - from the selection end to the end of the doc, and + // then we wrap and search from the doc start to (approximately) where we started. + + // Start at the end of the selection, search to edge of document. Starting at the selection end makes + // repeated "check spelling" commands work. + VisibleSelection selection(frame()->selection()->selection()); + RefPtr<Range> spellingSearchRange(rangeOfContents(frame()->document())); + + bool startedWithSelection = false; + if (selection.start().node()) { + startedWithSelection = true; + if (startBeforeSelection) { + VisiblePosition start(selection.visibleStart()); + // We match AppKit's rule: Start 1 character before the selection. + VisiblePosition oneBeforeStart = start.previous(); + setStart(spellingSearchRange.get(), oneBeforeStart.isNotNull() ? oneBeforeStart : start); + } else + setStart(spellingSearchRange.get(), selection.visibleEnd()); + } + + Position position = spellingSearchRange->startPosition(); + if (!isEditablePosition(position)) { + // This shouldn't happen in very often because the Spelling menu items aren't enabled unless the + // selection is editable. + // This can happen in Mail for a mix of non-editable and editable content (like Stationary), + // when spell checking the whole document before sending the message. + // In that case the document might not be editable, but there are editable pockets that need to be spell checked. + + position = firstEditablePositionAfterPositionInRoot(position, frame()->document()->documentElement()).deepEquivalent(); + if (position.isNull()) + return; + + Position rangeCompliantPosition = rangeCompliantEquivalent(position); + spellingSearchRange->setStart(rangeCompliantPosition.node(), rangeCompliantPosition.deprecatedEditingOffset(), ec); + startedWithSelection = false; // won't need to wrap + } + + // topNode defines the whole range we want to operate on + Node* topNode = highestEditableRoot(position); + // FIXME: lastOffsetForEditing() is wrong here if editingIgnoresContent(highestEditableRoot()) returns true (e.g. a <table>) + spellingSearchRange->setEnd(topNode, lastOffsetForEditing(topNode), ec); + + // If spellingSearchRange starts in the middle of a word, advance to the next word so we start checking + // at a word boundary. Going back by one char and then forward by a word does the trick. + if (startedWithSelection) { + VisiblePosition oneBeforeStart = startVisiblePosition(spellingSearchRange.get(), DOWNSTREAM).previous(); + if (oneBeforeStart.isNotNull()) + setStart(spellingSearchRange.get(), endOfWord(oneBeforeStart)); + // else we were already at the start of the editable node + } + + if (spellingSearchRange->collapsed(ec)) + return; // nothing to search in + + // Get the spell checker if it is available + if (!client()) + return; + + // We go to the end of our first range instead of the start of it, just to be sure + // we don't get foiled by any word boundary problems at the start. It means we might + // do a tiny bit more searching. + Node* searchEndNodeAfterWrap = spellingSearchRange->endContainer(ec); + int searchEndOffsetAfterWrap = spellingSearchRange->endOffset(ec); + + int misspellingOffset = 0; +#if PLATFORM(MAC) && !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD) + RefPtr<Range> grammarSearchRange = spellingSearchRange->cloneRange(ec); + String misspelledWord; + String badGrammarPhrase; + int grammarPhraseOffset = 0; + bool isSpelling = true; + int foundOffset = 0; + GrammarDetail grammarDetail; + String foundItem = TextCheckingHelper(client(), spellingSearchRange).findFirstMisspellingOrBadGrammar(isGrammarCheckingEnabled(), isSpelling, foundOffset, grammarDetail); + if (isSpelling) { + misspelledWord = foundItem; + misspellingOffset = foundOffset; + } else { + badGrammarPhrase = foundItem; + grammarPhraseOffset = foundOffset; + } +#else + RefPtr<Range> firstMisspellingRange; + String misspelledWord = TextCheckingHelper(client(), spellingSearchRange).findFirstMisspelling(misspellingOffset, false, firstMisspellingRange); + String badGrammarPhrase; + +#ifndef BUILDING_ON_TIGER + int grammarPhraseOffset = 0; + GrammarDetail grammarDetail; + + // Search for bad grammar that occurs prior to the next misspelled word (if any) + RefPtr<Range> grammarSearchRange = spellingSearchRange->cloneRange(ec); + if (!misspelledWord.isEmpty()) { + // Stop looking at start of next misspelled word + CharacterIterator chars(grammarSearchRange.get()); + chars.advance(misspellingOffset); + grammarSearchRange->setEnd(chars.range()->startContainer(ec), chars.range()->startOffset(ec), ec); + } + + if (isGrammarCheckingEnabled()) + badGrammarPhrase = TextCheckingHelper(client(), grammarSearchRange).findFirstBadGrammar(grammarDetail, grammarPhraseOffset, false); +#endif +#endif + + // If we found neither bad grammar nor a misspelled word, wrap and try again (but don't bother if we started at the beginning of the + // block rather than at a selection). + if (startedWithSelection && !misspelledWord && !badGrammarPhrase) { + spellingSearchRange->setStart(topNode, 0, ec); + // going until the end of the very first chunk we tested is far enough + spellingSearchRange->setEnd(searchEndNodeAfterWrap, searchEndOffsetAfterWrap, ec); + +#if PLATFORM(MAC) && !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD) + grammarSearchRange = spellingSearchRange->cloneRange(ec); + foundItem = TextCheckingHelper(client(), spellingSearchRange).findFirstMisspellingOrBadGrammar(isGrammarCheckingEnabled(), isSpelling, foundOffset, grammarDetail); + if (isSpelling) { + misspelledWord = foundItem; + misspellingOffset = foundOffset; + } else { + badGrammarPhrase = foundItem; + grammarPhraseOffset = foundOffset; + } +#else + misspelledWord = TextCheckingHelper(client(), spellingSearchRange).findFirstMisspelling(misspellingOffset, false, firstMisspellingRange); + +#ifndef BUILDING_ON_TIGER + grammarSearchRange = spellingSearchRange->cloneRange(ec); + if (!misspelledWord.isEmpty()) { + // Stop looking at start of next misspelled word + CharacterIterator chars(grammarSearchRange.get()); + chars.advance(misspellingOffset); + grammarSearchRange->setEnd(chars.range()->startContainer(ec), chars.range()->startOffset(ec), ec); + } + + if (isGrammarCheckingEnabled()) + badGrammarPhrase = TextCheckingHelper(client(), grammarSearchRange).findFirstBadGrammar(grammarDetail, grammarPhraseOffset, false); +#endif +#endif + } + + if (!badGrammarPhrase.isEmpty()) { +#ifdef BUILDING_ON_TIGER + ASSERT_NOT_REACHED(); +#else + // We found bad grammar. Since we only searched for bad grammar up to the first misspelled word, the bad grammar + // takes precedence and we ignore any potential misspelled word. Select the grammar detail, update the spelling + // panel, and store a marker so we draw the green squiggle later. + + ASSERT(badGrammarPhrase.length() > 0); + ASSERT(grammarDetail.location != -1 && grammarDetail.length > 0); + + // FIXME 4859190: This gets confused with doubled punctuation at the end of a paragraph + RefPtr<Range> badGrammarRange = TextIterator::subrange(grammarSearchRange.get(), grammarPhraseOffset + grammarDetail.location, grammarDetail.length); + frame()->selection()->setSelection(VisibleSelection(badGrammarRange.get(), SEL_DEFAULT_AFFINITY)); + frame()->selection()->revealSelection(); + + client()->updateSpellingUIWithGrammarString(badGrammarPhrase, grammarDetail); + frame()->document()->markers()->addMarker(badGrammarRange.get(), DocumentMarker::Grammar, grammarDetail.userDescription); +#endif + } else if (!misspelledWord.isEmpty()) { + // We found a misspelling, but not any earlier bad grammar. Select the misspelling, update the spelling panel, and store + // a marker so we draw the red squiggle later. + + RefPtr<Range> misspellingRange = TextIterator::subrange(spellingSearchRange.get(), misspellingOffset, misspelledWord.length()); + frame()->selection()->setSelection(VisibleSelection(misspellingRange.get(), DOWNSTREAM)); + frame()->selection()->revealSelection(); + + client()->updateSpellingUIWithMisspelledWord(misspelledWord); + frame()->document()->markers()->addMarker(misspellingRange.get(), DocumentMarker::Spelling); + } +} + +bool Editor::isSelectionMisspelled() +{ + String selectedString = selectedText(); + int length = selectedString.length(); + if (!length) + return false; + + if (!client()) + return false; + + int misspellingLocation = -1; + int misspellingLength = 0; + client()->checkSpellingOfString(selectedString.characters(), length, &misspellingLocation, &misspellingLength); + + // The selection only counts as misspelled if the selected text is exactly one misspelled word + if (misspellingLength != length) + return false; + + // Update the spelling panel to be displaying this error (whether or not the spelling panel is on screen). + // This is necessary to make a subsequent call to [NSSpellChecker ignoreWord:inSpellDocumentWithTag:] work + // correctly; that call behaves differently based on whether the spelling panel is displaying a misspelling + // or a grammar error. + client()->updateSpellingUIWithMisspelledWord(selectedString); + + return true; +} + +bool Editor::isSelectionUngrammatical() +{ +#ifdef BUILDING_ON_TIGER + return false; +#else + Vector<String> ignoredGuesses; + return TextCheckingHelper(client(), frame()->selection()->toNormalizedRange()).isUngrammatical(ignoredGuesses); +#endif +} + +Vector<String> Editor::guessesForUngrammaticalSelection() +{ +#ifdef BUILDING_ON_TIGER + return Vector<String>(); +#else + Vector<String> guesses; + // Ignore the result of isUngrammatical; we just want the guesses, whether or not there are any + TextCheckingHelper(client(), frame()->selection()->toNormalizedRange()).isUngrammatical(guesses); + return guesses; +#endif +} + +Vector<String> Editor::guessesForMisspelledSelection() +{ + String selectedString = selectedText(); + ASSERT(selectedString.length()); + + Vector<String> guesses; + if (client()) + client()->getGuessesForWord(selectedString, String(), guesses); + return guesses; +} + +Vector<String> Editor::guessesForMisspelledOrUngrammaticalSelection(bool& misspelled, bool& ungrammatical) +{ +#if PLATFORM(MAC) && !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD) + return TextCheckingHelper(client(), frame()->selection()->toNormalizedRange()).guessesForMisspelledOrUngrammaticalRange(isGrammarCheckingEnabled(), misspelled, ungrammatical); +#else + misspelled = isSelectionMisspelled(); + if (misspelled) { + ungrammatical = false; + return guessesForMisspelledSelection(); + } + if (isGrammarCheckingEnabled() && isSelectionUngrammatical()) { + ungrammatical = true; + return guessesForUngrammaticalSelection(); + } + ungrammatical = false; + return Vector<String>(); +#endif +} + +void Editor::showSpellingGuessPanel() +{ + if (!client()) { + LOG_ERROR("No NSSpellChecker"); + return; + } + +#ifndef BUILDING_ON_TIGER + // Post-Tiger, this menu item is a show/hide toggle, to match AppKit. Leave Tiger behavior alone + // to match rest of OS X. + if (client()->spellingUIIsShowing()) { + client()->showSpellingUI(false); + return; + } +#endif + + advanceToNextMisspelling(true); + client()->showSpellingUI(true); +} + +bool Editor::spellingPanelIsShowing() +{ + if (!client()) + return false; + return client()->spellingUIIsShowing(); +} + +void Editor::clearMisspellingsAndBadGrammar(const VisibleSelection &movingSelection) +{ + RefPtr<Range> selectedRange = movingSelection.toNormalizedRange(); + if (selectedRange) { + frame()->document()->markers()->removeMarkers(selectedRange.get(), DocumentMarker::Spelling); + frame()->document()->markers()->removeMarkers(selectedRange.get(), DocumentMarker::Grammar); + } +} + +void Editor::markMisspellingsAndBadGrammar(const VisibleSelection &movingSelection) +{ + bool markSpelling = isContinuousSpellCheckingEnabled(); + bool markGrammar = markSpelling && isGrammarCheckingEnabled(); + + if (markSpelling) { + RefPtr<Range> unusedFirstMisspellingRange; + markMisspellings(movingSelection, unusedFirstMisspellingRange); + } + + if (markGrammar) + markBadGrammar(movingSelection); +} + +void Editor::markMisspellingsAfterTypingToWord(const VisiblePosition &wordStart, const VisibleSelection& selectionAfterTyping) +{ +#if PLATFORM(MAC) && !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD) +#if SUPPORT_AUTOCORRECTION_PANEL + // Apply pending autocorrection before next round of spell checking. + bool doApplyCorrection = true; + VisiblePosition startOfSelection = selectionAfterTyping.visibleStart(); + VisibleSelection currentWord = VisibleSelection(startOfWord(startOfSelection, LeftWordIfOnBoundary), endOfWord(startOfSelection, RightWordIfOnBoundary)); + if (currentWord.visibleEnd() == startOfSelection) { + String wordText = plainText(currentWord.toNormalizedRange().get()); + if (wordText.length() > 0 && isAmbiguousBoundaryCharacter(wordText[wordText.length() - 1])) + doApplyCorrection = false; + } + if (doApplyCorrection) + dismissCorrectionPanel(ReasonForDismissingCorrectionPanelAccepted); + else + m_correctionPanelInfo.rangeToBeReplaced.clear(); +#else + UNUSED_PARAM(selectionAfterTyping); +#endif + TextCheckingOptions textCheckingOptions = 0; + if (isContinuousSpellCheckingEnabled()) + textCheckingOptions |= MarkSpelling; + + if (isAutomaticQuoteSubstitutionEnabled() + || isAutomaticLinkDetectionEnabled() + || isAutomaticDashSubstitutionEnabled() + || isAutomaticTextReplacementEnabled() + || ((textCheckingOptions & MarkSpelling) && isAutomaticSpellingCorrectionEnabled())) + textCheckingOptions |= PerformReplacement; + + if (!textCheckingOptions & (MarkSpelling | PerformReplacement)) + return; + + if (isGrammarCheckingEnabled()) + textCheckingOptions |= MarkGrammar; + + VisibleSelection adjacentWords = VisibleSelection(startOfWord(wordStart, LeftWordIfOnBoundary), endOfWord(wordStart, RightWordIfOnBoundary)); + if (textCheckingOptions & MarkGrammar) { + VisibleSelection selectedSentence = VisibleSelection(startOfSentence(wordStart), endOfSentence(wordStart)); + markAllMisspellingsAndBadGrammarInRanges(textCheckingOptions, adjacentWords.toNormalizedRange().get(), selectedSentence.toNormalizedRange().get()); + } else { + markAllMisspellingsAndBadGrammarInRanges(textCheckingOptions, adjacentWords.toNormalizedRange().get(), adjacentWords.toNormalizedRange().get()); + } +#else + UNUSED_PARAM(selectionAfterTyping); + if (!isContinuousSpellCheckingEnabled()) + return; + + // Check spelling of one word + RefPtr<Range> misspellingRange; + markMisspellings(VisibleSelection(startOfWord(wordStart, LeftWordIfOnBoundary), endOfWord(wordStart, RightWordIfOnBoundary)), misspellingRange); + + // Autocorrect the misspelled word. + if (!misspellingRange) + return; + + // Get the misspelled word. + const String misspelledWord = plainText(misspellingRange.get()); + String autocorrectedString = client()->getAutoCorrectSuggestionForMisspelledWord(misspelledWord); + + // If autocorrected word is non empty, replace the misspelled word by this word. + if (!autocorrectedString.isEmpty()) { + VisibleSelection newSelection(misspellingRange.get(), DOWNSTREAM); + if (newSelection != frame()->selection()->selection()) { + if (!frame()->selection()->shouldChangeSelection(newSelection)) + return; + frame()->selection()->setSelection(newSelection); + } + + if (!frame()->editor()->shouldInsertText(autocorrectedString, misspellingRange.get(), EditorInsertActionTyped)) + return; + frame()->editor()->replaceSelectionWithText(autocorrectedString, false, false); + + // Reset the charet one character further. + frame()->selection()->moveTo(frame()->selection()->end()); + frame()->selection()->modify(SelectionController::AlterationMove, DirectionForward, CharacterGranularity); + } + + if (!isGrammarCheckingEnabled()) + return; + + // Check grammar of entire sentence + markBadGrammar(VisibleSelection(startOfSentence(wordStart), endOfSentence(wordStart))); +#endif +} + +void Editor::markMisspellingsOrBadGrammar(const VisibleSelection& selection, bool checkSpelling, RefPtr<Range>& firstMisspellingRange) +{ + // This function is called with a selection already expanded to word boundaries. + // Might be nice to assert that here. + + // This function is used only for as-you-type checking, so if that's off we do nothing. Note that + // grammar checking can only be on if spell checking is also on. + if (!isContinuousSpellCheckingEnabled()) + return; + + RefPtr<Range> searchRange(selection.toNormalizedRange()); + if (!searchRange) + return; + + // If we're not in an editable node, bail. + Node* editableNode = searchRange->startContainer(); + if (!editableNode || !editableNode->isContentEditable()) + return; + + if (!isSpellCheckingEnabledFor(editableNode)) + return; + + // Get the spell checker if it is available + if (!client()) + return; + + TextCheckingHelper checker(client(), searchRange); + if (checkSpelling) + checker.markAllMisspellings(firstMisspellingRange); + else { +#ifdef BUILDING_ON_TIGER + ASSERT_NOT_REACHED(); +#else + if (isGrammarCheckingEnabled()) + checker.markAllBadGrammar(); +#endif + } +} + +bool Editor::isSpellCheckingEnabledFor(Node* node) const +{ + if (!node) + return false; + const Element* focusedElement = node->isElementNode() ? toElement(node) : node->parentElement(); + if (!focusedElement) + return false; + return focusedElement->isSpellCheckingEnabled(); +} + +bool Editor::isSpellCheckingEnabledInFocusedNode() const +{ + return isSpellCheckingEnabledFor(m_frame->selection()->start().node()); +} + +void Editor::markMisspellings(const VisibleSelection& selection, RefPtr<Range>& firstMisspellingRange) +{ + markMisspellingsOrBadGrammar(selection, true, firstMisspellingRange); +} + +void Editor::markBadGrammar(const VisibleSelection& selection) +{ +#ifndef BUILDING_ON_TIGER + RefPtr<Range> firstMisspellingRange; + markMisspellingsOrBadGrammar(selection, false, firstMisspellingRange); +#else + UNUSED_PARAM(selection); +#endif +} + +#if PLATFORM(MAC) && !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD) +void Editor::markAllMisspellingsAndBadGrammarInRanges(TextCheckingOptions textCheckingOptions, Range* spellingRange, Range* grammarRange) +{ + bool shouldMarkSpelling = textCheckingOptions & MarkSpelling; + bool shouldMarkGrammar = textCheckingOptions & MarkGrammar; + bool shouldPerformReplacement = textCheckingOptions & PerformReplacement; + bool shouldShowCorrectionPanel = textCheckingOptions & ShowCorrectionPanel; + + // This function is called with selections already expanded to word boundaries. + ExceptionCode ec = 0; + if (!client() || !spellingRange || (shouldMarkGrammar && !grammarRange)) + return; + + // If we're not in an editable node, bail. + Node* editableNode = spellingRange->startContainer(); + if (!editableNode || !editableNode->isContentEditable()) + return; + + if (!isSpellCheckingEnabledFor(editableNode)) + return; + + // Expand the range to encompass entire paragraphs, since text checking needs that much context. + int selectionOffset = 0; + int ambiguousBoundaryOffset = -1; + bool selectionChanged = false; + bool restoreSelectionAfterChange = false; + bool adjustSelectionForParagraphBoundaries = false; + + TextCheckingParagraph spellingParagraph(spellingRange); + TextCheckingParagraph grammarParagraph(shouldMarkGrammar ? grammarRange : 0); + TextCheckingParagraph& paragraph = shouldMarkGrammar ? grammarParagraph : spellingParagraph; + + if (shouldMarkGrammar ? (spellingParagraph.isRangeEmpty() && grammarParagraph.isEmpty()) : spellingParagraph.isEmpty()) + return; + + if (shouldPerformReplacement) { + if (m_frame->selection()->selectionType() == VisibleSelection::CaretSelection) { + // Attempt to save the caret position so we can restore it later if needed + Position caretPosition = m_frame->selection()->end(); + int offset = paragraph.offsetTo(caretPosition, ec); + if (!ec) { + selectionOffset = offset; + restoreSelectionAfterChange = true; + if (selectionOffset > 0 && (selectionOffset > paragraph.textLength() || paragraph.textCharAt(selectionOffset - 1) == newlineCharacter)) + adjustSelectionForParagraphBoundaries = true; + if (selectionOffset > 0 && selectionOffset <= paragraph.textLength() && isAmbiguousBoundaryCharacter(paragraph.textCharAt(selectionOffset - 1))) + ambiguousBoundaryOffset = selectionOffset - 1; + } + } + } + + Vector<TextCheckingResult> results; + uint64_t checkingTypes = 0; + if (shouldMarkSpelling) + checkingTypes |= TextCheckingTypeSpelling; + if (shouldMarkGrammar) + checkingTypes |= TextCheckingTypeGrammar; + if (shouldShowCorrectionPanel) + checkingTypes |= TextCheckingTypeCorrection; + if (shouldPerformReplacement) { + if (isAutomaticLinkDetectionEnabled()) + checkingTypes |= TextCheckingTypeLink; + if (isAutomaticQuoteSubstitutionEnabled()) + checkingTypes |= TextCheckingTypeQuote; + if (isAutomaticDashSubstitutionEnabled()) + checkingTypes |= TextCheckingTypeDash; + if (isAutomaticTextReplacementEnabled()) + checkingTypes |= TextCheckingTypeReplacement; + if (shouldMarkSpelling && isAutomaticSpellingCorrectionEnabled()) + checkingTypes |= TextCheckingTypeCorrection; + } + client()->checkTextOfParagraph(paragraph.textCharacters(), paragraph.textLength(), checkingTypes, results); + +#if SUPPORT_AUTOCORRECTION_PANEL + // If this checking is only for showing correction panel, we shouldn't bother to mark misspellings. + if (shouldShowCorrectionPanel) + shouldMarkSpelling = false; +#endif + + int offsetDueToReplacement = 0; + + for (unsigned i = 0; i < results.size(); i++) { + int spellingRangeEndOffset = spellingParagraph.checkingEnd() + offsetDueToReplacement; + const TextCheckingResult* result = &results[i]; + int resultLocation = result->location + offsetDueToReplacement; + int resultLength = result->length; + bool resultEndsAtAmbiguousBoundary = ambiguousBoundaryOffset >= 0 && resultLocation + resultLength == ambiguousBoundaryOffset; + + // Only mark misspelling if: + // 1. Current text checking isn't done for autocorrection, in which case shouldMarkSpelling is false. + // 2. Result falls within spellingRange. + // 3. The word in question doesn't end at an ambiguous boundary. For instance, we would not mark + // "wouldn'" as misspelled right after apostrophe is typed. + if (shouldMarkSpelling && result->type == TextCheckingTypeSpelling && resultLocation >= spellingParagraph.checkingStart() && resultLocation + resultLength <= spellingRangeEndOffset && !resultEndsAtAmbiguousBoundary) { + ASSERT(resultLength > 0 && resultLocation >= 0); + RefPtr<Range> misspellingRange = spellingParagraph.subrange(resultLocation, resultLength); + misspellingRange->startContainer(ec)->document()->markers()->addMarker(misspellingRange.get(), DocumentMarker::Spelling); + } else if (shouldMarkGrammar && result->type == TextCheckingTypeGrammar && grammarParagraph.checkingRangeCovers(resultLocation, resultLength)) { + ASSERT(resultLength > 0 && resultLocation >= 0); + for (unsigned j = 0; j < result->details.size(); j++) { + const GrammarDetail* detail = &result->details[j]; + ASSERT(detail->length > 0 && detail->location >= 0); + if (grammarParagraph.checkingRangeCovers(resultLocation + detail->location, detail->length)) { + RefPtr<Range> badGrammarRange = grammarParagraph.subrange(resultLocation + detail->location, detail->length); + grammarRange->startContainer(ec)->document()->markers()->addMarker(badGrammarRange.get(), DocumentMarker::Grammar, detail->userDescription); + } + } + } else if ((shouldPerformReplacement || shouldShowCorrectionPanel) && resultLocation + resultLength <= spellingRangeEndOffset && resultLocation + resultLength >= spellingParagraph.checkingStart() + && (result->type == TextCheckingTypeLink + || result->type == TextCheckingTypeQuote + || result->type == TextCheckingTypeDash + || result->type == TextCheckingTypeReplacement + || result->type == TextCheckingTypeCorrection)) { + // In this case the result range just has to touch the spelling range, so we can handle replacing non-word text such as punctuation. + ASSERT(resultLength > 0 && resultLocation >= 0); + + if (shouldShowCorrectionPanel && resultLocation + resultLength < spellingRangeEndOffset) + continue; + + int replacementLength = result->replacement.length(); + + // Apply replacement if: + // 1. The replacement length is non-zero. + // 2. The result doesn't end at an ambiguous boundary. + // (FIXME: this is required until 6853027 is fixed and text checking can do this for us + bool doReplacement = replacementLength > 0 && !resultEndsAtAmbiguousBoundary; + RefPtr<Range> rangeToReplace = paragraph.subrange(resultLocation, resultLength); + VisibleSelection selectionToReplace(rangeToReplace.get(), DOWNSTREAM); + + // adding links should be done only immediately after they are typed + if (result->type == TextCheckingTypeLink && selectionOffset > resultLocation + resultLength + 1) + doReplacement = false; + + // Don't correct spelling in an already-corrected word. + if (doReplacement && result->type == TextCheckingTypeCorrection) { + Node* node = rangeToReplace->startContainer(); + int startOffset = rangeToReplace->startOffset(); + int endOffset = startOffset + replacementLength; + Vector<DocumentMarker> markers = node->document()->markers()->markersForNode(node); + size_t markerCount = markers.size(); + for (size_t i = 0; i < markerCount; ++i) { + const DocumentMarker& marker = markers[i]; + if ((marker.type == DocumentMarker::Replacement || marker.type == DocumentMarker::RejectedCorrection) && static_cast<int>(marker.startOffset) < endOffset && static_cast<int>(marker.endOffset) > startOffset) { + doReplacement = false; + break; + } + if (static_cast<int>(marker.startOffset) >= endOffset) + break; + } + } + if (doReplacement && !shouldShowCorrectionPanel && selectionToReplace != m_frame->selection()->selection()) { + if (m_frame->selection()->shouldChangeSelection(selectionToReplace)) { + m_frame->selection()->setSelection(selectionToReplace); + selectionChanged = true; + } else { + doReplacement = false; + } + } + + String replacedString; + if (doReplacement) { + if (result->type == TextCheckingTypeLink) { + restoreSelectionAfterChange = false; + if (canEditRichly()) + applyCommand(CreateLinkCommand::create(m_frame->document(), result->replacement)); + } else if (canEdit() && shouldInsertText(result->replacement, rangeToReplace.get(), EditorInsertActionTyped)) { + if (result->type == TextCheckingTypeCorrection) + replacedString = plainText(rangeToReplace.get()); +#if SUPPORT_AUTOCORRECTION_PANEL + if (shouldShowCorrectionPanel && resultLocation + resultLength == spellingRangeEndOffset && result->type == TextCheckingTypeCorrection) { + // We only show the correction panel on the last word. + Vector<FloatQuad> textQuads; + rangeToReplace->getBorderAndTextQuads(textQuads); + Vector<FloatQuad>::const_iterator end = textQuads.end(); + FloatRect totalBoundingBox; + for (Vector<FloatQuad>::const_iterator it = textQuads.begin(); it < end; ++it) + totalBoundingBox.unite(it->boundingBox()); + m_correctionPanelInfo.rangeToBeReplaced = rangeToReplace; + m_correctionPanelInfo.replacedString = replacedString; + m_correctionPanelInfo.replacementString = result->replacement; + m_correctionPanelInfo.isActive = true; + client()->showCorrectionPanel(m_correctionPanelInfo.panelType, totalBoundingBox, m_correctionPanelInfo.replacedString, result->replacement, Vector<String>(), this); + doReplacement = false; + } +#endif + if (doReplacement) { + replaceSelectionWithText(result->replacement, false, false); + offsetDueToReplacement += replacementLength - resultLength; + if (resultLocation < selectionOffset) { + selectionOffset += replacementLength - resultLength; + if (ambiguousBoundaryOffset >= 0) + ambiguousBoundaryOffset = selectionOffset - 1; + } + + if (result->type == TextCheckingTypeCorrection) { + // Add a marker so that corrections can easily be undone and won't be re-corrected. + RefPtr<Range> replacedRange = paragraph.subrange(resultLocation, replacementLength); + replacedRange->startContainer()->document()->markers()->addMarker(replacedRange.get(), DocumentMarker::Replacement, replacedString); + replacedRange->startContainer()->document()->markers()->addMarker(replacedRange.get(), DocumentMarker::CorrectionIndicator, replacedString); + } + } + } + } + } + } + + if (selectionChanged) { + // Restore the caret position if we have made any replacements + paragraph.expandRangeToNextEnd(); + if (restoreSelectionAfterChange && selectionOffset >= 0 && selectionOffset <= paragraph.rangeLength()) { + RefPtr<Range> selectionRange = paragraph.subrange(0, selectionOffset); + m_frame->selection()->moveTo(selectionRange->endPosition(), DOWNSTREAM); + if (adjustSelectionForParagraphBoundaries) + m_frame->selection()->modify(SelectionController::AlterationMove, DirectionForward, CharacterGranularity); + } else { + // If this fails for any reason, the fallback is to go one position beyond the last replacement + m_frame->selection()->moveTo(m_frame->selection()->end()); + m_frame->selection()->modify(SelectionController::AlterationMove, DirectionForward, CharacterGranularity); + } + } +} + +void Editor::changeBackToReplacedString(const String& replacedString) +{ + if (replacedString.isEmpty()) + return; + + RefPtr<Range> selection = selectedRange(); + if (!shouldInsertText(replacedString, selection.get(), EditorInsertActionPasted)) + return; + + TextCheckingParagraph paragraph(selection); + replaceSelectionWithText(replacedString, false, false); + RefPtr<Range> changedRange = paragraph.subrange(paragraph.checkingStart(), replacedString.length()); + changedRange->startContainer()->document()->markers()->addMarker(changedRange.get(), DocumentMarker::Replacement, String()); +} + +#endif + +void Editor::markMisspellingsAndBadGrammar(const VisibleSelection& spellingSelection, bool markGrammar, const VisibleSelection& grammarSelection) +{ +#if PLATFORM(MAC) && !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD) + if (!isContinuousSpellCheckingEnabled()) + return; + TextCheckingOptions textCheckingOptions = MarkSpelling; + if (markGrammar && isGrammarCheckingEnabled()) + textCheckingOptions |= MarkGrammar; + markAllMisspellingsAndBadGrammarInRanges(textCheckingOptions, spellingSelection.toNormalizedRange().get(), grammarSelection.toNormalizedRange().get()); +#else + RefPtr<Range> firstMisspellingRange; + markMisspellings(spellingSelection, firstMisspellingRange); + if (markGrammar) + markBadGrammar(grammarSelection); +#endif +} + +void Editor::correctionPanelTimerFired(Timer<Editor>*) +{ +#if SUPPORT_AUTOCORRECTION_PANEL + m_correctionPanelIsDismissedByEditor = false; + switch (m_correctionPanelInfo.panelType) { + case CorrectionPanelInfo::PanelTypeCorrection: { + VisibleSelection selection(frame()->selection()->selection()); + VisiblePosition start(selection.start(), selection.affinity()); + VisiblePosition p = startOfWord(start, LeftWordIfOnBoundary); + VisibleSelection adjacentWords = VisibleSelection(p, start); + markAllMisspellingsAndBadGrammarInRanges(MarkSpelling | ShowCorrectionPanel, adjacentWords.toNormalizedRange().get(), 0); + } + break; + case CorrectionPanelInfo::PanelTypeReversion: { + m_correctionPanelInfo.isActive = true; + m_correctionPanelInfo.replacedString = plainText(m_correctionPanelInfo.rangeToBeReplaced.get()); + client()->showCorrectionPanel(m_correctionPanelInfo.panelType, boundingBoxForRange(m_correctionPanelInfo.rangeToBeReplaced.get()), m_correctionPanelInfo.replacedString, m_correctionPanelInfo.replacementString, Vector<String>(), this); + } + break; + case CorrectionPanelInfo::PanelTypeSpellingSuggestions: { + if (plainText(m_correctionPanelInfo.rangeToBeReplaced.get()) != m_correctionPanelInfo.replacedString) + break; + String paragraphText = plainText(TextCheckingParagraph(m_correctionPanelInfo.rangeToBeReplaced).paragraphRange().get()); + Vector<String> suggestions; + client()->getGuessesForWord(m_correctionPanelInfo.replacedString, paragraphText, suggestions); + if (suggestions.isEmpty()) + break; + String topSuggestion = suggestions.first(); + suggestions.remove(0); + m_correctionPanelInfo.isActive = true; + client()->showCorrectionPanel(m_correctionPanelInfo.panelType, boundingBoxForRange(m_correctionPanelInfo.rangeToBeReplaced.get()), m_correctionPanelInfo.replacedString, topSuggestion, suggestions, this); + } + break; + } +#endif +} + +void Editor::handleCorrectionPanelResult(const String& correction) +{ + Range* replacedRange = m_correctionPanelInfo.rangeToBeReplaced.get(); + if (!replacedRange || m_frame->document() != replacedRange->ownerDocument()) + return; + + String currentWord = plainText(m_correctionPanelInfo.rangeToBeReplaced.get()); + // Check to see if the word we are about to correct has been changed between timer firing and callback being triggered. + if (currentWord != m_correctionPanelInfo.replacedString) + return; + + m_correctionPanelInfo.isActive = false; + + switch (m_correctionPanelInfo.panelType) { + case CorrectionPanelInfo::PanelTypeCorrection: + if (correction.length()) { + m_correctionPanelInfo.replacementString = correction; + applyCorrectionPanelInfo(markerTypesForAutocorrection()); + } else { + if (!m_correctionPanelIsDismissedByEditor) + replacedRange->startContainer()->document()->markers()->addMarker(replacedRange, DocumentMarker::RejectedCorrection, m_correctionPanelInfo.replacedString); + } + break; + case CorrectionPanelInfo::PanelTypeReversion: + case CorrectionPanelInfo::PanelTypeSpellingSuggestions: + if (correction.length()) { + m_correctionPanelInfo.replacementString = correction; + applyCorrectionPanelInfo(markerTypesForReplacement()); + } + break; + } + + m_correctionPanelInfo.rangeToBeReplaced.clear(); +} + +void Editor::startCorrectionPanelTimer(CorrectionPanelInfo::PanelType type) +{ +#if SUPPORT_AUTOCORRECTION_PANEL + const double correctionPanelTimerInterval = 0.3; + if (isAutomaticSpellingCorrectionEnabled()) { + if (type == CorrectionPanelInfo::PanelTypeCorrection) + // If type is PanelTypeReversion, then the new range has been set. So we shouldn't clear it. + m_correctionPanelInfo.rangeToBeReplaced.clear(); + m_correctionPanelInfo.panelType = type; + m_correctionPanelTimer.startOneShot(correctionPanelTimerInterval); + } +#else + UNUSED_PARAM(type); +#endif +} + +void Editor::stopCorrectionPanelTimer() +{ +#if SUPPORT_AUTOCORRECTION_PANEL + m_correctionPanelTimer.stop(); + m_correctionPanelInfo.rangeToBeReplaced.clear(); +#endif +} + +void Editor::handleCancelOperation() +{ +#if SUPPORT_AUTOCORRECTION_PANEL + if (!m_correctionPanelInfo.isActive) + return; + m_correctionPanelInfo.isActive = false; + if (client()) + client()->dismissCorrectionPanel(ReasonForDismissingCorrectionPanelCancelled); +#endif +} + +bool Editor::isShowingCorrectionPanel() +{ +#if SUPPORT_AUTOCORRECTION_PANEL + if (client()) + return client()->isShowingCorrectionPanel(); +#endif + return false; +} + +void Editor::dismissCorrectionPanel(ReasonForDismissingCorrectionPanel reasonForDismissing) +{ +#if SUPPORT_AUTOCORRECTION_PANEL + if (!m_correctionPanelInfo.isActive) + return; + m_correctionPanelInfo.isActive = false; + m_correctionPanelIsDismissedByEditor = true; + if (client()) + client()->dismissCorrectionPanel(reasonForDismissing); +#else + UNUSED_PARAM(reasonForDismissing); +#endif +} +void Editor::removeSpellAndCorrectionMarkersFromWordsToBeEdited(bool doNotRemoveIfSelectionAtWordBoundary) +{ + // We want to remove the markers from a word if an editing command will change the word. This can happen in one of + // several scenarios: + // 1. Insert in the middle of a word. + // 2. Appending non whitespace at the beginning of word. + // 3. Appending non whitespace at the end of word. + // Note that, appending only whitespaces at the beginning or end of word won't change the word, so we don't need to + // remove the markers on that word. + // Of course, if current selection is a range, we potentially will edit two words that fall on the boundaries of + // selection, and remove words between the selection boundaries. + // + VisiblePosition startOfSelection = frame()->selection()->selection().start(); + VisiblePosition endOfSelection = frame()->selection()->selection().end(); + if (startOfSelection.isNull()) + return; + // First word is the word that ends after or on the start of selection. + VisiblePosition startOfFirstWord = startOfWord(startOfSelection, LeftWordIfOnBoundary); + VisiblePosition endOfFirstWord = endOfWord(startOfSelection, LeftWordIfOnBoundary); + // Last word is the word that begins before or on the end of selection + VisiblePosition startOfLastWord = startOfWord(endOfSelection, RightWordIfOnBoundary); + VisiblePosition endOfLastWord = endOfWord(endOfSelection, RightWordIfOnBoundary); + + // This can be the case if the end of selection is at the end of document. + if (endOfLastWord.deepEquivalent().anchorType() != Position::PositionIsOffsetInAnchor) { + startOfLastWord = startOfWord(frame()->selection()->selection().start(), LeftWordIfOnBoundary); + endOfLastWord = endOfWord(frame()->selection()->selection().start(), LeftWordIfOnBoundary); + } + + // If doNotRemoveIfSelectionAtWordBoundary is true, and first word ends at the start of selection, + // we choose next word as the first word. + if (doNotRemoveIfSelectionAtWordBoundary && endOfFirstWord == startOfSelection) { + startOfFirstWord = nextWordPosition(startOfFirstWord); + if (startOfFirstWord == endOfSelection) + return; + endOfFirstWord = endOfWord(startOfFirstWord, RightWordIfOnBoundary); + if (endOfFirstWord.deepEquivalent().anchorType() != Position::PositionIsOffsetInAnchor) + return; + } + + // If doNotRemoveIfSelectionAtWordBoundary is true, and last word begins at the end of selection, + // we choose previous word as the last word. + if (doNotRemoveIfSelectionAtWordBoundary && startOfLastWord == endOfSelection) { + startOfLastWord = previousWordPosition(startOfLastWord); + endOfLastWord = endOfWord(startOfLastWord, RightWordIfOnBoundary); + if (endOfLastWord == startOfFirstWord) + return; + } + + // Now we remove markers on everything between startOfFirstWord and endOfLastWord. + // However, if an autocorrection change a single word to multiple words, we want to remove correction mark from all the + // resulted words even we only edit one of them. For example, assuming autocorrection changes "avantgarde" to "avant + // garde", we will have CorrectionIndicator marker on both words and on the whitespace between them. If we then edit garde, + // we would like to remove the marker from word "avant" and whitespace as well. So we need to get the continous range of + // of marker that contains the word in question, and remove marker on that whole range. + Document* document = m_frame->document(); + RefPtr<Range> wordRange = Range::create(document, startOfFirstWord.deepEquivalent(), endOfLastWord.deepEquivalent()); + RefPtr<Range> rangeOfFirstWord = Range::create(document, startOfFirstWord.deepEquivalent(), endOfFirstWord.deepEquivalent()); + RefPtr<Range> rangeOfLastWord = Range::create(document, startOfLastWord.deepEquivalent(), endOfLastWord.deepEquivalent()); + + typedef pair<RefPtr<Range>, DocumentMarker::MarkerType> RangeMarkerPair; + // It's probably unsafe to remove marker while iterating a vector of markers. So we store the markers and ranges that we want to remove temporarily. Then remove them at the end of function. + // To avoid allocation on the heap, Give markersToRemove a small inline capacity + Vector<RangeMarkerPair, 16> markersToRemove; + for (TextIterator textIterator(wordRange.get()); !textIterator.atEnd(); textIterator.advance()) { + Node* node = textIterator.node(); + if (!node) + continue; + if (node == startOfFirstWord.deepEquivalent().containerNode() || node == endOfLastWord.deepEquivalent().containerNode()) { + // First word and last word can belong to the same node + bool processFirstWord = node == startOfFirstWord.deepEquivalent().containerNode() && document->markers()->hasMarkers(rangeOfFirstWord.get(), DocumentMarker::Spelling | DocumentMarker::CorrectionIndicator); + bool processLastWord = node == endOfLastWord.deepEquivalent().containerNode() && document->markers()->hasMarkers(rangeOfLastWord.get(), DocumentMarker::Spelling | DocumentMarker::CorrectionIndicator); + // Take note on the markers whose range overlaps with the range of the first word or the last word. + Vector<DocumentMarker> markers = document->markers()->markersForNode(node); + for (size_t i = 0; i < markers.size(); ++i) { + DocumentMarker marker = markers[i]; + if (processFirstWord && static_cast<int>(marker.endOffset) > startOfFirstWord.deepEquivalent().offsetInContainerNode() && (marker.type == DocumentMarker::Spelling || marker.type == DocumentMarker::CorrectionIndicator)) { + RefPtr<Range> markerRange = Range::create(document, node, marker.startOffset, node, marker.endOffset); + markersToRemove.append(std::make_pair(markerRange, marker.type)); + } + if (processLastWord && static_cast<int>(marker.startOffset) <= endOfLastWord.deepEquivalent().offsetInContainerNode() && (marker.type == DocumentMarker::Spelling || marker.type == DocumentMarker::CorrectionIndicator)) { + RefPtr<Range> markerRange = Range::create(document, node, marker.startOffset, node, marker.endOffset); + markersToRemove.append(std::make_pair(markerRange, marker.type)); + } + } + } else { + document->markers()->removeMarkers(node, DocumentMarker::Spelling); + document->markers()->removeMarkers(node, DocumentMarker::CorrectionIndicator); + } + } + + // Actually remove the markers. + Vector<RangeMarkerPair>::const_iterator pairEnd = markersToRemove.end(); + for (Vector<RangeMarkerPair>::const_iterator pairIterator = markersToRemove.begin(); pairIterator != pairEnd; ++pairIterator) + document->markers()->removeMarkers(pairIterator->first.get(), pairIterator->second); +} + +void Editor::applyCorrectionPanelInfo(const Vector<DocumentMarker::MarkerType>& markerTypesToAdd) +{ + if (!m_correctionPanelInfo.rangeToBeReplaced) + return; + + ExceptionCode ec = 0; + RefPtr<Range> paragraphRangeContainingCorrection = m_correctionPanelInfo.rangeToBeReplaced->cloneRange(ec); + if (ec) + return; + + setStart(paragraphRangeContainingCorrection.get(), startOfParagraph(m_correctionPanelInfo.rangeToBeReplaced->startPosition())); + setEnd(paragraphRangeContainingCorrection.get(), endOfParagraph(m_correctionPanelInfo.rangeToBeReplaced->endPosition())); + + // After we replace the word at range rangeToBeReplaced, we need to add markers to that range. + // However, once the replacement took place, the value of rangeToBeReplaced is not valid anymore. + // So before we carry out the replacement, we need to store the start position of rangeToBeReplaced + // relative to the start position of the containing paragraph. We use correctionStartOffsetInParagraph + // to store this value. In order to obtain this offset, we need to first create a range + // which spans from the start of paragraph to the start position of rangeToBeReplaced. + RefPtr<Range> correctionStartOffsetInParagraphAsRange = Range::create(paragraphRangeContainingCorrection->startContainer(ec)->document(), paragraphRangeContainingCorrection->startPosition(), paragraphRangeContainingCorrection->startPosition()); + if (ec) + return; + + Position startPositionOfRangeToBeReplaced = m_correctionPanelInfo.rangeToBeReplaced->startPosition(); + correctionStartOffsetInParagraphAsRange->setEnd(startPositionOfRangeToBeReplaced.containerNode(), startPositionOfRangeToBeReplaced.computeOffsetInContainerNode(), ec); + if (ec) + return; + + // Take note of the location of autocorrection so that we can add marker after the replacement took place. + int correctionStartOffsetInParagraph = TextIterator::rangeLength(correctionStartOffsetInParagraphAsRange.get()); + Position caretPosition = m_frame->selection()->selection().end(); + + // Clone the range, since the caller of this method may want to keep the original range around. + RefPtr<Range> rangeToBeReplaced = m_correctionPanelInfo.rangeToBeReplaced->cloneRange(ec); + VisibleSelection selectionToReplace(rangeToBeReplaced.get(), DOWNSTREAM); + if (m_frame->selection()->shouldChangeSelection(selectionToReplace)) { + m_frame->selection()->setSelection(selectionToReplace); + replaceSelectionWithText(m_correctionPanelInfo.replacementString, false, false); + caretPosition.moveToOffset(caretPosition.offsetInContainerNode() + m_correctionPanelInfo.replacementString.length() - m_correctionPanelInfo.replacedString.length()); + setEnd(paragraphRangeContainingCorrection.get(), endOfParagraph(caretPosition)); + RefPtr<Range> replacementRange = TextIterator::subrange(paragraphRangeContainingCorrection.get(), correctionStartOffsetInParagraph, m_correctionPanelInfo.replacementString.length()); + DocumentMarkerController* markers = replacementRange->startContainer()->document()->markers(); + size_t size = markerTypesToAdd.size(); + for (size_t i = 0; i < size; ++i) + markers->addMarker(replacementRange.get(), markerTypesToAdd[i], m_correctionPanelInfo.replacementString); + m_frame->selection()->moveTo(caretPosition, false); + } +} + +PassRefPtr<Range> Editor::rangeForPoint(const IntPoint& windowPoint) +{ + Document* document = m_frame->documentAtPoint(windowPoint); + if (!document) + return 0; + + Frame* frame = document->frame(); + ASSERT(frame); + FrameView* frameView = frame->view(); + if (!frameView) + return 0; + IntPoint framePoint = frameView->windowToContents(windowPoint); + VisibleSelection selection(frame->visiblePositionForPoint(framePoint)); + return avoidIntersectionWithNode(selection.toNormalizedRange().get(), m_deleteButtonController->containerElement()); +} + +void Editor::revealSelectionAfterEditingOperation() +{ + if (m_ignoreCompositionSelectionChange) + return; + + m_frame->selection()->revealSelection(ScrollAlignment::alignToEdgeIfNeeded); +} + +void Editor::setIgnoreCompositionSelectionChange(bool ignore) +{ + if (m_ignoreCompositionSelectionChange == ignore) + return; + + m_ignoreCompositionSelectionChange = ignore; + if (!ignore) + revealSelectionAfterEditingOperation(); +} + +PassRefPtr<Range> Editor::compositionRange() const +{ + if (!m_compositionNode) + return 0; + unsigned length = m_compositionNode->length(); + unsigned start = min(m_compositionStart, length); + unsigned end = min(max(start, m_compositionEnd), length); + if (start >= end) + return 0; + return Range::create(m_compositionNode->document(), m_compositionNode.get(), start, m_compositionNode.get(), end); +} + +bool Editor::getCompositionSelection(unsigned& selectionStart, unsigned& selectionEnd) const +{ + if (!m_compositionNode) + return false; + Position start = m_frame->selection()->start(); + if (start.node() != m_compositionNode) + return false; + Position end = m_frame->selection()->end(); + if (end.node() != m_compositionNode) + return false; + + if (static_cast<unsigned>(start.deprecatedEditingOffset()) < m_compositionStart) + return false; + if (static_cast<unsigned>(end.deprecatedEditingOffset()) > m_compositionEnd) + return false; + + selectionStart = start.deprecatedEditingOffset() - m_compositionStart; + selectionEnd = start.deprecatedEditingOffset() - m_compositionEnd; + return true; +} + +void Editor::transpose() +{ + if (!canEdit()) + return; + + VisibleSelection selection = m_frame->selection()->selection(); + if (!selection.isCaret()) + return; + + // Make a selection that goes back one character and forward two characters. + VisiblePosition caret = selection.visibleStart(); + VisiblePosition next = isEndOfParagraph(caret) ? caret : caret.next(); + VisiblePosition previous = next.previous(); + if (next == previous) + return; + previous = previous.previous(); + if (!inSameParagraph(next, previous)) + return; + RefPtr<Range> range = makeRange(previous, next); + if (!range) + return; + VisibleSelection newSelection(range.get(), DOWNSTREAM); + + // Transpose the two characters. + String text = plainText(range.get()); + if (text.length() != 2) + return; + String transposed = text.right(1) + text.left(1); + + // Select the two characters. + if (newSelection != m_frame->selection()->selection()) { + if (!m_frame->selection()->shouldChangeSelection(newSelection)) + return; + m_frame->selection()->setSelection(newSelection); + } + + // Insert the transposed characters. + if (!shouldInsertText(transposed, range.get(), EditorInsertActionTyped)) + return; + replaceSelectionWithText(transposed, false, false); +} + +void Editor::addToKillRing(Range* range, bool prepend) +{ + if (m_shouldStartNewKillRingSequence) + killRing()->startNewSequence(); + + String text = plainText(range); + if (prepend) + killRing()->prepend(text); + else + killRing()->append(text); + m_shouldStartNewKillRingSequence = false; +} + +bool Editor::insideVisibleArea(const IntPoint& point) const +{ + if (m_frame->excludeFromTextSearch()) + return false; + + // Right now, we only check the visibility of a point for disconnected frames. For all other + // frames, we assume visibility. + Frame* frame = m_frame->isDisconnected() ? m_frame : m_frame->tree()->top(true); + if (!frame->isDisconnected()) + return true; + + RenderPart* renderer = frame->ownerRenderer(); + if (!renderer) + return false; + + RenderBlock* container = renderer->containingBlock(); + if (!(container->style()->overflowX() == OHIDDEN || container->style()->overflowY() == OHIDDEN)) + return true; + + IntRect rectInPageCoords = container->overflowClipRect(0, 0); + IntRect rectInFrameCoords = IntRect(renderer->x() * -1, renderer->y() * -1, + rectInPageCoords.width(), rectInPageCoords.height()); + + return rectInFrameCoords.contains(point); +} + +bool Editor::insideVisibleArea(Range* range) const +{ + if (!range) + return true; + + if (m_frame->excludeFromTextSearch()) + return false; + + // Right now, we only check the visibility of a range for disconnected frames. For all other + // frames, we assume visibility. + Frame* frame = m_frame->isDisconnected() ? m_frame : m_frame->tree()->top(true); + if (!frame->isDisconnected()) + return true; + + RenderPart* renderer = frame->ownerRenderer(); + if (!renderer) + return false; + + RenderBlock* container = renderer->containingBlock(); + if (!(container->style()->overflowX() == OHIDDEN || container->style()->overflowY() == OHIDDEN)) + return true; + + IntRect rectInPageCoords = container->overflowClipRect(0, 0); + IntRect rectInFrameCoords = IntRect(renderer->x() * -1, renderer->y() * -1, + rectInPageCoords.width(), rectInPageCoords.height()); + IntRect resultRect = range->boundingBox(); + + return rectInFrameCoords.contains(resultRect); +} + +PassRefPtr<Range> Editor::firstVisibleRange(const String& target, FindOptions options) +{ + RefPtr<Range> searchRange(rangeOfContents(m_frame->document())); + RefPtr<Range> resultRange = findPlainText(searchRange.get(), target, options & ~Backwards); + ExceptionCode ec = 0; + + while (!insideVisibleArea(resultRange.get())) { + searchRange->setStartAfter(resultRange->endContainer(), ec); + if (searchRange->startContainer() == searchRange->endContainer()) + return Range::create(m_frame->document()); + resultRange = findPlainText(searchRange.get(), target, options & ~Backwards); + } + + return resultRange; +} + +PassRefPtr<Range> Editor::lastVisibleRange(const String& target, FindOptions options) +{ + RefPtr<Range> searchRange(rangeOfContents(m_frame->document())); + RefPtr<Range> resultRange = findPlainText(searchRange.get(), target, options | Backwards); + ExceptionCode ec = 0; + + while (!insideVisibleArea(resultRange.get())) { + searchRange->setEndBefore(resultRange->startContainer(), ec); + if (searchRange->startContainer() == searchRange->endContainer()) + return Range::create(m_frame->document()); + resultRange = findPlainText(searchRange.get(), target, options | Backwards); + } + + return resultRange; +} + +PassRefPtr<Range> Editor::nextVisibleRange(Range* currentRange, const String& target, FindOptions options) +{ + if (m_frame->excludeFromTextSearch()) + return Range::create(m_frame->document()); + + RefPtr<Range> resultRange = currentRange; + RefPtr<Range> searchRange(rangeOfContents(m_frame->document())); + ExceptionCode ec = 0; + bool forward = !(options & Backwards); + for ( ; !insideVisibleArea(resultRange.get()); resultRange = findPlainText(searchRange.get(), target, options)) { + if (resultRange->collapsed(ec)) { + if (!resultRange->startContainer()->isInShadowTree()) + break; + searchRange = rangeOfContents(m_frame->document()); + if (forward) + searchRange->setStartAfter(resultRange->startContainer()->shadowAncestorNode(), ec); + else + searchRange->setEndBefore(resultRange->startContainer()->shadowAncestorNode(), ec); + continue; + } + + if (forward) + searchRange->setStartAfter(resultRange->endContainer(), ec); + else + searchRange->setEndBefore(resultRange->startContainer(), ec); + + Node* shadowTreeRoot = searchRange->shadowTreeRootNode(); + if (searchRange->collapsed(ec) && shadowTreeRoot) { + if (forward) + searchRange->setEnd(shadowTreeRoot, shadowTreeRoot->childNodeCount(), ec); + else + searchRange->setStartBefore(shadowTreeRoot, ec); + } + + if (searchRange->startContainer()->isDocumentNode() && searchRange->endContainer()->isDocumentNode()) + break; + } + + if (insideVisibleArea(resultRange.get())) + return resultRange; + + if (!(options & WrapAround)) + return Range::create(m_frame->document()); + + if (options & Backwards) + return lastVisibleRange(target, options); + + return firstVisibleRange(target, options); +} + +void Editor::changeSelectionAfterCommand(const VisibleSelection& newSelection, bool closeTyping, bool clearTypingStyle) +{ + // If the new selection is orphaned, then don't update the selection. + if (newSelection.start().isOrphan() || newSelection.end().isOrphan()) + return; + +#if SUPPORT_AUTOCORRECTION_PANEL + // Check to see if the command introduced paragraph separator. If it did, we remove existing autocorrection underlines. + // This is in consistency with the behavior in AppKit + if (!inSameParagraph(m_frame->selection()->selection().visibleStart(), newSelection.visibleEnd())) + m_frame->document()->markers()->removeMarkers(DocumentMarker::CorrectionIndicator); +#endif + + // If there is no selection change, don't bother sending shouldChangeSelection, but still call setSelection, + // because there is work that it must do in this situation. + // The old selection can be invalid here and calling shouldChangeSelection can produce some strange calls. + // See <rdar://problem/5729315> Some shouldChangeSelectedDOMRange contain Ranges for selections that are no longer valid + bool selectionDidNotChangeDOMPosition = newSelection == m_frame->selection()->selection(); + if (selectionDidNotChangeDOMPosition || m_frame->selection()->shouldChangeSelection(newSelection)) + m_frame->selection()->setSelection(newSelection, closeTyping, clearTypingStyle); + + // Some editing operations change the selection visually without affecting its position within the DOM. + // For example when you press return in the following (the caret is marked by ^): + // <div contentEditable="true"><div>^Hello</div></div> + // WebCore inserts <div><br></div> *before* the current block, which correctly moves the paragraph down but which doesn't + // change the caret's DOM position (["hello", 0]). In these situations the above SelectionController::setSelection call + // does not call EditorClient::respondToChangedSelection(), which, on the Mac, sends selection change notifications and + // starts a new kill ring sequence, but we want to do these things (matches AppKit). + if (selectionDidNotChangeDOMPosition) + client()->respondToChangedSelection(); +} + +String Editor::selectedText() const +{ + return plainText(m_frame->selection()->toNormalizedRange().get()); +} + +IntRect Editor::firstRectForRange(Range* range) const +{ + int extraWidthToEndOfLine = 0; + ASSERT(range->startContainer()); + ASSERT(range->endContainer()); + + InlineBox* startInlineBox; + int startCaretOffset; + Position startPosition = VisiblePosition(range->startPosition()).deepEquivalent(); + if (startPosition.isNull()) + return IntRect(); + startPosition.getInlineBoxAndOffset(DOWNSTREAM, startInlineBox, startCaretOffset); + + RenderObject* startRenderer = startPosition.node()->renderer(); + ASSERT(startRenderer); + IntRect startCaretRect = startRenderer->localCaretRect(startInlineBox, startCaretOffset, &extraWidthToEndOfLine); + if (startCaretRect != IntRect()) + startCaretRect = startRenderer->localToAbsoluteQuad(FloatRect(startCaretRect)).enclosingBoundingBox(); + + InlineBox* endInlineBox; + int endCaretOffset; + Position endPosition = VisiblePosition(range->endPosition()).deepEquivalent(); + if (endPosition.isNull()) + return IntRect(); + endPosition.getInlineBoxAndOffset(UPSTREAM, endInlineBox, endCaretOffset); + + RenderObject* endRenderer = endPosition.node()->renderer(); + ASSERT(endRenderer); + IntRect endCaretRect = endRenderer->localCaretRect(endInlineBox, endCaretOffset); + if (endCaretRect != IntRect()) + endCaretRect = endRenderer->localToAbsoluteQuad(FloatRect(endCaretRect)).enclosingBoundingBox(); + + if (startCaretRect.y() == endCaretRect.y()) { + // start and end are on the same line + return IntRect(min(startCaretRect.x(), endCaretRect.x()), + startCaretRect.y(), + abs(endCaretRect.x() - startCaretRect.x()), + max(startCaretRect.height(), endCaretRect.height())); + } + + // start and end aren't on the same line, so go from start to the end of its line + return IntRect(startCaretRect.x(), + startCaretRect.y(), + startCaretRect.width() + extraWidthToEndOfLine, + startCaretRect.height()); +} + +bool Editor::shouldChangeSelection(const VisibleSelection& oldSelection, const VisibleSelection& newSelection, EAffinity affinity, bool stillSelecting) const +{ + return client()->shouldChangeSelectedRange(oldSelection.toNormalizedRange().get(), newSelection.toNormalizedRange().get(), affinity, stillSelecting); +} + +void Editor::computeAndSetTypingStyle(CSSStyleDeclaration* style, EditAction editingAction) +{ + if (!style || !style->length()) { + m_frame->selection()->clearTypingStyle(); + return; + } + + // Calculate the current typing style. + RefPtr<EditingStyle> typingStyle; + if (m_frame->selection()->typingStyle()) { + typingStyle = m_frame->selection()->typingStyle()->copy(); + typingStyle->overrideWithStyle(style->makeMutable().get()); + } else + typingStyle = EditingStyle::create(style); + + typingStyle->prepareToApplyAt(m_frame->selection()->selection().visibleStart().deepEquivalent(), EditingStyle::PreserveWritingDirection); + + // Handle block styles, substracting these from the typing style. + RefPtr<EditingStyle> blockStyle = typingStyle->extractAndRemoveBlockProperties(); + if (!blockStyle->isEmpty()) + applyCommand(ApplyStyleCommand::create(m_frame->document(), blockStyle.get(), editingAction)); + + // Set the remaining style as the typing style. + m_frame->selection()->setTypingStyle(typingStyle); +} + +PassRefPtr<CSSMutableStyleDeclaration> Editor::selectionComputedStyle(bool& shouldUseFixedFontDefaultSize) const +{ + if (m_frame->selection()->isNone()) + return 0; + + RefPtr<Range> range(m_frame->selection()->toNormalizedRange()); + Position position = range->editingStartPosition(); + + // If the pos is at the end of a text node, then this node is not fully selected. + // Move it to the next deep equivalent position to avoid removing the style from this node. + // e.g. if pos was at Position("hello", 5) in <b>hello<div>world</div></b>, we want Position("world", 0) instead. + // We only do this for range because caret at Position("hello", 5) in <b>hello</b>world should give you font-weight: bold. + Node* positionNode = position.containerNode(); + if (m_frame->selection()->isRange() && positionNode && positionNode->isTextNode() && position.computeOffsetInContainerNode() == positionNode->maxCharacterOffset()) + position = nextVisuallyDistinctCandidate(position); + + Element* element = position.element(); + if (!element) + return 0; + + RefPtr<Element> styleElement = element; + RefPtr<CSSComputedStyleDeclaration> style = computedStyle(styleElement.release()); + RefPtr<CSSMutableStyleDeclaration> mutableStyle = style->copy(); + shouldUseFixedFontDefaultSize = style->useFixedFontDefaultSize(); + + if (!m_frame->selection()->typingStyle()) + return mutableStyle; + + RefPtr<EditingStyle> typingStyle = m_frame->selection()->typingStyle(); + typingStyle->removeNonEditingProperties(); + typingStyle->prepareToApplyAt(position); + mutableStyle->merge(typingStyle->style()); + + return mutableStyle; +} + +void Editor::textFieldDidBeginEditing(Element* e) +{ + if (client()) + client()->textFieldDidBeginEditing(e); +} + +void Editor::textFieldDidEndEditing(Element* e) +{ + if (client()) + client()->textFieldDidEndEditing(e); +} + +void Editor::textDidChangeInTextField(Element* e) +{ + if (client()) + client()->textDidChangeInTextField(e); +} + +bool Editor::doTextFieldCommandFromEvent(Element* e, KeyboardEvent* ke) +{ + if (client()) + return client()->doTextFieldCommandFromEvent(e, ke); + + return false; +} + +void Editor::textWillBeDeletedInTextField(Element* input) +{ + if (client()) + client()->textWillBeDeletedInTextField(input); +} + +void Editor::textDidChangeInTextArea(Element* e) +{ + if (client()) + client()->textDidChangeInTextArea(e); +} + +void Editor::applyEditingStyleToBodyElement() const +{ + RefPtr<NodeList> list = m_frame->document()->getElementsByTagName("body"); + unsigned len = list->length(); + for (unsigned i = 0; i < len; i++) + applyEditingStyleToElement(static_cast<Element*>(list->item(i))); +} + +void Editor::applyEditingStyleToElement(Element* element) const +{ + if (!element) + return; + + CSSStyleDeclaration* style = element->style(); + ASSERT(style); + + ExceptionCode ec = 0; + style->setProperty(CSSPropertyWordWrap, "break-word", false, ec); + ASSERT(!ec); + style->setProperty(CSSPropertyWebkitNbspMode, "space", false, ec); + ASSERT(!ec); + style->setProperty(CSSPropertyWebkitLineBreak, "after-white-space", false, ec); + ASSERT(!ec); +} + +RenderStyle* Editor::styleForSelectionStart(Node *&nodeToRemove) const +{ + nodeToRemove = 0; + + if (m_frame->selection()->isNone()) + return 0; + + Position position = m_frame->selection()->selection().visibleStart().deepEquivalent(); + if (!position.isCandidate()) + return 0; + if (!position.node()) + return 0; + + RefPtr<EditingStyle> typingStyle = m_frame->selection()->typingStyle(); + if (!typingStyle || !typingStyle->style()) + return position.node()->renderer()->style(); + + RefPtr<Element> styleElement = m_frame->document()->createElement(spanTag, false); + + ExceptionCode ec = 0; + String styleText = typingStyle->style()->cssText() + " display: inline"; + styleElement->setAttribute(styleAttr, styleText.impl(), ec); + ASSERT(!ec); + + styleElement->appendChild(m_frame->document()->createEditingTextNode(""), ec); + ASSERT(!ec); + + position.node()->parentNode()->appendChild(styleElement, ec); + ASSERT(!ec); + + nodeToRemove = styleElement.get(); + return styleElement->renderer() ? styleElement->renderer()->style() : 0; +} + +// Searches from the beginning of the document if nothing is selected. +bool Editor::findString(const String& target, bool forward, bool caseFlag, bool wrapFlag, bool startInSelection) +{ + FindOptions options = (forward ? 0 : Backwards) | (caseFlag ? 0 : CaseInsensitive) | (wrapFlag ? WrapAround : 0) | (startInSelection ? StartInSelection : 0); + return findString(target, options); +} + +bool Editor::findString(const String& target, FindOptions options) +{ + if (target.isEmpty()) + return false; + + if (m_frame->excludeFromTextSearch()) + return false; + + // Start from an edge of the selection, if there's a selection that's not in shadow content. Which edge + // is used depends on whether we're searching forward or backward, and whether startInSelection is set. + RefPtr<Range> searchRange(rangeOfContents(m_frame->document())); + VisibleSelection selection = m_frame->selection()->selection(); + + bool forward = !(options & Backwards); + bool startInSelection = options & StartInSelection; + if (forward) + setStart(searchRange.get(), startInSelection ? selection.visibleStart() : selection.visibleEnd()); + else + setEnd(searchRange.get(), startInSelection ? selection.visibleEnd() : selection.visibleStart()); + + RefPtr<Node> shadowTreeRoot = selection.shadowTreeRootNode(); + if (shadowTreeRoot) { + ExceptionCode ec = 0; + if (forward) + searchRange->setEnd(shadowTreeRoot.get(), shadowTreeRoot->childNodeCount(), ec); + else + searchRange->setStart(shadowTreeRoot.get(), 0, ec); + } + + RefPtr<Range> resultRange(findPlainText(searchRange.get(), target, options)); + // If we started in the selection and the found range exactly matches the existing selection, find again. + // Build a selection with the found range to remove collapsed whitespace. + // Compare ranges instead of selection objects to ignore the way that the current selection was made. + if (startInSelection && areRangesEqual(VisibleSelection(resultRange.get()).toNormalizedRange().get(), selection.toNormalizedRange().get())) { + searchRange = rangeOfContents(m_frame->document()); + if (forward) + setStart(searchRange.get(), selection.visibleEnd()); + else + setEnd(searchRange.get(), selection.visibleStart()); + + if (shadowTreeRoot) { + ExceptionCode ec = 0; + if (forward) + searchRange->setEnd(shadowTreeRoot.get(), shadowTreeRoot->childNodeCount(), ec); + else + searchRange->setStart(shadowTreeRoot.get(), 0, ec); + } + + resultRange = findPlainText(searchRange.get(), target, options); + } + + ExceptionCode exception = 0; + + // If nothing was found in the shadow tree, search in main content following the shadow tree. + if (resultRange->collapsed(exception) && shadowTreeRoot) { + searchRange = rangeOfContents(m_frame->document()); + if (forward) + searchRange->setStartAfter(shadowTreeRoot->shadowHost(), exception); + else + searchRange->setEndBefore(shadowTreeRoot->shadowHost(), exception); + + resultRange = findPlainText(searchRange.get(), target, options); + } + + if (!insideVisibleArea(resultRange.get())) { + resultRange = nextVisibleRange(resultRange.get(), target, options); + if (!resultRange) + return false; + } + + // If we didn't find anything and we're wrapping, search again in the entire document (this will + // redundantly re-search the area already searched in some cases). + if (resultRange->collapsed(exception) && options & WrapAround) { + searchRange = rangeOfContents(m_frame->document()); + resultRange = findPlainText(searchRange.get(), target, options); + // We used to return false here if we ended up with the same range that we started with + // (e.g., the selection was already the only instance of this text). But we decided that + // this should be a success case instead, so we'll just fall through in that case. + } + + if (resultRange->collapsed(exception)) + return false; + + m_frame->selection()->setSelection(VisibleSelection(resultRange.get(), DOWNSTREAM)); + m_frame->selection()->revealSelection(); + return true; +} + +static bool isFrameInRange(Frame* frame, Range* range) +{ + bool inRange = false; + for (HTMLFrameOwnerElement* ownerElement = frame->ownerElement(); ownerElement; ownerElement = ownerElement->document()->ownerElement()) { + if (ownerElement->document() == range->ownerDocument()) { + ExceptionCode ec = 0; + inRange = range->intersectsNode(ownerElement, ec); + break; + } + } + return inRange; +} + +unsigned Editor::countMatchesForText(const String& target, FindOptions options, unsigned limit, bool markMatches) +{ + return countMatchesForText(target, 0, options, limit, markMatches); +} + +unsigned Editor::countMatchesForText(const String& target, Range* range, FindOptions options, unsigned limit, bool markMatches) +{ + if (target.isEmpty()) + return 0; + + RefPtr<Range> searchRange; + if (range) { + if (range->ownerDocument() == m_frame->document()) + searchRange = range; + else if (!isFrameInRange(m_frame, range)) + return 0; + } + if (!searchRange) + searchRange = rangeOfContents(m_frame->document()); + + Node* originalEndContainer = searchRange->endContainer(); + int originalEndOffset = searchRange->endOffset(); + + ExceptionCode exception = 0; + unsigned matchCount = 0; + do { + RefPtr<Range> resultRange(findPlainText(searchRange.get(), target, options & ~Backwards)); + if (resultRange->collapsed(exception)) { + if (!resultRange->startContainer()->isInShadowTree()) + break; + + searchRange->setStartAfter(resultRange->startContainer()->shadowAncestorNode(), exception); + searchRange->setEnd(originalEndContainer, originalEndOffset, exception); + continue; + } + + // Only treat the result as a match if it is visible + if (insideVisibleArea(resultRange.get())) { + ++matchCount; + if (markMatches) + m_frame->document()->markers()->addMarker(resultRange.get(), DocumentMarker::TextMatch); + } + + // Stop looking if we hit the specified limit. A limit of 0 means no limit. + if (limit > 0 && matchCount >= limit) + break; + + // Set the new start for the search range to be the end of the previous + // result range. There is no need to use a VisiblePosition here, + // since findPlainText will use a TextIterator to go over the visible + // text nodes. + searchRange->setStart(resultRange->endContainer(exception), resultRange->endOffset(exception), exception); + + Node* shadowTreeRoot = searchRange->shadowTreeRootNode(); + if (searchRange->collapsed(exception) && shadowTreeRoot) + searchRange->setEnd(shadowTreeRoot, shadowTreeRoot->childNodeCount(), exception); + } while (true); + + if (markMatches) { + // Do a "fake" paint in order to execute the code that computes the rendered rect for each text match. + if (m_frame->view() && m_frame->contentRenderer()) { + m_frame->document()->updateLayout(); // Ensure layout is up to date. + IntRect visibleRect = m_frame->view()->visibleContentRect(); + if (!visibleRect.isEmpty()) { + GraphicsContext context((PlatformGraphicsContext*)0); + context.setPaintingDisabled(true); + m_frame->view()->paintContents(&context, visibleRect); + } + } + } + + return matchCount; +} + +void Editor::setMarkedTextMatchesAreHighlighted(bool flag) +{ + if (flag == m_areMarkedTextMatchesHighlighted) + return; + + m_areMarkedTextMatchesHighlighted = flag; + m_frame->document()->markers()->repaintMarkers(DocumentMarker::TextMatch); +} + +void Editor::respondToChangedSelection(const VisibleSelection& oldSelection, bool closeTyping) +{ + bool isContinuousSpellCheckingEnabled = this->isContinuousSpellCheckingEnabled(); + bool isContinuousGrammarCheckingEnabled = isContinuousSpellCheckingEnabled && isGrammarCheckingEnabled(); + if (isContinuousSpellCheckingEnabled) { + VisibleSelection newAdjacentWords; + VisibleSelection newSelectedSentence; + bool caretBrowsing = m_frame->settings() && m_frame->settings()->caretBrowsingEnabled(); + if (m_frame->selection()->selection().isContentEditable() || caretBrowsing) { + VisiblePosition newStart(m_frame->selection()->selection().visibleStart()); + newAdjacentWords = VisibleSelection(startOfWord(newStart, LeftWordIfOnBoundary), endOfWord(newStart, RightWordIfOnBoundary)); + if (isContinuousGrammarCheckingEnabled) + newSelectedSentence = VisibleSelection(startOfSentence(newStart), endOfSentence(newStart)); + } + + // When typing we check spelling elsewhere, so don't redo it here. + // If this is a change in selection resulting from a delete operation, + // oldSelection may no longer be in the document. + if (closeTyping && oldSelection.isContentEditable() && oldSelection.start().node() && oldSelection.start().node()->inDocument()) { + VisiblePosition oldStart(oldSelection.visibleStart()); + VisibleSelection oldAdjacentWords = VisibleSelection(startOfWord(oldStart, LeftWordIfOnBoundary), endOfWord(oldStart, RightWordIfOnBoundary)); + if (oldAdjacentWords != newAdjacentWords) { + if (isContinuousGrammarCheckingEnabled) { + VisibleSelection oldSelectedSentence = VisibleSelection(startOfSentence(oldStart), endOfSentence(oldStart)); + markMisspellingsAndBadGrammar(oldAdjacentWords, oldSelectedSentence != newSelectedSentence, oldSelectedSentence); + } else + markMisspellingsAndBadGrammar(oldAdjacentWords, false, oldAdjacentWords); + } + } + +#if !PLATFORM(MAC) || (PLATFORM(MAC) && (defined(BUILDING_ON_TIGER) || defined(BUILDING_ON_LEOPARD) || defined(BUILDING_ON_SNOW_LEOPARD))) + // This only erases markers that are in the first unit (word or sentence) of the selection. + // Perhaps peculiar, but it matches AppKit on these Mac OSX versions. + if (RefPtr<Range> wordRange = newAdjacentWords.toNormalizedRange()) + m_frame->document()->markers()->removeMarkers(wordRange.get(), DocumentMarker::Spelling); +#endif + if (RefPtr<Range> sentenceRange = newSelectedSentence.toNormalizedRange()) + m_frame->document()->markers()->removeMarkers(sentenceRange.get(), DocumentMarker::Grammar); + } + + // When continuous spell checking is off, existing markers disappear after the selection changes. + if (!isContinuousSpellCheckingEnabled) + m_frame->document()->markers()->removeMarkers(DocumentMarker::Spelling); + if (!isContinuousGrammarCheckingEnabled) + m_frame->document()->markers()->removeMarkers(DocumentMarker::Grammar); + + respondToChangedSelection(oldSelection); +} + +static Node* findFirstMarkable(Node* node) +{ + while (node) { + if (!node->renderer()) + return 0; + if (node->renderer()->isText()) + return node; + if (node->renderer()->isTextControl()) + node = toRenderTextControl(node->renderer())->visiblePositionForIndex(1).deepEquivalent().node(); + else if (node->firstChild()) + node = node->firstChild(); + else + node = node->nextSibling(); + } + + return 0; +} + +bool Editor::selectionStartHasSpellingMarkerFor(int from, int length) const +{ + Node* node = findFirstMarkable(m_frame->selection()->start().node()); + if (!node) + return false; + + unsigned int startOffset = static_cast<unsigned int>(from); + unsigned int endOffset = static_cast<unsigned int>(from + length); + Vector<DocumentMarker> markers = m_frame->document()->markers()->markersForNode(node); + for (size_t i = 0; i < markers.size(); ++i) { + DocumentMarker marker = markers[i]; + if (marker.startOffset <= startOffset && endOffset <= marker.endOffset && marker.type == DocumentMarker::Spelling) + return true; + } + + return false; +} + + +} // namespace WebCore diff --git a/Source/WebCore/editing/Editor.h b/Source/WebCore/editing/Editor.h new file mode 100644 index 0000000..2e61ce6 --- /dev/null +++ b/Source/WebCore/editing/Editor.h @@ -0,0 +1,451 @@ +/* + * Copyright (C) 2006, 2007, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef Editor_h +#define Editor_h + +#include "ClipboardAccessPolicy.h" +#include "Color.h" +#include "CorrectionPanelInfo.h" +#include "DocumentMarker.h" +#include "EditAction.h" +#include "EditingBehavior.h" +#include "EditorDeleteAction.h" +#include "EditorInsertAction.h" +#include "FindOptions.h" +#include "Timer.h" +#include "VisibleSelection.h" +#include "WritingDirection.h" + +#if PLATFORM(MAC) && !defined(__OBJC__) +class NSDictionary; +typedef int NSWritingDirection; +#endif + +namespace WebCore { + +class CSSMutableStyleDeclaration; +class CSSStyleDeclaration; +class Clipboard; +class DeleteButtonController; +class EditCommand; +class EditorClient; +class EditorInternalCommand; +class Frame; +class HTMLElement; +class HitTestResult; +class KillRing; +class Pasteboard; +class SimpleFontData; +class SpellChecker; +class Text; +class TextEvent; + +struct CompositionUnderline { + CompositionUnderline() + : startOffset(0), endOffset(0), thick(false) { } + CompositionUnderline(unsigned s, unsigned e, const Color& c, bool t) + : startOffset(s), endOffset(e), color(c), thick(t) { } + unsigned startOffset; + unsigned endOffset; + Color color; + bool thick; +}; + +enum TriState { FalseTriState, TrueTriState, MixedTriState }; +enum EditorCommandSource { CommandFromMenuOrKeyBinding, CommandFromDOM, CommandFromDOMWithUserInterface }; + +class Editor { +public: + Editor(Frame*); + ~Editor(); + + EditorClient* client() const; + Frame* frame() const { return m_frame; } + DeleteButtonController* deleteButtonController() const { return m_deleteButtonController.get(); } + EditCommand* lastEditCommand() { return m_lastEditCommand.get(); } + + void handleKeyboardEvent(KeyboardEvent*); + void handleInputMethodKeydown(KeyboardEvent*); + bool handleTextEvent(TextEvent*); + + bool canEdit() const; + bool canEditRichly() const; + + bool canDHTMLCut(); + bool canDHTMLCopy(); + bool canDHTMLPaste(); + bool tryDHTMLCopy(); + bool tryDHTMLCut(); + bool tryDHTMLPaste(); + + bool canCut() const; + bool canCopy() const; + bool canPaste() const; + bool canDelete() const; + bool canSmartCopyOrDelete(); + + void cut(); + void copy(); + void paste(); + void pasteAsPlainText(); + void performDelete(); + + void copyURL(const KURL&, const String&); + void copyImage(const HitTestResult&); + + void indent(); + void outdent(); + void transpose(); + + bool shouldInsertFragment(PassRefPtr<DocumentFragment>, PassRefPtr<Range>, EditorInsertAction); + bool shouldInsertText(const String&, Range*, EditorInsertAction) const; + bool shouldShowDeleteInterface(HTMLElement*) const; + bool shouldDeleteRange(Range*) const; + bool shouldApplyStyle(CSSStyleDeclaration*, Range*); + + void respondToChangedSelection(const VisibleSelection& oldSelection); + void respondToChangedContents(const VisibleSelection& endingSelection); + + TriState selectionHasStyle(CSSStyleDeclaration*) const; + String selectionStartCSSPropertyValue(int propertyID); + const SimpleFontData* fontForSelection(bool&) const; + WritingDirection textDirectionForSelection(bool&) const; + + TriState selectionUnorderedListState() const; + TriState selectionOrderedListState() const; + PassRefPtr<Node> insertOrderedList(); + PassRefPtr<Node> insertUnorderedList(); + bool canIncreaseSelectionListLevel(); + bool canDecreaseSelectionListLevel(); + PassRefPtr<Node> increaseSelectionListLevel(); + PassRefPtr<Node> increaseSelectionListLevelOrdered(); + PassRefPtr<Node> increaseSelectionListLevelUnordered(); + void decreaseSelectionListLevel(); + + void removeFormattingAndStyle(); + + void clearLastEditCommand(); + + bool deleteWithDirection(SelectionDirection, TextGranularity, bool killRing, bool isTypingAction); + void deleteSelectionWithSmartDelete(bool smartDelete); + bool dispatchCPPEvent(const AtomicString&, ClipboardAccessPolicy); + + Node* removedAnchor() const { return m_removedAnchor.get(); } + void setRemovedAnchor(PassRefPtr<Node> n) { m_removedAnchor = n; } + + void applyStyle(CSSStyleDeclaration*, EditAction = EditActionUnspecified); + void applyParagraphStyle(CSSStyleDeclaration*, EditAction = EditActionUnspecified); + void applyStyleToSelection(CSSStyleDeclaration*, EditAction); + void applyParagraphStyleToSelection(CSSStyleDeclaration*, EditAction); + + void appliedEditing(PassRefPtr<EditCommand>); + void unappliedEditing(PassRefPtr<EditCommand>); + void reappliedEditing(PassRefPtr<EditCommand>); + + bool selectionStartHasStyle(CSSStyleDeclaration*) const; + + bool clientIsEditable() const; + + void setShouldStyleWithCSS(bool flag) { m_shouldStyleWithCSS = flag; } + bool shouldStyleWithCSS() const { return m_shouldStyleWithCSS; } + + class Command { + public: + Command(); + Command(const EditorInternalCommand*, EditorCommandSource, PassRefPtr<Frame>); + + bool execute(const String& parameter = String(), Event* triggeringEvent = 0) const; + bool execute(Event* triggeringEvent) const; + + bool isSupported() const; + bool isEnabled(Event* triggeringEvent = 0) const; + + TriState state(Event* triggeringEvent = 0) const; + String value(Event* triggeringEvent = 0) const; + + bool isTextInsertion() const; + + private: + const EditorInternalCommand* m_command; + EditorCommandSource m_source; + RefPtr<Frame> m_frame; + }; + Command command(const String& commandName); // Command source is CommandFromMenuOrKeyBinding. + Command command(const String& commandName, EditorCommandSource); + static bool commandIsSupportedFromMenuOrKeyBinding(const String& commandName); // Works without a frame. + + bool insertText(const String&, Event* triggeringEvent); + bool insertTextWithoutSendingTextEvent(const String&, bool selectInsertedText, Event* triggeringEvent); + bool insertLineBreak(); + bool insertParagraphSeparator(); + + bool isContinuousSpellCheckingEnabled(); + void toggleContinuousSpellChecking(); + bool isGrammarCheckingEnabled(); + void toggleGrammarChecking(); + void ignoreSpelling(); + void learnSpelling(); + int spellCheckerDocumentTag(); + bool isSelectionUngrammatical(); + bool isSelectionMisspelled(); + Vector<String> guessesForMisspelledSelection(); + Vector<String> guessesForUngrammaticalSelection(); + Vector<String> guessesForMisspelledOrUngrammaticalSelection(bool& misspelled, bool& ungrammatical); + bool isSpellCheckingEnabledInFocusedNode() const; + bool isSpellCheckingEnabledFor(Node*) const; + void markMisspellingsAfterTypingToWord(const VisiblePosition &wordStart, const VisibleSelection& selectionAfterTyping); + void markMisspellings(const VisibleSelection&, RefPtr<Range>& firstMisspellingRange); + void markBadGrammar(const VisibleSelection&); + void markMisspellingsAndBadGrammar(const VisibleSelection& spellingSelection, bool markGrammar, const VisibleSelection& grammarSelection); +#if PLATFORM(MAC) && !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD) + void uppercaseWord(); + void lowercaseWord(); + void capitalizeWord(); + void showSubstitutionsPanel(); + bool substitutionsPanelIsShowing(); + void toggleSmartInsertDelete(); + bool isAutomaticQuoteSubstitutionEnabled(); + void toggleAutomaticQuoteSubstitution(); + bool isAutomaticLinkDetectionEnabled(); + void toggleAutomaticLinkDetection(); + bool isAutomaticDashSubstitutionEnabled(); + void toggleAutomaticDashSubstitution(); + bool isAutomaticTextReplacementEnabled(); + void toggleAutomaticTextReplacement(); + bool isAutomaticSpellingCorrectionEnabled(); + void toggleAutomaticSpellingCorrection(); + enum TextCheckingOptionFlags { + MarkSpelling = 1 << 0, + MarkGrammar = 1 << 1, + PerformReplacement = 1 << 2, + ShowCorrectionPanel = 1 << 3, + }; + typedef unsigned TextCheckingOptions; + + void markAllMisspellingsAndBadGrammarInRanges(TextCheckingOptions, Range* spellingRange, Range* grammarRange); + void changeBackToReplacedString(const String& replacedString); +#endif + void advanceToNextMisspelling(bool startBeforeSelection = false); + void showSpellingGuessPanel(); + bool spellingPanelIsShowing(); + + bool shouldBeginEditing(Range*); + bool shouldEndEditing(Range*); + + void clearUndoRedoOperations(); + bool canUndo(); + void undo(); + bool canRedo(); + void redo(); + + void didBeginEditing(); + void didEndEditing(); + void didWriteSelectionToPasteboard(); + + void showFontPanel(); + void showStylesPanel(); + void showColorPanel(); + void toggleBold(); + void toggleUnderline(); + void setBaseWritingDirection(WritingDirection); + + // smartInsertDeleteEnabled and selectTrailingWhitespaceEnabled are + // mutually exclusive, meaning that enabling one will disable the other. + bool smartInsertDeleteEnabled(); + bool isSelectTrailingWhitespaceEnabled(); + + bool hasBidiSelection() const; + + // international text input composition + bool hasComposition() const { return m_compositionNode; } + void setComposition(const String&, const Vector<CompositionUnderline>&, unsigned selectionStart, unsigned selectionEnd); + void confirmComposition(); + void confirmComposition(const String&); // if no existing composition, replaces selection + void confirmCompositionWithoutDisturbingSelection(); + PassRefPtr<Range> compositionRange() const; + bool getCompositionSelection(unsigned& selectionStart, unsigned& selectionEnd) const; + + // getting international text input composition state (for use by InlineTextBox) + Text* compositionNode() const { return m_compositionNode.get(); } + unsigned compositionStart() const { return m_compositionStart; } + unsigned compositionEnd() const { return m_compositionEnd; } + bool compositionUsesCustomUnderlines() const { return !m_customCompositionUnderlines.isEmpty(); } + const Vector<CompositionUnderline>& customCompositionUnderlines() const { return m_customCompositionUnderlines; } + + bool ignoreCompositionSelectionChange() const { return m_ignoreCompositionSelectionChange; } + + void setStartNewKillRingSequence(bool); + + PassRefPtr<Range> rangeForPoint(const IntPoint& windowPoint); + + void clear(); + + VisibleSelection selectionForCommand(Event*); + + KillRing* killRing() const { return m_killRing.get(); } + SpellChecker* spellChecker() const { return m_spellChecker.get(); } + + EditingBehavior behavior() const; + + PassRefPtr<Range> selectedRange(); + + // We should make these functions private when their callers in Frame are moved over here to Editor + bool insideVisibleArea(const IntPoint&) const; + bool insideVisibleArea(Range*) const; + + void addToKillRing(Range*, bool prepend); + + void handleCancelOperation(); + void startCorrectionPanelTimer(CorrectionPanelInfo::PanelType); + // If user confirmed a correction in the correction panel, correction has non-zero length, otherwise it means that user has dismissed the panel. + void handleCorrectionPanelResult(const String& correction); + bool isShowingCorrectionPanel(); + + void pasteAsFragment(PassRefPtr<DocumentFragment>, bool smartReplace, bool matchStyle); + void pasteAsPlainText(const String&, bool smartReplace); + + // This is only called on the mac where paste is implemented primarily at the WebKit level. + void pasteAsPlainTextBypassingDHTML(); + + void clearMisspellingsAndBadGrammar(const VisibleSelection&); + void markMisspellingsAndBadGrammar(const VisibleSelection&); + + Node* findEventTargetFrom(const VisibleSelection& selection) const; + + String selectedText() const; + bool findString(const String&, FindOptions); + // FIXME: Switch callers over to the FindOptions version and retire this one. + bool findString(const String&, bool forward, bool caseFlag, bool wrapFlag, bool startInSelection); + + const VisibleSelection& mark() const; // Mark, to be used as emacs uses it. + void setMark(const VisibleSelection&); + + void computeAndSetTypingStyle(CSSStyleDeclaration* , EditAction = EditActionUnspecified); + void applyEditingStyleToBodyElement() const; + void applyEditingStyleToElement(Element*) const; + + IntRect firstRectForRange(Range*) const; + + void respondToChangedSelection(const VisibleSelection& oldSelection, bool closeTyping); + bool shouldChangeSelection(const VisibleSelection& oldSelection, const VisibleSelection& newSelection, EAffinity, bool stillSelecting) const; + + RenderStyle* styleForSelectionStart(Node*& nodeToRemove) const; + + unsigned countMatchesForText(const String&, FindOptions, unsigned limit, bool markMatches); + unsigned countMatchesForText(const String&, Range*, FindOptions, unsigned limit, bool markMatches); + bool markedTextMatchesAreHighlighted() const; + void setMarkedTextMatchesAreHighlighted(bool); + + PassRefPtr<CSSMutableStyleDeclaration> selectionComputedStyle(bool& shouldUseFixedFontDefaultSize) const; + + void textFieldDidBeginEditing(Element*); + void textFieldDidEndEditing(Element*); + void textDidChangeInTextField(Element*); + bool doTextFieldCommandFromEvent(Element*, KeyboardEvent*); + void textWillBeDeletedInTextField(Element* input); + void textDidChangeInTextArea(Element*); + +#if PLATFORM(MAC) + NSDictionary* fontAttributesForSelectionStart() const; + NSWritingDirection baseWritingDirectionForSelectionStart() const; + bool canCopyExcludingStandaloneImages(); + void takeFindStringFromSelection(); +#endif + + bool selectionStartHasSpellingMarkerFor(int from, int length) const; + void removeSpellAndCorrectionMarkersFromWordsToBeEdited(bool doNotRemoveIfSelectionAtWordBoundary); + +private: + Frame* m_frame; + OwnPtr<DeleteButtonController> m_deleteButtonController; + RefPtr<EditCommand> m_lastEditCommand; + RefPtr<Node> m_removedAnchor; + RefPtr<Text> m_compositionNode; + unsigned m_compositionStart; + unsigned m_compositionEnd; + Vector<CompositionUnderline> m_customCompositionUnderlines; + bool m_ignoreCompositionSelectionChange; + bool m_shouldStartNewKillRingSequence; + bool m_shouldStyleWithCSS; + OwnPtr<KillRing> m_killRing; + CorrectionPanelInfo m_correctionPanelInfo; + OwnPtr<SpellChecker> m_spellChecker; + Timer<Editor> m_correctionPanelTimer; + bool m_correctionPanelIsDismissedByEditor; + VisibleSelection m_mark; + bool m_areMarkedTextMatchesHighlighted; + + bool canDeleteRange(Range*) const; + bool canSmartReplaceWithPasteboard(Pasteboard*); + PassRefPtr<Clipboard> newGeneralClipboard(ClipboardAccessPolicy, Frame*); + void pasteAsPlainTextWithPasteboard(Pasteboard*); + void pasteWithPasteboard(Pasteboard*, bool allowPlainText); + void replaceSelectionWithFragment(PassRefPtr<DocumentFragment>, bool selectReplacement, bool smartReplace, bool matchStyle); + void replaceSelectionWithText(const String&, bool selectReplacement, bool smartReplace); + void writeSelectionToPasteboard(Pasteboard*); + void revealSelectionAfterEditingOperation(); + void markMisspellingsOrBadGrammar(const VisibleSelection&, bool checkSpelling, RefPtr<Range>& firstMisspellingRange); + + void selectComposition(); + void confirmComposition(const String&, bool preserveSelection); + void setIgnoreCompositionSelectionChange(bool ignore); + + PassRefPtr<Range> firstVisibleRange(const String&, FindOptions); + PassRefPtr<Range> lastVisibleRange(const String&, FindOptions); + PassRefPtr<Range> nextVisibleRange(Range*, const String&, FindOptions); + + void changeSelectionAfterCommand(const VisibleSelection& newSelection, bool closeTyping, bool clearTypingStyle); + void correctionPanelTimerFired(Timer<Editor>*); + Node* findEventTargetFromSelection() const; + void stopCorrectionPanelTimer(); + void dismissCorrectionPanel(ReasonForDismissingCorrectionPanel); + void applyCorrectionPanelInfo(const Vector<DocumentMarker::MarkerType>& markerTypesToAdd); +}; + +inline void Editor::setStartNewKillRingSequence(bool flag) +{ + m_shouldStartNewKillRingSequence = flag; +} + +inline const VisibleSelection& Editor::mark() const +{ + return m_mark; +} + +inline void Editor::setMark(const VisibleSelection& selection) +{ + m_mark = selection; +} + +inline bool Editor::markedTextMatchesAreHighlighted() const +{ + return m_areMarkedTextMatchesHighlighted; +} + + +} // namespace WebCore + +#endif // Editor_h diff --git a/Source/WebCore/editing/EditorCommand.cpp b/Source/WebCore/editing/EditorCommand.cpp new file mode 100644 index 0000000..5de44a6 --- /dev/null +++ b/Source/WebCore/editing/EditorCommand.cpp @@ -0,0 +1,1666 @@ +/* + * Copyright (C) 2006, 2007, 2008 Apple Inc. All rights reserved. + * Copyright (C) 2008 Nokia Corporation and/or its subsidiary(-ies) + * Copyright (C) 2009 Igalia S.L. + * + * 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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 "Editor.h" + +#include "CSSComputedStyleDeclaration.h" +#include "CSSMutableStyleDeclaration.h" +#include "CSSPropertyNames.h" +#include "CSSValueKeywords.h" +#include "Chrome.h" +#include "CreateLinkCommand.h" +#include "DocumentFragment.h" +#include "EditorClient.h" +#include "Event.h" +#include "EventHandler.h" +#include "FormatBlockCommand.h" +#include "Frame.h" +#include "FrameView.h" +#include "HTMLFontElement.h" +#include "HTMLHRElement.h" +#include "HTMLImageElement.h" +#include "IndentOutdentCommand.h" +#include "InsertListCommand.h" +#include "KillRing.h" +#include "Page.h" +#include "RenderBox.h" +#include "ReplaceSelectionCommand.h" +#include "Scrollbar.h" +#include "Settings.h" +#include "Sound.h" +#include "TypingCommand.h" +#include "UnlinkCommand.h" +#include "htmlediting.h" +#include "markup.h" +#include <wtf/text/AtomicString.h> + +namespace WebCore { + +using namespace HTMLNames; + +class EditorInternalCommand { +public: + bool (*execute)(Frame*, Event*, EditorCommandSource, const String&); + bool (*isSupportedFromDOM)(Frame*); + bool (*isEnabled)(Frame*, Event*, EditorCommandSource); + TriState (*state)(Frame*, Event*); + String (*value)(Frame*, Event*); + bool isTextInsertion; + bool allowExecutionWhenDisabled; +}; + +typedef HashMap<String, const EditorInternalCommand*, CaseFoldingHash> CommandMap; + +static const bool notTextInsertion = false; +static const bool isTextInsertion = true; + +static const bool allowExecutionWhenDisabled = true; +static const bool doNotAllowExecutionWhenDisabled = false; + +// Related to Editor::selectionForCommand. +// Certain operations continue to use the target control's selection even if the event handler +// already moved the selection outside of the text control. +static Frame* targetFrame(Frame* frame, Event* event) +{ + if (!event) + return frame; + Node* node = event->target()->toNode(); + if (!node) + return frame; + return node->document()->frame(); +} + +static bool applyCommandToFrame(Frame* frame, EditorCommandSource source, EditAction action, CSSMutableStyleDeclaration* style) +{ + // FIXME: We don't call shouldApplyStyle when the source is DOM; is there a good reason for that? + switch (source) { + case CommandFromMenuOrKeyBinding: + frame->editor()->applyStyleToSelection(style, action); + return true; + case CommandFromDOM: + case CommandFromDOMWithUserInterface: + frame->editor()->applyStyle(style); + return true; + } + ASSERT_NOT_REACHED(); + return false; +} + +static bool executeApplyStyle(Frame* frame, EditorCommandSource source, EditAction action, int propertyID, const String& propertyValue) +{ + RefPtr<CSSMutableStyleDeclaration> style = CSSMutableStyleDeclaration::create(); + style->setProperty(propertyID, propertyValue); + return applyCommandToFrame(frame, source, action, style.get()); +} + +static bool executeApplyStyle(Frame* frame, EditorCommandSource source, EditAction action, int propertyID, int propertyValue) +{ + RefPtr<CSSMutableStyleDeclaration> style = CSSMutableStyleDeclaration::create(); + style->setProperty(propertyID, propertyValue); + return applyCommandToFrame(frame, source, action, style.get()); +} + +// FIXME: executeToggleStyleInList does not handle complicated cases such as <b><u>hello</u>world</b> properly. +// This function must use Editor::selectionHasStyle to determine the current style but we cannot fix this +// until https://bugs.webkit.org/show_bug.cgi?id=27818 is resolved. +static bool executeToggleStyleInList(Frame* frame, EditorCommandSource source, EditAction action, int propertyID, CSSValue* value) +{ + ExceptionCode ec = 0; + bool shouldUseFixedFontDefaultSize; + RefPtr<CSSMutableStyleDeclaration> selectionStyle = frame->editor()->selectionComputedStyle(shouldUseFixedFontDefaultSize); + if (!selectionStyle) + return false; + + RefPtr<CSSValue> selectedCSSValue = selectionStyle->getPropertyCSSValue(propertyID); + String newStyle = "none"; + if (selectedCSSValue->isValueList()) { + RefPtr<CSSValueList> selectedCSSValueList = static_cast<CSSValueList*>(selectedCSSValue.get()); + if (!selectedCSSValueList->removeAll(value)) + selectedCSSValueList->append(value); + if (selectedCSSValueList->length()) + newStyle = selectedCSSValueList->cssText(); + + } else if (selectedCSSValue->cssText() == "none") + newStyle = value->cssText(); + + // FIXME: We shouldn't be having to convert new style into text. We should have setPropertyCSSValue. + RefPtr<CSSMutableStyleDeclaration> newMutableStyle = CSSMutableStyleDeclaration::create(); + newMutableStyle->setProperty(propertyID, newStyle, ec); + return applyCommandToFrame(frame, source, action, newMutableStyle.get()); +} + +static bool executeToggleStyle(Frame* frame, EditorCommandSource source, EditAction action, int propertyID, const char* offValue, const char* onValue) +{ + RefPtr<CSSMutableStyleDeclaration> style = CSSMutableStyleDeclaration::create(); + style->setProperty(propertyID, onValue); // We need to add this style to pass it to selectionStartHasStyle / selectionHasStyle + + // Style is considered present when + // Mac: present at the beginning of selection + // other: present throughout the selection + + bool styleIsPresent; + if (frame->editor()->behavior().shouldToggleStyleBasedOnStartOfSelection()) + styleIsPresent = frame->editor()->selectionStartHasStyle(style.get()); + else + styleIsPresent = frame->editor()->selectionHasStyle(style.get()) == TrueTriState; + + style->setProperty(propertyID, styleIsPresent ? offValue : onValue); + return applyCommandToFrame(frame, source, action, style.get()); +} + +static bool executeApplyParagraphStyle(Frame* frame, EditorCommandSource source, EditAction action, int propertyID, const String& propertyValue) +{ + RefPtr<CSSMutableStyleDeclaration> style = CSSMutableStyleDeclaration::create(); + style->setProperty(propertyID, propertyValue); + // FIXME: We don't call shouldApplyStyle when the source is DOM; is there a good reason for that? + switch (source) { + case CommandFromMenuOrKeyBinding: + frame->editor()->applyParagraphStyleToSelection(style.get(), action); + return true; + case CommandFromDOM: + case CommandFromDOMWithUserInterface: + frame->editor()->applyParagraphStyle(style.get()); + return true; + } + ASSERT_NOT_REACHED(); + return false; +} + +static bool executeInsertFragment(Frame* frame, PassRefPtr<DocumentFragment> fragment) +{ + applyCommand(ReplaceSelectionCommand::create(frame->document(), fragment, + false, false, false, true, false, EditActionUnspecified)); + return true; +} + +static bool executeInsertNode(Frame* frame, PassRefPtr<Node> content) +{ + RefPtr<DocumentFragment> fragment = DocumentFragment::create(frame->document()); + ExceptionCode ec = 0; + fragment->appendChild(content, ec); + if (ec) + return false; + return executeInsertFragment(frame, fragment.release()); +} + +static bool expandSelectionToGranularity(Frame* frame, TextGranularity granularity) +{ + VisibleSelection selection = frame->selection()->selection(); + selection.expandUsingGranularity(granularity); + RefPtr<Range> newRange = selection.toNormalizedRange(); + if (!newRange) + return false; + ExceptionCode ec = 0; + if (newRange->collapsed(ec)) + return false; + RefPtr<Range> oldRange = frame->selection()->selection().toNormalizedRange(); + EAffinity affinity = frame->selection()->affinity(); + if (!frame->editor()->client()->shouldChangeSelectedRange(oldRange.get(), newRange.get(), affinity, false)) + return false; + frame->selection()->setSelectedRange(newRange.get(), affinity, true); + return true; +} + +static TriState stateStyle(Frame* frame, int propertyID, const char* desiredValue) +{ + RefPtr<CSSMutableStyleDeclaration> style = CSSMutableStyleDeclaration::create(); + style->setProperty(propertyID, desiredValue); + + if (frame->editor()->behavior().shouldToggleStyleBasedOnStartOfSelection()) + return frame->editor()->selectionStartHasStyle(style.get()) ? TrueTriState : FalseTriState; + return frame->editor()->selectionHasStyle(style.get()); +} + +static String valueStyle(Frame* frame, int propertyID) +{ + // FIXME: Rather than retrieving the style at the start of the current selection, + // we should retrieve the style present throughout the selection for non-Mac platforms. + return frame->editor()->selectionStartCSSPropertyValue(propertyID); +} + +static TriState stateTextWritingDirection(Frame* frame, WritingDirection direction) +{ + bool hasNestedOrMultipleEmbeddings; + WritingDirection selectionDirection = frame->editor()->textDirectionForSelection(hasNestedOrMultipleEmbeddings); + return (selectionDirection == direction && !hasNestedOrMultipleEmbeddings) ? TrueTriState : FalseTriState; +} + +static int verticalScrollDistance(Frame* frame) +{ + Node* focusedNode = frame->document()->focusedNode(); + if (!focusedNode) + return 0; + RenderObject* renderer = focusedNode->renderer(); + if (!renderer || !renderer->isBox()) + return 0; + RenderStyle* style = renderer->style(); + if (!style) + return 0; + if (!(style->overflowY() == OSCROLL || style->overflowY() == OAUTO || focusedNode->isContentEditable())) + return 0; + int height = std::min<int>(toRenderBox(renderer)->clientHeight(), + frame->view()->visibleHeight()); + return max(max<int>(height * Scrollbar::minFractionToStepWhenPaging(), height - Scrollbar::maxOverlapBetweenPages()), 1); +} + +static RefPtr<Range> unionDOMRanges(Range* a, Range* b) +{ + ExceptionCode ec = 0; + Range* start = a->compareBoundaryPoints(Range::START_TO_START, b, ec) <= 0 ? a : b; + ASSERT(!ec); + Range* end = a->compareBoundaryPoints(Range::END_TO_END, b, ec) <= 0 ? b : a; + ASSERT(!ec); + + return Range::create(a->startContainer(ec)->ownerDocument(), start->startContainer(ec), start->startOffset(ec), end->endContainer(ec), end->endOffset(ec)); +} + +// Execute command functions + +static bool executeBackColor(Frame* frame, Event*, EditorCommandSource source, const String& value) +{ + return executeApplyStyle(frame, source, EditActionSetBackgroundColor, CSSPropertyBackgroundColor, value); +} + +static bool executeCopy(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->editor()->copy(); + return true; +} + +static bool executeCreateLink(Frame* frame, Event*, EditorCommandSource, const String& value) +{ + // FIXME: If userInterface is true, we should display a dialog box to let the user enter a URL. + if (value.isEmpty()) + return false; + applyCommand(CreateLinkCommand::create(frame->document(), value)); + return true; +} + +static bool executeCut(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->editor()->cut(); + return true; +} + +static bool executeDelete(Frame* frame, Event*, EditorCommandSource source, const String&) +{ + switch (source) { + case CommandFromMenuOrKeyBinding: + // Doesn't modify the text if the current selection isn't a range. + frame->editor()->performDelete(); + return true; + case CommandFromDOM: + case CommandFromDOMWithUserInterface: + // If the current selection is a caret, delete the preceding character. IE performs forwardDelete, but we currently side with Firefox. + // Doesn't scroll to make the selection visible, or modify the kill ring (this time, siding with IE, not Firefox). + TypingCommand::deleteKeyPressed(frame->document(), frame->selection()->granularity() == WordGranularity); + return true; + } + ASSERT_NOT_REACHED(); + return false; +} + +static bool executeDeleteBackward(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->editor()->deleteWithDirection(DirectionBackward, CharacterGranularity, false, true); + return true; +} + +static bool executeDeleteBackwardByDecomposingPreviousCharacter(Frame* frame, Event*, EditorCommandSource, const String&) +{ + LOG_ERROR("DeleteBackwardByDecomposingPreviousCharacter is not implemented, doing DeleteBackward instead"); + frame->editor()->deleteWithDirection(DirectionBackward, CharacterGranularity, false, true); + return true; +} + +static bool executeDeleteForward(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->editor()->deleteWithDirection(DirectionForward, CharacterGranularity, false, true); + return true; +} + +static bool executeDeleteToBeginningOfLine(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->editor()->deleteWithDirection(DirectionBackward, LineBoundary, true, false); + return true; +} + +static bool executeDeleteToBeginningOfParagraph(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->editor()->deleteWithDirection(DirectionBackward, ParagraphBoundary, true, false); + return true; +} + +static bool executeDeleteToEndOfLine(Frame* frame, Event*, EditorCommandSource, const String&) +{ + // Despite its name, this command should delete the newline at the end of + // a paragraph if you are at the end of a paragraph (like DeleteToEndOfParagraph). + frame->editor()->deleteWithDirection(DirectionForward, LineBoundary, true, false); + return true; +} + +static bool executeDeleteToEndOfParagraph(Frame* frame, Event*, EditorCommandSource, const String&) +{ + // Despite its name, this command should delete the newline at the end of + // a paragraph if you are at the end of a paragraph. + frame->editor()->deleteWithDirection(DirectionForward, ParagraphBoundary, true, false); + return true; +} + +static bool executeDeleteToMark(Frame* frame, Event*, EditorCommandSource, const String&) +{ + RefPtr<Range> mark = frame->editor()->mark().toNormalizedRange(); + if (mark) { + SelectionController* selection = frame->selection(); + bool selected = selection->setSelectedRange(unionDOMRanges(mark.get(), frame->editor()->selectedRange().get()).get(), DOWNSTREAM, true); + ASSERT(selected); + if (!selected) + return false; + } + frame->editor()->performDelete(); + frame->editor()->setMark(frame->selection()->selection()); + return true; +} + +static bool executeDeleteWordBackward(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->editor()->deleteWithDirection(DirectionBackward, WordGranularity, true, false); + return true; +} + +static bool executeDeleteWordForward(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->editor()->deleteWithDirection(DirectionForward, WordGranularity, true, false); + return true; +} + +static bool executeFindString(Frame* frame, Event*, EditorCommandSource, const String& value) +{ + return frame->editor()->findString(value, true, false, true, false); +} + +static bool executeFontName(Frame* frame, Event*, EditorCommandSource source, const String& value) +{ + return executeApplyStyle(frame, source, EditActionSetFont, CSSPropertyFontFamily, value); +} + +static bool executeFontSize(Frame* frame, Event*, EditorCommandSource source, const String& value) +{ + int size; + if (!HTMLFontElement::cssValueFromFontSizeNumber(value, size)) + return false; + return executeApplyStyle(frame, source, EditActionChangeAttributes, CSSPropertyFontSize, size); +} + +static bool executeFontSizeDelta(Frame* frame, Event*, EditorCommandSource source, const String& value) +{ + return executeApplyStyle(frame, source, EditActionChangeAttributes, CSSPropertyWebkitFontSizeDelta, value); +} + +static bool executeForeColor(Frame* frame, Event*, EditorCommandSource source, const String& value) +{ + return executeApplyStyle(frame, source, EditActionSetColor, CSSPropertyColor, value); +} + +static bool executeFormatBlock(Frame* frame, Event*, EditorCommandSource, const String& value) +{ + String tagName = value.lower(); + if (tagName[0] == '<' && tagName[tagName.length() - 1] == '>') + tagName = tagName.substring(1, tagName.length() - 2); + + ExceptionCode ec; + String localName, prefix; + if (!Document::parseQualifiedName(tagName, prefix, localName, ec)) + return false; + QualifiedName qualifiedTagName(prefix, localName, xhtmlNamespaceURI); + + RefPtr<FormatBlockCommand> command = FormatBlockCommand::create(frame->document(), qualifiedTagName); + applyCommand(command); + return command->didApply(); +} + +static bool executeForwardDelete(Frame* frame, Event*, EditorCommandSource source, const String&) +{ + switch (source) { + case CommandFromMenuOrKeyBinding: + frame->editor()->deleteWithDirection(DirectionForward, CharacterGranularity, false, true); + return true; + case CommandFromDOM: + case CommandFromDOMWithUserInterface: + // Doesn't scroll to make the selection visible, or modify the kill ring. + // ForwardDelete is not implemented in IE or Firefox, so this behavior is only needed for + // backward compatibility with ourselves, and for consistency with Delete. + TypingCommand::forwardDeleteKeyPressed(frame->document()); + return true; + } + ASSERT_NOT_REACHED(); + return false; +} + +static bool executeIgnoreSpelling(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->editor()->ignoreSpelling(); + return true; +} + +static bool executeIndent(Frame* frame, Event*, EditorCommandSource, const String&) +{ + applyCommand(IndentOutdentCommand::create(frame->document(), IndentOutdentCommand::Indent)); + return true; +} + +static bool executeInsertBacktab(Frame* frame, Event* event, EditorCommandSource, const String&) +{ + return targetFrame(frame, event)->eventHandler()->handleTextInputEvent("\t", event, false, true); +} + +static bool executeInsertHorizontalRule(Frame* frame, Event*, EditorCommandSource, const String& value) +{ + RefPtr<HTMLHRElement> rule = HTMLHRElement::create(frame->document()); + if (!value.isEmpty()) + rule->setIdAttribute(value); + return executeInsertNode(frame, rule.release()); +} + +static bool executeInsertHTML(Frame* frame, Event*, EditorCommandSource, const String& value) +{ + return executeInsertFragment(frame, createFragmentFromMarkup(frame->document(), value, "")); +} + +static bool executeInsertImage(Frame* frame, Event*, EditorCommandSource, const String& value) +{ + // FIXME: If userInterface is true, we should display a dialog box and let the user choose a local image. + RefPtr<HTMLImageElement> image = HTMLImageElement::create(frame->document()); + image->setSrc(value); + return executeInsertNode(frame, image.release()); +} + +static bool executeInsertLineBreak(Frame* frame, Event* event, EditorCommandSource source, const String&) +{ + switch (source) { + case CommandFromMenuOrKeyBinding: + return targetFrame(frame, event)->eventHandler()->handleTextInputEvent("\n", event, true); + case CommandFromDOM: + case CommandFromDOMWithUserInterface: + // Doesn't scroll to make the selection visible, or modify the kill ring. + // InsertLineBreak is not implemented in IE or Firefox, so this behavior is only needed for + // backward compatibility with ourselves, and for consistency with other commands. + TypingCommand::insertLineBreak(frame->document()); + return true; + } + ASSERT_NOT_REACHED(); + return false; +} + +static bool executeInsertNewline(Frame* frame, Event* event, EditorCommandSource, const String&) +{ + Frame* targetFrame = WebCore::targetFrame(frame, event); + return targetFrame->eventHandler()->handleTextInputEvent("\n", event, !targetFrame->editor()->canEditRichly()); +} + +static bool executeInsertNewlineInQuotedContent(Frame* frame, Event*, EditorCommandSource, const String&) +{ + TypingCommand::insertParagraphSeparatorInQuotedContent(frame->document()); + return true; +} + +static bool executeInsertOrderedList(Frame* frame, Event*, EditorCommandSource, const String&) +{ + applyCommand(InsertListCommand::create(frame->document(), InsertListCommand::OrderedList)); + return true; +} + +static bool executeInsertParagraph(Frame* frame, Event*, EditorCommandSource, const String&) +{ + TypingCommand::insertParagraphSeparator(frame->document()); + return true; +} + +static bool executeInsertTab(Frame* frame, Event* event, EditorCommandSource, const String&) +{ + return targetFrame(frame, event)->eventHandler()->handleTextInputEvent("\t", event, false, false); +} + +static bool executeInsertText(Frame* frame, Event*, EditorCommandSource, const String& value) +{ + TypingCommand::insertText(frame->document(), value); + return true; +} + +static bool executeInsertUnorderedList(Frame* frame, Event*, EditorCommandSource, const String&) +{ + applyCommand(InsertListCommand::create(frame->document(), InsertListCommand::UnorderedList)); + return true; +} + +static bool executeJustifyCenter(Frame* frame, Event*, EditorCommandSource source, const String&) +{ + return executeApplyParagraphStyle(frame, source, EditActionCenter, CSSPropertyTextAlign, "center"); +} + +static bool executeJustifyFull(Frame* frame, Event*, EditorCommandSource source, const String&) +{ + return executeApplyParagraphStyle(frame, source, EditActionJustify, CSSPropertyTextAlign, "justify"); +} + +static bool executeJustifyLeft(Frame* frame, Event*, EditorCommandSource source, const String&) +{ + return executeApplyParagraphStyle(frame, source, EditActionAlignLeft, CSSPropertyTextAlign, "left"); +} + +static bool executeJustifyRight(Frame* frame, Event*, EditorCommandSource source, const String&) +{ + return executeApplyParagraphStyle(frame, source, EditActionAlignRight, CSSPropertyTextAlign, "right"); +} + +static bool executeMakeTextWritingDirectionLeftToRight(Frame* frame, Event*, EditorCommandSource, const String&) +{ + RefPtr<CSSMutableStyleDeclaration> style = CSSMutableStyleDeclaration::create(); + style->setProperty(CSSPropertyUnicodeBidi, CSSValueEmbed); + style->setProperty(CSSPropertyDirection, CSSValueLtr); + frame->editor()->applyStyle(style.get(), EditActionSetWritingDirection); + return true; +} + +static bool executeMakeTextWritingDirectionNatural(Frame* frame, Event*, EditorCommandSource, const String&) +{ + RefPtr<CSSMutableStyleDeclaration> style = CSSMutableStyleDeclaration::create(); + style->setProperty(CSSPropertyUnicodeBidi, CSSValueNormal); + frame->editor()->applyStyle(style.get(), EditActionSetWritingDirection); + return true; +} + +static bool executeMakeTextWritingDirectionRightToLeft(Frame* frame, Event*, EditorCommandSource, const String&) +{ + RefPtr<CSSMutableStyleDeclaration> style = CSSMutableStyleDeclaration::create(); + style->setProperty(CSSPropertyUnicodeBidi, CSSValueEmbed); + style->setProperty(CSSPropertyDirection, CSSValueRtl); + frame->editor()->applyStyle(style.get(), EditActionSetWritingDirection); + return true; +} + +static bool executeMoveBackward(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->selection()->modify(SelectionController::AlterationMove, DirectionBackward, CharacterGranularity, true); + return true; +} + +static bool executeMoveBackwardAndModifySelection(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->selection()->modify(SelectionController::AlterationExtend, DirectionBackward, CharacterGranularity, true); + return true; +} + +static bool executeMoveDown(Frame* frame, Event*, EditorCommandSource, const String&) +{ + return frame->selection()->modify(SelectionController::AlterationMove, DirectionForward, LineGranularity, true); +} + +static bool executeMoveDownAndModifySelection(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->selection()->modify(SelectionController::AlterationExtend, DirectionForward, LineGranularity, true); + return true; +} + +static bool executeMoveForward(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->selection()->modify(SelectionController::AlterationMove, DirectionForward, CharacterGranularity, true); + return true; +} + +static bool executeMoveForwardAndModifySelection(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->selection()->modify(SelectionController::AlterationExtend, DirectionForward, CharacterGranularity, true); + return true; +} + +static bool executeMoveLeft(Frame* frame, Event*, EditorCommandSource, const String&) +{ + return frame->selection()->modify(SelectionController::AlterationMove, DirectionLeft, CharacterGranularity, true); +} + +static bool executeMoveLeftAndModifySelection(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->selection()->modify(SelectionController::AlterationExtend, DirectionLeft, CharacterGranularity, true); + return true; +} + +static bool executeMovePageDown(Frame* frame, Event*, EditorCommandSource, const String&) +{ + int distance = verticalScrollDistance(frame); + if (!distance) + return false; + return frame->selection()->modify(SelectionController::AlterationMove, distance, true, SelectionController::AlignCursorOnScrollAlways); +} + +static bool executeMovePageDownAndModifySelection(Frame* frame, Event*, EditorCommandSource, const String&) +{ + int distance = verticalScrollDistance(frame); + if (!distance) + return false; + return frame->selection()->modify(SelectionController::AlterationExtend, distance, true, SelectionController::AlignCursorOnScrollAlways); +} + +static bool executeMovePageUp(Frame* frame, Event*, EditorCommandSource, const String&) +{ + int distance = verticalScrollDistance(frame); + if (!distance) + return false; + return frame->selection()->modify(SelectionController::AlterationMove, -distance, true, SelectionController::AlignCursorOnScrollAlways); +} + +static bool executeMovePageUpAndModifySelection(Frame* frame, Event*, EditorCommandSource, const String&) +{ + int distance = verticalScrollDistance(frame); + if (!distance) + return false; + return frame->selection()->modify(SelectionController::AlterationExtend, -distance, true, SelectionController::AlignCursorOnScrollAlways); +} + +static bool executeMoveRight(Frame* frame, Event*, EditorCommandSource, const String&) +{ + return frame->selection()->modify(SelectionController::AlterationMove, DirectionRight, CharacterGranularity, true); +} + +static bool executeMoveRightAndModifySelection(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->selection()->modify(SelectionController::AlterationExtend, DirectionRight, CharacterGranularity, true); + return true; +} + +static bool executeMoveToBeginningOfDocument(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->selection()->modify(SelectionController::AlterationMove, DirectionBackward, DocumentBoundary, true); + return true; +} + +static bool executeMoveToBeginningOfDocumentAndModifySelection(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->selection()->modify(SelectionController::AlterationExtend, DirectionBackward, DocumentBoundary, true); + return true; +} + +static bool executeMoveToBeginningOfLine(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->selection()->modify(SelectionController::AlterationMove, DirectionBackward, LineBoundary, true); + return true; +} + +static bool executeMoveToBeginningOfLineAndModifySelection(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->selection()->modify(SelectionController::AlterationExtend, DirectionBackward, LineBoundary, true); + return true; +} + +static bool executeMoveToBeginningOfParagraph(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->selection()->modify(SelectionController::AlterationMove, DirectionBackward, ParagraphBoundary, true); + return true; +} + +static bool executeMoveToBeginningOfParagraphAndModifySelection(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->selection()->modify(SelectionController::AlterationExtend, DirectionBackward, ParagraphBoundary, true); + return true; +} + +static bool executeMoveToBeginningOfSentence(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->selection()->modify(SelectionController::AlterationMove, DirectionBackward, SentenceBoundary, true); + return true; +} + +static bool executeMoveToBeginningOfSentenceAndModifySelection(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->selection()->modify(SelectionController::AlterationExtend, DirectionBackward, SentenceBoundary, true); + return true; +} + +static bool executeMoveToEndOfDocument(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->selection()->modify(SelectionController::AlterationMove, DirectionForward, DocumentBoundary, true); + return true; +} + +static bool executeMoveToEndOfDocumentAndModifySelection(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->selection()->modify(SelectionController::AlterationExtend, DirectionForward, DocumentBoundary, true); + return true; +} + +static bool executeMoveToEndOfSentence(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->selection()->modify(SelectionController::AlterationMove, DirectionForward, SentenceBoundary, true); + return true; +} + +static bool executeMoveToEndOfSentenceAndModifySelection(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->selection()->modify(SelectionController::AlterationExtend, DirectionForward, SentenceBoundary, true); + return true; +} + +static bool executeMoveToEndOfLine(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->selection()->modify(SelectionController::AlterationMove, DirectionForward, LineBoundary, true); + return true; +} + +static bool executeMoveToEndOfLineAndModifySelection(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->selection()->modify(SelectionController::AlterationExtend, DirectionForward, LineBoundary, true); + return true; +} + +static bool executeMoveToEndOfParagraph(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->selection()->modify(SelectionController::AlterationMove, DirectionForward, ParagraphBoundary, true); + return true; +} + +static bool executeMoveToEndOfParagraphAndModifySelection(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->selection()->modify(SelectionController::AlterationExtend, DirectionForward, ParagraphBoundary, true); + return true; +} + +static bool executeMoveParagraphBackwardAndModifySelection(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->selection()->modify(SelectionController::AlterationExtend, DirectionBackward, ParagraphGranularity, true); + return true; +} + +static bool executeMoveParagraphForwardAndModifySelection(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->selection()->modify(SelectionController::AlterationExtend, DirectionForward, ParagraphGranularity, true); + return true; +} + +static bool executeMoveUp(Frame* frame, Event*, EditorCommandSource, const String&) +{ + return frame->selection()->modify(SelectionController::AlterationMove, DirectionBackward, LineGranularity, true); +} + +static bool executeMoveUpAndModifySelection(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->selection()->modify(SelectionController::AlterationExtend, DirectionBackward, LineGranularity, true); + return true; +} + +static bool executeMoveWordBackward(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->selection()->modify(SelectionController::AlterationMove, DirectionBackward, WordGranularity, true); + return true; +} + +static bool executeMoveWordBackwardAndModifySelection(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->selection()->modify(SelectionController::AlterationExtend, DirectionBackward, WordGranularity, true); + return true; +} + +static bool executeMoveWordForward(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->selection()->modify(SelectionController::AlterationMove, DirectionForward, WordGranularity, true); + return true; +} + +static bool executeMoveWordForwardAndModifySelection(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->selection()->modify(SelectionController::AlterationExtend, DirectionForward, WordGranularity, true); + return true; +} + +static bool executeMoveWordLeft(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->selection()->modify(SelectionController::AlterationMove, DirectionLeft, WordGranularity, true); + return true; +} + +static bool executeMoveWordLeftAndModifySelection(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->selection()->modify(SelectionController::AlterationExtend, DirectionLeft, WordGranularity, true); + return true; +} + +static bool executeMoveWordRight(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->selection()->modify(SelectionController::AlterationMove, DirectionRight, WordGranularity, true); + return true; +} + +static bool executeMoveWordRightAndModifySelection(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->selection()->modify(SelectionController::AlterationExtend, DirectionRight, WordGranularity, true); + return true; +} + +static bool executeMoveToLeftEndOfLine(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->selection()->modify(SelectionController::AlterationMove, DirectionLeft, LineBoundary, true); + return true; +} + +static bool executeMoveToLeftEndOfLineAndModifySelection(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->selection()->modify(SelectionController::AlterationExtend, DirectionLeft, LineBoundary, true); + return true; +} + +static bool executeMoveToRightEndOfLine(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->selection()->modify(SelectionController::AlterationMove, DirectionRight, LineBoundary, true); + return true; +} + +static bool executeMoveToRightEndOfLineAndModifySelection(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->selection()->modify(SelectionController::AlterationExtend, DirectionRight, LineBoundary, true); + return true; +} + +static bool executeOutdent(Frame* frame, Event*, EditorCommandSource, const String&) +{ + applyCommand(IndentOutdentCommand::create(frame->document(), IndentOutdentCommand::Outdent)); + return true; +} + +static bool executePaste(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->editor()->paste(); + return true; +} + +static bool executePasteAndMatchStyle(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->editor()->pasteAsPlainText(); + return true; +} + +static bool executePasteAsPlainText(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->editor()->pasteAsPlainText(); + return true; +} + +static bool executePrint(Frame* frame, Event*, EditorCommandSource, const String&) +{ + Page* page = frame->page(); + if (!page) + return false; + page->chrome()->print(frame); + return true; +} + +static bool executeRedo(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->editor()->redo(); + return true; +} + +static bool executeRemoveFormat(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->editor()->removeFormattingAndStyle(); + return true; +} + +static bool executeSelectAll(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->selection()->selectAll(); + return true; +} + +static bool executeSelectLine(Frame* frame, Event*, EditorCommandSource, const String&) +{ + return expandSelectionToGranularity(frame, LineGranularity); +} + +static bool executeSelectParagraph(Frame* frame, Event*, EditorCommandSource, const String&) +{ + return expandSelectionToGranularity(frame, ParagraphGranularity); +} + +static bool executeSelectSentence(Frame* frame, Event*, EditorCommandSource, const String&) +{ + return expandSelectionToGranularity(frame, SentenceGranularity); +} + +static bool executeSelectToMark(Frame* frame, Event*, EditorCommandSource, const String&) +{ + RefPtr<Range> mark = frame->editor()->mark().toNormalizedRange(); + RefPtr<Range> selection = frame->editor()->selectedRange(); + if (!mark || !selection) { + systemBeep(); + return false; + } + frame->selection()->setSelectedRange(unionDOMRanges(mark.get(), selection.get()).get(), DOWNSTREAM, true); + return true; +} + +static bool executeSelectWord(Frame* frame, Event*, EditorCommandSource, const String&) +{ + return expandSelectionToGranularity(frame, WordGranularity); +} + +static bool executeSetMark(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->editor()->setMark(frame->selection()->selection()); + return true; +} + +static bool executeStrikethrough(Frame* frame, Event*, EditorCommandSource source, const String&) +{ + RefPtr<CSSPrimitiveValue> lineThrough = CSSPrimitiveValue::createIdentifier(CSSValueLineThrough); + return executeToggleStyleInList(frame, source, EditActionUnderline, CSSPropertyWebkitTextDecorationsInEffect, lineThrough.get()); +} + +static bool executeStyleWithCSS(Frame* frame, Event*, EditorCommandSource, const String& value) +{ + if (value != "false" && value != "true") + return false; + + frame->editor()->setShouldStyleWithCSS(value == "true" ? true : false); + return true; +} + +static bool executeSubscript(Frame* frame, Event*, EditorCommandSource source, const String&) +{ + return executeToggleStyle(frame, source, EditActionSubscript, CSSPropertyVerticalAlign, "baseline", "sub"); +} + +static bool executeSuperscript(Frame* frame, Event*, EditorCommandSource source, const String&) +{ + return executeToggleStyle(frame, source, EditActionSuperscript, CSSPropertyVerticalAlign, "baseline", "super"); +} + +static bool executeSwapWithMark(Frame* frame, Event*, EditorCommandSource, const String&) +{ + const VisibleSelection& mark = frame->editor()->mark(); + const VisibleSelection& selection = frame->selection()->selection(); + if (mark.isNone() || selection.isNone()) { + systemBeep(); + return false; + } + frame->selection()->setSelection(mark); + frame->editor()->setMark(selection); + return true; +} + +#if PLATFORM(MAC) +static bool executeTakeFindStringFromSelection(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->editor()->takeFindStringFromSelection(); + return true; +} +#endif + +static bool executeToggleBold(Frame* frame, Event*, EditorCommandSource source, const String&) +{ + return executeToggleStyle(frame, source, EditActionChangeAttributes, CSSPropertyFontWeight, "normal", "bold"); +} + +static bool executeToggleItalic(Frame* frame, Event*, EditorCommandSource source, const String&) +{ + return executeToggleStyle(frame, source, EditActionChangeAttributes, CSSPropertyFontStyle, "normal", "italic"); +} + +static bool executeTranspose(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->editor()->transpose(); + return true; +} + +static bool executeUnderline(Frame* frame, Event*, EditorCommandSource source, const String&) +{ + RefPtr<CSSPrimitiveValue> underline = CSSPrimitiveValue::createIdentifier(CSSValueUnderline); + return executeToggleStyleInList(frame, source, EditActionUnderline, CSSPropertyWebkitTextDecorationsInEffect, underline.get()); +} + +static bool executeUndo(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->editor()->undo(); + return true; +} + +static bool executeUnlink(Frame* frame, Event*, EditorCommandSource, const String&) +{ + applyCommand(UnlinkCommand::create(frame->document())); + return true; +} + +static bool executeUnscript(Frame* frame, Event*, EditorCommandSource source, const String&) +{ + return executeApplyStyle(frame, source, EditActionUnscript, CSSPropertyVerticalAlign, "baseline"); +} + +static bool executeUnselect(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->selection()->clear(); + return true; +} + +static bool executeYank(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->editor()->insertTextWithoutSendingTextEvent(frame->editor()->killRing()->yank(), false, 0); + frame->editor()->killRing()->setToYankedState(); + return true; +} + +static bool executeYankAndSelect(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->editor()->insertTextWithoutSendingTextEvent(frame->editor()->killRing()->yank(), true, 0); + frame->editor()->killRing()->setToYankedState(); + return true; +} + +#if SUPPORT_AUTOCORRECTION_PANEL +static bool executeCancelOperation(Frame* frame, Event*, EditorCommandSource, const String&) +{ + frame->editor()->handleCancelOperation(); + return true; +} +#endif + +// Supported functions + +static bool supported(Frame*) +{ + return true; +} + +static bool supportedFromMenuOrKeyBinding(Frame*) +{ + return false; +} + +static bool supportedCopyCut(Frame* frame) +{ + Settings* settings = frame ? frame->settings() : 0; + return settings && settings->javaScriptCanAccessClipboard(); +} + +static bool supportedPaste(Frame* frame) +{ + Settings* settings = frame ? frame->settings() : 0; + return settings && (settings->javaScriptCanAccessClipboard() ? settings->isDOMPasteAllowed() : 0); +} + +// Enabled functions + +static bool enabled(Frame*, Event*, EditorCommandSource) +{ + return true; +} + +static bool enabledVisibleSelection(Frame* frame, Event* event, EditorCommandSource) +{ + // The term "visible" here includes a caret in editable text or a range in any text. + const VisibleSelection& selection = frame->editor()->selectionForCommand(event); + return (selection.isCaret() && selection.isContentEditable()) || selection.isRange(); +} + +static bool caretBrowsingEnabled(Frame* frame) +{ + return frame->settings() && frame->settings()->caretBrowsingEnabled(); +} + +static EditorCommandSource dummyEditorCommandSource = static_cast<EditorCommandSource>(0); + +static bool enabledVisibleSelectionOrCaretBrowsing(Frame* frame, Event* event, EditorCommandSource) +{ + // The EditorCommandSource parameter is unused in enabledVisibleSelection, so just pass a dummy variable + return caretBrowsingEnabled(frame) || enabledVisibleSelection(frame, event, dummyEditorCommandSource); +} + +static bool enabledVisibleSelectionAndMark(Frame* frame, Event* event, EditorCommandSource) +{ + const VisibleSelection& selection = frame->editor()->selectionForCommand(event); + return ((selection.isCaret() && selection.isContentEditable()) || selection.isRange()) + && frame->editor()->mark().isCaretOrRange(); +} + +static bool enableCaretInEditableText(Frame* frame, Event* event, EditorCommandSource) +{ + const VisibleSelection& selection = frame->editor()->selectionForCommand(event); + return selection.isCaret() && selection.isContentEditable(); +} + +static bool enabledCopy(Frame* frame, Event*, EditorCommandSource) +{ + return frame->editor()->canDHTMLCopy() || frame->editor()->canCopy(); +} + +static bool enabledCut(Frame* frame, Event*, EditorCommandSource) +{ + return frame->editor()->canDHTMLCut() || frame->editor()->canCut(); +} + +static bool enabledDelete(Frame* frame, Event* event, EditorCommandSource source) +{ + switch (source) { + case CommandFromMenuOrKeyBinding: + // "Delete" from menu only affects selected range, just like Cut but without affecting pasteboard + return frame->editor()->canDHTMLCut() || frame->editor()->canCut(); + case CommandFromDOM: + case CommandFromDOMWithUserInterface: + // "Delete" from DOM is like delete/backspace keypress, affects selected range if non-empty, + // otherwise removes a character + return frame->editor()->selectionForCommand(event).isContentEditable(); + } + ASSERT_NOT_REACHED(); + return false; +} + +static bool enabledInEditableText(Frame* frame, Event* event, EditorCommandSource) +{ + return frame->editor()->selectionForCommand(event).isContentEditable(); +} + +static bool enabledInEditableTextOrCaretBrowsing(Frame* frame, Event* event, EditorCommandSource) +{ + // The EditorCommandSource parameter is unused in enabledInEditableText, so just pass a dummy variable + return caretBrowsingEnabled(frame) || enabledInEditableText(frame, event, dummyEditorCommandSource); +} + +static bool enabledInRichlyEditableText(Frame* frame, Event*, EditorCommandSource) +{ + return frame->selection()->isCaretOrRange() && frame->selection()->isContentRichlyEditable(); +} + +static bool enabledPaste(Frame* frame, Event*, EditorCommandSource) +{ + return frame->editor()->canPaste(); +} + +static bool enabledRangeInEditableText(Frame* frame, Event*, EditorCommandSource) +{ + return frame->selection()->isRange() && frame->selection()->isContentEditable(); +} + +static bool enabledRangeInRichlyEditableText(Frame* frame, Event*, EditorCommandSource) +{ + return frame->selection()->isRange() && frame->selection()->isContentRichlyEditable(); +} + +static bool enabledRedo(Frame* frame, Event*, EditorCommandSource) +{ + return frame->editor()->canRedo(); +} + +#if PLATFORM(MAC) +static bool enabledTakeFindStringFromSelection(Frame* frame, Event*, EditorCommandSource) +{ + return frame->editor()->canCopyExcludingStandaloneImages(); +} +#endif + +static bool enabledUndo(Frame* frame, Event*, EditorCommandSource) +{ + return frame->editor()->canUndo(); +} + +#if SUPPORT_AUTOCORRECTION_PANEL +static bool enabledDismissCorrectionPanel(Frame* frame, Event*, EditorCommandSource) +{ + return frame->editor()->isShowingCorrectionPanel(); +} +#endif + +// State functions + +static TriState stateNone(Frame*, Event*) +{ + return FalseTriState; +} + +static TriState stateBold(Frame* frame, Event*) +{ + return stateStyle(frame, CSSPropertyFontWeight, "bold"); +} + +static TriState stateItalic(Frame* frame, Event*) +{ + return stateStyle(frame, CSSPropertyFontStyle, "italic"); +} + +static TriState stateOrderedList(Frame* frame, Event*) +{ + return frame->editor()->selectionOrderedListState(); +} + +static TriState stateStrikethrough(Frame* frame, Event*) +{ + return stateStyle(frame, CSSPropertyWebkitTextDecorationsInEffect, "line-through"); +} + +static TriState stateStyleWithCSS(Frame* frame, Event*) +{ + return frame->editor()->shouldStyleWithCSS() ? TrueTriState : FalseTriState; +} + +static TriState stateSubscript(Frame* frame, Event*) +{ + return stateStyle(frame, CSSPropertyVerticalAlign, "sub"); +} + +static TriState stateSuperscript(Frame* frame, Event*) +{ + return stateStyle(frame, CSSPropertyVerticalAlign, "super"); +} + +static TriState stateTextWritingDirectionLeftToRight(Frame* frame, Event*) +{ + return stateTextWritingDirection(frame, LeftToRightWritingDirection); +} + +static TriState stateTextWritingDirectionNatural(Frame* frame, Event*) +{ + return stateTextWritingDirection(frame, NaturalWritingDirection); +} + +static TriState stateTextWritingDirectionRightToLeft(Frame* frame, Event*) +{ + return stateTextWritingDirection(frame, RightToLeftWritingDirection); +} + +static TriState stateUnderline(Frame* frame, Event*) +{ + return stateStyle(frame, CSSPropertyWebkitTextDecorationsInEffect, "underline"); +} + +static TriState stateUnorderedList(Frame* frame, Event*) +{ + return frame->editor()->selectionUnorderedListState(); +} + +static TriState stateJustifyCenter(Frame* frame, Event*) +{ + return stateStyle(frame, CSSPropertyTextAlign, "center"); +} + +static TriState stateJustifyFull(Frame* frame, Event*) +{ + return stateStyle(frame, CSSPropertyTextAlign, "justify"); +} + +static TriState stateJustifyLeft(Frame* frame, Event*) +{ + return stateStyle(frame, CSSPropertyTextAlign, "left"); +} + +static TriState stateJustifyRight(Frame* frame, Event*) +{ + return stateStyle(frame, CSSPropertyTextAlign, "right"); +} + +// Value functions + +static String valueNull(Frame*, Event*) +{ + return String(); +} + +static String valueBackColor(Frame* frame, Event*) +{ + return valueStyle(frame, CSSPropertyBackgroundColor); +} + +static String valueFontName(Frame* frame, Event*) +{ + return valueStyle(frame, CSSPropertyFontFamily); +} + +static String valueFontSize(Frame* frame, Event*) +{ + return valueStyle(frame, CSSPropertyFontSize); +} + +static String valueFontSizeDelta(Frame* frame, Event*) +{ + return valueStyle(frame, CSSPropertyWebkitFontSizeDelta); +} + +static String valueForeColor(Frame* frame, Event*) +{ + return valueStyle(frame, CSSPropertyColor); +} + +static String valueFormatBlock(Frame* frame, Event*) +{ + const VisibleSelection& selection = frame->selection()->selection(); + if (!selection.isNonOrphanedCaretOrRange() || !selection.isContentEditable()) + return ""; + Element* formatBlockElement = FormatBlockCommand::elementForFormatBlockCommand(selection.firstRange().get()); + if (!formatBlockElement) + return ""; + return formatBlockElement->localName(); +} + +// Map of functions + +struct CommandEntry { + const char* name; + EditorInternalCommand command; +}; + +static const CommandMap& createCommandMap() +{ + static const CommandEntry commands[] = { + { "AlignCenter", { executeJustifyCenter, supportedFromMenuOrKeyBinding, enabledInRichlyEditableText, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "AlignJustified", { executeJustifyFull, supportedFromMenuOrKeyBinding, enabledInRichlyEditableText, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "AlignLeft", { executeJustifyLeft, supportedFromMenuOrKeyBinding, enabledInRichlyEditableText, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "AlignRight", { executeJustifyRight, supportedFromMenuOrKeyBinding, enabledInRichlyEditableText, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "BackColor", { executeBackColor, supported, enabledInRichlyEditableText, stateNone, valueBackColor, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "BackwardDelete", { executeDeleteBackward, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, // FIXME: remove BackwardDelete when Safari for Windows stops using it. + { "Bold", { executeToggleBold, supported, enabledInRichlyEditableText, stateBold, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "Copy", { executeCopy, supportedCopyCut, enabledCopy, stateNone, valueNull, notTextInsertion, allowExecutionWhenDisabled } }, + { "CreateLink", { executeCreateLink, supported, enabledInRichlyEditableText, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "Cut", { executeCut, supportedCopyCut, enabledCut, stateNone, valueNull, notTextInsertion, allowExecutionWhenDisabled } }, + { "Delete", { executeDelete, supported, enabledDelete, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "DeleteBackward", { executeDeleteBackward, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "DeleteBackwardByDecomposingPreviousCharacter", { executeDeleteBackwardByDecomposingPreviousCharacter, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "DeleteForward", { executeDeleteForward, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "DeleteToBeginningOfLine", { executeDeleteToBeginningOfLine, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "DeleteToBeginningOfParagraph", { executeDeleteToBeginningOfParagraph, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "DeleteToEndOfLine", { executeDeleteToEndOfLine, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "DeleteToEndOfParagraph", { executeDeleteToEndOfParagraph, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "DeleteToMark", { executeDeleteToMark, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "DeleteWordBackward", { executeDeleteWordBackward, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "DeleteWordForward", { executeDeleteWordForward, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "FindString", { executeFindString, supported, enabled, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "FontName", { executeFontName, supported, enabledInEditableText, stateNone, valueFontName, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "FontSize", { executeFontSize, supported, enabledInEditableText, stateNone, valueFontSize, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "FontSizeDelta", { executeFontSizeDelta, supported, enabledInEditableText, stateNone, valueFontSizeDelta, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "ForeColor", { executeForeColor, supported, enabledInRichlyEditableText, stateNone, valueForeColor, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "FormatBlock", { executeFormatBlock, supported, enabledInRichlyEditableText, stateNone, valueFormatBlock, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "ForwardDelete", { executeForwardDelete, supported, enabledInEditableText, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "HiliteColor", { executeBackColor, supported, enabledInRichlyEditableText, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "IgnoreSpelling", { executeIgnoreSpelling, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "Indent", { executeIndent, supported, enabledInRichlyEditableText, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "InsertBacktab", { executeInsertBacktab, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, isTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "InsertHTML", { executeInsertHTML, supported, enabledInEditableText, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "InsertHorizontalRule", { executeInsertHorizontalRule, supported, enabledInRichlyEditableText, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "InsertImage", { executeInsertImage, supported, enabledInRichlyEditableText, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "InsertLineBreak", { executeInsertLineBreak, supported, enabledInEditableText, stateNone, valueNull, isTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "InsertNewline", { executeInsertNewline, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, isTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "InsertNewlineInQuotedContent", { executeInsertNewlineInQuotedContent, supported, enabledInRichlyEditableText, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "InsertOrderedList", { executeInsertOrderedList, supported, enabledInRichlyEditableText, stateOrderedList, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "InsertParagraph", { executeInsertParagraph, supported, enabledInEditableText, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "InsertTab", { executeInsertTab, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, isTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "InsertText", { executeInsertText, supported, enabledInEditableText, stateNone, valueNull, isTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "InsertUnorderedList", { executeInsertUnorderedList, supported, enabledInRichlyEditableText, stateUnorderedList, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "Italic", { executeToggleItalic, supported, enabledInRichlyEditableText, stateItalic, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "JustifyCenter", { executeJustifyCenter, supported, enabledInRichlyEditableText, stateJustifyCenter, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "JustifyFull", { executeJustifyFull, supported, enabledInRichlyEditableText, stateJustifyFull, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "JustifyLeft", { executeJustifyLeft, supported, enabledInRichlyEditableText, stateJustifyLeft, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "JustifyNone", { executeJustifyLeft, supported, enabledInRichlyEditableText, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "JustifyRight", { executeJustifyRight, supported, enabledInRichlyEditableText, stateJustifyRight, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "MakeTextWritingDirectionLeftToRight", { executeMakeTextWritingDirectionLeftToRight, supportedFromMenuOrKeyBinding, enabledInRichlyEditableText, stateTextWritingDirectionLeftToRight, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "MakeTextWritingDirectionNatural", { executeMakeTextWritingDirectionNatural, supportedFromMenuOrKeyBinding, enabledInRichlyEditableText, stateTextWritingDirectionNatural, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "MakeTextWritingDirectionRightToLeft", { executeMakeTextWritingDirectionRightToLeft, supportedFromMenuOrKeyBinding, enabledInRichlyEditableText, stateTextWritingDirectionRightToLeft, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "MoveBackward", { executeMoveBackward, supportedFromMenuOrKeyBinding, enabledInEditableTextOrCaretBrowsing, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "MoveBackwardAndModifySelection", { executeMoveBackwardAndModifySelection, supportedFromMenuOrKeyBinding, enabledVisibleSelectionOrCaretBrowsing, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "MoveDown", { executeMoveDown, supportedFromMenuOrKeyBinding, enabledInEditableTextOrCaretBrowsing, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "MoveDownAndModifySelection", { executeMoveDownAndModifySelection, supportedFromMenuOrKeyBinding, enabledVisibleSelectionOrCaretBrowsing, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "MoveForward", { executeMoveForward, supportedFromMenuOrKeyBinding, enabledInEditableTextOrCaretBrowsing, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "MoveForwardAndModifySelection", { executeMoveForwardAndModifySelection, supportedFromMenuOrKeyBinding, enabledVisibleSelectionOrCaretBrowsing, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "MoveLeft", { executeMoveLeft, supportedFromMenuOrKeyBinding, enabledInEditableTextOrCaretBrowsing, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "MoveLeftAndModifySelection", { executeMoveLeftAndModifySelection, supportedFromMenuOrKeyBinding, enabledVisibleSelectionOrCaretBrowsing, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "MovePageDown", { executeMovePageDown, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "MovePageDownAndModifySelection", { executeMovePageDownAndModifySelection, supportedFromMenuOrKeyBinding, enabledVisibleSelection, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "MovePageUp", { executeMovePageUp, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "MovePageUpAndModifySelection", { executeMovePageUpAndModifySelection, supportedFromMenuOrKeyBinding, enabledVisibleSelection, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "MoveParagraphBackwardAndModifySelection", { executeMoveParagraphBackwardAndModifySelection, supportedFromMenuOrKeyBinding, enabledVisibleSelectionOrCaretBrowsing, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "MoveParagraphForwardAndModifySelection", { executeMoveParagraphForwardAndModifySelection, supportedFromMenuOrKeyBinding, enabledVisibleSelectionOrCaretBrowsing, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "MoveRight", { executeMoveRight, supportedFromMenuOrKeyBinding, enabledInEditableTextOrCaretBrowsing, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "MoveRightAndModifySelection", { executeMoveRightAndModifySelection, supportedFromMenuOrKeyBinding, enabledVisibleSelectionOrCaretBrowsing, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "MoveToBeginningOfDocument", { executeMoveToBeginningOfDocument, supportedFromMenuOrKeyBinding, enabledInEditableTextOrCaretBrowsing, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "MoveToBeginningOfDocumentAndModifySelection", { executeMoveToBeginningOfDocumentAndModifySelection, supportedFromMenuOrKeyBinding, enabledVisibleSelectionOrCaretBrowsing, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "MoveToBeginningOfLine", { executeMoveToBeginningOfLine, supportedFromMenuOrKeyBinding, enabledInEditableTextOrCaretBrowsing, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "MoveToBeginningOfLineAndModifySelection", { executeMoveToBeginningOfLineAndModifySelection, supportedFromMenuOrKeyBinding, enabledVisibleSelectionOrCaretBrowsing, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "MoveToBeginningOfParagraph", { executeMoveToBeginningOfParagraph, supportedFromMenuOrKeyBinding, enabledInEditableTextOrCaretBrowsing, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "MoveToBeginningOfParagraphAndModifySelection", { executeMoveToBeginningOfParagraphAndModifySelection, supportedFromMenuOrKeyBinding, enabledVisibleSelectionOrCaretBrowsing, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "MoveToBeginningOfSentence", { executeMoveToBeginningOfSentence, supportedFromMenuOrKeyBinding, enabledInEditableTextOrCaretBrowsing, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "MoveToBeginningOfSentenceAndModifySelection", { executeMoveToBeginningOfSentenceAndModifySelection, supportedFromMenuOrKeyBinding, enabledVisibleSelectionOrCaretBrowsing, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "MoveToEndOfDocument", { executeMoveToEndOfDocument, supportedFromMenuOrKeyBinding, enabledInEditableTextOrCaretBrowsing, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "MoveToEndOfDocumentAndModifySelection", { executeMoveToEndOfDocumentAndModifySelection, supportedFromMenuOrKeyBinding, enabledVisibleSelectionOrCaretBrowsing, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "MoveToEndOfLine", { executeMoveToEndOfLine, supportedFromMenuOrKeyBinding, enabledInEditableTextOrCaretBrowsing, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "MoveToEndOfLineAndModifySelection", { executeMoveToEndOfLineAndModifySelection, supportedFromMenuOrKeyBinding, enabledVisibleSelectionOrCaretBrowsing, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "MoveToEndOfParagraph", { executeMoveToEndOfParagraph, supportedFromMenuOrKeyBinding, enabledInEditableTextOrCaretBrowsing, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "MoveToEndOfParagraphAndModifySelection", { executeMoveToEndOfParagraphAndModifySelection, supportedFromMenuOrKeyBinding, enabledVisibleSelectionOrCaretBrowsing, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "MoveToEndOfSentence", { executeMoveToEndOfSentence, supportedFromMenuOrKeyBinding, enabledInEditableTextOrCaretBrowsing, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "MoveToEndOfSentenceAndModifySelection", { executeMoveToEndOfSentenceAndModifySelection, supportedFromMenuOrKeyBinding, enabledVisibleSelectionOrCaretBrowsing, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "MoveToLeftEndOfLine", { executeMoveToLeftEndOfLine, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "MoveToLeftEndOfLineAndModifySelection", { executeMoveToLeftEndOfLineAndModifySelection, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "MoveToRightEndOfLine", { executeMoveToRightEndOfLine, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "MoveToRightEndOfLineAndModifySelection", { executeMoveToRightEndOfLineAndModifySelection, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "MoveUp", { executeMoveUp, supportedFromMenuOrKeyBinding, enabledInEditableTextOrCaretBrowsing, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "MoveUpAndModifySelection", { executeMoveUpAndModifySelection, supportedFromMenuOrKeyBinding, enabledVisibleSelectionOrCaretBrowsing, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "MoveWordBackward", { executeMoveWordBackward, supportedFromMenuOrKeyBinding, enabledInEditableTextOrCaretBrowsing, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "MoveWordBackwardAndModifySelection", { executeMoveWordBackwardAndModifySelection, supportedFromMenuOrKeyBinding, enabledVisibleSelectionOrCaretBrowsing, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "MoveWordForward", { executeMoveWordForward, supportedFromMenuOrKeyBinding, enabledInEditableTextOrCaretBrowsing, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "MoveWordForwardAndModifySelection", { executeMoveWordForwardAndModifySelection, supportedFromMenuOrKeyBinding, enabledVisibleSelectionOrCaretBrowsing, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "MoveWordLeft", { executeMoveWordLeft, supportedFromMenuOrKeyBinding, enabledInEditableTextOrCaretBrowsing, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "MoveWordLeftAndModifySelection", { executeMoveWordLeftAndModifySelection, supportedFromMenuOrKeyBinding, enabledVisibleSelectionOrCaretBrowsing, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "MoveWordRight", { executeMoveWordRight, supportedFromMenuOrKeyBinding, enabledInEditableTextOrCaretBrowsing, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "MoveWordRightAndModifySelection", { executeMoveWordRightAndModifySelection, supportedFromMenuOrKeyBinding, enabledVisibleSelectionOrCaretBrowsing, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "Outdent", { executeOutdent, supported, enabledInRichlyEditableText, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "Paste", { executePaste, supportedPaste, enabledPaste, stateNone, valueNull, notTextInsertion, allowExecutionWhenDisabled } }, + { "PasteAndMatchStyle", { executePasteAndMatchStyle, supportedPaste, enabledPaste, stateNone, valueNull, notTextInsertion, allowExecutionWhenDisabled } }, + { "PasteAsPlainText", { executePasteAsPlainText, supportedPaste, enabledPaste, stateNone, valueNull, notTextInsertion, allowExecutionWhenDisabled } }, + { "Print", { executePrint, supported, enabled, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "Redo", { executeRedo, supported, enabledRedo, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "RemoveFormat", { executeRemoveFormat, supported, enabledRangeInEditableText, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "SelectAll", { executeSelectAll, supported, enabled, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "SelectLine", { executeSelectLine, supportedFromMenuOrKeyBinding, enabledVisibleSelection, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "SelectParagraph", { executeSelectParagraph, supportedFromMenuOrKeyBinding, enabledVisibleSelection, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "SelectSentence", { executeSelectSentence, supportedFromMenuOrKeyBinding, enabledVisibleSelection, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "SelectToMark", { executeSelectToMark, supportedFromMenuOrKeyBinding, enabledVisibleSelectionAndMark, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "SelectWord", { executeSelectWord, supportedFromMenuOrKeyBinding, enabledVisibleSelection, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "SetMark", { executeSetMark, supportedFromMenuOrKeyBinding, enabledVisibleSelection, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "Strikethrough", { executeStrikethrough, supported, enabledInRichlyEditableText, stateStrikethrough, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "StyleWithCSS", { executeStyleWithCSS, supported, enabled, stateStyleWithCSS, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "Subscript", { executeSubscript, supported, enabledInRichlyEditableText, stateSubscript, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "Superscript", { executeSuperscript, supported, enabledInRichlyEditableText, stateSuperscript, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "SwapWithMark", { executeSwapWithMark, supportedFromMenuOrKeyBinding, enabledVisibleSelectionAndMark, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "ToggleBold", { executeToggleBold, supportedFromMenuOrKeyBinding, enabledInRichlyEditableText, stateBold, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "ToggleItalic", { executeToggleItalic, supportedFromMenuOrKeyBinding, enabledInRichlyEditableText, stateItalic, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "ToggleUnderline", { executeUnderline, supportedFromMenuOrKeyBinding, enabledInRichlyEditableText, stateUnderline, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "Transpose", { executeTranspose, supported, enableCaretInEditableText, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "Underline", { executeUnderline, supported, enabledInRichlyEditableText, stateUnderline, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "Undo", { executeUndo, supported, enabledUndo, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "Unlink", { executeUnlink, supported, enabledRangeInRichlyEditableText, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "Unscript", { executeUnscript, supportedFromMenuOrKeyBinding, enabledInRichlyEditableText, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "Unselect", { executeUnselect, supported, enabledVisibleSelection, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "Yank", { executeYank, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + { "YankAndSelect", { executeYankAndSelect, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, + +#if PLATFORM(MAC) + { "TakeFindStringFromSelection", { executeTakeFindStringFromSelection, supportedFromMenuOrKeyBinding, enabledTakeFindStringFromSelection, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, +#endif + +#if SUPPORT_AUTOCORRECTION_PANEL + { "CancelOperation", { executeCancelOperation, supportedFromMenuOrKeyBinding, enabledDismissCorrectionPanel, stateNone, valueNull, notTextInsertion, doNotAllowExecutionWhenDisabled } }, +#endif + }; + + // These unsupported commands are listed here since they appear in the Microsoft + // documentation used as the starting point for our DOM executeCommand support. + // + // 2D-Position (not supported) + // AbsolutePosition (not supported) + // BlockDirLTR (not supported) + // BlockDirRTL (not supported) + // BrowseMode (not supported) + // ClearAuthenticationCache (not supported) + // CreateBookmark (not supported) + // DirLTR (not supported) + // DirRTL (not supported) + // EditMode (not supported) + // InlineDirLTR (not supported) + // InlineDirRTL (not supported) + // InsertButton (not supported) + // InsertFieldSet (not supported) + // InsertIFrame (not supported) + // InsertInputButton (not supported) + // InsertInputCheckbox (not supported) + // InsertInputFileUpload (not supported) + // InsertInputHidden (not supported) + // InsertInputImage (not supported) + // InsertInputPassword (not supported) + // InsertInputRadio (not supported) + // InsertInputReset (not supported) + // InsertInputSubmit (not supported) + // InsertInputText (not supported) + // InsertMarquee (not supported) + // InsertSelectDropDown (not supported) + // InsertSelectListBox (not supported) + // InsertTextArea (not supported) + // LiveResize (not supported) + // MultipleSelection (not supported) + // Open (not supported) + // Overwrite (not supported) + // PlayImage (not supported) + // Refresh (not supported) + // RemoveParaFormat (not supported) + // SaveAs (not supported) + // SizeToControl (not supported) + // SizeToControlHeight (not supported) + // SizeToControlWidth (not supported) + // Stop (not supported) + // StopImage (not supported) + // Unbookmark (not supported) + + CommandMap& commandMap = *new CommandMap; + + for (size_t i = 0; i < WTF_ARRAY_LENGTH(commands); ++i) { + ASSERT(!commandMap.get(commands[i].name)); + commandMap.set(commands[i].name, &commands[i].command); + } + + return commandMap; +} + +static const EditorInternalCommand* internalCommand(const String& commandName) +{ + static const CommandMap& commandMap = createCommandMap(); + return commandName.isEmpty() ? 0 : commandMap.get(commandName); +} + +Editor::Command Editor::command(const String& commandName) +{ + return Command(internalCommand(commandName), CommandFromMenuOrKeyBinding, m_frame); +} + +Editor::Command Editor::command(const String& commandName, EditorCommandSource source) +{ + return Command(internalCommand(commandName), source, m_frame); +} + +bool Editor::commandIsSupportedFromMenuOrKeyBinding(const String& commandName) +{ + return internalCommand(commandName); +} + +Editor::Command::Command() + : m_command(0) +{ +} + +Editor::Command::Command(const EditorInternalCommand* command, EditorCommandSource source, PassRefPtr<Frame> frame) + : m_command(command) + , m_source(source) + , m_frame(command ? frame : 0) +{ + // Use separate assertions so we can tell which bad thing happened. + if (!command) + ASSERT(!m_frame); + else + ASSERT(m_frame); +} + +bool Editor::Command::execute(const String& parameter, Event* triggeringEvent) const +{ + if (!isEnabled(triggeringEvent)) { + // Let certain commands be executed when performed explicitly even if they are disabled. + if (!isSupported() || !m_frame || !m_command->allowExecutionWhenDisabled) + return false; + } + m_frame->document()->updateLayoutIgnorePendingStylesheets(); + return m_command->execute(m_frame.get(), triggeringEvent, m_source, parameter); +} + +bool Editor::Command::execute(Event* triggeringEvent) const +{ + return execute(String(), triggeringEvent); +} + +bool Editor::Command::isSupported() const +{ + if (!m_command) + return false; + switch (m_source) { + case CommandFromMenuOrKeyBinding: + return true; + case CommandFromDOM: + case CommandFromDOMWithUserInterface: + return m_command->isSupportedFromDOM(m_frame.get()); + } + ASSERT_NOT_REACHED(); + return false; +} + +bool Editor::Command::isEnabled(Event* triggeringEvent) const +{ + if (!isSupported() || !m_frame) + return false; + return m_command->isEnabled(m_frame.get(), triggeringEvent, m_source); +} + +TriState Editor::Command::state(Event* triggeringEvent) const +{ + if (!isSupported() || !m_frame) + return FalseTriState; + return m_command->state(m_frame.get(), triggeringEvent); +} + +String Editor::Command::value(Event* triggeringEvent) const +{ + if (!isSupported() || !m_frame) + return String(); + if (m_command->value == valueNull && m_command->state != stateNone) + return m_command->state(m_frame.get(), triggeringEvent) == TrueTriState ? "true" : "false"; + return m_command->value(m_frame.get(), triggeringEvent); +} + +bool Editor::Command::isTextInsertion() const +{ + return m_command && m_command->isTextInsertion; +} + +} // namespace WebCore diff --git a/Source/WebCore/editing/EditorDeleteAction.h b/Source/WebCore/editing/EditorDeleteAction.h new file mode 100644 index 0000000..00bf683 --- /dev/null +++ b/Source/WebCore/editing/EditorDeleteAction.h @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2006 Apple Computer, 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef EditorDeleteAction_h +#define EditorDeleteAction_h + +namespace WebCore { + +enum EditorDeleteAction { + deleteSelectionAction, + deleteKeyAction, + forwardDeleteKeyAction +}; + +} // namespace + +#endif + diff --git a/Source/WebCore/editing/EditorInsertAction.h b/Source/WebCore/editing/EditorInsertAction.h new file mode 100644 index 0000000..5b732dc --- /dev/null +++ b/Source/WebCore/editing/EditorInsertAction.h @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2006 Apple Computer, 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef EditorInsertAction_h +#define EditorInsertAction_h + +namespace WebCore { + +// This must be kept in sync with WebViewInsertAction defined in WebEditingDelegate.h +enum EditorInsertAction { + EditorInsertActionTyped, + EditorInsertActionPasted, + EditorInsertActionDropped, +}; + +} // namespace + +#endif diff --git a/Source/WebCore/editing/FindOptions.h b/Source/WebCore/editing/FindOptions.h new file mode 100644 index 0000000..ae4aecf --- /dev/null +++ b/Source/WebCore/editing/FindOptions.h @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2010 Apple 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 APPLE INC. AND ITS 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 APPLE INC. OR ITS 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. + */ + +#ifndef FindOptions_h +#define FindOptions_h + +namespace WebCore { + +enum FindOptionFlag { + CaseInsensitive = 1 << 0, + AtWordStarts = 1 << 1, + // When combined with AtWordStarts, accepts a match in the middle of a word if the match begins with + // an uppercase letter followed by a lowercase or non-letter. Accepts several other intra-word matches. + TreatMedialCapitalAsWordStart = 1 << 2, + Backwards = 1 << 3, + WrapAround = 1 << 4, + StartInSelection = 1 << 5 +}; + +typedef unsigned FindOptions; + +} // namespace WebCore + +#endif // FindOptions_h diff --git a/Source/WebCore/editing/FormatBlockCommand.cpp b/Source/WebCore/editing/FormatBlockCommand.cpp new file mode 100644 index 0000000..e43f330 --- /dev/null +++ b/Source/WebCore/editing/FormatBlockCommand.cpp @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2006 Apple Computer, 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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 "Element.h" +#include "FormatBlockCommand.h" +#include "Document.h" +#include "htmlediting.h" +#include "HTMLElement.h" +#include "HTMLNames.h" +#include "Range.h" +#include "visible_units.h" + +namespace WebCore { + +using namespace HTMLNames; + +static Node* enclosingBlockToSplitTreeTo(Node* startNode); +static bool isElementForFormatBlock(const QualifiedName& tagName); +static inline bool isElementForFormatBlock(Node* node) +{ + return node->isElementNode() && isElementForFormatBlock(static_cast<Element*>(node)->tagQName()); +} + +FormatBlockCommand::FormatBlockCommand(Document* document, const QualifiedName& tagName) + : ApplyBlockElementCommand(document, tagName) + , m_didApply(false) +{ +} + +void FormatBlockCommand::formatSelection(const VisiblePosition& startOfSelection, const VisiblePosition& endOfSelection) +{ + if (!isElementForFormatBlock(tagName())) + return; + ApplyBlockElementCommand::formatSelection(startOfSelection, endOfSelection); + m_didApply = true; +} + +void FormatBlockCommand::formatRange(const Position& start, const Position& end, const Position& endOfSelection, RefPtr<Element>& blockNode) +{ + Node* nodeToSplitTo = enclosingBlockToSplitTreeTo(start.node()); + RefPtr<Node> outerBlock = (start.node() == nodeToSplitTo) ? start.node() : splitTreeToNode(start.node(), nodeToSplitTo); + RefPtr<Node> nodeAfterInsertionPosition = outerBlock; + + RefPtr<Range> range = Range::create(document(), start, endOfSelection); + Element* refNode = enclosingBlockFlowElement(end); + Element* root = editableRootForPosition(start); + if (isElementForFormatBlock(refNode->tagQName()) && start == startOfBlock(start) + && (end == endOfBlock(end) || isNodeVisiblyContainedWithin(refNode, range.get())) + && refNode != root && !root->isDescendantOf(refNode)) { + // Already in a block element that only contains the current paragraph + if (refNode->hasTagName(tagName())) + return; + nodeAfterInsertionPosition = refNode; + } + + if (!blockNode) { + // Create a new blockquote and insert it as a child of the root editable element. We accomplish + // this by splitting all parents of the current paragraph up to that point. + blockNode = createBlockElement(); + insertNodeBefore(blockNode, nodeAfterInsertionPosition); + } + + Position lastParagraphInBlockNode = lastPositionInNode(blockNode.get()); + bool wasEndOfParagraph = isEndOfParagraph(lastParagraphInBlockNode); + + moveParagraphWithClones(start, end, blockNode.get(), outerBlock.get()); + + if (wasEndOfParagraph && !isEndOfParagraph(lastParagraphInBlockNode) && !isStartOfParagraph(lastParagraphInBlockNode)) + insertBlockPlaceholder(lastParagraphInBlockNode); +} + +Element* FormatBlockCommand::elementForFormatBlockCommand(Range* range) +{ + if (!range) + return 0; + + ExceptionCode ec; + Node* commonAncestor = range->commonAncestorContainer(ec); + while (commonAncestor && !isElementForFormatBlock(commonAncestor)) + commonAncestor = commonAncestor->parentNode(); + + if (!commonAncestor) + return 0; + + Element* rootEditableElement = range->startContainer()->rootEditableElement(); + if (!rootEditableElement || commonAncestor->contains(rootEditableElement)) + return 0; + + ASSERT(commonAncestor->isElementNode()); + return static_cast<Element*>(commonAncestor); +} + +bool isElementForFormatBlock(const QualifiedName& tagName) +{ + DEFINE_STATIC_LOCAL(HashSet<QualifiedName>, blockTags, ()); + if (blockTags.isEmpty()) { + blockTags.add(addressTag); + blockTags.add(articleTag); + blockTags.add(asideTag); + blockTags.add(blockquoteTag); + blockTags.add(ddTag); + blockTags.add(divTag); + blockTags.add(dlTag); + blockTags.add(dtTag); + blockTags.add(footerTag); + blockTags.add(h1Tag); + blockTags.add(h2Tag); + blockTags.add(h3Tag); + blockTags.add(h4Tag); + blockTags.add(h5Tag); + blockTags.add(h6Tag); + blockTags.add(headerTag); + blockTags.add(hgroupTag); + blockTags.add(navTag); + blockTags.add(pTag); + blockTags.add(preTag); + blockTags.add(sectionTag); + } + return blockTags.contains(tagName); +} + +Node* enclosingBlockToSplitTreeTo(Node* startNode) +{ + Node* lastBlock = startNode; + for (Node* n = startNode; n; n = n->parentNode()) { + if (!n->isContentEditable()) + return lastBlock; + if (isTableCell(n) || n->hasTagName(bodyTag) || !n->parentNode() || !n->parentNode()->isContentEditable() || isElementForFormatBlock(n)) + return n; + if (isBlock(n)) + lastBlock = n; + if (isListElement(n)) + return n->parentNode()->isContentEditable() ? n->parentNode() : n; + } + return lastBlock; +} + +} diff --git a/Source/WebCore/editing/FormatBlockCommand.h b/Source/WebCore/editing/FormatBlockCommand.h new file mode 100644 index 0000000..4be235f --- /dev/null +++ b/Source/WebCore/editing/FormatBlockCommand.h @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2006, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef FormatBlockCommand_h +#define FormatBlockCommand_h + +#include "ApplyBlockElementCommand.h" +#include "CompositeEditCommand.h" + +namespace WebCore { + +class FormatBlockCommand : public ApplyBlockElementCommand { +public: + static PassRefPtr<FormatBlockCommand> create(Document* document, const QualifiedName& tagName) + { + return adoptRef(new FormatBlockCommand(document, tagName)); + } + + virtual bool preservesTypingStyle() const { return true; } + + static Element* elementForFormatBlockCommand(Range*); + bool didApply() const { return m_didApply; } + +private: + FormatBlockCommand(Document*, const QualifiedName& tagName); + + void formatSelection(const VisiblePosition& startOfSelection, const VisiblePosition& endOfSelection); + void formatRange(const Position& start, const Position& end, const Position& endOfSelection, RefPtr<Element>&); + EditAction editingAction() const { return EditActionFormatBlock; } + + bool m_didApply; +}; + +} // namespace WebCore + +#endif // FormatBlockCommand_h diff --git a/Source/WebCore/editing/HTMLInterchange.cpp b/Source/WebCore/editing/HTMLInterchange.cpp new file mode 100644 index 0000000..16b330d --- /dev/null +++ b/Source/WebCore/editing/HTMLInterchange.cpp @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2004, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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 "HTMLInterchange.h" + +#include "CharacterNames.h" +#include "Text.h" +#include "TextIterator.h" +#include <wtf/StdLibExtras.h> + +namespace WebCore { + +namespace { + +String convertedSpaceString() +{ + DEFINE_STATIC_LOCAL(String, convertedSpaceString, ()); + if (convertedSpaceString.isNull()) { + convertedSpaceString = "<span class=\""; + convertedSpaceString += AppleConvertedSpace; + convertedSpaceString += "\">"; + convertedSpaceString.append(noBreakSpace); + convertedSpaceString += "</span>"; + } + return convertedSpaceString; +} + +} // end anonymous namespace + +String convertHTMLTextToInterchangeFormat(const String& in, const Text* node) +{ + // Assume all the text comes from node. + if (node->renderer() && node->renderer()->style()->preserveNewline()) + return in; + + Vector<UChar> s; + + unsigned i = 0; + unsigned consumed = 0; + while (i < in.length()) { + consumed = 1; + if (isCollapsibleWhitespace(in[i])) { + // count number of adjoining spaces + unsigned j = i + 1; + while (j < in.length() && isCollapsibleWhitespace(in[j])) + j++; + unsigned count = j - i; + consumed = count; + while (count) { + unsigned add = count % 3; + switch (add) { + case 0: + append(s, convertedSpaceString()); + s.append(' '); + append(s, convertedSpaceString()); + add = 3; + break; + case 1: + if (i == 0 || i + 1 == in.length()) // at start or end of string + append(s, convertedSpaceString()); + else + s.append(' '); + break; + case 2: + if (i == 0) { + // at start of string + append(s, convertedSpaceString()); + s.append(' '); + } else if (i + 2 == in.length()) { + // at end of string + append(s, convertedSpaceString()); + append(s, convertedSpaceString()); + } else { + append(s, convertedSpaceString()); + s.append(' '); + } + break; + } + count -= add; + } + } else + s.append(in[i]); + i += consumed; + } + + return String::adopt(s); +} + +} // namespace WebCore diff --git a/Source/WebCore/editing/HTMLInterchange.h b/Source/WebCore/editing/HTMLInterchange.h new file mode 100644 index 0000000..4029ea2 --- /dev/null +++ b/Source/WebCore/editing/HTMLInterchange.h @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2004, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef HTMLInterchange_h +#define HTMLInterchange_h + +#include <wtf/Forward.h> + +namespace WebCore { + +class Text; + +#define AppleInterchangeNewline "Apple-interchange-newline" +#define AppleConvertedSpace "Apple-converted-space" +#define ApplePasteAsQuotation "Apple-paste-as-quotation" +#define AppleStyleSpanClass "Apple-style-span" +#define AppleTabSpanClass "Apple-tab-span" + +enum EAnnotateForInterchange { DoNotAnnotateForInterchange, AnnotateForInterchange }; + +String convertHTMLTextToInterchangeFormat(const String&, const Text*); + +} + +#endif diff --git a/Source/WebCore/editing/IndentOutdentCommand.cpp b/Source/WebCore/editing/IndentOutdentCommand.cpp new file mode 100644 index 0000000..13d0f88 --- /dev/null +++ b/Source/WebCore/editing/IndentOutdentCommand.cpp @@ -0,0 +1,235 @@ +/* + * Copyright (C) 2006, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (IndentOutdentCommandINCLUDING, 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 "IndentOutdentCommand.h" + +#include "Document.h" +#include "Element.h" +#include "HTMLBlockquoteElement.h" +#include "HTMLNames.h" +#include "InsertLineBreakCommand.h" +#include "InsertListCommand.h" +#include "Range.h" +#include "SplitElementCommand.h" +#include "Text.h" +#include "TextIterator.h" +#include "htmlediting.h" +#include "visible_units.h" +#include <wtf/StdLibExtras.h> + +namespace WebCore { + +using namespace HTMLNames; + +static bool isListOrIndentBlockquote(const Node* node) +{ + return node && (node->hasTagName(ulTag) || node->hasTagName(olTag) || node->hasTagName(blockquoteTag)); +} + +IndentOutdentCommand::IndentOutdentCommand(Document* document, EIndentType typeOfAction, int marginInPixels) + : ApplyBlockElementCommand(document, blockquoteTag, "webkit-indent-blockquote", "margin: 0 0 0 40px; border: none; padding: 0px;") + , m_typeOfAction(typeOfAction) + , m_marginInPixels(marginInPixels) +{ +} + +bool IndentOutdentCommand::tryIndentingAsListItem(const Position& start, const Position& end) +{ + // If our selection is not inside a list, bail out. + Node* lastNodeInSelectedParagraph = start.node(); + RefPtr<Element> listNode = enclosingList(lastNodeInSelectedParagraph); + if (!listNode) + return false; + + // Find the block that we want to indent. If it's not a list item (e.g., a div inside a list item), we bail out. + Element* selectedListItem = static_cast<Element*>(enclosingBlock(lastNodeInSelectedParagraph)); + + // FIXME: we need to deal with the case where there is no li (malformed HTML) + if (!selectedListItem->hasTagName(liTag)) + return false; + + // FIXME: previousElementSibling does not ignore non-rendered content like <span></span>. Should we? + Element* previousList = selectedListItem->previousElementSibling(); + Element* nextList = selectedListItem->nextElementSibling(); + + RefPtr<Element> newList = document()->createElement(listNode->tagQName(), false); + insertNodeBefore(newList, selectedListItem); + + moveParagraphWithClones(start, end, newList.get(), selectedListItem); + + if (canMergeLists(previousList, newList.get())) + mergeIdenticalElements(previousList, newList); + if (canMergeLists(newList.get(), nextList)) + mergeIdenticalElements(newList, nextList); + + return true; +} + +void IndentOutdentCommand::indentIntoBlockquote(const Position& start, const Position& end, RefPtr<Element>& targetBlockquote) +{ + Node* enclosingCell = enclosingNodeOfType(start, &isTableCell); + Node* nodeToSplitTo; + if (enclosingCell) + nodeToSplitTo = enclosingCell; + else if (enclosingList(start.node())) + nodeToSplitTo = enclosingBlock(start.node()); + else + nodeToSplitTo = editableRootForPosition(start); + + if (!nodeToSplitTo) + return; + + RefPtr<Node> outerBlock = (start.node() == nodeToSplitTo) ? start.node() : splitTreeToNode(start.node(), nodeToSplitTo); + + if (!targetBlockquote) { + // Create a new blockquote and insert it as a child of the root editable element. We accomplish + // this by splitting all parents of the current paragraph up to that point. + targetBlockquote = createBlockElement(); + insertNodeBefore(targetBlockquote, outerBlock); + } + + moveParagraphWithClones(start, end, targetBlockquote.get(), outerBlock.get()); +} + +void IndentOutdentCommand::outdentParagraph() +{ + VisiblePosition visibleStartOfParagraph = startOfParagraph(endingSelection().visibleStart()); + VisiblePosition visibleEndOfParagraph = endOfParagraph(visibleStartOfParagraph); + + Node* enclosingNode = enclosingNodeOfType(visibleStartOfParagraph.deepEquivalent(), &isListOrIndentBlockquote); + if (!enclosingNode || !enclosingNode->parentNode()->isContentEditable()) // We can't outdent if there is no place to go! + return; + + // Use InsertListCommand to remove the selection from the list + if (enclosingNode->hasTagName(olTag)) { + applyCommandToComposite(InsertListCommand::create(document(), InsertListCommand::OrderedList)); + return; + } + if (enclosingNode->hasTagName(ulTag)) { + applyCommandToComposite(InsertListCommand::create(document(), InsertListCommand::UnorderedList)); + return; + } + + // The selection is inside a blockquote i.e. enclosingNode is a blockquote + VisiblePosition positionInEnclosingBlock = VisiblePosition(Position(enclosingNode, 0)); + // If the blockquote is inline, the start of the enclosing block coincides with + // positionInEnclosingBlock. + VisiblePosition startOfEnclosingBlock = (enclosingNode->renderer() && enclosingNode->renderer()->isInline()) ? positionInEnclosingBlock : startOfBlock(positionInEnclosingBlock); + VisiblePosition lastPositionInEnclosingBlock = VisiblePosition(Position(enclosingNode, enclosingNode->childNodeCount())); + VisiblePosition endOfEnclosingBlock = endOfBlock(lastPositionInEnclosingBlock); + if (visibleStartOfParagraph == startOfEnclosingBlock && + visibleEndOfParagraph == endOfEnclosingBlock) { + // The blockquote doesn't contain anything outside the paragraph, so it can be totally removed. + Node* splitPoint = enclosingNode->nextSibling(); + removeNodePreservingChildren(enclosingNode); + // outdentRegion() assumes it is operating on the first paragraph of an enclosing blockquote, but if there are multiply nested blockquotes and we've + // just removed one, then this assumption isn't true. By splitting the next containing blockquote after this node, we keep this assumption true + if (splitPoint) { + if (ContainerNode* splitPointParent = splitPoint->parentNode()) { + if (splitPointParent->hasTagName(blockquoteTag) + && !splitPoint->hasTagName(blockquoteTag) + && splitPointParent->parentNode()->isContentEditable()) // We can't outdent if there is no place to go! + splitElement(static_cast<Element*>(splitPointParent), splitPoint); + } + } + + updateLayout(); + visibleStartOfParagraph = VisiblePosition(visibleStartOfParagraph.deepEquivalent()); + visibleEndOfParagraph = VisiblePosition(visibleEndOfParagraph.deepEquivalent()); + if (visibleStartOfParagraph.isNotNull() && !isStartOfParagraph(visibleStartOfParagraph)) + insertNodeAt(createBreakElement(document()), visibleStartOfParagraph.deepEquivalent()); + if (visibleEndOfParagraph.isNotNull() && !isEndOfParagraph(visibleEndOfParagraph)) + insertNodeAt(createBreakElement(document()), visibleEndOfParagraph.deepEquivalent()); + + return; + } + Node* enclosingBlockFlow = enclosingBlock(visibleStartOfParagraph.deepEquivalent().node()); + RefPtr<Node> splitBlockquoteNode = enclosingNode; + if (enclosingBlockFlow != enclosingNode) + splitBlockquoteNode = splitTreeToNode(enclosingBlockFlow, enclosingNode, true); + else { + // We split the blockquote at where we start outdenting. + splitElement(static_cast<Element*>(enclosingNode), visibleStartOfParagraph.deepEquivalent().node()); + } + RefPtr<Node> placeholder = createBreakElement(document()); + insertNodeBefore(placeholder, splitBlockquoteNode); + moveParagraph(startOfParagraph(visibleStartOfParagraph), endOfParagraph(visibleEndOfParagraph), VisiblePosition(Position(placeholder.get(), 0)), true); +} + +// FIXME: We should merge this function with ApplyBlockElementCommand::formatSelection +void IndentOutdentCommand::outdentRegion(const VisiblePosition& startOfSelection, const VisiblePosition& endOfSelection) +{ + VisiblePosition endOfLastParagraph = endOfParagraph(endOfSelection); + + if (endOfParagraph(startOfSelection) == endOfLastParagraph) { + outdentParagraph(); + return; + } + + Position originalSelectionEnd = endingSelection().end(); + VisiblePosition endOfCurrentParagraph = endOfParagraph(startOfSelection); + VisiblePosition endAfterSelection = endOfParagraph(endOfParagraph(endOfSelection).next()); + + while (endOfCurrentParagraph != endAfterSelection) { + VisiblePosition endOfNextParagraph = endOfParagraph(endOfCurrentParagraph.next()); + if (endOfCurrentParagraph == endOfLastParagraph) + setEndingSelection(VisibleSelection(originalSelectionEnd, DOWNSTREAM)); + else + setEndingSelection(endOfCurrentParagraph); + + outdentParagraph(); + + // outdentParagraph could move more than one paragraph if the paragraph + // is in a list item. As a result, endAfterSelection and endOfNextParagraph + // could refer to positions no longer in the document. + if (endAfterSelection.isNotNull() && !endAfterSelection.deepEquivalent().node()->inDocument()) + break; + + if (endOfNextParagraph.isNotNull() && !endOfNextParagraph.deepEquivalent().node()->inDocument()) { + endOfCurrentParagraph = endingSelection().end(); + endOfNextParagraph = endOfParagraph(endOfCurrentParagraph.next()); + } + endOfCurrentParagraph = endOfNextParagraph; + } +} + +void IndentOutdentCommand::formatSelection(const VisiblePosition& startOfSelection, const VisiblePosition& endOfSelection) +{ + if (m_typeOfAction == Indent) + ApplyBlockElementCommand::formatSelection(startOfSelection, endOfSelection); + else + outdentRegion(startOfSelection, endOfSelection); +} + +void IndentOutdentCommand::formatRange(const Position& start, const Position& end, const Position&, RefPtr<Element>& blockquoteForNextIndent) +{ + if (tryIndentingAsListItem(start, end)) + blockquoteForNextIndent = 0; + else + indentIntoBlockquote(start, end, blockquoteForNextIndent); +} + +} diff --git a/Source/WebCore/editing/IndentOutdentCommand.h b/Source/WebCore/editing/IndentOutdentCommand.h new file mode 100644 index 0000000..c28aea3 --- /dev/null +++ b/Source/WebCore/editing/IndentOutdentCommand.h @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2006, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef IndentOutdentCommand_h +#define IndentOutdentCommand_h + +#include "ApplyBlockElementCommand.h" +#include "CompositeEditCommand.h" + +namespace WebCore { + +class IndentOutdentCommand : public ApplyBlockElementCommand { +public: + enum EIndentType { Indent, Outdent }; + static PassRefPtr<IndentOutdentCommand> create(Document* document, EIndentType type, int marginInPixels = 0) + { + return adoptRef(new IndentOutdentCommand(document, type, marginInPixels)); + } + + virtual bool preservesTypingStyle() const { return true; } + +private: + IndentOutdentCommand(Document*, EIndentType, int marginInPixels); + + virtual EditAction editingAction() const { return m_typeOfAction == Indent ? EditActionIndent : EditActionOutdent; } + + void indentRegion(const VisiblePosition&, const VisiblePosition&); + void outdentRegion(const VisiblePosition&, const VisiblePosition&); + void outdentParagraph(); + bool tryIndentingAsListItem(const Position&, const Position&); + void indentIntoBlockquote(const Position&, const Position&, RefPtr<Element>&); + + void formatSelection(const VisiblePosition& startOfSelection, const VisiblePosition& endOfSelection); + void formatRange(const Position& start, const Position& end, const Position& endOfSelection, RefPtr<Element>& blockquoteForNextIndent); + + EIndentType m_typeOfAction; + int m_marginInPixels; +}; + +} // namespace WebCore + +#endif // IndentOutdentCommand_h diff --git a/Source/WebCore/editing/InsertIntoTextNodeCommand.cpp b/Source/WebCore/editing/InsertIntoTextNodeCommand.cpp new file mode 100644 index 0000000..9b7761c --- /dev/null +++ b/Source/WebCore/editing/InsertIntoTextNodeCommand.cpp @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2005, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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 "InsertIntoTextNodeCommand.h" + +#include "AXObjectCache.h" +#include "Text.h" + +namespace WebCore { + +InsertIntoTextNodeCommand::InsertIntoTextNodeCommand(PassRefPtr<Text> node, unsigned offset, const String& text) + : SimpleEditCommand(node->document()) + , m_node(node) + , m_offset(offset) + , m_text(text) +{ + ASSERT(m_node); + ASSERT(m_offset <= m_node->length()); + ASSERT(!m_text.isEmpty()); +} + +void InsertIntoTextNodeCommand::doApply() +{ + if (!m_node->isContentEditable()) + return; + + ExceptionCode ec; + m_node->insertData(m_offset, m_text, ec); + + if (AXObjectCache::accessibilityEnabled()) + document()->axObjectCache()->nodeTextChangeNotification(m_node->renderer(), AXObjectCache::AXTextInserted, m_offset, m_text.length()); +} + +void InsertIntoTextNodeCommand::doUnapply() +{ + if (!m_node->isContentEditable()) + return; + + // Need to notify this before actually deleting the text + if (AXObjectCache::accessibilityEnabled()) + document()->axObjectCache()->nodeTextChangeNotification(m_node->renderer(), AXObjectCache::AXTextDeleted, m_offset, m_text.length()); + + ExceptionCode ec; + m_node->deleteData(m_offset, m_text.length(), ec); +} + +} // namespace WebCore diff --git a/Source/WebCore/editing/InsertIntoTextNodeCommand.h b/Source/WebCore/editing/InsertIntoTextNodeCommand.h new file mode 100644 index 0000000..49cb58b --- /dev/null +++ b/Source/WebCore/editing/InsertIntoTextNodeCommand.h @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2005, 2006, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef InsertIntoTextNodeCommand_h +#define InsertIntoTextNodeCommand_h + +#include "EditCommand.h" + +namespace WebCore { + +class Text; + +class InsertIntoTextNodeCommand : public SimpleEditCommand { +public: + static PassRefPtr<InsertIntoTextNodeCommand> create(PassRefPtr<Text> node, unsigned offset, const String& text) + { + return adoptRef(new InsertIntoTextNodeCommand(node, offset, text)); + } + +private: + InsertIntoTextNodeCommand(PassRefPtr<Text> node, unsigned offset, const String& text); + + virtual void doApply(); + virtual void doUnapply(); + + RefPtr<Text> m_node; + unsigned m_offset; + String m_text; +}; + +} // namespace WebCore + +#endif // InsertIntoTextNodeCommand_h diff --git a/Source/WebCore/editing/InsertLineBreakCommand.cpp b/Source/WebCore/editing/InsertLineBreakCommand.cpp new file mode 100644 index 0000000..3070edf --- /dev/null +++ b/Source/WebCore/editing/InsertLineBreakCommand.cpp @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2005, 2006 Apple Computer, 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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 "InsertLineBreakCommand.h" + +#include "CSSMutableStyleDeclaration.h" +#include "Document.h" +#include "Frame.h" +#include "HTMLElement.h" +#include "HTMLNames.h" +#include "Range.h" +#include "RenderObject.h" +#include "Text.h" +#include "VisiblePosition.h" +#include "htmlediting.h" +#include "visible_units.h" + +namespace WebCore { + +using namespace HTMLNames; + +InsertLineBreakCommand::InsertLineBreakCommand(Document* document) + : CompositeEditCommand(document) +{ +} + +bool InsertLineBreakCommand::preservesTypingStyle() const +{ + return true; +} + +void InsertLineBreakCommand::insertNodeAfterPosition(Node* node, const Position& pos) +{ + // Insert the BR after the caret position. In the case the + // position is a block, do an append. We don't want to insert + // the BR *after* the block. + Element* cb = pos.node()->enclosingBlockFlowElement(); + if (cb == pos.node()) + appendNode(node, cb); + else + insertNodeAfter(node, pos.node()); +} + +void InsertLineBreakCommand::insertNodeBeforePosition(Node* node, const Position& pos) +{ + // Insert the BR after the caret position. In the case the + // position is a block, do an append. We don't want to insert + // the BR *before* the block. + Element* cb = pos.node()->enclosingBlockFlowElement(); + if (cb == pos.node()) + appendNode(node, cb); + else + insertNodeBefore(node, pos.node()); +} + +// Whether we should insert a break element or a '\n'. +bool InsertLineBreakCommand::shouldUseBreakElement(const Position& insertionPos) +{ + // An editing position like [input, 0] actually refers to the position before + // the input element, and in that case we need to check the input element's + // parent's renderer. + Position p(rangeCompliantEquivalent(insertionPos)); + return p.node()->renderer() && !p.node()->renderer()->style()->preserveNewline(); +} + +void InsertLineBreakCommand::doApply() +{ + deleteSelection(); + VisibleSelection selection = endingSelection(); + if (!selection.isNonOrphanedCaretOrRange()) + return; + + VisiblePosition caret(selection.visibleStart()); + // FIXME: If the node is hidden, we should still be able to insert text. + // For now, we return to avoid a crash. https://bugs.webkit.org/show_bug.cgi?id=40342 + if (caret.isNull()) + return; + + Position pos(caret.deepEquivalent()); + + pos = positionAvoidingSpecialElementBoundary(pos); + + pos = positionOutsideTabSpan(pos); + + RefPtr<Node> nodeToInsert; + if (shouldUseBreakElement(pos)) + nodeToInsert = createBreakElement(document()); + else + nodeToInsert = document()->createTextNode("\n"); + + // FIXME: Need to merge text nodes when inserting just after or before text. + + if (isEndOfParagraph(caret) && !lineBreakExistsAtVisiblePosition(caret)) { + bool needExtraLineBreak = !pos.node()->hasTagName(hrTag) && !pos.node()->hasTagName(tableTag); + + insertNodeAt(nodeToInsert.get(), pos); + + if (needExtraLineBreak) + insertNodeBefore(nodeToInsert->cloneNode(false), nodeToInsert); + + VisiblePosition endingPosition(Position(nodeToInsert.get(), 0)); + setEndingSelection(VisibleSelection(endingPosition)); + } else if (pos.deprecatedEditingOffset() <= caretMinOffset(pos.node())) { + insertNodeAt(nodeToInsert.get(), pos); + + // Insert an extra br or '\n' if the just inserted one collapsed. + if (!isStartOfParagraph(VisiblePosition(Position(nodeToInsert.get(), 0)))) + insertNodeBefore(nodeToInsert->cloneNode(false).get(), nodeToInsert.get()); + + setEndingSelection(VisibleSelection(positionInParentAfterNode(nodeToInsert.get()), DOWNSTREAM)); + // If we're inserting after all of the rendered text in a text node, or into a non-text node, + // a simple insertion is sufficient. + } else if (pos.deprecatedEditingOffset() >= caretMaxOffset(pos.node()) || !pos.node()->isTextNode()) { + insertNodeAt(nodeToInsert.get(), pos); + setEndingSelection(VisibleSelection(positionInParentAfterNode(nodeToInsert.get()), DOWNSTREAM)); + } else if (pos.node()->isTextNode()) { + // Split a text node + Text* textNode = static_cast<Text*>(pos.node()); + splitTextNode(textNode, pos.deprecatedEditingOffset()); + insertNodeBefore(nodeToInsert, textNode); + Position endingPosition = Position(textNode, 0); + + // Handle whitespace that occurs after the split + updateLayout(); + if (!endingPosition.isRenderedCharacter()) { + Position positionBeforeTextNode(positionInParentBeforeNode(textNode)); + // Clear out all whitespace and insert one non-breaking space + deleteInsignificantTextDownstream(endingPosition); + ASSERT(!textNode->renderer() || textNode->renderer()->style()->collapseWhiteSpace()); + // Deleting insignificant whitespace will remove textNode if it contains nothing but insignificant whitespace. + if (textNode->inDocument()) + insertTextIntoNode(textNode, 0, nonBreakingSpaceString()); + else { + RefPtr<Text> nbspNode = document()->createTextNode(nonBreakingSpaceString()); + insertNodeAt(nbspNode.get(), positionBeforeTextNode); + endingPosition = Position(nbspNode.get(), 0); + } + } + + setEndingSelection(VisibleSelection(endingPosition, DOWNSTREAM)); + } + + // Handle the case where there is a typing style. + + RefPtr<EditingStyle> typingStyle = document()->frame()->selection()->typingStyle(); + + if (typingStyle && !typingStyle->isEmpty()) { + // Apply the typing style to the inserted line break, so that if the selection + // leaves and then comes back, new input will have the right style. + // FIXME: We shouldn't always apply the typing style to the line break here, + // see <rdar://problem/5794462>. + applyStyle(typingStyle.get(), firstDeepEditingPositionForNode(nodeToInsert.get()), lastDeepEditingPositionForNode(nodeToInsert.get())); + // Even though this applyStyle operates on a Range, it still sets an endingSelection(). + // It tries to set a VisibleSelection around the content it operated on. So, that VisibleSelection + // will either (a) select the line break we inserted, or it will (b) be a caret just + // before the line break (if the line break is at the end of a block it isn't selectable). + // So, this next call sets the endingSelection() to a caret just after the line break + // that we inserted, or just before it if it's at the end of a block. + setEndingSelection(endingSelection().visibleEnd()); + } + + rebalanceWhitespace(); +} + +} diff --git a/Source/WebCore/editing/InsertLineBreakCommand.h b/Source/WebCore/editing/InsertLineBreakCommand.h new file mode 100644 index 0000000..9e73add --- /dev/null +++ b/Source/WebCore/editing/InsertLineBreakCommand.h @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2005, 2006, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef InsertLineBreakCommand_h +#define InsertLineBreakCommand_h + +#include "CompositeEditCommand.h" + +namespace WebCore { + +class InsertLineBreakCommand : public CompositeEditCommand { +public: + static PassRefPtr<InsertLineBreakCommand> create(Document* document) + { + return adoptRef(new InsertLineBreakCommand(document)); + } + +private: + InsertLineBreakCommand(Document*); + + virtual void doApply(); + + virtual bool preservesTypingStyle() const; + + void insertNodeAfterPosition(Node*, const Position&); + void insertNodeBeforePosition(Node*, const Position&); + bool shouldUseBreakElement(const Position&); +}; + +} // namespace WebCore + +#endif // InsertLineBreakCommand_h diff --git a/Source/WebCore/editing/InsertListCommand.cpp b/Source/WebCore/editing/InsertListCommand.cpp new file mode 100644 index 0000000..bb3cd93 --- /dev/null +++ b/Source/WebCore/editing/InsertListCommand.cpp @@ -0,0 +1,393 @@ +/* + * Copyright (C) 2006, 2010 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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 "Element.h" +#include "InsertListCommand.h" +#include "DocumentFragment.h" +#include "htmlediting.h" +#include "HTMLElement.h" +#include "HTMLNames.h" +#include "TextIterator.h" +#include "visible_units.h" + +namespace WebCore { + +using namespace HTMLNames; + +static Node* enclosingListChild(Node* node, Node* listNode) +{ + Node* listChild = enclosingListChild(node); + while (listChild && enclosingList(listChild) != listNode) + listChild = enclosingListChild(listChild->parentNode()); + return listChild; +} + +PassRefPtr<HTMLElement> InsertListCommand::insertList(Document* document, Type type) +{ + RefPtr<InsertListCommand> insertCommand = create(document, type); + insertCommand->apply(); + return insertCommand->m_listElement; +} + +HTMLElement* InsertListCommand::fixOrphanedListChild(Node* node) +{ + RefPtr<HTMLElement> listElement = createUnorderedListElement(document()); + insertNodeBefore(listElement, node); + removeNode(node); + appendNode(node, listElement); + m_listElement = listElement; + return listElement.get(); +} + +PassRefPtr<HTMLElement> InsertListCommand::mergeWithNeighboringLists(PassRefPtr<HTMLElement> passedList) +{ + RefPtr<HTMLElement> list = passedList; + Element* previousList = list->previousElementSibling(); + if (canMergeLists(previousList, list.get())) + mergeIdenticalElements(previousList, list); + + if (!list || !list->nextElementSibling() || !list->nextElementSibling()->isHTMLElement()) + return list.release(); + + RefPtr<HTMLElement> nextList = static_cast<HTMLElement*>(list->nextElementSibling()); + if (canMergeLists(list.get(), nextList.get())) { + mergeIdenticalElements(list, nextList); + return nextList.release(); + } + return list.release(); +} + +bool InsertListCommand::selectionHasListOfType(const VisibleSelection& selection, const QualifiedName& listTag) +{ + VisiblePosition start = selection.visibleStart(); + + if (!enclosingList(start.deepEquivalent().node())) + return false; + + VisiblePosition end = selection.visibleEnd(); + while (start.isNotNull() && start != end) { + Element* listNode = enclosingList(start.deepEquivalent().node()); + if (!listNode || !listNode->hasTagName(listTag)) + return false; + start = startOfNextParagraph(start); + } + + return true; +} + +InsertListCommand::InsertListCommand(Document* document, Type type) + : CompositeEditCommand(document), m_type(type) +{ +} + +void InsertListCommand::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 InsertUn{Ordered}List + // 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))); + + const QualifiedName& listTag = (m_type == OrderedList) ? olTag : ulTag; + if (endingSelection().isRange()) { + VisibleSelection selection = selectionForParagraphIteration(endingSelection()); + ASSERT(selection.isRange()); + VisiblePosition startOfSelection = selection.visibleStart(); + VisiblePosition endOfSelection = selection.visibleEnd(); + VisiblePosition startOfLastParagraph = startOfParagraph(endOfSelection); + + if (startOfParagraph(startOfSelection) != startOfLastParagraph) { + bool forceCreateList = !selectionHasListOfType(selection, listTag); + + RefPtr<Range> currentSelection = endingSelection().firstRange(); + VisiblePosition startOfCurrentParagraph = startOfSelection; + while (startOfCurrentParagraph != startOfLastParagraph) { + // doApply() may operate on and remove the last paragraph of the selection from the document + // if it's in the same list item as startOfCurrentParagraph. Return early to avoid an + // infinite loop and because there is no more work to be done. + // FIXME(<rdar://problem/5983974>): The endingSelection() may be incorrect here. Compute + // the new location of endOfSelection and use it as the end of the new selection. + if (!startOfLastParagraph.deepEquivalent().node()->inDocument()) + return; + setEndingSelection(startOfCurrentParagraph); + + // Save and restore endOfSelection and startOfLastParagraph when necessary + // since moveParagraph and movePragraphWithClones can remove nodes. + // FIXME: This is an inefficient way to keep selection alive because indexForVisiblePosition walks from + // the beginning of the document to the endOfSelection everytime this code is executed. + // But not using index is hard because there are so many ways we can lose selection inside doApplyForSingleParagraph. + int indexForEndOfSelection = indexForVisiblePosition(endOfSelection); + doApplyForSingleParagraph(forceCreateList, listTag, currentSelection.get()); + if (endOfSelection.isNull() || endOfSelection.isOrphan() || startOfLastParagraph.isNull() || startOfLastParagraph.isOrphan()) { + RefPtr<Range> lastSelectionRange = TextIterator::rangeFromLocationAndLength(document()->documentElement(), indexForEndOfSelection, 0, true); + // If lastSelectionRange is null, then some contents have been deleted from the document. + // This should never happen and if it did, exit early immediately because we've lost the loop invariant. + ASSERT(lastSelectionRange); + if (!lastSelectionRange) + return; + endOfSelection = lastSelectionRange->startPosition(); + startOfLastParagraph = startOfParagraph(endOfSelection); + } + + // Fetch the start of the selection after moving the first paragraph, + // because moving the paragraph will invalidate the original start. + // We'll use the new start to restore the original selection after + // we modified all selected paragraphs. + if (startOfCurrentParagraph == startOfSelection) + startOfSelection = endingSelection().visibleStart(); + + startOfCurrentParagraph = startOfNextParagraph(endingSelection().visibleStart()); + } + setEndingSelection(endOfSelection); + doApplyForSingleParagraph(forceCreateList, listTag, currentSelection.get()); + // Fetch the end of the selection, for the reason mentioned above. + endOfSelection = endingSelection().visibleEnd(); + setEndingSelection(VisibleSelection(startOfSelection, endOfSelection)); + return; + } + } + + doApplyForSingleParagraph(false, listTag, endingSelection().firstRange().get()); +} + +void InsertListCommand::doApplyForSingleParagraph(bool forceCreateList, const QualifiedName& listTag, Range* currentSelection) +{ + // FIXME: This will produce unexpected results for a selection that starts just before a + // table and ends inside the first cell, selectionForParagraphIteration should probably + // be renamed and deployed inside setEndingSelection(). + Node* selectionNode = endingSelection().start().node(); + Node* listChildNode = enclosingListChild(selectionNode); + bool switchListType = false; + if (listChildNode) { + // Remove the list chlild. + RefPtr<HTMLElement> listNode = enclosingList(listChildNode); + if (!listNode) { + listNode = fixOrphanedListChild(listChildNode); + listNode = mergeWithNeighboringLists(listNode); + } + if (!listNode->hasTagName(listTag)) + // listChildNode will be removed from the list and a list of type m_type will be created. + switchListType = true; + + // If the list is of the desired type, and we are not removing the list, then exit early. + if (!switchListType && forceCreateList) + return; + + // If the entire list is selected, then convert the whole list. + if (switchListType && isNodeVisiblyContainedWithin(listNode.get(), currentSelection)) { + bool rangeStartIsInList = visiblePositionBeforeNode(listNode.get()) == currentSelection->startPosition(); + bool rangeEndIsInList = visiblePositionAfterNode(listNode.get()) == currentSelection->endPosition(); + + RefPtr<HTMLElement> newList = createHTMLElement(document(), listTag); + insertNodeBefore(newList, listNode); + + Node* firstChildInList = enclosingListChild(VisiblePosition(Position(listNode, 0)).deepEquivalent().node(), listNode.get()); + Node* outerBlock = firstChildInList->isBlockFlow() ? firstChildInList : listNode.get(); + + moveParagraphWithClones(firstPositionInNode(listNode.get()), lastPositionInNode(listNode.get()), newList.get(), outerBlock); + + // Manually remove listNode because moveParagraphWithClones sometimes leaves it behind in the document. + // See the bug 33668 and editing/execCommand/insert-list-orphaned-item-with-nested-lists.html. + // FIXME: This might be a bug in moveParagraphWithClones or deleteSelection. + if (listNode && listNode->inDocument()) + removeNode(listNode); + + newList = mergeWithNeighboringLists(newList); + + // Restore the start and the end of current selection if they started inside listNode + // because moveParagraphWithClones could have removed them. + ExceptionCode ec; + if (rangeStartIsInList && newList) + currentSelection->setStart(newList, 0, ec); + if (rangeEndIsInList && newList) + currentSelection->setEnd(newList, lastOffsetInNode(newList.get()), ec); + + setEndingSelection(VisiblePosition(firstPositionInNode(newList.get()))); + + return; + } + + unlistifyParagraph(endingSelection().visibleStart(), listNode.get(), listChildNode); + } + + if (!listChildNode || switchListType || forceCreateList) + m_listElement = listifyParagraph(endingSelection().visibleStart(), listTag); +} + +void InsertListCommand::unlistifyParagraph(const VisiblePosition& originalStart, HTMLElement* listNode, Node* listChildNode) +{ + Node* nextListChild; + Node* previousListChild; + VisiblePosition start; + VisiblePosition end; + if (listChildNode->hasTagName(liTag)) { + start = firstDeepEditingPositionForNode(listChildNode); + end = lastDeepEditingPositionForNode(listChildNode); + nextListChild = listChildNode->nextSibling(); + previousListChild = listChildNode->previousSibling(); + } else { + // A paragraph is visually a list item minus a list marker. The paragraph will be moved. + start = startOfParagraph(originalStart); + end = endOfParagraph(start); + nextListChild = enclosingListChild(end.next().deepEquivalent().node(), listNode); + ASSERT(nextListChild != listChildNode); + previousListChild = enclosingListChild(start.previous().deepEquivalent().node(), listNode); + ASSERT(previousListChild != listChildNode); + } + // When removing a list, we must always create a placeholder to act as a point of insertion + // for the list content being removed. + RefPtr<Element> placeholder = createBreakElement(document()); + RefPtr<Element> nodeToInsert = placeholder; + // If the content of the list item will be moved into another list, put it in a list item + // so that we don't create an orphaned list child. + if (enclosingList(listNode)) { + nodeToInsert = createListItemElement(document()); + appendNode(placeholder, nodeToInsert); + } + + if (nextListChild && previousListChild) { + // We want to pull listChildNode out of listNode, and place it before nextListChild + // and after previousListChild, so we split listNode and insert it between the two lists. + // But to split listNode, we must first split ancestors of listChildNode between it and listNode, + // if any exist. + // FIXME: We appear to split at nextListChild as opposed to listChildNode so that when we remove + // listChildNode below in moveParagraphs, previousListChild will be removed along with it if it is + // unrendered. But we ought to remove nextListChild too, if it is unrendered. + splitElement(listNode, splitTreeToNode(nextListChild, listNode)); + insertNodeBefore(nodeToInsert, listNode); + } else if (nextListChild || listChildNode->parentNode() != listNode) { + // Just because listChildNode has no previousListChild doesn't mean there isn't any content + // in listNode that comes before listChildNode, as listChildNode could have ancestors + // between it and listNode. So, we split up to listNode before inserting the placeholder + // where we're about to move listChildNode to. + if (listChildNode->parentNode() != listNode) + splitElement(listNode, splitTreeToNode(listChildNode, listNode).get()); + insertNodeBefore(nodeToInsert, listNode); + } else + insertNodeAfter(nodeToInsert, listNode); + + VisiblePosition insertionPoint = VisiblePosition(Position(placeholder.get(), 0)); + moveParagraphs(start, end, insertionPoint, true); +} + +static Element* adjacentEnclosingList(const VisiblePosition& pos, const VisiblePosition& adjacentPos, const QualifiedName& listTag) +{ + Element* listNode = outermostEnclosingList(adjacentPos.deepEquivalent().node()); + + if (!listNode) + return 0; + + Node* previousCell = enclosingTableCell(pos.deepEquivalent()); + Node* currentCell = enclosingTableCell(adjacentPos.deepEquivalent()); + + if (!listNode->hasTagName(listTag) + || listNode->contains(pos.deepEquivalent().node()) + || previousCell != currentCell + || enclosingList(listNode) != enclosingList(pos.deepEquivalent().node())) + return 0; + + return listNode; +} + +PassRefPtr<HTMLElement> InsertListCommand::listifyParagraph(const VisiblePosition& originalStart, const QualifiedName& listTag) +{ + VisiblePosition start = startOfParagraph(originalStart); + VisiblePosition end = endOfParagraph(start); + + if (start.isNull() || end.isNull()) + return 0; + + // Check for adjoining lists. + RefPtr<HTMLElement> listItemElement = createListItemElement(document()); + RefPtr<HTMLElement> placeholder = createBreakElement(document()); + appendNode(placeholder, listItemElement); + + // Place list item into adjoining lists. + Element* previousList = adjacentEnclosingList(start.deepEquivalent(), start.previous(true), listTag); + Element* nextList = adjacentEnclosingList(start.deepEquivalent(), end.next(true), listTag); + RefPtr<HTMLElement> listElement; + if (previousList) + appendNode(listItemElement, previousList); + else if (nextList) + insertNodeAt(listItemElement, Position(nextList, 0)); + else { + // Create the list. + listElement = createHTMLElement(document(), listTag); + appendNode(listItemElement, listElement); + + if (start == end && isBlock(start.deepEquivalent().node())) { + // Inserting the list into an empty paragraph that isn't held open + // by a br or a '\n', will invalidate start and end. Insert + // a placeholder and then recompute start and end. + RefPtr<Node> placeholder = insertBlockPlaceholder(start.deepEquivalent()); + start = VisiblePosition(Position(placeholder.get(), 0)); + end = start; + } + + // Insert the list at a position visually equivalent to start of the + // paragraph that is being moved into the list. + // Try to avoid inserting it somewhere where it will be surrounded by + // inline ancestors of start, since it is easier for editing to produce + // clean markup when inline elements are pushed down as far as possible. + Position insertionPos(start.deepEquivalent().upstream()); + // Also avoid the containing list item. + Node* listChild = enclosingListChild(insertionPos.node()); + if (listChild && listChild->hasTagName(liTag)) + insertionPos = positionInParentBeforeNode(listChild); + + insertNodeAt(listElement, insertionPos); + + // We inserted the list at the start of the content we're about to move + // Update the start of content, so we don't try to move the list into itself. bug 19066 + if (insertionPos == start.deepEquivalent()) + start = startOfParagraph(originalStart); + } + + moveParagraph(start, end, VisiblePosition(Position(placeholder.get(), 0)), true); + + if (listElement) + return mergeWithNeighboringLists(listElement); + + if (canMergeLists(previousList, nextList)) + mergeIdenticalElements(previousList, nextList); + + return listElement; +} + +} diff --git a/Source/WebCore/editing/InsertListCommand.h b/Source/WebCore/editing/InsertListCommand.h new file mode 100644 index 0000000..b81ae74 --- /dev/null +++ b/Source/WebCore/editing/InsertListCommand.h @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2006, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef InsertListCommand_h +#define InsertListCommand_h + +#include "CompositeEditCommand.h" + +namespace WebCore { + +class HTMLElement; + +class InsertListCommand : public CompositeEditCommand { +public: + enum Type { OrderedList, UnorderedList }; + + static PassRefPtr<InsertListCommand> create(Document* document, Type listType) + { + return adoptRef(new InsertListCommand(document, listType)); + } + + static PassRefPtr<HTMLElement> insertList(Document*, Type); + + virtual bool preservesTypingStyle() const { return true; } + +private: + InsertListCommand(Document*, Type); + + virtual void doApply(); + virtual EditAction editingAction() const { return EditActionInsertList; } + + HTMLElement* fixOrphanedListChild(Node*); + bool selectionHasListOfType(const VisibleSelection& selection, const QualifiedName&); + PassRefPtr<HTMLElement> mergeWithNeighboringLists(PassRefPtr<HTMLElement>); + void doApplyForSingleParagraph(bool forceCreateList, const QualifiedName&, Range* currentSelection); + void unlistifyParagraph(const VisiblePosition& originalStart, HTMLElement* listNode, Node* listChildNode); + PassRefPtr<HTMLElement> listifyParagraph(const VisiblePosition& originalStart, const QualifiedName& listTag); + RefPtr<HTMLElement> m_listElement; + Type m_type; +}; + +} // namespace WebCore + +#endif // InsertListCommand_h diff --git a/Source/WebCore/editing/InsertNodeBeforeCommand.cpp b/Source/WebCore/editing/InsertNodeBeforeCommand.cpp new file mode 100644 index 0000000..5fae45e --- /dev/null +++ b/Source/WebCore/editing/InsertNodeBeforeCommand.cpp @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2005, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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 "InsertNodeBeforeCommand.h" + +#include "AXObjectCache.h" +#include "htmlediting.h" + +namespace WebCore { + +InsertNodeBeforeCommand::InsertNodeBeforeCommand(PassRefPtr<Node> insertChild, PassRefPtr<Node> refChild) + : SimpleEditCommand(refChild->document()) + , m_insertChild(insertChild) + , m_refChild(refChild) +{ + ASSERT(m_insertChild); + ASSERT(!m_insertChild->parentNode()); + ASSERT(m_refChild); + ASSERT(m_refChild->parentNode()); + + ASSERT(m_refChild->parentNode()->isContentEditable() || !m_refChild->parentNode()->attached()); +} + +void InsertNodeBeforeCommand::doApply() +{ + ContainerNode* parent = m_refChild->parentNode(); + if (!parent || !parent->isContentEditable()) + return; + + ExceptionCode ec; + parent->insertBefore(m_insertChild.get(), m_refChild.get(), ec); + + if (AXObjectCache::accessibilityEnabled()) + document()->axObjectCache()->nodeTextChangeNotification(m_insertChild->renderer(), AXObjectCache::AXTextInserted, 0, m_insertChild->nodeValue().length()); +} + +void InsertNodeBeforeCommand::doUnapply() +{ + if (!m_insertChild->isContentEditable()) + return; + + // Need to notify this before actually deleting the text + if (AXObjectCache::accessibilityEnabled()) + document()->axObjectCache()->nodeTextChangeNotification(m_insertChild->renderer(), AXObjectCache::AXTextDeleted, 0, m_insertChild->nodeValue().length()); + + ExceptionCode ec; + m_insertChild->remove(ec); +} + +} diff --git a/Source/WebCore/editing/InsertNodeBeforeCommand.h b/Source/WebCore/editing/InsertNodeBeforeCommand.h new file mode 100644 index 0000000..0904502 --- /dev/null +++ b/Source/WebCore/editing/InsertNodeBeforeCommand.h @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2005, 2006, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef InsertNodeBeforeCommand_h +#define InsertNodeBeforeCommand_h + +#include "EditCommand.h" + +namespace WebCore { + +class InsertNodeBeforeCommand : public SimpleEditCommand { +public: + static PassRefPtr<InsertNodeBeforeCommand> create(PassRefPtr<Node> childToInsert, PassRefPtr<Node> childToInsertBefore) + { + return adoptRef(new InsertNodeBeforeCommand(childToInsert, childToInsertBefore)); + } + +private: + InsertNodeBeforeCommand(PassRefPtr<Node> childToInsert, PassRefPtr<Node> childToInsertBefore); + + virtual void doApply(); + virtual void doUnapply(); + + RefPtr<Node> m_insertChild; + RefPtr<Node> m_refChild; +}; + +} // namespace WebCore + +#endif // InsertNodeBeforeCommand_h diff --git a/Source/WebCore/editing/InsertParagraphSeparatorCommand.cpp b/Source/WebCore/editing/InsertParagraphSeparatorCommand.cpp new file mode 100644 index 0000000..1838382 --- /dev/null +++ b/Source/WebCore/editing/InsertParagraphSeparatorCommand.cpp @@ -0,0 +1,395 @@ +/* + * Copyright (C) 2005, 2006 Apple Computer, 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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 "InsertParagraphSeparatorCommand.h" + +#include "CSSPropertyNames.h" +#include "Document.h" +#include "EditingStyle.h" +#include "HTMLElement.h" +#include "HTMLNames.h" +#include "InsertLineBreakCommand.h" +#include "RenderObject.h" +#include "Text.h" +#include "htmlediting.h" +#include "visible_units.h" + +namespace WebCore { + +using namespace HTMLNames; + +// When inserting a new line, we want to avoid nesting empty divs if we can. Otherwise, when +// pasting, it's easy to have each new line be a div deeper than the previous. E.g., in the case +// below, we want to insert at ^ instead of |. +// <div>foo<div>bar</div>|</div>^ +static Element* highestVisuallyEquivalentDivBelowRoot(Element* startBlock) +{ + Element* curBlock = startBlock; + // We don't want to return a root node (if it happens to be a div, e.g., in a document fragment) because there are no + // siblings for us to append to. + while (!curBlock->nextSibling() && curBlock->parentElement()->hasTagName(divTag) && curBlock->parentElement()->parentElement()) { + NamedNodeMap* attributes = curBlock->parentElement()->attributes(true); + if (attributes && !attributes->isEmpty()) + break; + curBlock = curBlock->parentElement(); + } + return curBlock; +} + +InsertParagraphSeparatorCommand::InsertParagraphSeparatorCommand(Document *document, bool mustUseDefaultParagraphElement) + : CompositeEditCommand(document) + , m_mustUseDefaultParagraphElement(mustUseDefaultParagraphElement) +{ +} + +bool InsertParagraphSeparatorCommand::preservesTypingStyle() const +{ + return true; +} + +void InsertParagraphSeparatorCommand::calculateStyleBeforeInsertion(const Position &pos) +{ + // It is only important to set a style to apply later if we're at the boundaries of + // a paragraph. Otherwise, content that is moved as part of the work of the command + // will lend their styles to the new paragraph without any extra work needed. + VisiblePosition visiblePos(pos, VP_DEFAULT_AFFINITY); + if (!isStartOfParagraph(visiblePos) && !isEndOfParagraph(visiblePos)) + return; + + m_style = editingStyleIncludingTypingStyle(pos); +} + +void InsertParagraphSeparatorCommand::applyStyleAfterInsertion(Node* originalEnclosingBlock) +{ + // Not only do we break out of header tags, but we also do not preserve the typing style, + // in order to match other browsers. + if (originalEnclosingBlock->hasTagName(h1Tag) || + originalEnclosingBlock->hasTagName(h2Tag) || + originalEnclosingBlock->hasTagName(h3Tag) || + originalEnclosingBlock->hasTagName(h4Tag) || + originalEnclosingBlock->hasTagName(h5Tag)) + return; + + if (!m_style) + return; + + m_style->prepareToApplyAt(endingSelection().start()); + if (!m_style->isEmpty()) + applyStyle(m_style.get()); +} + +bool InsertParagraphSeparatorCommand::shouldUseDefaultParagraphElement(Node* enclosingBlock) const +{ + if (m_mustUseDefaultParagraphElement) + return true; + + // Assumes that if there was a range selection, it was already deleted. + if (!isEndOfBlock(endingSelection().visibleStart())) + return false; + + return enclosingBlock->hasTagName(h1Tag) || + enclosingBlock->hasTagName(h2Tag) || + enclosingBlock->hasTagName(h3Tag) || + enclosingBlock->hasTagName(h4Tag) || + enclosingBlock->hasTagName(h5Tag); +} + +void InsertParagraphSeparatorCommand::getAncestorsInsideBlock(const Node* insertionNode, Element* outerBlock, Vector<Element*>& ancestors) +{ + ancestors.clear(); + + // Build up list of ancestors elements between the insertion node and the outer block. + if (insertionNode != outerBlock) { + for (Element* n = insertionNode->parentElement(); n && n != outerBlock; n = n->parentElement()) + ancestors.append(n); + } +} + +PassRefPtr<Element> InsertParagraphSeparatorCommand::cloneHierarchyUnderNewBlock(const Vector<Element*>& ancestors, PassRefPtr<Element> blockToInsert) +{ + // Make clones of ancestors in between the start node and the start block. + RefPtr<Element> parent = blockToInsert; + for (size_t i = ancestors.size(); i != 0; --i) { + RefPtr<Element> child = ancestors[i - 1]->cloneElementWithoutChildren(); + appendNode(child, parent); + parent = child.release(); + } + + return parent.release(); +} + +void InsertParagraphSeparatorCommand::doApply() +{ + bool splitText = false; + if (!endingSelection().isNonOrphanedCaretOrRange()) + return; + + Position insertionPosition = endingSelection().start(); + + EAffinity affinity = endingSelection().affinity(); + + // Delete the current selection. + if (endingSelection().isRange()) { + calculateStyleBeforeInsertion(insertionPosition); + deleteSelection(false, true); + insertionPosition = endingSelection().start(); + affinity = endingSelection().affinity(); + } + + // FIXME: The rangeCompliantEquivalent conversion needs to be moved into enclosingBlock. + Node* startBlockNode = enclosingBlock(rangeCompliantEquivalent(insertionPosition).node()); + Position canonicalPos = VisiblePosition(insertionPosition).deepEquivalent(); + Element* startBlock = static_cast<Element*>(startBlockNode); + if (!startBlockNode + || !startBlockNode->isElementNode() + || !startBlock->parentNode() + || isTableCell(startBlock) + || startBlock->hasTagName(formTag) + // FIXME: If the node is hidden, we don't have a canonical position so we will do the wrong thing for tables and <hr>. https://bugs.webkit.org/show_bug.cgi?id=40342 + || (!canonicalPos.isNull() && canonicalPos.node()->renderer() && canonicalPos.node()->renderer()->isTable()) + || (!canonicalPos.isNull() && canonicalPos.node()->hasTagName(hrTag))) { + applyCommandToComposite(InsertLineBreakCommand::create(document())); + return; + } + + // Use the leftmost candidate. + insertionPosition = insertionPosition.upstream(); + if (!insertionPosition.isCandidate()) + insertionPosition = insertionPosition.downstream(); + + // Adjust the insertion position after the delete + insertionPosition = positionAvoidingSpecialElementBoundary(insertionPosition); + VisiblePosition visiblePos(insertionPosition, affinity); + calculateStyleBeforeInsertion(insertionPosition); + + //--------------------------------------------------------------------- + // Handle special case of typing return on an empty list item + if (breakOutOfEmptyListItem()) + return; + + //--------------------------------------------------------------------- + // Prepare for more general cases. + + bool isFirstInBlock = isStartOfBlock(visiblePos); + bool isLastInBlock = isEndOfBlock(visiblePos); + bool nestNewBlock = false; + + // Create block to be inserted. + RefPtr<Element> blockToInsert; + if (startBlock == startBlock->rootEditableElement()) { + blockToInsert = createDefaultParagraphElement(document()); + nestNewBlock = true; + } else if (shouldUseDefaultParagraphElement(startBlock)) + blockToInsert = createDefaultParagraphElement(document()); + else + blockToInsert = startBlock->cloneElementWithoutChildren(); + + //--------------------------------------------------------------------- + // Handle case when position is in the last visible position in its block, + // including when the block is empty. + if (isLastInBlock) { + if (nestNewBlock) { + if (isFirstInBlock && !lineBreakExistsAtVisiblePosition(visiblePos)) { + // The block is empty. Create an empty block to + // represent the paragraph that we're leaving. + RefPtr<Element> extraBlock = createDefaultParagraphElement(document()); + appendNode(extraBlock, startBlock); + appendBlockPlaceholder(extraBlock); + } + appendNode(blockToInsert, startBlock); + } else { + // We can get here if we pasted a copied portion of a blockquote with a newline at the end and are trying to paste it + // into an unquoted area. We then don't want the newline within the blockquote or else it will also be quoted. + if (Node* highestBlockquote = highestEnclosingNodeOfType(canonicalPos, &isMailBlockquote)) + startBlock = static_cast<Element*>(highestBlockquote); + + // Most of the time we want to stay at the nesting level of the startBlock (e.g., when nesting within lists). However, + // for div nodes, this can result in nested div tags that are hard to break out of. + Element* siblingNode = startBlock; + if (blockToInsert->hasTagName(divTag)) + siblingNode = highestVisuallyEquivalentDivBelowRoot(startBlock); + insertNodeAfter(blockToInsert, siblingNode); + } + + // Recreate the same structure in the new paragraph. + + Vector<Element*> ancestors; + getAncestorsInsideBlock(insertionPosition.node(), startBlock, ancestors); + RefPtr<Element> parent = cloneHierarchyUnderNewBlock(ancestors, blockToInsert); + + appendBlockPlaceholder(parent); + + setEndingSelection(VisibleSelection(Position(parent.get(), 0), DOWNSTREAM)); + return; + } + + + //--------------------------------------------------------------------- + // Handle case when position is in the first visible position in its block, and + // similar case where previous position is in another, presumeably nested, block. + if (isFirstInBlock || !inSameBlock(visiblePos, visiblePos.previous())) { + Node *refNode; + if (isFirstInBlock && !nestNewBlock) + refNode = startBlock; + else if (insertionPosition.node() == startBlock && nestNewBlock) { + refNode = startBlock->childNode(insertionPosition.deprecatedEditingOffset()); + ASSERT(refNode); // must be true or we'd be in the end of block case + } else + refNode = insertionPosition.node(); + + // find ending selection position easily before inserting the paragraph + insertionPosition = insertionPosition.downstream(); + + insertNodeBefore(blockToInsert, refNode); + + // Recreate the same structure in the new paragraph. + + Vector<Element*> ancestors; + getAncestorsInsideBlock(positionAvoidingSpecialElementBoundary(insertionPosition).node(), startBlock, ancestors); + + appendBlockPlaceholder(cloneHierarchyUnderNewBlock(ancestors, blockToInsert)); + + // In this case, we need to set the new ending selection. + setEndingSelection(VisibleSelection(insertionPosition, DOWNSTREAM)); + return; + } + + //--------------------------------------------------------------------- + // Handle the (more complicated) general case, + + // All of the content in the current block after visiblePos is + // about to be wrapped in a new paragraph element. Add a br before + // it if visiblePos is at the start of a paragraph so that the + // content will move down a line. + if (isStartOfParagraph(visiblePos)) { + RefPtr<Element> br = createBreakElement(document()); + insertNodeAt(br.get(), insertionPosition); + insertionPosition = positionInParentAfterNode(br.get()); + } + + // Move downstream. Typing style code will take care of carrying along the + // style of the upstream position. + insertionPosition = insertionPosition.downstream(); + + // At this point, the insertionPosition's node could be a container, and we want to make sure we include + // all of the correct nodes when building the ancestor list. So this needs to be the deepest representation of the position + // before we walk the DOM tree. + insertionPosition = VisiblePosition(insertionPosition).deepEquivalent(); + + // Build up list of ancestors in between the start node and the start block. + Vector<Element*> ancestors; + getAncestorsInsideBlock(insertionPosition.node(), startBlock, ancestors); + + // Make sure we do not cause a rendered space to become unrendered. + // FIXME: We need the affinity for pos, but pos.downstream() does not give it + Position leadingWhitespace = insertionPosition.leadingWhitespacePosition(VP_DEFAULT_AFFINITY); + // FIXME: leadingWhitespacePosition is returning the position before preserved newlines for positions + // after the preserved newline, causing the newline to be turned into a nbsp. + if (leadingWhitespace.isNotNull() && leadingWhitespace.node()->isTextNode()) { + Text* textNode = static_cast<Text*>(leadingWhitespace.node()); + ASSERT(!textNode->renderer() || textNode->renderer()->style()->collapseWhiteSpace()); + replaceTextInNode(textNode, leadingWhitespace.deprecatedEditingOffset(), 1, nonBreakingSpaceString()); + } + + // Split at pos if in the middle of a text node. + if (insertionPosition.node()->isTextNode()) { + Text* textNode = static_cast<Text*>(insertionPosition.node()); + bool atEnd = (unsigned)insertionPosition.deprecatedEditingOffset() >= textNode->length(); + if (insertionPosition.deprecatedEditingOffset() > 0 && !atEnd) { + splitTextNode(textNode, insertionPosition.deprecatedEditingOffset()); + insertionPosition.moveToOffset(0); + visiblePos = VisiblePosition(insertionPosition); + splitText = true; + } + } + + // Put the added block in the tree. + if (nestNewBlock) + appendNode(blockToInsert.get(), startBlock); + else + insertNodeAfter(blockToInsert.get(), startBlock); + + updateLayout(); + + // Make clones of ancestors in between the start node and the outer block. + RefPtr<Element> parent = cloneHierarchyUnderNewBlock(ancestors, blockToInsert); + + // If the paragraph separator was inserted at the end of a paragraph, an empty line must be + // created. All of the nodes, starting at visiblePos, are about to be added to the new paragraph + // element. If the first node to be inserted won't be one that will hold an empty line open, add a br. + if (isEndOfParagraph(visiblePos) && !lineBreakExistsAtVisiblePosition(visiblePos)) + appendNode(createBreakElement(document()).get(), blockToInsert.get()); + + // Move the start node and the siblings of the start node. + if (insertionPosition.node() != startBlock) { + Node* n = insertionPosition.node(); + if (insertionPosition.deprecatedEditingOffset() >= caretMaxOffset(n)) + n = n->nextSibling(); + + while (n && n != blockToInsert) { + Node *next = n->nextSibling(); + removeNode(n); + appendNode(n, parent.get()); + n = next; + } + } + + // Move everything after the start node. + if (!ancestors.isEmpty()) { + Element* leftParent = ancestors.first(); + while (leftParent && leftParent != startBlock) { + parent = parent->parentElement(); + if (!parent) + break; + Node* n = leftParent->nextSibling(); + while (n && n != blockToInsert) { + Node* next = n->nextSibling(); + removeNode(n); + appendNode(n, parent.get()); + n = next; + } + leftParent = leftParent->parentElement(); + } + } + + // Handle whitespace that occurs after the split + if (splitText) { + updateLayout(); + insertionPosition = Position(insertionPosition.node(), 0); + if (!insertionPosition.isRenderedCharacter()) { + // Clear out all whitespace and insert one non-breaking space + ASSERT(!insertionPosition.node()->renderer() || insertionPosition.node()->renderer()->style()->collapseWhiteSpace()); + deleteInsignificantTextDownstream(insertionPosition); + if (insertionPosition.node()->isTextNode()) + insertTextIntoNode(static_cast<Text*>(insertionPosition.node()), 0, nonBreakingSpaceString()); + } + } + + setEndingSelection(VisibleSelection(Position(blockToInsert.get(), 0), DOWNSTREAM)); + applyStyleAfterInsertion(startBlock); +} + +} // namespace WebCore diff --git a/Source/WebCore/editing/InsertParagraphSeparatorCommand.h b/Source/WebCore/editing/InsertParagraphSeparatorCommand.h new file mode 100644 index 0000000..2eae77d --- /dev/null +++ b/Source/WebCore/editing/InsertParagraphSeparatorCommand.h @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2005, 2006, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef InsertParagraphSeparatorCommand_h +#define InsertParagraphSeparatorCommand_h + +#include "CompositeEditCommand.h" + +namespace WebCore { + +class EditingStyle; + +class InsertParagraphSeparatorCommand : public CompositeEditCommand { +public: + static PassRefPtr<InsertParagraphSeparatorCommand> create(Document* document, bool useDefaultParagraphElement = false) + { + return adoptRef(new InsertParagraphSeparatorCommand(document, useDefaultParagraphElement)); + } + +private: + InsertParagraphSeparatorCommand(Document*, bool useDefaultParagraphElement); + + virtual void doApply(); + + void calculateStyleBeforeInsertion(const Position&); + void applyStyleAfterInsertion(Node* originalEnclosingBlock); + void getAncestorsInsideBlock(const Node* insertionNode, Element* outerBlock, Vector<Element*>& ancestors); + PassRefPtr<Element> cloneHierarchyUnderNewBlock(const Vector<Element*>& ancestors, PassRefPtr<Element> blockToInsert); + + bool shouldUseDefaultParagraphElement(Node*) const; + + virtual bool preservesTypingStyle() const; + + RefPtr<EditingStyle> m_style; + + bool m_mustUseDefaultParagraphElement; +}; + +} + +#endif diff --git a/Source/WebCore/editing/InsertTextCommand.cpp b/Source/WebCore/editing/InsertTextCommand.cpp new file mode 100644 index 0000000..fc18e91 --- /dev/null +++ b/Source/WebCore/editing/InsertTextCommand.cpp @@ -0,0 +1,243 @@ +/* + * Copyright (C) 2005 Apple Computer, 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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 "InsertTextCommand.h" + +#include "CharacterNames.h" +#include "CSSComputedStyleDeclaration.h" +#include "CSSMutableStyleDeclaration.h" +#include "CSSPropertyNames.h" +#include "Document.h" +#include "Element.h" +#include "EditingText.h" +#include "Editor.h" +#include "Frame.h" +#include "Logging.h" +#include "HTMLInterchange.h" +#include "htmlediting.h" +#include "TextIterator.h" +#include "TypingCommand.h" +#include "visible_units.h" + +namespace WebCore { + +InsertTextCommand::InsertTextCommand(Document *document) + : CompositeEditCommand(document) +{ +} + +void InsertTextCommand::doApply() +{ +} + +Position InsertTextCommand::prepareForTextInsertion(const Position& p) +{ + Position pos = p; + // Prepare for text input by looking at the specified position. + // It may be necessary to insert a text node to receive characters. + if (!pos.node()->isTextNode()) { + RefPtr<Node> textNode = document()->createEditingTextNode(""); + insertNodeAt(textNode.get(), pos); + return Position(textNode.get(), 0); + } + + if (isTabSpanTextNode(pos.node())) { + RefPtr<Node> textNode = document()->createEditingTextNode(""); + insertNodeAtTabSpanPosition(textNode.get(), pos); + return Position(textNode.get(), 0); + } + + return pos; +} + +// This avoids the expense of a full fledged delete operation, and avoids a layout that typically results +// from text removal. +bool InsertTextCommand::performTrivialReplace(const String& text, bool selectInsertedText) +{ + if (!endingSelection().isRange()) + return false; + + if (text.contains('\t') || text.contains(' ') || text.contains('\n')) + return false; + + Position start = endingSelection().start(); + Position end = endingSelection().end(); + + if (start.node() != end.node() || !start.node()->isTextNode() || isTabSpanTextNode(start.node())) + return false; + + replaceTextInNode(static_cast<Text*>(start.node()), start.deprecatedEditingOffset(), end.deprecatedEditingOffset() - start.deprecatedEditingOffset(), text); + + Position endPosition(start.node(), start.deprecatedEditingOffset() + text.length()); + + // We could have inserted a part of composed character sequence, + // so we are basically treating ending selection as a range to avoid validation. + // <http://bugs.webkit.org/show_bug.cgi?id=15781> + VisibleSelection forcedEndingSelection; + forcedEndingSelection.setWithoutValidation(start, endPosition); + setEndingSelection(forcedEndingSelection); + + if (!selectInsertedText) + setEndingSelection(VisibleSelection(endingSelection().visibleEnd())); + + return true; +} + +void InsertTextCommand::input(const String& text, bool selectInsertedText) +{ + + ASSERT(text.find('\n') == notFound); + + if (!endingSelection().isNonOrphanedCaretOrRange()) + return; + + // Delete the current selection. + // FIXME: This delete operation blows away the typing style. + if (endingSelection().isRange()) { + if (performTrivialReplace(text, selectInsertedText)) + return; + deleteSelection(false, true, true, false); + } + + Position startPosition(endingSelection().start()); + + Position placeholder; + // We want to remove preserved newlines and brs that will collapse (and thus become unnecessary) when content + // is inserted just before them. + // FIXME: We shouldn't really have to do this, but removing placeholders is a workaround for 9661. + // If the caret is just before a placeholder, downstream will normalize the caret to it. + Position downstream(startPosition.downstream()); + if (lineBreakExistsAtPosition(downstream)) { + // FIXME: This doesn't handle placeholders at the end of anonymous blocks. + VisiblePosition caret(startPosition); + if (isEndOfBlock(caret) && isStartOfParagraph(caret)) + placeholder = downstream; + // Don't remove the placeholder yet, otherwise the block we're inserting into would collapse before + // we get a chance to insert into it. We check for a placeholder now, though, because doing so requires + // the creation of a VisiblePosition, and if we did that post-insertion it would force a layout. + } + + // Insert the character at the leftmost candidate. + startPosition = startPosition.upstream(); + + // It is possible for the node that contains startPosition to contain only unrendered whitespace, + // and so deleteInsignificantText could remove it. Save the position before the node in case that happens. + Position positionBeforeStartNode(positionInParentBeforeNode(startPosition.node())); + deleteInsignificantText(startPosition.upstream(), startPosition.downstream()); + if (!startPosition.node()->inDocument()) + startPosition = positionBeforeStartNode; + if (!startPosition.isCandidate()) + startPosition = startPosition.downstream(); + + startPosition = positionAvoidingSpecialElementBoundary(startPosition); + + Position endPosition; + + if (text == "\t") { + endPosition = insertTab(startPosition); + startPosition = endPosition.previous(); + if (placeholder.isNotNull()) + removePlaceholderAt(placeholder); + } else { + // Make sure the document is set up to receive text + startPosition = prepareForTextInsertion(startPosition); + if (placeholder.isNotNull()) + removePlaceholderAt(placeholder); + Text *textNode = static_cast<Text *>(startPosition.node()); + int offset = startPosition.deprecatedEditingOffset(); + + insertTextIntoNode(textNode, offset, text); + endPosition = Position(textNode, offset + text.length()); + + // The insertion may require adjusting adjacent whitespace, if it is present. + rebalanceWhitespaceAt(endPosition); + // Rebalancing on both sides isn't necessary if we've inserted a space. + if (text != " ") + rebalanceWhitespaceAt(startPosition); + } + + // We could have inserted a part of composed character sequence, + // so we are basically treating ending selection as a range to avoid validation. + // <http://bugs.webkit.org/show_bug.cgi?id=15781> + VisibleSelection forcedEndingSelection; + forcedEndingSelection.setWithoutValidation(startPosition, endPosition); + setEndingSelection(forcedEndingSelection); + + // Handle the case where there is a typing style. + if (RefPtr<EditingStyle> typingStyle = document()->frame()->selection()->typingStyle()) { + typingStyle->prepareToApplyAt(endPosition, EditingStyle::PreserveWritingDirection); + if (!typingStyle->isEmpty()) + applyStyle(typingStyle.get()); + } + + if (!selectInsertedText) + setEndingSelection(VisibleSelection(endingSelection().end(), endingSelection().affinity())); +} + +Position InsertTextCommand::insertTab(const Position& pos) +{ + Position insertPos = VisiblePosition(pos, DOWNSTREAM).deepEquivalent(); + + Node *node = insertPos.node(); + unsigned int offset = insertPos.deprecatedEditingOffset(); + + // keep tabs coalesced in tab span + if (isTabSpanTextNode(node)) { + insertTextIntoNode(static_cast<Text *>(node), offset, "\t"); + return Position(node, offset + 1); + } + + // create new tab span + RefPtr<Element> spanNode = createTabSpanElement(document()); + + // place it + if (!node->isTextNode()) { + insertNodeAt(spanNode.get(), insertPos); + } else { + Text *textNode = static_cast<Text *>(node); + if (offset >= textNode->length()) { + insertNodeAfter(spanNode.get(), textNode); + } else { + // split node to make room for the span + // NOTE: splitTextNode uses textNode for the + // second node in the split, so we need to + // insert the span before it. + if (offset > 0) + splitTextNode(textNode, offset); + insertNodeBefore(spanNode, textNode); + } + } + + // return the position following the new tab + return Position(spanNode->lastChild(), caretMaxOffset(spanNode->lastChild())); +} + +bool InsertTextCommand::isInsertTextCommand() const +{ + return true; +} + +} diff --git a/Source/WebCore/editing/InsertTextCommand.h b/Source/WebCore/editing/InsertTextCommand.h new file mode 100644 index 0000000..77ae016 --- /dev/null +++ b/Source/WebCore/editing/InsertTextCommand.h @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2005, 2006, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef InsertTextCommand_h +#define InsertTextCommand_h + +#include "CompositeEditCommand.h" + +namespace WebCore { + +class InsertTextCommand : public CompositeEditCommand { +public: + static PassRefPtr<InsertTextCommand> create(Document* document) + { + return adoptRef(new InsertTextCommand(document)); + } + + void input(const String& text, bool selectInsertedText = false); + +private: + InsertTextCommand(Document*); + + void deleteCharacter(); + + virtual void doApply(); + virtual bool isInsertTextCommand() const; + + Position prepareForTextInsertion(const Position&); + Position insertTab(const Position&); + + bool performTrivialReplace(const String&, bool selectInsertedText); + + friend class TypingCommand; +}; + +} // namespace WebCore + +#endif // InsertTextCommand_h diff --git a/Source/WebCore/editing/JoinTextNodesCommand.cpp b/Source/WebCore/editing/JoinTextNodesCommand.cpp new file mode 100644 index 0000000..2766b84 --- /dev/null +++ b/Source/WebCore/editing/JoinTextNodesCommand.cpp @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2005, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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 "JoinTextNodesCommand.h" + +#include "Text.h" + +namespace WebCore { + +JoinTextNodesCommand::JoinTextNodesCommand(PassRefPtr<Text> text1, PassRefPtr<Text> text2) + : SimpleEditCommand(text1->document()), m_text1(text1), m_text2(text2) +{ + ASSERT(m_text1); + ASSERT(m_text2); + ASSERT(m_text1->nextSibling() == m_text2); + ASSERT(m_text1->length() > 0); + ASSERT(m_text2->length() > 0); +} + +void JoinTextNodesCommand::doApply() +{ + if (m_text1->nextSibling() != m_text2) + return; + + ContainerNode* parent = m_text2->parentNode(); + if (!parent || !parent->isContentEditable()) + return; + + ExceptionCode ec = 0; + m_text2->insertData(0, m_text1->data(), ec); + if (ec) + return; + + m_text1->remove(ec); +} + +void JoinTextNodesCommand::doUnapply() +{ + if (m_text1->parentNode()) + return; + + ContainerNode* parent = m_text2->parentNode(); + if (!parent || !parent->isContentEditable()) + return; + + ExceptionCode ec = 0; + + parent->insertBefore(m_text1.get(), m_text2.get(), ec); + if (ec) + return; + + m_text2->deleteData(0, m_text1->length(), ec); +} + +} // namespace WebCore diff --git a/Source/WebCore/editing/JoinTextNodesCommand.h b/Source/WebCore/editing/JoinTextNodesCommand.h new file mode 100644 index 0000000..6bdc6e6 --- /dev/null +++ b/Source/WebCore/editing/JoinTextNodesCommand.h @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2005, 2006, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef JoinTextNodesCommand_h +#define JoinTextNodesCommand_h + +#include "EditCommand.h" + +namespace WebCore { + +class Text; + +class JoinTextNodesCommand : public SimpleEditCommand { +public: + static PassRefPtr<JoinTextNodesCommand> create(PassRefPtr<Text> text1, PassRefPtr<Text> text2) + { + return adoptRef(new JoinTextNodesCommand(text1, text2)); + } + +private: + JoinTextNodesCommand(PassRefPtr<Text>, PassRefPtr<Text>); + + virtual void doApply(); + virtual void doUnapply(); + + RefPtr<Text> m_text1; + RefPtr<Text> m_text2; +}; + +} // namespace WebCore + +#endif // JoinTextNodesCommand_h diff --git a/Source/WebCore/editing/MarkupAccumulator.cpp b/Source/WebCore/editing/MarkupAccumulator.cpp new file mode 100644 index 0000000..f6dbd8b --- /dev/null +++ b/Source/WebCore/editing/MarkupAccumulator.cpp @@ -0,0 +1,465 @@ +/* + * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009 Apple Inc. All rights reserved. + * Copyright (C) 2009, 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 "MarkupAccumulator.h" + +#include "CDATASection.h" +#include "CharacterNames.h" +#include "Comment.h" +#include "DocumentFragment.h" +#include "DocumentType.h" +#include "Editor.h" +#include "HTMLElement.h" +#include "HTMLNames.h" +#include "KURL.h" +#include "ProcessingInstruction.h" +#include "XMLNSNames.h" + +namespace WebCore { + +using namespace HTMLNames; + +void appendCharactersReplacingEntities(Vector<UChar>& out, const UChar* content, size_t length, EntityMask entityMask) +{ + DEFINE_STATIC_LOCAL(const String, ampReference, ("&")); + DEFINE_STATIC_LOCAL(const String, ltReference, ("<")); + DEFINE_STATIC_LOCAL(const String, gtReference, (">")); + DEFINE_STATIC_LOCAL(const String, quotReference, (""")); + DEFINE_STATIC_LOCAL(const String, nbspReference, (" ")); + + static const EntityDescription entityMaps[] = { + { '&', ampReference, EntityAmp }, + { '<', ltReference, EntityLt }, + { '>', gtReference, EntityGt }, + { '"', quotReference, EntityQuot }, + { noBreakSpace, nbspReference, EntityNbsp }, + }; + + size_t positionAfterLastEntity = 0; + for (size_t i = 0; i < length; ++i) { + for (size_t m = 0; m < WTF_ARRAY_LENGTH(entityMaps); ++m) { + if (content[i] == entityMaps[m].entity && entityMaps[m].mask & entityMask) { + out.append(content + positionAfterLastEntity, i - positionAfterLastEntity); + append(out, entityMaps[m].reference); + positionAfterLastEntity = i + 1; + break; + } + } + } + out.append(content + positionAfterLastEntity, length - positionAfterLastEntity); +} + +MarkupAccumulator::MarkupAccumulator(Vector<Node*>* nodes, EAbsoluteURLs shouldResolveURLs, const Range* range) + : m_nodes(nodes) + , m_range(range) + , m_shouldResolveURLs(shouldResolveURLs) +{ +} + +MarkupAccumulator::~MarkupAccumulator() +{ +} + +String MarkupAccumulator::serializeNodes(Node* node, Node* nodeToSkip, EChildrenOnly childrenOnly) +{ + Vector<UChar> out; + serializeNodesWithNamespaces(node, nodeToSkip, childrenOnly, 0); + out.reserveInitialCapacity(length()); + concatenateMarkup(out); + return String::adopt(out); +} + +void MarkupAccumulator::serializeNodesWithNamespaces(Node* node, Node* nodeToSkip, EChildrenOnly childrenOnly, const Namespaces* namespaces) +{ + if (node == nodeToSkip) + return; + + Namespaces namespaceHash; + if (namespaces) + namespaceHash = *namespaces; + + if (!childrenOnly) + appendStartTag(node, &namespaceHash); + + if (!(node->document()->isHTMLDocument() && elementCannotHaveEndTag(node))) { + for (Node* current = node->firstChild(); current; current = current->nextSibling()) + serializeNodesWithNamespaces(current, nodeToSkip, IncludeNode, &namespaceHash); + } + + if (!childrenOnly) + appendEndTag(node); +} + +void MarkupAccumulator::appendString(const String& string) +{ + m_succeedingMarkup.append(string); +} + +void MarkupAccumulator::appendStartTag(Node* node, Namespaces* namespaces) +{ + Vector<UChar> markup; + appendStartMarkup(markup, node, namespaces); + appendString(String::adopt(markup)); + if (m_nodes) + m_nodes->append(node); +} + +void MarkupAccumulator::appendEndTag(Node* node) +{ + Vector<UChar> markup; + appendEndMarkup(markup, node); + appendString(String::adopt(markup)); +} + +size_t MarkupAccumulator::totalLength(const Vector<String>& strings) +{ + size_t length = 0; + for (size_t i = 0; i < strings.size(); ++i) + length += strings[i].length(); + return length; +} + +// FIXME: This is a very inefficient way of accumulating the markup. +// We're converting results of appendStartMarkup and appendEndMarkup from Vector<UChar> to String +// and then back to Vector<UChar> and again to String here. +void MarkupAccumulator::concatenateMarkup(Vector<UChar>& out) +{ + for (size_t i = 0; i < m_succeedingMarkup.size(); ++i) + append(out, m_succeedingMarkup[i]); +} + +void MarkupAccumulator::appendAttributeValue(Vector<UChar>& result, const String& attribute, bool documentIsHTML) +{ + appendCharactersReplacingEntities(result, attribute.characters(), attribute.length(), + documentIsHTML ? EntityMaskInHTMLAttributeValue : EntityMaskInAttributeValue); +} + +void MarkupAccumulator::appendQuotedURLAttributeValue(Vector<UChar>& result, const String& urlString) +{ + UChar quoteChar = '\"'; + String strippedURLString = urlString.stripWhiteSpace(); + if (protocolIsJavaScript(strippedURLString)) { + // minimal escaping for javascript urls + if (strippedURLString.contains('"')) { + if (strippedURLString.contains('\'')) + strippedURLString.replace('\"', """); + else + quoteChar = '\''; + } + result.append(quoteChar); + append(result, strippedURLString); + result.append(quoteChar); + return; + } + + // FIXME: This does not fully match other browsers. Firefox percent-escapes non-ASCII characters for innerHTML. + result.append(quoteChar); + appendAttributeValue(result, urlString, false); + result.append(quoteChar); +} + +void MarkupAccumulator::appendNodeValue(Vector<UChar>& out, const Node* node, const Range* range, EntityMask entityMask) +{ + String str = node->nodeValue(); + const UChar* characters = str.characters(); + size_t length = str.length(); + + if (range) { + ExceptionCode ec; + if (node == range->endContainer(ec)) + length = range->endOffset(ec); + if (node == range->startContainer(ec)) { + size_t start = range->startOffset(ec); + characters += start; + length -= start; + } + } + + appendCharactersReplacingEntities(out, characters, length, entityMask); +} + +bool MarkupAccumulator::shouldAddNamespaceElement(const Element* element) +{ + // Don't add namespace attribute if it is already defined for this elem. + const AtomicString& prefix = element->prefix(); + AtomicString attr = !prefix.isEmpty() ? "xmlns:" + prefix : "xmlns"; + return !element->hasAttribute(attr); +} + +bool MarkupAccumulator::shouldAddNamespaceAttribute(const Attribute& attribute, Namespaces& namespaces) +{ + namespaces.checkConsistency(); + + // Don't add namespace attributes twice + if (attribute.name() == XMLNSNames::xmlnsAttr) { + namespaces.set(emptyAtom.impl(), attribute.value().impl()); + return false; + } + + QualifiedName xmlnsPrefixAttr(xmlnsAtom, attribute.localName(), XMLNSNames::xmlnsNamespaceURI); + if (attribute.name() == xmlnsPrefixAttr) { + namespaces.set(attribute.localName().impl(), attribute.value().impl()); + return false; + } + + return true; +} + +void MarkupAccumulator::appendNamespace(Vector<UChar>& result, const AtomicString& prefix, const AtomicString& namespaceURI, Namespaces& namespaces) +{ + namespaces.checkConsistency(); + if (namespaceURI.isEmpty()) + return; + + // Use emptyAtoms's impl() for both null and empty strings since the HashMap can't handle 0 as a key + AtomicStringImpl* pre = prefix.isEmpty() ? emptyAtom.impl() : prefix.impl(); + AtomicStringImpl* foundNS = namespaces.get(pre); + if (foundNS != namespaceURI.impl()) { + namespaces.set(pre, namespaceURI.impl()); + result.append(' '); + append(result, xmlnsAtom.string()); + if (!prefix.isEmpty()) { + result.append(':'); + append(result, prefix); + } + + result.append('='); + result.append('"'); + appendAttributeValue(result, namespaceURI, false); + result.append('"'); + } +} + +EntityMask MarkupAccumulator::entityMaskForText(Text* text) const +{ + const QualifiedName* parentName = 0; + if (text->parentElement()) + parentName = &static_cast<Element*>(text->parentElement())->tagQName(); + + if (parentName && (*parentName == scriptTag || *parentName == styleTag || *parentName == xmpTag)) + return EntityMaskInCDATA; + + return text->document()->isHTMLDocument() ? EntityMaskInHTMLPCDATA : EntityMaskInPCDATA; +} + +void MarkupAccumulator::appendText(Vector<UChar>& out, Text* text) +{ + appendNodeValue(out, text, m_range, entityMaskForText(text)); +} + +void MarkupAccumulator::appendComment(Vector<UChar>& out, const String& comment) +{ + // FIXME: Comment content is not escaped, but XMLSerializer (and possibly other callers) should raise an exception if it includes "-->". + append(out, "<!--"); + append(out, comment); + append(out, "-->"); +} + +void MarkupAccumulator::appendDocumentType(Vector<UChar>& result, const DocumentType* n) +{ + if (n->name().isEmpty()) + return; + + append(result, "<!DOCTYPE "); + append(result, n->name()); + if (!n->publicId().isEmpty()) { + append(result, " PUBLIC \""); + append(result, n->publicId()); + append(result, "\""); + if (!n->systemId().isEmpty()) { + append(result, " \""); + append(result, n->systemId()); + append(result, "\""); + } + } else if (!n->systemId().isEmpty()) { + append(result, " SYSTEM \""); + append(result, n->systemId()); + append(result, "\""); + } + if (!n->internalSubset().isEmpty()) { + append(result, " ["); + append(result, n->internalSubset()); + append(result, "]"); + } + append(result, ">"); +} + +void MarkupAccumulator::appendProcessingInstruction(Vector<UChar>& out, const String& target, const String& data) +{ + // FIXME: PI data is not escaped, but XMLSerializer (and possibly other callers) this should raise an exception if it includes "?>". + append(out, "<?"); + append(out, target); + append(out, " "); + append(out, data); + append(out, "?>"); +} + +void MarkupAccumulator::appendElement(Vector<UChar>& out, Element* element, Namespaces* namespaces) +{ + appendOpenTag(out, element, namespaces); + + NamedNodeMap* attributes = element->attributes(); + unsigned length = attributes->length(); + for (unsigned int i = 0; i < length; i++) + appendAttribute(out, element, *attributes->attributeItem(i), namespaces); + + appendCloseTag(out, element); +} + +void MarkupAccumulator::appendOpenTag(Vector<UChar>& out, Element* element, Namespaces* namespaces) +{ + out.append('<'); + append(out, element->nodeNamePreservingCase()); + if (!element->document()->isHTMLDocument() && namespaces && shouldAddNamespaceElement(element)) + appendNamespace(out, element->prefix(), element->namespaceURI(), *namespaces); +} + +void MarkupAccumulator::appendCloseTag(Vector<UChar>& out, Element* element) +{ + if (shouldSelfClose(element)) { + if (element->isHTMLElement()) + out.append(' '); // XHTML 1.0 <-> HTML compatibility. + out.append('/'); + } + out.append('>'); +} + +void MarkupAccumulator::appendAttribute(Vector<UChar>& out, Element* element, const Attribute& attribute, Namespaces* namespaces) +{ + bool documentIsHTML = element->document()->isHTMLDocument(); + + out.append(' '); + + if (documentIsHTML) + append(out, attribute.name().localName()); + else + append(out, attribute.name().toString()); + + out.append('='); + + if (element->isURLAttribute(const_cast<Attribute*>(&attribute))) { + // We don't want to complete file:/// URLs because it may contain sensitive information + // about the user's system. + if (shouldResolveURLs() && !element->document()->url().isLocalFile()) + appendQuotedURLAttributeValue(out, element->document()->completeURL(attribute.value()).string()); + else + appendQuotedURLAttributeValue(out, attribute.value()); + } else { + out.append('\"'); + appendAttributeValue(out, attribute.value(), documentIsHTML); + out.append('\"'); + } + + if (!documentIsHTML && namespaces && shouldAddNamespaceAttribute(attribute, *namespaces)) + appendNamespace(out, attribute.prefix(), attribute.namespaceURI(), *namespaces); +} + +void MarkupAccumulator::appendCDATASection(Vector<UChar>& out, const String& section) +{ + // FIXME: CDATA content is not escaped, but XMLSerializer (and possibly other callers) should raise an exception if it includes "]]>". + append(out, "<![CDATA["); + append(out, section); + append(out, "]]>"); +} + +void MarkupAccumulator::appendStartMarkup(Vector<UChar>& result, const Node* node, Namespaces* namespaces) +{ + if (namespaces) + namespaces->checkConsistency(); + + switch (node->nodeType()) { + case Node::TEXT_NODE: + appendText(result, static_cast<Text*>(const_cast<Node*>(node))); + break; + case Node::COMMENT_NODE: + appendComment(result, static_cast<const Comment*>(node)->data()); + break; + case Node::DOCUMENT_NODE: + case Node::DOCUMENT_FRAGMENT_NODE: + break; + case Node::DOCUMENT_TYPE_NODE: + appendDocumentType(result, static_cast<const DocumentType*>(node)); + break; + case Node::PROCESSING_INSTRUCTION_NODE: + appendProcessingInstruction(result, static_cast<const ProcessingInstruction*>(node)->target(), static_cast<const ProcessingInstruction*>(node)->data()); + break; + case Node::ELEMENT_NODE: + appendElement(result, static_cast<Element*>(const_cast<Node*>(node)), namespaces); + break; + case Node::CDATA_SECTION_NODE: + appendCDATASection(result, static_cast<const CDATASection*>(node)->data()); + break; + case Node::ATTRIBUTE_NODE: + case Node::ENTITY_NODE: + case Node::ENTITY_REFERENCE_NODE: + case Node::NOTATION_NODE: + case Node::XPATH_NAMESPACE_NODE: + ASSERT_NOT_REACHED(); + break; + } +} + +// Rules of self-closure +// 1. No elements in HTML documents use the self-closing syntax. +// 2. Elements w/ children never self-close because they use a separate end tag. +// 3. HTML elements which do not have a "forbidden" end tag will close with a separate end tag. +// 4. Other elements self-close. +bool MarkupAccumulator::shouldSelfClose(const Node* node) +{ + if (node->document()->isHTMLDocument()) + return false; + if (node->hasChildNodes()) + return false; + if (node->isHTMLElement() && !elementCannotHaveEndTag(node)) + return false; + return true; +} + +bool MarkupAccumulator::elementCannotHaveEndTag(const Node* node) +{ + if (!node->isHTMLElement()) + return false; + + // FIXME: ieForbidsInsertHTML may not be the right function to call here + // ieForbidsInsertHTML is used to disallow setting innerHTML/outerHTML + // or createContextualFragment. It does not necessarily align with + // which elements should be serialized w/o end tags. + return static_cast<const HTMLElement*>(node)->ieForbidsInsertHTML(); +} + +void MarkupAccumulator::appendEndMarkup(Vector<UChar>& result, const Node* node) +{ + if (!node->isElementNode() || shouldSelfClose(node) || (!node->hasChildNodes() && elementCannotHaveEndTag(node))) + return; + + result.append('<'); + result.append('/'); + append(result, static_cast<const Element*>(node)->nodeNamePreservingCase()); + result.append('>'); +} + +} diff --git a/Source/WebCore/editing/MarkupAccumulator.h b/Source/WebCore/editing/MarkupAccumulator.h new file mode 100644 index 0000000..5a9c884 --- /dev/null +++ b/Source/WebCore/editing/MarkupAccumulator.h @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef MarkupAccumulator_h +#define MarkupAccumulator_h + +#include "PlatformString.h" +#include "markup.h" +#include <wtf/HashMap.h> +#include <wtf/Vector.h> + +namespace WebCore { + +class Attribute; +class DocumentType; +class Element; +class Node; +class Range; + +typedef HashMap<AtomicStringImpl*, AtomicStringImpl*> Namespaces; + +enum EntityMask { + EntityAmp = 0x0001, + EntityLt = 0x0002, + EntityGt = 0x0004, + EntityQuot = 0x0008, + EntityNbsp = 0x0010, + + // Non-breaking space needs to be escaped in innerHTML for compatibility reason. See http://trac.webkit.org/changeset/32879 + // However, we cannot do this in a XML document because it does not have the entity reference defined (See the bug 19215). + EntityMaskInCDATA = 0, + EntityMaskInPCDATA = EntityAmp | EntityLt | EntityGt, + EntityMaskInHTMLPCDATA = EntityMaskInPCDATA | EntityNbsp, + EntityMaskInAttributeValue = EntityAmp | EntityLt | EntityGt | EntityQuot, + EntityMaskInHTMLAttributeValue = EntityMaskInAttributeValue | EntityNbsp, +}; + +struct EntityDescription { + UChar entity; + const String& reference; + EntityMask mask; +}; + +// FIXME: Noncopyable? +class MarkupAccumulator { +public: + MarkupAccumulator(Vector<Node*>* nodes, EAbsoluteURLs shouldResolveURLs, const Range* range = 0); + virtual ~MarkupAccumulator(); + + String serializeNodes(Node* node, Node* nodeToSkip, EChildrenOnly childrenOnly); + +protected: + void appendString(const String&); + void appendStartTag(Node*, Namespaces* = 0); + void appendEndTag(Node*); + static size_t totalLength(const Vector<String>&); + size_t length() const { return totalLength(m_succeedingMarkup); } + void concatenateMarkup(Vector<UChar>& out); + void appendAttributeValue(Vector<UChar>& result, const String& attribute, bool documentIsHTML); + void appendQuotedURLAttributeValue(Vector<UChar>& result, const String& urlString); + void appendNodeValue(Vector<UChar>& out, const Node*, const Range*, EntityMask); + bool shouldAddNamespaceElement(const Element*); + bool shouldAddNamespaceAttribute(const Attribute&, Namespaces&); + void appendNamespace(Vector<UChar>& result, const AtomicString& prefix, const AtomicString& namespaceURI, Namespaces&); + EntityMask entityMaskForText(Text* text) const; + virtual void appendText(Vector<UChar>& out, Text*); + void appendComment(Vector<UChar>& out, const String& comment); + void appendDocumentType(Vector<UChar>& result, const DocumentType*); + void appendProcessingInstruction(Vector<UChar>& out, const String& target, const String& data); + virtual void appendElement(Vector<UChar>& out, Element*, Namespaces*); + void appendOpenTag(Vector<UChar>& out, Element* element, Namespaces*); + void appendCloseTag(Vector<UChar>& out, Element* element); + void appendAttribute(Vector<UChar>& out, Element* element, const Attribute&, Namespaces*); + void appendCDATASection(Vector<UChar>& out, const String& section); + void appendStartMarkup(Vector<UChar>& result, const Node*, Namespaces*); + bool shouldSelfClose(const Node*); + bool elementCannotHaveEndTag(const Node* node); + void appendEndMarkup(Vector<UChar>& result, const Node*); + + bool shouldResolveURLs() { return m_shouldResolveURLs == AbsoluteURLs; } + + Vector<Node*>* const m_nodes; + const Range* const m_range; + +private: + void serializeNodesWithNamespaces(Node*, Node* nodeToSkip, EChildrenOnly, const Namespaces*); + + Vector<String> m_succeedingMarkup; + const bool m_shouldResolveURLs; +}; + +// FIXME: This method should be integrated with MarkupAccumulator. +void appendCharactersReplacingEntities(Vector<UChar>& out, const UChar* content, size_t length, EntityMask entityMask); + +} + +#endif diff --git a/Source/WebCore/editing/MergeIdenticalElementsCommand.cpp b/Source/WebCore/editing/MergeIdenticalElementsCommand.cpp new file mode 100644 index 0000000..ff59f49 --- /dev/null +++ b/Source/WebCore/editing/MergeIdenticalElementsCommand.cpp @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2005, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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 "MergeIdenticalElementsCommand.h" + +#include "Element.h" + +namespace WebCore { + +MergeIdenticalElementsCommand::MergeIdenticalElementsCommand(PassRefPtr<Element> first, PassRefPtr<Element> second) + : SimpleEditCommand(first->document()) + , m_element1(first) + , m_element2(second) +{ + ASSERT(m_element1); + ASSERT(m_element2); + ASSERT(m_element1->nextSibling() == m_element2); +} + +void MergeIdenticalElementsCommand::doApply() +{ + if (m_element1->nextSibling() != m_element2 || !m_element1->isContentEditable() || !m_element2->isContentEditable()) + return; + + m_atChild = m_element2->firstChild(); + + ExceptionCode ec = 0; + + Vector<RefPtr<Node> > children; + for (Node* child = m_element1->firstChild(); child; child = child->nextSibling()) + children.append(child); + + size_t size = children.size(); + for (size_t i = 0; i < size; ++i) + m_element2->insertBefore(children[i].release(), m_atChild.get(), ec); + + m_element1->remove(ec); +} + +void MergeIdenticalElementsCommand::doUnapply() +{ + ASSERT(m_element1); + ASSERT(m_element2); + + RefPtr<Node> atChild = m_atChild.release(); + + ContainerNode* parent = m_element2->parentNode(); + if (!parent || !parent->isContentEditable()) + return; + + ExceptionCode ec = 0; + + parent->insertBefore(m_element1.get(), m_element2.get(), ec); + if (ec) + return; + + Vector<RefPtr<Node> > children; + for (Node* child = m_element2->firstChild(); child && child != atChild; child = child->nextSibling()) + children.append(child); + + size_t size = children.size(); + for (size_t i = 0; i < size; ++i) + m_element1->appendChild(children[i].release(), ec); +} + +} // namespace WebCore diff --git a/Source/WebCore/editing/MergeIdenticalElementsCommand.h b/Source/WebCore/editing/MergeIdenticalElementsCommand.h new file mode 100644 index 0000000..1ce6302 --- /dev/null +++ b/Source/WebCore/editing/MergeIdenticalElementsCommand.h @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2005, 2006, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef MergeIdenticalElementsCommand_h +#define MergeIdenticalElementsCommand_h + +#include "EditCommand.h" + +namespace WebCore { + +class MergeIdenticalElementsCommand : public SimpleEditCommand { +public: + static PassRefPtr<MergeIdenticalElementsCommand> create(PassRefPtr<Element> element1, PassRefPtr<Element> element2) + { + return adoptRef(new MergeIdenticalElementsCommand(element1, element2)); + } + +private: + MergeIdenticalElementsCommand(PassRefPtr<Element>, PassRefPtr<Element>); + + virtual void doApply(); + virtual void doUnapply(); + + RefPtr<Element> m_element1; + RefPtr<Element> m_element2; + RefPtr<Node> m_atChild; +}; + +} // namespace WebCore + +#endif // MergeIdenticalElementsCommand_h diff --git a/Source/WebCore/editing/ModifySelectionListLevel.cpp b/Source/WebCore/editing/ModifySelectionListLevel.cpp new file mode 100644 index 0000000..3e6754e --- /dev/null +++ b/Source/WebCore/editing/ModifySelectionListLevel.cpp @@ -0,0 +1,295 @@ +/* + * Copyright (C) 2006, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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 "ModifySelectionListLevel.h" + +#include "Document.h" +#include "Frame.h" +#include "HTMLElement.h" +#include "RenderObject.h" +#include "SelectionController.h" +#include "htmlediting.h" + +namespace WebCore { + +ModifySelectionListLevelCommand::ModifySelectionListLevelCommand(Document* document) + : CompositeEditCommand(document) +{ +} + +bool ModifySelectionListLevelCommand::preservesTypingStyle() const +{ + return true; +} + +// This needs to be static so it can be called by canIncreaseSelectionListLevel and canDecreaseSelectionListLevel +static bool getStartEndListChildren(const VisibleSelection& selection, Node*& start, Node*& end) +{ + if (selection.isNone()) + return false; + + // start must be in a list child + Node* startListChild = enclosingListChild(selection.start().node()); + if (!startListChild) + return false; + + // end must be in a list child + Node* endListChild = selection.isRange() ? enclosingListChild(selection.end().node()) : startListChild; + if (!endListChild) + return false; + + // For a range selection we want the following behavior: + // - the start and end must be within the same overall list + // - the start must be at or above the level of the rest of the range + // - if the end is anywhere in a sublist lower than start, the whole sublist gets moved + // In terms of this function, this means: + // - endListChild must start out being be a sibling of startListChild, or be in a + // sublist of startListChild or a sibling + // - if endListChild is in a sublist of startListChild or a sibling, it must be adjusted + // to be the ancestor that is startListChild or its sibling + while (startListChild->parentNode() != endListChild->parentNode()) { + endListChild = endListChild->parentNode(); + if (!endListChild) + return false; + } + + // if the selection ends on a list item with a sublist, include the entire sublist + if (endListChild->renderer()->isListItem()) { + RenderObject* r = endListChild->renderer()->nextSibling(); + if (r && isListElement(r->node())) + endListChild = r->node(); + } + + start = startListChild; + end = endListChild; + return true; +} + +void ModifySelectionListLevelCommand::insertSiblingNodeRangeBefore(Node* startNode, Node* endNode, Node* refNode) +{ + Node* node = startNode; + while (1) { + Node* next = node->nextSibling(); + removeNode(node); + insertNodeBefore(node, refNode); + + if (node == endNode) + break; + + node = next; + } +} + +void ModifySelectionListLevelCommand::insertSiblingNodeRangeAfter(Node* startNode, Node* endNode, Node* refNode) +{ + Node* node = startNode; + while (1) { + Node* next = node->nextSibling(); + removeNode(node); + insertNodeAfter(node, refNode); + + if (node == endNode) + break; + + refNode = node; + node = next; + } +} + +void ModifySelectionListLevelCommand::appendSiblingNodeRange(Node* startNode, Node* endNode, Element* newParent) +{ + Node* node = startNode; + while (1) { + Node* next = node->nextSibling(); + removeNode(node); + appendNode(node, newParent); + + if (node == endNode) + break; + + node = next; + } +} + +IncreaseSelectionListLevelCommand::IncreaseSelectionListLevelCommand(Document* document, Type listType) + : ModifySelectionListLevelCommand(document) + , m_listType(listType) +{ +} + +// This needs to be static so it can be called by canIncreaseSelectionListLevel +static bool canIncreaseListLevel(const VisibleSelection& selection, Node*& start, Node*& end) +{ + if (!getStartEndListChildren(selection, start, end)) + return false; + + // start must not be the first child (because you need a prior one + // to increase relative to) + if (!start->renderer()->previousSibling()) + return false; + + return true; +} + +// For the moment, this is SPI and the only client (Mail.app) is satisfied. +// Here are two things to re-evaluate when making into API. +// 1. Currently, InheritedListType uses clones whereas OrderedList and +// UnorderedList create a new list node of the specified type. That is +// inconsistent wrt style. If that is not OK, here are some alternatives: +// - new nodes always inherit style (probably the best choice) +// - new nodes have always have no style +// - new nodes of the same type inherit style +// 2. Currently, the node we return may be either a pre-existing one or +// a new one. Is it confusing to return the pre-existing one without +// somehow indicating that it is not new? If so, here are some alternatives: +// - only return the list node if we created it +// - indicate whether the list node is new or pre-existing +// - (silly) client specifies whether to return pre-existing list nodes +void IncreaseSelectionListLevelCommand::doApply() +{ + Node* startListChild; + Node* endListChild; + if (!canIncreaseListLevel(endingSelection(), startListChild, endListChild)) + return; + + Node* previousItem = startListChild->renderer()->previousSibling()->node(); + if (isListElement(previousItem)) { + // move nodes up into preceding list + appendSiblingNodeRange(startListChild, endListChild, static_cast<Element*>(previousItem)); + m_listElement = previousItem; + } else { + // create a sublist for the preceding element and move nodes there + RefPtr<Element> newParent; + switch (m_listType) { + case InheritedListType: + newParent = startListChild->parentElement(); + if (newParent) + newParent = newParent->cloneElementWithoutChildren(); + break; + case OrderedList: + newParent = createOrderedListElement(document()); + break; + case UnorderedList: + newParent = createUnorderedListElement(document()); + break; + } + insertNodeBefore(newParent, startListChild); + appendSiblingNodeRange(startListChild, endListChild, newParent.get()); + m_listElement = newParent.release(); + } +} + +bool IncreaseSelectionListLevelCommand::canIncreaseSelectionListLevel(Document* document) +{ + Node* startListChild; + Node* endListChild; + return canIncreaseListLevel(document->frame()->selection()->selection(), startListChild, endListChild); +} + +PassRefPtr<Node> IncreaseSelectionListLevelCommand::increaseSelectionListLevel(Document* document, Type type) +{ + ASSERT(document); + ASSERT(document->frame()); + RefPtr<IncreaseSelectionListLevelCommand> command = create(document, type); + command->apply(); + return command->m_listElement.release(); +} + +PassRefPtr<Node> IncreaseSelectionListLevelCommand::increaseSelectionListLevel(Document* document) +{ + return increaseSelectionListLevel(document, InheritedListType); +} + +PassRefPtr<Node> IncreaseSelectionListLevelCommand::increaseSelectionListLevelOrdered(Document* document) +{ + return increaseSelectionListLevel(document, OrderedList); +} + +PassRefPtr<Node> IncreaseSelectionListLevelCommand::increaseSelectionListLevelUnordered(Document* document) +{ + return increaseSelectionListLevel(document, UnorderedList); +} + +DecreaseSelectionListLevelCommand::DecreaseSelectionListLevelCommand(Document* document) + : ModifySelectionListLevelCommand(document) +{ +} + +// This needs to be static so it can be called by canDecreaseSelectionListLevel +static bool canDecreaseListLevel(const VisibleSelection& selection, Node*& start, Node*& end) +{ + if (!getStartEndListChildren(selection, start, end)) + return false; + + // there must be a destination list to move the items to + if (!isListElement(start->parentNode()->parentNode())) + return false; + + return true; +} + +void DecreaseSelectionListLevelCommand::doApply() +{ + Node* startListChild; + Node* endListChild; + if (!canDecreaseListLevel(endingSelection(), startListChild, endListChild)) + return; + + Node* previousItem = startListChild->renderer()->previousSibling() ? startListChild->renderer()->previousSibling()->node() : 0; + Node* nextItem = endListChild->renderer()->nextSibling() ? endListChild->renderer()->nextSibling()->node() : 0; + Element* listNode = startListChild->parentElement(); + + if (!previousItem) { + // at start of sublist, move the child(ren) to before the sublist + insertSiblingNodeRangeBefore(startListChild, endListChild, listNode); + // if that was the whole sublist we moved, remove the sublist node + if (!nextItem) + removeNode(listNode); + } else if (!nextItem) { + // at end of list, move the child(ren) to after the sublist + insertSiblingNodeRangeAfter(startListChild, endListChild, listNode); + } else if (listNode) { + // in the middle of list, split the list and move the children to the divide + splitElement(listNode, startListChild); + insertSiblingNodeRangeBefore(startListChild, endListChild, listNode); + } +} + +bool DecreaseSelectionListLevelCommand::canDecreaseSelectionListLevel(Document* document) +{ + Node* startListChild; + Node* endListChild; + return canDecreaseListLevel(document->frame()->selection()->selection(), startListChild, endListChild); +} + +void DecreaseSelectionListLevelCommand::decreaseSelectionListLevel(Document* document) +{ + ASSERT(document); + ASSERT(document->frame()); + applyCommand(create(document)); +} + +} diff --git a/Source/WebCore/editing/ModifySelectionListLevel.h b/Source/WebCore/editing/ModifySelectionListLevel.h new file mode 100644 index 0000000..feefa91 --- /dev/null +++ b/Source/WebCore/editing/ModifySelectionListLevel.h @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2006, 2010 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef ModifySelectionListLevel_h +#define ModifySelectionListLevel_h + +#include "CompositeEditCommand.h" + +namespace WebCore { + +// ModifySelectionListLevelCommand provides functions useful for both increasing and decreasing the list level. +// It is the base class of IncreaseSelectionListLevelCommand and DecreaseSelectionListLevelCommand. +// It is not used on its own. +class ModifySelectionListLevelCommand : public CompositeEditCommand { +protected: + ModifySelectionListLevelCommand(Document*); + + void appendSiblingNodeRange(Node* startNode, Node* endNode, Element* newParent); + void insertSiblingNodeRangeBefore(Node* startNode, Node* endNode, Node* refNode); + void insertSiblingNodeRangeAfter(Node* startNode, Node* endNode, Node* refNode); + +private: + virtual bool preservesTypingStyle() const; +}; + +// IncreaseSelectionListLevelCommand moves the selected list items one level deeper. +class IncreaseSelectionListLevelCommand : public ModifySelectionListLevelCommand { +public: + static bool canIncreaseSelectionListLevel(Document*); + static PassRefPtr<Node> increaseSelectionListLevel(Document*); + static PassRefPtr<Node> increaseSelectionListLevelOrdered(Document*); + static PassRefPtr<Node> increaseSelectionListLevelUnordered(Document*); + +private: + enum Type { InheritedListType, OrderedList, UnorderedList }; + static PassRefPtr<Node> increaseSelectionListLevel(Document*, Type); + + static PassRefPtr<IncreaseSelectionListLevelCommand> create(Document* document, Type type) + { + return adoptRef(new IncreaseSelectionListLevelCommand(document, type)); + } + + IncreaseSelectionListLevelCommand(Document*, Type); + + virtual void doApply(); + + Type m_listType; + RefPtr<Node> m_listElement; +}; + +// DecreaseSelectionListLevelCommand moves the selected list items one level shallower. +class DecreaseSelectionListLevelCommand : public ModifySelectionListLevelCommand { +public: + static bool canDecreaseSelectionListLevel(Document*); + static void decreaseSelectionListLevel(Document*); + +private: + static PassRefPtr<DecreaseSelectionListLevelCommand> create(Document* document) + { + return adoptRef(new DecreaseSelectionListLevelCommand(document)); + } + + DecreaseSelectionListLevelCommand(Document*); + + virtual void doApply(); +}; + +} // namespace WebCore + +#endif // ModifySelectionListLevel_h diff --git a/Source/WebCore/editing/MoveSelectionCommand.cpp b/Source/WebCore/editing/MoveSelectionCommand.cpp new file mode 100644 index 0000000..3a1cae0 --- /dev/null +++ b/Source/WebCore/editing/MoveSelectionCommand.cpp @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2005, 2006 Apple Computer, 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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 "MoveSelectionCommand.h" + +#include "DocumentFragment.h" +#include "ReplaceSelectionCommand.h" + +namespace WebCore { + +MoveSelectionCommand::MoveSelectionCommand(PassRefPtr<DocumentFragment> fragment, const Position& position, bool smartInsert, bool smartDelete) + : CompositeEditCommand(position.node()->document()), m_fragment(fragment), m_position(position), m_smartInsert(smartInsert), m_smartDelete(smartDelete) +{ + ASSERT(m_fragment); +} + +void MoveSelectionCommand::doApply() +{ + VisibleSelection selection = endingSelection(); + ASSERT(selection.isNonOrphanedRange()); + + Position pos = m_position; + if (pos.isNull()) + return; + + // Update the position otherwise it may become invalid after the selection is deleted. + Node *positionNode = m_position.node(); + int positionOffset = m_position.deprecatedEditingOffset(); + Position selectionEnd = selection.end(); + int selectionEndOffset = selectionEnd.deprecatedEditingOffset(); + if (selectionEnd.node() == positionNode && selectionEndOffset < positionOffset) { + positionOffset -= selectionEndOffset; + Position selectionStart = selection.start(); + if (selectionStart.node() == positionNode) { + positionOffset += selectionStart.deprecatedEditingOffset(); + } + pos = Position(positionNode, positionOffset); + } + + deleteSelection(m_smartDelete); + + // If the node for the destination has been removed as a result of the deletion, + // set the destination to the ending point after the deletion. + // Fixes: <rdar://problem/3910425> REGRESSION (Mail): Crash in ReplaceSelectionCommand; + // selection is empty, leading to null deref + if (!pos.node()->inDocument()) + pos = endingSelection().start(); + + setEndingSelection(VisibleSelection(pos, endingSelection().affinity())); + if (!positionNode->inDocument()) { + // Document was modified out from under us. + return; + } + applyCommandToComposite(ReplaceSelectionCommand::create(positionNode->document(), m_fragment, true, m_smartInsert)); +} + +EditAction MoveSelectionCommand::editingAction() const +{ + return EditActionDrag; +} + +} // namespace WebCore diff --git a/Source/WebCore/editing/MoveSelectionCommand.h b/Source/WebCore/editing/MoveSelectionCommand.h new file mode 100644 index 0000000..6780caa --- /dev/null +++ b/Source/WebCore/editing/MoveSelectionCommand.h @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2005, 2006, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef MoveSelectionCommand_h +#define MoveSelectionCommand_h + +#include "CompositeEditCommand.h" + +namespace WebCore { + +class DocumentFragment; + +class MoveSelectionCommand : public CompositeEditCommand { +public: + static PassRefPtr<MoveSelectionCommand> create(PassRefPtr<DocumentFragment> fragment, const Position& position, bool smartInsert = false, bool smartDelete = false) + { + return adoptRef(new MoveSelectionCommand(fragment, position, smartInsert, smartDelete)); + } + +private: + MoveSelectionCommand(PassRefPtr<DocumentFragment>, const Position&, bool smartInsert, bool smartDelete); + + virtual void doApply(); + virtual EditAction editingAction() const; + + RefPtr<DocumentFragment> m_fragment; + Position m_position; + bool m_smartInsert; + bool m_smartDelete; +}; + +} // namespace WebCore + +#endif // MoveSelectionCommand_h diff --git a/Source/WebCore/editing/RemoveCSSPropertyCommand.cpp b/Source/WebCore/editing/RemoveCSSPropertyCommand.cpp new file mode 100644 index 0000000..8b37db8 --- /dev/null +++ b/Source/WebCore/editing/RemoveCSSPropertyCommand.cpp @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2005, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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 "RemoveCSSPropertyCommand.h" + +#include "CSSMutableStyleDeclaration.h" +#include <wtf/Assertions.h> + +namespace WebCore { + +RemoveCSSPropertyCommand::RemoveCSSPropertyCommand(Document* document, PassRefPtr<StyledElement> element, CSSPropertyID property) + : SimpleEditCommand(document) + , m_element(element) + , m_property(property) + , m_important(false) +{ + ASSERT(m_element); +} + +void RemoveCSSPropertyCommand::doApply() +{ + CSSMutableStyleDeclaration* style = m_element->inlineStyleDecl(); + m_oldValue = style->getPropertyValue(m_property); + m_important = style->getPropertyPriority(m_property); + style->removeProperty(m_property); +} + +void RemoveCSSPropertyCommand::doUnapply() +{ + CSSMutableStyleDeclaration* style = m_element->inlineStyleDecl(); + style->setProperty(m_property, m_oldValue, m_important); +} + +} // namespace WebCore diff --git a/Source/WebCore/editing/RemoveCSSPropertyCommand.h b/Source/WebCore/editing/RemoveCSSPropertyCommand.h new file mode 100644 index 0000000..46e0498 --- /dev/null +++ b/Source/WebCore/editing/RemoveCSSPropertyCommand.h @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2005, 2006, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef RemoveCSSPropertyCommand_h +#define RemoveCSSPropertyCommand_h + +#include "EditCommand.h" +#include "CSSMutableStyleDeclaration.h" +#include "CSSPropertyNames.h" +#include "StyledElement.h" + +namespace WebCore { + +class RemoveCSSPropertyCommand : public SimpleEditCommand { +public: + static PassRefPtr<RemoveCSSPropertyCommand> create(Document* document, PassRefPtr<StyledElement> element, CSSPropertyID property) + { + return adoptRef(new RemoveCSSPropertyCommand(document, element, property)); + } + +private: + RemoveCSSPropertyCommand(Document*, PassRefPtr<StyledElement>, CSSPropertyID property); + + virtual void doApply(); + virtual void doUnapply(); + + RefPtr<StyledElement> m_element; + CSSPropertyID m_property; + String m_oldValue; + bool m_important; +}; + +} // namespace WebCore + +#endif // RemoveCSSPropertyCommand_h diff --git a/Source/WebCore/editing/RemoveFormatCommand.cpp b/Source/WebCore/editing/RemoveFormatCommand.cpp new file mode 100644 index 0000000..0445b60 --- /dev/null +++ b/Source/WebCore/editing/RemoveFormatCommand.cpp @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2007 Apple Computer, 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 "RemoveFormatCommand.h" + +#include "ApplyStyleCommand.h" +#include "EditingStyle.h" +#include "Element.h" +#include "Frame.h" +#include "HTMLNames.h" +#include "SelectionController.h" + +namespace WebCore { + +using namespace HTMLNames; + +RemoveFormatCommand::RemoveFormatCommand(Document* document) + : CompositeEditCommand(document) +{ +} + +static bool isElementForRemoveFormatCommand(const Element* element) +{ + DEFINE_STATIC_LOCAL(HashSet<QualifiedName>, elements, ()); + if (elements.isEmpty()) { + elements.add(acronymTag); + elements.add(bTag); + elements.add(bdoTag); + elements.add(bigTag); + elements.add(citeTag); + elements.add(codeTag); + elements.add(dfnTag); + elements.add(emTag); + elements.add(fontTag); + elements.add(iTag); + elements.add(insTag); + elements.add(kbdTag); + elements.add(nobrTag); + elements.add(qTag); + elements.add(sTag); + elements.add(sampTag); + elements.add(smallTag); + elements.add(strikeTag); + elements.add(strongTag); + elements.add(subTag); + elements.add(supTag); + elements.add(ttTag); + elements.add(uTag); + elements.add(varTag); + } + return elements.contains(element->tagQName()); +} + +void RemoveFormatCommand::doApply() +{ + Frame* frame = document()->frame(); + + if (!frame->selection()->selection().isNonOrphanedCaretOrRange()) + return; + + // Get the default style for this editable root, it's the style that we'll give the + // content that we're operating on. + Node* root = frame->selection()->rootEditableElement(); + RefPtr<EditingStyle> defaultStyle = EditingStyle::create(root); + + applyCommandToComposite(ApplyStyleCommand::create(document(), defaultStyle.get(), isElementForRemoveFormatCommand, editingAction())); +} + +} diff --git a/Source/WebCore/editing/RemoveFormatCommand.h b/Source/WebCore/editing/RemoveFormatCommand.h new file mode 100644 index 0000000..daca2db --- /dev/null +++ b/Source/WebCore/editing/RemoveFormatCommand.h @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2007, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef RemoveFormatCommand_h +#define RemoveFormatCommand_h + +#include "CompositeEditCommand.h" + +namespace WebCore { + +class RemoveFormatCommand : public CompositeEditCommand { +public: + static PassRefPtr<RemoveFormatCommand> create(Document* document) + { + return adoptRef(new RemoveFormatCommand(document)); + } + +private: + RemoveFormatCommand(Document*); + + virtual void doApply(); + virtual EditAction editingAction() const { return EditActionUnspecified; } +}; + +} // namespace WebCore + +#endif // RemoveFormatCommand_h diff --git a/Source/WebCore/editing/RemoveNodeCommand.cpp b/Source/WebCore/editing/RemoveNodeCommand.cpp new file mode 100644 index 0000000..94e3e62 --- /dev/null +++ b/Source/WebCore/editing/RemoveNodeCommand.cpp @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2005, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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 "RemoveNodeCommand.h" + +#include "Node.h" +#include <wtf/Assertions.h> + +namespace WebCore { + +RemoveNodeCommand::RemoveNodeCommand(PassRefPtr<Node> node) + : SimpleEditCommand(node->document()) + , m_node(node) +{ + ASSERT(m_node); + ASSERT(m_node->parentNode()); +} + +void RemoveNodeCommand::doApply() +{ + ContainerNode* parent = m_node->parentNode(); + if (!parent || !parent->isContentEditable()) + return; + + m_parent = parent; + m_refChild = m_node->nextSibling(); + + ExceptionCode ec; + m_node->remove(ec); +} + +void RemoveNodeCommand::doUnapply() +{ + RefPtr<ContainerNode> parent = m_parent.release(); + RefPtr<Node> refChild = m_refChild.release(); + if (!parent || !parent->isContentEditable()) + return; + + ExceptionCode ec; + parent->insertBefore(m_node.get(), refChild.get(), ec); +} + +} diff --git a/Source/WebCore/editing/RemoveNodeCommand.h b/Source/WebCore/editing/RemoveNodeCommand.h new file mode 100644 index 0000000..b803964 --- /dev/null +++ b/Source/WebCore/editing/RemoveNodeCommand.h @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2005, 2006, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef RemoveNodeCommand_h +#define RemoveNodeCommand_h + +#include "EditCommand.h" + +namespace WebCore { + +class RemoveNodeCommand : public SimpleEditCommand { +public: + static PassRefPtr<RemoveNodeCommand> create(PassRefPtr<Node> node) + { + return adoptRef(new RemoveNodeCommand(node)); + } + +private: + RemoveNodeCommand(PassRefPtr<Node>); + + virtual void doApply(); + virtual void doUnapply(); + + RefPtr<Node> m_node; + RefPtr<ContainerNode> m_parent; + RefPtr<Node> m_refChild; +}; + +} // namespace WebCore + +#endif // RemoveNodeCommand_h diff --git a/Source/WebCore/editing/RemoveNodePreservingChildrenCommand.cpp b/Source/WebCore/editing/RemoveNodePreservingChildrenCommand.cpp new file mode 100644 index 0000000..1452f88 --- /dev/null +++ b/Source/WebCore/editing/RemoveNodePreservingChildrenCommand.cpp @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2005, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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 "RemoveNodePreservingChildrenCommand.h" + +#include "Node.h" +#include <wtf/Assertions.h> + +namespace WebCore { + +RemoveNodePreservingChildrenCommand::RemoveNodePreservingChildrenCommand(PassRefPtr<Node> node) + : CompositeEditCommand(node->document()) + , m_node(node) +{ + ASSERT(m_node); +} + +void RemoveNodePreservingChildrenCommand::doApply() +{ + Vector<RefPtr<Node> > children; + for (Node* child = m_node->firstChild(); child; child = child->nextSibling()) + children.append(child); + + size_t size = children.size(); + for (size_t i = 0; i < size; ++i) { + RefPtr<Node> child = children[i].release(); + removeNode(child); + insertNodeBefore(child.release(), m_node); + } + removeNode(m_node); +} + +} diff --git a/Source/WebCore/editing/RemoveNodePreservingChildrenCommand.h b/Source/WebCore/editing/RemoveNodePreservingChildrenCommand.h new file mode 100644 index 0000000..d2b635f --- /dev/null +++ b/Source/WebCore/editing/RemoveNodePreservingChildrenCommand.h @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2005, 2006, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef RemoveNodePreservingChildrenCommand_h +#define RemoveNodePreservingChildrenCommand_h + +#include "CompositeEditCommand.h" + +namespace WebCore { + +class RemoveNodePreservingChildrenCommand : public CompositeEditCommand { +public: + static PassRefPtr<RemoveNodePreservingChildrenCommand> create(PassRefPtr<Node> node) + { + return adoptRef(new RemoveNodePreservingChildrenCommand(node)); + } + +private: + RemoveNodePreservingChildrenCommand(PassRefPtr<Node>); + + virtual void doApply(); + + RefPtr<Node> m_node; +}; + +} // namespace WebCore + +#endif // RemoveNodePreservingChildrenCommand_h diff --git a/Source/WebCore/editing/ReplaceNodeWithSpanCommand.cpp b/Source/WebCore/editing/ReplaceNodeWithSpanCommand.cpp new file mode 100644 index 0000000..7ab3aba --- /dev/null +++ b/Source/WebCore/editing/ReplaceNodeWithSpanCommand.cpp @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2009 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: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * 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 "ReplaceNodeWithSpanCommand.h" + +#include "htmlediting.h" +#include "HTMLElement.h" +#include "HTMLNames.h" + +#include <wtf/Assertions.h> + +namespace WebCore { + +using namespace HTMLNames; + +ReplaceNodeWithSpanCommand::ReplaceNodeWithSpanCommand(PassRefPtr<HTMLElement> element) + : SimpleEditCommand(element->document()) + , m_elementToReplace(element) +{ + ASSERT(m_elementToReplace); +} + +static void swapInNodePreservingAttributesAndChildren(HTMLElement* newNode, HTMLElement* nodeToReplace) +{ + ASSERT(nodeToReplace->inDocument()); + ExceptionCode ec = 0; + ContainerNode* parentNode = nodeToReplace->parentNode(); + parentNode->insertBefore(newNode, nodeToReplace, ec); + ASSERT(!ec); + + Node* nextChild; + for (Node* child = nodeToReplace->firstChild(); child; child = nextChild) { + nextChild = child->nextSibling(); + newNode->appendChild(child, ec); + ASSERT(!ec); + } + + newNode->attributes()->setAttributes(*nodeToReplace->attributes()); + + parentNode->removeChild(nodeToReplace, ec); + ASSERT(!ec); +} + +void ReplaceNodeWithSpanCommand::doApply() +{ + if (!m_elementToReplace->inDocument()) + return; + if (!m_spanElement) + m_spanElement = createHTMLElement(m_elementToReplace->document(), spanTag); + swapInNodePreservingAttributesAndChildren(m_spanElement.get(), m_elementToReplace.get()); +} + +void ReplaceNodeWithSpanCommand::doUnapply() +{ + if (!m_spanElement->inDocument()) + return; + swapInNodePreservingAttributesAndChildren(m_elementToReplace.get(), m_spanElement.get()); +} + +} // namespace WebCore diff --git a/Source/WebCore/editing/ReplaceNodeWithSpanCommand.h b/Source/WebCore/editing/ReplaceNodeWithSpanCommand.h new file mode 100644 index 0000000..0154f29 --- /dev/null +++ b/Source/WebCore/editing/ReplaceNodeWithSpanCommand.h @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2009 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: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * 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. + */ + +#ifndef ReplaceNodeWithSpanCommand_h +#define ReplaceNodeWithSpanCommand_h + +#include "CompositeEditCommand.h" + +namespace WebCore { + +class HTMLElement; + +// More accurately, this is ReplaceElementWithSpanPreservingChildrenAndAttributesCommand +class ReplaceNodeWithSpanCommand : public SimpleEditCommand { +public: + static PassRefPtr<ReplaceNodeWithSpanCommand> create(PassRefPtr<HTMLElement> element) + { + return adoptRef(new ReplaceNodeWithSpanCommand(element)); + } + + HTMLElement* spanElement() { return m_spanElement.get(); } + +private: + ReplaceNodeWithSpanCommand(PassRefPtr<HTMLElement>); + + virtual void doApply(); + virtual void doUnapply(); + + RefPtr<HTMLElement> m_elementToReplace; + RefPtr<HTMLElement> m_spanElement; +}; + +} // namespace WebCore + +#endif // ReplaceNodeWithSpanCommand diff --git a/Source/WebCore/editing/ReplaceSelectionCommand.cpp b/Source/WebCore/editing/ReplaceSelectionCommand.cpp new file mode 100644 index 0000000..044ce63 --- /dev/null +++ b/Source/WebCore/editing/ReplaceSelectionCommand.cpp @@ -0,0 +1,1289 @@ +/* + * Copyright (C) 2005, 2006, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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 "ReplaceSelectionCommand.h" + +#include "ApplyStyleCommand.h" +#include "BeforeTextInsertedEvent.h" +#include "BreakBlockquoteCommand.h" +#include "CSSComputedStyleDeclaration.h" +#include "CSSMutableStyleDeclaration.h" +#include "CSSPropertyNames.h" +#include "CSSValueKeywords.h" +#include "Document.h" +#include "DocumentFragment.h" +#include "EditingText.h" +#include "Element.h" +#include "EventNames.h" +#include "Frame.h" +#include "HTMLElement.h" +#include "HTMLInputElement.h" +#include "HTMLInterchange.h" +#include "HTMLNames.h" +#include "SelectionController.h" +#include "SmartReplace.h" +#include "TextIterator.h" +#include "htmlediting.h" +#include "markup.h" +#include "visible_units.h" +#include <wtf/StdLibExtras.h> +#include <wtf/Vector.h> + +namespace WebCore { + +typedef Vector<RefPtr<Node> > NodeVector; + +using namespace HTMLNames; + +enum EFragmentType { EmptyFragment, SingleTextNodeFragment, TreeFragment }; + +// --- ReplacementFragment helper class + +class ReplacementFragment : public Noncopyable { +public: + ReplacementFragment(Document*, DocumentFragment*, bool matchStyle, const VisibleSelection&); + + Node* firstChild() const; + Node* lastChild() const; + + bool isEmpty() const; + + bool hasInterchangeNewlineAtStart() const { return m_hasInterchangeNewlineAtStart; } + bool hasInterchangeNewlineAtEnd() const { return m_hasInterchangeNewlineAtEnd; } + + void removeNode(PassRefPtr<Node>); + void removeNodePreservingChildren(Node*); + +private: + PassRefPtr<StyledElement> insertFragmentForTestRendering(Node* context); + void removeUnrenderedNodes(Node*); + void restoreTestRenderingNodesToFragment(StyledElement*); + void removeInterchangeNodes(Node*); + + void insertNodeBefore(PassRefPtr<Node> node, Node* refNode); + + RefPtr<Document> m_document; + RefPtr<DocumentFragment> m_fragment; + bool m_matchStyle; + bool m_hasInterchangeNewlineAtStart; + bool m_hasInterchangeNewlineAtEnd; +}; + +static bool isInterchangeNewlineNode(const Node *node) +{ + DEFINE_STATIC_LOCAL(String, interchangeNewlineClassString, (AppleInterchangeNewline)); + return node && node->hasTagName(brTag) && + static_cast<const Element *>(node)->getAttribute(classAttr) == interchangeNewlineClassString; +} + +static bool isInterchangeConvertedSpaceSpan(const Node *node) +{ + DEFINE_STATIC_LOCAL(String, convertedSpaceSpanClassString, (AppleConvertedSpace)); + return node->isHTMLElement() && + static_cast<const HTMLElement *>(node)->getAttribute(classAttr) == convertedSpaceSpanClassString; +} + +static Position positionAvoidingPrecedingNodes(Position pos) +{ + // If we're already on a break, it's probably a placeholder and we shouldn't change our position. + if (pos.node()->hasTagName(brTag)) + return pos; + + // We also stop when changing block flow elements because even though the visual position is the + // same. E.g., + // <div>foo^</div>^ + // The two positions above are the same visual position, but we want to stay in the same block. + Node* stopNode = pos.node()->enclosingBlockFlowElement(); + while (stopNode != pos.node() && VisiblePosition(pos) == VisiblePosition(pos.next())) + pos = pos.next(); + return pos; +} + +ReplacementFragment::ReplacementFragment(Document* document, DocumentFragment* fragment, bool matchStyle, const VisibleSelection& selection) + : m_document(document), + m_fragment(fragment), + m_matchStyle(matchStyle), + m_hasInterchangeNewlineAtStart(false), + m_hasInterchangeNewlineAtEnd(false) +{ + if (!m_document) + return; + if (!m_fragment) + return; + if (!m_fragment->firstChild()) + return; + + Element* editableRoot = selection.rootEditableElement(); + ASSERT(editableRoot); + if (!editableRoot) + return; + + Node* shadowAncestorNode = editableRoot->shadowAncestorNode(); + + if (!editableRoot->getAttributeEventListener(eventNames().webkitBeforeTextInsertedEvent) && + // FIXME: Remove these checks once textareas and textfields actually register an event handler. + !(shadowAncestorNode && shadowAncestorNode->renderer() && shadowAncestorNode->renderer()->isTextControl()) && + editableRoot->isContentRichlyEditable()) { + removeInterchangeNodes(m_fragment.get()); + return; + } + + Node* styleNode = selection.base().node(); + RefPtr<StyledElement> holder = insertFragmentForTestRendering(styleNode); + + RefPtr<Range> range = VisibleSelection::selectionFromContentsOfNode(holder.get()).toNormalizedRange(); + String text = plainText(range.get()); + // Give the root a chance to change the text. + RefPtr<BeforeTextInsertedEvent> evt = BeforeTextInsertedEvent::create(text); + ExceptionCode ec = 0; + editableRoot->dispatchEvent(evt, ec); + ASSERT(ec == 0); + if (text != evt->text() || !editableRoot->isContentRichlyEditable()) { + restoreTestRenderingNodesToFragment(holder.get()); + removeNode(holder); + + m_fragment = createFragmentFromText(selection.toNormalizedRange().get(), evt->text()); + if (!m_fragment->firstChild()) + return; + holder = insertFragmentForTestRendering(styleNode); + } + + removeInterchangeNodes(holder.get()); + + removeUnrenderedNodes(holder.get()); + restoreTestRenderingNodesToFragment(holder.get()); + removeNode(holder); +} + +bool ReplacementFragment::isEmpty() const +{ + return (!m_fragment || !m_fragment->firstChild()) && !m_hasInterchangeNewlineAtStart && !m_hasInterchangeNewlineAtEnd; +} + +Node *ReplacementFragment::firstChild() const +{ + return m_fragment ? m_fragment->firstChild() : 0; +} + +Node *ReplacementFragment::lastChild() const +{ + return m_fragment ? m_fragment->lastChild() : 0; +} + +void ReplacementFragment::removeNodePreservingChildren(Node *node) +{ + if (!node) + return; + + while (RefPtr<Node> n = node->firstChild()) { + removeNode(n); + insertNodeBefore(n.release(), node); + } + removeNode(node); +} + +void ReplacementFragment::removeNode(PassRefPtr<Node> node) +{ + if (!node) + return; + + ContainerNode* parent = node->parentNode(); + if (!parent) + return; + + ExceptionCode ec = 0; + parent->removeChild(node.get(), ec); + ASSERT(ec == 0); +} + +void ReplacementFragment::insertNodeBefore(PassRefPtr<Node> node, Node* refNode) +{ + if (!node || !refNode) + return; + + ContainerNode* parent = refNode->parentNode(); + if (!parent) + return; + + ExceptionCode ec = 0; + parent->insertBefore(node, refNode, ec); + ASSERT(ec == 0); +} + +PassRefPtr<StyledElement> ReplacementFragment::insertFragmentForTestRendering(Node* context) +{ + HTMLElement* body = m_document->body(); + if (!body) + return 0; + + RefPtr<StyledElement> holder = createDefaultParagraphElement(m_document.get()); + + ExceptionCode ec = 0; + + // Copy the whitespace and user-select style from the context onto this element. + // FIXME: We should examine other style properties to see if they would be appropriate to consider during the test rendering. + Node* n = context; + while (n && !n->isElementNode()) + n = n->parentNode(); + if (n) { + RefPtr<CSSComputedStyleDeclaration> conFontStyle = computedStyle(n); + CSSStyleDeclaration* style = holder->style(); + style->setProperty(CSSPropertyWhiteSpace, conFontStyle->getPropertyValue(CSSPropertyWhiteSpace), false, ec); + ASSERT(ec == 0); + style->setProperty(CSSPropertyWebkitUserSelect, conFontStyle->getPropertyValue(CSSPropertyWebkitUserSelect), false, ec); + ASSERT(ec == 0); + } + + holder->appendChild(m_fragment, ec); + ASSERT(ec == 0); + + body->appendChild(holder.get(), ec); + ASSERT(ec == 0); + + m_document->updateLayoutIgnorePendingStylesheets(); + + return holder.release(); +} + +void ReplacementFragment::restoreTestRenderingNodesToFragment(StyledElement* holder) +{ + if (!holder) + return; + + ExceptionCode ec = 0; + while (RefPtr<Node> node = holder->firstChild()) { + holder->removeChild(node.get(), ec); + ASSERT(ec == 0); + m_fragment->appendChild(node.get(), ec); + ASSERT(ec == 0); + } +} + +void ReplacementFragment::removeUnrenderedNodes(Node* holder) +{ + Vector<Node*> unrendered; + + for (Node* node = holder->firstChild(); node; node = node->traverseNextNode(holder)) + if (!isNodeRendered(node) && !isTableStructureNode(node)) + unrendered.append(node); + + size_t n = unrendered.size(); + for (size_t i = 0; i < n; ++i) + removeNode(unrendered[i]); +} + +void ReplacementFragment::removeInterchangeNodes(Node* container) +{ + // Interchange newlines at the "start" of the incoming fragment must be + // either the first node in the fragment or the first leaf in the fragment. + Node* node = container->firstChild(); + while (node) { + if (isInterchangeNewlineNode(node)) { + m_hasInterchangeNewlineAtStart = true; + removeNode(node); + break; + } + node = node->firstChild(); + } + if (!container->hasChildNodes()) + return; + // Interchange newlines at the "end" of the incoming fragment must be + // either the last node in the fragment or the last leaf in the fragment. + node = container->lastChild(); + while (node) { + if (isInterchangeNewlineNode(node)) { + m_hasInterchangeNewlineAtEnd = true; + removeNode(node); + break; + } + node = node->lastChild(); + } + + node = container->firstChild(); + while (node) { + Node *next = node->traverseNextNode(); + if (isInterchangeConvertedSpaceSpan(node)) { + RefPtr<Node> n = 0; + while ((n = node->firstChild())) { + removeNode(n); + insertNodeBefore(n, node); + } + removeNode(node); + if (n) + next = n->traverseNextNode(); + } + node = next; + } +} + +ReplaceSelectionCommand::ReplaceSelectionCommand(Document* document, PassRefPtr<DocumentFragment> fragment, + bool selectReplacement, bool smartReplace, bool matchStyle, bool preventNesting, bool movingParagraph, + EditAction editAction) + : CompositeEditCommand(document), + m_selectReplacement(selectReplacement), + m_smartReplace(smartReplace), + m_matchStyle(matchStyle), + m_documentFragment(fragment), + m_preventNesting(preventNesting), + m_movingParagraph(movingParagraph), + m_editAction(editAction), + m_shouldMergeEnd(false) +{ +} + +static bool hasMatchingQuoteLevel(VisiblePosition endOfExistingContent, VisiblePosition endOfInsertedContent) +{ + Position existing = endOfExistingContent.deepEquivalent(); + Position inserted = endOfInsertedContent.deepEquivalent(); + bool isInsideMailBlockquote = nearestMailBlockquote(inserted.node()); + return isInsideMailBlockquote && (numEnclosingMailBlockquotes(existing) == numEnclosingMailBlockquotes(inserted)); +} + +bool ReplaceSelectionCommand::shouldMergeStart(bool selectionStartWasStartOfParagraph, bool fragmentHasInterchangeNewlineAtStart, bool selectionStartWasInsideMailBlockquote) +{ + if (m_movingParagraph) + return false; + + VisiblePosition startOfInsertedContent(positionAtStartOfInsertedContent()); + VisiblePosition prev = startOfInsertedContent.previous(true); + if (prev.isNull()) + return false; + + // When we have matching quote levels, its ok to merge more frequently. + // For a successful merge, we still need to make sure that the inserted content starts with the beginning of a paragraph. + // And we should only merge here if the selection start was inside a mail blockquote. This prevents against removing a + // blockquote from newly pasted quoted content that was pasted into an unquoted position. If that unquoted position happens + // to be right after another blockquote, we don't want to merge and risk stripping a valid block (and newline) from the pasted content. + if (isStartOfParagraph(startOfInsertedContent) && selectionStartWasInsideMailBlockquote && hasMatchingQuoteLevel(prev, positionAtEndOfInsertedContent())) + return true; + + return !selectionStartWasStartOfParagraph && + !fragmentHasInterchangeNewlineAtStart && + isStartOfParagraph(startOfInsertedContent) && + !startOfInsertedContent.deepEquivalent().node()->hasTagName(brTag) && + shouldMerge(startOfInsertedContent, prev); +} + +bool ReplaceSelectionCommand::shouldMergeEnd(bool selectionEndWasEndOfParagraph) +{ + VisiblePosition endOfInsertedContent(positionAtEndOfInsertedContent()); + VisiblePosition next = endOfInsertedContent.next(true); + if (next.isNull()) + return false; + + return !selectionEndWasEndOfParagraph && + isEndOfParagraph(endOfInsertedContent) && + !endOfInsertedContent.deepEquivalent().node()->hasTagName(brTag) && + shouldMerge(endOfInsertedContent, next); +} + +static bool isMailPasteAsQuotationNode(const Node* node) +{ + return node && node->hasTagName(blockquoteTag) && node->isElementNode() && static_cast<const Element*>(node)->getAttribute(classAttr) == ApplePasteAsQuotation; +} + +// Wrap CompositeEditCommand::removeNodePreservingChildren() so we can update the nodes we track +void ReplaceSelectionCommand::removeNodePreservingChildren(Node* node) +{ + if (m_firstNodeInserted == node) + m_firstNodeInserted = node->traverseNextNode(); + if (m_lastLeafInserted == node) + m_lastLeafInserted = node->lastChild() ? node->lastChild() : node->traverseNextSibling(); + CompositeEditCommand::removeNodePreservingChildren(node); +} + +// Wrap CompositeEditCommand::removeNodeAndPruneAncestors() so we can update the nodes we track +void ReplaceSelectionCommand::removeNodeAndPruneAncestors(Node* node) +{ + // prepare in case m_firstNodeInserted and/or m_lastLeafInserted get removed + // FIXME: shouldn't m_lastLeafInserted be adjusted using traversePreviousNode()? + Node* afterFirst = m_firstNodeInserted ? m_firstNodeInserted->traverseNextSibling() : 0; + Node* afterLast = m_lastLeafInserted ? m_lastLeafInserted->traverseNextSibling() : 0; + + CompositeEditCommand::removeNodeAndPruneAncestors(node); + + // adjust m_firstNodeInserted and m_lastLeafInserted since either or both may have been removed + if (m_lastLeafInserted && !m_lastLeafInserted->inDocument()) + m_lastLeafInserted = afterLast; + if (m_firstNodeInserted && !m_firstNodeInserted->inDocument()) + m_firstNodeInserted = m_lastLeafInserted && m_lastLeafInserted->inDocument() ? afterFirst : 0; +} + +static bool isHeaderElement(Node* a) +{ + if (!a) + return false; + + return a->hasTagName(h1Tag) || + a->hasTagName(h2Tag) || + a->hasTagName(h3Tag) || + a->hasTagName(h4Tag) || + a->hasTagName(h5Tag); +} + +static bool haveSameTagName(Node* a, Node* b) +{ + return a && b && a->isElementNode() && b->isElementNode() && static_cast<Element*>(a)->tagName() == static_cast<Element*>(b)->tagName(); +} + +bool ReplaceSelectionCommand::shouldMerge(const VisiblePosition& source, const VisiblePosition& destination) +{ + if (source.isNull() || destination.isNull()) + return false; + + Node* sourceNode = source.deepEquivalent().node(); + Node* destinationNode = destination.deepEquivalent().node(); + Node* sourceBlock = enclosingBlock(sourceNode); + Node* destinationBlock = enclosingBlock(destinationNode); + return !enclosingNodeOfType(source.deepEquivalent(), &isMailPasteAsQuotationNode) && + sourceBlock && (!sourceBlock->hasTagName(blockquoteTag) || isMailBlockquote(sourceBlock)) && + enclosingListChild(sourceBlock) == enclosingListChild(destinationNode) && + enclosingTableCell(source.deepEquivalent()) == enclosingTableCell(destination.deepEquivalent()) && + (!isHeaderElement(sourceBlock) || haveSameTagName(sourceBlock, destinationBlock)) && + // Don't merge to or from a position before or after a block because it would + // be a no-op and cause infinite recursion. + !isBlock(sourceNode) && !isBlock(destinationNode); +} + +// Style rules that match just inserted elements could change their appearance, like +// a div inserted into a document with div { display:inline; }. +void ReplaceSelectionCommand::negateStyleRulesThatAffectAppearance() +{ + for (RefPtr<Node> node = m_firstNodeInserted.get(); node; node = node->traverseNextNode()) { + // FIXME: <rdar://problem/5371536> Style rules that match pasted content can change it's appearance + if (isStyleSpan(node.get())) { + HTMLElement* e = static_cast<HTMLElement*>(node.get()); + // There are other styles that style rules can give to style spans, + // but these are the two important ones because they'll prevent + // inserted content from appearing in the right paragraph. + // FIXME: Hyatt is concerned that selectively using display:inline will give inconsistent + // results. We already know one issue because td elements ignore their display property + // in quirks mode (which Mail.app is always in). We should look for an alternative. + if (isBlock(e)) + e->getInlineStyleDecl()->setProperty(CSSPropertyDisplay, CSSValueInline); + if (e->renderer() && e->renderer()->style()->floating() != FNONE) + e->getInlineStyleDecl()->setProperty(CSSPropertyFloat, CSSValueNone); + } + if (node == m_lastLeafInserted) + break; + } +} + +void ReplaceSelectionCommand::removeUnrenderedTextNodesAtEnds() +{ + document()->updateLayoutIgnorePendingStylesheets(); + if (!m_lastLeafInserted->renderer() && + m_lastLeafInserted->isTextNode() && + !enclosingNodeWithTag(Position(m_lastLeafInserted.get(), 0), selectTag) && + !enclosingNodeWithTag(Position(m_lastLeafInserted.get(), 0), scriptTag)) { + if (m_firstNodeInserted == m_lastLeafInserted) { + removeNode(m_lastLeafInserted.get()); + m_lastLeafInserted = 0; + m_firstNodeInserted = 0; + return; + } + RefPtr<Node> previous = m_lastLeafInserted->traversePreviousNode(); + removeNode(m_lastLeafInserted.get()); + m_lastLeafInserted = previous; + } + + // We don't have to make sure that m_firstNodeInserted isn't inside a select or script element, because + // it is a top level node in the fragment and the user can't insert into those elements. + if (!m_firstNodeInserted->renderer() && + m_firstNodeInserted->isTextNode()) { + if (m_firstNodeInserted == m_lastLeafInserted) { + removeNode(m_firstNodeInserted.get()); + m_firstNodeInserted = 0; + m_lastLeafInserted = 0; + return; + } + RefPtr<Node> next = m_firstNodeInserted->traverseNextSibling(); + removeNode(m_firstNodeInserted.get()); + m_firstNodeInserted = next; + } +} + +void ReplaceSelectionCommand::handlePasteAsQuotationNode() +{ + Node* node = m_firstNodeInserted.get(); + if (isMailPasteAsQuotationNode(node)) + removeNodeAttribute(static_cast<Element*>(node), classAttr); +} + +VisiblePosition ReplaceSelectionCommand::positionAtEndOfInsertedContent() +{ + Node* lastNode = m_lastLeafInserted.get(); + // FIXME: Why is this hack here? What's special about <select> tags? + Node* enclosingSelect = enclosingNodeWithTag(firstDeepEditingPositionForNode(lastNode), selectTag); + if (enclosingSelect) + lastNode = enclosingSelect; + return lastDeepEditingPositionForNode(lastNode); +} + +VisiblePosition ReplaceSelectionCommand::positionAtStartOfInsertedContent() +{ + // Return the inserted content's first VisiblePosition. + return VisiblePosition(nextCandidate(positionInParentBeforeNode(m_firstNodeInserted.get()))); +} + +// Remove style spans before insertion if they are unnecessary. It's faster because we'll +// avoid doing a layout. +static bool handleStyleSpansBeforeInsertion(ReplacementFragment& fragment, const Position& insertionPos) +{ + Node* topNode = fragment.firstChild(); + + // Handling the case where we are doing Paste as Quotation or pasting into quoted content is more complicated (see handleStyleSpans) + // and doesn't receive the optimization. + if (isMailPasteAsQuotationNode(topNode) || nearestMailBlockquote(topNode)) + return false; + + // Either there are no style spans in the fragment or a WebKit client has added content to the fragment + // before inserting it. Look for and handle style spans after insertion. + if (!isStyleSpan(topNode)) + return false; + + Node* sourceDocumentStyleSpan = topNode; + RefPtr<Node> copiedRangeStyleSpan = sourceDocumentStyleSpan->firstChild(); + + RefPtr<EditingStyle> styleAtInsertionPos = EditingStyle::create(rangeCompliantEquivalent(insertionPos)); + String styleText = styleAtInsertionPos->style()->cssText(); + + // FIXME: This string comparison is a naive way of comparing two styles. + // We should be taking the diff and check that the diff is empty. + if (styleText == static_cast<Element*>(sourceDocumentStyleSpan)->getAttribute(styleAttr)) { + fragment.removeNodePreservingChildren(sourceDocumentStyleSpan); + if (!isStyleSpan(copiedRangeStyleSpan.get())) + return true; + } + + if (isStyleSpan(copiedRangeStyleSpan.get()) && styleText == static_cast<Element*>(copiedRangeStyleSpan.get())->getAttribute(styleAttr)) { + fragment.removeNodePreservingChildren(copiedRangeStyleSpan.get()); + return true; + } + + return false; +} + +// At copy time, WebKit wraps copied content in a span that contains the source document's +// default styles. If the copied Range inherits any other styles from its ancestors, we put +// those styles on a second span. +// This function removes redundant styles from those spans, and removes the spans if all their +// styles are redundant. +// We should remove the Apple-style-span class when we're done, see <rdar://problem/5685600>. +// We should remove styles from spans that are overridden by all of their children, either here +// or at copy time. +void ReplaceSelectionCommand::handleStyleSpans() +{ + Node* sourceDocumentStyleSpan = 0; + Node* copiedRangeStyleSpan = 0; + // The style span that contains the source document's default style should be at + // the top of the fragment, but Mail sometimes adds a wrapper (for Paste As Quotation), + // so search for the top level style span instead of assuming it's at the top. + for (Node* node = m_firstNodeInserted.get(); node; node = node->traverseNextNode()) { + if (isStyleSpan(node)) { + sourceDocumentStyleSpan = node; + // If the copied Range's common ancestor had user applied inheritable styles + // on it, they'll be on a second style span, just below the one that holds the + // document defaults. + if (isStyleSpan(node->firstChild())) + copiedRangeStyleSpan = node->firstChild(); + break; + } + } + + // There might not be any style spans if we're pasting from another application or if + // we are here because of a document.execCommand("InsertHTML", ...) call. + if (!sourceDocumentStyleSpan) + return; + + RefPtr<EditingStyle> sourceDocumentStyle = EditingStyle::create(static_cast<HTMLElement*>(sourceDocumentStyleSpan)->getInlineStyleDecl()); + ContainerNode* context = sourceDocumentStyleSpan->parentNode(); + + // If Mail wraps the fragment with a Paste as Quotation blockquote, or if you're pasting into a quoted region, + // styles from blockquoteNode are allowed to override those from the source document, see <rdar://problem/4930986> and <rdar://problem/5089327>. + Node* blockquoteNode = isMailPasteAsQuotationNode(context) ? context : nearestMailBlockquote(context); + if (blockquoteNode) { + sourceDocumentStyle->removeStyleConflictingWithStyleOfNode(blockquoteNode); + context = blockquoteNode->parentNode(); + } + + // This operation requires that only editing styles to be removed from sourceDocumentStyle. + sourceDocumentStyle->prepareToApplyAt(firstPositionInNode(context)); + + // Remove block properties in the span's style. This prevents properties that probably have no effect + // currently from affecting blocks later if the style is cloned for a new block element during a future + // editing operation. + // FIXME: They *can* have an effect currently if blocks beneath the style span aren't individually marked + // with block styles by the editing engine used to style them. WebKit doesn't do this, but others might. + sourceDocumentStyle->removeBlockProperties(); + + // The styles on sourceDocumentStyleSpan are all redundant, and there is no copiedRangeStyleSpan + // to consider. We're finished. + if (sourceDocumentStyle->isEmpty() && !copiedRangeStyleSpan) { + removeNodePreservingChildren(sourceDocumentStyleSpan); + return; + } + + // There are non-redundant styles on sourceDocumentStyleSpan, but there is no + // copiedRangeStyleSpan. Remove the span, because it could be surrounding block elements, + // and apply the styles to its children. + if (!sourceDocumentStyle->isEmpty() && !copiedRangeStyleSpan) { + copyStyleToChildren(sourceDocumentStyleSpan, sourceDocumentStyle->style()); + removeNodePreservingChildren(sourceDocumentStyleSpan); + return; + } + + RefPtr<EditingStyle> copiedRangeStyle = EditingStyle::create(static_cast<HTMLElement*>(copiedRangeStyleSpan)->getInlineStyleDecl()); + + // We're going to put sourceDocumentStyleSpan's non-redundant styles onto copiedRangeStyleSpan, + // as long as they aren't overridden by ones on copiedRangeStyleSpan. + copiedRangeStyle->style()->merge(sourceDocumentStyle->style(), false); + + removeNodePreservingChildren(sourceDocumentStyleSpan); + + // Remove redundant styles. + context = copiedRangeStyleSpan->parentNode(); + copiedRangeStyle->prepareToApplyAt(firstPositionInNode(context)); + copiedRangeStyle->removeBlockProperties(); + if (copiedRangeStyle->isEmpty()) { + removeNodePreservingChildren(copiedRangeStyleSpan); + return; + } + + // Clear the redundant styles from the span's style attribute. + // FIXME: If font-family:-webkit-monospace is non-redundant, then the font-size should stay, even if it + // appears redundant. + setNodeAttribute(static_cast<Element*>(copiedRangeStyleSpan), styleAttr, copiedRangeStyle->style()->cssText()); +} + +// Take the style attribute of a span and apply it to it's children instead. This allows us to +// convert invalid HTML where a span contains block elements into valid HTML while preserving +// styles. +void ReplaceSelectionCommand::copyStyleToChildren(Node* parentNode, const CSSMutableStyleDeclaration* parentStyle) +{ + ASSERT(parentNode->hasTagName(spanTag)); + NodeVector childNodes; + for (RefPtr<Node> childNode = parentNode->firstChild(); childNode; childNode = childNode->nextSibling()) + childNodes.append(childNode); + + for (NodeVector::const_iterator it = childNodes.begin(); it != childNodes.end(); it++) { + Node* childNode = it->get(); + if (childNode->isTextNode() || !isBlock(childNode) || childNode->hasTagName(preTag)) { + // In this case, put a span tag around the child node. + RefPtr<Node> newNode = parentNode->cloneNode(false); + ASSERT(newNode->hasTagName(spanTag)); + HTMLElement* newSpan = static_cast<HTMLElement*>(newNode.get()); + setNodeAttribute(newSpan, styleAttr, parentStyle->cssText()); + insertNodeAfter(newSpan, childNode); + ExceptionCode ec = 0; + newSpan->appendChild(childNode, ec); + ASSERT(!ec); + childNode = newSpan; + } else if (childNode->isHTMLElement()) { + // Copy the style attribute and merge them into the child node. We don't want to override + // existing styles, so don't clobber on merge. + RefPtr<CSSMutableStyleDeclaration> newStyle = parentStyle->copy(); + HTMLElement* childElement = static_cast<HTMLElement*>(childNode); + RefPtr<CSSMutableStyleDeclaration> existingStyles = childElement->getInlineStyleDecl()->copy(); + existingStyles->merge(newStyle.get(), false); + setNodeAttribute(childElement, styleAttr, existingStyles->cssText()); + } + } +} + +void ReplaceSelectionCommand::mergeEndIfNeeded() +{ + if (!m_shouldMergeEnd) + return; + + VisiblePosition startOfInsertedContent(positionAtStartOfInsertedContent()); + VisiblePosition endOfInsertedContent(positionAtEndOfInsertedContent()); + + // Bail to avoid infinite recursion. + if (m_movingParagraph) { + ASSERT_NOT_REACHED(); + return; + } + + // Merging two paragraphs will destroy the moved one's block styles. Always move the end of inserted forward + // to preserve the block style of the paragraph already in the document, unless the paragraph to move would + // include the what was the start of the selection that was pasted into, so that we preserve that paragraph's + // block styles. + bool mergeForward = !(inSameParagraph(startOfInsertedContent, endOfInsertedContent) && !isStartOfParagraph(startOfInsertedContent)); + + VisiblePosition destination = mergeForward ? endOfInsertedContent.next() : endOfInsertedContent; + VisiblePosition startOfParagraphToMove = mergeForward ? startOfParagraph(endOfInsertedContent) : endOfInsertedContent.next(); + + // Merging forward could result in deleting the destination anchor node. + // To avoid this, we add a placeholder node before the start of the paragraph. + if (endOfParagraph(startOfParagraphToMove) == destination) { + RefPtr<Node> placeholder = createBreakElement(document()); + insertNodeBefore(placeholder, startOfParagraphToMove.deepEquivalent().node()); + destination = VisiblePosition(Position(placeholder.get(), 0)); + } + + moveParagraph(startOfParagraphToMove, endOfParagraph(startOfParagraphToMove), destination); + + // Merging forward will remove m_lastLeafInserted from the document. + // FIXME: Maintain positions for the start and end of inserted content instead of keeping nodes. The nodes are + // only ever used to create positions where inserted content starts/ends. Also, we sometimes insert content + // directly into text nodes already in the document, in which case tracking inserted nodes is inadequate. + if (mergeForward) { + m_lastLeafInserted = destination.previous().deepEquivalent().node(); + if (!m_firstNodeInserted->inDocument()) + m_firstNodeInserted = endingSelection().visibleStart().deepEquivalent().node(); + // If we merged text nodes, m_lastLeafInserted could be null. If this is the case, + // we use m_firstNodeInserted. + if (!m_lastLeafInserted) + m_lastLeafInserted = m_firstNodeInserted; + } +} + +static Node* enclosingInline(Node* node) +{ + while (ContainerNode* parent = node->parentNode()) { + if (parent->isBlockFlow() || parent->hasTagName(bodyTag)) + return node; + // Stop if any previous sibling is a block. + for (Node* sibling = node->previousSibling(); sibling; sibling = sibling->previousSibling()) { + if (sibling->isBlockFlow()) + return node; + } + node = parent; + } + return node; +} + +void ReplaceSelectionCommand::doApply() +{ + VisibleSelection selection = endingSelection(); + ASSERT(selection.isCaretOrRange()); + ASSERT(selection.start().node()); + if (!selection.isNonOrphanedCaretOrRange() || !selection.start().node()) + return; + + bool selectionIsPlainText = !selection.isContentRichlyEditable(); + + Element* currentRoot = selection.rootEditableElement(); + ReplacementFragment fragment(document(), m_documentFragment.get(), m_matchStyle, selection); + + if (performTrivialReplace(fragment)) + return; + + // We can skip matching the style if the selection is plain text. + if ((selection.start().node()->renderer() && selection.start().node()->renderer()->style()->userModify() == READ_WRITE_PLAINTEXT_ONLY) && + (selection.end().node()->renderer() && selection.end().node()->renderer()->style()->userModify() == READ_WRITE_PLAINTEXT_ONLY)) + m_matchStyle = false; + + if (m_matchStyle) + m_insertionStyle = editingStyleIncludingTypingStyle(selection.start()); + + VisiblePosition visibleStart = selection.visibleStart(); + VisiblePosition visibleEnd = selection.visibleEnd(); + + bool selectionEndWasEndOfParagraph = isEndOfParagraph(visibleEnd); + bool selectionStartWasStartOfParagraph = isStartOfParagraph(visibleStart); + + Node* startBlock = enclosingBlock(visibleStart.deepEquivalent().node()); + + Position insertionPos = selection.start(); + bool startIsInsideMailBlockquote = nearestMailBlockquote(insertionPos.node()); + + if ((selectionStartWasStartOfParagraph && selectionEndWasEndOfParagraph && !startIsInsideMailBlockquote) || + startBlock == currentRoot || isListItem(startBlock) || selectionIsPlainText) + m_preventNesting = false; + + if (selection.isRange()) { + // When the end of the selection being pasted into is at the end of a paragraph, and that selection + // spans multiple blocks, not merging may leave an empty line. + // When the start of the selection being pasted into is at the start of a block, not merging + // will leave hanging block(s). + // Merge blocks if the start of the selection was in a Mail blockquote, since we handle + // that case specially to prevent nesting. + bool mergeBlocksAfterDelete = startIsInsideMailBlockquote || isEndOfParagraph(visibleEnd) || isStartOfBlock(visibleStart); + // FIXME: We should only expand to include fully selected special elements if we are copying a + // selection and pasting it on top of itself. + deleteSelection(false, mergeBlocksAfterDelete, true, false); + visibleStart = endingSelection().visibleStart(); + if (fragment.hasInterchangeNewlineAtStart()) { + if (isEndOfParagraph(visibleStart) && !isStartOfParagraph(visibleStart)) { + if (!isEndOfDocument(visibleStart)) + setEndingSelection(visibleStart.next()); + } else + insertParagraphSeparator(); + } + insertionPos = endingSelection().start(); + } else { + ASSERT(selection.isCaret()); + if (fragment.hasInterchangeNewlineAtStart()) { + VisiblePosition next = visibleStart.next(true); + if (isEndOfParagraph(visibleStart) && !isStartOfParagraph(visibleStart) && next.isNotNull()) + setEndingSelection(next); + else + insertParagraphSeparator(); + } + // We split the current paragraph in two to avoid nesting the blocks from the fragment inside the current block. + // For example paste <div>foo</div><div>bar</div><div>baz</div> into <div>x^x</div>, where ^ is the caret. + // As long as the div styles are the same, visually you'd expect: <div>xbar</div><div>bar</div><div>bazx</div>, + // not <div>xbar<div>bar</div><div>bazx</div></div>. + // Don't do this if the selection started in a Mail blockquote. + if (m_preventNesting && !startIsInsideMailBlockquote && !isEndOfParagraph(visibleStart) && !isStartOfParagraph(visibleStart)) { + insertParagraphSeparator(); + setEndingSelection(endingSelection().visibleStart().previous()); + } + insertionPos = endingSelection().start(); + } + + // We don't want any of the pasted content to end up nested in a Mail blockquote, so first break + // out of any surrounding Mail blockquotes. Unless we're inserting in a table, in which case + // breaking the blockquote will prevent the content from actually being inserted in the table. + if (startIsInsideMailBlockquote && m_preventNesting && !(enclosingNodeOfType(insertionPos, &isTableStructureNode))) { + applyCommandToComposite(BreakBlockquoteCommand::create(document())); + // This will leave a br between the split. + Node* br = endingSelection().start().node(); + ASSERT(br->hasTagName(brTag)); + // Insert content between the two blockquotes, but remove the br (since it was just a placeholder). + insertionPos = positionInParentBeforeNode(br); + removeNode(br); + } + + // Inserting content could cause whitespace to collapse, e.g. inserting <div>foo</div> into hello^ world. + prepareWhitespaceAtPositionForSplit(insertionPos); + + // If the downstream node has been removed there's no point in continuing. + if (!insertionPos.downstream().node()) + return; + + // NOTE: This would be an incorrect usage of downstream() if downstream() were changed to mean the last position after + // p that maps to the same visible position as p (since in the case where a br is at the end of a block and collapsed + // away, there are positions after the br which map to the same visible position as [br, 0]). + Node* endBR = insertionPos.downstream().node()->hasTagName(brTag) ? insertionPos.downstream().node() : 0; + VisiblePosition originalVisPosBeforeEndBR; + if (endBR) + originalVisPosBeforeEndBR = VisiblePosition(endBR, 0, DOWNSTREAM).previous(); + + startBlock = enclosingBlock(insertionPos.node()); + + // Adjust insertionPos to prevent nesting. + // If the start was in a Mail blockquote, we will have already handled adjusting insertionPos above. + if (m_preventNesting && startBlock && !startIsInsideMailBlockquote) { + ASSERT(startBlock != currentRoot); + VisiblePosition visibleInsertionPos(insertionPos); + if (isEndOfBlock(visibleInsertionPos) && !(isStartOfBlock(visibleInsertionPos) && fragment.hasInterchangeNewlineAtEnd())) + insertionPos = positionInParentAfterNode(startBlock); + else if (isStartOfBlock(visibleInsertionPos)) + insertionPos = positionInParentBeforeNode(startBlock); + } + + // Paste into run of tabs splits the tab span. + insertionPos = positionOutsideTabSpan(insertionPos); + + // Paste at start or end of link goes outside of link. + insertionPos = positionAvoidingSpecialElementBoundary(insertionPos); + + // FIXME: Can this wait until after the operation has been performed? There doesn't seem to be + // any work performed after this that queries or uses the typing style. + if (Frame* frame = document()->frame()) + frame->selection()->clearTypingStyle(); + + bool handledStyleSpans = handleStyleSpansBeforeInsertion(fragment, insertionPos); + + // We don't want the destination to end up inside nodes that weren't selected. To avoid that, we move the + // position forward without changing the visible position so we're still at the same visible location, but + // outside of preceding tags. + insertionPos = positionAvoidingPrecedingNodes(insertionPos); + + // FIXME: When pasting rich content we're often prevented from heading down the fast path by style spans. Try + // again here if they've been removed. + + // We're finished if there is nothing to add. + if (fragment.isEmpty() || !fragment.firstChild()) + return; + + // 1) Insert the content. + // 2) Remove redundant styles and style tags, this inner <b> for example: <b>foo <b>bar</b> baz</b>. + // 3) Merge the start of the added content with the content before the position being pasted into. + // 4) Do one of the following: a) expand the last br if the fragment ends with one and it collapsed, + // b) merge the last paragraph of the incoming fragment with the paragraph that contained the + // end of the selection that was pasted into, or c) handle an interchange newline at the end of the + // incoming fragment. + // 5) Add spaces for smart replace. + // 6) Select the replacement if requested, and match style if requested. + + VisiblePosition startOfInsertedContent, endOfInsertedContent; + + RefPtr<Node> refNode = fragment.firstChild(); + RefPtr<Node> node = refNode->nextSibling(); + + fragment.removeNode(refNode); + + Node* blockStart = enclosingBlock(insertionPos.node()); + if ((isListElement(refNode.get()) || (isStyleSpan(refNode.get()) && isListElement(refNode->firstChild()))) + && blockStart->renderer()->isListItem()) + refNode = insertAsListItems(refNode, blockStart, insertionPos); + else + insertNodeAtAndUpdateNodesInserted(refNode, insertionPos); + + // Mutation events (bug 22634) may have already removed the inserted content + if (!refNode->inDocument()) + return; + + bool plainTextFragment = isPlainTextMarkup(refNode.get()); + + while (node) { + RefPtr<Node> next = node->nextSibling(); + fragment.removeNode(node.get()); + insertNodeAfterAndUpdateNodesInserted(node, refNode.get()); + + // Mutation events (bug 22634) may have already removed the inserted content + if (!node->inDocument()) + return; + + refNode = node; + if (node && plainTextFragment) + plainTextFragment = isPlainTextMarkup(node.get()); + node = next; + } + + removeUnrenderedTextNodesAtEnds(); + + negateStyleRulesThatAffectAppearance(); + + if (!handledStyleSpans) + handleStyleSpans(); + + // Mutation events (bug 20161) may have already removed the inserted content + if (!m_firstNodeInserted || !m_firstNodeInserted->inDocument()) + return; + + endOfInsertedContent = positionAtEndOfInsertedContent(); + startOfInsertedContent = positionAtStartOfInsertedContent(); + + // We inserted before the startBlock to prevent nesting, and the content before the startBlock wasn't in its own block and + // didn't have a br after it, so the inserted content ended up in the same paragraph. + if (startBlock && insertionPos.node() == startBlock->parentNode() && (unsigned)insertionPos.deprecatedEditingOffset() < startBlock->nodeIndex() && !isStartOfParagraph(startOfInsertedContent)) + insertNodeAt(createBreakElement(document()).get(), startOfInsertedContent.deepEquivalent()); + + Position lastPositionToSelect; + + bool interchangeNewlineAtEnd = fragment.hasInterchangeNewlineAtEnd(); + + if (endBR && (plainTextFragment || shouldRemoveEndBR(endBR, originalVisPosBeforeEndBR))) + removeNodeAndPruneAncestors(endBR); + + // Determine whether or not we should merge the end of inserted content with what's after it before we do + // the start merge so that the start merge doesn't effect our decision. + m_shouldMergeEnd = shouldMergeEnd(selectionEndWasEndOfParagraph); + + if (shouldMergeStart(selectionStartWasStartOfParagraph, fragment.hasInterchangeNewlineAtStart(), startIsInsideMailBlockquote)) { + VisiblePosition destination = startOfInsertedContent.previous(); + VisiblePosition startOfParagraphToMove = startOfInsertedContent; + // We need to handle the case where we need to merge the end + // but our destination node is inside an inline that is the last in the block. + // We insert a placeholder before the newly inserted content to avoid being merged into the inline. + Node* destinationNode = destination.deepEquivalent().node(); + if (m_shouldMergeEnd && destinationNode != enclosingInline(destinationNode) && enclosingInline(destinationNode)->nextSibling()) + insertNodeBefore(createBreakElement(document()), refNode.get()); + + // Merging the the first paragraph of inserted content with the content that came + // before the selection that was pasted into would also move content after + // the selection that was pasted into if: only one paragraph was being pasted, + // and it was not wrapped in a block, the selection that was pasted into ended + // at the end of a block and the next paragraph didn't start at the start of a block. + // Insert a line break just after the inserted content to separate it from what + // comes after and prevent that from happening. + VisiblePosition endOfInsertedContent = positionAtEndOfInsertedContent(); + if (startOfParagraph(endOfInsertedContent) == startOfParagraphToMove) { + insertNodeAt(createBreakElement(document()).get(), endOfInsertedContent.deepEquivalent()); + // Mutation events (bug 22634) triggered by inserting the <br> might have removed the content we're about to move + if (!startOfParagraphToMove.deepEquivalent().node()->inDocument()) + return; + } + + // FIXME: Maintain positions for the start and end of inserted content instead of keeping nodes. The nodes are + // only ever used to create positions where inserted content starts/ends. + moveParagraph(startOfParagraphToMove, endOfParagraph(startOfParagraphToMove), destination); + m_firstNodeInserted = endingSelection().visibleStart().deepEquivalent().downstream().node(); + if (!m_lastLeafInserted->inDocument()) + m_lastLeafInserted = endingSelection().visibleEnd().deepEquivalent().upstream().node(); + } + + endOfInsertedContent = positionAtEndOfInsertedContent(); + startOfInsertedContent = positionAtStartOfInsertedContent(); + + if (interchangeNewlineAtEnd) { + VisiblePosition next = endOfInsertedContent.next(true); + + if (selectionEndWasEndOfParagraph || !isEndOfParagraph(endOfInsertedContent) || next.isNull()) { + if (!isStartOfParagraph(endOfInsertedContent)) { + setEndingSelection(endOfInsertedContent); + Node* enclosingNode = enclosingBlock(endOfInsertedContent.deepEquivalent().node()); + if (isListItem(enclosingNode)) { + RefPtr<Node> newListItem = createListItemElement(document()); + insertNodeAfter(newListItem, enclosingNode); + setEndingSelection(VisiblePosition(Position(newListItem, 0))); + } else + // Use a default paragraph element (a plain div) for the empty paragraph, using the last paragraph + // block's style seems to annoy users. + insertParagraphSeparator(true); + + // Select up to the paragraph separator that was added. + lastPositionToSelect = endingSelection().visibleStart().deepEquivalent(); + updateNodesInserted(lastPositionToSelect.node()); + } + } else { + // Select up to the beginning of the next paragraph. + lastPositionToSelect = next.deepEquivalent().downstream(); + } + + } else + mergeEndIfNeeded(); + + handlePasteAsQuotationNode(); + + endOfInsertedContent = positionAtEndOfInsertedContent(); + startOfInsertedContent = positionAtStartOfInsertedContent(); + + // Add spaces for smart replace. + if (m_smartReplace && currentRoot) { + // Disable smart replace for password fields. + Node* start = currentRoot->shadowAncestorNode(); + if (start->hasTagName(inputTag) && static_cast<HTMLInputElement*>(start)->isPasswordField()) + m_smartReplace = false; + } + if (m_smartReplace) { + bool needsTrailingSpace = !isEndOfParagraph(endOfInsertedContent) && + !isCharacterSmartReplaceExempt(endOfInsertedContent.characterAfter(), false); + if (needsTrailingSpace) { + RenderObject* renderer = m_lastLeafInserted->renderer(); + bool collapseWhiteSpace = !renderer || renderer->style()->collapseWhiteSpace(); + Node* endNode = positionAtEndOfInsertedContent().deepEquivalent().upstream().node(); + if (endNode->isTextNode()) { + Text* text = static_cast<Text*>(endNode); + insertTextIntoNode(text, text->length(), collapseWhiteSpace ? nonBreakingSpaceString() : " "); + } else { + RefPtr<Node> node = document()->createEditingTextNode(collapseWhiteSpace ? nonBreakingSpaceString() : " "); + insertNodeAfterAndUpdateNodesInserted(node, endNode); + } + } + + bool needsLeadingSpace = !isStartOfParagraph(startOfInsertedContent) && + !isCharacterSmartReplaceExempt(startOfInsertedContent.previous().characterAfter(), true); + if (needsLeadingSpace) { + RenderObject* renderer = m_lastLeafInserted->renderer(); + bool collapseWhiteSpace = !renderer || renderer->style()->collapseWhiteSpace(); + Node* startNode = positionAtStartOfInsertedContent().deepEquivalent().downstream().node(); + if (startNode->isTextNode()) { + Text* text = static_cast<Text*>(startNode); + insertTextIntoNode(text, 0, collapseWhiteSpace ? nonBreakingSpaceString() : " "); + } else { + RefPtr<Node> node = document()->createEditingTextNode(collapseWhiteSpace ? nonBreakingSpaceString() : " "); + // Don't updateNodesInserted. Doing so would set m_lastLeafInserted to be the node containing the + // leading space, but m_lastLeafInserted is supposed to mark the end of pasted content. + insertNodeBefore(node, startNode); + // FIXME: Use positions to track the start/end of inserted content. + m_firstNodeInserted = node; + } + } + } + + // If we are dealing with a fragment created from plain text + // no style matching is necessary. + if (plainTextFragment) + m_matchStyle = false; + + completeHTMLReplacement(lastPositionToSelect); +} + +bool ReplaceSelectionCommand::shouldRemoveEndBR(Node* endBR, const VisiblePosition& originalVisPosBeforeEndBR) +{ + if (!endBR || !endBR->inDocument()) + return false; + + VisiblePosition visiblePos(Position(endBR, 0)); + + // Don't remove the br if nothing was inserted. + if (visiblePos.previous() == originalVisPosBeforeEndBR) + return false; + + // Remove the br if it is collapsed away and so is unnecessary. + if (!document()->inNoQuirksMode() && isEndOfBlock(visiblePos) && !isStartOfParagraph(visiblePos)) + return true; + + // A br that was originally holding a line open should be displaced by inserted content or turned into a line break. + // A br that was originally acting as a line break should still be acting as a line break, not as a placeholder. + return isStartOfParagraph(visiblePos) && isEndOfParagraph(visiblePos); +} + +void ReplaceSelectionCommand::completeHTMLReplacement(const Position &lastPositionToSelect) +{ + Position start; + Position end; + + // FIXME: This should never not be the case. + if (m_firstNodeInserted && m_firstNodeInserted->inDocument() && m_lastLeafInserted && m_lastLeafInserted->inDocument()) { + + start = positionAtStartOfInsertedContent().deepEquivalent(); + end = positionAtEndOfInsertedContent().deepEquivalent(); + + // FIXME (11475): Remove this and require that the creator of the fragment to use nbsps. + rebalanceWhitespaceAt(start); + rebalanceWhitespaceAt(end); + + if (m_matchStyle) { + ASSERT(m_insertionStyle); + applyStyle(m_insertionStyle.get(), start, end); + } + + if (lastPositionToSelect.isNotNull()) + end = lastPositionToSelect; + } else if (lastPositionToSelect.isNotNull()) + start = end = lastPositionToSelect; + else + return; + + if (m_selectReplacement) + setEndingSelection(VisibleSelection(start, end, SEL_DEFAULT_AFFINITY)); + else + setEndingSelection(VisibleSelection(end, SEL_DEFAULT_AFFINITY)); +} + +EditAction ReplaceSelectionCommand::editingAction() const +{ + return m_editAction; +} + +void ReplaceSelectionCommand::insertNodeAfterAndUpdateNodesInserted(PassRefPtr<Node> insertChild, Node* refChild) +{ + Node* nodeToUpdate = insertChild.get(); // insertChild will be cleared when passed + insertNodeAfter(insertChild, refChild); + updateNodesInserted(nodeToUpdate); +} + +void ReplaceSelectionCommand::insertNodeAtAndUpdateNodesInserted(PassRefPtr<Node> insertChild, const Position& p) +{ + Node* nodeToUpdate = insertChild.get(); // insertChild will be cleared when passed + insertNodeAt(insertChild, p); + updateNodesInserted(nodeToUpdate); +} + +void ReplaceSelectionCommand::insertNodeBeforeAndUpdateNodesInserted(PassRefPtr<Node> insertChild, Node* refChild) +{ + Node* nodeToUpdate = insertChild.get(); // insertChild will be cleared when passed + insertNodeBefore(insertChild, refChild); + updateNodesInserted(nodeToUpdate); +} + +// If the user is inserting a list into an existing list, instead of nesting the list, +// we put the list items into the existing list. +Node* ReplaceSelectionCommand::insertAsListItems(PassRefPtr<Node> listElement, Node* insertionBlock, const Position& insertPos) +{ + while (listElement->hasChildNodes() && isListElement(listElement->firstChild()) && listElement->childNodeCount() == 1) + listElement = listElement->firstChild(); + + bool isStart = isStartOfParagraph(insertPos); + bool isEnd = isEndOfParagraph(insertPos); + bool isMiddle = !isStart && !isEnd; + Node* lastNode = insertionBlock; + + // If we're in the middle of a list item, we should split it into two separate + // list items and insert these nodes between them. + if (isMiddle) { + int textNodeOffset = insertPos.offsetInContainerNode(); + if (insertPos.node()->isTextNode() && textNodeOffset > 0) + splitTextNode(static_cast<Text*>(insertPos.node()), textNodeOffset); + splitTreeToNode(insertPos.node(), lastNode, true); + } + + while (RefPtr<Node> listItem = listElement->firstChild()) { + ExceptionCode ec = 0; + toContainerNode(listElement.get())->removeChild(listItem.get(), ec); + ASSERT(!ec); + if (isStart || isMiddle) + insertNodeBefore(listItem, lastNode); + else if (isEnd) { + insertNodeAfter(listItem, lastNode); + lastNode = listItem.get(); + } else + ASSERT_NOT_REACHED(); + } + if (isStart || isMiddle) + lastNode = lastNode->previousSibling(); + if (isMiddle) + insertNodeAfter(createListItemElement(document()), lastNode); + updateNodesInserted(lastNode); + return lastNode; +} + +void ReplaceSelectionCommand::updateNodesInserted(Node *node) +{ + if (!node) + return; + + if (!m_firstNodeInserted) + m_firstNodeInserted = node; + + if (node == m_lastLeafInserted) + return; + + m_lastLeafInserted = node->lastDescendant(); +} + +// During simple pastes, where we're just pasting a text node into a run of text, we insert the text node +// directly into the text node that holds the selection. This is much faster than the generalized code in +// ReplaceSelectionCommand, and works around <https://bugs.webkit.org/show_bug.cgi?id=6148> since we don't +// split text nodes. +bool ReplaceSelectionCommand::performTrivialReplace(const ReplacementFragment& fragment) +{ + if (!fragment.firstChild() || fragment.firstChild() != fragment.lastChild() || !fragment.firstChild()->isTextNode()) + return false; + + // FIXME: Would be nice to handle smart replace in the fast path. + if (m_smartReplace || fragment.hasInterchangeNewlineAtStart() || fragment.hasInterchangeNewlineAtEnd()) + return false; + + Text* textNode = static_cast<Text*>(fragment.firstChild()); + // Our fragment creation code handles tabs, spaces, and newlines, so we don't have to worry about those here. + String text(textNode->data()); + + Position start = endingSelection().start(); + Position end = endingSelection().end(); + + if (start.anchorNode() != end.anchorNode() || !start.anchorNode()->isTextNode()) + return false; + + replaceTextInNode(static_cast<Text*>(start.anchorNode()), start.offsetInContainerNode(), end.offsetInContainerNode() - start.offsetInContainerNode(), text); + + end = Position(start.anchorNode(), start.offsetInContainerNode() + text.length()); + + VisibleSelection selectionAfterReplace(m_selectReplacement ? start : end, end); + + setEndingSelection(selectionAfterReplace); + + return true; +} + +} // namespace WebCore diff --git a/Source/WebCore/editing/ReplaceSelectionCommand.h b/Source/WebCore/editing/ReplaceSelectionCommand.h new file mode 100644 index 0000000..9fc4a49 --- /dev/null +++ b/Source/WebCore/editing/ReplaceSelectionCommand.h @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2005, 2006, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef ReplaceSelectionCommand_h +#define ReplaceSelectionCommand_h + +#include "CompositeEditCommand.h" + +namespace WebCore { + +class DocumentFragment; +class EditingStyle; +class ReplacementFragment; + +class ReplaceSelectionCommand : public CompositeEditCommand { +public: + static PassRefPtr<ReplaceSelectionCommand> create(Document* document, PassRefPtr<DocumentFragment> fragment, + bool selectReplacement = true, bool smartReplace = false, bool matchStyle = false, bool preventNesting = true, bool movingParagraph = false, + EditAction action = EditActionPaste) + { + return adoptRef(new ReplaceSelectionCommand(document, fragment, selectReplacement, smartReplace, matchStyle, preventNesting, movingParagraph, action)); + } + +private: + ReplaceSelectionCommand(Document*, PassRefPtr<DocumentFragment>, + bool selectReplacement, bool smartReplace, bool matchStyle, bool preventNesting, bool movingParagraph, EditAction); + + virtual void doApply(); + virtual EditAction editingAction() const; + + void completeHTMLReplacement(const Position& lastPositionToSelect); + + void insertNodeAfterAndUpdateNodesInserted(PassRefPtr<Node> insertChild, Node* refChild); + void insertNodeAtAndUpdateNodesInserted(PassRefPtr<Node>, const Position&); + void insertNodeBeforeAndUpdateNodesInserted(PassRefPtr<Node> insertChild, Node* refChild); + Node* insertAsListItems(PassRefPtr<Node>, Node* insertionNode, const Position&); + + void updateNodesInserted(Node*); + bool shouldRemoveEndBR(Node*, const VisiblePosition&); + + bool shouldMergeStart(bool, bool, bool); + bool shouldMergeEnd(bool selectEndWasEndOfParagraph); + bool shouldMerge(const VisiblePosition&, const VisiblePosition&); + + void mergeEndIfNeeded(); + + void removeUnrenderedTextNodesAtEnds(); + + void negateStyleRulesThatAffectAppearance(); + void handleStyleSpans(); + void copyStyleToChildren(Node* parentNode, const CSSMutableStyleDeclaration* parentStyle); + void handlePasteAsQuotationNode(); + + virtual void removeNodePreservingChildren(Node*); + virtual void removeNodeAndPruneAncestors(Node*); + + VisiblePosition positionAtStartOfInsertedContent(); + VisiblePosition positionAtEndOfInsertedContent(); + + bool performTrivialReplace(const ReplacementFragment&); + + RefPtr<Node> m_firstNodeInserted; + RefPtr<Node> m_lastLeafInserted; + RefPtr<EditingStyle> m_insertionStyle; + bool m_selectReplacement; + bool m_smartReplace; + bool m_matchStyle; + RefPtr<DocumentFragment> m_documentFragment; + bool m_preventNesting; + bool m_movingParagraph; + EditAction m_editAction; + bool m_shouldMergeEnd; +}; + +} // namespace WebCore + +#endif // ReplaceSelectionCommand_h diff --git a/Source/WebCore/editing/SelectionController.cpp b/Source/WebCore/editing/SelectionController.cpp new file mode 100644 index 0000000..eca0711 --- /dev/null +++ b/Source/WebCore/editing/SelectionController.cpp @@ -0,0 +1,1802 @@ +/* + * Copyright (C) 2004, 2008, 2009, 2010 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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 "SelectionController.h" + +#include "CharacterData.h" +#include "DeleteSelectionCommand.h" +#include "Document.h" +#include "Editor.h" +#include "EditorClient.h" +#include "Element.h" +#include "EventHandler.h" +#include "ExceptionCode.h" +#include "FloatQuad.h" +#include "FocusController.h" +#include "Frame.h" +#include "FrameTree.h" +#include "FrameView.h" +#include "GraphicsContext.h" +#include "HTMLFormElement.h" +#include "HTMLFrameElementBase.h" +#include "HTMLInputElement.h" +#include "HTMLNames.h" +#include "HitTestRequest.h" +#include "HitTestResult.h" +#include "Page.h" +#include "Range.h" +#include "RenderLayer.h" +#include "RenderTextControl.h" +#include "RenderTheme.h" +#include "RenderView.h" +#include "RenderWidget.h" +#include "SecureTextInput.h" +#include "Settings.h" +#include "TextIterator.h" +#include "TypingCommand.h" +#include "htmlediting.h" +#include "visible_units.h" +#ifdef ANDROID_ALLOW_TURNING_OFF_CARET +#include "WebViewCore.h" +#endif +#include <stdio.h> +#include <wtf/text/CString.h> + +#define EDIT_DEBUG 0 + +namespace WebCore { + +using namespace HTMLNames; + +const int NoXPosForVerticalArrowNavigation = INT_MIN; + +SelectionController::SelectionController(Frame* frame, bool isDragCaretController) + : m_frame(frame) + , m_xPosForVerticalArrowNavigation(NoXPosForVerticalArrowNavigation) + , m_granularity(CharacterGranularity) + , m_caretBlinkTimer(this, &SelectionController::caretBlinkTimerFired) + , m_caretRectNeedsUpdate(true) + , m_absCaretBoundsDirty(true) + , m_isDragCaretController(isDragCaretController) + , m_isCaretBlinkingSuspended(false) + , m_focused(frame && frame->page() && frame->page()->focusController()->focusedFrame() == frame) + , m_caretVisible(isDragCaretController) + , m_caretPaint(true) +{ + setIsDirectional(false); +} + +void SelectionController::moveTo(const VisiblePosition &pos, bool userTriggered, CursorAlignOnScroll align) +{ + setSelection(VisibleSelection(pos.deepEquivalent(), pos.deepEquivalent(), pos.affinity()), true, true, userTriggered, align); +} + +void SelectionController::moveTo(const VisiblePosition &base, const VisiblePosition &extent, bool userTriggered) +{ + setSelection(VisibleSelection(base.deepEquivalent(), extent.deepEquivalent(), base.affinity()), true, true, userTriggered); +} + +void SelectionController::moveTo(const Position &pos, EAffinity affinity, bool userTriggered) +{ + setSelection(VisibleSelection(pos, affinity), true, true, userTriggered); +} + +void SelectionController::moveTo(const Range *r, EAffinity affinity, bool userTriggered) +{ + VisibleSelection selection = r ? VisibleSelection(r->startPosition(), r->endPosition(), affinity) : VisibleSelection(Position(), Position(), affinity); + setSelection(selection, true, true, userTriggered); +} + +void SelectionController::moveTo(const Position &base, const Position &extent, EAffinity affinity, bool userTriggered) +{ + setSelection(VisibleSelection(base, extent, affinity), true, true, userTriggered); +} + +void SelectionController::setSelection(const VisibleSelection& s, bool closeTyping, bool shouldClearTypingStyle, bool userTriggered, CursorAlignOnScroll align, TextGranularity granularity, DirectionalityPolicy directionalityPolicy) +{ + m_granularity = granularity; + + setIsDirectional(directionalityPolicy == MakeDirectionalSelection); + + if (m_isDragCaretController) { + invalidateCaretRect(); + m_selection = s; + m_caretRectNeedsUpdate = true; + invalidateCaretRect(); + updateCaretRect(); + return; + } + if (!m_frame) { + m_selection = s; + return; + } + + Node* baseNode = s.base().node(); + Document* document = 0; + if (baseNode) + document = baseNode->document(); + + // <http://bugs.webkit.org/show_bug.cgi?id=23464>: Infinite recursion at SelectionController::setSelection + // if document->frame() == m_frame we can get into an infinite loop + if (document && document->frame() && document->frame() != m_frame && document != m_frame->document()) { + document->frame()->selection()->setSelection(s, closeTyping, shouldClearTypingStyle, userTriggered); + return; + } + + if (closeTyping) + TypingCommand::closeTyping(m_frame->editor()->lastEditCommand()); + + if (shouldClearTypingStyle) + clearTypingStyle(); + + if (m_selection == s) + return; + + VisibleSelection oldSelection = m_selection; + + m_selection = s; + + m_caretRectNeedsUpdate = true; + + if (!s.isNone()) + setFocusedNodeIfNeeded(); + + updateAppearance(); + + // Always clear the x position used for vertical arrow navigation. + // It will be restored by the vertical arrow navigation code if necessary. + m_xPosForVerticalArrowNavigation = NoXPosForVerticalArrowNavigation; + selectFrameElementInParentIfFullySelected(); + notifyRendererOfSelectionChange(userTriggered); + m_frame->editor()->respondToChangedSelection(oldSelection, closeTyping); + if (userTriggered) { + ScrollAlignment alignment; + + if (m_frame->editor()->behavior().shouldCenterAlignWhenSelectionIsRevealed()) + alignment = (align == AlignCursorOnScrollAlways) ? ScrollAlignment::alignCenterAlways : ScrollAlignment::alignCenterIfNeeded; + else + alignment = (align == AlignCursorOnScrollAlways) ? ScrollAlignment::alignTopAlways : ScrollAlignment::alignToEdgeIfNeeded; + + revealSelection(alignment, true); + } + + notifyAccessibilityForSelectionChange(); +} + +static bool removingNodeRemovesPosition(Node* node, const Position& position) +{ + if (!position.node()) + return false; + + if (position.node() == node) + return true; + + if (!node->isElementNode()) + return false; + + Element* element = static_cast<Element*>(node); + return element->contains(position.node()) || element->contains(position.node()->shadowAncestorNode()); +} + +void SelectionController::nodeWillBeRemoved(Node *node) +{ + if (isNone()) + return; + + // There can't be a selection inside a fragment, so if a fragment's node is being removed, + // the selection in the document that created the fragment needs no adjustment. + if (node && highestAncestor(node)->nodeType() == Node::DOCUMENT_FRAGMENT_NODE) + return; + + respondToNodeModification(node, removingNodeRemovesPosition(node, m_selection.base()), removingNodeRemovesPosition(node, m_selection.extent()), + removingNodeRemovesPosition(node, m_selection.start()), removingNodeRemovesPosition(node, m_selection.end())); +} + +void SelectionController::respondToNodeModification(Node* node, bool baseRemoved, bool extentRemoved, bool startRemoved, bool endRemoved) +{ + bool clearRenderTreeSelection = false; + bool clearDOMTreeSelection = false; + + if (startRemoved || endRemoved) { + // FIXME: When endpoints are removed, we should just alter the selection, instead of blowing it away. + clearRenderTreeSelection = true; + clearDOMTreeSelection = true; + } else if (baseRemoved || extentRemoved) { + // The base and/or extent are about to be removed, but the start and end aren't. + // Change the base and extent to the start and end, but don't re-validate the + // selection, since doing so could move the start and end into the node + // that is about to be removed. + if (m_selection.isBaseFirst()) + m_selection.setWithoutValidation(m_selection.start(), m_selection.end()); + else + m_selection.setWithoutValidation(m_selection.end(), m_selection.start()); + } else if (m_selection.firstRange()) { + ExceptionCode ec = 0; + Range::CompareResults compareResult = m_selection.firstRange()->compareNode(node, ec); + if (!ec && (compareResult == Range::NODE_BEFORE_AND_AFTER || compareResult == Range::NODE_INSIDE)) { + // If we did nothing here, when this node's renderer was destroyed, the rect that it + // occupied would be invalidated, but, selection gaps that change as a result of + // the removal wouldn't be invalidated. + // FIXME: Don't do so much unnecessary invalidation. + clearRenderTreeSelection = true; + } + } + + if (clearRenderTreeSelection) { + RefPtr<Document> document = m_selection.start().node()->document(); + document->updateStyleIfNeeded(); + if (RenderView* view = toRenderView(document->renderer())) + view->clearSelection(); + } + + if (clearDOMTreeSelection) + setSelection(VisibleSelection(), false, false); +} + +enum EndPointType { EndPointIsStart, EndPointIsEnd }; + +static bool shouldRemovePositionAfterAdoptingTextReplacement(Position& position, EndPointType type, CharacterData* node, unsigned offset, unsigned oldLength, unsigned newLength) +{ + if (!position.anchorNode() || position.anchorNode() != node || position.anchorType() != Position::PositionIsOffsetInAnchor) + return false; + + if (static_cast<unsigned>(position.offsetInContainerNode()) > offset && static_cast<unsigned>(position.offsetInContainerNode()) < offset + oldLength) + return true; + + if ((type == EndPointIsStart && static_cast<unsigned>(position.offsetInContainerNode()) >= offset + oldLength) + || (type == EndPointIsEnd && static_cast<unsigned>(position.offsetInContainerNode()) > offset + oldLength)) + position.moveToOffset(position.offsetInContainerNode() - oldLength + newLength); + + return false; +} + +void SelectionController::textWillBeReplaced(CharacterData* node, unsigned offset, unsigned oldLength, unsigned newLength) +{ + // The fragment check is a performance optimization. See http://trac.webkit.org/changeset/30062. + if (isNone() || !node || highestAncestor(node)->nodeType() == Node::DOCUMENT_FRAGMENT_NODE) + return; + + Position base = m_selection.base(); + Position extent = m_selection.extent(); + Position start = m_selection.start(); + Position end = m_selection.end(); + bool shouldRemoveBase = shouldRemovePositionAfterAdoptingTextReplacement(base, m_selection.isBaseFirst() ? EndPointIsStart : EndPointIsEnd, node, offset, oldLength, newLength); + bool shouldRemoveExtent = shouldRemovePositionAfterAdoptingTextReplacement(extent, m_selection.isBaseFirst() ? EndPointIsEnd : EndPointIsStart, node, offset, oldLength, newLength); + bool shouldRemoveStart = shouldRemovePositionAfterAdoptingTextReplacement(start, EndPointIsStart, node, offset, oldLength, newLength); + bool shouldRemoveEnd = shouldRemovePositionAfterAdoptingTextReplacement(end, EndPointIsEnd, node, offset, oldLength, newLength); + + if ((base != m_selection.base() || extent != m_selection.extent() || start != m_selection.start() || end != m_selection.end()) + && !shouldRemoveStart && !shouldRemoveEnd) { + if (!shouldRemoveBase && !shouldRemoveExtent) + m_selection.setWithoutValidation(base, extent); + else { + if (m_selection.isBaseFirst()) + m_selection.setWithoutValidation(m_selection.start(), m_selection.end()); + else + m_selection.setWithoutValidation(m_selection.end(), m_selection.start()); + } + } + + respondToNodeModification(node, shouldRemoveBase, shouldRemoveExtent, shouldRemoveStart, shouldRemoveEnd); +} + +void SelectionController::setIsDirectional(bool isDirectional) +{ + m_isDirectional = !m_frame || m_frame->editor()->behavior().shouldConsiderSelectionAsDirectional() || isDirectional; +} + +void SelectionController::willBeModified(EAlteration alter, SelectionDirection direction) +{ + if (alter != AlterationExtend) + return; + + Position start = m_selection.start(); + Position end = m_selection.end(); + + if (m_isDirectional) { + // Make base and extent match start and end so we extend the user-visible selection. + // This only matters for cases where base and extend point to different positions than + // start and end (e.g. after a double-click to select a word). + if (m_selection.isBaseFirst()) { + m_selection.setBase(start); + m_selection.setExtent(end); + } else { + m_selection.setBase(end); + m_selection.setExtent(start); + } + } else { + // FIXME: This is probably not correct for right and left when the direction is RTL. + switch (direction) { + case DirectionRight: + case DirectionForward: + m_selection.setBase(start); + m_selection.setExtent(end); + break; + case DirectionLeft: + case DirectionBackward: + m_selection.setBase(end); + m_selection.setExtent(start); + break; + } + } +} + +TextDirection SelectionController::directionOfEnclosingBlock() +{ + Node* enclosingBlockNode = enclosingBlock(m_selection.extent().node()); + if (!enclosingBlockNode) + return LTR; + RenderObject* renderer = enclosingBlockNode->renderer(); + if (renderer) + return renderer->style()->direction(); + return LTR; +} + +VisiblePosition SelectionController::positionForPlatform(bool isGetStart) const +{ + Settings* settings = m_frame ? m_frame->settings() : 0; + if (settings && settings->editingBehaviorType() == EditingMacBehavior) + return isGetStart ? m_selection.visibleStart() : m_selection.visibleEnd(); + // Linux and Windows always extend selections from the extent endpoint. + // FIXME: VisibleSelection should be fixed to ensure as an invariant that + // base/extent always point to the same nodes as start/end, but which points + // to which depends on the value of isBaseFirst. Then this can be changed + // to just return m_sel.extent(). + return m_selection.isBaseFirst() ? m_selection.visibleEnd() : m_selection.visibleStart(); +} + +VisiblePosition SelectionController::startForPlatform() const +{ + return positionForPlatform(true); +} + +VisiblePosition SelectionController::endForPlatform() const +{ + return positionForPlatform(false); +} + +VisiblePosition SelectionController::modifyExtendingRight(TextGranularity granularity) +{ + VisiblePosition pos(m_selection.extent(), m_selection.affinity()); + + // The difference between modifyExtendingRight and modifyExtendingForward is: + // modifyExtendingForward always extends forward logically. + // modifyExtendingRight behaves the same as modifyExtendingForward except for extending character or word, + // it extends forward logically if the enclosing block is LTR direction, + // but it extends backward logically if the enclosing block is RTL direction. + switch (granularity) { + case CharacterGranularity: + if (directionOfEnclosingBlock() == LTR) + pos = pos.next(true); + else + pos = pos.previous(true); + break; + case WordGranularity: + if (directionOfEnclosingBlock() == LTR) + pos = nextWordPosition(pos); + else + pos = previousWordPosition(pos); + break; + case SentenceGranularity: + case LineGranularity: + case ParagraphGranularity: + case SentenceBoundary: + case LineBoundary: + case ParagraphBoundary: + case DocumentBoundary: + // FIXME: implement all of the above? + pos = modifyExtendingForward(granularity); + } + return pos; +} + +VisiblePosition SelectionController::modifyExtendingForward(TextGranularity granularity) +{ + VisiblePosition pos(m_selection.extent(), m_selection.affinity()); + switch (granularity) { + case CharacterGranularity: + pos = pos.next(true); + break; + case WordGranularity: + pos = nextWordPosition(pos); + break; + case SentenceGranularity: + pos = nextSentencePosition(pos); + break; + case LineGranularity: + pos = nextLinePosition(pos, xPosForVerticalArrowNavigation(EXTENT)); + break; + case ParagraphGranularity: + pos = nextParagraphPosition(pos, xPosForVerticalArrowNavigation(EXTENT)); + break; + case SentenceBoundary: + pos = endOfSentence(endForPlatform()); + break; + case LineBoundary: + pos = logicalEndOfLine(endForPlatform()); + break; + case ParagraphBoundary: + pos = endOfParagraph(endForPlatform()); + break; + case DocumentBoundary: + pos = endForPlatform(); + if (isEditablePosition(pos.deepEquivalent())) + pos = endOfEditableContent(pos); + else + pos = endOfDocument(pos); + break; + } + + return pos; +} + +VisiblePosition SelectionController::modifyMovingRight(TextGranularity granularity) +{ + VisiblePosition pos; + switch (granularity) { + case CharacterGranularity: + if (isRange()) + pos = VisiblePosition(m_selection.end(), m_selection.affinity()); + else + pos = VisiblePosition(m_selection.extent(), m_selection.affinity()).right(true); + break; + case WordGranularity: + case SentenceGranularity: + case LineGranularity: + case ParagraphGranularity: + case SentenceBoundary: + case ParagraphBoundary: + case DocumentBoundary: + // FIXME: Implement all of the above. + pos = modifyMovingForward(granularity); + break; + case LineBoundary: + pos = rightBoundaryOfLine(startForPlatform(), directionOfEnclosingBlock()); + break; + } + return pos; +} + +VisiblePosition SelectionController::modifyMovingForward(TextGranularity granularity) +{ + VisiblePosition pos; + // FIXME: Stay in editable content for the less common granularities. + switch (granularity) { + case CharacterGranularity: + if (isRange()) + pos = VisiblePosition(m_selection.end(), m_selection.affinity()); + else + pos = VisiblePosition(m_selection.extent(), m_selection.affinity()).next(true); + break; + case WordGranularity: + pos = nextWordPosition(VisiblePosition(m_selection.extent(), m_selection.affinity())); + break; + case SentenceGranularity: + pos = nextSentencePosition(VisiblePosition(m_selection.extent(), m_selection.affinity())); + break; + case LineGranularity: { + // down-arrowing from a range selection that ends at the start of a line needs + // to leave the selection at that line start (no need to call nextLinePosition!) + pos = endForPlatform(); + if (!isRange() || !isStartOfLine(pos)) + pos = nextLinePosition(pos, xPosForVerticalArrowNavigation(START)); + break; + } + case ParagraphGranularity: + pos = nextParagraphPosition(endForPlatform(), xPosForVerticalArrowNavigation(START)); + break; + case SentenceBoundary: + pos = endOfSentence(endForPlatform()); + break; + case LineBoundary: + pos = logicalEndOfLine(endForPlatform()); + break; + case ParagraphBoundary: + pos = endOfParagraph(endForPlatform()); + break; + case DocumentBoundary: + pos = endForPlatform(); + if (isEditablePosition(pos.deepEquivalent())) + pos = endOfEditableContent(pos); + else + pos = endOfDocument(pos); + break; + } + return pos; +} + +VisiblePosition SelectionController::modifyExtendingLeft(TextGranularity granularity) +{ + VisiblePosition pos(m_selection.extent(), m_selection.affinity()); + + // The difference between modifyExtendingLeft and modifyExtendingBackward is: + // modifyExtendingBackward always extends backward logically. + // modifyExtendingLeft behaves the same as modifyExtendingBackward except for extending character or word, + // it extends backward logically if the enclosing block is LTR direction, + // but it extends forward logically if the enclosing block is RTL direction. + switch (granularity) { + case CharacterGranularity: + if (directionOfEnclosingBlock() == LTR) + pos = pos.previous(true); + else + pos = pos.next(true); + break; + case WordGranularity: + if (directionOfEnclosingBlock() == LTR) + pos = previousWordPosition(pos); + else + pos = nextWordPosition(pos); + break; + case SentenceGranularity: + case LineGranularity: + case ParagraphGranularity: + case SentenceBoundary: + case LineBoundary: + case ParagraphBoundary: + case DocumentBoundary: + pos = modifyExtendingBackward(granularity); + } + return pos; +} + +VisiblePosition SelectionController::modifyExtendingBackward(TextGranularity granularity) +{ + VisiblePosition pos(m_selection.extent(), m_selection.affinity()); + + // Extending a selection backward by word or character from just after a table selects + // the table. This "makes sense" from the user perspective, esp. when deleting. + // It was done here instead of in VisiblePosition because we want VPs to iterate + // over everything. + switch (granularity) { + case CharacterGranularity: + pos = pos.previous(true); + break; + case WordGranularity: + pos = previousWordPosition(pos); + break; + case SentenceGranularity: + pos = previousSentencePosition(pos); + break; + case LineGranularity: + pos = previousLinePosition(pos, xPosForVerticalArrowNavigation(EXTENT)); + break; + case ParagraphGranularity: + pos = previousParagraphPosition(pos, xPosForVerticalArrowNavigation(EXTENT)); + break; + case SentenceBoundary: + pos = startOfSentence(startForPlatform()); + break; + case LineBoundary: + pos = logicalStartOfLine(startForPlatform()); + break; + case ParagraphBoundary: + pos = startOfParagraph(startForPlatform()); + break; + case DocumentBoundary: + pos = startForPlatform(); + if (isEditablePosition(pos.deepEquivalent())) + pos = startOfEditableContent(pos); + else + pos = startOfDocument(pos); + break; + } + return pos; +} + +VisiblePosition SelectionController::modifyMovingLeft(TextGranularity granularity) +{ + VisiblePosition pos; + switch (granularity) { + case CharacterGranularity: + if (isRange()) + pos = VisiblePosition(m_selection.start(), m_selection.affinity()); + else + pos = VisiblePosition(m_selection.extent(), m_selection.affinity()).left(true); + break; + case WordGranularity: + case SentenceGranularity: + case LineGranularity: + case ParagraphGranularity: + case SentenceBoundary: + case ParagraphBoundary: + case DocumentBoundary: + // FIXME: Implement all of the above. + pos = modifyMovingBackward(granularity); + break; + case LineBoundary: + pos = leftBoundaryOfLine(startForPlatform(), directionOfEnclosingBlock()); + break; + } + return pos; +} + +VisiblePosition SelectionController::modifyMovingBackward(TextGranularity granularity) +{ + VisiblePosition pos; + switch (granularity) { + case CharacterGranularity: + if (isRange()) + pos = VisiblePosition(m_selection.start(), m_selection.affinity()); + else + pos = VisiblePosition(m_selection.extent(), m_selection.affinity()).previous(true); + break; + case WordGranularity: + pos = previousWordPosition(VisiblePosition(m_selection.extent(), m_selection.affinity())); + break; + case SentenceGranularity: + pos = previousSentencePosition(VisiblePosition(m_selection.extent(), m_selection.affinity())); + break; + case LineGranularity: + pos = previousLinePosition(startForPlatform(), xPosForVerticalArrowNavigation(START)); + break; + case ParagraphGranularity: + pos = previousParagraphPosition(startForPlatform(), xPosForVerticalArrowNavigation(START)); + break; + case SentenceBoundary: + pos = startOfSentence(startForPlatform()); + break; + case LineBoundary: + pos = logicalStartOfLine(startForPlatform()); + break; + case ParagraphBoundary: + pos = startOfParagraph(startForPlatform()); + break; + case DocumentBoundary: + pos = startForPlatform(); + if (isEditablePosition(pos.deepEquivalent())) + pos = startOfEditableContent(pos); + else + pos = startOfDocument(pos); + break; + } + return pos; +} + +static bool isBoundary(TextGranularity granularity) +{ + return granularity == LineBoundary || granularity == ParagraphBoundary || granularity == DocumentBoundary; +} + +bool SelectionController::modify(EAlteration alter, SelectionDirection direction, TextGranularity granularity, bool userTriggered) +{ + if (userTriggered) { + SelectionController trialSelectionController; + trialSelectionController.setSelection(m_selection); + trialSelectionController.setIsDirectional(m_isDirectional); + trialSelectionController.modify(alter, direction, granularity, false); + + bool change = shouldChangeSelection(trialSelectionController.selection()); + if (!change) + return false; + } + + willBeModified(alter, direction); + + bool wasRange = m_selection.isRange(); + Position originalStartPosition = m_selection.start(); + VisiblePosition position; + switch (direction) { + case DirectionRight: + if (alter == AlterationMove) + position = modifyMovingRight(granularity); + else + position = modifyExtendingRight(granularity); + break; + case DirectionForward: + if (alter == AlterationExtend) + position = modifyExtendingForward(granularity); + else + position = modifyMovingForward(granularity); + break; + case DirectionLeft: + if (alter == AlterationMove) + position = modifyMovingLeft(granularity); + else + position = modifyExtendingLeft(granularity); + break; + case DirectionBackward: + if (alter == AlterationExtend) + position = modifyExtendingBackward(granularity); + else + position = modifyMovingBackward(granularity); + break; + } + + if (position.isNull()) + return false; + + if (isSpatialNavigationEnabled(m_frame)) + if (!wasRange && alter == AlterationMove && position == originalStartPosition) + return false; + + // Some of the above operations set an xPosForVerticalArrowNavigation. + // Setting a selection will clear it, so save it to possibly restore later. + // Note: the START position type is arbitrary because it is unused, it would be + // the requested position type if there were no xPosForVerticalArrowNavigation set. + int x = xPosForVerticalArrowNavigation(START); + + switch (alter) { + case AlterationMove: + moveTo(position, userTriggered); + break; + case AlterationExtend: + // Standard Mac behavior when extending to a boundary is grow the selection rather than leaving the + // base in place and moving the extent. Matches NSTextView. + if (!m_frame || !m_frame->editor()->behavior().shouldAlwaysGrowSelectionWhenExtendingToBoundary() || m_selection.isCaret() || !isBoundary(granularity)) + setExtent(position, userTriggered); + else { + if (direction == DirectionForward || direction == DirectionRight) + setEnd(position, userTriggered); + else + setStart(position, userTriggered); + } + break; + } + + if (granularity == LineGranularity || granularity == ParagraphGranularity) + m_xPosForVerticalArrowNavigation = x; + + if (userTriggered) + m_granularity = CharacterGranularity; + + + setCaretRectNeedsUpdate(); + + setIsDirectional(alter == AlterationExtend); + + return true; +} + +// FIXME: Maybe baseline would be better? +static bool absoluteCaretY(const VisiblePosition &c, int &y) +{ + IntRect rect = c.absoluteCaretBounds(); + if (rect.isEmpty()) + return false; + y = rect.y() + rect.height() / 2; + return true; +} + +bool SelectionController::modify(EAlteration alter, int verticalDistance, bool userTriggered, CursorAlignOnScroll align) +{ + if (!verticalDistance) + return false; + + if (userTriggered) { + SelectionController trialSelectionController; + trialSelectionController.setSelection(m_selection); + trialSelectionController.setIsDirectional(m_isDirectional); + trialSelectionController.modify(alter, verticalDistance, false); + + bool change = shouldChangeSelection(trialSelectionController.selection()); + if (!change) + return false; + } + + bool up = verticalDistance < 0; + if (up) + verticalDistance = -verticalDistance; + + willBeModified(alter, up ? DirectionBackward : DirectionForward); + + VisiblePosition pos; + int xPos = 0; + switch (alter) { + case AlterationMove: + pos = VisiblePosition(up ? m_selection.start() : m_selection.end(), m_selection.affinity()); + xPos = xPosForVerticalArrowNavigation(up ? START : END); + m_selection.setAffinity(up ? UPSTREAM : DOWNSTREAM); + break; + case AlterationExtend: + pos = VisiblePosition(m_selection.extent(), m_selection.affinity()); + xPos = xPosForVerticalArrowNavigation(EXTENT); + m_selection.setAffinity(DOWNSTREAM); + break; + } + + int startY; + if (!absoluteCaretY(pos, startY)) + return false; + if (up) + startY = -startY; + int lastY = startY; + + VisiblePosition result; + VisiblePosition next; + for (VisiblePosition p = pos; ; p = next) { + next = (up ? previousLinePosition : nextLinePosition)(p, xPos); + if (next.isNull() || next == p) + break; + int nextY; + if (!absoluteCaretY(next, nextY)) + break; + if (up) + nextY = -nextY; + if (nextY - startY > verticalDistance) + break; + if (nextY >= lastY) { + lastY = nextY; + result = next; + } + } + + if (result.isNull()) + return false; + + switch (alter) { + case AlterationMove: + moveTo(result, userTriggered, align); + break; + case AlterationExtend: + setExtent(result, userTriggered); + break; + } + + if (userTriggered) + m_granularity = CharacterGranularity; + + setIsDirectional(alter == AlterationExtend); + + return true; +} + +int SelectionController::xPosForVerticalArrowNavigation(EPositionType type) +{ + int x = 0; + + if (isNone()) + return x; + + Position pos; + switch (type) { + case START: + pos = m_selection.start(); + break; + case END: + pos = m_selection.end(); + break; + case BASE: + pos = m_selection.base(); + break; + case EXTENT: + pos = m_selection.extent(); + break; + } + + Frame* frame = pos.node()->document()->frame(); + if (!frame) + return x; + + if (m_xPosForVerticalArrowNavigation == NoXPosForVerticalArrowNavigation) { + VisiblePosition visiblePosition(pos, m_selection.affinity()); + // VisiblePosition creation can fail here if a node containing the selection becomes visibility:hidden + // after the selection is created and before this function is called. + x = visiblePosition.isNotNull() ? visiblePosition.xOffsetForVerticalNavigation() : 0; + m_xPosForVerticalArrowNavigation = x; + } else + x = m_xPosForVerticalArrowNavigation; + + return x; +} + +void SelectionController::clear() +{ + m_granularity = CharacterGranularity; + setSelection(VisibleSelection()); +} + +void SelectionController::setStart(const VisiblePosition &pos, bool userTriggered) +{ + if (m_selection.isBaseFirst()) + setBase(pos, userTriggered); + else + setExtent(pos, userTriggered); +} + +void SelectionController::setEnd(const VisiblePosition &pos, bool userTriggered) +{ + if (m_selection.isBaseFirst()) + setExtent(pos, userTriggered); + else + setBase(pos, userTriggered); +} + +void SelectionController::setBase(const VisiblePosition &pos, bool userTriggered) +{ + setSelection(VisibleSelection(pos.deepEquivalent(), m_selection.extent(), pos.affinity()), true, true, userTriggered); +} + +void SelectionController::setExtent(const VisiblePosition &pos, bool userTriggered) +{ + setSelection(VisibleSelection(m_selection.base(), pos.deepEquivalent(), pos.affinity()), true, true, userTriggered); +} + +void SelectionController::setBase(const Position &pos, EAffinity affinity, bool userTriggered) +{ + setSelection(VisibleSelection(pos, m_selection.extent(), affinity), true, true, userTriggered); +} + +void SelectionController::setExtent(const Position &pos, EAffinity affinity, bool userTriggered) +{ + setSelection(VisibleSelection(m_selection.base(), pos, affinity), true, true, userTriggered); +} + +void SelectionController::setCaretRectNeedsUpdate(bool flag) +{ + m_caretRectNeedsUpdate = flag; +} + +void SelectionController::updateCaretRect() +{ + if (isNone() || !m_selection.start().node()->inDocument() || !m_selection.end().node()->inDocument()) { + m_caretRect = IntRect(); + return; + } + + m_selection.start().node()->document()->updateStyleIfNeeded(); + + m_caretRect = IntRect(); + + if (isCaret()) { + VisiblePosition pos(m_selection.start(), m_selection.affinity()); + if (pos.isNotNull()) { + ASSERT(pos.deepEquivalent().node()->renderer()); + + // First compute a rect local to the renderer at the selection start + RenderObject* renderer; + IntRect localRect = pos.localCaretRect(renderer); + + // Get the renderer that will be responsible for painting the caret (which + // is either the renderer we just found, or one of its containers) + RenderObject* caretPainter = caretRenderer(); + + // Compute an offset between the renderer and the caretPainter + bool unrooted = false; + while (renderer != caretPainter) { + RenderObject* containerObject = renderer->container(); + if (!containerObject) { + unrooted = true; + break; + } + localRect.move(renderer->offsetFromContainer(containerObject, localRect.location())); + renderer = containerObject; + } + + if (!unrooted) + m_caretRect = localRect; + + m_absCaretBoundsDirty = true; + } + } + + m_caretRectNeedsUpdate = false; +} + +RenderObject* SelectionController::caretRenderer() const +{ + Node* node = m_selection.start().node(); + if (!node) + return 0; + + RenderObject* renderer = node->renderer(); + if (!renderer) + return 0; + + // if caretNode is a block and caret is inside it then caret should be painted by that block + bool paintedByBlock = renderer->isBlockFlow() && caretRendersInsideNode(node); + return paintedByBlock ? renderer : renderer->containingBlock(); +} + +IntRect SelectionController::localCaretRect() +{ + if (m_caretRectNeedsUpdate) + updateCaretRect(); + + return m_caretRect; +} + +IntRect SelectionController::absoluteBoundsForLocalRect(const IntRect& rect) const +{ + RenderObject* caretPainter = caretRenderer(); + if (!caretPainter) + return IntRect(); + + IntRect localRect(rect); + if (caretPainter->isBox()) + toRenderBox(caretPainter)->flipForWritingMode(localRect); + return caretPainter->localToAbsoluteQuad(FloatRect(localRect)).enclosingBoundingBox(); +} + +IntRect SelectionController::absoluteCaretBounds() +{ + recomputeCaretRect(); + return m_absCaretBounds; +} + +static IntRect repaintRectForCaret(IntRect caret) +{ + if (caret.isEmpty()) + return IntRect(); + // Ensure that the dirty rect intersects the block that paints the caret even in the case where + // the caret itself is just outside the block. See <https://bugs.webkit.org/show_bug.cgi?id=19086>. + caret.inflateX(1); + return caret; +} + +IntRect SelectionController::caretRepaintRect() const +{ + return absoluteBoundsForLocalRect(repaintRectForCaret(localCaretRectForPainting())); +} + +bool SelectionController::recomputeCaretRect() +{ + if (!m_caretRectNeedsUpdate) + return false; + + if (!m_frame) + return false; + + FrameView* v = m_frame->document()->view(); + if (!v) + return false; + + IntRect oldRect = m_caretRect; + IntRect newRect = localCaretRect(); + if (oldRect == newRect && !m_absCaretBoundsDirty) + return false; + + IntRect oldAbsCaretBounds = m_absCaretBounds; + // FIXME: Rename m_caretRect to m_localCaretRect. + m_absCaretBounds = absoluteBoundsForLocalRect(m_caretRect); + m_absCaretBoundsDirty = false; + + if (oldAbsCaretBounds == m_absCaretBounds) + return false; + + IntRect oldAbsoluteCaretRepaintBounds = m_absoluteCaretRepaintBounds; + // We believe that we need to inflate the local rect before transforming it to obtain the repaint bounds. + m_absoluteCaretRepaintBounds = caretRepaintRect(); + +#if ENABLE(TEXT_CARET) + if (RenderView* view = toRenderView(m_frame->document()->renderer())) { + // FIXME: make caret repainting container-aware. + view->repaintRectangleInViewAndCompositedLayers(oldAbsoluteCaretRepaintBounds, false); + if (shouldRepaintCaret(view)) + view->repaintRectangleInViewAndCompositedLayers(m_absoluteCaretRepaintBounds, false); + } +#endif + return true; +} + +bool SelectionController::shouldRepaintCaret(const RenderView* view) const +{ + ASSERT(view); + Frame* frame = view->frameView() ? view->frameView()->frame() : 0; // The frame where the selection started. + bool caretBrowsing = frame && frame->settings() && frame->settings()->caretBrowsingEnabled(); + return (caretBrowsing || isContentEditable()); +} + +void SelectionController::invalidateCaretRect() +{ + if (!isCaret()) + return; + + Document* d = m_selection.start().node()->document(); + + // recomputeCaretRect will always return false for the drag caret, + // because its m_frame is always 0. + bool caretRectChanged = recomputeCaretRect(); + + // EDIT FIXME: This is an unfortunate hack. + // Basically, we can't trust this layout position since we + // can't guarantee that the check to see if we are in unrendered + // content will work at this point. We may have to wait for + // a layout and re-render of the document to happen. So, resetting this + // flag will cause another caret layout to happen the first time + // that we try to paint the caret after this call. That one will work since + // it happens after the document has accounted for any editing + // changes which may have been done. + // And, we need to leave this layout here so the caret moves right + // away after clicking. + m_caretRectNeedsUpdate = true; + + if (!caretRectChanged) { + RenderView* view = toRenderView(d->renderer()); + if (view && shouldRepaintCaret(view)) + view->repaintRectangleInViewAndCompositedLayers(caretRepaintRect(), false); + } +} + +void SelectionController::paintCaret(GraphicsContext* context, int tx, int ty, const IntRect& clipRect) +{ +#ifdef ANDROID_ALLOW_TURNING_OFF_CARET + if (m_frame && !android::WebViewCore::getWebViewCore(m_frame->view())->shouldPaintCaret()) + return; +#endif +#if ENABLE(TEXT_CARET) + if (!m_caretVisible) + return; + if (!m_caretPaint) + return; + if (!m_selection.isCaret()) + return; + + IntRect drawingRect = localCaretRectForPainting(); + if (caretRenderer() && caretRenderer()->isBox()) + toRenderBox(caretRenderer())->flipForWritingMode(drawingRect); + drawingRect.move(tx, ty); + IntRect caret = intersection(drawingRect, clipRect); + if (caret.isEmpty()) + return; + + Color caretColor = Color::black; + ColorSpace colorSpace = ColorSpaceDeviceRGB; + Element* element = rootEditableElement(); + if (element && element->renderer()) { + caretColor = element->renderer()->style()->visitedDependentColor(CSSPropertyColor); + colorSpace = element->renderer()->style()->colorSpace(); + } + + context->fillRect(caret, caretColor, colorSpace); +#else + UNUSED_PARAM(context); + UNUSED_PARAM(tx); + UNUSED_PARAM(ty); + UNUSED_PARAM(clipRect); +#endif +} + +void SelectionController::debugRenderer(RenderObject *r, bool selected) const +{ + if (r->node()->isElementNode()) { + Element* element = static_cast<Element *>(r->node()); + fprintf(stderr, "%s%s\n", selected ? "==> " : " ", element->localName().string().utf8().data()); + } else if (r->isText()) { + RenderText* textRenderer = toRenderText(r); + if (!textRenderer->textLength() || !textRenderer->firstTextBox()) { + fprintf(stderr, "%s#text (empty)\n", selected ? "==> " : " "); + return; + } + + static const int max = 36; + String text = textRenderer->text(); + int textLength = text.length(); + if (selected) { + int offset = 0; + if (r->node() == m_selection.start().node()) + offset = m_selection.start().deprecatedEditingOffset(); + else if (r->node() == m_selection.end().node()) + offset = m_selection.end().deprecatedEditingOffset(); + + int pos; + InlineTextBox* box = textRenderer->findNextInlineTextBox(offset, pos); + text = text.substring(box->start(), box->len()); + + String show; + int mid = max / 2; + int caret = 0; + + // text is shorter than max + if (textLength < max) { + show = text; + caret = pos; + } else if (pos - mid < 0) { + // too few characters to left + show = text.left(max - 3) + "..."; + caret = pos; + } else if (pos - mid >= 0 && pos + mid <= textLength) { + // enough characters on each side + show = "..." + text.substring(pos - mid + 3, max - 6) + "..."; + caret = mid; + } else { + // too few characters on right + show = "..." + text.right(max - 3); + caret = pos - (textLength - show.length()); + } + + show.replace('\n', ' '); + show.replace('\r', ' '); + fprintf(stderr, "==> #text : \"%s\" at offset %d\n", show.utf8().data(), pos); + fprintf(stderr, " "); + for (int i = 0; i < caret; i++) + fprintf(stderr, " "); + fprintf(stderr, "^\n"); + } else { + if ((int)text.length() > max) + text = text.left(max - 3) + "..."; + else + text = text.left(max); + fprintf(stderr, " #text : \"%s\"\n", text.utf8().data()); + } + } +} + +bool SelectionController::contains(const IntPoint& point) +{ + Document* document = m_frame->document(); + + // Treat a collapsed selection like no selection. + if (!isRange()) + return false; + if (!document->renderer()) + return false; + + HitTestRequest request(HitTestRequest::ReadOnly | + HitTestRequest::Active); + HitTestResult result(point); + document->renderView()->layer()->hitTest(request, result); + Node* innerNode = result.innerNode(); + if (!innerNode || !innerNode->renderer()) + return false; + + VisiblePosition visiblePos(innerNode->renderer()->positionForPoint(result.localPoint())); + if (visiblePos.isNull()) + return false; + + if (m_selection.visibleStart().isNull() || m_selection.visibleEnd().isNull()) + return false; + + Position start(m_selection.visibleStart().deepEquivalent()); + Position end(m_selection.visibleEnd().deepEquivalent()); + Position p(visiblePos.deepEquivalent()); + + return comparePositions(start, p) <= 0 && comparePositions(p, end) <= 0; +} + +// Workaround for the fact that it's hard to delete a frame. +// Call this after doing user-triggered selections to make it easy to delete the frame you entirely selected. +// Can't do this implicitly as part of every setSelection call because in some contexts it might not be good +// for the focus to move to another frame. So instead we call it from places where we are selecting with the +// mouse or the keyboard after setting the selection. +void SelectionController::selectFrameElementInParentIfFullySelected() +{ + // Find the parent frame; if there is none, then we have nothing to do. + Frame* parent = m_frame->tree()->parent(); + if (!parent) + return; + Page* page = m_frame->page(); + if (!page) + return; + + // Check if the selection contains the entire frame contents; if not, then there is nothing to do. + if (!isRange()) + return; + if (!isStartOfDocument(selection().visibleStart())) + return; + if (!isEndOfDocument(selection().visibleEnd())) + return; + + // Get to the <iframe> or <frame> (or even <object>) element in the parent frame. + Element* ownerElement = m_frame->document()->ownerElement(); + if (!ownerElement) + return; + ContainerNode* ownerElementParent = ownerElement->parentNode(); + if (!ownerElementParent) + return; + + // This method's purpose is it to make it easier to select iframes (in order to delete them). Don't do anything if the iframe isn't deletable. + if (!ownerElementParent->isContentEditable()) + return; + + // Create compute positions before and after the element. + unsigned ownerElementNodeIndex = ownerElement->nodeIndex(); + VisiblePosition beforeOwnerElement(VisiblePosition(ownerElementParent, ownerElementNodeIndex, SEL_DEFAULT_AFFINITY)); + VisiblePosition afterOwnerElement(VisiblePosition(ownerElementParent, ownerElementNodeIndex + 1, VP_UPSTREAM_IF_POSSIBLE)); + + // Focus on the parent frame, and then select from before this element to after. + VisibleSelection newSelection(beforeOwnerElement, afterOwnerElement); + if (parent->selection()->shouldChangeSelection(newSelection)) { + page->focusController()->setFocusedFrame(parent); + parent->selection()->setSelection(newSelection); + } +} + +void SelectionController::selectAll() +{ + Document* document = m_frame->document(); + + if (document->focusedNode() && document->focusedNode()->canSelectAll()) { + document->focusedNode()->selectAll(); + return; + } + + Node* root = 0; + if (isContentEditable()) + root = highestEditableRoot(m_selection.start()); + else { + root = shadowTreeRootNode(); + if (!root) + root = document->documentElement(); + } + if (!root) + return; + VisibleSelection newSelection(VisibleSelection::selectionFromContentsOfNode(root)); + if (shouldChangeSelection(newSelection)) + setSelection(newSelection); + selectFrameElementInParentIfFullySelected(); + notifyRendererOfSelectionChange(true); +} + +bool SelectionController::setSelectedRange(Range* range, EAffinity affinity, bool closeTyping) +{ + if (!range) + return false; + + ExceptionCode ec = 0; + Node* startContainer = range->startContainer(ec); + if (ec) + return false; + + Node* endContainer = range->endContainer(ec); + if (ec) + return false; + + ASSERT(startContainer); + ASSERT(endContainer); + ASSERT(startContainer->document() == endContainer->document()); + + m_frame->document()->updateLayoutIgnorePendingStylesheets(); + + // Non-collapsed ranges are not allowed to start at the end of a line that is wrapped, + // they start at the beginning of the next line instead + bool collapsed = range->collapsed(ec); + if (ec) + return false; + + int startOffset = range->startOffset(ec); + if (ec) + return false; + + int endOffset = range->endOffset(ec); + if (ec) + return false; + + // FIXME: Can we provide extentAffinity? + VisiblePosition visibleStart(startContainer, startOffset, collapsed ? affinity : DOWNSTREAM); + VisiblePosition visibleEnd(endContainer, endOffset, SEL_DEFAULT_AFFINITY); + setSelection(VisibleSelection(visibleStart, visibleEnd), closeTyping); + return true; +} + +bool SelectionController::isInPasswordField() const +{ + Node* startNode = start().node(); + if (!startNode) + return false; + + startNode = startNode->shadowAncestorNode(); + if (!startNode) + return false; + + if (!startNode->hasTagName(inputTag)) + return false; + + return static_cast<HTMLInputElement*>(startNode)->isPasswordField(); +} + +bool SelectionController::caretRendersInsideNode(Node* node) const +{ + if (!node) + return false; + return !isTableElement(node) && !editingIgnoresContent(node); +} + +void SelectionController::focusedOrActiveStateChanged() +{ + bool activeAndFocused = isFocusedAndActive(); + + // Because RenderObject::selectionBackgroundColor() and + // RenderObject::selectionForegroundColor() check if the frame is active, + // we have to update places those colors were painted. + if (RenderView* view = toRenderView(m_frame->document()->renderer())) + view->repaintRectangleInViewAndCompositedLayers(enclosingIntRect(bounds())); + + // Caret appears in the active frame. + if (activeAndFocused) + setSelectionFromNone(); + setCaretVisible(activeAndFocused); + + // Update for caps lock state + m_frame->eventHandler()->capsLockStateMayHaveChanged(); + + // Because CSSStyleSelector::checkOneSelector() and + // RenderTheme::isFocused() check if the frame is active, we have to + // update style and theme state that depended on those. + if (Node* node = m_frame->document()->focusedNode()) { + node->setNeedsStyleRecalc(); + if (RenderObject* renderer = node->renderer()) + if (renderer && renderer->style()->hasAppearance()) + renderer->theme()->stateChanged(renderer, FocusState); + } + + // Secure keyboard entry is set by the active frame. + if (m_frame->document()->useSecureKeyboardEntryWhenActive()) + setUseSecureKeyboardEntry(activeAndFocused); +} + +void SelectionController::pageActivationChanged() +{ + focusedOrActiveStateChanged(); +} + +void SelectionController::updateSecureKeyboardEntryIfActive() +{ + if (m_frame->document() && isFocusedAndActive()) + setUseSecureKeyboardEntry(m_frame->document()->useSecureKeyboardEntryWhenActive()); +} + +void SelectionController::setUseSecureKeyboardEntry(bool enable) +{ + if (enable) + enableSecureTextInput(); + else + disableSecureTextInput(); +} + +void SelectionController::setFocused(bool flag) +{ + if (m_focused == flag) + return; + m_focused = flag; + + focusedOrActiveStateChanged(); +} + +bool SelectionController::isFocusedAndActive() const +{ + return m_focused && m_frame->page() && m_frame->page()->focusController()->isActive(); +} + +void SelectionController::updateAppearance() +{ + ASSERT(!m_isDragCaretController); + +#if ENABLE(TEXT_CARET) + bool caretRectChanged = recomputeCaretRect(); + + bool caretBrowsing = m_frame->settings() && m_frame->settings()->caretBrowsingEnabled(); + bool shouldBlink = m_caretVisible + && isCaret() && (isContentEditable() || caretBrowsing); + + // If the caret moved, stop the blink timer so we can restart with a + // black caret in the new location. + if (caretRectChanged || !shouldBlink) + m_caretBlinkTimer.stop(); + + // Start blinking with a black caret. Be sure not to restart if we're + // already blinking in the right location. + if (shouldBlink && !m_caretBlinkTimer.isActive()) { + if (double blinkInterval = m_frame->page()->theme()->caretBlinkInterval()) + m_caretBlinkTimer.startRepeating(blinkInterval); + + if (!m_caretPaint) { + m_caretPaint = true; + invalidateCaretRect(); + } + } +#endif + + // We need to update style in case the node containing the selection is made display:none. + m_frame->document()->updateStyleIfNeeded(); + + RenderView* view = m_frame->contentRenderer(); + if (!view) + return; + + VisibleSelection selection = this->selection(); + + if (!selection.isRange()) { + view->clearSelection(); + return; + } + + // Use the rightmost candidate for the start of the selection, and the leftmost candidate for the end of the selection. + // Example: foo <a>bar</a>. Imagine that a line wrap occurs after 'foo', and that 'bar' is selected. If we pass [foo, 3] + // as the start of the selection, the selection painting code will think that content on the line containing 'foo' is selected + // and will fill the gap before 'bar'. + Position startPos = selection.start(); + Position candidate = startPos.downstream(); + if (candidate.isCandidate()) + startPos = candidate; + Position endPos = selection.end(); + candidate = endPos.upstream(); + if (candidate.isCandidate()) + endPos = candidate; + + // We can get into a state where the selection endpoints map to the same VisiblePosition when a selection is deleted + // because we don't yet notify the SelectionController of text removal. + if (startPos.isNotNull() && endPos.isNotNull() && selection.visibleStart() != selection.visibleEnd()) { + RenderObject* startRenderer = startPos.node()->renderer(); + RenderObject* endRenderer = endPos.node()->renderer(); + view->setSelection(startRenderer, startPos.deprecatedEditingOffset(), endRenderer, endPos.deprecatedEditingOffset()); + } +} + +void SelectionController::setCaretVisible(bool flag) +{ + if (m_caretVisible == flag) + return; + clearCaretRectIfNeeded(); + m_caretVisible = flag; + updateAppearance(); +} + +void SelectionController::clearCaretRectIfNeeded() +{ +#if ENABLE(TEXT_CARET) + if (!m_caretPaint) + return; + m_caretPaint = false; + invalidateCaretRect(); +#endif +} + +void SelectionController::caretBlinkTimerFired(Timer<SelectionController>*) +{ +#if ENABLE(TEXT_CARET) + ASSERT(m_caretVisible); + ASSERT(isCaret()); + bool caretPaint = m_caretPaint; + if (isCaretBlinkingSuspended() && caretPaint) + return; + m_caretPaint = !caretPaint; + invalidateCaretRect(); +#endif +} + +void SelectionController::notifyRendererOfSelectionChange(bool userTriggered) +{ + m_frame->document()->updateStyleIfNeeded(); + + if (!rootEditableElement()) + return; + + RenderObject* renderer = rootEditableElement()->shadowAncestorNode()->renderer(); + if (!renderer || !renderer->isTextControl()) + return; + + toRenderTextControl(renderer)->selectionChanged(userTriggered); +} + +// Helper function that tells whether a particular node is an element that has an entire +// Frame and FrameView, a <frame>, <iframe>, or <object>. +static bool isFrameElement(const Node* n) +{ + if (!n) + return false; + RenderObject* renderer = n->renderer(); + if (!renderer || !renderer->isWidget()) + return false; + Widget* widget = toRenderWidget(renderer)->widget(); + return widget && widget->isFrameView(); +} + +void SelectionController::setFocusedNodeIfNeeded() +{ + if (isNone() || !isFocused()) + return; + + bool caretBrowsing = m_frame->settings() && m_frame->settings()->caretBrowsingEnabled(); + if (caretBrowsing) { + if (Node* anchor = enclosingAnchorElement(base())) { + m_frame->page()->focusController()->setFocusedNode(anchor, m_frame); + return; + } + } + + if (Node* target = rootEditableElement()) { + // Walk up the DOM tree to search for a node to focus. + while (target) { + // We don't want to set focus on a subframe when selecting in a parent frame, + // so add the !isFrameElement check here. There's probably a better way to make this + // work in the long term, but this is the safest fix at this time. + if (target && target->isMouseFocusable() && !isFrameElement(target)) { + m_frame->page()->focusController()->setFocusedNode(target, m_frame); + return; + } + target = target->parentOrHostNode(); + } + m_frame->document()->setFocusedNode(0); + } + + if (caretBrowsing) + m_frame->page()->focusController()->setFocusedNode(0, m_frame); +} + +void SelectionController::paintDragCaret(GraphicsContext* p, int tx, int ty, const IntRect& clipRect) const +{ +#if ENABLE(TEXT_CARET) + SelectionController* dragCaretController = m_frame->page()->dragCaretController(); + ASSERT(dragCaretController->selection().isCaret()); + if (dragCaretController->selection().start().node()->document()->frame() == m_frame) + dragCaretController->paintCaret(p, tx, ty, clipRect); +#else + UNUSED_PARAM(p); + UNUSED_PARAM(tx); + UNUSED_PARAM(ty); + UNUSED_PARAM(clipRect); +#endif +} + +PassRefPtr<CSSMutableStyleDeclaration> SelectionController::copyTypingStyle() const +{ + if (!m_typingStyle || !m_typingStyle->style()) + return 0; + return m_typingStyle->style()->copy(); +} + +bool SelectionController::shouldDeleteSelection(const VisibleSelection& selection) const +{ + return m_frame->editor()->client()->shouldDeleteRange(selection.toNormalizedRange().get()); +} + +FloatRect SelectionController::bounds(bool clipToVisibleContent) const +{ + RenderView* root = m_frame->contentRenderer(); + FrameView* view = m_frame->view(); + if (!root || !view) + return IntRect(); + + IntRect selectionRect = root->selectionBounds(clipToVisibleContent); + return clipToVisibleContent ? intersection(selectionRect, view->visibleContentRect()) : selectionRect; +} + +void SelectionController::getClippedVisibleTextRectangles(Vector<FloatRect>& rectangles) const +{ + RenderView* root = m_frame->contentRenderer(); + if (!root) + return; + + FloatRect visibleContentRect = m_frame->view()->visibleContentRect(); + + Vector<FloatQuad> quads; + toNormalizedRange()->textQuads(quads, true); + + // FIXME: We are appending empty rectangles to the list for those that fall outside visibleContentRect. + // It might be better to omit those rectangles entirely. + size_t size = quads.size(); + for (size_t i = 0; i < size; ++i) + rectangles.append(intersection(quads[i].enclosingBoundingBox(), visibleContentRect)); +} + +// Scans logically forward from "start", including any child frames. +static HTMLFormElement* scanForForm(Node* start) +{ + for (Node* node = start; node; node = node->traverseNextNode()) { + if (node->hasTagName(formTag)) + return static_cast<HTMLFormElement*>(node); + if (node->isHTMLElement() && static_cast<HTMLElement*>(node)->isFormControlElement()) + return static_cast<HTMLFormControlElement*>(node)->form(); + if (node->hasTagName(frameTag) || node->hasTagName(iframeTag)) { + Node* childDocument = static_cast<HTMLFrameElementBase*>(node)->contentDocument(); + if (HTMLFormElement* frameResult = scanForForm(childDocument)) + return frameResult; + } + } + return 0; +} + +// We look for either the form containing the current focus, or for one immediately after it +HTMLFormElement* SelectionController::currentForm() const +{ + // Start looking either at the active (first responder) node, or where the selection is. + Node* start = m_frame->document()->focusedNode(); + if (!start) + start = this->start().node(); + + // Try walking up the node tree to find a form element. + Node* node; + for (node = start; node; node = node->parentNode()) { + if (node->hasTagName(formTag)) + return static_cast<HTMLFormElement*>(node); + if (node->isHTMLElement() && static_cast<HTMLElement*>(node)->isFormControlElement()) + return static_cast<HTMLFormControlElement*>(node)->form(); + } + + // Try walking forward in the node tree to find a form element. + return scanForForm(start); +} + +void SelectionController::revealSelection(const ScrollAlignment& alignment, bool revealExtent) +{ + IntRect rect; + + switch (selectionType()) { + case VisibleSelection::NoSelection: + return; + case VisibleSelection::CaretSelection: + rect = absoluteCaretBounds(); + break; + case VisibleSelection::RangeSelection: + rect = revealExtent ? VisiblePosition(extent()).absoluteCaretBounds() : enclosingIntRect(bounds(false)); + break; + } + + Position start = this->start(); + ASSERT(start.node()); + if (start.node() && start.node()->renderer()) { + // FIXME: This code only handles scrolling the startContainer's layer, but + // the selection rect could intersect more than just that. + // See <rdar://problem/4799899>. + if (RenderLayer* layer = start.node()->renderer()->enclosingLayer()) { + layer->scrollRectToVisible(rect, false, alignment, alignment); + updateAppearance(); + } + } +} + +void SelectionController::setSelectionFromNone() +{ + // Put a caret inside the body if the entire frame is editable (either the + // entire WebView is editable or designMode is on for this document). + + Document* document = m_frame->document(); + bool caretBrowsing = m_frame->settings() && m_frame->settings()->caretBrowsingEnabled(); + if (!isNone() || !(m_frame->isContentEditable() || caretBrowsing)) + return; + + Node* node = document->documentElement(); + while (node && !node->hasTagName(bodyTag)) + node = node->traverseNextNode(); + if (node) + setSelection(VisibleSelection(Position(node, 0), DOWNSTREAM)); +} + +bool SelectionController::shouldChangeSelection(const VisibleSelection& newSelection) const +{ + return m_frame->editor()->shouldChangeSelection(selection(), newSelection, newSelection.affinity(), false); +} + +#ifndef NDEBUG + +void SelectionController::formatForDebugger(char* buffer, unsigned length) const +{ + m_selection.formatForDebugger(buffer, length); +} + +void SelectionController::showTreeForThis() const +{ + m_selection.showTreeForThis(); +} + +#endif + +} + +#ifndef NDEBUG + +void showTree(const WebCore::SelectionController& sel) +{ + sel.showTreeForThis(); +} + +void showTree(const WebCore::SelectionController* sel) +{ + if (sel) + sel->showTreeForThis(); +} + +#endif diff --git a/Source/WebCore/editing/SelectionController.h b/Source/WebCore/editing/SelectionController.h new file mode 100644 index 0000000..ee52187 --- /dev/null +++ b/Source/WebCore/editing/SelectionController.h @@ -0,0 +1,269 @@ +/* + * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef SelectionController_h +#define SelectionController_h + +#include "EditingStyle.h" +#include "IntRect.h" +#include "Range.h" +#include "ScrollBehavior.h" +#include "Timer.h" +#include "VisibleSelection.h" +#include <wtf/Noncopyable.h> + +namespace WebCore { + +class CharacterData; +class CSSMutableStyleDeclaration; +class Frame; +class GraphicsContext; +class HTMLFormElement; +class RenderObject; +class RenderView; +class Settings; +class VisiblePosition; + +enum DirectionalityPolicy { MakeNonDirectionalSelection, MakeDirectionalSelection }; + +class SelectionController : public Noncopyable { +public: + enum EAlteration { AlterationMove, AlterationExtend }; + enum CursorAlignOnScroll { AlignCursorOnScrollIfNeeded, + AlignCursorOnScrollAlways }; + + SelectionController(Frame* = 0, bool isDragCaretController = false); + + Element* rootEditableElement() const { return m_selection.rootEditableElement(); } + bool isContentEditable() const { return m_selection.isContentEditable(); } + bool isContentRichlyEditable() const { return m_selection.isContentRichlyEditable(); } + Node* shadowTreeRootNode() const { return m_selection.shadowTreeRootNode(); } + + void moveTo(const Range*, EAffinity, bool userTriggered = false); + void moveTo(const VisiblePosition&, bool userTriggered = false, CursorAlignOnScroll = AlignCursorOnScrollIfNeeded); + void moveTo(const VisiblePosition&, const VisiblePosition&, bool userTriggered = false); + void moveTo(const Position&, EAffinity, bool userTriggered = false); + void moveTo(const Position&, const Position&, EAffinity, bool userTriggered = false); + + const VisibleSelection& selection() const { return m_selection; } + void setSelection(const VisibleSelection&, bool closeTyping = true, bool clearTypingStyle = true, bool userTriggered = false, CursorAlignOnScroll = AlignCursorOnScrollIfNeeded, TextGranularity = CharacterGranularity, DirectionalityPolicy = MakeDirectionalSelection); + void setSelection(const VisibleSelection& selection, TextGranularity granularity, DirectionalityPolicy directionality = MakeDirectionalSelection) { setSelection(selection, true, true, false, AlignCursorOnScrollIfNeeded, granularity, directionality); } + bool setSelectedRange(Range*, EAffinity, bool closeTyping); + void selectAll(); + void clear(); + + // Call this after doing user-triggered selections to make it easy to delete the frame you entirely selected. + void selectFrameElementInParentIfFullySelected(); + + bool contains(const IntPoint&); + + VisibleSelection::SelectionType selectionType() const { return m_selection.selectionType(); } + + EAffinity affinity() const { return m_selection.affinity(); } + + bool modify(EAlteration, SelectionDirection, TextGranularity, bool userTriggered = false); + bool modify(EAlteration, int verticalDistance, bool userTriggered = false, CursorAlignOnScroll = AlignCursorOnScrollIfNeeded); + TextGranularity granularity() const { return m_granularity; } + + void setStart(const VisiblePosition &, bool userTriggered = false); + void setEnd(const VisiblePosition &, bool userTriggered = false); + + void setBase(const VisiblePosition&, bool userTriggered = false); + void setBase(const Position&, EAffinity, bool userTriggered = false); + void setExtent(const VisiblePosition&, bool userTriggered = false); + void setExtent(const Position&, EAffinity, bool userTriggered = false); + + Position base() const { return m_selection.base(); } + Position extent() const { return m_selection.extent(); } + Position start() const { return m_selection.start(); } + Position end() const { return m_selection.end(); } + + // Return the renderer that is responsible for painting the caret (in the selection start node) + RenderObject* caretRenderer() const; + + // Caret rect local to the caret's renderer + IntRect localCaretRect(); + IntRect localCaretRectForPainting() const { return m_caretRect; } + + // Bounds of (possibly transformed) caret in absolute coords + IntRect absoluteCaretBounds(); + void setCaretRectNeedsUpdate(bool flag = true); + + void setIsDirectional(bool); + void willBeModified(EAlteration, SelectionDirection); + + bool isNone() const { return m_selection.isNone(); } + bool isCaret() const { return m_selection.isCaret(); } + bool isRange() const { return m_selection.isRange(); } + bool isCaretOrRange() const { return m_selection.isCaretOrRange(); } + bool isInPasswordField() const; + bool isAll(StayInEditableContent stayInEditableContent = MustStayInEditableContent) const { return m_selection.isAll(stayInEditableContent); } + + PassRefPtr<Range> toNormalizedRange() const { return m_selection.toNormalizedRange(); } + + void debugRenderer(RenderObject*, bool selected) const; + + void nodeWillBeRemoved(Node*); + void textWillBeReplaced(CharacterData*, unsigned offset, unsigned oldLength, unsigned newLength); + + void setCaretVisible(bool = true); + void clearCaretRectIfNeeded(); + bool recomputeCaretRect(); // returns true if caret rect moved + void invalidateCaretRect(); + void paintCaret(GraphicsContext*, int tx, int ty, const IntRect& clipRect); + + // Used to suspend caret blinking while the mouse is down. + void setCaretBlinkingSuspended(bool suspended) { m_isCaretBlinkingSuspended = suspended; } + bool isCaretBlinkingSuspended() const { return m_isCaretBlinkingSuspended; } + + // Focus + void setFocused(bool); + bool isFocused() const { return m_focused; } + bool isFocusedAndActive() const; + void pageActivationChanged(); + + // Painting. + void updateAppearance(); + + void updateSecureKeyboardEntryIfActive(); + +#ifndef NDEBUG + void formatForDebugger(char* buffer, unsigned length) const; + void showTreeForThis() const; +#endif + + bool shouldChangeSelection(const VisibleSelection&) const; + bool shouldDeleteSelection(const VisibleSelection&) const; + void setFocusedNodeIfNeeded(); + void notifyRendererOfSelectionChange(bool userTriggered); + + void paintDragCaret(GraphicsContext*, int tx, int ty, const IntRect& clipRect) const; + + EditingStyle* typingStyle() const; + PassRefPtr<CSSMutableStyleDeclaration> copyTypingStyle() const; + void setTypingStyle(PassRefPtr<EditingStyle>); + void clearTypingStyle(); + + FloatRect bounds(bool clipToVisibleContent = true) const; + + void getClippedVisibleTextRectangles(Vector<FloatRect>&) const; + + HTMLFormElement* currentForm() const; + + void revealSelection(const ScrollAlignment& = ScrollAlignment::alignCenterIfNeeded, bool revealExtent = false); + void setSelectionFromNone(); + +private: + enum EPositionType { START, END, BASE, EXTENT }; + + void respondToNodeModification(Node*, bool baseRemoved, bool extentRemoved, bool startRemoved, bool endRemoved); + TextDirection directionOfEnclosingBlock(); + + VisiblePosition positionForPlatform(bool isGetStart) const; + VisiblePosition startForPlatform() const; + VisiblePosition endForPlatform() const; + + VisiblePosition modifyExtendingRight(TextGranularity); + VisiblePosition modifyExtendingForward(TextGranularity); + VisiblePosition modifyMovingRight(TextGranularity); + VisiblePosition modifyMovingForward(TextGranularity); + VisiblePosition modifyExtendingLeft(TextGranularity); + VisiblePosition modifyExtendingBackward(TextGranularity); + VisiblePosition modifyMovingLeft(TextGranularity); + VisiblePosition modifyMovingBackward(TextGranularity); + + void updateCaretRect(); + IntRect caretRepaintRect() const; + bool shouldRepaintCaret(const RenderView* view) const; + + int xPosForVerticalArrowNavigation(EPositionType); + + void notifyAccessibilityForSelectionChange(); + + void focusedOrActiveStateChanged(); + bool caretRendersInsideNode(Node*) const; + + IntRect absoluteBoundsForLocalRect(const IntRect&) const; + + void caretBlinkTimerFired(Timer<SelectionController>*); + + void setUseSecureKeyboardEntry(bool); + + Frame* m_frame; + + int m_xPosForVerticalArrowNavigation; + + VisibleSelection m_selection; + TextGranularity m_granularity; + + RefPtr<EditingStyle> m_typingStyle; + + Timer<SelectionController> m_caretBlinkTimer; + + IntRect m_caretRect; // caret rect in coords local to the renderer responsible for painting the caret + IntRect m_absCaretBounds; // absolute bounding rect for the caret + IntRect m_absoluteCaretRepaintBounds; + + bool m_caretRectNeedsUpdate; // true if m_caretRect and m_absCaretBounds need to be calculated + bool m_absCaretBoundsDirty; + bool m_isDirectional; + bool m_isDragCaretController; + bool m_isCaretBlinkingSuspended; + bool m_focused; + bool m_caretVisible; + bool m_caretPaint; +}; + +inline EditingStyle* SelectionController::typingStyle() const +{ + return m_typingStyle.get(); +} + +inline void SelectionController::clearTypingStyle() +{ + m_typingStyle.clear(); +} + +inline void SelectionController::setTypingStyle(PassRefPtr<EditingStyle> style) +{ + m_typingStyle = style; +} + +#if !(PLATFORM(MAC) || PLATFORM(GTK) || PLATFORM(CHROMIUM)) +inline void SelectionController::notifyAccessibilityForSelectionChange() +{ +} +#endif + +} // namespace WebCore + +#ifndef NDEBUG +// Outside the WebCore namespace for ease of invocation from gdb. +void showTree(const WebCore::SelectionController&); +void showTree(const WebCore::SelectionController*); +#endif + +#endif // SelectionController_h + diff --git a/Source/WebCore/editing/SetNodeAttributeCommand.cpp b/Source/WebCore/editing/SetNodeAttributeCommand.cpp new file mode 100644 index 0000000..d49c18c --- /dev/null +++ b/Source/WebCore/editing/SetNodeAttributeCommand.cpp @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2005, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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 "SetNodeAttributeCommand.h" + +#include "Element.h" +#include <wtf/Assertions.h> + +namespace WebCore { + +SetNodeAttributeCommand::SetNodeAttributeCommand(PassRefPtr<Element> element, + const QualifiedName& attribute, const AtomicString& value) + : SimpleEditCommand(element->document()) + , m_element(element) + , m_attribute(attribute) + , m_value(value) +{ + ASSERT(m_element); +} + +void SetNodeAttributeCommand::doApply() +{ + m_oldValue = m_element->getAttribute(m_attribute); + m_element->setAttribute(m_attribute, m_value); +} + +void SetNodeAttributeCommand::doUnapply() +{ + m_element->setAttribute(m_attribute, m_oldValue); + AtomicStringImpl* nullString = 0; + m_oldValue = nullString; +} + +} // namespace WebCore diff --git a/Source/WebCore/editing/SetNodeAttributeCommand.h b/Source/WebCore/editing/SetNodeAttributeCommand.h new file mode 100644 index 0000000..ce3a1ec --- /dev/null +++ b/Source/WebCore/editing/SetNodeAttributeCommand.h @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2005, 2006, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef SetNodeAttributeCommand_h +#define SetNodeAttributeCommand_h + +#include "EditCommand.h" + +namespace WebCore { + +class SetNodeAttributeCommand : public SimpleEditCommand { +public: + static PassRefPtr<SetNodeAttributeCommand> create(PassRefPtr<Element> element, const QualifiedName& attribute, const AtomicString& value) + { + return adoptRef(new SetNodeAttributeCommand(element, attribute, value)); + } + +private: + SetNodeAttributeCommand(PassRefPtr<Element>, const QualifiedName& attribute, const AtomicString& value); + + virtual void doApply(); + virtual void doUnapply(); + + RefPtr<Element> m_element; + QualifiedName m_attribute; + AtomicString m_value; + AtomicString m_oldValue; +}; + +} // namespace WebCore + +#endif // SetNodeAttributeCommand_h diff --git a/Source/WebCore/editing/SmartReplace.cpp b/Source/WebCore/editing/SmartReplace.cpp new file mode 100644 index 0000000..c5f5240 --- /dev/null +++ b/Source/WebCore/editing/SmartReplace.cpp @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2007 Apple 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. + * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of + * its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS 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 APPLE OR ITS 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 "SmartReplace.h" + +#if !PLATFORM(CF) && !USE(ICU_UNICODE) + +namespace WebCore { + +bool isCharacterSmartReplaceExempt(UChar32 c, bool isPreviousCharacter) +{ + return false; +} + +} + +#endif // !PLATFORM(CF) diff --git a/Source/WebCore/editing/SmartReplace.h b/Source/WebCore/editing/SmartReplace.h new file mode 100644 index 0000000..5a37137 --- /dev/null +++ b/Source/WebCore/editing/SmartReplace.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2007 Apple 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. + * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of + * its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS 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 APPLE OR ITS 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 <wtf/unicode/Unicode.h> + +namespace WebCore { + +bool isCharacterSmartReplaceExempt(UChar32 c, bool isPreviousCharacter); + +} // namespace WebCore diff --git a/Source/WebCore/editing/SmartReplaceCF.cpp b/Source/WebCore/editing/SmartReplaceCF.cpp new file mode 100644 index 0000000..c5fa9a8 --- /dev/null +++ b/Source/WebCore/editing/SmartReplaceCF.cpp @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2007 Apple 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. + * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of + * its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS 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 APPLE OR ITS 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 "SmartReplace.h" + +#include <CoreFoundation/CFCharacterSet.h> +#include <CoreFoundation/CFString.h> + +namespace WebCore { + +static CFMutableCharacterSetRef getSmartSet(bool isPreviousCharacter) +{ + static CFMutableCharacterSetRef preSmartSet = NULL; + static CFMutableCharacterSetRef postSmartSet = NULL; + CFMutableCharacterSetRef smartSet = isPreviousCharacter ? preSmartSet : postSmartSet; + if (!smartSet) { + smartSet = CFCharacterSetCreateMutable(kCFAllocatorDefault); + CFCharacterSetAddCharactersInString(smartSet, isPreviousCharacter ? CFSTR("([\"\'#$/-`{") : CFSTR(")].,;:?\'!\"%*-/}")); + CFCharacterSetUnion(smartSet, CFCharacterSetGetPredefined(kCFCharacterSetWhitespaceAndNewline)); + // Adding CJK ranges + CFCharacterSetAddCharactersInRange(smartSet, CFRangeMake(0x1100, 256)); // Hangul Jamo (0x1100 - 0x11FF) + CFCharacterSetAddCharactersInRange(smartSet, CFRangeMake(0x2E80, 352)); // CJK & Kangxi Radicals (0x2E80 - 0x2FDF) + CFCharacterSetAddCharactersInRange(smartSet, CFRangeMake(0x2FF0, 464)); // Ideograph Descriptions, CJK Symbols, Hiragana, Katakana, Bopomofo, Hangul Compatibility Jamo, Kanbun, & Bopomofo Ext (0x2FF0 - 0x31BF) + CFCharacterSetAddCharactersInRange(smartSet, CFRangeMake(0x3200, 29392)); // Enclosed CJK, CJK Ideographs (Uni Han & Ext A), & Yi (0x3200 - 0xA4CF) + CFCharacterSetAddCharactersInRange(smartSet, CFRangeMake(0xAC00, 11183)); // Hangul Syllables (0xAC00 - 0xD7AF) + CFCharacterSetAddCharactersInRange(smartSet, CFRangeMake(0xF900, 352)); // CJK Compatibility Ideographs (0xF900 - 0xFA5F) + CFCharacterSetAddCharactersInRange(smartSet, CFRangeMake(0xFE30, 32)); // CJK Compatibility From (0xFE30 - 0xFE4F) + CFCharacterSetAddCharactersInRange(smartSet, CFRangeMake(0xFF00, 240)); // Half/Full Width Form (0xFF00 - 0xFFEF) + CFCharacterSetAddCharactersInRange(smartSet, CFRangeMake(0x20000, 0xA6D7)); // CJK Ideograph Exntension B + CFCharacterSetAddCharactersInRange(smartSet, CFRangeMake(0x2F800, 0x021E)); // CJK Compatibility Ideographs (0x2F800 - 0x2FA1D) + + if (isPreviousCharacter) + preSmartSet = smartSet; + else { + CFCharacterSetUnion(smartSet, CFCharacterSetGetPredefined(kCFCharacterSetPunctuation)); + postSmartSet = smartSet; + } + } + return smartSet; +} + +bool isCharacterSmartReplaceExempt(UChar32 c, bool isPreviousCharacter) +{ + return CFCharacterSetIsLongCharacterMember(getSmartSet(isPreviousCharacter), c); +} + +} diff --git a/Source/WebCore/editing/SmartReplaceICU.cpp b/Source/WebCore/editing/SmartReplaceICU.cpp new file mode 100644 index 0000000..9acd350 --- /dev/null +++ b/Source/WebCore/editing/SmartReplaceICU.cpp @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2007 Apple Inc. All rights reserved. + * Copyright (C) 2008 Tony Chang <idealisms@gmail.com> + * + * 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. + * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of + * its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS 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 APPLE OR ITS 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 "SmartReplace.h" + +#if !PLATFORM(CF) && USE(ICU_UNICODE) +#include "PlatformString.h" +#include <unicode/uset.h> +#include <wtf/Assertions.h> + +namespace WebCore { + +static void addAllCodePoints(USet* smartSet, const String& string) +{ + const UChar* characters = string.characters(); + for (size_t i = 0; i < string.length(); i++) + uset_add(smartSet, characters[i]); +} + +// This is mostly a port of the code in WebCore/editing/SmartReplaceCF.cpp +// except we use icu in place of CoreFoundations character classes. +static USet* getSmartSet(bool isPreviousCharacter) +{ + static USet* preSmartSet = NULL; + static USet* postSmartSet = NULL; + USet* smartSet = isPreviousCharacter ? preSmartSet : postSmartSet; + if (!smartSet) { + // Whitespace and newline (kCFCharacterSetWhitespaceAndNewline) + UErrorCode ec = U_ZERO_ERROR; + String whitespaceAndNewline = "[[:WSpace:] [\\u000A\\u000B\\u000C\\u000D\\u0085]]"; + smartSet = uset_openPattern(whitespaceAndNewline.characters(), whitespaceAndNewline.length(), &ec); + ASSERT(U_SUCCESS(ec)); + + // CJK ranges + uset_addRange(smartSet, 0x1100, 0x1100 + 256); // Hangul Jamo (0x1100 - 0x11FF) + uset_addRange(smartSet, 0x2E80, 0x2E80 + 352); // CJK & Kangxi Radicals (0x2E80 - 0x2FDF) + uset_addRange(smartSet, 0x2FF0, 0x2FF0 + 464); // Ideograph Descriptions, CJK Symbols, Hiragana, Katakana, Bopomofo, Hangul Compatibility Jamo, Kanbun, & Bopomofo Ext (0x2FF0 - 0x31BF) + uset_addRange(smartSet, 0x3200, 0x3200 + 29392); // Enclosed CJK, CJK Ideographs (Uni Han & Ext A), & Yi (0x3200 - 0xA4CF) + uset_addRange(smartSet, 0xAC00, 0xAC00 + 11183); // Hangul Syllables (0xAC00 - 0xD7AF) + uset_addRange(smartSet, 0xF900, 0xF900 + 352); // CJK Compatibility Ideographs (0xF900 - 0xFA5F) + uset_addRange(smartSet, 0xFE30, 0xFE30 + 32); // CJK Compatibility From (0xFE30 - 0xFE4F) + uset_addRange(smartSet, 0xFF00, 0xFF00 + 240); // Half/Full Width Form (0xFF00 - 0xFFEF) + uset_addRange(smartSet, 0x20000, 0x20000 + 0xA6D7); // CJK Ideograph Exntension B + uset_addRange(smartSet, 0x2F800, 0x2F800 + 0x021E); // CJK Compatibility Ideographs (0x2F800 - 0x2FA1D) + + if (isPreviousCharacter) { + addAllCodePoints(smartSet, "([\"\'#$/-`{"); + preSmartSet = smartSet; + } else { + addAllCodePoints(smartSet, ")].,;:?\'!\"%*-/}"); + + // Punctuation (kCFCharacterSetPunctuation) + UErrorCode ec = U_ZERO_ERROR; + String punctuationClass = "[:P:]"; + USet* icuPunct = uset_openPattern(punctuationClass.characters(), punctuationClass.length(), &ec); + ASSERT(U_SUCCESS(ec)); + uset_addAll(smartSet, icuPunct); + uset_close(icuPunct); + + postSmartSet = smartSet; + } + } + return smartSet; +} + +bool isCharacterSmartReplaceExempt(UChar32 c, bool isPreviousCharacter) +{ + return uset_contains(getSmartSet(isPreviousCharacter), c); +} + +} + +#endif // !PLATFORM(CF) && USE(ICU_UNICODE) diff --git a/Source/WebCore/editing/SpellChecker.cpp b/Source/WebCore/editing/SpellChecker.cpp new file mode 100644 index 0000000..1807474 --- /dev/null +++ b/Source/WebCore/editing/SpellChecker.cpp @@ -0,0 +1,176 @@ +/* + * 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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 "SpellChecker.h" + +#include "Document.h" +#include "DocumentMarkerController.h" +#include "EditorClient.h" +#include "Frame.h" +#include "HTMLInputElement.h" +#include "HTMLTextAreaElement.h" +#include "Node.h" +#include "PositionIterator.h" +#include "Range.h" +#include "RenderObject.h" +#include "Settings.h" +#include "TextIterator.h" +#include "htmlediting.h" + +namespace WebCore { + +SpellChecker::SpellChecker(Frame* frame, EditorClient* client) + : m_frame(frame) + , m_client(client) + , m_requestSequence(0) +{ +} + +SpellChecker::~SpellChecker() +{ +} + +bool SpellChecker::initRequest(Node* node) +{ + ASSERT(canCheckAsynchronously(node)); + + String text = node->textContent(); + if (!text.length()) + return false; + + m_requestNode = node; + m_requestText = text; + m_requestSequence++; + + return true; +} + +void SpellChecker::clearRequest() +{ + m_requestNode.clear(); + m_requestText = String(); +} + +bool SpellChecker::isAsynchronousEnabled() const +{ + return m_frame->settings() && m_frame->settings()->asynchronousSpellCheckingEnabled(); +} + +bool SpellChecker::canCheckAsynchronously(Node* node) const +{ + return isCheckable(node) && isAsynchronousEnabled() && !isBusy(); +} + +bool SpellChecker::isBusy() const +{ + return m_requestNode.get(); +} + +bool SpellChecker::isValid(int sequence) const +{ + return m_requestNode.get() && m_requestText.length() && m_requestSequence == sequence; +} + +bool SpellChecker::isCheckable(Node* node) const +{ + return node && node->renderer(); +} + +void SpellChecker::requestCheckingFor(Node* node) +{ + ASSERT(canCheckAsynchronously(node)); + + if (!initRequest(node)) + return; + m_client->requestCheckingOfString(this, m_requestSequence, m_requestText); +} + +static bool forwardIterator(PositionIterator& iterator, int distance) +{ + int remaining = distance; + while (!iterator.atEnd()) { + if (iterator.node()->isCharacterDataNode()) { + int length = lastOffsetForEditing(iterator.node()); + int last = length - iterator.offsetInLeafNode(); + if (remaining < last) { + iterator.setOffsetInLeafNode(iterator.offsetInLeafNode() + remaining); + return true; + } + + remaining -= last; + iterator.setOffsetInLeafNode(iterator.offsetInLeafNode() + last); + } + + iterator.increment(); + } + + return false; +} + +void SpellChecker::didCheck(int sequence, const Vector<SpellCheckingResult>& results) +{ + if (!isValid(sequence)) + return; + + if (!m_requestNode->renderer()) { + clearRequest(); + return; + } + + int startOffset = 0; + PositionIterator start = Position(m_requestNode, 0); + for (size_t i = 0; i < results.size(); ++i) { + if (results[i].type() != DocumentMarker::Spelling && results[i].type() != DocumentMarker::Grammar) + continue; + + // To avoid moving the position backward, we assume the given results are sorted with + // startOffset as the ones returned by [NSSpellChecker requestCheckingOfString:]. + ASSERT(startOffset <= results[i].location()); + if (!forwardIterator(start, results[i].location() - startOffset)) + break; + PositionIterator end = start; + if (!forwardIterator(end, results[i].length())) + break; + + // Users or JavaScript applications may change text while a spell-checker checks its + // spellings in the background. To avoid adding markers to the words modified by users or + // JavaScript applications, retrieve the words in the specified region and compare them with + // the original ones. + RefPtr<Range> range = Range::create(m_requestNode->document(), start, end); + // FIXME: Use textContent() compatible string conversion. + String destination = range->text(); + String source = m_requestText.substring(results[i].location(), results[i].length()); + if (destination == source) + m_requestNode->document()->markers()->addMarker(range.get(), results[i].type()); + + startOffset = results[i].location(); + } + + clearRequest(); +} + + +} // namespace WebCore diff --git a/Source/WebCore/editing/SpellChecker.h b/Source/WebCore/editing/SpellChecker.h new file mode 100644 index 0000000..f6215d2 --- /dev/null +++ b/Source/WebCore/editing/SpellChecker.h @@ -0,0 +1,84 @@ +/* + * 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef SpellChecker_h +#define SpellChecker_h + +#include "DocumentMarker.h" +#include <wtf/Noncopyable.h> + +namespace WebCore { + +class EditorClient; +class Frame; +class Node; + +class SpellCheckingResult { +public: + explicit SpellCheckingResult(DocumentMarker::MarkerType type = DocumentMarker::Spelling, int location = 0, int length = 0) + : m_type(type) + , m_location(location) + , m_length(length) + { + } + + DocumentMarker::MarkerType type() const { return m_type; } + int location() const { return m_location; } + int length() const { return m_length; } + +private: + DocumentMarker::MarkerType m_type; + int m_location; + int m_length; +}; + +class SpellChecker : public Noncopyable { +public: + explicit SpellChecker(Frame*, EditorClient*); + ~SpellChecker(); + + bool isAsynchronousEnabled() const; + bool canCheckAsynchronously(Node*) const; + bool isBusy() const; + bool isValid(int sequence) const; + bool isCheckable(Node*) const; + void requestCheckingFor(Node*); + void didCheck(int sequence, const Vector<SpellCheckingResult>&); + +private: + bool initRequest(Node*); + void clearRequest(); + + Frame* m_frame; + EditorClient* m_client; + + RefPtr<Node> m_requestNode; + String m_requestText; + int m_requestSequence; +}; + +} // namespace WebCore + +#endif // SpellChecker_h diff --git a/Source/WebCore/editing/SplitElementCommand.cpp b/Source/WebCore/editing/SplitElementCommand.cpp new file mode 100644 index 0000000..888c45f --- /dev/null +++ b/Source/WebCore/editing/SplitElementCommand.cpp @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2005, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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 "SplitElementCommand.h" + +#include "Element.h" +#include "HTMLNames.h" +#include <wtf/Assertions.h> + +namespace WebCore { + +SplitElementCommand::SplitElementCommand(PassRefPtr<Element> element, PassRefPtr<Node> atChild) + : SimpleEditCommand(element->document()) + , m_element2(element) + , m_atChild(atChild) +{ + ASSERT(m_element2); + ASSERT(m_atChild); + ASSERT(m_atChild->parentNode() == m_element2); +} + +void SplitElementCommand::executeApply() +{ + if (m_atChild->parentNode() != m_element2) + return; + + Vector<RefPtr<Node> > children; + for (Node* node = m_element2->firstChild(); node != m_atChild; node = node->nextSibling()) + children.append(node); + + ExceptionCode ec = 0; + + ContainerNode* parent = m_element2->parentNode(); + if (!parent || !parent->isContentEditable()) + return; + parent->insertBefore(m_element1.get(), m_element2.get(), ec); + if (ec) + return; + + // Delete id attribute from the second element because the same id cannot be used for more than one element + m_element2->removeAttribute(HTMLNames::idAttr, ec); + ASSERT(!ec); + + size_t size = children.size(); + for (size_t i = 0; i < size; ++i) + m_element1->appendChild(children[i], ec); +} + +void SplitElementCommand::doApply() +{ + m_element1 = m_element2->cloneElementWithoutChildren(); + + executeApply(); +} + +void SplitElementCommand::doUnapply() +{ + if (!m_element1 || !m_element1->isContentEditable() || !m_element2->isContentEditable()) + return; + + Vector<RefPtr<Node> > children; + for (Node* node = m_element1->firstChild(); node; node = node->nextSibling()) + children.append(node); + + RefPtr<Node> refChild = m_element2->firstChild(); + + ExceptionCode ec = 0; + + size_t size = children.size(); + for (size_t i = 0; i < size; ++i) + m_element2->insertBefore(children[i].get(), refChild.get(), ec); + + // Recover the id attribute of the original element. + if (m_element1->hasAttribute(HTMLNames::idAttr)) + m_element2->setAttribute(HTMLNames::idAttr, m_element1->getAttribute(HTMLNames::idAttr)); + + m_element1->remove(ec); +} + +void SplitElementCommand::doReapply() +{ + if (!m_element1) + return; + + executeApply(); +} + +} // namespace WebCore diff --git a/Source/WebCore/editing/SplitElementCommand.h b/Source/WebCore/editing/SplitElementCommand.h new file mode 100644 index 0000000..7ea8f5b --- /dev/null +++ b/Source/WebCore/editing/SplitElementCommand.h @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2005, 2006, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef SplitElementCommand_h +#define SplitElementCommand_h + +#include "EditCommand.h" + +namespace WebCore { + +class SplitElementCommand : public SimpleEditCommand { +public: + static PassRefPtr<SplitElementCommand> create(PassRefPtr<Element> element, PassRefPtr<Node> splitPointChild) + { + return adoptRef(new SplitElementCommand(element, splitPointChild)); + } + +private: + SplitElementCommand(PassRefPtr<Element>, PassRefPtr<Node> splitPointChild); + + virtual void doApply(); + virtual void doUnapply(); + virtual void doReapply(); + void executeApply(); + + RefPtr<Element> m_element1; + RefPtr<Element> m_element2; + RefPtr<Node> m_atChild; +}; + +} // namespace WebCore + +#endif // SplitElementCommand_h diff --git a/Source/WebCore/editing/SplitTextNodeCommand.cpp b/Source/WebCore/editing/SplitTextNodeCommand.cpp new file mode 100644 index 0000000..aea36b9 --- /dev/null +++ b/Source/WebCore/editing/SplitTextNodeCommand.cpp @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2005, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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 "SplitTextNodeCommand.h" + +#include "Document.h" +#include "DocumentMarkerController.h" +#include "Text.h" +#include <wtf/Assertions.h> + +namespace WebCore { + +SplitTextNodeCommand::SplitTextNodeCommand(PassRefPtr<Text> text, int offset) + : SimpleEditCommand(text->document()) + , m_text2(text) + , m_offset(offset) +{ + // NOTE: Various callers rely on the fact that the original node becomes + // the second node (i.e. the new node is inserted before the existing one). + // That is not a fundamental dependency (i.e. it could be re-coded), but + // rather is based on how this code happens to work. + ASSERT(m_text2); + ASSERT(m_text2->length() > 0); + ASSERT(m_offset > 0); + ASSERT(m_offset < m_text2->length()); +} + +void SplitTextNodeCommand::doApply() +{ + ContainerNode* parent = m_text2->parentNode(); + if (!parent || !parent->isContentEditable()) + return; + + ExceptionCode ec = 0; + String prefixText = m_text2->substringData(0, m_offset, ec); + if (prefixText.isEmpty()) + return; + + m_text1 = Text::create(document(), prefixText); + ASSERT(m_text1); + document()->markers()->copyMarkers(m_text2.get(), 0, m_offset, m_text1.get(), 0); + + insertText1AndTrimText2(); +} + +void SplitTextNodeCommand::doUnapply() +{ + if (!m_text1 || !m_text1->isContentEditable()) + return; + + ASSERT(m_text1->document() == document()); + + String prefixText = m_text1->data(); + + ExceptionCode ec = 0; + m_text2->insertData(0, prefixText, ec); + ASSERT(!ec); + + document()->markers()->copyMarkers(m_text1.get(), 0, prefixText.length(), m_text2.get(), 0); + m_text1->remove(ec); +} + +void SplitTextNodeCommand::doReapply() +{ + if (!m_text1 || !m_text2) + return; + + ContainerNode* parent = m_text2->parentNode(); + if (!parent || !parent->isContentEditable()) + return; + + insertText1AndTrimText2(); +} + +void SplitTextNodeCommand::insertText1AndTrimText2() +{ + ExceptionCode ec = 0; + m_text2->parentNode()->insertBefore(m_text1.get(), m_text2.get(), ec); + if (ec) + return; + m_text2->deleteData(0, m_offset, ec); +} + +} // namespace WebCore diff --git a/Source/WebCore/editing/SplitTextNodeCommand.h b/Source/WebCore/editing/SplitTextNodeCommand.h new file mode 100644 index 0000000..8d67d82 --- /dev/null +++ b/Source/WebCore/editing/SplitTextNodeCommand.h @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2005, 2006, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef SplitTextNodeCommand_h +#define SplitTextNodeCommand_h + +#include "EditCommand.h" + +namespace WebCore { + +class Text; + +class SplitTextNodeCommand : public SimpleEditCommand { +public: + static PassRefPtr<SplitTextNodeCommand> create(PassRefPtr<Text> node, int offset) + { + return adoptRef(new SplitTextNodeCommand(node, offset)); + } + +private: + SplitTextNodeCommand(PassRefPtr<Text>, int offset); + + virtual void doApply(); + virtual void doUnapply(); + virtual void doReapply(); + void insertText1AndTrimText2(); + + RefPtr<Text> m_text1; + RefPtr<Text> m_text2; + unsigned m_offset; +}; + +} // namespace WebCore + +#endif // SplitTextNodeCommand_h diff --git a/Source/WebCore/editing/SplitTextNodeContainingElementCommand.cpp b/Source/WebCore/editing/SplitTextNodeContainingElementCommand.cpp new file mode 100644 index 0000000..8c90fb0 --- /dev/null +++ b/Source/WebCore/editing/SplitTextNodeContainingElementCommand.cpp @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2005, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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 "SplitTextNodeContainingElementCommand.h" + +#include "Element.h" +#include "Text.h" +#include "RenderObject.h" +#include <wtf/Assertions.h> + +namespace WebCore { + +SplitTextNodeContainingElementCommand::SplitTextNodeContainingElementCommand(PassRefPtr<Text> text, int offset) + : CompositeEditCommand(text->document()), m_text(text), m_offset(offset) +{ + ASSERT(m_text); + ASSERT(m_text->length() > 0); +} + +void SplitTextNodeContainingElementCommand::doApply() +{ + ASSERT(m_text); + ASSERT(m_offset > 0); + + splitTextNode(m_text.get(), m_offset); + + Element* parent = m_text->parentElement(); + if (!parent || !parent->parentElement() || !parent->parentElement()->isContentEditable()) + return; + + RenderObject* parentRenderer = parent->renderer(); + if (!parentRenderer || !parentRenderer->isInline()) { + wrapContentsInDummySpan(parent); + Node* firstChild = parent->firstChild(); + if (!firstChild || !firstChild->isElementNode()) + return; + parent = static_cast<Element*>(firstChild); + } + + splitElement(parent, m_text); +} + +} diff --git a/Source/WebCore/editing/SplitTextNodeContainingElementCommand.h b/Source/WebCore/editing/SplitTextNodeContainingElementCommand.h new file mode 100644 index 0000000..4e6af4f --- /dev/null +++ b/Source/WebCore/editing/SplitTextNodeContainingElementCommand.h @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2005, 2006, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef SplitTextNodeContainingElementCommand_h +#define SplitTextNodeContainingElementCommand_h + +#include "CompositeEditCommand.h" + +namespace WebCore { + +class SplitTextNodeContainingElementCommand : public CompositeEditCommand { +public: + static PassRefPtr<SplitTextNodeContainingElementCommand> create(PassRefPtr<Text> node, int offset) + { + return adoptRef(new SplitTextNodeContainingElementCommand(node, offset)); + } + +private: + SplitTextNodeContainingElementCommand(PassRefPtr<Text>, int offset); + + virtual void doApply(); + + RefPtr<Text> m_text; + int m_offset; +}; + +} // namespace WebCore + +#endif // SplitTextNodeContainingElementCommand_h diff --git a/Source/WebCore/editing/TextAffinity.h b/Source/WebCore/editing/TextAffinity.h new file mode 100644 index 0000000..5310ca9 --- /dev/null +++ b/Source/WebCore/editing/TextAffinity.h @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2004 Apple Computer, 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef TextAffinity_h +#define TextAffinity_h + +#ifdef __OBJC__ +#include <AppKit/NSTextView.h> +#endif + +namespace WebCore { + +// These match the AppKit values for these concepts. +// From NSTextView.h: +// NSSelectionAffinityUpstream = 0 +// NSSelectionAffinityDownstream = 1 +enum EAffinity { UPSTREAM = 0, DOWNSTREAM = 1 }; + +} // namespace WebCore + +#ifdef __OBJC__ + +inline NSSelectionAffinity kit(WebCore::EAffinity affinity) +{ + return static_cast<NSSelectionAffinity>(affinity); +} + +inline WebCore::EAffinity core(NSSelectionAffinity affinity) +{ + return static_cast<WebCore::EAffinity>(affinity); +} + +#endif + +#endif // TextAffinity_h diff --git a/Source/WebCore/editing/TextCheckingHelper.cpp b/Source/WebCore/editing/TextCheckingHelper.cpp new file mode 100644 index 0000000..b4429a6 --- /dev/null +++ b/Source/WebCore/editing/TextCheckingHelper.cpp @@ -0,0 +1,605 @@ +/* + * Copyright (C) 2006, 2007 Apple Inc. All rights reserved. + * Copyright (C) 2008 Nokia Corporation and/or its subsidiary(-ies) + * + * 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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 "TextCheckingHelper.h" + +#include "DocumentMarkerController.h" +#include "Range.h" +#include "TextIterator.h" +#include "VisiblePosition.h" +#include "visible_units.h" + +namespace WebCore { + +static PassRefPtr<Range> expandToParagraphBoundary(PassRefPtr<Range> range) +{ + ExceptionCode ec = 0; + RefPtr<Range> paragraphRange = range->cloneRange(ec); + setStart(paragraphRange.get(), startOfParagraph(range->startPosition())); + setEnd(paragraphRange.get(), endOfParagraph(range->endPosition())); + return paragraphRange; +} + +TextCheckingParagraph::TextCheckingParagraph(PassRefPtr<Range> checkingRange) + : m_checkingRange(checkingRange) + , m_checkingStart(-1) + , m_checkingEnd(-1) + , m_checkingLength(-1) +{ +} + +TextCheckingParagraph::~TextCheckingParagraph() +{ +} + +void TextCheckingParagraph::expandRangeToNextEnd() +{ + ASSERT(m_checkingRange); + setEnd(paragraphRange().get(), endOfParagraph(startOfNextParagraph(paragraphRange()->startPosition()))); + invalidateParagraphRangeValues(); +} + +void TextCheckingParagraph::invalidateParagraphRangeValues() +{ + m_checkingStart = m_checkingEnd = -1; + m_offsetAsRange = 0; + m_text = String(); +} + +int TextCheckingParagraph::rangeLength() const +{ + ASSERT(m_checkingRange); + return TextIterator::rangeLength(paragraphRange().get()); +} + +PassRefPtr<Range> TextCheckingParagraph::paragraphRange() const +{ + ASSERT(m_checkingRange); + if (!m_paragraphRange) + m_paragraphRange = expandToParagraphBoundary(checkingRange()); + return m_paragraphRange; +} + +PassRefPtr<Range> TextCheckingParagraph::subrange(int characterOffset, int characterCount) const +{ + ASSERT(m_checkingRange); + return TextIterator::subrange(paragraphRange().get(), characterOffset, characterCount); +} + +int TextCheckingParagraph::offsetTo(const Position& position, ExceptionCode& ec) const +{ + ASSERT(m_checkingRange); + RefPtr<Range> range = offsetAsRange(); + range->setEnd(position.containerNode(), position.computeOffsetInContainerNode(), ec); + if (ec) + return 0; + return TextIterator::rangeLength(range.get()); +} + +bool TextCheckingParagraph::isEmpty() const +{ + // Both predicates should have same result, but we check both just for sure. + // We need to investigate to remove this redundancy. + return isRangeEmpty() || isTextEmpty(); +} + +PassRefPtr<Range> TextCheckingParagraph::offsetAsRange() const +{ + ASSERT(m_checkingRange); + if (!m_offsetAsRange) { + ExceptionCode ec = 0; + m_offsetAsRange = Range::create(paragraphRange()->startContainer(ec)->document(), paragraphRange()->startPosition(), checkingRange()->startPosition()); + } + + return m_offsetAsRange; +} + +const String& TextCheckingParagraph::text() const +{ + ASSERT(m_checkingRange); + if (m_text.isEmpty()) + m_text = plainText(paragraphRange().get()); + return m_text; +} + +int TextCheckingParagraph::checkingStart() const +{ + ASSERT(m_checkingRange); + if (m_checkingStart == -1) + m_checkingStart = TextIterator::rangeLength(offsetAsRange().get()); + return m_checkingStart; +} + +int TextCheckingParagraph::checkingEnd() const +{ + ASSERT(m_checkingRange); + if (m_checkingEnd == -1) + m_checkingEnd = checkingStart() + TextIterator::rangeLength(checkingRange().get()); + return m_checkingEnd; +} + +int TextCheckingParagraph::checkingLength() const +{ + ASSERT(m_checkingRange); + if (-1 == m_checkingLength) + m_checkingLength = TextIterator::rangeLength(checkingRange().get()); + return m_checkingLength; +} + +TextCheckingHelper::TextCheckingHelper(EditorClient* client, PassRefPtr<Range> range) + : m_client(client) + , m_range(range) +{ + ASSERT_ARG(m_client, m_client); + ASSERT_ARG(m_range, m_range); +} + +TextCheckingHelper::~TextCheckingHelper() +{ +} + +String TextCheckingHelper::findFirstMisspelling(int& firstMisspellingOffset, bool markAll, RefPtr<Range>& firstMisspellingRange) +{ + WordAwareIterator it(m_range.get()); + firstMisspellingOffset = 0; + + String firstMisspelling; + int currentChunkOffset = 0; + + while (!it.atEnd()) { + const UChar* chars = it.characters(); + int len = it.length(); + + // Skip some work for one-space-char hunks + if (!(len == 1 && chars[0] == ' ')) { + + int misspellingLocation = -1; + int misspellingLength = 0; + m_client->checkSpellingOfString(chars, len, &misspellingLocation, &misspellingLength); + + // 5490627 shows that there was some code path here where the String constructor below crashes. + // We don't know exactly what combination of bad input caused this, so we're making this much + // more robust against bad input on release builds. + ASSERT(misspellingLength >= 0); + ASSERT(misspellingLocation >= -1); + ASSERT(!misspellingLength || misspellingLocation >= 0); + ASSERT(misspellingLocation < len); + ASSERT(misspellingLength <= len); + ASSERT(misspellingLocation + misspellingLength <= len); + + if (misspellingLocation >= 0 && misspellingLength > 0 && misspellingLocation < len && misspellingLength <= len && misspellingLocation + misspellingLength <= len) { + + // Compute range of misspelled word + RefPtr<Range> misspellingRange = TextIterator::subrange(m_range.get(), currentChunkOffset + misspellingLocation, misspellingLength); + + // Remember first-encountered misspelling and its offset. + if (!firstMisspelling) { + firstMisspellingOffset = currentChunkOffset + misspellingLocation; + firstMisspelling = String(chars + misspellingLocation, misspellingLength); + firstMisspellingRange = misspellingRange; + } + + // Store marker for misspelled word. + ExceptionCode ec = 0; + misspellingRange->startContainer(ec)->document()->markers()->addMarker(misspellingRange.get(), DocumentMarker::Spelling); + ASSERT(!ec); + + // Bail out if we're marking only the first misspelling, and not all instances. + if (!markAll) + break; + } + } + + currentChunkOffset += len; + it.advance(); + } + + return firstMisspelling; +} + +String TextCheckingHelper::findFirstMisspellingOrBadGrammar(bool checkGrammar, bool& outIsSpelling, int& outFirstFoundOffset, GrammarDetail& outGrammarDetail) +{ +#if PLATFORM(MAC) && !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD) + String firstFoundItem; + String misspelledWord; + String badGrammarPhrase; + ExceptionCode ec = 0; + + // Initialize out parameters; these will be updated if we find something to return. + outIsSpelling = true; + outFirstFoundOffset = 0; + outGrammarDetail.location = -1; + outGrammarDetail.length = 0; + outGrammarDetail.guesses.clear(); + outGrammarDetail.userDescription = ""; + + // Expand the search range to encompass entire paragraphs, since text checking needs that much context. + // Determine the character offset from the start of the paragraph to the start of the original search range, + // since we will want to ignore results in this area. + RefPtr<Range> paragraphRange = m_range->cloneRange(ec); + setStart(paragraphRange.get(), startOfParagraph(m_range->startPosition())); + int totalRangeLength = TextIterator::rangeLength(paragraphRange.get()); + setEnd(paragraphRange.get(), endOfParagraph(m_range->startPosition())); + + RefPtr<Range> offsetAsRange = Range::create(paragraphRange->startContainer(ec)->document(), paragraphRange->startPosition(), m_range->startPosition()); + int rangeStartOffset = TextIterator::rangeLength(offsetAsRange.get()); + int totalLengthProcessed = 0; + + bool firstIteration = true; + bool lastIteration = false; + while (totalLengthProcessed < totalRangeLength) { + // Iterate through the search range by paragraphs, checking each one for spelling and grammar. + int currentLength = TextIterator::rangeLength(paragraphRange.get()); + int currentStartOffset = firstIteration ? rangeStartOffset : 0; + int currentEndOffset = currentLength; + if (inSameParagraph(paragraphRange->startPosition(), m_range->endPosition())) { + // Determine the character offset from the end of the original search range to the end of the paragraph, + // since we will want to ignore results in this area. + RefPtr<Range> endOffsetAsRange = Range::create(paragraphRange->startContainer(ec)->document(), paragraphRange->startPosition(), m_range->endPosition()); + currentEndOffset = TextIterator::rangeLength(endOffsetAsRange.get()); + lastIteration = true; + } + if (currentStartOffset < currentEndOffset) { + String paragraphString = plainText(paragraphRange.get()); + if (paragraphString.length() > 0) { + bool foundGrammar = false; + int spellingLocation = 0; + int grammarPhraseLocation = 0; + int grammarDetailLocation = 0; + unsigned grammarDetailIndex = 0; + + Vector<TextCheckingResult> results; + uint64_t checkingTypes = checkGrammar ? (TextCheckingTypeSpelling | TextCheckingTypeGrammar) : TextCheckingTypeSpelling; + m_client->checkTextOfParagraph(paragraphString.characters(), paragraphString.length(), checkingTypes, results); + + for (unsigned i = 0; i < results.size(); i++) { + const TextCheckingResult* result = &results[i]; + if (result->type == TextCheckingTypeSpelling && result->location >= currentStartOffset && result->location + result->length <= currentEndOffset) { + ASSERT(result->length > 0 && result->location >= 0); + spellingLocation = result->location; + misspelledWord = paragraphString.substring(result->location, result->length); + ASSERT(misspelledWord.length()); + break; + } + if (checkGrammar && result->type == TextCheckingTypeGrammar && result->location < currentEndOffset && result->location + result->length > currentStartOffset) { + ASSERT(result->length > 0 && result->location >= 0); + // We can't stop after the first grammar result, since there might still be a spelling result after + // it begins but before the first detail in it, but we can stop if we find a second grammar result. + if (foundGrammar) + break; + for (unsigned j = 0; j < result->details.size(); j++) { + const GrammarDetail* detail = &result->details[j]; + ASSERT(detail->length > 0 && detail->location >= 0); + if (result->location + detail->location >= currentStartOffset && result->location + detail->location + detail->length <= currentEndOffset && (!foundGrammar || result->location + detail->location < grammarDetailLocation)) { + grammarDetailIndex = j; + grammarDetailLocation = result->location + detail->location; + foundGrammar = true; + } + } + if (foundGrammar) { + grammarPhraseLocation = result->location; + outGrammarDetail = result->details[grammarDetailIndex]; + badGrammarPhrase = paragraphString.substring(result->location, result->length); + ASSERT(badGrammarPhrase.length()); + } + } + } + + if (!misspelledWord.isEmpty() && (!checkGrammar || badGrammarPhrase.isEmpty() || spellingLocation <= grammarDetailLocation)) { + int spellingOffset = spellingLocation - currentStartOffset; + if (!firstIteration) { + RefPtr<Range> paragraphOffsetAsRange = Range::create(paragraphRange->startContainer(ec)->document(), m_range->startPosition(), paragraphRange->startPosition()); + spellingOffset += TextIterator::rangeLength(paragraphOffsetAsRange.get()); + } + outIsSpelling = true; + outFirstFoundOffset = spellingOffset; + firstFoundItem = misspelledWord; + break; + } + if (checkGrammar && !badGrammarPhrase.isEmpty()) { + int grammarPhraseOffset = grammarPhraseLocation - currentStartOffset; + if (!firstIteration) { + RefPtr<Range> paragraphOffsetAsRange = Range::create(paragraphRange->startContainer(ec)->document(), m_range->startPosition(), paragraphRange->startPosition()); + grammarPhraseOffset += TextIterator::rangeLength(paragraphOffsetAsRange.get()); + } + outIsSpelling = false; + outFirstFoundOffset = grammarPhraseOffset; + firstFoundItem = badGrammarPhrase; + break; + } + } + } + if (lastIteration || totalLengthProcessed + currentLength >= totalRangeLength) + break; + VisiblePosition newParagraphStart = startOfNextParagraph(paragraphRange->endPosition()); + setStart(paragraphRange.get(), newParagraphStart); + setEnd(paragraphRange.get(), endOfParagraph(newParagraphStart)); + firstIteration = false; + totalLengthProcessed += currentLength; + } + return firstFoundItem; +#else + ASSERT_NOT_REACHED(); + UNUSED_PARAM(checkGrammar); + UNUSED_PARAM(outIsSpelling); + UNUSED_PARAM(outFirstFoundOffset); + UNUSED_PARAM(outGrammarDetail); + return ""; +#endif +} + +int TextCheckingHelper::findFirstGrammarDetail(const Vector<GrammarDetail>& grammarDetails, int badGrammarPhraseLocation, int /*badGrammarPhraseLength*/, int startOffset, int endOffset, bool markAll) +{ +#ifndef BUILDING_ON_TIGER + // Found some bad grammar. Find the earliest detail range that starts in our search range (if any). + // Optionally add a DocumentMarker for each detail in the range. + int earliestDetailLocationSoFar = -1; + int earliestDetailIndex = -1; + for (unsigned i = 0; i < grammarDetails.size(); i++) { + const GrammarDetail* detail = &grammarDetails[i]; + ASSERT(detail->length > 0 && detail->location >= 0); + + int detailStartOffsetInParagraph = badGrammarPhraseLocation + detail->location; + + // Skip this detail if it starts before the original search range + if (detailStartOffsetInParagraph < startOffset) + continue; + + // Skip this detail if it starts after the original search range + if (detailStartOffsetInParagraph >= endOffset) + continue; + + if (markAll) { + RefPtr<Range> badGrammarRange = TextIterator::subrange(m_range.get(), badGrammarPhraseLocation - startOffset + detail->location, detail->length); + ExceptionCode ec = 0; + badGrammarRange->startContainer(ec)->document()->markers()->addMarker(badGrammarRange.get(), DocumentMarker::Grammar, detail->userDescription); + ASSERT(!ec); + } + + // Remember this detail only if it's earlier than our current candidate (the details aren't in a guaranteed order) + if (earliestDetailIndex < 0 || earliestDetailLocationSoFar > detail->location) { + earliestDetailIndex = i; + earliestDetailLocationSoFar = detail->location; + } + } + + return earliestDetailIndex; +#else + ASSERT_NOT_REACHED(); + UNUSED_PARAM(grammarDetails); + UNUSED_PARAM(badGrammarPhraseLocation); + UNUSED_PARAM(startOffset); + UNUSED_PARAM(endOffset); + UNUSED_PARAM(markAll); + return 0; +#endif +} + +String TextCheckingHelper::findFirstBadGrammar(GrammarDetail& outGrammarDetail, int& outGrammarPhraseOffset, bool markAll) +{ +#ifndef BUILDING_ON_TIGER + // Initialize out parameters; these will be updated if we find something to return. + outGrammarDetail.location = -1; + outGrammarDetail.length = 0; + outGrammarDetail.guesses.clear(); + outGrammarDetail.userDescription = ""; + outGrammarPhraseOffset = 0; + + String firstBadGrammarPhrase; + + // Expand the search range to encompass entire paragraphs, since grammar checking needs that much context. + // Determine the character offset from the start of the paragraph to the start of the original search range, + // since we will want to ignore results in this area. + TextCheckingParagraph paragraph(m_range); + + // Start checking from beginning of paragraph, but skip past results that occur before the start of the original search range. + int startOffset = 0; + while (startOffset < paragraph.checkingEnd()) { + Vector<GrammarDetail> grammarDetails; + int badGrammarPhraseLocation = -1; + int badGrammarPhraseLength = 0; + m_client->checkGrammarOfString(paragraph.textCharacters() + startOffset, paragraph.textLength() - startOffset, grammarDetails, &badGrammarPhraseLocation, &badGrammarPhraseLength); + + if (!badGrammarPhraseLength) { + ASSERT(badGrammarPhraseLocation == -1); + return String(); + } + + ASSERT(badGrammarPhraseLocation >= 0); + badGrammarPhraseLocation += startOffset; + + + // Found some bad grammar. Find the earliest detail range that starts in our search range (if any). + int badGrammarIndex = findFirstGrammarDetail(grammarDetails, badGrammarPhraseLocation, badGrammarPhraseLength, paragraph.checkingStart(), paragraph.checkingEnd(), markAll); + if (badGrammarIndex >= 0) { + ASSERT(static_cast<unsigned>(badGrammarIndex) < grammarDetails.size()); + outGrammarDetail = grammarDetails[badGrammarIndex]; + } + + // If we found a detail in range, then we have found the first bad phrase (unless we found one earlier but + // kept going so we could mark all instances). + if (badGrammarIndex >= 0 && firstBadGrammarPhrase.isEmpty()) { + outGrammarPhraseOffset = badGrammarPhraseLocation - paragraph.checkingStart(); + firstBadGrammarPhrase = paragraph.textSubstring(badGrammarPhraseLocation, badGrammarPhraseLength); + + // Found one. We're done now, unless we're marking each instance. + if (!markAll) + break; + } + + // These results were all between the start of the paragraph and the start of the search range; look + // beyond this phrase. + startOffset = badGrammarPhraseLocation + badGrammarPhraseLength; + } + + return firstBadGrammarPhrase; +#else + ASSERT_NOT_REACHED(); + UNUSED_PARAM(outGrammarDetail); + UNUSED_PARAM(outGrammarPhraseOffset); + UNUSED_PARAM(markAll); +#endif +} + + +bool TextCheckingHelper::isUngrammatical(Vector<String>& guessesVector) const +{ +#ifndef BUILDING_ON_TIGER + if (!m_client) + return false; + + ExceptionCode ec; + if (!m_range || m_range->collapsed(ec)) + return false; + + // Returns true only if the passed range exactly corresponds to a bad grammar detail range. This is analogous + // to isSelectionMisspelled. It's not good enough for there to be some bad grammar somewhere in the range, + // or overlapping the range; the ranges must exactly match. + guessesVector.clear(); + int grammarPhraseOffset; + + GrammarDetail grammarDetail; + String badGrammarPhrase = const_cast<TextCheckingHelper*>(this)->findFirstBadGrammar(grammarDetail, grammarPhraseOffset, false); + + // No bad grammar in these parts at all. + if (badGrammarPhrase.isEmpty()) + return false; + + // Bad grammar, but phrase (e.g. sentence) starts beyond start of range. + if (grammarPhraseOffset > 0) + return false; + + ASSERT(grammarDetail.location >= 0 && grammarDetail.length > 0); + + // Bad grammar, but start of detail (e.g. ungrammatical word) doesn't match start of range + if (grammarDetail.location + grammarPhraseOffset) + return false; + + // Bad grammar at start of range, but end of bad grammar is before or after end of range + if (grammarDetail.length != TextIterator::rangeLength(m_range.get())) + return false; + + // Update the spelling panel to be displaying this error (whether or not the spelling panel is on screen). + // This is necessary to make a subsequent call to [NSSpellChecker ignoreWord:inSpellDocumentWithTag:] work + // correctly; that call behaves differently based on whether the spelling panel is displaying a misspelling + // or a grammar error. + m_client->updateSpellingUIWithGrammarString(badGrammarPhrase, grammarDetail); + + return true; +#else + ASSERT_NOT_REACHED(); + UNUSED_PARAM(guessesVector); + return true; +#endif +} + +Vector<String> TextCheckingHelper::guessesForMisspelledOrUngrammaticalRange(bool checkGrammar, bool& misspelled, bool& ungrammatical) const +{ +#if PLATFORM(MAC) && !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD) + Vector<String> guesses; + ExceptionCode ec; + misspelled = false; + ungrammatical = false; + + if (!m_client || !m_range || m_range->collapsed(ec)) + return guesses; + + // Expand the range to encompass entire paragraphs, since text checking needs that much context. + TextCheckingParagraph paragraph(m_range); + if (paragraph.isEmpty()) + return guesses; + + Vector<TextCheckingResult> results; + uint64_t checkingTypes = checkGrammar ? (TextCheckingTypeSpelling | TextCheckingTypeGrammar) : TextCheckingTypeSpelling; + m_client->checkTextOfParagraph(paragraph.textCharacters(), paragraph.textLength(), checkingTypes, results); + + for (unsigned i = 0; i < results.size(); i++) { + const TextCheckingResult* result = &results[i]; + if (result->type == TextCheckingTypeSpelling && paragraph.checkingRangeMatches(result->location, result->length)) { + String misspelledWord = paragraph.checkingSubstring(); + ASSERT(misspelledWord.length()); + m_client->getGuessesForWord(misspelledWord, String(), guesses); + m_client->updateSpellingUIWithMisspelledWord(misspelledWord); + misspelled = true; + return guesses; + } + } + + if (!checkGrammar) + return guesses; + + for (unsigned i = 0; i < results.size(); i++) { + const TextCheckingResult* result = &results[i]; + if (result->type == TextCheckingTypeGrammar && paragraph.isCheckingRangeCoveredBy(result->location, result->length)) { + for (unsigned j = 0; j < result->details.size(); j++) { + const GrammarDetail* detail = &result->details[j]; + ASSERT(detail->length > 0 && detail->location >= 0); + if (paragraph.checkingRangeMatches(result->location + detail->location, detail->length)) { + String badGrammarPhrase = paragraph.textSubstring(result->location, result->length); + ASSERT(badGrammarPhrase.length()); + for (unsigned k = 0; k < detail->guesses.size(); k++) + guesses.append(detail->guesses[k]); + m_client->updateSpellingUIWithGrammarString(badGrammarPhrase, *detail); + ungrammatical = true; + return guesses; + } + } + } + } + return guesses; +#else + ASSERT_NOT_REACHED(); + UNUSED_PARAM(checkGrammar); + UNUSED_PARAM(misspelled); + UNUSED_PARAM(ungrammatical); + return Vector<String>(); +#endif +} + + +void TextCheckingHelper::markAllMisspellings(RefPtr<Range>& firstMisspellingRange) +{ + // Use the "markAll" feature of findFirstMisspelling. Ignore the return value and the "out parameter"; + // all we need to do is mark every instance. + int ignoredOffset; + findFirstMisspelling(ignoredOffset, true, firstMisspellingRange); +} + +void TextCheckingHelper::markAllBadGrammar() +{ +#ifndef BUILDING_ON_TIGER + // Use the "markAll" feature of findFirstBadGrammar. Ignore the return value and "out parameters"; all we need to + // do is mark every instance. + GrammarDetail ignoredGrammarDetail; + int ignoredOffset; + findFirstBadGrammar(ignoredGrammarDetail, ignoredOffset, true); +#else + ASSERT_NOT_REACHED(); +#endif +} + +} diff --git a/Source/WebCore/editing/TextCheckingHelper.h b/Source/WebCore/editing/TextCheckingHelper.h new file mode 100644 index 0000000..227530f --- /dev/null +++ b/Source/WebCore/editing/TextCheckingHelper.h @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2006, 2007, 2008 Apple Inc. All rights reserved. + * Copyright (C) 2008 Nokia Corporation and/or its subsidiary(-ies) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef TextCheckingHelper_h +#define TextCheckingHelper_h + +#include "EditorClient.h" + +namespace WebCore { + +class Range; +class Position; + +class TextCheckingParagraph { +public: + explicit TextCheckingParagraph(PassRefPtr<Range> checkingRange); + ~TextCheckingParagraph(); + + int rangeLength() const; + PassRefPtr<Range> subrange(int characterOffset, int characterCount) const; + int offsetTo(const Position&, ExceptionCode&) const; + void expandRangeToNextEnd(); + + int textLength() const { return text().length(); } + String textSubstring(unsigned pos, unsigned len = UINT_MAX) const { return text().substring(pos, len); } + const UChar* textCharacters() const { return text().characters(); } + UChar textCharAt(int index) const { return text()[index]; } + + bool isEmpty() const; + bool isTextEmpty() const { return text().isEmpty(); } + bool isRangeEmpty() const { return checkingStart() >= checkingEnd(); } + + int checkingStart() const; + int checkingEnd() const; + int checkingLength() const; + String checkingSubstring() const { return textSubstring(checkingStart(), checkingLength()); } + + bool checkingRangeMatches(int location, int length) const { return location == checkingStart() && length == checkingLength(); } + bool isCheckingRangeCoveredBy(int location, int length) const { return location <= checkingStart() && location + length >= checkingStart() + checkingLength(); } + bool checkingRangeCovers(int location, int length) const { return location < checkingEnd() && location + length > checkingStart(); } + PassRefPtr<Range> paragraphRange() const; + +private: + void invalidateParagraphRangeValues(); + PassRefPtr<Range> checkingRange() const { return m_checkingRange; } + PassRefPtr<Range> offsetAsRange() const; + const String& text() const; + + RefPtr<Range> m_checkingRange; + mutable RefPtr<Range> m_paragraphRange; + mutable RefPtr<Range> m_offsetAsRange; + mutable String m_text; + mutable int m_checkingStart; + mutable int m_checkingEnd; + mutable int m_checkingLength; +}; + +class TextCheckingHelper : public Noncopyable { +public: + TextCheckingHelper(EditorClient*, PassRefPtr<Range>); + ~TextCheckingHelper(); + + String findFirstMisspelling(int& firstMisspellingOffset, bool markAll, RefPtr<Range>& firstMisspellingRange); + String findFirstMisspellingOrBadGrammar(bool checkGrammar, bool& outIsSpelling, int& outFirstFoundOffset, GrammarDetail& outGrammarDetail); + String findFirstBadGrammar(GrammarDetail& outGrammarDetail, int& outGrammarPhraseOffset, bool markAll); + int findFirstGrammarDetail(const Vector<GrammarDetail>& grammarDetails, int badGrammarPhraseLocation, int badGrammarPhraseLength, int startOffset, int endOffset, bool markAll); + void markAllMisspellings(RefPtr<Range>& firstMisspellingRange); + void markAllBadGrammar(); + + bool isUngrammatical(Vector<String>& guessesVector) const; + Vector<String> guessesForMisspelledOrUngrammaticalRange(bool checkGrammar, bool& misspelled, bool& ungrammatical) const; +private: + EditorClient* m_client; + RefPtr<Range> m_range; +}; + +} // namespace WebCore + +#endif // TextCheckingHelper_h diff --git a/Source/WebCore/editing/TextGranularity.h b/Source/WebCore/editing/TextGranularity.h new file mode 100644 index 0000000..09cc4ed --- /dev/null +++ b/Source/WebCore/editing/TextGranularity.h @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2004 Apple Computer, 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef TextGranularity_h +#define TextGranularity_h + +namespace WebCore { + +// FIXME: This really should be broken up into more than one concept. +// Frame doesn't need the 3 boundaries in this enum. +enum TextGranularity { + CharacterGranularity, + WordGranularity, + SentenceGranularity, + LineGranularity, + ParagraphGranularity, + SentenceBoundary, + LineBoundary, + ParagraphBoundary, + DocumentBoundary +}; + +} + +#endif diff --git a/Source/WebCore/editing/TextIterator.cpp b/Source/WebCore/editing/TextIterator.cpp new file mode 100644 index 0000000..7e41420 --- /dev/null +++ b/Source/WebCore/editing/TextIterator.cpp @@ -0,0 +1,2557 @@ +/* + * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010 Apple Inc. All rights reserved. + * Copyright (C) 2005 Alexey Proskuryakov. + * + * 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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 "TextIterator.h" + +#include "CharacterNames.h" +#include "Document.h" +#include "HTMLElement.h" +#include "HTMLNames.h" +#include "htmlediting.h" +#include "InlineTextBox.h" +#include "Range.h" +#include "RenderTableCell.h" +#include "RenderTableRow.h" +#include "RenderTextControl.h" +#include "RenderTextFragment.h" +#include "TextBoundaries.h" +#include "TextBreakIterator.h" +#include "VisiblePosition.h" +#include "visible_units.h" + +#if USE(ICU_UNICODE) && !UCONFIG_NO_COLLATION +#include "TextBreakIteratorInternalICU.h" +#include <unicode/usearch.h> +#endif + +using namespace WTF::Unicode; +using namespace std; + +namespace WebCore { + +using namespace HTMLNames; + +// Buffer that knows how to compare with a search target. +// Keeps enough of the previous text to be able to search in the future, but no more. +// Non-breaking spaces are always equal to normal spaces. +// Case folding is also done if the CaseInsensitive option is specified. +// Matches are further filtered if the AtWordStarts option is specified, although some +// matches inside a word are permitted if TreatMedialCapitalAsWordStart is specified as well. +class SearchBuffer : public Noncopyable { +public: + SearchBuffer(const String& target, FindOptions); + ~SearchBuffer(); + + // Returns number of characters appended; guaranteed to be in the range [1, length]. + size_t append(const UChar*, size_t length); + bool needsMoreContext() const; + void prependContext(const UChar*, size_t length); + void reachedBreak(); + + // Result is the size in characters of what was found. + // And <startOffset> is the number of characters back to the start of what was found. + size_t search(size_t& startOffset); + bool atBreak() const; + +#if USE(ICU_UNICODE) && !UCONFIG_NO_COLLATION + +private: + bool isBadMatch(const UChar*, size_t length) const; + bool isWordStartMatch(size_t start, size_t length) const; + + String m_target; + FindOptions m_options; + + Vector<UChar> m_buffer; + size_t m_overlap; + size_t m_prefixLength; + bool m_atBreak; + bool m_needsMoreContext; + + bool m_targetRequiresKanaWorkaround; + Vector<UChar> m_normalizedTarget; + mutable Vector<UChar> m_normalizedMatch; + +#else + +private: + void append(UChar, bool isCharacterStart); + size_t length() const; + + String m_target; + FindOptions m_options; + + Vector<UChar> m_buffer; + Vector<bool> m_isCharacterStartBuffer; + bool m_isBufferFull; + size_t m_cursor; + +#endif +}; + +// -------- + +static const unsigned bitsInWord = sizeof(unsigned) * 8; +static const unsigned bitInWordMask = bitsInWord - 1; + +BitStack::BitStack() + : m_size(0) +{ +} + +BitStack::~BitStack() +{ +} + +void BitStack::push(bool bit) +{ + unsigned index = m_size / bitsInWord; + unsigned shift = m_size & bitInWordMask; + if (!shift && index == m_words.size()) { + m_words.grow(index + 1); + m_words[index] = 0; + } + unsigned& word = m_words[index]; + unsigned mask = 1U << shift; + if (bit) + word |= mask; + else + word &= ~mask; + ++m_size; +} + +void BitStack::pop() +{ + if (m_size) + --m_size; +} + +bool BitStack::top() const +{ + if (!m_size) + return false; + unsigned shift = (m_size - 1) & bitInWordMask; + return m_words.last() & (1U << shift); +} + +unsigned BitStack::size() const +{ + return m_size; +} + +// -------- + +#if !ASSERT_DISABLED + +static unsigned depthCrossingShadowBoundaries(Node* node) +{ + unsigned depth = 0; + for (Node* parent = node->parentOrHostNode(); parent; parent = parent->parentOrHostNode()) + ++depth; + return depth; +} + +#endif + +// This function is like Range::pastLastNode, except for the fact that it can climb up out of shadow trees. +static Node* nextInPreOrderCrossingShadowBoundaries(Node* rangeEndContainer, int rangeEndOffset) +{ + if (!rangeEndContainer) + return 0; + if (rangeEndOffset >= 0 && !rangeEndContainer->offsetInCharacters()) { + if (Node* next = rangeEndContainer->childNode(rangeEndOffset)) + return next; + } + for (Node* node = rangeEndContainer; node; node = node->parentOrHostNode()) { + if (Node* next = node->nextSibling()) + return next; + } + return 0; +} + +static Node* previousInPostOrderCrossingShadowBoundaries(Node* rangeStartContainer, int rangeStartOffset) +{ + if (!rangeStartContainer) + return 0; + if (rangeStartOffset > 0 && !rangeStartContainer->offsetInCharacters()) { + if (Node* previous = rangeStartContainer->childNode(rangeStartOffset - 1)) + return previous; + } + for (Node* node = rangeStartContainer; node; node = node->parentOrHostNode()) { + if (Node* previous = node->previousSibling()) + return previous; + } + return 0; +} + +// -------- + +static inline bool fullyClipsContents(Node* node) +{ + RenderObject* renderer = node->renderer(); + if (!renderer || !renderer->isBox() || !renderer->hasOverflowClip()) + return false; + return toRenderBox(renderer)->size().isEmpty(); +} + +static inline bool ignoresContainerClip(Node* node) +{ + RenderObject* renderer = node->renderer(); + if (!renderer || renderer->isText()) + return false; + EPosition position = renderer->style()->position(); + return position == AbsolutePosition || position == FixedPosition; +} + +static void pushFullyClippedState(BitStack& stack, Node* node) +{ + ASSERT(stack.size() == depthCrossingShadowBoundaries(node)); + + // Push true if this node full clips its contents, or if a parent already has fully + // clipped and this is not a node that ignores its container's clip. + stack.push(fullyClipsContents(node) || (stack.top() && !ignoresContainerClip(node))); +} + +static void setUpFullyClippedStack(BitStack& stack, Node* node) +{ + // Put the nodes in a vector so we can iterate in reverse order. + Vector<Node*, 100> ancestry; + for (Node* parent = node->parentOrHostNode(); parent; parent = parent->parentOrHostNode()) + ancestry.append(parent); + + // Call pushFullyClippedState on each node starting with the earliest ancestor. + size_t size = ancestry.size(); + for (size_t i = 0; i < size; ++i) + pushFullyClippedState(stack, ancestry[size - i - 1]); + pushFullyClippedState(stack, node); + + ASSERT(stack.size() == 1 + depthCrossingShadowBoundaries(node)); +} + +// -------- + +TextIterator::TextIterator() + : m_startContainer(0) + , m_startOffset(0) + , m_endContainer(0) + , m_endOffset(0) + , m_positionNode(0) + , m_textCharacters(0) + , m_textLength(0) + , m_remainingTextBox(0) + , m_firstLetterText(0) + , m_lastCharacter(0) + , m_emitsCharactersBetweenAllVisiblePositions(false) + , m_entersTextControls(false) + , m_emitsTextWithoutTranscoding(false) + , m_handledFirstLetter(false) + , m_ignoresStyleVisibility(false) +{ +} + +TextIterator::TextIterator(const Range* r, TextIteratorBehavior behavior) + : m_startContainer(0) + , m_startOffset(0) + , m_endContainer(0) + , m_endOffset(0) + , m_positionNode(0) + , m_textCharacters(0) + , m_textLength(0) + , m_remainingTextBox(0) + , m_firstLetterText(0) + , m_emitsCharactersBetweenAllVisiblePositions(behavior & TextIteratorEmitsCharactersBetweenAllVisiblePositions) + , m_entersTextControls(behavior & TextIteratorEntersTextControls) + , m_emitsTextWithoutTranscoding(behavior & TextIteratorEmitsTextsWithoutTranscoding) + , m_handledFirstLetter(false) + , m_ignoresStyleVisibility(behavior & TextIteratorIgnoresStyleVisibility) +{ + // FIXME: should support TextIteratorEndsAtEditingBoundary http://webkit.org/b/43609 + ASSERT(behavior != TextIteratorEndsAtEditingBoundary); + + if (!r) + return; + + // get and validate the range endpoints + Node* startContainer = r->startContainer(); + if (!startContainer) + return; + int startOffset = r->startOffset(); + Node* endContainer = r->endContainer(); + int endOffset = r->endOffset(); + + // Callers should be handing us well-formed ranges. If we discover that this isn't + // the case, we could consider changing this assertion to an early return. + ASSERT(r->boundaryPointsValid()); + + // remember range - this does not change + m_startContainer = startContainer; + m_startOffset = startOffset; + m_endContainer = endContainer; + m_endOffset = endOffset; + + // set up the current node for processing + m_node = r->firstNode(); + if (!m_node) + return; + setUpFullyClippedStack(m_fullyClippedStack, m_node); + m_offset = m_node == m_startContainer ? m_startOffset : 0; + m_handledNode = false; + m_handledChildren = false; + + // calculate first out of bounds node + m_pastEndNode = nextInPreOrderCrossingShadowBoundaries(endContainer, endOffset); + + // initialize node processing state + m_needsAnotherNewline = false; + m_textBox = 0; + + // initialize record of previous node processing + m_hasEmitted = false; + m_lastTextNode = 0; + m_lastTextNodeEndedWithCollapsedSpace = false; + m_lastCharacter = 0; + +#ifndef NDEBUG + // need this just because of the assert in advance() + m_positionNode = m_node; +#endif + + // identify the first run + advance(); +} + +TextIterator::~TextIterator() +{ +} + +void TextIterator::advance() +{ + // reset the run information + m_positionNode = 0; + m_textLength = 0; + + // handle remembered node that needed a newline after the text node's newline + if (m_needsAnotherNewline) { + // Emit the extra newline, and position it *inside* m_node, after m_node's + // contents, in case it's a block, in the same way that we position the first + // newline. The range for the emitted newline should start where the line + // break begins. + // FIXME: It would be cleaner if we emitted two newlines during the last + // iteration, instead of using m_needsAnotherNewline. + Node* baseNode = m_node->lastChild() ? m_node->lastChild() : m_node; + emitCharacter('\n', baseNode->parentNode(), baseNode, 1, 1); + m_needsAnotherNewline = false; + return; + } + + if (!m_textBox && m_remainingTextBox) { + m_textBox = m_remainingTextBox; + m_remainingTextBox = 0; + m_firstLetterText = 0; + m_offset = 0; + } + // handle remembered text box + if (m_textBox) { + handleTextBox(); + if (m_positionNode) + return; + } + + while (m_node && m_node != m_pastEndNode) { + // if the range ends at offset 0 of an element, represent the + // position, but not the content, of that element e.g. if the + // node is a blockflow element, emit a newline that + // precedes the element + if (m_node == m_endContainer && m_endOffset == 0) { + representNodeOffsetZero(); + m_node = 0; + return; + } + + RenderObject* renderer = m_node->renderer(); + if (!renderer) { + m_handledNode = true; + m_handledChildren = true; + } else { + // handle current node according to its type + if (!m_handledNode) { + if (renderer->isText() && m_node->nodeType() == Node::TEXT_NODE) // FIXME: What about CDATA_SECTION_NODE? + m_handledNode = handleTextNode(); + else if (renderer && (renderer->isImage() || renderer->isWidget() || + (renderer->node() && renderer->node()->isElementNode() && + static_cast<Element*>(renderer->node())->isFormControlElement()))) + m_handledNode = handleReplacedElement(); + else + m_handledNode = handleNonTextNode(); + if (m_positionNode) + return; + } + } + + // find a new current node to handle in depth-first manner, + // calling exitNode() as we come back thru a parent node + Node* next = m_handledChildren ? 0 : m_node->firstChild(); + m_offset = 0; + if (!next) { + next = m_node->nextSibling(); + if (!next) { + bool pastEnd = m_node->traverseNextNode() == m_pastEndNode; + Node* parentNode = m_node->parentOrHostNode(); + while (!next && parentNode) { + if ((pastEnd && parentNode == m_endContainer) || m_endContainer->isDescendantOf(parentNode)) + return; + bool haveRenderer = m_node->renderer(); + m_node = parentNode; + m_fullyClippedStack.pop(); + parentNode = m_node->parentOrHostNode(); + if (haveRenderer) + exitNode(); + if (m_positionNode) { + m_handledNode = true; + m_handledChildren = true; + return; + } + next = m_node->nextSibling(); + } + } + m_fullyClippedStack.pop(); + } + + // set the new current node + m_node = next; + if (m_node) + pushFullyClippedState(m_fullyClippedStack, m_node); + m_handledNode = false; + m_handledChildren = false; + m_handledFirstLetter = false; + m_firstLetterText = 0; + + // how would this ever be? + if (m_positionNode) + return; + } +} + +bool TextIterator::handleTextNode() +{ + if (m_fullyClippedStack.top() && !m_ignoresStyleVisibility) + return false; + + RenderText* renderer = toRenderText(m_node->renderer()); + + m_lastTextNode = m_node; + String str = renderer->text(); + + // handle pre-formatted text + if (!renderer->style()->collapseWhiteSpace()) { + int runStart = m_offset; + if (m_lastTextNodeEndedWithCollapsedSpace && hasVisibleTextNode(renderer)) { + emitCharacter(' ', m_node, 0, runStart, runStart); + return false; + } + if (!m_handledFirstLetter && renderer->isTextFragment()) { + handleTextNodeFirstLetter(static_cast<RenderTextFragment*>(renderer)); + if (m_firstLetterText) { + String firstLetter = m_firstLetterText->text(); + emitText(m_node, m_firstLetterText, m_offset, m_offset + firstLetter.length()); + m_firstLetterText = 0; + m_textBox = 0; + return false; + } + } + if (renderer->style()->visibility() != VISIBLE && !m_ignoresStyleVisibility) + return false; + int strLength = str.length(); + int end = (m_node == m_endContainer) ? m_endOffset : INT_MAX; + int runEnd = min(strLength, end); + + if (runStart >= runEnd) + return true; + + emitText(m_node, runStart, runEnd); + return true; + } + + if (!renderer->firstTextBox() && str.length() > 0) { + if (!m_handledFirstLetter && renderer->isTextFragment()) { + handleTextNodeFirstLetter(static_cast<RenderTextFragment*>(renderer)); + if (m_firstLetterText) { + handleTextBox(); + return false; + } + } + if (renderer->style()->visibility() != VISIBLE && !m_ignoresStyleVisibility) + return false; + m_lastTextNodeEndedWithCollapsedSpace = true; // entire block is collapsed space + return true; + } + + // Used when text boxes are out of order (Hebrew/Arabic w/ embeded LTR text) + if (renderer->containsReversedText()) { + m_sortedTextBoxes.clear(); + for (InlineTextBox* textBox = renderer->firstTextBox(); textBox; textBox = textBox->nextTextBox()) { + m_sortedTextBoxes.append(textBox); + } + std::sort(m_sortedTextBoxes.begin(), m_sortedTextBoxes.end(), InlineTextBox::compareByStart); + m_sortedTextBoxesPosition = 0; + } + + m_textBox = renderer->containsReversedText() ? (m_sortedTextBoxes.isEmpty() ? 0 : m_sortedTextBoxes[0]) : renderer->firstTextBox(); + if (!m_handledFirstLetter && renderer->isTextFragment() && !m_offset) + handleTextNodeFirstLetter(static_cast<RenderTextFragment*>(renderer)); + handleTextBox(); + return true; +} + +void TextIterator::handleTextBox() +{ + RenderText* renderer = m_firstLetterText ? m_firstLetterText : toRenderText(m_node->renderer()); + if (renderer->style()->visibility() != VISIBLE && !m_ignoresStyleVisibility) { + m_textBox = 0; + return; + } + String str = renderer->text(); + unsigned start = m_offset; + unsigned end = (m_node == m_endContainer) ? static_cast<unsigned>(m_endOffset) : UINT_MAX; + while (m_textBox) { + unsigned textBoxStart = m_textBox->start(); + unsigned runStart = max(textBoxStart, start); + + // Check for collapsed space at the start of this run. + InlineTextBox* firstTextBox = renderer->containsReversedText() ? m_sortedTextBoxes[0] : renderer->firstTextBox(); + bool needSpace = m_lastTextNodeEndedWithCollapsedSpace + || (m_textBox == firstTextBox && textBoxStart == runStart && runStart > 0); + if (needSpace && !isCollapsibleWhitespace(m_lastCharacter) && m_lastCharacter) { + if (m_lastTextNode == m_node && runStart > 0 && str[runStart - 1] == ' ') { + unsigned spaceRunStart = runStart - 1; + while (spaceRunStart > 0 && str[spaceRunStart - 1] == ' ') + --spaceRunStart; + emitText(m_node, spaceRunStart, spaceRunStart + 1); + } else + emitCharacter(' ', m_node, 0, runStart, runStart); + return; + } + unsigned textBoxEnd = textBoxStart + m_textBox->len(); + unsigned runEnd = min(textBoxEnd, end); + + // Determine what the next text box will be, but don't advance yet + InlineTextBox* nextTextBox = 0; + if (renderer->containsReversedText()) { + if (m_sortedTextBoxesPosition + 1 < m_sortedTextBoxes.size()) + nextTextBox = m_sortedTextBoxes[m_sortedTextBoxesPosition + 1]; + } else + nextTextBox = m_textBox->nextTextBox(); + + if (runStart < runEnd) { + // Handle either a single newline character (which becomes a space), + // or a run of characters that does not include a newline. + // This effectively translates newlines to spaces without copying the text. + if (str[runStart] == '\n') { + emitCharacter(' ', m_node, 0, runStart, runStart + 1); + m_offset = runStart + 1; + } else { + size_t subrunEnd = str.find('\n', runStart); + if (subrunEnd == notFound || subrunEnd > runEnd) + subrunEnd = runEnd; + + m_offset = subrunEnd; + emitText(m_node, renderer, runStart, subrunEnd); + } + + // If we are doing a subrun that doesn't go to the end of the text box, + // come back again to finish handling this text box; don't advance to the next one. + if (static_cast<unsigned>(m_positionEndOffset) < textBoxEnd) + return; + + // Advance and return + unsigned nextRunStart = nextTextBox ? nextTextBox->start() : str.length(); + if (nextRunStart > runEnd) + m_lastTextNodeEndedWithCollapsedSpace = true; // collapsed space between runs or at the end + m_textBox = nextTextBox; + if (renderer->containsReversedText()) + ++m_sortedTextBoxesPosition; + return; + } + // Advance and continue + m_textBox = nextTextBox; + if (renderer->containsReversedText()) + ++m_sortedTextBoxesPosition; + } + if (!m_textBox && m_remainingTextBox) { + m_textBox = m_remainingTextBox; + m_remainingTextBox = 0; + m_firstLetterText = 0; + m_offset = 0; + handleTextBox(); + } +} + +void TextIterator::handleTextNodeFirstLetter(RenderTextFragment* renderer) +{ + if (renderer->firstLetter()) { + RenderObject* r = renderer->firstLetter(); + if (r->style()->visibility() != VISIBLE && !m_ignoresStyleVisibility) + return; + for (RenderObject *currChild = r->firstChild(); currChild; currChild->nextSibling()) { + if (currChild->isText()) { + RenderText* firstLetter = toRenderText(currChild); + m_handledFirstLetter = true; + m_remainingTextBox = m_textBox; + m_textBox = firstLetter->firstTextBox(); + m_firstLetterText = firstLetter; + return; + } + } + } + m_handledFirstLetter = true; +} + +bool TextIterator::handleReplacedElement() +{ + if (m_fullyClippedStack.top()) + return false; + + RenderObject* renderer = m_node->renderer(); + if (renderer->style()->visibility() != VISIBLE && !m_ignoresStyleVisibility) + return false; + + if (m_lastTextNodeEndedWithCollapsedSpace) { + emitCharacter(' ', m_lastTextNode->parentNode(), m_lastTextNode, 1, 1); + return false; + } + + if (m_entersTextControls && renderer->isTextControl()) { + if (HTMLElement* innerTextElement = toRenderTextControl(renderer)->innerTextElement()) { + m_node = innerTextElement->shadowTreeRootNode(); + pushFullyClippedState(m_fullyClippedStack, m_node); + m_offset = 0; + return false; + } + } + + m_hasEmitted = true; + + if (m_emitsCharactersBetweenAllVisiblePositions) { + // We want replaced elements to behave like punctuation for boundary + // finding, and to simply take up space for the selection preservation + // code in moveParagraphs, so we use a comma. + emitCharacter(',', m_node->parentNode(), m_node, 0, 1); + return true; + } + + m_positionNode = m_node->parentNode(); + m_positionOffsetBaseNode = m_node; + m_positionStartOffset = 0; + m_positionEndOffset = 1; + + m_textCharacters = 0; + m_textLength = 0; + + m_lastCharacter = 0; + + return true; +} + +bool TextIterator::hasVisibleTextNode(RenderText* renderer) +{ + if (renderer->style()->visibility() == VISIBLE) + return true; + if (renderer->isTextFragment()) { + RenderTextFragment* fragment = static_cast<RenderTextFragment*>(renderer); + if (fragment->firstLetter() && fragment->firstLetter()->style()->visibility() == VISIBLE) + return true; + } + return false; +} + +static bool shouldEmitTabBeforeNode(Node* node) +{ + RenderObject* r = node->renderer(); + + // Table cells are delimited by tabs. + if (!r || !isTableCell(node)) + return false; + + // Want a tab before every cell other than the first one + RenderTableCell* rc = toRenderTableCell(r); + RenderTable* t = rc->table(); + return t && (t->cellBefore(rc) || t->cellAbove(rc)); +} + +static bool shouldEmitNewlineForNode(Node* node) +{ + // br elements are represented by a single newline. + RenderObject* r = node->renderer(); + if (!r) + return node->hasTagName(brTag); + + return r->isBR(); +} + +static bool shouldEmitNewlinesBeforeAndAfterNode(Node* node) +{ + // Block flow (versus inline flow) is represented by having + // a newline both before and after the element. + RenderObject* r = node->renderer(); + if (!r) { + return (node->hasTagName(blockquoteTag) + || node->hasTagName(ddTag) + || node->hasTagName(divTag) + || node->hasTagName(dlTag) + || node->hasTagName(dtTag) + || node->hasTagName(h1Tag) + || node->hasTagName(h2Tag) + || node->hasTagName(h3Tag) + || node->hasTagName(h4Tag) + || node->hasTagName(h5Tag) + || node->hasTagName(h6Tag) + || node->hasTagName(hrTag) + || node->hasTagName(liTag) + || node->hasTagName(listingTag) + || node->hasTagName(olTag) + || node->hasTagName(pTag) + || node->hasTagName(preTag) + || node->hasTagName(trTag) + || node->hasTagName(ulTag)); + } + + // Need to make an exception for table cells, because they are blocks, but we + // want them tab-delimited rather than having newlines before and after. + if (isTableCell(node)) + return false; + + // Need to make an exception for table row elements, because they are neither + // "inline" or "RenderBlock", but we want newlines for them. + if (r->isTableRow()) { + RenderTable* t = toRenderTableRow(r)->table(); + if (t && !t->isInline()) + return true; + } + + return !r->isInline() && r->isRenderBlock() && !r->isFloatingOrPositioned() && !r->isBody(); +} + +static bool shouldEmitNewlineAfterNode(Node* node) +{ + // FIXME: It should be better but slower to create a VisiblePosition here. + if (!shouldEmitNewlinesBeforeAndAfterNode(node)) + return false; + // Check if this is the very last renderer in the document. + // If so, then we should not emit a newline. + while ((node = node->traverseNextSibling())) + if (node->renderer()) + return true; + return false; +} + +static bool shouldEmitNewlineBeforeNode(Node* node) +{ + return shouldEmitNewlinesBeforeAndAfterNode(node); +} + +static bool shouldEmitExtraNewlineForNode(Node* node) +{ + // When there is a significant collapsed bottom margin, emit an extra + // newline for a more realistic result. We end up getting the right + // result even without margin collapsing. For example: <div><p>text</p></div> + // will work right even if both the <div> and the <p> have bottom margins. + RenderObject* r = node->renderer(); + if (!r || !r->isBox()) + return false; + + // NOTE: We only do this for a select set of nodes, and fwiw WinIE appears + // not to do this at all + if (node->hasTagName(h1Tag) + || node->hasTagName(h2Tag) + || node->hasTagName(h3Tag) + || node->hasTagName(h4Tag) + || node->hasTagName(h5Tag) + || node->hasTagName(h6Tag) + || node->hasTagName(pTag)) { + RenderStyle* style = r->style(); + if (style) { + int bottomMargin = toRenderBox(r)->collapsedMarginAfter(); + int fontSize = style->fontDescription().computedPixelSize(); + if (bottomMargin * 2 >= fontSize) + return true; + } + } + + return false; +} + +static int collapsedSpaceLength(RenderText* renderer, int textEnd) +{ + const UChar* characters = renderer->text()->characters(); + int length = renderer->text()->length(); + for (int i = textEnd; i < length; ++i) { + if (!renderer->style()->isCollapsibleWhiteSpace(characters[i])) + return i - textEnd; + } + + return length - textEnd; +} + +static int maxOffsetIncludingCollapsedSpaces(Node* node) +{ + int offset = caretMaxOffset(node); + + if (node->renderer() && node->renderer()->isText()) + offset += collapsedSpaceLength(toRenderText(node->renderer()), offset); + + return offset; +} + +// Whether or not we should emit a character as we enter m_node (if it's a container) or as we hit it (if it's atomic). +bool TextIterator::shouldRepresentNodeOffsetZero() +{ + if (m_emitsCharactersBetweenAllVisiblePositions && m_node->renderer() && m_node->renderer()->isTable()) + return true; + + // Leave element positioned flush with start of a paragraph + // (e.g. do not insert tab before a table cell at the start of a paragraph) + if (m_lastCharacter == '\n') + return false; + + // Otherwise, show the position if we have emitted any characters + if (m_hasEmitted) + return true; + + // We've not emitted anything yet. Generally, there is no need for any positioning then. + // The only exception is when the element is visually not in the same line as + // the start of the range (e.g. the range starts at the end of the previous paragraph). + // NOTE: Creating VisiblePositions and comparing them is relatively expensive, so we + // make quicker checks to possibly avoid that. Another check that we could make is + // is whether the inline vs block flow changed since the previous visible element. + // I think we're already in a special enough case that that won't be needed, tho. + + // No character needed if this is the first node in the range. + if (m_node == m_startContainer) + return false; + + // If we are outside the start container's subtree, assume we need to emit. + // FIXME: m_startContainer could be an inline block + if (!m_node->isDescendantOf(m_startContainer)) + return true; + + // If we started as m_startContainer offset 0 and the current node is a descendant of + // the start container, we already had enough context to correctly decide whether to + // emit after a preceding block. We chose not to emit (m_hasEmitted is false), + // so don't second guess that now. + // NOTE: Is this really correct when m_node is not a leftmost descendant? Probably + // immaterial since we likely would have already emitted something by now. + if (m_startOffset == 0) + return false; + + // If this node is unrendered or invisible the VisiblePosition checks below won't have much meaning. + // Additionally, if the range we are iterating over contains huge sections of unrendered content, + // we would create VisiblePositions on every call to this function without this check. + if (!m_node->renderer() || m_node->renderer()->style()->visibility() != VISIBLE) + return false; + + // The startPos.isNotNull() check is needed because the start could be before the body, + // and in that case we'll get null. We don't want to put in newlines at the start in that case. + // The currPos.isNotNull() check is needed because positions in non-HTML content + // (like SVG) do not have visible positions, and we don't want to emit for them either. + VisiblePosition startPos = VisiblePosition(m_startContainer, m_startOffset, DOWNSTREAM); + VisiblePosition currPos = VisiblePosition(m_node, 0, DOWNSTREAM); + return startPos.isNotNull() && currPos.isNotNull() && !inSameLine(startPos, currPos); +} + +bool TextIterator::shouldEmitSpaceBeforeAndAfterNode(Node* node) +{ + return node->renderer() && node->renderer()->isTable() && (node->renderer()->isInline() || m_emitsCharactersBetweenAllVisiblePositions); +} + +void TextIterator::representNodeOffsetZero() +{ + // Emit a character to show the positioning of m_node. + + // When we haven't been emitting any characters, shouldRepresentNodeOffsetZero() can + // create VisiblePositions, which is expensive. So, we perform the inexpensive checks + // on m_node to see if it necessitates emitting a character first and will early return + // before encountering shouldRepresentNodeOffsetZero()s worse case behavior. + if (shouldEmitTabBeforeNode(m_node)) { + if (shouldRepresentNodeOffsetZero()) + emitCharacter('\t', m_node->parentNode(), m_node, 0, 0); + } else if (shouldEmitNewlineBeforeNode(m_node)) { + if (shouldRepresentNodeOffsetZero()) + emitCharacter('\n', m_node->parentNode(), m_node, 0, 0); + } else if (shouldEmitSpaceBeforeAndAfterNode(m_node)) { + if (shouldRepresentNodeOffsetZero()) + emitCharacter(' ', m_node->parentNode(), m_node, 0, 0); + } +} + +bool TextIterator::handleNonTextNode() +{ + if (shouldEmitNewlineForNode(m_node)) + emitCharacter('\n', m_node->parentNode(), m_node, 0, 1); + else if (m_emitsCharactersBetweenAllVisiblePositions && m_node->renderer() && m_node->renderer()->isHR()) + emitCharacter(' ', m_node->parentNode(), m_node, 0, 1); + else + representNodeOffsetZero(); + + return true; +} + +void TextIterator::exitNode() +{ + // prevent emitting a newline when exiting a collapsed block at beginning of the range + // FIXME: !m_hasEmitted does not necessarily mean there was a collapsed block... it could + // have been an hr (e.g.). Also, a collapsed block could have height (e.g. a table) and + // therefore look like a blank line. + if (!m_hasEmitted) + return; + + // Emit with a position *inside* m_node, after m_node's contents, in + // case it is a block, because the run should start where the + // emitted character is positioned visually. + Node* baseNode = m_node->lastChild() ? m_node->lastChild() : m_node; + // FIXME: This shouldn't require the m_lastTextNode to be true, but we can't change that without making + // the logic in _web_attributedStringFromRange match. We'll get that for free when we switch to use + // TextIterator in _web_attributedStringFromRange. + // See <rdar://problem/5428427> for an example of how this mismatch will cause problems. + if (m_lastTextNode && shouldEmitNewlineAfterNode(m_node)) { + // use extra newline to represent margin bottom, as needed + bool addNewline = shouldEmitExtraNewlineForNode(m_node); + + // FIXME: We need to emit a '\n' as we leave an empty block(s) that + // contain a VisiblePosition when doing selection preservation. + if (m_lastCharacter != '\n') { + // insert a newline with a position following this block's contents. + emitCharacter('\n', baseNode->parentNode(), baseNode, 1, 1); + // remember whether to later add a newline for the current node + ASSERT(!m_needsAnotherNewline); + m_needsAnotherNewline = addNewline; + } else if (addNewline) + // insert a newline with a position following this block's contents. + emitCharacter('\n', baseNode->parentNode(), baseNode, 1, 1); + } + + // If nothing was emitted, see if we need to emit a space. + if (!m_positionNode && shouldEmitSpaceBeforeAndAfterNode(m_node)) + emitCharacter(' ', baseNode->parentNode(), baseNode, 1, 1); +} + +void TextIterator::emitCharacter(UChar c, Node* textNode, Node* offsetBaseNode, int textStartOffset, int textEndOffset) +{ + m_hasEmitted = true; + + // remember information with which to construct the TextIterator::range() + // NOTE: textNode is often not a text node, so the range will specify child nodes of positionNode + m_positionNode = textNode; + m_positionOffsetBaseNode = offsetBaseNode; + m_positionStartOffset = textStartOffset; + m_positionEndOffset = textEndOffset; + + // remember information with which to construct the TextIterator::characters() and length() + m_singleCharacterBuffer = c; + m_textCharacters = &m_singleCharacterBuffer; + m_textLength = 1; + + // remember some iteration state + m_lastTextNodeEndedWithCollapsedSpace = false; + m_lastCharacter = c; +} + +void TextIterator::emitText(Node* textNode, RenderObject* renderObject, int textStartOffset, int textEndOffset) +{ + RenderText* renderer = toRenderText(renderObject); + m_text = m_emitsTextWithoutTranscoding ? renderer->textWithoutTranscoding() : renderer->text(); + ASSERT(m_text.characters()); + + m_positionNode = textNode; + m_positionOffsetBaseNode = 0; + m_positionStartOffset = textStartOffset; + m_positionEndOffset = textEndOffset; + m_textCharacters = m_text.characters() + textStartOffset; + m_textLength = textEndOffset - textStartOffset; + m_lastCharacter = m_text[textEndOffset - 1]; + + m_lastTextNodeEndedWithCollapsedSpace = false; + m_hasEmitted = true; +} + +void TextIterator::emitText(Node* textNode, int textStartOffset, int textEndOffset) +{ + emitText(textNode, m_node->renderer(), textStartOffset, textEndOffset); +} + +PassRefPtr<Range> TextIterator::range() const +{ + // use the current run information, if we have it + if (m_positionNode) { + if (m_positionOffsetBaseNode) { + int index = m_positionOffsetBaseNode->nodeIndex(); + m_positionStartOffset += index; + m_positionEndOffset += index; + m_positionOffsetBaseNode = 0; + } + return Range::create(m_positionNode->document(), m_positionNode, m_positionStartOffset, m_positionNode, m_positionEndOffset); + } + + // otherwise, return the end of the overall range we were given + if (m_endContainer) + return Range::create(m_endContainer->document(), m_endContainer, m_endOffset, m_endContainer, m_endOffset); + + return 0; +} + +Node* TextIterator::node() const +{ + RefPtr<Range> textRange = range(); + if (!textRange) + return 0; + + Node* node = textRange->startContainer(); + if (!node) + return 0; + if (node->offsetInCharacters()) + return node; + + return node->childNode(textRange->startOffset()); +} + +// -------- + +SimplifiedBackwardsTextIterator::SimplifiedBackwardsTextIterator() + : m_behavior(TextIteratorDefaultBehavior) + , m_node(0) + , m_positionNode(0) +{ +} + +SimplifiedBackwardsTextIterator::SimplifiedBackwardsTextIterator(const Range* r, TextIteratorBehavior behavior) + : m_behavior(behavior) + , m_node(0) + , m_positionNode(0) +{ + ASSERT(m_behavior == TextIteratorDefaultBehavior || m_behavior == TextIteratorEndsAtEditingBoundary); + + if (!r) + return; + + Node* startNode = r->startContainer(); + if (!startNode) + return; + Node* endNode = r->endContainer(); + int startOffset = r->startOffset(); + int endOffset = r->endOffset(); + + if (!startNode->offsetInCharacters()) { + if (startOffset >= 0 && startOffset < static_cast<int>(startNode->childNodeCount())) { + startNode = startNode->childNode(startOffset); + startOffset = 0; + } + } + if (!endNode->offsetInCharacters()) { + if (endOffset > 0 && endOffset <= static_cast<int>(endNode->childNodeCount())) { + endNode = endNode->childNode(endOffset - 1); + endOffset = lastOffsetInNode(endNode); + } + } + + setCurrentNode(endNode); + setUpFullyClippedStack(m_fullyClippedStack, m_node); + m_offset = endOffset; + m_handledNode = false; + m_handledChildren = endOffset == 0; + + m_startNode = startNode; + m_startOffset = startOffset; + m_endNode = endNode; + m_endOffset = endOffset; + +#ifndef NDEBUG + // Need this just because of the assert. + m_positionNode = endNode; +#endif + + m_lastTextNode = 0; + m_lastCharacter = '\n'; + + m_pastStartNode = previousInPostOrderCrossingShadowBoundaries(startNode, startOffset); + + advance(); +} + +void SimplifiedBackwardsTextIterator::advance() +{ + ASSERT(m_positionNode); + + m_positionNode = 0; + m_textLength = 0; + + while (m_node && m_node != m_pastStartNode) { + // Don't handle node if we start iterating at [node, 0]. + if (!m_handledNode && !(m_node == m_endNode && m_endOffset == 0)) { + RenderObject* renderer = m_node->renderer(); + if (renderer && renderer->isText() && m_node->nodeType() == Node::TEXT_NODE) { + // FIXME: What about CDATA_SECTION_NODE? + if (renderer->style()->visibility() == VISIBLE && m_offset > 0) + m_handledNode = handleTextNode(); + } else if (renderer && (renderer->isImage() || renderer->isWidget())) { + if (renderer->style()->visibility() == VISIBLE && m_offset > 0) + m_handledNode = handleReplacedElement(); + } else + m_handledNode = handleNonTextNode(); + if (m_positionNode) + return; + } + + Node* next = m_handledChildren ? 0 : m_node->lastChild(); + if (!next) { + // Exit empty containers as we pass over them or containers + // where [container, 0] is where we started iterating. + if (!m_handledNode && + canHaveChildrenForEditing(m_node) && + m_node->parentNode() && + (!m_node->lastChild() || (m_node == m_endNode && m_endOffset == 0))) { + exitNode(); + if (m_positionNode) { + m_handledNode = true; + m_handledChildren = true; + return; + } + } + // Exit all other containers. + while (!m_node->previousSibling()) { + if (!setCurrentNode(m_node->parentOrHostNode())) + break; + m_fullyClippedStack.pop(); + exitNode(); + if (m_positionNode) { + m_handledNode = true; + m_handledChildren = true; + return; + } + } + + next = m_node->previousSibling(); + m_fullyClippedStack.pop(); + } + + if (m_node && setCurrentNode(next)) + pushFullyClippedState(m_fullyClippedStack, m_node); + else + clearCurrentNode(); + + // For the purpose of word boundary detection, + // we should iterate all visible text and trailing (collapsed) whitespaces. + m_offset = m_node ? maxOffsetIncludingCollapsedSpaces(m_node) : 0; + m_handledNode = false; + m_handledChildren = false; + + if (m_positionNode) + return; + } +} + +bool SimplifiedBackwardsTextIterator::handleTextNode() +{ + m_lastTextNode = m_node; + + RenderText* renderer = toRenderText(m_node->renderer()); + String str = renderer->text(); + + if (!renderer->firstTextBox() && str.length() > 0) + return true; + + m_positionEndOffset = m_offset; + + m_offset = (m_node == m_startNode) ? m_startOffset : 0; + m_positionNode = m_node; + m_positionStartOffset = m_offset; + m_textLength = m_positionEndOffset - m_positionStartOffset; + m_textCharacters = str.characters() + m_positionStartOffset; + + m_lastCharacter = str[m_positionEndOffset - 1]; + + return true; +} + +bool SimplifiedBackwardsTextIterator::handleReplacedElement() +{ + unsigned index = m_node->nodeIndex(); + // We want replaced elements to behave like punctuation for boundary + // finding, and to simply take up space for the selection preservation + // code in moveParagraphs, so we use a comma. Unconditionally emit + // here because this iterator is only used for boundary finding. + emitCharacter(',', m_node->parentNode(), index, index + 1); + return true; +} + +bool SimplifiedBackwardsTextIterator::handleNonTextNode() +{ + // We can use a linefeed in place of a tab because this simple iterator is only used to + // find boundaries, not actual content. A linefeed breaks words, sentences, and paragraphs. + if (shouldEmitNewlineForNode(m_node) || shouldEmitNewlineAfterNode(m_node) || shouldEmitTabBeforeNode(m_node)) { + unsigned index = m_node->nodeIndex(); + // The start of this emitted range is wrong. Ensuring correctness would require + // VisiblePositions and so would be slow. previousBoundary expects this. + emitCharacter('\n', m_node->parentNode(), index + 1, index + 1); + } + return true; +} + +void SimplifiedBackwardsTextIterator::exitNode() +{ + if (shouldEmitNewlineForNode(m_node) || shouldEmitNewlineBeforeNode(m_node) || shouldEmitTabBeforeNode(m_node)) { + // The start of this emitted range is wrong. Ensuring correctness would require + // VisiblePositions and so would be slow. previousBoundary expects this. + emitCharacter('\n', m_node, 0, 0); + } +} + +void SimplifiedBackwardsTextIterator::emitCharacter(UChar c, Node* node, int startOffset, int endOffset) +{ + m_singleCharacterBuffer = c; + m_positionNode = node; + m_positionStartOffset = startOffset; + m_positionEndOffset = endOffset; + m_textCharacters = &m_singleCharacterBuffer; + m_textLength = 1; + m_lastCharacter = c; +} + +bool SimplifiedBackwardsTextIterator::crossesEditingBoundary(Node* node) const +{ + return m_node && m_node->isContentEditable() != node->isContentEditable(); +} + +bool SimplifiedBackwardsTextIterator::setCurrentNode(Node* node) +{ + if (!node) + return false; + if (m_behavior == TextIteratorEndsAtEditingBoundary && crossesEditingBoundary(node)) + return false; + m_node = node; + return true; +} + +void SimplifiedBackwardsTextIterator::clearCurrentNode() +{ + m_node = 0; +} + +PassRefPtr<Range> SimplifiedBackwardsTextIterator::range() const +{ + if (m_positionNode) + return Range::create(m_positionNode->document(), m_positionNode, m_positionStartOffset, m_positionNode, m_positionEndOffset); + + return Range::create(m_startNode->document(), m_startNode, m_startOffset, m_startNode, m_startOffset); +} + +// -------- + +CharacterIterator::CharacterIterator() + : m_offset(0) + , m_runOffset(0) + , m_atBreak(true) +{ +} + +CharacterIterator::CharacterIterator(const Range* r, TextIteratorBehavior behavior) + : m_offset(0) + , m_runOffset(0) + , m_atBreak(true) + , m_textIterator(r, behavior) +{ + while (!atEnd() && m_textIterator.length() == 0) + m_textIterator.advance(); +} + +PassRefPtr<Range> CharacterIterator::range() const +{ + RefPtr<Range> r = m_textIterator.range(); + if (!m_textIterator.atEnd()) { + if (m_textIterator.length() <= 1) { + ASSERT(m_runOffset == 0); + } else { + Node* n = r->startContainer(); + ASSERT(n == r->endContainer()); + int offset = r->startOffset() + m_runOffset; + ExceptionCode ec = 0; + r->setStart(n, offset, ec); + r->setEnd(n, offset + 1, ec); + ASSERT(!ec); + } + } + return r.release(); +} + +void CharacterIterator::advance(int count) +{ + if (count <= 0) { + ASSERT(count == 0); + return; + } + + m_atBreak = false; + + // easy if there is enough left in the current m_textIterator run + int remaining = m_textIterator.length() - m_runOffset; + if (count < remaining) { + m_runOffset += count; + m_offset += count; + return; + } + + // exhaust the current m_textIterator run + count -= remaining; + m_offset += remaining; + + // move to a subsequent m_textIterator run + for (m_textIterator.advance(); !atEnd(); m_textIterator.advance()) { + int runLength = m_textIterator.length(); + if (runLength == 0) + m_atBreak = true; + else { + // see whether this is m_textIterator to use + if (count < runLength) { + m_runOffset = count; + m_offset += count; + return; + } + + // exhaust this m_textIterator run + count -= runLength; + m_offset += runLength; + } + } + + // ran to the end of the m_textIterator... no more runs left + m_atBreak = true; + m_runOffset = 0; +} + +String CharacterIterator::string(int numChars) +{ + Vector<UChar> result; + result.reserveInitialCapacity(numChars); + while (numChars > 0 && !atEnd()) { + int runSize = min(numChars, length()); + result.append(characters(), runSize); + numChars -= runSize; + advance(runSize); + } + return String::adopt(result); +} + +static PassRefPtr<Range> characterSubrange(CharacterIterator& it, int offset, int length) +{ + it.advance(offset); + RefPtr<Range> start = it.range(); + + if (length > 1) + it.advance(length - 1); + RefPtr<Range> end = it.range(); + + return Range::create(start->startContainer()->document(), + start->startContainer(), start->startOffset(), + end->endContainer(), end->endOffset()); +} + +BackwardsCharacterIterator::BackwardsCharacterIterator() + : m_offset(0) + , m_runOffset(0) + , m_atBreak(true) +{ +} + +BackwardsCharacterIterator::BackwardsCharacterIterator(const Range* range, TextIteratorBehavior behavior) + : m_offset(0) + , m_runOffset(0) + , m_atBreak(true) + , m_textIterator(range, behavior) +{ + while (!atEnd() && !m_textIterator.length()) + m_textIterator.advance(); +} + +PassRefPtr<Range> BackwardsCharacterIterator::range() const +{ + RefPtr<Range> r = m_textIterator.range(); + if (!m_textIterator.atEnd()) { + if (m_textIterator.length() <= 1) + ASSERT(m_runOffset == 0); + else { + Node* n = r->startContainer(); + ASSERT(n == r->endContainer()); + int offset = r->endOffset() - m_runOffset; + ExceptionCode ec = 0; + r->setStart(n, offset - 1, ec); + r->setEnd(n, offset, ec); + ASSERT(!ec); + } + } + return r.release(); +} + +void BackwardsCharacterIterator::advance(int count) +{ + if (count <= 0) { + ASSERT(!count); + return; + } + + m_atBreak = false; + + int remaining = m_textIterator.length() - m_runOffset; + if (count < remaining) { + m_runOffset += count; + m_offset += count; + return; + } + + count -= remaining; + m_offset += remaining; + + for (m_textIterator.advance(); !atEnd(); m_textIterator.advance()) { + int runLength = m_textIterator.length(); + if (runLength == 0) + m_atBreak = true; + else { + if (count < runLength) { + m_runOffset = count; + m_offset += count; + return; + } + + count -= runLength; + m_offset += runLength; + } + } + + m_atBreak = true; + m_runOffset = 0; +} + +// -------- + +WordAwareIterator::WordAwareIterator() + : m_previousText(0) + , m_didLookAhead(false) +{ +} + +WordAwareIterator::WordAwareIterator(const Range* r) + : m_previousText(0) + , m_didLookAhead(true) // so we consider the first chunk from the text iterator + , m_textIterator(r) +{ + advance(); // get in position over the first chunk of text +} + +WordAwareIterator::~WordAwareIterator() +{ +} + +// We're always in one of these modes: +// - The current chunk in the text iterator is our current chunk +// (typically its a piece of whitespace, or text that ended with whitespace) +// - The previous chunk in the text iterator is our current chunk +// (we looked ahead to the next chunk and found a word boundary) +// - We built up our own chunk of text from many chunks from the text iterator + +// FIXME: Performance could be bad for huge spans next to each other that don't fall on word boundaries. + +void WordAwareIterator::advance() +{ + m_previousText = 0; + m_buffer.clear(); // toss any old buffer we built up + + // If last time we did a look-ahead, start with that looked-ahead chunk now + if (!m_didLookAhead) { + ASSERT(!m_textIterator.atEnd()); + m_textIterator.advance(); + } + m_didLookAhead = false; + + // Go to next non-empty chunk + while (!m_textIterator.atEnd() && m_textIterator.length() == 0) + m_textIterator.advance(); + m_range = m_textIterator.range(); + + if (m_textIterator.atEnd()) + return; + + while (1) { + // If this chunk ends in whitespace we can just use it as our chunk. + if (isSpaceOrNewline(m_textIterator.characters()[m_textIterator.length() - 1])) + return; + + // If this is the first chunk that failed, save it in previousText before look ahead + if (m_buffer.isEmpty()) { + m_previousText = m_textIterator.characters(); + m_previousLength = m_textIterator.length(); + } + + // Look ahead to next chunk. If it is whitespace or a break, we can use the previous stuff + m_textIterator.advance(); + if (m_textIterator.atEnd() || m_textIterator.length() == 0 || isSpaceOrNewline(m_textIterator.characters()[0])) { + m_didLookAhead = true; + return; + } + + if (m_buffer.isEmpty()) { + // Start gobbling chunks until we get to a suitable stopping point + m_buffer.append(m_previousText, m_previousLength); + m_previousText = 0; + } + m_buffer.append(m_textIterator.characters(), m_textIterator.length()); + int exception = 0; + m_range->setEnd(m_textIterator.range()->endContainer(), m_textIterator.range()->endOffset(), exception); + } +} + +int WordAwareIterator::length() const +{ + if (!m_buffer.isEmpty()) + return m_buffer.size(); + if (m_previousText) + return m_previousLength; + return m_textIterator.length(); +} + +const UChar* WordAwareIterator::characters() const +{ + if (!m_buffer.isEmpty()) + return m_buffer.data(); + if (m_previousText) + return m_previousText; + return m_textIterator.characters(); +} + +// -------- + +static inline UChar foldQuoteMarkOrSoftHyphen(UChar c) +{ + switch (c) { + case hebrewPunctuationGershayim: + case leftDoubleQuotationMark: + case rightDoubleQuotationMark: + return '"'; + case hebrewPunctuationGeresh: + case leftSingleQuotationMark: + case rightSingleQuotationMark: + return '\''; + case softHyphen: + // Replace soft hyphen with an ignorable character so that their presence or absence will + // not affect string comparison. + return 0; + default: + return c; + } +} + +static inline void foldQuoteMarksAndSoftHyphens(String& s) +{ + s.replace(hebrewPunctuationGeresh, '\''); + s.replace(hebrewPunctuationGershayim, '"'); + s.replace(leftDoubleQuotationMark, '"'); + s.replace(leftSingleQuotationMark, '\''); + s.replace(rightDoubleQuotationMark, '"'); + s.replace(rightSingleQuotationMark, '\''); + // Replace soft hyphen with an ignorable character so that their presence or absence will + // not affect string comparison. + s.replace(softHyphen, 0); +} + +#if USE(ICU_UNICODE) && !UCONFIG_NO_COLLATION + +static inline void foldQuoteMarksAndSoftHyphens(UChar* data, size_t length) +{ + for (size_t i = 0; i < length; ++i) + data[i] = foldQuoteMarkOrSoftHyphen(data[i]); +} + +static const size_t minimumSearchBufferSize = 8192; + +#ifndef NDEBUG +static bool searcherInUse; +#endif + +static UStringSearch* createSearcher() +{ + // Provide a non-empty pattern and non-empty text so usearch_open will not fail, + // but it doesn't matter exactly what it is, since we don't perform any searches + // without setting both the pattern and the text. + UErrorCode status = U_ZERO_ERROR; + UStringSearch* searcher = usearch_open(&newlineCharacter, 1, &newlineCharacter, 1, currentSearchLocaleID(), 0, &status); + ASSERT(status == U_ZERO_ERROR || status == U_USING_FALLBACK_WARNING || status == U_USING_DEFAULT_WARNING); + return searcher; +} + +static UStringSearch* searcher() +{ + static UStringSearch* searcher = createSearcher(); + return searcher; +} + +static inline void lockSearcher() +{ +#ifndef NDEBUG + ASSERT(!searcherInUse); + searcherInUse = true; +#endif +} + +static inline void unlockSearcher() +{ +#ifndef NDEBUG + ASSERT(searcherInUse); + searcherInUse = false; +#endif +} + +// ICU's search ignores the distinction between small kana letters and ones +// that are not small, and also characters that differ only in the voicing +// marks when considering only primary collation strength diffrences. +// This is not helpful for end users, since these differences make words +// distinct, so for our purposes we need these to be considered. +// The Unicode folks do not think the collation algorithm should be +// changed. To work around this, we would like to tailor the ICU searcher, +// but we can't get that to work yet. So instead, we check for cases where +// these differences occur, and skip those matches. + +// We refer to the above technique as the "kana workaround". The next few +// functions are helper functinos for the kana workaround. + +static inline bool isKanaLetter(UChar character) +{ + // Hiragana letters. + if (character >= 0x3041 && character <= 0x3096) + return true; + + // Katakana letters. + if (character >= 0x30A1 && character <= 0x30FA) + return true; + if (character >= 0x31F0 && character <= 0x31FF) + return true; + + // Halfwidth katakana letters. + if (character >= 0xFF66 && character <= 0xFF9D && character != 0xFF70) + return true; + + return false; +} + +static inline bool isSmallKanaLetter(UChar character) +{ + ASSERT(isKanaLetter(character)); + + switch (character) { + case 0x3041: // HIRAGANA LETTER SMALL A + case 0x3043: // HIRAGANA LETTER SMALL I + case 0x3045: // HIRAGANA LETTER SMALL U + case 0x3047: // HIRAGANA LETTER SMALL E + case 0x3049: // HIRAGANA LETTER SMALL O + case 0x3063: // HIRAGANA LETTER SMALL TU + case 0x3083: // HIRAGANA LETTER SMALL YA + case 0x3085: // HIRAGANA LETTER SMALL YU + case 0x3087: // HIRAGANA LETTER SMALL YO + case 0x308E: // HIRAGANA LETTER SMALL WA + case 0x3095: // HIRAGANA LETTER SMALL KA + case 0x3096: // HIRAGANA LETTER SMALL KE + case 0x30A1: // KATAKANA LETTER SMALL A + case 0x30A3: // KATAKANA LETTER SMALL I + case 0x30A5: // KATAKANA LETTER SMALL U + case 0x30A7: // KATAKANA LETTER SMALL E + case 0x30A9: // KATAKANA LETTER SMALL O + case 0x30C3: // KATAKANA LETTER SMALL TU + case 0x30E3: // KATAKANA LETTER SMALL YA + case 0x30E5: // KATAKANA LETTER SMALL YU + case 0x30E7: // KATAKANA LETTER SMALL YO + case 0x30EE: // KATAKANA LETTER SMALL WA + case 0x30F5: // KATAKANA LETTER SMALL KA + case 0x30F6: // KATAKANA LETTER SMALL KE + case 0x31F0: // KATAKANA LETTER SMALL KU + case 0x31F1: // KATAKANA LETTER SMALL SI + case 0x31F2: // KATAKANA LETTER SMALL SU + case 0x31F3: // KATAKANA LETTER SMALL TO + case 0x31F4: // KATAKANA LETTER SMALL NU + case 0x31F5: // KATAKANA LETTER SMALL HA + case 0x31F6: // KATAKANA LETTER SMALL HI + case 0x31F7: // KATAKANA LETTER SMALL HU + case 0x31F8: // KATAKANA LETTER SMALL HE + case 0x31F9: // KATAKANA LETTER SMALL HO + case 0x31FA: // KATAKANA LETTER SMALL MU + case 0x31FB: // KATAKANA LETTER SMALL RA + case 0x31FC: // KATAKANA LETTER SMALL RI + case 0x31FD: // KATAKANA LETTER SMALL RU + case 0x31FE: // KATAKANA LETTER SMALL RE + case 0x31FF: // KATAKANA LETTER SMALL RO + case 0xFF67: // HALFWIDTH KATAKANA LETTER SMALL A + case 0xFF68: // HALFWIDTH KATAKANA LETTER SMALL I + case 0xFF69: // HALFWIDTH KATAKANA LETTER SMALL U + case 0xFF6A: // HALFWIDTH KATAKANA LETTER SMALL E + case 0xFF6B: // HALFWIDTH KATAKANA LETTER SMALL O + case 0xFF6C: // HALFWIDTH KATAKANA LETTER SMALL YA + case 0xFF6D: // HALFWIDTH KATAKANA LETTER SMALL YU + case 0xFF6E: // HALFWIDTH KATAKANA LETTER SMALL YO + case 0xFF6F: // HALFWIDTH KATAKANA LETTER SMALL TU + return true; + } + return false; +} + +enum VoicedSoundMarkType { NoVoicedSoundMark, VoicedSoundMark, SemiVoicedSoundMark }; + +static inline VoicedSoundMarkType composedVoicedSoundMark(UChar character) +{ + ASSERT(isKanaLetter(character)); + + switch (character) { + case 0x304C: // HIRAGANA LETTER GA + case 0x304E: // HIRAGANA LETTER GI + case 0x3050: // HIRAGANA LETTER GU + case 0x3052: // HIRAGANA LETTER GE + case 0x3054: // HIRAGANA LETTER GO + case 0x3056: // HIRAGANA LETTER ZA + case 0x3058: // HIRAGANA LETTER ZI + case 0x305A: // HIRAGANA LETTER ZU + case 0x305C: // HIRAGANA LETTER ZE + case 0x305E: // HIRAGANA LETTER ZO + case 0x3060: // HIRAGANA LETTER DA + case 0x3062: // HIRAGANA LETTER DI + case 0x3065: // HIRAGANA LETTER DU + case 0x3067: // HIRAGANA LETTER DE + case 0x3069: // HIRAGANA LETTER DO + case 0x3070: // HIRAGANA LETTER BA + case 0x3073: // HIRAGANA LETTER BI + case 0x3076: // HIRAGANA LETTER BU + case 0x3079: // HIRAGANA LETTER BE + case 0x307C: // HIRAGANA LETTER BO + case 0x3094: // HIRAGANA LETTER VU + case 0x30AC: // KATAKANA LETTER GA + case 0x30AE: // KATAKANA LETTER GI + case 0x30B0: // KATAKANA LETTER GU + case 0x30B2: // KATAKANA LETTER GE + case 0x30B4: // KATAKANA LETTER GO + case 0x30B6: // KATAKANA LETTER ZA + case 0x30B8: // KATAKANA LETTER ZI + case 0x30BA: // KATAKANA LETTER ZU + case 0x30BC: // KATAKANA LETTER ZE + case 0x30BE: // KATAKANA LETTER ZO + case 0x30C0: // KATAKANA LETTER DA + case 0x30C2: // KATAKANA LETTER DI + case 0x30C5: // KATAKANA LETTER DU + case 0x30C7: // KATAKANA LETTER DE + case 0x30C9: // KATAKANA LETTER DO + case 0x30D0: // KATAKANA LETTER BA + case 0x30D3: // KATAKANA LETTER BI + case 0x30D6: // KATAKANA LETTER BU + case 0x30D9: // KATAKANA LETTER BE + case 0x30DC: // KATAKANA LETTER BO + case 0x30F4: // KATAKANA LETTER VU + case 0x30F7: // KATAKANA LETTER VA + case 0x30F8: // KATAKANA LETTER VI + case 0x30F9: // KATAKANA LETTER VE + case 0x30FA: // KATAKANA LETTER VO + return VoicedSoundMark; + case 0x3071: // HIRAGANA LETTER PA + case 0x3074: // HIRAGANA LETTER PI + case 0x3077: // HIRAGANA LETTER PU + case 0x307A: // HIRAGANA LETTER PE + case 0x307D: // HIRAGANA LETTER PO + case 0x30D1: // KATAKANA LETTER PA + case 0x30D4: // KATAKANA LETTER PI + case 0x30D7: // KATAKANA LETTER PU + case 0x30DA: // KATAKANA LETTER PE + case 0x30DD: // KATAKANA LETTER PO + return SemiVoicedSoundMark; + } + return NoVoicedSoundMark; +} + +static inline bool isCombiningVoicedSoundMark(UChar character) +{ + switch (character) { + case 0x3099: // COMBINING KATAKANA-HIRAGANA VOICED SOUND MARK + case 0x309A: // COMBINING KATAKANA-HIRAGANA SEMI-VOICED SOUND MARK + return true; + } + return false; +} + +static inline bool containsKanaLetters(const String& pattern) +{ + const UChar* characters = pattern.characters(); + unsigned length = pattern.length(); + for (unsigned i = 0; i < length; ++i) { + if (isKanaLetter(characters[i])) + return true; + } + return false; +} + +static void normalizeCharacters(const UChar* characters, unsigned length, Vector<UChar>& buffer) +{ + ASSERT(length); + + buffer.resize(length); + + UErrorCode status = U_ZERO_ERROR; + size_t bufferSize = unorm_normalize(characters, length, UNORM_NFC, 0, buffer.data(), length, &status); + ASSERT(status == U_ZERO_ERROR || status == U_STRING_NOT_TERMINATED_WARNING || status == U_BUFFER_OVERFLOW_ERROR); + ASSERT(bufferSize); + + buffer.resize(bufferSize); + + if (status == U_ZERO_ERROR || status == U_STRING_NOT_TERMINATED_WARNING) + return; + + status = U_ZERO_ERROR; + unorm_normalize(characters, length, UNORM_NFC, 0, buffer.data(), bufferSize, &status); + ASSERT(status == U_STRING_NOT_TERMINATED_WARNING); +} + +static bool isNonLatin1Separator(UChar32 character) +{ + ASSERT_ARG(character, character >= 256); + + return U_GET_GC_MASK(character) & (U_GC_S_MASK | U_GC_P_MASK | U_GC_Z_MASK | U_GC_CF_MASK); +} + +static inline bool isSeparator(UChar32 character) +{ + static const bool latin1SeparatorTable[256] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // space ! " # $ % & ' ( ) * + , - . / + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, // : ; < = > ? + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // @ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, // [ \ ] ^ _ + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // ` + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, // { | } ~ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0 + }; + + if (character < 256) + return latin1SeparatorTable[character]; + + return isNonLatin1Separator(character); +} + +inline SearchBuffer::SearchBuffer(const String& target, FindOptions options) + : m_target(target) + , m_options(options) + , m_prefixLength(0) + , m_atBreak(true) + , m_needsMoreContext(options & AtWordStarts) + , m_targetRequiresKanaWorkaround(containsKanaLetters(m_target)) +{ + ASSERT(!m_target.isEmpty()); + + // FIXME: We'd like to tailor the searcher to fold quote marks for us instead + // of doing it in a separate replacement pass here, but ICU doesn't offer a way + // to add tailoring on top of the locale-specific tailoring as of this writing. + foldQuoteMarksAndSoftHyphens(m_target); + + size_t targetLength = m_target.length(); + m_buffer.reserveInitialCapacity(max(targetLength * 8, minimumSearchBufferSize)); + m_overlap = m_buffer.capacity() / 4; + + if ((m_options & AtWordStarts) && targetLength) { + UChar32 targetFirstCharacter; + U16_GET(m_target.characters(), 0, 0, targetLength, targetFirstCharacter); + // Characters in the separator category never really occur at the beginning of a word, + // so if the target begins with such a character, we just ignore the AtWordStart option. + if (isSeparator(targetFirstCharacter)) { + m_options &= ~AtWordStarts; + m_needsMoreContext = false; + } + } + + // Grab the single global searcher. + // If we ever have a reason to do more than once search buffer at once, we'll have + // to move to multiple searchers. + lockSearcher(); + + UStringSearch* searcher = WebCore::searcher(); + UCollator* collator = usearch_getCollator(searcher); + + UCollationStrength strength = m_options & CaseInsensitive ? UCOL_PRIMARY : UCOL_TERTIARY; + if (ucol_getStrength(collator) != strength) { + ucol_setStrength(collator, strength); + usearch_reset(searcher); + } + + UErrorCode status = U_ZERO_ERROR; + usearch_setPattern(searcher, m_target.characters(), targetLength, &status); + ASSERT(status == U_ZERO_ERROR); + + // The kana workaround requires a normalized copy of the target string. + if (m_targetRequiresKanaWorkaround) + normalizeCharacters(m_target.characters(), m_target.length(), m_normalizedTarget); +} + +inline SearchBuffer::~SearchBuffer() +{ + unlockSearcher(); +} + +inline size_t SearchBuffer::append(const UChar* characters, size_t length) +{ + ASSERT(length); + + if (m_atBreak) { + m_buffer.shrink(0); + m_prefixLength = 0; + m_atBreak = false; + } else if (m_buffer.size() == m_buffer.capacity()) { + memcpy(m_buffer.data(), m_buffer.data() + m_buffer.size() - m_overlap, m_overlap * sizeof(UChar)); + m_prefixLength -= min(m_prefixLength, m_buffer.size() - m_overlap); + m_buffer.shrink(m_overlap); + } + + size_t oldLength = m_buffer.size(); + size_t usableLength = min(m_buffer.capacity() - oldLength, length); + ASSERT(usableLength); + m_buffer.append(characters, usableLength); + foldQuoteMarksAndSoftHyphens(m_buffer.data() + oldLength, usableLength); + return usableLength; +} + +inline bool SearchBuffer::needsMoreContext() const +{ + return m_needsMoreContext; +} + +inline void SearchBuffer::prependContext(const UChar* characters, size_t length) +{ + ASSERT(m_needsMoreContext); + ASSERT(m_prefixLength == m_buffer.size()); + + if (!length) + return; + + m_atBreak = false; + + size_t wordBoundaryContextStart = length; + if (wordBoundaryContextStart) { + U16_BACK_1(characters, 0, wordBoundaryContextStart); + wordBoundaryContextStart = startOfLastWordBoundaryContext(characters, wordBoundaryContextStart); + } + + size_t usableLength = min(m_buffer.capacity() - m_prefixLength, length - wordBoundaryContextStart); + m_buffer.prepend(characters + length - usableLength, usableLength); + m_prefixLength += usableLength; + + if (wordBoundaryContextStart || m_prefixLength == m_buffer.capacity()) + m_needsMoreContext = false; +} + +inline bool SearchBuffer::atBreak() const +{ + return m_atBreak; +} + +inline void SearchBuffer::reachedBreak() +{ + m_atBreak = true; +} + +inline bool SearchBuffer::isBadMatch(const UChar* match, size_t matchLength) const +{ + // This function implements the kana workaround. If usearch treats + // it as a match, but we do not want to, then it's a "bad match". + if (!m_targetRequiresKanaWorkaround) + return false; + + // Normalize into a match buffer. We reuse a single buffer rather than + // creating a new one each time. + normalizeCharacters(match, matchLength, m_normalizedMatch); + + const UChar* a = m_normalizedTarget.begin(); + const UChar* aEnd = m_normalizedTarget.end(); + + const UChar* b = m_normalizedMatch.begin(); + const UChar* bEnd = m_normalizedMatch.end(); + + while (true) { + // Skip runs of non-kana-letter characters. This is necessary so we can + // correctly handle strings where the target and match have different-length + // runs of characters that match, while still double checking the correctness + // of matches of kana letters with other kana letters. + while (a != aEnd && !isKanaLetter(*a)) + ++a; + while (b != bEnd && !isKanaLetter(*b)) + ++b; + + // If we reached the end of either the target or the match, we should have + // reached the end of both; both should have the same number of kana letters. + if (a == aEnd || b == bEnd) { + ASSERT(a == aEnd); + ASSERT(b == bEnd); + return false; + } + + // Check for differences in the kana letter character itself. + if (isSmallKanaLetter(*a) != isSmallKanaLetter(*b)) + return true; + if (composedVoicedSoundMark(*a) != composedVoicedSoundMark(*b)) + return true; + ++a; + ++b; + + // Check for differences in combining voiced sound marks found after the letter. + while (1) { + if (!(a != aEnd && isCombiningVoicedSoundMark(*a))) { + if (b != bEnd && isCombiningVoicedSoundMark(*b)) + return true; + break; + } + if (!(b != bEnd && isCombiningVoicedSoundMark(*b))) + return true; + if (*a != *b) + return true; + ++a; + ++b; + } + } +} + +inline bool SearchBuffer::isWordStartMatch(size_t start, size_t length) const +{ + ASSERT(m_options & AtWordStarts); + + if (!start) + return true; + + if (m_options & TreatMedialCapitalAsWordStart) { + int size = m_buffer.size(); + int offset = start; + UChar32 firstCharacter; + U16_GET(m_buffer.data(), 0, offset, size, firstCharacter); + UChar32 previousCharacter; + U16_PREV(m_buffer.data(), 0, offset, previousCharacter); + + if (isSeparator(firstCharacter)) { + // The start of a separator run is a word start (".org" in "webkit.org"). + if (!isSeparator(previousCharacter)) + return true; + } else if (isASCIIUpper(firstCharacter)) { + // The start of an uppercase run is a word start ("Kit" in "WebKit"). + if (!isASCIIUpper(previousCharacter)) + return true; + // The last character of an uppercase run followed by a non-separator, non-digit + // is a word start ("Request" in "XMLHTTPRequest"). + offset = start; + U16_FWD_1(m_buffer.data(), offset, size); + UChar32 nextCharacter = 0; + if (offset < size) + U16_GET(m_buffer.data(), 0, offset, size, nextCharacter); + if (!isASCIIUpper(nextCharacter) && !isASCIIDigit(nextCharacter) && !isSeparator(nextCharacter)) + return true; + } else if (isASCIIDigit(firstCharacter)) { + // The start of a digit run is a word start ("2" in "WebKit2"). + if (!isASCIIDigit(previousCharacter)) + return true; + } else if (isSeparator(previousCharacter) || isASCIIDigit(previousCharacter)) { + // The start of a non-separator, non-uppercase, non-digit run is a word start, + // except after an uppercase. ("org" in "webkit.org", but not "ore" in "WebCore"). + return true; + } + } + + size_t wordBreakSearchStart = start + length; + while (wordBreakSearchStart > start) + wordBreakSearchStart = findNextWordFromIndex(m_buffer.data(), m_buffer.size(), wordBreakSearchStart, false /* backwards */); + return wordBreakSearchStart == start; +} + +inline size_t SearchBuffer::search(size_t& start) +{ + size_t size = m_buffer.size(); + if (m_atBreak) { + if (!size) + return 0; + } else { + if (size != m_buffer.capacity()) + return 0; + } + + UStringSearch* searcher = WebCore::searcher(); + + UErrorCode status = U_ZERO_ERROR; + usearch_setText(searcher, m_buffer.data(), size, &status); + ASSERT(status == U_ZERO_ERROR); + + usearch_setOffset(searcher, m_prefixLength, &status); + ASSERT(status == U_ZERO_ERROR); + + int matchStart = usearch_next(searcher, &status); + ASSERT(status == U_ZERO_ERROR); + +nextMatch: + if (!(matchStart >= 0 && static_cast<size_t>(matchStart) < size)) { + ASSERT(matchStart == USEARCH_DONE); + return 0; + } + + // Matches that start in the overlap area are only tentative. + // The same match may appear later, matching more characters, + // possibly including a combining character that's not yet in the buffer. + if (!m_atBreak && static_cast<size_t>(matchStart) >= size - m_overlap) { + size_t overlap = m_overlap; + if (m_options & AtWordStarts) { + // Ensure that there is sufficient context before matchStart the next time around for + // determining if it is at a word boundary. + int wordBoundaryContextStart = matchStart; + U16_BACK_1(m_buffer.data(), 0, wordBoundaryContextStart); + wordBoundaryContextStart = startOfLastWordBoundaryContext(m_buffer.data(), wordBoundaryContextStart); + overlap = min(size - 1, max(overlap, size - wordBoundaryContextStart)); + } + memcpy(m_buffer.data(), m_buffer.data() + size - overlap, overlap * sizeof(UChar)); + m_prefixLength -= min(m_prefixLength, size - overlap); + m_buffer.shrink(overlap); + return 0; + } + + size_t matchedLength = usearch_getMatchedLength(searcher); + ASSERT(matchStart + matchedLength <= size); + + // If this match is "bad", move on to the next match. + if (isBadMatch(m_buffer.data() + matchStart, matchedLength) || ((m_options & AtWordStarts) && !isWordStartMatch(matchStart, matchedLength))) { + matchStart = usearch_next(searcher, &status); + ASSERT(status == U_ZERO_ERROR); + goto nextMatch; + } + + size_t newSize = size - (matchStart + 1); + memmove(m_buffer.data(), m_buffer.data() + matchStart + 1, newSize * sizeof(UChar)); + m_prefixLength -= min<size_t>(m_prefixLength, matchStart + 1); + m_buffer.shrink(newSize); + + start = size - matchStart; + return matchedLength; +} + +#else // !ICU_UNICODE + +inline SearchBuffer::SearchBuffer(const String& target, FindOptions options) + : m_target(options & CaseInsensitive ? target.foldCase() : target) + , m_options(options) + , m_buffer(m_target.length()) + , m_isCharacterStartBuffer(m_target.length()) + , m_isBufferFull(false) + , m_cursor(0) +{ + ASSERT(!m_target.isEmpty()); + m_target.replace(noBreakSpace, ' '); + foldQuoteMarksAndSoftHyphens(m_target); +} + +inline SearchBuffer::~SearchBuffer() +{ +} + +inline void SearchBuffer::reachedBreak() +{ + m_cursor = 0; + m_isBufferFull = false; +} + +inline bool SearchBuffer::atBreak() const +{ + return !m_cursor && !m_isBufferFull; +} + +inline void SearchBuffer::append(UChar c, bool isStart) +{ + m_buffer[m_cursor] = c == noBreakSpace ? ' ' : foldQuoteMarkOrSoftHyphen(c); + m_isCharacterStartBuffer[m_cursor] = isStart; + if (++m_cursor == m_target.length()) { + m_cursor = 0; + m_isBufferFull = true; + } +} + +inline size_t SearchBuffer::append(const UChar* characters, size_t length) +{ + ASSERT(length); + if (!(m_options & CaseInsensitive)) { + append(characters[0], true); + return 1; + } + const int maxFoldedCharacters = 16; // sensible maximum is 3, this should be more than enough + UChar foldedCharacters[maxFoldedCharacters]; + bool error; + int numFoldedCharacters = foldCase(foldedCharacters, maxFoldedCharacters, characters, 1, &error); + ASSERT(!error); + ASSERT(numFoldedCharacters); + ASSERT(numFoldedCharacters <= maxFoldedCharacters); + if (!error && numFoldedCharacters) { + numFoldedCharacters = min(numFoldedCharacters, maxFoldedCharacters); + append(foldedCharacters[0], true); + for (int i = 1; i < numFoldedCharacters; ++i) + append(foldedCharacters[i], false); + } + return 1; +} + +inline bool SearchBuffer::needsMoreContext() const +{ + return false; +} + +void SearchBuffer::prependContext(const UChar*, size_t) +{ + ASSERT_NOT_REACHED(); +} + +inline size_t SearchBuffer::search(size_t& start) +{ + if (!m_isBufferFull) + return 0; + if (!m_isCharacterStartBuffer[m_cursor]) + return 0; + + size_t tailSpace = m_target.length() - m_cursor; + if (memcmp(&m_buffer[m_cursor], m_target.characters(), tailSpace * sizeof(UChar)) != 0) + return 0; + if (memcmp(&m_buffer[0], m_target.characters() + tailSpace, m_cursor * sizeof(UChar)) != 0) + return 0; + + start = length(); + + // Now that we've found a match once, we don't want to find it again, because those + // are the SearchBuffer semantics, allowing for a buffer where you append more than one + // character at a time. To do this we take advantage of m_isCharacterStartBuffer, but if + // we want to get rid of that in the future we could track this with a separate boolean + // or even move the characters to the start of the buffer and set m_isBufferFull to false. + m_isCharacterStartBuffer[m_cursor] = false; + + return start; +} + +// Returns the number of characters that were appended to the buffer (what we are searching in). +// That's not necessarily the same length as the passed-in target string, because case folding +// can make two strings match even though they're not the same length. +size_t SearchBuffer::length() const +{ + size_t bufferSize = m_target.length(); + size_t length = 0; + for (size_t i = 0; i < bufferSize; ++i) + length += m_isCharacterStartBuffer[i]; + return length; +} + +#endif // !ICU_UNICODE + +// -------- + +int TextIterator::rangeLength(const Range* r, bool forSelectionPreservation) +{ + int length = 0; + for (TextIterator it(r, forSelectionPreservation ? TextIteratorEmitsCharactersBetweenAllVisiblePositions : TextIteratorDefaultBehavior); !it.atEnd(); it.advance()) + length += it.length(); + + return length; +} + +PassRefPtr<Range> TextIterator::subrange(Range* entireRange, int characterOffset, int characterCount) +{ + CharacterIterator entireRangeIterator(entireRange); + return characterSubrange(entireRangeIterator, characterOffset, characterCount); +} + +PassRefPtr<Range> TextIterator::rangeFromLocationAndLength(Element* scope, int rangeLocation, int rangeLength, bool forSelectionPreservation) +{ + RefPtr<Range> resultRange = scope->document()->createRange(); + + int docTextPosition = 0; + int rangeEnd = rangeLocation + rangeLength; + bool startRangeFound = false; + + RefPtr<Range> textRunRange; + + TextIterator it(rangeOfContents(scope).get(), forSelectionPreservation ? TextIteratorEmitsCharactersBetweenAllVisiblePositions : TextIteratorDefaultBehavior); + + // FIXME: the atEnd() check shouldn't be necessary, workaround for <http://bugs.webkit.org/show_bug.cgi?id=6289>. + if (rangeLocation == 0 && rangeLength == 0 && it.atEnd()) { + textRunRange = it.range(); + + ExceptionCode ec = 0; + resultRange->setStart(textRunRange->startContainer(), 0, ec); + ASSERT(!ec); + resultRange->setEnd(textRunRange->startContainer(), 0, ec); + ASSERT(!ec); + + return resultRange.release(); + } + + for (; !it.atEnd(); it.advance()) { + int len = it.length(); + textRunRange = it.range(); + + bool foundStart = rangeLocation >= docTextPosition && rangeLocation <= docTextPosition + len; + bool foundEnd = rangeEnd >= docTextPosition && rangeEnd <= docTextPosition + len; + + // Fix textRunRange->endPosition(), but only if foundStart || foundEnd, because it is only + // in those cases that textRunRange is used. + if (foundEnd) { + // FIXME: This is a workaround for the fact that the end of a run is often at the wrong + // position for emitted '\n's. + if (len == 1 && it.characters()[0] == '\n') { + scope->document()->updateLayoutIgnorePendingStylesheets(); + it.advance(); + if (!it.atEnd()) { + RefPtr<Range> range = it.range(); + ExceptionCode ec = 0; + textRunRange->setEnd(range->startContainer(), range->startOffset(), ec); + ASSERT(!ec); + } else { + Position runStart = textRunRange->startPosition(); + Position runEnd = VisiblePosition(runStart).next().deepEquivalent(); + if (runEnd.isNotNull()) { + ExceptionCode ec = 0; + textRunRange->setEnd(runEnd.node(), runEnd.deprecatedEditingOffset(), ec); + ASSERT(!ec); + } + } + } + } + + if (foundStart) { + startRangeFound = true; + int exception = 0; + if (textRunRange->startContainer()->isTextNode()) { + int offset = rangeLocation - docTextPosition; + resultRange->setStart(textRunRange->startContainer(), offset + textRunRange->startOffset(), exception); + } else { + if (rangeLocation == docTextPosition) + resultRange->setStart(textRunRange->startContainer(), textRunRange->startOffset(), exception); + else + resultRange->setStart(textRunRange->endContainer(), textRunRange->endOffset(), exception); + } + } + + if (foundEnd) { + int exception = 0; + if (textRunRange->startContainer()->isTextNode()) { + int offset = rangeEnd - docTextPosition; + resultRange->setEnd(textRunRange->startContainer(), offset + textRunRange->startOffset(), exception); + } else { + if (rangeEnd == docTextPosition) + resultRange->setEnd(textRunRange->startContainer(), textRunRange->startOffset(), exception); + else + resultRange->setEnd(textRunRange->endContainer(), textRunRange->endOffset(), exception); + } + docTextPosition += len; + break; + } + docTextPosition += len; + } + + if (!startRangeFound) + return 0; + + if (rangeLength != 0 && rangeEnd > docTextPosition) { // rangeEnd is out of bounds + int exception = 0; + resultRange->setEnd(textRunRange->endContainer(), textRunRange->endOffset(), exception); + } + + return resultRange.release(); +} + +// -------- + +UChar* plainTextToMallocAllocatedBuffer(const Range* r, unsigned& bufferLength, bool isDisplayString, TextIteratorBehavior defaultBehavior) +{ + UChar* result = 0; + + // Do this in pieces to avoid massive reallocations if there is a large amount of text. + // Use system malloc for buffers since they can consume lots of memory and current TCMalloc is unable return it back to OS. + static const unsigned cMaxSegmentSize = 1 << 16; + bufferLength = 0; + typedef pair<UChar*, unsigned> TextSegment; + OwnPtr<Vector<TextSegment> > textSegments; + Vector<UChar> textBuffer; + textBuffer.reserveInitialCapacity(cMaxSegmentSize); + TextIteratorBehavior behavior = defaultBehavior; + if (!isDisplayString) + behavior = static_cast<TextIteratorBehavior>(behavior | TextIteratorEmitsTextsWithoutTranscoding); + + for (TextIterator it(r, behavior); !it.atEnd(); it.advance()) { + if (textBuffer.size() && textBuffer.size() + it.length() > cMaxSegmentSize) { + UChar* newSegmentBuffer = static_cast<UChar*>(malloc(textBuffer.size() * sizeof(UChar))); + if (!newSegmentBuffer) + goto exit; + memcpy(newSegmentBuffer, textBuffer.data(), textBuffer.size() * sizeof(UChar)); + if (!textSegments) + textSegments = adoptPtr(new Vector<TextSegment>); + textSegments->append(make_pair(newSegmentBuffer, (unsigned)textBuffer.size())); + textBuffer.clear(); + } + textBuffer.append(it.characters(), it.length()); + bufferLength += it.length(); + } + + if (!bufferLength) + return 0; + + // Since we know the size now, we can make a single buffer out of the pieces with one big alloc + result = static_cast<UChar*>(malloc(bufferLength * sizeof(UChar))); + if (!result) + goto exit; + + { + UChar* resultPos = result; + if (textSegments) { + unsigned size = textSegments->size(); + for (unsigned i = 0; i < size; ++i) { + const TextSegment& segment = textSegments->at(i); + memcpy(resultPos, segment.first, segment.second * sizeof(UChar)); + resultPos += segment.second; + } + } + memcpy(resultPos, textBuffer.data(), textBuffer.size() * sizeof(UChar)); + } + +exit: + if (textSegments) { + unsigned size = textSegments->size(); + for (unsigned i = 0; i < size; ++i) + free(textSegments->at(i).first); + } + + if (isDisplayString && r->ownerDocument()) + r->ownerDocument()->displayBufferModifiedByEncoding(result, bufferLength); + + return result; +} + +String plainText(const Range* r, TextIteratorBehavior defaultBehavior) +{ + unsigned length; + UChar* buf = plainTextToMallocAllocatedBuffer(r, length, false, defaultBehavior); + if (!buf) + return ""; + String result(buf, length); + free(buf); + return result; +} + +static inline bool isAllCollapsibleWhitespace(const String& string) +{ + const UChar* characters = string.characters(); + unsigned length = string.length(); + for (unsigned i = 0; i < length; ++i) { + if (!isCollapsibleWhitespace(characters[i])) + return false; + } + return true; +} + +static PassRefPtr<Range> collapsedToBoundary(const Range* range, bool forward) +{ + ExceptionCode ec = 0; + RefPtr<Range> result = range->cloneRange(ec); + ASSERT(!ec); + result->collapse(!forward, ec); + ASSERT(!ec); + return result.release(); +} + +static size_t findPlainText(CharacterIterator& it, const String& target, FindOptions options, size_t& matchStart) +{ + matchStart = 0; + size_t matchLength = 0; + + SearchBuffer buffer(target, options); + + if (buffer.needsMoreContext()) { + RefPtr<Range> startRange = it.range(); + RefPtr<Range> beforeStartRange = startRange->ownerDocument()->createRange(); + ExceptionCode ec = 0; + beforeStartRange->setEnd(startRange->startContainer(), startRange->startOffset(), ec); + for (SimplifiedBackwardsTextIterator backwardsIterator(beforeStartRange.get()); !backwardsIterator.atEnd(); backwardsIterator.advance()) { + buffer.prependContext(backwardsIterator.characters(), backwardsIterator.length()); + if (!buffer.needsMoreContext()) + break; + } + } + + while (!it.atEnd()) { + it.advance(buffer.append(it.characters(), it.length())); +tryAgain: + size_t matchStartOffset; + if (size_t newMatchLength = buffer.search(matchStartOffset)) { + // Note that we found a match, and where we found it. + size_t lastCharacterInBufferOffset = it.characterOffset(); + ASSERT(lastCharacterInBufferOffset >= matchStartOffset); + matchStart = lastCharacterInBufferOffset - matchStartOffset; + matchLength = newMatchLength; + // If searching forward, stop on the first match. + // If searching backward, don't stop, so we end up with the last match. + if (!(options & Backwards)) + break; + goto tryAgain; + } + if (it.atBreak() && !buffer.atBreak()) { + buffer.reachedBreak(); + goto tryAgain; + } + } + + return matchLength; +} + +PassRefPtr<Range> findPlainText(const Range* range, const String& target, bool forward, bool caseSensitive) +{ + return findPlainText(range, target, (forward ? 0 : Backwards) | (caseSensitive ? 0 : CaseInsensitive)); +} + +PassRefPtr<Range> findPlainText(const Range* range, const String& target, FindOptions options) +{ + // First, find the text. + size_t matchStart; + size_t matchLength; + { + CharacterIterator findIterator(range, TextIteratorEntersTextControls); + matchLength = findPlainText(findIterator, target, options, matchStart); + if (!matchLength) + return collapsedToBoundary(range, !(options & Backwards)); + } + + // Then, find the document position of the start and the end of the text. + CharacterIterator computeRangeIterator(range, TextIteratorEntersTextControls); + return characterSubrange(computeRangeIterator, matchStart, matchLength); +} + +} diff --git a/Source/WebCore/editing/TextIterator.h b/Source/WebCore/editing/TextIterator.h new file mode 100644 index 0000000..8b61afe --- /dev/null +++ b/Source/WebCore/editing/TextIterator.h @@ -0,0 +1,328 @@ +/* + * Copyright (C) 2004, 2006, 2009 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef TextIterator_h +#define TextIterator_h + +#include "FindOptions.h" +#include "InlineTextBox.h" +#include "Range.h" +#include <wtf/Vector.h> + +namespace WebCore { + +class RenderText; +class RenderTextFragment; + +enum TextIteratorBehavior { + TextIteratorDefaultBehavior = 0, + TextIteratorEmitsCharactersBetweenAllVisiblePositions = 1 << 0, + TextIteratorEntersTextControls = 1 << 1, + TextIteratorEmitsTextsWithoutTranscoding = 1 << 2, + TextIteratorEndsAtEditingBoundary = 1 << 3, + TextIteratorIgnoresStyleVisibility = 1 << 4 +}; + +// FIXME: Can't really answer this question correctly without knowing the white-space mode. +// FIXME: Move this somewhere else in the editing directory. It doesn't belong here. +inline bool isCollapsibleWhitespace(UChar c) +{ + switch (c) { + case ' ': + case '\n': + return true; + default: + return false; + } +} + +String plainText(const Range*, TextIteratorBehavior defaultBehavior = TextIteratorDefaultBehavior); +UChar* plainTextToMallocAllocatedBuffer(const Range*, unsigned& bufferLength, bool isDisplayString, TextIteratorBehavior = TextIteratorDefaultBehavior); +PassRefPtr<Range> findPlainText(const Range*, const String&, FindOptions); +// FIXME: Switch callers over to the FindOptions version and retire this one. +PassRefPtr<Range> findPlainText(const Range*, const String&, bool forward, bool caseSensitive); + +class BitStack { +public: + BitStack(); + ~BitStack(); + + void push(bool); + void pop(); + + bool top() const; + unsigned size() const; + +private: + unsigned m_size; + Vector<unsigned, 1> m_words; +}; + +// Iterates through the DOM range, returning all the text, and 0-length boundaries +// at points where replaced elements break up the text flow. The text comes back in +// chunks so as to optimize for performance of the iteration. + +class TextIterator { +public: + TextIterator(); + ~TextIterator(); + explicit TextIterator(const Range*, TextIteratorBehavior = TextIteratorDefaultBehavior); + + bool atEnd() const { return !m_positionNode; } + void advance(); + + int length() const { return m_textLength; } + const UChar* characters() const { return m_textCharacters; } + + PassRefPtr<Range> range() const; + Node* node() const; + + static int rangeLength(const Range*, bool spacesForReplacedElements = false); + static PassRefPtr<Range> rangeFromLocationAndLength(Element* scope, int rangeLocation, int rangeLength, bool spacesForReplacedElements = false); + static PassRefPtr<Range> subrange(Range* entireRange, int characterOffset, int characterCount); + +private: + void exitNode(); + bool shouldRepresentNodeOffsetZero(); + bool shouldEmitSpaceBeforeAndAfterNode(Node*); + void representNodeOffsetZero(); + bool handleTextNode(); + bool handleReplacedElement(); + bool handleNonTextNode(); + void handleTextBox(); + void handleTextNodeFirstLetter(RenderTextFragment*); + bool hasVisibleTextNode(RenderText*); + void emitCharacter(UChar, Node* textNode, Node* offsetBaseNode, int textStartOffset, int textEndOffset); + void emitText(Node* textNode, RenderObject* renderObject, int textStartOffset, int textEndOffset); + void emitText(Node* textNode, int textStartOffset, int textEndOffset); + + // Current position, not necessarily of the text being returned, but position + // as we walk through the DOM tree. + Node* m_node; + int m_offset; + bool m_handledNode; + bool m_handledChildren; + BitStack m_fullyClippedStack; + + // The range. + Node* m_startContainer; + int m_startOffset; + Node* m_endContainer; + int m_endOffset; + Node* m_pastEndNode; + + // The current text and its position, in the form to be returned from the iterator. + Node* m_positionNode; + mutable Node* m_positionOffsetBaseNode; + mutable int m_positionStartOffset; + mutable int m_positionEndOffset; + const UChar* m_textCharacters; + int m_textLength; + // Hold string m_textCharacters points to so we ensure it won't be deleted. + String m_text; + + // Used when there is still some pending text from the current node; when these + // are false and 0, we go back to normal iterating. + bool m_needsAnotherNewline; + InlineTextBox* m_textBox; + // Used when iteration over :first-letter text to save pointer to + // remaining text box. + InlineTextBox* m_remainingTextBox; + // Used to point to RenderText object for :first-letter. + RenderText *m_firstLetterText; + + // Used to do the whitespace collapsing logic. + Node* m_lastTextNode; + bool m_lastTextNodeEndedWithCollapsedSpace; + UChar m_lastCharacter; + + // Used for whitespace characters that aren't in the DOM, so we can point at them. + UChar m_singleCharacterBuffer; + + // Used when text boxes are out of order (Hebrew/Arabic w/ embeded LTR text) + Vector<InlineTextBox*> m_sortedTextBoxes; + size_t m_sortedTextBoxesPosition; + + // Used when deciding whether to emit a "positioning" (e.g. newline) before any other content + bool m_hasEmitted; + + // Used by selection preservation code. There should be one character emitted between every VisiblePosition + // in the Range used to create the TextIterator. + // FIXME <rdar://problem/6028818>: This functionality should eventually be phased out when we rewrite + // moveParagraphs to not clone/destroy moved content. + bool m_emitsCharactersBetweenAllVisiblePositions; + bool m_entersTextControls; + + // Used when we want texts for copying, pasting, and transposing. + bool m_emitsTextWithoutTranscoding; + // Used when deciding text fragment created by :first-letter should be looked into. + bool m_handledFirstLetter; + // Used when the visibility of the style should not affect text gathering. + bool m_ignoresStyleVisibility; +}; + +// Iterates through the DOM range, returning all the text, and 0-length boundaries +// at points where replaced elements break up the text flow. The text comes back in +// chunks so as to optimize for performance of the iteration. +class SimplifiedBackwardsTextIterator { +public: + SimplifiedBackwardsTextIterator(); + explicit SimplifiedBackwardsTextIterator(const Range*, TextIteratorBehavior = TextIteratorDefaultBehavior); + + bool atEnd() const { return !m_positionNode; } + void advance(); + + int length() const { return m_textLength; } + const UChar* characters() const { return m_textCharacters; } + + PassRefPtr<Range> range() const; + +private: + void exitNode(); + bool handleTextNode(); + bool handleReplacedElement(); + bool handleNonTextNode(); + void emitCharacter(UChar, Node*, int startOffset, int endOffset); + bool crossesEditingBoundary(Node*) const; + bool setCurrentNode(Node*); + void clearCurrentNode(); + + TextIteratorBehavior m_behavior; + // Current position, not necessarily of the text being returned, but position + // as we walk through the DOM tree. + Node* m_node; + int m_offset; + bool m_handledNode; + bool m_handledChildren; + BitStack m_fullyClippedStack; + + // End of the range. + Node* m_startNode; + int m_startOffset; + // Start of the range. + Node* m_endNode; + int m_endOffset; + + // The current text and its position, in the form to be returned from the iterator. + Node* m_positionNode; + int m_positionStartOffset; + int m_positionEndOffset; + const UChar* m_textCharacters; + int m_textLength; + + // Used to do the whitespace logic. + Node* m_lastTextNode; + UChar m_lastCharacter; + + // Used for whitespace characters that aren't in the DOM, so we can point at them. + UChar m_singleCharacterBuffer; + + // The node after the last node this iterator should process. + Node* m_pastStartNode; +}; + +// Builds on the text iterator, adding a character position so we can walk one +// character at a time, or faster, as needed. Useful for searching. +class CharacterIterator { +public: + CharacterIterator(); + explicit CharacterIterator(const Range*, TextIteratorBehavior = TextIteratorDefaultBehavior); + + void advance(int numCharacters); + + bool atBreak() const { return m_atBreak; } + bool atEnd() const { return m_textIterator.atEnd(); } + + int length() const { return m_textIterator.length() - m_runOffset; } + const UChar* characters() const { return m_textIterator.characters() + m_runOffset; } + String string(int numChars); + + int characterOffset() const { return m_offset; } + PassRefPtr<Range> range() const; + +private: + int m_offset; + int m_runOffset; + bool m_atBreak; + + TextIterator m_textIterator; +}; + +class BackwardsCharacterIterator { +public: + BackwardsCharacterIterator(); + explicit BackwardsCharacterIterator(const Range*, TextIteratorBehavior = TextIteratorDefaultBehavior); + + void advance(int); + + bool atEnd() const { return m_textIterator.atEnd(); } + + PassRefPtr<Range> range() const; + +private: + TextIteratorBehavior m_behavior; + int m_offset; + int m_runOffset; + bool m_atBreak; + + SimplifiedBackwardsTextIterator m_textIterator; +}; + +// Very similar to the TextIterator, except that the chunks of text returned are "well behaved", +// meaning they never end split up a word. This is useful for spellcheck or (perhaps one day) searching. +class WordAwareIterator { +public: + WordAwareIterator(); + explicit WordAwareIterator(const Range*); + ~WordAwareIterator(); + + bool atEnd() const { return !m_didLookAhead && m_textIterator.atEnd(); } + void advance(); + + int length() const; + const UChar* characters() const; + + // Range of the text we're currently returning + PassRefPtr<Range> range() const { return m_range; } + +private: + // text from the previous chunk from the textIterator + const UChar* m_previousText; + int m_previousLength; + + // many chunks from textIterator concatenated + Vector<UChar> m_buffer; + + // Did we have to look ahead in the textIterator to confirm the current chunk? + bool m_didLookAhead; + + RefPtr<Range> m_range; + + TextIterator m_textIterator; +}; + +} + +#endif diff --git a/Source/WebCore/editing/TypingCommand.cpp b/Source/WebCore/editing/TypingCommand.cpp new file mode 100644 index 0000000..d54b388 --- /dev/null +++ b/Source/WebCore/editing/TypingCommand.cpp @@ -0,0 +1,651 @@ +/* + * Copyright (C) 2005, 2006, 2007, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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 "TypingCommand.h" + +#include "BeforeTextInsertedEvent.h" +#include "BreakBlockquoteCommand.h" +#include "DeleteSelectionCommand.h" +#include "Document.h" +#include "Editor.h" +#include "Element.h" +#include "Frame.h" +#include "HTMLNames.h" +#include "InsertLineBreakCommand.h" +#include "InsertParagraphSeparatorCommand.h" +#include "InsertTextCommand.h" +#include "RenderObject.h" +#include "SelectionController.h" +#include "VisiblePosition.h" +#include "htmlediting.h" +#include "visible_units.h" + +namespace WebCore { + +using namespace HTMLNames; + +TypingCommand::TypingCommand(Document *document, ETypingCommand commandType, const String &textToInsert, bool selectInsertedText, TextGranularity granularity, bool killRing) + : CompositeEditCommand(document), + m_commandType(commandType), + m_textToInsert(textToInsert), + m_openForMoreTyping(true), + m_selectInsertedText(selectInsertedText), + m_smartDelete(false), + m_granularity(granularity), + m_killRing(killRing), + m_openedByBackwardDelete(false) +{ + updatePreservesTypingStyle(m_commandType); +} + +void TypingCommand::deleteSelection(Document* document, bool smartDelete) +{ + ASSERT(document); + + Frame* frame = document->frame(); + ASSERT(frame); + + if (!frame->selection()->isRange()) + return; + + EditCommand* lastEditCommand = frame->editor()->lastEditCommand(); + if (isOpenForMoreTypingCommand(lastEditCommand)) { + static_cast<TypingCommand*>(lastEditCommand)->deleteSelection(smartDelete); + return; + } + + RefPtr<TypingCommand> typingCommand = TypingCommand::create(document, DeleteSelection, "", false); + typingCommand->setSmartDelete(smartDelete); + typingCommand->apply(); +} + +void TypingCommand::deleteKeyPressed(Document *document, bool smartDelete, TextGranularity granularity, bool killRing) +{ + ASSERT(document); + + Frame* frame = document->frame(); + ASSERT(frame); + + EditCommand* lastEditCommand = frame->editor()->lastEditCommand(); + if (granularity == CharacterGranularity && isOpenForMoreTypingCommand(lastEditCommand)) { + updateSelectionIfDifferentFromCurrentSelection(static_cast<TypingCommand*>(lastEditCommand), frame); + static_cast<TypingCommand*>(lastEditCommand)->deleteKeyPressed(granularity, killRing); + return; + } + + RefPtr<TypingCommand> typingCommand = TypingCommand::create(document, DeleteKey, "", false, granularity, killRing); + typingCommand->setSmartDelete(smartDelete); + typingCommand->apply(); +} + +void TypingCommand::forwardDeleteKeyPressed(Document *document, bool smartDelete, TextGranularity granularity, bool killRing) +{ + // FIXME: Forward delete in TextEdit appears to open and close a new typing command. + ASSERT(document); + + Frame* frame = document->frame(); + ASSERT(frame); + + EditCommand* lastEditCommand = frame->editor()->lastEditCommand(); + if (granularity == CharacterGranularity && isOpenForMoreTypingCommand(lastEditCommand)) { + updateSelectionIfDifferentFromCurrentSelection(static_cast<TypingCommand*>(lastEditCommand), frame); + static_cast<TypingCommand*>(lastEditCommand)->forwardDeleteKeyPressed(granularity, killRing); + return; + } + + RefPtr<TypingCommand> typingCommand = TypingCommand::create(document, ForwardDeleteKey, "", false, granularity, killRing); + typingCommand->setSmartDelete(smartDelete); + typingCommand->apply(); +} + +void TypingCommand::updateSelectionIfDifferentFromCurrentSelection(TypingCommand* typingCommand, Frame* frame) +{ + ASSERT(frame); + VisibleSelection currentSelection = frame->selection()->selection(); + if (currentSelection == typingCommand->endingSelection()) + return; + + typingCommand->setStartingSelection(currentSelection); + typingCommand->setEndingSelection(currentSelection); +} + + +void TypingCommand::insertText(Document* document, const String& text, bool selectInsertedText, bool insertedTextIsComposition) +{ + ASSERT(document); + + Frame* frame = document->frame(); + ASSERT(frame); + + insertText(document, text, frame->selection()->selection(), selectInsertedText, insertedTextIsComposition); +} + +// FIXME: We shouldn't need to take selectionForInsertion. It should be identical to SelectionController's current selection. +void TypingCommand::insertText(Document* document, const String& text, const VisibleSelection& selectionForInsertion, bool selectInsertedText, bool insertedTextIsComposition) +{ +#if REMOVE_MARKERS_UPON_EDITING + if (!text.isEmpty()) + document->frame()->editor()->removeSpellAndCorrectionMarkersFromWordsToBeEdited(isSpaceOrNewline(text.characters()[0])); +#endif + + ASSERT(document); + + RefPtr<Frame> frame = document->frame(); + ASSERT(frame); + + VisibleSelection currentSelection = frame->selection()->selection(); + bool changeSelection = currentSelection != selectionForInsertion; + String newText = text; + Node* startNode = selectionForInsertion.start().node(); + + if (startNode && startNode->rootEditableElement() && !insertedTextIsComposition) { + // Send BeforeTextInsertedEvent. The event handler will update text if necessary. + ExceptionCode ec = 0; + RefPtr<BeforeTextInsertedEvent> evt = BeforeTextInsertedEvent::create(text); + startNode->rootEditableElement()->dispatchEvent(evt, ec); + newText = evt->text(); + } + + if (newText.isEmpty()) + return; + + // Set the starting and ending selection appropriately if we are using a selection + // that is different from the current selection. In the future, we should change EditCommand + // to deal with custom selections in a general way that can be used by all of the commands. + RefPtr<EditCommand> lastEditCommand = frame->editor()->lastEditCommand(); + if (isOpenForMoreTypingCommand(lastEditCommand.get())) { + TypingCommand* lastTypingCommand = static_cast<TypingCommand*>(lastEditCommand.get()); + if (lastTypingCommand->endingSelection() != selectionForInsertion) { + lastTypingCommand->setStartingSelection(selectionForInsertion); + lastTypingCommand->setEndingSelection(selectionForInsertion); + } + lastTypingCommand->insertText(newText, selectInsertedText); + return; + } + + RefPtr<TypingCommand> cmd = TypingCommand::create(document, InsertText, newText, selectInsertedText); + if (changeSelection) { + cmd->setStartingSelection(selectionForInsertion); + cmd->setEndingSelection(selectionForInsertion); + } + applyCommand(cmd); + if (changeSelection) { + cmd->setEndingSelection(currentSelection); + frame->selection()->setSelection(currentSelection); + } +} + +void TypingCommand::insertLineBreak(Document *document) +{ + ASSERT(document); + + Frame* frame = document->frame(); + ASSERT(frame); + + EditCommand* lastEditCommand = frame->editor()->lastEditCommand(); + if (isOpenForMoreTypingCommand(lastEditCommand)) { + static_cast<TypingCommand*>(lastEditCommand)->insertLineBreak(); + return; + } + + applyCommand(TypingCommand::create(document, InsertLineBreak)); +} + +void TypingCommand::insertParagraphSeparatorInQuotedContent(Document *document) +{ + ASSERT(document); + + Frame* frame = document->frame(); + ASSERT(frame); + + EditCommand* lastEditCommand = frame->editor()->lastEditCommand(); + if (isOpenForMoreTypingCommand(lastEditCommand)) { + static_cast<TypingCommand*>(lastEditCommand)->insertParagraphSeparatorInQuotedContent(); + return; + } + + applyCommand(TypingCommand::create(document, InsertParagraphSeparatorInQuotedContent)); +} + +void TypingCommand::insertParagraphSeparator(Document *document) +{ + ASSERT(document); + + Frame* frame = document->frame(); + ASSERT(frame); + + EditCommand* lastEditCommand = frame->editor()->lastEditCommand(); + if (isOpenForMoreTypingCommand(lastEditCommand)) { + static_cast<TypingCommand*>(lastEditCommand)->insertParagraphSeparator(); + return; + } + + applyCommand(TypingCommand::create(document, InsertParagraphSeparator)); +} + +bool TypingCommand::isOpenForMoreTypingCommand(const EditCommand* cmd) +{ + return cmd && cmd->isTypingCommand() && static_cast<const TypingCommand*>(cmd)->isOpenForMoreTyping(); +} + +void TypingCommand::closeTyping(EditCommand* cmd) +{ + if (isOpenForMoreTypingCommand(cmd)) + static_cast<TypingCommand*>(cmd)->closeTyping(); +} + +void TypingCommand::doApply() +{ + if (!endingSelection().isNonOrphanedCaretOrRange()) + return; + + if (m_commandType == DeleteKey) + if (m_commands.isEmpty()) + m_openedByBackwardDelete = true; + + switch (m_commandType) { + case DeleteSelection: + deleteSelection(m_smartDelete); + return; + case DeleteKey: + deleteKeyPressed(m_granularity, m_killRing); + return; + case ForwardDeleteKey: + forwardDeleteKeyPressed(m_granularity, m_killRing); + return; + case InsertLineBreak: + insertLineBreak(); + return; + case InsertParagraphSeparator: + insertParagraphSeparator(); + return; + case InsertParagraphSeparatorInQuotedContent: + insertParagraphSeparatorInQuotedContent(); + return; + case InsertText: + insertText(m_textToInsert, m_selectInsertedText); + return; + } + + ASSERT_NOT_REACHED(); +} + +EditAction TypingCommand::editingAction() const +{ + return EditActionTyping; +} + +void TypingCommand::markMisspellingsAfterTyping(ETypingCommand commandType) +{ +#if PLATFORM(MAC) && !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD) + if (!document()->frame()->editor()->isContinuousSpellCheckingEnabled() + && !document()->frame()->editor()->isAutomaticQuoteSubstitutionEnabled() + && !document()->frame()->editor()->isAutomaticLinkDetectionEnabled() + && !document()->frame()->editor()->isAutomaticDashSubstitutionEnabled() + && !document()->frame()->editor()->isAutomaticTextReplacementEnabled()) + return; +#else + if (!document()->frame()->editor()->isContinuousSpellCheckingEnabled()) + return; +#endif + // Take a look at the selection that results after typing and determine whether we need to spellcheck. + // Since the word containing the current selection is never marked, this does a check to + // see if typing made a new word that is not in the current selection. Basically, you + // get this by being at the end of a word and typing a space. + VisiblePosition start(endingSelection().start(), endingSelection().affinity()); + VisiblePosition previous = start.previous(); + if (previous.isNotNull()) { + VisiblePosition p1 = startOfWord(previous, LeftWordIfOnBoundary); + VisiblePosition p2 = startOfWord(start, LeftWordIfOnBoundary); + if (p1 != p2) + document()->frame()->editor()->markMisspellingsAfterTypingToWord(p1, endingSelection()); +#if SUPPORT_AUTOCORRECTION_PANEL + else if (commandType == TypingCommand::InsertText) + document()->frame()->editor()->startCorrectionPanelTimer(CorrectionPanelInfo::PanelTypeCorrection); +#else + UNUSED_PARAM(commandType); +#endif + } +} + +void TypingCommand::typingAddedToOpenCommand(ETypingCommand commandTypeForAddedTyping) +{ + updatePreservesTypingStyle(commandTypeForAddedTyping); + +#if PLATFORM(MAC) && !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD) + document()->frame()->editor()->appliedEditing(this); + // Since the spellchecking code may also perform corrections and other replacements, it should happen after the typing changes. + markMisspellingsAfterTyping(commandTypeForAddedTyping); +#else + // The old spellchecking code requires that checking be done first, to prevent issues like that in 6864072, where <doesn't> is marked as misspelled. + markMisspellingsAfterTyping(commandTypeForAddedTyping); + document()->frame()->editor()->appliedEditing(this); +#endif +} + +void TypingCommand::insertText(const String &text, bool selectInsertedText) +{ + // FIXME: Need to implement selectInsertedText for cases where more than one insert is involved. + // This requires support from insertTextRunWithoutNewlines and insertParagraphSeparator for extending + // an existing selection; at the moment they can either put the caret after what's inserted or + // select what's inserted, but there's no way to "extend selection" to include both an old selection + // that ends just before where we want to insert text and the newly inserted text. + unsigned offset = 0; + size_t newline; + while ((newline = text.find('\n', offset)) != notFound) { + if (newline != offset) + insertTextRunWithoutNewlines(text.substring(offset, newline - offset), false); + insertParagraphSeparator(); + offset = newline + 1; + } + if (!offset) + insertTextRunWithoutNewlines(text, selectInsertedText); + else { + unsigned length = text.length(); + if (length != offset) + insertTextRunWithoutNewlines(text.substring(offset, length - offset), selectInsertedText); + } +} + +void TypingCommand::insertTextRunWithoutNewlines(const String &text, bool selectInsertedText) +{ + RefPtr<InsertTextCommand> command; + if (!document()->frame()->selection()->typingStyle() && !m_commands.isEmpty()) { + EditCommand* lastCommand = m_commands.last().get(); + if (lastCommand->isInsertTextCommand()) + command = static_cast<InsertTextCommand*>(lastCommand); + } + if (!command) { + command = InsertTextCommand::create(document()); + applyCommandToComposite(command); + } + if (endingSelection() != command->endingSelection()) { + command->setStartingSelection(endingSelection()); + command->setEndingSelection(endingSelection()); + } + command->input(text, selectInsertedText); + typingAddedToOpenCommand(InsertText); +} + +void TypingCommand::insertLineBreak() +{ + applyCommandToComposite(InsertLineBreakCommand::create(document())); + typingAddedToOpenCommand(InsertLineBreak); +} + +void TypingCommand::insertParagraphSeparator() +{ + applyCommandToComposite(InsertParagraphSeparatorCommand::create(document())); + typingAddedToOpenCommand(InsertParagraphSeparator); +} + +void TypingCommand::insertParagraphSeparatorInQuotedContent() +{ + // If the selection starts inside a table, just insert the paragraph separator normally + // Breaking the blockquote would also break apart the table, which is unecessary when inserting a newline + if (enclosingNodeOfType(endingSelection().start(), &isTableStructureNode)) { + insertParagraphSeparator(); + return; + } + + applyCommandToComposite(BreakBlockquoteCommand::create(document())); + typingAddedToOpenCommand(InsertParagraphSeparatorInQuotedContent); +} + +bool TypingCommand::makeEditableRootEmpty() +{ + Element* root = endingSelection().rootEditableElement(); + if (!root->firstChild()) + return false; + + if (root->firstChild() == root->lastChild() && root->firstElementChild() && root->firstElementChild()->hasTagName(brTag)) { + // If there is a single child and it could be a placeholder, leave it alone. + if (root->renderer() && root->renderer()->isBlockFlow()) + return false; + } + + while (Node* child = root->firstChild()) + removeNode(child); + + addBlockPlaceholderIfNeeded(root); + setEndingSelection(VisibleSelection(Position(root, 0), DOWNSTREAM)); + + return true; +} + +void TypingCommand::deleteKeyPressed(TextGranularity granularity, bool killRing) +{ +#if REMOVE_MARKERS_UPON_EDITING + document()->frame()->editor()->removeSpellAndCorrectionMarkersFromWordsToBeEdited(false); +#endif + VisibleSelection selectionToDelete; + VisibleSelection selectionAfterUndo; + + switch (endingSelection().selectionType()) { + case VisibleSelection::RangeSelection: + selectionToDelete = endingSelection(); + selectionAfterUndo = selectionToDelete; + break; + case VisibleSelection::CaretSelection: { + // After breaking out of an empty mail blockquote, we still want continue with the deletion + // so actual content will get deleted, and not just the quote style. + if (breakOutOfEmptyMailBlockquotedParagraph()) + typingAddedToOpenCommand(DeleteKey); + + m_smartDelete = false; + + SelectionController selection; + selection.setSelection(endingSelection()); + selection.modify(SelectionController::AlterationExtend, DirectionBackward, granularity); + if (killRing && selection.isCaret() && granularity != CharacterGranularity) + selection.modify(SelectionController::AlterationExtend, DirectionBackward, CharacterGranularity); + + if (endingSelection().visibleStart().previous(true).isNull()) { + // When the caret is at the start of the editable area in an empty list item, break out of the list item. + if (breakOutOfEmptyListItem()) { + typingAddedToOpenCommand(DeleteKey); + return; + } + // When there are no visible positions in the editing root, delete its entire contents. + if (endingSelection().visibleStart().next(true).isNull() && makeEditableRootEmpty()) { + typingAddedToOpenCommand(DeleteKey); + return; + } + } + + VisiblePosition visibleStart(endingSelection().visibleStart()); + // If we have a caret selection on an empty cell, we have nothing to do. + if (isEmptyTableCell(visibleStart.deepEquivalent().node())) + return; + + // If the caret is at the start of a paragraph after a table, move content into the last table cell. + if (isStartOfParagraph(visibleStart) && isFirstPositionAfterTable(visibleStart.previous(true))) { + // Unless the caret is just before a table. We don't want to move a table into the last table cell. + if (isLastPositionBeforeTable(visibleStart)) + return; + // Extend the selection backward into the last cell, then deletion will handle the move. + selection.modify(SelectionController::AlterationExtend, DirectionBackward, granularity); + // If the caret is just after a table, select the table and don't delete anything. + } else if (Node* table = isFirstPositionAfterTable(visibleStart)) { + setEndingSelection(VisibleSelection(Position(table, 0), endingSelection().start(), DOWNSTREAM)); + typingAddedToOpenCommand(DeleteKey); + return; + } + + selectionToDelete = selection.selection(); + + if (granularity == CharacterGranularity && selectionToDelete.end().node() == selectionToDelete.start().node() && selectionToDelete.end().deprecatedEditingOffset() - selectionToDelete.start().deprecatedEditingOffset() > 1) { + // If there are multiple Unicode code points to be deleted, adjust the range to match platform conventions. + selectionToDelete.setWithoutValidation(selectionToDelete.end(), selectionToDelete.end().previous(BackwardDeletion)); + } + + if (!startingSelection().isRange() || selectionToDelete.base() != startingSelection().start()) + selectionAfterUndo = selectionToDelete; + else + // It's a little tricky to compute what the starting selection would have been in the original document. + // We can't let the VisibleSelection class's validation kick in or it'll adjust for us based on + // the current state of the document and we'll get the wrong result. + selectionAfterUndo.setWithoutValidation(startingSelection().end(), selectionToDelete.extent()); + break; + } + case VisibleSelection::NoSelection: + ASSERT_NOT_REACHED(); + break; + } + + ASSERT(!selectionToDelete.isNone()); + if (selectionToDelete.isNone()) + return; + + if (selectionToDelete.isCaret() || !document()->frame()->selection()->shouldDeleteSelection(selectionToDelete)) + return; + + if (killRing) + document()->frame()->editor()->addToKillRing(selectionToDelete.toNormalizedRange().get(), false); + // Make undo select everything that has been deleted, unless an undo will undo more than just this deletion. + // FIXME: This behaves like TextEdit except for the case where you open with text insertion and then delete + // more text than you insert. In that case all of the text that was around originally should be selected. + if (m_openedByBackwardDelete) + setStartingSelection(selectionAfterUndo); + CompositeEditCommand::deleteSelection(selectionToDelete, m_smartDelete); + setSmartDelete(false); + typingAddedToOpenCommand(DeleteKey); +} + +void TypingCommand::forwardDeleteKeyPressed(TextGranularity granularity, bool killRing) +{ +#if REMOVE_MARKERS_UPON_EDITING + document()->frame()->editor()->removeSpellAndCorrectionMarkersFromWordsToBeEdited(false); +#endif + VisibleSelection selectionToDelete; + VisibleSelection selectionAfterUndo; + + switch (endingSelection().selectionType()) { + case VisibleSelection::RangeSelection: + selectionToDelete = endingSelection(); + selectionAfterUndo = selectionToDelete; + break; + case VisibleSelection::CaretSelection: { + m_smartDelete = false; + + // Handle delete at beginning-of-block case. + // Do nothing in the case that the caret is at the start of a + // root editable element or at the start of a document. + SelectionController selection; + selection.setSelection(endingSelection()); + selection.modify(SelectionController::AlterationExtend, DirectionForward, granularity); + if (killRing && selection.isCaret() && granularity != CharacterGranularity) + selection.modify(SelectionController::AlterationExtend, DirectionForward, CharacterGranularity); + + Position downstreamEnd = endingSelection().end().downstream(); + VisiblePosition visibleEnd = endingSelection().visibleEnd(); + if (visibleEnd == endOfParagraph(visibleEnd)) + downstreamEnd = visibleEnd.next(true).deepEquivalent().downstream(); + // When deleting tables: Select the table first, then perform the deletion + if (downstreamEnd.node() && downstreamEnd.node()->renderer() && downstreamEnd.node()->renderer()->isTable() && !downstreamEnd.deprecatedEditingOffset()) { + setEndingSelection(VisibleSelection(endingSelection().end(), lastDeepEditingPositionForNode(downstreamEnd.node()), DOWNSTREAM)); + typingAddedToOpenCommand(ForwardDeleteKey); + return; + } + + // deleting to end of paragraph when at end of paragraph needs to merge the next paragraph (if any) + if (granularity == ParagraphBoundary && selection.selection().isCaret() && isEndOfParagraph(selection.selection().visibleEnd())) + selection.modify(SelectionController::AlterationExtend, DirectionForward, CharacterGranularity); + + selectionToDelete = selection.selection(); + if (!startingSelection().isRange() || selectionToDelete.base() != startingSelection().start()) + selectionAfterUndo = selectionToDelete; + else { + // It's a little tricky to compute what the starting selection would have been in the original document. + // We can't let the VisibleSelection class's validation kick in or it'll adjust for us based on + // the current state of the document and we'll get the wrong result. + Position extent = startingSelection().end(); + if (extent.node() != selectionToDelete.end().node()) + extent = selectionToDelete.extent(); + else { + int extraCharacters; + if (selectionToDelete.start().node() == selectionToDelete.end().node()) + extraCharacters = selectionToDelete.end().deprecatedEditingOffset() - selectionToDelete.start().deprecatedEditingOffset(); + else + extraCharacters = selectionToDelete.end().deprecatedEditingOffset(); + extent = Position(extent.node(), extent.deprecatedEditingOffset() + extraCharacters); + } + selectionAfterUndo.setWithoutValidation(startingSelection().start(), extent); + } + break; + } + case VisibleSelection::NoSelection: + ASSERT_NOT_REACHED(); + break; + } + + ASSERT(!selectionToDelete.isNone()); + if (selectionToDelete.isNone()) + return; + + if (selectionToDelete.isCaret() || !document()->frame()->selection()->shouldDeleteSelection(selectionToDelete)) + return; + + if (killRing) + document()->frame()->editor()->addToKillRing(selectionToDelete.toNormalizedRange().get(), false); + // make undo select what was deleted + setStartingSelection(selectionAfterUndo); + CompositeEditCommand::deleteSelection(selectionToDelete, m_smartDelete); + setSmartDelete(false); + typingAddedToOpenCommand(ForwardDeleteKey); +} + +void TypingCommand::deleteSelection(bool smartDelete) +{ + CompositeEditCommand::deleteSelection(smartDelete); + typingAddedToOpenCommand(DeleteSelection); +} + +void TypingCommand::updatePreservesTypingStyle(ETypingCommand commandType) +{ + switch (commandType) { + case DeleteSelection: + case DeleteKey: + case ForwardDeleteKey: + case InsertParagraphSeparator: + case InsertLineBreak: + m_preservesTypingStyle = true; + return; + case InsertParagraphSeparatorInQuotedContent: + case InsertText: + m_preservesTypingStyle = false; + return; + } + ASSERT_NOT_REACHED(); + m_preservesTypingStyle = false; +} + +bool TypingCommand::isTypingCommand() const +{ + return true; +} + +} // namespace WebCore diff --git a/Source/WebCore/editing/TypingCommand.h b/Source/WebCore/editing/TypingCommand.h new file mode 100644 index 0000000..284ebc0 --- /dev/null +++ b/Source/WebCore/editing/TypingCommand.h @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2005, 2006, 2007, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef TypingCommand_h +#define TypingCommand_h + +#include "CompositeEditCommand.h" + +namespace WebCore { + +class TypingCommand : public CompositeEditCommand { +public: + enum ETypingCommand { + DeleteSelection, + DeleteKey, + ForwardDeleteKey, + InsertText, + InsertLineBreak, + InsertParagraphSeparator, + InsertParagraphSeparatorInQuotedContent + }; + + static void deleteSelection(Document*, bool smartDelete = false); + static void deleteKeyPressed(Document*, bool smartDelete = false, TextGranularity = CharacterGranularity, bool killRing = false); + static void forwardDeleteKeyPressed(Document*, bool smartDelete = false, TextGranularity = CharacterGranularity, bool killRing = false); + static void insertText(Document*, const String&, bool selectInsertedText = false, bool insertedTextIsComposition = false); + static void insertText(Document*, const String&, const VisibleSelection&, bool selectInsertedText = false, bool insertedTextIsComposition = false); + static void insertLineBreak(Document*); + static void insertParagraphSeparator(Document*); + static void insertParagraphSeparatorInQuotedContent(Document*); + static bool isOpenForMoreTypingCommand(const EditCommand*); + static void closeTyping(EditCommand*); + + bool isOpenForMoreTyping() const { return m_openForMoreTyping; } + void closeTyping() { m_openForMoreTyping = false; } + + void insertText(const String &text, bool selectInsertedText); + void insertTextRunWithoutNewlines(const String &text, bool selectInsertedText); + void insertLineBreak(); + void insertParagraphSeparatorInQuotedContent(); + void insertParagraphSeparator(); + void deleteKeyPressed(TextGranularity, bool killRing); + void forwardDeleteKeyPressed(TextGranularity, bool killRing); + void deleteSelection(bool smartDelete); + +private: + static PassRefPtr<TypingCommand> create(Document* document, ETypingCommand command, const String& text = "", bool selectInsertedText = false, TextGranularity granularity = CharacterGranularity, bool killRing = false) + { + return adoptRef(new TypingCommand(document, command, text, selectInsertedText, granularity, killRing)); + } + + TypingCommand(Document*, ETypingCommand, const String& text, bool selectInsertedText, TextGranularity, bool killRing); + + bool smartDelete() const { return m_smartDelete; } + void setSmartDelete(bool smartDelete) { m_smartDelete = smartDelete; } + + virtual void doApply(); + virtual EditAction editingAction() const; + virtual bool isTypingCommand() const; + virtual bool preservesTypingStyle() const { return m_preservesTypingStyle; } + + static void updateSelectionIfDifferentFromCurrentSelection(TypingCommand*, Frame*); + + void updatePreservesTypingStyle(ETypingCommand); + void markMisspellingsAfterTyping(ETypingCommand); + void typingAddedToOpenCommand(ETypingCommand); + bool makeEditableRootEmpty(); + + ETypingCommand m_commandType; + String m_textToInsert; + bool m_openForMoreTyping; + bool m_selectInsertedText; + bool m_smartDelete; + TextGranularity m_granularity; + bool m_killRing; + bool m_preservesTypingStyle; + + // Undoing a series of backward deletes will restore a selection around all of the + // characters that were deleted, but only if the typing command being undone + // was opened with a backward delete. + bool m_openedByBackwardDelete; +}; + +} // namespace WebCore + +#endif // TypingCommand_h diff --git a/Source/WebCore/editing/UnlinkCommand.cpp b/Source/WebCore/editing/UnlinkCommand.cpp new file mode 100644 index 0000000..0518838 --- /dev/null +++ b/Source/WebCore/editing/UnlinkCommand.cpp @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2006 Apple Computer, 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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 "UnlinkCommand.h" + +#include "HTMLAnchorElement.h" + +namespace WebCore { + +UnlinkCommand::UnlinkCommand(Document* document) + : CompositeEditCommand(document) +{ +} + +void UnlinkCommand::doApply() +{ + // FIXME: If a caret is inside a link, we should remove it, but currently we don't. + if (!endingSelection().isNonOrphanedRange()) + return; + + removeStyledElement(HTMLAnchorElement::create(document())); +} + +} diff --git a/Source/WebCore/editing/UnlinkCommand.h b/Source/WebCore/editing/UnlinkCommand.h new file mode 100644 index 0000000..f3d560f --- /dev/null +++ b/Source/WebCore/editing/UnlinkCommand.h @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2006, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef UnlinkCommand_h +#define UnlinkCommand_h + +#include "CompositeEditCommand.h" + +namespace WebCore { + +class UnlinkCommand : public CompositeEditCommand { +public: + static PassRefPtr<UnlinkCommand> create(Document* document) + { + return adoptRef(new UnlinkCommand(document)); + } + +private: + UnlinkCommand(Document*); + + virtual void doApply(); + virtual EditAction editingAction() const { return EditActionUnlink; } +}; + +} // namespace WebCore + +#endif // UnlinkCommand_h diff --git a/Source/WebCore/editing/VisiblePosition.cpp b/Source/WebCore/editing/VisiblePosition.cpp new file mode 100644 index 0000000..adfead1 --- /dev/null +++ b/Source/WebCore/editing/VisiblePosition.cpp @@ -0,0 +1,684 @@ +/* + * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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 "VisiblePosition.h" + +#include "Document.h" +#include "FloatQuad.h" +#include "HTMLElement.h" +#include "HTMLNames.h" +#include "InlineTextBox.h" +#include "Logging.h" +#include "Range.h" +#include "Text.h" +#include "htmlediting.h" +#include "visible_units.h" +#include <stdio.h> +#include <wtf/text/CString.h> + +namespace WebCore { + +using namespace HTMLNames; + +VisiblePosition::VisiblePosition(const Position &pos, EAffinity affinity) +{ + init(pos, affinity); +} + +VisiblePosition::VisiblePosition(Node *node, int offset, EAffinity affinity) +{ + ASSERT(offset >= 0); + init(Position(node, offset), affinity); +} + +void VisiblePosition::init(const Position& position, EAffinity affinity) +{ + m_affinity = affinity; + + m_deepPosition = canonicalPosition(position); + + // When not at a line wrap, make sure to end up with DOWNSTREAM affinity. + if (m_affinity == UPSTREAM && (isNull() || inSameLine(VisiblePosition(position, DOWNSTREAM), *this))) + m_affinity = DOWNSTREAM; +} + +VisiblePosition VisiblePosition::next(bool stayInEditableContent) const +{ + VisiblePosition next(nextVisuallyDistinctCandidate(m_deepPosition), m_affinity); + + if (!stayInEditableContent) + return next; + + return honorEditableBoundaryAtOrAfter(next); +} + +VisiblePosition VisiblePosition::previous(bool stayInEditableContent) const +{ + // find first previous DOM position that is visible + Position pos = previousVisuallyDistinctCandidate(m_deepPosition); + + // return null visible position if there is no previous visible position + if (pos.atStartOfTree()) + return VisiblePosition(); + + VisiblePosition prev = VisiblePosition(pos, DOWNSTREAM); + ASSERT(prev != *this); + +#ifndef NDEBUG + // we should always be able to make the affinity DOWNSTREAM, because going previous from an + // UPSTREAM position can never yield another UPSTREAM position (unless line wrap length is 0!). + if (prev.isNotNull() && m_affinity == UPSTREAM) { + VisiblePosition temp = prev; + temp.setAffinity(UPSTREAM); + ASSERT(inSameLine(temp, prev)); + } +#endif + + if (!stayInEditableContent) + return prev; + + return honorEditableBoundaryAtOrBefore(prev); +} + +Position VisiblePosition::leftVisuallyDistinctCandidate() const +{ + Position p = m_deepPosition; + if (!p.node()) + return Position(); + + Position downstreamStart = p.downstream(); + TextDirection primaryDirection = p.primaryDirection(); + + while (true) { + InlineBox* box; + int offset; + p.getInlineBoxAndOffset(m_affinity, primaryDirection, box, offset); + if (!box) + return primaryDirection == LTR ? previousVisuallyDistinctCandidate(m_deepPosition) : nextVisuallyDistinctCandidate(m_deepPosition); + + RenderObject* renderer = box->renderer(); + + while (true) { + if ((renderer->isReplaced() || renderer->isBR()) && offset == box->caretRightmostOffset()) + return box->isLeftToRightDirection() ? previousVisuallyDistinctCandidate(m_deepPosition) : nextVisuallyDistinctCandidate(m_deepPosition); + + offset = box->isLeftToRightDirection() ? renderer->previousOffset(offset) : renderer->nextOffset(offset); + + int caretMinOffset = box->caretMinOffset(); + int caretMaxOffset = box->caretMaxOffset(); + + if (offset > caretMinOffset && offset < caretMaxOffset) + break; + + if (box->isLeftToRightDirection() ? offset < caretMinOffset : offset > caretMaxOffset) { + // Overshot to the left. + InlineBox* prevBox = box->prevLeafChild(); + if (!prevBox) + return primaryDirection == LTR ? previousVisuallyDistinctCandidate(m_deepPosition) : nextVisuallyDistinctCandidate(m_deepPosition); + + // Reposition at the other logical position corresponding to our edge's visual position and go for another round. + box = prevBox; + renderer = box->renderer(); + offset = prevBox->caretRightmostOffset(); + continue; + } + + ASSERT(offset == box->caretLeftmostOffset()); + + unsigned char level = box->bidiLevel(); + InlineBox* prevBox = box->prevLeafChild(); + + if (box->direction() == primaryDirection) { + if (!prevBox || prevBox->bidiLevel() >= level) + break; + + level = prevBox->bidiLevel(); + + InlineBox* nextBox = box; + do { + nextBox = nextBox->nextLeafChild(); + } while (nextBox && nextBox->bidiLevel() > level); + + if (nextBox && nextBox->bidiLevel() == level) + break; + + while (InlineBox* prevBox = box->prevLeafChild()) { + if (prevBox->bidiLevel() < level) + break; + box = prevBox; + } + renderer = box->renderer(); + offset = box->caretRightmostOffset(); + if (box->direction() == primaryDirection) + break; + continue; + } + + if (prevBox) { + box = prevBox; + renderer = box->renderer(); + offset = box->caretRightmostOffset(); + if (box->bidiLevel() > level) { + do { + prevBox = prevBox->prevLeafChild(); + } while (prevBox && prevBox->bidiLevel() > level); + + if (!prevBox || prevBox->bidiLevel() < level) + continue; + } + } else { + // Trailing edge of a secondary run. Set to the leading edge of the entire run. + while (true) { + while (InlineBox* nextBox = box->nextLeafChild()) { + if (nextBox->bidiLevel() < level) + break; + box = nextBox; + } + if (box->bidiLevel() == level) + break; + level = box->bidiLevel(); + while (InlineBox* prevBox = box->prevLeafChild()) { + if (prevBox->bidiLevel() < level) + break; + box = prevBox; + } + if (box->bidiLevel() == level) + break; + level = box->bidiLevel(); + } + renderer = box->renderer(); + offset = primaryDirection == LTR ? box->caretMinOffset() : box->caretMaxOffset(); + } + break; + } + + p = Position(renderer->node(), offset); + + if ((p.isCandidate() && p.downstream() != downstreamStart) || p.atStartOfTree() || p.atEndOfTree()) + return p; + } +} + +VisiblePosition VisiblePosition::left(bool stayInEditableContent) const +{ + Position pos = leftVisuallyDistinctCandidate(); + // FIXME: Why can't we move left from the last position in a tree? + if (pos.atStartOfTree() || pos.atEndOfTree()) + return VisiblePosition(); + + VisiblePosition left = VisiblePosition(pos, DOWNSTREAM); + ASSERT(left != *this); + + if (!stayInEditableContent) + return left; + + // FIXME: This may need to do something different from "before". + return honorEditableBoundaryAtOrBefore(left); +} + +Position VisiblePosition::rightVisuallyDistinctCandidate() const +{ + Position p = m_deepPosition; + if (!p.node()) + return Position(); + + Position downstreamStart = p.downstream(); + TextDirection primaryDirection = p.primaryDirection(); + + while (true) { + InlineBox* box; + int offset; + p.getInlineBoxAndOffset(m_affinity, primaryDirection, box, offset); + if (!box) + return primaryDirection == LTR ? nextVisuallyDistinctCandidate(m_deepPosition) : previousVisuallyDistinctCandidate(m_deepPosition); + + RenderObject* renderer = box->renderer(); + + while (true) { + if ((renderer->isReplaced() || renderer->isBR()) && offset == box->caretLeftmostOffset()) + return box->isLeftToRightDirection() ? nextVisuallyDistinctCandidate(m_deepPosition) : previousVisuallyDistinctCandidate(m_deepPosition); + + offset = box->isLeftToRightDirection() ? renderer->nextOffset(offset) : renderer->previousOffset(offset); + + int caretMinOffset = box->caretMinOffset(); + int caretMaxOffset = box->caretMaxOffset(); + + if (offset > caretMinOffset && offset < caretMaxOffset) + break; + + if (box->isLeftToRightDirection() ? offset > caretMaxOffset : offset < caretMinOffset) { + // Overshot to the right. + InlineBox* nextBox = box->nextLeafChild(); + if (!nextBox) + return primaryDirection == LTR ? nextVisuallyDistinctCandidate(m_deepPosition) : previousVisuallyDistinctCandidate(m_deepPosition); + + // Reposition at the other logical position corresponding to our edge's visual position and go for another round. + box = nextBox; + renderer = box->renderer(); + offset = nextBox->caretLeftmostOffset(); + continue; + } + + ASSERT(offset == box->caretRightmostOffset()); + + unsigned char level = box->bidiLevel(); + InlineBox* nextBox = box->nextLeafChild(); + + if (box->direction() == primaryDirection) { + if (!nextBox || nextBox->bidiLevel() >= level) + break; + + level = nextBox->bidiLevel(); + + InlineBox* prevBox = box; + do { + prevBox = prevBox->prevLeafChild(); + } while (prevBox && prevBox->bidiLevel() > level); + + if (prevBox && prevBox->bidiLevel() == level) // For example, abc FED 123 ^ CBA + break; + + // For example, abc 123 ^ CBA + while (InlineBox* nextBox = box->nextLeafChild()) { + if (nextBox->bidiLevel() < level) + break; + box = nextBox; + } + renderer = box->renderer(); + offset = box->caretLeftmostOffset(); + if (box->direction() == primaryDirection) + break; + continue; + } + + if (nextBox) { + box = nextBox; + renderer = box->renderer(); + offset = box->caretLeftmostOffset(); + if (box->bidiLevel() > level) { + do { + nextBox = nextBox->nextLeafChild(); + } while (nextBox && nextBox->bidiLevel() > level); + + if (!nextBox || nextBox->bidiLevel() < level) + continue; + } + } else { + // Trailing edge of a secondary run. Set to the leading edge of the entire run. + while (true) { + while (InlineBox* prevBox = box->prevLeafChild()) { + if (prevBox->bidiLevel() < level) + break; + box = prevBox; + } + if (box->bidiLevel() == level) + break; + level = box->bidiLevel(); + while (InlineBox* nextBox = box->nextLeafChild()) { + if (nextBox->bidiLevel() < level) + break; + box = nextBox; + } + if (box->bidiLevel() == level) + break; + level = box->bidiLevel(); + } + renderer = box->renderer(); + offset = primaryDirection == LTR ? box->caretMaxOffset() : box->caretMinOffset(); + } + break; + } + + p = Position(renderer->node(), offset); + + if ((p.isCandidate() && p.downstream() != downstreamStart) || p.atStartOfTree() || p.atEndOfTree()) + return p; + } +} + +VisiblePosition VisiblePosition::right(bool stayInEditableContent) const +{ + Position pos = rightVisuallyDistinctCandidate(); + // FIXME: Why can't we move left from the last position in a tree? + if (pos.atStartOfTree() || pos.atEndOfTree()) + return VisiblePosition(); + + VisiblePosition right = VisiblePosition(pos, DOWNSTREAM); + ASSERT(right != *this); + + if (!stayInEditableContent) + return right; + + // FIXME: This may need to do something different from "after". + return honorEditableBoundaryAtOrAfter(right); +} + +VisiblePosition VisiblePosition::honorEditableBoundaryAtOrBefore(const VisiblePosition &pos) const +{ + if (pos.isNull()) + return pos; + + Node* highestRoot = highestEditableRoot(deepEquivalent()); + + // Return empty position if pos is not somewhere inside the editable region containing this position + if (highestRoot && !pos.deepEquivalent().node()->isDescendantOf(highestRoot)) + return VisiblePosition(); + + // Return pos itself if the two are from the very same editable region, or both are non-editable + // FIXME: In the non-editable case, just because the new position is non-editable doesn't mean movement + // to it is allowed. VisibleSelection::adjustForEditableContent has this problem too. + if (highestEditableRoot(pos.deepEquivalent()) == highestRoot) + return pos; + + // Return empty position if this position is non-editable, but pos is editable + // FIXME: Move to the previous non-editable region. + if (!highestRoot) + return VisiblePosition(); + + // Return the last position before pos that is in the same editable region as this position + return lastEditablePositionBeforePositionInRoot(pos.deepEquivalent(), highestRoot); +} + +VisiblePosition VisiblePosition::honorEditableBoundaryAtOrAfter(const VisiblePosition &pos) const +{ + if (pos.isNull()) + return pos; + + Node* highestRoot = highestEditableRoot(deepEquivalent()); + + // Return empty position if pos is not somewhere inside the editable region containing this position + if (highestRoot && !pos.deepEquivalent().node()->isDescendantOf(highestRoot)) + return VisiblePosition(); + + // Return pos itself if the two are from the very same editable region, or both are non-editable + // FIXME: In the non-editable case, just because the new position is non-editable doesn't mean movement + // to it is allowed. VisibleSelection::adjustForEditableContent has this problem too. + if (highestEditableRoot(pos.deepEquivalent()) == highestRoot) + return pos; + + // Return empty position if this position is non-editable, but pos is editable + // FIXME: Move to the next non-editable region. + if (!highestRoot) + return VisiblePosition(); + + // Return the next position after pos that is in the same editable region as this position + return firstEditablePositionAfterPositionInRoot(pos.deepEquivalent(), highestRoot); +} + +static Position canonicalizeCandidate(const Position& candidate) +{ + if (candidate.isNull()) + return Position(); + ASSERT(candidate.isCandidate()); + Position upstream = candidate.upstream(); + if (upstream.isCandidate()) + return upstream; + return candidate; +} + +Position VisiblePosition::canonicalPosition(const Position& passedPosition) +{ + // The updateLayout call below can do so much that even the position passed + // in to us might get changed as a side effect. Specifically, there are code + // paths that pass selection endpoints, and updateLayout can change the selection. + Position position = passedPosition; + + // FIXME (9535): Canonicalizing to the leftmost candidate means that if we're at a line wrap, we will + // ask renderers to paint downstream carets for other renderers. + // To fix this, we need to either a) add code to all paintCarets to pass the responsibility off to + // the appropriate renderer for VisiblePosition's like these, or b) canonicalize to the rightmost candidate + // unless the affinity is upstream. + Node* node = position.node(); + if (!node) + return Position(); + + ASSERT(node->document()); + node->document()->updateLayoutIgnorePendingStylesheets(); + + Position candidate = position.upstream(); + if (candidate.isCandidate()) + return candidate; + candidate = position.downstream(); + if (candidate.isCandidate()) + return candidate; + + // When neither upstream or downstream gets us to a candidate (upstream/downstream won't leave + // blocks or enter new ones), we search forward and backward until we find one. + Position next = canonicalizeCandidate(nextCandidate(position)); + Position prev = canonicalizeCandidate(previousCandidate(position)); + Node* nextNode = next.node(); + Node* prevNode = prev.node(); + + // The new position must be in the same editable element. Enforce that first. + // Unless the descent is from a non-editable html element to an editable body. + if (node->hasTagName(htmlTag) && !node->isContentEditable() && node->document()->body() && node->document()->body()->isContentEditable()) + return next.isNotNull() ? next : prev; + + Node* editingRoot = editableRootForPosition(position); + + // If the html element is editable, descending into its body will look like a descent + // from non-editable to editable content since rootEditableElement() always stops at the body. + if ((editingRoot && editingRoot->hasTagName(htmlTag)) || position.node()->isDocumentNode()) + return next.isNotNull() ? next : prev; + + bool prevIsInSameEditableElement = prevNode && editableRootForPosition(prev) == editingRoot; + bool nextIsInSameEditableElement = nextNode && editableRootForPosition(next) == editingRoot; + if (prevIsInSameEditableElement && !nextIsInSameEditableElement) + return prev; + + if (nextIsInSameEditableElement && !prevIsInSameEditableElement) + return next; + + if (!nextIsInSameEditableElement && !prevIsInSameEditableElement) + return Position(); + + // The new position should be in the same block flow element. Favor that. + Node *originalBlock = node->enclosingBlockFlowElement(); + bool nextIsOutsideOriginalBlock = !nextNode->isDescendantOf(originalBlock) && nextNode != originalBlock; + bool prevIsOutsideOriginalBlock = !prevNode->isDescendantOf(originalBlock) && prevNode != originalBlock; + if (nextIsOutsideOriginalBlock && !prevIsOutsideOriginalBlock) + return prev; + + return next; +} + +UChar32 VisiblePosition::characterAfter() const +{ + // We canonicalize to the first of two equivalent candidates, but the second of the two candidates + // is the one that will be inside the text node containing the character after this visible position. + Position pos = m_deepPosition.downstream(); + Node* node = pos.node(); + if (!node || !node->isTextNode()) + return 0; + Text* textNode = static_cast<Text*>(pos.node()); + unsigned offset = pos.deprecatedEditingOffset(); + unsigned length = textNode->length(); + if (offset >= length) + return 0; + + UChar32 ch; + const UChar* characters = textNode->data().characters(); + U16_NEXT(characters, offset, length, ch); + return ch; +} + +IntRect VisiblePosition::localCaretRect(RenderObject*& renderer) const +{ + Node* node = m_deepPosition.node(); + if (!node) { + renderer = 0; + return IntRect(); + } + + renderer = node->renderer(); + if (!renderer) + return IntRect(); + + InlineBox* inlineBox; + int caretOffset; + getInlineBoxAndOffset(inlineBox, caretOffset); + + if (inlineBox) + renderer = inlineBox->renderer(); + + return renderer->localCaretRect(inlineBox, caretOffset); +} + +IntRect VisiblePosition::absoluteCaretBounds() const +{ + RenderObject* renderer; + IntRect localRect = localCaretRect(renderer); + if (localRect.isEmpty() || !renderer) + return IntRect(); + + return renderer->localToAbsoluteQuad(FloatRect(localRect)).enclosingBoundingBox(); +} + +int VisiblePosition::xOffsetForVerticalNavigation() const +{ + RenderObject* renderer; + IntRect localRect = localCaretRect(renderer); + if (localRect.isEmpty() || !renderer) + return 0; + + // This ignores transforms on purpose, for now. Vertical navigation is done + // without consulting transforms, so that 'up' in transformed text is 'up' + // relative to the text, not absolute 'up'. + return renderer->localToAbsolute(localRect.location()).x(); +} + +void VisiblePosition::debugPosition(const char* msg) const +{ + if (isNull()) + fprintf(stderr, "Position [%s]: null\n", msg); + else + fprintf(stderr, "Position [%s]: %s [%p] at %d\n", msg, m_deepPosition.node()->nodeName().utf8().data(), m_deepPosition.node(), m_deepPosition.deprecatedEditingOffset()); +} + +#ifndef NDEBUG + +void VisiblePosition::formatForDebugger(char* buffer, unsigned length) const +{ + m_deepPosition.formatForDebugger(buffer, length); +} + +void VisiblePosition::showTreeForThis() const +{ + m_deepPosition.showTreeForThis(); +} + +#endif + +PassRefPtr<Range> makeRange(const VisiblePosition &start, const VisiblePosition &end) +{ + if (start.isNull() || end.isNull()) + return 0; + + Position s = rangeCompliantEquivalent(start); + Position e = rangeCompliantEquivalent(end); + return Range::create(s.node()->document(), s.node(), s.deprecatedEditingOffset(), e.node(), e.deprecatedEditingOffset()); +} + +VisiblePosition startVisiblePosition(const Range *r, EAffinity affinity) +{ + int exception = 0; + return VisiblePosition(r->startContainer(exception), r->startOffset(exception), affinity); +} + +VisiblePosition endVisiblePosition(const Range *r, EAffinity affinity) +{ + int exception = 0; + return VisiblePosition(r->endContainer(exception), r->endOffset(exception), affinity); +} + +bool setStart(Range *r, const VisiblePosition &visiblePosition) +{ + if (!r) + return false; + Position p = rangeCompliantEquivalent(visiblePosition); + int code = 0; + r->setStart(p.node(), p.deprecatedEditingOffset(), code); + return code == 0; +} + +bool setEnd(Range *r, const VisiblePosition &visiblePosition) +{ + if (!r) + return false; + Position p = rangeCompliantEquivalent(visiblePosition); + int code = 0; + r->setEnd(p.node(), p.deprecatedEditingOffset(), code); + return code == 0; +} + +Element* enclosingBlockFlowElement(const VisiblePosition &visiblePosition) +{ + if (visiblePosition.isNull()) + return NULL; + + return visiblePosition.deepEquivalent().node()->enclosingBlockFlowElement(); +} + +bool isFirstVisiblePositionInNode(const VisiblePosition &visiblePosition, const Node *node) +{ + if (visiblePosition.isNull()) + return false; + + if (!visiblePosition.deepEquivalent().node()->isDescendantOf(node)) + return false; + + VisiblePosition previous = visiblePosition.previous(); + return previous.isNull() || !previous.deepEquivalent().node()->isDescendantOf(node); +} + +bool isLastVisiblePositionInNode(const VisiblePosition &visiblePosition, const Node *node) +{ + if (visiblePosition.isNull()) + return false; + + if (!visiblePosition.deepEquivalent().node()->isDescendantOf(node)) + return false; + + VisiblePosition next = visiblePosition.next(); + return next.isNull() || !next.deepEquivalent().node()->isDescendantOf(node); +} + +} // namespace WebCore + +#ifndef NDEBUG + +void showTree(const WebCore::VisiblePosition* vpos) +{ + if (vpos) + vpos->showTreeForThis(); +} + +void showTree(const WebCore::VisiblePosition& vpos) +{ + vpos.showTreeForThis(); +} + +#endif diff --git a/Source/WebCore/editing/VisiblePosition.h b/Source/WebCore/editing/VisiblePosition.h new file mode 100644 index 0000000..e649b68 --- /dev/null +++ b/Source/WebCore/editing/VisiblePosition.h @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2004, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef VisiblePosition_h +#define VisiblePosition_h + +#include "Node.h" +#include "Position.h" +#include "TextDirection.h" + +namespace WebCore { + +// VisiblePosition default affinity is downstream because +// the callers do not really care (they just want the +// deep position without regard to line position), and this +// is cheaper than UPSTREAM +#define VP_DEFAULT_AFFINITY DOWNSTREAM + +// Callers who do not know where on the line the position is, +// but would like UPSTREAM if at a line break or DOWNSTREAM +// otherwise, need a clear way to specify that. The +// constructors auto-correct UPSTREAM to DOWNSTREAM if the +// position is not at a line break. +#define VP_UPSTREAM_IF_POSSIBLE UPSTREAM + +class InlineBox; + +enum StayInEditableContent { MayLeaveEditableContent, MustStayInEditableContent }; + +class VisiblePosition { +public: + // NOTE: UPSTREAM affinity will be used only if pos is at end of a wrapped line, + // otherwise it will be converted to DOWNSTREAM + VisiblePosition() : m_affinity(VP_DEFAULT_AFFINITY) { } + VisiblePosition(Node*, int offset, EAffinity); + VisiblePosition(const Position&, EAffinity = VP_DEFAULT_AFFINITY); + + void clear() { m_deepPosition.clear(); } + + bool isNull() const { return m_deepPosition.isNull(); } + bool isNotNull() const { return m_deepPosition.isNotNull(); } + bool isOrphan() const { return m_deepPosition.isOrphan(); } + + Position deepEquivalent() const { return m_deepPosition; } + EAffinity affinity() const { ASSERT(m_affinity == UPSTREAM || m_affinity == DOWNSTREAM); return m_affinity; } + void setAffinity(EAffinity affinity) { m_affinity = affinity; } + + // FIXME: Change the following functions' parameter from a boolean to StayInEditableContent. + + // next() and previous() will increment/decrement by a character cluster. + VisiblePosition next(bool stayInEditableContent = false) const; + VisiblePosition previous(bool stayInEditableContent = false) const; + VisiblePosition honorEditableBoundaryAtOrBefore(const VisiblePosition&) const; + VisiblePosition honorEditableBoundaryAtOrAfter(const VisiblePosition&) const; + + VisiblePosition left(bool stayInEditableContent = false) const; + VisiblePosition right(bool stayInEditableContent = false) const; + + UChar32 characterAfter() const; + UChar32 characterBefore() const { return previous().characterAfter(); } + + void debugPosition(const char* msg = "") const; + + Element* rootEditableElement() const { return m_deepPosition.isNotNull() ? m_deepPosition.node()->rootEditableElement() : 0; } + + void getInlineBoxAndOffset(InlineBox*& inlineBox, int& caretOffset) const + { + m_deepPosition.getInlineBoxAndOffset(m_affinity, inlineBox, caretOffset); + } + + void getInlineBoxAndOffset(TextDirection primaryDirection, InlineBox*& inlineBox, int& caretOffset) const + { + m_deepPosition.getInlineBoxAndOffset(m_affinity, primaryDirection, inlineBox, caretOffset); + } + + // Rect is local to the returned renderer + IntRect localCaretRect(RenderObject*&) const; + // Bounds of (possibly transformed) caret in absolute coords + IntRect absoluteCaretBounds() const; + // Abs x position of the caret ignoring transforms. + // FIXME: navigation with transforms should be smarter. + int xOffsetForVerticalNavigation() const; + +#ifndef NDEBUG + void formatForDebugger(char* buffer, unsigned length) const; + void showTreeForThis() const; +#endif + +private: + void init(const Position&, EAffinity); + Position canonicalPosition(const Position&); + + Position leftVisuallyDistinctCandidate() const; + Position rightVisuallyDistinctCandidate() const; + + Position m_deepPosition; + EAffinity m_affinity; +}; + +// FIXME: This shouldn't ignore affinity. +inline bool operator==(const VisiblePosition& a, const VisiblePosition& b) +{ + return a.deepEquivalent() == b.deepEquivalent(); +} + +inline bool operator!=(const VisiblePosition& a, const VisiblePosition& b) +{ + return !(a == b); +} + +PassRefPtr<Range> makeRange(const VisiblePosition&, const VisiblePosition&); +bool setStart(Range*, const VisiblePosition&); +bool setEnd(Range*, const VisiblePosition&); +VisiblePosition startVisiblePosition(const Range*, EAffinity); +VisiblePosition endVisiblePosition(const Range*, EAffinity); + +Element* enclosingBlockFlowElement(const VisiblePosition&); + +bool isFirstVisiblePositionInNode(const VisiblePosition&, const Node*); +bool isLastVisiblePositionInNode(const VisiblePosition&, const Node*); + +} // namespace WebCore + +#ifndef NDEBUG +// Outside the WebCore namespace for ease of invocation from gdb. +void showTree(const WebCore::VisiblePosition*); +void showTree(const WebCore::VisiblePosition&); +#endif + +#endif // VisiblePosition_h diff --git a/Source/WebCore/editing/VisibleSelection.cpp b/Source/WebCore/editing/VisibleSelection.cpp new file mode 100644 index 0000000..4037670 --- /dev/null +++ b/Source/WebCore/editing/VisibleSelection.cpp @@ -0,0 +1,647 @@ +/* + * Copyright (C) 2004, 2005, 2006 Apple Computer, 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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 "VisibleSelection.h" + +#include "CharacterNames.h" +#include "Document.h" +#include "Element.h" +#include "htmlediting.h" +#include "TextIterator.h" +#include "VisiblePosition.h" +#include "visible_units.h" +#include "Range.h" + +#include <wtf/Assertions.h> +#include <wtf/text/CString.h> +#include <stdio.h> + +namespace WebCore { + +VisibleSelection::VisibleSelection() + : m_affinity(DOWNSTREAM) + , m_selectionType(NoSelection) + , m_baseIsFirst(true) +{ +} + +VisibleSelection::VisibleSelection(const Position& pos, EAffinity affinity) + : m_base(pos) + , m_extent(pos) + , m_affinity(affinity) +{ + validate(); +} + +VisibleSelection::VisibleSelection(const Position& base, const Position& extent, EAffinity affinity) + : m_base(base) + , m_extent(extent) + , m_affinity(affinity) +{ + validate(); +} + +VisibleSelection::VisibleSelection(const VisiblePosition& pos) + : m_base(pos.deepEquivalent()) + , m_extent(pos.deepEquivalent()) + , m_affinity(pos.affinity()) +{ + validate(); +} + +VisibleSelection::VisibleSelection(const VisiblePosition& base, const VisiblePosition& extent) + : m_base(base.deepEquivalent()) + , m_extent(extent.deepEquivalent()) + , m_affinity(base.affinity()) +{ + validate(); +} + +VisibleSelection::VisibleSelection(const Range* range, EAffinity affinity) + : m_base(range->startPosition()) + , m_extent(range->endPosition()) + , m_affinity(affinity) +{ + validate(); +} + +VisibleSelection VisibleSelection::selectionFromContentsOfNode(Node* node) +{ + return VisibleSelection(firstDeepEditingPositionForNode(node), lastDeepEditingPositionForNode(node), DOWNSTREAM); +} + +void VisibleSelection::setBase(const Position& position) +{ + m_base = position; + validate(); +} + +void VisibleSelection::setBase(const VisiblePosition& visiblePosition) +{ + m_base = visiblePosition.deepEquivalent(); + validate(); +} + +void VisibleSelection::setExtent(const Position& position) +{ + m_extent = position; + validate(); +} + +void VisibleSelection::setExtent(const VisiblePosition& visiblePosition) +{ + m_extent = visiblePosition.deepEquivalent(); + validate(); +} + +PassRefPtr<Range> VisibleSelection::firstRange() const +{ + if (isNone()) + return 0; + Position start = rangeCompliantEquivalent(m_start); + Position end = rangeCompliantEquivalent(m_end); + return Range::create(start.node()->document(), start, end); +} + +PassRefPtr<Range> VisibleSelection::toNormalizedRange() const +{ + if (isNone()) + return 0; + + // Make sure we have an updated layout since this function is called + // in the course of running edit commands which modify the DOM. + // Failing to call this can result in equivalentXXXPosition calls returning + // incorrect results. + m_start.node()->document()->updateLayout(); + + // Check again, because updating layout can clear the selection. + if (isNone()) + return 0; + + Position s, e; + if (isCaret()) { + // If the selection is a caret, move the range start upstream. This helps us match + // the conventions of text editors tested, which make style determinations based + // on the character before the caret, if any. + s = rangeCompliantEquivalent(m_start.upstream()); + e = s; + } else { + // If the selection is a range, select the minimum range that encompasses the selection. + // Again, this is to match the conventions of text editors tested, which make style + // determinations based on the first character of the selection. + // For instance, this operation helps to make sure that the "X" selected below is the + // only thing selected. The range should not be allowed to "leak" out to the end of the + // previous text node, or to the beginning of the next text node, each of which has a + // different style. + // + // On a treasure map, <b>X</b> marks the spot. + // ^ selected + // + ASSERT(isRange()); + s = m_start.downstream(); + e = m_end.upstream(); + if (comparePositions(s, e) > 0) { + // Make sure the start is before the end. + // The end can wind up before the start if collapsed whitespace is the only thing selected. + Position tmp = s; + s = e; + e = tmp; + } + s = rangeCompliantEquivalent(s); + e = rangeCompliantEquivalent(e); + } + + // VisibleSelections are supposed to always be valid. This constructor will ASSERT + // if a valid range could not be created, which is fine for this callsite. + return Range::create(s.node()->document(), s, e); +} + +bool VisibleSelection::expandUsingGranularity(TextGranularity granularity) +{ + if (isNone()) + return false; + + validate(granularity); + return true; +} + +static PassRefPtr<Range> makeSearchRange(const Position& pos) +{ + Node* n = pos.node(); + if (!n) + return 0; + Document* d = n->document(); + Node* de = d->documentElement(); + if (!de) + return 0; + Node* boundary = n->enclosingBlockFlowElement(); + if (!boundary) + return 0; + + RefPtr<Range> searchRange(Range::create(d)); + ExceptionCode ec = 0; + + Position start(rangeCompliantEquivalent(pos)); + searchRange->selectNodeContents(boundary, ec); + searchRange->setStart(start.node(), start.deprecatedEditingOffset(), ec); + + ASSERT(!ec); + if (ec) + return 0; + + return searchRange.release(); +} + +bool VisibleSelection::isAll(StayInEditableContent stayInEditableContent) const +{ + return !shadowTreeRootNode() && visibleStart().previous(stayInEditableContent).isNull() && visibleEnd().next(stayInEditableContent).isNull(); +} + +void VisibleSelection::appendTrailingWhitespace() +{ + RefPtr<Range> searchRange = makeSearchRange(m_end); + if (!searchRange) + return; + + CharacterIterator charIt(searchRange.get(), TextIteratorEmitsCharactersBetweenAllVisiblePositions); + + for (; charIt.length(); charIt.advance(1)) { + UChar c = charIt.characters()[0]; + if ((!isSpaceOrNewline(c) && c != noBreakSpace) || c == '\n') + break; + m_end = charIt.range()->endPosition(); + } +} + +void VisibleSelection::setBaseAndExtentToDeepEquivalents() +{ + // Move the selection to rendered positions, if possible. + bool baseAndExtentEqual = m_base == m_extent; + if (m_base.isNotNull()) { + m_base = VisiblePosition(m_base, m_affinity).deepEquivalent(); + if (baseAndExtentEqual) + m_extent = m_base; + } + if (m_extent.isNotNull() && !baseAndExtentEqual) + m_extent = VisiblePosition(m_extent, m_affinity).deepEquivalent(); + + // Make sure we do not have a dangling base or extent. + if (m_base.isNull() && m_extent.isNull()) + m_baseIsFirst = true; + else if (m_base.isNull()) { + m_base = m_extent; + m_baseIsFirst = true; + } else if (m_extent.isNull()) { + m_extent = m_base; + m_baseIsFirst = true; + } else + m_baseIsFirst = comparePositions(m_base, m_extent) <= 0; +} + +void VisibleSelection::setStartAndEndFromBaseAndExtentRespectingGranularity(TextGranularity granularity) +{ + if (m_baseIsFirst) { + m_start = m_base; + m_end = m_extent; + } else { + m_start = m_extent; + m_end = m_base; + } + + switch (granularity) { + case CharacterGranularity: + // Don't do any expansion. + break; + case WordGranularity: { + // General case: Select the word the caret is positioned inside of, or at the start of (RightWordIfOnBoundary). + // Edge case: If the caret is after the last word in a soft-wrapped line or the last word in + // the document, select that last word (LeftWordIfOnBoundary). + // Edge case: If the caret is after the last word in a paragraph, select from the the end of the + // last word to the line break (also RightWordIfOnBoundary); + VisiblePosition start = VisiblePosition(m_start, m_affinity); + VisiblePosition originalEnd(m_end, m_affinity); + EWordSide side = RightWordIfOnBoundary; + if (isEndOfDocument(start) || (isEndOfLine(start) && !isStartOfLine(start) && !isEndOfParagraph(start))) + side = LeftWordIfOnBoundary; + m_start = startOfWord(start, side).deepEquivalent(); + side = RightWordIfOnBoundary; + if (isEndOfDocument(originalEnd) || (isEndOfLine(originalEnd) && !isStartOfLine(originalEnd) && !isEndOfParagraph(originalEnd))) + side = LeftWordIfOnBoundary; + + VisiblePosition wordEnd(endOfWord(originalEnd, side)); + VisiblePosition end(wordEnd); + + if (isEndOfParagraph(originalEnd) && !isEmptyTableCell(m_start.node())) { + // Select the paragraph break (the space from the end of a paragraph to the start of + // the next one) to match TextEdit. + end = wordEnd.next(); + + if (Node* table = isFirstPositionAfterTable(end)) { + // The paragraph break after the last paragraph in the last cell of a block table ends + // at the start of the paragraph after the table. + if (isBlock(table)) + end = end.next(true); + else + end = wordEnd; + } + + if (end.isNull()) + end = wordEnd; + + } + + m_end = end.deepEquivalent(); + break; + } + case SentenceGranularity: { + m_start = startOfSentence(VisiblePosition(m_start, m_affinity)).deepEquivalent(); + m_end = endOfSentence(VisiblePosition(m_end, m_affinity)).deepEquivalent(); + break; + } + case LineGranularity: { + m_start = startOfLine(VisiblePosition(m_start, m_affinity)).deepEquivalent(); + VisiblePosition end = endOfLine(VisiblePosition(m_end, m_affinity)); + // If the end of this line is at the end of a paragraph, include the space + // after the end of the line in the selection. + if (isEndOfParagraph(end)) { + VisiblePosition next = end.next(); + if (next.isNotNull()) + end = next; + } + m_end = end.deepEquivalent(); + break; + } + case LineBoundary: + m_start = startOfLine(VisiblePosition(m_start, m_affinity)).deepEquivalent(); + m_end = endOfLine(VisiblePosition(m_end, m_affinity)).deepEquivalent(); + break; + case ParagraphGranularity: { + VisiblePosition pos(m_start, m_affinity); + if (isStartOfLine(pos) && isEndOfDocument(pos)) + pos = pos.previous(); + m_start = startOfParagraph(pos).deepEquivalent(); + VisiblePosition visibleParagraphEnd = endOfParagraph(VisiblePosition(m_end, m_affinity)); + + // Include the "paragraph break" (the space from the end of this paragraph to the start + // of the next one) in the selection. + VisiblePosition end(visibleParagraphEnd.next()); + + if (Node* table = isFirstPositionAfterTable(end)) { + // The paragraph break after the last paragraph in the last cell of a block table ends + // at the start of the paragraph after the table, not at the position just after the table. + if (isBlock(table)) + end = end.next(true); + // There is no parargraph break after the last paragraph in the last cell of an inline table. + else + end = visibleParagraphEnd; + } + + if (end.isNull()) + end = visibleParagraphEnd; + + m_end = end.deepEquivalent(); + break; + } + case DocumentBoundary: + m_start = startOfDocument(VisiblePosition(m_start, m_affinity)).deepEquivalent(); + m_end = endOfDocument(VisiblePosition(m_end, m_affinity)).deepEquivalent(); + break; + case ParagraphBoundary: + m_start = startOfParagraph(VisiblePosition(m_start, m_affinity)).deepEquivalent(); + m_end = endOfParagraph(VisiblePosition(m_end, m_affinity)).deepEquivalent(); + break; + case SentenceBoundary: + m_start = startOfSentence(VisiblePosition(m_start, m_affinity)).deepEquivalent(); + m_end = endOfSentence(VisiblePosition(m_end, m_affinity)).deepEquivalent(); + break; + } + + // Make sure we do not have a dangling start or end. + if (m_start.isNull()) + m_start = m_end; + if (m_end.isNull()) + m_end = m_start; +} + +void VisibleSelection::updateSelectionType() +{ + if (m_start.isNull()) { + ASSERT(m_end.isNull()); + m_selectionType = NoSelection; + } else if (m_start == m_end || m_start.upstream() == m_end.upstream()) { + m_selectionType = CaretSelection; + } else + m_selectionType = RangeSelection; + + // Affinity only makes sense for a caret + if (m_selectionType != CaretSelection) + m_affinity = DOWNSTREAM; +} + +void VisibleSelection::validate(TextGranularity granularity) +{ + setBaseAndExtentToDeepEquivalents(); + setStartAndEndFromBaseAndExtentRespectingGranularity(granularity); + adjustSelectionToAvoidCrossingEditingBoundaries(); + updateSelectionType(); + + if (selectionType() == RangeSelection) { + // "Constrain" the selection to be the smallest equivalent range of nodes. + // This is a somewhat arbitrary choice, but experience shows that it is + // useful to make to make the selection "canonical" (if only for + // purposes of comparing selections). This is an ideal point of the code + // to do this operation, since all selection changes that result in a RANGE + // come through here before anyone uses it. + // FIXME: Canonicalizing is good, but haven't we already done it (when we + // set these two positions to VisiblePosition deepEquivalent()s above)? + m_start = m_start.downstream(); + m_end = m_end.upstream(); + } +} + +// FIXME: This function breaks the invariant of this class. +// But because we use VisibleSelection to store values in editing commands for use when +// undoing the command, we need to be able to create a selection that while currently +// invalid, will be valid once the changes are undone. This is a design problem. +// To fix it we either need to change the invariants of VisibleSelection or create a new +// class for editing to use that can manipulate selections that are not currently valid. +void VisibleSelection::setWithoutValidation(const Position& base, const Position& extent) +{ + ASSERT(!base.isNull()); + ASSERT(!extent.isNull()); + ASSERT(base != extent); + ASSERT(m_affinity == DOWNSTREAM); + m_base = base; + m_extent = extent; + m_baseIsFirst = comparePositions(base, extent) <= 0; + if (m_baseIsFirst) { + m_start = base; + m_end = extent; + } else { + m_start = extent; + m_end = base; + } + m_selectionType = RangeSelection; +} + +void VisibleSelection::adjustSelectionToAvoidCrossingEditingBoundaries() +{ + if (m_base.isNull() || m_start.isNull() || m_end.isNull()) + return; + + Node* baseRoot = highestEditableRoot(m_base); + Node* startRoot = highestEditableRoot(m_start); + Node* endRoot = highestEditableRoot(m_end); + + Node* baseEditableAncestor = lowestEditableAncestor(m_base.node()); + + // The base, start and end are all in the same region. No adjustment necessary. + if (baseRoot == startRoot && baseRoot == endRoot) + return; + + // The selection is based in editable content. + if (baseRoot) { + // If the start is outside the base's editable root, cap it at the start of that root. + // If the start is in non-editable content that is inside the base's editable root, put it + // at the first editable position after start inside the base's editable root. + if (startRoot != baseRoot) { + VisiblePosition first = firstEditablePositionAfterPositionInRoot(m_start, baseRoot); + m_start = first.deepEquivalent(); + if (m_start.isNull()) { + ASSERT_NOT_REACHED(); + m_start = m_end; + } + } + // If the end is outside the base's editable root, cap it at the end of that root. + // If the end is in non-editable content that is inside the base's root, put it + // at the last editable position before the end inside the base's root. + if (endRoot != baseRoot) { + VisiblePosition last = lastEditablePositionBeforePositionInRoot(m_end, baseRoot); + m_end = last.deepEquivalent(); + if (m_end.isNull()) + m_end = m_start; + } + // The selection is based in non-editable content. + } else { + // FIXME: Non-editable pieces inside editable content should be atomic, in the same way that editable + // pieces in non-editable content are atomic. + + // The selection ends in editable content or non-editable content inside a different editable ancestor, + // move backward until non-editable content inside the same lowest editable ancestor is reached. + Node* endEditableAncestor = lowestEditableAncestor(m_end.node()); + if (endRoot || endEditableAncestor != baseEditableAncestor) { + + Position p = previousVisuallyDistinctCandidate(m_end); + Node* shadowAncestor = endRoot ? endRoot->shadowAncestorNode() : 0; + if (p.isNull() && endRoot && (shadowAncestor != endRoot)) + p = lastDeepEditingPositionForNode(shadowAncestor); + while (p.isNotNull() && !(lowestEditableAncestor(p.node()) == baseEditableAncestor && !isEditablePosition(p))) { + Node* root = editableRootForPosition(p); + shadowAncestor = root ? root->shadowAncestorNode() : 0; + p = isAtomicNode(p.node()) ? positionInParentBeforeNode(p.node()) : previousVisuallyDistinctCandidate(p); + if (p.isNull() && (shadowAncestor != root)) + p = lastDeepEditingPositionForNode(shadowAncestor); + } + VisiblePosition previous(p); + + if (previous.isNull()) { + // The selection crosses an Editing boundary. This is a + // programmer error in the editing code. Happy debugging! + ASSERT_NOT_REACHED(); + m_base = Position(); + m_extent = Position(); + validate(); + return; + } + m_end = previous.deepEquivalent(); + } + + // The selection starts in editable content or non-editable content inside a different editable ancestor, + // move forward until non-editable content inside the same lowest editable ancestor is reached. + Node* startEditableAncestor = lowestEditableAncestor(m_start.node()); + if (startRoot || startEditableAncestor != baseEditableAncestor) { + Position p = nextVisuallyDistinctCandidate(m_start); + Node* shadowAncestor = startRoot ? startRoot->shadowAncestorNode() : 0; + if (p.isNull() && startRoot && (shadowAncestor != startRoot)) + p = Position(shadowAncestor, 0); + while (p.isNotNull() && !(lowestEditableAncestor(p.node()) == baseEditableAncestor && !isEditablePosition(p))) { + Node* root = editableRootForPosition(p); + shadowAncestor = root ? root->shadowAncestorNode() : 0; + p = isAtomicNode(p.node()) ? positionInParentAfterNode(p.node()) : nextVisuallyDistinctCandidate(p); + if (p.isNull() && (shadowAncestor != root)) + p = Position(shadowAncestor, 0); + } + VisiblePosition next(p); + + if (next.isNull()) { + // The selection crosses an Editing boundary. This is a + // programmer error in the editing code. Happy debugging! + ASSERT_NOT_REACHED(); + m_base = Position(); + m_extent = Position(); + validate(); + return; + } + m_start = next.deepEquivalent(); + } + } + + // Correct the extent if necessary. + if (baseEditableAncestor != lowestEditableAncestor(m_extent.node())) + m_extent = m_baseIsFirst ? m_end : m_start; +} + +bool VisibleSelection::isContentEditable() const +{ + return isEditablePosition(start()); +} + +bool VisibleSelection::isContentRichlyEditable() const +{ + return isRichlyEditablePosition(start()); +} + +Element* VisibleSelection::rootEditableElement() const +{ + return editableRootForPosition(start()); +} + +Node* VisibleSelection::shadowTreeRootNode() const +{ + return start().node() ? start().node()->shadowTreeRootNode() : 0; +} + +void VisibleSelection::debugPosition() const +{ + if (!m_start.node()) + return; + + fprintf(stderr, "VisibleSelection =================\n"); + + if (m_start == m_end) { + Position pos = m_start; + fprintf(stderr, "pos: %s %p:%d\n", pos.node()->nodeName().utf8().data(), pos.node(), pos.deprecatedEditingOffset()); + } else { + Position pos = m_start; + fprintf(stderr, "start: %s %p:%d\n", pos.node()->nodeName().utf8().data(), pos.node(), pos.deprecatedEditingOffset()); + fprintf(stderr, "-----------------------------------\n"); + pos = m_end; + fprintf(stderr, "end: %s %p:%d\n", pos.node()->nodeName().utf8().data(), pos.node(), pos.deprecatedEditingOffset()); + fprintf(stderr, "-----------------------------------\n"); + } + + fprintf(stderr, "================================\n"); +} + +#ifndef NDEBUG + +void VisibleSelection::formatForDebugger(char* buffer, unsigned length) const +{ + String result; + String s; + + if (isNone()) { + result = "<none>"; + } else { + const int FormatBufferSize = 1024; + char s[FormatBufferSize]; + result += "from "; + start().formatForDebugger(s, FormatBufferSize); + result += s; + result += " to "; + end().formatForDebugger(s, FormatBufferSize); + result += s; + } + + strncpy(buffer, result.utf8().data(), length - 1); +} + +void VisibleSelection::showTreeForThis() const +{ + if (start().node()) { + start().node()->showTreeAndMark(start().node(), "S", end().node(), "E"); + fprintf(stderr, "start offset: %d, end offset: %d\n", start().deprecatedEditingOffset(), end().deprecatedEditingOffset()); + } +} + +#endif + +} // namespace WebCore + +#ifndef NDEBUG + +void showTree(const WebCore::VisibleSelection& sel) +{ + sel.showTreeForThis(); +} + +void showTree(const WebCore::VisibleSelection* sel) +{ + if (sel) + sel->showTreeForThis(); +} + +#endif diff --git a/Source/WebCore/editing/VisibleSelection.h b/Source/WebCore/editing/VisibleSelection.h new file mode 100644 index 0000000..10b5c8f --- /dev/null +++ b/Source/WebCore/editing/VisibleSelection.h @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2004 Apple Computer, 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef VisibleSelection_h +#define VisibleSelection_h + +#include "TextGranularity.h" +#include "VisiblePosition.h" + +namespace WebCore { + +class Position; + +const EAffinity SEL_DEFAULT_AFFINITY = DOWNSTREAM; +enum SelectionDirection { DirectionForward, DirectionBackward, DirectionRight, DirectionLeft }; + +class VisibleSelection { +public: + enum SelectionType { NoSelection, CaretSelection, RangeSelection }; + + VisibleSelection(); + + VisibleSelection(const Position&, EAffinity); + VisibleSelection(const Position&, const Position&, EAffinity = SEL_DEFAULT_AFFINITY); + + VisibleSelection(const Range*, EAffinity = SEL_DEFAULT_AFFINITY); + + VisibleSelection(const VisiblePosition&); + VisibleSelection(const VisiblePosition&, const VisiblePosition&); + + static VisibleSelection selectionFromContentsOfNode(Node*); + + SelectionType selectionType() const { return m_selectionType; } + + void setAffinity(EAffinity affinity) { m_affinity = affinity; } + EAffinity affinity() const { return m_affinity; } + + void setBase(const Position&); + void setBase(const VisiblePosition&); + void setExtent(const Position&); + void setExtent(const VisiblePosition&); + + Position base() const { return m_base; } + Position extent() const { return m_extent; } + Position start() const { return m_start; } + Position end() const { return m_end; } + + VisiblePosition visibleStart() const { return VisiblePosition(m_start, isRange() ? DOWNSTREAM : affinity()); } + VisiblePosition visibleEnd() const { return VisiblePosition(m_end, isRange() ? UPSTREAM : affinity()); } + + bool isNone() const { return selectionType() == NoSelection; } + bool isCaret() const { return selectionType() == CaretSelection; } + bool isRange() const { return selectionType() == RangeSelection; } + bool isCaretOrRange() const { return selectionType() != NoSelection; } + bool isNonOrphanedRange() const { return isRange() && !start().isOrphan() && !end().isOrphan(); } + bool isNonOrphanedCaretOrRange() const { return isCaretOrRange() && !start().isOrphan() && !end().isOrphan(); } + + bool isBaseFirst() const { return m_baseIsFirst; } + + bool isAll(StayInEditableContent) const; + + void appendTrailingWhitespace(); + + bool expandUsingGranularity(TextGranularity granularity); + + // We don't yet support multi-range selections, so we only ever have one range to return. + PassRefPtr<Range> firstRange() const; + + // FIXME: Most callers probably don't want this function, but are using it + // for historical reasons. toNormalizedRange contracts the range around + // text, and moves the caret upstream before returning the range. + PassRefPtr<Range> toNormalizedRange() const; + + Element* rootEditableElement() const; + bool isContentEditable() const; + bool isContentRichlyEditable() const; + Node* shadowTreeRootNode() const; + + void debugPosition() const; + +#ifndef NDEBUG + void formatForDebugger(char* buffer, unsigned length) const; + void showTreeForThis() const; +#endif + + void setWithoutValidation(const Position&, const Position&); + +private: + void validate(TextGranularity = CharacterGranularity); + + // Support methods for validate() + void setBaseAndExtentToDeepEquivalents(); + void setStartAndEndFromBaseAndExtentRespectingGranularity(TextGranularity); + void adjustSelectionToAvoidCrossingEditingBoundaries(); + void updateSelectionType(); + + // We need to store these as Positions because VisibleSelection is + // used to store values in editing commands for use when + // undoing the command. We need to be able to create a selection that, while currently + // invalid, will be valid once the changes are undone. + + Position m_base; // Where the first click happened + Position m_extent; // Where the end click happened + Position m_start; // Leftmost position when expanded to respect granularity + Position m_end; // Rightmost position when expanded to respect granularity + + EAffinity m_affinity; // the upstream/downstream affinity of the caret + + // these are cached, can be recalculated by validate() + SelectionType m_selectionType; // None, Caret, Range + bool m_baseIsFirst; // true if base is before the extent +}; + +inline bool operator==(const VisibleSelection& a, const VisibleSelection& b) +{ + return a.start() == b.start() && a.end() == b.end() && a.affinity() == b.affinity() && a.isBaseFirst() == b.isBaseFirst(); +} + +inline bool operator!=(const VisibleSelection& a, const VisibleSelection& b) +{ + return !(a == b); +} + +} // namespace WebCore + +#ifndef NDEBUG +// Outside the WebCore namespace for ease of invocation from gdb. +void showTree(const WebCore::VisibleSelection&); +void showTree(const WebCore::VisibleSelection*); +#endif + +#endif // VisibleSelection_h diff --git a/Source/WebCore/editing/WrapContentsInDummySpanCommand.cpp b/Source/WebCore/editing/WrapContentsInDummySpanCommand.cpp new file mode 100644 index 0000000..5fa0b39 --- /dev/null +++ b/Source/WebCore/editing/WrapContentsInDummySpanCommand.cpp @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2005, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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 "WrapContentsInDummySpanCommand.h" + +#include "ApplyStyleCommand.h" +#include "HTMLElement.h" + +namespace WebCore { + +WrapContentsInDummySpanCommand::WrapContentsInDummySpanCommand(PassRefPtr<Element> element) + : SimpleEditCommand(element->document()) + , m_element(element) +{ + ASSERT(m_element); +} + +void WrapContentsInDummySpanCommand::executeApply() +{ + Vector<RefPtr<Node> > children; + for (Node* child = m_element->firstChild(); child; child = child->nextSibling()) + children.append(child); + + ExceptionCode ec; + + size_t size = children.size(); + for (size_t i = 0; i < size; ++i) + m_dummySpan->appendChild(children[i].release(), ec); + + m_element->appendChild(m_dummySpan.get(), ec); +} + +void WrapContentsInDummySpanCommand::doApply() +{ + m_dummySpan = createStyleSpanElement(document()); + + executeApply(); +} + +void WrapContentsInDummySpanCommand::doUnapply() +{ + ASSERT(m_element); + + if (!m_dummySpan || !m_element->isContentEditable()) + return; + + Vector<RefPtr<Node> > children; + for (Node* child = m_dummySpan->firstChild(); child; child = child->nextSibling()) + children.append(child); + + ExceptionCode ec; + + size_t size = children.size(); + for (size_t i = 0; i < size; ++i) + m_element->appendChild(children[i].release(), ec); + + m_dummySpan->remove(ec); +} + +void WrapContentsInDummySpanCommand::doReapply() +{ + ASSERT(m_element); + + if (!m_dummySpan || !m_element->isContentEditable()) + return; + + executeApply(); +} + +} // namespace WebCore diff --git a/Source/WebCore/editing/WrapContentsInDummySpanCommand.h b/Source/WebCore/editing/WrapContentsInDummySpanCommand.h new file mode 100644 index 0000000..be3af66 --- /dev/null +++ b/Source/WebCore/editing/WrapContentsInDummySpanCommand.h @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2005, 2006, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef WrapContentsInDummySpanCommand_h +#define WrapContentsInDummySpanCommand_h + +#include "EditCommand.h" + +namespace WebCore { + +class HTMLElement; + +class WrapContentsInDummySpanCommand : public SimpleEditCommand { +public: + static PassRefPtr<WrapContentsInDummySpanCommand> create(PassRefPtr<Element> element) + { + return adoptRef(new WrapContentsInDummySpanCommand(element)); + } + +private: + WrapContentsInDummySpanCommand(PassRefPtr<Element>); + + virtual void doApply(); + virtual void doUnapply(); + virtual void doReapply(); + void executeApply(); + + RefPtr<Element> m_element; + RefPtr<HTMLElement> m_dummySpan; +}; + +} // namespace WebCore + +#endif // WrapContentsInDummySpanCommand_h diff --git a/Source/WebCore/editing/WritingDirection.h b/Source/WebCore/editing/WritingDirection.h new file mode 100644 index 0000000..b3cff66 --- /dev/null +++ b/Source/WebCore/editing/WritingDirection.h @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef WritingDirection_h +#define WritingDirection_h + +enum WritingDirection { NaturalWritingDirection, LeftToRightWritingDirection, RightToLeftWritingDirection }; + +#endif diff --git a/Source/WebCore/editing/android/EditorAndroid.cpp b/Source/WebCore/editing/android/EditorAndroid.cpp new file mode 100644 index 0000000..29c0be5 --- /dev/null +++ b/Source/WebCore/editing/android/EditorAndroid.cpp @@ -0,0 +1,39 @@ +/* + * Copyright 2009, The Android Open Source Project + * Copyright (C) 2006, 2007 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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 "Editor.h" + +#include "ClipboardAndroid.h" + +namespace WebCore { + +PassRefPtr<Clipboard> Editor::newGeneralClipboard(ClipboardAccessPolicy policy, Frame*) +{ + return new ClipboardAndroid(policy, Clipboard::CopyAndPaste); +} + +} // namespace WebCore diff --git a/Source/WebCore/editing/brew/EditorBrew.cpp b/Source/WebCore/editing/brew/EditorBrew.cpp new file mode 100644 index 0000000..e609891 --- /dev/null +++ b/Source/WebCore/editing/brew/EditorBrew.cpp @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2006, 2007 Apple Inc. All rights reserved. + * Copyright (C) 2010 Company 100, Inc. + * + * 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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 "Editor.h" + +#include "ClipboardBrew.h" + +namespace WebCore { + +PassRefPtr<Clipboard> Editor::newGeneralClipboard(ClipboardAccessPolicy policy, Frame*) +{ + return new ClipboardBrew(policy, Clipboard::CopyAndPaste); +} + +} // namespace WebCore diff --git a/Source/WebCore/editing/chromium/EditorChromium.cpp b/Source/WebCore/editing/chromium/EditorChromium.cpp new file mode 100644 index 0000000..ca7f41a --- /dev/null +++ b/Source/WebCore/editing/chromium/EditorChromium.cpp @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2008, 2009, 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: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * 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 "Editor.h" + +#include "ChromiumDataObject.h" +#include "ClipboardChromium.h" +#include "Frame.h" + +namespace WebCore { + +PassRefPtr<Clipboard> Editor::newGeneralClipboard(ClipboardAccessPolicy policy, Frame* frame) +{ + return ClipboardChromium::create(Clipboard::CopyAndPaste, policy, frame); +} + +} // namespace WebCore diff --git a/Source/WebCore/editing/chromium/SelectionControllerChromium.cpp b/Source/WebCore/editing/chromium/SelectionControllerChromium.cpp new file mode 100644 index 0000000..d627d9b --- /dev/null +++ b/Source/WebCore/editing/chromium/SelectionControllerChromium.cpp @@ -0,0 +1,48 @@ +/* + * 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: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * 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 "SelectionController.h" + +#include "AXObjectCache.h" +#include "Frame.h" + +namespace WebCore { + +void SelectionController::notifyAccessibilityForSelectionChange() +{ + // FIXME: Support editable text in chromium. + if (AXObjectCache::accessibilityEnabled() && m_selection.start().isNotNull() && m_selection.end().isNotNull()) { + Document* document = m_frame->document(); + document->axObjectCache()->postNotification(m_selection.start().node()->renderer(), AXObjectCache::AXSelectedTextChanged, false); + } +} + +} // namespace WebCore diff --git a/Source/WebCore/editing/gtk/SelectionControllerGtk.cpp b/Source/WebCore/editing/gtk/SelectionControllerGtk.cpp new file mode 100644 index 0000000..19097b2 --- /dev/null +++ b/Source/WebCore/editing/gtk/SelectionControllerGtk.cpp @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2009 Igalia S.L. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#include "config.h" +#include "SelectionController.h" + +#include "AccessibilityObjectWrapperAtk.h" +#include "AXObjectCache.h" +#include "Frame.h" +#include "RefPtr.h" + +#include <gtk/gtk.h> + +namespace WebCore { + +static void emitTextSelectionChange(AccessibilityObject* object, VisibleSelection selection, int offset) +{ + AtkObject* axObject = object->wrapper(); + if (!axObject || !ATK_IS_TEXT(axObject)) + return; + + g_signal_emit_by_name(axObject, "text-caret-moved", offset); + if (selection.isRange()) + g_signal_emit_by_name(axObject, "text-selection-changed"); +} + +static void maybeEmitTextFocusChange(PassRefPtr<AccessibilityObject> prpObject) +{ + // This static variable is needed to keep track of the old object + // as per previous calls to this function, in order to properly + // decide whether to emit some signals or not. + DEFINE_STATIC_LOCAL(RefPtr<AccessibilityObject>, oldObject, ()); + + RefPtr<AccessibilityObject> object = prpObject; + + // Ensure the oldObject belongs to the same document that the + // current object so further comparisons make sense. Otherwise, + // just reset oldObject to 0 so it won't be taken into account in + // the immediately following call to this function. + if (object && oldObject && oldObject->document() != object->document()) + oldObject = 0; + + AtkObject* axObject = object ? object->wrapper() : 0; + AtkObject* oldAxObject = oldObject ? oldObject->wrapper() : 0; + + if (axObject != oldAxObject) { + if (oldAxObject && ATK_IS_TEXT(oldAxObject)) { + g_signal_emit_by_name(oldAxObject, "focus-event", false); + g_signal_emit_by_name(oldAxObject, "state-change", "focused", false); + } + if (axObject && ATK_IS_TEXT(axObject)) { + g_signal_emit_by_name(axObject, "focus-event", true); + g_signal_emit_by_name(axObject, "state-change", "focused", true); + } + } + + // Update pointer to last focused object. + oldObject = object; +} + + +void SelectionController::notifyAccessibilityForSelectionChange() +{ + if (!AXObjectCache::accessibilityEnabled()) + return; + + // Reset lastFocuseNode and return for no valid selections. + if (!m_selection.start().isNotNull() || !m_selection.end().isNotNull()) + return; + + RenderObject* focusedNode = m_selection.end().node()->renderer(); + AccessibilityObject* accessibilityObject = m_frame->document()->axObjectCache()->getOrCreate(focusedNode); + + // Need to check this as getOrCreate could return 0, + if (!accessibilityObject) + return; + + int offset; + // Always report the events w.r.t. the non-linked unignored parent. (i.e. ignoreLinks == true). + RefPtr<AccessibilityObject> object = objectAndOffsetUnignored(accessibilityObject, offset, true); + if (!object) + return; + + // Emit relatedsignals. + emitTextSelectionChange(object.get(), m_selection, offset); + maybeEmitTextFocusChange(object.release()); +} + +} // namespace WebCore diff --git a/Source/WebCore/editing/haiku/EditorHaiku.cpp b/Source/WebCore/editing/haiku/EditorHaiku.cpp new file mode 100644 index 0000000..2b3d0ba --- /dev/null +++ b/Source/WebCore/editing/haiku/EditorHaiku.cpp @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2007 Ryan Leavengood <leavengood@gmail.com> + * + * 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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 "Editor.h" + +#include "Clipboard.h" +#include "ClipboardAccessPolicy.h" +#include "ClipboardHaiku.h" + + +namespace WebCore { + +PassRefPtr<Clipboard> Editor::newGeneralClipboard(ClipboardAccessPolicy policy, Frame*) +{ + return ClipboardHaiku::create(policy, Clipboard::CopyAndPaste); +} + +} // namespace WebCore + diff --git a/Source/WebCore/editing/htmlediting.cpp b/Source/WebCore/editing/htmlediting.cpp new file mode 100644 index 0000000..d08ac2e --- /dev/null +++ b/Source/WebCore/editing/htmlediting.cpp @@ -0,0 +1,1180 @@ +/* + * Copyright (C) 2004, 2005, 2006, 2007 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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 "htmlediting.h" + +#include "CharacterNames.h" +#include "Document.h" +#include "EditingText.h" +#include "HTMLBRElement.h" +#include "HTMLDivElement.h" +#include "HTMLElementFactory.h" +#include "HTMLInterchange.h" +#include "HTMLLIElement.h" +#include "HTMLNames.h" +#include "HTMLOListElement.h" +#include "HTMLUListElement.h" +#include "PositionIterator.h" +#include "RenderObject.h" +#include "Range.h" +#include "VisibleSelection.h" +#include "Text.h" +#include "TextIterator.h" +#include "VisiblePosition.h" +#include "visible_units.h" +#include <wtf/StdLibExtras.h> + +#if ENABLE(WML) +#include "WMLNames.h" +#endif + +using namespace std; + +namespace WebCore { + +using namespace HTMLNames; + +// Atomic means that the node has no children, or has children which are ignored for the +// purposes of editing. +bool isAtomicNode(const Node *node) +{ + return node && (!node->hasChildNodes() || editingIgnoresContent(node)); +} + +// Returns true for nodes that either have no content, or have content that is ignored (skipped +// over) while editing. There are no VisiblePositions inside these nodes. +bool editingIgnoresContent(const Node* node) +{ + return !canHaveChildrenForEditing(node) && !node->isTextNode(); +} + +bool canHaveChildrenForEditing(const Node* node) +{ + return !node->hasTagName(hrTag) && + !node->hasTagName(brTag) && + !node->hasTagName(imgTag) && + !node->hasTagName(buttonTag) && + !node->hasTagName(inputTag) && + !node->hasTagName(textareaTag) && + !node->hasTagName(objectTag) && + !node->hasTagName(iframeTag) && + !node->hasTagName(embedTag) && + !node->hasTagName(appletTag) && + !node->hasTagName(selectTag) && + !node->hasTagName(datagridTag) && +#if ENABLE(WML) + !node->hasTagName(WMLNames::doTag) && +#endif + !node->isTextNode(); +} + +// Compare two positions, taking into account the possibility that one or both +// could be inside a shadow tree. Only works for non-null values. +int comparePositions(const Position& a, const Position& b) +{ + Node* nodeA = a.node(); + ASSERT(nodeA); + Node* nodeB = b.node(); + ASSERT(nodeB); + int offsetA = a.deprecatedEditingOffset(); + int offsetB = b.deprecatedEditingOffset(); + + Node* shadowAncestorA = nodeA->shadowAncestorNode(); + if (shadowAncestorA == nodeA) + shadowAncestorA = 0; + Node* shadowAncestorB = nodeB->shadowAncestorNode(); + if (shadowAncestorB == nodeB) + shadowAncestorB = 0; + + int bias = 0; + if (shadowAncestorA != shadowAncestorB) { + if (shadowAncestorA) { + nodeA = shadowAncestorA; + offsetA = 0; + bias = 1; + } + if (shadowAncestorB) { + nodeB = shadowAncestorB; + offsetB = 0; + bias = -1; + } + } + + int result = Range::compareBoundaryPoints(nodeA, offsetA, nodeB, offsetB); + return result ? result : bias; +} + +int comparePositions(const VisiblePosition& a, const VisiblePosition& b) +{ + return comparePositions(a.deepEquivalent(), b.deepEquivalent()); +} + +Node* highestEditableRoot(const Position& position) +{ + Node* node = position.node(); + if (!node) + return 0; + + Node* highestRoot = editableRootForPosition(position); + if (!highestRoot) + return 0; + + node = highestRoot; + while (node) { + if (node->isContentEditable()) + highestRoot = node; + if (node->hasTagName(bodyTag)) + break; + node = node->parentNode(); + } + + return highestRoot; +} + +Node* lowestEditableAncestor(Node* node) +{ + if (!node) + return 0; + + Node *lowestRoot = 0; + while (node) { + if (node->isContentEditable()) + return node->rootEditableElement(); + if (node->hasTagName(bodyTag)) + break; + node = node->parentNode(); + } + + return lowestRoot; +} + +bool isEditablePosition(const Position& p) +{ + Node* node = p.node(); + if (!node) + return false; + + if (node->renderer() && node->renderer()->isTable()) + node = node->parentNode(); + + return node->isContentEditable(); +} + +bool isAtUnsplittableElement(const Position& pos) +{ + Node* node = pos.node(); + return (node == editableRootForPosition(pos) || node == enclosingNodeOfType(pos, &isTableCell)); +} + + +bool isRichlyEditablePosition(const Position& p) +{ + Node* node = p.node(); + if (!node) + return false; + + if (node->renderer() && node->renderer()->isTable()) + node = node->parentNode(); + + return node->isContentRichlyEditable(); +} + +Element* editableRootForPosition(const Position& p) +{ + Node* node = p.node(); + if (!node) + return 0; + + if (node->renderer() && node->renderer()->isTable()) + node = node->parentNode(); + + return node->rootEditableElement(); +} + +// Finds the enclosing element until which the tree can be split. +// When a user hits ENTER, he/she won't expect this element to be split into two. +// You may pass it as the second argument of splitTreeToNode. +Element* unsplittableElementForPosition(const Position& p) +{ + // Since enclosingNodeOfType won't search beyond the highest root editable node, + // this code works even if the closest table cell was outside of the root editable node. + Element* enclosingCell = static_cast<Element*>(enclosingNodeOfType(p, &isTableCell, true)); + if (enclosingCell) + return enclosingCell; + + return editableRootForPosition(p); +} + +Position nextCandidate(const Position& position) +{ + PositionIterator p = position; + while (!p.atEnd()) { + p.increment(); + if (p.isCandidate()) + return p; + } + return Position(); +} + +Position nextVisuallyDistinctCandidate(const Position& position) +{ + Position p = position; + Position downstreamStart = p.downstream(); + while (!p.atEndOfTree()) { + p = p.next(Character); + if (p.isCandidate() && p.downstream() != downstreamStart) + return p; + } + return Position(); +} + +Position previousCandidate(const Position& position) +{ + PositionIterator p = position; + while (!p.atStart()) { + p.decrement(); + if (p.isCandidate()) + return p; + } + return Position(); +} + +Position previousVisuallyDistinctCandidate(const Position& position) +{ + Position p = position; + Position downstreamStart = p.downstream(); + while (!p.atStartOfTree()) { + p = p.previous(Character); + if (p.isCandidate() && p.downstream() != downstreamStart) + return p; + } + return Position(); +} + +VisiblePosition firstEditablePositionAfterPositionInRoot(const Position& position, Node* highestRoot) +{ + // position falls before highestRoot. + if (comparePositions(position, firstDeepEditingPositionForNode(highestRoot)) == -1 && highestRoot->isContentEditable()) + return firstDeepEditingPositionForNode(highestRoot); + + Position p = position; + + if (Node* shadowAncestor = p.node()->shadowAncestorNode()) + if (shadowAncestor != p.node()) + p = lastDeepEditingPositionForNode(shadowAncestor); + + while (p.node() && !isEditablePosition(p) && p.node()->isDescendantOf(highestRoot)) + p = isAtomicNode(p.node()) ? positionInParentAfterNode(p.node()) : nextVisuallyDistinctCandidate(p); + + if (p.node() && p.node() != highestRoot && !p.node()->isDescendantOf(highestRoot)) + return VisiblePosition(); + + return VisiblePosition(p); +} + +VisiblePosition lastEditablePositionBeforePositionInRoot(const Position& position, Node* highestRoot) +{ + // When position falls after highestRoot, the result is easy to compute. + if (comparePositions(position, lastDeepEditingPositionForNode(highestRoot)) == 1) + return lastDeepEditingPositionForNode(highestRoot); + + Position p = position; + + if (Node* shadowAncestor = p.node()->shadowAncestorNode()) + if (shadowAncestor != p.node()) + p = firstDeepEditingPositionForNode(shadowAncestor); + + while (p.node() && !isEditablePosition(p) && p.node()->isDescendantOf(highestRoot)) + p = isAtomicNode(p.node()) ? positionInParentBeforeNode(p.node()) : previousVisuallyDistinctCandidate(p); + + if (p.node() && p.node() != highestRoot && !p.node()->isDescendantOf(highestRoot)) + return VisiblePosition(); + + return VisiblePosition(p); +} + +// FIXME: The method name, comment, and code say three different things here! +// Whether or not content before and after this node will collapse onto the same line as it. +bool isBlock(const Node* node) +{ + return node && node->renderer() && !node->renderer()->isInline(); +} + +// FIXME: Deploy this in all of the places where enclosingBlockFlow/enclosingBlockFlowOrTableElement are used. +// FIXME: Pass a position to this function. The enclosing block of [table, x] for example, should be the +// block that contains the table and not the table, and this function should be the only one responsible for +// knowing about these kinds of special cases. +Node* enclosingBlock(Node* node) +{ + return static_cast<Element*>(enclosingNodeOfType(Position(node, 0), isBlock)); +} + +// Internally editing uses "invalid" positions for historical reasons. For +// example, in <div><img /></div>, Editing might use (img, 1) for the position +// after <img>, but we have to convert that to (div, 1) before handing the +// position to a Range object. Ideally all internal positions should +// be "range compliant" for simplicity. +Position rangeCompliantEquivalent(const Position& pos) +{ + if (pos.isNull()) + return Position(); + + Node* node = pos.node(); + + if (pos.deprecatedEditingOffset() <= 0) { + if (node->parentNode() && (editingIgnoresContent(node) || isTableElement(node))) + return positionInParentBeforeNode(node); + return Position(node, 0); + } + + if (node->offsetInCharacters()) + return Position(node, min(node->maxCharacterOffset(), pos.deprecatedEditingOffset())); + + int maxCompliantOffset = node->childNodeCount(); + if (pos.deprecatedEditingOffset() > maxCompliantOffset) { + if (node->parentNode()) + return positionInParentAfterNode(node); + + // there is no other option at this point than to + // use the highest allowed position in the node + return Position(node, maxCompliantOffset); + } + + // Editing should never generate positions like this. + if ((pos.deprecatedEditingOffset() < maxCompliantOffset) && editingIgnoresContent(node)) { + ASSERT_NOT_REACHED(); + return node->parentNode() ? positionInParentBeforeNode(node) : Position(node, 0); + } + + if (pos.deprecatedEditingOffset() == maxCompliantOffset && (editingIgnoresContent(node) || isTableElement(node))) + return positionInParentAfterNode(node); + + return Position(pos); +} + +Position rangeCompliantEquivalent(const VisiblePosition& vpos) +{ + return rangeCompliantEquivalent(vpos.deepEquivalent()); +} + +// This method is used to create positions in the DOM. It returns the maximum valid offset +// in a node. It returns 1 for some elements even though they do not have children, which +// creates technically invalid DOM Positions. Be sure to call rangeCompliantEquivalent +// on a Position before using it to create a DOM Range, or an exception will be thrown. +int lastOffsetForEditing(const Node* node) +{ + ASSERT(node); + if (!node) + return 0; + if (node->offsetInCharacters()) + return node->maxCharacterOffset(); + + if (node->hasChildNodes()) + return node->childNodeCount(); + + // NOTE: This should preempt the childNodeCount for, e.g., select nodes + if (editingIgnoresContent(node)) + return 1; + + return 0; +} + +String stringWithRebalancedWhitespace(const String& string, bool startIsStartOfParagraph, bool endIsEndOfParagraph) +{ + DEFINE_STATIC_LOCAL(String, twoSpaces, (" ")); + DEFINE_STATIC_LOCAL(String, nbsp, ("\xa0")); + DEFINE_STATIC_LOCAL(String, pattern, (" \xa0")); + + String rebalancedString = string; + + rebalancedString.replace(noBreakSpace, ' '); + rebalancedString.replace('\n', ' '); + rebalancedString.replace('\t', ' '); + + rebalancedString.replace(twoSpaces, pattern); + + if (startIsStartOfParagraph && rebalancedString[0] == ' ') + rebalancedString.replace(0, 1, nbsp); + int end = rebalancedString.length() - 1; + if (endIsEndOfParagraph && rebalancedString[end] == ' ') + rebalancedString.replace(end, 1, nbsp); + + return rebalancedString; +} + +bool isTableStructureNode(const Node *node) +{ + RenderObject *r = node->renderer(); + return (r && (r->isTableCell() || r->isTableRow() || r->isTableSection() || r->isTableCol())); +} + +const String& nonBreakingSpaceString() +{ + DEFINE_STATIC_LOCAL(String, nonBreakingSpaceString, (&noBreakSpace, 1)); + return nonBreakingSpaceString; +} + +// FIXME: need to dump this +bool isSpecialElement(const Node *n) +{ + if (!n) + return false; + + if (!n->isHTMLElement()) + return false; + + if (n->isLink()) + return true; + + RenderObject *renderer = n->renderer(); + if (!renderer) + return false; + + if (renderer->style()->display() == TABLE || renderer->style()->display() == INLINE_TABLE) + return true; + + if (renderer->style()->isFloating()) + return true; + + if (renderer->style()->position() != StaticPosition) + return true; + + return false; +} + +static Node* firstInSpecialElement(const Position& pos) +{ + // FIXME: This begins at pos.node(), which doesn't necessarily contain pos (suppose pos was [img, 0]). See <rdar://problem/5027702>. + Node* rootEditableElement = pos.node()->rootEditableElement(); + for (Node* n = pos.node(); n && n->rootEditableElement() == rootEditableElement; n = n->parentNode()) + if (isSpecialElement(n)) { + VisiblePosition vPos = VisiblePosition(pos, DOWNSTREAM); + VisiblePosition firstInElement = VisiblePosition(n, 0, DOWNSTREAM); + if (isTableElement(n) && vPos == firstInElement.next()) + return n; + if (vPos == firstInElement) + return n; + } + return 0; +} + +static Node* lastInSpecialElement(const Position& pos) +{ + // FIXME: This begins at pos.node(), which doesn't necessarily contain pos (suppose pos was [img, 0]). See <rdar://problem/5027702>. + Node* rootEditableElement = pos.node()->rootEditableElement(); + for (Node* n = pos.node(); n && n->rootEditableElement() == rootEditableElement; n = n->parentNode()) + if (isSpecialElement(n)) { + VisiblePosition vPos = VisiblePosition(pos, DOWNSTREAM); + VisiblePosition lastInElement = VisiblePosition(n, n->childNodeCount(), DOWNSTREAM); + if (isTableElement(n) && vPos == lastInElement.previous()) + return n; + if (vPos == lastInElement) + return n; + } + return 0; +} + +bool isFirstVisiblePositionInSpecialElement(const Position& pos) +{ + return firstInSpecialElement(pos); +} + +Position positionBeforeContainingSpecialElement(const Position& pos, Node** containingSpecialElement) +{ + Node* n = firstInSpecialElement(pos); + if (!n) + return pos; + Position result = positionInParentBeforeNode(n); + if (result.isNull() || result.node()->rootEditableElement() != pos.node()->rootEditableElement()) + return pos; + if (containingSpecialElement) + *containingSpecialElement = n; + return result; +} + +bool isLastVisiblePositionInSpecialElement(const Position& pos) +{ + return lastInSpecialElement(pos); +} + +Position positionAfterContainingSpecialElement(const Position& pos, Node **containingSpecialElement) +{ + Node* n = lastInSpecialElement(pos); + if (!n) + return pos; + Position result = positionInParentAfterNode(n); + if (result.isNull() || result.node()->rootEditableElement() != pos.node()->rootEditableElement()) + return pos; + if (containingSpecialElement) + *containingSpecialElement = n; + return result; +} + +Position positionOutsideContainingSpecialElement(const Position &pos, Node **containingSpecialElement) +{ + if (isFirstVisiblePositionInSpecialElement(pos)) + return positionBeforeContainingSpecialElement(pos, containingSpecialElement); + if (isLastVisiblePositionInSpecialElement(pos)) + return positionAfterContainingSpecialElement(pos, containingSpecialElement); + return pos; +} + +Node* isFirstPositionAfterTable(const VisiblePosition& visiblePosition) +{ + Position upstream(visiblePosition.deepEquivalent().upstream()); + if (upstream.node() && upstream.node()->renderer() && upstream.node()->renderer()->isTable() && upstream.atLastEditingPositionForNode()) + return upstream.node(); + + return 0; +} + +Node* isLastPositionBeforeTable(const VisiblePosition& visiblePosition) +{ + Position downstream(visiblePosition.deepEquivalent().downstream()); + if (downstream.node() && downstream.node()->renderer() && downstream.node()->renderer()->isTable() && downstream.atFirstEditingPositionForNode()) + return downstream.node(); + + return 0; +} + +// Returns the visible position at the beginning of a node +VisiblePosition visiblePositionBeforeNode(Node* node) +{ + ASSERT(node); + if (node->childNodeCount()) + return VisiblePosition(node, 0, DOWNSTREAM); + ASSERT(node->parentNode()); + return positionInParentBeforeNode(node); +} + +// Returns the visible position at the ending of a node +VisiblePosition visiblePositionAfterNode(Node* node) +{ + ASSERT(node); + if (node->childNodeCount()) + return VisiblePosition(node, node->childNodeCount(), DOWNSTREAM); + ASSERT(node->parentNode()); + return positionInParentAfterNode(node); +} + +// Create a range object with two visible positions, start and end. +// create(PassRefPtr<Document>, const Position&, const Position&); will use deprecatedEditingOffset +// Use this function instead of create a regular range object (avoiding editing offset). +PassRefPtr<Range> createRange(PassRefPtr<Document> document, const VisiblePosition& start, const VisiblePosition& end, ExceptionCode& ec) +{ + ec = 0; + RefPtr<Range> selectedRange = Range::create(document); + selectedRange->setStart(start.deepEquivalent().containerNode(), start.deepEquivalent().computeOffsetInContainerNode(), ec); + if (!ec) + selectedRange->setEnd(end.deepEquivalent().containerNode(), end.deepEquivalent().computeOffsetInContainerNode(), ec); + return selectedRange.release(); +} + +// Extend rangeToExtend to include nodes that wraps range and visibly starts and ends inside or at the boudnaries of maximumRange +// e.g. if the original range spaned "hello" in <div>hello</div>, then this function extends the range to contain div's around it. +// Call this function before copying / moving paragraphs to contain all wrapping nodes. +// This function stops extending the range immediately below rootNode; i.e. the extended range can contain a child node of rootNode +// but it can never contain rootNode itself. +PassRefPtr<Range> extendRangeToWrappingNodes(PassRefPtr<Range> range, const Range* maximumRange, const Node* rootNode) +{ + ASSERT(range); + ASSERT(maximumRange); + + ExceptionCode ec = 0; + Node* ancestor = range->commonAncestorContainer(ec);// find the cloeset common ancestor + Node* highestNode = 0; + // traverse through ancestors as long as they are contained within the range, content-editable, and below rootNode (could be =0). + while (ancestor && ancestor->isContentEditable() && isNodeVisiblyContainedWithin(ancestor, maximumRange) && ancestor != rootNode) { + highestNode = ancestor; + ancestor = ancestor->parentNode(); + } + + if (!highestNode) + return range; + + // Create new range with the highest editable node contained within the range + RefPtr<Range> extendedRange = Range::create(range->ownerDocument()); + extendedRange->selectNode(highestNode, ec); + return extendedRange.release(); +} + +bool isListElement(Node *n) +{ + return (n && (n->hasTagName(ulTag) || n->hasTagName(olTag) || n->hasTagName(dlTag))); +} + +bool isListItem(Node *n) +{ + return n && n->renderer() && n->renderer()->isListItem(); +} + +Node* enclosingNodeWithTag(const Position& p, const QualifiedName& tagName) +{ + if (p.isNull()) + return 0; + + Node* root = highestEditableRoot(p); + for (Node* n = p.node(); n; n = n->parentNode()) { + if (root && !n->isContentEditable()) + continue; + if (n->hasTagName(tagName)) + return n; + if (n == root) + return 0; + } + + return 0; +} + +Node* enclosingNodeOfType(const Position& p, bool (*nodeIsOfType)(const Node*), bool onlyReturnEditableNodes) +{ + if (p.isNull()) + return 0; + + Node* root = highestEditableRoot(p); + for (Node* n = p.node(); n; n = n->parentNode()) { + // Don't return a non-editable node if the input position was editable, since + // the callers from editing will no doubt want to perform editing inside the returned node. + if (root && !n->isContentEditable() && onlyReturnEditableNodes) + continue; + if ((*nodeIsOfType)(n)) + return n; + if (n == root) + return 0; + } + + return 0; +} + +Node* highestEnclosingNodeOfType(const Position& p, bool (*nodeIsOfType)(const Node*)) +{ + Node* highest = 0; + Node* root = highestEditableRoot(p); + for (Node* n = p.node(); n; n = n->parentNode()) { + if ((*nodeIsOfType)(n)) + highest = n; + if (n == root) + break; + } + + return highest; +} + +Node* enclosingTableCell(const Position& p) +{ + return static_cast<Element*>(enclosingNodeOfType(p, isTableCell)); +} + +Node* enclosingAnchorElement(const Position& p) +{ + if (p.isNull()) + return 0; + + Node* node = p.node(); + while (node && !(node->isElementNode() && node->isLink())) + node = node->parentNode(); + return node; +} + +HTMLElement* enclosingList(Node* node) +{ + if (!node) + return 0; + + Node* root = highestEditableRoot(Position(node, 0)); + + for (ContainerNode* n = node->parentNode(); n; n = n->parentNode()) { + if (n->hasTagName(ulTag) || n->hasTagName(olTag)) + return static_cast<HTMLElement*>(n); + if (n == root) + return 0; + } + + return 0; +} + +HTMLElement* enclosingListChild(Node *node) +{ + if (!node) + return 0; + // Check for a list item element, or for a node whose parent is a list element. Such a node + // will appear visually as a list item (but without a list marker) + Node* root = highestEditableRoot(Position(node, 0)); + + // FIXME: This function is inappropriately named if it starts with node instead of node->parentNode() + for (Node* n = node; n && n->parentNode(); n = n->parentNode()) { + if (n->hasTagName(liTag) || isListElement(n->parentNode())) + return static_cast<HTMLElement*>(n); + if (n == root || isTableCell(n)) + return 0; + } + + return 0; +} + +static HTMLElement* embeddedSublist(Node* listItem) +{ + // Check the DOM so that we'll find collapsed sublists without renderers. + for (Node* n = listItem->firstChild(); n; n = n->nextSibling()) { + if (isListElement(n)) + return static_cast<HTMLElement*>(n); + } + + return 0; +} + +static Node* appendedSublist(Node* listItem) +{ + // Check the DOM so that we'll find collapsed sublists without renderers. + for (Node* n = listItem->nextSibling(); n; n = n->nextSibling()) { + if (isListElement(n)) + return static_cast<HTMLElement*>(n); + if (isListItem(listItem)) + return 0; + } + + return 0; +} + +// FIXME: This method should not need to call isStartOfParagraph/isEndOfParagraph +Node* enclosingEmptyListItem(const VisiblePosition& visiblePos) +{ + // Check that position is on a line by itself inside a list item + Node* listChildNode = enclosingListChild(visiblePos.deepEquivalent().node()); + if (!listChildNode || !isStartOfParagraph(visiblePos) || !isEndOfParagraph(visiblePos)) + return 0; + + VisiblePosition firstInListChild(firstDeepEditingPositionForNode(listChildNode)); + VisiblePosition lastInListChild(lastDeepEditingPositionForNode(listChildNode)); + + if (firstInListChild != visiblePos || lastInListChild != visiblePos) + return 0; + + if (embeddedSublist(listChildNode) || appendedSublist(listChildNode)) + return 0; + + return listChildNode; +} + +HTMLElement* outermostEnclosingList(Node* node, Node* rootList) +{ + HTMLElement* list = enclosingList(node); + if (!list) + return 0; + + while (HTMLElement* nextList = enclosingList(list)) { + if (nextList == rootList) + break; + list = nextList; + } + + return list; +} + +bool canMergeLists(Element* firstList, Element* secondList) +{ + if (!firstList || !secondList || !firstList->isHTMLElement() || !secondList->isHTMLElement()) + return false; + + return firstList->hasTagName(secondList->tagQName())// make sure the list types match (ol vs. ul) + && firstList->isContentEditable() && secondList->isContentEditable()// both lists are editable + && firstList->rootEditableElement() == secondList->rootEditableElement()// don't cross editing boundaries + && isVisiblyAdjacent(positionInParentAfterNode(firstList), positionInParentBeforeNode(secondList)); + // Make sure there is no visible content between this li and the previous list +} + +Node* highestAncestor(Node* node) +{ + ASSERT(node); + Node* parent = node; + while ((node = node->parentNode())) + parent = node; + return parent; +} + +// FIXME: do not require renderer, so that this can be used within fragments, or rename to isRenderedTable() +bool isTableElement(Node* n) +{ + if (!n || !n->isElementNode()) + return false; + + RenderObject* renderer = n->renderer(); + return (renderer && (renderer->style()->display() == TABLE || renderer->style()->display() == INLINE_TABLE)); +} + +bool isTableCell(const Node* node) +{ + RenderObject* r = node->renderer(); + if (!r) + return node->hasTagName(tdTag) || node->hasTagName(thTag); + + return r->isTableCell(); +} + +bool isEmptyTableCell(const Node* node) +{ + // Returns true IFF the passed in node is one of: + // .) a table cell with no children, + // .) a table cell with a single BR child, and which has no other child renderers, including :before and :after renderers + // .) the BR child of such a table cell + + // Find rendered node + while (node && !node->renderer()) + node = node->parentNode(); + if (!node) + return false; + + // Make sure the rendered node is a table cell or <br>. + // If it's a <br>, then the parent node has to be a table cell. + RenderObject* renderer = node->renderer(); + if (renderer->isBR()) { + renderer = renderer->parent(); + if (!renderer) + return false; + } + if (!renderer->isTableCell()) + return false; + + // Check that the table cell contains no child renderers except for perhaps a single <br>. + RenderObject* childRenderer = renderer->firstChild(); + if (!childRenderer) + return true; + if (!childRenderer->isBR()) + return false; + return !childRenderer->nextSibling(); +} + +PassRefPtr<HTMLElement> createDefaultParagraphElement(Document* document) +{ + return HTMLDivElement::create(document); +} + +PassRefPtr<HTMLElement> createBreakElement(Document* document) +{ + return HTMLBRElement::create(document); +} + +PassRefPtr<HTMLElement> createOrderedListElement(Document* document) +{ + return HTMLOListElement::create(document); +} + +PassRefPtr<HTMLElement> createUnorderedListElement(Document* document) +{ + return HTMLUListElement::create(document); +} + +PassRefPtr<HTMLElement> createListItemElement(Document* document) +{ + return HTMLLIElement::create(document); +} + +PassRefPtr<HTMLElement> createHTMLElement(Document* document, const QualifiedName& name) +{ + return HTMLElementFactory::createHTMLElement(name, document, 0, false); +} + +PassRefPtr<HTMLElement> createHTMLElement(Document* document, const AtomicString& tagName) +{ + return createHTMLElement(document, QualifiedName(nullAtom, tagName, xhtmlNamespaceURI)); +} + +bool isTabSpanNode(const Node *node) +{ + return node && node->hasTagName(spanTag) && node->isElementNode() && static_cast<const Element *>(node)->getAttribute(classAttr) == AppleTabSpanClass; +} + +bool isTabSpanTextNode(const Node *node) +{ + return node && node->isTextNode() && node->parentNode() && isTabSpanNode(node->parentNode()); +} + +Node *tabSpanNode(const Node *node) +{ + return isTabSpanTextNode(node) ? node->parentNode() : 0; +} + +bool isNodeInTextFormControl(Node* node) +{ + if (!node) + return false; + Node* ancestor = node->shadowAncestorNode(); + if (ancestor == node) + return false; + return ancestor->isElementNode() && static_cast<Element*>(ancestor)->isTextFormControl(); +} + +Position positionBeforeTabSpan(const Position& pos) +{ + Node *node = pos.node(); + if (isTabSpanTextNode(node)) + node = tabSpanNode(node); + else if (!isTabSpanNode(node)) + return pos; + + return positionInParentBeforeNode(node); +} + +PassRefPtr<Element> createTabSpanElement(Document* document, PassRefPtr<Node> tabTextNode) +{ + // Make the span to hold the tab. + RefPtr<Element> spanElement = document->createElement(spanTag, false); + spanElement->setAttribute(classAttr, AppleTabSpanClass); + spanElement->setAttribute(styleAttr, "white-space:pre"); + + // Add tab text to that span. + if (!tabTextNode) + tabTextNode = document->createEditingTextNode("\t"); + + ExceptionCode ec = 0; + spanElement->appendChild(tabTextNode, ec); + ASSERT(ec == 0); + + return spanElement.release(); +} + +PassRefPtr<Element> createTabSpanElement(Document* document, const String& tabText) +{ + return createTabSpanElement(document, document->createTextNode(tabText)); +} + +PassRefPtr<Element> createTabSpanElement(Document* document) +{ + return createTabSpanElement(document, PassRefPtr<Node>()); +} + +bool isNodeRendered(const Node *node) +{ + if (!node) + return false; + + RenderObject *renderer = node->renderer(); + if (!renderer) + return false; + + return renderer->style()->visibility() == VISIBLE; +} + +Node *nearestMailBlockquote(const Node *node) +{ + for (Node *n = const_cast<Node *>(node); n; n = n->parentNode()) { + if (isMailBlockquote(n)) + return n; + } + return 0; +} + +unsigned numEnclosingMailBlockquotes(const Position& p) +{ + unsigned num = 0; + for (Node* n = p.node(); n; n = n->parentNode()) + if (isMailBlockquote(n)) + num++; + + return num; +} + +bool isMailBlockquote(const Node *node) +{ + if (!node || !node->hasTagName(blockquoteTag)) + return false; + + return static_cast<const Element *>(node)->getAttribute("type") == "cite"; +} + +int caretMinOffset(const Node* n) +{ + RenderObject* r = n->renderer(); + ASSERT(!n->isCharacterDataNode() || !r || r->isText()); // FIXME: This was a runtime check that seemingly couldn't fail; changed it to an assertion for now. + return r ? r->caretMinOffset() : 0; +} + +// If a node can contain candidates for VisiblePositions, return the offset of the last candidate, otherwise +// return the number of children for container nodes and the length for unrendered text nodes. +int caretMaxOffset(const Node* n) +{ + // For rendered text nodes, return the last position that a caret could occupy. + if (n->isTextNode() && n->renderer()) + return n->renderer()->caretMaxOffset(); + // For containers return the number of children. For others do the same as above. + return lastOffsetForEditing(n); +} + +bool lineBreakExistsAtVisiblePosition(const VisiblePosition& visiblePosition) +{ + return lineBreakExistsAtPosition(visiblePosition.deepEquivalent().downstream()); +} + +bool lineBreakExistsAtPosition(const Position& position) +{ + if (position.isNull()) + return false; + + if (position.anchorNode()->hasTagName(brTag) && position.atFirstEditingPositionForNode()) + return true; + + if (!position.anchorNode()->renderer()) + return false; + + if (!position.anchorNode()->isTextNode() || !position.anchorNode()->renderer()->style()->preserveNewline()) + return false; + + Text* textNode = static_cast<Text*>(position.anchorNode()); + unsigned offset = position.offsetInContainerNode(); + return offset < textNode->length() && textNode->data()[offset] == '\n'; +} + +// Modifies selections that have an end point at the edge of a table +// that contains the other endpoint so that they don't confuse +// code that iterates over selected paragraphs. +VisibleSelection selectionForParagraphIteration(const VisibleSelection& original) +{ + VisibleSelection newSelection(original); + VisiblePosition startOfSelection(newSelection.visibleStart()); + VisiblePosition endOfSelection(newSelection.visibleEnd()); + + // If the end of the selection to modify is just after a table, and + // if the start of the selection is inside that table, then the last paragraph + // that we'll want modify is the last one inside the table, not the table itself + // (a table is itself a paragraph). + if (Node* table = isFirstPositionAfterTable(endOfSelection)) + if (startOfSelection.deepEquivalent().node()->isDescendantOf(table)) + newSelection = VisibleSelection(startOfSelection, endOfSelection.previous(true)); + + // If the start of the selection to modify is just before a table, + // and if the end of the selection is inside that table, then the first paragraph + // we'll want to modify is the first one inside the table, not the paragraph + // containing the table itself. + if (Node* table = isLastPositionBeforeTable(startOfSelection)) + if (endOfSelection.deepEquivalent().node()->isDescendantOf(table)) + newSelection = VisibleSelection(startOfSelection.next(true), endOfSelection); + + return newSelection; +} + + +int indexForVisiblePosition(const VisiblePosition& visiblePosition) +{ + if (visiblePosition.isNull()) + return 0; + Position p(visiblePosition.deepEquivalent()); + RefPtr<Range> range = Range::create(p.node()->document(), Position(p.node()->document(), 0), rangeCompliantEquivalent(p)); + return TextIterator::rangeLength(range.get(), true); +} + +// Determines whether two positions are visibly next to each other (first then second) +// while ignoring whitespaces and unrendered nodes +bool isVisiblyAdjacent(const Position& first, const Position& second) +{ + return VisiblePosition(first) == VisiblePosition(second.upstream()); +} + +// Determines whether a node is inside a range or visibly starts and ends at the boundaries of the range. +// Call this function to determine whether a node is visibly fit inside selectedRange +bool isNodeVisiblyContainedWithin(Node* node, const Range* selectedRange) +{ + ASSERT(node); + ASSERT(selectedRange); + // If the node is inside the range, then it surely is contained within + ExceptionCode ec = 0; + if (selectedRange->compareNode(node, ec) == Range::NODE_INSIDE) + return true; + + bool startIsVisuallySame = visiblePositionBeforeNode(node) == selectedRange->startPosition(); + if (startIsVisuallySame && comparePositions(Position(node->parentNode(), node->nodeIndex()+1), selectedRange->endPosition()) < 0) + return true; + + bool endIsVisuallySame = visiblePositionAfterNode(node) == selectedRange->endPosition(); + if (endIsVisuallySame && comparePositions(selectedRange->startPosition(), Position(node->parentNode(), node->nodeIndex())) < 0) + return true; + + return startIsVisuallySame && endIsVisuallySame; +} + +bool isRenderedAsNonInlineTableImageOrHR(const Node* node) +{ + if (!node) + return false; + RenderObject* renderer = node->renderer(); + return renderer && ((renderer->isTable() && !renderer->isInline()) || (renderer->isImage() && !renderer->isInline()) || renderer->isHR()); +} + +PassRefPtr<Range> avoidIntersectionWithNode(const Range* range, Node* node) +{ + if (!range) + return 0; + + Document* document = range->ownerDocument(); + + Node* startContainer = range->startContainer(); + int startOffset = range->startOffset(); + Node* endContainer = range->endContainer(); + int endOffset = range->endOffset(); + + if (!startContainer) + return 0; + + ASSERT(endContainer); + + if (startContainer == node || startContainer->isDescendantOf(node)) { + ASSERT(node->parentNode()); + startContainer = node->parentNode(); + startOffset = node->nodeIndex(); + } + if (endContainer == node || endContainer->isDescendantOf(node)) { + ASSERT(node->parentNode()); + endContainer = node->parentNode(); + endOffset = node->nodeIndex(); + } + + return Range::create(document, startContainer, startOffset, endContainer, endOffset); +} + +VisibleSelection avoidIntersectionWithNode(const VisibleSelection& selection, Node* node) +{ + if (selection.isNone()) + return VisibleSelection(selection); + + VisibleSelection updatedSelection(selection); + Node* base = selection.base().node(); + Node* extent = selection.extent().node(); + ASSERT(base); + ASSERT(extent); + + if (base == node || base->isDescendantOf(node)) { + ASSERT(node->parentNode()); + updatedSelection.setBase(Position(node->parentNode(), node->nodeIndex())); + } + + if (extent == node || extent->isDescendantOf(node)) { + ASSERT(node->parentNode()); + updatedSelection.setExtent(Position(node->parentNode(), node->nodeIndex())); + } + + return updatedSelection; +} + +} // namespace WebCore diff --git a/Source/WebCore/editing/htmlediting.h b/Source/WebCore/editing/htmlediting.h new file mode 100644 index 0000000..1892357 --- /dev/null +++ b/Source/WebCore/editing/htmlediting.h @@ -0,0 +1,234 @@ +/* + * Copyright (C) 2004, 2006, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef htmlediting_h +#define htmlediting_h + +#include <wtf/Forward.h> +#include "HTMLNames.h" +#include "ExceptionCode.h" +#include "Position.h" + +namespace WebCore { + +class Document; +class Element; +class HTMLElement; +class Node; +class Position; +class Range; +class VisiblePosition; +class VisibleSelection; + + +// This file contains a set of helper functions used by the editing commands + +// ------------------------------------------------------------------------- +// Node +// ------------------------------------------------------------------------- + +// Functions returning Node + +Node* highestAncestor(Node*); +Node* highestEditableRoot(const Position&); +Node* highestEnclosingNodeOfType(const Position&, bool (*nodeIsOfType)(const Node*)); +Node* lowestEditableAncestor(Node*); + +Node* enclosingBlock(Node*); +Node* enclosingTableCell(const Position&); +Node* enclosingEmptyListItem(const VisiblePosition&); +Node* enclosingAnchorElement(const Position&); +Node* enclosingNodeWithTag(const Position&, const QualifiedName&); +Node* enclosingNodeOfType(const Position&, bool (*nodeIsOfType)(const Node*), bool onlyReturnEditableNodes = true); + +Node* tabSpanNode(const Node*); +Node* nearestMailBlockquote(const Node*); +Node* isLastPositionBeforeTable(const VisiblePosition&); +Node* isFirstPositionAfterTable(const VisiblePosition&); + +// offset functions on Node + +int lastOffsetForEditing(const Node*); +int caretMinOffset(const Node*); +int caretMaxOffset(const Node*); + +// boolean functions on Node + +bool editingIgnoresContent(const Node*); +bool canHaveChildrenForEditing(const Node*); +bool isAtomicNode(const Node*); +bool isBlock(const Node*); +bool isSpecialElement(const Node*); +bool isTabSpanNode(const Node*); +bool isTabSpanTextNode(const Node*); +bool isMailBlockquote(const Node*); +bool isTableElement(Node*); +bool isTableCell(const Node*); +bool isEmptyTableCell(const Node*); +bool isTableStructureNode(const Node*); +bool isListElement(Node*); +bool isListItem(Node*); +bool isNodeRendered(const Node*); +bool isNodeVisiblyContainedWithin(Node*, const Range*); +bool isRenderedAsNonInlineTableImageOrHR(const Node*); +bool isNodeInTextFormControl(Node* node); + +// ------------------------------------------------------------------------- +// Position +// ------------------------------------------------------------------------- + +// Functions returning Position + +Position rangeCompliantEquivalent(const Position&); +Position rangeCompliantEquivalent(const VisiblePosition&); + +Position nextCandidate(const Position&); +Position previousCandidate(const Position&); + +Position nextVisuallyDistinctCandidate(const Position&); +Position previousVisuallyDistinctCandidate(const Position&); + +Position positionBeforeTabSpan(const Position&); +Position positionBeforeContainingSpecialElement(const Position&, Node** containingSpecialElement=0); +Position positionAfterContainingSpecialElement(const Position&, Node** containingSpecialElement=0); +Position positionOutsideContainingSpecialElement(const Position&, Node** containingSpecialElement=0); + +// Position creation functions are inline to prevent ref-churn. +// Other Position creation functions are in Position.h +// but these depend on lastOffsetForEditing which is defined in htmlediting.h. + +// NOTE: first/lastDeepEditingPositionForNode return legacy editing positions (like [img, 0]) +// for elements which editing ignores. The rest of the editing code will treat [img, 0] +// as "the last position before the img". +// New code should use the creation functions in Position.h instead. +inline Position firstDeepEditingPositionForNode(Node* anchorNode) +{ + ASSERT(anchorNode); + return Position(anchorNode, 0); +} + +inline Position lastDeepEditingPositionForNode(Node* anchorNode) +{ + ASSERT(anchorNode); + return Position(anchorNode, lastOffsetForEditing(anchorNode)); +} + +// comparision functions on Position + +int comparePositions(const Position&, const Position&); + +// boolean functions on Position + +bool isEditablePosition(const Position&); +bool isRichlyEditablePosition(const Position&); +bool isFirstVisiblePositionInSpecialElement(const Position&); +bool isLastVisiblePositionInSpecialElement(const Position&); +bool lineBreakExistsAtPosition(const Position&); +bool isVisiblyAdjacent(const Position& first, const Position& second); +bool isAtUnsplittableElement(const Position&); + +// miscellaneous functions on Position + +unsigned numEnclosingMailBlockquotes(const Position&); + +// ------------------------------------------------------------------------- +// VisiblePosition +// ------------------------------------------------------------------------- + +// Functions returning VisiblePosition + +VisiblePosition firstEditablePositionAfterPositionInRoot(const Position&, Node*); +VisiblePosition lastEditablePositionBeforePositionInRoot(const Position&, Node*); +VisiblePosition visiblePositionBeforeNode(Node*); +VisiblePosition visiblePositionAfterNode(Node*); + +bool lineBreakExistsAtVisiblePosition(const VisiblePosition&); + +int comparePositions(const VisiblePosition&, const VisiblePosition&); +int indexForVisiblePosition(const VisiblePosition&); + +// ------------------------------------------------------------------------- +// Range +// ------------------------------------------------------------------------- + +// Functions returning Range + +PassRefPtr<Range> createRange(PassRefPtr<Document>, const VisiblePosition& start, const VisiblePosition& end, ExceptionCode&); +PassRefPtr<Range> extendRangeToWrappingNodes(PassRefPtr<Range> rangeToExtend, const Range* maximumRange, const Node* rootNode); +PassRefPtr<Range> avoidIntersectionWithNode(const Range*, Node*); + +// ------------------------------------------------------------------------- +// HTMLElement +// ------------------------------------------------------------------------- + +// Functions returning HTMLElement + +PassRefPtr<HTMLElement> createDefaultParagraphElement(Document*); +PassRefPtr<HTMLElement> createBreakElement(Document*); +PassRefPtr<HTMLElement> createOrderedListElement(Document*); +PassRefPtr<HTMLElement> createUnorderedListElement(Document*); +PassRefPtr<HTMLElement> createListItemElement(Document*); +PassRefPtr<HTMLElement> createHTMLElement(Document*, const QualifiedName&); +PassRefPtr<HTMLElement> createHTMLElement(Document*, const AtomicString&); + +HTMLElement* enclosingList(Node*); +HTMLElement* outermostEnclosingList(Node*, Node* rootList = 0); +HTMLElement* enclosingListChild(Node*); + +// ------------------------------------------------------------------------- +// Element +// ------------------------------------------------------------------------- + +// Functions returning Element + +PassRefPtr<Element> createTabSpanElement(Document*); +PassRefPtr<Element> createTabSpanElement(Document*, PassRefPtr<Node> tabTextNode); +PassRefPtr<Element> createTabSpanElement(Document*, const String& tabText); +PassRefPtr<Element> createBlockPlaceholderElement(Document*); + +Element* editableRootForPosition(const Position&); +Element* unsplittableElementForPosition(const Position&); + +// Boolean functions on Element + +bool canMergeLists(Element* firstList, Element* secondList); + +// ------------------------------------------------------------------------- +// VisibleSelection +// ------------------------------------------------------------------------- + +// Functions returning VisibleSelection +VisibleSelection avoidIntersectionWithNode(const VisibleSelection&, Node*); +VisibleSelection selectionForParagraphIteration(const VisibleSelection&); + + +// Miscellaneous functions on String + +String stringWithRebalancedWhitespace(const String&, bool, bool); +const String& nonBreakingSpaceString(); + +} + +#endif diff --git a/Source/WebCore/editing/mac/EditorMac.mm b/Source/WebCore/editing/mac/EditorMac.mm new file mode 100644 index 0000000..56b9f71 --- /dev/null +++ b/Source/WebCore/editing/mac/EditorMac.mm @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2006, 2007, 2008 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#import "config.h" +#import "Editor.h" + +#import "ColorMac.h" +#import "ClipboardMac.h" +#import "CachedResourceLoader.h" +#import "DocumentFragment.h" +#import "Editor.h" +#import "EditorClient.h" +#import "Frame.h" +#import "FrameView.h" +#import "Pasteboard.h" +#import "RenderBlock.h" +#import "RuntimeApplicationChecks.h" +#import "Sound.h" + +namespace WebCore { + +PassRefPtr<Clipboard> Editor::newGeneralClipboard(ClipboardAccessPolicy policy, Frame* frame) +{ + return ClipboardMac::create(Clipboard::CopyAndPaste, [NSPasteboard generalPasteboard], policy, frame); +} + +void Editor::showFontPanel() +{ + [[NSFontManager sharedFontManager] orderFrontFontPanel:nil]; +} + +void Editor::showStylesPanel() +{ + [[NSFontManager sharedFontManager] orderFrontStylesPanel:nil]; +} + +void Editor::showColorPanel() +{ + [[NSApplication sharedApplication] orderFrontColorPanel:nil]; +} + +void Editor::pasteWithPasteboard(Pasteboard* pasteboard, bool allowPlainText) +{ + RefPtr<Range> range = selectedRange(); + bool choosePlainText; + + m_frame->editor()->client()->setInsertionPasteboard([NSPasteboard generalPasteboard]); +#if !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD) && !defined(BUILDING_ON_SNOW_LEOPARD) + RefPtr<DocumentFragment> fragment = pasteboard->documentFragment(m_frame, range, allowPlainText, choosePlainText); + if (fragment && shouldInsertFragment(fragment, range, EditorInsertActionPasted)) + pasteAsFragment(fragment, canSmartReplaceWithPasteboard(pasteboard), false); +#else + // Mail is ignoring the frament passed to the delegate and creates a new one. + // We want to avoid creating the fragment twice. + if (applicationIsAppleMail()) { + if (shouldInsertFragment(NULL, range, EditorInsertActionPasted)) { + RefPtr<DocumentFragment> fragment = pasteboard->documentFragment(m_frame, range, allowPlainText, choosePlainText); + if (fragment) + pasteAsFragment(fragment, canSmartReplaceWithPasteboard(pasteboard), false); + } + } else { + RefPtr<DocumentFragment>fragment = pasteboard->documentFragment(m_frame, range, allowPlainText, choosePlainText); + if (fragment && shouldInsertFragment(fragment, range, EditorInsertActionPasted)) + pasteAsFragment(fragment, canSmartReplaceWithPasteboard(pasteboard), false); + } +#endif + m_frame->editor()->client()->setInsertionPasteboard(nil); +} + +NSDictionary* Editor::fontAttributesForSelectionStart() const +{ + Node* nodeToRemove; + RenderStyle* style = styleForSelectionStart(nodeToRemove); + if (!style) + return nil; + + NSMutableDictionary* result = [NSMutableDictionary dictionary]; + + if (style->visitedDependentColor(CSSPropertyBackgroundColor).isValid() && style->visitedDependentColor(CSSPropertyBackgroundColor).alpha() != 0) + [result setObject:nsColor(style->visitedDependentColor(CSSPropertyBackgroundColor)) forKey:NSBackgroundColorAttributeName]; + + if (style->font().primaryFont()->getNSFont()) + [result setObject:style->font().primaryFont()->getNSFont() forKey:NSFontAttributeName]; + + if (style->visitedDependentColor(CSSPropertyColor).isValid() && style->visitedDependentColor(CSSPropertyColor) != Color::black) + [result setObject:nsColor(style->visitedDependentColor(CSSPropertyColor)) forKey:NSForegroundColorAttributeName]; + + const ShadowData* shadow = style->textShadow(); + if (shadow) { + NSShadow* s = [[NSShadow alloc] init]; + [s setShadowOffset:NSMakeSize(shadow->x(), shadow->y())]; + [s setShadowBlurRadius:shadow->blur()]; + [s setShadowColor:nsColor(shadow->color())]; + [result setObject:s forKey:NSShadowAttributeName]; + } + + int decoration = style->textDecorationsInEffect(); + if (decoration & LINE_THROUGH) + [result setObject:[NSNumber numberWithInt:NSUnderlineStyleSingle] forKey:NSStrikethroughStyleAttributeName]; + + int superscriptInt = 0; + switch (style->verticalAlign()) { + case BASELINE: + case BOTTOM: + case BASELINE_MIDDLE: + case LENGTH: + case MIDDLE: + case TEXT_BOTTOM: + case TEXT_TOP: + case TOP: + break; + case SUB: + superscriptInt = -1; + break; + case SUPER: + superscriptInt = 1; + break; + } + if (superscriptInt) + [result setObject:[NSNumber numberWithInt:superscriptInt] forKey:NSSuperscriptAttributeName]; + + if (decoration & UNDERLINE) + [result setObject:[NSNumber numberWithInt:NSUnderlineStyleSingle] forKey:NSUnderlineStyleAttributeName]; + + if (nodeToRemove) { + ExceptionCode ec = 0; + nodeToRemove->remove(ec); + ASSERT(ec == 0); + } + + return result; +} + +NSWritingDirection Editor::baseWritingDirectionForSelectionStart() const +{ + NSWritingDirection result = NSWritingDirectionLeftToRight; + + Position pos = m_frame->selection()->selection().visibleStart().deepEquivalent(); + Node* node = pos.node(); + if (!node) + return result; + + RenderObject* renderer = node->renderer(); + if (!renderer) + return result; + + if (!renderer->isBlockFlow()) { + renderer = renderer->containingBlock(); + if (!renderer) + return result; + } + + RenderStyle* style = renderer->style(); + if (!style) + return result; + + switch (style->direction()) { + case LTR: + result = NSWritingDirectionLeftToRight; + break; + case RTL: + result = NSWritingDirectionRightToLeft; + break; + } + + return result; +} + +bool Editor::canCopyExcludingStandaloneImages() +{ + SelectionController* selection = m_frame->selection(); + return selection->isRange() && !selection->isInPasswordField(); +} + +void Editor::takeFindStringFromSelection() +{ + if (!canCopyExcludingStandaloneImages()) { + systemBeep(); + return; + } + + NSString *nsSelectedText = m_frame->displayStringModifiedByEncoding(selectedText()); + + NSPasteboard *findPasteboard = [NSPasteboard pasteboardWithName:NSFindPboard]; + [findPasteboard declareTypes:[NSArray arrayWithObject:NSStringPboardType] owner:nil]; + [findPasteboard setString:nsSelectedText forType:NSStringPboardType]; +} + +} // namespace WebCore diff --git a/Source/WebCore/editing/mac/SelectionControllerMac.mm b/Source/WebCore/editing/mac/SelectionControllerMac.mm new file mode 100644 index 0000000..730eb60 --- /dev/null +++ b/Source/WebCore/editing/mac/SelectionControllerMac.mm @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2007 Apple 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 APPLE INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#import "config.h" +#import "SelectionController.h" + +#import "AXObjectCache.h" +#import "Frame.h" +#import "RenderView.h" +#import "WebCoreViewFactory.h" + +namespace WebCore { + +void SelectionController::notifyAccessibilityForSelectionChange() +{ + Document* document = m_frame->document(); + + if (AXObjectCache::accessibilityEnabled() && m_selection.start().isNotNull() && m_selection.end().isNotNull()) + document->axObjectCache()->postNotification(m_selection.start().node()->renderer(), AXObjectCache::AXSelectedTextChanged, false); + + // if zoom feature is enabled, insertion point changes should update the zoom + if (!UAZoomEnabled() || !m_selection.isCaret()) + return; + + RenderView* renderView = document->renderView(); + if (!renderView) + return; + FrameView* frameView = m_frame->view(); + if (!frameView) + return; + + IntRect selectionRect = absoluteCaretBounds(); + IntRect viewRect = renderView->viewRect(); + + selectionRect = frameView->contentsToScreen(selectionRect); + viewRect = frameView->contentsToScreen(viewRect); + CGRect cgCaretRect = CGRectMake(selectionRect.x(), selectionRect.y(), selectionRect.width(), selectionRect.height()); + CGRect cgViewRect = CGRectMake(viewRect.x(), viewRect.y(), viewRect.width(), viewRect.height()); + cgCaretRect = [[WebCoreViewFactory sharedFactory] accessibilityConvertScreenRect:cgCaretRect]; + cgViewRect = [[WebCoreViewFactory sharedFactory] accessibilityConvertScreenRect:cgViewRect]; + + UAZoomChangeFocus(&cgViewRect, &cgCaretRect, kUAZoomFocusTypeInsertionPoint); +} + +} // namespace WebCore diff --git a/Source/WebCore/editing/markup.cpp b/Source/WebCore/editing/markup.cpp new file mode 100644 index 0000000..4cbdcce --- /dev/null +++ b/Source/WebCore/editing/markup.cpp @@ -0,0 +1,915 @@ +/* + * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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 "markup.h" + +#include "CDATASection.h" +#include "CharacterNames.h" +#include "CSSComputedStyleDeclaration.h" +#include "CSSMutableStyleDeclaration.h" +#include "CSSPrimitiveValue.h" +#include "CSSProperty.h" +#include "CSSPropertyNames.h" +#include "CSSRule.h" +#include "CSSRuleList.h" +#include "CSSStyleRule.h" +#include "CSSStyleSelector.h" +#include "CSSValue.h" +#include "CSSValueKeywords.h" +#include "DeleteButtonController.h" +#include "DocumentFragment.h" +#include "DocumentType.h" +#include "Editor.h" +#include "Frame.h" +#include "HTMLBodyElement.h" +#include "HTMLElement.h" +#include "HTMLNames.h" +#include "KURL.h" +#include "MarkupAccumulator.h" +#include "Range.h" +#include "TextIterator.h" +#include "VisibleSelection.h" +#include "XMLNSNames.h" +#include "htmlediting.h" +#include "visible_units.h" +#include <wtf/StdLibExtras.h> + +using namespace std; + +namespace WebCore { + +using namespace HTMLNames; + +static bool propertyMissingOrEqualToNone(CSSStyleDeclaration*, int propertyID); + +class AttributeChange { +public: + AttributeChange() + : m_name(nullAtom, nullAtom, nullAtom) + { + } + + AttributeChange(PassRefPtr<Element> element, const QualifiedName& name, const String& value) + : m_element(element), m_name(name), m_value(value) + { + } + + void apply() + { + m_element->setAttribute(m_name, m_value); + } + +private: + RefPtr<Element> m_element; + QualifiedName m_name; + String m_value; +}; + +static void completeURLs(Node* node, const String& baseURL) +{ + Vector<AttributeChange> changes; + + KURL parsedBaseURL(ParsedURLString, baseURL); + + Node* end = node->traverseNextSibling(); + for (Node* n = node; n != end; n = n->traverseNextNode()) { + if (n->isElementNode()) { + Element* e = static_cast<Element*>(n); + NamedNodeMap* attributes = e->attributes(); + unsigned length = attributes->length(); + for (unsigned i = 0; i < length; i++) { + Attribute* attribute = attributes->attributeItem(i); + if (e->isURLAttribute(attribute)) + changes.append(AttributeChange(e, attribute->name(), KURL(parsedBaseURL, attribute->value()).string())); + } + } + } + + size_t numChanges = changes.size(); + for (size_t i = 0; i < numChanges; ++i) + changes[i].apply(); +} + +class StyledMarkupAccumulator : public MarkupAccumulator { +public: + enum RangeFullySelectsNode { DoesFullySelectNode, DoesNotFullySelectNode }; + + StyledMarkupAccumulator(Vector<Node*>* nodes, EAbsoluteURLs shouldResolveURLs, EAnnotateForInterchange shouldAnnotate, const Range* range) + : MarkupAccumulator(nodes, shouldResolveURLs, range) + , m_shouldAnnotate(shouldAnnotate) + { + } + + Node* serializeNodes(Node* startNode, Node* pastEnd); + void appendString(const String& s) { return MarkupAccumulator::appendString(s); } + void wrapWithNode(Node*, bool convertBlocksToInlines = false, RangeFullySelectsNode = DoesFullySelectNode); + void wrapWithStyleNode(CSSStyleDeclaration*, Document*, bool isBlock = false); + String takeResults(); + +private: + virtual void appendText(Vector<UChar>& out, Text*); + String renderedText(const Node*, const Range*); + String stringValueForRange(const Node*, const Range*); + void removeExteriorStyles(CSSMutableStyleDeclaration*); + void appendElement(Vector<UChar>& out, Element* element, bool addDisplayInline, RangeFullySelectsNode); + void appendElement(Vector<UChar>& out, Element* element, Namespaces*) { appendElement(out, element, false, DoesFullySelectNode); } + + bool shouldAnnotate() { return m_shouldAnnotate == AnnotateForInterchange; } + + Vector<String> m_reversedPrecedingMarkup; + const EAnnotateForInterchange m_shouldAnnotate; +}; + +void StyledMarkupAccumulator::wrapWithNode(Node* node, bool convertBlocksToInlines, RangeFullySelectsNode rangeFullySelectsNode) +{ + Vector<UChar> markup; + if (node->isElementNode()) + appendElement(markup, static_cast<Element*>(node), convertBlocksToInlines && isBlock(const_cast<Node*>(node)), rangeFullySelectsNode); + else + appendStartMarkup(markup, node, 0); + m_reversedPrecedingMarkup.append(String::adopt(markup)); + appendEndTag(node); + if (m_nodes) + m_nodes->append(node); +} + +void StyledMarkupAccumulator::wrapWithStyleNode(CSSStyleDeclaration* style, Document* document, bool isBlock) +{ + // All text-decoration-related elements should have been treated as special ancestors + // If we ever hit this ASSERT, we should export StyleChange in ApplyStyleCommand and use it here + ASSERT(propertyMissingOrEqualToNone(style, CSSPropertyTextDecoration) && propertyMissingOrEqualToNone(style, CSSPropertyWebkitTextDecorationsInEffect)); + DEFINE_STATIC_LOCAL(const String, divStyle, ("<div style=\"")); + DEFINE_STATIC_LOCAL(const String, divClose, ("</div>")); + DEFINE_STATIC_LOCAL(const String, styleSpanOpen, ("<span class=\"" AppleStyleSpanClass "\" style=\"")); + DEFINE_STATIC_LOCAL(const String, styleSpanClose, ("</span>")); + Vector<UChar> openTag; + append(openTag, isBlock ? divStyle : styleSpanOpen); + appendAttributeValue(openTag, style->cssText(), document->isHTMLDocument()); + openTag.append('\"'); + openTag.append('>'); + m_reversedPrecedingMarkup.append(String::adopt(openTag)); + appendString(isBlock ? divClose : styleSpanClose); +} + +String StyledMarkupAccumulator::takeResults() +{ + Vector<UChar> result; + result.reserveInitialCapacity(totalLength(m_reversedPrecedingMarkup) + length()); + + for (size_t i = m_reversedPrecedingMarkup.size(); i > 0; --i) + append(result, m_reversedPrecedingMarkup[i - 1]); + + concatenateMarkup(result); + + return String::adopt(result); +} + +void StyledMarkupAccumulator::appendText(Vector<UChar>& out, Text* text) +{ + if (!shouldAnnotate() || (text->parentElement() && text->parentElement()->tagQName() == textareaTag)) { + MarkupAccumulator::appendText(out, text); + return; + } + + bool useRenderedText = !enclosingNodeWithTag(Position(text, 0), selectTag); + String content = useRenderedText ? renderedText(text, m_range) : stringValueForRange(text, m_range); + Vector<UChar> buffer; + appendCharactersReplacingEntities(buffer, content.characters(), content.length(), EntityMaskInPCDATA); + append(out, convertHTMLTextToInterchangeFormat(String::adopt(buffer), text)); +} + +String StyledMarkupAccumulator::renderedText(const Node* node, const Range* range) +{ + if (!node->isTextNode()) + return String(); + + ExceptionCode ec; + const Text* textNode = static_cast<const Text*>(node); + unsigned startOffset = 0; + unsigned endOffset = textNode->length(); + + if (range && node == range->startContainer(ec)) + startOffset = range->startOffset(ec); + if (range && node == range->endContainer(ec)) + endOffset = range->endOffset(ec); + + Position start(const_cast<Node*>(node), startOffset); + Position end(const_cast<Node*>(node), endOffset); + return plainText(Range::create(node->document(), start, end).get()); +} + +String StyledMarkupAccumulator::stringValueForRange(const Node* node, const Range* range) +{ + if (!range) + return node->nodeValue(); + + String str = node->nodeValue(); + ExceptionCode ec; + if (node == range->endContainer(ec)) + str.truncate(range->endOffset(ec)); + if (node == range->startContainer(ec)) + str.remove(0, range->startOffset(ec)); + return str; +} + +static PassRefPtr<CSSMutableStyleDeclaration> styleFromMatchedRulesForElement(Element* element, bool authorOnly = true) +{ + RefPtr<CSSMutableStyleDeclaration> style = CSSMutableStyleDeclaration::create(); + RefPtr<CSSRuleList> matchedRules = element->document()->styleSelector()->styleRulesForElement(element, authorOnly); + if (matchedRules) { + for (unsigned i = 0; i < matchedRules->length(); i++) { + if (matchedRules->item(i)->type() == CSSRule::STYLE_RULE) { + RefPtr<CSSMutableStyleDeclaration> s = static_cast<CSSStyleRule*>(matchedRules->item(i))->style(); + style->merge(s.get(), true); + } + } + } + + return style.release(); +} + +void StyledMarkupAccumulator::appendElement(Vector<UChar>& out, Element* element, bool addDisplayInline, RangeFullySelectsNode rangeFullySelectsNode) +{ + bool documentIsHTML = element->document()->isHTMLDocument(); + appendOpenTag(out, element, 0); + + NamedNodeMap* attributes = element->attributes(); + unsigned length = attributes->length(); + for (unsigned int i = 0; i < length; i++) { + Attribute* attribute = attributes->attributeItem(i); + // We'll handle the style attribute separately, below. + if (attribute->name() == styleAttr && element->isHTMLElement() && (shouldAnnotate() || addDisplayInline)) + continue; + appendAttribute(out, element, *attribute, 0); + } + + if (element->isHTMLElement() && (shouldAnnotate() || addDisplayInline)) { + RefPtr<CSSMutableStyleDeclaration> style = static_cast<HTMLElement*>(element)->getInlineStyleDecl()->copy(); + if (shouldAnnotate()) { + RefPtr<CSSMutableStyleDeclaration> styleFromMatchedRules = styleFromMatchedRulesForElement(const_cast<Element*>(element)); + // Styles from the inline style declaration, held in the variable "style", take precedence + // over those from matched rules. + styleFromMatchedRules->merge(style.get()); + style = styleFromMatchedRules; + + RefPtr<CSSComputedStyleDeclaration> computedStyleForElement = computedStyle(element); + RefPtr<CSSMutableStyleDeclaration> fromComputedStyle = CSSMutableStyleDeclaration::create(); + + { + CSSMutableStyleDeclaration::const_iterator end = style->end(); + for (CSSMutableStyleDeclaration::const_iterator it = style->begin(); it != end; ++it) { + const CSSProperty& property = *it; + CSSValue* value = property.value(); + // The property value, if it's a percentage, may not reflect the actual computed value. + // For example: style="height: 1%; overflow: visible;" in quirksmode + // FIXME: There are others like this, see <rdar://problem/5195123> Slashdot copy/paste fidelity problem + if (value->cssValueType() == CSSValue::CSS_PRIMITIVE_VALUE) + if (static_cast<CSSPrimitiveValue*>(value)->primitiveType() == CSSPrimitiveValue::CSS_PERCENTAGE) + if (RefPtr<CSSValue> computedPropertyValue = computedStyleForElement->getPropertyCSSValue(property.id())) + fromComputedStyle->addParsedProperty(CSSProperty(property.id(), computedPropertyValue)); + } + } + style->merge(fromComputedStyle.get()); + } + if (addDisplayInline) + style->setProperty(CSSPropertyDisplay, CSSValueInline, true); + // If the node is not fully selected by the range, then we don't want to keep styles that affect its relationship to the nodes around it + // only the ones that affect it and the nodes within it. + if (rangeFullySelectsNode == DoesNotFullySelectNode) + removeExteriorStyles(style.get()); + if (style->length() > 0) { + DEFINE_STATIC_LOCAL(const String, stylePrefix, (" style=\"")); + append(out, stylePrefix); + appendAttributeValue(out, style->cssText(), documentIsHTML); + out.append('\"'); + } + } + + appendCloseTag(out, element); +} + +void StyledMarkupAccumulator::removeExteriorStyles(CSSMutableStyleDeclaration* style) +{ + style->removeProperty(CSSPropertyFloat); +} + +Node* StyledMarkupAccumulator::serializeNodes(Node* startNode, Node* pastEnd) +{ + Vector<Node*> ancestorsToClose; + Node* next; + Node* lastClosed = 0; + for (Node* n = startNode; n != pastEnd; n = next) { + // According to <rdar://problem/5730668>, it is possible for n to blow + // past pastEnd and become null here. This shouldn't be possible. + // This null check will prevent crashes (but create too much markup) + // and the ASSERT will hopefully lead us to understanding the problem. + ASSERT(n); + if (!n) + break; + + next = n->traverseNextNode(); + bool openedTag = false; + + if (isBlock(n) && canHaveChildrenForEditing(n) && next == pastEnd) + // Don't write out empty block containers that aren't fully selected. + continue; + + if (!n->renderer() && !enclosingNodeWithTag(Position(n, 0), selectTag)) { + next = n->traverseNextSibling(); + // Don't skip over pastEnd. + if (pastEnd && pastEnd->isDescendantOf(n)) + next = pastEnd; + } else { + // Add the node to the markup if we're not skipping the descendants + appendStartTag(n); + + // If node has no children, close the tag now. + if (!n->childNodeCount()) { + appendEndTag(n); + lastClosed = n; + } else { + openedTag = true; + ancestorsToClose.append(n); + } + } + + // If we didn't insert open tag and there's no more siblings or we're at the end of the traversal, take care of ancestors. + // FIXME: What happens if we just inserted open tag and reached the end? + if (!openedTag && (!n->nextSibling() || next == pastEnd)) { + // Close up the ancestors. + while (!ancestorsToClose.isEmpty()) { + Node* ancestor = ancestorsToClose.last(); + if (next != pastEnd && next->isDescendantOf(ancestor)) + break; + // Not at the end of the range, close ancestors up to sibling of next node. + appendEndTag(ancestor); + lastClosed = ancestor; + ancestorsToClose.removeLast(); + } + + // Surround the currently accumulated markup with markup for ancestors we never opened as we leave the subtree(s) rooted at those ancestors. + ContainerNode* nextParent = next ? next->parentNode() : 0; + if (next != pastEnd && n != nextParent) { + Node* lastAncestorClosedOrSelf = n->isDescendantOf(lastClosed) ? lastClosed : n; + for (ContainerNode* parent = lastAncestorClosedOrSelf->parentNode(); parent && parent != nextParent; parent = parent->parentNode()) { + // All ancestors that aren't in the ancestorsToClose list should either be a) unrendered: + if (!parent->renderer()) + continue; + // or b) ancestors that we never encountered during a pre-order traversal starting at startNode: + ASSERT(startNode->isDescendantOf(parent)); + wrapWithNode(parent); + lastClosed = parent; + } + } + } + } + + return lastClosed; +} + +static Node* ancestorToRetainStructureAndAppearance(Node* commonAncestor) +{ + Node* commonAncestorBlock = enclosingBlock(commonAncestor); + + if (!commonAncestorBlock) + return 0; + + if (commonAncestorBlock->hasTagName(tbodyTag) || commonAncestorBlock->hasTagName(trTag)) { + ContainerNode* table = commonAncestorBlock->parentNode(); + while (table && !table->hasTagName(tableTag)) + table = table->parentNode(); + + return table; + } + + if (commonAncestorBlock->hasTagName(listingTag) + || commonAncestorBlock->hasTagName(olTag) + || commonAncestorBlock->hasTagName(preTag) + || commonAncestorBlock->hasTagName(tableTag) + || commonAncestorBlock->hasTagName(ulTag) + || commonAncestorBlock->hasTagName(xmpTag) + || commonAncestorBlock->hasTagName(h1Tag) + || commonAncestorBlock->hasTagName(h2Tag) + || commonAncestorBlock->hasTagName(h3Tag) + || commonAncestorBlock->hasTagName(h4Tag) + || commonAncestorBlock->hasTagName(h5Tag)) + return commonAncestorBlock; + + return 0; +} + +static bool propertyMissingOrEqualToNone(CSSStyleDeclaration* style, int propertyID) +{ + if (!style) + return false; + RefPtr<CSSValue> value = style->getPropertyCSSValue(propertyID); + if (!value) + return true; + if (!value->isPrimitiveValue()) + return false; + return static_cast<CSSPrimitiveValue*>(value.get())->getIdent() == CSSValueNone; +} + +static bool needInterchangeNewlineAfter(const VisiblePosition& v) +{ + VisiblePosition next = v.next(); + Node* upstreamNode = next.deepEquivalent().upstream().node(); + Node* downstreamNode = v.deepEquivalent().downstream().node(); + // Add an interchange newline if a paragraph break is selected and a br won't already be added to the markup to represent it. + return isEndOfParagraph(v) && isStartOfParagraph(next) && !(upstreamNode->hasTagName(brTag) && upstreamNode == downstreamNode); +} + +static PassRefPtr<CSSMutableStyleDeclaration> styleFromMatchedRulesAndInlineDecl(const Node* node) +{ + if (!node->isHTMLElement()) + return 0; + + // FIXME: Having to const_cast here is ugly, but it is quite a bit of work to untangle + // the non-const-ness of styleFromMatchedRulesForElement. + HTMLElement* element = const_cast<HTMLElement*>(static_cast<const HTMLElement*>(node)); + RefPtr<CSSMutableStyleDeclaration> style = styleFromMatchedRulesForElement(element); + RefPtr<CSSMutableStyleDeclaration> inlineStyleDecl = element->getInlineStyleDecl(); + style->merge(inlineStyleDecl.get()); + return style.release(); +} + +static bool isElementPresentational(const Node* node) +{ + if (node->hasTagName(uTag) || node->hasTagName(sTag) || node->hasTagName(strikeTag) + || node->hasTagName(iTag) || node->hasTagName(emTag) || node->hasTagName(bTag) || node->hasTagName(strongTag)) + return true; + RefPtr<CSSMutableStyleDeclaration> style = styleFromMatchedRulesAndInlineDecl(node); + if (!style) + return false; + return !propertyMissingOrEqualToNone(style.get(), CSSPropertyTextDecoration); +} + +static bool shouldIncludeWrapperForFullySelectedRoot(Node* fullySelectedRoot, CSSMutableStyleDeclaration* style) +{ + if (fullySelectedRoot->isElementNode() && static_cast<Element*>(fullySelectedRoot)->hasAttribute(backgroundAttr)) + return true; + + return style->getPropertyCSSValue(CSSPropertyBackgroundImage) || style->getPropertyCSSValue(CSSPropertyBackgroundColor); +} + +static Node* highestAncestorToWrapMarkup(const Range* range, Node* fullySelectedRoot, EAnnotateForInterchange shouldAnnotate) +{ + ExceptionCode ec; + Node* commonAncestor = range->commonAncestorContainer(ec); + ASSERT(commonAncestor); + Node* specialCommonAncestor = 0; + if (shouldAnnotate == AnnotateForInterchange) { + // Include ancestors that aren't completely inside the range but are required to retain + // the structure and appearance of the copied markup. + specialCommonAncestor = ancestorToRetainStructureAndAppearance(commonAncestor); + + // Retain the Mail quote level by including all ancestor mail block quotes. + for (Node* ancestor = range->firstNode(); ancestor; ancestor = ancestor->parentNode()) { + if (isMailBlockquote(ancestor)) + specialCommonAncestor = ancestor; + } + } + + Node* checkAncestor = specialCommonAncestor ? specialCommonAncestor : commonAncestor; + if (checkAncestor->renderer()) { + Node* newSpecialCommonAncestor = highestEnclosingNodeOfType(Position(checkAncestor, 0), &isElementPresentational); + if (newSpecialCommonAncestor) + specialCommonAncestor = newSpecialCommonAncestor; + } + + // If a single tab is selected, commonAncestor will be a text node inside a tab span. + // If two or more tabs are selected, commonAncestor will be the tab span. + // In either case, if there is a specialCommonAncestor already, it will necessarily be above + // any tab span that needs to be included. + if (!specialCommonAncestor && isTabSpanTextNode(commonAncestor)) + specialCommonAncestor = commonAncestor->parentNode(); + if (!specialCommonAncestor && isTabSpanNode(commonAncestor)) + specialCommonAncestor = commonAncestor; + + if (Node *enclosingAnchor = enclosingNodeWithTag(Position(specialCommonAncestor ? specialCommonAncestor : commonAncestor, 0), aTag)) + specialCommonAncestor = enclosingAnchor; + + if (shouldAnnotate == AnnotateForInterchange && fullySelectedRoot) { + RefPtr<CSSMutableStyleDeclaration> fullySelectedRootStyle = styleFromMatchedRulesAndInlineDecl(fullySelectedRoot); + if (shouldIncludeWrapperForFullySelectedRoot(fullySelectedRoot, fullySelectedRootStyle.get())) + specialCommonAncestor = fullySelectedRoot; + } + return specialCommonAncestor; +} + +// FIXME: Shouldn't we omit style info when annotate == DoNotAnnotateForInterchange? +// FIXME: At least, annotation and style info should probably not be included in range.markupString() +String createMarkup(const Range* range, Vector<Node*>* nodes, EAnnotateForInterchange shouldAnnotate, bool convertBlocksToInlines, EAbsoluteURLs shouldResolveURLs) +{ + DEFINE_STATIC_LOCAL(const String, interchangeNewlineString, ("<br class=\"" AppleInterchangeNewline "\">")); + + if (!range) + return ""; + + Document* document = range->ownerDocument(); + if (!document) + return ""; + + // Disable the delete button so it's elements are not serialized into the markup, + // but make sure neither endpoint is inside the delete user interface. + Frame* frame = document->frame(); + DeleteButtonController* deleteButton = frame ? frame->editor()->deleteButtonController() : 0; + RefPtr<Range> updatedRange = avoidIntersectionWithNode(range, deleteButton ? deleteButton->containerElement() : 0); + if (!updatedRange) + return ""; + + if (deleteButton) + deleteButton->disable(); + + ExceptionCode ec = 0; + bool collapsed = updatedRange->collapsed(ec); + ASSERT(!ec); + if (collapsed) + return ""; + Node* commonAncestor = updatedRange->commonAncestorContainer(ec); + ASSERT(!ec); + if (!commonAncestor) + return ""; + + document->updateLayoutIgnorePendingStylesheets(); + + StyledMarkupAccumulator accumulator(nodes, shouldResolveURLs, shouldAnnotate, updatedRange.get()); + Node* pastEnd = updatedRange->pastLastNode(); + + Node* startNode = updatedRange->firstNode(); + VisiblePosition visibleStart(updatedRange->startPosition(), VP_DEFAULT_AFFINITY); + VisiblePosition visibleEnd(updatedRange->endPosition(), VP_DEFAULT_AFFINITY); + if (shouldAnnotate == AnnotateForInterchange && needInterchangeNewlineAfter(visibleStart)) { + if (visibleStart == visibleEnd.previous()) { + if (deleteButton) + deleteButton->enable(); + return interchangeNewlineString; + } + + accumulator.appendString(interchangeNewlineString); + startNode = visibleStart.next().deepEquivalent().node(); + + if (pastEnd && Range::compareBoundaryPoints(startNode, 0, pastEnd, 0) >= 0) { + if (deleteButton) + deleteButton->enable(); + return interchangeNewlineString; + } + } + + Node* body = enclosingNodeWithTag(Position(commonAncestor, 0), bodyTag); + Node* fullySelectedRoot = 0; + // FIXME: Do this for all fully selected blocks, not just the body. + if (body && areRangesEqual(VisibleSelection::selectionFromContentsOfNode(body).toNormalizedRange().get(), range)) + fullySelectedRoot = body; + + Node* specialCommonAncestor = highestAncestorToWrapMarkup(updatedRange.get(), fullySelectedRoot, shouldAnnotate); + + Node* lastClosed = accumulator.serializeNodes(startNode, pastEnd); + + if (specialCommonAncestor && lastClosed) { + // Also include all of the ancestors of lastClosed up to this special ancestor. + for (ContainerNode* ancestor = lastClosed->parentNode(); ancestor; ancestor = ancestor->parentNode()) { + if (ancestor == fullySelectedRoot && !convertBlocksToInlines) { + RefPtr<CSSMutableStyleDeclaration> fullySelectedRootStyle = styleFromMatchedRulesAndInlineDecl(fullySelectedRoot); + + // Bring the background attribute over, but not as an attribute because a background attribute on a div + // appears to have no effect. + if (!fullySelectedRootStyle->getPropertyCSSValue(CSSPropertyBackgroundImage) && static_cast<Element*>(fullySelectedRoot)->hasAttribute(backgroundAttr)) + fullySelectedRootStyle->setProperty(CSSPropertyBackgroundImage, "url('" + static_cast<Element*>(fullySelectedRoot)->getAttribute(backgroundAttr) + "')"); + + if (fullySelectedRootStyle->length()) { + // Reset the CSS properties to avoid an assertion error in addStyleMarkup(). + // This assertion is caused at least when we select all text of a <body> element whose + // 'text-decoration' property is "inherit", and copy it. + if (!propertyMissingOrEqualToNone(fullySelectedRootStyle.get(), CSSPropertyTextDecoration)) + fullySelectedRootStyle->setProperty(CSSPropertyTextDecoration, CSSValueNone); + if (!propertyMissingOrEqualToNone(fullySelectedRootStyle.get(), CSSPropertyWebkitTextDecorationsInEffect)) + fullySelectedRootStyle->setProperty(CSSPropertyWebkitTextDecorationsInEffect, CSSValueNone); + accumulator.wrapWithStyleNode(fullySelectedRootStyle.get(), document, true); + } + } else { + // Since this node and all the other ancestors are not in the selection we want to set RangeFullySelectsNode to DoesNotFullySelectNode + // so that styles that affect the exterior of the node are not included. + accumulator.wrapWithNode(ancestor, convertBlocksToInlines, StyledMarkupAccumulator::DoesNotFullySelectNode); + } + if (nodes) + nodes->append(ancestor); + + lastClosed = ancestor; + + if (ancestor == specialCommonAncestor) + break; + } + } + + // Add a wrapper span with the styles that all of the nodes in the markup inherit. + ContainerNode* parentOfLastClosed = lastClosed ? lastClosed->parentNode() : 0; + if (parentOfLastClosed && parentOfLastClosed->renderer()) { + RefPtr<EditingStyle> style = EditingStyle::create(parentOfLastClosed); + + // Styles that Mail blockquotes contribute should only be placed on the Mail blockquote, to help + // us differentiate those styles from ones that the user has applied. This helps us + // get the color of content pasted into blockquotes right. + style->removeStyleAddedByNode(nearestMailBlockquote(parentOfLastClosed)); + + // Document default styles will be added on another wrapper span. + if (document && document->documentElement()) + style->prepareToApplyAt(firstPositionInNode(document->documentElement())); + + // Since we are converting blocks to inlines, remove any inherited block properties that are in the style. + // This cuts out meaningless properties and prevents properties from magically affecting blocks later + // if the style is cloned for a new block element during a future editing operation. + if (convertBlocksToInlines) + style->removeBlockProperties(); + + if (!style->isEmpty()) + accumulator.wrapWithStyleNode(style->style(), document); + } + + if (lastClosed && lastClosed != document->documentElement()) { + // Add a style span with the document's default styles. We add these in a separate + // span so that at paste time we can differentiate between document defaults and user + // applied styles. + RefPtr<EditingStyle> defaultStyle = EditingStyle::create(document->documentElement()); + if (!defaultStyle->isEmpty()) + accumulator.wrapWithStyleNode(defaultStyle->style(), document); + } + + // FIXME: The interchange newline should be placed in the block that it's in, not after all of the content, unconditionally. + if (shouldAnnotate == AnnotateForInterchange && needInterchangeNewlineAfter(visibleEnd.previous())) + accumulator.appendString(interchangeNewlineString); + + if (deleteButton) + deleteButton->enable(); + + return accumulator.takeResults(); +} + +PassRefPtr<DocumentFragment> createFragmentFromMarkup(Document* document, const String& markup, const String& baseURL, FragmentScriptingPermission scriptingPermission) +{ + // We use a fake body element here to trick the HTML parser to using the + // InBody insertion mode. Really, all this code is wrong and need to be + // changed not to use deprecatedCreateContextualFragment. + RefPtr<HTMLBodyElement> fakeBody = HTMLBodyElement::create(document); + // FIXME: This should not use deprecatedCreateContextualFragment + RefPtr<DocumentFragment> fragment = fakeBody->deprecatedCreateContextualFragment(markup, scriptingPermission); + + if (fragment && !baseURL.isEmpty() && baseURL != blankURL() && baseURL != document->baseURL()) + completeURLs(fragment.get(), baseURL); + + return fragment.release(); +} + +String createMarkup(const Node* node, EChildrenOnly childrenOnly, Vector<Node*>* nodes, EAbsoluteURLs shouldResolveURLs) +{ + if (!node) + return ""; + + HTMLElement* deleteButtonContainerElement = 0; + if (Frame* frame = node->document()->frame()) { + deleteButtonContainerElement = frame->editor()->deleteButtonController()->containerElement(); + if (node->isDescendantOf(deleteButtonContainerElement)) + return ""; + } + + MarkupAccumulator accumulator(nodes, shouldResolveURLs); + return accumulator.serializeNodes(const_cast<Node*>(node), deleteButtonContainerElement, childrenOnly); +} + +static void fillContainerFromString(ContainerNode* paragraph, const String& string) +{ + Document* document = paragraph->document(); + + ExceptionCode ec = 0; + if (string.isEmpty()) { + paragraph->appendChild(createBlockPlaceholderElement(document), ec); + ASSERT(!ec); + return; + } + + ASSERT(string.find('\n') == notFound); + + Vector<String> tabList; + string.split('\t', true, tabList); + String tabText = ""; + bool first = true; + size_t numEntries = tabList.size(); + for (size_t i = 0; i < numEntries; ++i) { + const String& s = tabList[i]; + + // append the non-tab textual part + if (!s.isEmpty()) { + if (!tabText.isEmpty()) { + paragraph->appendChild(createTabSpanElement(document, tabText), ec); + ASSERT(!ec); + tabText = ""; + } + RefPtr<Node> textNode = document->createTextNode(stringWithRebalancedWhitespace(s, first, i + 1 == numEntries)); + paragraph->appendChild(textNode.release(), ec); + ASSERT(!ec); + } + + // there is a tab after every entry, except the last entry + // (if the last character is a tab, the list gets an extra empty entry) + if (i + 1 != numEntries) + tabText.append('\t'); + else if (!tabText.isEmpty()) { + paragraph->appendChild(createTabSpanElement(document, tabText), ec); + ASSERT(!ec); + } + + first = false; + } +} + +bool isPlainTextMarkup(Node *node) +{ + if (!node->isElementNode() || !node->hasTagName(divTag) || static_cast<Element*>(node)->attributes()->length()) + return false; + + if (node->childNodeCount() == 1 && (node->firstChild()->isTextNode() || (node->firstChild()->firstChild()))) + return true; + + return (node->childNodeCount() == 2 && isTabSpanTextNode(node->firstChild()->firstChild()) && node->firstChild()->nextSibling()->isTextNode()); +} + +PassRefPtr<DocumentFragment> createFragmentFromText(Range* context, const String& text) +{ + if (!context) + return 0; + + Node* styleNode = context->firstNode(); + if (!styleNode) { + styleNode = context->startPosition().node(); + if (!styleNode) + return 0; + } + + Document* document = styleNode->document(); + RefPtr<DocumentFragment> fragment = document->createDocumentFragment(); + + if (text.isEmpty()) + return fragment.release(); + + String string = text; + string.replace("\r\n", "\n"); + string.replace('\r', '\n'); + + ExceptionCode ec = 0; + RenderObject* renderer = styleNode->renderer(); + if (renderer && renderer->style()->preserveNewline()) { + fragment->appendChild(document->createTextNode(string), ec); + ASSERT(!ec); + if (string.endsWith("\n")) { + RefPtr<Element> element = createBreakElement(document); + element->setAttribute(classAttr, AppleInterchangeNewline); + fragment->appendChild(element.release(), ec); + ASSERT(!ec); + } + return fragment.release(); + } + + // A string with no newlines gets added inline, rather than being put into a paragraph. + if (string.find('\n') == notFound) { + fillContainerFromString(fragment.get(), string); + return fragment.release(); + } + + // Break string into paragraphs. Extra line breaks turn into empty paragraphs. + Node* blockNode = enclosingBlock(context->firstNode()); + Element* block = static_cast<Element*>(blockNode); + bool useClonesOfEnclosingBlock = blockNode + && blockNode->isElementNode() + && !block->hasTagName(bodyTag) + && !block->hasTagName(htmlTag) + && block != editableRootForPosition(context->startPosition()); + + Vector<String> list; + string.split('\n', true, list); // true gets us empty strings in the list + size_t numLines = list.size(); + for (size_t i = 0; i < numLines; ++i) { + const String& s = list[i]; + + RefPtr<Element> element; + if (s.isEmpty() && i + 1 == numLines) { + // For last line, use the "magic BR" rather than a P. + element = createBreakElement(document); + element->setAttribute(classAttr, AppleInterchangeNewline); + } else { + if (useClonesOfEnclosingBlock) + element = block->cloneElementWithoutChildren(); + else + element = createDefaultParagraphElement(document); + fillContainerFromString(element.get(), s); + } + fragment->appendChild(element.release(), ec); + ASSERT(!ec); + } + return fragment.release(); +} + +PassRefPtr<DocumentFragment> createFragmentFromNodes(Document *document, const Vector<Node*>& nodes) +{ + if (!document) + return 0; + + // disable the delete button so it's elements are not serialized into the markup + if (document->frame()) + document->frame()->editor()->deleteButtonController()->disable(); + + RefPtr<DocumentFragment> fragment = document->createDocumentFragment(); + + ExceptionCode ec = 0; + size_t size = nodes.size(); + for (size_t i = 0; i < size; ++i) { + RefPtr<Element> element = createDefaultParagraphElement(document); + element->appendChild(nodes[i], ec); + ASSERT(!ec); + fragment->appendChild(element.release(), ec); + ASSERT(!ec); + } + + if (document->frame()) + document->frame()->editor()->deleteButtonController()->enable(); + + return fragment.release(); +} + +String createFullMarkup(const Node* node) +{ + if (!node) + return String(); + + Document* document = node->document(); + if (!document) + return String(); + + Frame* frame = document->frame(); + if (!frame) + return String(); + + // FIXME: This is never "for interchange". Is that right? + String markupString = createMarkup(node, IncludeNode, 0); + Node::NodeType nodeType = node->nodeType(); + if (nodeType != Node::DOCUMENT_NODE && nodeType != Node::DOCUMENT_TYPE_NODE) + markupString = frame->documentTypeString() + markupString; + + return markupString; +} + +String createFullMarkup(const Range* range) +{ + if (!range) + return String(); + + Node* node = range->startContainer(); + if (!node) + return String(); + + Document* document = node->document(); + if (!document) + return String(); + + Frame* frame = document->frame(); + if (!frame) + return String(); + + // FIXME: This is always "for interchange". Is that right? See the previous method. + return frame->documentTypeString() + createMarkup(range, 0, AnnotateForInterchange); +} + +String urlToMarkup(const KURL& url, const String& title) +{ + Vector<UChar> markup; + append(markup, "<a href=\""); + append(markup, url.string()); + append(markup, "\">"); + appendCharactersReplacingEntities(markup, title.characters(), title.length(), EntityMaskInPCDATA); + append(markup, "</a>"); + return String::adopt(markup); +} + +} diff --git a/Source/WebCore/editing/markup.h b/Source/WebCore/editing/markup.h new file mode 100644 index 0000000..dbf8b80 --- /dev/null +++ b/Source/WebCore/editing/markup.h @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2004 Apple Computer, 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef markup_h +#define markup_h + +#include "FragmentScriptingPermission.h" +#include "HTMLInterchange.h" +#include <wtf/Forward.h> +#include <wtf/Vector.h> + +namespace WebCore { + + class Document; + class DocumentFragment; + class KURL; + class Node; + class Range; + + enum EChildrenOnly { IncludeNode, ChildrenOnly }; + enum EAbsoluteURLs { DoNotResolveURLs, AbsoluteURLs }; + + PassRefPtr<DocumentFragment> createFragmentFromText(Range* context, const String& text); + PassRefPtr<DocumentFragment> createFragmentFromMarkup(Document*, const String& markup, const String& baseURL, FragmentScriptingPermission = FragmentScriptingAllowed); + PassRefPtr<DocumentFragment> createFragmentFromNodes(Document*, const Vector<Node*>&); + + bool isPlainTextMarkup(Node *node); + + String createMarkup(const Range*, + Vector<Node*>* = 0, EAnnotateForInterchange = DoNotAnnotateForInterchange, bool convertBlocksToInlines = false, EAbsoluteURLs = DoNotResolveURLs); + String createMarkup(const Node*, EChildrenOnly = IncludeNode, Vector<Node*>* = 0, EAbsoluteURLs = DoNotResolveURLs); + + String createFullMarkup(const Node*); + String createFullMarkup(const Range*); + + String urlToMarkup(const KURL&, const String& title); +} + +#endif // markup_h diff --git a/Source/WebCore/editing/qt/EditorQt.cpp b/Source/WebCore/editing/qt/EditorQt.cpp new file mode 100644 index 0000000..7fb3634 --- /dev/null +++ b/Source/WebCore/editing/qt/EditorQt.cpp @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2006 Zack Rusin <zack@kde.org> + * Copyright (C) 2006 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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 "Editor.h" + +#include "ClipboardAccessPolicy.h" +#include "ClipboardQt.h" +#include "Document.h" +#include "Element.h" +#include "VisibleSelection.h" +#include "SelectionController.h" +#include "TextIterator.h" +#include "htmlediting.h" +#include "visible_units.h" + +namespace WebCore { + +PassRefPtr<Clipboard> Editor::newGeneralClipboard(ClipboardAccessPolicy policy, Frame*) +{ + return ClipboardQt::create(policy); +} + +} // namespace WebCore diff --git a/Source/WebCore/editing/qt/SmartReplaceQt.cpp b/Source/WebCore/editing/qt/SmartReplaceQt.cpp new file mode 100644 index 0000000..1436afe --- /dev/null +++ b/Source/WebCore/editing/qt/SmartReplaceQt.cpp @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2010 Robert Hogan <robert@roberthogan.net>. All rights reserved. + * Copyright (C) 2007,2008,2009 Apple 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. + * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of + * its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS 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 APPLE OR ITS 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 "SmartReplace.h" + +namespace WebCore { + +bool isCharacterSmartReplaceExempt(UChar32 c, bool isPreviousCharacter) +{ + QChar d(c); + if (d.isSpace()) + return true; + if (!isPreviousCharacter && d.isPunct()) + return true; + + if ((c >= 0x1100 && c <= (0x1100 + 256)) // Hangul Jamo (0x1100 - 0x11FF) + || (c >= 0x2E80 && c <= (0x2E80 + 352)) // CJK & Kangxi Radicals (0x2E80 - 0x2FDF) + || (c >= 0x2FF0 && c <= (0x2FF0 + 464)) // Ideograph Deseriptions, CJK Symbols, Hiragana, Katakana, Bopomofo, Hangul Compatibility Jamo, Kanbun, & Bopomofo Ext (0x2FF0 - 0x31BF) + || (c >= 0x3200 && c <= (0x3200 + 29392)) // Enclosed CJK, CJK Ideographs (Uni Han & Ext A), & Yi (0x3200 - 0xA4CF) + || (c >= 0xAC00 && c <= (0xAC00 + 11183)) // Hangul Syllables (0xAC00 - 0xD7AF) + || (c >= 0xF900 && c <= (0xF900 + 352)) // CJK Compatibility Ideographs (0xF900 - 0xFA5F) + || (c >= 0xFE30 && c <= (0xFE30 + 32)) // CJK Compatibility From (0xFE30 - 0xFE4F) + || (c >= 0xFF00 && c <= (0xFF00 + 240)) // Half/Full Width Form (0xFF00 - 0xFFEF) + || (c >= 0x20000 && c <= (0x20000 + 0xA6D7)) // CJK Ideograph Exntension B + || (c >= 0x2F800 && c <= (0x2F800 + 0x021E))) // CJK Compatibility Ideographs (0x2F800 - 0x2FA1D) + return true; + + const char prev[] = "([\"\'#$/-`{\0"; + const char next[] = ")].,;:?\'!\"%*-/}\0"; + const char* str = (isPreviousCharacter) ? prev : next; + for (int i = 0; i < strlen(str); ++i) { + if (str[i] == c) + return true; + } + + return false; +} + +} diff --git a/Source/WebCore/editing/visible_units.cpp b/Source/WebCore/editing/visible_units.cpp new file mode 100644 index 0000000..7bb1515 --- /dev/null +++ b/Source/WebCore/editing/visible_units.cpp @@ -0,0 +1,1210 @@ +/* + * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009 Apple 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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 "visible_units.h" + +#include "Document.h" +#include "Element.h" +#include "HTMLNames.h" +#include "Position.h" +#include "RenderBlock.h" +#include "RenderLayer.h" +#include "RenderObject.h" +#include "TextBoundaries.h" +#include "TextBreakIterator.h" +#include "TextIterator.h" +#include "VisiblePosition.h" +#include "htmlediting.h" +#include <wtf/unicode/Unicode.h> + +namespace WebCore { + +using namespace HTMLNames; +using namespace WTF::Unicode; + +enum BoundarySearchContextAvailability { DontHaveMoreContext, MayHaveMoreContext }; + +typedef unsigned (*BoundarySearchFunction)(const UChar*, unsigned length, unsigned offset, BoundarySearchContextAvailability, bool& needMoreContext); + +static VisiblePosition previousBoundary(const VisiblePosition& c, BoundarySearchFunction searchFunction) +{ + Position pos = c.deepEquivalent(); + Node* boundary = pos.parentEditingBoundary(); + if (!boundary) + return VisiblePosition(); + + Document* d = boundary->document(); + Position start = rangeCompliantEquivalent(Position(boundary, 0)); + Position end = rangeCompliantEquivalent(pos); + RefPtr<Range> searchRange = Range::create(d); + + Vector<UChar, 1024> string; + unsigned suffixLength = 0; + + ExceptionCode ec = 0; + if (requiresContextForWordBoundary(c.characterBefore())) { + RefPtr<Range> forwardsScanRange(d->createRange()); + forwardsScanRange->setEndAfter(boundary, ec); + forwardsScanRange->setStart(end.node(), end.deprecatedEditingOffset(), ec); + TextIterator forwardsIterator(forwardsScanRange.get()); + while (!forwardsIterator.atEnd()) { + const UChar* characters = forwardsIterator.characters(); + int length = forwardsIterator.length(); + int i = endOfFirstWordBoundaryContext(characters, length); + string.append(characters, i); + suffixLength += i; + if (i < length) + break; + forwardsIterator.advance(); + } + } + + searchRange->setStart(start.node(), start.deprecatedEditingOffset(), ec); + searchRange->setEnd(end.node(), end.deprecatedEditingOffset(), ec); + + ASSERT(!ec); + if (ec) + return VisiblePosition(); + + SimplifiedBackwardsTextIterator it(searchRange.get(), TextIteratorEndsAtEditingBoundary); + unsigned next = 0; + bool inTextSecurityMode = start.node() && start.node()->renderer() && start.node()->renderer()->style()->textSecurity() != TSNONE; + bool needMoreContext = false; + while (!it.atEnd()) { + // iterate to get chunks until the searchFunction returns a non-zero value. + if (!inTextSecurityMode) + string.prepend(it.characters(), it.length()); + else { + // Treat bullets used in the text security mode as regular characters when looking for boundaries + String iteratorString(it.characters(), it.length()); + iteratorString = iteratorString.impl()->secure('x'); + string.prepend(iteratorString.characters(), iteratorString.length()); + } + next = searchFunction(string.data(), string.size(), string.size() - suffixLength, MayHaveMoreContext, needMoreContext); + if (next != 0) + break; + it.advance(); + } + if (needMoreContext) { + // The last search returned the beginning of the buffer and asked for more context, + // but there is no earlier text. Force a search with what's available. + next = searchFunction(string.data(), string.size(), string.size() - suffixLength, DontHaveMoreContext, needMoreContext); + ASSERT(!needMoreContext); + } + + if (it.atEnd() && next == 0) { + pos = it.range()->startPosition(); + } else if (next != 0) { + Node *node = it.range()->startContainer(ec); + if ((node->isTextNode() && static_cast<int>(next) <= node->maxCharacterOffset()) || (node->renderer() && node->renderer()->isBR() && !next)) + // The next variable contains a usable index into a text node + pos = Position(node, next); + else { + // Use the character iterator to translate the next value into a DOM position. + BackwardsCharacterIterator charIt(searchRange.get(), TextIteratorEndsAtEditingBoundary); + charIt.advance(string.size() - suffixLength - next); + pos = charIt.range()->endPosition(); + } + } + + return VisiblePosition(pos, DOWNSTREAM); +} + +static VisiblePosition nextBoundary(const VisiblePosition& c, BoundarySearchFunction searchFunction) +{ + Position pos = c.deepEquivalent(); + Node* boundary = pos.parentEditingBoundary(); + if (!boundary) + return VisiblePosition(); + + Document* d = boundary->document(); + RefPtr<Range> searchRange(d->createRange()); + Position start(rangeCompliantEquivalent(pos)); + + Vector<UChar, 1024> string; + unsigned prefixLength = 0; + + ExceptionCode ec = 0; + if (requiresContextForWordBoundary(c.characterAfter())) { + RefPtr<Range> backwardsScanRange(d->createRange()); + backwardsScanRange->setEnd(start.node(), start.deprecatedEditingOffset(), ec); + SimplifiedBackwardsTextIterator backwardsIterator(backwardsScanRange.get()); + while (!backwardsIterator.atEnd()) { + const UChar* characters = backwardsIterator.characters(); + int length = backwardsIterator.length(); + int i = startOfLastWordBoundaryContext(characters, length); + string.prepend(characters + i, length - i); + prefixLength += length - i; + if (i > 0) + break; + backwardsIterator.advance(); + } + } + + searchRange->selectNodeContents(boundary, ec); + searchRange->setStart(start.node(), start.deprecatedEditingOffset(), ec); + TextIterator it(searchRange.get(), TextIteratorEmitsCharactersBetweenAllVisiblePositions); + unsigned next = 0; + bool inTextSecurityMode = start.node() && start.node()->renderer() && start.node()->renderer()->style()->textSecurity() != TSNONE; + bool needMoreContext = false; + while (!it.atEnd()) { + // Keep asking the iterator for chunks until the search function + // returns an end value not equal to the length of the string passed to it. + if (!inTextSecurityMode) + string.append(it.characters(), it.length()); + else { + // Treat bullets used in the text security mode as regular characters when looking for boundaries + String iteratorString(it.characters(), it.length()); + iteratorString = iteratorString.impl()->secure('x'); + string.append(iteratorString.characters(), iteratorString.length()); + } + next = searchFunction(string.data(), string.size(), prefixLength, MayHaveMoreContext, needMoreContext); + if (next != string.size()) + break; + it.advance(); + } + if (needMoreContext) { + // The last search returned the end of the buffer and asked for more context, + // but there is no further text. Force a search with what's available. + next = searchFunction(string.data(), string.size(), prefixLength, DontHaveMoreContext, needMoreContext); + ASSERT(!needMoreContext); + } + + if (it.atEnd() && next == string.size()) { + pos = it.range()->startPosition(); + } else if (next != prefixLength) { + // Use the character iterator to translate the next value into a DOM position. + CharacterIterator charIt(searchRange.get(), TextIteratorEmitsCharactersBetweenAllVisiblePositions); + charIt.advance(next - prefixLength - 1); + RefPtr<Range> characterRange = charIt.range(); + pos = characterRange->endPosition(); + + if (*charIt.characters() == '\n') { + // FIXME: workaround for collapsed range (where only start position is correct) emitted for some emitted newlines (see rdar://5192593) + VisiblePosition visPos = VisiblePosition(pos); + if (visPos == VisiblePosition(characterRange->startPosition())) { + charIt.advance(1); + pos = charIt.range()->startPosition(); + } + } + } + + // generate VisiblePosition, use UPSTREAM affinity if possible + return VisiblePosition(pos, VP_UPSTREAM_IF_POSSIBLE); +} + +static bool canHaveCursor(RenderObject* o) +{ + return (o->isText() && toRenderText(o)->linesBoundingBox().height()) + || (o->isBox() && toRenderBox(o)->borderBoundingBox().height()); +} + +// --------- + +static unsigned startWordBoundary(const UChar* characters, unsigned length, unsigned offset, BoundarySearchContextAvailability mayHaveMoreContext, bool& needMoreContext) +{ + ASSERT(offset); + if (mayHaveMoreContext && !startOfLastWordBoundaryContext(characters, offset)) { + needMoreContext = true; + return 0; + } + needMoreContext = false; + int start, end; + findWordBoundary(characters, length, offset - 1, &start, &end); + return start; +} + +VisiblePosition startOfWord(const VisiblePosition &c, EWordSide side) +{ + // FIXME: This returns a null VP for c at the start of the document + // and side == LeftWordIfOnBoundary + VisiblePosition p = c; + if (side == RightWordIfOnBoundary) { + // at paragraph end, the startofWord is the current position + if (isEndOfParagraph(c)) + return c; + + p = c.next(); + if (p.isNull()) + return c; + } + return previousBoundary(p, startWordBoundary); +} + +static unsigned endWordBoundary(const UChar* characters, unsigned length, unsigned offset, BoundarySearchContextAvailability mayHaveMoreContext, bool& needMoreContext) +{ + ASSERT(offset <= length); + if (mayHaveMoreContext && endOfFirstWordBoundaryContext(characters + offset, length - offset) == static_cast<int>(length - offset)) { + needMoreContext = true; + return length; + } + needMoreContext = false; + int start, end; + findWordBoundary(characters, length, offset, &start, &end); + return end; +} + +VisiblePosition endOfWord(const VisiblePosition &c, EWordSide side) +{ + VisiblePosition p = c; + if (side == LeftWordIfOnBoundary) { + if (isStartOfParagraph(c)) + return c; + + p = c.previous(); + if (p.isNull()) + return c; + } else if (isEndOfParagraph(c)) + return c; + + return nextBoundary(p, endWordBoundary); +} + +static unsigned previousWordPositionBoundary(const UChar* characters, unsigned length, unsigned offset, BoundarySearchContextAvailability mayHaveMoreContext, bool& needMoreContext) +{ + if (mayHaveMoreContext && !startOfLastWordBoundaryContext(characters, offset)) { + needMoreContext = true; + return 0; + } + needMoreContext = false; + return findNextWordFromIndex(characters, length, offset, false); +} + +VisiblePosition previousWordPosition(const VisiblePosition &c) +{ + VisiblePosition prev = previousBoundary(c, previousWordPositionBoundary); + return c.honorEditableBoundaryAtOrAfter(prev); +} + +static unsigned nextWordPositionBoundary(const UChar* characters, unsigned length, unsigned offset, BoundarySearchContextAvailability mayHaveMoreContext, bool& needMoreContext) +{ + if (mayHaveMoreContext && endOfFirstWordBoundaryContext(characters + offset, length - offset) == static_cast<int>(length - offset)) { + needMoreContext = true; + return length; + } + needMoreContext = false; + return findNextWordFromIndex(characters, length, offset, true); +} + +VisiblePosition nextWordPosition(const VisiblePosition &c) +{ + VisiblePosition next = nextBoundary(c, nextWordPositionBoundary); + return c.honorEditableBoundaryAtOrBefore(next); +} + +// --------- + +static RootInlineBox *rootBoxForLine(const VisiblePosition &c) +{ + Position p = c.deepEquivalent(); + Node *node = p.node(); + if (!node) + return 0; + + RenderObject *renderer = node->renderer(); + if (!renderer) + return 0; + + InlineBox* box; + int offset; + c.getInlineBoxAndOffset(box, offset); + + return box ? box->root() : 0; +} + +static VisiblePosition positionAvoidingFirstPositionInTable(const VisiblePosition& c) +{ + // return table offset 0 instead of the first VisiblePosition inside the table + VisiblePosition previous = c.previous(); + if (isLastPositionBeforeTable(previous) && isEditablePosition(previous.deepEquivalent())) + return previous; + + return c; +} + +static VisiblePosition startPositionForLine(const VisiblePosition& c) +{ + if (c.isNull()) + return VisiblePosition(); + + RootInlineBox *rootBox = rootBoxForLine(c); + if (!rootBox) { + // There are VisiblePositions at offset 0 in blocks without + // RootInlineBoxes, like empty editable blocks and bordered blocks. + Position p = c.deepEquivalent(); + if (p.node()->renderer() && p.node()->renderer()->isRenderBlock() && p.deprecatedEditingOffset() == 0) + return positionAvoidingFirstPositionInTable(c); + + return VisiblePosition(); + } + + // Generated content (e.g. list markers and CSS :before and :after + // pseudoelements) have no corresponding DOM element, and so cannot be + // represented by a VisiblePosition. Use whatever follows instead. + InlineBox *startBox = rootBox->firstLeafChild(); + Node *startNode; + while (1) { + if (!startBox) + return VisiblePosition(); + + RenderObject *startRenderer = startBox->renderer(); + if (!startRenderer) + return VisiblePosition(); + + startNode = startRenderer->node(); + if (startNode) + break; + + startBox = startBox->nextLeafChild(); + } + + int startOffset = 0; + if (startBox->isInlineTextBox()) { + InlineTextBox *startTextBox = static_cast<InlineTextBox *>(startBox); + startOffset = startTextBox->start(); + } + + VisiblePosition visPos = VisiblePosition(startNode, startOffset, DOWNSTREAM); + return positionAvoidingFirstPositionInTable(visPos); +} + +VisiblePosition startOfLine(const VisiblePosition& c) +{ + VisiblePosition visPos = startPositionForLine(c); + + return c.honorEditableBoundaryAtOrAfter(visPos); +} + +static VisiblePosition endPositionForLine(const VisiblePosition& c) +{ + if (c.isNull()) + return VisiblePosition(); + + RootInlineBox *rootBox = rootBoxForLine(c); + if (!rootBox) { + // There are VisiblePositions at offset 0 in blocks without + // RootInlineBoxes, like empty editable blocks and bordered blocks. + Position p = c.deepEquivalent(); + if (p.node()->renderer() && p.node()->renderer()->isRenderBlock() && p.deprecatedEditingOffset() == 0) + return c; + return VisiblePosition(); + } + + // Generated content (e.g. list markers and CSS :before and :after + // pseudoelements) have no corresponding DOM element, and so cannot be + // represented by a VisiblePosition. Use whatever precedes instead. + Node *endNode; + InlineBox *endBox = rootBox->lastLeafChild(); + while (1) { + if (!endBox) + return VisiblePosition(); + + RenderObject *endRenderer = endBox->renderer(); + if (!endRenderer) + return VisiblePosition(); + + endNode = endRenderer->node(); + if (endNode) + break; + + endBox = endBox->prevLeafChild(); + } + + int endOffset = 1; + if (endNode->hasTagName(brTag)) { + endOffset = 0; + } else if (endBox->isInlineTextBox()) { + InlineTextBox *endTextBox = static_cast<InlineTextBox *>(endBox); + endOffset = endTextBox->start(); + if (!endTextBox->isLineBreak()) + endOffset += endTextBox->len(); + } + + return VisiblePosition(endNode, endOffset, VP_UPSTREAM_IF_POSSIBLE); +} + +VisiblePosition endOfLine(const VisiblePosition& c) +{ + VisiblePosition visPos = endPositionForLine(c); + + // Make sure the end of line is at the same line as the given input position. Else use the previous position to + // obtain end of line. This condition happens when the input position is before the space character at the end + // of a soft-wrapped non-editable line. In this scenario, endPositionForLine would incorrectly hand back a position + // in the next line instead. This fix is to account for the discrepancy between lines with webkit-line-break:after-white-space style + // versus lines without that style, which would break before a space by default. + if (!inSameLine(c, visPos)) { + visPos = c.previous(); + if (visPos.isNull()) + return VisiblePosition(); + visPos = endPositionForLine(visPos); + } + + return c.honorEditableBoundaryAtOrBefore(visPos); +} + +bool inSameLine(const VisiblePosition &a, const VisiblePosition &b) +{ + return a.isNotNull() && startOfLine(a) == startOfLine(b); +} + +bool isStartOfLine(const VisiblePosition &p) +{ + return p.isNotNull() && p == startOfLine(p); +} + +bool isEndOfLine(const VisiblePosition &p) +{ + return p.isNotNull() && p == endOfLine(p); +} + +// The first leaf before node that has the same editability as node. +static Node* previousLeafWithSameEditability(Node* node) +{ + bool editable = node->isContentEditable(); + Node* n = node->previousLeafNode(); + while (n) { + if (editable == n->isContentEditable()) + return n; + n = n->previousLeafNode(); + } + return 0; +} + +static Node* enclosingNodeWithNonInlineRenderer(Node* n) +{ + for (Node* p = n; p; p = p->parentNode()) { + if (p->renderer() && !p->renderer()->isInline()) + return p; + } + return 0; +} + +VisiblePosition previousLinePosition(const VisiblePosition &visiblePosition, int x) +{ + Position p = visiblePosition.deepEquivalent(); + Node *node = p.node(); + Node* highestRoot = highestEditableRoot(p); + if (!node) + return VisiblePosition(); + + node->document()->updateLayoutIgnorePendingStylesheets(); + + RenderObject *renderer = node->renderer(); + if (!renderer) + return VisiblePosition(); + + RenderBlock *containingBlock = 0; + RootInlineBox *root = 0; + InlineBox* box; + int ignoredCaretOffset; + visiblePosition.getInlineBoxAndOffset(box, ignoredCaretOffset); + if (box) { + root = box->root()->prevRootBox(); + // We want to skip zero height boxes. + // This could happen in case it is a TrailingFloatsRootInlineBox. + if (root && root->logicalHeight()) + containingBlock = renderer->containingBlock(); + else + root = 0; + } + + if (!root) { + // This containing editable block does not have a previous line. + // Need to move back to previous containing editable block in this root editable + // block and find the last root line box in that block. + Node* startBlock = enclosingNodeWithNonInlineRenderer(node); + Node* n = previousLeafWithSameEditability(node); + while (n && startBlock == enclosingNodeWithNonInlineRenderer(n)) + n = previousLeafWithSameEditability(n); + while (n) { + if (highestEditableRoot(Position(n, 0)) != highestRoot) + break; + Position pos(n, caretMinOffset(n)); + if (pos.isCandidate()) { + RenderObject* o = n->renderer(); + ASSERT(o); + if (canHaveCursor(o)) { + Position maxPos(n, caretMaxOffset(n)); + maxPos.getInlineBoxAndOffset(DOWNSTREAM, box, ignoredCaretOffset); + if (box) { + // previous root line box found + root = box->root(); + containingBlock = n->renderer()->containingBlock(); + break; + } + + return VisiblePosition(pos, DOWNSTREAM); + } + } + n = previousLeafWithSameEditability(n); + } + } + + if (root) { + // FIXME: Can be wrong for multi-column layout and with transforms. + FloatPoint absPos = containingBlock->localToAbsolute(FloatPoint()); + if (containingBlock->hasOverflowClip()) + absPos -= containingBlock->layer()->scrolledContentOffset(); + RenderObject* renderer = root->closestLeafChildForLogicalLeftPosition(x - absPos.x(), isEditablePosition(p))->renderer(); + Node* node = renderer->node(); + if (node && editingIgnoresContent(node)) + return Position(node->parentNode(), node->nodeIndex()); + return renderer->positionForPoint(IntPoint(x - absPos.x(), root->lineTop())); + } + + // Could not find a previous line. This means we must already be on the first line. + // Move to the start of the content in this block, which effectively moves us + // to the start of the line we're on. + Element* rootElement = node->isContentEditable() ? node->rootEditableElement() : node->document()->documentElement(); + return VisiblePosition(rootElement, 0, DOWNSTREAM); +} + +static Node* nextLeafWithSameEditability(Node* node, int offset) +{ + bool editable = node->isContentEditable(); + ASSERT(offset >= 0); + Node* child = node->childNode(offset); + Node* n = child ? child->nextLeafNode() : node->nextLeafNode(); + while (n) { + if (editable == n->isContentEditable()) + return n; + n = n->nextLeafNode(); + } + return 0; +} + +static Node* nextLeafWithSameEditability(Node* node) +{ + if (!node) + return 0; + + bool editable = node->isContentEditable(); + Node* n = node->nextLeafNode(); + while (n) { + if (editable == n->isContentEditable()) + return n; + n = n->nextLeafNode(); + } + return 0; +} + +VisiblePosition nextLinePosition(const VisiblePosition &visiblePosition, int x) +{ + Position p = visiblePosition.deepEquivalent(); + Node *node = p.node(); + Node* highestRoot = highestEditableRoot(p); + if (!node) + return VisiblePosition(); + + node->document()->updateLayoutIgnorePendingStylesheets(); + + RenderObject *renderer = node->renderer(); + if (!renderer) + return VisiblePosition(); + + RenderBlock *containingBlock = 0; + RootInlineBox *root = 0; + InlineBox* box; + int ignoredCaretOffset; + visiblePosition.getInlineBoxAndOffset(box, ignoredCaretOffset); + if (box) { + root = box->root()->nextRootBox(); + // We want to skip zero height boxes. + // This could happen in case it is a TrailingFloatsRootInlineBox. + if (root && root->logicalHeight()) + containingBlock = renderer->containingBlock(); + else + root = 0; + } + + if (!root) { + // This containing editable block does not have a next line. + // Need to move forward to next containing editable block in this root editable + // block and find the first root line box in that block. + Node* startBlock = enclosingNodeWithNonInlineRenderer(node); + Node* n = nextLeafWithSameEditability(node, p.deprecatedEditingOffset()); + while (n && startBlock == enclosingNodeWithNonInlineRenderer(n)) + n = nextLeafWithSameEditability(n); + while (n) { + if (highestEditableRoot(Position(n, 0)) != highestRoot) + break; + Position pos(n, caretMinOffset(n)); + if (pos.isCandidate()) { + ASSERT(n->renderer()); + pos.getInlineBoxAndOffset(DOWNSTREAM, box, ignoredCaretOffset); + if (box) { + // next root line box found + root = box->root(); + containingBlock = n->renderer()->containingBlock(); + break; + } + + return VisiblePosition(pos, DOWNSTREAM); + } + n = nextLeafWithSameEditability(n); + } + } + + if (root) { + // FIXME: Can be wrong for multi-column layout and with transforms. + FloatPoint absPos = containingBlock->localToAbsolute(FloatPoint()); + if (containingBlock->hasOverflowClip()) + absPos -= containingBlock->layer()->scrolledContentOffset(); + RenderObject* renderer = root->closestLeafChildForLogicalLeftPosition(x - absPos.x(), isEditablePosition(p))->renderer(); + Node* node = renderer->node(); + if (node && editingIgnoresContent(node)) + return Position(node->parentNode(), node->nodeIndex()); + return renderer->positionForPoint(IntPoint(x - absPos.x(), root->lineTop())); + } + + // Could not find a next line. This means we must already be on the last line. + // Move to the end of the content in this block, which effectively moves us + // to the end of the line we're on. + Element* rootElement = node->isContentEditable() ? node->rootEditableElement() : node->document()->documentElement(); + return VisiblePosition(rootElement, rootElement ? rootElement->childNodeCount() : 0, DOWNSTREAM); +} + +// --------- + +static unsigned startSentenceBoundary(const UChar* characters, unsigned length, unsigned, BoundarySearchContextAvailability, bool&) +{ + TextBreakIterator* iterator = sentenceBreakIterator(characters, length); + // FIXME: The following function can return -1; we don't handle that. + return textBreakPreceding(iterator, length); +} + +VisiblePosition startOfSentence(const VisiblePosition &c) +{ + return previousBoundary(c, startSentenceBoundary); +} + +static unsigned endSentenceBoundary(const UChar* characters, unsigned length, unsigned, BoundarySearchContextAvailability, bool&) +{ + TextBreakIterator* iterator = sentenceBreakIterator(characters, length); + return textBreakNext(iterator); +} + +// FIXME: This includes the space after the punctuation that marks the end of the sentence. +VisiblePosition endOfSentence(const VisiblePosition &c) +{ + return nextBoundary(c, endSentenceBoundary); +} + +static unsigned previousSentencePositionBoundary(const UChar* characters, unsigned length, unsigned, BoundarySearchContextAvailability, bool&) +{ + // FIXME: This is identical to startSentenceBoundary. I'm pretty sure that's not right. + TextBreakIterator* iterator = sentenceBreakIterator(characters, length); + // FIXME: The following function can return -1; we don't handle that. + return textBreakPreceding(iterator, length); +} + +VisiblePosition previousSentencePosition(const VisiblePosition &c) +{ + VisiblePosition prev = previousBoundary(c, previousSentencePositionBoundary); + return c.honorEditableBoundaryAtOrAfter(prev); +} + +static unsigned nextSentencePositionBoundary(const UChar* characters, unsigned length, unsigned, BoundarySearchContextAvailability, bool&) +{ + // FIXME: This is identical to endSentenceBoundary. This isn't right, it needs to + // move to the equivlant position in the following sentence. + TextBreakIterator* iterator = sentenceBreakIterator(characters, length); + return textBreakFollowing(iterator, 0); +} + +VisiblePosition nextSentencePosition(const VisiblePosition &c) +{ + VisiblePosition next = nextBoundary(c, nextSentencePositionBoundary); + return c.honorEditableBoundaryAtOrBefore(next); +} + +VisiblePosition startOfParagraph(const VisiblePosition& c, EditingBoundaryCrossingRule boundaryCrossingRule) +{ + Position p = c.deepEquivalent(); + Node *startNode = p.node(); + + if (!startNode) + return VisiblePosition(); + + if (isRenderedAsNonInlineTableImageOrHR(startNode)) + return firstDeepEditingPositionForNode(startNode); + + Node* startBlock = enclosingBlock(startNode); + + Node *node = startNode; + int offset = p.deprecatedEditingOffset(); + + Node *n = startNode; + while (n) { + if (boundaryCrossingRule == CannotCrossEditingBoundary && n->isContentEditable() != startNode->isContentEditable()) + break; + RenderObject *r = n->renderer(); + if (!r) { + n = n->traversePreviousNodePostOrder(startBlock); + continue; + } + RenderStyle *style = r->style(); + if (style->visibility() != VISIBLE) { + n = n->traversePreviousNodePostOrder(startBlock); + continue; + } + + if (r->isBR() || isBlock(n)) + break; + + if (r->isText() && r->caretMaxRenderedOffset() > 0) { + if (style->preserveNewline()) { + const UChar* chars = toRenderText(r)->characters(); + int i = toRenderText(r)->textLength(); + int o = offset; + if (n == startNode && o < i) + i = max(0, o); + while (--i >= 0) + if (chars[i] == '\n') + return VisiblePosition(n, i + 1, DOWNSTREAM); + } + node = n; + offset = 0; + n = n->traversePreviousNodePostOrder(startBlock); + } else if (editingIgnoresContent(n) || isTableElement(n)) { + node = n; + offset = 0; + n = n->previousSibling() ? n->previousSibling() : n->traversePreviousNodePostOrder(startBlock); + } else + n = n->traversePreviousNodePostOrder(startBlock); + } + + return VisiblePosition(node, offset, DOWNSTREAM); +} + +VisiblePosition endOfParagraph(const VisiblePosition &c, EditingBoundaryCrossingRule boundaryCrossingRule) +{ + if (c.isNull()) + return VisiblePosition(); + + Position p = c.deepEquivalent(); + Node* startNode = p.node(); + + if (isRenderedAsNonInlineTableImageOrHR(startNode)) + return lastDeepEditingPositionForNode(startNode); + + Node* startBlock = enclosingBlock(startNode); + Node *stayInsideBlock = startBlock; + + Node *node = startNode; + int offset = p.deprecatedEditingOffset(); + + Node *n = startNode; + while (n) { + if (boundaryCrossingRule == CannotCrossEditingBoundary && n->isContentEditable() != startNode->isContentEditable()) + break; + RenderObject *r = n->renderer(); + if (!r) { + n = n->traverseNextNode(stayInsideBlock); + continue; + } + RenderStyle *style = r->style(); + if (style->visibility() != VISIBLE) { + n = n->traverseNextNode(stayInsideBlock); + continue; + } + + if (r->isBR() || isBlock(n)) + break; + + // FIXME: We avoid returning a position where the renderer can't accept the caret. + if (r->isText() && r->caretMaxRenderedOffset() > 0) { + int length = toRenderText(r)->textLength(); + if (style->preserveNewline()) { + const UChar* chars = toRenderText(r)->characters(); + int o = n == startNode ? offset : 0; + for (int i = o; i < length; ++i) + if (chars[i] == '\n') + return VisiblePosition(n, i, DOWNSTREAM); + } + node = n; + offset = r->caretMaxOffset(); + n = n->traverseNextNode(stayInsideBlock); + } else if (editingIgnoresContent(n) || isTableElement(n)) { + node = n; + offset = lastOffsetForEditing(n); + n = n->traverseNextSibling(stayInsideBlock); + } else + n = n->traverseNextNode(stayInsideBlock); + } + + return VisiblePosition(node, offset, DOWNSTREAM); +} + +VisiblePosition startOfNextParagraph(const VisiblePosition& visiblePosition) +{ + VisiblePosition paragraphEnd(endOfParagraph(visiblePosition)); + VisiblePosition afterParagraphEnd(paragraphEnd.next(true)); + // The position after the last position in the last cell of a table + // is not the start of the next paragraph. + if (isFirstPositionAfterTable(afterParagraphEnd)) + return afterParagraphEnd.next(true); + return afterParagraphEnd; +} + +bool inSameParagraph(const VisiblePosition &a, const VisiblePosition &b) +{ + return a.isNotNull() && startOfParagraph(a) == startOfParagraph(b); +} + +bool isStartOfParagraph(const VisiblePosition &pos, EditingBoundaryCrossingRule boundaryCrossingRule) +{ + return pos.isNotNull() && pos == startOfParagraph(pos, boundaryCrossingRule); +} + +bool isEndOfParagraph(const VisiblePosition &pos, EditingBoundaryCrossingRule boundaryCrossingRule) +{ + return pos.isNotNull() && pos == endOfParagraph(pos, boundaryCrossingRule); +} + +VisiblePosition previousParagraphPosition(const VisiblePosition& p, int x) +{ + VisiblePosition pos = p; + do { + VisiblePosition n = previousLinePosition(pos, x); + if (n.isNull() || n == pos) + break; + pos = n; + } while (inSameParagraph(p, pos)); + return pos; +} + +VisiblePosition nextParagraphPosition(const VisiblePosition& p, int x) +{ + VisiblePosition pos = p; + do { + VisiblePosition n = nextLinePosition(pos, x); + if (n.isNull() || n == pos) + break; + pos = n; + } while (inSameParagraph(p, pos)); + return pos; +} + +// --------- + +VisiblePosition startOfBlock(const VisiblePosition &c) +{ + Position p = c.deepEquivalent(); + Node *startNode = p.node(); + if (!startNode) + return VisiblePosition(); + return VisiblePosition(Position(startNode->enclosingBlockFlowElement(), 0), DOWNSTREAM); +} + +VisiblePosition endOfBlock(const VisiblePosition &c) +{ + Position p = c.deepEquivalent(); + + Node *startNode = p.node(); + if (!startNode) + return VisiblePosition(); + + Node *startBlock = startNode->enclosingBlockFlowElement(); + + return VisiblePosition(startBlock, startBlock->childNodeCount(), VP_DEFAULT_AFFINITY); +} + +bool inSameBlock(const VisiblePosition &a, const VisiblePosition &b) +{ + return !a.isNull() && enclosingBlockFlowElement(a) == enclosingBlockFlowElement(b); +} + +bool isStartOfBlock(const VisiblePosition &pos) +{ + return pos.isNotNull() && pos == startOfBlock(pos); +} + +bool isEndOfBlock(const VisiblePosition &pos) +{ + return pos.isNotNull() && pos == endOfBlock(pos); +} + +// --------- + +VisiblePosition startOfDocument(const Node* node) +{ + if (!node) + return VisiblePosition(); + + return VisiblePosition(node->document()->documentElement(), 0, DOWNSTREAM); +} + +VisiblePosition startOfDocument(const VisiblePosition &c) +{ + return startOfDocument(c.deepEquivalent().node()); +} + +VisiblePosition endOfDocument(const Node* node) +{ + if (!node || !node->document() || !node->document()->documentElement()) + return VisiblePosition(); + + Element* doc = node->document()->documentElement(); + return VisiblePosition(doc, doc->childNodeCount(), DOWNSTREAM); +} + +VisiblePosition endOfDocument(const VisiblePosition &c) +{ + return endOfDocument(c.deepEquivalent().node()); +} + +bool inSameDocument(const VisiblePosition &a, const VisiblePosition &b) +{ + Position ap = a.deepEquivalent(); + Node *an = ap.node(); + if (!an) + return false; + Position bp = b.deepEquivalent(); + Node *bn = bp.node(); + if (an == bn) + return true; + + return an->document() == bn->document(); +} + +bool isStartOfDocument(const VisiblePosition &p) +{ + return p.isNotNull() && p.previous().isNull(); +} + +bool isEndOfDocument(const VisiblePosition &p) +{ + return p.isNotNull() && p.next().isNull(); +} + +// --------- + +VisiblePosition startOfEditableContent(const VisiblePosition& visiblePosition) +{ + Node* highestRoot = highestEditableRoot(visiblePosition.deepEquivalent()); + if (!highestRoot) + return VisiblePosition(); + + return firstDeepEditingPositionForNode(highestRoot); +} + +VisiblePosition endOfEditableContent(const VisiblePosition& visiblePosition) +{ + Node* highestRoot = highestEditableRoot(visiblePosition.deepEquivalent()); + if (!highestRoot) + return VisiblePosition(); + + return lastDeepEditingPositionForNode(highestRoot); +} + +static void getLeafBoxesInLogicalOrder(RootInlineBox* rootBox, Vector<InlineBox*>& leafBoxesInLogicalOrder) +{ + unsigned char minLevel = 128; + unsigned char maxLevel = 0; + unsigned count = 0; + InlineBox* r = rootBox->firstLeafChild(); + // First find highest and lowest levels, + // and initialize leafBoxesInLogicalOrder with the leaf boxes in visual order. + while (r) { + if (r->bidiLevel() > maxLevel) + maxLevel = r->bidiLevel(); + if (r->bidiLevel() < minLevel) + minLevel = r->bidiLevel(); + leafBoxesInLogicalOrder.append(r); + r = r->nextLeafChild(); + ++count; + } + + if (rootBox->renderer()->style()->visuallyOrdered()) + return; + // Reverse of reordering of the line (L2 according to Bidi spec): + // L2. From the highest level found in the text to the lowest odd level on each line, + // reverse any contiguous sequence of characters that are at that level or higher. + + // Reversing the reordering of the line is only done up to the lowest odd level. + if (!(minLevel % 2)) + minLevel++; + + InlineBox** end = leafBoxesInLogicalOrder.end(); + while (minLevel <= maxLevel) { + InlineBox** iter = leafBoxesInLogicalOrder.begin(); + while (iter != end) { + while (iter != end) { + if ((*iter)->bidiLevel() >= minLevel) + break; + ++iter; + } + InlineBox** first = iter; + while (iter != end) { + if ((*iter)->bidiLevel() < minLevel) + break; + ++iter; + } + InlineBox** last = iter; + std::reverse(first, last); + } + ++minLevel; + } +} + +static void getLogicalStartBoxAndNode(RootInlineBox* rootBox, InlineBox*& startBox, Node*& startNode) +{ + Vector<InlineBox*> leafBoxesInLogicalOrder; + getLeafBoxesInLogicalOrder(rootBox, leafBoxesInLogicalOrder); + startBox = 0; + startNode = 0; + for (size_t i = 0; i < leafBoxesInLogicalOrder.size(); ++i) { + startBox = leafBoxesInLogicalOrder[i]; + startNode = startBox->renderer()->node(); + if (startNode) + return; + } +} + +static void getLogicalEndBoxAndNode(RootInlineBox* rootBox, InlineBox*& endBox, Node*& endNode) +{ + Vector<InlineBox*> leafBoxesInLogicalOrder; + getLeafBoxesInLogicalOrder(rootBox, leafBoxesInLogicalOrder); + endBox = 0; + endNode = 0; + // Generated content (e.g. list markers and CSS :before and :after + // pseudoelements) have no corresponding DOM element, and so cannot be + // represented by a VisiblePosition. Use whatever precedes instead. + for (size_t i = leafBoxesInLogicalOrder.size(); i > 0; --i) { + endBox = leafBoxesInLogicalOrder[i - 1]; + endNode = endBox->renderer()->node(); + if (endNode) + return; + } +} + +static VisiblePosition logicalStartPositionForLine(const VisiblePosition& c) +{ + if (c.isNull()) + return VisiblePosition(); + + RootInlineBox* rootBox = rootBoxForLine(c); + if (!rootBox) { + // There are VisiblePositions at offset 0 in blocks without + // RootInlineBoxes, like empty editable blocks and bordered blocks. + Position p = c.deepEquivalent(); + if (p.node()->renderer() && p.node()->renderer()->isRenderBlock() && !p.deprecatedEditingOffset()) + return positionAvoidingFirstPositionInTable(c); + + return VisiblePosition(); + } + + InlineBox* logicalStartBox; + Node* logicalStartNode; + getLogicalStartBoxAndNode(rootBox, logicalStartBox, logicalStartNode); + + if (!logicalStartNode) + return VisiblePosition(); + + int startOffset = logicalStartBox->caretMinOffset(); + + VisiblePosition visPos = VisiblePosition(logicalStartNode, startOffset, DOWNSTREAM); + return positionAvoidingFirstPositionInTable(visPos); +} + +VisiblePosition logicalStartOfLine(const VisiblePosition& c) +{ + // TODO: this is the current behavior that might need to be fixed. + // Please refer to https://bugs.webkit.org/show_bug.cgi?id=49107 for detail. + VisiblePosition visPos = logicalStartPositionForLine(c); + + return c.honorEditableBoundaryAtOrAfter(visPos); +} + +static VisiblePosition logicalEndPositionForLine(const VisiblePosition& c) +{ + if (c.isNull()) + return VisiblePosition(); + + RootInlineBox* rootBox = rootBoxForLine(c); + if (!rootBox) { + // There are VisiblePositions at offset 0 in blocks without + // RootInlineBoxes, like empty editable blocks and bordered blocks. + Position p = c.deepEquivalent(); + if (p.node()->renderer() && p.node()->renderer()->isRenderBlock() && !p.deprecatedEditingOffset()) + return c; + return VisiblePosition(); + } + + InlineBox* logicalEndBox; + Node* logicalEndNode; + getLogicalEndBoxAndNode(rootBox, logicalEndBox, logicalEndNode); + if (!logicalEndNode) + return VisiblePosition(); + + int endOffset = 1; + if (logicalEndNode->hasTagName(brTag)) + endOffset = 0; + else if (logicalEndBox->isInlineTextBox()) { + InlineTextBox* endTextBox = static_cast<InlineTextBox*>(logicalEndBox); + endOffset = endTextBox->start(); + if (!endTextBox->isLineBreak()) + endOffset += endTextBox->len(); + } + + return VisiblePosition(logicalEndNode, endOffset, VP_UPSTREAM_IF_POSSIBLE); +} + +bool inSameLogicalLine(const VisiblePosition& a, const VisiblePosition& b) +{ + return a.isNotNull() && logicalStartOfLine(a) == logicalStartOfLine(b); +} + +VisiblePosition logicalEndOfLine(const VisiblePosition& c) +{ + // TODO: this is the current behavior that might need to be fixed. + // Please refer to https://bugs.webkit.org/show_bug.cgi?id=49107 for detail. + + VisiblePosition visPos = logicalEndPositionForLine(c); + + // Make sure the end of line is at the same line as the given input position. For a wrapping line, the logical end + // position for the not-last-2-lines might incorrectly hand back the logical beginning of the next line. + // For example, <div contenteditable dir="rtl" style="line-break:before-white-space">abcdefg abcdefg abcdefg + // a abcdefg abcdefg abcdefg abcdefg abcdefg abcdefg abcdefg abcdefg abcdefg abcdefg </div> + // In this case, use the previous position of the computed logical end position. + if (!inSameLogicalLine(c, visPos)) + visPos = visPos.previous(); + + return c.honorEditableBoundaryAtOrBefore(visPos); +} + +VisiblePosition leftBoundaryOfLine(const VisiblePosition& c, TextDirection direction) +{ + return direction == LTR ? logicalStartOfLine(c) : logicalEndOfLine(c); +} + +VisiblePosition rightBoundaryOfLine(const VisiblePosition& c, TextDirection direction) +{ + return direction == LTR ? logicalEndOfLine(c) : logicalStartOfLine(c); +} + +} diff --git a/Source/WebCore/editing/visible_units.h b/Source/WebCore/editing/visible_units.h new file mode 100644 index 0000000..167bd2c --- /dev/null +++ b/Source/WebCore/editing/visible_units.h @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2004 Apple Computer, 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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. + */ + +#ifndef visible_units_h +#define visible_units_h + +#include "EditingBoundary.h" +#include "Position.h" +#include "TextAffinity.h" + +namespace WebCore { + +class VisiblePosition; + +enum EWordSide { RightWordIfOnBoundary = false, LeftWordIfOnBoundary = true }; + +// words +VisiblePosition startOfWord(const VisiblePosition &, EWordSide = RightWordIfOnBoundary); +VisiblePosition endOfWord(const VisiblePosition &, EWordSide = RightWordIfOnBoundary); +VisiblePosition previousWordPosition(const VisiblePosition &); +VisiblePosition nextWordPosition(const VisiblePosition &); + +// sentences +VisiblePosition startOfSentence(const VisiblePosition &); +VisiblePosition endOfSentence(const VisiblePosition &); +VisiblePosition previousSentencePosition(const VisiblePosition &); +VisiblePosition nextSentencePosition(const VisiblePosition &); + +// lines +VisiblePosition startOfLine(const VisiblePosition &); +VisiblePosition endOfLine(const VisiblePosition &); +VisiblePosition previousLinePosition(const VisiblePosition &, int x); +VisiblePosition nextLinePosition(const VisiblePosition &, int x); +bool inSameLine(const VisiblePosition &, const VisiblePosition &); +bool inSameLogicalLine(const VisiblePosition &, const VisiblePosition &); +bool isStartOfLine(const VisiblePosition &); +bool isEndOfLine(const VisiblePosition &); +VisiblePosition logicalStartOfLine(const VisiblePosition &); +VisiblePosition logicalEndOfLine(const VisiblePosition &); +VisiblePosition leftBoundaryOfLine(const VisiblePosition&, TextDirection); +VisiblePosition rightBoundaryOfLine(const VisiblePosition&, TextDirection); + +// paragraphs (perhaps a misnomer, can be divided by line break elements) +VisiblePosition startOfParagraph(const VisiblePosition&, EditingBoundaryCrossingRule = CannotCrossEditingBoundary); +VisiblePosition endOfParagraph(const VisiblePosition&, EditingBoundaryCrossingRule = CannotCrossEditingBoundary); +VisiblePosition startOfNextParagraph(const VisiblePosition&); +VisiblePosition previousParagraphPosition(const VisiblePosition &, int x); +VisiblePosition nextParagraphPosition(const VisiblePosition &, int x); +bool isStartOfParagraph(const VisiblePosition &, EditingBoundaryCrossingRule = CannotCrossEditingBoundary); +bool isEndOfParagraph(const VisiblePosition &, EditingBoundaryCrossingRule = CannotCrossEditingBoundary); +bool inSameParagraph(const VisiblePosition &, const VisiblePosition &); + +// blocks (true paragraphs; line break elements don't break blocks) +VisiblePosition startOfBlock(const VisiblePosition &); +VisiblePosition endOfBlock(const VisiblePosition &); +bool inSameBlock(const VisiblePosition &, const VisiblePosition &); +bool isStartOfBlock(const VisiblePosition &); +bool isEndOfBlock(const VisiblePosition &); + +// document +VisiblePosition startOfDocument(const Node*); +VisiblePosition endOfDocument(const Node*); +VisiblePosition startOfDocument(const VisiblePosition &); +VisiblePosition endOfDocument(const VisiblePosition &); +bool inSameDocument(const VisiblePosition &, const VisiblePosition &); +bool isStartOfDocument(const VisiblePosition &); +bool isEndOfDocument(const VisiblePosition &); + +// editable content +VisiblePosition startOfEditableContent(const VisiblePosition&); +VisiblePosition endOfEditableContent(const VisiblePosition&); + +} // namespace WebCore + +#endif // VisiblePosition_h diff --git a/Source/WebCore/editing/wx/EditorWx.cpp b/Source/WebCore/editing/wx/EditorWx.cpp new file mode 100644 index 0000000..83d6b78 --- /dev/null +++ b/Source/WebCore/editing/wx/EditorWx.cpp @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2007 Kevin Ollivier <kevino@theolliviers.com> + * + * 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 APPLE COMPUTER, INC. ``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 APPLE COMPUTER, INC. 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 "Editor.h" + +#include "ClipboardWx.h" +#include "NotImplemented.h" + +namespace WebCore { + +PassRefPtr<Clipboard> Editor::newGeneralClipboard(ClipboardAccessPolicy policy, Frame*) +{ + return ClipboardWx::create(policy, Clipboard::CopyAndPaste); +} + +void Editor::showColorPanel() +{ + notImplemented(); +} + +void Editor::showFontPanel() +{ + notImplemented(); +} + +void Editor::showStylesPanel() +{ + notImplemented(); +} + +} |