diff options
author | Ben Murdoch <benm@google.com> | 2009-08-11 17:01:47 +0100 |
---|---|---|
committer | Ben Murdoch <benm@google.com> | 2009-08-11 18:21:02 +0100 |
commit | 0bf48ef3be53ddaa52bbead65dfd75bf90e7a2b5 (patch) | |
tree | 2943df35f62d885c89d01063cc528dd73b480fea /WebCore/editing | |
parent | 7e7a70bfa49a1122b2597a1e6367d89eb4035eca (diff) | |
download | external_webkit-0bf48ef3be53ddaa52bbead65dfd75bf90e7a2b5.zip external_webkit-0bf48ef3be53ddaa52bbead65dfd75bf90e7a2b5.tar.gz external_webkit-0bf48ef3be53ddaa52bbead65dfd75bf90e7a2b5.tar.bz2 |
Merge in WebKit r47029.
Diffstat (limited to 'WebCore/editing')
31 files changed, 1118 insertions, 540 deletions
diff --git a/WebCore/editing/AppendNodeCommand.cpp b/WebCore/editing/AppendNodeCommand.cpp index 06d0158..ef79e9c 100644 --- a/WebCore/editing/AppendNodeCommand.cpp +++ b/WebCore/editing/AppendNodeCommand.cpp @@ -39,7 +39,7 @@ AppendNodeCommand::AppendNodeCommand(PassRefPtr<Element> parent, PassRefPtr<Node ASSERT(m_node); ASSERT(!m_node->parent()); - ASSERT(enclosingNodeOfType(Position(m_parent.get(), 0), isContentEditable) || !m_parent->attached()); + ASSERT(m_parent->isContentEditable() || !m_parent->attached()); } void AppendNodeCommand::doApply() diff --git a/WebCore/editing/ApplyStyleCommand.cpp b/WebCore/editing/ApplyStyleCommand.cpp index 8d0312b..86ef214 100644 --- a/WebCore/editing/ApplyStyleCommand.cpp +++ b/WebCore/editing/ApplyStyleCommand.cpp @@ -98,13 +98,12 @@ void StyleChange::init(PassRefPtr<CSSStyleDeclaration> style, const Position& po Document* document = position.node() ? position.node()->document() : 0; if (!document || !document->frame()) return; - + bool useHTMLFormattingTags = !document->frame()->editor()->shouldStyleWithCSS(); - RefPtr<CSSMutableStyleDeclaration> mutableStyle = style->makeMutable(); - + // We shouldn't have both text-decoration and -webkit-text-decorations-in-effect because that wouldn't make sense. + ASSERT(!mutableStyle->getPropertyCSSValue(CSSPropertyTextDecoration) || !mutableStyle->getPropertyCSSValue(CSSPropertyWebkitTextDecorationsInEffect)); String styleText(""); - bool addedDirection = false; CSSMutableStyleDeclaration::const_iterator end = mutableStyle->end(); for (CSSMutableStyleDeclaration::const_iterator it = mutableStyle->begin(); it != end; ++it) { @@ -130,12 +129,12 @@ void StyleChange::init(PassRefPtr<CSSStyleDeclaration> style, const Position& po } // Add this property - - if (property->id() == CSSPropertyWebkitTextDecorationsInEffect) { - // we have to special-case text decorations - // FIXME: Why? + if (property->id() == CSSPropertyTextDecoration || property->id() == CSSPropertyWebkitTextDecorationsInEffect) { + // Always use text-decoration because -webkit-text-decoration-in-effect is internal. CSSProperty alteredProperty(CSSPropertyTextDecoration, property->value(), property->isImportant()); - styleText += alteredProperty.cssText(); + // We don't add "text-decoration: none" because it doesn't override the existing text decorations; i.e. redundant + if (!equalIgnoringCase(alteredProperty.value()->cssText(), "none")) + styleText += alteredProperty.cssText(); } else styleText += property->cssText(); @@ -232,8 +231,11 @@ bool StyleChange::currentlyHasStyle(const Position &pos, const CSSProperty *prop RefPtr<CSSValue> value; if (property->id() == CSSPropertyFontSize) value = style->getFontSizeCSSValuePreferringKeyword(); + // We need to use -webkit-text-decorations-in-effect to take care of text decorations set by u, s, and strike + else if (property->id() == CSSPropertyTextDecoration) + value = style->getPropertyCSSValue(CSSPropertyWebkitTextDecorationsInEffect); else - value = style->getPropertyCSSValue(property->id(), DoNotUpdateLayout); + value = style->getPropertyCSSValue(property->id()); if (!value) return false; return equalIgnoringCase(value->cssText(), property->value()->cssText()); @@ -300,7 +302,129 @@ PassRefPtr<HTMLElement> createStyleSpanElement(Document* document) styleElement->setAttribute(classAttr, styleSpanClassString()); return styleElement.release(); } + +RefPtr<CSSMutableStyleDeclaration> getPropertiesNotInComputedStyle(CSSStyleDeclaration* style, CSSComputedStyleDeclaration* computedStyle) +{ + ASSERT(style); + ASSERT(computedStyle); + RefPtr<CSSMutableStyleDeclaration> result = style->copy(); + computedStyle->diff(result.get()); + + // If text decorations in effect is not present in the computed style, then there is nothing to remove from result + RefPtr<CSSValue> computedValue = computedStyle->getPropertyCSSValue(CSSPropertyWebkitTextDecorationsInEffect); + if (!computedValue || !computedValue->isValueList()) + return result; + + // Take care of both text-decoration and -webkit-text-decorations-in-effect + static const int textDecorationProperties[]={CSSPropertyTextDecoration, CSSPropertyWebkitTextDecorationsInEffect}; + for (size_t n = 0; n < sizeof(textDecorationProperties)/sizeof(textDecorationProperties[1]); n++) { + RefPtr<CSSValue> styleValue = style->getPropertyCSSValue(textDecorationProperties[n]); + if (!styleValue || !styleValue->isValueList()) + continue; + + CSSValueList* desiredValueList = static_cast<CSSValueList*>(styleValue.get()); + CSSValueList* computedValueList = static_cast<CSSValueList*>(computedValue.get()); + for (size_t i = 0; i < desiredValueList->length(); i++) { + if (!computedValueList->hasValue(desiredValueList->item(i))) { + result->removeProperty(textDecorationProperties[n]); + break; + } + } + } + + return result; +} + +// Editing style properties must be preserved during editing operation. +// e.g. when a user inserts a new paragraph, all properties listed here must be copied to the new paragraph. +// FIXME: The current editingStyleProperties contains all inheritableProperties but we may not need to preserve all inheritable properties +static const int editingStyleProperties[] = { + CSSPropertyBackgroundColor, + // CSS inheritable properties + CSSPropertyBorderCollapse, + CSSPropertyColor, + CSSPropertyFontFamily, + CSSPropertyFontSize, + CSSPropertyFontStyle, + CSSPropertyFontVariant, + CSSPropertyFontWeight, + CSSPropertyLetterSpacing, + CSSPropertyLineHeight, + CSSPropertyOrphans, + CSSPropertyTextAlign, + CSSPropertyTextIndent, + CSSPropertyTextTransform, + CSSPropertyWhiteSpace, + CSSPropertyWidows, + CSSPropertyWordSpacing, + CSSPropertyWebkitBorderHorizontalSpacing, + CSSPropertyWebkitBorderVerticalSpacing, + CSSPropertyWebkitTextDecorationsInEffect, + CSSPropertyWebkitTextFillColor, + CSSPropertyWebkitTextSizeAdjust, + CSSPropertyWebkitTextStrokeColor, + CSSPropertyWebkitTextStrokeWidth, +}; +size_t numEditingStyleProperties = sizeof(editingStyleProperties)/sizeof(editingStyleProperties[0]); + +PassRefPtr<CSSMutableStyleDeclaration> editingStyleAtPosition(Position pos, ShouldIncludeTypingStyle shouldIncludeTypingStyle) +{ + RefPtr<CSSComputedStyleDeclaration> computedStyleAtPosition = pos.computedStyle(); + RefPtr<CSSMutableStyleDeclaration> style = computedStyleAtPosition->copyPropertiesInSet(editingStyleProperties, numEditingStyleProperties); + + if (style && pos.node() && pos.node()->computedStyle()) { + RenderStyle* renderStyle = pos.node()->computedStyle(); + // If a node's text fill color is invalid, then its children use + // their font-color as their text fill color (they don't + // inherit it). Likewise for stroke color. + ExceptionCode ec = 0; + if (!renderStyle->textFillColor().isValid()) + style->removeProperty(CSSPropertyWebkitTextFillColor, ec); + if (!renderStyle->textStrokeColor().isValid()) + style->removeProperty(CSSPropertyWebkitTextStrokeColor, ec); + ASSERT(ec == 0); + if (renderStyle->fontDescription().keywordSize()) + style->setProperty(CSSPropertyFontSize, computedStyleAtPosition->getFontSizeCSSValuePreferringKeyword()->cssText()); + } + + if (shouldIncludeTypingStyle == IncludeTypingStyle) { + CSSMutableStyleDeclaration* typingStyle = pos.node()->document()->frame()->typingStyle(); + if (typingStyle) + style->merge(typingStyle); + } + + return style.release(); +} + +void prepareEditingStyleToApplyAt(CSSMutableStyleDeclaration* editingStyle, Position pos) +{ + // ReplaceSelectionCommand::handleStyleSpans() requiers that this function only removes the editing style. + // If this function was modified in the futureto delete all redundant properties, then add a boolean value to indicate + // which one of editingStyleAtPosition or computedStyle is called. + RefPtr<CSSMutableStyleDeclaration> style = editingStyleAtPosition(pos); + style->diff(editingStyle); + + // if alpha value is zero, we don't add the background color. + RefPtr<CSSValue> backgroundColor = editingStyle->getPropertyCSSValue(CSSPropertyBackgroundColor); + if (backgroundColor && backgroundColor->isPrimitiveValue()) { + CSSPrimitiveValue* primitiveValue = static_cast<CSSPrimitiveValue*>(backgroundColor.get()); + Color color = Color(primitiveValue->getRGBA32Value()); + ExceptionCode ec; + if (color.alpha() == 0) + editingStyle->removeProperty(CSSPropertyBackgroundColor, ec); + } +} +void removeStylesAddedByNode(CSSMutableStyleDeclaration* editingStyle, Node* node) +{ + ASSERT(node); + ASSERT(node->parentNode()); + RefPtr<CSSMutableStyleDeclaration> parentStyle = editingStyleAtPosition(Position(node->parentNode(), 0)); + RefPtr<CSSMutableStyleDeclaration> style = editingStyleAtPosition(Position(node, 0)); + parentStyle->diff(style.get()); + style->diff(editingStyle); +} + ApplyStyleCommand::ApplyStyleCommand(Document* document, CSSStyleDeclaration* style, EditAction editingAction, EPropertyLevel propertyLevel) : CompositeEditCommand(document) , m_style(style->makeMutable()) @@ -787,7 +911,7 @@ void ApplyStyleCommand::applyInlineStyle(CSSMutableStyleDeclaration *style) styleWithoutEmbedding->removeProperty(CSSPropertyUnicodeBidi); styleWithoutEmbedding->removeProperty(CSSPropertyDirection); removeInlineStyle(styleWithoutEmbedding, removeStart, end); - } else + } else removeInlineStyle(style, removeStart, end); start = startPosition(); @@ -857,7 +981,7 @@ void ApplyStyleCommand::applyInlineStyle(CSSMutableStyleDeclaration *style) styleWithoutEmbedding->removeProperty(CSSPropertyUnicodeBidi); styleWithoutEmbedding->removeProperty(CSSPropertyDirection); applyInlineStyleToRange(styleWithoutEmbedding.get(), start, end); - } else + } else applyInlineStyleToRange(style, start, end); // Remove dummy style spans created by splitting text elements. @@ -962,6 +1086,25 @@ bool ApplyStyleCommand::implicitlyStyledElementShouldBeRemovedWhenApplyingStyle( if (elem->hasLocalName(iTag) || elem->hasLocalName(emTag)) return true; break; + case CSSPropertyTextDecoration: + case CSSPropertyWebkitTextDecorationsInEffect: + ASSERT(property.value()); + if (property.value()->isValueList()) { + CSSValueList* valueList = static_cast<CSSValueList*>(property.value()); + DEFINE_STATIC_LOCAL(RefPtr<CSSPrimitiveValue>, underline, (CSSPrimitiveValue::createIdentifier(CSSValueUnderline))); + DEFINE_STATIC_LOCAL(RefPtr<CSSPrimitiveValue>, lineThrough, (CSSPrimitiveValue::createIdentifier(CSSValueLineThrough))); + // Because style is new style to be applied, we delete element only if the element is not used in style. + if (!valueList->hasValue(underline.get()) && elem->hasLocalName(uTag)) + return true; + if (!valueList->hasValue(lineThrough.get()) && (elem->hasLocalName(strikeTag) || elem->hasLocalName(sTag))) + return true; + } else { + // If the value is NOT a list, then it must be "none", in which case we should remove all text decorations. + ASSERT(property.value()->cssText() == "none"); + if (elem->hasLocalName(uTag) || elem->hasLocalName(strikeTag) || elem->hasLocalName(sTag)) + return true; + } + break; } } return false; @@ -1075,11 +1218,17 @@ static bool hasTextDecorationProperty(Node *node) static Node* highestAncestorWithTextDecoration(Node *node) { - Node *result = NULL; + ASSERT(node); + Node* result = 0; + Node* unsplittableElement = unsplittableElementForPosition(Position(node, 0)); for (Node *n = node; n; n = n->parentNode()) { if (hasTextDecorationProperty(n)) result = n; + // Should stop at the editable root (cannot cross editing boundary) and + // also stop at the unsplittable element to be consistent with other UAs + if (n == unsplittableElement) + break; } return result; @@ -1162,32 +1311,35 @@ void ApplyStyleCommand::applyTextDecorationStyle(Node *node, CSSMutableStyleDecl } } -void ApplyStyleCommand::pushDownTextDecorationStyleAroundNode(Node* node, bool force) +void ApplyStyleCommand::pushDownTextDecorationStyleAroundNode(Node* targetNode, bool forceNegate) { - 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); + ASSERT(targetNode); + Node* highestAncestor = highestAncestorWithTextDecoration(targetNode); + if (!highestAncestor) + return; - for (Node *child = current->firstChild(); child; child = nextChild) { - nextChild = child->nextSibling(); + // The outer loop is traversing the tree vertically from highestAncestor to targetNode + Node* current = highestAncestor; + while (current != targetNode) { + ASSERT(current); + ASSERT(current->contains(targetNode)); + RefPtr<CSSMutableStyleDeclaration> decoration = forceNegate ? extractAndNegateTextDecorationStyle(current) : extractTextDecorationStyle(current); + + // The inner loop will go through children on each level + Node* child = current->firstChild(); + while (child) { + Node* nextChild = child->nextSibling(); + + // Apply text decoration to all nodes containing targetNode and their siblings but NOT to targetNode + if (child != targetNode) + applyTextDecorationStyle(child, decoration.get()); + + // We found the next node for the outer loop (contains targetNode) + // When reached targetNode, stop the outer loop upon the completion of the current inner loop + if (child == targetNode || child->contains(targetNode)) + current = child; - if (node == child) { - nextCurrent = child; - } else if (node->isDescendantOf(child)) { - applyTextDecorationStyle(child, decoration.get()); - nextCurrent = child; - } else { - applyTextDecorationStyle(child, decoration.get()); - } - } + child = nextChild; } } } diff --git a/WebCore/editing/ApplyStyleCommand.h b/WebCore/editing/ApplyStyleCommand.h index 74fe605..29aa483 100644 --- a/WebCore/editing/ApplyStyleCommand.h +++ b/WebCore/editing/ApplyStyleCommand.h @@ -73,7 +73,7 @@ private: PassRefPtr<CSSMutableStyleDeclaration> extractTextDecorationStyle(Node*); PassRefPtr<CSSMutableStyleDeclaration> extractAndNegateTextDecorationStyle(Node*); void applyTextDecorationStyle(Node*, CSSMutableStyleDeclaration *style); - void pushDownTextDecorationStyleAroundNode(Node*, bool force); + void pushDownTextDecorationStyleAroundNode(Node*, bool forceNegate); void pushDownTextDecorationStyleAtBoundaries(const Position& start, const Position& end); // style-application helpers @@ -114,6 +114,16 @@ private: bool isStyleSpan(const Node*); PassRefPtr<HTMLElement> createStyleSpanElement(Document*); +RefPtr<CSSMutableStyleDeclaration> getPropertiesNotInComputedStyle(CSSStyleDeclaration* style, CSSComputedStyleDeclaration* computedStyle); + +enum ShouldIncludeTypingStyle { + IncludeTypingStyle, + IgnoreTypingStyle +}; + +PassRefPtr<CSSMutableStyleDeclaration> editingStyleAtPosition(Position, ShouldIncludeTypingStyle = IgnoreTypingStyle); +void prepareEditingStyleToApplyAt(CSSMutableStyleDeclaration*, Position); +void removeStylesAddedByNode(CSSMutableStyleDeclaration*, Node*); } // namespace WebCore diff --git a/WebCore/editing/BreakBlockquoteCommand.cpp b/WebCore/editing/BreakBlockquoteCommand.cpp index e3d66ba..1ca2d87 100644 --- a/WebCore/editing/BreakBlockquoteCommand.cpp +++ b/WebCore/editing/BreakBlockquoteCommand.cpp @@ -67,9 +67,11 @@ void BreakBlockquoteCommand::doApply() RefPtr<Element> breakNode = createBreakElement(document()); + bool isLastVisPosInNode = isLastVisiblePositionInNode(visiblePos, topBlockquote); + // If the position is at the beginning of the top quoted content, we don't need to break the quote. - // Instead, insert the break before the blockquote. - if (isFirstVisiblePositionInNode(visiblePos, topBlockquote)) { + // Instead, insert the break before the blockquote, unless the position is as the end of the the quoted content. + if (isFirstVisiblePositionInNode(visiblePos, topBlockquote) && !isLastVisPosInNode) { insertNodeBefore(breakNode.get(), topBlockquote); setEndingSelection(VisibleSelection(Position(breakNode.get(), 0), DOWNSTREAM)); rebalanceWhitespace(); @@ -78,9 +80,9 @@ void BreakBlockquoteCommand::doApply() // Insert a break after the top blockquote. insertNodeAfter(breakNode.get(), topBlockquote); - + // If we're inserting the break at the end of the quoted content, we don't need to break the quote. - if (isLastVisiblePositionInNode(visiblePos, topBlockquote)) { + if (isLastVisPosInNode) { setEndingSelection(VisibleSelection(Position(breakNode.get(), 0), DOWNSTREAM)); rebalanceWhitespace(); return; @@ -142,7 +144,7 @@ void BreakBlockquoteCommand::doApply() 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())); + setNodeAttribute(static_cast<Element*>(clonedChild.get()), startAttr, String::number(toRenderListItem(listChildNode->renderer())->value())); } appendNode(clonedChild.get(), clonedAncestor.get()); diff --git a/WebCore/editing/CompositeEditCommand.cpp b/WebCore/editing/CompositeEditCommand.cpp index 89a0f8a..25f167c 100644 --- a/WebCore/editing/CompositeEditCommand.cpp +++ b/WebCore/editing/CompositeEditCommand.cpp @@ -802,7 +802,7 @@ void CompositeEditCommand::moveParagraphs(const VisiblePosition& startOfParagrap // too, <div><b><br></b></div> for example. Save it so that we can preserve it later. RefPtr<CSSMutableStyleDeclaration> styleInEmptyParagraph; if (startOfParagraphToMove == endOfParagraphToMove && preserveStyle) { - styleInEmptyParagraph = styleAtPosition(startOfParagraphToMove.deepEquivalent()); + styleInEmptyParagraph = editingStyleAtPosition(startOfParagraphToMove.deepEquivalent(), IncludeTypingStyle); // The moved paragraph should assume the block style of the destination. styleInEmptyParagraph->removeBlockProperties(); } @@ -891,7 +891,7 @@ bool CompositeEditCommand::breakOutOfEmptyListItem() if (!emptyListItem) return false; - RefPtr<CSSMutableStyleDeclaration> style = styleAtPosition(endingSelection().start()); + RefPtr<CSSMutableStyleDeclaration> style = editingStyleAtPosition(endingSelection().start(), IncludeTypingStyle); Node* listNode = emptyListItem->parentNode(); @@ -988,6 +988,10 @@ Position CompositeEditCommand::positionAvoidingSpecialElementBoundary(const Posi VisiblePosition visiblePos(original); Node* enclosingAnchor = enclosingAnchorElement(original); Position result = original; + + if (!enclosingAnchor) + return result; + // Don't avoid block level anchors, because that would insert content into the wrong paragraph. if (enclosingAnchor && !isBlock(enclosingAnchor)) { VisiblePosition firstInAnchor(firstDeepEditingPositionForNode(enclosingAnchor)); @@ -1020,6 +1024,9 @@ Position CompositeEditCommand::positionAvoidingSpecialElementBoundary(const Posi pushAnchorElementDown(enclosingAnchor); enclosingAnchor = enclosingAnchorElement(original); } + if (!enclosingAnchor) + return original; + result = positionBeforeNode(enclosingAnchor); } } @@ -1034,6 +1041,8 @@ Position CompositeEditCommand::positionAvoidingSpecialElementBoundary(const Posi // to determine if the split is necessary. Returns the last split node. PassRefPtr<Node> CompositeEditCommand::splitTreeToNode(Node* start, Node* end, bool splitAncestor) { + ASSERT(start != end); + RefPtr<Node> node; for (node = start; node && node->parent() != end; node = node->parent()) { VisiblePosition positionInParent(Position(node->parent(), 0), DOWNSTREAM); diff --git a/WebCore/editing/DeleteSelectionCommand.cpp b/WebCore/editing/DeleteSelectionCommand.cpp index 5a0d8fc..d57c241 100644 --- a/WebCore/editing/DeleteSelectionCommand.cpp +++ b/WebCore/editing/DeleteSelectionCommand.cpp @@ -44,6 +44,7 @@ #include "Text.h" #include "TextIterator.h" #include "visible_units.h" +#include "ApplyStyleCommand.h" namespace WebCore { @@ -259,11 +260,8 @@ static void removeEnclosingAnchorStyle(CSSMutableStyleDeclaration* style, const Node* enclosingAnchor = enclosingAnchorElement(position); if (!enclosingAnchor || !enclosingAnchor->parentNode()) return; - - RefPtr<CSSMutableStyleDeclaration> parentStyle = Position(enclosingAnchor->parentNode(), 0).computedStyle()->copyInheritableProperties(); - RefPtr<CSSMutableStyleDeclaration> anchorStyle = Position(enclosingAnchor, 0).computedStyle()->copyInheritableProperties(); - parentStyle->diff(anchorStyle.get()); - anchorStyle->diff(style); + + removeStylesAddedByNode(style, enclosingAnchor); } void DeleteSelectionCommand::saveTypingStyleState() @@ -277,19 +275,17 @@ void DeleteSelectionCommand::saveTypingStyleState() // 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. - RefPtr<CSSComputedStyleDeclaration> computedStyle = positionBeforeTabSpan(m_selectionToDelete.start()).computedStyle(); - m_typingStyle = computedStyle->copyInheritableProperties(); - + m_typingStyle = editingStyleAtPosition(positionBeforeTabSpan(m_selectionToDelete.start())); + removeEnclosingAnchorStyle(m_typingStyle.get(), m_selectionToDelete.start()); - + // If we're deleting into a Mail blockquote, save the style at end() instead of start() // We'll use this later in computeTypingStyleAfterDelete if we end up outside of a Mail blockquote - if (nearestMailBlockquote(m_selectionToDelete.start().node())) { - computedStyle = m_selectionToDelete.end().computedStyle(); - m_deleteIntoBlockquoteStyle = computedStyle->copyInheritableProperties(); - } else + if (nearestMailBlockquote(m_selectionToDelete.start().node())) + m_deleteIntoBlockquoteStyle = editingStyleAtPosition(m_selectionToDelete.end()); + else m_deleteIntoBlockquoteStyle = 0; } @@ -364,8 +360,8 @@ void DeleteSelectionCommand::removeNode(PassRefPtr<Node> node) // make sure empty cell has some height updateLayout(); RenderObject *r = node->renderer(); - if (r && r->isTableCell() && static_cast<RenderTableCell*>(r)->contentHeight() <= 0) - insertBlockPlaceholder(Position(node,0)); + if (r && r->isTableCell() && toRenderTableCell(r)->contentHeight() <= 0) + insertBlockPlaceholder(Position(node, 0)); return; } @@ -668,9 +664,8 @@ void DeleteSelectionCommand::calculateTypingStyleAfterDelete() if (m_deleteIntoBlockquoteStyle && !nearestMailBlockquote(m_endingPosition.node())) m_typingStyle = m_deleteIntoBlockquoteStyle; m_deleteIntoBlockquoteStyle = 0; - - RefPtr<CSSComputedStyleDeclaration> endingStyle = computedStyle(m_endingPosition.node()); - endingStyle->diff(m_typingStyle.get()); + + prepareEditingStyleToApplyAt(m_typingStyle.get(), m_endingPosition); if (!m_typingStyle->length()) m_typingStyle = 0; VisiblePosition visibleEnd(m_endingPosition); diff --git a/WebCore/editing/EditCommand.cpp b/WebCore/editing/EditCommand.cpp index fefe658..2301c54 100644 --- a/WebCore/editing/EditCommand.cpp +++ b/WebCore/editing/EditCommand.cpp @@ -195,18 +195,6 @@ bool EditCommand::isTypingCommand() const return false; } -PassRefPtr<CSSMutableStyleDeclaration> EditCommand::styleAtPosition(const Position &pos) -{ - RefPtr<CSSMutableStyleDeclaration> style = positionBeforeTabSpan(pos).computedStyle()->copyInheritableProperties(); - - // FIXME: It seems misleading to also include the typing style when returning the style at some arbitrary - // position in the document. - CSSMutableStyleDeclaration* typingStyle = document()->frame()->typingStyle(); - if (typingStyle) - style->merge(typingStyle); - - return style.release(); -} void EditCommand::updateLayout() const { diff --git a/WebCore/editing/EditCommand.h b/WebCore/editing/EditCommand.h index c4969f6..7dc4d92 100644 --- a/WebCore/editing/EditCommand.h +++ b/WebCore/editing/EditCommand.h @@ -66,7 +66,6 @@ protected: void setStartingSelection(const VisibleSelection&); void setEndingSelection(const VisibleSelection&); - PassRefPtr<CSSMutableStyleDeclaration> styleAtPosition(const Position&); void updateLayout() const; private: diff --git a/WebCore/editing/Editor.cpp b/WebCore/editing/Editor.cpp index 2c303f9..b9dfc5d 100644 --- a/WebCore/editing/Editor.cpp +++ b/WebCore/editing/Editor.cpp @@ -340,9 +340,10 @@ bool Editor::tryDHTMLCopy() if (m_frame->selection()->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(); + if (canCopy()) + // Must be done before oncopy adds types and data to the pboard, + // also done for security, as it erases data from the last copy/paste. + Pasteboard::generalPasteboard()->clear(); return !dispatchCPPEvent(eventNames().copyEvent, ClipboardWritable); } @@ -351,10 +352,11 @@ bool Editor::tryDHTMLCut() { if (m_frame->selection()->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(); + + if (canCut()) + // Must be done before oncut adds types and data to the pboard, + // also done for security, as it erases data from the last copy/paste. + Pasteboard::generalPasteboard()->clear(); return !dispatchCPPEvent(eventNames().cutEvent, ClipboardWritable); } @@ -653,9 +655,9 @@ PassRefPtr<Node> Editor::increaseSelectionListLevelOrdered() if (!canEditRichly() || m_frame->selection()->isNone()) return 0; - PassRefPtr<Node> newList = IncreaseSelectionListLevelCommand::increaseSelectionListLevelOrdered(m_frame->document()); + RefPtr<Node> newList = IncreaseSelectionListLevelCommand::increaseSelectionListLevelOrdered(m_frame->document()); revealSelectionAfterEditingOperation(); - return newList; + return newList.release(); } PassRefPtr<Node> Editor::increaseSelectionListLevelUnordered() @@ -663,9 +665,9 @@ PassRefPtr<Node> Editor::increaseSelectionListLevelUnordered() if (!canEditRichly() || m_frame->selection()->isNone()) return 0; - PassRefPtr<Node> newList = IncreaseSelectionListLevelCommand::increaseSelectionListLevelUnordered(m_frame->document()); + RefPtr<Node> newList = IncreaseSelectionListLevelCommand::increaseSelectionListLevelUnordered(m_frame->document()); revealSelectionAfterEditingOperation(); - return newList; + return newList.release(); } void Editor::decreaseSelectionListLevel() @@ -769,66 +771,54 @@ bool Editor::clientIsEditable() const return client() && client()->isEditable(); } +// CSS properties that only has a visual difference when applied to text. +static const int textOnlyProperties[] = { + CSSPropertyTextDecoration, + CSSPropertyWebkitTextDecorationsInEffect, + CSSPropertyFontStyle, + CSSPropertyFontWeight, + CSSPropertyColor, +}; + +static TriState triStateOfStyleInComputedStyle(CSSStyleDeclaration* desiredStyle, CSSComputedStyleDeclaration* computedStyle, bool ignoreTextOnlyProperties = false) +{ + RefPtr<CSSMutableStyleDeclaration> diff = getPropertiesNotInComputedStyle(desiredStyle, computedStyle); + + if (ignoreTextOnlyProperties) + diff->removePropertiesInSet(textOnlyProperties, sizeof(textOnlyProperties)/sizeof(textOnlyProperties[0])); + + if (!diff->length()) + return TrueTriState; + else if (diff->length() == desiredStyle->length()) + return FalseTriState; + return MixedTriState; +} + 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; - CSSMutableStyleDeclaration::const_iterator end = mutableStyle->end(); - for (CSSMutableStyleDeclaration::const_iterator it = mutableStyle->begin(); it != end; ++it) { - int propertyID = (*it).id(); - if (!equalIgnoringCase(mutableStyle->getPropertyValue(propertyID), selectionStyle->getPropertyValue(propertyID))) { - match = false; - break; - } - } - + TriState state = triStateOfStyleInComputedStyle(style, selectionStyle.get()); if (nodeToRemove) { ExceptionCode ec = 0; nodeToRemove->remove(ec); ASSERT(ec == 0); } - - return match; -} - -static void updateState(CSSMutableStyleDeclaration* desiredStyle, CSSComputedStyleDeclaration* computedStyle, bool& atStart, TriState& state) -{ - CSSMutableStyleDeclaration::const_iterator end = desiredStyle->end(); - for (CSSMutableStyleDeclaration::const_iterator it = desiredStyle->begin(); 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; - } - } + return state == TrueTriState; } TriState Editor::selectionHasStyle(CSSStyleDeclaration* style) const { - bool atStart = true; TriState state = FalseTriState; - RefPtr<CSSMutableStyleDeclaration> mutableStyle = style->makeMutable(); - if (!m_frame->selection()->isRange()) { Node* nodeToRemove; RefPtr<CSSComputedStyleDeclaration> selectionStyle = m_frame->selectionComputedStyle(nodeToRemove); if (!selectionStyle) return FalseTriState; - updateState(mutableStyle.get(), selectionStyle.get(), atStart, state); + state = triStateOfStyleInComputedStyle(style, selectionStyle.get()); if (nodeToRemove) { ExceptionCode ec = 0; nodeToRemove->remove(ec); @@ -837,10 +827,15 @@ TriState Editor::selectionHasStyle(CSSStyleDeclaration* style) const } else { for (Node* node = m_frame->selection()->start().node(); node; node = node->traverseNextNode()) { RefPtr<CSSComputedStyleDeclaration> nodeStyle = computedStyle(node); - if (nodeStyle) - updateState(mutableStyle.get(), nodeStyle.get(), atStart, state); - if (state == MixedTriState) - break; + if (nodeStyle) { + TriState nodeState = triStateOfStyleInComputedStyle(style, nodeStyle.get(), !node->isTextNode()); + if (node == m_frame->selection()->start().node()) + state = nodeState; + else if (state != nodeState) { + state = MixedTriState; + break; + } + } if (node == m_frame->selection()->end().node()) break; } @@ -1064,9 +1059,9 @@ void Editor::paste() void Editor::pasteAsPlainText() { - if (!canPaste()) + if (!canPaste()) return; - pasteAsPlainTextWithPasteboard(Pasteboard::generalPasteboard()); + pasteAsPlainTextWithPasteboard(Pasteboard::generalPasteboard()); } void Editor::performDelete() @@ -2204,7 +2199,7 @@ void Editor::markMisspellingsAfterTypingToPosition(const VisiblePosition &p) if (!frame()->editor()->shouldInsertText(autocorrectedString, misspellingRange.get(), EditorInsertActionTyped)) return; - frame()->editor()->replaceSelectionWithText(autocorrectedString, false, true); + frame()->editor()->replaceSelectionWithText(autocorrectedString, false, false); // Reset the charet one character further. frame()->selection()->moveTo(frame()->selection()->end()); @@ -2256,20 +2251,9 @@ static void markMisspellingsOrBadGrammar(Editor* editor, const VisibleSelection& Node* editableNode = searchRange->startContainer(); if (!editableNode || !editableNode->isContentEditable()) return; - - // Ascend the DOM tree to find a "spellcheck" attribute. - // When we find a "spellcheck" attribute, retrieve its value and exit if its value is "false". - const Node* node = editor->frame()->document()->focusedNode(); - while (node) { - if (node->isElementNode()) { - const WebCore::AtomicString& value = static_cast<const Element*>(node)->getAttribute(spellcheckAttr); - if (equalIgnoringCase(value, "true")) - break; - if (equalIgnoringCase(value, "false")) - return; - } - node = node->parent(); - } + + if (!editor->spellCheckingEnabledInFocusedNode()) + return; // Get the spell checker if it is available if (!editor->client()) @@ -2287,6 +2271,24 @@ static void markMisspellingsOrBadGrammar(Editor* editor, const VisibleSelection& } } +bool Editor::spellCheckingEnabledInFocusedNode() const +{ + // Ascend the DOM tree to find a "spellcheck" attribute. + // When we find a "spellcheck" attribute, retrieve its value and return false if its value is "false". + const Node* node = frame()->document()->focusedNode(); + while (node) { + if (node->isElementNode()) { + const WebCore::AtomicString& value = static_cast<const Element*>(node)->getAttribute(spellcheckAttr); + if (equalIgnoringCase(value, "true")) + return true; + if (equalIgnoringCase(value, "false")) + return false; + } + node = node->parent(); + } + return true; +} + void Editor::markMisspellings(const VisibleSelection& selection, RefPtr<Range>& firstMisspellingRange) { markMisspellingsOrBadGrammar(this, selection, true, firstMisspellingRange); @@ -2323,7 +2325,10 @@ void Editor::markAllMisspellingsAndBadGrammarInRanges(bool markSpelling, Range* Node* editableNode = spellingRange->startContainer(); if (!editableNode || !editableNode->isContentEditable()) return; - + + if (!spellCheckingEnabledInFocusedNode()) + return; + // Expand the range to encompass entire paragraphs, since text checking needs that much context. int spellingRangeStartOffset = 0; int spellingRangeEndOffset = 0; diff --git a/WebCore/editing/Editor.h b/WebCore/editing/Editor.h index 67a4b59..5b0cc9c 100644 --- a/WebCore/editing/Editor.h +++ b/WebCore/editing/Editor.h @@ -196,6 +196,7 @@ public: Vector<String> guessesForMisspelledSelection(); Vector<String> guessesForUngrammaticalSelection(); Vector<String> guessesForMisspelledOrUngrammaticalSelection(bool& misspelled, bool& ungrammatical); + bool spellCheckingEnabledInFocusedNode() const; void markMisspellingsAfterTypingToPosition(const VisiblePosition&); void markMisspellings(const VisibleSelection&, RefPtr<Range>& firstMisspellingRange); void markBadGrammar(const VisibleSelection&); diff --git a/WebCore/editing/EditorCommand.cpp b/WebCore/editing/EditorCommand.cpp index 5a189d4..55750ff 100644 --- a/WebCore/editing/EditorCommand.cpp +++ b/WebCore/editing/EditorCommand.cpp @@ -27,6 +27,7 @@ #include "config.h" #include "AtomicString.h" +#include "CSSComputedStyleDeclaration.h" #include "CSSMutableStyleDeclaration.h" #include "CSSPropertyNames.h" #include "CSSValueKeywords.h" @@ -89,64 +90,85 @@ static Frame* targetFrame(Frame* frame, Event* event) return node->document()->frame(); } -static bool executeApplyStyle(Frame* frame, EditorCommandSource source, EditAction action, int propertyID, const String& propertyValue) +static bool applyCommandToFrame(Frame* frame, EditorCommandSource source, EditAction action, CSSMutableStyleDeclaration* style) { - RefPtr<CSSMutableStyleDeclaration> style = CSSMutableStyleDeclaration::create(); - style->setProperty(propertyID, propertyValue); // FIXME: We don't call shouldApplyStyle when the source is DOM; is there a good reason for that? switch (source) { case CommandFromMenuOrKeyBinding: - frame->editor()->applyStyleToSelection(style.get(), action); + frame->editor()->applyStyleToSelection(style, action); return true; case CommandFromDOM: case CommandFromDOMWithUserInterface: - frame->editor()->applyStyle(style.get()); + frame->editor()->applyStyle(style); return true; } ASSERT_NOT_REACHED(); return false; } -static bool executeApplyStyle(Frame* frame, EditorCommandSource source, EditAction action, int propertyID, const char* propertyValue) +static bool executeApplyStyle(Frame* frame, EditorCommandSource source, EditAction action, int propertyID, const String& propertyValue) { - return executeApplyStyle(frame, source, action, propertyID, String(propertyValue)); + RefPtr<CSSMutableStyleDeclaration> style = CSSMutableStyleDeclaration::create(); + style->setProperty(propertyID, propertyValue); + return applyCommandToFrame(frame, source, action, style.get()); } static bool executeApplyStyle(Frame* frame, EditorCommandSource source, EditAction action, int propertyID, int propertyValue) { RefPtr<CSSMutableStyleDeclaration> style = CSSMutableStyleDeclaration::create(); style->setProperty(propertyID, propertyValue); - // 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; + return applyCommandToFrame(frame, source, action, style.get()); +} + +// FIXME: executeToggleStyleInList does not handle complicated cases such as <b><u>hello</u>world</b> properly. +// This function must use Editor::selectionHasStyle to determine the current style but we cannot fix this +// until https://bugs.webkit.org/show_bug.cgi?id=27818 is resolved. +static bool executeToggleStyleInList(Frame* frame, EditorCommandSource source, EditAction action, int propertyID, CSSValue* value) +{ + ExceptionCode ec = 0; + Node* nodeToRemove = 0; + RefPtr<CSSComputedStyleDeclaration> selectionStyle = frame->selectionComputedStyle(nodeToRemove); + RefPtr<CSSValue> selectedCSSValue = selectionStyle->getPropertyCSSValue(propertyID); + String newStyle = "none"; + if (selectedCSSValue->isValueList()) { + RefPtr<CSSValueList> selectedCSSValueList = static_cast<CSSValueList*>(selectedCSSValue.get()); + if (!selectedCSSValueList->removeAll(value)) + selectedCSSValueList->append(value); + if (selectedCSSValueList->length()) + newStyle = selectedCSSValueList->cssText(); + + } else if (selectedCSSValue->cssText() == "none") + newStyle = value->cssText(); + + ASSERT(ec == 0); + if (nodeToRemove) { + nodeToRemove->remove(ec); + ASSERT(ec == 0); } - ASSERT_NOT_REACHED(); - return false; + + // FIXME: We shouldn't be having to convert new style into text. We should have setPropertyCSSValue. + RefPtr<CSSMutableStyleDeclaration> newMutableStyle = CSSMutableStyleDeclaration::create(); + newMutableStyle->setProperty(propertyID, newStyle,ec); + return applyCommandToFrame(frame, source, action, newMutableStyle.get()); } static bool executeToggleStyle(Frame* frame, EditorCommandSource source, EditAction action, int propertyID, const char* offValue, const char* onValue) { RefPtr<CSSMutableStyleDeclaration> style = CSSMutableStyleDeclaration::create(); - style->setProperty(propertyID, onValue); - 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; + style->setProperty(propertyID, onValue); // We need to add this style to pass it to selectionStartHasStyle / selectionHasStyle + + // Style is considered present when + // mac: present at the beginning of selection + // other: present throughout the selection + Settings* settings = frame->document()->settings(); + bool styleIsPresent; + if (settings && settings->editingBehavior() == EditingMacBehavior) + styleIsPresent = frame->editor()->selectionStartHasStyle(style.get()); + else + styleIsPresent = frame->editor()->selectionHasStyle(style.get()) == TrueTriState; + + style->setProperty(propertyID, styleIsPresent ? offValue : onValue); + return applyCommandToFrame(frame, source, action, style.get()); } static bool executeApplyParagraphStyle(Frame* frame, EditorCommandSource source, EditAction action, int propertyID, const String& propertyValue) @@ -937,7 +959,8 @@ static bool executeSetMark(Frame* frame, Event*, EditorCommandSource, const Stri static bool executeStrikethrough(Frame* frame, Event*, EditorCommandSource source, const String&) { - return executeToggleStyle(frame, source, EditActionChangeAttributes, CSSPropertyWebkitTextDecorationsInEffect, "none", "line-through"); + RefPtr<CSSPrimitiveValue> lineThrough = CSSPrimitiveValue::createIdentifier(CSSValueLineThrough); + return executeToggleStyleInList(frame, source, EditActionUnderline, CSSPropertyWebkitTextDecorationsInEffect, lineThrough.get()); } static bool executeStyleWithCSS(Frame* frame, Event*, EditorCommandSource, const String& value) @@ -990,8 +1013,8 @@ static bool executeTranspose(Frame* frame, Event*, EditorCommandSource, const St 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, CSSPropertyWebkitTextDecorationsInEffect, "none", "underline"); + RefPtr<CSSPrimitiveValue> underline = CSSPrimitiveValue::createIdentifier(CSSValueUnderline); + return executeToggleStyleInList(frame, source, EditActionUnderline, CSSPropertyWebkitTextDecorationsInEffect, underline.get()); } static bool executeUndo(Frame* frame, Event*, EditorCommandSource, const String&) diff --git a/WebCore/editing/IndentOutdentCommand.cpp b/WebCore/editing/IndentOutdentCommand.cpp index 0f9b106..8c4ba01 100644 --- a/WebCore/editing/IndentOutdentCommand.cpp +++ b/WebCore/editing/IndentOutdentCommand.cpp @@ -33,6 +33,7 @@ #include "InsertLineBreakCommand.h" #include "InsertListCommand.h" #include "Range.h" +#include "DocumentFragment.h" #include "SplitElementCommand.h" #include "TextIterator.h" #include "htmlediting.h" @@ -57,18 +58,9 @@ static PassRefPtr<HTMLBlockquoteElement> createIndentBlockquoteElement(Document* return element.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)); + return node && (node->hasTagName(ulTag) || node->hasTagName(olTag) || node->hasTagName(blockquoteTag)); } IndentOutdentCommand::IndentOutdentCommand(Document* document, EIndentType typeOfAction, int marginInPixels) @@ -76,35 +68,115 @@ IndentOutdentCommand::IndentOutdentCommand(Document* document, EIndentType typeO { } -// 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. -PassRefPtr<Element> IndentOutdentCommand::prepareBlockquoteLevelForInsertion(VisiblePosition& currentParagraph, RefPtr<Element>& lastBlockquote) +bool IndentOutdentCommand::tryIndentingAsListItem(const VisiblePosition& endOfCurrentParagraph) +{ + // If our selection is not inside a list, bail out. + Node* lastNodeInSelectedParagraph = endOfCurrentParagraph.deepEquivalent().node(); + RefPtr<Element> listNode = enclosingList(lastNodeInSelectedParagraph); + if (!listNode) + return false; + + // Find the list item enclosing the current paragraph + Element* selectedListItem = static_cast<Element*>(enclosingBlock(endOfCurrentParagraph.deepEquivalent().node())); + // FIXME: we need to deal with the case where there is no li (malformed HTML) + if (!selectedListItem->hasTagName(liTag)) + return false; + + // FIXME: previousElementSibling does not ignore non-rendered content like <span></span>. Should we? + Element* previousList = selectedListItem->previousElementSibling(); + Element* nextList = selectedListItem->nextElementSibling(); + + RefPtr<Element> newList = document()->createElement(listNode->tagQName(), false); + insertNodeBefore(newList, selectedListItem); + appendParagraphIntoNode(visiblePositionBeforeNode(selectedListItem), visiblePositionAfterNode(selectedListItem), newList.get()); + + if (canMergeLists(previousList, newList.get())) + mergeIdenticalElements(previousList, newList); + if (canMergeLists(newList.get(), nextList)) + mergeIdenticalElements(newList, nextList); + + return true; +} + +void IndentOutdentCommand::indentIntoBlockquote(const VisiblePosition& startOfCurrentParagraph, const VisiblePosition& endOfCurrentParagraph, RefPtr<Element>& targetBlockquote, Node* nodeToSplitTo) { - int currentBlockquoteLevel = 0; - int lastBlockquoteLevel = 0; - Node* node = currentParagraph.deepEquivalent().node(); - while ((node = enclosingNodeOfType(Position(node->parentNode(), 0), &isIndentBlockquote))) - currentBlockquoteLevel++; - node = lastBlockquote.get(); - while ((node = enclosingNodeOfType(Position(node->parentNode(), 0), &isIndentBlockquote))) - lastBlockquoteLevel++; - while (currentBlockquoteLevel > lastBlockquoteLevel) { - RefPtr<Element> newBlockquote = createIndentBlockquoteElement(document()); - appendNode(newBlockquote, lastBlockquote); - lastBlockquote = newBlockquote; - lastBlockquoteLevel++; + Node* enclosingCell = 0; + + if (!targetBlockquote) { + // Create a new blockquote and insert it as a child of the enclosing block element. We accomplish + // this by splitting all parents of the current paragraph up to that point. + targetBlockquote = createIndentBlockquoteElement(document()); + if (isTableCell(nodeToSplitTo)) + enclosingCell = nodeToSplitTo; + RefPtr<Node> startOfNewBlock = splitTreeToNode(startOfCurrentParagraph.deepEquivalent().node(), nodeToSplitTo); + insertNodeBefore(targetBlockquote, startOfNewBlock); } - while (currentBlockquoteLevel < lastBlockquoteLevel) { - lastBlockquote = static_cast<Element*>(enclosingNodeOfType(Position(lastBlockquote->parentNode(), 0), isIndentBlockquote)); - lastBlockquoteLevel--; + + VisiblePosition endOfNextParagraph = endOfParagraph(endOfCurrentParagraph.next()); + appendParagraphIntoNode(startOfCurrentParagraph, endOfCurrentParagraph, targetBlockquote.get()); + + // Don't put the next paragraph in the blockquote we just created for this paragraph unless + // the next paragraph is in the same cell. + if (enclosingCell && enclosingCell != enclosingNodeOfType(endOfNextParagraph.deepEquivalent(), &isTableCell)) + targetBlockquote = 0; +} + +bool IndentOutdentCommand::isAtUnsplittableElement(const Position& pos) const +{ + Node* node = pos.node(); + return node == editableRootForPosition(pos) || node == enclosingNodeOfType(pos, &isTableCell); +} + +// Enclose all nodes between start and end by newParent, which is a sibling node of nodes between start and end +// FIXME: moveParagraph is overly complicated. We need to clean up moveParagraph so that it uses appendParagraphIntoNode +// or prepare more specialized functions and delete moveParagraph +void IndentOutdentCommand::appendParagraphIntoNode(const VisiblePosition& start, const VisiblePosition& end, Node* newParent) +{ + ASSERT(newParent); + ASSERT(newParent->isContentEditable()); + ASSERT(isStartOfParagraph(start) && isEndOfParagraph(end)); + + Position endOfParagraph = end.deepEquivalent().downstream(); + Node* insertionPoint = newParent->lastChild();// Remember the place to put br later + // Look for the beginning of the last paragraph in newParent + Node* startOfLastParagraph = startOfParagraph(Position(newParent, newParent->childNodeCount())).deepEquivalent().node(); + if (startOfLastParagraph && !startOfLastParagraph->isDescendantOf(newParent)) + startOfLastParagraph = 0; + + // Extend the range so that we can append wrapping nodes as well if they're containd within the paragraph + ExceptionCode ec = 0; + RefPtr<Range> selectedRange = createRange(document(), start, end, ec); + RefPtr<Range> extendedRange = extendRangeToWrappingNodes(selectedRange, selectedRange.get(), newParent->parentNode()); + newParent->appendChild(extendedRange->extractContents(ec), ec); + + // If the start of paragraph didn't change by appending nodes, we should insert br to seperate the paragraphs. + Node* startOfNewParagraph = startOfParagraph(Position(newParent, newParent->childNodeCount())).deepEquivalent().node(); + if (startOfNewParagraph == startOfLastParagraph) { + if (insertionPoint) + newParent->insertBefore(createBreakElement(document()), insertionPoint->nextSibling(), ec); + else + newParent->appendChild(createBreakElement(document()), ec); } - RefPtr<Element> placeholder = createBreakElement(document()); - appendNode(placeholder, lastBlockquote); - // Add another br before the placeholder if it collapsed. - VisiblePosition visiblePos(Position(placeholder.get(), 0)); - if (!isStartOfParagraph(visiblePos)) - insertNodeBefore(createBreakElement(document()), placeholder); - return placeholder.release(); + + // Remove unnecessary br from the place where we moved the paragraph from + removeUnnecessaryLineBreakAt(endOfParagraph); +} + +void IndentOutdentCommand::removeUnnecessaryLineBreakAt(const Position& endOfParagraph) +{ + // If there is something in this paragraph, then don't remove br. + if (!isStartOfParagraph(endOfParagraph) || !isEndOfParagraph(endOfParagraph)) + return; + + // We only care about br at the end of paragraph + Node* br = endOfParagraph.node(); + Node* parentNode = br->parentNode(); + + // If the node isn't br or the parent node is empty, then don't remove. + if (!br->hasTagName(brTag) || isVisiblyAdjacent(positionBeforeNode(parentNode), positionAfterNode(parentNode))) + return; + + removeNodeAndPruneAncestors(br); } void IndentOutdentCommand::indentRegion() @@ -112,16 +184,15 @@ void IndentOutdentCommand::indentRegion() VisibleSelection selection = selectionForParagraphIteration(endingSelection()); VisiblePosition startOfSelection = selection.visibleStart(); VisiblePosition endOfSelection = selection.visibleEnd(); - int startIndex = indexForVisiblePosition(startOfSelection); - int endIndex = indexForVisiblePosition(endOfSelection); + RefPtr<Range> selectedRange = selection.firstRange(); ASSERT(!startOfSelection.isNull()); ASSERT(!endOfSelection.isNull()); - - // Special case empty root editable elements because there's nothing to split + + // Special case empty unsplittable elements because there's nothing to split // and there's nothing to move. Position start = startOfSelection.deepEquivalent().downstream(); - if (start.node() == editableRootForPosition(start)) { + if (isAtUnsplittableElement(start)) { RefPtr<Element> blockquote = createIndentBlockquoteElement(document()); insertNodeAt(blockquote, start); RefPtr<Element> placeholder = createBreakElement(document()); @@ -129,69 +200,42 @@ void IndentOutdentCommand::indentRegion() setEndingSelection(VisibleSelection(Position(placeholder.get(), 0), DOWNSTREAM)); return; } - - RefPtr<Element> previousListNode; - RefPtr<Element> newListNode; - RefPtr<Element> newBlockquote; + + RefPtr<Element> blockquoteForNextIndent; VisiblePosition endOfCurrentParagraph = endOfParagraph(startOfSelection); VisiblePosition endAfterSelection = endOfParagraph(endOfParagraph(endOfSelection).next()); while (endOfCurrentParagraph != endAfterSelection) { // Iterate across the selected paragraphs... VisiblePosition endOfNextParagraph = endOfParagraph(endOfCurrentParagraph.next()); - RefPtr<Element> listNode = enclosingList(endOfCurrentParagraph.deepEquivalent().node()); - RefPtr<Element> insertionPoint; - if (listNode) { - RefPtr<Element> placeholder = createBreakElement(document()); - insertionPoint = placeholder; - newBlockquote = 0; - RefPtr<Element> 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, newListNode); - appendNode(placeholder, listItem); - } else { - // Clone the list element, insert it before the current paragraph, and move the paragraph into it. - RefPtr<Element> clonedList = listNode->cloneElementWithoutChildren(); - insertNodeBefore(clonedList, enclosingListChild(endOfCurrentParagraph.deepEquivalent().node())); - appendNode(listItem, clonedList); - appendNode(placeholder, listItem); - newListNode = clonedList; - 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); + if (tryIndentingAsListItem(endOfCurrentParagraph)) + blockquoteForNextIndent = 0; 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<Element> blockquote = createIndentBlockquoteElement(document()); - Position start = startOfParagraph(endOfCurrentParagraph).deepEquivalent(); - - Node* enclosingCell = enclosingNodeOfType(start, &isTableCell); - Node* nodeToSplitTo = enclosingCell ? enclosingCell : editableRootForPosition(start); - RefPtr<Node> startOfNewBlock = splitTreeToNode(start.node(), nodeToSplitTo); - insertNodeBefore(blockquote, startOfNewBlock); - newBlockquote = blockquote; - insertionPoint = prepareBlockquoteLevelForInsertion(endOfCurrentParagraph, newBlockquote); - // Don't put the next paragraph in the blockquote we just created for this paragraph unless - // the next paragraph is in the same cell. - if (enclosingCell && enclosingCell != enclosingNodeOfType(endOfNextParagraph.deepEquivalent(), &isTableCell)) - newBlockquote = 0; + VisiblePosition startOfCurrentParagraph = startOfParagraph(endOfCurrentParagraph); + Node* blockNode = enclosingBlock(endOfCurrentParagraph.deepEquivalent().node()); + // extend the region so that it contains all the ancestor blocks within the selection + ExceptionCode ec; + Element* unsplittableNode = unsplittableElementForPosition(endOfCurrentParagraph.deepEquivalent()); + RefPtr<Range> originalRange = createRange(document(), endOfCurrentParagraph, endOfCurrentParagraph, ec); + RefPtr<Range> extendedRange = extendRangeToWrappingNodes(originalRange, selectedRange.get(), unsplittableNode); + if (originalRange != extendedRange) { + ExceptionCode ec = 0; + endOfCurrentParagraph = endOfParagraph(extendedRange->endPosition().previous()); + blockNode = enclosingBlock(extendedRange->commonAncestorContainer(ec)); + } + + endOfNextParagraph = endOfParagraph(endOfCurrentParagraph.next()); + indentIntoBlockquote(startOfCurrentParagraph, endOfCurrentParagraph, blockquoteForNextIndent, blockNode); + // blockquoteForNextIndent will be updated in the function } - moveParagraph(startOfParagraph(endOfCurrentParagraph), endOfCurrentParagraph, VisiblePosition(Position(insertionPoint, 0)), true); - // moveParagraph should not destroy content that contains endOfNextParagraph, but if it does, return here - // to avoid a crash. + // Sanity check: Make sure our moveParagraph calls didn't remove endOfNextParagraph.deepEquivalent().node() + // If somehow we did, return to prevent crashes. if (endOfNextParagraph.isNotNull() && !endOfNextParagraph.deepEquivalent().node()->inDocument()) { ASSERT_NOT_REACHED(); return; } endOfCurrentParagraph = endOfNextParagraph; } - - RefPtr<Range> startRange = TextIterator::rangeFromLocationAndLength(document()->documentElement(), startIndex, 0, true); - RefPtr<Range> endRange = TextIterator::rangeFromLocationAndLength(document()->documentElement(), endIndex, 0, true); - if (startRange && endRange) - setEndingSelection(VisibleSelection(startRange->startPosition(), endRange->startPosition(), DOWNSTREAM)); + } void IndentOutdentCommand::outdentParagraph() @@ -200,7 +244,7 @@ void IndentOutdentCommand::outdentParagraph() VisiblePosition visibleEndOfParagraph = endOfParagraph(visibleStartOfParagraph); Node* enclosingNode = enclosingNodeOfType(visibleStartOfParagraph.deepEquivalent(), &isListOrIndentBlockquote); - if (!enclosingNode || !isContentEditable(enclosingNode->parentNode())) // We can't outdent if there is no place to go! + if (!enclosingNode || !enclosingNode->parentNode()->isContentEditable()) // We can't outdent if there is no place to go! return; // Use InsertListCommand to remove the selection from the list @@ -213,7 +257,7 @@ void IndentOutdentCommand::outdentParagraph() return; } - // The selection is inside a blockquote + // The selection is inside a blockquote i.e. enclosingNode is a blockquote VisiblePosition positionInEnclosingBlock = VisiblePosition(Position(enclosingNode, 0)); VisiblePosition startOfEnclosingBlock = startOfBlock(positionInEnclosingBlock); VisiblePosition lastPositionInEnclosingBlock = VisiblePosition(Position(enclosingNode, enclosingNode->childNodeCount())); @@ -227,9 +271,9 @@ void IndentOutdentCommand::outdentParagraph() // just removed one, then this assumption isn't true. By splitting the next containing blockquote after this node, we keep this assumption true if (splitPoint) { if (Node* splitPointParent = splitPoint->parentNode()) { - if (isIndentBlockquote(splitPointParent) - && !isIndentBlockquote(splitPoint) - && isContentEditable(splitPointParent->parentNode())) // We can't outdent if there is no place to go! + if (splitPointParent->hasTagName(blockquoteTag) + && !splitPoint->hasTagName(blockquoteTag) + && splitPointParent->parentNode()->isContentEditable()) // We can't outdent if there is no place to go! splitElement(static_cast<Element*>(splitPointParent), splitPoint); } } @@ -244,10 +288,14 @@ void IndentOutdentCommand::outdentParagraph() return; } - Node* enclosingBlockFlow = enclosingBlockFlowElement(visibleStartOfParagraph); + Node* enclosingBlockFlow = enclosingBlock(visibleStartOfParagraph.deepEquivalent().node()); RefPtr<Node> splitBlockquoteNode = enclosingNode; if (enclosingBlockFlow != enclosingNode) - splitBlockquoteNode = splitTreeToNode(enclosingBlockFlowElement(visibleStartOfParagraph), enclosingNode, true); + splitBlockquoteNode = splitTreeToNode(enclosingBlockFlow, enclosingNode, true); + else { + // We split the blockquote at where we start outdenting. + splitElement(static_cast<Element*>(enclosingNode), visibleStartOfParagraph.deepEquivalent().node()); + } RefPtr<Node> placeholder = createBreakElement(document()); insertNodeBefore(placeholder, splitBlockquoteNode); moveParagraph(startOfParagraph(visibleStartOfParagraph), endOfParagraph(visibleEndOfParagraph), VisiblePosition(Position(placeholder.get(), 0)), true); diff --git a/WebCore/editing/IndentOutdentCommand.h b/WebCore/editing/IndentOutdentCommand.h index bb1a1d2..104a0e7 100644 --- a/WebCore/editing/IndentOutdentCommand.h +++ b/WebCore/editing/IndentOutdentCommand.h @@ -46,10 +46,16 @@ private: virtual void doApply(); virtual EditAction editingAction() const { return m_typeOfAction == Indent ? EditActionIndent : EditActionOutdent; } + // FIXME: Does this belong in htmlediting.cpp? + bool isAtUnsplittableElement(const Position&) const; + void appendParagraphIntoNode(const VisiblePosition& start, const VisiblePosition& end, Node* newParent); + void removeUnnecessaryLineBreakAt(const Position& endOfParagraph); + void indentRegion(); void outdentRegion(); void outdentParagraph(); - PassRefPtr<Element> prepareBlockquoteLevelForInsertion(VisiblePosition&, RefPtr<Element>&); + bool tryIndentingAsListItem(const VisiblePosition&); + void indentIntoBlockquote(const VisiblePosition&, const VisiblePosition&, RefPtr<Element>& targetBlockquote, Node* nodeToSplitTo); EIndentType m_typeOfAction; int m_marginInPixels; diff --git a/WebCore/editing/InsertNodeBeforeCommand.cpp b/WebCore/editing/InsertNodeBeforeCommand.cpp index c3acd9d..4f60963 100644 --- a/WebCore/editing/InsertNodeBeforeCommand.cpp +++ b/WebCore/editing/InsertNodeBeforeCommand.cpp @@ -40,7 +40,7 @@ InsertNodeBeforeCommand::InsertNodeBeforeCommand(PassRefPtr<Node> insertChild, P ASSERT(m_refChild); ASSERT(m_refChild->parentNode()); - ASSERT(enclosingNodeOfType(Position(m_refChild->parentNode(), 0), isContentEditable) || !m_refChild->parentNode()->attached()); + ASSERT(m_refChild->parentNode()->isContentEditable() || !m_refChild->parentNode()->attached()); } void InsertNodeBeforeCommand::doApply() diff --git a/WebCore/editing/InsertParagraphSeparatorCommand.cpp b/WebCore/editing/InsertParagraphSeparatorCommand.cpp index 734d8fc..c5fbf0a 100644 --- a/WebCore/editing/InsertParagraphSeparatorCommand.cpp +++ b/WebCore/editing/InsertParagraphSeparatorCommand.cpp @@ -38,6 +38,7 @@ #include "Text.h" #include "htmlediting.h" #include "visible_units.h" +#include "ApplyStyleCommand.h" namespace WebCore { @@ -63,7 +64,7 @@ void InsertParagraphSeparatorCommand::calculateStyleBeforeInsertion(const Positi if (!isStartOfParagraph(visiblePos) && !isEndOfParagraph(visiblePos)) return; - m_style = styleAtPosition(pos); + m_style = editingStyleAtPosition(pos, IncludeTypingStyle); } void InsertParagraphSeparatorCommand::applyStyleAfterInsertion(Node* originalEnclosingBlock) @@ -79,8 +80,9 @@ void InsertParagraphSeparatorCommand::applyStyleAfterInsertion(Node* originalEnc if (!m_style) return; + + prepareEditingStyleToApplyAt(m_style.get(), endingSelection().start()); - computedStyle(endingSelection().start().node())->diff(m_style.get()); if (m_style->length() > 0) applyStyle(m_style.get()); } @@ -170,6 +172,7 @@ void InsertParagraphSeparatorCommand::doApply() // Handle case when position is in the last visible position in its block, // including when the block is empty. if (isLastInBlock) { + bool shouldApplyStyleAfterInsertion = true; if (nestNewBlock) { if (isFirstInBlock && !lineBreakExistsAtVisiblePosition(visiblePos)) { // The block is empty. Create an empty block to @@ -182,13 +185,18 @@ void InsertParagraphSeparatorCommand::doApply() } else { // We can get here if we pasted a copied portion of a blockquote with a newline at the end and are trying to paste it // into an unquoted area. We then don't want the newline within the blockquote or else it will also be quoted. - Node* highestBlockquote = highestEnclosingNodeOfType(canonicalPos, &isMailBlockquote); - insertNodeAfter(blockToInsert, highestBlockquote ? highestBlockquote : startBlock); + if (Node* highestBlockquote = highestEnclosingNodeOfType(canonicalPos, &isMailBlockquote)) { + startBlock = static_cast<Element*>(highestBlockquote); + // When inserting the newline after the blockquote, we don't want to apply the original style after the insertion + shouldApplyStyleAfterInsertion = false; + } + insertNodeAfter(blockToInsert, startBlock); } appendBlockPlaceholder(blockToInsert); setEndingSelection(VisibleSelection(Position(blockToInsert.get(), 0), DOWNSTREAM)); - applyStyleAfterInsertion(startBlock); + if (shouldApplyStyleAfterInsertion) + applyStyleAfterInsertion(startBlock); return; } diff --git a/WebCore/editing/InsertTextCommand.cpp b/WebCore/editing/InsertTextCommand.cpp index bf6fd19..6b0af9b 100644 --- a/WebCore/editing/InsertTextCommand.cpp +++ b/WebCore/editing/InsertTextCommand.cpp @@ -106,22 +106,14 @@ bool InsertTextCommand::performTrivialReplace(const String& text, bool selectIns return true; } -void InsertTextCommand::input(const String& originalText, bool selectInsertedText) +void InsertTextCommand::input(const String& text, 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()) { @@ -129,7 +121,7 @@ void InsertTextCommand::input(const String& originalText, bool selectInsertedTex return; deleteSelection(false, true, true, false); } - + Position startPosition(endingSelection().start()); Position placeholder; @@ -184,7 +176,7 @@ void InsertTextCommand::input(const String& originalText, bool selectInsertedTex // 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 != " ") + if (text != " ") rebalanceWhitespaceAt(startPosition); m_charactersAdded += text.length(); diff --git a/WebCore/editing/RemoveFormatCommand.cpp b/WebCore/editing/RemoveFormatCommand.cpp index 6d681ee..9243adc 100644 --- a/WebCore/editing/RemoveFormatCommand.cpp +++ b/WebCore/editing/RemoveFormatCommand.cpp @@ -35,6 +35,7 @@ #include "SelectionController.h" #include "TextIterator.h" #include "TypingCommand.h" +#include "ApplyStyleCommand.h" namespace WebCore { @@ -55,8 +56,8 @@ void RemoveFormatCommand::doApply() // Get the default style for this editable root, it's the style that we'll give the // content that we're operating on. Node* root = frame->selection()->rootEditableElement(); - RefPtr<CSSMutableStyleDeclaration> defaultStyle = computedStyle(root)->copyInheritableProperties(); - + RefPtr<CSSMutableStyleDeclaration> defaultStyle = editingStyleAtPosition(Position(root, 0)); + // 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. diff --git a/WebCore/editing/ReplaceSelectionCommand.cpp b/WebCore/editing/ReplaceSelectionCommand.cpp index c6da864..8ae083c 100644 --- a/WebCore/editing/ReplaceSelectionCommand.cpp +++ b/WebCore/editing/ReplaceSelectionCommand.cpp @@ -60,7 +60,7 @@ enum EFragmentType { EmptyFragment, SingleTextNodeFragment, TreeFragment }; // --- ReplacementFragment helper class -class ReplacementFragment : Noncopyable { +class ReplacementFragment : public Noncopyable { public: ReplacementFragment(Document*, DocumentFragment*, bool matchStyle, const VisibleSelection&); @@ -549,8 +549,9 @@ static bool handleStyleSpansBeforeInsertion(ReplacementFragment& fragment, const Node* sourceDocumentStyleSpan = topNode; RefPtr<Node> copiedRangeStyleSpan = sourceDocumentStyleSpan->firstChild(); - - RefPtr<CSSMutableStyleDeclaration> styleAtInsertionPos = rangeCompliantEquivalent(insertionPos).computedStyle()->copyInheritableProperties(); + + RefPtr<CSSMutableStyleDeclaration> styleAtInsertionPos = editingStyleAtPosition(rangeCompliantEquivalent(insertionPos)); + String styleText = styleAtInsertionPos->cssText(); if (styleText == static_cast<Element*>(sourceDocumentStyleSpan)->getAttribute(styleAttr)) { @@ -606,8 +607,8 @@ void ReplaceSelectionCommand::handleStyleSpans() // styles from blockquoteNode are allowed to override those from the source document, see <rdar://problem/4930986> and <rdar://problem/5089327>. Node* blockquoteNode = isMailPasteAsQuotationNode(context) ? context : nearestMailBlockquote(context); if (blockquoteNode) { - RefPtr<CSSMutableStyleDeclaration> blockquoteStyle = computedStyle(blockquoteNode)->copyInheritableProperties(); - RefPtr<CSSMutableStyleDeclaration> parentStyle = computedStyle(blockquoteNode->parentNode())->copyInheritableProperties(); + RefPtr<CSSMutableStyleDeclaration> blockquoteStyle = editingStyleAtPosition(Position(blockquoteNode, 0)); + RefPtr<CSSMutableStyleDeclaration> parentStyle = editingStyleAtPosition(Position(blockquoteNode->parentNode(), 0)); parentStyle->diff(blockquoteStyle.get()); CSSMutableStyleDeclaration::const_iterator end = blockquoteStyle->end(); @@ -618,10 +619,10 @@ void ReplaceSelectionCommand::handleStyleSpans() context = blockquoteNode->parentNode(); } - - RefPtr<CSSMutableStyleDeclaration> contextStyle = computedStyle(context)->copyInheritableProperties(); - contextStyle->diff(sourceDocumentStyle.get()); - + + // This operation requires that only editing styles to be removed from sourceDocumentStyle. + prepareEditingStyleToApplyAt(sourceDocumentStyle.get(), Position(context, 0)); + // 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. @@ -655,9 +656,8 @@ void ReplaceSelectionCommand::handleStyleSpans() // Remove redundant styles. context = copiedRangeStyleSpan->parentNode(); - contextStyle = computedStyle(context)->copyInheritableProperties(); - contextStyle->diff(copiedRangeStyle.get()); - + prepareEditingStyleToApplyAt(copiedRangeStyle.get(), Position(context, 0)); + // See the comments above about removing block properties. copiedRangeStyle->removeBlockProperties(); @@ -725,7 +725,7 @@ void ReplaceSelectionCommand::doApply() return; if (m_matchStyle) - m_insertionStyle = styleAtPosition(selection.start()); + m_insertionStyle = editingStyleAtPosition(selection.start(), IncludeTypingStyle); VisiblePosition visibleStart = selection.visibleStart(); VisiblePosition visibleEnd = selection.visibleEnd(); diff --git a/WebCore/editing/SelectionController.cpp b/WebCore/editing/SelectionController.cpp index d0427c0..b6874d6 100644 --- a/WebCore/editing/SelectionController.cpp +++ b/WebCore/editing/SelectionController.cpp @@ -100,6 +100,8 @@ void SelectionController::moveTo(const Position &base, const Position &extent, E void SelectionController::setSelection(const VisibleSelection& s, bool closeTyping, bool clearTypingStyle, bool userTriggered) { + m_lastChangeWasHorizontalExtension = false; + if (m_isDragCaretController) { invalidateCaretRect(); m_sel = s; @@ -223,34 +225,30 @@ void SelectionController::nodeWillBeRemoved(Node *node) void SelectionController::willBeModified(EAlteration alter, EDirection direction) { - switch (alter) { - case MOVE: - m_lastChangeWasHorizontalExtension = false; + if (alter != EXTEND) + return; + if (m_lastChangeWasHorizontalExtension) + return; + + Position start = m_sel.start(); + Position end = m_sel.end(); + // FIXME: This is probably not correct for right and left when the direction is RTL. + switch (direction) { + case RIGHT: + case FORWARD: + m_sel.setBase(start); + m_sel.setExtent(end); 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; - } - } + case LEFT: + case BACKWARD: + m_sel.setBase(end); + m_sel.setExtent(start); break; } } -TextDirection SelectionController::directionOfEnclosingBlock() { +TextDirection SelectionController::directionOfEnclosingBlock() +{ Node* n = m_sel.extent().node(); Node* enclosingBlockNode = enclosingBlock(n); if (!enclosingBlockNode) @@ -559,8 +557,8 @@ bool SelectionController::modify(EAlteration alter, EDirection dir, TextGranular { if (userTriggered) { SelectionController trialSelectionController; - trialSelectionController.setLastChangeWasHorizontalExtension(m_lastChangeWasHorizontalExtension); trialSelectionController.setSelection(m_sel); + trialSelectionController.setLastChangeWasHorizontalExtension(m_lastChangeWasHorizontalExtension); trialSelectionController.modify(alter, dir, granularity, false); bool change = m_frame->shouldChangeSelection(trialSelectionController.selection()); @@ -633,6 +631,8 @@ bool SelectionController::modify(EAlteration alter, EDirection dir, TextGranular setNeedsLayout(); + m_lastChangeWasHorizontalExtension = alter == EXTEND; + return true; } @@ -654,6 +654,7 @@ bool SelectionController::modify(EAlteration alter, int verticalDistance, bool u if (userTriggered) { SelectionController trialSelectionController; trialSelectionController.setSelection(m_sel); + trialSelectionController.setLastChangeWasHorizontalExtension(m_lastChangeWasHorizontalExtension); trialSelectionController.modify(alter, verticalDistance, false); bool change = m_frame->shouldChangeSelection(trialSelectionController.selection()); @@ -1267,7 +1268,7 @@ void SelectionController::focusedOrActiveStateChanged() node->setNeedsStyleRecalc(); if (RenderObject* renderer = node->renderer()) if (renderer && renderer->style()->hasAppearance()) - theme()->stateChanged(renderer, FocusState); + renderer->theme()->stateChanged(renderer, FocusState); } // Secure keyboard entry is set by the active frame. diff --git a/WebCore/editing/SelectionController.h b/WebCore/editing/SelectionController.h index bbd343c..4a13a30 100644 --- a/WebCore/editing/SelectionController.h +++ b/WebCore/editing/SelectionController.h @@ -38,7 +38,7 @@ class GraphicsContext; class RenderObject; class VisiblePosition; -class SelectionController : Noncopyable { +class SelectionController : public Noncopyable { public: enum EAlteration { MOVE, EXTEND }; enum EDirection { FORWARD, BACKWARD, RIGHT, LEFT }; diff --git a/WebCore/editing/SmartReplaceICU.cpp b/WebCore/editing/SmartReplaceICU.cpp index 18be647..9acd350 100644 --- a/WebCore/editing/SmartReplaceICU.cpp +++ b/WebCore/editing/SmartReplaceICU.cpp @@ -37,7 +37,8 @@ namespace WebCore { -static void addAllCodePoints(USet* smartSet, const String& string) { +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]); diff --git a/WebCore/editing/TextIterator.cpp b/WebCore/editing/TextIterator.cpp index b311853..08fda16 100644 --- a/WebCore/editing/TextIterator.cpp +++ b/WebCore/editing/TextIterator.cpp @@ -42,6 +42,7 @@ #include "visible_units.h" #if USE(ICU_UNICODE) && !UCONFIG_NO_COLLATION +#include "TextBreakIteratorInternalICU.h" #include <unicode/usearch.h> #endif @@ -56,7 +57,7 @@ using namespace HTMLNames; // Keeps enough of the previous text to be able to search in the future, but no more. // Non-breaking spaces are always equal to normal spaces. // Case folding is also done if <isCaseSensitive> is false. -class SearchBuffer : Noncopyable { +class SearchBuffer : public Noncopyable { public: SearchBuffer(const String& target, bool isCaseSensitive); ~SearchBuffer(); @@ -97,6 +98,148 @@ private: // -------- +static const unsigned bitsInWord = sizeof(unsigned) * 8; +static const unsigned bitInWordMask = bitsInWord - 1; + +BitStack::BitStack() + : m_size(0) +{ +} + +void BitStack::push(bool bit) +{ + unsigned index = m_size / bitsInWord; + unsigned shift = m_size & bitInWordMask; + if (!shift && index == m_words.size()) { + m_words.grow(index + 1); + m_words[index] = 0; + } + unsigned& word = m_words[index]; + unsigned mask = 1U << shift; + if (bit) + word |= mask; + else + word &= ~mask; + ++m_size; +} + +void BitStack::pop() +{ + if (m_size) + --m_size; +} + +bool BitStack::top() const +{ + if (!m_size) + return false; + unsigned shift = (m_size - 1) & bitInWordMask; + return m_words.last() & (1U << shift); +} + +unsigned BitStack::size() const +{ + return m_size; +} + +// -------- + +static inline Node* parentCrossingShadowBoundaries(Node* node) +{ + if (Node* parent = node->parentNode()) + return parent; + return node->shadowParentNode(); +} + +#ifndef NDEBUG + +static unsigned depthCrossingShadowBoundaries(Node* node) +{ + unsigned depth = 0; + for (Node* parent = parentCrossingShadowBoundaries(node); parent; parent = parentCrossingShadowBoundaries(parent)) + ++depth; + return depth; +} + +#endif + +// This function is like Range::pastLastNode, except for the fact that it can climb up out of shadow trees. +static Node* nextInPreOrderCrossingShadowBoundaries(Node* rangeEndContainer, int rangeEndOffset) +{ + if (!rangeEndContainer) + return 0; + if (rangeEndOffset >= 0 && !rangeEndContainer->offsetInCharacters()) { + if (Node* next = rangeEndContainer->childNode(rangeEndOffset)) + return next; + } + for (Node* node = rangeEndContainer; node; node = parentCrossingShadowBoundaries(node)) { + if (Node* next = node->nextSibling()) + return next; + } + return 0; +} + +static Node* previousInPostOrderCrossingShadowBoundaries(Node* rangeStartContainer, int rangeStartOffset) +{ + if (!rangeStartContainer) + return 0; + if (rangeStartOffset > 0 && !rangeStartContainer->offsetInCharacters()) { + if (Node* previous = rangeStartContainer->childNode(rangeStartOffset - 1)) + return previous; + } + for (Node* node = rangeStartContainer; node; node = parentCrossingShadowBoundaries(node)) { + if (Node* previous = node->previousSibling()) + return previous; + } + return 0; +} + +// -------- + +static inline bool fullyClipsContents(Node* node) +{ + RenderObject* renderer = node->renderer(); + if (!renderer || !renderer->isBox() || !renderer->hasOverflowClip()) + return false; + return toRenderBox(renderer)->size().isEmpty(); +} + +static inline bool ignoresContainerClip(Node* node) +{ + RenderObject* renderer = node->renderer(); + if (!renderer || renderer->isText()) + return false; + EPosition position = renderer->style()->position(); + return position == AbsolutePosition || position == FixedPosition; +} + +static void pushFullyClippedState(BitStack& stack, Node* node) +{ + ASSERT(stack.size() == depthCrossingShadowBoundaries(node)); + + // Push true if this node full clips its contents, or if a parent already has fully + // clipped and this is not a node that ignores its container's clip. + stack.push(fullyClipsContents(node) || stack.top() && !ignoresContainerClip(node)); +} + +static void setUpFullyClippedStack(BitStack& stack, Node* node) +{ + // Put the nodes in a vector so we can iterate in reverse order. + Vector<Node*, 100> ancestry; + for (Node* parent = parentCrossingShadowBoundaries(node); parent; parent = parentCrossingShadowBoundaries(parent)) + ancestry.append(parent); + + // Call pushFullyClippedState on each node starting with the earliest ancestor. + size_t size = ancestry.size(); + for (size_t i = 0; i < size; ++i) + pushFullyClippedState(stack, ancestry[size - i - 1]); + pushFullyClippedState(stack, node); + + ASSERT(stack.size() == 1 + depthCrossingShadowBoundaries(node)); +} + +// -------- + TextIterator::TextIterator() : m_startContainer(0) , m_startOffset(0) @@ -112,8 +255,7 @@ TextIterator::TextIterator() } TextIterator::TextIterator(const Range* r, bool emitCharactersBetweenAllVisiblePositions, bool enterTextControls) - : m_inShadowContent(false) - , m_startContainer(0) + : m_startContainer(0) , m_startOffset(0) , m_endContainer(0) , m_endOffset(0) @@ -144,23 +286,17 @@ TextIterator::TextIterator(const Range* r, bool emitCharactersBetweenAllVisibleP m_endContainer = endContainer; m_endOffset = endOffset; - for (Node* n = startContainer; n; n = n->parentNode()) { - if (n->isShadowNode()) { - m_inShadowContent = true; - break; - } - } - // set up the current node for processing m_node = r->firstNode(); - if (m_node == 0) + if (!m_node) return; + setUpFullyClippedStack(m_fullyClippedStack, m_node); m_offset = m_node == m_startContainer ? m_startOffset : 0; m_handledNode = false; m_handledChildren = false; // calculate first out of bounds node - m_pastEndNode = r->pastLastNode(); + m_pastEndNode = nextInPreOrderCrossingShadowBoundaries(endContainer, endOffset); // initialize node processing state m_needAnotherNewline = false; @@ -219,7 +355,7 @@ void TextIterator::advance() return; } - RenderObject *renderer = m_node->renderer(); + RenderObject* renderer = m_node->renderer(); if (!renderer) { m_handledNode = true; m_handledChildren = true; @@ -241,27 +377,20 @@ void TextIterator::advance() // 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(); + Node* next = m_handledChildren ? 0 : m_node->firstChild(); m_offset = 0; if (!next) { next = m_node->nextSibling(); if (!next) { bool pastEnd = m_node->traverseNextNode() == m_pastEndNode; - Node* parentNode = m_node->parentNode(); - if (!parentNode && m_inShadowContent) { - m_inShadowContent = false; - parentNode = m_node->shadowParentNode(); - } + Node* parentNode = parentCrossingShadowBoundaries(m_node); while (!next && parentNode) { if ((pastEnd && parentNode == m_endContainer) || m_endContainer->isDescendantOf(parentNode)) return; bool haveRenderer = m_node->renderer(); m_node = parentNode; - parentNode = m_node->parentNode(); - if (!parentNode && m_inShadowContent) { - m_inShadowContent = false; - parentNode = m_node->shadowParentNode(); - } + m_fullyClippedStack.pop(); + parentNode = parentCrossingShadowBoundaries(m_node); if (haveRenderer) exitNode(); if (m_positionNode) { @@ -272,10 +401,13 @@ void TextIterator::advance() next = m_node->nextSibling(); } } + m_fullyClippedStack.pop(); } // set the new current node m_node = next; + if (m_node) + pushFullyClippedState(m_fullyClippedStack, m_node); m_handledNode = false; m_handledChildren = false; @@ -285,13 +417,16 @@ void TextIterator::advance() } } -static inline bool compareBoxStart(const InlineTextBox *first, const InlineTextBox *second) +static inline bool compareBoxStart(const InlineTextBox* first, const InlineTextBox* second) { return first->start() < second->start(); } bool TextIterator::handleTextNode() { + if (m_fullyClippedStack.top()) + return false; + RenderText* renderer = toRenderText(m_node->renderer()); if (renderer->style()->visibility() != VISIBLE) return false; @@ -325,7 +460,7 @@ bool TextIterator::handleTextNode() // 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()) { + for (InlineTextBox* textBox = renderer->firstTextBox(); textBox; textBox = textBox->nextTextBox()) { m_sortedTextBoxes.append(textBox); } std::sort(m_sortedTextBoxes.begin(), m_sortedTextBoxes.end(), compareBoxStart); @@ -339,7 +474,7 @@ bool TextIterator::handleTextNode() void TextIterator::handleTextBox() { - RenderText *renderer = toRenderText(m_node->renderer()); + RenderText* renderer = toRenderText(m_node->renderer()); String str = renderer->text(); int start = m_offset; int end = (m_node == m_endContainer) ? m_endOffset : INT_MAX; @@ -348,7 +483,7 @@ void TextIterator::handleTextBox() int runStart = max(textBoxStart, start); // Check for collapsed space at the start of this run. - InlineTextBox *firstTextBox = renderer->containsReversedText() ? m_sortedTextBoxes[0] : renderer->firstTextBox(); + 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) { @@ -365,7 +500,7 @@ void TextIterator::handleTextBox() int runEnd = min(textBoxEnd, end); // Determine what the next text box will be, but don't advance yet - InlineTextBox *nextTextBox = 0; + InlineTextBox* nextTextBox = 0; if (renderer->containsReversedText()) { if (m_sortedTextBoxesPosition + 1 < m_sortedTextBoxes.size()) nextTextBox = m_sortedTextBoxes[m_sortedTextBoxesPosition + 1]; @@ -411,6 +546,9 @@ void TextIterator::handleTextBox() bool TextIterator::handleReplacedElement() { + if (m_fullyClippedStack.top()) + return false; + RenderObject* renderer = m_node->renderer(); if (renderer->style()->visibility() != VISIBLE) return false; @@ -421,10 +559,12 @@ bool TextIterator::handleReplacedElement() } if (m_enterTextControls && renderer->isTextControl()) { - m_node = toRenderTextControl(renderer)->innerTextElement(); - m_offset = 0; - m_inShadowContent = true; - return false; + if (HTMLElement* innerTextElement = toRenderTextControl(renderer)->innerTextElement()) { + m_node = innerTextElement->shadowTreeRootNode(); + pushFullyClippedState(m_fullyClippedStack, m_node); + m_offset = 0; + return false; + } } m_haveEmitted = true; @@ -459,7 +599,7 @@ static bool shouldEmitTabBeforeNode(Node* node) return false; // Want a tab before every cell other than the first one - RenderTableCell* rc = static_cast<RenderTableCell*>(r); + RenderTableCell* rc = toRenderTableCell(r); RenderTable* t = rc->table(); return t && (t->cellBefore(rc) || t->cellAbove(rc)); } @@ -509,7 +649,7 @@ static bool shouldEmitNewlinesBeforeAndAfterNode(Node* node) // 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(); + RenderTable* t = toRenderTableRow(r)->table(); if (t && !t->isInline()) return true; } @@ -698,7 +838,7 @@ void TextIterator::exitNode() emitCharacter(' ', baseNode->parentNode(), baseNode, 1, 1); } -void TextIterator::emitCharacter(UChar c, Node *textNode, Node *offsetBaseNode, int textStartOffset, int textEndOffset) +void TextIterator::emitCharacter(UChar c, Node* textNode, Node* offsetBaseNode, int textStartOffset, int textEndOffset) { m_haveEmitted = true; @@ -774,14 +914,14 @@ Node* TextIterator::node() const // -------- -SimplifiedBackwardsTextIterator::SimplifiedBackwardsTextIterator() : m_positionNode(0) +SimplifiedBackwardsTextIterator::SimplifiedBackwardsTextIterator() + : m_positionNode(0) { } -SimplifiedBackwardsTextIterator::SimplifiedBackwardsTextIterator(const Range *r) +SimplifiedBackwardsTextIterator::SimplifiedBackwardsTextIterator(const Range* r) + : m_positionNode(0) { - m_positionNode = 0; - if (!r) return; @@ -806,6 +946,7 @@ SimplifiedBackwardsTextIterator::SimplifiedBackwardsTextIterator(const Range *r) } m_node = endNode; + setUpFullyClippedStack(m_fullyClippedStack, m_node); m_offset = endOffset; m_handledNode = false; m_handledChildren = endOffset == 0; @@ -822,15 +963,8 @@ SimplifiedBackwardsTextIterator::SimplifiedBackwardsTextIterator(const Range *r) 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); + + m_pastStartNode = previousInPostOrderCrossingShadowBoundaries(startNode, startOffset); advance(); } @@ -845,7 +979,7 @@ void SimplifiedBackwardsTextIterator::advance() 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(); + 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) @@ -877,9 +1011,11 @@ void SimplifiedBackwardsTextIterator::advance() // Exit all other containers. next = m_node->previousSibling(); while (!next) { - if (!m_node->parentNode()) + Node* parentNode = parentCrossingShadowBoundaries(m_node); + if (!parentNode) break; - m_node = m_node->parentNode(); + m_node = parentNode; + m_fullyClippedStack.pop(); exitNode(); if (m_positionNode) { m_handledNode = true; @@ -888,9 +1024,12 @@ void SimplifiedBackwardsTextIterator::advance() } next = m_node->previousSibling(); } + m_fullyClippedStack.pop(); } m_node = next; + if (m_node) + pushFullyClippedState(m_fullyClippedStack, m_node); m_offset = m_node ? caretMaxOffset(m_node) : 0; m_handledNode = false; m_handledChildren = false; @@ -904,7 +1043,7 @@ bool SimplifiedBackwardsTextIterator::handleTextNode() { m_lastTextNode = m_node; - RenderText *renderer = toRenderText(m_node->renderer()); + RenderText* renderer = toRenderText(m_node->renderer()); String str = renderer->text(); if (!renderer->firstTextBox() && str.length() > 0) @@ -938,29 +1077,25 @@ 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)) { + 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. + // 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. + 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) +void SimplifiedBackwardsTextIterator::emitCharacter(UChar c, Node* node, int startOffset, int endOffset) { m_singleCharacterBuffer = c; m_positionNode = node; @@ -988,7 +1123,7 @@ CharacterIterator::CharacterIterator() { } -CharacterIterator::CharacterIterator(const Range *r, bool emitCharactersBetweenAllVisiblePositions, bool enterTextControls) +CharacterIterator::CharacterIterator(const Range* r, bool emitCharactersBetweenAllVisiblePositions, bool enterTextControls) : m_offset(0) , m_runOffset(0) , m_atBreak(true) @@ -1167,15 +1302,17 @@ void BackwardsCharacterIterator::advance(int count) // -------- WordAwareIterator::WordAwareIterator() -: m_previousText(0), m_didLookAhead(false) + : m_previousText(0) + , m_didLookAhead(false) { } -WordAwareIterator::WordAwareIterator(const Range *r) -: m_previousText(0), m_didLookAhead(false), m_textIterator(r) +WordAwareIterator::WordAwareIterator(const Range* r) + : m_previousText(0) + , m_didLookAhead(true) // so we consider the first chunk from the text iterator + , 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 + advance(); // get in position over the first chunk of text } // We're always in one of these modes: @@ -1185,7 +1322,7 @@ WordAwareIterator::WordAwareIterator(const Range *r) // (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 +// FIXME: Performance could be bad for huge spans next to each other that don't fall on word boundaries. void WordAwareIterator::advance() { @@ -1256,8 +1393,40 @@ const UChar* WordAwareIterator::characters() const // -------- +static inline UChar foldQuoteMark(UChar c) +{ + switch (c) { + case hebrewPunctuationGershayim: + case leftDoubleQuotationMark: + case rightDoubleQuotationMark: + return '"'; + case hebrewPunctuationGeresh: + case leftSingleQuotationMark: + case rightSingleQuotationMark: + return '\''; + default: + return c; + } +} + +static inline void foldQuoteMarks(String& s) +{ + s.replace(hebrewPunctuationGeresh, '\''); + s.replace(hebrewPunctuationGershayim, '"'); + s.replace(leftDoubleQuotationMark, '"'); + s.replace(leftSingleQuotationMark, '\''); + s.replace(rightDoubleQuotationMark, '"'); + s.replace(rightSingleQuotationMark, '\''); +} + #if USE(ICU_UNICODE) && !UCONFIG_NO_COLLATION +static inline void foldQuoteMarks(UChar* data, size_t length) +{ + for (size_t i = 0; i < length; ++i) + data[i] = foldQuoteMark(data[i]); +} + static const size_t minimumSearchBufferSize = 8192; #ifndef NDEBUG @@ -1269,13 +1438,9 @@ static UStringSearch* createSearcher() // Provide a non-empty pattern and non-empty text so usearch_open will not fail, // but it doesn't matter exactly what it is, since we don't perform any searches // without setting both the pattern and the text. - - // Pass empty string for the locale for now to get the Unicode Collation Algorithm, - // rather than something locale-specific. - UErrorCode status = U_ZERO_ERROR; - UStringSearch* searcher = usearch_open(&newlineCharacter, 1, &newlineCharacter, 1, "", 0, &status); - ASSERT(status == U_ZERO_ERROR); + UStringSearch* searcher = usearch_open(&newlineCharacter, 1, &newlineCharacter, 1, currentSearchLocaleID(), 0, &status); + ASSERT(status == U_ZERO_ERROR || status == U_USING_FALLBACK_WARNING || status == U_USING_DEFAULT_WARNING); return searcher; } @@ -1307,7 +1472,12 @@ inline SearchBuffer::SearchBuffer(const String& target, bool isCaseSensitive) { ASSERT(!m_target.isEmpty()); - size_t targetLength = target.length(); + // FIXME: We'd like to tailor the searcher to fold quote marks for us instead + // of doing it in a separate replacement pass here, but ICU doesn't offer a way + // to add tailoring on top of the locale-specific tailoring as of this writing. + foldQuoteMarks(m_target); + + size_t targetLength = m_target.length(); m_buffer.reserveInitialCapacity(max(targetLength * 8, minimumSearchBufferSize)); m_overlap = m_buffer.capacity() / 4; @@ -1347,9 +1517,11 @@ inline size_t SearchBuffer::append(const UChar* characters, size_t length) m_buffer.shrink(m_overlap); } - size_t usableLength = min(m_buffer.capacity() - m_buffer.size(), length); + size_t oldLength = m_buffer.size(); + size_t usableLength = min(m_buffer.capacity() - oldLength, length); ASSERT(usableLength); m_buffer.append(characters, usableLength); + foldQuoteMarks(m_buffer.data() + oldLength, usableLength); return usableLength; } @@ -1416,6 +1588,7 @@ inline SearchBuffer::SearchBuffer(const String& target, bool isCaseSensitive) { ASSERT(!m_target.isEmpty()); m_target.replace(noBreakSpace, ' '); + foldQuoteMarks(m_target); } inline SearchBuffer::~SearchBuffer() @@ -1435,7 +1608,7 @@ inline bool SearchBuffer::atBreak() const inline void SearchBuffer::append(UChar c, bool isStart) { - m_buffer[m_cursor] = c == noBreakSpace ? ' ' : c; + m_buffer[m_cursor] = c == noBreakSpace ? ' ' : foldQuoteMark(c); m_isCharacterStartBuffer[m_cursor] = isStart; if (++m_cursor == m_target.length()) { m_cursor = 0; @@ -1507,7 +1680,7 @@ size_t SearchBuffer::length() const // -------- -int TextIterator::rangeLength(const Range *r, bool forSelectionPreservation) +int TextIterator::rangeLength(const Range* r, bool forSelectionPreservation) { int length = 0; for (TextIterator it(r, forSelectionPreservation); !it.atEnd(); it.advance()) @@ -1522,7 +1695,7 @@ PassRefPtr<Range> TextIterator::subrange(Range* entireRange, int characterOffset return characterSubrange(entireRangeIterator, characterOffset, characterCount); } -PassRefPtr<Range> TextIterator::rangeFromLocationAndLength(Element *scope, int rangeLocation, int rangeLength, bool forSelectionPreservation) +PassRefPtr<Range> TextIterator::rangeFromLocationAndLength(Element* scope, int rangeLocation, int rangeLength, bool forSelectionPreservation) { RefPtr<Range> resultRange = scope->document()->createRange(); @@ -1742,11 +1915,6 @@ tryAgain: PassRefPtr<Range> findPlainText(const Range* range, const String& target, bool forward, bool caseSensitive) { - // We can't search effectively for a string that's entirely made of collapsible - // whitespace, so we won't even try. This also takes care of the empty string case. - if (isAllCollapsibleWhitespace(target)) - return collapsedToBoundary(range, forward); - // First, find the text. size_t matchStart; size_t matchLength; diff --git a/WebCore/editing/TextIterator.h b/WebCore/editing/TextIterator.h index f00bea4..44af3e5 100644 --- a/WebCore/editing/TextIterator.h +++ b/WebCore/editing/TextIterator.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2004, 2006 Apple Computer, Inc. All rights reserved. + * Copyright (C) 2004, 2006, 2009 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions @@ -49,6 +49,21 @@ String plainText(const Range*); UChar* plainTextToMallocAllocatedBuffer(const Range*, unsigned& bufferLength, bool isDisplayString); PassRefPtr<Range> findPlainText(const Range*, const String&, bool forward, bool caseSensitive); +class BitStack { +public: + BitStack(); + + void push(bool); + void pop(); + + bool top() const; + unsigned size() const; + +private: + unsigned m_size; + Vector<unsigned, 1> m_words; +}; + // Iterates through the DOM range, returning all the text, and 0-length boundaries // at points where replaced elements break up the text flow. The text comes back in // chunks so as to optimize for performance of the iteration. @@ -80,27 +95,27 @@ private: 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); + 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; + Node* m_node; int m_offset; bool m_handledNode; bool m_handledChildren; - bool m_inShadowContent; + BitStack m_fullyClippedStack; // The range. - Node *m_startContainer; + Node* m_startContainer; int m_startOffset; - Node *m_endContainer; + Node* m_endContainer; int m_endOffset; - Node *m_pastEndNode; + 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; + Node* m_positionNode; + mutable Node* m_positionOffsetBaseNode; mutable int m_positionStartOffset; mutable int m_positionEndOffset; const UChar* m_textCharacters; @@ -109,10 +124,10 @@ private: // 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; + InlineTextBox* m_textBox; // Used to do the whitespace collapsing logic. - Node *m_lastTextNode; + Node* m_lastTextNode; bool m_lastTextNodeEndedWithCollapsedSpace; UChar m_lastCharacter; @@ -135,12 +150,12 @@ private: }; // 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 +// 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 *); + explicit SimplifiedBackwardsTextIterator(const Range*); bool atEnd() const { return !m_positionNode; } void advance(); @@ -155,7 +170,7 @@ private: bool handleTextNode(); bool handleReplacedElement(); bool handleNonTextNode(); - void emitCharacter(UChar, Node *Node, int startOffset, int endOffset); + void emitCharacter(UChar, Node*, int startOffset, int endOffset); // Current position, not necessarily of the text being returned, but position // as we walk through the DOM tree. @@ -163,7 +178,8 @@ private: int m_offset; bool m_handledNode; bool m_handledChildren; - + BitStack m_fullyClippedStack; + // End of the range. Node* m_startNode; int m_startOffset; @@ -194,7 +210,7 @@ private: class CharacterIterator { public: CharacterIterator(); - explicit CharacterIterator(const Range* r, bool emitCharactersBetweenAllVisiblePositions = false, bool enterTextControls = false); + explicit CharacterIterator(const Range*, bool emitCharactersBetweenAllVisiblePositions = false, bool enterTextControls = false); void advance(int numCharacters); @@ -240,7 +256,7 @@ private: class WordAwareIterator { public: WordAwareIterator(); - explicit WordAwareIterator(const Range *r); + explicit WordAwareIterator(const Range*); bool atEnd() const { return !m_didLookAhead && m_textIterator.atEnd(); } void advance(); diff --git a/WebCore/editing/TypingCommand.cpp b/WebCore/editing/TypingCommand.cpp index 5ce4e56..f5901d7 100644 --- a/WebCore/editing/TypingCommand.cpp +++ b/WebCore/editing/TypingCommand.cpp @@ -58,6 +58,7 @@ TypingCommand::TypingCommand(Document *document, ETypingCommand commandType, con m_killRing(killRing), m_openedByBackwardDelete(false) { + updatePreservesTypingStyle(m_commandType); } void TypingCommand::deleteSelection(Document* document, bool smartDelete) @@ -309,8 +310,10 @@ void TypingCommand::markMisspellingsAfterTyping() } } -void TypingCommand::typingAddedToOpenCommand() +void TypingCommand::typingAddedToOpenCommand(ETypingCommand commandTypeForAddedTyping) { + updatePreservesTypingStyle(commandTypeForAddedTyping); + #if PLATFORM(MAC) && !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD) document()->frame()->editor()->appliedEditing(this); // Since the spellchecking code may also perform corrections and other replacements, it should happen after the typing changes. @@ -360,19 +363,19 @@ void TypingCommand::insertTextRunWithoutNewlines(const String &text, bool select applyCommandToComposite(command); } command->input(text, selectInsertedText); - typingAddedToOpenCommand(); + typingAddedToOpenCommand(InsertText); } void TypingCommand::insertLineBreak() { applyCommandToComposite(InsertLineBreakCommand::create(document())); - typingAddedToOpenCommand(); + typingAddedToOpenCommand(InsertLineBreak); } void TypingCommand::insertParagraphSeparator() { applyCommandToComposite(InsertParagraphSeparatorCommand::create(document())); - typingAddedToOpenCommand(); + typingAddedToOpenCommand(InsertParagraphSeparator); } void TypingCommand::insertParagraphSeparatorInQuotedContent() @@ -385,7 +388,7 @@ void TypingCommand::insertParagraphSeparatorInQuotedContent() } applyCommandToComposite(BreakBlockquoteCommand::create(document())); - typingAddedToOpenCommand(); + typingAddedToOpenCommand(InsertParagraphSeparatorInQuotedContent); } bool TypingCommand::makeEditableRootEmpty() @@ -423,7 +426,7 @@ void TypingCommand::deleteKeyPressed(TextGranularity granularity, bool killRing) // After breaking out of an empty mail blockquote, we still want continue with the deletion // so actual content will get deleted, and not just the quote style. if (breakOutOfEmptyMailBlockquotedParagraph()) - typingAddedToOpenCommand(); + typingAddedToOpenCommand(DeleteKey); m_smartDelete = false; @@ -436,12 +439,12 @@ void TypingCommand::deleteKeyPressed(TextGranularity granularity, bool killRing) if (endingSelection().visibleStart().previous(true).isNull()) { // When the caret is at the start of the editable area in an empty list item, break out of the list item. if (breakOutOfEmptyListItem()) { - typingAddedToOpenCommand(); + typingAddedToOpenCommand(DeleteKey); return; } // When there are no visible positions in the editing root, delete its entire contents. if (endingSelection().visibleStart().next(true).isNull() && makeEditableRootEmpty()) { - typingAddedToOpenCommand(); + typingAddedToOpenCommand(DeleteKey); return; } } @@ -457,7 +460,7 @@ void TypingCommand::deleteKeyPressed(TextGranularity granularity, bool killRing) // If the caret is just after a table, select the table and don't delete anything. } else if (Node* table = isFirstPositionAfterTable(visibleStart)) { setEndingSelection(VisibleSelection(Position(table, 0), endingSelection().start(), DOWNSTREAM)); - typingAddedToOpenCommand(); + typingAddedToOpenCommand(DeleteKey); return; } @@ -498,7 +501,7 @@ void TypingCommand::deleteKeyPressed(TextGranularity granularity, bool killRing) setStartingSelection(selectionAfterUndo); CompositeEditCommand::deleteSelection(selectionToDelete, m_smartDelete); setSmartDelete(false); - typingAddedToOpenCommand(); + typingAddedToOpenCommand(DeleteKey); } void TypingCommand::forwardDeleteKeyPressed(TextGranularity granularity, bool killRing) @@ -530,7 +533,7 @@ void TypingCommand::forwardDeleteKeyPressed(TextGranularity granularity, bool ki // When deleting tables: Select the table first, then perform the deletion if (downstreamEnd.node() && downstreamEnd.node()->renderer() && downstreamEnd.node()->renderer()->isTable() && downstreamEnd.deprecatedEditingOffset() == 0) { setEndingSelection(VisibleSelection(endingSelection().end(), lastDeepEditingPositionForNode(downstreamEnd.node()), DOWNSTREAM)); - typingAddedToOpenCommand(); + typingAddedToOpenCommand(ForwardDeleteKey); return; } @@ -578,30 +581,32 @@ void TypingCommand::forwardDeleteKeyPressed(TextGranularity granularity, bool ki setStartingSelection(selectionAfterUndo); CompositeEditCommand::deleteSelection(selectionToDelete, m_smartDelete); setSmartDelete(false); - typingAddedToOpenCommand(); + typingAddedToOpenCommand(ForwardDeleteKey); } void TypingCommand::deleteSelection(bool smartDelete) { CompositeEditCommand::deleteSelection(smartDelete); - typingAddedToOpenCommand(); + typingAddedToOpenCommand(DeleteSelection); } -bool TypingCommand::preservesTypingStyle() const +void TypingCommand::updatePreservesTypingStyle(ETypingCommand commandType) { - switch (m_commandType) { + switch (commandType) { case DeleteSelection: case DeleteKey: case ForwardDeleteKey: case InsertParagraphSeparator: case InsertLineBreak: - return true; + m_preservesTypingStyle = true; + return; case InsertParagraphSeparatorInQuotedContent: case InsertText: - return false; + m_preservesTypingStyle = false; + return; } ASSERT_NOT_REACHED(); - return false; + m_preservesTypingStyle = false; } bool TypingCommand::isTypingCommand() const diff --git a/WebCore/editing/TypingCommand.h b/WebCore/editing/TypingCommand.h index 2c52447..7c89a7c 100644 --- a/WebCore/editing/TypingCommand.h +++ b/WebCore/editing/TypingCommand.h @@ -79,10 +79,11 @@ private: virtual void doApply(); virtual EditAction editingAction() const; virtual bool isTypingCommand() const; - virtual bool preservesTypingStyle() const; + virtual bool preservesTypingStyle() const { return m_preservesTypingStyle; } + void updatePreservesTypingStyle(ETypingCommand); void markMisspellingsAfterTyping(); - void typingAddedToOpenCommand(); + void typingAddedToOpenCommand(ETypingCommand); bool makeEditableRootEmpty(); ETypingCommand m_commandType; @@ -92,6 +93,7 @@ private: bool m_smartDelete; TextGranularity m_granularity; bool m_killRing; + bool m_preservesTypingStyle; // Undoing a series of backward deletes will restore a selection around all of the // characters that were deleted, but only if the typing command being undone diff --git a/WebCore/editing/gtk/SelectionControllerGtk.cpp b/WebCore/editing/gtk/SelectionControllerGtk.cpp index 52fbbab..f3bd4bc 100644 --- a/WebCore/editing/gtk/SelectionControllerGtk.cpp +++ b/WebCore/editing/gtk/SelectionControllerGtk.cpp @@ -30,11 +30,11 @@ namespace WebCore { void SelectionController::notifyAccessibilityForSelectionChange() { if (AXObjectCache::accessibilityEnabled() && m_sel.start().isNotNull() && m_sel.end().isNotNull()) { - RenderObject* focusedNode = m_sel.start().node()->renderer(); + RenderObject* focusedNode = m_sel.end().node()->renderer(); AccessibilityObject* accessibilityObject = m_frame->document()->axObjectCache()->getOrCreate(focusedNode); AtkObject* wrapper = accessibilityObject->wrapper(); if (ATK_IS_TEXT(wrapper)) { - g_signal_emit_by_name(wrapper, "text-caret-moved", m_sel.start().offsetInContainerNode()); + g_signal_emit_by_name(wrapper, "text-caret-moved", m_sel.end().computeOffsetInContainerNode()); if (m_sel.isRange()) g_signal_emit_by_name(wrapper, "text-selection-changed"); diff --git a/WebCore/editing/haiku/EditorHaiku.cpp b/WebCore/editing/haiku/EditorHaiku.cpp new file mode 100644 index 0000000..17fde1f --- /dev/null +++ b/WebCore/editing/haiku/EditorHaiku.cpp @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2007 Ryan Leavengood <leavengood@gmail.com> + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "config.h" +#include "Editor.h" + +#include "ClipboardAccessPolicy.h" +#include "Clipboard.h" + +#include "ClipboardHaiku.h" + + +namespace WebCore { + +PassRefPtr<Clipboard> Editor::newGeneralClipboard(ClipboardAccessPolicy policy) +{ + return new ClipboardHaiku(policy, false); +} + +} // namespace WebCore + diff --git a/WebCore/editing/htmlediting.cpp b/WebCore/editing/htmlediting.cpp index 7b51295..4124c48 100644 --- a/WebCore/editing/htmlediting.cpp +++ b/WebCore/editing/htmlediting.cpp @@ -84,6 +84,7 @@ bool canHaveChildrenForEditing(const Node* node) !node->hasTagName(embedTag) && !node->hasTagName(appletTag) && !node->hasTagName(selectTag) && + !node->hasTagName(datagridTag) && #if ENABLE(WML) !node->hasTagName(WMLNames::doTag) && #endif @@ -206,9 +207,18 @@ Element* editableRootForPosition(const Position& p) return node->rootEditableElement(); } -bool isContentEditable(const Node* node) +// Finds the enclosing element until which the tree can be split. +// When a user hits ENTER, he/she won't expect this element to be split into two. +// You may pass it as the second argument of splitTreeToNode. +Element* unsplittableElementForPosition(const Position& p) { - return node->isContentEditable(); + // Since enclosingNodeOfType won't search beyond the highest root editable node, + // this code works even if the closest table cell was outside of the root editable node. + Element* enclosingCell = static_cast<Element*>(enclosingNodeOfType(p, &isTableCell, true)); + if (enclosingCell) + return enclosingCell; + + return editableRootForPosition(p); } Position nextCandidate(const Position& position) @@ -564,16 +574,79 @@ Node* isLastPositionBeforeTable(const VisiblePosition& visiblePosition) Position positionBeforeNode(const Node* node) { - // FIXME: This should ASSERT(node->parentNode()) + ASSERT(node); + // FIXME: Should ASSERT(node->parentNode()) but doing so results in editing/deleting/delete-ligature-001.html crashing return Position(node->parentNode(), node->nodeIndex()); } Position positionAfterNode(const Node* node) { - // FIXME: This should ASSERT(node->parentNode()) + ASSERT(node); + // FIXME: Should ASSERT(node->parentNode()) but doing so results in editing/deleting/delete-ligature-001.html crashing return Position(node->parentNode(), node->nodeIndex() + 1); } +// Returns the visible position at the beginning of a node +VisiblePosition visiblePositionBeforeNode(Node* node) +{ + ASSERT(node); + if (node->childNodeCount()) + return VisiblePosition(node, 0, DOWNSTREAM); + ASSERT(node->parentNode()); + return positionBeforeNode(node); +} + +// Returns the visible position at the ending of a node +VisiblePosition visiblePositionAfterNode(Node* node) +{ + ASSERT(node); + if (node->childNodeCount()) + return VisiblePosition(node, node->childNodeCount(), DOWNSTREAM); + ASSERT(node->parentNode()); + return positionAfterNode(node); +} + +// Create a range object with two visible positions, start and end. +// create(PassRefPtr<Document>, const Position&, const Position&); will use deprecatedEditingOffset +// Use this function instead of create a regular range object (avoiding editing offset). +PassRefPtr<Range> createRange(PassRefPtr<Document> document, const VisiblePosition& start, const VisiblePosition& end, ExceptionCode& ec) +{ + ec = 0; + RefPtr<Range> selectedRange = Range::create(document); + selectedRange->setStart(start.deepEquivalent().containerNode(), start.deepEquivalent().computeOffsetInContainerNode(), ec); + if (!ec) + selectedRange->setEnd(end.deepEquivalent().containerNode(), end.deepEquivalent().computeOffsetInContainerNode(), ec); + return selectedRange.release(); +} + +// Extend rangeToExtend to include nodes that wraps range and visibly starts and ends inside or at the boudnaries of maximumRange +// e.g. if the original range spaned "hello" in <div>hello</div>, then this function extends the range to contain div's around it. +// Call this function before copying / moving paragraphs to contain all wrapping nodes. +// This function stops extending the range immediately below rootNode; i.e. the extended range can contain a child node of rootNode +// but it can never contain rootNode itself. +PassRefPtr<Range> extendRangeToWrappingNodes(PassRefPtr<Range> range, const Range* maximumRange, const Node* rootNode) +{ + ASSERT(range); + ASSERT(maximumRange); + + ExceptionCode ec = 0; + Node* ancestor = range->commonAncestorContainer(ec);// find the cloeset common ancestor + Node* highestNode = 0; + // traverse through ancestors as long as they are contained within the range, content-editable, and below rootNode (could be =0). + while (ancestor && ancestor->isContentEditable() && isNodeVisiblyContainedWithin(ancestor, maximumRange) && ancestor != rootNode) { + highestNode = ancestor; + ancestor = ancestor->parentNode(); + } + + if (!highestNode) + return range; + + // Create new range with the highest editable node contained within the range + RefPtr<Range> extendedRange = Range::create(range->ownerDocument()); + extendedRange->selectNode(highestNode, ec); + return extendedRange.release(); +} + bool isListElement(Node *n) { return (n && (n->hasTagName(ulTag) || n->hasTagName(olTag) || n->hasTagName(dlTag))); @@ -586,7 +659,7 @@ Node* enclosingNodeWithTag(const Position& p, const QualifiedName& tagName) Node* root = highestEditableRoot(p); for (Node* n = p.node(); n; n = n->parentNode()) { - if (root && !isContentEditable(n)) + if (root && !n->isContentEditable()) continue; if (n->hasTagName(tagName)) return n; @@ -606,7 +679,7 @@ Node* enclosingNodeOfType(const Position& p, bool (*nodeIsOfType)(const Node*), 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) && onlyReturnEditableNodes) + if (root && !n->isContentEditable() && onlyReturnEditableNodes) continue; if ((*nodeIsOfType)(n)) return n; @@ -664,7 +737,7 @@ HTMLElement* enclosingList(Node* node) return 0; } -Node* enclosingListChild(Node *node) +HTMLElement* enclosingListChild(Node *node) { if (!node) return 0; @@ -675,7 +748,7 @@ Node* enclosingListChild(Node *node) // 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; + return static_cast<HTMLElement*>(n); if (n == root || isTableCell(n)) return 0; } @@ -737,6 +810,18 @@ HTMLElement* outermostEnclosingList(Node* node) return list; } +bool canMergeLists(Element* firstList, Element* secondList) +{ + if (!firstList || !secondList) + return false; + + return firstList->hasTagName(secondList->tagQName())// make sure the list types match (ol vs. ul) + && firstList->isContentEditable() && secondList->isContentEditable()// both lists are editable + && firstList->rootEditableElement() == secondList->rootEditableElement()// don't cross editing boundaries + && isVisiblyAdjacent(positionAfterNode(firstList), positionBeforeNode(secondList)); + // Make sure there is no visible content between this li and the previous list +} + Node* highestAncestor(Node* node) { ASSERT(node); @@ -961,7 +1046,7 @@ VisibleSelection selectionForParagraphIteration(const VisibleSelection& original } -int indexForVisiblePosition(VisiblePosition& visiblePosition) +int indexForVisiblePosition(const VisiblePosition& visiblePosition) { if (visiblePosition.isNull()) return 0; @@ -970,6 +1055,29 @@ int indexForVisiblePosition(VisiblePosition& visiblePosition) return TextIterator::rangeLength(range.get(), true); } +// Determines whether two positions are visibly next to each other (first then second) +// while ignoring whitespaces and unrendered nodes +bool isVisiblyAdjacent(const Position& first, const Position& second) +{ + return VisiblePosition(first) == VisiblePosition(second.upstream()); +} + +// Determines whether a node is inside a range or visibly starts and ends at the boundaries of the range. +// Call this function to determine whether a node is visibly fit inside selectedRange +bool isNodeVisiblyContainedWithin(Node* node, const Range* selectedRange) +{ + ASSERT(node); + ASSERT(selectedRange); + // If the node is inside the range, then it surely is contained within + ExceptionCode ec = 0; + if (selectedRange->compareNode(node, ec) == Range::NODE_INSIDE) + return true; + + // If the node starts and ends at where selectedRange starts and ends, the node is contained within + return visiblePositionBeforeNode(node) == selectedRange->startPosition() + && visiblePositionAfterNode(node) == selectedRange->endPosition(); +} + PassRefPtr<Range> avoidIntersectionWithNode(const Range* range, Node* node) { if (!range) diff --git a/WebCore/editing/htmlediting.h b/WebCore/editing/htmlediting.h index 374b512..f98b149 100644 --- a/WebCore/editing/htmlediting.h +++ b/WebCore/editing/htmlediting.h @@ -28,6 +28,7 @@ #include <wtf/Forward.h> #include "HTMLNames.h" +#include "ExceptionCode.h" namespace WebCore { @@ -53,7 +54,6 @@ VisiblePosition lastEditablePositionBeforePositionInRoot(const Position&, Node*) int comparePositions(const Position&, const Position&); int comparePositions(const VisiblePosition&, const VisiblePosition&); Node* lowestEditableAncestor(Node*); -bool isContentEditable(const Node*); Position nextCandidate(const Position&); Position nextVisuallyDistinctCandidate(const Position&); Position previousCandidate(const Position&); @@ -61,6 +61,7 @@ Position previousVisuallyDistinctCandidate(const Position&); bool isEditablePosition(const Position&); bool isRichlyEditablePosition(const Position&); Element* editableRootForPosition(const Position&); +Element* unsplittableElementForPosition(const Position&); bool isBlock(const Node*); Node* enclosingBlock(Node*); @@ -71,6 +72,10 @@ const String& nonBreakingSpaceString(); Position positionBeforeNode(const Node*); Position positionAfterNode(const Node*); +VisiblePosition visiblePositionBeforeNode(Node*); +VisiblePosition visiblePositionAfterNode(Node*); +PassRefPtr<Range> createRange(PassRefPtr<Document>, const VisiblePosition& start, const VisiblePosition& end, ExceptionCode&); +PassRefPtr<Range> extendRangeToWrappingNodes(PassRefPtr<Range> rangeToExtend, const Range* maximumRange, const Node* rootNode); PassRefPtr<Range> avoidIntersectionWithNode(const Range*, Node*); VisibleSelection avoidIntersectionWithNode(const VisibleSelection&, Node*); @@ -123,7 +128,8 @@ Node* enclosingAnchorElement(const Position&); bool isListElement(Node*); HTMLElement* enclosingList(Node*); HTMLElement* outermostEnclosingList(Node*); -Node* enclosingListChild(Node*); +HTMLElement* enclosingListChild(Node*); +bool canMergeLists(Element* firstList, Element* secondList); Node* highestAncestor(Node*); bool isTableElement(Node*); bool isTableCell(const Node*); @@ -133,8 +139,9 @@ bool lineBreakExistsAtVisiblePosition(const VisiblePosition&); VisibleSelection selectionForParagraphIteration(const VisibleSelection&); -int indexForVisiblePosition(VisiblePosition&); - +int indexForVisiblePosition(const VisiblePosition&); +bool isVisiblyAdjacent(const Position& first, const Position& second); +bool isNodeVisiblyContainedWithin(Node*, const Range*); } #endif diff --git a/WebCore/editing/markup.cpp b/WebCore/editing/markup.cpp index d6fe1ce..14ce7f6 100644 --- a/WebCore/editing/markup.cpp +++ b/WebCore/editing/markup.cpp @@ -58,6 +58,7 @@ #include "htmlediting.h" #include "visible_units.h" #include <wtf/StdLibExtras.h> +#include "ApplyStyleCommand.h" using namespace std; @@ -291,20 +292,16 @@ static void removeEnclosingMailBlockquoteStyle(CSSMutableStyleDeclaration* style 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); + + removeStylesAddedByNode(style, blockquote); } static void removeDefaultStyles(CSSMutableStyleDeclaration* style, Document* document) { if (!document || !document->documentElement()) return; - - RefPtr<CSSMutableStyleDeclaration> documentStyle = computedStyle(document->documentElement())->copyInheritableProperties(); - documentStyle->diff(style); + + prepareEditingStyleToApplyAt(style, Position(document->documentElement(), 0)); } static bool shouldAddNamespaceElem(const Element* elem) @@ -692,7 +689,7 @@ static PassRefPtr<CSSMutableStyleDeclaration> styleFromMatchedRulesAndInlineDecl return style.release(); } -static bool propertyMissingOrEqualToNone(CSSMutableStyleDeclaration* style, int propertyID) +static bool propertyMissingOrEqualToNone(CSSStyleDeclaration* style, int propertyID) { if (!style) return false; @@ -704,8 +701,11 @@ static bool propertyMissingOrEqualToNone(CSSMutableStyleDeclaration* style, int return static_cast<CSSPrimitiveValue*>(value.get())->getIdent() == CSSValueNone; } -static bool elementHasTextDecorationProperty(const Node* node) +static bool isElementPresentational(const Node* node) { + if (node->hasTagName(uTag) || node->hasTagName(sTag) || node->hasTagName(strikeTag) || + node->hasTagName(iTag) || node->hasTagName(emTag) || node->hasTagName(bTag) || node->hasTagName(strongTag)) + return true; RefPtr<CSSMutableStyleDeclaration> style = styleFromMatchedRulesAndInlineDecl(node); if (!style) return false; @@ -763,6 +763,25 @@ static bool shouldIncludeWrapperForFullySelectedRoot(Node* fullySelectedRoot, CS style->getPropertyCSSValue(CSSPropertyBackgroundColor); } +static void addStyleMarkup(Vector<String>& preMarkups, Vector<String>& postMarkups, CSSStyleDeclaration* style, Document* document, bool isBlock = false) +{ + // All text-decoration-related elements should have been treated as special ancestors + // If we ever hit this ASSERT, we should export StyleChange in ApplyStyleCommand and use it here + ASSERT(propertyMissingOrEqualToNone(style, CSSPropertyTextDecoration) && propertyMissingOrEqualToNone(style, CSSPropertyWebkitTextDecorationsInEffect)); + DEFINE_STATIC_LOCAL(const String, divStyle, ("<div style=\"")); + DEFINE_STATIC_LOCAL(const String, divClose, ("</div>")); + DEFINE_STATIC_LOCAL(const String, styleSpanOpen, ("<span class=\"" AppleStyleSpanClass "\" style=\"")); + DEFINE_STATIC_LOCAL(const String, styleSpanClose, ("</span>")); + Vector<UChar> openTag; + append(openTag, isBlock ? divStyle : styleSpanOpen); + appendAttributeValue(openTag, style->cssText(), document->isHTMLDocument()); + openTag.append('\"'); + openTag.append('>'); + preMarkups.append(String::adopt(openTag)); + + postMarkups.append(isBlock ? divClose : styleSpanClose); +} + // 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) @@ -776,8 +795,6 @@ String createMarkup(const Range* range, Vector<Node*>* nodes, EAnnotateForInterc if (!document) return ""; - bool documentIsHTML = document->isHTMLDocument(); - // 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(); @@ -927,12 +944,12 @@ String createMarkup(const Range* range, Vector<Node*>* nodes, EAnnotateForInterc if (isMailBlockquote(ancestor)) specialCommonAncestor = ancestor; } - + Node* checkAncestor = specialCommonAncestor ? specialCommonAncestor : commonAncestor; if (checkAncestor->renderer()) { - RefPtr<CSSMutableStyleDeclaration> checkAncestorStyle = computedStyle(checkAncestor)->copyInheritableProperties(); - if (!propertyMissingOrEqualToNone(checkAncestorStyle.get(), CSSPropertyWebkitTextDecorationsInEffect)) - specialCommonAncestor = enclosingNodeOfType(Position(checkAncestor, 0), &elementHasTextDecorationProperty); + Node* newSpecialCommonAncestor = highestEnclosingNodeOfType(Position(checkAncestor, 0), &isElementPresentational); + if (newSpecialCommonAncestor) + specialCommonAncestor = newSpecialCommonAncestor; } // If a single tab is selected, commonAncestor will be a text node inside a tab span. @@ -966,18 +983,8 @@ String createMarkup(const Range* range, Vector<Node*>* nodes, EAnnotateForInterc if (!fullySelectedRootStyle->getPropertyCSSValue(CSSPropertyBackgroundImage) && static_cast<Element*>(fullySelectedRoot)->hasAttribute(backgroundAttr)) fullySelectedRootStyle->setProperty(CSSPropertyBackgroundImage, "url('" + static_cast<Element*>(fullySelectedRoot)->getAttribute(backgroundAttr) + "')"); - if (fullySelectedRootStyle->length()) { - Vector<UChar> openTag; - DEFINE_STATIC_LOCAL(const String, divStyle, ("<div style=\"")); - append(openTag, divStyle); - appendAttributeValue(openTag, fullySelectedRootStyle->cssText(), documentIsHTML); - openTag.append('\"'); - openTag.append('>'); - preMarkups.append(String::adopt(openTag)); - - DEFINE_STATIC_LOCAL(const String, divCloseTag, ("</div>")); - markups.append(divCloseTag); - } + if (fullySelectedRootStyle->length()) + addStyleMarkup(preMarkups, markups, fullySelectedRootStyle.get(), document, true); } else { // Since this node and all the other ancestors are not in the selection we want to set RangeFullySelectsNode to DoesNotFullySelectNode // so that styles that affect the exterior of the node are not included. @@ -993,14 +1000,11 @@ String createMarkup(const Range* range, Vector<Node*>* nodes, EAnnotateForInterc break; } } - - DEFINE_STATIC_LOCAL(const String, styleSpanOpen, ("<span class=\"" AppleStyleSpanClass "\" style=\"")); - DEFINE_STATIC_LOCAL(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(); + RefPtr<CSSMutableStyleDeclaration> style = editingStyleAtPosition(Position(parentOfLastClosed, 0)); // 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 @@ -1016,33 +1020,18 @@ String createMarkup(const Range* range, Vector<Node*>* nodes, EAnnotateForInterc if (convertBlocksToInlines) style->removeBlockProperties(); - if (style->length() > 0) { - Vector<UChar> openTag; - append(openTag, styleSpanOpen); - appendAttributeValue(openTag, style->cssText(), documentIsHTML); - openTag.append('\"'); - openTag.append('>'); - preMarkups.append(String::adopt(openTag)); - - markups.append(styleSpanClose); - } + if (style->length() > 0) + addStyleMarkup(preMarkups, markups, style.get(), document); } if (lastClosed && lastClosed != document->documentElement()) { // Add a style span with the document's default styles. We add these in a separate // span so that at paste time we can differentiate between document defaults and user // applied styles. - RefPtr<CSSMutableStyleDeclaration> defaultStyle = computedStyle(document->documentElement())->copyInheritableProperties(); - - if (defaultStyle->length() > 0) { - Vector<UChar> openTag; - append(openTag, styleSpanOpen); - appendAttributeValue(openTag, defaultStyle->cssText(), documentIsHTML); - openTag.append('\"'); - openTag.append('>'); - preMarkups.append(String::adopt(openTag)); - markups.append(styleSpanClose); - } + RefPtr<CSSMutableStyleDeclaration> defaultStyle = editingStyleAtPosition(Position(document->documentElement(), 0)); + + if (defaultStyle->length() > 0) + addStyleMarkup(preMarkups, markups, defaultStyle.get(), document); } // FIXME: The interchange newline should be placed in the block that it's in, not after all of the content, unconditionally. diff --git a/WebCore/editing/visible_units.cpp b/WebCore/editing/visible_units.cpp index 02e9fb8..869c893 100644 --- a/WebCore/editing/visible_units.cpp +++ b/WebCore/editing/visible_units.cpp @@ -1178,9 +1178,6 @@ VisiblePosition logicalStartOfLine(const VisiblePosition& c) { VisiblePosition visPos = logicalStartPositionForLine(c); - if (visPos.isNull()) - return c.honorEditableBoundaryAtOrAfter(visPos); - return c.honorEditableBoundaryAtOrAfter(visPos); } |