summaryrefslogtreecommitdiffstats
path: root/WebCore/editing
diff options
context:
space:
mode:
Diffstat (limited to 'WebCore/editing')
-rw-r--r--WebCore/editing/AppendNodeCommand.cpp63
-rw-r--r--WebCore/editing/AppendNodeCommand.h50
-rw-r--r--WebCore/editing/ApplyStyleCommand.cpp1349
-rw-r--r--WebCore/editing/ApplyStyleCommand.h102
-rw-r--r--WebCore/editing/BreakBlockquoteCommand.cpp179
-rw-r--r--WebCore/editing/BreakBlockquoteCommand.h41
-rw-r--r--WebCore/editing/CompositeEditCommand.cpp938
-rw-r--r--WebCore/editing/CompositeEditCommand.h116
-rw-r--r--WebCore/editing/CreateLinkCommand.cpp60
-rw-r--r--WebCore/editing/CreateLinkCommand.h45
-rw-r--r--WebCore/editing/DeleteButton.cpp57
-rw-r--r--WebCore/editing/DeleteButton.h42
-rw-r--r--WebCore/editing/DeleteButtonController.cpp309
-rw-r--r--WebCore/editing/DeleteButtonController.h77
-rw-r--r--WebCore/editing/DeleteFromTextNodeCommand.cpp64
-rw-r--r--WebCore/editing/DeleteFromTextNodeCommand.h55
-rw-r--r--WebCore/editing/DeleteSelectionCommand.cpp790
-rw-r--r--WebCore/editing/DeleteSelectionCommand.h88
-rw-r--r--WebCore/editing/EditAction.h71
-rw-r--r--WebCore/editing/EditCommand.cpp255
-rw-r--r--WebCore/editing/EditCommand.h94
-rw-r--r--WebCore/editing/Editor.cpp1941
-rw-r--r--WebCore/editing/Editor.h309
-rw-r--r--WebCore/editing/EditorCommand.cpp1402
-rw-r--r--WebCore/editing/EditorDeleteAction.h40
-rw-r--r--WebCore/editing/EditorInsertAction.h40
-rw-r--r--WebCore/editing/FormatBlockCommand.cpp133
-rw-r--r--WebCore/editing/FormatBlockCommand.h45
-rw-r--r--WebCore/editing/HTMLInterchange.cpp111
-rw-r--r--WebCore/editing/HTMLInterchange.h46
-rw-r--r--WebCore/editing/IndentOutdentCommand.cpp285
-rw-r--r--WebCore/editing/IndentOutdentCommand.h51
-rw-r--r--WebCore/editing/InsertIntoTextNodeCommand.cpp58
-rw-r--r--WebCore/editing/InsertIntoTextNodeCommand.h54
-rw-r--r--WebCore/editing/InsertLineBreakCommand.cpp182
-rw-r--r--WebCore/editing/InsertLineBreakCommand.h48
-rw-r--r--WebCore/editing/InsertListCommand.cpp258
-rw-r--r--WebCore/editing/InsertListCommand.h51
-rw-r--r--WebCore/editing/InsertNodeBeforeCommand.cpp65
-rw-r--r--WebCore/editing/InsertNodeBeforeCommand.h50
-rw-r--r--WebCore/editing/InsertParagraphSeparatorCommand.cpp310
-rw-r--r--WebCore/editing/InsertParagraphSeparatorCommand.h52
-rw-r--r--WebCore/editing/InsertTextCommand.cpp208
-rw-r--r--WebCore/editing/InsertTextCommand.h55
-rw-r--r--WebCore/editing/JoinTextNodesCommand.cpp76
-rw-r--r--WebCore/editing/JoinTextNodesCommand.h54
-rw-r--r--WebCore/editing/MergeIdenticalElementsCommand.cpp77
-rw-r--r--WebCore/editing/MergeIdenticalElementsCommand.h48
-rw-r--r--WebCore/editing/ModifySelectionListLevel.cpp293
-rw-r--r--WebCore/editing/ModifySelectionListLevel.h80
-rw-r--r--WebCore/editing/MoveSelectionCommand.cpp85
-rw-r--r--WebCore/editing/MoveSelectionCommand.h51
-rw-r--r--WebCore/editing/RemoveCSSPropertyCommand.cpp66
-rw-r--r--WebCore/editing/RemoveCSSPropertyCommand.h55
-rw-r--r--WebCore/editing/RemoveFormatCommand.cpp78
-rw-r--r--WebCore/editing/RemoveFormatCommand.h42
-rw-r--r--WebCore/editing/RemoveNodeAttributeCommand.cpp62
-rw-r--r--WebCore/editing/RemoveNodeAttributeCommand.h52
-rw-r--r--WebCore/editing/RemoveNodeCommand.cpp63
-rw-r--r--WebCore/editing/RemoveNodeCommand.h50
-rw-r--r--WebCore/editing/RemoveNodePreservingChildrenCommand.cpp49
-rw-r--r--WebCore/editing/RemoveNodePreservingChildrenCommand.h47
-rw-r--r--WebCore/editing/ReplaceSelectionCommand.cpp948
-rw-r--r--WebCore/editing/ReplaceSelectionCommand.h118
-rw-r--r--WebCore/editing/Selection.cpp590
-rw-r--r--WebCore/editing/Selection.h133
-rw-r--r--WebCore/editing/SelectionController.cpp1117
-rw-r--r--WebCore/editing/SelectionController.h183
-rw-r--r--WebCore/editing/SetNodeAttributeCommand.cpp66
-rw-r--r--WebCore/editing/SetNodeAttributeCommand.h54
-rw-r--r--WebCore/editing/SmartReplace.cpp43
-rw-r--r--WebCore/editing/SmartReplace.h35
-rw-r--r--WebCore/editing/SmartReplaceCF.cpp72
-rw-r--r--WebCore/editing/SmartReplaceICU.cpp99
-rw-r--r--WebCore/editing/SplitElementCommand.cpp90
-rw-r--r--WebCore/editing/SplitElementCommand.h48
-rw-r--r--WebCore/editing/SplitTextNodeCommand.cpp91
-rw-r--r--WebCore/editing/SplitTextNodeCommand.h54
-rw-r--r--WebCore/editing/SplitTextNodeContainingElementCommand.cpp59
-rw-r--r--WebCore/editing/SplitTextNodeContainingElementCommand.h46
-rw-r--r--WebCore/editing/TextAffinity.h57
-rw-r--r--WebCore/editing/TextGranularity.h47
-rw-r--r--WebCore/editing/TextIterator.cpp1383
-rw-r--r--WebCore/editing/TextIterator.h248
-rw-r--r--WebCore/editing/TypingCommand.cpp535
-rw-r--r--WebCore/editing/TypingCommand.h99
-rw-r--r--WebCore/editing/UnlinkCommand.cpp50
-rw-r--r--WebCore/editing/UnlinkCommand.h42
-rw-r--r--WebCore/editing/VisiblePosition.cpp359
-rw-r--r--WebCore/editing/VisiblePosition.h121
-rw-r--r--WebCore/editing/WrapContentsInDummySpanCommand.cpp77
-rw-r--r--WebCore/editing/WrapContentsInDummySpanCommand.h47
-rw-r--r--WebCore/editing/htmlediting.cpp997
-rw-r--r--WebCore/editing/htmlediting.h134
-rw-r--r--WebCore/editing/mac/EditorMac.mm113
-rw-r--r--WebCore/editing/mac/SelectionControllerMac.mm65
-rw-r--r--WebCore/editing/markup.cpp1084
-rw-r--r--WebCore/editing/markup.h53
-rw-r--r--WebCore/editing/qt/EditorQt.cpp47
-rw-r--r--WebCore/editing/visible_units.cpp912
-rw-r--r--WebCore/editing/visible_units.h91
-rw-r--r--WebCore/editing/wx/EditorWx.cpp40
102 files changed, 22414 insertions, 0 deletions
diff --git a/WebCore/editing/AppendNodeCommand.cpp b/WebCore/editing/AppendNodeCommand.cpp
new file mode 100644
index 0000000..ab68d6b
--- /dev/null
+++ b/WebCore/editing/AppendNodeCommand.cpp
@@ -0,0 +1,63 @@
+/*
+ * 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 "AppendNodeCommand.h"
+#include "htmlediting.h"
+
+namespace WebCore {
+
+AppendNodeCommand::AppendNodeCommand(Node* parentNode, PassRefPtr<Node> childToAppend)
+ : EditCommand(parentNode->document()), m_parentNode(parentNode), m_childToAppend(childToAppend)
+{
+ ASSERT(m_childToAppend);
+ ASSERT(m_parentNode);
+}
+
+void AppendNodeCommand::doApply()
+{
+ ASSERT(m_childToAppend);
+ ASSERT(m_parentNode);
+ // If the child to append is already in a tree, appending it will remove it from it's old location
+ // in an non-undoable way. We might eventually find it useful to do an undoable remove in this case.
+ ASSERT(!m_childToAppend->parent());
+ ASSERT(enclosingNodeOfType(Position(m_parentNode.get(), 0), &isContentEditable) || !m_parentNode->attached());
+
+ ExceptionCode ec = 0;
+ m_parentNode->appendChild(m_childToAppend.get(), ec);
+ ASSERT(ec == 0);
+}
+
+void AppendNodeCommand::doUnapply()
+{
+ ASSERT(m_childToAppend);
+ ASSERT(m_parentNode);
+
+ ExceptionCode ec = 0;
+ m_parentNode->removeChild(m_childToAppend.get(), ec);
+ ASSERT(ec == 0);
+}
+
+} // namespace WebCore
diff --git a/WebCore/editing/AppendNodeCommand.h b/WebCore/editing/AppendNodeCommand.h
new file mode 100644
index 0000000..ce9da6d
--- /dev/null
+++ b/WebCore/editing/AppendNodeCommand.h
@@ -0,0 +1,50 @@
+/*
+ * 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.
+ */
+
+#ifndef AppendNodeCommand_h
+#define AppendNodeCommand_h
+
+#include "EditCommand.h"
+
+namespace WebCore {
+
+class AppendNodeCommand : public EditCommand {
+public:
+ AppendNodeCommand(Node* parentNode, PassRefPtr<Node> childToAppend);
+
+ virtual void doApply();
+ virtual void doUnapply();
+
+ Node* parentNode() const { return m_parentNode.get(); }
+ Node* childToAppend() const { return m_childToAppend.get(); }
+
+private:
+ RefPtr<Node> m_parentNode;
+ RefPtr<Node> m_childToAppend;
+};
+
+} // namespace WebCore
+
+#endif // AppendNodeCommand_h
diff --git a/WebCore/editing/ApplyStyleCommand.cpp b/WebCore/editing/ApplyStyleCommand.cpp
new file mode 100644
index 0000000..ff0a04e
--- /dev/null
+++ b/WebCore/editing/ApplyStyleCommand.cpp
@@ -0,0 +1,1349 @@
+/*
+ * 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 "ApplyStyleCommand.h"
+
+#include "CSSComputedStyleDeclaration.h"
+#include "CSSParser.h"
+#include "CSSProperty.h"
+#include "CSSPropertyNames.h"
+#include "Document.h"
+#include "HTMLElement.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"
+
+namespace WebCore {
+
+using namespace HTMLNames;
+
+class StyleChange {
+public:
+ enum ELegacyHTMLStyles { DoNotUseLegacyHTMLStyles, UseLegacyHTMLStyles };
+
+ explicit StyleChange(CSSStyleDeclaration *, ELegacyHTMLStyles usesLegacyStyles=UseLegacyHTMLStyles);
+ StyleChange(CSSStyleDeclaration *, const Position &, ELegacyHTMLStyles usesLegacyStyles=UseLegacyHTMLStyles);
+
+ static ELegacyHTMLStyles styleModeForParseMode(bool);
+
+ String cssStyle() const { return m_cssStyle; }
+ bool applyBold() const { return m_applyBold; }
+ bool applyItalic() const { return m_applyItalic; }
+ 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 usesLegacyStyles() const { return m_usesLegacyStyles; }
+
+private:
+ void init(PassRefPtr<CSSStyleDeclaration>, const Position &);
+ bool checkForLegacyHTMLStyleChange(const CSSProperty *);
+ static bool currentlyHasStyle(const Position &, const CSSProperty *);
+
+ String m_cssStyle;
+ bool m_applyBold;
+ bool m_applyItalic;
+ String m_applyFontColor;
+ String m_applyFontFace;
+ String m_applyFontSize;
+ bool m_usesLegacyStyles;
+};
+
+
+
+StyleChange::StyleChange(CSSStyleDeclaration *style, ELegacyHTMLStyles usesLegacyStyles)
+ : m_applyBold(false), m_applyItalic(false), m_usesLegacyStyles(usesLegacyStyles)
+{
+ init(style, Position());
+}
+
+StyleChange::StyleChange(CSSStyleDeclaration *style, const Position &position, ELegacyHTMLStyles usesLegacyStyles)
+ : m_applyBold(false), m_applyItalic(false), m_usesLegacyStyles(usesLegacyStyles)
+{
+ init(style, position);
+}
+
+void StyleChange::init(PassRefPtr<CSSStyleDeclaration> style, const Position &position)
+{
+ RefPtr<CSSMutableStyleDeclaration> mutableStyle = style->makeMutable();
+
+ String styleText("");
+
+ DeprecatedValueListConstIterator<CSSProperty> end;
+ for (DeprecatedValueListConstIterator<CSSProperty> it = mutableStyle->valuesIterator(); it != end; ++it) {
+ const CSSProperty *property = &*it;
+
+ // If position is empty or the position passed in already has the
+ // style, just move on.
+ if (position.isNotNull() && currentlyHasStyle(position, property))
+ continue;
+
+ // Changing the whitespace style in a tab span would collapse the tab into a space.
+ if (property->id() == CSS_PROP_WHITE_SPACE && (isTabSpanTextNode(position.node()) || isTabSpanNode((position.node()))))
+ continue;
+
+ // If needed, figure out if this change is a legacy HTML style change.
+ if (m_usesLegacyStyles && checkForLegacyHTMLStyleChange(property))
+ continue;
+
+ // Add this property
+
+ if (property->id() == CSS_PROP__WEBKIT_TEXT_DECORATIONS_IN_EFFECT) {
+ // we have to special-case text decorations
+ CSSProperty alteredProperty = CSSProperty(CSS_PROP_TEXT_DECORATION, property->value(), property->isImportant());
+ styleText += alteredProperty.cssText();
+ } else
+ styleText += property->cssText();
+ }
+
+ // Save the result for later
+ m_cssStyle = styleText.stripWhiteSpace();
+}
+
+StyleChange::ELegacyHTMLStyles StyleChange::styleModeForParseMode(bool isQuirksMode)
+{
+ return isQuirksMode ? UseLegacyHTMLStyles : DoNotUseLegacyHTMLStyles;
+}
+
+bool StyleChange::checkForLegacyHTMLStyleChange(const CSSProperty *property)
+{
+ if (!property || !property->value()) {
+ return false;
+ }
+
+ String valueText(property->value()->cssText());
+ switch (property->id()) {
+ case CSS_PROP_FONT_WEIGHT:
+ if (equalIgnoringCase(valueText, "bold")) {
+ m_applyBold = true;
+ return true;
+ }
+ break;
+ case CSS_PROP_FONT_STYLE:
+ if (equalIgnoringCase(valueText, "italic") || equalIgnoringCase(valueText, "oblique")) {
+ m_applyItalic = true;
+ return true;
+ }
+ break;
+ case CSS_PROP_COLOR: {
+ RGBA32 rgba = 0;
+ CSSParser::parseColor(rgba, valueText);
+ Color color(rgba);
+ m_applyFontColor = color.name();
+ return true;
+ }
+ case CSS_PROP_FONT_FAMILY:
+ m_applyFontFace = valueText;
+ return true;
+ case CSS_PROP_FONT_SIZE:
+ if (property->value()->cssValueType() == CSSValue::CSS_PRIMITIVE_VALUE) {
+ CSSPrimitiveValue *value = static_cast<CSSPrimitiveValue *>(property->value());
+
+ if (value->primitiveType() < CSSPrimitiveValue::CSS_PX || value->primitiveType() > CSSPrimitiveValue::CSS_PC)
+ // Size keyword or relative unit.
+ return false;
+
+ float number = value->getFloatValue(CSSPrimitiveValue::CSS_PX);
+ if (number <= 9)
+ m_applyFontSize = "1";
+ else if (number <= 10)
+ m_applyFontSize = "2";
+ else if (number <= 13)
+ m_applyFontSize = "3";
+ else if (number <= 16)
+ m_applyFontSize = "4";
+ else if (number <= 18)
+ m_applyFontSize = "5";
+ else if (number <= 24)
+ m_applyFontSize = "6";
+ else
+ m_applyFontSize = "7";
+ // Huge quirk in Microsft Entourage is that they understand CSS font-size, but also write
+ // out legacy 1-7 values in font tags (I guess for mailers that are not CSS-savvy at all,
+ // like Eudora). Yes, they write out *both*. We need to write out both as well. Return false.
+ return false;
+ }
+ else {
+ // Can't make sense of the number. Put no font size.
+ return true;
+ }
+ }
+ return false;
+}
+
+bool StyleChange::currentlyHasStyle(const Position &pos, const CSSProperty *property)
+{
+ ASSERT(pos.isNotNull());
+ RefPtr<CSSComputedStyleDeclaration> style = pos.computedStyle();
+ RefPtr<CSSValue> value = style->getPropertyCSSValue(property->id(), DoNotUpdateLayout);
+ if (!value)
+ return false;
+ return equalIgnoringCase(value->cssText(), property->value()->cssText());
+}
+
+static String &styleSpanClassString()
+{
+ static 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->length() == 0) && elem->getAttribute(classAttr) == styleSpanClassString();
+}
+
+static bool isEmptyFontTag(const Node *node)
+{
+ if (!node || !node->hasTagName(fontTag))
+ return false;
+
+ const Element *elem = static_cast<const Element *>(node);
+ NamedAttrMap *map = elem->attributes(true); // true for read-only
+ return (!map || map->length() == 1) && elem->getAttribute(classAttr) == styleSpanClassString();
+}
+
+static PassRefPtr<Element> createFontElement(Document* document)
+{
+ ExceptionCode ec = 0;
+ RefPtr<Element> fontNode = document->createElementNS(xhtmlNamespaceURI, "font", ec);
+ ASSERT(ec == 0);
+ fontNode->setAttribute(classAttr, styleSpanClassString());
+ return fontNode.release();
+}
+
+PassRefPtr<HTMLElement> createStyleSpanElement(Document* document)
+{
+ ExceptionCode ec = 0;
+ RefPtr<Element> styleElement = document->createElementNS(xhtmlNamespaceURI, "span", ec);
+ ASSERT(ec == 0);
+ styleElement->setAttribute(classAttr, styleSpanClassString());
+ return static_pointer_cast<HTMLElement>(styleElement.release());
+}
+
+ApplyStyleCommand::ApplyStyleCommand(Document* document, CSSStyleDeclaration* style, EditAction editingAction, EPropertyLevel propertyLevel)
+ : CompositeEditCommand(document)
+ , m_style(style->makeMutable())
+ , 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)
+{
+}
+
+ApplyStyleCommand::ApplyStyleCommand(Document* document, CSSStyleDeclaration* style, const Position& start, const Position& end, EditAction editingAction, EPropertyLevel propertyLevel)
+ : CompositeEditCommand(document)
+ , m_style(style->makeMutable())
+ , m_editingAction(editingAction)
+ , m_propertyLevel(propertyLevel)
+ , m_start(start)
+ , m_end(end)
+ , m_useEndingSelection(false)
+ , m_styledInlineElement(0)
+ , m_removeOnly(false)
+{
+}
+
+ApplyStyleCommand::ApplyStyleCommand(Element* element, bool removeOnly, EditAction editingAction)
+ : CompositeEditCommand(element->document())
+ , m_style(new CSSMutableStyleDeclaration())
+ , 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)
+{
+}
+
+void ApplyStyleCommand::updateStartEnd(const Position& newStart, const Position& newEnd)
+{
+ ASSERT(Range::compareBoundaryPoints(newEnd, newStart) >= 0);
+
+ if (!m_useEndingSelection && (newStart != m_start || newEnd != m_end))
+ m_useEndingSelection = true;
+
+ setEndingSelection(Selection(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<CSSMutableStyleDeclaration> blockStyle = m_style->copyBlockProperties();
+ if (blockStyle->length())
+ applyBlockStyle(blockStyle.get());
+ // apply any remaining styles to the inline elements
+ // NOTE: hopefully, this string comparison is the same as checking for a non-null diff
+ if (blockStyle->length() < m_style->length() || m_styledInlineElement) {
+ RefPtr<CSSMutableStyleDeclaration> inlineStyle = m_style->copy();
+ applyRelativeFontStyleChange(inlineStyle.get());
+ blockStyle->diff(inlineStyle.get());
+ applyInlineStyle(inlineStyle.get());
+ }
+ break;
+ }
+ case ForceBlockProperties:
+ // Force all properties to be applied as block styles.
+ applyBlockStyle(m_style.get());
+ 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 (Range::compareBoundaryPoints(end, start) < 0) {
+ Position swap = start;
+ start = end;
+ end = swap;
+ }
+
+ VisiblePosition visibleStart(start);
+ VisiblePosition visibleEnd(end);
+ // 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 = new Range(document(), rangeStart, rangeCompliantEquivalent(visibleStart.deepEquivalent()));
+ RefPtr<Range> endRange = new Range(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(), StyleChange::styleModeForParseMode(document()->inCompatMode()));
+ if (styleChange.cssStyle().length() > 0 || m_removeOnly) {
+ Node* block = enclosingBlock(paragraphStart.deepEquivalent().node());
+ Node* newBlock = moveParagraphContentsToNewBlockIfNecessary(paragraphStart.deepEquivalent());
+ if (newBlock)
+ block = newBlock;
+ ASSERT(block->isHTMLElement());
+ if (block->isHTMLElement()) {
+ removeCSSStyle(style, static_cast<HTMLElement*>(block));
+ if (!m_removeOnly)
+ addBlockStyle(styleChange, static_cast<HTMLElement*>(block));
+ }
+ }
+ 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());
+}
+
+#define NoFontDelta (0.0f)
+#define MinimumFontSize (0.1f)
+
+void ApplyStyleCommand::applyRelativeFontStyleChange(CSSMutableStyleDeclaration *style)
+{
+ RefPtr<CSSValue> value = style->getPropertyCSSValue(CSS_PROP_FONT_SIZE);
+ if (value) {
+ // Explicit font size overrides any delta.
+ style->removeProperty(CSS_PROP__WEBKIT_FONT_SIZE_DELTA);
+ return;
+ }
+
+ // Get the adjustment amount out of the style.
+ value = style->getPropertyCSSValue(CSS_PROP__WEBKIT_FONT_SIZE_DELTA);
+ if (!value)
+ return;
+ float adjustment = NoFontDelta;
+ if (value->cssValueType() == CSSValue::CSS_PRIMITIVE_VALUE) {
+ CSSPrimitiveValue *primitiveValue = static_cast<CSSPrimitiveValue *>(value.get());
+ if (primitiveValue->primitiveType() == CSSPrimitiveValue::CSS_PX) {
+ // Only PX handled now. If we handle more types in the future, perhaps
+ // a switch statement here would be more appropriate.
+ adjustment = primitiveValue->getFloatValue();
+ }
+ }
+ style->removeProperty(CSS_PROP__WEBKIT_FONT_SIZE_DELTA);
+ if (adjustment == NoFontDelta)
+ return;
+
+ Position start = startPosition();
+ Position end = endPosition();
+ if (Range::compareBoundaryPoints(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.
+ bool splitStart = splitTextAtStartIfNeeded(start, end);
+ if (splitStart) {
+ start = startPosition();
+ end = endPosition();
+ }
+ bool splitEnd = splitTextAtEndIfNeeded(start, end);
+ if (splitEnd) {
+ 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.offset() >= 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.
+ DeprecatedPtrList<Node> unstyledSpans;
+
+ Node *lastStyledNode = 0;
+ for (Node *node = startNode; node != beyondEnd; node = node->traverseNextNode()) {
+ HTMLElement *elem = 0;
+ if (node->isHTMLElement()) {
+ // Only work on fully selected nodes.
+ if (!nodeFullySelected(node, start, end))
+ continue;
+ elem = 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());
+ insertNodeBefore(span.get(), node);
+ surroundNodeRangeWithElement(node, node, span.get());
+ elem = span.get();
+ } else {
+ // Only handle HTML elements and text nodes.
+ continue;
+ }
+ lastStyledNode = node;
+
+ CSSMutableStyleDeclaration* inlineStyleDecl = elem->getInlineStyleDecl();
+ float currentFontSize = computedFontSize(node);
+ float desiredFontSize = max(MinimumFontSize, startingFontSizes.get(node) + adjustment);
+ RefPtr<CSSValue> value = inlineStyleDecl->getPropertyCSSValue(CSS_PROP_FONT_SIZE);
+ if (value) {
+ inlineStyleDecl->removeProperty(CSS_PROP_FONT_SIZE, true);
+ currentFontSize = computedFontSize(node);
+ }
+ if (currentFontSize != desiredFontSize) {
+ inlineStyleDecl->setProperty(CSS_PROP_FONT_SIZE, String::number(desiredFontSize) + "px", false, false);
+ setNodeAttribute(elem, styleAttr, inlineStyleDecl->cssText());
+ }
+ if (inlineStyleDecl->length() == 0) {
+ removeNodeAttribute(elem, styleAttr);
+ if (isUnstyledStyleSpan(elem))
+ unstyledSpans.append(elem);
+ }
+ }
+
+ for (DeprecatedPtrListIterator<Node> it(unstyledSpans); it.current(); ++it)
+ removeNodePreservingChildren(it.current());
+}
+
+#undef NoFontDelta
+#undef MinimumFontSize
+
+static Node* dummySpanAncestorForNode(const Node* node)
+{
+ while (node && !isStyleSpan(node))
+ node = node->parent();
+
+ return node ? node->parent() : 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;
+ }
+}
+
+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 (Range::compareBoundaryPoints(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 = splitTextElementAtStartIfNeeded(start, end);
+ if (splitStart) {
+ start = startPosition();
+ end = endPosition();
+ startDummySpanAncestor = dummySpanAncestorForNode(start.node());
+ }
+
+ // split the end node and containing element if the selection ends inside of it
+ bool splitEnd = splitTextElementAtEndIfNeeded(start, end);
+ if (splitEnd) {
+ 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
+ removeInlineStyle(style, start.upstream(), end);
+ start = startPosition();
+ end = endPosition();
+
+ if (splitStart) {
+ bool mergedStart = mergeStartWithPreviousIfIdentical(start, end);
+ if (mergedStart) {
+ 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();
+
+ Node* node = start.node();
+
+ bool rangeIsEmpty = false;
+
+ if (start.offset() >= caretMaxOffset(start.node())) {
+ node = node->traverseNextNode();
+ Position newStart = Position(node, 0);
+ if (Range::compareBoundaryPoints(end, newStart) < 0)
+ rangeIsEmpty = true;
+ }
+
+ if (!rangeIsEmpty) {
+ // 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))
+ end = positionAfterNode(start.node());
+ // Add the style to selected inline runs.
+ Node* pastEnd = Range(document(), rangeCompliantEquivalent(start), rangeCompliantEquivalent(end)).pastEndNode();
+ for (Node* next; node && node != pastEnd; node = next) {
+
+ 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.
+ if (end.node()->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 (editingIgnoresContent(node)) {
+ next = node->traverseNextSibling();
+ continue;
+ }
+ continue;
+ }
+
+ Node* runStart = node;
+ // Find the end of the run.
+ Node* sibling = node->nextSibling();
+ while (sibling && sibling != pastEnd && (!sibling->isElementNode() || sibling->hasTagName(brTag)) && !isBlock(sibling)) {
+ node = sibling;
+ sibling = node->nextSibling();
+ }
+ // Recompute next, since node has changed.
+ next = node->traverseNextNode();
+ // Apply the style to the run.
+ addInlineStyleIfNeeded(style, runStart, node);
+ }
+ }
+
+ // Remove dummy style spans created by splitting text elements.
+ cleanupUnstyledAppleStyleSpans(startDummySpanAncestor);
+ if (endDummySpanAncestor != startDummySpanAncestor)
+ cleanupUnstyledAppleStyleSpans(endDummySpanAncestor);
+}
+
+bool ApplyStyleCommand::isHTMLStyleNode(CSSMutableStyleDeclaration *style, HTMLElement *elem)
+{
+ DeprecatedValueListConstIterator<CSSProperty> end;
+ for (DeprecatedValueListConstIterator<CSSProperty> it = style->valuesIterator(); it != end; ++it) {
+ switch ((*it).id()) {
+ case CSS_PROP_FONT_WEIGHT:
+ if (elem->hasLocalName(bTag))
+ return true;
+ break;
+ case CSS_PROP_FONT_STYLE:
+ if (elem->hasLocalName(iTag))
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void ApplyStyleCommand::removeHTMLStyleNode(HTMLElement *elem)
+{
+ // This node can be removed.
+ // EDIT FIXME: This does not handle the case where the node
+ // has attributes. But how often do people add attributes to <B> tags?
+ // Not so often I think.
+ ASSERT(elem);
+ removeNodePreservingChildren(elem);
+}
+
+void ApplyStyleCommand::removeHTMLFontStyle(CSSMutableStyleDeclaration *style, HTMLElement *elem)
+{
+ ASSERT(style);
+ ASSERT(elem);
+
+ if (!elem->hasLocalName(fontTag))
+ return;
+
+ DeprecatedValueListConstIterator<CSSProperty> end;
+ for (DeprecatedValueListConstIterator<CSSProperty> it = style->valuesIterator(); it != end; ++it) {
+ switch ((*it).id()) {
+ case CSS_PROP_COLOR:
+ removeNodeAttribute(elem, colorAttr);
+ break;
+ case CSS_PROP_FONT_FAMILY:
+ removeNodeAttribute(elem, faceAttr);
+ break;
+ case CSS_PROP_FONT_SIZE:
+ removeNodeAttribute(elem, sizeAttr);
+ break;
+ }
+ }
+
+ if (isEmptyFontTag(elem))
+ removeNodePreservingChildren(elem);
+}
+
+void ApplyStyleCommand::removeCSSStyle(CSSMutableStyleDeclaration *style, HTMLElement *elem)
+{
+ ASSERT(style);
+ ASSERT(elem);
+
+ CSSMutableStyleDeclaration *decl = elem->inlineStyleDecl();
+ if (!decl)
+ return;
+
+ DeprecatedValueListConstIterator<CSSProperty> end;
+ for (DeprecatedValueListConstIterator<CSSProperty> it = style->valuesIterator(); it != end; ++it) {
+ int propertyID = (*it).id();
+ RefPtr<CSSValue> value = decl->getPropertyCSSValue(propertyID);
+ if (value && (propertyID != CSS_PROP_WHITE_SPACE || !isTabSpanNode(elem)))
+ removeCSSProperty(decl, propertyID);
+ }
+
+ if (isUnstyledStyleSpan(elem))
+ removeNodePreservingChildren(elem);
+}
+
+void ApplyStyleCommand::removeBlockStyle(CSSMutableStyleDeclaration *style, const Position &start, const Position &end)
+{
+ ASSERT(start.isNotNull());
+ ASSERT(end.isNotNull());
+ ASSERT(start.node()->inDocument());
+ ASSERT(end.node()->inDocument());
+ ASSERT(Range::compareBoundaryPoints(start, end) <= 0);
+
+}
+
+static bool hasTextDecorationProperty(Node *node)
+{
+ if (!node->isElementNode())
+ return false;
+
+ Element *element = static_cast<Element *>(node);
+ CSSComputedStyleDeclaration style(element);
+ RefPtr<CSSValue> value = style.getPropertyCSSValue(CSS_PROP_TEXT_DECORATION, DoNotUpdateLayout);
+ return value && !equalIgnoringCase(value->cssText(), "none");
+}
+
+static Node* highestAncestorWithTextDecoration(Node *node)
+{
+ Node *result = NULL;
+
+ for (Node *n = node; n; n = n->parentNode()) {
+ if (hasTextDecorationProperty(n))
+ result = n;
+ }
+
+ return result;
+}
+
+PassRefPtr<CSSMutableStyleDeclaration> ApplyStyleCommand::extractTextDecorationStyle(Node* node)
+{
+ ASSERT(node);
+ ASSERT(node->isElementNode());
+
+ // non-html elements not handled yet
+ if (!node->isHTMLElement())
+ return 0;
+
+ HTMLElement *element = static_cast<HTMLElement *>(node);
+ RefPtr<CSSMutableStyleDeclaration> style = element->inlineStyleDecl();
+ if (!style)
+ return 0;
+
+ int properties[1] = { CSS_PROP_TEXT_DECORATION };
+ RefPtr<CSSMutableStyleDeclaration> textDecorationStyle = style->copyPropertiesInSet(properties, 1);
+
+ RefPtr<CSSValue> property = style->getPropertyCSSValue(CSS_PROP_TEXT_DECORATION);
+ if (property && !equalIgnoringCase(property->cssText(), "none"))
+ removeCSSProperty(style.get(), CSS_PROP_TEXT_DECORATION);
+
+ return textDecorationStyle.release();
+}
+
+PassRefPtr<CSSMutableStyleDeclaration> ApplyStyleCommand::extractAndNegateTextDecorationStyle(Node *node)
+{
+ ASSERT(node);
+ ASSERT(node->isElementNode());
+
+ // non-html elements not handled yet
+ if (!node->isHTMLElement())
+ return 0;
+
+ HTMLElement *element = static_cast<HTMLElement *>(node);
+ RefPtr<CSSComputedStyleDeclaration> computedStyle = new CSSComputedStyleDeclaration(element);
+ ASSERT(computedStyle);
+
+ int properties[1] = { CSS_PROP_TEXT_DECORATION };
+ RefPtr<CSSMutableStyleDeclaration> textDecorationStyle = computedStyle->copyPropertiesInSet(properties, 1);
+
+ RefPtr<CSSValue> property = computedStyle->getPropertyCSSValue(CSS_PROP_TEXT_DECORATION);
+ if (property && !equalIgnoringCase(property->cssText(), "none")) {
+ RefPtr<CSSMutableStyleDeclaration> newStyle = textDecorationStyle->copy();
+ newStyle->setProperty(CSS_PROP_TEXT_DECORATION, "none");
+ applyTextDecorationStyle(node, newStyle.get());
+ }
+
+ return textDecorationStyle.release();
+}
+
+void ApplyStyleCommand::applyTextDecorationStyle(Node *node, CSSMutableStyleDeclaration *style)
+{
+ ASSERT(node);
+
+ if (!style || !style->cssText().length())
+ return;
+
+ if (node->isTextNode()) {
+ RefPtr<HTMLElement> styleSpan = createStyleSpanElement(document());
+ insertNodeBefore(styleSpan.get(), node);
+ surroundNodeRangeWithElement(node, node, styleSpan.get());
+ node = styleSpan.get();
+ }
+
+ if (!node->isElementNode())
+ return;
+
+ HTMLElement *element = static_cast<HTMLElement *>(node);
+
+ StyleChange styleChange(style, Position(element, 0), StyleChange::styleModeForParseMode(document()->inCompatMode()));
+ if (styleChange.cssStyle().length() > 0) {
+ String cssText = styleChange.cssStyle();
+ CSSMutableStyleDeclaration *decl = element->inlineStyleDecl();
+ if (decl)
+ cssText += decl->cssText();
+ setNodeAttribute(element, styleAttr, cssText);
+ }
+}
+
+void ApplyStyleCommand::pushDownTextDecorationStyleAroundNode(Node *node, const Position &start, const Position &end, bool force)
+{
+ Node *highestAncestor = highestAncestorWithTextDecoration(node);
+
+ if (highestAncestor) {
+ Node *nextCurrent;
+ Node *nextChild;
+ for (Node *current = highestAncestor; current != node; current = nextCurrent) {
+ ASSERT(current);
+
+ nextCurrent = NULL;
+
+ RefPtr<CSSMutableStyleDeclaration> decoration = force ? extractAndNegateTextDecorationStyle(current) : extractTextDecorationStyle(current);
+
+ for (Node *child = current->firstChild(); child; child = nextChild) {
+ nextChild = child->nextSibling();
+
+ if (node == child) {
+ nextCurrent = child;
+ } else if (node->isDescendantOf(child)) {
+ applyTextDecorationStyle(child, decoration.get());
+ nextCurrent = child;
+ } else {
+ applyTextDecorationStyle(child, decoration.get());
+ }
+ }
+ }
+ }
+}
+
+void ApplyStyleCommand::pushDownTextDecorationStyleAtBoundaries(const Position &start, const Position &end)
+{
+ // We need to work in two passes. First we push down any inline
+ // styles that set text decoration. Then we look for any remaining
+ // styles (caused by stylesheets) and explicitly negate text
+ // decoration while pushing down.
+
+ pushDownTextDecorationStyleAroundNode(start.node(), start, end, false);
+ updateLayout();
+ pushDownTextDecorationStyleAroundNode(start.node(), start, end, true);
+
+ pushDownTextDecorationStyleAroundNode(end.node(), start, end, false);
+ updateLayout();
+ pushDownTextDecorationStyleAroundNode(end.node(), start, end, true);
+}
+
+static int maxRangeOffset(Node *n)
+{
+ if (n->offsetInCharacters())
+ return n->maxCharacterOffset();
+
+ if (n->isElementNode())
+ return n->childNodeCount();
+
+ return 1;
+}
+
+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(Range::compareBoundaryPoints(start, end) <= 0);
+
+ RefPtr<CSSValue> textDecorationSpecialProperty = style->getPropertyCSSValue(CSS_PROP__WEBKIT_TEXT_DECORATIONS_IN_EFFECT);
+
+ if (textDecorationSpecialProperty) {
+ pushDownTextDecorationStyleAtBoundaries(start.downstream(), end.upstream());
+ style = style->copy();
+ style->setProperty(CSS_PROP_TEXT_DECORATION, textDecorationSpecialProperty->cssText(), style->getPropertyPriority(CSS_PROP__WEBKIT_TEXT_DECORATIONS_IN_EFFECT));
+ }
+
+ // 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.
+ Position s = start;
+ Position e = end;
+
+ Node *node = start.node();
+ while (node) {
+ Node *next = node->traverseNextNode();
+ if (node->isHTMLElement() && nodeFullySelected(node, start, end)) {
+ HTMLElement *elem = static_cast<HTMLElement *>(node);
+ Node *prev = elem->traversePreviousNodePostOrder();
+ Node *next = elem->traverseNextNode();
+ if (m_styledInlineElement && elem->hasTagName(m_styledInlineElement->tagQName()))
+ removeNodePreservingChildren(elem);
+ if (isHTMLStyleNode(style.get(), elem))
+ removeHTMLStyleNode(elem);
+ else {
+ removeHTMLFontStyle(style.get(), elem);
+ removeCSSStyle(style.get(), elem);
+ }
+ 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.offset() <= 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.offset() >= maxRangeOffset(e.node()));
+ e = Position(prev, maxRangeOffset(prev));
+ }
+ }
+ }
+ if (node == end.node())
+ break;
+ node = next;
+ }
+
+ ASSERT(s.node()->inDocument());
+ ASSERT(e.node()->inDocument());
+ 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 Range::compareBoundaryPoints(node, 0, start.node(), start.offset()) >= 0 &&
+ Range::compareBoundaryPoints(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 = Range::compareBoundaryPoints(pos, start) < 0;
+ bool isFullyAfterEnd = Range::compareBoundaryPoints(node, 0, end.node(), end.offset()) > 0;
+
+ return isFullyBeforeStart || isFullyAfterEnd;
+}
+
+
+bool ApplyStyleCommand::splitTextAtStartIfNeeded(const Position &start, const Position &end)
+{
+ if (start.node()->isTextNode() && start.offset() > caretMinOffset(start.node()) && start.offset() < caretMaxOffset(start.node())) {
+ int endOffsetAdjustment = start.node() == end.node() ? start.offset() : 0;
+ Text *text = static_cast<Text *>(start.node());
+ splitTextNode(text, start.offset());
+ updateStartEnd(Position(start.node(), 0), Position(end.node(), end.offset() - endOffsetAdjustment));
+ return true;
+ }
+ return false;
+}
+
+bool ApplyStyleCommand::splitTextAtEndIfNeeded(const Position &start, const Position &end)
+{
+ if (end.node()->isTextNode() && end.offset() > caretMinOffset(end.node()) && end.offset() < caretMaxOffset(end.node())) {
+ Text *text = static_cast<Text *>(end.node());
+ splitTextNode(text, end.offset());
+
+ Node *prevNode = text->previousSibling();
+ ASSERT(prevNode);
+ Node *startNode = start.node() == end.node() ? prevNode : start.node();
+ ASSERT(startNode);
+ updateStartEnd(Position(startNode, start.offset()), Position(prevNode, caretMaxOffset(prevNode)));
+ return true;
+ }
+ return false;
+}
+
+bool ApplyStyleCommand::splitTextElementAtStartIfNeeded(const Position &start, const Position &end)
+{
+ if (start.node()->isTextNode() && start.offset() > caretMinOffset(start.node()) && start.offset() < caretMaxOffset(start.node())) {
+ int endOffsetAdjustment = start.node() == end.node() ? start.offset() : 0;
+ Text *text = static_cast<Text *>(start.node());
+ splitTextNodeContainingElement(text, start.offset());
+
+ updateStartEnd(Position(start.node()->parentNode(), start.node()->nodeIndex()), Position(end.node(), end.offset() - endOffsetAdjustment));
+ return true;
+ }
+ return false;
+}
+
+bool ApplyStyleCommand::splitTextElementAtEndIfNeeded(const Position &start, const Position &end)
+{
+ if (end.node()->isTextNode() && end.offset() > caretMinOffset(end.node()) && end.offset() < caretMaxOffset(end.node())) {
+ Text *text = static_cast<Text *>(end.node());
+ splitTextNodeContainingElement(text, end.offset());
+
+ Node *prevNode = text->parent()->previousSibling()->lastChild();
+ ASSERT(prevNode);
+ Node *startNode = start.node() == end.node() ? prevNode : start.node();
+ ASSERT(startNode);
+ updateStartEnd(Position(startNode, start.offset()), Position(prevNode->parent(), prevNode->nodeIndex() + 1));
+ return true;
+ }
+ return false;
+}
+
+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;
+
+ NamedAttrMap *firstMap = firstElement->attributes();
+ NamedAttrMap *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.offset();
+
+ if (isAtomicNode(start.node())) {
+ if (start.offset() != 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()->parent();
+ 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.offset() + endOffsetAdjustment));
+ return true;
+ }
+
+ return false;
+}
+
+bool ApplyStyleCommand::mergeEndWithNextIfIdentical(const Position &start, const Position &end)
+{
+ Node *endNode = end.node();
+ int endOffset = end.offset();
+
+ if (isAtomicNode(endNode)) {
+ if (endOffset < caretMaxOffset(endNode))
+ return false;
+
+ unsigned parentLastOffset = end.node()->parent()->childNodes()->length() - 1;
+ if (end.node()->nextSibling())
+ return false;
+
+ endNode = end.node()->parent();
+ 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.offset()), Position(nextElement, endOffset));
+ return true;
+ }
+
+ return false;
+}
+
+void ApplyStyleCommand::surroundNodeRangeWithElement(Node *startNode, Node *endNode, Element *element)
+{
+ ASSERT(startNode);
+ ASSERT(endNode);
+ ASSERT(element);
+
+ Node *node = startNode;
+ while (1) {
+ Node *next = node->traverseNextNode();
+ if (node->childNodeCount() == 0 && node->renderer() && node->renderer()->isInline()) {
+ removeNode(node);
+ appendNode(node, element);
+ }
+ if (node == endNode)
+ break;
+ node = next;
+ }
+}
+
+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, Node *startNode, Node *endNode)
+{
+ if (m_removeOnly)
+ return;
+
+ StyleChange styleChange(style, Position(startNode, 0), StyleChange::styleModeForParseMode(document()->inCompatMode()));
+ ExceptionCode ec = 0;
+
+ //
+ // Font tags need to go outside of CSS so that CSS font sizes override leagcy font sizes.
+ //
+ if (styleChange.applyFontColor() || styleChange.applyFontFace() || styleChange.applyFontSize()) {
+ RefPtr<Element> fontElement = createFontElement(document());
+ ASSERT(ec == 0);
+ insertNodeBefore(fontElement.get(), startNode);
+ 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() > 0) {
+ RefPtr<Element> styleElement = createStyleSpanElement(document());
+ styleElement->setAttribute(styleAttr, styleChange.cssStyle());
+ insertNodeBefore(styleElement.get(), startNode);
+ surroundNodeRangeWithElement(startNode, endNode, styleElement.get());
+ }
+
+ if (styleChange.applyBold()) {
+ RefPtr<Element> boldElement = document()->createElementNS(xhtmlNamespaceURI, "b", ec);
+ ASSERT(ec == 0);
+ insertNodeBefore(boldElement.get(), startNode);
+ surroundNodeRangeWithElement(startNode, endNode, boldElement.get());
+ }
+
+ if (styleChange.applyItalic()) {
+ RefPtr<Element> italicElement = document()->createElementNS(xhtmlNamespaceURI, "i", ec);
+ ASSERT(ec == 0);
+ insertNodeBefore(italicElement.get(), startNode);
+ surroundNodeRangeWithElement(startNode, endNode, italicElement.get());
+ }
+
+ if (m_styledInlineElement) {
+ RefPtr<Element> clonedElement = static_pointer_cast<Element>(m_styledInlineElement->cloneNode(false));
+ insertNodeBefore(clonedElement.get(), startNode);
+ surroundNodeRangeWithElement(startNode, endNode, clonedElement.get());
+ }
+}
+
+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(CSS_PROP_FONT_SIZE));
+ 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.offset());
+ if (next == end.node())
+ newEnd = Position(childText, childText->length() + end.offset());
+ 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/WebCore/editing/ApplyStyleCommand.h b/WebCore/editing/ApplyStyleCommand.h
new file mode 100644
index 0000000..eafec59
--- /dev/null
+++ b/WebCore/editing/ApplyStyleCommand.h
@@ -0,0 +1,102 @@
+/*
+ * 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.
+ */
+
+#ifndef ApplyStyleCommand_h
+#define ApplyStyleCommand_h
+
+#include "CompositeEditCommand.h"
+
+namespace WebCore {
+
+class HTMLElement;
+class StyleChange;
+
+class ApplyStyleCommand : public CompositeEditCommand {
+public:
+ enum EPropertyLevel { PropertyDefault, ForceBlockProperties };
+
+ ApplyStyleCommand(Document*, CSSStyleDeclaration*, EditAction = EditActionChangeAttributes, EPropertyLevel = PropertyDefault);
+ ApplyStyleCommand(Document*, CSSStyleDeclaration*, const Position& start, const Position& end, EditAction = EditActionChangeAttributes, EPropertyLevel = PropertyDefault);
+ ApplyStyleCommand(Element*, bool = false, EditAction = EditActionChangeAttributes);
+
+ virtual void doApply();
+ virtual EditAction editingAction() const;
+
+ CSSMutableStyleDeclaration* style() const { return m_style.get(); }
+
+private:
+ // style-removal helpers
+ bool isHTMLStyleNode(CSSMutableStyleDeclaration*, HTMLElement*);
+ void removeHTMLStyleNode(HTMLElement*);
+ void removeHTMLFontStyle(CSSMutableStyleDeclaration*, HTMLElement*);
+ void removeCSSStyle(CSSMutableStyleDeclaration*, HTMLElement*);
+ void removeBlockStyle(CSSMutableStyleDeclaration*, const Position& start, const Position& end);
+ 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;
+ PassRefPtr<CSSMutableStyleDeclaration> extractTextDecorationStyle(Node*);
+ PassRefPtr<CSSMutableStyleDeclaration> extractAndNegateTextDecorationStyle(Node*);
+ void applyTextDecorationStyle(Node*, CSSMutableStyleDeclaration *style);
+ void pushDownTextDecorationStyleAroundNode(Node*, const Position& start, const Position& end, bool force);
+ void pushDownTextDecorationStyleAtBoundaries(const Position& start, const Position& end);
+
+ // style-application helpers
+ void applyBlockStyle(CSSMutableStyleDeclaration*);
+ void applyRelativeFontStyleChange(CSSMutableStyleDeclaration*);
+ void applyInlineStyle(CSSMutableStyleDeclaration*);
+ void addBlockStyle(const StyleChange&, HTMLElement*);
+ void addInlineStyleIfNeeded(CSSMutableStyleDeclaration*, Node* start, Node* end);
+ bool splitTextAtStartIfNeeded(const Position& start, const Position& end);
+ bool splitTextAtEndIfNeeded(const Position& start, const Position& end);
+ bool splitTextElementAtStartIfNeeded(const Position& start, const Position& end);
+ bool splitTextElementAtEndIfNeeded(const Position& start, const Position& end);
+ bool mergeStartWithPreviousIfIdentical(const Position& start, const Position& end);
+ bool mergeEndWithNextIfIdentical(const Position& start, const Position& end);
+ void cleanupUnstyledAppleStyleSpans(Node* dummySpanAncestor);
+
+ void surroundNodeRangeWithElement(Node* start, Node* end, Element* element);
+ float computedFontSize(const Node*);
+ void joinChildTextNodes(Node*, const Position& start, const Position& end);
+
+ void updateStartEnd(const Position& newStart, const Position& newEnd);
+ Position startPosition();
+ Position endPosition();
+
+ RefPtr<CSSMutableStyleDeclaration> m_style;
+ EditAction m_editingAction;
+ EPropertyLevel m_propertyLevel;
+ Position m_start;
+ Position m_end;
+ bool m_useEndingSelection;
+ RefPtr<Element> m_styledInlineElement;
+ bool m_removeOnly;
+};
+
+bool isStyleSpan(const Node*);
+PassRefPtr<HTMLElement> createStyleSpanElement(Document*);
+
+} // namespace WebCore
+
+#endif
diff --git a/WebCore/editing/BreakBlockquoteCommand.cpp b/WebCore/editing/BreakBlockquoteCommand.cpp
new file mode 100644
index 0000000..0c56766
--- /dev/null
+++ b/WebCore/editing/BreakBlockquoteCommand.cpp
@@ -0,0 +1,179 @@
+/*
+ * 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 "Element.h"
+#include "HTMLNames.h"
+#include "Text.h"
+#include "VisiblePosition.h"
+#include "htmlediting.h"
+#include "RenderListItem.h"
+
+namespace WebCore {
+
+using namespace HTMLNames;
+
+BreakBlockquoteCommand::BreakBlockquoteCommand(Document *document)
+ : CompositeEditCommand(document)
+{
+}
+
+void BreakBlockquoteCommand::doApply()
+{
+ Selection selection = endingSelection();
+ if (selection.isNone())
+ return;
+
+ // Delete the current selection.
+ Position pos = selection.start();
+ EAffinity affinity = selection.affinity();
+ if (selection.isRange()) {
+ deleteSelection(false, false);
+ pos = endingSelection().start().upstream();
+ affinity = endingSelection().affinity();
+ }
+
+ // Find the top-most blockquote from the start.
+ Node *startNode = pos.node();
+ Node *topBlockquote = 0;
+ for (Node *node = startNode->parentNode(); node; node = node->parentNode()) {
+ if (isMailBlockquote(node))
+ topBlockquote = node;
+ }
+ if (!topBlockquote || !topBlockquote->parentNode())
+ return;
+
+ // Insert a break after the top blockquote.
+ RefPtr<Element> breakNode = createBreakElement(document());
+ insertNodeAfter(breakNode.get(), topBlockquote);
+
+ if (!isLastVisiblePositionInNode(VisiblePosition(pos, affinity), topBlockquote)) {
+
+ Node *newStartNode = 0;
+ // Split at pos if in the middle of a text node.
+ if (startNode->isTextNode()) {
+ Text *textNode = static_cast<Text *>(startNode);
+ if ((unsigned)pos.offset() >= textNode->length()) {
+ newStartNode = startNode->traverseNextNode();
+ ASSERT(newStartNode);
+ } else if (pos.offset() > 0)
+ splitTextNode(textNode, pos.offset());
+ } else if (startNode->hasTagName(brTag)) {
+ newStartNode = startNode->traverseNextNode();
+ ASSERT(newStartNode);
+ } else if (pos.offset() > 0) {
+ newStartNode = startNode->traverseNextNode();
+ ASSERT(newStartNode);
+ }
+
+ // If a new start node was determined, find a new top block quote.
+ if (newStartNode) {
+ startNode = newStartNode;
+ topBlockquote = 0;
+ for (Node *node = startNode->parentNode(); node; node = node->parentNode()) {
+ if (isMailBlockquote(node))
+ topBlockquote = node;
+ }
+ if (!topBlockquote || !topBlockquote->parentNode()) {
+ setEndingSelection(Selection(VisiblePosition(Position(startNode, 0))));
+ return;
+ }
+ }
+
+ // Build up list of ancestors in between the start node and the top blockquote.
+ Vector<Node*> ancestors;
+ for (Node* node = startNode->parentNode(); node != topBlockquote; node = node->parentNode())
+ ancestors.append(node);
+
+ // Insert a clone of the top blockquote after the break.
+ RefPtr<Node> clonedBlockquote = topBlockquote->cloneNode(false);
+ 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<Node> clonedAncestor = clonedBlockquote;
+ for (size_t i = ancestors.size(); i != 0; --i) {
+ RefPtr<Node> clonedChild = ancestors[i - 1]->cloneNode(false); // shallow clone
+ // 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(static_cast<RenderListItem*>(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;
+ }
+
+ // Hold open startNode's original parent if we emptied it
+ if (!ancestors.isEmpty()) {
+ addBlockPlaceholderIfNeeded(ancestors.first());
+
+ // 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.
+ Node* ancestor;
+ Node* clonedParent;
+ for (ancestor = ancestors.first(), clonedParent = clonedAncestor->parentNode();
+ ancestor && ancestor != topBlockquote;
+ ancestor = ancestor->parentNode(), clonedParent = clonedParent->parentNode()) {
+ moveNode = ancestor->nextSibling();
+ while (moveNode) {
+ Node *next = moveNode->nextSibling();
+ removeNode(moveNode);
+ appendNode(moveNode, clonedParent);
+ moveNode = next;
+ }
+ }
+ }
+
+ // Make sure the cloned block quote renders.
+ addBlockPlaceholderIfNeeded(clonedBlockquote.get());
+ }
+
+ // Put the selection right before the break.
+ setEndingSelection(Selection(Position(breakNode.get(), 0), DOWNSTREAM));
+ rebalanceWhitespace();
+}
+
+} // namespace WebCore
diff --git a/WebCore/editing/BreakBlockquoteCommand.h b/WebCore/editing/BreakBlockquoteCommand.h
new file mode 100644
index 0000000..466bf6c
--- /dev/null
+++ b/WebCore/editing/BreakBlockquoteCommand.h
@@ -0,0 +1,41 @@
+/*
+ * 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.
+ */
+
+#ifndef BreakBlockquoteCommand_h
+#define BreakBlockquoteCommand_h
+
+#include "CompositeEditCommand.h"
+
+namespace WebCore {
+
+class BreakBlockquoteCommand : public CompositeEditCommand {
+public:
+ BreakBlockquoteCommand(Document*);
+ virtual void doApply();
+};
+
+} // namespace WebCore
+
+#endif
diff --git a/WebCore/editing/CompositeEditCommand.cpp b/WebCore/editing/CompositeEditCommand.cpp
new file mode 100644
index 0000000..dae1d1b
--- /dev/null
+++ b/WebCore/editing/CompositeEditCommand.cpp
@@ -0,0 +1,938 @@
+/*
+ * 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 "CompositeEditCommand.h"
+
+#include "AppendNodeCommand.h"
+#include "ApplyStyleCommand.h"
+#include "CSSComputedStyleDeclaration.h"
+#include "CSSMutableStyleDeclaration.h"
+#include "CharacterNames.h"
+#include "DeleteFromTextNodeCommand.h"
+#include "DeleteSelectionCommand.h"
+#include "Document.h"
+#include "DocumentFragment.h"
+#include "EditorInsertAction.h"
+#include "Element.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 "RemoveNodeAttributeCommand.h"
+#include "RemoveNodeCommand.h"
+#include "RemoveNodePreservingChildrenCommand.h"
+#include "ReplaceSelectionCommand.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)
+{
+}
+
+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(CSSStyleDeclaration* style, EditAction editingAction)
+{
+ applyCommandToComposite(new ApplyStyleCommand(document(), style, editingAction));
+}
+
+void CompositeEditCommand::applyStyle(CSSStyleDeclaration* style, const Position& start, const Position& end, EditAction editingAction)
+{
+ applyCommandToComposite(new ApplyStyleCommand(document(), style, start, end, editingAction));
+}
+
+void CompositeEditCommand::applyStyledElement(Element* element)
+{
+ applyCommandToComposite(new ApplyStyleCommand(element, false));
+}
+
+void CompositeEditCommand::removeStyledElement(Element* element)
+{
+ applyCommandToComposite(new ApplyStyleCommand(element, true));
+}
+
+void CompositeEditCommand::insertParagraphSeparator(bool useDefaultParagraphElement)
+{
+ applyCommandToComposite(new InsertParagraphSeparatorCommand(document(), useDefaultParagraphElement));
+}
+
+void CompositeEditCommand::insertLineBreak()
+{
+ applyCommandToComposite(new InsertLineBreakCommand(document()));
+}
+
+void CompositeEditCommand::insertNodeBefore(Node* insertChild, Node* refChild)
+{
+ ASSERT(!refChild->hasTagName(bodyTag));
+ applyCommandToComposite(new InsertNodeBeforeCommand(insertChild, refChild));
+}
+
+void CompositeEditCommand::insertNodeAfter(Node* insertChild, Node* refChild)
+{
+ ASSERT(!refChild->hasTagName(bodyTag));
+ if (refChild->parentNode()->lastChild() == refChild)
+ appendNode(insertChild, refChild->parentNode());
+ else {
+ ASSERT(refChild->nextSibling());
+ insertNodeBefore(insertChild, refChild->nextSibling());
+ }
+}
+
+void CompositeEditCommand::insertNodeAt(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.offset();
+
+ 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, refChild);
+ } else if (caretMinOffset(refChild) >= offset) {
+ insertNodeBefore(insertChild, refChild);
+ } else if (refChild->isTextNode() && caretMaxOffset(refChild) > offset) {
+ splitTextNode(static_cast<Text *>(refChild), offset);
+ insertNodeBefore(insertChild, refChild);
+ } else {
+ insertNodeAfter(insertChild, refChild);
+ }
+}
+
+void CompositeEditCommand::appendNode(Node* newChild, Node* parent)
+{
+ ASSERT(canHaveChildrenForEditing(parent));
+ applyCommandToComposite(new AppendNodeCommand(parent, newChild));
+}
+
+void CompositeEditCommand::removeChildrenInRange(Node* node, int from, int to)
+{
+ Node* nodeToRemove = node->childNode(from);
+ for (int i = from; i < to; i++) {
+ ASSERT(nodeToRemove);
+ Node* next = nodeToRemove->nextSibling();
+ removeNode(nodeToRemove);
+ nodeToRemove = next;
+ }
+}
+
+void CompositeEditCommand::removeNode(Node* removeChild)
+{
+ applyCommandToComposite(new RemoveNodeCommand(removeChild));
+}
+
+void CompositeEditCommand::removeNodePreservingChildren(Node* removeChild)
+{
+ applyCommandToComposite(new RemoveNodePreservingChildrenCommand(removeChild));
+}
+
+void CompositeEditCommand::removeNodeAndPruneAncestors(Node* node)
+{
+ RefPtr<Node> parent = node->parentNode();
+ removeNode(node);
+ prune(parent);
+}
+
+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<Node> next = node->parentNode();
+ removeNode(node.get());
+ node = next;
+ }
+}
+
+void CompositeEditCommand::splitTextNode(Text *text, int offset)
+{
+ applyCommandToComposite(new SplitTextNodeCommand(text, offset));
+}
+
+void CompositeEditCommand::splitElement(Element* element, Node* atChild)
+{
+ applyCommandToComposite(new SplitElementCommand(element, atChild));
+}
+
+void CompositeEditCommand::mergeIdenticalElements(Element* first, Element* second)
+{
+ ASSERT(!first->isDescendantOf(second) && second != first);
+ if (first->nextSibling() != second) {
+ removeNode(second);
+ insertNodeAfter(second, first);
+ }
+ applyCommandToComposite(new MergeIdenticalElementsCommand(first, second));
+}
+
+void CompositeEditCommand::wrapContentsInDummySpan(Element* element)
+{
+ applyCommandToComposite(new WrapContentsInDummySpanCommand(element));
+}
+
+void CompositeEditCommand::splitTextNodeContainingElement(Text *text, int offset)
+{
+ applyCommandToComposite(new SplitTextNodeContainingElementCommand(text, offset));
+}
+
+void CompositeEditCommand::joinTextNodes(Text *text1, Text *text2)
+{
+ applyCommandToComposite(new JoinTextNodesCommand(text1, text2));
+}
+
+void CompositeEditCommand::inputText(const String &text, bool selectInsertedText)
+{
+ int offset = 0;
+ int length = text.length();
+ RefPtr<Range> startRange = new Range(document(), Position(document()->documentElement(), 0), endingSelection().start());
+ int startIndex = TextIterator::rangeLength(startRange.get());
+ int newline;
+ do {
+ newline = text.find('\n', offset);
+ if (newline != offset) {
+ RefPtr<InsertTextCommand> command = new InsertTextCommand(document());
+ applyCommandToComposite(command);
+ int substringLength = newline == -1 ? length - offset : newline - offset;
+ command->input(text.substring(offset, substringLength), false);
+ }
+ if (newline != -1)
+ insertLineBreak();
+
+ offset = newline + 1;
+ } while (newline != -1 && offset != length);
+
+ if (selectInsertedText) {
+ RefPtr<Range> selectedRange = TextIterator::rangeFromLocationAndLength(document()->documentElement(), startIndex, length);
+ setEndingSelection(Selection(selectedRange.get()));
+ }
+}
+
+void CompositeEditCommand::insertTextIntoNode(Text *node, int offset, const String &text)
+{
+ applyCommandToComposite(new InsertIntoTextNodeCommand(node, offset, text));
+}
+
+void CompositeEditCommand::deleteTextFromNode(Text *node, int offset, int count)
+{
+ applyCommandToComposite(new DeleteFromTextNodeCommand(node, offset, count));
+}
+
+void CompositeEditCommand::replaceTextInNode(Text *node, int offset, int count, const String &replacementText)
+{
+ applyCommandToComposite(new DeleteFromTextNodeCommand(node, offset, count));
+ applyCommandToComposite(new InsertIntoTextNodeCommand(node, offset, replacementText));
+}
+
+Position CompositeEditCommand::positionOutsideTabSpan(const Position& pos)
+{
+ if (!isTabSpanTextNode(pos.node()))
+ return pos;
+
+ Node* tabSpan = tabSpanNode(pos.node());
+
+ if (pos.offset() <= caretMinOffset(pos.node()))
+ return positionBeforeNode(tabSpan);
+
+ if (pos.offset() >= caretMaxOffset(pos.node()))
+ return positionAfterNode(tabSpan);
+
+ splitTextNodeContainingElement(static_cast<Text *>(pos.node()), pos.offset());
+ return positionBeforeNode(tabSpan);
+}
+
+void CompositeEditCommand::insertNodeAtTabSpanPosition(Node* node, const Position& pos)
+{
+ // insert node before, after, or at split of tab span
+ Position insertPos = positionOutsideTabSpan(pos);
+ insertNodeAt(node, insertPos);
+}
+
+void CompositeEditCommand::deleteSelection(bool smartDelete, bool mergeBlocksAfterDelete, bool replace, bool expandForSpecialElements)
+{
+ if (endingSelection().isRange())
+ applyCommandToComposite(new DeleteSelectionCommand(document(), smartDelete, mergeBlocksAfterDelete, replace, expandForSpecialElements));
+}
+
+void CompositeEditCommand::deleteSelection(const Selection &selection, bool smartDelete, bool mergeBlocksAfterDelete, bool replace, bool expandForSpecialElements)
+{
+ if (selection.isRange())
+ applyCommandToComposite(new DeleteSelectionCommand(selection, smartDelete, mergeBlocksAfterDelete, replace, expandForSpecialElements));
+}
+
+void CompositeEditCommand::removeCSSProperty(CSSStyleDeclaration *decl, int property)
+{
+ applyCommandToComposite(new RemoveCSSPropertyCommand(document(), decl, property));
+}
+
+void CompositeEditCommand::removeNodeAttribute(Element* element, const QualifiedName& attribute)
+{
+ if (element->getAttribute(attribute).isNull())
+ return;
+ applyCommandToComposite(new RemoveNodeAttributeCommand(element, attribute));
+}
+
+void CompositeEditCommand::setNodeAttribute(Element* element, const QualifiedName& attribute, const String &value)
+{
+ applyCommandToComposite(new SetNodeAttributeCommand(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.offset();
+ // 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.offset(), 1, nonBreakingSpaceString());
+ if (isCollapsibleWhitespace(visiblePos.characterAfter()) && position.node()->isTextNode() && !position.node()->hasTagName(brTag))
+ replaceTextInNode(static_cast<Text*>(position.node()), position.offset(), 1, nonBreakingSpaceString());
+}
+
+void CompositeEditCommand::rebalanceWhitespace()
+{
+ Selection selection = endingSelection();
+ if (selection.isNone())
+ return;
+
+ rebalanceWhitespaceAt(selection.start());
+ if (selection.isRange())
+ rebalanceWhitespaceAt(selection.end());
+}
+
+void CompositeEditCommand::deleteInsignificantText(Text* textNode, int start, int end)
+{
+ if (!textNode || !textNode->renderer() || start >= end)
+ return;
+
+ RenderText* textRenderer = static_cast<RenderText*>(textNode->renderer());
+ InlineTextBox* box = textRenderer->firstTextBox();
+ if (!box) {
+ // whole text node is empty
+ removeNode(textNode);
+ return;
+ }
+
+ int length = textNode->length();
+ if (start >= length || end > length)
+ return;
+
+ int 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) {
+ int gapStart = prevBox ? prevBox->m_start + prevBox->m_len : 0;
+ if (end < gapStart)
+ // No more chance for any intersections
+ break;
+
+ int gapEnd = box ? box->m_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->string()->substring(start, end - start);
+ // remove text in the gap
+ str.remove(gapStart - start - removed, gapLen);
+ removed += gapLen;
+ }
+
+ prevBox = box;
+ if (box)
+ box = box->nextTextBox();
+ }
+
+ 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 || (unsigned)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 (Range::compareBoundaryPoints(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.offset() : 0;
+ int endOffset = node == end.node() ? end.offset() : 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);
+}
+
+Node* CompositeEditCommand::appendBlockPlaceholder(Node* node)
+{
+ if (!node)
+ return 0;
+
+ // Should assert isBlockFlow || isInlineFlow when deletion improves. See 4244964.
+ ASSERT(node->renderer());
+
+ RefPtr<Node> placeholder = createBlockPlaceholderElement(document());
+ appendNode(placeholder.get(), node);
+ return placeholder.get();
+}
+
+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.get(), pos);
+ return placeholder.get();
+}
+
+Node* CompositeEditCommand::addBlockPlaceholderIfNeeded(Node* node)
+{
+ if (!node)
+ return 0;
+
+ updateLayout();
+
+ RenderObject *renderer = node->renderer();
+ if (!renderer || !renderer->isBlockFlow())
+ return 0;
+
+ // append the placeholder to make sure it follows
+ // any unrendered blocks
+ if (renderer->height() == 0 || (renderer->isListItem() && renderer->isEmpty()))
+ return appendBlockPlaceholder(node);
+
+ return 0;
+}
+
+// Removes '\n's and brs that will collapse when content is inserted just before them.
+// FIXME: We shouldn't really have to remove placeholders, but removing them is a workaround for 9661.
+void CompositeEditCommand::removePlaceholderAt(const VisiblePosition& visiblePosition)
+{
+ if (visiblePosition.isNull())
+ return;
+
+ Position p = visiblePosition.deepEquivalent().downstream();
+ // If a br or '\n' is at the end of a block and not at the start of a paragraph,
+ // then it is superfluous, so adding content before a br or '\n' that is at
+ // the start of a paragraph will render it superfluous.
+ // FIXME: This doesn't remove placeholders at the end of anonymous blocks.
+ if (isEndOfBlock(visiblePosition) && isStartOfParagraph(visiblePosition)) {
+ if (p.node()->hasTagName(brTag) && p.offset() == 0)
+ removeNode(p.node());
+ else if (lineBreakExistsAtPosition(visiblePosition))
+ deleteTextFromNode(static_cast<Text*>(p.node()), p.offset(), 1);
+ }
+}
+
+Node* CompositeEditCommand::moveParagraphContentsToNewBlockIfNecessary(const Position& pos)
+{
+ if (pos.isNull())
+ return 0;
+
+ updateLayout();
+
+ 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 paragraphStart = visibleParagraphStart.deepEquivalent().upstream();
+ Position end = visibleEnd.deepEquivalent().upstream();
+
+ // If there are no VisiblePositions in the same block as pos then
+ // paragraphStart will be outside the paragraph
+ if (Range::compareBoundaryPoints(pos, paragraphStart) < 0)
+ return 0;
+
+ // Perform some checks to see if we need to perform work in this function.
+ if (isBlock(paragraphStart.node())) {
+ if (isBlock(end.node())) {
+ if (!end.node()->isDescendantOf(paragraphStart.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(end.node()) != paragraphStart.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(paragraphStart.node()->isDescendantOf(enclosingBlock(end.node())));
+ return 0;
+ }
+ else if (isEndOfDocument(visibleEnd)) {
+ // At the end of the document. We can bail here as well.
+ return 0;
+ }
+ }
+
+ RefPtr<Node> newBlock = createDefaultParagraphElement(document());
+ appendNode(createBreakElement(document()).get(), newBlock.get());
+ insertNodeAt(newBlock.get(), paragraphStart);
+
+ moveParagraphs(visibleParagraphStart, visibleParagraphEnd, VisiblePosition(Position(newBlock.get(), 0)));
+
+ return newBlock.get();
+}
+
+void CompositeEditCommand::pushAnchorElementDown(Node* anchorNode)
+{
+ if (!anchorNode)
+ return;
+
+ ASSERT(anchorNode->isLink());
+
+ setEndingSelection(Selection::selectionFromContentsOfNode(anchorNode));
+ applyStyledElement(static_cast<Element*>(anchorNode));
+ // Clones of anchorNode have been pushed down, now remove it.
+ if (anchorNode->inDocument())
+ removeNodePreservingChildren(anchorNode);
+}
+
+// We must push partially selected anchors down before creating or removing
+// links from a selection to create fully selected chunks that can be removed.
+// ApplyStyleCommand doesn't do this for us because styles can be nested.
+// Anchors cannot be nested.
+void CompositeEditCommand::pushPartiallySelectedAnchorElementsDown()
+{
+ Selection originalSelection = endingSelection();
+ VisiblePosition visibleStart(originalSelection.start());
+ VisiblePosition visibleEnd(originalSelection.end());
+
+ Node* startAnchor = enclosingAnchorElement(originalSelection.start());
+ VisiblePosition startOfStartAnchor(Position(startAnchor, 0));
+ if (startAnchor && startOfStartAnchor != visibleStart)
+ pushAnchorElementDown(startAnchor);
+
+ Node* endAnchor = enclosingAnchorElement(originalSelection.end());
+ VisiblePosition endOfEndAnchor(Position(endAnchor, 0));
+ if (endAnchor && endOfEndAnchor != visibleEnd)
+ pushAnchorElementDown(endAnchor);
+
+ ASSERT(originalSelection.start().node()->inDocument() && originalSelection.end().node()->inDocument());
+ setEndingSelection(originalSelection);
+}
+
+// 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 = Range::compareBoundaryPoints(visibleStart.deepEquivalent(), endOfParagraphToMove.deepEquivalent()) > 0;
+ bool endBeforeParagraph = Range::compareBoundaryPoints(visibleEnd.deepEquivalent(), startOfParagraphToMove.deepEquivalent()) < 0;
+
+ if (!startAfterParagraph && !endBeforeParagraph) {
+ bool startInParagraph = Range::compareBoundaryPoints(visibleStart.deepEquivalent(), startOfParagraphToMove.deepEquivalent()) >= 0;
+ bool endInParagraph = Range::compareBoundaryPoints(visibleEnd.deepEquivalent(), endOfParagraphToMove.deepEquivalent()) <= 0;
+
+ startIndex = 0;
+ if (startInParagraph) {
+ RefPtr<Range> startRange = new Range(document(), rangeCompliantEquivalent(startOfParagraphToMove.deepEquivalent()), rangeCompliantEquivalent(visibleStart.deepEquivalent()));
+ startIndex = TextIterator::rangeLength(startRange.get(), true);
+ }
+
+ endIndex = 0;
+ if (endInParagraph) {
+ RefPtr<Range> endRange = new Range(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 = new Range(document(), startRangeCompliant.node(), startRangeCompliant.offset(), endRangeCompliant.node(), endRangeCompliant.offset());
+
+ // 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 = startOfParagraphToMove != endOfParagraphToMove ? createFragmentFromMarkup(document(), createMarkup(range.get(), 0, DoNotAnnotateForInterchange, true), "") : 0;
+
+ // FIXME (5098931): We should add a new insert action "WebViewInsertActionMoved" and call shouldInsertFragment here.
+
+ setEndingSelection(Selection(start, end, DOWNSTREAM));
+ deleteSelection(false, false, false, false);
+
+ ASSERT(destination.deepEquivalent().node()->inDocument());
+
+ // 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).
+ 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(caretAfterDelete))
+ deleteTextFromNode(static_cast<Text*>(node), position.offset(), 1);
+ }
+
+ // 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()).get(), beforeParagraph.deepEquivalent());
+ // Need an updateLayout here in case inserting the br has split a text node.
+ updateLayout();
+ }
+
+ RefPtr<Range> startToDestinationRange(new Range(document(), Position(document(), 0), rangeCompliantEquivalent(destination.deepEquivalent())));
+ destinationIndex = TextIterator::rangeLength(startToDestinationRange.get(), true);
+
+ setEndingSelection(destination);
+ applyCommandToComposite(new ReplaceSelectionCommand(document(), fragment.get(), true, false, !preserveStyle, false, true));
+
+ 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(Selection(start->startPosition(), end->startPosition(), DOWNSTREAM));
+ }
+}
+
+// FIXME: Send an appropriate shouldDeleteRange call.
+bool CompositeEditCommand::breakOutOfEmptyListItem()
+{
+ Node* emptyListItem = enclosingEmptyListItem(endingSelection().visibleStart());
+ if (!emptyListItem)
+ return false;
+
+ RefPtr<CSSMutableStyleDeclaration> style = styleAtPosition(endingSelection().start());
+
+ Node* listNode = emptyListItem->parentNode();
+ RefPtr<Node> newBlock = isListElement(listNode->parentNode()) ? createListItemElement(document()) : createDefaultParagraphElement(document());
+
+ if (emptyListItem->renderer()->nextSibling()) {
+ if (emptyListItem->renderer()->previousSibling())
+ splitElement(static_cast<Element*>(listNode), emptyListItem);
+ insertNodeBefore(newBlock.get(), listNode);
+ removeNode(emptyListItem);
+ } else {
+ insertNodeAfter(newBlock.get(), listNode);
+ removeNode(emptyListItem->renderer()->previousSibling() ? emptyListItem : listNode);
+ }
+
+ appendBlockPlaceholder(newBlock.get());
+ setEndingSelection(Selection(Position(newBlock.get(), 0), DOWNSTREAM));
+
+ CSSComputedStyleDeclaration endingStyle(endingSelection().start().node());
+ endingStyle.diff(style.get());
+ if (style->length() > 0)
+ applyStyle(style.get());
+
+ 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, bool alwaysAvoidAnchors)
+{
+ if (original.isNull())
+ return original;
+
+ VisiblePosition visiblePos(original);
+ Node* enclosingAnchor = enclosingAnchorElement(original);
+ Position result = original;
+ // Don't avoid block level anchors, because that would insert content into the wrong paragraph.
+ if (enclosingAnchor && !isBlock(enclosingAnchor)) {
+ VisiblePosition firstInAnchor(Position(enclosingAnchor, 0));
+ VisiblePosition lastInAnchor(Position(enclosingAnchor, maxDeepOffset(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 && (isEndOfDocument(visiblePos) || alwaysAvoidAnchors)) {
+ // 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 (lineBreakExistsAtPosition(visiblePos) && downstream.node()->isDescendantOf(enclosingAnchor))
+ return original;
+
+ result = positionAfterNode(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 && (!isStartOfParagraph(visiblePos) || alwaysAvoidAnchors)) {
+ // 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);
+ }
+ result = positionBeforeNode(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.
+Node* CompositeEditCommand::splitTreeToNode(Node* start, Node* end, bool splitAncestor)
+{
+ Node* node;
+ for (node = start; node && node->parent() != end; node = node->parent()) {
+ VisiblePosition positionInParent(Position(node->parent(), 0), DOWNSTREAM);
+ VisiblePosition positionInNode(Position(node, 0), DOWNSTREAM);
+ if (positionInParent != positionInNode)
+ applyCommandToComposite(new SplitElementCommand(static_cast<Element*>(node->parent()), node));
+ }
+ if (splitAncestor)
+ return splitTreeToNode(end, end->parent());
+ return node;
+}
+
+PassRefPtr<Element> createBlockPlaceholderElement(Document* document)
+{
+ ExceptionCode ec = 0;
+ RefPtr<Element> breakNode = document->createElementNS(xhtmlNamespaceURI, "br", ec);
+ ASSERT(ec == 0);
+ return breakNode.release();
+}
+
+} // namespace WebCore
diff --git a/WebCore/editing/CompositeEditCommand.h b/WebCore/editing/CompositeEditCommand.h
new file mode 100644
index 0000000..9be8ee7
--- /dev/null
+++ b/WebCore/editing/CompositeEditCommand.h
@@ -0,0 +1,116 @@
+/*
+ * 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.
+ */
+
+#ifndef CompositeEditCommand_h
+#define CompositeEditCommand_h
+
+#include "EditCommand.h"
+#include <wtf/Vector.h>
+
+namespace WebCore {
+
+class CSSStyleDeclaration;
+class Text;
+
+class CompositeEditCommand : public EditCommand {
+public:
+ CompositeEditCommand(Document*);
+
+ bool isFirstCommand(EditCommand* c) { return !m_commands.isEmpty() && m_commands.first() == c; }
+
+protected:
+ //
+ // sugary-sweet convenience functions to help create and apply edit commands in composite commands
+ //
+ void appendNode(Node* appendChild, Node* parentNode);
+ void applyCommandToComposite(PassRefPtr<EditCommand>);
+ void applyStyle(CSSStyleDeclaration*, EditAction = EditActionChangeAttributes);
+ void applyStyle(CSSStyleDeclaration*, const Position& start, const Position& end, EditAction = EditActionChangeAttributes);
+ void applyStyledElement(Element*);
+ void removeStyledElement(Element*);
+ void deleteSelection(bool smartDelete = false, bool mergeBlocksAfterDelete = true, bool replace = false, bool expandForSpecialElements = true);
+ void deleteSelection(const Selection&, bool smartDelete = false, bool mergeBlocksAfterDelete = true, bool replace = false, bool expandForSpecialElements = true);
+ virtual void deleteTextFromNode(Text* node, int offset, int count);
+ void inputText(const String&, bool selectInsertedText = false);
+ void insertNodeAfter(Node* insertChild, Node* refChild);
+ void insertNodeAt(Node* insertChild, const Position&);
+ void insertNodeBefore(Node* insertChild, Node* refChild);
+ void insertParagraphSeparator(bool useDefaultParagraphElement = false);
+ void insertLineBreak();
+ void insertTextIntoNode(Text* node, int offset, const String& text);
+ void joinTextNodes(Text*, Text*);
+ void rebalanceWhitespace();
+ void rebalanceWhitespaceAt(const Position&);
+ void prepareWhitespaceAtPositionForSplit(Position& position);
+ void removeCSSProperty(CSSStyleDeclaration*, int property);
+ void removeNodeAttribute(Element*, const QualifiedName& attribute);
+ void removeChildrenInRange(Node*, int from, int to);
+ virtual void removeNode(Node*);
+ void removeNodePreservingChildren(Node*);
+ void removeNodeAndPruneAncestors(Node*);
+ void prune(PassRefPtr<Node>);
+ void replaceTextInNode(Text* node, int offset, int count, const String& replacementText);
+ Position positionOutsideTabSpan(const Position&);
+ void insertNodeAtTabSpanPosition(Node*, const Position&);
+ void setNodeAttribute(Element*, const QualifiedName& attribute, const String& value);
+ void splitTextNode(Text*, int offset);
+ void splitElement(Element*, Node* atChild);
+ void mergeIdenticalElements(Element*, Element*);
+ void wrapContentsInDummySpan(Element*);
+ void splitTextNodeContainingElement(Text*, int offset);
+
+ void deleteInsignificantText(Text*, int start, int end);
+ void deleteInsignificantText(const Position& start, const Position& end);
+ void deleteInsignificantTextDownstream(const Position&);
+
+ Node *appendBlockPlaceholder(Node*);
+ Node *insertBlockPlaceholder(const Position&);
+ Node *addBlockPlaceholderIfNeeded(Node*);
+ void removePlaceholderAt(const VisiblePosition&);
+
+ Node* moveParagraphContentsToNewBlockIfNecessary(const Position&);
+
+ void pushAnchorElementDown(Node*);
+ void pushPartiallySelectedAnchorElementsDown();
+
+ 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);
+
+ bool breakOutOfEmptyListItem();
+
+ Position positionAvoidingSpecialElementBoundary(const Position&, bool alwaysAvoidAnchors = true);
+
+ 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/WebCore/editing/CreateLinkCommand.cpp b/WebCore/editing/CreateLinkCommand.cpp
new file mode 100644
index 0000000..c5d68dd
--- /dev/null
+++ b/WebCore/editing/CreateLinkCommand.cpp
@@ -0,0 +1,60 @@
+/*
+ * 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 = new HTMLAnchorElement(document());
+ anchorElement->setHref(m_url);
+
+ if (endingSelection().isRange()) {
+ pushPartiallySelectedAnchorElementsDown();
+ applyStyledElement(anchorElement.get());
+ } else {
+ insertNodeAt(anchorElement.get(), endingSelection().start());
+ RefPtr<Text> textNode = new Text(document(), m_url);
+ appendNode(textNode.get(), anchorElement.get());
+ setEndingSelection(Selection(positionBeforeNode(anchorElement.get()), positionAfterNode(anchorElement.get()), DOWNSTREAM));
+ }
+}
+
+}
diff --git a/WebCore/editing/CreateLinkCommand.h b/WebCore/editing/CreateLinkCommand.h
new file mode 100644
index 0000000..9174fd2
--- /dev/null
+++ b/WebCore/editing/CreateLinkCommand.h
@@ -0,0 +1,45 @@
+/*
+ * 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 CreateLinkCommand_h
+#define CreateLinkCommand_h
+
+#include "CompositeEditCommand.h"
+
+namespace WebCore {
+
+class CreateLinkCommand : public CompositeEditCommand
+{
+public:
+ CreateLinkCommand(Document*, const String&);
+ virtual void doApply();
+ virtual EditAction editingAction() const { return EditActionCreateLink; }
+private:
+ String m_url;
+};
+
+} // namespace WebCore
+
+#endif // CreateLinkCommand_h
diff --git a/WebCore/editing/DeleteButton.cpp b/WebCore/editing/DeleteButton.cpp
new file mode 100644
index 0000000..6d64fd1
--- /dev/null
+++ b/WebCore/editing/DeleteButton.cpp
@@ -0,0 +1,57 @@
+/*
+ * 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 "DeleteButton.h"
+
+#include "DeleteButtonController.h"
+#include "Document.h"
+#include "Editor.h"
+#include "Event.h"
+#include "EventNames.h"
+#include "Frame.h"
+
+namespace WebCore {
+
+using namespace EventNames;
+
+DeleteButton::DeleteButton(Document* document)
+ : HTMLImageElement(document)
+{
+}
+
+void DeleteButton::defaultEventHandler(Event* event)
+{
+ if (event->isMouseEvent()) {
+ if (event->type() == clickEvent) {
+ document()->frame()->editor()->deleteButtonController()->deleteTarget();
+ event->setDefaultHandled();
+ }
+ }
+
+ HTMLImageElement::defaultEventHandler(event);
+}
+
+} // namespace
diff --git a/WebCore/editing/DeleteButton.h b/WebCore/editing/DeleteButton.h
new file mode 100644
index 0000000..ac3cdac
--- /dev/null
+++ b/WebCore/editing/DeleteButton.h
@@ -0,0 +1,42 @@
+/*
+ * 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 DeleteButton_h
+#define DeleteButton_h
+
+#include "HTMLImageElement.h"
+
+namespace WebCore {
+
+class DeleteButton : public HTMLImageElement {
+public:
+ DeleteButton(Document*);
+
+ virtual void defaultEventHandler(Event*);
+};
+
+} // namespace
+
+#endif
diff --git a/WebCore/editing/DeleteButtonController.cpp b/WebCore/editing/DeleteButtonController.cpp
new file mode 100644
index 0000000..a6b7c79
--- /dev/null
+++ b/WebCore/editing/DeleteButtonController.cpp
@@ -0,0 +1,309 @@
+/*
+ * 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 "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 "RenderObject.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;
+
+ const int minimumWidth = 25;
+ const int minimumHeight = 25;
+ const unsigned minimumVisibleBorders = 3;
+
+ RenderObject* renderer = node->renderer();
+ if (!renderer || renderer->width() < minimumWidth || renderer->height() < minimumHeight)
+ return false;
+
+ if (renderer->isTable())
+ return true;
+
+ if (node->hasTagName(ulTag) || node->hasTagName(olTag))
+ return true;
+
+ if (renderer->isPositioned())
+ return true;
+
+ // allow block elements (excluding table cells) that have some non-transparent borders
+ if (renderer->isRenderBlock() && !renderer->isTableCell()) {
+ RenderStyle* style = renderer->style();
+ if (style && style->hasBorder()) {
+ unsigned visibleBorders = style->borderTop().isVisible() + style->borderBottom().isVisible() + style->borderLeft().isVisible() + style->borderRight().isVisible();
+ if (visibleBorders >= minimumVisibleBorders)
+ return true;
+ }
+ }
+
+ return false;
+}
+
+static HTMLElement* enclosingDeletableElement(const Selection& selection)
+{
+ if (!selection.isContentEditable())
+ return 0;
+
+ RefPtr<Range> range = selection.toRange();
+ 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 Selection& oldSelection)
+{
+ if (!enabled())
+ return;
+
+ HTMLElement* oldElement = enclosingDeletableElement(oldSelection);
+ HTMLElement* newElement = enclosingDeletableElement(m_frame->selectionController()->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 = new HTMLDivElement(m_target->document());
+ container->setId(containerElementIdentifier);
+
+ CSSMutableStyleDeclaration* style = container->getInlineStyleDecl();
+ style->setProperty(CSS_PROP__WEBKIT_USER_DRAG, CSS_VAL_NONE);
+ style->setProperty(CSS_PROP__WEBKIT_USER_SELECT, CSS_VAL_NONE);
+ style->setProperty(CSS_PROP__WEBKIT_USER_MODIFY, CSS_VAL_NONE);
+
+ RefPtr<HTMLDivElement> outline = new HTMLDivElement(m_target->document());
+ outline->setId(outlineElementIdentifier);
+
+ const int borderWidth = 4;
+ const int borderRadius = 6;
+
+ style = outline->getInlineStyleDecl();
+ style->setProperty(CSS_PROP_POSITION, CSS_VAL_ABSOLUTE);
+ style->setProperty(CSS_PROP_CURSOR, CSS_VAL_DEFAULT);
+ style->setProperty(CSS_PROP__WEBKIT_USER_DRAG, CSS_VAL_NONE);
+ style->setProperty(CSS_PROP__WEBKIT_USER_SELECT, CSS_VAL_NONE);
+ style->setProperty(CSS_PROP__WEBKIT_USER_MODIFY, CSS_VAL_NONE);
+ style->setProperty(CSS_PROP_Z_INDEX, String::number(-1000000));
+ style->setProperty(CSS_PROP_TOP, String::number(-borderWidth - m_target->renderer()->borderTop()) + "px");
+ style->setProperty(CSS_PROP_RIGHT, String::number(-borderWidth - m_target->renderer()->borderRight()) + "px");
+ style->setProperty(CSS_PROP_BOTTOM, String::number(-borderWidth - m_target->renderer()->borderBottom()) + "px");
+ style->setProperty(CSS_PROP_LEFT, String::number(-borderWidth - m_target->renderer()->borderLeft()) + "px");
+ style->setProperty(CSS_PROP_BORDER, String::number(borderWidth) + "px solid rgba(0, 0, 0, 0.6)");
+ style->setProperty(CSS_PROP__WEBKIT_BORDER_RADIUS, String::number(borderRadius) + "px");
+
+ ExceptionCode ec = 0;
+ container->appendChild(outline.get(), ec);
+ ASSERT(ec == 0);
+ if (ec)
+ return;
+
+ RefPtr<DeleteButton> button = new DeleteButton(m_target->document());
+ button->setId(buttonElementIdentifier);
+
+ const int buttonWidth = 30;
+ const int buttonHeight = 30;
+ const int buttonBottomShadowOffset = 2;
+
+ style = button->getInlineStyleDecl();
+ style->setProperty(CSS_PROP_POSITION, CSS_VAL_ABSOLUTE);
+ style->setProperty(CSS_PROP_CURSOR, CSS_VAL_DEFAULT);
+ style->setProperty(CSS_PROP__WEBKIT_USER_DRAG, CSS_VAL_NONE);
+ style->setProperty(CSS_PROP__WEBKIT_USER_SELECT, CSS_VAL_NONE);
+ style->setProperty(CSS_PROP__WEBKIT_USER_MODIFY, CSS_VAL_NONE);
+ style->setProperty(CSS_PROP_Z_INDEX, String::number(1000000));
+ style->setProperty(CSS_PROP_TOP, String::number((-buttonHeight / 2) - m_target->renderer()->borderTop() - (borderWidth / 2) + buttonBottomShadowOffset) + "px");
+ style->setProperty(CSS_PROP_LEFT, String::number((-buttonWidth / 2) - m_target->renderer()->borderLeft() - (borderWidth / 2)) + "px");
+ style->setProperty(CSS_PROP_WIDTH, String::number(buttonWidth) + "px");
+ style->setProperty(CSS_PROP_HEIGHT, String::number(buttonHeight) + "px");
+
+ Image* buttonImage = Image::loadPlatformResource("deleteButton");
+ if (buttonImage->isNull())
+ return;
+
+ button->setCachedImage(new CachedImage(buttonImage));
+
+ 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(CSS_PROP_POSITION, CSS_VAL_RELATIVE);
+ m_wasStaticPositioned = true;
+ }
+
+ if (m_target->renderer()->style()->hasAutoZIndex()) {
+ m_target->getInlineStyleDecl()->setProperty(CSS_PROP_Z_INDEX, "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(CSS_PROP_POSITION, CSS_VAL_STATIC);
+ if (m_wasAutoZIndex)
+ m_target->getInlineStyleDecl()->setProperty(CSS_PROP_Z_INDEX, CSS_VAL_AUTO);
+ }
+
+ m_wasStaticPositioned = false;
+ m_wasAutoZIndex = false;
+}
+
+void DeleteButtonController::enable()
+{
+ ASSERT(m_disableStack > 0);
+ if (m_disableStack > 0)
+ m_disableStack--;
+ if (enabled())
+ show(enclosingDeletableElement(m_frame->selectionController()->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 = positionBeforeNode(element.get());
+ RefPtr<RemoveNodeCommand> command = new RemoveNodeCommand(element.get());
+ command->apply();
+ m_frame->selectionController()->setSelection(VisiblePosition(pos));
+}
+
+} // namespace WebCore
diff --git a/WebCore/editing/DeleteButtonController.h b/WebCore/editing/DeleteButtonController.h
new file mode 100644
index 0000000..ab2d0b0
--- /dev/null
+++ b/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 Selection;
+
+class DeleteButtonController {
+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 Selection& 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/WebCore/editing/DeleteFromTextNodeCommand.cpp b/WebCore/editing/DeleteFromTextNodeCommand.cpp
new file mode 100644
index 0000000..90f4514
--- /dev/null
+++ b/WebCore/editing/DeleteFromTextNodeCommand.cpp
@@ -0,0 +1,64 @@
+/*
+ * 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 "DeleteFromTextNodeCommand.h"
+
+#include "Text.h"
+
+namespace WebCore {
+
+DeleteFromTextNodeCommand::DeleteFromTextNodeCommand(Text *node, int offset, int count)
+ : EditCommand(node->document()), m_node(node), m_offset(offset), m_count(count)
+{
+ ASSERT(m_node);
+ ASSERT(m_offset >= 0);
+ ASSERT(m_offset < (int)m_node->length());
+ ASSERT(m_count >= 0);
+}
+
+void DeleteFromTextNodeCommand::doApply()
+{
+ ASSERT(m_node);
+
+ ExceptionCode ec = 0;
+ m_text = m_node->substringData(m_offset, m_count, ec);
+ ASSERT(ec == 0);
+
+ m_node->deleteData(m_offset, m_count, ec);
+ ASSERT(ec == 0);
+}
+
+void DeleteFromTextNodeCommand::doUnapply()
+{
+ ASSERT(m_node);
+ ASSERT(!m_text.isEmpty());
+
+ ExceptionCode ec = 0;
+ m_node->insertData(m_offset, m_text, ec);
+ ASSERT(ec == 0);
+}
+
+} // namespace WebCore
diff --git a/WebCore/editing/DeleteFromTextNodeCommand.h b/WebCore/editing/DeleteFromTextNodeCommand.h
new file mode 100644
index 0000000..dfad153
--- /dev/null
+++ b/WebCore/editing/DeleteFromTextNodeCommand.h
@@ -0,0 +1,55 @@
+/*
+ * 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.
+ */
+
+#ifndef DeleteFromTextNodeCommand_h
+#define DeleteFromTextNodeCommand_h
+
+#include "EditCommand.h"
+
+namespace WebCore {
+
+class Text;
+
+class DeleteFromTextNodeCommand : public EditCommand {
+public:
+ DeleteFromTextNodeCommand(Text*, int offset, int count);
+
+ virtual void doApply();
+ virtual void doUnapply();
+
+ Text* node() const { return m_node.get(); }
+ int offset() const { return m_offset; }
+ int count() const { return m_count; }
+
+private:
+ RefPtr<Text> m_node;
+ int m_offset;
+ int m_count;
+ String m_text;
+};
+
+} // namespace WebCore
+
+#endif // DeleteFromTextNodeCommand_h
diff --git a/WebCore/editing/DeleteSelectionCommand.cpp b/WebCore/editing/DeleteSelectionCommand.cpp
new file mode 100644
index 0000000..28eca34
--- /dev/null
+++ b/WebCore/editing/DeleteSelectionCommand.cpp
@@ -0,0 +1,790 @@
+/*
+ * 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 "Editor.h"
+#include "EditorClient.h"
+#include "Element.h"
+#include "Frame.h"
+#include "Logging.h"
+#include "CSSComputedStyleDeclaration.h"
+#include "htmlediting.h"
+#include "HTMLInputElement.h"
+#include "HTMLNames.h"
+#include "markup.h"
+#include "ReplaceSelectionCommand.h"
+#include "Text.h"
+#include "TextIterator.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));
+ VisiblePosition firstInCell(Position(cell, 0));
+ VisiblePosition lastInCell(Position(cell, maxDeepOffset(cell)));
+ return firstInCell == lastInCell;
+}
+
+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_startBlock(0),
+ m_endBlock(0),
+ m_typingStyle(0),
+ m_deleteIntoBlockquoteStyle(0)
+{
+}
+
+DeleteSelectionCommand::DeleteSelectionCommand(const Selection& 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_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 expanion.
+ 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 && Range::compareBoundaryPoints(positionAfterNode(startSpecialContainer), end) > -1)
+ break;
+
+ // If we're going to expand to include the endSpecialContainer, it must be fully selected.
+ if (endSpecialContainer && !startSpecialContainer && Range::compareBoundaryPoints(start, positionBeforeNode(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::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);
+
+ Node* startCell = enclosingTableCell(m_upstreamStart);
+ Node* endCell = enclosingTableCell(m_downstreamEnd);
+ // Don't move content out of a table cell.
+ // 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;
+
+ // 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());
+ }
+
+ // 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);
+ }
+ }
+
+ //
+ // Handle setting start and end blocks and the start node.
+ //
+ m_startBlock = m_downstreamStart.node()->enclosingBlockFlowOrTableElement();
+ m_endBlock = m_upstreamEnd.node()->enclosingBlockFlowOrTableElement();
+}
+
+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.
+ // FIXME: Improve typing style.
+ // See this bug: <rdar://problem/3769899> Implementation of typing style needs improvement
+ RefPtr<CSSComputedStyleDeclaration> computedStyle = positionBeforeTabSpan(m_selectionToDelete.start()).computedStyle();
+ m_typingStyle = computedStyle->copyInheritableProperties();
+
+ // 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())) {
+ computedStyle = m_selectionToDelete.end().computedStyle();
+ m_deleteIntoBlockquoteStyle = computedStyle->copyInheritableProperties();
+ } 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_mergeBlocksAfterDelete = false;
+ m_endingPosition = m_downstreamEnd;
+ }
+
+ return false;
+}
+
+static void updatePositionForNodeRemoval(Node* node, Position& position)
+{
+ if (position.isNull())
+ return;
+ if (node->parent() == position.node() && node->nodeIndex() < (unsigned)position.offset())
+ position = Position(position.node(), position.offset() - 1);
+ if (position.node() == node || position.node()->isDescendantOf(node))
+ position = positionBeforeNode(node);
+}
+
+void DeleteSelectionCommand::removeNode(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) || 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() && r->contentHeight() <= 0)
+ insertBlockPlaceholder(Position(node,0));
+ return;
+ }
+
+ if (node == m_startBlock && !isEndOfBlock(VisiblePosition(m_startBlock.get(), 0, DOWNSTREAM).previous()))
+ m_needPlaceholder = true;
+ else if (node == m_endBlock && !isStartOfBlock(VisiblePosition(m_endBlock.get(), maxDeepOffset(m_endBlock.get()), DOWNSTREAM).next()))
+ m_needPlaceholder = true;
+
+ // FIXME: Update the endpoints of the range being deleted.
+ updatePositionForNodeRemoval(node, m_endingPosition);
+ updatePositionForNodeRemoval(node, m_leadingWhitespace);
+ updatePositionForNodeRemoval(node, m_trailingWhitespace);
+
+ CompositeEditCommand::removeNode(node);
+}
+
+
+void updatePositionForTextRemoval(Node* node, int offset, int count, Position& position)
+{
+ if (position.node() == node) {
+ if (position.offset() > offset + count)
+ position = Position(position.node(), position.offset() - count);
+ else if (position.offset() > offset)
+ position = Position(position.node(), offset);
+ }
+}
+
+void DeleteSelectionCommand::deleteTextFromNode(Text *node, int offset, int count)
+{
+ // FIXME: Update the endpoints of the range being deleted.
+ updatePositionForTextRemoval(node, offset, count, m_endingPosition);
+ updatePositionForTextRemoval(node, offset, count, m_leadingWhitespace);
+ updatePositionForTextRemoval(node, offset, count, m_trailingWhitespace);
+
+ CompositeEditCommand::deleteTextFromNode(node, offset, count);
+}
+
+void DeleteSelectionCommand::handleGeneralDelete()
+{
+ int startOffset = m_upstreamStart.offset();
+ 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 >= maxDeepOffset(startNode)) {
+ startNode = startNode->traverseNextSibling();
+ startOffset = 0;
+ }
+
+ // Done adjusting the start. See if we're all done.
+ if (!startNode)
+ return;
+
+ if (startNode == m_downstreamEnd.node()) {
+ // The selection to delete is all in one node.
+ if (!startNode->renderer() ||
+ (startOffset == 0 && m_downstreamEnd.offset() >= maxDeepOffset(startNode))) {
+ // just delete
+ removeNode(startNode);
+ } else if (m_downstreamEnd.offset() - 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.offset() - startOffset);
+ } else {
+ removeChildrenInRange(startNode, startOffset, m_downstreamEnd.offset());
+ m_endingPosition = m_upstreamStart;
+ }
+ }
+ }
+ 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);
+ }
+ }
+
+ // handle deleting all nodes that are completely selected
+ while (node && node != m_downstreamEnd.node()) {
+ if (Range::compareBoundaryPoints(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.offset());
+ m_downstreamEnd = Position(m_downstreamEnd.node(), m_downstreamEnd.offset() - 1);
+ }
+ removeNode(node.get());
+ node = nextNode.get();
+ } else {
+ Node* n = node->lastDescendant();
+ if (m_downstreamEnd.node() == n && m_downstreamEnd.offset() >= 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.offset() >= caretMinOffset(m_downstreamEnd.node())) {
+ if (m_downstreamEnd.offset() >= maxDeepOffset(m_downstreamEnd.node()) && !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.offset() > 0) {
+ deleteTextFromNode(text, 0, m_downstreamEnd.offset());
+ m_downstreamEnd = Position(text, 0);
+ }
+ // 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.offset());
+ 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()) {
+ Text* textNode = static_cast<Text*>(m_leadingWhitespace.node());
+ ASSERT(!textNode->renderer() || textNode->renderer()->style()->collapseWhiteSpace());
+ replaceTextInNode(textNode, m_leadingWhitespace.offset(), 1, nonBreakingSpaceString());
+ }
+ if (m_trailingWhitespace.isNotNull() && !m_trailingWhitespace.isRenderedCharacter()) {
+ Text* textNode = static_cast<Text*>(m_trailingWhitespace.node());
+ ASSERT(!textNode->renderer() ||textNode->renderer()->style()->collapseWhiteSpace());
+ replaceTextInNode(textNode, m_trailingWhitespace.offset(), 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)
+ return;
+
+ // 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 (Range::compareBoundaryPoints(m_upstreamStart, m_downstreamEnd) > 0)
+ return;
+
+ // FIXME: Merging will always be unnecessary in this case, but we really bail here because this is a case where
+ // deletion commonly fails to adjust its endpoints, which would cause the visible position comparison below to false negative.
+ if (m_endBlock == m_startBlock)
+ 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())) {
+ 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.
+ // FIXME: handleSpecialCaseBRDelete prevents us from getting here in a case like <ul><li>foo<br><br></li></ul>^foo
+ if (isStartOfParagraph(mergeDestination) &&
+ startOfParagraphToMove.deepEquivalent().node()->renderer()->caretRect(startOfParagraphToMove.deepEquivalent().offset()).location().x() >
+ mergeDestination.deepEquivalent().node()->renderer()->caretRect(startOfParagraphToMove.deepEquivalent().offset()).location().x()) {
+ ASSERT(mergeDestination.deepEquivalent().downstream().node()->hasTagName(brTag));
+ removeNodeAndPruneAncestors(mergeDestination.deepEquivalent().downstream().node());
+ m_endingPosition = startOfParagraphToMove.deepEquivalent();
+ return;
+ }
+
+ RefPtr<Range> range = new Range(document(), rangeCompliantEquivalent(startOfParagraphToMove.deepEquivalent()), rangeCompliantEquivalent(endOfParagraphToMove.deepEquivalent()));
+ RefPtr<Range> rangeToBeReplaced = new Range(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;
+ moveParagraph(startOfParagraphToMove, endOfParagraphToMove, mergeDestination);
+ 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(Node *insertedPlaceholder)
+{
+ 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.
+ // FIXME: Improve typing style.
+ // See this bug: <rdar://problem/3769899> Implementation of typing style needs improvement
+
+ // 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;
+
+ RefPtr<CSSComputedStyleDeclaration> endingStyle = new CSSComputedStyleDeclaration(m_endingPosition.node());
+ endingStyle->diff(m_typingStyle.get());
+ if (!m_typingStyle->length())
+ m_typingStyle = 0;
+ if (insertedPlaceholder && m_typingStyle) {
+ // Apply style to the placeholder. This makes sure that the single line in the
+ // paragraph has the right height, and that the paragraph takes on the style
+ // of the preceding line and retains it even if you click away, click back, and
+ // then start typing. In this case, the typing style is applied right now, and
+ // is not retained until the next typing action.
+
+ setEndingSelection(Selection(Position(insertedPlaceholder, 0), DOWNSTREAM));
+ applyStyle(m_typingStyle.get(), EditActionUnspecified);
+ m_typingStyle = 0;
+ }
+ // Set m_typingStyle as the typing style.
+ // It's perfectly OK for m_typingStyle to be null.
+ document()->frame()->setTypingStyle(m_typingStyle.get());
+ setTypingStyle(m_typingStyle.get());
+}
+
+void DeleteSelectionCommand::clearTransientState()
+{
+ m_selectionToDelete = Selection();
+ m_upstreamStart.clear();
+ m_downstreamStart.clear();
+ m_upstreamEnd.clear();
+ m_downstreamEnd.clear();
+ m_endingPosition.clear();
+ m_leadingWhitespace.clear();
+ m_trailingWhitespace.clear();
+}
+
+void DeleteSelectionCommand::saveFullySelectedAnchor()
+{
+ // If deleting an anchor element, save it away so that it can be restored
+ // when the user begins entering text.
+
+ Position start = m_selectionToDelete.start();
+ Node* startAnchor = enclosingNodeWithTag(start.downstream(), aTag);
+ if (!startAnchor)
+ return;
+
+ Position end = m_selectionToDelete.end();
+ Node* endAnchor = enclosingNodeWithTag(end.upstream(), aTag);
+ if (startAnchor != endAnchor)
+ return;
+
+ VisiblePosition visibleStart(m_selectionToDelete.visibleStart());
+ VisiblePosition visibleEnd(m_selectionToDelete.visibleEnd());
+
+ Node* beforeStartAnchor = enclosingNodeWithTag(visibleStart.previous().deepEquivalent().downstream(), aTag);
+ Node* afterEndAnchor = enclosingNodeWithTag(visibleEnd.next().deepEquivalent().upstream(), aTag);
+
+ if (startAnchor && startAnchor == endAnchor && startAnchor != beforeStartAnchor && endAnchor != afterEndAnchor)
+ document()->frame()->editor()->setRemovedAnchor(startAnchor->cloneNode(false));
+}
+
+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.isRange())
+ 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()->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()) &&
+ isEndOfParagraph(m_selectionToDelete.visibleEnd()) &&
+ !lineBreakExistsAtPosition(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();
+ if (!m_startBlock || !m_endBlock) {
+ // Can't figure out what blocks we're in. This can happen if
+ // the document structure is not what we are expecting, like if
+ // the document has no body element, or if the editable block
+ // has been changed to display: inline. Some day it might
+ // be nice to be able to deal with this, but for now, bail.
+ clearTransientState();
+ return;
+ }
+
+ // Delete any text that may hinder our ability to fixup whitespace after the delete
+ deleteInsignificantTextDownstream(m_trailingWhitespace);
+
+ saveTypingStyleState();
+
+ saveFullySelectedAnchor();
+
+ // deleting just a BR is handled specially, at least because we do not
+ // want to replace it with a placeholder BR!
+ if (handleSpecialCaseBRDelete()) {
+ calculateTypingStyleAfterDelete(false);
+ setEndingSelection(Selection(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(placeholder.get());
+
+ setEndingSelection(Selection(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;
+}
+
+bool DeleteSelectionCommand::preservesTypingStyle() const
+{
+ return true;
+}
+
+} // namespace WebCore
diff --git a/WebCore/editing/DeleteSelectionCommand.h b/WebCore/editing/DeleteSelectionCommand.h
new file mode 100644
index 0000000..7904ffa
--- /dev/null
+++ b/WebCore/editing/DeleteSelectionCommand.h
@@ -0,0 +1,88 @@
+/*
+ * 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.
+ */
+
+#ifndef DeleteSelectionCommand_h
+#define DeleteSelectionCommand_h
+
+#include "CompositeEditCommand.h"
+
+namespace WebCore {
+
+class DeleteSelectionCommand : public CompositeEditCommand {
+public:
+ DeleteSelectionCommand(Document*, bool smartDelete = false, bool mergeBlocksAfterDelete = true, bool replace = false, bool expandForSpecialElements = false);
+ DeleteSelectionCommand(const Selection&, bool smartDelete = false, bool mergeBlocksAfterDelete = true, bool replace = false, bool expandForSpecialElements = false);
+
+ virtual void doApply();
+ virtual EditAction editingAction() const;
+
+private:
+ virtual bool preservesTypingStyle() const;
+
+ void initializeStartEnd(Position&, Position&);
+ void initializePositionData();
+ void saveTypingStyleState();
+ void saveFullySelectedAnchor();
+ void insertPlaceholderForAncestorBlockContent();
+ bool handleSpecialCaseBRDelete();
+ void handleGeneralDelete();
+ void fixupWhitespace();
+ void mergeParagraphs();
+ void removePreviouslySelectedEmptyTableRows();
+ void calculateEndingPosition();
+ void calculateTypingStyleAfterDelete(Node*);
+ void clearTransientState();
+ virtual void removeNode(Node*);
+ virtual void deleteTextFromNode(Text*, int, int);
+
+ bool m_hasSelectionToDelete;
+ bool m_smartDelete;
+ bool m_mergeBlocksAfterDelete;
+ bool m_needPlaceholder;
+ bool m_replace;
+ bool m_expandForSpecialElements;
+
+ // This data is transient and should be cleared at the end of the doApply function.
+ Selection 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<CSSMutableStyleDeclaration> m_typingStyle;
+ RefPtr<CSSMutableStyleDeclaration> m_deleteIntoBlockquoteStyle;
+ RefPtr<Node> m_startRoot;
+ RefPtr<Node> m_endRoot;
+ RefPtr<Node> m_startTableRow;
+ RefPtr<Node> m_endTableRow;
+};
+
+} // namespace WebCore
+
+#endif // DeleteSelectionCommand_h
diff --git a/WebCore/editing/EditAction.h b/WebCore/editing/EditAction.h
new file mode 100644
index 0000000..8046f3c
--- /dev/null
+++ b/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/WebCore/editing/EditCommand.cpp b/WebCore/editing/EditCommand.cpp
new file mode 100644
index 0000000..a7c7ed0
--- /dev/null
+++ b/WebCore/editing/EditCommand.cpp
@@ -0,0 +1,255 @@
+/*
+ * 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 "CSSComputedStyleDeclaration.h"
+#include "CSSMutableStyleDeclaration.h"
+#include "DeleteButtonController.h"
+#include "Document.h"
+#include "Editor.h"
+#include "Element.h"
+#include "EventNames.h"
+#include "Frame.h"
+#include "SelectionController.h"
+#include "VisiblePosition.h"
+#include "htmlediting.h"
+
+namespace WebCore {
+
+using namespace EventNames;
+
+EditCommand::EditCommand(Document* document)
+ : RefCounted<EditCommand>(0)
+ , m_document(document)
+ , m_parent(0)
+{
+ ASSERT(m_document);
+ ASSERT(m_document->frame());
+ DeleteButtonController* deleteButton = m_document->frame()->editor()->deleteButtonController();
+ setStartingSelection(avoidIntersectionWithNode(m_document->frame()->selectionController()->selection(), deleteButton ? deleteButton->containerElement() : 0));
+ setEndingSelection(m_startingSelection);
+}
+
+EditCommand::~EditCommand()
+{
+}
+
+void EditCommand::apply()
+{
+ ASSERT(m_document);
+ ASSERT(m_document->frame());
+
+ Frame* frame = m_document->frame();
+
+ if (!m_parent) {
+ 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 (!m_parent)
+ updateLayout();
+
+ DeleteButtonController* deleteButtonController = frame->editor()->deleteButtonController();
+ deleteButtonController->disable();
+ doApply();
+ deleteButtonController->enable();
+
+ // FIXME: Improve typing style.
+ // See this bug: <rdar://problem/3769899> Implementation of typing style needs improvement
+ if (!preservesTypingStyle()) {
+ setTypingStyle(0);
+ if (!m_parent)
+ frame->editor()->setRemovedAnchor(0);
+ }
+
+ if (!m_parent) {
+ updateLayout();
+ 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 (!m_parent)
+ updateLayout();
+
+ DeleteButtonController* deleteButtonController = frame->editor()->deleteButtonController();
+ deleteButtonController->disable();
+ doUnapply();
+ deleteButtonController->enable();
+
+ if (!m_parent) {
+ updateLayout();
+ 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 (!m_parent)
+ updateLayout();
+
+ DeleteButtonController* deleteButtonController = frame->editor()->deleteButtonController();
+ deleteButtonController->disable();
+ doReapply();
+ deleteButtonController->enable();
+
+ if (!m_parent) {
+ updateLayout();
+ frame->editor()->reappliedEditing(this);
+ }
+}
+
+void EditCommand::doReapply()
+{
+ doApply();
+}
+
+EditAction EditCommand::editingAction() const
+{
+ return EditActionUnspecified;
+}
+
+void EditCommand::setStartingSelection(const Selection& 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 Selection &s)
+{
+ Element* root = s.rootEditableElement();
+ for (EditCommand* cmd = this; cmd; cmd = cmd->m_parent) {
+ cmd->m_endingSelection = s;
+ cmd->m_endingRootEditableElement = root;
+ }
+}
+
+void EditCommand::setTypingStyle(PassRefPtr<CSSMutableStyleDeclaration> style)
+{
+ // FIXME: Improve typing style.
+ // See this bug: <rdar://problem/3769899> Implementation of typing style needs improvement
+ if (!m_parent) {
+ // Special more-efficient case for where there's only one command that
+ // takes advantage of the ability of PassRefPtr to pass its ref to a RefPtr.
+ m_typingStyle = style;
+ return;
+ }
+ for (EditCommand* cmd = this; cmd; cmd = cmd->m_parent)
+ cmd->m_typingStyle = style.get(); // must use get() to avoid setting parent styles to 0
+}
+
+bool EditCommand::preservesTypingStyle() const
+{
+ return false;
+}
+
+bool EditCommand::isInsertTextCommand() const
+{
+ return false;
+}
+
+bool EditCommand::isTypingCommand() const
+{
+ return false;
+}
+
+PassRefPtr<CSSMutableStyleDeclaration> EditCommand::styleAtPosition(const Position &pos)
+{
+ RefPtr<CSSMutableStyleDeclaration> style = positionBeforeTabSpan(pos).computedStyle()->copyInheritableProperties();
+
+ // FIXME: Improve typing style.
+ // See this bug: <rdar://problem/3769899> Implementation of typing style needs improvement
+ CSSMutableStyleDeclaration* typingStyle = document()->frame()->typingStyle();
+ if (typingStyle)
+ style->merge(typingStyle);
+
+ return style.release();
+}
+
+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/WebCore/editing/EditCommand.h b/WebCore/editing/EditCommand.h
new file mode 100644
index 0000000..8f4401c
--- /dev/null
+++ b/WebCore/editing/EditCommand.h
@@ -0,0 +1,94 @@
+/*
+ * 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.
+ */
+
+#ifndef EditCommand_h
+#define EditCommand_h
+
+#include "EditAction.h"
+#include "Element.h"
+#include "Selection.h"
+
+namespace WebCore {
+
+class CompositeEditCommand;
+class CSSMutableStyleDeclaration;
+
+class EditCommand : public RefCounted<EditCommand> {
+public:
+ EditCommand(Document*);
+ virtual ~EditCommand();
+
+ void setParent(CompositeEditCommand*);
+
+ void apply();
+ void unapply();
+ void reapply();
+
+ virtual EditAction editingAction() const;
+
+ const Selection& startingSelection() const { return m_startingSelection; }
+ const Selection& endingSelection() const { return m_endingSelection; }
+
+ Element* startingRootEditableElement() const { return m_startingRootEditableElement.get(); }
+ Element* endingRootEditableElement() const { return m_endingRootEditableElement.get(); }
+
+ CSSMutableStyleDeclaration* typingStyle() const { return m_typingStyle.get(); };
+ void setTypingStyle(PassRefPtr<CSSMutableStyleDeclaration>);
+
+ virtual bool isInsertTextCommand() const;
+ virtual bool isTypingCommand() const;
+
+protected:
+ Document* document() const { return m_document.get(); }
+
+ void setStartingSelection(const Selection&);
+ void setEndingSelection(const Selection&);
+
+ PassRefPtr<CSSMutableStyleDeclaration> styleAtPosition(const Position&);
+ void updateLayout() const;
+
+private:
+ virtual void doApply() = 0;
+ virtual void doUnapply() = 0;
+ virtual void doReapply(); // calls doApply()
+
+ virtual bool preservesTypingStyle() const;
+
+ RefPtr<Document> m_document;
+ Selection m_startingSelection;
+ Selection m_endingSelection;
+ RefPtr<Element> m_startingRootEditableElement;
+ RefPtr<Element> m_endingRootEditableElement;
+ RefPtr<CSSMutableStyleDeclaration> m_typingStyle;
+ CompositeEditCommand* m_parent;
+
+ friend void applyCommand(PassRefPtr<EditCommand>);
+};
+
+void applyCommand(PassRefPtr<EditCommand>);
+
+} // namespace WebCore
+
+#endif // EditCommand_h
diff --git a/WebCore/editing/Editor.cpp b/WebCore/editing/Editor.cpp
new file mode 100644
index 0000000..7b25bec
--- /dev/null
+++ b/WebCore/editing/Editor.cpp
@@ -0,0 +1,1941 @@
+/*
+ * Copyright (C) 2006, 2007 Apple Inc. All rights reserved.
+ * Copyright (C) 2007 Trolltech ASA
+ *
+ * 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 "CSSProperty.h"
+#include "CSSPropertyNames.h"
+#include "ClipboardEvent.h"
+#include "DeleteButtonController.h"
+#include "DeleteSelectionCommand.h"
+#include "DocLoader.h"
+#include "DocumentFragment.h"
+#include "EditorClient.h"
+#include "EventHandler.h"
+#include "EventNames.h"
+#include "FocusController.h"
+#include "FontData.h"
+#include "FrameView.h"
+#include "HTMLInputElement.h"
+#include "HTMLTextAreaElement.h"
+#include "HitTestResult.h"
+#include "IndentOutdentCommand.h"
+#include "InsertListCommand.h"
+#include "KeyboardEvent.h"
+#include "ModifySelectionListLevel.h"
+#include "Page.h"
+#include "Pasteboard.h"
+#include "RemoveFormatCommand.h"
+#include "ReplaceSelectionCommand.h"
+#include "Sound.h"
+#include "Text.h"
+#include "TextIterator.h"
+#include "TypingCommand.h"
+#include "htmlediting.h"
+#include "markup.h"
+#include "visible_units.h"
+
+namespace WebCore {
+
+using namespace std;
+using namespace EventNames;
+using namespace HTMLNames;
+
+// 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.
+Selection Editor::selectionForCommand(Event* event)
+{
+ Selection selection = m_frame->selectionController()->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())) {
+ if (target->hasTagName(inputTag) && static_cast<HTMLInputElement*>(target)->isTextField())
+ return static_cast<HTMLInputElement*>(target)->selection();
+ if (target->hasTagName(textareaTag))
+ return static_cast<HTMLTextAreaElement*>(target)->selection();
+ }
+ return selection;
+}
+
+EditorClient* Editor::client() const
+{
+ if (Page* page = m_frame->page())
+ return page->editorClient();
+ return 0;
+}
+
+void Editor::handleKeyboardEvent(KeyboardEvent* event)
+{
+ if (EditorClient* c = client())
+ if (selectionForCommand(event).isContentEditable())
+ c->handleKeyboardEvent(event);
+}
+
+void Editor::handleInputMethodKeydown(KeyboardEvent* event)
+{
+ if (EditorClient* c = client())
+ if (selectionForCommand(event).isContentEditable())
+ c->handleInputMethodKeydown(event);
+}
+
+bool Editor::canEdit() const
+{
+ return m_frame->selectionController()->isContentEditable();
+}
+
+bool Editor::canEditRichly() const
+{
+ return m_frame->selectionController()->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->selectionController()->isInPasswordField() && !dispatchCPPEvent(beforecutEvent, ClipboardNumb);
+}
+
+bool Editor::canDHTMLCopy()
+{
+ return !m_frame->selectionController()->isInPasswordField() && !dispatchCPPEvent(beforecopyEvent, ClipboardNumb);
+}
+
+bool Editor::canDHTMLPaste()
+{
+ return !dispatchCPPEvent(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* selectionController = m_frame->selectionController();
+ return selectionController->isRange() && !selectionController->isInPasswordField();
+}
+
+bool Editor::canPaste() const
+{
+ return canEdit();
+}
+
+bool Editor::canDelete() const
+{
+ SelectionController* selectionController = m_frame->selectionController();
+ return selectionController->isRange() && selectionController->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->selectionGranularity() == WordGranularity;
+}
+
+bool Editor::deleteWithDirection(SelectionController::EDirection direction, TextGranularity granularity, bool killRing, bool isTypingAction)
+{
+ // Delete the selection, if there is one.
+ // If not, make a selection using the passed-in direction and granularity.
+
+ if (!canEdit())
+ return false;
+
+ if (m_frame->selectionController()->isRange()) {
+ if (killRing)
+ addToKillRing(selectedRange().get(), false);
+ if (isTypingAction) {
+ if (m_frame->document()) {
+ TypingCommand::deleteKeyPressed(m_frame->document(), canSmartCopyOrDelete(), granularity);
+ revealSelectionAfterEditingOperation();
+ }
+ } else {
+ deleteSelectionWithSmartDelete(canSmartCopyOrDelete());
+ // Implicitly calls revealSelectionAfterEditingOperation().
+ }
+ } else {
+ SelectionController selectionToDelete;
+ selectionToDelete.setSelection(m_frame->selectionController()->selection());
+ selectionToDelete.modify(SelectionController::EXTEND, direction, granularity);
+ if (killRing && selectionToDelete.isCaret() && granularity != CharacterGranularity)
+ selectionToDelete.modify(SelectionController::EXTEND, direction, CharacterGranularity);
+
+ RefPtr<Range> range = selectionToDelete.toRange();
+
+ if (killRing)
+ addToKillRing(range.get(), false);
+
+ if (!m_frame->selectionController()->setSelectedRange(range.get(), DOWNSTREAM, (granularity != CharacterGranularity)))
+ return true;
+
+ switch (direction) {
+ case SelectionController::FORWARD:
+ case SelectionController::RIGHT:
+ if (m_frame->document())
+ TypingCommand::forwardDeleteKeyPressed(m_frame->document(), false, granularity);
+ break;
+ case SelectionController::BACKWARD:
+ case SelectionController::LEFT:
+ if (m_frame->document())
+ TypingCommand::deleteKeyPressed(m_frame->document(), false, granularity);
+ break;
+ }
+ revealSelectionAfterEditingOperation();
+ }
+
+ // 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->selectionController()->isNone())
+ return;
+
+ applyCommand(new DeleteSelectionCommand(m_frame->document(), smartDelete));
+}
+
+void Editor::pasteAsPlainTextWithPasteboard(Pasteboard* pasteboard)
+{
+ String text = pasteboard->plainText(m_frame);
+ if (client() && client()->shouldInsertText(text, selectedRange().get(), EditorInsertActionPasted))
+ replaceSelectionWithText(text, false, canSmartReplaceWithPasteboard(pasteboard));
+}
+
+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))
+ replaceSelectionWithFragment(fragment, false, canSmartReplaceWithPasteboard(pasteboard), chosePlainText);
+}
+
+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;
+
+ 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->selectionController()->isNone() || !fragment)
+ return;
+
+ applyCommand(new ReplaceSelectionCommand(m_frame->document(), fragment, selectReplacement, smartReplace, matchStyle));
+ revealSelectionAfterEditingOperation();
+}
+
+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->selectionController()->toRange();
+}
+
+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->selectionController()->isInPasswordField())
+ return false;
+
+ // 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(copyEvent, ClipboardWritable);
+}
+
+bool Editor::tryDHTMLCut()
+{
+ if (m_frame->selectionController()->isInPasswordField())
+ return false;
+
+ // 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(cutEvent, ClipboardWritable);
+}
+
+bool Editor::tryDHTMLPaste()
+{
+ return !dispatchCPPEvent(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 Selection& oldSelection)
+{
+ if (client())
+ client()->respondToChangedSelection();
+ m_deleteButtonController->respondToChangedSelection(oldSelection);
+}
+
+void Editor::respondToChangedContents(const Selection& endingSelection)
+{
+ if (AXObjectCache::accessibilityEnabled()) {
+ Node* node = endingSelection.start().node();
+ if (node)
+ m_frame->renderer()->document()->axObjectCache()->postNotification(node->renderer(), "AXValueChanged");
+ }
+
+ if (client())
+ client()->respondToChangedContents();
+}
+
+const SimpleFontData* Editor::fontForSelection(bool& hasMultipleFonts) const
+{
+#if !PLATFORM(QT)
+ hasMultipleFonts = false;
+
+ if (!m_frame->selectionController()->isRange()) {
+ Node* nodeToRemove;
+ RenderStyle* style = m_frame->styleForSelectionStart(nodeToRemove); // sets nodeToRemove
+
+ const SimpleFontData* result = 0;
+ if (style)
+ result = style->font().primaryFont();
+
+ if (nodeToRemove) {
+ ExceptionCode ec;
+ nodeToRemove->remove(ec);
+ ASSERT(ec == 0);
+ }
+
+ return result;
+ }
+
+ const SimpleFontData* font = 0;
+
+ RefPtr<Range> range = m_frame->selectionController()->toRange();
+ Node* startNode = range->editingStartPosition().node();
+ if (startNode) {
+ Node* pastEnd = range->pastEndNode();
+ // 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
+}
+
+TriState Editor::selectionUnorderedListState() const
+{
+ if (m_frame->selectionController()->isCaret()) {
+ if (enclosingNodeWithTag(m_frame->selectionController()->selection().start(), ulTag))
+ return TrueTriState;
+ } else if (m_frame->selectionController()->isRange()) {
+ Node* startNode = enclosingNodeWithTag(m_frame->selectionController()->selection().start(), ulTag);
+ Node* endNode = enclosingNodeWithTag(m_frame->selectionController()->selection().end(), ulTag);
+ if (startNode && endNode && startNode == endNode)
+ return TrueTriState;
+ }
+
+ return FalseTriState;
+}
+
+TriState Editor::selectionOrderedListState() const
+{
+ if (m_frame->selectionController()->isCaret()) {
+ if (enclosingNodeWithTag(m_frame->selectionController()->selection().start(), olTag))
+ return TrueTriState;
+ } else if (m_frame->selectionController()->isRange()) {
+ Node* startNode = enclosingNodeWithTag(m_frame->selectionController()->selection().start(), olTag);
+ Node* endNode = enclosingNodeWithTag(m_frame->selectionController()->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->selectionController()->isNone())
+ return 0;
+
+ RefPtr<Node> newList = IncreaseSelectionListLevelCommand::increaseSelectionListLevel(m_frame->document());
+ revealSelectionAfterEditingOperation();
+ return newList;
+}
+
+PassRefPtr<Node> Editor::increaseSelectionListLevelOrdered()
+{
+ if (!canEditRichly() || m_frame->selectionController()->isNone())
+ return 0;
+
+ PassRefPtr<Node> newList = IncreaseSelectionListLevelCommand::increaseSelectionListLevelOrdered(m_frame->document());
+ revealSelectionAfterEditingOperation();
+ return newList;
+}
+
+PassRefPtr<Node> Editor::increaseSelectionListLevelUnordered()
+{
+ if (!canEditRichly() || m_frame->selectionController()->isNone())
+ return 0;
+
+ PassRefPtr<Node> newList = IncreaseSelectionListLevelCommand::increaseSelectionListLevelUnordered(m_frame->document());
+ revealSelectionAfterEditingOperation();
+ return newList;
+}
+
+void Editor::decreaseSelectionListLevel()
+{
+ if (!canEditRichly() || m_frame->selectionController()->isNone())
+ return;
+
+ DecreaseSelectionListLevelCommand::decreaseSelectionListLevel(m_frame->document());
+ revealSelectionAfterEditingOperation();
+}
+
+void Editor::removeFormattingAndStyle()
+{
+ applyCommand(new RemoveFormatCommand(m_frame->document()));
+}
+
+void Editor::setLastEditCommand(PassRefPtr<EditCommand> lastEditCommand)
+{
+ m_lastEditCommand = lastEditCommand;
+}
+
+// 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 = m_frame->selectionController()->start().element();
+ if (!target && m_frame->document())
+ target = m_frame->document()->body();
+ if (!target)
+ return true;
+ target = target->shadowAncestorNode();
+
+ RefPtr<Clipboard> clipboard = newGeneralClipboard(policy);
+
+ ExceptionCode ec = 0;
+ RefPtr<Event> evt = new ClipboardEvent(eventType, true, true, clipboard.get());
+ EventTargetNodeCast(target)->dispatchEvent(evt, ec, true);
+ bool noDefaultProcessing = evt->defaultPrevented();
+
+ // invalidate clipboard here for security
+ clipboard->setAccessPolicy(ClipboardNumb);
+
+ return !noDefaultProcessing;
+}
+
+void Editor::applyStyle(CSSStyleDeclaration* style, EditAction editingAction)
+{
+ switch (m_frame->selectionController()->state()) {
+ case Selection::NONE:
+ // do nothing
+ break;
+ case Selection::CARET:
+ m_frame->computeAndSetTypingStyle(style, editingAction);
+ break;
+ case Selection::RANGE:
+ if (m_frame->document() && style)
+ applyCommand(new ApplyStyleCommand(m_frame->document(), style, editingAction));
+ break;
+ }
+}
+
+bool Editor::shouldApplyStyle(CSSStyleDeclaration* style, Range* range)
+{
+ return client()->shouldApplyStyle(style, range);
+}
+
+void Editor::applyParagraphStyle(CSSStyleDeclaration* style, EditAction editingAction)
+{
+ switch (m_frame->selectionController()->state()) {
+ case Selection::NONE:
+ // do nothing
+ break;
+ case Selection::CARET:
+ case Selection::RANGE:
+ if (m_frame->document() && style)
+ applyCommand(new ApplyStyleCommand(m_frame->document(), style, editingAction, ApplyStyleCommand::ForceBlockProperties));
+ break;
+ }
+}
+
+void Editor::applyStyleToSelection(CSSStyleDeclaration* style, EditAction editingAction)
+{
+ if (!style || style->length() == 0 || !canEditRichly())
+ return;
+
+ if (client() && client()->shouldApplyStyle(style, m_frame->selectionController()->toRange().get()))
+ applyStyle(style, editingAction);
+}
+
+void Editor::applyParagraphStyleToSelection(CSSStyleDeclaration* style, EditAction editingAction)
+{
+ if (!style || style->length() == 0 || !canEditRichly())
+ return;
+
+ if (client() && client()->shouldApplyStyle(style, m_frame->selectionController()->toRange().get()))
+ applyParagraphStyle(style, editingAction);
+}
+
+bool Editor::clientIsEditable() const
+{
+ return client() && client()->isEditable();
+}
+
+bool Editor::selectionStartHasStyle(CSSStyleDeclaration* style) const
+{
+ Node* nodeToRemove;
+ RefPtr<CSSComputedStyleDeclaration> selectionStyle = m_frame->selectionComputedStyle(nodeToRemove);
+ if (!selectionStyle)
+ return false;
+
+ RefPtr<CSSMutableStyleDeclaration> mutableStyle = style->makeMutable();
+
+ bool match = true;
+ DeprecatedValueListConstIterator<CSSProperty> end;
+ for (DeprecatedValueListConstIterator<CSSProperty> it = mutableStyle->valuesIterator(); it != end; ++it) {
+ int propertyID = (*it).id();
+ if (!equalIgnoringCase(mutableStyle->getPropertyValue(propertyID), selectionStyle->getPropertyValue(propertyID))) {
+ match = false;
+ break;
+ }
+ }
+
+ if (nodeToRemove) {
+ ExceptionCode ec = 0;
+ nodeToRemove->remove(ec);
+ ASSERT(ec == 0);
+ }
+
+ return match;
+}
+
+static void updateState(CSSMutableStyleDeclaration* desiredStyle, CSSComputedStyleDeclaration* computedStyle, bool& atStart, TriState& state)
+{
+ DeprecatedValueListConstIterator<CSSProperty> end;
+ for (DeprecatedValueListConstIterator<CSSProperty> it = desiredStyle->valuesIterator(); it != end; ++it) {
+ int propertyID = (*it).id();
+ String desiredProperty = desiredStyle->getPropertyValue(propertyID);
+ String computedProperty = computedStyle->getPropertyValue(propertyID);
+ TriState propertyState = equalIgnoringCase(desiredProperty, computedProperty)
+ ? TrueTriState : FalseTriState;
+ if (atStart) {
+ state = propertyState;
+ atStart = false;
+ } else if (state != propertyState) {
+ state = MixedTriState;
+ break;
+ }
+ }
+}
+
+TriState Editor::selectionHasStyle(CSSStyleDeclaration* style) const
+{
+ bool atStart = true;
+ TriState state = FalseTriState;
+
+ RefPtr<CSSMutableStyleDeclaration> mutableStyle = style->makeMutable();
+
+ if (!m_frame->selectionController()->isRange()) {
+ Node* nodeToRemove;
+ RefPtr<CSSComputedStyleDeclaration> selectionStyle = m_frame->selectionComputedStyle(nodeToRemove);
+ if (!selectionStyle)
+ return FalseTriState;
+ updateState(mutableStyle.get(), selectionStyle.get(), atStart, state);
+ if (nodeToRemove) {
+ ExceptionCode ec = 0;
+ nodeToRemove->remove(ec);
+ ASSERT(ec == 0);
+ }
+ } else {
+ for (Node* node = m_frame->selectionController()->start().node(); node; node = node->traverseNextNode()) {
+ RefPtr<CSSComputedStyleDeclaration> computedStyle = new CSSComputedStyleDeclaration(node);
+ if (computedStyle)
+ updateState(mutableStyle.get(), computedStyle.get(), atStart, state);
+ if (state == MixedTriState)
+ break;
+ if (node == m_frame->selectionController()->end().node())
+ break;
+ }
+ }
+
+ return state;
+}
+void Editor::indent()
+{
+ applyCommand(new IndentOutdentCommand(m_frame->document(), IndentOutdentCommand::Indent));
+}
+
+void Editor::outdent()
+{
+ applyCommand(new IndentOutdentCommand(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(new Event(webkitEditableContentChangedEvent, false, false), ec, true);
+ if (endRoot && endRoot != startRoot)
+ endRoot->dispatchEvent(new Event(webkitEditableContentChangedEvent, false, false), ec, true);
+}
+
+void Editor::appliedEditing(PassRefPtr<EditCommand> cmd)
+{
+ dispatchEditableContentChangedEvents(*cmd);
+
+ // FIXME: We shouldn't tell setSelection to clear the typing style or removed anchor here.
+ // If we didn't, we wouldn't have to save/restore the removedAnchor, and we wouldn't have to have
+ // the typing style stored in two places (the Frame and commands).
+ RefPtr<Node> anchor = removedAnchor();
+
+ Selection newSelection(cmd->endingSelection());
+ // 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
+ if (newSelection == m_frame->selectionController()->selection() || m_frame->shouldChangeSelection(newSelection))
+ m_frame->selectionController()->setSelection(newSelection, false);
+
+ setRemovedAnchor(anchor);
+
+ // Now set the typing style from the command. Clear it when done.
+ // This helps make the case work where you completely delete a piece
+ // of styled text and then type a character immediately after.
+ // That new character needs to take on the style of the just-deleted text.
+ // FIXME: Improve typing style.
+ // See this bug: <rdar://problem/3769899> Implementation of typing style needs improvement
+ if (cmd->typingStyle()) {
+ m_frame->setTypingStyle(cmd->typingStyle());
+ cmd->setTypingStyle(0);
+ }
+
+ // 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)
+{
+ dispatchEditableContentChangedEvents(*cmd);
+
+ Selection newSelection(cmd->startingSelection());
+ // 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
+ if (newSelection == m_frame->selectionController()->selection() || m_frame->shouldChangeSelection(newSelection))
+ m_frame->selectionController()->setSelection(newSelection, true);
+
+ m_lastEditCommand = 0;
+ if (client())
+ client()->registerCommandForRedo(cmd);
+ respondToChangedContents(newSelection);
+}
+
+void Editor::reappliedEditing(PassRefPtr<EditCommand> cmd)
+{
+ dispatchEditableContentChangedEvents(*cmd);
+
+ Selection newSelection(cmd->endingSelection());
+ // 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
+ if (newSelection == m_frame->selectionController()->selection() || m_frame->shouldChangeSelection(newSelection))
+ m_frame->selectionController()->setSelection(newSelection, true);
+
+ m_lastEditCommand = 0;
+ if (client())
+ client()->registerCommandForUndo(cmd);
+ respondToChangedContents(newSelection);
+}
+
+Editor::Editor(Frame* frame)
+ : m_frame(frame)
+ , m_deleteButtonController(new DeleteButtonController(frame))
+ , m_ignoreCompositionSelectionChange(false)
+ , m_shouldStartNewKillRingSequence(false)
+{
+}
+
+Editor::~Editor()
+{
+}
+
+void Editor::clear()
+{
+ m_compositionNode = 0;
+ m_customCompositionUnderlines.clear();
+}
+
+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;
+
+ Selection selection = selectionForCommand(triggeringEvent);
+ if (!selection.isContentEditable())
+ return false;
+ RefPtr<Range> range = selection.toRange();
+
+ 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()->revealSelection(RenderLayer::gAlignToEdgeIfNeeded);
+ }
+ }
+
+ return true;
+}
+
+bool Editor::insertLineBreak()
+{
+ if (!canEdit())
+ return false;
+
+ if (!shouldInsertText("\n", m_frame->selectionController()->toRange().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->selectionController()->toRange().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())) {
+ 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;
+ }
+
+ 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());
+ DocLoader* loader = m_frame->document()->docLoader();
+#if PLATFORM(MAC)
+ // using the platform independent code below requires moving all of
+ // WEBHTMLView: _documentFragmentFromPasteboard over to PasteboardMac.
+ loader->setAllowStaleResources(true);
+ m_frame->issuePasteCommand();
+ loader->setAllowStaleResources(false);
+#else
+ if (tryDHTMLPaste())
+ return; // DHTML did the whole operation
+ if (!canPaste())
+ return;
+ loader->setAllowStaleResources(true);
+ if (m_frame->selectionController()->isContentRichlyEditable())
+ pasteWithPasteboard(Pasteboard::generalPasteboard(), true);
+ else
+ pasteAsPlainTextWithPasteboard(Pasteboard::generalPasteboard());
+ loader->setAllowStaleResources(false);
+#endif
+}
+
+void Editor::pasteAsPlainText()
+{
+ if (!canPaste())
+ return;
+ 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;
+}
+
+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(const String& direction)
+{
+ ExceptionCode ec = 0;
+
+ RefPtr<CSSMutableStyleDeclaration> style = new CSSMutableStyleDeclaration;
+ style->setProperty(CSS_PROP_DIRECTION, direction, false, ec);
+ 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>
+ Selection selection;
+ selection.setWithoutValidation(range->startPosition(), range->endPosition());
+ m_frame->selectionController()->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)
+{
+ setIgnoreCompositionSelectionChange(true);
+
+ Selection oldSelection = m_frame->selectionController()->selection();
+
+ selectComposition();
+
+ if (m_frame->selectionController()->isNone()) {
+ setIgnoreCompositionSelectionChange(false);
+ return;
+ }
+
+ // If there is a composition to replace, remove it with a deletion that will be part of the
+ // same Undo step as the next and previous insertions.
+ TypingCommand::deleteSelection(m_frame->document(), false);
+
+ m_compositionNode = 0;
+ m_customCompositionUnderlines.clear();
+
+ insertText(text, 0);
+
+ if (preserveSelection)
+ m_frame->selectionController()->setSelection(oldSelection, false, false);
+
+ setIgnoreCompositionSelectionChange(false);
+}
+
+void Editor::setComposition(const String& text, const Vector<CompositionUnderline>& underlines, unsigned selectionStart, unsigned selectionEnd)
+{
+ setIgnoreCompositionSelectionChange(true);
+
+ selectComposition();
+
+ if (m_frame->selectionController()->isNone()) {
+ setIgnoreCompositionSelectionChange(false);
+ return;
+ }
+
+ // If there is a composition to replace, remove it with a deletion that will be part of the
+ // same Undo step as the next and previous insertions.
+ TypingCommand::deleteSelection(m_frame->document(), false);
+
+ m_compositionNode = 0;
+ m_customCompositionUnderlines.clear();
+
+ if (!text.isEmpty()) {
+ TypingCommand::insertText(m_frame->document(), text, true, true);
+
+ Node* baseNode = m_frame->selectionController()->base().node();
+ unsigned baseOffset = m_frame->selectionController()->base().offset();
+ Node* extentNode = m_frame->selectionController()->extent().node();
+ unsigned extentOffset = m_frame->selectionController()->extent().offset();
+
+ 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 = new Range(baseNode->document(), baseNode, start, baseNode, end);
+ m_frame->selectionController()->setSelectedRange(selectedRange.get(), DOWNSTREAM, false);
+ }
+ }
+
+ setIgnoreCompositionSelectionChange(false);
+}
+
+void Editor::ignoreSpelling()
+{
+ if (!client())
+ return;
+
+ String text = frame()->selectedText();
+ ASSERT(text.length() != 0);
+ client()->ignoreWordInSpellDocument(text);
+}
+
+void Editor::learnSpelling()
+{
+ if (!client())
+ return;
+
+ String text = frame()->selectedText();
+ ASSERT(text.length() != 0);
+ client()->learnWord(text);
+}
+
+static String findFirstMisspellingInRange(EditorClient* client, Range* searchRange, int& firstMisspellingOffset, bool markAll)
+{
+ ASSERT_ARG(client, client);
+ ASSERT_ARG(searchRange, searchRange);
+
+ WordAwareIterator it(searchRange);
+ 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;
+ 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 == 0 || misspellingLocation >= 0);
+ ASSERT(misspellingLocation < len);
+ ASSERT(misspellingLength <= len);
+ ASSERT(misspellingLocation + misspellingLength <= len);
+
+ if (misspellingLocation >= 0 && misspellingLength > 0 && misspellingLocation < len && misspellingLength <= len && misspellingLocation + misspellingLength <= len) {
+
+ // Remember first-encountered misspelling and its offset
+ if (!firstMisspelling) {
+ firstMisspellingOffset = currentChunkOffset + misspellingLocation;
+ firstMisspelling = String(chars + misspellingLocation, misspellingLength);
+ }
+
+ // Mark this instance if we're marking all instances. Otherwise bail out because we found the first one.
+ if (!markAll)
+ break;
+
+ // Compute range of misspelled word
+ RefPtr<Range> misspellingRange = TextIterator::subrange(searchRange, currentChunkOffset + misspellingLocation, misspellingLength);
+
+ // Store marker for misspelled word
+ ExceptionCode ec = 0;
+ misspellingRange->startContainer(ec)->document()->addMarker(misspellingRange.get(), DocumentMarker::Spelling);
+ ASSERT(ec == 0);
+ }
+ }
+
+ currentChunkOffset += len;
+ it.advance();
+ }
+
+ return firstMisspelling;
+}
+
+#ifndef BUILDING_ON_TIGER
+
+static PassRefPtr<Range> paragraphAlignedRangeForRange(Range* arbitraryRange, int& offsetIntoParagraphAlignedRange, String& paragraphString)
+{
+ ASSERT_ARG(arbitraryRange, arbitraryRange);
+
+ ExceptionCode ec = 0;
+
+ // Expand range to paragraph boundaries
+ RefPtr<Range> paragraphRange = arbitraryRange->cloneRange(ec);
+ setStart(paragraphRange.get(), startOfParagraph(arbitraryRange->startPosition()));
+ setEnd(paragraphRange.get(), endOfParagraph(arbitraryRange->endPosition()));
+
+ // Compute offset from start of expanded range to start of original range
+ RefPtr<Range> offsetAsRange = new Range(paragraphRange->startContainer(ec)->document(), paragraphRange->startPosition(), arbitraryRange->startPosition());
+ offsetIntoParagraphAlignedRange = TextIterator::rangeLength(offsetAsRange.get());
+
+ // Fill in out parameter with string representing entire paragraph range.
+ // Someday we might have a caller that doesn't use this, but for now all callers do.
+ paragraphString = plainText(paragraphRange.get());
+
+ return paragraphRange;
+}
+
+static int findFirstGrammarDetailInRange(const Vector<GrammarDetail>& grammarDetails, int badGrammarPhraseLocation, int badGrammarPhraseLength, Range *searchRange, int startOffset, int endOffset, bool markAll)
+{
+ // 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(searchRange, badGrammarPhraseLocation - startOffset + detail->location, detail->length);
+ ExceptionCode ec = 0;
+ badGrammarRange->startContainer(ec)->document()->addMarker(badGrammarRange.get(), DocumentMarker::Grammar, detail->userDescription);
+ ASSERT(ec == 0);
+ }
+
+ // 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;
+}
+
+static String findFirstBadGrammarInRange(EditorClient* client, Range* searchRange, GrammarDetail& outGrammarDetail, int& outGrammarPhraseOffset, bool markAll)
+{
+ ASSERT_ARG(client, client);
+ ASSERT_ARG(searchRange, searchRange);
+
+ // 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.
+ int searchRangeStartOffset;
+ String paragraphString;
+ RefPtr<Range> paragraphRange = paragraphAlignedRangeForRange(searchRange, searchRangeStartOffset, paragraphString);
+
+ // Determine the character offset from the start of the paragraph to the end of the original search range,
+ // since we will want to ignore results in this area also.
+ int searchRangeEndOffset = searchRangeStartOffset + TextIterator::rangeLength(searchRange);
+
+ // 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 < searchRangeEndOffset) {
+ Vector<GrammarDetail> grammarDetails;
+ int badGrammarPhraseLocation = -1;
+ int badGrammarPhraseLength = 0;
+ client->checkGrammarOfString(paragraphString.characters() + startOffset, paragraphString.length() - startOffset, grammarDetails, &badGrammarPhraseLocation, &badGrammarPhraseLength);
+
+ if (badGrammarPhraseLength == 0) {
+ 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 = findFirstGrammarDetailInRange(grammarDetails, badGrammarPhraseLocation, badGrammarPhraseLength, searchRange, searchRangeStartOffset, searchRangeEndOffset, 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 - searchRangeStartOffset;
+ firstBadGrammarPhrase = paragraphString.substring(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;
+}
+
+#endif /* not BUILDING_ON_TIGER */
+
+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.
+ Selection selection(frame()->selectionController()->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.offset(), ec);
+ startedWithSelection = false; // won't need to wrap
+ }
+
+ // topNode defines the whole range we want to operate on
+ Node* topNode = highestEditableRoot(position);
+ spellingSearchRange->setEnd(topNode, maxDeepOffset(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;
+ String misspelledWord = findFirstMisspellingInRange(client(), spellingSearchRange.get(), misspellingOffset, false);
+
+ 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 = findFirstBadGrammarInRange(client(), grammarSearchRange.get(), grammarDetail, grammarPhraseOffset, false);
+#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);
+
+ misspelledWord = findFirstMisspellingInRange(client(), spellingSearchRange.get(), misspellingOffset, false);
+
+#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 = findFirstBadGrammarInRange(client(), grammarSearchRange.get(), grammarDetail, grammarPhraseOffset, false);
+#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()->selectionController()->setSelection(Selection(badGrammarRange.get(), SEL_DEFAULT_AFFINITY));
+ frame()->revealSelection();
+
+ client()->updateSpellingUIWithGrammarString(badGrammarPhrase, grammarDetail);
+ frame()->document()->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()->selectionController()->setSelection(Selection(misspellingRange.get(), DOWNSTREAM));
+ frame()->revealSelection();
+
+ client()->updateSpellingUIWithMisspelledWord(misspelledWord);
+ frame()->document()->addMarker(misspellingRange.get(), DocumentMarker::Spelling);
+ }
+}
+
+bool Editor::isSelectionMisspelled()
+{
+ String selectedString = frame()->selectedText();
+ int length = selectedString.length();
+ if (length == 0)
+ 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;
+}
+
+#ifndef BUILDING_ON_TIGER
+static bool isRangeUngrammatical(EditorClient* client, Range *range, Vector<String>& guessesVector)
+{
+ if (!client)
+ return false;
+
+ ExceptionCode ec;
+ if (!range || 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 = findFirstBadGrammarInRange(client, range, 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 != 0)
+ 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(range))
+ 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->updateSpellingUIWithGrammarString(badGrammarPhrase, grammarDetail);
+
+ return true;
+}
+#endif
+
+bool Editor::isSelectionUngrammatical()
+{
+#ifdef BUILDING_ON_TIGER
+ return false;
+#else
+ Vector<String> ignoredGuesses;
+ return isRangeUngrammatical(client(), frame()->selectionController()->toRange().get(), ignoredGuesses);
+#endif
+}
+
+Vector<String> Editor::guessesForUngrammaticalSelection()
+{
+#ifdef BUILDING_ON_TIGER
+ return Vector<String>();
+#else
+ Vector<String> guesses;
+ // Ignore the result of isRangeUngrammatical; we just want the guesses, whether or not there are any
+ isRangeUngrammatical(client(), frame()->selectionController()->toRange().get(), guesses);
+ return guesses;
+#endif
+}
+
+Vector<String> Editor::guessesForMisspelledSelection()
+{
+ String selectedString = frame()->selectedText();
+ ASSERT(selectedString.length() != 0);
+
+ Vector<String> guesses;
+ if (client())
+ client()->getGuessesForWord(selectedString, guesses);
+ return guesses;
+}
+
+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::markMisspellingsAfterTypingToPosition(const VisiblePosition &p)
+{
+ if (!isContinuousSpellCheckingEnabled())
+ return;
+
+ // Check spelling of one word
+ markMisspellings(Selection(startOfWord(p, LeftWordIfOnBoundary), endOfWord(p, RightWordIfOnBoundary)));
+
+ if (!isGrammarCheckingEnabled())
+ return;
+
+ // Check grammar of entire sentence
+ markBadGrammar(Selection(startOfSentence(p), endOfSentence(p)));
+}
+
+static void markAllMisspellingsInRange(EditorClient* client, Range* searchRange)
+{
+ // Use the "markAll" feature of findFirstMisspellingInRange. Ignore the return value and the "out parameter";
+ // all we need to do is mark every instance.
+ int ignoredOffset;
+ findFirstMisspellingInRange(client, searchRange, ignoredOffset, true);
+}
+
+#ifndef BUILDING_ON_TIGER
+static void markAllBadGrammarInRange(EditorClient* client, Range* searchRange)
+{
+ // Use the "markAll" feature of findFirstBadGrammarInRange. Ignore the return value and "out parameters"; all we need to
+ // do is mark every instance.
+ GrammarDetail ignoredGrammarDetail;
+ int ignoredOffset;
+ findFirstBadGrammarInRange(client, searchRange, ignoredGrammarDetail, ignoredOffset, true);
+}
+#endif
+
+static void markMisspellingsOrBadGrammar(Editor* editor, const Selection& selection, bool checkSpelling)
+{
+ // 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 (!editor->isContinuousSpellCheckingEnabled())
+ return;
+
+ RefPtr<Range> searchRange(selection.toRange());
+ if (!searchRange || searchRange->isDetached())
+ return;
+
+ // If we're not in an editable node, bail.
+ int exception = 0;
+ Node *editableNode = searchRange->startContainer(exception);
+ if (!editableNode->isContentEditable())
+ return;
+
+ // Get the spell checker if it is available
+ if (!editor->client())
+ return;
+
+ if (checkSpelling)
+ markAllMisspellingsInRange(editor->client(), searchRange.get());
+ else {
+#ifdef BUILDING_ON_TIGER
+ ASSERT_NOT_REACHED();
+#else
+ if (editor->isGrammarCheckingEnabled())
+ markAllBadGrammarInRange(editor->client(), searchRange.get());
+#endif
+ }
+}
+
+void Editor::markMisspellings(const Selection& selection)
+{
+ markMisspellingsOrBadGrammar(this, selection, true);
+}
+
+void Editor::markBadGrammar(const Selection& selection)
+{
+#ifndef BUILDING_ON_TIGER
+ markMisspellingsOrBadGrammar(this, selection, false);
+#endif
+}
+
+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);
+ Selection selection(frame->visiblePositionForPoint(framePoint));
+ return avoidIntersectionWithNode(selection.toRange().get(), deleteButtonController() ? deleteButtonController()->containerElement() : 0);
+}
+
+void Editor::revealSelectionAfterEditingOperation()
+{
+ if (m_ignoreCompositionSelectionChange)
+ return;
+
+ m_frame->revealSelection(RenderLayer::gAlignToEdgeIfNeeded);
+}
+
+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 new Range(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->selectionController()->start();
+ if (start.node() != m_compositionNode)
+ return false;
+ Position end = m_frame->selectionController()->end();
+ if (end.node() != m_compositionNode)
+ return false;
+
+ if (static_cast<unsigned>(start.offset()) < m_compositionStart)
+ return false;
+ if (static_cast<unsigned>(end.offset()) > m_compositionEnd)
+ return false;
+
+ selectionStart = start.offset() - m_compositionStart;
+ selectionEnd = start.offset() - m_compositionEnd;
+ return true;
+}
+
+void Editor::transpose()
+{
+ if (!canEdit())
+ return;
+
+ Selection selection = m_frame->selectionController()->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;
+ Selection 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->selectionController()->selection()) {
+ if (!m_frame->shouldChangeSelection(newSelection))
+ return;
+ m_frame->selectionController()->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)
+ startNewKillRingSequence();
+
+ String text = plainText(range);
+ text.replace('\\', m_frame->backslashAsCurrencySymbol());
+ if (prepend)
+ prependToKillRing(text);
+ else
+ appendToKillRing(text);
+ m_shouldStartNewKillRingSequence = false;
+}
+
+#if !PLATFORM(MAC)
+
+void Editor::appendToKillRing(const String&)
+{
+}
+
+void Editor::prependToKillRing(const String&)
+{
+}
+
+String Editor::yankFromKillRing()
+{
+ return String();
+}
+
+void Editor::startNewKillRingSequence()
+{
+}
+
+void Editor::setKillRingToYankedState()
+{
+}
+
+#endif
+
+} // namespace WebCore
diff --git a/WebCore/editing/Editor.h b/WebCore/editing/Editor.h
new file mode 100644
index 0000000..a42ad12
--- /dev/null
+++ b/WebCore/editing/Editor.h
@@ -0,0 +1,309 @@
+/*
+ * 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 Editor_h
+#define Editor_h
+
+#include "ClipboardAccessPolicy.h"
+#include "EditorDeleteAction.h"
+#include "EditorInsertAction.h"
+#include "Frame.h"
+#include "SelectionController.h"
+#include <wtf/Forward.h>
+#include <wtf/OwnPtr.h>
+#include <wtf/RefPtr.h>
+
+#if PLATFORM(MAC)
+class NSString;
+class NSURL;
+#endif
+
+namespace WebCore {
+
+class Clipboard;
+class DeleteButtonController;
+class DocumentFragment;
+class EditCommand;
+class EditorInternalCommand;
+class EditorClient;
+class EventTargetNode;
+class Frame;
+class HTMLElement;
+class Pasteboard;
+class Range;
+class SelectionController;
+class Selection;
+class SimpleFontData;
+
+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 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> fragment, PassRefPtr<Range> replacingDOMRange, EditorInsertAction givenAction);
+ bool shouldInsertText(const String&, Range*, EditorInsertAction) const;
+ bool shouldShowDeleteInterface(HTMLElement*) const;
+ bool shouldDeleteRange(Range*) const;
+ bool shouldApplyStyle(CSSStyleDeclaration*, Range*);
+
+ void respondToChangedSelection(const Selection& oldSelection);
+ void respondToChangedContents(const Selection& endingSelection);
+
+ TriState selectionHasStyle(CSSStyleDeclaration*) const;
+ const SimpleFontData* fontForSelection(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();
+
+ // FIXME: Once the Editor implements all editing commands, it should track
+ // the lastEditCommand on its own, and we should remove this function.
+ void setLastEditCommand(PassRefPtr<EditCommand> lastEditCommand);
+
+ bool deleteWithDirection(SelectionController::EDirection, 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;
+
+ class Command {
+ public:
+ Command();
+ Command(PassRefPtr<Frame>, const EditorInternalCommand*, EditorCommandSource);
+
+ 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:
+ RefPtr<Frame> m_frame;
+ const EditorInternalCommand* m_command;
+ EditorCommandSource m_source;
+ };
+ Command command(const String& commandName); // Default is CommandFromMenuOrKeyBinding.
+ Command command(const String& commandName, EditorCommandSource);
+
+ bool insertText(const String&, Event* triggeringEvent);
+ bool insertTextWithoutSendingTextEvent(const String&, bool selectInsertedText, Event* triggeringEvent = 0);
+ 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();
+ void markMisspellingsAfterTypingToPosition(const VisiblePosition&);
+ void markMisspellings(const Selection&);
+ void markBadGrammar(const Selection&);
+ 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(const String&);
+
+ bool smartInsertDeleteEnabled();
+
+ // 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();
+
+ Selection selectionForCommand(Event*);
+
+#if PLATFORM(MAC)
+ NSString* userVisibleString(NSURL*);
+#endif
+
+ void appendToKillRing(const String&);
+ void prependToKillRing(const String&);
+ String yankFromKillRing();
+ void startNewKillRingSequence();
+ void setKillRingToYankedState();
+
+ PassRefPtr<Range> selectedRange();
+
+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 canDeleteRange(Range*) const;
+ bool canSmartReplaceWithPasteboard(Pasteboard*);
+ PassRefPtr<Clipboard> newGeneralClipboard(ClipboardAccessPolicy);
+ 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 selectComposition();
+ void confirmComposition(const String&, bool preserveSelection);
+ void setIgnoreCompositionSelectionChange(bool ignore);
+
+ void addToKillRing(Range*, bool prepend);
+};
+
+inline void Editor::setStartNewKillRingSequence(bool flag)
+{
+ m_shouldStartNewKillRingSequence = flag;
+}
+
+} // namespace WebCore
+
+#endif // Editor_h
diff --git a/WebCore/editing/EditorCommand.cpp b/WebCore/editing/EditorCommand.cpp
new file mode 100644
index 0000000..0fb64c1
--- /dev/null
+++ b/WebCore/editing/EditorCommand.cpp
@@ -0,0 +1,1402 @@
+/*
+ * Copyright (C) 2006, 2007 Apple Inc. All rights reserved.
+ * Copyright (C) 2007 Trolltech ASA
+ *
+ * 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 "AtomicString.h"
+#include "CSSPropertyNames.h"
+#include "CreateLinkCommand.h"
+#include "DocumentFragment.h"
+#include "Editor.h"
+#include "EditorClient.h"
+#include "Event.h"
+#include "EventHandler.h"
+#include "FormatBlockCommand.h"
+#include "HTMLFontElement.h"
+#include "HTMLImageElement.h"
+#include "IndentOutdentCommand.h"
+#include "InsertListCommand.h"
+#include "Page.h"
+#include "ReplaceSelectionCommand.h"
+#include "Settings.h"
+#include "Sound.h"
+#include "TypingCommand.h"
+#include "UnlinkCommand.h"
+#include "htmlediting.h"
+#include "markup.h"
+
+namespace WebCore {
+
+using namespace HTMLNames;
+
+class EditorInternalCommand {
+public:
+ bool (*execute)(Frame*, Event*, EditorCommandSource, const String&);
+ bool (*isSupported)(Frame*, EditorCommandSource);
+ bool (*isEnabled)(Frame*, Event*, EditorCommandSource);
+ TriState (*state)(Frame*, Event*);
+ String (*value)(Frame*, Event*);
+ bool isTextInsertion;
+};
+
+typedef HashMap<String, const EditorInternalCommand*, CaseFoldingHash> CommandMap;
+
+static const bool notTextInsertion = false;
+static const bool isTextInsertion = true;
+
+// 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 executeApplyStyle(Frame* frame, EditorCommandSource source, EditAction action, int propertyID, const String& propertyValue)
+{
+ RefPtr<CSSMutableStyleDeclaration> style = new CSSMutableStyleDeclaration;
+ 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()->applyStyleToSelection(style.get(), action);
+ return true;
+ case CommandFromDOM:
+ case CommandFromDOMWithUserInterface:
+ frame->editor()->applyStyle(style.get());
+ return true;
+ }
+ ASSERT_NOT_REACHED();
+ return false;
+}
+
+static bool executeApplyStyle(Frame* frame, EditorCommandSource source, EditAction action, int propertyID, const char* propertyValue)
+{
+ return executeApplyStyle(frame, source, action, propertyID, String(propertyValue));
+}
+
+static bool executeApplyStyle(Frame* frame, EditorCommandSource source, EditAction action, int propertyID, int propertyValue)
+{
+ RefPtr<CSSMutableStyleDeclaration> style = new CSSMutableStyleDeclaration;
+ 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()->applyStyleToSelection(style.get(), action);
+ return true;
+ case CommandFromDOM:
+ case CommandFromDOMWithUserInterface:
+ frame->editor()->applyStyle(style.get());
+ return true;
+ }
+ ASSERT_NOT_REACHED();
+ return false;
+}
+
+static bool executeToggleStyle(Frame* frame, EditorCommandSource source, EditAction action, int propertyID, const char* offValue, const char* onValue)
+{
+ RefPtr<CSSMutableStyleDeclaration> style = new CSSMutableStyleDeclaration;
+ style->setProperty(propertyID, onValue);
+ style->setProperty(propertyID, frame->editor()->selectionStartHasStyle(style.get()) ? offValue : onValue);
+ // 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.get(), action);
+ return true;
+ case CommandFromDOM:
+ case CommandFromDOMWithUserInterface:
+ frame->editor()->applyStyle(style.get());
+ return true;
+ }
+ ASSERT_NOT_REACHED();
+ return false;
+}
+
+static bool executeApplyParagraphStyle(Frame* frame, EditorCommandSource source, EditAction action, int propertyID, const String& propertyValue)
+{
+ RefPtr<CSSMutableStyleDeclaration> style = new CSSMutableStyleDeclaration;
+ 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(new ReplaceSelectionCommand(frame->document(), fragment,
+ false, false, false, true, false, EditActionUnspecified));
+ return true;
+}
+
+static bool executeInsertNode(Frame* frame, PassRefPtr<Node> content)
+{
+ RefPtr<DocumentFragment> fragment = new DocumentFragment(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)
+{
+ Selection selection = frame->selectionController()->selection();
+ selection.expandUsingGranularity(granularity);
+ RefPtr<Range> newRange = selection.toRange();
+ if (!newRange)
+ return false;
+ ExceptionCode ec = 0;
+ if (newRange->collapsed(ec))
+ return false;
+ RefPtr<Range> oldRange = frame->selectionController()->selection().toRange();
+ EAffinity affinity = frame->selectionController()->affinity();
+ if (!frame->editor()->client()->shouldChangeSelectedRange(oldRange.get(), newRange.get(), affinity, false))
+ return false;
+ frame->selectionController()->setSelectedRange(newRange.get(), affinity, true);
+ return true;
+}
+
+static TriState stateStyle(Frame* frame, int propertyID, const char* desiredValue)
+{
+ RefPtr<CSSMutableStyleDeclaration> style = new CSSMutableStyleDeclaration;
+ style->setProperty(propertyID, desiredValue);
+ return frame->editor()->selectionHasStyle(style.get());
+}
+
+static String valueStyle(Frame* frame, int propertyID)
+{
+ return frame->selectionStartStylePropertyValue(propertyID);
+}
+
+static int verticalScrollDistance(Frame* frame)
+{
+ Node* focusedNode = frame->document()->focusedNode();
+ if (!focusedNode)
+ return 0;
+ RenderObject* renderer = focusedNode->renderer();
+ if (!renderer)
+ return 0;
+ RenderStyle* style = renderer->style();
+ if (!style)
+ return 0;
+ if (!(style->overflowY() == OSCROLL || style->overflowY() == OAUTO || renderer->isTextArea()))
+ return 0;
+ int height = renderer->clientHeight();
+ return max((height + 1) / 2, height - PAGE_KEEP);
+}
+
+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 new Range(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, CSS_PROP_BACKGROUND_COLOR, 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(new CreateLinkCommand(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->selectionGranularity() == WordGranularity);
+ return true;
+ }
+ ASSERT_NOT_REACHED();
+ return false;
+}
+
+static bool executeDeleteBackward(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ frame->editor()->deleteWithDirection(SelectionController::BACKWARD, 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(SelectionController::BACKWARD, CharacterGranularity, false, true);
+ return true;
+}
+
+static bool executeDeleteForward(Frame* frame, Event*, EditorCommandSource source, const String&)
+{
+ frame->editor()->deleteWithDirection(SelectionController::FORWARD, CharacterGranularity, false, true);
+ return true;
+}
+
+static bool executeDeleteToBeginningOfLine(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ frame->editor()->deleteWithDirection(SelectionController::BACKWARD, LineBoundary, true, false);
+ return true;
+}
+
+static bool executeDeleteToBeginningOfParagraph(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ frame->editor()->deleteWithDirection(SelectionController::BACKWARD, 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(SelectionController::FORWARD, 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(SelectionController::FORWARD, ParagraphBoundary, true, false);
+ return true;
+}
+
+static bool executeDeleteToMark(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ RefPtr<Range> mark = frame->mark().toRange();
+ if (mark) {
+ SelectionController* selectionController = frame->selectionController();
+ bool selected = selectionController->setSelectedRange(unionDOMRanges(mark.get(), frame->editor()->selectedRange().get()).get(), DOWNSTREAM, true);
+ ASSERT(selected);
+ if (!selected)
+ return false;
+ }
+ frame->editor()->performDelete();
+ frame->setMark(frame->selectionController()->selection());
+ return true;
+}
+
+static bool executeDeleteWordBackward(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ frame->editor()->deleteWithDirection(SelectionController::BACKWARD, WordGranularity, true, false);
+ return true;
+}
+
+static bool executeDeleteWordForward(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ frame->editor()->deleteWithDirection(SelectionController::FORWARD, WordGranularity, true, false);
+ return true;
+}
+
+static bool executeFindString(Frame* frame, Event*, EditorCommandSource, const String& value)
+{
+ return frame->findString(value, true, false, true, false);
+}
+
+static bool executeFontName(Frame* frame, Event*, EditorCommandSource source, const String& value)
+{
+ return executeApplyStyle(frame, source, EditActionSetFont, CSS_PROP_FONT_FAMILY, 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, CSS_PROP_FONT_SIZE, size);
+}
+
+static bool executeFontSizeDelta(Frame* frame, Event*, EditorCommandSource source, const String& value)
+{
+ return executeApplyStyle(frame, source, EditActionChangeAttributes, CSS_PROP__WEBKIT_FONT_SIZE_DELTA, value);
+}
+
+static bool executeForeColor(Frame* frame, Event*, EditorCommandSource source, const String& value)
+{
+ return executeApplyStyle(frame, source, EditActionSetColor, CSS_PROP_COLOR, 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);
+ if (!validBlockTag(tagName))
+ return false;
+ applyCommand(new FormatBlockCommand(frame->document(), tagName));
+ return true;
+}
+
+static bool executeForwardDelete(Frame* frame, Event*, EditorCommandSource source, const String&)
+{
+ switch (source) {
+ case CommandFromMenuOrKeyBinding:
+ frame->editor()->deleteWithDirection(SelectionController::FORWARD, 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 executeIndent(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ applyCommand(new IndentOutdentCommand(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<HTMLElement> hr = new HTMLElement(hrTag, frame->document());
+ if (!value.isEmpty())
+ hr->setId(value);
+ return executeInsertNode(frame, hr.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 = new HTMLImageElement(imgTag, 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& value)
+{
+ applyCommand(new InsertListCommand(frame->document(), InsertListCommand::OrderedList, value));
+ 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& value)
+{
+ applyCommand(new InsertListCommand(frame->document(), InsertListCommand::UnorderedList, value));
+ return true;
+}
+
+static bool executeJustifyCenter(Frame* frame, Event*, EditorCommandSource source, const String&)
+{
+ return executeApplyParagraphStyle(frame, source, EditActionCenter, CSS_PROP_TEXT_ALIGN, "center");
+}
+
+static bool executeJustifyFull(Frame* frame, Event*, EditorCommandSource source, const String&)
+{
+ return executeApplyParagraphStyle(frame, source, EditActionJustify, CSS_PROP_TEXT_ALIGN, "justify");
+}
+
+static bool executeJustifyLeft(Frame* frame, Event*, EditorCommandSource source, const String&)
+{
+ return executeApplyParagraphStyle(frame, source, EditActionAlignLeft, CSS_PROP_TEXT_ALIGN, "left");
+}
+
+static bool executeJustifyRight(Frame* frame, Event*, EditorCommandSource source, const String&)
+{
+ return executeApplyParagraphStyle(frame, source, EditActionAlignRight, CSS_PROP_TEXT_ALIGN, "right");
+}
+
+static bool executeMoveBackward(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ frame->selectionController()->modify(SelectionController::MOVE, SelectionController::BACKWARD, CharacterGranularity, true);
+ return true;
+}
+
+static bool executeMoveBackwardAndModifySelection(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ frame->selectionController()->modify(SelectionController::EXTEND, SelectionController::BACKWARD, CharacterGranularity, true);
+ return true;
+}
+
+static bool executeMoveDown(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ frame->selectionController()->modify(SelectionController::MOVE, SelectionController::FORWARD, LineGranularity, true);
+ return true;
+}
+
+static bool executeMoveDownAndModifySelection(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ frame->selectionController()->modify(SelectionController::EXTEND, SelectionController::FORWARD, LineGranularity, true);
+ return true;
+}
+
+static bool executeMoveForward(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ frame->selectionController()->modify(SelectionController::MOVE, SelectionController::FORWARD, CharacterGranularity, true);
+ return true;
+}
+
+static bool executeMoveForwardAndModifySelection(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ frame->selectionController()->modify(SelectionController::EXTEND, SelectionController::FORWARD, CharacterGranularity, true);
+ return true;
+}
+
+static bool executeMoveLeft(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ frame->selectionController()->modify(SelectionController::MOVE, SelectionController::LEFT, CharacterGranularity, true);
+ return true;
+}
+
+static bool executeMoveLeftAndModifySelection(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ frame->selectionController()->modify(SelectionController::EXTEND, SelectionController::LEFT, CharacterGranularity, true);
+ return true;
+}
+
+static bool executeMovePageDown(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ int distance = verticalScrollDistance(frame);
+ if (!distance)
+ return false;
+ return frame->selectionController()->modify(SelectionController::MOVE, distance, true);
+}
+
+static bool executeMovePageDownAndModifySelection(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ int distance = verticalScrollDistance(frame);
+ if (!distance)
+ return false;
+ return frame->selectionController()->modify(SelectionController::EXTEND, distance, true);
+}
+
+static bool executeMovePageUp(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ int distance = verticalScrollDistance(frame);
+ if (!distance)
+ return false;
+ return frame->selectionController()->modify(SelectionController::MOVE, -distance, true);
+}
+
+static bool executeMovePageUpAndModifySelection(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ int distance = verticalScrollDistance(frame);
+ if (!distance)
+ return false;
+ return frame->selectionController()->modify(SelectionController::EXTEND, -distance, true);
+}
+
+static bool executeMoveRight(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ frame->selectionController()->modify(SelectionController::MOVE, SelectionController::RIGHT, CharacterGranularity, true);
+ return true;
+}
+
+static bool executeMoveRightAndModifySelection(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ frame->selectionController()->modify(SelectionController::EXTEND, SelectionController::RIGHT, CharacterGranularity, true);
+ return true;
+}
+
+static bool executeMoveToBeginningOfDocument(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ frame->selectionController()->modify(SelectionController::MOVE, SelectionController::BACKWARD, DocumentBoundary, true);
+ return true;
+}
+
+static bool executeMoveToBeginningOfDocumentAndModifySelection(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ frame->selectionController()->modify(SelectionController::EXTEND, SelectionController::BACKWARD, DocumentBoundary, true);
+ return true;
+}
+
+static bool executeMoveToBeginningOfLine(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ frame->selectionController()->modify(SelectionController::MOVE, SelectionController::BACKWARD, LineBoundary, true);
+ return true;
+}
+
+static bool executeMoveToBeginningOfLineAndModifySelection(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ frame->selectionController()->modify(SelectionController::EXTEND, SelectionController::BACKWARD, LineBoundary, true);
+ return true;
+}
+
+static bool executeMoveToBeginningOfParagraph(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ frame->selectionController()->modify(SelectionController::MOVE, SelectionController::BACKWARD, ParagraphBoundary, true);
+ return true;
+}
+
+static bool executeMoveToBeginningOfParagraphAndModifySelection(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ frame->selectionController()->modify(SelectionController::EXTEND, SelectionController::BACKWARD, ParagraphBoundary, true);
+ return true;
+}
+
+static bool executeMoveToBeginningOfSentence(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ frame->selectionController()->modify(SelectionController::MOVE, SelectionController::BACKWARD, SentenceBoundary, true);
+ return true;
+}
+
+static bool executeMoveToBeginningOfSentenceAndModifySelection(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ frame->selectionController()->modify(SelectionController::EXTEND, SelectionController::BACKWARD, SentenceBoundary, true);
+ return true;
+}
+
+static bool executeMoveToEndOfDocument(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ frame->selectionController()->modify(SelectionController::MOVE, SelectionController::FORWARD, DocumentBoundary, true);
+ return true;
+}
+
+static bool executeMoveToEndOfDocumentAndModifySelection(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ frame->selectionController()->modify(SelectionController::EXTEND, SelectionController::FORWARD, DocumentBoundary, true);
+ return true;
+}
+
+static bool executeMoveToEndOfSentence(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ frame->selectionController()->modify(SelectionController::MOVE, SelectionController::FORWARD, SentenceBoundary, true);
+ return true;
+}
+
+static bool executeMoveToEndOfSentenceAndModifySelection(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ frame->selectionController()->modify(SelectionController::EXTEND, SelectionController::FORWARD, SentenceBoundary, true);
+ return true;
+}
+
+static bool executeMoveToEndOfLine(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ frame->selectionController()->modify(SelectionController::MOVE, SelectionController::FORWARD, LineBoundary, true);
+ return true;
+}
+
+static bool executeMoveToEndOfLineAndModifySelection(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ frame->selectionController()->modify(SelectionController::EXTEND, SelectionController::FORWARD, LineBoundary, true);
+ return true;
+}
+
+static bool executeMoveToEndOfParagraph(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ frame->selectionController()->modify(SelectionController::MOVE, SelectionController::FORWARD, ParagraphBoundary, true);
+ return true;
+}
+
+static bool executeMoveToEndOfParagraphAndModifySelection(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ frame->selectionController()->modify(SelectionController::EXTEND, SelectionController::FORWARD, ParagraphBoundary, true);
+ return true;
+}
+
+static bool executeMoveParagraphBackwardAndModifySelection(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ frame->selectionController()->modify(SelectionController::EXTEND, SelectionController::BACKWARD, ParagraphGranularity, true);
+ return true;
+}
+
+static bool executeMoveParagraphForwardAndModifySelection(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ frame->selectionController()->modify(SelectionController::EXTEND, SelectionController::FORWARD, ParagraphGranularity, true);
+ return true;
+}
+
+static bool executeMoveUp(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ frame->selectionController()->modify(SelectionController::MOVE, SelectionController::BACKWARD, LineGranularity, true);
+ return true;
+}
+
+static bool executeMoveUpAndModifySelection(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ frame->selectionController()->modify(SelectionController::EXTEND, SelectionController::BACKWARD, LineGranularity, true);
+ return true;
+}
+
+static bool executeMoveWordBackward(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ frame->selectionController()->modify(SelectionController::MOVE, SelectionController::BACKWARD, WordGranularity, true);
+ return true;
+}
+
+static bool executeMoveWordBackwardAndModifySelection(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ frame->selectionController()->modify(SelectionController::EXTEND, SelectionController::BACKWARD, WordGranularity, true);
+ return true;
+}
+
+static bool executeMoveWordForward(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ frame->selectionController()->modify(SelectionController::MOVE, SelectionController::FORWARD, WordGranularity, true);
+ return true;
+}
+
+static bool executeMoveWordForwardAndModifySelection(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ frame->selectionController()->modify(SelectionController::EXTEND, SelectionController::FORWARD, WordGranularity, true);
+ return true;
+}
+
+static bool executeMoveWordLeft(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ frame->selectionController()->modify(SelectionController::MOVE, SelectionController::LEFT, WordGranularity, true);
+ return true;
+}
+
+static bool executeMoveWordLeftAndModifySelection(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ frame->selectionController()->modify(SelectionController::EXTEND, SelectionController::LEFT, WordGranularity, true);
+ return true;
+}
+
+static bool executeMoveWordRight(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ frame->selectionController()->modify(SelectionController::MOVE, SelectionController::RIGHT, WordGranularity, true);
+ return true;
+}
+
+static bool executeMoveWordRightAndModifySelection(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ frame->selectionController()->modify(SelectionController::EXTEND, SelectionController::RIGHT, WordGranularity, true);
+ return true;
+}
+
+static bool executeOutdent(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ applyCommand(new IndentOutdentCommand(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 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->selectionController()->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->mark().toRange();
+ RefPtr<Range> selection = frame->editor()->selectedRange();
+ if (!mark || !selection) {
+ systemBeep();
+ return false;
+ }
+ frame->selectionController()->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->setMark(frame->selectionController()->selection());
+ return true;
+}
+
+static bool executeStrikethrough(Frame* frame, Event*, EditorCommandSource source, const String&)
+{
+ return executeToggleStyle(frame, source, EditActionChangeAttributes, CSS_PROP__WEBKIT_TEXT_DECORATIONS_IN_EFFECT, "none", "line-through");
+}
+
+static bool executeSubscript(Frame* frame, Event*, EditorCommandSource source, const String&)
+{
+ return executeApplyStyle(frame, source, EditActionSubscript, CSS_PROP_VERTICAL_ALIGN, "sub");
+}
+
+static bool executeSuperscript(Frame* frame, Event*, EditorCommandSource source, const String&)
+{
+ return executeApplyStyle(frame, source, EditActionSuperscript, CSS_PROP_VERTICAL_ALIGN, "super");
+}
+
+static bool executeSwapWithMark(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ const Selection& mark = frame->mark();
+ const Selection& selection = frame->selectionController()->selection();
+ if (mark.isNone() || selection.isNone()) {
+ systemBeep();
+ return false;
+ }
+ frame->selectionController()->setSelection(mark);
+ frame->setMark(selection);
+ return true;
+}
+
+static bool executeToggleBold(Frame* frame, Event*, EditorCommandSource source, const String&)
+{
+ return executeToggleStyle(frame, source, EditActionChangeAttributes, CSS_PROP_FONT_WEIGHT, "normal", "bold");
+}
+
+static bool executeToggleItalic(Frame* frame, Event*, EditorCommandSource source, const String&)
+{
+ return executeToggleStyle(frame, source, EditActionChangeAttributes, CSS_PROP_FONT_STYLE, "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&)
+{
+ // FIXME: This currently clears overline, line-through, and blink as an unwanted side effect.
+ return executeToggleStyle(frame, source, EditActionUnderline, CSS_PROP__WEBKIT_TEXT_DECORATIONS_IN_EFFECT, "none", "underline");
+}
+
+static bool executeUndo(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ frame->editor()->undo();
+ return true;
+}
+
+static bool executeUnlink(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ applyCommand(new UnlinkCommand(frame->document()));
+ return true;
+}
+
+static bool executeUnscript(Frame* frame, Event*, EditorCommandSource source, const String&)
+{
+ return executeApplyStyle(frame, source, EditActionUnscript, CSS_PROP_VERTICAL_ALIGN, "baseline");
+}
+
+static bool executeUnselect(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ frame->selectionController()->clear();
+ return true;
+}
+
+static bool executeYank(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ frame->editor()->insertTextWithoutSendingTextEvent(frame->editor()->yankFromKillRing(), false);
+ frame->editor()->setKillRingToYankedState();
+ return true;
+}
+
+static bool executeYankAndSelect(Frame* frame, Event*, EditorCommandSource, const String&)
+{
+ frame->editor()->insertTextWithoutSendingTextEvent(frame->editor()->yankFromKillRing(), true);
+ frame->editor()->setKillRingToYankedState();
+ return true;
+}
+
+// Supported functions
+
+static bool supported(Frame*, EditorCommandSource)
+{
+ return true;
+}
+
+static bool supportedFromMenuOrKeyBinding(Frame*, EditorCommandSource source)
+{
+ return source == CommandFromMenuOrKeyBinding;
+}
+
+static bool supportedPaste(Frame* frame, EditorCommandSource source)
+{
+ switch (source) {
+ case CommandFromMenuOrKeyBinding:
+ return true;
+ case CommandFromDOM:
+ case CommandFromDOMWithUserInterface: {
+ Settings* settings = frame ? frame->settings() : 0;
+ return settings && settings->isDOMPasteAllowed();
+ }
+ }
+ ASSERT_NOT_REACHED();
+ return false;
+}
+
+// Enabled functions
+
+static bool enabled(Frame*, Event*, EditorCommandSource)
+{
+ return true;
+}
+
+static bool enabledAnySelection(Frame* frame, Event*, EditorCommandSource)
+{
+ return frame->selectionController()->isCaretOrRange();
+}
+
+static bool enabledAnySelectionAndMark(Frame* frame, Event*, EditorCommandSource)
+{
+ return frame->selectionController()->isCaretOrRange() && frame->mark().isCaretOrRange();
+}
+
+static bool enableCaretInEditableText(Frame* frame, Event* event, EditorCommandSource)
+{
+ const Selection& 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 enabledInRichlyEditableText(Frame* frame, Event*, EditorCommandSource)
+{
+ return frame->selectionController()->isCaretOrRange() && frame->selectionController()->isContentRichlyEditable();
+}
+
+static bool enabledPaste(Frame* frame, Event*, EditorCommandSource)
+{
+ return frame->editor()->canPaste();
+}
+
+static bool enabledRangeInEditableText(Frame* frame, Event*, EditorCommandSource)
+{
+ return frame->selectionController()->isRange() && frame->selectionController()->isContentEditable();
+}
+
+static bool enabledRangeInRichlyEditableText(Frame* frame, Event*, EditorCommandSource)
+{
+ return frame->selectionController()->isRange() && frame->selectionController()->isContentRichlyEditable();
+}
+
+static bool enabledRedo(Frame* frame, Event*, EditorCommandSource)
+{
+ return frame->editor()->canRedo();
+}
+
+static bool enabledUndo(Frame* frame, Event*, EditorCommandSource)
+{
+ return frame->editor()->canUndo();
+}
+
+// State functions
+
+static TriState stateNone(Frame*, Event*)
+{
+ return FalseTriState;
+}
+
+static TriState stateBold(Frame* frame, Event*)
+{
+ return stateStyle(frame, CSS_PROP_FONT_WEIGHT, "bold");
+}
+
+static TriState stateItalic(Frame* frame, Event*)
+{
+ return stateStyle(frame, CSS_PROP_FONT_STYLE, "italic");
+}
+
+static TriState stateOrderedList(Frame* frame, Event*)
+{
+ return frame->editor()->selectionOrderedListState();
+}
+
+static TriState stateStrikethrough(Frame* frame, Event*)
+{
+ return stateStyle(frame, CSS_PROP_TEXT_DECORATION, "line-through");
+}
+
+static TriState stateSubscript(Frame* frame, Event*)
+{
+ return stateStyle(frame, CSS_PROP_VERTICAL_ALIGN, "sub");
+}
+
+static TriState stateSuperscript(Frame* frame, Event*)
+{
+ return stateStyle(frame, CSS_PROP_VERTICAL_ALIGN, "super");
+}
+
+static TriState stateUnderline(Frame* frame, Event*)
+{
+ return stateStyle(frame, CSS_PROP_TEXT_DECORATION, "underline");
+}
+
+static TriState stateUnorderedList(Frame* frame, Event*)
+{
+ return frame->editor()->selectionUnorderedListState();
+}
+
+// Value functions
+
+static String valueNull(Frame*, Event*)
+{
+ return String();
+}
+
+String valueBackColor(Frame* frame, Event*)
+{
+ return valueStyle(frame, CSS_PROP_BACKGROUND_COLOR);
+}
+
+String valueFontName(Frame* frame, Event*)
+{
+ return valueStyle(frame, CSS_PROP_FONT_FAMILY);
+}
+
+String valueFontSize(Frame* frame, Event*)
+{
+ return valueStyle(frame, CSS_PROP_FONT_SIZE);
+}
+
+String valueFontSizeDelta(Frame* frame, Event*)
+{
+ return valueStyle(frame, CSS_PROP__WEBKIT_FONT_SIZE_DELTA);
+}
+
+String valueForeColor(Frame* frame, Event*)
+{
+ return valueStyle(frame, CSS_PROP_COLOR);
+}
+
+// Map of functions
+
+static const CommandMap& createCommandMap()
+{
+ struct CommandEntry { const char* name; EditorInternalCommand command; };
+
+ static const CommandEntry commands[] = {
+ { "AlignCenter", { executeJustifyCenter, supportedFromMenuOrKeyBinding, enabledInRichlyEditableText, stateNone, valueNull, notTextInsertion } },
+ { "AlignJustified", { executeJustifyFull, supportedFromMenuOrKeyBinding, enabledInRichlyEditableText, stateNone, valueNull, notTextInsertion } },
+ { "AlignLeft", { executeJustifyLeft, supportedFromMenuOrKeyBinding, enabledInRichlyEditableText, stateNone, valueNull, notTextInsertion } },
+ { "AlignRight", { executeJustifyRight, supportedFromMenuOrKeyBinding, enabledInRichlyEditableText, stateNone, valueNull, notTextInsertion } },
+ { "BackColor", { executeBackColor, supported, enabledRangeInRichlyEditableText, stateNone, valueBackColor, notTextInsertion } },
+ { "BackwardDelete", { executeDeleteBackward, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } }, // FIXME: remove BackwardDelete when Safari for Windows stops using it.
+ { "Bold", { executeToggleBold, supported, enabledInRichlyEditableText, stateBold, valueNull, notTextInsertion } },
+ { "Copy", { executeCopy, supported, enabledCopy, stateNone, valueNull, notTextInsertion } },
+ { "CreateLink", { executeCreateLink, supported, enabledInRichlyEditableText, stateNone, valueNull, notTextInsertion } },
+ { "Cut", { executeCut, supported, enabledCut, stateNone, valueNull, notTextInsertion } },
+ { "Delete", { executeDelete, supported, enabledDelete, stateNone, valueNull, notTextInsertion } },
+ { "DeleteBackward", { executeDeleteBackward, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "DeleteBackwardByDecomposingPreviousCharacter", { executeDeleteBackwardByDecomposingPreviousCharacter, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "DeleteForward", { executeDeleteForward, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "DeleteToBeginningOfLine", { executeDeleteToBeginningOfLine, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "DeleteToBeginningOfParagraph", { executeDeleteToBeginningOfParagraph, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "DeleteToEndOfLine", { executeDeleteToEndOfLine, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "DeleteToEndOfParagraph", { executeDeleteToEndOfParagraph, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "DeleteToMark", { executeDeleteToMark, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "DeleteWordBackward", { executeDeleteWordBackward, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "DeleteWordForward", { executeDeleteWordForward, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "FindString", { executeFindString, supported, enabled, stateNone, valueNull, notTextInsertion } },
+ { "FontName", { executeFontName, supported, enabledInEditableText, stateNone, valueFontName, notTextInsertion } },
+ { "FontSize", { executeFontSize, supported, enabledInEditableText, stateNone, valueFontSize, notTextInsertion } },
+ { "FontSizeDelta", { executeFontSizeDelta, supported, enabledInEditableText, stateNone, valueFontSizeDelta, notTextInsertion } },
+ { "ForeColor", { executeForeColor, supported, enabledInEditableText, stateNone, valueForeColor, notTextInsertion } },
+ { "FormatBlock", { executeFormatBlock, supported, enabledInRichlyEditableText, stateNone, valueNull, notTextInsertion } },
+ { "ForwardDelete", { executeForwardDelete, supported, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "HiliteColor", { executeBackColor, supported, enabledInRichlyEditableText, stateNone, valueNull, notTextInsertion } },
+ { "Indent", { executeIndent, supported, enabledInRichlyEditableText, stateNone, valueNull, notTextInsertion } },
+ { "InsertBacktab", { executeInsertBacktab, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, isTextInsertion } },
+ { "InsertHorizontalRule", { executeInsertHorizontalRule, supported, enabledInRichlyEditableText, stateNone, valueNull, notTextInsertion } },
+ { "InsertHTML", { executeInsertHTML, supported, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "InsertImage", { executeInsertImage, supported, enabledInRichlyEditableText, stateNone, valueNull, notTextInsertion } },
+ { "InsertLineBreak", { executeInsertLineBreak, supported, enabledInEditableText, stateNone, valueNull, isTextInsertion } },
+ { "InsertNewline", { executeInsertNewline, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, isTextInsertion } },
+ { "InsertNewlineInQuotedContent", { executeInsertNewlineInQuotedContent, supported, enabledInRichlyEditableText, stateNone, valueNull, notTextInsertion } },
+ { "InsertOrderedList", { executeInsertOrderedList, supported, enabledInRichlyEditableText, stateOrderedList, valueNull, notTextInsertion } },
+ { "InsertParagraph", { executeInsertParagraph, supported, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "InsertTab", { executeInsertTab, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, isTextInsertion } },
+ { "InsertText", { executeInsertText, supported, enabledInEditableText, stateNone, valueNull, isTextInsertion } },
+ { "InsertUnorderedList", { executeInsertUnorderedList, supported, enabledInRichlyEditableText, stateUnorderedList, valueNull, notTextInsertion } },
+ { "Italic", { executeToggleItalic, supported, enabledInRichlyEditableText, stateItalic, valueNull, notTextInsertion } },
+ { "JustifyCenter", { executeJustifyCenter, supported, enabledInRichlyEditableText, stateNone, valueNull, notTextInsertion } },
+ { "JustifyFull", { executeJustifyFull, supported, enabledInRichlyEditableText, stateNone, valueNull, notTextInsertion } },
+ { "JustifyLeft", { executeJustifyLeft, supported, enabledInRichlyEditableText, stateNone, valueNull, notTextInsertion } },
+ { "JustifyNone", { executeJustifyLeft, supported, enabledInRichlyEditableText, stateNone, valueNull, notTextInsertion } },
+ { "JustifyRight", { executeJustifyRight, supported, enabledInRichlyEditableText, stateNone, valueNull, notTextInsertion } },
+ { "MoveBackward", { executeMoveBackward, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "MoveBackwardAndModifySelection", { executeMoveBackwardAndModifySelection, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "MoveDown", { executeMoveDown, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "MoveDownAndModifySelection", { executeMoveDownAndModifySelection, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "MoveForward", { executeMoveForward, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "MoveForwardAndModifySelection", { executeMoveForwardAndModifySelection, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "MoveLeft", { executeMoveLeft, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "MoveLeftAndModifySelection", { executeMoveLeftAndModifySelection, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "MovePageDown", { executeMovePageDown, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "MovePageDownAndModifySelection", { executeMovePageDownAndModifySelection, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "MovePageUp", { executeMovePageUp, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "MovePageUpAndModifySelection", { executeMovePageUpAndModifySelection, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "MoveParagraphBackwardAndModifySelection", { executeMoveParagraphBackwardAndModifySelection, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "MoveParagraphForwardAndModifySelection", { executeMoveParagraphForwardAndModifySelection, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "MoveRight", { executeMoveRight, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "MoveRightAndModifySelection", { executeMoveRightAndModifySelection, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "MoveToBeginningOfDocument", { executeMoveToBeginningOfDocument, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "MoveToBeginningOfDocumentAndModifySelection", { executeMoveToBeginningOfDocumentAndModifySelection, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "MoveToBeginningOfLine", { executeMoveToBeginningOfLine, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "MoveToBeginningOfLineAndModifySelection", { executeMoveToBeginningOfLineAndModifySelection, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "MoveToBeginningOfParagraph", { executeMoveToBeginningOfParagraph, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "MoveToBeginningOfParagraphAndModifySelection", { executeMoveToBeginningOfParagraphAndModifySelection, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "MoveToBeginningOfSentence", { executeMoveToBeginningOfSentence, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "MoveToBeginningOfSentenceAndModifySelection", { executeMoveToBeginningOfSentenceAndModifySelection, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "MoveToEndOfDocument", { executeMoveToEndOfDocument, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "MoveToEndOfDocumentAndModifySelection", { executeMoveToEndOfDocumentAndModifySelection, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "MoveToEndOfLine", { executeMoveToEndOfLine, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "MoveToEndOfLineAndModifySelection", { executeMoveToEndOfLineAndModifySelection, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "MoveToEndOfParagraph", { executeMoveToEndOfParagraph, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "MoveToEndOfParagraphAndModifySelection", { executeMoveToEndOfParagraphAndModifySelection, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "MoveToEndOfSentence", { executeMoveToEndOfSentence, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "MoveToEndOfSentenceAndModifySelection", { executeMoveToEndOfSentenceAndModifySelection, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "MoveUp", { executeMoveUp, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "MoveUpAndModifySelection", { executeMoveUpAndModifySelection, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "MoveWordBackward", { executeMoveWordBackward, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "MoveWordBackwardAndModifySelection", { executeMoveWordBackwardAndModifySelection, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "MoveWordForward", { executeMoveWordForward, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "MoveWordForwardAndModifySelection", { executeMoveWordForwardAndModifySelection, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "MoveWordLeft", { executeMoveWordLeft, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "MoveWordLeftAndModifySelection", { executeMoveWordLeftAndModifySelection, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "MoveWordRight", { executeMoveWordRight, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "MoveWordRightAndModifySelection", { executeMoveWordRightAndModifySelection, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "Outdent", { executeOutdent, supported, enabledInRichlyEditableText, stateNone, valueNull, notTextInsertion } },
+ { "Paste", { executePaste, supportedPaste, enabledPaste, stateNone, valueNull, notTextInsertion } },
+ { "PasteAndMatchStyle", { executePasteAndMatchStyle, supportedPaste, enabledPaste, stateNone, valueNull, notTextInsertion } },
+ { "Print", { executePrint, supported, enabled, stateNone, valueNull, notTextInsertion } },
+ { "Redo", { executeRedo, supported, enabledRedo, stateNone, valueNull, notTextInsertion } },
+ { "RemoveFormat", { executeRemoveFormat, supported, enabledRangeInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "SelectAll", { executeSelectAll, supported, enabled, stateNone, valueNull, notTextInsertion } },
+ { "SelectLine", { executeSelectLine, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "SelectParagraph", { executeSelectParagraph, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "SelectSentence", { executeSelectSentence, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "SelectToMark", { executeSelectToMark, supportedFromMenuOrKeyBinding, enabledAnySelectionAndMark, stateNone, valueNull, notTextInsertion } },
+ { "SelectWord", { executeSelectWord, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "SetMark", { executeSetMark, supportedFromMenuOrKeyBinding, enabledAnySelection, stateNone, valueNull, notTextInsertion } },
+ { "Strikethrough", { executeStrikethrough, supported, enabledInRichlyEditableText, stateStrikethrough, valueNull, notTextInsertion } },
+ { "Subscript", { executeSubscript, supported, enabledInRichlyEditableText, stateSubscript, valueNull, notTextInsertion } },
+ { "Superscript", { executeSuperscript, supported, enabledInRichlyEditableText, stateSuperscript, valueNull, notTextInsertion } },
+ { "SwapWithMark", { executeSwapWithMark, supportedFromMenuOrKeyBinding, enabledAnySelectionAndMark, stateNone, valueNull, notTextInsertion } },
+ { "ToggleBold", { executeToggleBold, supportedFromMenuOrKeyBinding, enabledInRichlyEditableText, stateBold, valueNull, notTextInsertion } },
+ { "ToggleItalic", { executeToggleItalic, supportedFromMenuOrKeyBinding, enabledInRichlyEditableText, stateItalic, valueNull, notTextInsertion } },
+ { "ToggleUnderline", { executeUnderline, supportedFromMenuOrKeyBinding, enabledInRichlyEditableText, stateUnderline, valueNull, notTextInsertion } },
+ { "Transpose", { executeTranspose, supported, enableCaretInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "Underline", { executeUnderline, supported, enabledInRichlyEditableText, stateUnderline, valueNull, notTextInsertion } },
+ { "Undo", { executeUndo, supported, enabledUndo, stateNone, valueNull, notTextInsertion } },
+ { "Unlink", { executeUnlink, supported, enabledRangeInRichlyEditableText, stateNone, valueNull, notTextInsertion } },
+ { "Unscript", { executeUnscript, supportedFromMenuOrKeyBinding, enabledInRichlyEditableText, stateNone, valueNull, notTextInsertion } },
+ { "Unselect", { executeUnselect, supported, enabledAnySelection, stateNone, valueNull, notTextInsertion } },
+ { "Yank", { executeYank, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ { "YankAndSelect", { executeYankAndSelect, supportedFromMenuOrKeyBinding, enabledInEditableText, stateNone, valueNull, notTextInsertion } },
+ };
+
+ // 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;
+
+ const unsigned numCommands = sizeof(commands) / sizeof(commands[0]);
+ for (unsigned i = 0; i < numCommands; i++) {
+ ASSERT(!commandMap.get(commands[i].name));
+ commandMap.set(commands[i].name, &commands[i].command);
+ }
+
+ return commandMap;
+}
+
+Editor::Command Editor::command(const String& commandName)
+{
+ return command(commandName, CommandFromMenuOrKeyBinding);
+}
+
+Editor::Command Editor::command(const String& commandName, EditorCommandSource source)
+{
+ if (commandName.isEmpty())
+ return Command();
+
+ static const CommandMap& commandMap = createCommandMap();
+ const EditorInternalCommand* internalCommand = commandMap.get(commandName);
+ return internalCommand ? Command(m_frame, internalCommand, source) : Command();
+}
+
+Editor::Command::Command()
+ : m_command(0)
+ , m_source()
+{
+}
+
+Editor::Command::Command(PassRefPtr<Frame> frame, const EditorInternalCommand* command, EditorCommandSource source)
+ : m_frame(frame)
+ , m_command(command)
+ , m_source(source)
+{
+ ASSERT(m_frame);
+ ASSERT(m_command);
+}
+
+bool Editor::Command::execute(const String& parameter, Event* triggeringEvent) const
+{
+ if (!isEnabled(triggeringEvent))
+ 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
+{
+ return m_command && m_command->isSupported(m_frame.get(), m_source);
+}
+
+bool Editor::Command::isEnabled(Event* triggeringEvent) const
+{
+ if (!isSupported() || !m_frame || !m_frame->document())
+ return false;
+ return m_command->isEnabled(m_frame.get(), triggeringEvent, m_source);
+}
+
+TriState Editor::Command::state(Event* triggeringEvent) const
+{
+ if (!isSupported() || !m_frame || !m_frame->document())
+ return FalseTriState;
+ return m_command->state(m_frame.get(), triggeringEvent);
+}
+
+String Editor::Command::value(Event* triggeringEvent) const
+{
+ if (!isSupported() || !m_frame || !m_frame->document())
+ return String();
+ return m_command->value(m_frame.get(), triggeringEvent);
+}
+
+bool Editor::Command::isTextInsertion() const
+{
+ return m_command && m_command->isTextInsertion;
+}
+
+} // namespace WebCore
diff --git a/WebCore/editing/EditorDeleteAction.h b/WebCore/editing/EditorDeleteAction.h
new file mode 100644
index 0000000..00bf683
--- /dev/null
+++ b/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/WebCore/editing/EditorInsertAction.h b/WebCore/editing/EditorInsertAction.h
new file mode 100644
index 0000000..5b732dc
--- /dev/null
+++ b/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/WebCore/editing/FormatBlockCommand.cpp b/WebCore/editing/FormatBlockCommand.cpp
new file mode 100644
index 0000000..492ee40
--- /dev/null
+++ b/WebCore/editing/FormatBlockCommand.cpp
@@ -0,0 +1,133 @@
+/*
+ * 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 "visible_units.h"
+
+namespace WebCore {
+
+using namespace HTMLNames;
+
+FormatBlockCommand::FormatBlockCommand(Document* document, const String& tagName)
+ : CompositeEditCommand(document), m_tagName(tagName)
+{
+}
+
+bool FormatBlockCommand::modifyRange()
+{
+ ASSERT(endingSelection().isRange());
+ VisiblePosition visibleStart = endingSelection().visibleStart();
+ VisiblePosition visibleEnd = endingSelection().visibleEnd();
+ VisiblePosition startOfLastParagraph = startOfParagraph(visibleEnd);
+
+ if (startOfParagraph(visibleStart) == startOfLastParagraph)
+ return false;
+
+ setEndingSelection(visibleStart);
+ doApply();
+ visibleStart = endingSelection().visibleStart();
+ VisiblePosition nextParagraph = endOfParagraph(visibleStart).next();
+ while (nextParagraph.isNotNull() && nextParagraph != startOfLastParagraph) {
+ setEndingSelection(nextParagraph);
+ doApply();
+ nextParagraph = endOfParagraph(endingSelection().visibleStart()).next();
+ }
+ setEndingSelection(visibleEnd);
+ doApply();
+ visibleEnd = endingSelection().visibleEnd();
+ setEndingSelection(Selection(visibleStart.deepEquivalent(), visibleEnd.deepEquivalent(), DOWNSTREAM));
+
+ return true;
+}
+
+void FormatBlockCommand::doApply()
+{
+ if (endingSelection().isNone())
+ 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 FormatBlock
+ // 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(Selection(visibleStart, visibleEnd.previous(true)));
+
+ if (endingSelection().isRange() && modifyRange())
+ return;
+
+ String localName, prefix;
+ if (!Document::parseQualifiedName(m_tagName, prefix, localName))
+ return;
+ QualifiedName qTypeOfBlock = QualifiedName(AtomicString(prefix), AtomicString(localName), xhtmlNamespaceURI);
+
+ Node* refNode = enclosingBlockFlowElement(endingSelection().visibleStart());
+ if (refNode->hasTagName(qTypeOfBlock))
+ // We're already in a block with the format we want, so we don't have to do anything
+ return;
+
+ VisiblePosition paragraphStart = startOfParagraph(endingSelection().visibleStart());
+ VisiblePosition paragraphEnd = endOfParagraph(endingSelection().visibleStart());
+ VisiblePosition blockStart = startOfBlock(endingSelection().visibleStart());
+ VisiblePosition blockEnd = endOfBlock(endingSelection().visibleStart());
+ RefPtr<Node> blockNode = createElement(document(), m_tagName);
+ RefPtr<Node> placeholder = createBreakElement(document());
+
+ Node* root = endingSelection().start().node()->rootEditableElement();
+ if (validBlockTag(refNode->nodeName().lower()) &&
+ paragraphStart == blockStart && paragraphEnd == blockEnd &&
+ refNode != root && !root->isDescendantOf(refNode))
+ // Already in a valid block tag that only contains the current paragraph, so we can swap with the new tag
+ insertNodeBefore(blockNode.get(), refNode);
+ else {
+ // Avoid inserting inside inline elements that surround paragraphStart with upstream().
+ // This is only to avoid creating bloated markup.
+ insertNodeAt(blockNode.get(), paragraphStart.deepEquivalent().upstream());
+ }
+ appendNode(placeholder.get(), blockNode.get());
+
+ VisiblePosition destination(Position(placeholder.get(), 0));
+ if (paragraphStart == paragraphEnd && !lineBreakExistsAtPosition(paragraphStart)) {
+ setEndingSelection(destination);
+ return;
+ }
+ moveParagraph(paragraphStart, paragraphEnd, destination, true, false);
+}
+
+}
diff --git a/WebCore/editing/FormatBlockCommand.h b/WebCore/editing/FormatBlockCommand.h
new file mode 100644
index 0000000..a11d882
--- /dev/null
+++ b/WebCore/editing/FormatBlockCommand.h
@@ -0,0 +1,45 @@
+/*
+ * 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 FormatBlockCommand_h
+#define FormatBlockCommand_h
+
+#include "CompositeEditCommand.h"
+
+namespace WebCore {
+
+class FormatBlockCommand : public CompositeEditCommand {
+public:
+ FormatBlockCommand(Document*, const String& tagName);
+ virtual void doApply();
+ virtual EditAction editingAction() const { return EditActionFormatBlock; }
+private:
+ bool modifyRange();
+ String m_tagName;
+};
+
+} // namespace WebCore
+
+#endif // FormatBlockCommand_h
diff --git a/WebCore/editing/HTMLInterchange.cpp b/WebCore/editing/HTMLInterchange.cpp
new file mode 100644
index 0000000..024ac9f
--- /dev/null
+++ b/WebCore/editing/HTMLInterchange.cpp
@@ -0,0 +1,111 @@
+/*
+ * 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"
+
+namespace WebCore {
+
+namespace {
+
+String convertedSpaceString()
+{
+ static 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/WebCore/editing/HTMLInterchange.h b/WebCore/editing/HTMLInterchange.h
new file mode 100644
index 0000000..3b68efb
--- /dev/null
+++ b/WebCore/editing/HTMLInterchange.h
@@ -0,0 +1,46 @@
+/*
+ * 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
+
+namespace WebCore {
+
+class String;
+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/WebCore/editing/IndentOutdentCommand.cpp b/WebCore/editing/IndentOutdentCommand.cpp
new file mode 100644
index 0000000..0d66467
--- /dev/null
+++ b/WebCore/editing/IndentOutdentCommand.cpp
@@ -0,0 +1,285 @@
+/*
+ * 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 (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 "Element.h"
+#include "IndentOutdentCommand.h"
+#include "InsertListCommand.h"
+#include "Document.h"
+#include "htmlediting.h"
+#include "HTMLElement.h"
+#include "HTMLNames.h"
+#include "InsertLineBreakCommand.h"
+#include "Range.h"
+#include "SplitElementCommand.h"
+#include "TextIterator.h"
+#include "visible_units.h"
+
+namespace WebCore {
+
+using namespace HTMLNames;
+
+static String indentBlockquoteString()
+{
+ static String string = "webkit-indent-blockquote";
+ return string;
+}
+
+static PassRefPtr<Element> createIndentBlockquoteElement(Document* document)
+{
+ RefPtr<Element> indentBlockquoteElement = createElement(document, "blockquote");
+ indentBlockquoteElement->setAttribute(classAttr, indentBlockquoteString());
+ indentBlockquoteElement->setAttribute(styleAttr, "margin: 0 0 0 40px; border: none; padding: 0px;");
+ return indentBlockquoteElement.release();
+}
+
+static bool isIndentBlockquote(const Node* node)
+{
+ if (!node || !node->hasTagName(blockquoteTag) || !node->isElementNode())
+ return false;
+
+ const Element* elem = static_cast<const Element*>(node);
+ return elem->getAttribute(classAttr) == indentBlockquoteString();
+}
+
+static bool isListOrIndentBlockquote(const Node* node)
+{
+ return node && (node->hasTagName(ulTag) || node->hasTagName(olTag) || isIndentBlockquote(node));
+}
+
+IndentOutdentCommand::IndentOutdentCommand(Document* document, EIndentType typeOfAction, int marginInPixels)
+ : CompositeEditCommand(document), m_typeOfAction(typeOfAction), m_marginInPixels(marginInPixels)
+{}
+
+// This function is a workaround for moveParagraph's tendency to strip blockquotes. It updates lastBlockquote to point to the
+// correct level for the current paragraph, and returns a pointer to a placeholder br where the insertion should be performed.
+Node* IndentOutdentCommand::prepareBlockquoteLevelForInsertion(VisiblePosition& currentParagraph, Node** lastBlockquote)
+{
+ int currentBlockquoteLevel = 0;
+ int lastBlockquoteLevel = 0;
+ Node* node = currentParagraph.deepEquivalent().node();
+ while ((node = enclosingNodeOfType(Position(node->parentNode(), 0), &isIndentBlockquote)))
+ currentBlockquoteLevel++;
+ node = *lastBlockquote;
+ while ((node = enclosingNodeOfType(Position(node->parentNode(), 0), &isIndentBlockquote)))
+ lastBlockquoteLevel++;
+ while (currentBlockquoteLevel > lastBlockquoteLevel) {
+ RefPtr<Node> newBlockquote = createIndentBlockquoteElement(document());
+ appendNode(newBlockquote.get(), *lastBlockquote);
+ *lastBlockquote = newBlockquote.get();
+ lastBlockquoteLevel++;
+ }
+ while (currentBlockquoteLevel < lastBlockquoteLevel) {
+ *lastBlockquote = enclosingNodeOfType(Position((*lastBlockquote)->parentNode(), 0), &isIndentBlockquote);
+ lastBlockquoteLevel--;
+ }
+ RefPtr<Node> placeholder = createBreakElement(document());
+ appendNode(placeholder.get(), *lastBlockquote);
+ // Add another br before the placeholder if it collapsed.
+ VisiblePosition visiblePos(Position(placeholder.get(), 0));
+ if (!isStartOfParagraph(visiblePos))
+ insertNodeBefore(createBreakElement(document()).get(), placeholder.get());
+ return placeholder.get();
+}
+
+void IndentOutdentCommand::indentRegion()
+{
+ VisiblePosition startOfSelection = endingSelection().visibleStart();
+ VisiblePosition endOfSelection = endingSelection().visibleEnd();
+ int startIndex = indexForVisiblePosition(startOfSelection);
+ int endIndex = indexForVisiblePosition(endOfSelection);
+
+ ASSERT(!startOfSelection.isNull());
+ ASSERT(!endOfSelection.isNull());
+
+ // Special case empty root editable elements because there's nothing to split
+ // and there's nothing to move.
+ Position start = startOfSelection.deepEquivalent().downstream();
+ if (start.node() == editableRootForPosition(start)) {
+ RefPtr<Node> blockquote = createIndentBlockquoteElement(document());
+ insertNodeAt(blockquote.get(), start);
+ RefPtr<Node> placeholder = createBreakElement(document());
+ appendNode(placeholder.get(), blockquote.get());
+ setEndingSelection(Selection(Position(placeholder.get(), 0), DOWNSTREAM));
+ return;
+ }
+
+ Node* previousListNode = 0;
+ Node* newListNode = 0;
+ Node* newBlockquote = 0;
+ VisiblePosition endOfCurrentParagraph = endOfParagraph(startOfSelection);
+ VisiblePosition endAfterSelection = endOfParagraph(endOfParagraph(endOfSelection).next());
+ while (endOfCurrentParagraph != endAfterSelection) {
+ // Iterate across the selected paragraphs...
+ VisiblePosition endOfNextParagraph = endOfParagraph(endOfCurrentParagraph.next());
+ Node* listNode = enclosingList(endOfCurrentParagraph.deepEquivalent().node());
+ Node* insertionPoint;
+ if (listNode) {
+ RefPtr<Node> placeholder = createBreakElement(document());
+ insertionPoint = placeholder.get();
+ newBlockquote = 0;
+ RefPtr<Node> listItem = createListItemElement(document());
+ if (listNode == previousListNode) {
+ // The previous paragraph was inside the same list, so add this list item to the list we already created
+ appendNode(listItem.get(), newListNode);
+ appendNode(placeholder.get(), listItem.get());
+ } else {
+ // Clone the list element, insert it before the current paragraph, and move the paragraph into it.
+ RefPtr<Node> clonedList = static_cast<Element*>(listNode)->cloneNode(false);
+ insertNodeBefore(clonedList.get(), enclosingListChild(endOfCurrentParagraph.deepEquivalent().node()));
+ appendNode(listItem.get(), clonedList.get());
+ appendNode(placeholder.get(), listItem.get());
+ newListNode = clonedList.get();
+ previousListNode = listNode;
+ }
+ } else if (newBlockquote)
+ // The previous paragraph was put into a new blockquote, so move this paragraph there as well
+ insertionPoint = prepareBlockquoteLevelForInsertion(endOfCurrentParagraph, &newBlockquote);
+ else {
+ // 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.
+ RefPtr<Node> blockquote = createIndentBlockquoteElement(document());
+ Position start = startOfParagraph(endOfCurrentParagraph).deepEquivalent();
+
+ // FIXME: This will break table structure.
+ Node* startOfNewBlock = splitTreeToNode(start.node(), editableRootForPosition(start));
+ insertNodeBefore(blockquote.get(), startOfNewBlock);
+ newBlockquote = blockquote.get();
+ insertionPoint = prepareBlockquoteLevelForInsertion(endOfCurrentParagraph, &newBlockquote);
+ }
+ moveParagraph(startOfParagraph(endOfCurrentParagraph), endOfCurrentParagraph, VisiblePosition(Position(insertionPoint, 0)), true);
+ endOfCurrentParagraph = endOfNextParagraph;
+ }
+
+ RefPtr<Range> startRange = TextIterator::rangeFromLocationAndLength(document()->documentElement(), startIndex, 0, true);
+ RefPtr<Range> endRange = TextIterator::rangeFromLocationAndLength(document()->documentElement(), endIndex, 0, true);
+ if (startRange && endRange)
+ setEndingSelection(Selection(startRange->startPosition(), endRange->startPosition(), DOWNSTREAM));
+}
+
+void IndentOutdentCommand::outdentParagraph()
+{
+ VisiblePosition visibleStartOfParagraph = startOfParagraph(endingSelection().visibleStart());
+ VisiblePosition visibleEndOfParagraph = endOfParagraph(visibleStartOfParagraph);
+
+ Node* enclosingNode = enclosingNodeOfType(visibleStartOfParagraph.deepEquivalent(), &isListOrIndentBlockquote);
+ if (!enclosingNode)
+ return;
+
+ // Use InsertListCommand to remove the selection from the list
+ if (enclosingNode->hasTagName(olTag)) {
+ applyCommandToComposite(new InsertListCommand(document(), InsertListCommand::OrderedList, ""));
+ return;
+ } else if (enclosingNode->hasTagName(ulTag)) {
+ applyCommandToComposite(new InsertListCommand(document(), InsertListCommand::UnorderedList, ""));
+ return;
+ }
+
+ // The selection is inside a blockquote
+ VisiblePosition positionInEnclosingBlock = VisiblePosition(Position(enclosingNode, 0));
+ VisiblePosition startOfEnclosingBlock = startOfBlock(positionInEnclosingBlock);
+ VisiblePosition endOfEnclosingBlock = endOfBlock(positionInEnclosingBlock);
+ if (visibleStartOfParagraph == startOfEnclosingBlock &&
+ visibleEndOfParagraph == endOfEnclosingBlock) {
+ // The blockquote doesn't contain anything outside the paragraph, so it can be totally removed.
+ removeNodePreservingChildren(enclosingNode);
+ updateLayout();
+ visibleStartOfParagraph = VisiblePosition(visibleStartOfParagraph.deepEquivalent());
+ visibleEndOfParagraph = VisiblePosition(visibleEndOfParagraph.deepEquivalent());
+ if (visibleStartOfParagraph.isNotNull() && !isStartOfParagraph(visibleStartOfParagraph))
+ insertNodeAt(createBreakElement(document()).get(), visibleStartOfParagraph.deepEquivalent());
+ if (visibleEndOfParagraph.isNotNull() && !isEndOfParagraph(visibleEndOfParagraph))
+ insertNodeAt(createBreakElement(document()).get(), visibleEndOfParagraph.deepEquivalent());
+ return;
+ }
+ Node* enclosingBlockFlow = enclosingBlockFlowElement(visibleStartOfParagraph);
+ Node* splitBlockquoteNode = enclosingNode;
+ if (enclosingBlockFlow != enclosingNode)
+ splitBlockquoteNode = splitTreeToNode(enclosingBlockFlowElement(visibleStartOfParagraph), enclosingNode, true);
+ RefPtr<Node> placeholder = createBreakElement(document());
+ insertNodeBefore(placeholder.get(), splitBlockquoteNode);
+ moveParagraph(startOfParagraph(visibleStartOfParagraph), endOfParagraph(visibleEndOfParagraph), VisiblePosition(Position(placeholder.get(), 0)), true);
+}
+
+void IndentOutdentCommand::outdentRegion()
+{
+ VisiblePosition startOfSelection = endingSelection().visibleStart();
+ VisiblePosition endOfSelection = endingSelection().visibleEnd();
+ VisiblePosition endOfLastParagraph = endOfParagraph(endOfSelection);
+
+ ASSERT(!startOfSelection.isNull());
+ ASSERT(!endOfSelection.isNull());
+
+ if (endOfParagraph(startOfSelection) == endOfLastParagraph) {
+ outdentParagraph();
+ return;
+ }
+
+ Position originalSelectionEnd = endingSelection().end();
+ setEndingSelection(endingSelection().visibleStart());
+ outdentParagraph();
+ Position originalSelectionStart = endingSelection().start();
+ VisiblePosition endOfCurrentParagraph = endOfParagraph(endOfParagraph(endingSelection().visibleStart()).next(true));
+ VisiblePosition endAfterSelection = endOfParagraph(endOfParagraph(endOfSelection).next());
+ while (endOfCurrentParagraph != endAfterSelection) {
+ VisiblePosition endOfNextParagraph = endOfParagraph(endOfCurrentParagraph.next());
+ if (endOfCurrentParagraph == endOfLastParagraph)
+ setEndingSelection(Selection(originalSelectionEnd, DOWNSTREAM));
+ else
+ setEndingSelection(endOfCurrentParagraph);
+ outdentParagraph();
+ endOfCurrentParagraph = endOfNextParagraph;
+ }
+ setEndingSelection(Selection(originalSelectionStart, endingSelection().end(), DOWNSTREAM));
+}
+
+void IndentOutdentCommand::doApply()
+{
+ if (endingSelection().isNone())
+ 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(Selection(visibleStart, visibleEnd.previous(true)));
+
+ if (m_typeOfAction == Indent)
+ indentRegion();
+ else
+ outdentRegion();
+}
+
+}
diff --git a/WebCore/editing/IndentOutdentCommand.h b/WebCore/editing/IndentOutdentCommand.h
new file mode 100644
index 0000000..584eb78
--- /dev/null
+++ b/WebCore/editing/IndentOutdentCommand.h
@@ -0,0 +1,51 @@
+/*
+ * 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 IndentOutdentCommand_h
+#define IndentOutdentCommand_h
+
+#include "CompositeEditCommand.h"
+
+namespace WebCore {
+
+class IndentOutdentCommand : public CompositeEditCommand
+{
+public:
+ enum EIndentType { Indent, Outdent };
+ IndentOutdentCommand(Document*, EIndentType, int marginInPixels = 0);
+ virtual void doApply();
+ virtual EditAction editingAction() const { return m_typeOfAction == Indent ? EditActionIndent : EditActionOutdent; }
+private:
+ EIndentType m_typeOfAction;
+ int m_marginInPixels;
+ void indentRegion();
+ void outdentRegion();
+ void outdentParagraph();
+ Node* prepareBlockquoteLevelForInsertion(VisiblePosition&, Node**);
+};
+
+} // namespace WebCore
+
+#endif // IndentOutdentCommand_h
diff --git a/WebCore/editing/InsertIntoTextNodeCommand.cpp b/WebCore/editing/InsertIntoTextNodeCommand.cpp
new file mode 100644
index 0000000..dc2fbbf
--- /dev/null
+++ b/WebCore/editing/InsertIntoTextNodeCommand.cpp
@@ -0,0 +1,58 @@
+/*
+ * 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 "Text.h"
+
+namespace WebCore {
+
+InsertIntoTextNodeCommand::InsertIntoTextNodeCommand(Text* node, int offset, const String& text)
+ : EditCommand(node->document())
+ , m_node(node)
+ , m_offset(offset)
+ , m_text(text)
+{
+ ASSERT(node);
+ ASSERT(offset >= 0);
+ ASSERT(!text.isEmpty());
+}
+
+void InsertIntoTextNodeCommand::doApply()
+{
+ ExceptionCode ec = 0;
+ m_node->insertData(m_offset, m_text, ec);
+ ASSERT(ec == 0);
+}
+
+void InsertIntoTextNodeCommand::doUnapply()
+{
+ ExceptionCode ec = 0;
+ m_node->deleteData(m_offset, m_text.length(), ec);
+ ASSERT(ec == 0);
+}
+
+} // namespace WebCore
diff --git a/WebCore/editing/InsertIntoTextNodeCommand.h b/WebCore/editing/InsertIntoTextNodeCommand.h
new file mode 100644
index 0000000..8358e8d
--- /dev/null
+++ b/WebCore/editing/InsertIntoTextNodeCommand.h
@@ -0,0 +1,54 @@
+/*
+ * 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.
+ */
+
+#ifndef InsertIntoTextNodeCommand_h
+#define InsertIntoTextNodeCommand_h
+
+#include "EditCommand.h"
+
+namespace WebCore {
+
+class Text;
+
+class InsertIntoTextNodeCommand : public EditCommand {
+public:
+ InsertIntoTextNodeCommand(Text* node, int offset, const String& text);
+
+ virtual void doApply();
+ virtual void doUnapply();
+
+ Text* node() const { return m_node.get(); }
+ int offset() const { return m_offset; }
+ String text() const { return m_text; }
+
+private:
+ RefPtr<Text> m_node;
+ int m_offset;
+ String m_text;
+};
+
+} // namespace WebCore
+
+#endif // InsertIntoTextNodeCommand_h
diff --git a/WebCore/editing/InsertLineBreakCommand.cpp b/WebCore/editing/InsertLineBreakCommand.cpp
new file mode 100644
index 0000000..1da16d5
--- /dev/null
+++ b/WebCore/editing/InsertLineBreakCommand.cpp
@@ -0,0 +1,182 @@
+/*
+ * 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 "Element.h"
+#include "Frame.h"
+#include "Text.h"
+#include "VisiblePosition.h"
+#include "Range.h"
+#include "htmlediting.h"
+#include "HTMLNames.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.
+ Position upstream(pos.upstream());
+ Node *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.
+ Position upstream(pos.upstream());
+ Node *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();
+ Selection selection = endingSelection();
+ if (selection.isNone())
+ return;
+
+ VisiblePosition caret(selection.visibleStart());
+ 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) && !lineBreakExistsAtPosition(caret)) {
+ bool needExtraLineBreak = !pos.node()->hasTagName(hrTag) && !pos.node()->hasTagName(tableTag);
+
+ insertNodeAt(nodeToInsert.get(), pos);
+
+ if (needExtraLineBreak)
+ insertNodeBefore(nodeToInsert->cloneNode(false).get(), nodeToInsert.get());
+
+ VisiblePosition endingPosition(Position(nodeToInsert.get(), 0));
+ setEndingSelection(Selection(endingPosition));
+ } else if (pos.offset() <= 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(Selection(positionAfterNode(nodeToInsert.get()), DOWNSTREAM));
+ } else if (pos.offset() >= caretMaxOffset(pos.node())) {
+ insertNodeAt(nodeToInsert.get(), pos);
+ setEndingSelection(Selection(positionAfterNode(nodeToInsert.get()), DOWNSTREAM));
+ } else {
+ // Split a text node
+ ASSERT(pos.node()->isTextNode());
+
+ // Do the split
+ ExceptionCode ec = 0;
+ Text *textNode = static_cast<Text *>(pos.node());
+ RefPtr<Text> textBeforeNode = document()->createTextNode(textNode->substringData(0, selection.start().offset(), ec));
+ deleteTextFromNode(textNode, 0, pos.offset());
+ insertNodeBefore(textBeforeNode.get(), textNode);
+ insertNodeBefore(nodeToInsert.get(), textNode);
+ Position endingPosition = Position(textNode, 0);
+
+ // Handle whitespace that occurs after the split
+ updateLayout();
+ if (!endingPosition.isRenderedCharacter()) {
+ Position positionBeforeTextNode(positionBeforeNode(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(Selection(endingPosition, DOWNSTREAM));
+ }
+
+ // Handle the case where there is a typing style.
+ // FIXME: Improve typing style.
+ // See this bug: <rdar://problem/3769899> Implementation of typing style needs improvement
+
+ CSSMutableStyleDeclaration* typingStyle = document()->frame()->typingStyle();
+
+ if (typingStyle && typingStyle->length() > 0) {
+ Selection selectionBeforeStyle = endingSelection();
+ applyStyle(typingStyle, Position(nodeToInsert.get(), 0),
+ Position(nodeToInsert.get(), maxDeepOffset(nodeToInsert.get())));
+ setEndingSelection(selectionBeforeStyle);
+ }
+
+ rebalanceWhitespace();
+}
+
+}
diff --git a/WebCore/editing/InsertLineBreakCommand.h b/WebCore/editing/InsertLineBreakCommand.h
new file mode 100644
index 0000000..1f5cdc8
--- /dev/null
+++ b/WebCore/editing/InsertLineBreakCommand.h
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+
+#ifndef InsertLineBreakCommand_h
+#define InsertLineBreakCommand_h
+
+#include "CompositeEditCommand.h"
+
+namespace WebCore {
+
+class InsertLineBreakCommand : public CompositeEditCommand {
+public:
+ InsertLineBreakCommand(Document*);
+
+ virtual void doApply();
+
+private:
+ 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/WebCore/editing/InsertListCommand.cpp b/WebCore/editing/InsertListCommand.cpp
new file mode 100644
index 0000000..8408a20
--- /dev/null
+++ b/WebCore/editing/InsertListCommand.cpp
@@ -0,0 +1,258 @@
+/*
+ * 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 "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;
+
+PassRefPtr<Node> InsertListCommand::insertList(Document* document, Type type)
+{
+ RefPtr<InsertListCommand> insertCommand = new InsertListCommand(document, type, "");
+ insertCommand->apply();
+ return insertCommand->m_listElement;
+}
+
+Node* InsertListCommand::fixOrphanedListChild(Node* node)
+{
+ RefPtr<Element> listElement = createUnorderedListElement(document());
+ insertNodeBefore(listElement.get(), node);
+ removeNode(node);
+ appendNode(node, listElement.get());
+ m_listElement = listElement;
+ return listElement.get();
+}
+
+InsertListCommand::InsertListCommand(Document* document, Type type, const String& id)
+ : CompositeEditCommand(document), m_type(type), m_id(id), m_forceCreateList(false)
+{
+}
+
+bool InsertListCommand::modifyRange()
+{
+ Selection selection = selectionForParagraphIteration(endingSelection());
+ ASSERT(selection.isRange());
+ VisiblePosition startOfSelection = selection.visibleStart();
+ VisiblePosition endOfSelection = selection.visibleEnd();
+ VisiblePosition startOfLastParagraph = startOfParagraph(endOfSelection);
+
+ if (startOfParagraph(startOfSelection) == startOfLastParagraph)
+ return false;
+
+ Node* startList = enclosingList(startOfSelection.deepEquivalent().node());
+ Node* endList = enclosingList(endOfSelection.deepEquivalent().node());
+ if (!startList || startList != endList)
+ m_forceCreateList = true;
+
+ setEndingSelection(startOfSelection);
+ doApply();
+ // 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.
+ startOfSelection = endingSelection().visibleStart();
+ VisiblePosition startOfCurrentParagraph = startOfNextParagraph(startOfSelection);
+ while (startOfCurrentParagraph != startOfLastParagraph) {
+ setEndingSelection(startOfCurrentParagraph);
+ doApply();
+ startOfCurrentParagraph = startOfNextParagraph(endingSelection().visibleStart());
+ }
+ setEndingSelection(endOfSelection);
+ doApply();
+ // Fetch the end of the selection, for the reason mentioned above.
+ endOfSelection = endingSelection().visibleEnd();
+ setEndingSelection(Selection(startOfSelection, endOfSelection));
+ m_forceCreateList = false;
+ return true;
+}
+
+void InsertListCommand::doApply()
+{
+ if (endingSelection().isNone())
+ 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(Selection(visibleStart, visibleEnd.previous(true)));
+
+ if (endingSelection().isRange() && modifyRange())
+ return;
+
+ // 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();
+ const QualifiedName listTag = (m_type == OrderedList) ? olTag : ulTag;
+ Node* listChildNode = enclosingListChild(selectionNode);
+ bool switchListType = false;
+ if (listChildNode) {
+ // Remove the list chlild.
+ Node* listNode = enclosingList(listChildNode);
+ if (!listNode)
+ listNode = fixOrphanedListChild(listChildNode);
+ if (!listNode->hasTagName(listTag))
+ // listChildNode will be removed from the list and a list of type m_type will be created.
+ switchListType = true;
+ Node* nextListChild;
+ Node* previousListChild;
+ VisiblePosition start;
+ VisiblePosition end;
+ if (listChildNode->hasTagName(liTag)) {
+ start = VisiblePosition(Position(listChildNode, 0));
+ end = VisiblePosition(Position(listChildNode, maxDeepOffset(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(endingSelection().visibleStart());
+ end = endOfParagraph(endingSelection().visibleEnd());
+ nextListChild = enclosingListChild(end.next().deepEquivalent().node());
+ ASSERT(nextListChild != listChildNode);
+ if (enclosingList(nextListChild) != listNode)
+ nextListChild = 0;
+ previousListChild = enclosingListChild(start.previous().deepEquivalent().node());
+ ASSERT(previousListChild != listChildNode);
+ if (enclosingList(previousListChild) != listNode)
+ previousListChild = 0;
+ }
+ // 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<Node> 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.get(), nodeToInsert.get());
+ }
+
+ 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(static_cast<Element *>(listNode), splitTreeToNode(nextListChild, listNode));
+ insertNodeBefore(nodeToInsert.get(), 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(static_cast<Element *>(listNode), splitTreeToNode(listChildNode, listNode));
+ insertNodeBefore(nodeToInsert.get(), listNode);
+ } else
+ insertNodeAfter(nodeToInsert.get(), listNode);
+
+ VisiblePosition insertionPoint = VisiblePosition(Position(placeholder.get(), 0));
+ moveParagraphs(start, end, insertionPoint, true);
+ }
+ if (!listChildNode || switchListType || m_forceCreateList) {
+ // Create list.
+ VisiblePosition start = startOfParagraph(endingSelection().visibleStart());
+ VisiblePosition end = endOfParagraph(endingSelection().visibleEnd());
+
+ // Check for adjoining lists.
+ VisiblePosition previousPosition = start.previous(true);
+ VisiblePosition nextPosition = end.next(true);
+ RefPtr<Element> listItemElement = createListItemElement(document());
+ RefPtr<Element> placeholder = createBreakElement(document());
+ appendNode(placeholder.get(), listItemElement.get());
+ Node* previousList = outermostEnclosingList(previousPosition.deepEquivalent().node());
+ Node* nextList = outermostEnclosingList(nextPosition.deepEquivalent().node());
+ Node* startNode = start.deepEquivalent().node();
+ Node* previousCell = enclosingTableCell(previousPosition.deepEquivalent());
+ Node* nextCell = enclosingTableCell(nextPosition.deepEquivalent());
+ Node* currentCell = enclosingTableCell(start.deepEquivalent());
+ if (previousList && (!previousList->hasTagName(listTag) || startNode->isDescendantOf(previousList) || previousCell != currentCell))
+ previousList = 0;
+ if (nextList && (!nextList->hasTagName(listTag) || startNode->isDescendantOf(nextList) || nextCell != currentCell))
+ nextList = 0;
+ // Place list item into adjoining lists.
+ if (previousList)
+ appendNode(listItemElement.get(), previousList);
+ else if (nextList)
+ insertNodeAt(listItemElement.get(), Position(nextList, 0));
+ else {
+ // Create the list.
+ RefPtr<Element> listElement = m_type == OrderedList ? createOrderedListElement(document()) : createUnorderedListElement(document());
+ m_listElement = listElement;
+ if (!m_id.isEmpty())
+ static_cast<HTMLElement*>(listElement.get())->setId(m_id);
+ appendNode(listItemElement.get(), listElement.get());
+
+ 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.
+ Node* placeholder = insertBlockPlaceholder(start.deepEquivalent());
+ start = VisiblePosition(Position(placeholder, 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 = positionBeforeNode(listChild);
+
+ insertNodeAt(listElement.get(), insertionPos);
+ }
+ moveParagraph(start, end, VisiblePosition(Position(placeholder.get(), 0)), true);
+ if (nextList && previousList)
+ mergeIdenticalElements(static_cast<Element*>(previousList), static_cast<Element*>(nextList));
+ }
+}
+
+}
diff --git a/WebCore/editing/InsertListCommand.h b/WebCore/editing/InsertListCommand.h
new file mode 100644
index 0000000..8846248
--- /dev/null
+++ b/WebCore/editing/InsertListCommand.h
@@ -0,0 +1,51 @@
+/*
+ * 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 InsertListCommand_h
+#define InsertListCommand_h
+
+#include "CompositeEditCommand.h"
+
+namespace WebCore {
+
+class InsertListCommand : public CompositeEditCommand {
+public:
+ enum Type { OrderedList, UnorderedList };
+ static PassRefPtr<Node> insertList(Document*, Type);
+ InsertListCommand(Document*, Type, const String&);
+ virtual void doApply();
+ virtual EditAction editingAction() const { return EditActionInsertList; }
+private:
+ Node* fixOrphanedListChild(Node*);
+ bool modifyRange();
+ RefPtr<Node> m_listElement;
+ Type m_type;
+ String m_id;
+ bool m_forceCreateList;
+};
+
+} // namespace WebCore
+
+#endif // InsertListCommand_h
diff --git a/WebCore/editing/InsertNodeBeforeCommand.cpp b/WebCore/editing/InsertNodeBeforeCommand.cpp
new file mode 100644
index 0000000..ddc4768
--- /dev/null
+++ b/WebCore/editing/InsertNodeBeforeCommand.cpp
@@ -0,0 +1,65 @@
+/*
+ * 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 "htmlediting.h"
+#include "InsertNodeBeforeCommand.h"
+
+namespace WebCore {
+
+InsertNodeBeforeCommand::InsertNodeBeforeCommand(PassRefPtr<Node> insertChild, Node* refChild)
+ : EditCommand(refChild->document()), m_insertChild(insertChild), m_refChild(refChild)
+{
+ ASSERT(m_insertChild);
+ ASSERT(m_refChild);
+}
+
+void InsertNodeBeforeCommand::doApply()
+{
+ ASSERT(m_insertChild);
+ ASSERT(m_refChild);
+ ASSERT(m_refChild->parentNode());
+ // If the child to insert is already in a tree, inserting it will remove it from it's old location
+ // in an non-undoable way. We might eventually find it useful to do an undoable remove in this case.
+ ASSERT(!m_insertChild->parent());
+ ASSERT(enclosingNodeOfType(Position(m_refChild->parentNode(), 0), &isContentEditable) || !m_refChild->parentNode()->attached());
+
+ ExceptionCode ec = 0;
+ m_refChild->parentNode()->insertBefore(m_insertChild.get(), m_refChild.get(), ec);
+ ASSERT(ec == 0);
+}
+
+void InsertNodeBeforeCommand::doUnapply()
+{
+ ASSERT(m_insertChild);
+ ASSERT(m_refChild);
+ ASSERT(m_refChild->parentNode());
+
+ ExceptionCode ec = 0;
+ m_refChild->parentNode()->removeChild(m_insertChild.get(), ec);
+ ASSERT(ec == 0);
+}
+
+}
diff --git a/WebCore/editing/InsertNodeBeforeCommand.h b/WebCore/editing/InsertNodeBeforeCommand.h
new file mode 100644
index 0000000..0ec5f4f
--- /dev/null
+++ b/WebCore/editing/InsertNodeBeforeCommand.h
@@ -0,0 +1,50 @@
+/*
+ * 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.
+ */
+
+#ifndef InsertNodeBeforeCommand_h
+#define InsertNodeBeforeCommand_h
+
+#include "EditCommand.h"
+
+namespace WebCore {
+
+class InsertNodeBeforeCommand : public EditCommand {
+public:
+ InsertNodeBeforeCommand(PassRefPtr<Node>, Node* refChild);
+
+ virtual void doApply();
+ virtual void doUnapply();
+
+ Node* insertChild() const { return m_insertChild.get(); }
+ Node* refChild() const { return m_refChild.get(); }
+
+private:
+ RefPtr<Node> m_insertChild;
+ RefPtr<Node> m_refChild;
+};
+
+} // namespace WebCore
+
+#endif // InsertNodeBeforeCommand_h
diff --git a/WebCore/editing/InsertParagraphSeparatorCommand.cpp b/WebCore/editing/InsertParagraphSeparatorCommand.cpp
new file mode 100644
index 0000000..3c1bb14
--- /dev/null
+++ b/WebCore/editing/InsertParagraphSeparatorCommand.cpp
@@ -0,0 +1,310 @@
+/*
+ * 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 "Document.h"
+#include "Logging.h"
+#include "CSSComputedStyleDeclaration.h"
+#include "CSSPropertyNames.h"
+#include "Text.h"
+#include "htmlediting.h"
+#include "HTMLElement.h"
+#include "HTMLNames.h"
+#include "InsertLineBreakCommand.h"
+#include "RenderObject.h"
+#include "visible_units.h"
+
+namespace WebCore {
+
+using namespace HTMLNames;
+
+InsertParagraphSeparatorCommand::InsertParagraphSeparatorCommand(Document *document, bool useDefaultParagraphElement)
+ : CompositeEditCommand(document)
+ , m_useDefaultParagraphElement(useDefaultParagraphElement)
+{
+}
+
+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 = styleAtPosition(pos);
+}
+
+void InsertParagraphSeparatorCommand::applyStyleAfterInsertion()
+{
+ // FIXME: Improve typing style.
+ // See this bug: <rdar://problem/3769899> Implementation of typing style needs improvement
+ if (!m_style)
+ return;
+
+ CSSComputedStyleDeclaration endingStyle(endingSelection().start().node());
+ endingStyle.diff(m_style.get());
+ if (m_style->length() > 0)
+ applyStyle(m_style.get());
+}
+
+void InsertParagraphSeparatorCommand::doApply()
+{
+ bool splitText = false;
+ if (endingSelection().isNone())
+ return;
+
+ Position pos = endingSelection().start();
+
+ EAffinity affinity = endingSelection().affinity();
+
+ // Delete the current selection.
+ if (endingSelection().isRange()) {
+ calculateStyleBeforeInsertion(pos);
+ deleteSelection(false, true);
+ pos = endingSelection().start();
+ affinity = endingSelection().affinity();
+ }
+
+ // FIXME: The rangeCompliantEquivalent conversion needs to be moved into enclosingBlock.
+ Node* startBlock = enclosingBlock(rangeCompliantEquivalent(pos).node());
+ Position canonicalPos = VisiblePosition(pos).deepEquivalent();
+ if (!startBlock || !startBlock->parentNode() ||
+ isTableCell(startBlock) ||
+ startBlock->hasTagName(formTag) ||
+ canonicalPos.node()->renderer() && canonicalPos.node()->renderer()->isTable() ||
+ canonicalPos.node()->hasTagName(hrTag)) {
+ applyCommandToComposite(new InsertLineBreakCommand(document()));
+ return;
+ }
+
+ // Use the leftmost candidate.
+ pos = pos.upstream();
+ if (!pos.isCandidate())
+ pos = pos.downstream();
+
+ // Adjust the insertion position after the delete
+ pos = positionAvoidingSpecialElementBoundary(pos);
+ VisiblePosition visiblePos(pos, affinity);
+ calculateStyleBeforeInsertion(pos);
+
+ //---------------------------------------------------------------------
+ // Handle special case of typing return on an empty list item
+ if (breakOutOfEmptyListItem())
+ return;
+
+ //---------------------------------------------------------------------
+ // Prepare for more general cases.
+ // FIXME: We shouldn't peel off the node here because then we lose track of
+ // the fact that it's the node that belongs to an editing position and
+ // not a rangeCompliantEquivalent.
+ Node *startNode = pos.node();
+
+ bool isFirstInBlock = isStartOfBlock(visiblePos);
+ bool isLastInBlock = isEndOfBlock(visiblePos);
+ bool nestNewBlock = false;
+
+ // Create block to be inserted.
+ RefPtr<Node> blockToInsert;
+ if (startBlock == startBlock->rootEditableElement()) {
+ blockToInsert = static_pointer_cast<Node>(createDefaultParagraphElement(document()));
+ nestNewBlock = true;
+ } else if (m_useDefaultParagraphElement)
+ blockToInsert = static_pointer_cast<Node>(createDefaultParagraphElement(document()));
+ else
+ blockToInsert = startBlock->cloneNode(false);
+
+ //---------------------------------------------------------------------
+ // 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 && !lineBreakExistsAtPosition(visiblePos)) {
+ // The block is empty. Create an empty block to
+ // represent the paragraph that we're leaving.
+ RefPtr<Node> extraBlock = createDefaultParagraphElement(document());
+ appendNode(extraBlock.get(), startBlock);
+ appendBlockPlaceholder(extraBlock.get());
+ }
+ appendNode(blockToInsert.get(), startBlock);
+ } else
+ insertNodeAfter(blockToInsert.get(), startBlock);
+
+ appendBlockPlaceholder(blockToInsert.get());
+ setEndingSelection(Selection(Position(blockToInsert.get(), 0), DOWNSTREAM));
+ applyStyleAfterInsertion();
+ 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 (pos.node() == startBlock && nestNewBlock) {
+ refNode = startBlock->childNode(pos.offset());
+ ASSERT(refNode); // must be true or we'd be in the end of block case
+ } else
+ refNode = pos.node();
+
+ // find ending selection position easily before inserting the paragraph
+ pos = pos.downstream();
+
+ insertNodeBefore(blockToInsert.get(), refNode);
+ appendBlockPlaceholder(blockToInsert.get());
+ setEndingSelection(Selection(Position(blockToInsert.get(), 0), DOWNSTREAM));
+ applyStyleAfterInsertion();
+ setEndingSelection(Selection(pos, 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(), pos);
+ pos = positionAfterNode(br.get());
+ }
+
+ // Move downstream. Typing style code will take care of carrying along the
+ // style of the upstream position.
+ pos = pos.downstream();
+ startNode = pos.node();
+
+ // Build up list of ancestors in between the start node and the start block.
+ Vector<Node*> ancestors;
+ if (startNode != startBlock)
+ for (Node* n = startNode->parentNode(); n && n != startBlock; n = n->parentNode())
+ ancestors.append(n);
+
+ // 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 = pos.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()) {
+ Text* textNode = static_cast<Text*>(leadingWhitespace.node());
+ ASSERT(!textNode->renderer() || textNode->renderer()->style()->collapseWhiteSpace());
+ replaceTextInNode(textNode, leadingWhitespace.offset(), 1, nonBreakingSpaceString());
+ }
+
+ // Split at pos if in the middle of a text node.
+ if (startNode->isTextNode()) {
+ Text *textNode = static_cast<Text *>(startNode);
+ bool atEnd = (unsigned)pos.offset() >= textNode->length();
+ if (pos.offset() > 0 && !atEnd) {
+ splitTextNode(textNode, pos.offset());
+ pos = Position(startNode, 0);
+ visiblePos = VisiblePosition(pos);
+ 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 start block.
+ RefPtr<Node> parent = blockToInsert;
+ for (size_t i = ancestors.size(); i != 0; --i) {
+ RefPtr<Node> child = ancestors[i - 1]->cloneNode(false); // shallow clone
+ appendNode(child.get(), parent.get());
+ parent = child.release();
+ }
+
+ // 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) && !lineBreakExistsAtPosition(visiblePos))
+ appendNode(createBreakElement(document()).get(), blockToInsert.get());
+
+ // Move the start node and the siblings of the start node.
+ if (startNode != startBlock) {
+ Node *n = startNode;
+ if (pos.offset() >= caretMaxOffset(startNode))
+ n = startNode->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()) {
+ Node* leftParent = ancestors.first();
+ while (leftParent && leftParent != startBlock) {
+ parent = parent->parentNode();
+ Node* n = leftParent->nextSibling();
+ while (n && n != blockToInsert) {
+ Node* next = n->nextSibling();
+ removeNode(n);
+ appendNode(n, parent.get());
+ n = next;
+ }
+ leftParent = leftParent->parentNode();
+ }
+ }
+
+ // Handle whitespace that occurs after the split
+ if (splitText) {
+ updateLayout();
+ pos = Position(startNode, 0);
+ if (!pos.isRenderedCharacter()) {
+ // Clear out all whitespace and insert one non-breaking space
+ ASSERT(startNode);
+ ASSERT(startNode->isTextNode());
+ ASSERT(!startNode->renderer() || startNode->renderer()->style()->collapseWhiteSpace());
+ deleteInsignificantTextDownstream(pos);
+ insertTextIntoNode(static_cast<Text*>(startNode), 0, nonBreakingSpaceString());
+ }
+ }
+
+ setEndingSelection(Selection(Position(blockToInsert.get(), 0), DOWNSTREAM));
+ applyStyleAfterInsertion();
+}
+
+} // namespace WebCore
diff --git a/WebCore/editing/InsertParagraphSeparatorCommand.h b/WebCore/editing/InsertParagraphSeparatorCommand.h
new file mode 100644
index 0000000..d866ed5
--- /dev/null
+++ b/WebCore/editing/InsertParagraphSeparatorCommand.h
@@ -0,0 +1,52 @@
+/*
+ * 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.
+ */
+
+#ifndef InsertParagraphSeparatorCommand_h
+#define InsertParagraphSeparatorCommand_h
+
+#include "CompositeEditCommand.h"
+
+namespace WebCore {
+
+class InsertParagraphSeparatorCommand : public CompositeEditCommand {
+public:
+ InsertParagraphSeparatorCommand(Document*, bool useDefaultParagraphElement = false);
+
+ virtual void doApply();
+
+private:
+ void calculateStyleBeforeInsertion(const Position&);
+ void applyStyleAfterInsertion();
+
+ virtual bool preservesTypingStyle() const;
+
+ RefPtr<CSSMutableStyleDeclaration> m_style;
+
+ bool m_useDefaultParagraphElement;
+};
+
+}
+
+#endif
diff --git a/WebCore/editing/InsertTextCommand.cpp b/WebCore/editing/InsertTextCommand.cpp
new file mode 100644
index 0000000..8be8d1e
--- /dev/null
+++ b/WebCore/editing/InsertTextCommand.cpp
@@ -0,0 +1,208 @@
+/*
+ * 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 "CSSMutableStyleDeclaration.h"
+#include "CSSComputedStyleDeclaration.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), m_charactersAdded(0)
+{
+}
+
+void InsertTextCommand::doApply()
+{
+}
+
+Position InsertTextCommand::prepareForTextInsertion(const Position& p)
+{
+ Position pos = p;
+ // If an anchor was removed and the selection hasn't changed, we restore it.
+ RefPtr<Node> anchor = document()->frame()->editor()->removedAnchor();
+ if (anchor) {
+ insertNodeAt(anchor.get(), pos);
+ document()->frame()->editor()->setRemovedAnchor(0);
+ pos = Position(anchor.get(), 0);
+ }
+ // 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;
+}
+
+void InsertTextCommand::input(const String& originalText, bool selectInsertedText)
+{
+ String text = originalText;
+
+ ASSERT(text.find('\n') == -1);
+
+ if (endingSelection().isNone())
+ return;
+
+ if (RenderObject* renderer = endingSelection().start().node()->renderer())
+ if (renderer->style()->collapseWhiteSpace())
+ // Turn all spaces into non breaking spaces, to make sure that they are treated
+ // literally, and aren't collapsed after insertion. They will be rebalanced
+ // (turned into a sequence of regular and non breaking spaces) below.
+ text.replace(' ', noBreakSpace);
+
+ // Delete the current selection.
+ // FIXME: This delete operation blows away the typing style.
+ if (endingSelection().isRange())
+ deleteSelection(false, true, true, false);
+
+ // Insert the character at the leftmost candidate.
+ Position startPosition = endingSelection().start().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(positionBeforeNode(startPosition.node()));
+ deleteInsignificantText(startPosition.upstream(), startPosition.downstream());
+ if (!startPosition.node()->inDocument())
+ startPosition = positionBeforeStartNode;
+ if (!startPosition.isCandidate())
+ startPosition = startPosition.downstream();
+
+ // FIXME: This typing around anchor behavior doesn't exactly match TextEdit. In TextEdit,
+ // you won't be placed inside a link when typing after it if you've just placed the caret
+ // there with the mouse.
+ startPosition = positionAvoidingSpecialElementBoundary(startPosition, false);
+
+ Position endPosition;
+
+ if (text == "\t") {
+ endPosition = insertTab(startPosition);
+ startPosition = endPosition.previous();
+ removePlaceholderAt(VisiblePosition(startPosition));
+ m_charactersAdded += 1;
+ } else {
+ // Make sure the document is set up to receive text
+ startPosition = prepareForTextInsertion(startPosition);
+ removePlaceholderAt(VisiblePosition(startPosition));
+ Text *textNode = static_cast<Text *>(startPosition.node());
+ int offset = startPosition.offset();
+
+ 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 (originalText != " ")
+ rebalanceWhitespaceAt(startPosition);
+
+ m_charactersAdded += 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>
+ Selection forcedEndingSelection;
+ forcedEndingSelection.setWithoutValidation(startPosition, endPosition);
+ setEndingSelection(forcedEndingSelection);
+
+ // Handle the case where there is a typing style.
+ // FIXME: Improve typing style.
+ // See this bug: <rdar://problem/3769899> Implementation of typing style needs improvement
+ CSSMutableStyleDeclaration* typingStyle = document()->frame()->typingStyle();
+ RefPtr<CSSComputedStyleDeclaration> endingStyle = endPosition.computedStyle();
+ endingStyle->diff(typingStyle);
+ if (typingStyle && typingStyle->length() > 0)
+ applyStyle(typingStyle);
+
+ if (!selectInsertedText)
+ setEndingSelection(Selection(endingSelection().end(), endingSelection().affinity()));
+}
+
+Position InsertTextCommand::insertTab(const Position& pos)
+{
+ Position insertPos = VisiblePosition(pos, DOWNSTREAM).deepEquivalent();
+
+ Node *node = insertPos.node();
+ unsigned int offset = insertPos.offset();
+
+ // 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.get(), textNode);
+ }
+ }
+
+ // return the position following the new tab
+ return Position(spanNode->lastChild(), caretMaxOffset(spanNode->lastChild()));
+}
+
+bool InsertTextCommand::isInsertTextCommand() const
+{
+ return true;
+}
+
+}
diff --git a/WebCore/editing/InsertTextCommand.h b/WebCore/editing/InsertTextCommand.h
new file mode 100644
index 0000000..8b25c8f
--- /dev/null
+++ b/WebCore/editing/InsertTextCommand.h
@@ -0,0 +1,55 @@
+/*
+ * 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.
+ */
+
+#ifndef InsertTextCommand_h
+#define InsertTextCommand_h
+
+#include "CompositeEditCommand.h"
+
+namespace WebCore {
+
+class InsertTextCommand : public CompositeEditCommand {
+public:
+ InsertTextCommand(Document*);
+
+ virtual void doApply();
+
+ void deleteCharacter();
+ void input(const String& text, bool selectInsertedText = false);
+
+ unsigned charactersAdded() const { return m_charactersAdded; }
+
+private:
+ virtual bool isInsertTextCommand() const;
+
+ Position prepareForTextInsertion(const Position&);
+ Position insertTab(const Position&);
+
+ unsigned m_charactersAdded;
+};
+
+} // namespace WebCore
+
+#endif // InsertTextCommand_h
diff --git a/WebCore/editing/JoinTextNodesCommand.cpp b/WebCore/editing/JoinTextNodesCommand.cpp
new file mode 100644
index 0000000..9d51043
--- /dev/null
+++ b/WebCore/editing/JoinTextNodesCommand.cpp
@@ -0,0 +1,76 @@
+/*
+ * 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 "JoinTextNodesCommand.h"
+
+#include "Text.h"
+
+namespace WebCore {
+
+JoinTextNodesCommand::JoinTextNodesCommand(Text *text1, Text *text2)
+ : EditCommand(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()
+{
+ ASSERT(m_text1);
+ ASSERT(m_text2);
+ ASSERT(m_text1->nextSibling() == m_text2);
+
+ ExceptionCode ec = 0;
+ m_text2->insertData(0, m_text1->data(), ec);
+ ASSERT(ec == 0);
+
+ m_text2->parentNode()->removeChild(m_text1.get(), ec);
+ ASSERT(ec == 0);
+
+ m_offset = m_text1->length();
+}
+
+void JoinTextNodesCommand::doUnapply()
+{
+ ASSERT(m_text2);
+ ASSERT(m_offset > 0);
+
+ ExceptionCode ec = 0;
+
+ m_text2->deleteData(0, m_offset, ec);
+ ASSERT(ec == 0);
+
+ m_text2->parentNode()->insertBefore(m_text1.get(), m_text2.get(), ec);
+ ASSERT(ec == 0);
+
+ ASSERT(m_text2->previousSibling()->isTextNode());
+ ASSERT(m_text2->previousSibling() == m_text1);
+}
+
+} // namespace WebCore
diff --git a/WebCore/editing/JoinTextNodesCommand.h b/WebCore/editing/JoinTextNodesCommand.h
new file mode 100644
index 0000000..a65cb4f
--- /dev/null
+++ b/WebCore/editing/JoinTextNodesCommand.h
@@ -0,0 +1,54 @@
+/*
+ * 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.
+ */
+
+#ifndef JoinTextNodesCommand_h
+#define JoinTextNodesCommand_h
+
+#include "EditCommand.h"
+
+namespace WebCore {
+
+class Text;
+
+class JoinTextNodesCommand : public EditCommand
+{
+public:
+ JoinTextNodesCommand(Text*, Text*);
+
+ virtual void doApply();
+ virtual void doUnapply();
+
+ Text* firstNode() const { return m_text1.get(); }
+ Text* secondNode() const { return m_text2.get(); }
+
+private:
+ RefPtr<Text> m_text1;
+ RefPtr<Text> m_text2;
+ unsigned m_offset;
+};
+
+} // namespace WebCore
+
+#endif // JoinTextNodesCommand_h
diff --git a/WebCore/editing/MergeIdenticalElementsCommand.cpp b/WebCore/editing/MergeIdenticalElementsCommand.cpp
new file mode 100644
index 0000000..0b9b019
--- /dev/null
+++ b/WebCore/editing/MergeIdenticalElementsCommand.cpp
@@ -0,0 +1,77 @@
+/*
+ * 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 "MergeIdenticalElementsCommand.h"
+
+#include "Element.h"
+
+namespace WebCore {
+
+MergeIdenticalElementsCommand::MergeIdenticalElementsCommand(Element* first, Element* second)
+ : EditCommand(first->document()), m_element1(first), m_element2(second)
+{
+ ASSERT(m_element1);
+ ASSERT(m_element2);
+}
+
+void MergeIdenticalElementsCommand::doApply()
+{
+ ASSERT(m_element1);
+ ASSERT(m_element2);
+ ASSERT(m_element1->nextSibling() == m_element2);
+
+ ExceptionCode ec = 0;
+
+ if (!m_atChild)
+ m_atChild = m_element2->firstChild();
+
+ while (m_element1->lastChild()) {
+ m_element2->insertBefore(m_element1->lastChild(), m_element2->firstChild(), ec);
+ ASSERT(ec == 0);
+ }
+
+ m_element2->parentNode()->removeChild(m_element1.get(), ec);
+ ASSERT(ec == 0);
+}
+
+void MergeIdenticalElementsCommand::doUnapply()
+{
+ ASSERT(m_element1);
+ ASSERT(m_element2);
+
+ ExceptionCode ec = 0;
+
+ m_element2->parent()->insertBefore(m_element1.get(), m_element2.get(), ec);
+ ASSERT(ec == 0);
+
+ while (m_element2->firstChild() != m_atChild) {
+ ASSERT(m_element2->firstChild());
+ m_element1->appendChild(m_element2->firstChild(), ec);
+ ASSERT(ec == 0);
+ }
+}
+
+} // namespace WebCore
diff --git a/WebCore/editing/MergeIdenticalElementsCommand.h b/WebCore/editing/MergeIdenticalElementsCommand.h
new file mode 100644
index 0000000..4d0e50d
--- /dev/null
+++ b/WebCore/editing/MergeIdenticalElementsCommand.h
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+
+#ifndef MergeIdenticalElementsCommand_h
+#define MergeIdenticalElementsCommand_h
+
+#include "EditCommand.h"
+
+namespace WebCore {
+
+class MergeIdenticalElementsCommand : public EditCommand {
+public:
+ MergeIdenticalElementsCommand(Element*, Element*);
+
+ virtual void doApply();
+ virtual void doUnapply();
+
+private:
+ RefPtr<Element> m_element1;
+ RefPtr<Element> m_element2;
+ RefPtr<Node> m_atChild;
+};
+
+} // namespace WebCore
+
+#endif // MergeIdenticalElementsCommand_h
diff --git a/WebCore/editing/ModifySelectionListLevel.cpp b/WebCore/editing/ModifySelectionListLevel.cpp
new file mode 100644
index 0000000..674afeb
--- /dev/null
+++ b/WebCore/editing/ModifySelectionListLevel.cpp
@@ -0,0 +1,293 @@
+/*
+ * 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 "ModifySelectionListLevel.h"
+
+#include "Document.h"
+#include "Element.h"
+#include "Frame.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 Selection& 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->element()))
+ endListChild = r->element();
+ }
+
+ 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, Node* 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 Selection& 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()->element();
+ if (isListElement(previousItem)) {
+ // move nodes up into preceding list
+ appendSiblingNodeRange(startListChild, endListChild, previousItem);
+ m_listElement = previousItem;
+ } else {
+ // create a sublist for the preceding element and move nodes there
+ RefPtr<Node> newParent;
+ switch (m_listType) {
+ case InheritedListType:
+ newParent = startListChild->parentNode()->cloneNode(false);
+ break;
+ case OrderedList:
+ newParent = createOrderedListElement(document());
+ break;
+ case UnorderedList:
+ newParent = createUnorderedListElement(document());
+ break;
+ }
+ insertNodeBefore(newParent.get(), startListChild);
+ appendSiblingNodeRange(startListChild, endListChild, newParent.get());
+ m_listElement = newParent.get();
+ }
+}
+
+bool IncreaseSelectionListLevelCommand::canIncreaseSelectionListLevel(Document* document)
+{
+ Node* startListChild;
+ Node* endListChild;
+ return canIncreaseListLevel(document->frame()->selectionController()->selection(), startListChild, endListChild);
+}
+
+PassRefPtr<Node> IncreaseSelectionListLevelCommand::increaseSelectionListLevelWithType(Document* document, Type listType)
+{
+ ASSERT(document);
+ ASSERT(document->frame());
+ RefPtr<IncreaseSelectionListLevelCommand> modCommand = new IncreaseSelectionListLevelCommand(document, listType);
+ modCommand->apply();
+ return modCommand->m_listElement.get();
+}
+
+PassRefPtr<Node> IncreaseSelectionListLevelCommand::increaseSelectionListLevel(Document* document)
+{
+ return increaseSelectionListLevelWithType(document, InheritedListType);
+}
+
+PassRefPtr<Node> IncreaseSelectionListLevelCommand::increaseSelectionListLevelOrdered(Document* document)
+{
+ return increaseSelectionListLevelWithType(document, OrderedList);
+}
+
+PassRefPtr<Node> IncreaseSelectionListLevelCommand::increaseSelectionListLevelUnordered(Document* document)
+{
+ return increaseSelectionListLevelWithType(document, UnorderedList);
+}
+
+DecreaseSelectionListLevelCommand::DecreaseSelectionListLevelCommand(Document* document)
+ : ModifySelectionListLevelCommand(document)
+{
+}
+
+// This needs to be static so it can be called by canDecreaseSelectionListLevel
+static bool canDecreaseListLevel(const Selection& 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()->element() : 0;
+ Node* nextItem = endListChild->renderer()->nextSibling() ? endListChild->renderer()->nextSibling()->element() : 0;
+ Node* listNode = startListChild->parentNode();
+
+ 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 {
+ // in the middle of list, split the list and move the children to the divide
+ splitElement(static_cast<Element*>(listNode), startListChild);
+ insertSiblingNodeRangeBefore(startListChild, endListChild, listNode);
+ }
+}
+
+bool DecreaseSelectionListLevelCommand::canDecreaseSelectionListLevel(Document* document)
+{
+ Node* startListChild;
+ Node* endListChild;
+ return canDecreaseListLevel(document->frame()->selectionController()->selection(), startListChild, endListChild);
+}
+
+void DecreaseSelectionListLevelCommand::decreaseSelectionListLevel(Document* document)
+{
+ ASSERT(document);
+ ASSERT(document->frame());
+ applyCommand(new DecreaseSelectionListLevelCommand(document));
+}
+
+}
diff --git a/WebCore/editing/ModifySelectionListLevel.h b/WebCore/editing/ModifySelectionListLevel.h
new file mode 100644
index 0000000..1e66d10
--- /dev/null
+++ b/WebCore/editing/ModifySelectionListLevel.h
@@ -0,0 +1,80 @@
+/*
+ * 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 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, Node* 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> increaseSelectionListLevelWithType(Document*, Type listType);
+
+ 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:
+ DecreaseSelectionListLevelCommand(Document*);
+ virtual void doApply();
+};
+
+} // namespace WebCore
+
+#endif // ModifySelectionListLevel_h
diff --git a/WebCore/editing/MoveSelectionCommand.cpp b/WebCore/editing/MoveSelectionCommand.cpp
new file mode 100644
index 0000000..b2637a7
--- /dev/null
+++ b/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 smartMove)
+ : CompositeEditCommand(position.node()->document()), m_fragment(fragment), m_position(position), m_smartMove(smartMove)
+{
+ ASSERT(m_fragment);
+}
+
+MoveSelectionCommand::~MoveSelectionCommand()
+{
+}
+
+void MoveSelectionCommand::doApply()
+{
+ Selection selection = endingSelection();
+ ASSERT(selection.isRange());
+
+ 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.offset();
+ Position selectionEnd = selection.end();
+ int selectionEndOffset = selectionEnd.offset();
+ if (selectionEnd.node() == positionNode && selectionEndOffset < positionOffset) {
+ positionOffset -= selectionEndOffset;
+ Position selectionStart = selection.start();
+ if (selectionStart.node() == positionNode) {
+ positionOffset += selectionStart.offset();
+ }
+ pos = Position(positionNode, positionOffset);
+ }
+
+ deleteSelection(m_smartMove);
+
+ // 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(Selection(pos, endingSelection().affinity()));
+ applyCommandToComposite(new ReplaceSelectionCommand(positionNode->document(), m_fragment.get(), true, m_smartMove));
+}
+
+EditAction MoveSelectionCommand::editingAction() const
+{
+ return EditActionDrag;
+}
+
+} // namespace WebCore
diff --git a/WebCore/editing/MoveSelectionCommand.h b/WebCore/editing/MoveSelectionCommand.h
new file mode 100644
index 0000000..d521335
--- /dev/null
+++ b/WebCore/editing/MoveSelectionCommand.h
@@ -0,0 +1,51 @@
+/*
+ * 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.
+ */
+
+#ifndef MoveSelectionCommand_h
+#define MoveSelectionCommand_h
+
+#include "CompositeEditCommand.h"
+
+namespace WebCore {
+
+class DocumentFragment;
+
+class MoveSelectionCommand : public CompositeEditCommand {
+public:
+ MoveSelectionCommand(PassRefPtr<DocumentFragment>, const Position&, bool smartMove = false);
+ virtual ~MoveSelectionCommand();
+
+ virtual void doApply();
+ virtual EditAction editingAction() const;
+
+private:
+ RefPtr<DocumentFragment> m_fragment;
+ Position m_position;
+ bool m_smartMove;
+};
+
+} // namespace WebCore
+
+#endif // __MoveSelectionCommand_h
diff --git a/WebCore/editing/RemoveCSSPropertyCommand.cpp b/WebCore/editing/RemoveCSSPropertyCommand.cpp
new file mode 100644
index 0000000..b51ebf3
--- /dev/null
+++ b/WebCore/editing/RemoveCSSPropertyCommand.cpp
@@ -0,0 +1,66 @@
+/*
+ * 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 "RemoveCSSPropertyCommand.h"
+
+#include "CSSMutableStyleDeclaration.h"
+#include <wtf/Assertions.h>
+
+namespace WebCore {
+
+RemoveCSSPropertyCommand::RemoveCSSPropertyCommand(Document* document, CSSStyleDeclaration* decl, int property)
+ : EditCommand(document)
+ , m_decl(decl->makeMutable())
+ , m_property(property)
+ , m_important(false)
+{
+ ASSERT(m_decl);
+}
+
+RemoveCSSPropertyCommand::~RemoveCSSPropertyCommand()
+{
+}
+
+void RemoveCSSPropertyCommand::doApply()
+{
+ ASSERT(m_decl);
+
+ m_oldValue = m_decl->getPropertyValue(m_property);
+ ASSERT(!m_oldValue.isNull());
+
+ m_important = m_decl->getPropertyPriority(m_property);
+ m_decl->removeProperty(m_property);
+}
+
+void RemoveCSSPropertyCommand::doUnapply()
+{
+ ASSERT(m_decl);
+ ASSERT(!m_oldValue.isNull());
+
+ m_decl->setProperty(m_property, m_oldValue, m_important);
+}
+
+} // namespace WebCore
diff --git a/WebCore/editing/RemoveCSSPropertyCommand.h b/WebCore/editing/RemoveCSSPropertyCommand.h
new file mode 100644
index 0000000..6c97140
--- /dev/null
+++ b/WebCore/editing/RemoveCSSPropertyCommand.h
@@ -0,0 +1,55 @@
+/*
+ * 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.
+ */
+
+#ifndef RemoveCSSPropertyCommand_h
+#define RemoveCSSPropertyCommand_h
+
+#include "EditCommand.h"
+
+namespace WebCore {
+
+class CSSStyleDeclaration;
+
+class RemoveCSSPropertyCommand : public EditCommand {
+public:
+ RemoveCSSPropertyCommand(Document*, CSSStyleDeclaration*, int property);
+ virtual ~RemoveCSSPropertyCommand();
+
+ virtual void doApply();
+ virtual void doUnapply();
+
+ CSSMutableStyleDeclaration* styleDeclaration() const { return m_decl.get(); }
+ int property() const { return m_property; }
+
+private:
+ RefPtr<CSSMutableStyleDeclaration> m_decl;
+ int m_property;
+ String m_oldValue;
+ bool m_important;
+};
+
+} // namespace WebCore
+
+#endif // __remove_css_property_command_h__
diff --git a/WebCore/editing/RemoveFormatCommand.cpp b/WebCore/editing/RemoveFormatCommand.cpp
new file mode 100644
index 0000000..57f526a
--- /dev/null
+++ b/WebCore/editing/RemoveFormatCommand.cpp
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2007 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 "RemoveFormatCommand.h"
+
+#include "CSSComputedStyleDeclaration.h"
+#include "Editor.h"
+#include "Frame.h"
+#include "HTMLNames.h"
+#include "Selection.h"
+#include "SelectionController.h"
+#include "TextIterator.h"
+#include "TypingCommand.h"
+
+namespace WebCore {
+
+using namespace HTMLNames;
+
+RemoveFormatCommand::RemoveFormatCommand(Document* document)
+ : CompositeEditCommand(document)
+{
+}
+
+void RemoveFormatCommand::doApply()
+{
+ Frame* frame = document()->frame();
+
+ // Make a plain text string from the selection to remove formatting like tables and lists.
+ String string = plainText(frame->selectionController()->selection().toRange().get());
+
+ // 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->selectionController()->rootEditableElement();
+ RefPtr<CSSComputedStyleDeclaration> computedStyle = new CSSComputedStyleDeclaration(root);
+ RefPtr<CSSMutableStyleDeclaration> defaultStyle = computedStyle->copyInheritableProperties();
+
+ // Delete the selected content.
+ // FIXME: We should be able to leave this to insertText, but its delete operation
+ // doesn't preserve the style we're about to set.
+ deleteSelection();
+
+ // Delete doesn't remove fully selected lists.
+ while (breakOutOfEmptyListItem())
+ ;
+
+ // Normally, deleting a fully selected anchor and then inserting text will re-create
+ // the removed anchor, but we don't want that behavior here.
+ frame->editor()->setRemovedAnchor(0);
+ // Insert the content with the default style.
+ frame->setTypingStyle(defaultStyle.get());
+
+ inputText(string, true);
+}
+
+}
diff --git a/WebCore/editing/RemoveFormatCommand.h b/WebCore/editing/RemoveFormatCommand.h
new file mode 100644
index 0000000..a6df271
--- /dev/null
+++ b/WebCore/editing/RemoveFormatCommand.h
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2007 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 RemoveFormatCommand_h
+#define RemoveFormatCommand_h
+
+#include "CompositeEditCommand.h"
+
+namespace WebCore {
+
+class RemoveFormatCommand : public CompositeEditCommand {
+public:
+ RemoveFormatCommand(Document*);
+ virtual void doApply();
+ virtual EditAction editingAction() const { return EditActionUnspecified; }
+};
+
+} // namespace WebCore
+
+#endif // RemoveFormatCommand_h
diff --git a/WebCore/editing/RemoveNodeAttributeCommand.cpp b/WebCore/editing/RemoveNodeAttributeCommand.cpp
new file mode 100644
index 0000000..ab3d4bd
--- /dev/null
+++ b/WebCore/editing/RemoveNodeAttributeCommand.cpp
@@ -0,0 +1,62 @@
+/*
+ * 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 "RemoveNodeAttributeCommand.h"
+#include "Element.h"
+
+#include <wtf/Assertions.h>
+
+namespace WebCore {
+
+RemoveNodeAttributeCommand::RemoveNodeAttributeCommand(Element* element, const QualifiedName& attribute)
+ : EditCommand(element->document()), m_element(element), m_attribute(attribute)
+{
+ ASSERT(m_element);
+}
+
+void RemoveNodeAttributeCommand::doApply()
+{
+ ASSERT(m_element);
+
+ m_oldValue = m_element->getAttribute(m_attribute);
+ ASSERT(!m_oldValue.isNull());
+
+ ExceptionCode ec = 0;
+ m_element->removeAttribute(m_attribute, ec);
+ ASSERT(ec == 0);
+}
+
+void RemoveNodeAttributeCommand::doUnapply()
+{
+ ASSERT(m_element);
+ ASSERT(!m_oldValue.isNull());
+
+ ExceptionCode ec = 0;
+ m_element->setAttribute(m_attribute, m_oldValue.impl(), ec);
+ ASSERT(ec == 0);
+}
+
+} // namespace WebCore
diff --git a/WebCore/editing/RemoveNodeAttributeCommand.h b/WebCore/editing/RemoveNodeAttributeCommand.h
new file mode 100644
index 0000000..4d1c7a6
--- /dev/null
+++ b/WebCore/editing/RemoveNodeAttributeCommand.h
@@ -0,0 +1,52 @@
+/*
+ * 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.
+ */
+
+#ifndef RemoveNodeAttributeCommand_h
+#define RemoveNodeAttributeCommand_h
+
+#include "EditCommand.h"
+#include "QualifiedName.h"
+
+namespace WebCore {
+
+class RemoveNodeAttributeCommand : public EditCommand {
+public:
+ RemoveNodeAttributeCommand(Element*, const QualifiedName& attribute);
+
+ virtual void doApply();
+ virtual void doUnapply();
+
+ Element* element() const { return m_element.get(); }
+ const QualifiedName& attribute() const { return m_attribute; }
+
+private:
+ RefPtr<Element> m_element;
+ QualifiedName m_attribute;
+ String m_oldValue;
+};
+
+} // namespace WebCore
+
+#endif // RemoveNodeAttributeCommand_h
diff --git a/WebCore/editing/RemoveNodeCommand.cpp b/WebCore/editing/RemoveNodeCommand.cpp
new file mode 100644
index 0000000..6daedd5
--- /dev/null
+++ b/WebCore/editing/RemoveNodeCommand.cpp
@@ -0,0 +1,63 @@
+/*
+ * 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 "RemoveNodeCommand.h"
+
+#include "Node.h"
+#include <wtf/Assertions.h>
+
+namespace WebCore {
+
+RemoveNodeCommand::RemoveNodeCommand(Node* removeChild)
+ : EditCommand(removeChild->document())
+ , m_removeChild(removeChild)
+ , m_parent(m_removeChild->parentNode())
+ , m_refChild(m_removeChild->nextSibling())
+{
+ ASSERT(m_parent);
+}
+
+void RemoveNodeCommand::doApply()
+{
+ ASSERT(m_parent);
+ ASSERT(m_removeChild);
+
+ ExceptionCode ec = 0;
+ m_parent->removeChild(m_removeChild.get(), ec);
+ ASSERT(ec == 0);
+}
+
+void RemoveNodeCommand::doUnapply()
+{
+ ASSERT(m_parent);
+ ASSERT(m_removeChild);
+
+ ExceptionCode ec = 0;
+ m_parent->insertBefore(m_removeChild.get(), m_refChild.get(), ec);
+ ASSERT(ec == 0);
+}
+
+}
diff --git a/WebCore/editing/RemoveNodeCommand.h b/WebCore/editing/RemoveNodeCommand.h
new file mode 100644
index 0000000..0ef0546
--- /dev/null
+++ b/WebCore/editing/RemoveNodeCommand.h
@@ -0,0 +1,50 @@
+/*
+ * 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.
+ */
+
+#ifndef RemoveNodeCommand_h
+#define RemoveNodeCommand_h
+
+#include "EditCommand.h"
+
+namespace WebCore {
+
+class RemoveNodeCommand : public EditCommand {
+public:
+ RemoveNodeCommand(Node*);
+
+ virtual void doApply();
+ virtual void doUnapply();
+
+ Node* node() const { return m_removeChild.get(); }
+
+private:
+ RefPtr<Node> m_removeChild;
+ RefPtr<Node> m_parent;
+ RefPtr<Node> m_refChild;
+};
+
+} // namespace WebCore
+
+#endif // RemoveNodeCommand_h
diff --git a/WebCore/editing/RemoveNodePreservingChildrenCommand.cpp b/WebCore/editing/RemoveNodePreservingChildrenCommand.cpp
new file mode 100644
index 0000000..6695a01
--- /dev/null
+++ b/WebCore/editing/RemoveNodePreservingChildrenCommand.cpp
@@ -0,0 +1,49 @@
+/*
+ * 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 "RemoveNodePreservingChildrenCommand.h"
+
+#include "Node.h"
+#include <wtf/Assertions.h>
+
+namespace WebCore {
+
+RemoveNodePreservingChildrenCommand::RemoveNodePreservingChildrenCommand(Node* node)
+ : CompositeEditCommand(node->document()), m_node(node)
+{
+ ASSERT(m_node);
+}
+
+void RemoveNodePreservingChildrenCommand::doApply()
+{
+ while (Node* curr = node()->firstChild()) {
+ removeNode(curr);
+ insertNodeBefore(curr, node());
+ }
+ removeNode(node());
+}
+
+}
diff --git a/WebCore/editing/RemoveNodePreservingChildrenCommand.h b/WebCore/editing/RemoveNodePreservingChildrenCommand.h
new file mode 100644
index 0000000..4282141
--- /dev/null
+++ b/WebCore/editing/RemoveNodePreservingChildrenCommand.h
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+
+#ifndef RemoveNodePreservingChildrenCommand_h
+#define RemoveNodePreservingChildrenCommand_h
+
+#include "CompositeEditCommand.h"
+
+namespace WebCore {
+
+class RemoveNodePreservingChildrenCommand : public CompositeEditCommand {
+public:
+ RemoveNodePreservingChildrenCommand(Node*);
+
+ virtual void doApply();
+
+ Node* node() const { return m_node.get(); }
+
+private:
+ RefPtr<Node> m_node;
+};
+
+} // namespace WebCore
+
+#endif // RemoveNodePreservingChildrenCommand_h
diff --git a/WebCore/editing/ReplaceSelectionCommand.cpp b/WebCore/editing/ReplaceSelectionCommand.cpp
new file mode 100644
index 0000000..1f66de4
--- /dev/null
+++ b/WebCore/editing/ReplaceSelectionCommand.cpp
@@ -0,0 +1,948 @@
+/*
+ * 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 "ReplaceSelectionCommand.h"
+
+#include "ApplyStyleCommand.h"
+#include "BeforeTextInsertedEvent.h"
+#include "CSSComputedStyleDeclaration.h"
+#include "CSSProperty.h"
+#include "CSSPropertyNames.h"
+#include "CSSValueKeywords.h"
+#include "Document.h"
+#include "DocumentFragment.h"
+#include "EditingText.h"
+#include "EventNames.h"
+#include "Element.h"
+#include "Frame.h"
+#include "HTMLElement.h"
+#include "HTMLInterchange.h"
+#include "HTMLInputElement.h"
+#include "HTMLNames.h"
+#include "SelectionController.h"
+#include "SmartReplace.h"
+#include "TextIterator.h"
+#include "htmlediting.h"
+#include "markup.h"
+#include "visible_units.h"
+
+namespace WebCore {
+
+using namespace EventNames;
+using namespace HTMLNames;
+
+static bool isInterchangeNewlineNode(const Node *node)
+{
+ static String interchangeNewlineClassString(AppleInterchangeNewline);
+ return node && node->hasTagName(brTag) &&
+ static_cast<const Element *>(node)->getAttribute(classAttr) == interchangeNewlineClassString;
+}
+
+static bool isInterchangeConvertedSpaceSpan(const Node *node)
+{
+ static String convertedSpaceSpanClassString(AppleConvertedSpace);
+ return node->isHTMLElement() &&
+ static_cast<const HTMLElement *>(node)->getAttribute(classAttr) == convertedSpaceSpanClassString;
+}
+
+ReplacementFragment::ReplacementFragment(Document* document, DocumentFragment* fragment, bool matchStyle, const Selection& 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->getHTMLEventListener(webkitBeforeTextInsertedEvent) &&
+ // FIXME: Remove these checks once textareas and textfields actually register an event handler.
+ !(shadowAncestorNode && shadowAncestorNode->renderer() && shadowAncestorNode->renderer()->isTextField()) &&
+ !(shadowAncestorNode && shadowAncestorNode->renderer() && shadowAncestorNode->renderer()->isTextArea()) &&
+ editableRoot->isContentRichlyEditable()) {
+ removeInterchangeNodes(m_fragment->firstChild());
+ return;
+ }
+
+ Node* styleNode = selection.base().node();
+ RefPtr<Node> holder = insertFragmentForTestRendering(styleNode);
+
+ RefPtr<Range> range = Selection::selectionFromContentsOfNode(holder.get()).toRange();
+ String text = plainText(range.get());
+ // Give the root a chance to change the text.
+ RefPtr<BeforeTextInsertedEvent> evt = new BeforeTextInsertedEvent(text);
+ ExceptionCode ec = 0;
+ editableRoot->dispatchEvent(evt, ec, true);
+ ASSERT(ec == 0);
+ if (text != evt->text() || !editableRoot->isContentRichlyEditable()) {
+ restoreTestRenderingNodesToFragment(holder.get());
+ removeNode(holder);
+
+ m_fragment = createFragmentFromText(selection.toRange().get(), evt->text());
+ if (!m_fragment->firstChild())
+ return;
+ holder = insertFragmentForTestRendering(styleNode);
+ }
+
+ removeInterchangeNodes(holder->firstChild());
+
+ 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.get(), node);
+ }
+ removeNode(node);
+}
+
+void ReplacementFragment::removeNode(PassRefPtr<Node> node)
+{
+ if (!node)
+ return;
+
+ Node *parent = node->parentNode();
+ if (!parent)
+ return;
+
+ ExceptionCode ec = 0;
+ parent->removeChild(node.get(), ec);
+ ASSERT(ec == 0);
+}
+
+void ReplacementFragment::insertNodeBefore(Node *node, Node *refNode)
+{
+ if (!node || !refNode)
+ return;
+
+ Node *parent = refNode->parentNode();
+ if (!parent)
+ return;
+
+ ExceptionCode ec = 0;
+ parent->insertBefore(node, refNode, ec);
+ ASSERT(ec == 0);
+}
+
+PassRefPtr<Node> ReplacementFragment::insertFragmentForTestRendering(Node* context)
+{
+ Node* body = m_document->body();
+ if (!body)
+ return 0;
+
+ RefPtr<StyledElement> holder = static_pointer_cast<StyledElement>(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 = new CSSComputedStyleDeclaration(static_cast<Element*>(n));
+ CSSStyleDeclaration* style = holder->style();
+ style->setProperty(CSS_PROP_WHITE_SPACE, conFontStyle->getPropertyValue(CSS_PROP_WHITE_SPACE), false, ec);
+ ASSERT(ec == 0);
+ style->setProperty(CSS_PROP__WEBKIT_USER_SELECT, conFontStyle->getPropertyValue(CSS_PROP__WEBKIT_USER_SELECT), 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(Node *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* startNode)
+{
+ Node* node = startNode;
+ Node* newlineAtStartNode = 0;
+ Node* newlineAtEndNode = 0;
+ while (node) {
+ Node *next = node->traverseNextNode();
+ if (isInterchangeNewlineNode(node)) {
+ if (next || node == startNode) {
+ m_hasInterchangeNewlineAtStart = true;
+ newlineAtStartNode = node;
+ }
+ else {
+ m_hasInterchangeNewlineAtEnd = true;
+ newlineAtEndNode = node;
+ }
+ }
+ else if (isInterchangeConvertedSpaceSpan(node)) {
+ RefPtr<Node> n = 0;
+ while ((n = node->firstChild())) {
+ removeNode(n);
+ insertNodeBefore(n.get(), node);
+ }
+ removeNode(node);
+ if (n)
+ next = n->traverseNextNode();
+ }
+ node = next;
+ }
+
+ if (newlineAtStartNode)
+ removeNode(newlineAtStartNode);
+ if (newlineAtEndNode)
+ removeNode(newlineAtEndNode);
+}
+
+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)
+{
+}
+
+bool ReplaceSelectionCommand::shouldMergeStart(bool selectionStartWasStartOfParagraph, bool fragmentHasInterchangeNewlineAtStart)
+{
+ VisiblePosition startOfInsertedContent(positionAtStartOfInsertedContent());
+ VisiblePosition prev = startOfInsertedContent.previous(true);
+ if (prev.isNull())
+ return false;
+
+ 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;
+}
+
+bool ReplaceSelectionCommand::shouldMerge(const VisiblePosition& from, const VisiblePosition& to)
+{
+ if (from.isNull() || to.isNull())
+ return false;
+
+ Node* fromNode = from.deepEquivalent().node();
+ Node* toNode = to.deepEquivalent().node();
+ Node* fromNodeBlock = enclosingBlock(fromNode);
+ return !enclosingNodeOfType(from.deepEquivalent(), &isMailPasteAsQuotationNode) &&
+ fromNodeBlock && (!fromNodeBlock->hasTagName(blockquoteTag) || isMailBlockquote(fromNodeBlock)) &&
+ enclosingListChild(fromNode) == enclosingListChild(toNode) &&
+ enclosingTableCell(from.deepEquivalent()) == enclosingTableCell(from.deepEquivalent()) &&
+ // 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(fromNode) && !isBlock(toNode);
+}
+
+// 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(CSS_PROP_DISPLAY, CSS_VAL_INLINE);
+ if (e->renderer() && e->renderer()->style()->floating() != FNONE)
+ e->getInlineStyleDecl()->setProperty(CSS_PROP_FLOAT, CSS_VAL_NONE);
+ }
+ 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)) {
+ RefPtr<Node> previous = m_firstNodeInserted == m_lastLeafInserted ? 0 : 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()) {
+ RefPtr<Node> next = m_firstNodeInserted == m_lastLeafInserted ? 0 : 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();
+ Node* enclosingSelect = enclosingNodeWithTag(Position(lastNode, 0), selectTag);
+ if (enclosingSelect)
+ lastNode = enclosingSelect;
+ return VisiblePosition(Position(lastNode, maxDeepOffset(lastNode)));
+}
+
+VisiblePosition ReplaceSelectionCommand::positionAtStartOfInsertedContent()
+{
+ // Return the inserted content's first VisiblePosition.
+ return VisiblePosition(nextCandidate(positionBeforeNode(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 this case is more complicated (see handleStyleSpans) and doesn't receive the optimization.
+ if (isMailPasteAsQuotationNode(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<CSSMutableStyleDeclaration> styleAtInsertionPos = rangeCompliantEquivalent(insertionPos).computedStyle()->copyInheritableProperties();
+ String styleText = styleAtInsertionPos->cssText();
+
+ 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<CSSMutableStyleDeclaration> sourceDocumentStyle = static_cast<HTMLElement*>(sourceDocumentStyleSpan)->getInlineStyleDecl()->copy();
+ Node* context = sourceDocumentStyleSpan->parentNode();
+
+ // If Mail wraps the fragment with a Paste as Quotation blockquote, styles from that element are
+ // allowed to override those from the source document, see <rdar://problem/4930986>.
+ if (isMailPasteAsQuotationNode(context)) {
+ RefPtr<CSSMutableStyleDeclaration> blockquoteStyle = computedStyle(context)->copyInheritableProperties();
+ RefPtr<CSSMutableStyleDeclaration> parentStyle = computedStyle(context->parentNode())->copyInheritableProperties();
+ parentStyle->diff(blockquoteStyle.get());
+
+ DeprecatedValueListConstIterator<CSSProperty> end;
+ for (DeprecatedValueListConstIterator<CSSProperty> it = blockquoteStyle->valuesIterator(); it != end; ++it) {
+ const CSSProperty& property = *it;
+ sourceDocumentStyle->removeProperty(property.id());
+ }
+
+ context = context->parentNode();
+ }
+
+ RefPtr<CSSMutableStyleDeclaration> contextStyle = computedStyle(context)->copyInheritableProperties();
+ String contextStyleText = contextStyle->cssText();
+ String sourceDocumentStyleText = sourceDocumentStyle->cssText();
+ contextStyle->diff(sourceDocumentStyle.get());
+
+ // 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->length() == 0 && !copiedRangeStyleSpan) {
+ removeNodePreservingChildren(sourceDocumentStyleSpan);
+ return;
+ }
+
+ // There are non-redundant styles on sourceDocumentStyleSpan, but there is no
+ // copiedRangeStyleSpan. Clear the redundant styles from sourceDocumentStyleSpan
+ // and return.
+ if (sourceDocumentStyle->length() > 0 && !copiedRangeStyleSpan) {
+ setNodeAttribute(static_cast<Element*>(sourceDocumentStyleSpan), styleAttr, sourceDocumentStyle->cssText());
+ return;
+ }
+
+ RefPtr<CSSMutableStyleDeclaration> copiedRangeStyle = static_cast<HTMLElement*>(copiedRangeStyleSpan)->getInlineStyleDecl()->copy();
+
+ // We're going to put sourceDocumentStyleSpan's non-redundant styles onto copiedRangeStyleSpan,
+ // as long as they aren't overridden by ones on copiedRangeStyleSpan.
+ sourceDocumentStyle->merge(copiedRangeStyle.get(), true);
+ copiedRangeStyle = sourceDocumentStyle;
+
+ removeNodePreservingChildren(sourceDocumentStyleSpan);
+
+ // Remove redundant styles.
+ context = copiedRangeStyleSpan->parentNode();
+ contextStyle = computedStyle(context)->copyInheritableProperties();
+ contextStyle->diff(copiedRangeStyle.get());
+
+ // See the comments above about removing block properties.
+ copiedRangeStyle->removeBlockProperties();
+
+ // All the styles on copiedRangeStyleSpan are redundant, remove it.
+ if (copiedRangeStyle->length() == 0) {
+ removeNodePreservingChildren(copiedRangeStyleSpan);
+ return;
+ }
+
+ // Clear the redundant styles from the span's style attribute.
+ setNodeAttribute(static_cast<Element*>(copiedRangeStyleSpan), styleAttr, copiedRangeStyle->cssText());
+}
+
+void ReplaceSelectionCommand::doApply()
+{
+ Selection selection = endingSelection();
+ ASSERT(selection.isCaretOrRange());
+ ASSERT(selection.start().node());
+ if (selection.isNone() || !selection.start().node())
+ return;
+
+ bool selectionIsPlainText = !selection.isContentRichlyEditable();
+
+ Element* currentRoot = selection.rootEditableElement();
+ ReplacementFragment fragment(document(), m_documentFragment.get(), m_matchStyle, selection);
+
+ if (m_matchStyle)
+ m_insertionStyle = styleAtPosition(selection.start());
+
+ VisiblePosition visibleStart = selection.visibleStart();
+ VisiblePosition visibleEnd = selection.visibleEnd();
+
+ bool selectionEndWasEndOfParagraph = isEndOfParagraph(visibleEnd);
+ bool selectionStartWasStartOfParagraph = isStartOfParagraph(visibleStart);
+
+ Node* startBlock = enclosingBlock(visibleStart.deepEquivalent().node());
+
+ if (selectionStartWasStartOfParagraph && selectionEndWasEndOfParagraph ||
+ startBlock == currentRoot ||
+ startBlock && startBlock->renderer() && startBlock->renderer()->isListItem() ||
+ selectionIsPlainText)
+ m_preventNesting = false;
+
+ Position insertionPos = selection.start();
+
+ 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).
+ bool mergeBlocksAfterDelete = 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>
+ if (m_preventNesting && !isEndOfParagraph(visibleStart) && !isStartOfParagraph(visibleStart)) {
+ insertParagraphSeparator();
+ setEndingSelection(endingSelection().visibleStart().previous());
+ }
+ insertionPos = endingSelection().start();
+ }
+
+ // Inserting content could cause whitespace to collapse, e.g. inserting <div>foo</div> into hello^ world.
+ prepareWhitespaceAtPositionForSplit(insertionPos);
+
+ // 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 (m_preventNesting && startBlock) {
+ ASSERT(startBlock != currentRoot);
+ VisiblePosition visibleInsertionPos(insertionPos);
+ if (isEndOfBlock(visibleInsertionPos) && !(isStartOfBlock(visibleInsertionPos) && fragment.hasInterchangeNewlineAtEnd()))
+ insertionPos = positionAfterNode(startBlock);
+ else if (isStartOfBlock(visibleInsertionPos))
+ insertionPos = positionBeforeNode(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);
+
+ Frame *frame = document()->frame();
+
+ // FIXME: Improve typing style.
+ // See this bug: <rdar://problem/3769899> Implementation of typing style needs improvement
+ frame->clearTypingStyle();
+ setTypingStyle(0);
+
+ bool handledStyleSpans = handleStyleSpansBeforeInsertion(fragment, insertionPos);
+
+ // 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);
+ insertNodeAtAndUpdateNodesInserted(refNode.get(), insertionPos);
+
+ while (node) {
+ Node* next = node->nextSibling();
+ fragment.removeNode(node);
+ insertNodeAfterAndUpdateNodesInserted(node.get(), refNode.get());
+ refNode = node;
+ node = next;
+ }
+
+ removeUnrenderedTextNodesAtEnds();
+
+ negateStyleRulesThatAffectAppearance();
+
+ if (!handledStyleSpans)
+ handleStyleSpans();
+
+ if (!m_firstNodeInserted)
+ 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.offset() < startBlock->nodeIndex() && !isStartOfParagraph(startOfInsertedContent))
+ insertNodeAt(createBreakElement(document()).get(), startOfInsertedContent.deepEquivalent());
+
+ Position lastPositionToSelect;
+
+ bool interchangeNewlineAtEnd = fragment.hasInterchangeNewlineAtEnd();
+
+ if (shouldRemoveEndBR(endBR, originalVisPosBeforeEndBR))
+ removeNodeAndPruneAncestors(endBR);
+
+ if (shouldMergeStart(selectionStartWasStartOfParagraph, fragment.hasInterchangeNewlineAtStart())) {
+ // Bail to avoid infinite recursion.
+ if (m_movingParagraph) {
+ // setting display:inline does not work for td elements in quirks mode
+ ASSERT(m_firstNodeInserted->hasTagName(tdTag));
+ return;
+ }
+ VisiblePosition destination = startOfInsertedContent.previous();
+ VisiblePosition startOfParagraphToMove = startOfInsertedContent;
+
+ // 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);
+ // 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 if (shouldMergeEnd(selectionEndWasEndOfParagraph)) {
+ // 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 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.
+ bool mergeForward = !inSameParagraph(startOfInsertedContent, endOfInsertedContent) || isStartOfParagraph(startOfInsertedContent);
+
+ VisiblePosition destination = mergeForward ? endOfInsertedContent.next() : endOfInsertedContent;
+ VisiblePosition startOfParagraphToMove = mergeForward ? startOfParagraph(endOfInsertedContent) : endOfInsertedContent.next();
+
+ 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.
+ if (mergeForward) {
+ m_lastLeafInserted = destination.previous().deepEquivalent().node();
+ if (!m_firstNodeInserted->inDocument())
+ m_firstNodeInserted = endingSelection().visibleStart().deepEquivalent().node();
+ }
+ }
+
+ 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)->inputType() == HTMLInputElement::PASSWORD)
+ 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.get(), 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.get(), startNode);
+ // FIXME: Use positions to track the start/end of inserted content.
+ m_firstNodeInserted = node;
+ }
+ }
+ }
+
+ 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()->inStrictMode() && 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(Selection(start, end, SEL_DEFAULT_AFFINITY));
+ else
+ setEndingSelection(Selection(end, SEL_DEFAULT_AFFINITY));
+}
+
+EditAction ReplaceSelectionCommand::editingAction() const
+{
+ return m_editAction;
+}
+
+void ReplaceSelectionCommand::insertNodeAfterAndUpdateNodesInserted(Node *insertChild, Node *refChild)
+{
+ insertNodeAfter(insertChild, refChild);
+ updateNodesInserted(insertChild);
+}
+
+void ReplaceSelectionCommand::insertNodeAtAndUpdateNodesInserted(Node *insertChild, const Position& p)
+{
+ insertNodeAt(insertChild, p);
+ updateNodesInserted(insertChild);
+}
+
+void ReplaceSelectionCommand::insertNodeBeforeAndUpdateNodesInserted(Node *insertChild, Node *refChild)
+{
+ insertNodeBefore(insertChild, refChild);
+ updateNodesInserted(insertChild);
+}
+
+void ReplaceSelectionCommand::updateNodesInserted(Node *node)
+{
+ if (!node)
+ return;
+
+ if (!m_firstNodeInserted)
+ m_firstNodeInserted = node;
+
+ if (node == m_lastLeafInserted)
+ return;
+
+ m_lastLeafInserted = node->lastDescendant();
+}
+
+} // namespace WebCore
diff --git a/WebCore/editing/ReplaceSelectionCommand.h b/WebCore/editing/ReplaceSelectionCommand.h
new file mode 100644
index 0000000..d900c71
--- /dev/null
+++ b/WebCore/editing/ReplaceSelectionCommand.h
@@ -0,0 +1,118 @@
+/*
+ * 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.
+ */
+
+#ifndef ReplaceSelectionCommand_h
+#define ReplaceSelectionCommand_h
+
+#include "CompositeEditCommand.h"
+
+namespace WebCore {
+
+class DocumentFragment;
+
+enum EFragmentType { EmptyFragment, SingleTextNodeFragment, TreeFragment };
+
+// --- ReplacementFragment helper class
+
+class ReplacementFragment : Noncopyable {
+public:
+ ReplacementFragment(Document*, DocumentFragment*, bool matchStyle, const Selection&);
+
+ 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<Node> insertFragmentForTestRendering(Node* context);
+ void removeUnrenderedNodes(Node*);
+ void restoreTestRenderingNodesToFragment(Node*);
+ void removeInterchangeNodes(Node*);
+
+ void insertNodeBefore(Node* node, Node* refNode);
+
+ RefPtr<Document> m_document;
+ RefPtr<DocumentFragment> m_fragment;
+ bool m_matchStyle;
+ bool m_hasInterchangeNewlineAtStart;
+ bool m_hasInterchangeNewlineAtEnd;
+};
+
+class ReplaceSelectionCommand : public CompositeEditCommand {
+public:
+ ReplaceSelectionCommand(Document*, PassRefPtr<DocumentFragment>,
+ bool selectReplacement = true, bool smartReplace = false, bool matchStyle = false, bool preventNesting = true, bool movingParagraph = false,
+ EditAction = EditActionPaste);
+
+ virtual void doApply();
+ virtual EditAction editingAction() const;
+
+private:
+ void completeHTMLReplacement(const Position& lastPositionToSelect);
+
+ void insertNodeAfterAndUpdateNodesInserted(Node* insertChild, Node* refChild);
+ void insertNodeAtAndUpdateNodesInserted(Node*, const Position&);
+ void insertNodeBeforeAndUpdateNodesInserted(Node* insertChild, Node* refChild);
+
+ void updateNodesInserted(Node*);
+ bool shouldRemoveEndBR(Node*, const VisiblePosition&);
+
+ bool shouldMergeStart(bool, bool);
+ bool shouldMergeEnd(bool);
+ bool shouldMerge(const VisiblePosition&, const VisiblePosition&);
+
+ void removeUnrenderedTextNodesAtEnds();
+
+ void negateStyleRulesThatAffectAppearance();
+ void handleStyleSpans();
+ void handlePasteAsQuotationNode();
+
+ virtual void removeNodePreservingChildren(Node*);
+ virtual void removeNodeAndPruneAncestors(Node*);
+
+ VisiblePosition positionAtStartOfInsertedContent();
+ VisiblePosition positionAtEndOfInsertedContent();
+
+ RefPtr<Node> m_firstNodeInserted;
+ RefPtr<Node> m_lastLeafInserted;
+ RefPtr<CSSMutableStyleDeclaration> m_insertionStyle;
+ bool m_selectReplacement;
+ bool m_smartReplace;
+ bool m_matchStyle;
+ RefPtr<DocumentFragment> m_documentFragment;
+ bool m_preventNesting;
+ bool m_movingParagraph;
+ EditAction m_editAction;
+};
+
+} // namespace WebCore
+
+#endif // ReplaceSelectionCommand_h
diff --git a/WebCore/editing/Selection.cpp b/WebCore/editing/Selection.cpp
new file mode 100644
index 0000000..d29ac40
--- /dev/null
+++ b/WebCore/editing/Selection.cpp
@@ -0,0 +1,590 @@
+/*
+ * 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 "Selection.h"
+
+#include "CString.h"
+#include "Document.h"
+#include "Element.h"
+#include "htmlediting.h"
+#include "VisiblePosition.h"
+#include "visible_units.h"
+#include "Range.h"
+#include <wtf/Assertions.h>
+#include <stdio.h>
+
+namespace WebCore {
+
+Selection::Selection()
+ : m_affinity(DOWNSTREAM)
+ , m_granularity(CharacterGranularity)
+ , m_state(NONE)
+ , m_baseIsFirst(true)
+{
+}
+
+Selection::Selection(const Position& pos, EAffinity affinity)
+ : m_base(pos)
+ , m_extent(pos)
+ , m_affinity(affinity)
+ , m_granularity(CharacterGranularity)
+{
+ validate();
+}
+
+Selection::Selection(const Position& base, const Position& extent, EAffinity affinity)
+ : m_base(base)
+ , m_extent(extent)
+ , m_affinity(affinity)
+ , m_granularity(CharacterGranularity)
+{
+ validate();
+}
+
+Selection::Selection(const VisiblePosition& pos)
+ : m_base(pos.deepEquivalent())
+ , m_extent(pos.deepEquivalent())
+ , m_affinity(pos.affinity())
+ , m_granularity(CharacterGranularity)
+{
+ validate();
+}
+
+Selection::Selection(const VisiblePosition& base, const VisiblePosition& extent)
+ : m_base(base.deepEquivalent())
+ , m_extent(extent.deepEquivalent())
+ , m_affinity(base.affinity())
+ , m_granularity(CharacterGranularity)
+{
+ validate();
+}
+
+Selection::Selection(const Range* range, EAffinity affinity)
+ : m_base(range->startPosition())
+ , m_extent(range->endPosition())
+ , m_affinity(affinity)
+ , m_granularity(CharacterGranularity)
+{
+ validate();
+}
+
+Selection Selection::selectionFromContentsOfNode(Node* node)
+{
+ return Selection(Position(node, 0), Position(node, maxDeepOffset(node)), DOWNSTREAM);
+}
+
+void Selection::setBase(const Position& position)
+{
+ m_base = position;
+ validate();
+}
+
+void Selection::setBase(const VisiblePosition& visiblePosition)
+{
+ m_base = visiblePosition.deepEquivalent();
+ validate();
+}
+
+void Selection::setExtent(const Position& position)
+{
+ m_extent = position;
+ validate();
+}
+
+void Selection::setExtent(const VisiblePosition& visiblePosition)
+{
+ m_extent = visiblePosition.deepEquivalent();
+ validate();
+}
+
+PassRefPtr<Range> Selection::toRange() 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 (Range::compareBoundaryPoints(s.node(), s.offset(), e.node(), e.offset()) > 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);
+ }
+
+ ExceptionCode ec = 0;
+ RefPtr<Range> result(new Range(s.node()->document()));
+ result->setStart(s.node(), s.offset(), ec);
+ if (ec) {
+ LOG_ERROR("Exception setting Range start from Selection: %d", ec);
+ return 0;
+ }
+ result->setEnd(e.node(), e.offset(), ec);
+ if (ec) {
+ LOG_ERROR("Exception setting Range end from Selection: %d", ec);
+ return 0;
+ }
+ return result.release();
+}
+
+bool Selection::expandUsingGranularity(TextGranularity granularity)
+{
+ if (isNone())
+ return false;
+
+ m_granularity = granularity;
+ validate();
+ return true;
+}
+
+void Selection::validate()
+{
+ // 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;
+ }
+
+ if (m_baseIsFirst) {
+ m_start = m_base;
+ m_end = m_extent;
+ } else {
+ m_start = m_extent;
+ m_end = m_base;
+ }
+
+ // Expand the selection if requested.
+ switch (m_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)) {
+ // 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;
+
+ adjustForEditableContent();
+
+ // adjust the state
+ if (m_start.isNull()) {
+ ASSERT(m_end.isNull());
+ m_state = NONE;
+
+ // enforce downstream affinity if not caret, as affinity only
+ // makes sense for caret
+ m_affinity = DOWNSTREAM;
+ } else if (m_start == m_end || m_start.upstream() == m_end.upstream()) {
+ m_state = CARET;
+ } else {
+ m_state = RANGE;
+
+ // enforce downstream affinity if not caret, as affinity only
+ // makes sense for caret
+ m_affinity = DOWNSTREAM;
+
+ // "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.
+ m_start = m_start.downstream();
+ m_end = m_end.upstream();
+ }
+}
+
+// FIXME: This function breaks the invariant of this class.
+// But because we use Selection 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 Selection or create a new
+// class for editing to use that can manipulate selections that are not currently valid.
+void Selection::setWithoutValidation(const Position& base, const Position& extent)
+{
+ ASSERT(!base.isNull());
+ ASSERT(!extent.isNull());
+ ASSERT(base != extent);
+ ASSERT(m_affinity == DOWNSTREAM);
+ ASSERT(m_granularity == CharacterGranularity);
+ 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_state = RANGE;
+}
+
+void Selection::adjustForEditableContent()
+{
+ 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()) {
+ ASSERT_NOT_REACHED();
+ 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 = Position(shadowAncestor, maxDeepOffset(shadowAncestor));
+ while (p.isNotNull() && !(lowestEditableAncestor(p.node()) == baseEditableAncestor && !isEditablePosition(p))) {
+ Node* root = editableRootForPosition(p);
+ shadowAncestor = root ? root->shadowAncestorNode() : 0;
+ p = isAtomicNode(p.node()) ? positionBeforeNode(p.node()) : previousVisuallyDistinctCandidate(p);
+ if (p.isNull() && (shadowAncestor != root))
+ p = Position(shadowAncestor, maxDeepOffset(shadowAncestor));
+ }
+ VisiblePosition previous(p);
+
+ if (previous.isNull()) {
+ 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()) ? positionAfterNode(p.node()) : nextVisuallyDistinctCandidate(p);
+ if (p.isNull() && (shadowAncestor != root))
+ p = Position(shadowAncestor, 0);
+ }
+ VisiblePosition next(p);
+
+ if (next.isNull()) {
+ 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 Selection::isContentEditable() const
+{
+ return isEditablePosition(start());
+}
+
+bool Selection::isContentRichlyEditable() const
+{
+ return isRichlyEditablePosition(start());
+}
+
+Element* Selection::rootEditableElement() const
+{
+ return editableRootForPosition(start());
+}
+
+void Selection::debugPosition() const
+{
+ if (!m_start.node())
+ return;
+
+ fprintf(stderr, "Selection =================\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.offset());
+ } else {
+ Position pos = m_start;
+ fprintf(stderr, "start: %s %p:%d\n", pos.node()->nodeName().utf8().data(), pos.node(), pos.offset());
+ fprintf(stderr, "-----------------------------------\n");
+ pos = m_end;
+ fprintf(stderr, "end: %s %p:%d\n", pos.node()->nodeName().utf8().data(), pos.node(), pos.offset());
+ fprintf(stderr, "-----------------------------------\n");
+ }
+
+ fprintf(stderr, "================================\n");
+}
+
+#ifndef NDEBUG
+
+void Selection::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 Selection::showTreeForThis() const
+{
+ if (start().node()) {
+ start().node()->showTreeAndMark(start().node(), "S", end().node(), "E");
+ fprintf(stderr, "start offset: %d, end offset: %d\n", start().offset(), end().offset());
+ }
+}
+
+#endif
+
+} // namespace WebCore
+
+#ifndef NDEBUG
+
+void showTree(const WebCore::Selection& sel)
+{
+ sel.showTreeForThis();
+}
+
+void showTree(const WebCore::Selection* sel)
+{
+ if (sel)
+ sel->showTreeForThis();
+}
+
+#endif
diff --git a/WebCore/editing/Selection.h b/WebCore/editing/Selection.h
new file mode 100644
index 0000000..c8fcdf5
--- /dev/null
+++ b/WebCore/editing/Selection.h
@@ -0,0 +1,133 @@
+/*
+ * 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 Selection_h
+#define Selection_h
+
+#include "TextGranularity.h"
+#include "VisiblePosition.h"
+
+namespace WebCore {
+
+class Position;
+
+const EAffinity SEL_DEFAULT_AFFINITY = DOWNSTREAM;
+
+class Selection {
+public:
+ enum EState { NONE, CARET, RANGE };
+ enum EDirection { FORWARD, BACKWARD, RIGHT, LEFT };
+
+ Selection();
+
+ Selection(const Position&, EAffinity);
+ Selection(const Position&, const Position&, EAffinity);
+
+ Selection(const Range*, EAffinity = SEL_DEFAULT_AFFINITY);
+
+ Selection(const VisiblePosition&);
+ Selection(const VisiblePosition&, const VisiblePosition&);
+
+ static Selection selectionFromContentsOfNode(Node*);
+
+ EState state() const { return m_state; }
+
+ 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 state() == NONE; }
+ bool isCaret() const { return state() == CARET; }
+ bool isRange() const { return state() == RANGE; }
+ bool isCaretOrRange() const { return state() != NONE; }
+
+ bool isBaseFirst() const { return m_baseIsFirst; }
+
+ bool expandUsingGranularity(TextGranularity granularity);
+ TextGranularity granularity() const { return m_granularity; }
+
+ PassRefPtr<Range> toRange() const;
+
+ Element* rootEditableElement() const;
+ bool isContentEditable() const;
+ bool isContentRichlyEditable() 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();
+ void adjustForEditableContent();
+
+ Position m_base; // base position for the selection
+ Position m_extent; // extent position for the selection
+ Position m_start; // start position for the selection
+ Position m_end; // end position for the selection
+
+ EAffinity m_affinity; // the upstream/downstream affinity of the caret
+ TextGranularity m_granularity; // granularity of start/end selection
+
+ // these are cached, can be recalculated by validate()
+ EState m_state; // the state of the selection
+ bool m_baseIsFirst; // true if base is before the extent
+};
+
+inline bool operator==(const Selection& a, const Selection& b)
+{
+ return a.start() == b.start() && a.end() == b.end() && a.affinity() == b.affinity() && a.granularity() == b.granularity();
+}
+
+inline bool operator!=(const Selection& a, const Selection& b)
+{
+ return !(a == b);
+}
+
+} // namespace WebCore
+
+#ifndef NDEBUG
+// Outside the WebCore namespace for ease of invocation from gdb.
+void showTree(const WebCore::Selection&);
+void showTree(const WebCore::Selection*);
+#endif
+
+#endif // Selection_h
diff --git a/WebCore/editing/SelectionController.cpp b/WebCore/editing/SelectionController.cpp
new file mode 100644
index 0000000..cd6286a
--- /dev/null
+++ b/WebCore/editing/SelectionController.cpp
@@ -0,0 +1,1117 @@
+/*
+ * 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 "SelectionController.h"
+
+#include "CString.h"
+#include "DeleteSelectionCommand.h"
+#include "Document.h"
+#include "Editor.h"
+#include "Element.h"
+#include "EventHandler.h"
+#include "EventNames.h"
+#include "ExceptionCode.h"
+#include "FocusController.h"
+#include "Frame.h"
+#include "FrameTree.h"
+#include "FrameView.h"
+#include "GraphicsContext.h"
+#include "HTMLInputElement.h"
+#include "HTMLNames.h"
+#include "HitTestRequest.h"
+#include "HitTestResult.h"
+#include "Page.h"
+#include "Range.h"
+#include "RenderTheme.h"
+#include "RenderView.h"
+#include "TextIterator.h"
+#include "TypingCommand.h"
+#include "htmlediting.h"
+#include "visible_units.h"
+#include <stdio.h>
+
+#define EDIT_DEBUG 0
+
+namespace WebCore {
+
+using namespace EventNames;
+using namespace HTMLNames;
+
+const int NoXPosForVerticalArrowNavigation = INT_MIN;
+
+SelectionController::SelectionController(Frame* frame, bool isDragCaretController)
+ : m_needsLayout(true)
+ , m_lastChangeWasHorizontalExtension(false)
+ , m_frame(frame)
+ , m_isDragCaretController(isDragCaretController)
+ , m_isCaretBlinkingSuspended(false)
+ , m_xPosForVerticalArrowNavigation(NoXPosForVerticalArrowNavigation)
+ , m_focused(false)
+{
+}
+
+void SelectionController::moveTo(const VisiblePosition &pos, bool userTriggered)
+{
+ setSelection(Selection(pos.deepEquivalent(), pos.deepEquivalent(), pos.affinity()), true, true, userTriggered);
+}
+
+void SelectionController::moveTo(const VisiblePosition &base, const VisiblePosition &extent, bool userTriggered)
+{
+ setSelection(Selection(base.deepEquivalent(), extent.deepEquivalent(), base.affinity()), true, true, userTriggered);
+}
+
+void SelectionController::moveTo(const Position &pos, EAffinity affinity, bool userTriggered)
+{
+ setSelection(Selection(pos, affinity), true, true, userTriggered);
+}
+
+void SelectionController::moveTo(const Range *r, EAffinity affinity, bool userTriggered)
+{
+ setSelection(Selection(startPosition(r), endPosition(r), affinity), true, true, userTriggered);
+}
+
+void SelectionController::moveTo(const Position &base, const Position &extent, EAffinity affinity, bool userTriggered)
+{
+ setSelection(Selection(base, extent, affinity), true, true, userTriggered);
+}
+
+void SelectionController::setSelection(const Selection& s, bool closeTyping, bool clearTypingStyle, bool userTriggered)
+{
+ if (m_isDragCaretController) {
+ invalidateCaretRect();
+ m_sel = s;
+ m_needsLayout = true;
+ invalidateCaretRect();
+ return;
+ }
+ if (!m_frame) {
+ m_sel = s;
+ return;
+ }
+
+ if (s.base().node() && s.base().node()->document() != m_frame->document()) {
+ s.base().node()->document()->frame()->selectionController()->setSelection(s, closeTyping, clearTypingStyle, userTriggered);
+ return;
+ }
+
+ if (closeTyping)
+ TypingCommand::closeTyping(m_frame->editor()->lastEditCommand());
+
+ if (clearTypingStyle) {
+ m_frame->clearTypingStyle();
+ m_frame->editor()->setRemovedAnchor(0);
+ }
+
+ if (m_sel == s)
+ return;
+
+ Selection oldSelection = m_sel;
+
+ m_sel = s;
+
+ m_needsLayout = true;
+
+ if (!s.isNone())
+ m_frame->setFocusedNodeIfNeeded();
+
+ m_frame->selectionLayoutChanged();
+ // 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();
+ m_frame->notifyRendererOfSelectionChange(userTriggered);
+ m_frame->respondToChangedSelection(oldSelection, closeTyping);
+ if (userTriggered)
+ m_frame->revealCaret(RenderLayer::gAlignToEdgeIfNeeded);
+
+ 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;
+
+ bool baseRemoved = removingNodeRemovesPosition(node, m_sel.base());
+ bool extentRemoved = removingNodeRemovesPosition(node, m_sel.extent());
+ bool startRemoved = removingNodeRemovesPosition(node, m_sel.start());
+ bool endRemoved = removingNodeRemovesPosition(node, m_sel.end());
+
+ 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) {
+ if (m_sel.isBaseFirst()) {
+ m_sel.setBase(m_sel.start());
+ m_sel.setExtent(m_sel.end());
+ } else {
+ m_sel.setBase(m_sel.start());
+ m_sel.setExtent(m_sel.end());
+ }
+ // FIXME: This could be more efficient if we had an isNodeInRange function on Ranges.
+ } else if (Range::compareBoundaryPoints(m_sel.start(), Position(node, 0)) == -1 &&
+ Range::compareBoundaryPoints(m_sel.end(), Position(node, 0)) == 1) {
+ // 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_sel.start().node()->document();
+ document->updateRendering();
+ if (RenderView* view = static_cast<RenderView*>(document->renderer()))
+ view->clearSelection();
+ }
+
+ if (clearDOMTreeSelection)
+ setSelection(Selection(), false, false);
+}
+
+void SelectionController::willBeModified(EAlteration alter, EDirection direction)
+{
+ switch (alter) {
+ case MOVE:
+ m_lastChangeWasHorizontalExtension = false;
+ break;
+ case EXTEND:
+ if (!m_lastChangeWasHorizontalExtension) {
+ m_lastChangeWasHorizontalExtension = true;
+ Position start = m_sel.start();
+ Position end = m_sel.end();
+ switch (direction) {
+ // FIXME: right for bidi?
+ case RIGHT:
+ case FORWARD:
+ m_sel.setBase(start);
+ m_sel.setExtent(end);
+ break;
+ case LEFT:
+ case BACKWARD:
+ m_sel.setBase(end);
+ m_sel.setExtent(start);
+ break;
+ }
+ }
+ break;
+ }
+}
+
+VisiblePosition SelectionController::modifyExtendingRightForward(TextGranularity granularity)
+{
+ VisiblePosition pos(m_sel.extent(), m_sel.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(VisiblePosition(m_sel.end(), m_sel.affinity()));
+ break;
+ case LineBoundary:
+ pos = endOfLine(VisiblePosition(m_sel.end(), m_sel.affinity()));
+ break;
+ case ParagraphBoundary:
+ pos = endOfParagraph(VisiblePosition(m_sel.end(), m_sel.affinity()));
+ break;
+ case DocumentBoundary:
+ pos = VisiblePosition(m_sel.end(), m_sel.affinity());
+ if (isEditablePosition(pos.deepEquivalent()))
+ pos = endOfEditableContent(pos);
+ else
+ pos = endOfDocument(pos);
+ break;
+ }
+
+ return pos;
+}
+
+VisiblePosition SelectionController::modifyMovingRightForward(TextGranularity granularity)
+{
+ VisiblePosition pos;
+ // FIXME: Stay in editable content for the less common granularities.
+ switch (granularity) {
+ case CharacterGranularity:
+ if (isRange())
+ pos = VisiblePosition(m_sel.end(), m_sel.affinity());
+ else
+ pos = VisiblePosition(m_sel.extent(), m_sel.affinity()).next(true);
+ break;
+ case WordGranularity:
+ pos = nextWordPosition(VisiblePosition(m_sel.extent(), m_sel.affinity()));
+ break;
+ case SentenceGranularity:
+ pos = nextSentencePosition(VisiblePosition(m_sel.extent(), m_sel.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 = VisiblePosition(m_sel.end(), m_sel.affinity());
+ if (!isRange() || !isStartOfLine(pos))
+ pos = nextLinePosition(pos, xPosForVerticalArrowNavigation(START));
+ break;
+ }
+ case ParagraphGranularity:
+ pos = nextParagraphPosition(VisiblePosition(m_sel.end(), m_sel.affinity()), xPosForVerticalArrowNavigation(START));
+ break;
+ case SentenceBoundary:
+ pos = endOfSentence(VisiblePosition(m_sel.end(), m_sel.affinity()));
+ break;
+ case LineBoundary:
+ pos = endOfLine(VisiblePosition(m_sel.end(), m_sel.affinity()));
+ break;
+ case ParagraphBoundary:
+ pos = endOfParagraph(VisiblePosition(m_sel.end(), m_sel.affinity()));
+ break;
+ case DocumentBoundary:
+ pos = VisiblePosition(m_sel.end(), m_sel.affinity());
+ if (isEditablePosition(pos.deepEquivalent()))
+ pos = endOfEditableContent(pos);
+ else
+ pos = endOfDocument(pos);
+ break;
+
+ }
+ return pos;
+}
+
+VisiblePosition SelectionController::modifyExtendingLeftBackward(TextGranularity granularity)
+{
+ VisiblePosition pos(m_sel.extent(), m_sel.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(VisiblePosition(m_sel.start(), m_sel.affinity()));
+ break;
+ case LineBoundary:
+ pos = startOfLine(VisiblePosition(m_sel.start(), m_sel.affinity()));
+ break;
+ case ParagraphBoundary:
+ pos = startOfParagraph(VisiblePosition(m_sel.start(), m_sel.affinity()));
+ break;
+ case DocumentBoundary:
+ pos = VisiblePosition(m_sel.start(), m_sel.affinity());
+ if (isEditablePosition(pos.deepEquivalent()))
+ pos = startOfEditableContent(pos);
+ else
+ pos = startOfDocument(pos);
+ break;
+ }
+ return pos;
+}
+
+VisiblePosition SelectionController::modifyMovingLeftBackward(TextGranularity granularity)
+{
+ VisiblePosition pos;
+ switch (granularity) {
+ case CharacterGranularity:
+ if (isRange())
+ pos = VisiblePosition(m_sel.start(), m_sel.affinity());
+ else
+ pos = VisiblePosition(m_sel.extent(), m_sel.affinity()).previous(true);
+ break;
+ case WordGranularity:
+ pos = previousWordPosition(VisiblePosition(m_sel.extent(), m_sel.affinity()));
+ break;
+ case SentenceGranularity:
+ pos = previousSentencePosition(VisiblePosition(m_sel.extent(), m_sel.affinity()));
+ break;
+ case LineGranularity:
+ pos = previousLinePosition(VisiblePosition(m_sel.start(), m_sel.affinity()), xPosForVerticalArrowNavigation(START));
+ break;
+ case ParagraphGranularity:
+ pos = previousParagraphPosition(VisiblePosition(m_sel.start(), m_sel.affinity()), xPosForVerticalArrowNavigation(START));
+ break;
+ case SentenceBoundary:
+ pos = startOfSentence(VisiblePosition(m_sel.start(), m_sel.affinity()));
+ break;
+ case LineBoundary:
+ pos = startOfLine(VisiblePosition(m_sel.start(), m_sel.affinity()));
+ break;
+ case ParagraphBoundary:
+ pos = startOfParagraph(VisiblePosition(m_sel.start(), m_sel.affinity()));
+ break;
+ case DocumentBoundary:
+ pos = VisiblePosition(m_sel.start(), m_sel.affinity());
+ if (isEditablePosition(pos.deepEquivalent()))
+ pos = startOfEditableContent(pos);
+ else
+ pos = startOfDocument(pos);
+ break;
+ }
+ return pos;
+}
+
+bool SelectionController::modify(EAlteration alter, EDirection dir, TextGranularity granularity, bool userTriggered)
+{
+ if (userTriggered) {
+ SelectionController trialSelectionController;
+ trialSelectionController.setLastChangeWasHorizontalExtension(m_lastChangeWasHorizontalExtension);
+ trialSelectionController.setSelection(m_sel);
+ trialSelectionController.modify(alter, dir, granularity, false);
+
+ bool change = m_frame->shouldChangeSelection(trialSelectionController.selection());
+ if (!change)
+ return false;
+ }
+
+ if (m_frame)
+ m_frame->setSelectionGranularity(granularity);
+
+ willBeModified(alter, dir);
+
+ VisiblePosition pos;
+ switch (dir) {
+ // EDIT FIXME: These need to handle bidi
+ case RIGHT:
+ case FORWARD:
+ if (alter == EXTEND)
+ pos = modifyExtendingRightForward(granularity);
+ else
+ pos = modifyMovingRightForward(granularity);
+ break;
+ case LEFT:
+ case BACKWARD:
+ if (alter == EXTEND)
+ pos = modifyExtendingLeftBackward(granularity);
+ else
+ pos = modifyMovingLeftBackward(granularity);
+ break;
+ }
+
+ if (pos.isNull())
+ 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 MOVE:
+ moveTo(pos, userTriggered);
+ break;
+ case EXTEND:
+ setExtent(pos, userTriggered);
+ break;
+ }
+
+ if (granularity == LineGranularity || granularity == ParagraphGranularity)
+ m_xPosForVerticalArrowNavigation = x;
+
+ if (userTriggered) {
+ // User modified selection change also sets the granularity back to character.
+ // NOTE: The one exception is that we need to keep word granularity to
+ // preserve smart delete behavior when extending by word (e.g. double-click),
+ // then shift-option-right arrow, then delete needs to smart delete, per TextEdit.
+ if (!(alter == EXTEND && granularity == WordGranularity && m_frame->selectionGranularity() == WordGranularity))
+ m_frame->setSelectionGranularity(CharacterGranularity);
+ }
+
+ setNeedsLayout();
+
+ return true;
+}
+
+// FIXME: Maybe baseline would be better?
+static bool caretY(const VisiblePosition &c, int &y)
+{
+ Position p = c.deepEquivalent();
+ Node *n = p.node();
+ if (!n)
+ return false;
+ RenderObject *r = p.node()->renderer();
+ if (!r)
+ return false;
+ IntRect rect = r->caretRect(p.offset());
+ if (rect.isEmpty())
+ return false;
+ y = rect.y() + rect.height() / 2;
+ return true;
+}
+
+bool SelectionController::modify(EAlteration alter, int verticalDistance, bool userTriggered)
+{
+ if (verticalDistance == 0)
+ return false;
+
+ if (userTriggered) {
+ SelectionController trialSelectionController;
+ trialSelectionController.setSelection(m_sel);
+ trialSelectionController.modify(alter, verticalDistance, false);
+
+ bool change = m_frame->shouldChangeSelection(trialSelectionController.selection());
+ if (!change)
+ return false;
+ }
+
+ bool up = verticalDistance < 0;
+ if (up)
+ verticalDistance = -verticalDistance;
+
+ willBeModified(alter, up ? BACKWARD : FORWARD);
+
+ VisiblePosition pos;
+ int xPos = 0;
+ switch (alter) {
+ case MOVE:
+ pos = VisiblePosition(up ? m_sel.start() : m_sel.end(), m_sel.affinity());
+ xPos = xPosForVerticalArrowNavigation(up ? START : END);
+ m_sel.setAffinity(up ? UPSTREAM : DOWNSTREAM);
+ break;
+ case EXTEND:
+ pos = VisiblePosition(m_sel.extent(), m_sel.affinity());
+ xPos = xPosForVerticalArrowNavigation(EXTENT);
+ m_sel.setAffinity(DOWNSTREAM);
+ break;
+ }
+
+ int startY;
+ if (!caretY(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 (!caretY(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 MOVE:
+ moveTo(result, userTriggered);
+ break;
+ case EXTEND:
+ setExtent(result, userTriggered);
+ break;
+ }
+
+ if (userTriggered)
+ m_frame->setSelectionGranularity(CharacterGranularity);
+
+ return true;
+}
+
+bool SelectionController::expandUsingGranularity(TextGranularity granularity)
+{
+ if (isNone())
+ return false;
+
+ m_sel.expandUsingGranularity(granularity);
+ m_needsLayout = true;
+ return true;
+}
+
+int SelectionController::xPosForVerticalArrowNavigation(EPositionType type)
+{
+ int x = 0;
+
+ if (isNone())
+ return x;
+
+ Position pos;
+ switch (type) {
+ case START:
+ pos = m_sel.start();
+ break;
+ case END:
+ pos = m_sel.end();
+ break;
+ case BASE:
+ pos = m_sel.base();
+ break;
+ case EXTENT:
+ pos = m_sel.extent();
+ break;
+ }
+
+ Frame *frame = pos.node()->document()->frame();
+ if (!frame)
+ return x;
+
+ if (m_xPosForVerticalArrowNavigation == NoXPosForVerticalArrowNavigation) {
+ pos = VisiblePosition(pos, m_sel.affinity()).deepEquivalent();
+ // 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 = pos.isNotNull() ? pos.node()->renderer()->caretRect(pos.offset(), m_sel.affinity()).x() : 0;
+ m_xPosForVerticalArrowNavigation = x;
+ }
+ else
+ x = m_xPosForVerticalArrowNavigation;
+
+ return x;
+}
+
+void SelectionController::clear()
+{
+ setSelection(Selection());
+}
+
+void SelectionController::setBase(const VisiblePosition &pos, bool userTriggered)
+{
+ setSelection(Selection(pos.deepEquivalent(), m_sel.extent(), pos.affinity()), true, true, userTriggered);
+}
+
+void SelectionController::setExtent(const VisiblePosition &pos, bool userTriggered)
+{
+ setSelection(Selection(m_sel.base(), pos.deepEquivalent(), pos.affinity()), true, true, userTriggered);
+}
+
+void SelectionController::setBase(const Position &pos, EAffinity affinity, bool userTriggered)
+{
+ setSelection(Selection(pos, m_sel.extent(), affinity), true, true, userTriggered);
+}
+
+void SelectionController::setExtent(const Position &pos, EAffinity affinity, bool userTriggered)
+{
+ setSelection(Selection(m_sel.base(), pos, affinity), true, true, userTriggered);
+}
+
+void SelectionController::setNeedsLayout(bool flag)
+{
+ m_needsLayout = flag;
+}
+
+void SelectionController::layout()
+{
+ if (isNone() || !m_sel.start().node()->inDocument() || !m_sel.end().node()->inDocument()) {
+ m_caretRect = IntRect();
+ m_caretPositionOnLayout = IntPoint();
+ return;
+ }
+
+ m_sel.start().node()->document()->updateRendering();
+
+ m_caretRect = IntRect();
+ m_caretPositionOnLayout = IntPoint();
+
+ if (isCaret()) {
+ Position pos = m_sel.start();
+ pos = VisiblePosition(m_sel.start(), m_sel.affinity()).deepEquivalent();
+ if (pos.isNotNull()) {
+ ASSERT(pos.node()->renderer());
+ m_caretRect = pos.node()->renderer()->caretRect(pos.offset(), m_sel.affinity());
+
+ int x, y;
+ pos.node()->renderer()->absolutePositionForContent(x, y);
+ m_caretPositionOnLayout = IntPoint(x, y);
+ }
+ }
+
+ m_needsLayout = false;
+}
+
+IntRect SelectionController::caretRect() const
+{
+ if (m_needsLayout)
+ const_cast<SelectionController *>(this)->layout();
+
+ IntRect caret = m_caretRect;
+
+ if (m_sel.start().node() && m_sel.start().node()->renderer()) {
+ int x, y;
+ m_sel.start().node()->renderer()->absolutePositionForContent(x, y);
+ caret.move(IntPoint(x, y) - m_caretPositionOnLayout);
+ }
+
+ return caret;
+}
+
+IntRect SelectionController::caretRepaintRect() const
+{
+ return caretRect();
+}
+
+bool SelectionController::recomputeCaretRect()
+{
+ if (!m_frame || !m_frame->document())
+ return false;
+
+ FrameView* v = m_frame->document()->view();
+ if (!v)
+ return false;
+
+ if (!m_needsLayout)
+ return false;
+
+ IntRect oldRect = m_caretRect;
+ m_needsLayout = true;
+ IntRect newRect = caretRect();
+ if (oldRect == newRect)
+ return false;
+
+ v->updateContents(oldRect, false);
+ v->updateContents(newRect, false);
+ return true;
+}
+
+void SelectionController::invalidateCaretRect()
+{
+ if (!isCaret())
+ return;
+
+ FrameView* v = m_sel.start().node()->document()->view();
+ if (!v)
+ return;
+
+ 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_needsLayout = true;
+
+ if (!caretRectChanged)
+ v->updateContents(caretRepaintRect(), false);
+}
+
+void SelectionController::paintCaret(GraphicsContext *p, const IntRect &rect)
+{
+ if (! m_sel.isCaret())
+ return;
+
+ if (m_needsLayout)
+ layout();
+
+ IntRect caret = intersection(caretRect(), rect);
+ if (!caret.isEmpty()) {
+ Color caretColor = Color::black;
+ Element* element = rootEditableElement();
+ if (element && element->renderer())
+ caretColor = element->renderer()->style()->color();
+
+ p->fillRect(caret, caretColor);
+ }
+}
+
+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 = static_cast<RenderText*>(r);
+ if (textRenderer->textLength() == 0 || !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_sel.start().node())
+ offset = m_sel.start().offset();
+ else if (r->node() == m_sel.end().node())
+ offset = m_sel.end().offset();
+
+ int pos;
+ InlineTextBox *box = textRenderer->findNextInlineTextBox(offset, pos);
+ text = text.substring(box->m_start, box->m_len);
+
+ String show;
+ int mid = max / 2;
+ int caret = 0;
+
+ // text is shorter than max
+ if (textLength < max) {
+ show = text;
+ caret = pos;
+ }
+
+ // too few characters to left
+ else if (pos - mid < 0) {
+ show = text.left(max - 3) + "...";
+ caret = pos;
+ }
+
+ // enough characters on each side
+ else if (pos - mid >= 0 && pos + mid <= textLength) {
+ show = "..." + text.substring(pos - mid + 3, max - 6) + "...";
+ caret = mid;
+ }
+
+ // too few characters on right
+ else {
+ 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(true, true);
+ HitTestResult result(point);
+ document->renderer()->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_sel.visibleStart().isNull() || m_sel.visibleEnd().isNull())
+ return false;
+
+ Position start(m_sel.visibleStart().deepEquivalent());
+ Position end(m_sel.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.
+ Document* doc = m_frame->document();
+ if (!doc)
+ return;
+ Element* ownerElement = doc->ownerElement();
+ if (!ownerElement)
+ return;
+ Node* 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.
+ Selection newSelection(beforeOwnerElement, afterOwnerElement);
+ if (parent->shouldChangeSelection(newSelection)) {
+ page->focusController()->setFocusedFrame(parent);
+ parent->selectionController()->setSelection(newSelection);
+ }
+}
+
+void SelectionController::selectAll()
+{
+ Document* document = m_frame->document();
+ if (!document)
+ return;
+
+ if (document->focusedNode() && document->focusedNode()->canSelectAll()) {
+ document->focusedNode()->selectAll();
+ return;
+ }
+
+ Node* root = isContentEditable() ? highestEditableRoot(m_sel.start()) : document->documentElement();
+ if (!root)
+ return;
+ Selection newSelection(Selection::selectionFromContentsOfNode(root));
+ if (m_frame->shouldChangeSelection(newSelection))
+ setSelection(newSelection);
+ selectFrameElementInParentIfFullySelected();
+ m_frame->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(Selection(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)->inputType() == HTMLInputElement::PASSWORD;
+}
+
+bool SelectionController::isInsideNode() const
+{
+ Node* startNode = start().node();
+ if (!startNode)
+ return false;
+ return !isTableElement(startNode) && !editingIgnoresContent(startNode);
+}
+
+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 (m_frame->view())
+ m_frame->view()->updateContents(enclosingIntRect(m_frame->selectionRect()));
+
+ // Caret appears in the active frame.
+ if (activeAndFocused)
+ m_frame->setSelectionFromNone();
+ m_frame->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->setChanged();
+ if (RenderObject* renderer = node->renderer())
+ if (renderer && renderer->style()->hasAppearance())
+ theme()->stateChanged(renderer, FocusState);
+ }
+
+ // Secure keyboard entry is set by the active frame.
+ if (m_frame->document()->useSecureKeyboardEntryWhenActive())
+ m_frame->setUseSecureKeyboardEntry(activeAndFocused);
+}
+
+void SelectionController::pageActivationChanged()
+{
+ focusedOrActiveStateChanged();
+}
+
+void SelectionController::setFocused(bool flag)
+{
+ if (m_focused == flag)
+ return;
+ m_focused = flag;
+
+ focusedOrActiveStateChanged();
+
+ if (Document* doc = m_frame->document())
+ doc->dispatchWindowEvent(flag ? focusEvent : blurEvent, false, false);
+}
+
+bool SelectionController::isFocusedAndActive() const
+{
+ return m_focused && m_frame->page() && m_frame->page()->focusController()->isActive();
+}
+
+#ifndef NDEBUG
+
+void SelectionController::formatForDebugger(char* buffer, unsigned length) const
+{
+ m_sel.formatForDebugger(buffer, length);
+}
+
+void SelectionController::showTreeForThis() const
+{
+ m_sel.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/WebCore/editing/SelectionController.h b/WebCore/editing/SelectionController.h
new file mode 100644
index 0000000..5e2f5ed
--- /dev/null
+++ b/WebCore/editing/SelectionController.h
@@ -0,0 +1,183 @@
+/*
+ * 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 SelectionController_h
+#define SelectionController_h
+
+#include "IntRect.h"
+#include "Selection.h"
+#include "Range.h"
+#include <wtf/Noncopyable.h>
+
+namespace WebCore {
+
+class Frame;
+class GraphicsContext;
+class RenderObject;
+class VisiblePosition;
+
+class SelectionController : Noncopyable {
+public:
+ enum EAlteration { MOVE, EXTEND };
+ enum EDirection { FORWARD, BACKWARD, RIGHT, LEFT };
+
+ SelectionController(Frame* = 0, bool isDragCaretController = false);
+
+ Element* rootEditableElement() const { return m_sel.rootEditableElement(); }
+ bool isContentEditable() const { return m_sel.isContentEditable(); }
+ bool isContentRichlyEditable() const { return m_sel.isContentRichlyEditable(); }
+
+ void moveTo(const Range*, EAffinity, bool userTriggered = false);
+ void moveTo(const VisiblePosition&, bool userTriggered = false);
+ 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 Selection& selection() const { return m_sel; }
+ void setSelection(const Selection&, bool closeTyping = true, bool clearTypingStyle = true, bool userTriggered = false);
+ 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&);
+
+ Selection::EState state() const { return m_sel.state(); }
+
+ EAffinity affinity() const { return m_sel.affinity(); }
+
+ bool modify(EAlteration, EDirection, TextGranularity, bool userTriggered = false);
+ bool modify(EAlteration, int verticalDistance, bool userTriggered = false);
+ bool expandUsingGranularity(TextGranularity);
+
+ 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_sel.base(); }
+ Position extent() const { return m_sel.extent(); }
+ Position start() const { return m_sel.start(); }
+ Position end() const { return m_sel.end(); }
+
+ IntRect caretRect() const;
+ void setNeedsLayout(bool flag = true);
+
+ void setLastChangeWasHorizontalExtension(bool b) { m_lastChangeWasHorizontalExtension = b; }
+ void willBeModified(EAlteration, EDirection);
+
+ bool isNone() const { return m_sel.isNone(); }
+ bool isCaret() const { return m_sel.isCaret(); }
+ bool isRange() const { return m_sel.isRange(); }
+ bool isCaretOrRange() const { return m_sel.isCaretOrRange(); }
+ bool isInPasswordField() const;
+ bool isInsideNode() const;
+
+ PassRefPtr<Range> toRange() const { return m_sel.toRange(); }
+
+ void debugRenderer(RenderObject*, bool selected) const;
+
+ void nodeWillBeRemoved(Node*);
+
+ bool recomputeCaretRect(); // returns true if caret rect moved
+ void invalidateCaretRect();
+ void paintCaret(GraphicsContext*, const IntRect&);
+
+ // 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 isFocusedAndActive() const;
+ void pageActivationChanged();
+
+#ifndef NDEBUG
+ void formatForDebugger(char* buffer, unsigned length) const;
+ void showTreeForThis() const;
+#endif
+
+private:
+ enum EPositionType { START, END, BASE, EXTENT };
+
+ VisiblePosition modifyExtendingRightForward(TextGranularity);
+ VisiblePosition modifyMovingRightForward(TextGranularity);
+ VisiblePosition modifyExtendingLeftBackward(TextGranularity);
+ VisiblePosition modifyMovingLeftBackward(TextGranularity);
+
+ void layout();
+ IntRect caretRepaintRect() const;
+
+ int xPosForVerticalArrowNavigation(EPositionType);
+
+#if PLATFORM(MAC)
+ void notifyAccessibilityForSelectionChange();
+#else
+ void notifyAccessibilityForSelectionChange() {};
+#endif
+
+ void focusedOrActiveStateChanged();
+
+ Selection m_sel;
+
+ IntRect m_caretRect; // caret coordinates, size, and position
+
+ // m_caretPositionOnLayout stores the scroll offset on the previous call to SelectionController::layout().
+ // When asked for caretRect(), we correct m_caretRect for offset due to scrolling since the last layout().
+ // This is faster than doing another layout().
+ IntPoint m_caretPositionOnLayout;
+
+ bool m_needsLayout : 1; // true if the caret and expectedVisible rectangles need to be calculated
+ bool m_lastChangeWasHorizontalExtension : 1;
+ Frame* m_frame;
+ bool m_isDragCaretController;
+
+ bool m_isCaretBlinkingSuspended;
+
+ int m_xPosForVerticalArrowNavigation;
+ bool m_focused;
+};
+
+inline bool operator==(const SelectionController& a, const SelectionController& b)
+{
+ return a.start() == b.start() && a.end() == b.end() && a.affinity() == b.affinity();
+}
+
+inline bool operator!=(const SelectionController& a, const SelectionController& b)
+{
+ return !(a == b);
+}
+
+} // 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/WebCore/editing/SetNodeAttributeCommand.cpp b/WebCore/editing/SetNodeAttributeCommand.cpp
new file mode 100644
index 0000000..faa8e5d
--- /dev/null
+++ b/WebCore/editing/SetNodeAttributeCommand.cpp
@@ -0,0 +1,66 @@
+/*
+ * 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 "SetNodeAttributeCommand.h"
+#include "Element.h"
+
+#include <wtf/Assertions.h>
+
+namespace WebCore {
+
+SetNodeAttributeCommand::SetNodeAttributeCommand(Element* element,
+ const QualifiedName& attribute, const String &value)
+ : EditCommand(element->document()), m_element(element), m_attribute(attribute), m_value(value)
+{
+ ASSERT(m_element);
+ ASSERT(!m_value.isNull());
+}
+
+void SetNodeAttributeCommand::doApply()
+{
+ ASSERT(m_element);
+ ASSERT(!m_value.isNull());
+
+ ExceptionCode ec = 0;
+ m_oldValue = m_element->getAttribute(m_attribute);
+ m_element->setAttribute(m_attribute, m_value.impl(), ec);
+ ASSERT(ec == 0);
+}
+
+void SetNodeAttributeCommand::doUnapply()
+{
+ ASSERT(m_element);
+
+ ExceptionCode ec = 0;
+ if (m_oldValue.isNull())
+ m_element->removeAttribute(m_attribute, ec);
+ else
+ m_element->setAttribute(m_attribute, m_oldValue.impl(), ec);
+ ASSERT(ec == 0);
+}
+
+} // namespace WebCore
+
diff --git a/WebCore/editing/SetNodeAttributeCommand.h b/WebCore/editing/SetNodeAttributeCommand.h
new file mode 100644
index 0000000..b494f33
--- /dev/null
+++ b/WebCore/editing/SetNodeAttributeCommand.h
@@ -0,0 +1,54 @@
+/*
+ * 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.
+ */
+
+#ifndef SetNodeAttributeCommand_h
+#define SetNodeAttributeCommand_h
+
+#include "EditCommand.h"
+#include "QualifiedName.h"
+
+namespace WebCore {
+
+class SetNodeAttributeCommand : public EditCommand {
+public:
+ SetNodeAttributeCommand(Element*, const QualifiedName& attribute, const String &value);
+
+ virtual void doApply();
+ virtual void doUnapply();
+
+ Element* element() const { return m_element.get(); }
+ const QualifiedName& attribute() const { return m_attribute; }
+ String value() const { return m_value; }
+
+private:
+ RefPtr<Element> m_element;
+ QualifiedName m_attribute;
+ String m_value;
+ String m_oldValue;
+};
+
+} // namespace WebCore
+
+#endif // SetNodeAttributeCommand_h
diff --git a/WebCore/editing/SmartReplace.cpp b/WebCore/editing/SmartReplace.cpp
new file mode 100644
index 0000000..c5f5240
--- /dev/null
+++ b/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/WebCore/editing/SmartReplace.h b/WebCore/editing/SmartReplace.h
new file mode 100644
index 0000000..5a37137
--- /dev/null
+++ b/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/WebCore/editing/SmartReplaceCF.cpp b/WebCore/editing/SmartReplaceCF.cpp
new file mode 100644
index 0000000..f2fd985
--- /dev/null
+++ b/WebCore/editing/SmartReplaceCF.cpp
@@ -0,0 +1,72 @@
+/*
+ * 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>
+
+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/WebCore/editing/SmartReplaceICU.cpp b/WebCore/editing/SmartReplaceICU.cpp
new file mode 100644
index 0000000..18be647
--- /dev/null
+++ b/WebCore/editing/SmartReplaceICU.cpp
@@ -0,0 +1,99 @@
+/*
+ * 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/WebCore/editing/SplitElementCommand.cpp b/WebCore/editing/SplitElementCommand.cpp
new file mode 100644
index 0000000..9b4241d
--- /dev/null
+++ b/WebCore/editing/SplitElementCommand.cpp
@@ -0,0 +1,90 @@
+/*
+ * 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 "SplitElementCommand.h"
+#include "Element.h"
+
+#include <wtf/Assertions.h>
+
+namespace WebCore {
+
+SplitElementCommand::SplitElementCommand(Element* element, Node* atChild)
+ : EditCommand(element->document()), m_element2(element), m_atChild(atChild)
+{
+ ASSERT(m_element2);
+ ASSERT(m_atChild);
+}
+
+void SplitElementCommand::doApply()
+{
+ ASSERT(m_element2);
+ ASSERT(m_atChild);
+ ASSERT(m_atChild->parentNode() == m_element2);
+
+ ExceptionCode ec = 0;
+
+ if (!m_element1) {
+ // create only if needed.
+ // if reapplying, this object will already exist.
+ m_element1 = static_pointer_cast<Element>(m_element2->cloneNode(false));
+ ASSERT(m_element1);
+ }
+
+ m_element2->parent()->insertBefore(m_element1.get(), m_element2.get(), ec);
+ ASSERT(ec == 0);
+
+ // Bail if we were asked to split at a bogus child, to avoid hanging below.
+ if (!m_atChild || m_atChild->parentNode() != m_element2)
+ return;
+
+ while (m_element2->firstChild() != m_atChild) {
+ ASSERT(m_element2->firstChild());
+ m_element1->appendChild(m_element2->firstChild(), ec);
+ ASSERT(ec == 0);
+ }
+}
+
+void SplitElementCommand::doUnapply()
+{
+ ASSERT(m_element1);
+ ASSERT(m_element2);
+ ASSERT(m_atChild);
+
+ ASSERT(m_element1->nextSibling() == m_element2);
+ ASSERT(m_element2->firstChild() && m_element2->firstChild() == m_atChild);
+
+ ExceptionCode ec = 0;
+
+ while (m_element1->lastChild()) {
+ m_element2->insertBefore(m_element1->lastChild(), m_element2->firstChild(), ec);
+ ASSERT(ec == 0);
+ }
+
+ m_element2->parentNode()->removeChild(m_element1.get(), ec);
+ ASSERT(ec == 0);
+}
+
+} // namespace WebCore
diff --git a/WebCore/editing/SplitElementCommand.h b/WebCore/editing/SplitElementCommand.h
new file mode 100644
index 0000000..f404882
--- /dev/null
+++ b/WebCore/editing/SplitElementCommand.h
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+
+#ifndef SplitElementCommand_h
+#define SplitElementCommand_h
+
+#include "EditCommand.h"
+
+namespace WebCore {
+
+class SplitElementCommand : public EditCommand {
+public:
+ SplitElementCommand(Element*, Node* splitPointChild);
+
+ virtual void doApply();
+ virtual void doUnapply();
+
+private:
+ RefPtr<Element> m_element1;
+ RefPtr<Element> m_element2;
+ RefPtr<Node> m_atChild;
+};
+
+} // namespace WebCore
+
+#endif // SplitElementCommand_h
diff --git a/WebCore/editing/SplitTextNodeCommand.cpp b/WebCore/editing/SplitTextNodeCommand.cpp
new file mode 100644
index 0000000..0aef323
--- /dev/null
+++ b/WebCore/editing/SplitTextNodeCommand.cpp
@@ -0,0 +1,91 @@
+/*
+ * 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 "SplitTextNodeCommand.h"
+
+#include "Document.h"
+#include "Text.h"
+
+#include <wtf/Assertions.h>
+
+namespace WebCore {
+
+SplitTextNodeCommand::SplitTextNodeCommand(Text* text, int offset)
+ : EditCommand(text->document()), m_text2(text), m_offset(offset)
+{
+ ASSERT(m_text2);
+ ASSERT(m_text2->length() > 0);
+}
+
+void SplitTextNodeCommand::doApply()
+{
+ ASSERT(m_text2);
+ ASSERT(m_offset > 0);
+
+ ExceptionCode ec = 0;
+
+ // 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.
+ if (!m_text1) {
+ // create only if needed.
+ // if reapplying, this object will already exist.
+ m_text1 = document()->createTextNode(m_text2->substringData(0, m_offset, ec));
+ ASSERT(ec == 0);
+ ASSERT(m_text1);
+ }
+
+ document()->copyMarkers(m_text2.get(), 0, m_offset, m_text1.get(), 0);
+ m_text2->deleteData(0, m_offset, ec);
+ ASSERT(ec == 0);
+
+ m_text2->parentNode()->insertBefore(m_text1.get(), m_text2.get(), ec);
+ ASSERT(ec == 0);
+
+ ASSERT(m_text2->previousSibling()->isTextNode());
+ ASSERT(m_text2->previousSibling() == m_text1);
+}
+
+void SplitTextNodeCommand::doUnapply()
+{
+ ASSERT(m_text1);
+ ASSERT(m_text2);
+ ASSERT(m_text1->nextSibling() == m_text2);
+
+ ExceptionCode ec = 0;
+ m_text2->insertData(0, m_text1->data(), ec);
+ ASSERT(ec == 0);
+
+ document()->copyMarkers(m_text1.get(), 0, m_offset, m_text2.get(), 0);
+
+ m_text2->parentNode()->removeChild(m_text1.get(), ec);
+ ASSERT(ec == 0);
+
+ m_offset = m_text1->length();
+}
+
+} // namespace WebCore
diff --git a/WebCore/editing/SplitTextNodeCommand.h b/WebCore/editing/SplitTextNodeCommand.h
new file mode 100644
index 0000000..38d794a
--- /dev/null
+++ b/WebCore/editing/SplitTextNodeCommand.h
@@ -0,0 +1,54 @@
+/*
+ * 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.
+ */
+
+#ifndef SplitTextNodeCommand_h
+#define SplitTextNodeCommand_h
+
+#include "EditCommand.h"
+
+namespace WebCore {
+
+class Text;
+
+class SplitTextNodeCommand : public EditCommand {
+public:
+ SplitTextNodeCommand(Text*, int offset);
+ virtual ~SplitTextNodeCommand() { }
+
+ virtual void doApply();
+ virtual void doUnapply();
+
+ Text* node() const { return m_text2.get(); }
+ int offset() const { return m_offset; }
+
+private:
+ RefPtr<Text> m_text1;
+ RefPtr<Text> m_text2;
+ unsigned m_offset;
+};
+
+} // namespace WebCore
+
+#endif // SplitTextNodeCommand_h
diff --git a/WebCore/editing/SplitTextNodeContainingElementCommand.cpp b/WebCore/editing/SplitTextNodeContainingElementCommand.cpp
new file mode 100644
index 0000000..eb1b3e1
--- /dev/null
+++ b/WebCore/editing/SplitTextNodeContainingElementCommand.cpp
@@ -0,0 +1,59 @@
+/*
+ * 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 "SplitTextNodeContainingElementCommand.h"
+
+#include "Element.h"
+#include "Text.h"
+#include "RenderObject.h"
+#include <wtf/Assertions.h>
+
+namespace WebCore {
+
+SplitTextNodeContainingElementCommand::SplitTextNodeContainingElementCommand(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);
+
+ Node *parentNode = m_text->parentNode();
+ if (!parentNode->renderer() || !parentNode->renderer()->isInline()) {
+ wrapContentsInDummySpan(static_cast<Element *>(parentNode));
+ parentNode = parentNode->firstChild();
+ }
+
+ splitElement(static_cast<Element *>(parentNode), m_text.get());
+}
+
+}
diff --git a/WebCore/editing/SplitTextNodeContainingElementCommand.h b/WebCore/editing/SplitTextNodeContainingElementCommand.h
new file mode 100644
index 0000000..cd19a1f
--- /dev/null
+++ b/WebCore/editing/SplitTextNodeContainingElementCommand.h
@@ -0,0 +1,46 @@
+/*
+ * 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.
+ */
+
+#ifndef SplitTextNodeContainingElementCommand_h
+#define SplitTextNodeContainingElementCommand_h
+
+#include "CompositeEditCommand.h"
+
+namespace WebCore {
+
+class SplitTextNodeContainingElementCommand : public CompositeEditCommand {
+public:
+ SplitTextNodeContainingElementCommand(Text*, int offset);
+
+ virtual void doApply();
+
+private:
+ RefPtr<Text> m_text;
+ int m_offset;
+};
+
+} // namespace WebCore
+
+#endif // SplitTextNodeContainingElementCommand_h
diff --git a/WebCore/editing/TextAffinity.h b/WebCore/editing/TextAffinity.h
new file mode 100644
index 0000000..5562cc4
--- /dev/null
+++ b/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
+
+#include <wtf/Platform.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
+typedef enum { UPSTREAM = 0, DOWNSTREAM = 1 } EAffinity;
+
+#ifdef __OBJC__
+inline NSSelectionAffinity kit(EAffinity affinity)
+{
+ return static_cast<NSSelectionAffinity>(affinity);
+}
+
+inline EAffinity core(NSSelectionAffinity affinity)
+{
+ return static_cast<EAffinity>(affinity);
+}
+#endif
+
+} // namespace WebCore
+
+#endif // TextAffinity_h
diff --git a/WebCore/editing/TextGranularity.h b/WebCore/editing/TextGranularity.h
new file mode 100644
index 0000000..09cc4ed
--- /dev/null
+++ b/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/WebCore/editing/TextIterator.cpp b/WebCore/editing/TextIterator.cpp
new file mode 100644
index 0000000..233361e
--- /dev/null
+++ b/WebCore/editing/TextIterator.cpp
@@ -0,0 +1,1383 @@
+/*
+ * Copyright (C) 2004, 2005, 2006, 2007 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 "Element.h"
+#include "HTMLNames.h"
+#include "htmlediting.h"
+#include "InlineTextBox.h"
+#include "Position.h"
+#include "Range.h"
+#include "RenderTableCell.h"
+#include "RenderTableRow.h"
+#include "visible_units.h"
+
+using namespace std;
+using namespace WTF::Unicode;
+
+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.
+class CircularSearchBuffer : Noncopyable {
+public:
+ CircularSearchBuffer(const String& target, bool isCaseSensitive);
+
+ void clear() { m_cursor = 0; m_isBufferFull = false; }
+ void append(UChar);
+
+ bool isMatch() const;
+ unsigned length() const;
+
+private:
+ void append(UChar, bool isCharacterStart);
+
+ String m_target;
+ bool m_isCaseSensitive;
+
+ Vector<UChar> m_characterBuffer;
+ Vector<bool> m_isCharacterStartBuffer;
+ bool m_isBufferFull;
+ unsigned m_cursor;
+};
+
+// --------
+
+TextIterator::TextIterator() : m_startContainer(0), m_startOffset(0), m_endContainer(0), m_endOffset(0), m_positionNode(0), m_lastCharacter(0)
+{
+}
+
+TextIterator::TextIterator(const Range* r, bool emitCharactersBetweenAllVisiblePositions)
+ : m_startContainer(0)
+ , m_startOffset(0)
+ , m_endContainer(0)
+ , m_endOffset(0)
+ , m_positionNode(0)
+ , m_emitCharactersBetweenAllVisiblePositions(emitCharactersBetweenAllVisiblePositions)
+{
+ if (!r)
+ return;
+
+ ExceptionCode ec = 0;
+
+ // get and validate the range endpoints
+ Node *startContainer = r->startContainer(ec);
+ int startOffset = r->startOffset(ec);
+ Node *endContainer = r->endContainer(ec);
+ int endOffset = r->endOffset(ec);
+ if (ec)
+ return;
+
+ // 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->startNode();
+ if (m_node == 0)
+ return;
+ m_offset = m_node == m_startContainer ? m_startOffset : 0;
+ m_handledNode = false;
+ m_handledChildren = false;
+
+ // calculate first out of bounds node
+ m_pastEndNode = r->pastEndNode();
+
+ // initialize node processing state
+ m_needAnotherNewline = false;
+ m_textBox = 0;
+
+ // initialize record of previous node processing
+ m_haveEmitted = 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();
+}
+
+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_needAnotherNewline) {
+ // emit the newline, with position a collapsed range at the end of current node.
+ emitCharacter('\n', m_node->parentNode(), m_node, 1, 1);
+ m_needAnotherNewline = false;
+ return;
+ }
+
+ // 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->element() && renderer->element()->isControl())))
+ 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;
+ while (!next && m_node->parentNode()) {
+ if (pastEnd && m_node->parentNode() == m_endContainer || m_endContainer->isDescendantOf(m_node->parentNode()))
+ return;
+ bool haveRenderer = m_node->renderer();
+ m_node = m_node->parentNode();
+ if (haveRenderer)
+ exitNode();
+ if (m_positionNode) {
+ m_handledNode = true;
+ m_handledChildren = true;
+ return;
+ }
+ next = m_node->nextSibling();
+ }
+ }
+ }
+
+ // set the new current node
+ m_node = next;
+ m_handledNode = false;
+ m_handledChildren = false;
+
+ // how would this ever be?
+ if (m_positionNode)
+ return;
+ }
+}
+
+static inline bool compareBoxStart(const InlineTextBox *first, const InlineTextBox *second)
+{
+ return first->start() < second->start();
+}
+
+bool TextIterator::handleTextNode()
+{
+ RenderText* renderer = static_cast<RenderText*>(m_node->renderer());
+ if (renderer->style()->visibility() != VISIBLE)
+ return false;
+
+ m_lastTextNode = m_node;
+ String str = renderer->text();
+
+ // handle pre-formatted text
+ if (!renderer->style()->collapseWhiteSpace()) {
+ int runStart = m_offset;
+ if (m_lastTextNodeEndedWithCollapsedSpace) {
+ emitCharacter(' ', m_node, 0, runStart, runStart);
+ 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) {
+ 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(), compareBoxStart);
+ m_sortedTextBoxesPosition = 0;
+ }
+
+ m_textBox = renderer->containsReversedText() ? m_sortedTextBoxes[0] : renderer->firstTextBox();
+ handleTextBox();
+ return true;
+}
+
+void TextIterator::handleTextBox()
+{
+ RenderText *renderer = static_cast<RenderText *>(m_node->renderer());
+ String str = renderer->text();
+ int start = m_offset;
+ int end = (m_node == m_endContainer) ? m_endOffset : INT_MAX;
+ while (m_textBox) {
+ int textBoxStart = m_textBox->m_start;
+ int 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) {
+ emitCharacter(' ', m_node, 0, runStart, runStart);
+ return;
+ }
+ int textBoxEnd = textBoxStart + m_textBox->m_len;
+ int 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 {
+ int subrunEnd = str.find('\n', runStart);
+ if (subrunEnd == -1 || subrunEnd > runEnd)
+ subrunEnd = runEnd;
+
+ m_offset = subrunEnd;
+ emitText(m_node, 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 (m_positionEndOffset < textBoxEnd)
+ return;
+
+ // Advance and return
+ int nextRunStart = nextTextBox ? nextTextBox->m_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;
+ }
+}
+
+bool TextIterator::handleReplacedElement()
+{
+ if (m_node->renderer()->style()->visibility() != VISIBLE)
+ return false;
+
+ if (m_lastTextNodeEndedWithCollapsedSpace) {
+ emitCharacter(' ', m_lastTextNode->parentNode(), m_lastTextNode, 1, 1);
+ return false;
+ }
+
+ m_haveEmitted = true;
+
+ if (m_emitCharactersBetweenAllVisiblePositions) {
+ // 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;
+}
+
+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 = static_cast<RenderTableCell*>(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 = static_cast<RenderTableRow*>(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)
+ 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 = r->collapsedMarginBottom();
+ int fontSize = style->fontDescription().computedPixelSize();
+ if (bottomMargin * 2 >= fontSize)
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool TextIterator::shouldRepresentNodeOffsetZero()
+{
+ if (m_emitCharactersBetweenAllVisiblePositions && 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_haveEmitted)
+ 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.
+
+ // If we are at the start, obviously no newline is needed.
+ if (m_node == m_startContainer)
+ return false;
+
+ // If we are outside the start container's subtree, assume we need a newline.
+ // 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 a newline after a preceding block. We chose not to emit (m_haveEmitted 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;
+
+ // 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 currPos.isNotNull() && !inSameLine(startPos, currPos);
+}
+
+bool TextIterator::shouldEmitSpaceBeforeAndAfterNode(Node* node)
+{
+ return node->renderer() && node->renderer()->isTable() && (node->renderer()->isInline() || m_emitCharactersBetweenAllVisiblePositions);
+}
+
+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_emitCharactersBetweenAllVisiblePositions && 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_haveEmitted 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_haveEmitted)
+ 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_needAnotherNewline);
+ m_needAnotherNewline = 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_haveEmitted = 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, int textStartOffset, int textEndOffset)
+{
+ RenderText* renderer = static_cast<RenderText*>(m_node->renderer());
+ String str = renderer->text();
+
+ m_positionNode = textNode;
+ m_positionOffsetBaseNode = 0;
+ m_positionStartOffset = textStartOffset;
+ m_positionEndOffset = textEndOffset;
+ m_textCharacters = str.characters() + textStartOffset;
+ m_textLength = textEndOffset - textStartOffset;
+ m_lastCharacter = str[textEndOffset - 1];
+
+ m_lastTextNodeEndedWithCollapsedSpace = false;
+ m_haveEmitted = true;
+}
+
+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 new Range(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 new Range(m_endContainer->document(), m_endContainer, m_endOffset, m_endContainer, m_endOffset);
+
+ return 0;
+}
+
+// --------
+
+SimplifiedBackwardsTextIterator::SimplifiedBackwardsTextIterator() : m_positionNode(0)
+{
+}
+
+SimplifiedBackwardsTextIterator::SimplifiedBackwardsTextIterator(const Range *r)
+{
+ m_positionNode = 0;
+
+ if (!r)
+ return;
+
+ int exception = 0;
+ Node *startNode = r->startContainer(exception);
+ if (exception)
+ return;
+ Node *endNode = r->endContainer(exception);
+ if (exception)
+ return;
+ int startOffset = r->startOffset(exception);
+ if (exception)
+ return;
+ int endOffset = r->endOffset(exception);
+ if (exception)
+ return;
+
+ 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 = endNode->offsetInCharacters() ? endNode->maxCharacterOffset() : endNode->childNodeCount();
+ }
+ }
+
+ m_node = endNode;
+ 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';
+
+ if (startOffset == 0 || !startNode->firstChild()) {
+ m_pastStartNode = startNode->previousSibling();
+ while (!m_pastStartNode && startNode->parentNode()) {
+ startNode = startNode->parentNode();
+ m_pastStartNode = startNode->previousSibling();
+ }
+ } else
+ m_pastStartNode = startNode->childNode(startOffset - 1);
+
+ 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.
+ next = m_node->previousSibling();
+ while (!next) {
+ if (!m_node->parentNode())
+ break;
+ m_node = m_node->parentNode();
+ exitNode();
+ if (m_positionNode) {
+ m_handledNode = true;
+ m_handledChildren = true;
+ return;
+ }
+ next = m_node->previousSibling();
+ }
+ }
+
+ m_node = next;
+ m_offset = m_node ? caretMaxOffset(m_node) : 0;
+ m_handledNode = false;
+ m_handledChildren = false;
+
+ if (m_positionNode)
+ return;
+ }
+}
+
+bool SimplifiedBackwardsTextIterator::handleTextNode()
+{
+ m_lastTextNode = m_node;
+
+ RenderText *renderer = static_cast<RenderText *>(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;
+}
+
+PassRefPtr<Range> SimplifiedBackwardsTextIterator::range() const
+{
+ if (m_positionNode)
+ return new Range(m_positionNode->document(), m_positionNode, m_positionStartOffset, m_positionNode, m_positionEndOffset);
+
+ return new Range(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, bool emitCharactersBetweenAllVisiblePositions)
+ : m_offset(0), m_runOffset(0), m_atBreak(true), m_textIterator(r, emitCharactersBetweenAllVisiblePositions)
+{
+ 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 {
+ int exception = 0;
+ Node *n = r->startContainer(exception);
+ ASSERT(n == r->endContainer(exception));
+ int offset = r->startOffset(exception) + m_runOffset;
+ r->setStart(n, offset, exception);
+ r->setEnd(n, offset + 1, exception);
+ }
+ }
+ 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.reserveCapacity(numChars);
+ while (numChars > 0 && !atEnd()) {
+ int runSize = min(numChars, length());
+ result.append(characters(), runSize);
+ numChars -= runSize;
+ advance(runSize);
+ }
+ return String::adopt(result);
+}
+
+// --------
+
+WordAwareIterator::WordAwareIterator()
+: m_previousText(0), m_didLookAhead(false)
+{
+}
+
+WordAwareIterator::WordAwareIterator(const Range *r)
+: m_previousText(0), m_didLookAhead(false), m_textIterator(r)
+{
+ m_didLookAhead = true; // so we consider the first chunk from the text iterator
+ advance(); // get in position over the first chunk of text
+}
+
+// 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: Perf 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(exception), m_textIterator.range()->endOffset(exception), 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();
+}
+
+// --------
+
+CircularSearchBuffer::CircularSearchBuffer(const String& s, bool isCaseSensitive)
+ : m_target(isCaseSensitive ? s : s.foldCase())
+ , m_isCaseSensitive(isCaseSensitive)
+ , m_characterBuffer(m_target.length())
+ , m_isCharacterStartBuffer(m_target.length())
+ , m_isBufferFull(false)
+ , m_cursor(0)
+{
+ ASSERT(!m_target.isEmpty());
+ m_target.replace(noBreakSpace, ' ');
+}
+
+inline void CircularSearchBuffer::append(UChar c, bool isStart)
+{
+ m_characterBuffer[m_cursor] = c == noBreakSpace ? ' ' : c;
+ m_isCharacterStartBuffer[m_cursor] = isStart;
+ if (++m_cursor == m_target.length()) {
+ m_cursor = 0;
+ m_isBufferFull = true;
+ }
+}
+
+inline void CircularSearchBuffer::append(UChar c)
+{
+ if (m_isCaseSensitive) {
+ append(c, true);
+ return;
+ }
+ const int maxFoldedCharacters = 16; // sensible maximum is 3, this should be more than enough
+ UChar foldedCharacters[maxFoldedCharacters];
+ bool error;
+ int numFoldedCharacters = foldCase(foldedCharacters, maxFoldedCharacters, &c, 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);
+ }
+}
+
+inline bool CircularSearchBuffer::isMatch() const
+{
+ if (!m_isBufferFull)
+ return false;
+ if (!m_isCharacterStartBuffer[m_cursor])
+ return false;
+
+ unsigned tailSpace = m_target.length() - m_cursor;
+ return memcmp(&m_characterBuffer[m_cursor], m_target.characters(), tailSpace * sizeof(UChar)) == 0
+ && memcmp(&m_characterBuffer[0], m_target.characters() + tailSpace, m_cursor * sizeof(UChar)) == 0;
+}
+
+// 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.
+unsigned CircularSearchBuffer::length() const
+{
+ ASSERT(isMatch());
+
+ unsigned bufferSize = m_target.length();
+ unsigned length = 0;
+ for (unsigned i = 0; i < bufferSize; ++i)
+ length += m_isCharacterStartBuffer[i];
+ return length;
+}
+
+// --------
+
+int TextIterator::rangeLength(const Range *r, bool forSelectionPreservation)
+{
+ int length = 0;
+ for (TextIterator it(r, forSelectionPreservation); !it.atEnd(); it.advance())
+ length += it.length();
+
+ return length;
+}
+
+PassRefPtr<Range> TextIterator::subrange(Range* entireRange, int characterOffset, int characterCount)
+{
+ CharacterIterator chars(entireRange);
+
+ chars.advance(characterOffset);
+ RefPtr<Range> start = chars.range();
+
+ chars.advance(characterCount);
+ RefPtr<Range> end = chars.range();
+
+ ExceptionCode ec = 0;
+ RefPtr<Range> result(new Range(entireRange->ownerDocument(),
+ start->startContainer(ec),
+ start->startOffset(ec),
+ end->startContainer(ec),
+ end->startOffset(ec)));
+ ASSERT(!ec);
+
+ return result.release();
+}
+
+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);
+
+ // 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()) {
+ int exception = 0;
+ textRunRange = it.range();
+
+ resultRange->setStart(textRunRange->startContainer(exception), 0, exception);
+ ASSERT(exception == 0);
+ resultRange->setEnd(textRunRange->startContainer(exception), 0, exception);
+ ASSERT(exception == 0);
+
+ 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 (foundStart || 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] == UChar('\n')) {
+ Position runStart = textRunRange->startPosition();
+ Position runEnd = VisiblePosition(runStart).next().deepEquivalent();
+ if (runEnd.isNotNull()) {
+ ExceptionCode ec = 0;
+ textRunRange->setEnd(runEnd.node(), runEnd.offset(), ec);
+ }
+ }
+ }
+
+ if (foundStart) {
+ startRangeFound = true;
+ int exception = 0;
+ if (textRunRange->startContainer(exception)->isTextNode()) {
+ int offset = rangeLocation - docTextPosition;
+ resultRange->setStart(textRunRange->startContainer(exception), offset + textRunRange->startOffset(exception), exception);
+ } else {
+ if (rangeLocation == docTextPosition)
+ resultRange->setStart(textRunRange->startContainer(exception), textRunRange->startOffset(exception), exception);
+ else
+ resultRange->setStart(textRunRange->endContainer(exception), textRunRange->endOffset(exception), exception);
+ }
+ }
+
+ if (foundEnd) {
+ int exception = 0;
+ if (textRunRange->startContainer(exception)->isTextNode()) {
+ int offset = rangeEnd - docTextPosition;
+ resultRange->setEnd(textRunRange->startContainer(exception), offset + textRunRange->startOffset(exception), exception);
+ } else {
+ if (rangeEnd == docTextPosition)
+ resultRange->setEnd(textRunRange->startContainer(exception), textRunRange->startOffset(exception), exception);
+ else
+ resultRange->setEnd(textRunRange->endContainer(exception), textRunRange->endOffset(exception), 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(exception), textRunRange->endOffset(exception), exception);
+ }
+
+ return resultRange.release();
+}
+
+// --------
+
+UChar* plainTextToMallocAllocatedBuffer(const Range* r, unsigned& bufferLength)
+{
+ 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;
+ Vector<TextSegment>* textSegments = 0;
+ Vector<UChar> textBuffer;
+ textBuffer.reserveCapacity(cMaxSegmentSize);
+ for (TextIterator it(r); !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 = new Vector<TextSegment>;
+ textSegments->append(make_pair(newSegmentBuffer, 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);
+ delete textSegments;
+ }
+ return result;
+}
+
+String plainText(const Range* r)
+{
+ unsigned length;
+ UChar* buf = plainTextToMallocAllocatedBuffer(r, length);
+ if (!buf)
+ return "";
+ String result(buf, length);
+ free(buf);
+ return result;
+}
+
+PassRefPtr<Range> findPlainText(const Range* range, const String& target, bool forward, bool caseSensitive)
+{
+ // FIXME: Can we do Boyer-Moore or equivalent instead for speed?
+
+ ExceptionCode ec = 0;
+ RefPtr<Range> result = range->cloneRange(ec);
+ result->collapse(!forward, ec);
+
+ // FIXME: This code does not allow \n at the moment because of issues with <br>.
+ // Once we fix those, we can remove this check.
+ if (target.isEmpty() || target.find('\n') != -1)
+ return result.release();
+
+ unsigned matchStart = 0;
+ unsigned matchLength = 0;
+ {
+ CircularSearchBuffer searchBuffer(target, caseSensitive);
+ CharacterIterator it(range);
+ for (;;) {
+ if (searchBuffer.isMatch()) {
+ // Note that we found a match, and where we found it.
+ unsigned matchEnd = it.characterOffset();
+ matchLength = searchBuffer.length();
+ ASSERT(matchLength);
+ ASSERT(matchEnd >= matchLength);
+ matchStart = matchEnd - matchLength;
+ // If searching forward, stop on the first match.
+ // If searching backward, don't stop, so we end up with the last match.
+ if (forward)
+ break;
+ }
+ if (it.atBreak()) {
+ if (it.atEnd())
+ break;
+ searchBuffer.clear();
+ }
+ searchBuffer.append(it.characters()[0]);
+ it.advance(1);
+ }
+ }
+
+ if (matchLength) {
+ CharacterIterator it(range);
+ it.advance(matchStart);
+ result->setStart(it.range()->startContainer(ec), it.range()->startOffset(ec), ec);
+ it.advance(matchLength - 1);
+ result->setEnd(it.range()->endContainer(ec), it.range()->endOffset(ec), ec);
+ }
+
+ return result.release();
+}
+
+}
diff --git a/WebCore/editing/TextIterator.h b/WebCore/editing/TextIterator.h
new file mode 100644
index 0000000..56f42be
--- /dev/null
+++ b/WebCore/editing/TextIterator.h
@@ -0,0 +1,248 @@
+/*
+ * Copyright (C) 2004, 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 TextIterator_h
+#define TextIterator_h
+
+#include "InlineTextBox.h"
+#include "Range.h"
+#include <wtf/Vector.h>
+
+namespace WebCore {
+
+// 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*);
+UChar* plainTextToMallocAllocatedBuffer(const Range*, unsigned& bufferLength);
+PassRefPtr<Range> findPlainText(const Range*, const String&, bool forward, bool caseSensitive);
+
+// 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();
+ explicit TextIterator(const Range*, bool emitCharactersBetweenAllVisiblePositions = false);
+
+ 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;
+
+ 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 emitCharacter(UChar, Node *textNode, Node *offsetBaseNode, 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;
+
+ // 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;
+
+ // 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_needAnotherNewline;
+ InlineTextBox *m_textBox;
+
+ // 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_haveEmitted;
+
+ // Used by selection preservation code. There should be one character emitted between every VisiblePosition
+ // in the Range used to create the TextIterator.
+ bool m_emitCharactersBetweenAllVisiblePositions;
+};
+
+// 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 *);
+
+ 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 *Node, int startOffset, int endOffset);
+
+ // 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;
+
+ // 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* r, bool emitCharactersBetweenAllVisiblePositions = false);
+
+ 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;
+};
+
+// 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 *r);
+
+ 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/WebCore/editing/TypingCommand.cpp b/WebCore/editing/TypingCommand.cpp
new file mode 100644
index 0000000..2c0518e
--- /dev/null
+++ b/WebCore/editing/TypingCommand.cpp
@@ -0,0 +1,535 @@
+/*
+ * 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 "TypingCommand.h"
+
+#include "BeforeTextInsertedEvent.h"
+#include "BreakBlockquoteCommand.h"
+#include "DeleteSelectionCommand.h"
+#include "Document.h"
+#include "Editor.h"
+#include "Element.h"
+#include "Frame.h"
+#include "InsertLineBreakCommand.h"
+#include "InsertParagraphSeparatorCommand.h"
+#include "InsertTextCommand.h"
+#include "SelectionController.h"
+#include "VisiblePosition.h"
+#include "htmlediting.h"
+#include "visible_units.h"
+
+namespace WebCore {
+
+TypingCommand::TypingCommand(Document *document, ETypingCommand commandType, const String &textToInsert, bool selectInsertedText, TextGranularity granularity)
+ : CompositeEditCommand(document),
+ m_commandType(commandType),
+ m_textToInsert(textToInsert),
+ m_openForMoreTyping(true),
+ m_applyEditing(false),
+ m_selectInsertedText(selectInsertedText),
+ m_smartDelete(false),
+ m_granularity(granularity),
+ m_openedByBackwardDelete(false)
+{
+}
+
+void TypingCommand::deleteSelection(Document* document, bool smartDelete)
+{
+ ASSERT(document);
+
+ Frame* frame = document->frame();
+ ASSERT(frame);
+
+ if (!frame->selectionController()->isRange())
+ return;
+
+ EditCommand* lastEditCommand = frame->editor()->lastEditCommand();
+ if (isOpenForMoreTypingCommand(lastEditCommand)) {
+ static_cast<TypingCommand*>(lastEditCommand)->deleteSelection(smartDelete);
+ return;
+ }
+
+ RefPtr<TypingCommand> typingCommand = new TypingCommand(document, DeleteSelection, "", false);
+ typingCommand->setSmartDelete(smartDelete);
+ typingCommand->apply();
+}
+
+void TypingCommand::deleteKeyPressed(Document *document, bool smartDelete, TextGranularity granularity)
+{
+ ASSERT(document);
+
+ Frame *frame = document->frame();
+ ASSERT(frame);
+
+ EditCommand* lastEditCommand = frame->editor()->lastEditCommand();
+ if (isOpenForMoreTypingCommand(lastEditCommand)) {
+ static_cast<TypingCommand*>(lastEditCommand)->deleteKeyPressed(granularity);
+ return;
+ }
+
+ RefPtr<TypingCommand> typingCommand = new TypingCommand(document, DeleteKey, "", false, granularity);
+ typingCommand->setSmartDelete(smartDelete);
+ typingCommand->apply();
+}
+
+void TypingCommand::forwardDeleteKeyPressed(Document *document, bool smartDelete, TextGranularity granularity)
+{
+ // 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 (isOpenForMoreTypingCommand(lastEditCommand)) {
+ static_cast<TypingCommand*>(lastEditCommand)->forwardDeleteKeyPressed(granularity);
+ return;
+ }
+
+ RefPtr<TypingCommand> typingCommand = new TypingCommand(document, ForwardDeleteKey, "", false, granularity);
+ typingCommand->setSmartDelete(smartDelete);
+ typingCommand->apply();
+}
+
+void TypingCommand::insertText(Document* document, const String& text, bool selectInsertedText, bool insertedTextIsComposition)
+{
+ ASSERT(document);
+
+ Frame* frame = document->frame();
+ ASSERT(frame);
+
+ insertText(document, text, frame->selectionController()->selection(), selectInsertedText, insertedTextIsComposition);
+}
+
+void TypingCommand::insertText(Document* document, const String& text, const Selection& selectionForInsertion, bool selectInsertedText, bool insertedTextIsComposition)
+{
+ ASSERT(document);
+
+ RefPtr<Frame> frame = document->frame();
+ ASSERT(frame);
+
+ Selection currentSelection = frame->selectionController()->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 = new BeforeTextInsertedEvent(text);
+ startNode->rootEditableElement()->dispatchEvent(evt, ec, true);
+ 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 (changeSelection) {
+ lastTypingCommand->setStartingSelection(selectionForInsertion);
+ lastTypingCommand->setEndingSelection(selectionForInsertion);
+ }
+ lastTypingCommand->insertText(newText, selectInsertedText);
+ if (changeSelection) {
+ lastTypingCommand->setEndingSelection(currentSelection);
+ frame->selectionController()->setSelection(currentSelection);
+ }
+ return;
+ }
+
+ RefPtr<TypingCommand> cmd = new TypingCommand(document, InsertText, newText, selectInsertedText);
+ if (changeSelection) {
+ cmd->setStartingSelection(selectionForInsertion);
+ cmd->setEndingSelection(selectionForInsertion);
+ }
+ applyCommand(cmd);
+ if (changeSelection) {
+ cmd->setEndingSelection(currentSelection);
+ frame->selectionController()->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(new TypingCommand(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(new TypingCommand(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(new TypingCommand(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().isNone())
+ 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);
+ return;
+ case ForwardDeleteKey:
+ forwardDeleteKeyPressed(m_granularity);
+ 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()
+{
+ if (!document()->frame()->editor()->isContinuousSpellCheckingEnabled())
+ return;
+ // 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()->markMisspellingsAfterTypingToPosition(p1);
+ }
+}
+
+void TypingCommand::typingAddedToOpenCommand()
+{
+ markMisspellingsAfterTyping();
+ // Do not apply editing to the frame on the first time through.
+ // The frame will get told in the same way as all other commands.
+ // But since this command stays open and is used for additional typing,
+ // we need to tell the frame here as other commands are added.
+ if (m_applyEditing)
+ document()->frame()->editor()->appliedEditing(this);
+ m_applyEditing = true;
+}
+
+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.
+ int offset = 0;
+ int newline;
+ while ((newline = text.find('\n', offset)) != -1) {
+ if (newline != offset)
+ insertTextRunWithoutNewlines(text.substring(offset, newline - offset), false);
+ insertParagraphSeparator();
+ offset = newline + 1;
+ }
+ if (offset == 0)
+ insertTextRunWithoutNewlines(text, selectInsertedText);
+ else {
+ int 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()->typingStyle() && !m_commands.isEmpty()) {
+ EditCommand* lastCommand = m_commands.last().get();
+ if (lastCommand->isInsertTextCommand())
+ command = static_cast<InsertTextCommand*>(lastCommand);
+ }
+ if (!command) {
+ command = new InsertTextCommand(document());
+ applyCommandToComposite(command);
+ }
+ command->input(text, selectInsertedText);
+ typingAddedToOpenCommand();
+}
+
+void TypingCommand::insertLineBreak()
+{
+ applyCommandToComposite(new InsertLineBreakCommand(document()));
+ typingAddedToOpenCommand();
+}
+
+void TypingCommand::insertParagraphSeparator()
+{
+ applyCommandToComposite(new InsertParagraphSeparatorCommand(document()));
+ typingAddedToOpenCommand();
+}
+
+void TypingCommand::insertParagraphSeparatorInQuotedContent()
+{
+ applyCommandToComposite(new BreakBlockquoteCommand(document()));
+ typingAddedToOpenCommand();
+}
+
+void TypingCommand::deleteKeyPressed(TextGranularity granularity)
+{
+ Selection selectionToDelete;
+ Selection selectionAfterUndo;
+
+ switch (endingSelection().state()) {
+ case Selection::RANGE:
+ selectionToDelete = endingSelection();
+ selectionAfterUndo = selectionToDelete;
+ break;
+ case Selection::CARET: {
+ m_smartDelete = false;
+
+ SelectionController selectionController;
+ selectionController.setSelection(endingSelection());
+ selectionController.modify(SelectionController::EXTEND, SelectionController::BACKWARD, granularity);
+
+ // When the caret is at the start of the editable area in an empty list item, break out of the list item.
+ if (endingSelection().visibleStart().previous(true).isNull()) {
+ if (breakOutOfEmptyListItem()) {
+ typingAddedToOpenCommand();
+ return;
+ }
+ }
+
+ VisiblePosition visibleStart(endingSelection().visibleStart());
+ // 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.
+ selectionController.modify(SelectionController::EXTEND, SelectionController::BACKWARD, granularity);
+ // If the caret is just after a table, select the table and don't delete anything.
+ } else if (Node* table = isFirstPositionAfterTable(visibleStart)) {
+ setEndingSelection(Selection(Position(table, 0), endingSelection().start(), DOWNSTREAM));
+ typingAddedToOpenCommand();
+ return;
+ }
+
+ selectionToDelete = selectionController.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 Selection 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 Selection::NONE:
+ ASSERT_NOT_REACHED();
+ break;
+ }
+
+ if (selectionToDelete.isCaretOrRange() && document()->frame()->shouldDeleteSelection(selectionToDelete)) {
+ // 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();
+ }
+}
+
+void TypingCommand::forwardDeleteKeyPressed(TextGranularity granularity)
+{
+ Selection selectionToDelete;
+ Selection selectionAfterUndo;
+
+ switch (endingSelection().state()) {
+ case Selection::RANGE:
+ selectionToDelete = endingSelection();
+ selectionAfterUndo = selectionToDelete;
+ break;
+ case Selection::CARET: {
+ 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 selectionController;
+ selectionController.setSelection(endingSelection());
+ selectionController.modify(SelectionController::EXTEND, SelectionController::FORWARD, granularity);
+ 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.offset() == 0) {
+ setEndingSelection(Selection(endingSelection().end(), Position(downstreamEnd.node(), maxDeepOffset(downstreamEnd.node())), DOWNSTREAM));
+ typingAddedToOpenCommand();
+ return;
+ }
+
+ // deleting to end of paragraph when at end of paragraph needs to merge the next paragraph (if any)
+ if (granularity == ParagraphBoundary && selectionController.selection().isCaret() && isEndOfParagraph(selectionController.selection().visibleEnd()))
+ selectionController.modify(SelectionController::EXTEND, SelectionController::FORWARD, CharacterGranularity);
+
+ selectionToDelete = selectionController.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 Selection 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().offset() - selectionToDelete.start().offset();
+ else
+ extraCharacters = selectionToDelete.end().offset();
+ extent = Position(extent.node(), extent.offset() + extraCharacters);
+ }
+ selectionAfterUndo.setWithoutValidation(startingSelection().start(), extent);
+ }
+ break;
+ }
+ case Selection::NONE:
+ ASSERT_NOT_REACHED();
+ break;
+ }
+
+ if (selectionToDelete.isCaretOrRange() && document()->frame()->shouldDeleteSelection(selectionToDelete)) {
+ // make undo select what was deleted
+ setStartingSelection(selectionAfterUndo);
+ CompositeEditCommand::deleteSelection(selectionToDelete, m_smartDelete);
+ setSmartDelete(false);
+ typingAddedToOpenCommand();
+ }
+}
+
+void TypingCommand::deleteSelection(bool smartDelete)
+{
+ CompositeEditCommand::deleteSelection(smartDelete);
+ typingAddedToOpenCommand();
+}
+
+bool TypingCommand::preservesTypingStyle() const
+{
+ switch (m_commandType) {
+ case DeleteSelection:
+ case DeleteKey:
+ case ForwardDeleteKey:
+ case InsertParagraphSeparator:
+ case InsertLineBreak:
+ return true;
+ case InsertParagraphSeparatorInQuotedContent:
+ case InsertText:
+ return false;
+ }
+ ASSERT_NOT_REACHED();
+ return false;
+}
+
+bool TypingCommand::isTypingCommand() const
+{
+ return true;
+}
+
+} // namespace WebCore
diff --git a/WebCore/editing/TypingCommand.h b/WebCore/editing/TypingCommand.h
new file mode 100644
index 0000000..c939da4
--- /dev/null
+++ b/WebCore/editing/TypingCommand.h
@@ -0,0 +1,99 @@
+/*
+ * 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.
+ */
+
+#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
+ };
+
+ TypingCommand(Document*, ETypingCommand, const String& text = "", bool selectInsertedText = false, TextGranularity = CharacterGranularity);
+
+ static void deleteSelection(Document*, bool smartDelete = false);
+ static void deleteKeyPressed(Document*, bool smartDelete = false, TextGranularity = CharacterGranularity);
+ static void forwardDeleteKeyPressed(Document*, bool smartDelete = false, TextGranularity = CharacterGranularity);
+ static void insertText(Document*, const String&, bool selectInsertedText = false, bool insertedTextIsComposition = false);
+ static void insertText(Document*, const String&, const Selection&, 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*);
+
+ virtual void doApply();
+ virtual EditAction editingAction() const;
+
+ 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);
+ void forwardDeleteKeyPressed(TextGranularity);
+ void deleteSelection(bool);
+
+private:
+ bool smartDelete() { return m_smartDelete; }
+ void setSmartDelete(bool smartDelete) { m_smartDelete = smartDelete; }
+
+ virtual bool isTypingCommand() const;
+ virtual bool preservesTypingStyle() const;
+
+ void markMisspellingsAfterTyping();
+ void typingAddedToOpenCommand();
+
+ ETypingCommand m_commandType;
+ String m_textToInsert;
+ bool m_openForMoreTyping;
+ bool m_applyEditing;
+ bool m_selectInsertedText;
+ bool m_smartDelete;
+ TextGranularity m_granularity;
+
+ // 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/WebCore/editing/UnlinkCommand.cpp b/WebCore/editing/UnlinkCommand.cpp
new file mode 100644
index 0000000..0ba9a06
--- /dev/null
+++ b/WebCore/editing/UnlinkCommand.cpp
@@ -0,0 +1,50 @@
+/*
+ * 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, remove it.
+ if (!endingSelection().isRange())
+ return;
+
+ pushPartiallySelectedAnchorElementsDown();
+
+ HTMLAnchorElement* anchorElement = new HTMLAnchorElement(document());
+ removeStyledElement(anchorElement);
+}
+
+}
diff --git a/WebCore/editing/UnlinkCommand.h b/WebCore/editing/UnlinkCommand.h
new file mode 100644
index 0000000..9c88068
--- /dev/null
+++ b/WebCore/editing/UnlinkCommand.h
@@ -0,0 +1,42 @@
+/*
+ * 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 UnlinkCommand_h
+#define UnlinkCommand_h
+
+#include "CompositeEditCommand.h"
+
+namespace WebCore {
+
+class UnlinkCommand : public CompositeEditCommand {
+public:
+ UnlinkCommand(Document*);
+ virtual void doApply();
+ virtual EditAction editingAction() const { return EditActionUnlink; }
+};
+
+} // namespace WebCore
+
+#endif // UnlinkCommand_h
diff --git a/WebCore/editing/VisiblePosition.cpp b/WebCore/editing/VisiblePosition.cpp
new file mode 100644
index 0000000..7571035
--- /dev/null
+++ b/WebCore/editing/VisiblePosition.cpp
@@ -0,0 +1,359 @@
+/*
+ * 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.
+ */
+
+#include "config.h"
+#include "VisiblePosition.h"
+
+#include "CString.h"
+#include "Document.h"
+#include "Element.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>
+
+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.atStart())
+ 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);
+}
+
+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. Selection::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. Selection::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);
+}
+
+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& position)
+{
+ // 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();
+
+ 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())
+ 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;
+}
+
+UChar 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());
+ int offset = pos.offset();
+ if ((unsigned)offset >= textNode->length())
+ return 0;
+ return textNode->data()[offset];
+}
+
+IntRect VisiblePosition::caretRect() const
+{
+ if (!m_deepPosition.node() || !m_deepPosition.node()->renderer())
+ return IntRect();
+
+ return m_deepPosition.node()->renderer()->caretRect(m_deepPosition.offset(), m_affinity);
+}
+
+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.offset());
+}
+
+#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)
+{
+ Position s = rangeCompliantEquivalent(start);
+ Position e = rangeCompliantEquivalent(end);
+ return new Range(s.node()->document(), s.node(), s.offset(), e.node(), e.offset());
+}
+
+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.offset(), 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.offset(), code);
+ return code == 0;
+}
+
+Node *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/WebCore/editing/VisiblePosition.h b/WebCore/editing/VisiblePosition.h
new file mode 100644
index 0000000..d2e9bce
--- /dev/null
+++ b/WebCore/editing/VisiblePosition.h
@@ -0,0 +1,121 @@
+/*
+ * 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 VisiblePosition_h
+#define VisiblePosition_h
+
+#include "Position.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 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(); }
+
+ 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; }
+
+ // 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;
+
+ UChar characterAfter() const;
+ UChar characterBefore() const { return previous().characterAfter(); }
+
+ void debugPosition(const char* msg = "") const;
+
+ Element* rootEditableElement() const { return m_deepPosition.isNotNull() ? m_deepPosition.node()->rootEditableElement() : 0; }
+
+ IntRect caretRect() 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 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);
+
+Node *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/WebCore/editing/WrapContentsInDummySpanCommand.cpp b/WebCore/editing/WrapContentsInDummySpanCommand.cpp
new file mode 100644
index 0000000..89be9a4
--- /dev/null
+++ b/WebCore/editing/WrapContentsInDummySpanCommand.cpp
@@ -0,0 +1,77 @@
+/*
+ * 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 "WrapContentsInDummySpanCommand.h"
+
+#include "ApplyStyleCommand.h"
+#include "HTMLElement.h"
+
+namespace WebCore {
+
+WrapContentsInDummySpanCommand::WrapContentsInDummySpanCommand(Element* element)
+ : EditCommand(element->document()), m_element(element)
+{
+ ASSERT(m_element);
+}
+
+void WrapContentsInDummySpanCommand::doApply()
+{
+ ASSERT(m_element);
+
+ ExceptionCode ec = 0;
+
+ if (!m_dummySpan)
+ m_dummySpan = static_pointer_cast<HTMLElement>(createStyleSpanElement(document()));
+
+ while (m_element->firstChild()) {
+ m_dummySpan->appendChild(m_element->firstChild(), ec);
+ ASSERT(ec == 0);
+ }
+
+ m_element->appendChild(m_dummySpan.get(), ec);
+ ASSERT(ec == 0);
+}
+
+void WrapContentsInDummySpanCommand::doUnapply()
+{
+ ASSERT(m_element);
+ ASSERT(m_dummySpan);
+
+ ASSERT(m_element->firstChild() == m_dummySpan);
+ ASSERT(!m_element->firstChild()->nextSibling());
+
+ ExceptionCode ec = 0;
+
+ while (m_dummySpan->firstChild()) {
+ m_element->appendChild(m_dummySpan->firstChild(), ec);
+ ASSERT(ec == 0);
+ }
+
+ m_element->removeChild(m_dummySpan.get(), ec);
+ ASSERT(ec == 0);
+}
+
+} // namespace WebCore
diff --git a/WebCore/editing/WrapContentsInDummySpanCommand.h b/WebCore/editing/WrapContentsInDummySpanCommand.h
new file mode 100644
index 0000000..02574ba
--- /dev/null
+++ b/WebCore/editing/WrapContentsInDummySpanCommand.h
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+
+#ifndef WrapContentsInDummySpanCommand_h
+#define WrapContentsInDummySpanCommand_h
+
+#include "EditCommand.h"
+
+namespace WebCore {
+
+class WrapContentsInDummySpanCommand : public EditCommand {
+public:
+ WrapContentsInDummySpanCommand(Element*);
+
+ virtual void doApply();
+ virtual void doUnapply();
+
+private:
+ RefPtr<Element> m_element;
+ RefPtr<Element> m_dummySpan;
+};
+
+} // namespace WebCore
+
+#endif // WrapContentsInDummySpanCommand_h
diff --git a/WebCore/editing/htmlediting.cpp b/WebCore/editing/htmlediting.cpp
new file mode 100644
index 0000000..c2d1fb5
--- /dev/null
+++ b/WebCore/editing/htmlediting.cpp
@@ -0,0 +1,997 @@
+/*
+ * 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 "HTMLElement.h"
+#include "HTMLInterchange.h"
+#include "HTMLNames.h"
+#include "PositionIterator.h"
+#include "RenderObject.h"
+#include "RegularExpression.h"
+#include "Range.h"
+#include "Selection.h"
+#include "Text.h"
+#include "TextIterator.h"
+#include "VisiblePosition.h"
+#include "visible_units.h"
+
+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(buttonTag) &&
+ !node->hasTagName(embedTag) &&
+ !node->hasTagName(appletTag) &&
+ !node->hasTagName(selectTag) &&
+ !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.offset();
+ int offsetB = b.offset();
+
+ 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;
+}
+
+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 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();
+}
+
+bool isContentEditable(const Node* node)
+{
+ return node->isContentEditable();
+}
+
+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.atEnd()) {
+ p = p.next(UsingComposedCharacters);
+ 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.atStart()) {
+ p = p.previous(UsingComposedCharacters);
+ if (p.isCandidate() && p.downstream() != downstreamStart)
+ return p;
+ }
+ return Position();
+}
+
+VisiblePosition firstEditablePositionAfterPositionInRoot(const Position& position, Node* highestRoot)
+{
+ if (comparePositions(position, Position(highestRoot, 0)) == -1 && highestRoot->isContentEditable())
+ return VisiblePosition(Position(highestRoot, 0));
+
+ Position p = nextVisuallyDistinctCandidate(position);
+ Node* root = editableRootForPosition(position);
+ Node* shadowAncestor = root ? root->shadowAncestorNode() : 0;
+ if (p.isNull() && root && (shadowAncestor != root))
+ p = Position(shadowAncestor, maxDeepOffset(shadowAncestor));
+ while (p.isNotNull() && !isEditablePosition(p) && p.node()->isDescendantOf(highestRoot)) {
+ p = isAtomicNode(p.node()) ? positionAfterNode(p.node()) : nextVisuallyDistinctCandidate(p);
+
+ root = editableRootForPosition(position);
+ shadowAncestor = root ? root->shadowAncestorNode() : 0;
+ if (p.isNull() && root && (shadowAncestor != root))
+ p = Position(shadowAncestor, maxDeepOffset(shadowAncestor));
+ }
+
+ return VisiblePosition(p);
+}
+
+VisiblePosition lastEditablePositionBeforePositionInRoot(const Position& position, Node* highestRoot)
+{
+ if (comparePositions(position, Position(highestRoot, maxDeepOffset(highestRoot))) == 1)
+ return VisiblePosition(Position(highestRoot, maxDeepOffset(highestRoot)));
+
+ Position p = previousVisuallyDistinctCandidate(position);
+ Node* root = editableRootForPosition(position);
+ Node* shadowAncestor = root ? root->shadowAncestorNode() : 0;
+ if (p.isNull() && root && (shadowAncestor != root))
+ p = Position(shadowAncestor, 0);
+ while (p.isNotNull() && !isEditablePosition(p) && p.node()->isDescendantOf(highestRoot)) {
+ p = isAtomicNode(p.node()) ? positionBeforeNode(p.node()) : previousVisuallyDistinctCandidate(p);
+
+ root = editableRootForPosition(position);
+ shadowAncestor = root ? root->shadowAncestorNode() : 0;
+ if (p.isNull() && root && (shadowAncestor != root))
+ p = Position(shadowAncestor, 0);
+ }
+
+ return VisiblePosition(p);
+}
+
+// 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 enclosingNodeOfType(Position(node, 0), &isBlock);
+}
+
+Position rangeCompliantEquivalent(const Position& pos)
+{
+ if (pos.isNull())
+ return Position();
+
+ Node *node = pos.node();
+
+ if (pos.offset() <= 0) {
+ if (node->parentNode() && (editingIgnoresContent(node) || isTableElement(node)))
+ return positionBeforeNode(node);
+ return Position(node, 0);
+ }
+
+ if (node->offsetInCharacters())
+ return Position(node, min(node->maxCharacterOffset(), pos.offset()));
+
+ int maxCompliantOffset = node->childNodeCount();
+ if (pos.offset() > maxCompliantOffset) {
+ if (node->parentNode())
+ return positionAfterNode(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.offset() < maxCompliantOffset) && editingIgnoresContent(node)) {
+ ASSERT_NOT_REACHED();
+ return node->parentNode() ? positionBeforeNode(node) : Position(node, 0);
+ }
+
+ if (pos.offset() == maxCompliantOffset && (editingIgnoresContent(node) || isTableElement(node)))
+ return positionAfterNode(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 maxDeepOffset(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)
+{
+ static String twoSpaces(" ");
+ static String nbsp("\xa0");
+ static 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()
+{
+ static String nonBreakingSpaceString = String(&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;
+}
+
+// Checks if a string is a valid tag for the FormatBlockCommand function of execCommand. Expects lower case strings.
+bool validBlockTag(const String& blockTag)
+{
+ if (blockTag == "address" ||
+ blockTag == "blockquote" ||
+ blockTag == "dd" ||
+ blockTag == "div" ||
+ blockTag == "dl" ||
+ blockTag == "dt" ||
+ blockTag == "h1" ||
+ blockTag == "h2" ||
+ blockTag == "h3" ||
+ blockTag == "h4" ||
+ blockTag == "h5" ||
+ blockTag == "h6" ||
+ blockTag == "p" ||
+ blockTag == "pre")
+ return true;
+ return false;
+}
+
+static Node* firstInSpecialElement(const Position& pos)
+{
+ 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)
+{
+ 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 = positionBeforeNode(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 = positionAfterNode(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.offset() == maxDeepOffset(upstream.node()))
+ 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.offset() == 0)
+ return downstream.node();
+
+ return 0;
+}
+
+Position positionBeforeNode(const Node *node)
+{
+ return Position(node->parentNode(), node->nodeIndex());
+}
+
+Position positionAfterNode(const Node *node)
+{
+ return Position(node->parentNode(), node->nodeIndex() + 1);
+}
+
+bool isListElement(Node *n)
+{
+ return (n && (n->hasTagName(ulTag) || n->hasTagName(olTag) || n->hasTagName(dlTag)));
+}
+
+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 && !isContentEditable(n))
+ continue;
+ if (n->hasTagName(tagName))
+ return n;
+ if (n == root)
+ return 0;
+ }
+
+ return 0;
+}
+
+Node* enclosingNodeOfType(const Position& p, bool (*nodeIsOfType)(const Node*))
+{
+ 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 && !isContentEditable(n))
+ continue;
+ if ((*nodeIsOfType)(n))
+ return n;
+ if (n == root)
+ return 0;
+ }
+
+ return 0;
+}
+
+Node* enclosingTableCell(const Position& p)
+{
+ return 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;
+}
+
+Node* enclosingList(Node* node)
+{
+ if (!node)
+ return 0;
+
+ Node* root = highestEditableRoot(Position(node, 0));
+
+ for (Node* n = node->parentNode(); n; n = n->parentNode()) {
+ if (n->hasTagName(ulTag) || n->hasTagName(olTag))
+ return n;
+ if (n == root)
+ return 0;
+ }
+
+ return 0;
+}
+
+Node* 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 n;
+ if (n == root || isTableCell(n))
+ return 0;
+ }
+
+ return 0;
+}
+
+static Node* 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 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 n;
+ if (n->renderer() && n->renderer()->isListItem())
+ return 0;
+ }
+
+ return 0;
+}
+
+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(Position(listChildNode, 0));
+ VisiblePosition lastInListChild(Position(listChildNode, maxDeepOffset(listChildNode)));
+
+ if (firstInListChild != visiblePos || lastInListChild != visiblePos)
+ return 0;
+
+ if (embeddedSublist(listChildNode) || appendedSublist(listChildNode))
+ return 0;
+
+ return listChildNode;
+}
+
+Node* outermostEnclosingListChild(Node* node)
+{
+ Node* listNode = 0;
+ Node* nextNode = node;
+ while ((nextNode = enclosingListChild(nextNode)))
+ listNode = nextNode;
+ return listNode;
+}
+
+Node* outermostEnclosingList(Node* node)
+{
+ Node* listNode = 0;
+ Node* nextNode = node;
+ while ((nextNode = enclosingList(nextNode)))
+ listNode = nextNode;
+ return listNode;
+}
+
+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();
+}
+
+PassRefPtr<Element> createDefaultParagraphElement(Document *document)
+{
+ ExceptionCode ec = 0;
+ RefPtr<Element> element = document->createElementNS(xhtmlNamespaceURI, "div", ec);
+ ASSERT(ec == 0);
+ return element.release();
+}
+
+PassRefPtr<Element> createBreakElement(Document *document)
+{
+ ExceptionCode ec = 0;
+ RefPtr<Element> breakNode = document->createElementNS(xhtmlNamespaceURI, "br", ec);
+ ASSERT(ec == 0);
+ return breakNode.release();
+}
+
+PassRefPtr<Element> createOrderedListElement(Document *document)
+{
+ ExceptionCode ec = 0;
+ RefPtr<Element> element = document->createElementNS(xhtmlNamespaceURI, "ol", ec);
+ ASSERT(ec == 0);
+ return element.release();
+}
+
+PassRefPtr<Element> createUnorderedListElement(Document *document)
+{
+ ExceptionCode ec = 0;
+ RefPtr<Element> element = document->createElementNS(xhtmlNamespaceURI, "ul", ec);
+ ASSERT(ec == 0);
+ return element.release();
+}
+
+PassRefPtr<Element> createListItemElement(Document *document)
+{
+ ExceptionCode ec = 0;
+ RefPtr<Element> breakNode = document->createElementNS(xhtmlNamespaceURI, "li", ec);
+ ASSERT(ec == 0);
+ return breakNode.release();
+}
+
+PassRefPtr<Element> createElement(Document* document, const String& tagName)
+{
+ ExceptionCode ec = 0;
+ RefPtr<Element> breakNode = document->createElementNS(xhtmlNamespaceURI, tagName, ec);
+ ASSERT(ec == 0);
+ return breakNode.release();
+}
+
+bool isTabSpanNode(const Node *node)
+{
+ return (node && node->isElementNode() && static_cast<const Element *>(node)->getAttribute("class") == 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;
+}
+
+Position positionBeforeTabSpan(const Position& pos)
+{
+ Node *node = pos.node();
+ if (isTabSpanTextNode(node))
+ node = tabSpanNode(node);
+ else if (!isTabSpanNode(node))
+ return pos;
+
+ return positionBeforeNode(node);
+}
+
+PassRefPtr<Element> createTabSpanElement(Document* document, PassRefPtr<Node> tabTextNode)
+{
+ // make the span to hold the tab
+ ExceptionCode ec = 0;
+ RefPtr<Element> spanElement = document->createElementNS(xhtmlNamespaceURI, "span", ec);
+ ASSERT(ec == 0);
+ spanElement->setAttribute(classAttr, AppleTabSpanClass);
+ spanElement->setAttribute(styleAttr, "white-space:pre");
+
+ // add tab text to that span
+ if (!tabTextNode)
+ tabTextNode = document->createEditingTextNode("\t");
+ 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;
+}
+
+bool isMailBlockquote(const Node *node)
+{
+ if (!node || !node->isElementNode() && !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;
+}
+
+int caretMaxOffset(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.
+ if (r)
+ return r->caretMaxOffset();
+
+ if (n->isCharacterDataNode()) {
+ const CharacterData* c = static_cast<const CharacterData*>(n);
+ return static_cast<int>(c->length());
+ }
+ return 1;
+}
+
+bool lineBreakExistsAtPosition(const VisiblePosition& visiblePosition)
+{
+ if (visiblePosition.isNull())
+ return false;
+
+ Position downstream(visiblePosition.deepEquivalent().downstream());
+ return downstream.node()->hasTagName(brTag) ||
+ downstream.node()->isTextNode() && downstream.node()->renderer()->style()->preserveNewline() && visiblePosition.characterAfter() == '\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.
+Selection selectionForParagraphIteration(const Selection& original)
+{
+ Selection 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 = Selection(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 = Selection(startOfSelection.next(true), endOfSelection);
+
+ return newSelection;
+}
+
+
+int indexForVisiblePosition(VisiblePosition& visiblePosition)
+{
+ if (visiblePosition.isNull())
+ return 0;
+ Position p(visiblePosition.deepEquivalent());
+ RefPtr<Range> range = new Range(p.node()->document(), Position(p.node()->document(), 0), rangeCompliantEquivalent(p));
+ return TextIterator::rangeLength(range.get(), true);
+}
+
+PassRefPtr<Range> avoidIntersectionWithNode(const Range* range, Node* node)
+{
+ if (!range || range->isDetached())
+ return 0;
+
+ Document* document = range->ownerDocument();
+
+ ExceptionCode ec = 0;
+ Node* startContainer = range->startContainer(ec);
+ ASSERT(ec == 0);
+ int startOffset = range->startOffset(ec);
+ ASSERT(ec == 0);
+ Node* endContainer = range->endContainer(ec);
+ ASSERT(ec == 0);
+ int endOffset = range->endOffset(ec);
+ ASSERT(ec == 0);
+
+ ASSERT(startContainer);
+ 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 new Range(document, startContainer, startOffset, endContainer, endOffset);
+}
+
+Selection avoidIntersectionWithNode(const Selection& selection, Node* node)
+{
+ if (selection.isNone())
+ return Selection(selection);
+
+ Selection 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/WebCore/editing/htmlediting.h b/WebCore/editing/htmlediting.h
new file mode 100644
index 0000000..62138e5
--- /dev/null
+++ b/WebCore/editing/htmlediting.h
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2004, 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 htmlediting_h
+#define htmlediting_h
+
+#include <wtf/Forward.h>
+#include "HTMLNames.h"
+
+namespace WebCore {
+
+class Document;
+class Element;
+class Node;
+class Position;
+class Range;
+class Selection;
+class String;
+class VisiblePosition;
+
+Position rangeCompliantEquivalent(const Position&);
+Position rangeCompliantEquivalent(const VisiblePosition&);
+int maxDeepOffset(const Node*);
+bool isAtomicNode(const Node*);
+bool editingIgnoresContent(const Node*);
+bool canHaveChildrenForEditing(const Node*);
+Node* highestEditableRoot(const Position&);
+VisiblePosition firstEditablePositionAfterPositionInRoot(const Position&, Node*);
+VisiblePosition lastEditablePositionBeforePositionInRoot(const Position&, Node*);
+int comparePositions(const Position&, const Position&);
+Node* lowestEditableAncestor(Node*);
+bool isContentEditable(const Node*);
+Position nextCandidate(const Position&);
+Position nextVisuallyDistinctCandidate(const Position&);
+Position previousCandidate(const Position&);
+Position previousVisuallyDistinctCandidate(const Position&);
+bool isEditablePosition(const Position&);
+bool isRichlyEditablePosition(const Position&);
+Element* editableRootForPosition(const Position&);
+bool isBlock(const Node*);
+Node* enclosingBlock(Node*);
+
+String stringWithRebalancedWhitespace(const String&, bool, bool);
+const String& nonBreakingSpaceString();
+
+//------------------------------------------------------------------------------------------
+
+Position positionBeforeNode(const Node*);
+Position positionAfterNode(const Node*);
+
+PassRefPtr<Range> avoidIntersectionWithNode(const Range*, Node*);
+Selection avoidIntersectionWithNode(const Selection&, Node*);
+
+bool isSpecialElement(const Node*);
+bool validBlockTag(const String&);
+
+PassRefPtr<Element> createDefaultParagraphElement(Document*);
+PassRefPtr<Element> createBreakElement(Document*);
+PassRefPtr<Element> createOrderedListElement(Document*);
+PassRefPtr<Element> createUnorderedListElement(Document*);
+PassRefPtr<Element> createListItemElement(Document*);
+PassRefPtr<Element> createElement(Document*, const String&);
+
+bool isTabSpanNode(const Node*);
+bool isTabSpanTextNode(const Node*);
+Node* tabSpanNode(const Node*);
+Position positionBeforeTabSpan(const Position&);
+PassRefPtr<Element> createTabSpanElement(Document*);
+PassRefPtr<Element> createTabSpanElement(Document*, PassRefPtr<Node> tabTextNode);
+PassRefPtr<Element> createTabSpanElement(Document*, const String& tabText);
+
+bool isNodeRendered(const Node*);
+bool isMailBlockquote(const Node*);
+Node* nearestMailBlockquote(const Node*);
+int caretMinOffset(const Node*);
+int caretMaxOffset(const Node*);
+
+//------------------------------------------------------------------------------------------
+
+bool isTableStructureNode(const Node*);
+PassRefPtr<Element> createBlockPlaceholderElement(Document*);
+
+bool isFirstVisiblePositionInSpecialElement(const Position&);
+Position positionBeforeContainingSpecialElement(const Position&, Node** containingSpecialElement=0);
+bool isLastVisiblePositionInSpecialElement(const Position&);
+Position positionAfterContainingSpecialElement(const Position&, Node** containingSpecialElement=0);
+Position positionOutsideContainingSpecialElement(const Position&, Node** containingSpecialElement=0);
+Node* isLastPositionBeforeTable(const VisiblePosition&);
+Node* isFirstPositionAfterTable(const VisiblePosition&);
+
+Node* enclosingNodeWithTag(const Position&, const QualifiedName&);
+Node* enclosingNodeOfType(const Position&, bool (*nodeIsOfType)(const Node*));
+Node* enclosingTableCell(const Position&);
+Node* enclosingEmptyListItem(const VisiblePosition&);
+Node* enclosingAnchorElement(const Position&);
+bool isListElement(Node*);
+Node* enclosingList(Node*);
+Node* outermostEnclosingList(Node*);
+Node* enclosingListChild(Node*);
+Node* highestAncestor(Node*);
+bool isTableElement(Node*);
+bool isTableCell(const Node*);
+
+bool lineBreakExistsAtPosition(const VisiblePosition&);
+
+Selection selectionForParagraphIteration(const Selection&);
+
+int indexForVisiblePosition(VisiblePosition&);
+
+}
+
+#endif
diff --git a/WebCore/editing/mac/EditorMac.mm b/WebCore/editing/mac/EditorMac.mm
new file mode 100644
index 0000000..f7ed539
--- /dev/null
+++ b/WebCore/editing/mac/EditorMac.mm
@@ -0,0 +1,113 @@
+/*
+ * 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.
+ */
+
+#import "config.h"
+#import "Editor.h"
+
+#import "ClipboardMac.h"
+#import "EditorClient.h"
+
+namespace WebCore {
+
+extern "C" {
+
+// Kill ring calls. Would be better to use NSKillRing.h, but that's not available as API or SPI.
+
+void _NSInitializeKillRing();
+void _NSAppendToKillRing(NSString *);
+void _NSPrependToKillRing(NSString *);
+NSString *_NSYankFromKillRing();
+void _NSNewKillRingSequence();
+void _NSSetKillRingToYankedState();
+
+}
+
+PassRefPtr<Clipboard> Editor::newGeneralClipboard(ClipboardAccessPolicy policy)
+{
+ return new ClipboardMac(false, [NSPasteboard generalPasteboard], policy);
+}
+
+NSString* Editor::userVisibleString(NSURL* nsURL)
+{
+ if (client())
+ return client()->userVisibleString(nsURL);
+ return nil;
+}
+
+static void initializeKillRingIfNeeded()
+{
+ static bool initializedKillRing = false;
+ if (!initializedKillRing) {
+ initializedKillRing = true;
+ _NSInitializeKillRing();
+ }
+}
+
+void Editor::appendToKillRing(const String& string)
+{
+ initializeKillRingIfNeeded();
+ _NSAppendToKillRing(string);
+}
+
+void Editor::prependToKillRing(const String& string)
+{
+ initializeKillRingIfNeeded();
+ _NSPrependToKillRing(string);
+}
+
+String Editor::yankFromKillRing()
+{
+ initializeKillRingIfNeeded();
+ return _NSYankFromKillRing();
+}
+
+void Editor::startNewKillRingSequence()
+{
+ initializeKillRingIfNeeded();
+ _NSNewKillRingSequence();
+}
+
+void Editor::setKillRingToYankedState()
+{
+ initializeKillRingIfNeeded();
+ _NSSetKillRingToYankedState();
+}
+
+void Editor::showFontPanel()
+{
+ [[NSFontManager sharedFontManager] orderFrontFontPanel:nil];
+}
+
+void Editor::showStylesPanel()
+{
+ [[NSFontManager sharedFontManager] orderFrontStylesPanel:nil];
+}
+
+void Editor::showColorPanel()
+{
+ [[NSApplication sharedApplication] orderFrontColorPanel:nil];
+}
+
+} // namespace WebCore
diff --git a/WebCore/editing/mac/SelectionControllerMac.mm b/WebCore/editing/mac/SelectionControllerMac.mm
new file mode 100644
index 0000000..987d371
--- /dev/null
+++ b/WebCore/editing/mac/SelectionControllerMac.mm
@@ -0,0 +1,65 @@
+/*
+ * 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 "Document.h"
+#import "Frame.h"
+#import "FrameView.h"
+#import "RenderView.h"
+#import "Selection.h"
+
+#import <ApplicationServices/ApplicationServices.h>
+
+namespace WebCore {
+
+void SelectionController::notifyAccessibilityForSelectionChange()
+{
+ if (AXObjectCache::accessibilityEnabled() && m_sel.start().isNotNull() && m_sel.end().isNotNull())
+ m_frame->document()->axObjectCache()->postNotification(m_sel.start().node()->renderer(), "AXSelectedTextChanged");
+
+ // if zoom feature is enabled, insertion point changes should update the zoom
+ if (UAZoomEnabled() && m_sel.isCaret() && m_sel.start().node()) {
+ RenderView *renderView = static_cast<RenderView*>(m_sel.start().node()->renderer());
+ if (renderView) {
+ IntRect selectionRect = caretRect();
+ IntRect viewRect = renderView->viewRect();
+ FrameView* frameView = renderView->view()->frameView();
+ NSView *view = frameView->getDocumentView();
+ if (view) {
+ selectionRect.setLocation(frameView->convertToScreenCoordinate(view, selectionRect.location()));
+ viewRect.setLocation(frameView->convertToScreenCoordinate(view, viewRect.location()));
+ CGRect cgCaretRect = CGRectMake(selectionRect.x(), selectionRect.y(), selectionRect.width(), selectionRect.height());
+ CGRect cgViewRect = CGRectMake(viewRect.x(), viewRect.y(), viewRect.width(), viewRect.height());
+ (void)UAZoomChangeFocus(&cgViewRect, &cgCaretRect, kUAZoomFocusTypeInsertionPoint);
+ }
+ }
+ }
+}
+
+
+} // namespace WebCore
diff --git a/WebCore/editing/markup.cpp b/WebCore/editing/markup.cpp
new file mode 100644
index 0000000..4dad8ca
--- /dev/null
+++ b/WebCore/editing/markup.cpp
@@ -0,0 +1,1084 @@
+/*
+ * Copyright (C) 2004, 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 "markup.h"
+
+#include "CDATASection.h"
+#include "CSSComputedStyleDeclaration.h"
+#include "CSSPropertyNames.h"
+#include "CSSRule.h"
+#include "CSSRuleList.h"
+#include "CSSStyleRule.h"
+#include "CSSStyleSelector.h"
+#include "CSSValueKeywords.h"
+#include "Comment.h"
+#include "DeleteButtonController.h"
+#include "Document.h"
+#include "DocumentFragment.h"
+#include "DocumentType.h"
+#include "Editor.h"
+#include "Frame.h"
+#include "HTMLElement.h"
+#include "HTMLNames.h"
+#include "InlineTextBox.h"
+#include "Logging.h"
+#include "ProcessingInstruction.h"
+#include "QualifiedName.h"
+#include "Range.h"
+#include "Selection.h"
+#include "TextIterator.h"
+#include "htmlediting.h"
+#include "visible_units.h"
+
+using namespace std;
+
+namespace WebCore {
+
+using namespace HTMLNames;
+
+static inline bool shouldSelfClose(const Node *node);
+
+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 appendAttributeValue(Vector<UChar>& result, const String& attr)
+{
+ const UChar* uchars = attr.characters();
+ unsigned len = attr.length();
+ unsigned lastCopiedFrom = 0;
+
+ static const String ampEntity("&amp;");
+ static const String ltEntity("&lt;");
+ static const String quotEntity("&quot;");
+
+ for (unsigned i = 0; i < len; ++i) {
+ UChar c = uchars[i];
+ switch (c) {
+ case '&':
+ result.append(uchars + lastCopiedFrom, i - lastCopiedFrom);
+ append(result, ampEntity);
+ lastCopiedFrom = i + 1;
+ break;
+ case '<':
+ result.append(uchars + lastCopiedFrom, i - lastCopiedFrom);
+ append(result, ltEntity);
+ lastCopiedFrom = i + 1;
+ break;
+ case '"':
+ result.append(uchars + lastCopiedFrom, i - lastCopiedFrom);
+ append(result, quotEntity);
+ lastCopiedFrom = i + 1;
+ }
+ }
+
+ result.append(uchars + lastCopiedFrom, len - lastCopiedFrom);
+}
+
+static void append(Vector<UChar>& vector, const char* string)
+{
+ const char* p = string;
+ while (*p) {
+ UChar c = *p++;
+ vector.append(c);
+ }
+}
+
+static String escapeContentText(const String& in)
+{
+ Vector<UChar> s;
+
+ unsigned len = in.length();
+ unsigned lastCopiedFrom = 0;
+
+ s.reserveCapacity(len);
+
+ const UChar* characters = in.characters();
+
+ for (unsigned i = 0; i < len; ++i) {
+ UChar c = characters[i];
+ if ((c == '&') | (c == '<')) {
+ s.append(characters + lastCopiedFrom, i - lastCopiedFrom);
+ if (c == '&')
+ append(s, "&amp;");
+ else
+ append(s, "&lt;");
+ lastCopiedFrom = i + 1;
+ }
+ }
+
+ s.append(characters + lastCopiedFrom, len - lastCopiedFrom);
+
+ return String::adopt(s);
+}
+
+static void appendEscapedContent(Vector<UChar>& result, pair<const UChar*, size_t> range)
+{
+ const UChar* uchars = range.first;
+ unsigned len = range.second;
+ unsigned lastCopiedFrom = 0;
+
+ static const String ampEntity("&amp;");
+ static const String ltEntity("&lt;");
+
+ for (unsigned i = 0; i < len; ++i) {
+ UChar c = uchars[i];
+ if ((c == '&') | (c == '<')) {
+ result.append(uchars + lastCopiedFrom, i - lastCopiedFrom);
+ if (c == '&')
+ append(result, ampEntity);
+ else
+ append(result, ltEntity);
+ lastCopiedFrom = i + 1;
+ }
+ }
+
+ result.append(uchars + lastCopiedFrom, len - lastCopiedFrom);
+}
+
+static void appendQuotedURLAttributeValue(Vector<UChar>& result, const String& urlString)
+{
+ UChar quoteChar = '\"';
+ String strippedURLString = urlString.stripWhiteSpace();
+ if (protocolIs(strippedURLString, "javascript")) {
+ // 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 escapes spaces and other special characters.
+ result.append(quoteChar);
+ appendAttributeValue(result, urlString);
+ result.append(quoteChar);
+}
+
+static String 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 inline pair<const UChar*, size_t> ucharRange(const Node *node, const Range *range)
+{
+ 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;
+ }
+ }
+
+ return make_pair(characters, length);
+}
+
+static inline void appendUCharRange(Vector<UChar>& result, const pair<const UChar*, size_t> range)
+{
+ result.append(range.first, range.second);
+}
+
+static String 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);
+ Range r(node->document(), start, end);
+ return plainText(&r);
+}
+
+static PassRefPtr<CSSMutableStyleDeclaration> styleFromMatchedRulesForElement(Element* element, bool authorOnly = true)
+{
+ RefPtr<CSSMutableStyleDeclaration> style = new CSSMutableStyleDeclaration();
+ 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();
+}
+
+static void removeEnclosingMailBlockquoteStyle(CSSMutableStyleDeclaration* style, Node* node)
+{
+ Node* blockquote = nearestMailBlockquote(node);
+ if (!blockquote || !blockquote->parentNode())
+ return;
+
+ RefPtr<CSSMutableStyleDeclaration> parentStyle = Position(blockquote->parentNode(), 0).computedStyle()->copyInheritableProperties();
+ RefPtr<CSSMutableStyleDeclaration> blockquoteStyle = Position(blockquote, 0).computedStyle()->copyInheritableProperties();
+ parentStyle->diff(blockquoteStyle.get());
+ blockquoteStyle->diff(style);
+}
+
+static void removeDefaultStyles(CSSMutableStyleDeclaration* style, Document* document)
+{
+ if (!document || !document->documentElement())
+ return;
+
+ RefPtr<CSSMutableStyleDeclaration> documentStyle = computedStyle(document->documentElement())->copyInheritableProperties();
+ documentStyle->diff(style);
+}
+
+static bool shouldAddNamespaceElem(const Element* elem)
+{
+ // Don't add namespace attribute if it is already defined for this elem.
+ const AtomicString& prefix = elem->prefix();
+ AtomicString attr = !prefix.isEmpty() ? "xmlns:" + prefix : "xmlns";
+ return !elem->hasAttribute(attr);
+}
+
+static bool shouldAddNamespaceAttr(const Attribute* attr, HashMap<AtomicStringImpl*, AtomicStringImpl*>& namespaces)
+{
+ // Don't add namespace attributes twice
+ static const AtomicString xmlnsURI = "http://www.w3.org/2000/xmlns/";
+ static const QualifiedName xmlnsAttr(nullAtom, "xmlns", xmlnsURI);
+ if (attr->name() == xmlnsAttr) {
+ namespaces.set(emptyAtom.impl(), attr->value().impl());
+ return false;
+ }
+
+ QualifiedName xmlnsPrefixAttr("xmlns", attr->localName(), xmlnsURI);
+ if (attr->name() == xmlnsPrefixAttr) {
+ namespaces.set(attr->localName().impl(), attr->value().impl());
+ return false;
+ }
+
+ return true;
+}
+
+static void appendNamespace(Vector<UChar>& result, const AtomicString& prefix, const AtomicString& ns, HashMap<AtomicStringImpl*, AtomicStringImpl*>& namespaces)
+{
+ if (ns.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 != ns.impl()) {
+ namespaces.set(pre, ns.impl());
+ static const String xmlns("xmlns");
+ result.append(' ');
+ append(result, xmlns);
+ if (!prefix.isEmpty()) {
+ result.append(':');
+ append(result, prefix);
+ }
+
+ result.append('=');
+ result.append('"');
+ appendAttributeValue(result, ns);
+ result.append('"');
+ }
+}
+
+static void appendStartMarkup(Vector<UChar>& result, const Node *node, const Range *range, EAnnotateForInterchange annotate, bool convertBlocksToInlines = false, HashMap<AtomicStringImpl*, AtomicStringImpl*>* namespaces = 0)
+{
+ bool documentIsHTML = node->document()->isHTMLDocument();
+ switch (node->nodeType()) {
+ case Node::TEXT_NODE: {
+ if (Node* parent = node->parentNode()) {
+ if (parent->hasTagName(listingTag)
+ || parent->hasTagName(scriptTag)
+ || parent->hasTagName(styleTag)
+ || parent->hasTagName(textareaTag)
+ || parent->hasTagName(xmpTag)) {
+ appendUCharRange(result, ucharRange(node, range));
+ break;
+ }
+ }
+ if (!annotate) {
+ appendEscapedContent(result, ucharRange(node, range));
+ break;
+ }
+
+ bool useRenderedText = !enclosingNodeWithTag(Position(const_cast<Node*>(node), 0), selectTag);
+ String markup = escapeContentText(useRenderedText ? renderedText(node, range) : stringValueForRange(node, range));
+ if (annotate)
+ markup = convertHTMLTextToInterchangeFormat(markup, static_cast<const Text*>(node));
+ append(result, markup);
+ break;
+ }
+ case Node::COMMENT_NODE:
+ append(result, static_cast<const Comment*>(node)->toString());
+ break;
+ case Node::DOCUMENT_NODE:
+ case Node::DOCUMENT_FRAGMENT_NODE:
+ break;
+ case Node::DOCUMENT_TYPE_NODE:
+ append(result, static_cast<const DocumentType*>(node)->toString());
+ break;
+ case Node::PROCESSING_INSTRUCTION_NODE:
+ append(result, static_cast<const ProcessingInstruction*>(node)->toString());
+ break;
+ case Node::ELEMENT_NODE: {
+ result.append('<');
+ const Element* el = static_cast<const Element*>(node);
+ bool convert = convertBlocksToInlines & isBlock(const_cast<Node*>(node));
+ append(result, el->nodeNamePreservingCase());
+ NamedAttrMap *attrs = el->attributes();
+ unsigned length = attrs->length();
+ if (!documentIsHTML && namespaces && shouldAddNamespaceElem(el))
+ appendNamespace(result, el->prefix(), el->namespaceURI(), *namespaces);
+
+ for (unsigned int i = 0; i < length; i++) {
+ Attribute *attr = attrs->attributeItem(i);
+ // We'll handle the style attribute separately, below.
+ if (attr->name() == styleAttr && el->isHTMLElement() && (annotate || convert))
+ continue;
+ result.append(' ');
+
+ if (documentIsHTML)
+ append(result, attr->name().localName());
+ else
+ append(result, attr->name().toString());
+
+ result.append('=');
+
+ if (el->isURLAttribute(attr))
+ appendQuotedURLAttributeValue(result, attr->value());
+ else {
+ result.append('\"');
+ appendAttributeValue(result, attr->value());
+ result.append('\"');
+ }
+
+ if (!documentIsHTML && namespaces && shouldAddNamespaceAttr(attr, *namespaces))
+ appendNamespace(result, attr->prefix(), attr->namespaceURI(), *namespaces);
+ }
+
+ if (el->isHTMLElement() && (annotate || convert)) {
+ Element* element = const_cast<Element*>(el);
+ RefPtr<CSSMutableStyleDeclaration> style = static_cast<HTMLElement*>(element)->getInlineStyleDecl()->copy();
+ if (annotate) {
+ RefPtr<CSSMutableStyleDeclaration> styleFromMatchedRules = styleFromMatchedRulesForElement(const_cast<Element*>(el));
+ style->merge(styleFromMatchedRules.get());
+ }
+ if (convert)
+ style->setProperty(CSS_PROP_DISPLAY, CSS_VAL_INLINE, true);
+ if (style->length() > 0) {
+ static const String stylePrefix(" style=\"");
+ append(result, stylePrefix);
+ appendAttributeValue(result, style->cssText());
+ result.append('\"');
+ }
+ }
+
+ if (shouldSelfClose(el)) {
+ if (el->isHTMLElement())
+ result.append(' '); // XHTML 1.0 <-> HTML compatibility.
+ result.append('/');
+ }
+ result.append('>');
+ break;
+ }
+ case Node::CDATA_SECTION_NODE:
+ append(result, static_cast<const CDATASection*>(node)->toString());
+ 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;
+ }
+}
+
+static String getStartMarkup(const Node *node, const Range *range, EAnnotateForInterchange annotate, bool convertBlocksToInlines = false, HashMap<AtomicStringImpl*, AtomicStringImpl*>* namespaces = 0)
+{
+ Vector<UChar> result;
+ appendStartMarkup(result, node, range, annotate, convertBlocksToInlines, namespaces);
+ return String::adopt(result);
+}
+
+static inline bool doesHTMLForbidEndTag(const Node *node)
+{
+ if (node->isHTMLElement()) {
+ const HTMLElement* htmlElt = static_cast<const HTMLElement*>(node);
+ return (htmlElt->endTagRequirement() == TagStatusForbidden);
+ }
+ return false;
+}
+
+// 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.
+static inline bool shouldSelfClose(const Node *node)
+{
+ if (node->document()->isHTMLDocument())
+ return false;
+ if (node->hasChildNodes())
+ return false;
+ if (node->isHTMLElement() && !doesHTMLForbidEndTag(node))
+ return false;
+ return true;
+}
+
+static void appendEndMarkup(Vector<UChar>& result, const Node* node)
+{
+ if (!node->isElementNode() || shouldSelfClose(node) || (!node->hasChildNodes() && doesHTMLForbidEndTag(node)))
+ return;
+
+ result.append('<');
+ result.append('/');
+ append(result, static_cast<const Element*>(node)->nodeNamePreservingCase());
+ result.append('>');
+}
+
+static String getEndMarkup(const Node *node)
+{
+ Vector<UChar> result;
+ appendEndMarkup(result, node);
+ return String::adopt(result);
+}
+
+static void appendMarkup(Vector<UChar>& result, Node* startNode, bool onlyIncludeChildren, Vector<Node*>* nodes, const HashMap<AtomicStringImpl*, AtomicStringImpl*>* namespaces = 0)
+{
+ HashMap<AtomicStringImpl*, AtomicStringImpl*> namespaceHash;
+ if (namespaces)
+ namespaceHash = *namespaces;
+
+ if (!onlyIncludeChildren) {
+ if (nodes)
+ nodes->append(startNode);
+
+ appendStartMarkup(result,startNode, 0, DoNotAnnotateForInterchange, false, &namespaceHash);
+ }
+ // print children
+ if (!(startNode->document()->isHTMLDocument() && doesHTMLForbidEndTag(startNode)))
+ for (Node* current = startNode->firstChild(); current; current = current->nextSibling())
+ appendMarkup(result, current, false, nodes, &namespaceHash);
+
+ // Print my ending tag
+ if (!onlyIncludeChildren)
+ appendEndMarkup(result, startNode);
+}
+
+static void completeURLs(Node* node, const String& baseURL)
+{
+ Vector<AttributeChange> changes;
+
+ KURL parsedBaseURL(baseURL);
+
+ Node* end = node->traverseNextSibling();
+ for (Node* n = node; n != end; n = n->traverseNextNode()) {
+ if (n->isElementNode()) {
+ Element* e = static_cast<Element*>(n);
+ NamedAttrMap* attrs = e->attributes();
+ unsigned length = attrs->length();
+ for (unsigned i = 0; i < length; i++) {
+ Attribute* attr = attrs->attributeItem(i);
+ if (e->isURLAttribute(attr))
+ changes.append(AttributeChange(e, attr->name(), KURL(parsedBaseURL, attr->value()).string()));
+ }
+ }
+ }
+
+ size_t numChanges = changes.size();
+ for (size_t i = 0; i < numChanges; ++i)
+ changes[i].apply();
+}
+
+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 propertyMissingOrEqualToNone(CSSMutableStyleDeclaration* 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() == CSS_VAL_NONE;
+}
+
+static bool elementHasTextDecorationProperty(const Node* node)
+{
+ RefPtr<CSSMutableStyleDeclaration> style = styleFromMatchedRulesAndInlineDecl(node);
+ if (!style)
+ return false;
+ return !propertyMissingOrEqualToNone(style.get(), CSS_PROP_TEXT_DECORATION);
+}
+
+String joinMarkups(const Vector<String> preMarkups, const Vector<String>& postMarkups)
+{
+ size_t length = 0;
+
+ size_t preCount = preMarkups.size();
+ for (size_t i = 0; i < preCount; ++i)
+ length += preMarkups[i].length();
+
+ size_t postCount = postMarkups.size();
+ for (size_t i = 0; i < postCount; ++i)
+ length += postMarkups[i].length();
+
+ Vector<UChar> result;
+ result.reserveCapacity(length);
+
+ for (size_t i = preCount; i > 0; --i)
+ append(result, preMarkups[i - 1]);
+
+ for (size_t i = 0; i < postCount; ++i)
+ append(result, postMarkups[i]);
+
+ return String::adopt(result);
+}
+
+// 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 annotate, bool convertBlocksToInlines)
+{
+ static const String interchangeNewlineString = String("<br class=\"") + AppleInterchangeNewline + "\">";
+
+ if (!range || range->isDetached())
+ 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 (deleteButton)
+ deleteButton->disable();
+
+ ExceptionCode ec = 0;
+ bool collapsed = updatedRange->collapsed(ec);
+ ASSERT(ec == 0);
+ if (collapsed)
+ return "";
+ Node* commonAncestor = updatedRange->commonAncestorContainer(ec);
+ ASSERT(ec == 0);
+ if (!commonAncestor)
+ return "";
+
+ document->updateLayoutIgnorePendingStylesheets();
+
+ Vector<String> markups;
+ Vector<String> preMarkups;
+ Node* pastEnd = updatedRange->pastEndNode();
+ Node* lastClosed = 0;
+ Vector<Node*> ancestorsToClose;
+
+ Node* startNode = updatedRange->startNode();
+ VisiblePosition visibleStart(updatedRange->startPosition(), VP_DEFAULT_AFFINITY);
+ VisiblePosition visibleEnd(updatedRange->endPosition(), VP_DEFAULT_AFFINITY);
+ if (annotate && needInterchangeNewlineAfter(visibleStart)) {
+ if (visibleStart == visibleEnd.previous()) {
+ if (deleteButton)
+ deleteButton->enable();
+ return interchangeNewlineString;
+ }
+
+ markups.append(interchangeNewlineString);
+ startNode = visibleStart.next().deepEquivalent().node();
+ }
+
+ Node* next;
+ for (Node* n = startNode; n != pastEnd; n = next) {
+ next = n->traverseNextNode();
+ bool skipDescendants = false;
+ bool addMarkupForNode = true;
+
+ if (!n->renderer() && !enclosingNodeWithTag(Position(n, 0), selectTag)) {
+ skipDescendants = true;
+ addMarkupForNode = false;
+ next = n->traverseNextSibling();
+ // Don't skip over pastEnd.
+ if (pastEnd && pastEnd->isDescendantOf(n))
+ next = pastEnd;
+ }
+
+ if (isBlock(n) && canHaveChildrenForEditing(n) && next == pastEnd)
+ // Don't write out empty block containers that aren't fully selected.
+ continue;
+
+ // Add the node to the markup.
+ if (addMarkupForNode) {
+ markups.append(getStartMarkup(n, updatedRange.get(), annotate));
+ if (nodes)
+ nodes->append(n);
+ }
+
+ if (n->firstChild() == 0 || skipDescendants) {
+ // Node has no children, or we are skipping it's descendants, add its close tag now.
+ if (addMarkupForNode) {
+ markups.append(getEndMarkup(n));
+ lastClosed = n;
+ }
+
+ // Check if the node is the last leaf of a tree.
+ if (!n->nextSibling() || next == pastEnd) {
+ if (!ancestorsToClose.isEmpty()) {
+ // Close up the ancestors.
+ do {
+ 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.
+ markups.append(getEndMarkup(ancestor));
+ lastClosed = ancestor;
+ ancestorsToClose.removeLast();
+ } while (!ancestorsToClose.isEmpty());
+ }
+
+ // Surround the currently accumulated markup with markup for ancestors we never opened as we leave the subtree(s) rooted at those ancestors.
+ Node* nextParent = next ? next->parentNode() : 0;
+ if (next != pastEnd && n != nextParent) {
+ Node* lastAncestorClosedOrSelf = n->isDescendantOf(lastClosed) ? lastClosed : n;
+ for (Node *parent = lastAncestorClosedOrSelf->parent(); parent != 0 && 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));
+ preMarkups.append(getStartMarkup(parent, updatedRange.get(), annotate));
+ markups.append(getEndMarkup(parent));
+ if (nodes)
+ nodes->append(parent);
+ lastClosed = parent;
+ }
+ }
+ }
+ } else if (addMarkupForNode && !skipDescendants)
+ // We added markup for this node, and we're descending into it. Set it to close eventually.
+ ancestorsToClose.append(n);
+ }
+
+ // Include ancestors that aren't completely inside the range but are required to retain
+ // the structure and appearance of the copied markup.
+ Node* specialCommonAncestor = 0;
+ Node* commonAncestorBlock = commonAncestor ? enclosingBlock(commonAncestor) : 0;
+ if (annotate && commonAncestorBlock) {
+ if (commonAncestorBlock->hasTagName(tbodyTag) || commonAncestorBlock->hasTagName(trTag)) {
+ Node* table = commonAncestorBlock->parentNode();
+ while (table && !table->hasTagName(tableTag))
+ table = table->parentNode();
+ if (table)
+ specialCommonAncestor = table;
+ } else if (commonAncestorBlock->hasTagName(listingTag)
+ || commonAncestorBlock->hasTagName(olTag)
+ || commonAncestorBlock->hasTagName(preTag)
+ || commonAncestorBlock->hasTagName(tableTag)
+ || commonAncestorBlock->hasTagName(ulTag)
+ || commonAncestorBlock->hasTagName(xmpTag))
+ specialCommonAncestor = commonAncestorBlock;
+ }
+
+ bool selectedOneOrMoreParagraphs = startOfParagraph(visibleStart) != startOfParagraph(visibleEnd) ||
+ isStartOfParagraph(visibleStart) && isEndOfParagraph(visibleEnd);
+
+ // Retain the Mail quote level by including all ancestor mail block quotes.
+ if (lastClosed && annotate && selectedOneOrMoreParagraphs) {
+ for (Node *ancestor = lastClosed->parentNode(); ancestor; ancestor = ancestor->parentNode())
+ if (isMailBlockquote(ancestor))
+ specialCommonAncestor = ancestor;
+ }
+
+ Node* checkAncestor = specialCommonAncestor ? specialCommonAncestor : commonAncestor;
+ if (checkAncestor->renderer()) {
+ RefPtr<CSSMutableStyleDeclaration> checkAncestorStyle = computedStyle(checkAncestor)->copyInheritableProperties();
+ if (!propertyMissingOrEqualToNone(checkAncestorStyle.get(), CSS_PROP__WEBKIT_TEXT_DECORATIONS_IN_EFFECT))
+ specialCommonAncestor = enclosingNodeOfType(Position(checkAncestor, 0), &elementHasTextDecorationProperty);
+ }
+
+ if (Node *enclosingAnchor = enclosingNodeWithTag(Position(specialCommonAncestor ? specialCommonAncestor : commonAncestor, 0), aTag))
+ specialCommonAncestor = enclosingAnchor;
+
+ Node* body = enclosingNodeWithTag(Position(commonAncestor, 0), bodyTag);
+ // FIXME: Only include markup for a fully selected root (and ancestors of lastClosed up to that root) if
+ // there are styles/attributes on those nodes that need to be included to preserve the appearance of the copied markup.
+ // FIXME: Do this for all fully selected blocks, not just the body.
+ Node* fullySelectedRoot = body && *Selection::selectionFromContentsOfNode(body).toRange() == *updatedRange ? body : 0;
+ if (annotate && fullySelectedRoot)
+ specialCommonAncestor = fullySelectedRoot;
+
+ if (specialCommonAncestor) {
+ // Also include all of the ancestors of lastClosed up to this special ancestor.
+ for (Node* ancestor = lastClosed->parentNode(); ancestor; ancestor = ancestor->parentNode()) {
+ if (ancestor == fullySelectedRoot && !convertBlocksToInlines) {
+ RefPtr<CSSMutableStyleDeclaration> style = 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 (!style->getPropertyCSSValue(CSS_PROP_BACKGROUND_IMAGE) && static_cast<Element*>(fullySelectedRoot)->hasAttribute(backgroundAttr))
+ style->setProperty(CSS_PROP_BACKGROUND_IMAGE, "url('" + static_cast<Element*>(fullySelectedRoot)->getAttribute(backgroundAttr) + "')");
+
+ if (style->length()) {
+ Vector<UChar> openTag;
+ static const String divStyle("<div style=\"");
+ append(openTag, divStyle);
+ appendAttributeValue(openTag, style->cssText());
+ openTag.append('\"');
+ openTag.append('>');
+ preMarkups.append(String::adopt(openTag));
+
+ static const String divCloseTag("</div>");
+ markups.append(divCloseTag);
+ }
+ } else {
+ preMarkups.append(getStartMarkup(ancestor, updatedRange.get(), annotate, convertBlocksToInlines));
+ markups.append(getEndMarkup(ancestor));
+ }
+ if (nodes)
+ nodes->append(ancestor);
+
+ lastClosed = ancestor;
+
+ if (ancestor == specialCommonAncestor)
+ break;
+ }
+ }
+
+ static const String styleSpanOpen = String("<span class=\"" AppleStyleSpanClass "\" style=\"");
+ static const String styleSpanClose("</span>");
+
+ // Add a wrapper span with the styles that all of the nodes in the markup inherit.
+ Node* parentOfLastClosed = lastClosed ? lastClosed->parentNode() : 0;
+ if (parentOfLastClosed && parentOfLastClosed->renderer()) {
+ RefPtr<CSSMutableStyleDeclaration> style = computedStyle(parentOfLastClosed)->copyInheritableProperties();
+
+ // 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.
+ removeEnclosingMailBlockquoteStyle(style.get(), parentOfLastClosed);
+
+ // Document default styles will be added on another wrapper span.
+ removeDefaultStyles(style.get(), document);
+
+ // 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->length() > 0) {
+ Vector<UChar> openTag;
+ append(openTag, styleSpanOpen);
+ appendAttributeValue(openTag, style->cssText());
+ openTag.append('\"');
+ openTag.append('>');
+ preMarkups.append(String::adopt(openTag));
+
+ markups.append(styleSpanClose);
+ }
+ }
+
+ 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<CSSMutableStyleDeclaration> defaultStyle = computedStyle(document->documentElement())->copyInheritableProperties();
+
+ if (defaultStyle->length() > 0) {
+ Vector<UChar> openTag;
+ append(openTag, styleSpanOpen);
+ appendAttributeValue(openTag, defaultStyle->cssText());
+ openTag.append('\"');
+ openTag.append('>');
+ preMarkups.append(String::adopt(openTag));
+ markups.append(styleSpanClose);
+ }
+ }
+
+ // FIXME: The interchange newline should be placed in the block that it's in, not after all of the content, unconditionally.
+ if (annotate && needInterchangeNewlineAfter(visibleEnd.previous()))
+ markups.append(interchangeNewlineString);
+
+ if (deleteButton)
+ deleteButton->enable();
+
+ return joinMarkups(preMarkups, markups);
+}
+
+PassRefPtr<DocumentFragment> createFragmentFromMarkup(Document* document, const String& markup, const String& baseURL)
+{
+ ASSERT(document->documentElement()->isHTMLElement());
+ // FIXME: What if the document element is not an HTML element?
+ HTMLElement *element = static_cast<HTMLElement*>(document->documentElement());
+
+ RefPtr<DocumentFragment> fragment = element->createContextualFragment(markup);
+
+ if (fragment && !baseURL.isEmpty() && baseURL != blankURL() && baseURL != document->baseURL())
+ completeURLs(fragment.get(), baseURL);
+
+ return fragment.release();
+}
+
+String createMarkup(const Node* node, EChildrenOnly includeChildren, Vector<Node*>* nodes)
+{
+ Vector<UChar> result;
+
+ if (!node)
+ return "";
+
+ Document* document = node->document();
+ Frame* frame = document->frame();
+ DeleteButtonController* deleteButton = frame ? frame->editor()->deleteButtonController() : 0;
+
+ // disable the delete button so it's elements are not serialized into the markup
+ if (deleteButton) {
+ if (node->isDescendantOf(deleteButton->containerElement()))
+ return "";
+ deleteButton->disable();
+ }
+
+ appendMarkup(result, const_cast<Node*>(node), includeChildren, nodes);
+
+ if (deleteButton)
+ deleteButton->enable();
+
+ return String::adopt(result);
+}
+
+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 == 0);
+ return;
+ }
+
+ ASSERT(string.find('\n') == -1);
+
+ 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 == 0);
+ tabText = "";
+ }
+ RefPtr<Node> textNode = document->createTextNode(stringWithRebalancedWhitespace(s, first, i + 1 == numEntries));
+ paragraph->appendChild(textNode.release(), ec);
+ ASSERT(ec == 0);
+ }
+
+ // 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 == 0);
+ }
+
+ first = false;
+ }
+}
+
+PassRefPtr<DocumentFragment> createFragmentFromText(Range* context, const String& text)
+{
+ if (!context)
+ return 0;
+
+ Node* styleNode = context->startNode();
+ 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 == 0);
+ if (string.endsWith("\n")) {
+ RefPtr<Element> element;
+ element = document->createElementNS(xhtmlNamespaceURI, "br", ec);
+ ASSERT(ec == 0);
+ element->setAttribute(classAttr, AppleInterchangeNewline);
+ fragment->appendChild(element.release(), ec);
+ ASSERT(ec == 0);
+ }
+ return fragment.release();
+ }
+
+ // A string with no newlines gets added inline, rather than being put into a paragraph.
+ if (string.find('\n') == -1) {
+ fillContainerFromString(fragment.get(), string);
+ return fragment.release();
+ }
+
+ // Break string into paragraphs. Extra line breaks turn into empty paragraphs.
+ Node* block = enclosingBlock(context->startNode());
+ bool useClonesOfEnclosingBlock = !block->hasTagName(bodyTag);
+
+ 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 = document->createElementNS(xhtmlNamespaceURI, "br", ec);
+ ASSERT(ec == 0);
+ element->setAttribute(classAttr, AppleInterchangeNewline);
+ } else {
+ element = useClonesOfEnclosingBlock ? static_cast<Element*>(block->cloneNode(false).get()) : createDefaultParagraphElement(document);
+ fillContainerFromString(element.get(), s);
+ }
+ fragment->appendChild(element.release(), ec);
+ ASSERT(ec == 0);
+ }
+ 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 == 0);
+ fragment->appendChild(element.release(), ec);
+ ASSERT(ec == 0);
+ }
+
+ if (document->frame())
+ document->frame()->editor()->deleteButtonController()->enable();
+
+ return fragment.release();
+}
+
+}
diff --git a/WebCore/editing/markup.h b/WebCore/editing/markup.h
new file mode 100644
index 0000000..9dcd303
--- /dev/null
+++ b/WebCore/editing/markup.h
@@ -0,0 +1,53 @@
+/*
+ * 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 "HTMLInterchange.h"
+#include <wtf/Forward.h>
+#include <wtf/Vector.h>
+
+namespace WebCore {
+
+ class Document;
+ class DocumentFragment;
+ class Node;
+ class Range;
+ class String;
+
+ enum EChildrenOnly { IncludeNode, ChildrenOnly };
+
+ PassRefPtr<DocumentFragment> createFragmentFromText(Range* context, const String& text);
+ PassRefPtr<DocumentFragment> createFragmentFromMarkup(Document*, const String& markup, const String& baseURL);
+ PassRefPtr<DocumentFragment> createFragmentFromNodes(Document*, const Vector<Node*>&);
+
+ String createMarkup(const Range*,
+ Vector<Node*>* = 0, EAnnotateForInterchange = DoNotAnnotateForInterchange, bool convertBlocksToInlines = false);
+ String createMarkup(const Node*, EChildrenOnly = IncludeNode, Vector<Node*>* = 0);
+
+}
+
+#endif // markup_h
diff --git a/WebCore/editing/qt/EditorQt.cpp b/WebCore/editing/qt/EditorQt.cpp
new file mode 100644
index 0000000..5f12450
--- /dev/null
+++ b/WebCore/editing/qt/EditorQt.cpp
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2006 Zack Rusin <zack@kde.org>
+ * 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 "Editor.h"
+
+#include "ClipboardAccessPolicy.h"
+#include "ClipboardQt.h"
+#include "Document.h"
+#include "Element.h"
+#include "Selection.h"
+#include "SelectionController.h"
+#include "TextIterator.h"
+#include "htmlediting.h"
+#include "visible_units.h"
+
+namespace WebCore {
+
+PassRefPtr<Clipboard> Editor::newGeneralClipboard(ClipboardAccessPolicy policy)
+{
+ return new ClipboardQt(policy);
+}
+
+} // namespace WebCore
diff --git a/WebCore/editing/visible_units.cpp b/WebCore/editing/visible_units.cpp
new file mode 100644
index 0000000..3bcdc09
--- /dev/null
+++ b/WebCore/editing/visible_units.cpp
@@ -0,0 +1,912 @@
+/*
+ * 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.
+ */
+
+#include "config.h"
+#include "visible_units.h"
+
+#include "Document.h"
+#include "Element.h"
+#include "HTMLNames.h"
+#include "RenderBlock.h"
+#include "RenderLayer.h"
+#include "TextBoundaries.h"
+#include "TextBreakIterator.h"
+#include "TextIterator.h"
+#include "htmlediting.h"
+
+namespace WebCore {
+
+using namespace HTMLNames;
+
+static VisiblePosition previousBoundary(const VisiblePosition &c, unsigned (*searchFunction)(const UChar *, unsigned))
+{
+ Position pos = c.deepEquivalent();
+ Node *n = pos.node();
+ if (!n)
+ return VisiblePosition();
+ Document *d = n->document();
+ Node *de = d->documentElement();
+ if (!de)
+ return VisiblePosition();
+ Node *boundary = n->enclosingBlockFlowElement();
+ if (!boundary)
+ return VisiblePosition();
+ bool isContentEditable = boundary->isContentEditable();
+ while (boundary && boundary != de && boundary->parentNode() && isContentEditable == boundary->parentNode()->isContentEditable())
+ boundary = boundary->parentNode();
+
+ Position start = rangeCompliantEquivalent(Position(boundary, 0));
+ Position end = rangeCompliantEquivalent(pos);
+ RefPtr<Range> searchRange = new Range(d);
+
+ int exception = 0;
+ searchRange->setStart(start.node(), start.offset(), exception);
+ searchRange->setEnd(end.node(), end.offset(), exception);
+
+ ASSERT(!exception);
+ if (exception)
+ return VisiblePosition();
+
+ SimplifiedBackwardsTextIterator it(searchRange.get());
+ Vector<UChar, 1024> string;
+ unsigned next = 0;
+ bool inTextSecurityMode = start.node() && start.node()->renderer() && start.node()->renderer()->style()->textSecurity() != TSNONE;
+ 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());
+ if (next != 0)
+ break;
+ it.advance();
+ }
+
+ if (it.atEnd() && next == 0) {
+ pos = it.range()->startPosition();
+ } else if (next != 0) {
+ Node *node = it.range()->startContainer(exception);
+ if (node->isTextNode() || (node->renderer() && node->renderer()->isBR()))
+ // The next variable contains a usable index into a text node
+ pos = Position(node, next);
+ else {
+ // Use the end of the found range, the start is not guaranteed to
+ // be correct.
+ Position end = it.range()->endPosition();
+ VisiblePosition boundary(end);
+ unsigned i = it.length() - next;
+ while (i--)
+ boundary = boundary.previous();
+ return boundary;
+ }
+ }
+
+ return VisiblePosition(pos, DOWNSTREAM);
+}
+
+static VisiblePosition nextBoundary(const VisiblePosition &c, unsigned (*searchFunction)(const UChar *, unsigned))
+{
+ Position pos = c.deepEquivalent();
+ Node *n = pos.node();
+ if (!n)
+ return VisiblePosition();
+ Document *d = n->document();
+ Node *de = d->documentElement();
+ if (!de)
+ return VisiblePosition();
+ Node *boundary = n->enclosingBlockFlowElement();
+ if (!boundary)
+ return VisiblePosition();
+ bool isContentEditable = boundary->isContentEditable();
+ while (boundary && boundary != de && boundary->parentNode() && isContentEditable == boundary->parentNode()->isContentEditable())
+ boundary = boundary->parentNode();
+
+ RefPtr<Range> searchRange(d->createRange());
+ Position start(rangeCompliantEquivalent(pos));
+ ExceptionCode ec = 0;
+ searchRange->selectNodeContents(boundary, ec);
+ searchRange->setStart(start.node(), start.offset(), ec);
+ TextIterator it(searchRange.get(), true);
+ Vector<UChar, 1024> string;
+ unsigned next = 0;
+ bool inTextSecurityMode = start.node() && start.node()->renderer() && start.node()->renderer()->style()->textSecurity() != TSNONE;
+ 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());
+ if (next != string.size())
+ break;
+ it.advance();
+ }
+
+ if (it.atEnd() && next == string.size()) {
+ pos = it.range()->startPosition();
+ } else if (next != 0) {
+ // Use the character iterator to translate the next value into a DOM position.
+ CharacterIterator charIt(searchRange.get(), true);
+ charIt.advance(next - 1);
+ pos = charIt.range()->endPosition();
+
+ // 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(charIt.range()->startPosition()))
+ pos = visPos.next(true).deepEquivalent();
+ }
+
+ // generate VisiblePosition, use UPSTREAM affinity if possible
+ return VisiblePosition(pos, VP_UPSTREAM_IF_POSSIBLE);
+}
+
+// ---------
+
+static unsigned startWordBoundary(const UChar* characters, unsigned length)
+{
+ int start, end;
+ findWordBoundary(characters, length, length, &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)
+{
+ int start, end;
+ findWordBoundary(characters, length, 0, &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)
+{
+ return findNextWordFromIndex(characters, length, length, false);
+}
+
+VisiblePosition previousWordPosition(const VisiblePosition &c)
+{
+ VisiblePosition prev = previousBoundary(c, previousWordPositionBoundary);
+ return c.honorEditableBoundaryAtOrAfter(prev);
+}
+
+static unsigned nextWordPositionBoundary(const UChar* characters, unsigned length)
+{
+ return findNextWordFromIndex(characters, length, 0, 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 = renderer->inlineBox(p.offset(), c.affinity());
+ if (!box)
+ return 0;
+
+ return box->root();
+}
+
+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))
+ 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.offset() == 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->object();
+ if (!startRenderer)
+ return VisiblePosition();
+
+ startNode = startRenderer->element();
+ if (startNode)
+ break;
+
+ startBox = startBox->nextLeafChild();
+ }
+
+ int startOffset = 0;
+ if (startBox->isInlineTextBox()) {
+ InlineTextBox *startTextBox = static_cast<InlineTextBox *>(startBox);
+ startOffset = startTextBox->m_start;
+ }
+
+ VisiblePosition visPos = VisiblePosition(startNode, startOffset, DOWNSTREAM);
+ return positionAvoidingFirstPositionInTable(visPos);
+}
+
+VisiblePosition startOfLine(const VisiblePosition& c)
+{
+ VisiblePosition visPos = startPositionForLine(c);
+
+ if (visPos.isNotNull()) {
+ // Make sure the start of line is not greater than the given input position. Else use the previous position to
+ // obtain start 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, startPositionForLine would incorrectly hand back a position
+ // greater than the input position. 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.
+ Position p = visPos.deepEquivalent();
+ if (p.offset() > c.deepEquivalent().offset() && p.node()->isSameNode(c.deepEquivalent().node())) {
+ visPos = c.previous();
+ if (visPos.isNull())
+ return VisiblePosition();
+ visPos = startPositionForLine(visPos);
+ }
+ }
+
+ 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.offset() == 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->object();
+ if (!endRenderer)
+ return VisiblePosition();
+
+ endNode = endRenderer->element();
+ 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->m_start;
+ if (!endTextBox->isLineBreak())
+ endOffset += endTextBox->m_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);
+}
+
+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 = renderer->inlineBox(p.offset(), visiblePosition.affinity());
+ if (box) {
+ root = box->root()->prevRootBox();
+ if (root)
+ containingBlock = renderer->containingBlock();
+ }
+
+ 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 = enclosingBlock(node);
+ Node *n = node->previousEditable();
+ while (n && startBlock == enclosingBlock(n))
+ n = n->previousEditable();
+ while (n) {
+ if (highestEditableRoot(Position(n, 0)) != highestRoot)
+ break;
+ Position pos(n, caretMinOffset(n));
+ if (pos.isCandidate()) {
+ ASSERT(n->renderer());
+ box = n->renderer()->inlineBox(caretMaxOffset(n));
+ if (box) {
+ // previous root line box found
+ root = box->root();
+ containingBlock = n->renderer()->containingBlock();
+ break;
+ }
+
+ return VisiblePosition(pos, DOWNSTREAM);
+ }
+ n = n->previousEditable();
+ }
+ }
+
+ if (root) {
+ // FIXME: Can be wrong for multi-column layout.
+ int absx, absy;
+ containingBlock->absolutePositionForContent(absx, absy);
+ if (containingBlock->hasOverflowClip())
+ containingBlock->layer()->subtractScrollOffset(absx, absy);
+ RenderObject *renderer = root->closestLeafChildForXPos(x - absx, isEditablePosition(p))->object();
+ Node* node = renderer->element();
+ if (editingIgnoresContent(node))
+ return Position(node->parent(), node->nodeIndex());
+ return renderer->positionForCoordinates(x - absx, root->topOverflow());
+ }
+
+ // 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.
+ return VisiblePosition(node->rootEditableElement(), 0, DOWNSTREAM);
+}
+
+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 = renderer->inlineBox(p.offset(), visiblePosition.affinity());
+ if (box) {
+ root = box->root()->nextRootBox();
+ if (root)
+ containingBlock = renderer->containingBlock();
+ }
+
+ 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 = enclosingBlock(node);
+ Node *n = node->nextEditable(p.offset());
+ while (n && startBlock == enclosingBlock(n))
+ n = n->nextEditable();
+ while (n) {
+ if (highestEditableRoot(Position(n, 0)) != highestRoot)
+ break;
+ Position pos(n, caretMinOffset(n));
+ if (pos.isCandidate()) {
+ ASSERT(n->renderer());
+ box = n->renderer()->inlineBox(caretMinOffset(n));
+ if (box) {
+ // next root line box found
+ root = box->root();
+ containingBlock = n->renderer()->containingBlock();
+ break;
+ }
+
+ return VisiblePosition(pos, DOWNSTREAM);
+ }
+ n = n->nextEditable();
+ }
+ }
+
+ if (root) {
+ // FIXME: Can be wrong for multi-column layout.
+ int absx, absy;
+ containingBlock->absolutePositionForContent(absx, absy);
+ if (containingBlock->hasOverflowClip())
+ containingBlock->layer()->subtractScrollOffset(absx, absy);
+ RenderObject *renderer = root->closestLeafChildForXPos(x - absx, isEditablePosition(p))->object();
+ Node* node = renderer->element();
+ if (editingIgnoresContent(node))
+ return Position(node->parent(), node->nodeIndex());
+ return renderer->positionForCoordinates(x - absx, root->topOverflow());
+ }
+
+ // 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->rootEditableElement();
+ return VisiblePosition(rootElement, rootElement ? rootElement->childNodeCount() : 0, DOWNSTREAM);
+}
+
+// ---------
+
+static unsigned startSentenceBoundary(const UChar* characters, unsigned length)
+{
+ 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)
+{
+ 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)
+{
+ // 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)
+{
+ // 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);
+}
+
+// FIXME: Broken for positions before/after images that aren't inline (5027702)
+VisiblePosition startOfParagraph(const VisiblePosition &c)
+{
+ Position p = c.deepEquivalent();
+ Node *startNode = p.node();
+
+ if (!startNode)
+ return VisiblePosition();
+
+ if (startNode->renderer()
+ && ((startNode->renderer()->isTable() && !startNode->renderer()->isInline())
+ || startNode->renderer()->isHR())
+ && p.offset() == maxDeepOffset(startNode))
+ return VisiblePosition(Position(startNode, 0));
+
+ Node* startBlock = enclosingBlock(startNode);
+
+ Node *node = startNode;
+ int offset = p.offset();
+
+ Node *n = startNode;
+ while (n) {
+ if (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()) {
+ if (style->preserveNewline()) {
+ const UChar* chars = static_cast<RenderText*>(r)->characters();
+ int i = static_cast<RenderText*>(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);
+}
+
+// FIXME: Broken for positions before/after images that aren't inline (5027702)
+VisiblePosition endOfParagraph(const VisiblePosition &c)
+{
+ if (c.isNull())
+ return VisiblePosition();
+
+ Position p = c.deepEquivalent();
+ Node* startNode = p.node();
+
+ if (startNode->renderer()
+ && ((startNode->renderer()->isTable() && !startNode->renderer()->isInline())
+ || startNode->renderer()->isHR())
+ && p.offset() == 0)
+ return VisiblePosition(Position(startNode, maxDeepOffset(startNode)));
+
+ Node* startBlock = enclosingBlock(startNode);
+ Node *stayInsideBlock = startBlock;
+
+ Node *node = startNode;
+ int offset = p.offset();
+
+ Node *n = startNode;
+ while (n) {
+ if (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.
+ // We should probably do this in other cases such as startOfParagraph.
+ if (r->isText() && r->caretMaxRenderedOffset() > 0) {
+ int length = static_cast<RenderText*>(r)->textLength();
+ if (style->preserveNewline()) {
+ const UChar* chars = static_cast<RenderText*>(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 = maxDeepOffset(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)
+{
+ return pos.isNotNull() && pos == startOfParagraph(pos);
+}
+
+bool isEndOfParagraph(const VisiblePosition &pos)
+{
+ return pos.isNotNull() && pos == endOfParagraph(pos);
+}
+
+VisiblePosition previousParagraphPosition(const VisiblePosition &p, int x)
+{
+ VisiblePosition pos = p;
+ do {
+ VisiblePosition n = previousLinePosition(pos, x);
+ if (n.isNull() || n == pos)
+ return p;
+ 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)
+ return p;
+ 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())
+ 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 VisiblePosition(highestRoot, 0, DOWNSTREAM);
+}
+
+VisiblePosition endOfEditableContent(const VisiblePosition& visiblePosition)
+{
+ Node* highestRoot = highestEditableRoot(visiblePosition.deepEquivalent());
+ if (!highestRoot)
+ return VisiblePosition();
+
+ return VisiblePosition(highestRoot, maxDeepOffset(highestRoot), DOWNSTREAM);
+}
+
+}
diff --git a/WebCore/editing/visible_units.h b/WebCore/editing/visible_units.h
new file mode 100644
index 0000000..2663888
--- /dev/null
+++ b/WebCore/editing/visible_units.h
@@ -0,0 +1,91 @@
+/*
+ * 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 "Document.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 isStartOfLine(const VisiblePosition &);
+bool isEndOfLine(const VisiblePosition &);
+
+// paragraphs (perhaps a misnomer, can be divided by line break elements)
+VisiblePosition startOfParagraph(const VisiblePosition&);
+VisiblePosition endOfParagraph(const VisiblePosition&);
+VisiblePosition startOfNextParagraph(const VisiblePosition&);
+VisiblePosition previousParagraphPosition(const VisiblePosition &, int x);
+VisiblePosition nextParagraphPosition(const VisiblePosition &, int x);
+bool inSameParagraph(const VisiblePosition &, const VisiblePosition &);
+bool isStartOfParagraph(const VisiblePosition &);
+bool isEndOfParagraph(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/WebCore/editing/wx/EditorWx.cpp b/WebCore/editing/wx/EditorWx.cpp
new file mode 100644
index 0000000..3cb0472
--- /dev/null
+++ b/WebCore/editing/wx/EditorWx.cpp
@@ -0,0 +1,40 @@
+/*
+ * 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"
+
+namespace WebCore {
+
+PassRefPtr<Clipboard> Editor::newGeneralClipboard(ClipboardAccessPolicy policy)
+{
+ return new ClipboardWx(policy, true);
+}
+
+}
+
+