summaryrefslogtreecommitdiffstats
path: root/Source/WebCore/editing
diff options
context:
space:
mode:
authorSteve Block <steveblock@google.com>2011-05-06 11:45:16 +0100
committerSteve Block <steveblock@google.com>2011-05-12 13:44:10 +0100
commitcad810f21b803229eb11403f9209855525a25d57 (patch)
tree29a6fd0279be608e0fe9ffe9841f722f0f4e4269 /Source/WebCore/editing
parent121b0cf4517156d0ac5111caf9830c51b69bae8f (diff)
downloadexternal_webkit-cad810f21b803229eb11403f9209855525a25d57.zip
external_webkit-cad810f21b803229eb11403f9209855525a25d57.tar.gz
external_webkit-cad810f21b803229eb11403f9209855525a25d57.tar.bz2
Merge WebKit at r75315: Initial merge by git.
Change-Id: I570314b346ce101c935ed22a626b48c2af266b84
Diffstat (limited to 'Source/WebCore/editing')
-rw-r--r--Source/WebCore/editing/AppendNodeCommand.cpp82
-rw-r--r--Source/WebCore/editing/AppendNodeCommand.h52
-rw-r--r--Source/WebCore/editing/ApplyBlockElementCommand.cpp286
-rw-r--r--Source/WebCore/editing/ApplyBlockElementCommand.h61
-rw-r--r--Source/WebCore/editing/ApplyStyleCommand.cpp1931
-rw-r--r--Source/WebCore/editing/ApplyStyleCommand.h138
-rw-r--r--Source/WebCore/editing/BreakBlockquoteCommand.cpp205
-rw-r--r--Source/WebCore/editing/BreakBlockquoteCommand.h47
-rw-r--r--Source/WebCore/editing/CompositeEditCommand.cpp1212
-rw-r--r--Source/WebCore/editing/CompositeEditCommand.h128
-rw-r--r--Source/WebCore/editing/CorrectionPanelInfo.h65
-rw-r--r--Source/WebCore/editing/CreateLinkCommand.cpp59
-rw-r--r--Source/WebCore/editing/CreateLinkCommand.h51
-rw-r--r--Source/WebCore/editing/DeleteButton.cpp62
-rw-r--r--Source/WebCore/editing/DeleteButton.h45
-rw-r--r--Source/WebCore/editing/DeleteButtonController.cpp358
-rw-r--r--Source/WebCore/editing/DeleteButtonController.h77
-rw-r--r--Source/WebCore/editing/DeleteFromTextNodeCommand.cpp78
-rw-r--r--Source/WebCore/editing/DeleteFromTextNodeCommand.h56
-rw-r--r--Source/WebCore/editing/DeleteSelectionCommand.cpp811
-rw-r--r--Source/WebCore/editing/DeleteSelectionCommand.h102
-rw-r--r--Source/WebCore/editing/EditAction.h71
-rw-r--r--Source/WebCore/editing/EditCommand.cpp217
-rw-r--r--Source/WebCore/editing/EditCommand.h96
-rw-r--r--Source/WebCore/editing/EditingAllInOne.cpp79
-rw-r--r--Source/WebCore/editing/EditingBehavior.h69
-rw-r--r--Source/WebCore/editing/EditingBehaviorTypes.h47
-rw-r--r--Source/WebCore/editing/EditingBoundary.h38
-rw-r--r--Source/WebCore/editing/EditingStyle.cpp341
-rw-r--r--Source/WebCore/editing/EditingStyle.h111
-rw-r--r--Source/WebCore/editing/Editor.cpp3525
-rw-r--r--Source/WebCore/editing/Editor.h451
-rw-r--r--Source/WebCore/editing/EditorCommand.cpp1666
-rw-r--r--Source/WebCore/editing/EditorDeleteAction.h40
-rw-r--r--Source/WebCore/editing/EditorInsertAction.h40
-rw-r--r--Source/WebCore/editing/FindOptions.h46
-rw-r--r--Source/WebCore/editing/FormatBlockCommand.cpp161
-rw-r--r--Source/WebCore/editing/FormatBlockCommand.h58
-rw-r--r--Source/WebCore/editing/HTMLInterchange.cpp112
-rw-r--r--Source/WebCore/editing/HTMLInterchange.h47
-rw-r--r--Source/WebCore/editing/IndentOutdentCommand.cpp235
-rw-r--r--Source/WebCore/editing/IndentOutdentCommand.h64
-rw-r--r--Source/WebCore/editing/InsertIntoTextNodeCommand.cpp70
-rw-r--r--Source/WebCore/editing/InsertIntoTextNodeCommand.h55
-rw-r--r--Source/WebCore/editing/InsertLineBreakCommand.cpp188
-rw-r--r--Source/WebCore/editing/InsertLineBreakCommand.h54
-rw-r--r--Source/WebCore/editing/InsertListCommand.cpp393
-rw-r--r--Source/WebCore/editing/InsertListCommand.h66
-rw-r--r--Source/WebCore/editing/InsertNodeBeforeCommand.cpp73
-rw-r--r--Source/WebCore/editing/InsertNodeBeforeCommand.h52
-rw-r--r--Source/WebCore/editing/InsertParagraphSeparatorCommand.cpp395
-rw-r--r--Source/WebCore/editing/InsertParagraphSeparatorCommand.h63
-rw-r--r--Source/WebCore/editing/InsertTextCommand.cpp243
-rw-r--r--Source/WebCore/editing/InsertTextCommand.h60
-rw-r--r--Source/WebCore/editing/JoinTextNodesCommand.cpp78
-rw-r--r--Source/WebCore/editing/JoinTextNodesCommand.h54
-rw-r--r--Source/WebCore/editing/MarkupAccumulator.cpp465
-rw-r--r--Source/WebCore/editing/MarkupAccumulator.h119
-rw-r--r--Source/WebCore/editing/MergeIdenticalElementsCommand.cpp89
-rw-r--r--Source/WebCore/editing/MergeIdenticalElementsCommand.h53
-rw-r--r--Source/WebCore/editing/ModifySelectionListLevel.cpp295
-rw-r--r--Source/WebCore/editing/ModifySelectionListLevel.h92
-rw-r--r--Source/WebCore/editing/MoveSelectionCommand.cpp85
-rw-r--r--Source/WebCore/editing/MoveSelectionCommand.h56
-rw-r--r--Source/WebCore/editing/RemoveCSSPropertyCommand.cpp57
-rw-r--r--Source/WebCore/editing/RemoveCSSPropertyCommand.h57
-rw-r--r--Source/WebCore/editing/RemoveFormatCommand.cpp93
-rw-r--r--Source/WebCore/editing/RemoveFormatCommand.h49
-rw-r--r--Source/WebCore/editing/RemoveNodeCommand.cpp66
-rw-r--r--Source/WebCore/editing/RemoveNodeCommand.h53
-rw-r--r--Source/WebCore/editing/RemoveNodePreservingChildrenCommand.cpp56
-rw-r--r--Source/WebCore/editing/RemoveNodePreservingChildrenCommand.h50
-rw-r--r--Source/WebCore/editing/ReplaceNodeWithSpanCommand.cpp88
-rw-r--r--Source/WebCore/editing/ReplaceNodeWithSpanCommand.h62
-rw-r--r--Source/WebCore/editing/ReplaceSelectionCommand.cpp1289
-rw-r--r--Source/WebCore/editing/ReplaceSelectionCommand.h99
-rw-r--r--Source/WebCore/editing/SelectionController.cpp1802
-rw-r--r--Source/WebCore/editing/SelectionController.h269
-rw-r--r--Source/WebCore/editing/SetNodeAttributeCommand.cpp57
-rw-r--r--Source/WebCore/editing/SetNodeAttributeCommand.h54
-rw-r--r--Source/WebCore/editing/SmartReplace.cpp43
-rw-r--r--Source/WebCore/editing/SmartReplace.h35
-rw-r--r--Source/WebCore/editing/SmartReplaceCF.cpp73
-rw-r--r--Source/WebCore/editing/SmartReplaceICU.cpp100
-rw-r--r--Source/WebCore/editing/SpellChecker.cpp176
-rw-r--r--Source/WebCore/editing/SpellChecker.h84
-rw-r--r--Source/WebCore/editing/SplitElementCommand.cpp111
-rw-r--r--Source/WebCore/editing/SplitElementCommand.h55
-rw-r--r--Source/WebCore/editing/SplitTextNodeCommand.cpp107
-rw-r--r--Source/WebCore/editing/SplitTextNodeCommand.h57
-rw-r--r--Source/WebCore/editing/SplitTextNodeContainingElementCommand.cpp66
-rw-r--r--Source/WebCore/editing/SplitTextNodeContainingElementCommand.h51
-rw-r--r--Source/WebCore/editing/TextAffinity.h57
-rw-r--r--Source/WebCore/editing/TextCheckingHelper.cpp605
-rw-r--r--Source/WebCore/editing/TextCheckingHelper.h96
-rw-r--r--Source/WebCore/editing/TextGranularity.h47
-rw-r--r--Source/WebCore/editing/TextIterator.cpp2557
-rw-r--r--Source/WebCore/editing/TextIterator.h328
-rw-r--r--Source/WebCore/editing/TypingCommand.cpp651
-rw-r--r--Source/WebCore/editing/TypingCommand.h108
-rw-r--r--Source/WebCore/editing/UnlinkCommand.cpp47
-rw-r--r--Source/WebCore/editing/UnlinkCommand.h49
-rw-r--r--Source/WebCore/editing/VisiblePosition.cpp684
-rw-r--r--Source/WebCore/editing/VisiblePosition.h152
-rw-r--r--Source/WebCore/editing/VisibleSelection.cpp647
-rw-r--r--Source/WebCore/editing/VisibleSelection.h154
-rw-r--r--Source/WebCore/editing/WrapContentsInDummySpanCommand.cpp93
-rw-r--r--Source/WebCore/editing/WrapContentsInDummySpanCommand.h56
-rw-r--r--Source/WebCore/editing/WritingDirection.h31
-rw-r--r--Source/WebCore/editing/android/EditorAndroid.cpp39
-rw-r--r--Source/WebCore/editing/brew/EditorBrew.cpp39
-rw-r--r--Source/WebCore/editing/chromium/EditorChromium.cpp45
-rw-r--r--Source/WebCore/editing/chromium/SelectionControllerChromium.cpp48
-rw-r--r--Source/WebCore/editing/gtk/SelectionControllerGtk.cpp105
-rw-r--r--Source/WebCore/editing/haiku/EditorHaiku.cpp44
-rw-r--r--Source/WebCore/editing/htmlediting.cpp1180
-rw-r--r--Source/WebCore/editing/htmlediting.h234
-rw-r--r--Source/WebCore/editing/mac/EditorMac.mm211
-rw-r--r--Source/WebCore/editing/mac/SelectionControllerMac.mm67
-rw-r--r--Source/WebCore/editing/markup.cpp915
-rw-r--r--Source/WebCore/editing/markup.h61
-rw-r--r--Source/WebCore/editing/qt/EditorQt.cpp47
-rw-r--r--Source/WebCore/editing/qt/SmartReplaceQt.cpp66
-rw-r--r--Source/WebCore/editing/visible_units.cpp1210
-rw-r--r--Source/WebCore/editing/visible_units.h97
-rw-r--r--Source/WebCore/editing/wx/EditorWx.cpp54
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, ("&amp;"));
+ DEFINE_STATIC_LOCAL(const String, ltReference, ("&lt;"));
+ DEFINE_STATIC_LOCAL(const String, gtReference, ("&gt;"));
+ DEFINE_STATIC_LOCAL(const String, quotReference, ("&quot;"));
+ DEFINE_STATIC_LOCAL(const String, nbspReference, ("&nbsp;"));
+
+ 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('\"', "&quot;");
+ 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();
+}
+
+}