diff options
Diffstat (limited to 'WebCore/editing')
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("&"); + static const String ltEntity("<"); + static const String quotEntity("""); + + 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, "&"); + else + append(s, "<"); + 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("&"); + static const String ltEntity("<"); + + 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('\"', """); + 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); +} + +} + + |