diff options
author | Steve Block <steveblock@google.com> | 2011-05-13 06:44:40 -0700 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2011-05-13 06:44:40 -0700 |
commit | 08014c20784f3db5df3a89b73cce46037b77eb59 (patch) | |
tree | 47749210d31e19e6e2f64036fa8fae2ad693476f /Source/WebCore/editing/ApplyStyleCommand.cpp | |
parent | 860220379e56aeb66424861ad602b07ee22b4055 (diff) | |
parent | 4c3661f7918f8b3f139f824efb7855bedccb4c94 (diff) | |
download | external_webkit-08014c20784f3db5df3a89b73cce46037b77eb59.zip external_webkit-08014c20784f3db5df3a89b73cce46037b77eb59.tar.gz external_webkit-08014c20784f3db5df3a89b73cce46037b77eb59.tar.bz2 |
Merge changes Ide388898,Ic49f367c,I1158a808,Iacb6ca5d,I2100dd3a,I5c1abe54,Ib0ef9902,I31dbc523,I570314b3
* changes:
Merge WebKit at r75315: Update WebKit version
Merge WebKit at r75315: Add FrameLoaderClient PageCache stubs
Merge WebKit at r75315: Stub out AXObjectCache::remove()
Merge WebKit at r75315: Fix ImageBuffer
Merge WebKit at r75315: Fix PluginData::initPlugins()
Merge WebKit at r75315: Fix conflicts
Merge WebKit at r75315: Fix Makefiles
Merge WebKit at r75315: Move Android-specific WebCore files to Source
Merge WebKit at r75315: Initial merge by git.
Diffstat (limited to 'Source/WebCore/editing/ApplyStyleCommand.cpp')
-rw-r--r-- | Source/WebCore/editing/ApplyStyleCommand.cpp | 1931 |
1 files changed, 1931 insertions, 0 deletions
diff --git a/Source/WebCore/editing/ApplyStyleCommand.cpp b/Source/WebCore/editing/ApplyStyleCommand.cpp new file mode 100644 index 0000000..71b6a27 --- /dev/null +++ b/Source/WebCore/editing/ApplyStyleCommand.cpp @@ -0,0 +1,1931 @@ +/* + * Copyright (C) 2005, 2006, 2008, 2009 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "config.h" +#include "ApplyStyleCommand.h" + +#include "CSSComputedStyleDeclaration.h" +#include "CSSMutableStyleDeclaration.h" +#include "CSSParser.h" +#include "CSSProperty.h" +#include "CSSPropertyNames.h" +#include "CSSStyleSelector.h" +#include "CSSValueKeywords.h" +#include "Document.h" +#include "EditingStyle.h" +#include "Editor.h" +#include "Frame.h" +#include "HTMLFontElement.h" +#include "HTMLInterchange.h" +#include "HTMLNames.h" +#include "NodeList.h" +#include "Range.h" +#include "RenderObject.h" +#include "Text.h" +#include "TextIterator.h" +#include "htmlediting.h" +#include "visible_units.h" +#include <wtf/StdLibExtras.h> + +namespace WebCore { + +using namespace HTMLNames; + +static RGBA32 getRGBAFontColor(CSSStyleDeclaration* style) +{ + RefPtr<CSSValue> colorValue = style->getPropertyCSSValue(CSSPropertyColor); + if (!colorValue) + return Color::transparent; + + ASSERT(colorValue->isPrimitiveValue()); + + CSSPrimitiveValue* primitiveColor = static_cast<CSSPrimitiveValue*>(colorValue.get()); + RGBA32 rgba = 0; + if (primitiveColor->primitiveType() != CSSPrimitiveValue::CSS_RGBCOLOR) { + CSSParser::parseColor(rgba, colorValue->cssText()); + // Need to take care of named color such as green and black + // This code should be removed after https://bugs.webkit.org/show_bug.cgi?id=28282 is fixed. + } else + rgba = primitiveColor->getRGBA32Value(); + + return rgba; +} + +class StyleChange { +public: + explicit StyleChange(CSSStyleDeclaration*, const Position&); + + String cssStyle() const { return m_cssStyle; } + bool applyBold() const { return m_applyBold; } + bool applyItalic() const { return m_applyItalic; } + bool applyUnderline() const { return m_applyUnderline; } + bool applyLineThrough() const { return m_applyLineThrough; } + bool applySubscript() const { return m_applySubscript; } + bool applySuperscript() const { return m_applySuperscript; } + bool applyFontColor() const { return m_applyFontColor.length() > 0; } + bool applyFontFace() const { return m_applyFontFace.length() > 0; } + bool applyFontSize() const { return m_applyFontSize.length() > 0; } + + String fontColor() { return m_applyFontColor; } + String fontFace() { return m_applyFontFace; } + String fontSize() { return m_applyFontSize; } + + bool operator==(const StyleChange& other) + { + return m_cssStyle == other.m_cssStyle + && m_applyBold == other.m_applyBold + && m_applyItalic == other.m_applyItalic + && m_applyUnderline == other.m_applyUnderline + && m_applyLineThrough == other.m_applyLineThrough + && m_applySubscript == other.m_applySubscript + && m_applySuperscript == other.m_applySuperscript + && m_applyFontColor == other.m_applyFontColor + && m_applyFontFace == other.m_applyFontFace + && m_applyFontSize == other.m_applyFontSize; + } + bool operator!=(const StyleChange& other) + { + return !(*this == other); + } +private: + void init(PassRefPtr<CSSStyleDeclaration>, const Position&); + void reconcileTextDecorationProperties(CSSMutableStyleDeclaration*); + void extractTextStyles(Document*, CSSMutableStyleDeclaration*, bool shouldUseFixedFontDefautlSize); + + String m_cssStyle; + bool m_applyBold; + bool m_applyItalic; + bool m_applyUnderline; + bool m_applyLineThrough; + bool m_applySubscript; + bool m_applySuperscript; + String m_applyFontColor; + String m_applyFontFace; + String m_applyFontSize; +}; + + +StyleChange::StyleChange(CSSStyleDeclaration* style, const Position& position) + : m_applyBold(false) + , m_applyItalic(false) + , m_applyUnderline(false) + , m_applyLineThrough(false) + , m_applySubscript(false) + , m_applySuperscript(false) +{ + init(style, position); +} + +void StyleChange::init(PassRefPtr<CSSStyleDeclaration> style, const Position& position) +{ + Document* document = position.node() ? position.node()->document() : 0; + if (!document || !document->frame()) + return; + + RefPtr<CSSComputedStyleDeclaration> computedStyle = position.computedStyle(); + RefPtr<CSSMutableStyleDeclaration> mutableStyle = getPropertiesNotIn(style.get(), computedStyle.get()); + + reconcileTextDecorationProperties(mutableStyle.get()); + if (!document->frame()->editor()->shouldStyleWithCSS()) + extractTextStyles(document, mutableStyle.get(), computedStyle->useFixedFontDefaultSize()); + + // Changing the whitespace style in a tab span would collapse the tab into a space. + if (isTabSpanTextNode(position.node()) || isTabSpanNode((position.node()))) + mutableStyle->removeProperty(CSSPropertyWhiteSpace); + + // If unicode-bidi is present in mutableStyle and direction is not, then add direction to mutableStyle. + // FIXME: Shouldn't this be done in getPropertiesNotIn? + if (mutableStyle->getPropertyCSSValue(CSSPropertyUnicodeBidi) && !style->getPropertyCSSValue(CSSPropertyDirection)) + mutableStyle->setProperty(CSSPropertyDirection, style->getPropertyValue(CSSPropertyDirection)); + + // Save the result for later + m_cssStyle = mutableStyle->cssText().stripWhiteSpace(); +} + +void StyleChange::reconcileTextDecorationProperties(CSSMutableStyleDeclaration* style) +{ + RefPtr<CSSValue> textDecorationsInEffect = style->getPropertyCSSValue(CSSPropertyWebkitTextDecorationsInEffect); + RefPtr<CSSValue> textDecoration = style->getPropertyCSSValue(CSSPropertyTextDecoration); + // We shouldn't have both text-decoration and -webkit-text-decorations-in-effect because that wouldn't make sense. + ASSERT(!textDecorationsInEffect || !textDecoration); + if (textDecorationsInEffect) { + style->setProperty(CSSPropertyTextDecoration, textDecorationsInEffect->cssText()); + style->removeProperty(CSSPropertyWebkitTextDecorationsInEffect); + textDecoration = textDecorationsInEffect; + } + + // If text-decoration is set to "none", remove the property because we don't want to add redundant "text-decoration: none". + if (textDecoration && !textDecoration->isValueList()) + style->removeProperty(CSSPropertyTextDecoration); +} + +static int getIdentifierValue(CSSStyleDeclaration* style, int propertyID) +{ + if (!style) + return 0; + + RefPtr<CSSValue> value = style->getPropertyCSSValue(propertyID); + if (!value || !value->isPrimitiveValue()) + return 0; + + return static_cast<CSSPrimitiveValue*>(value.get())->getIdent(); +} + +static void setTextDecorationProperty(CSSMutableStyleDeclaration* style, const CSSValueList* newTextDecoration, int propertyID) +{ + if (newTextDecoration->length()) + style->setProperty(propertyID, newTextDecoration->cssText(), style->getPropertyPriority(propertyID)); + else { + // text-decoration: none is redundant since it does not remove any text decorations. + ASSERT(!style->getPropertyPriority(propertyID)); + style->removeProperty(propertyID); + } +} + +void StyleChange::extractTextStyles(Document* document, CSSMutableStyleDeclaration* style, bool shouldUseFixedFontDefautlSize) +{ + ASSERT(style); + + if (getIdentifierValue(style, CSSPropertyFontWeight) == CSSValueBold) { + style->removeProperty(CSSPropertyFontWeight); + m_applyBold = true; + } + + int fontStyle = getIdentifierValue(style, CSSPropertyFontStyle); + if (fontStyle == CSSValueItalic || fontStyle == CSSValueOblique) { + style->removeProperty(CSSPropertyFontStyle); + m_applyItalic = true; + } + + // Assuming reconcileTextDecorationProperties has been called, there should not be -webkit-text-decorations-in-effect + // Furthermore, text-decoration: none has been trimmed so that text-decoration property is always a CSSValueList. + RefPtr<CSSValue> textDecoration = style->getPropertyCSSValue(CSSPropertyTextDecoration); + if (textDecoration && textDecoration->isValueList()) { + DEFINE_STATIC_LOCAL(RefPtr<CSSPrimitiveValue>, underline, (CSSPrimitiveValue::createIdentifier(CSSValueUnderline))); + DEFINE_STATIC_LOCAL(RefPtr<CSSPrimitiveValue>, lineThrough, (CSSPrimitiveValue::createIdentifier(CSSValueLineThrough))); + + RefPtr<CSSValueList> newTextDecoration = static_cast<CSSValueList*>(textDecoration.get())->copy(); + if (newTextDecoration->removeAll(underline.get())) + m_applyUnderline = true; + if (newTextDecoration->removeAll(lineThrough.get())) + m_applyLineThrough = true; + + // If trimTextDecorations, delete underline and line-through + setTextDecorationProperty(style, newTextDecoration.get(), CSSPropertyTextDecoration); + } + + int verticalAlign = getIdentifierValue(style, CSSPropertyVerticalAlign); + switch (verticalAlign) { + case CSSValueSub: + style->removeProperty(CSSPropertyVerticalAlign); + m_applySubscript = true; + break; + case CSSValueSuper: + style->removeProperty(CSSPropertyVerticalAlign); + m_applySuperscript = true; + break; + } + + if (style->getPropertyCSSValue(CSSPropertyColor)) { + m_applyFontColor = Color(getRGBAFontColor(style)).name(); + style->removeProperty(CSSPropertyColor); + } + + m_applyFontFace = style->getPropertyValue(CSSPropertyFontFamily); + style->removeProperty(CSSPropertyFontFamily); + + if (RefPtr<CSSValue> fontSize = style->getPropertyCSSValue(CSSPropertyFontSize)) { + if (!fontSize->isPrimitiveValue()) + style->removeProperty(CSSPropertyFontSize); // Can't make sense of the number. Put no font size. + else { + CSSPrimitiveValue* value = static_cast<CSSPrimitiveValue*>(fontSize.get()); + if (value->primitiveType() >= CSSPrimitiveValue::CSS_PX && value->primitiveType() <= CSSPrimitiveValue::CSS_PC) { + int pixelFontSize = value->getFloatValue(CSSPrimitiveValue::CSS_PX); + int legacyFontSize = CSSStyleSelector::legacyFontSize(document, pixelFontSize, shouldUseFixedFontDefautlSize); + // Use legacy font size only if pixel value matches exactly to that of legacy font size. + if (CSSStyleSelector::fontSizeForKeyword(document, legacyFontSize - 1 + CSSValueXSmall, shouldUseFixedFontDefautlSize) == pixelFontSize) { + m_applyFontSize = String::number(legacyFontSize); + style->removeProperty(CSSPropertyFontSize); + } + } else if (CSSValueXSmall <= value->getIdent() && value->getIdent() <= CSSValueWebkitXxxLarge) { + m_applyFontSize = String::number(value->getIdent() - CSSValueXSmall + 1); + style->removeProperty(CSSPropertyFontSize); + } + } + } +} + +static String& styleSpanClassString() +{ + DEFINE_STATIC_LOCAL(String, styleSpanClassString, ((AppleStyleSpanClass))); + return styleSpanClassString; +} + +bool isStyleSpan(const Node *node) +{ + if (!node || !node->isHTMLElement()) + return false; + + const HTMLElement* elem = static_cast<const HTMLElement*>(node); + return elem->hasLocalName(spanAttr) && elem->getAttribute(classAttr) == styleSpanClassString(); +} + +static bool isUnstyledStyleSpan(const Node* node) +{ + if (!node || !node->isHTMLElement() || !node->hasTagName(spanTag)) + return false; + + const HTMLElement* elem = static_cast<const HTMLElement*>(node); + CSSMutableStyleDeclaration* inlineStyleDecl = elem->inlineStyleDecl(); + return (!inlineStyleDecl || inlineStyleDecl->isEmpty()) && elem->getAttribute(classAttr) == styleSpanClassString(); +} + +static bool isSpanWithoutAttributesOrUnstyleStyleSpan(const Node* node) +{ + if (!node || !node->isHTMLElement() || !node->hasTagName(spanTag)) + return false; + + const HTMLElement* elem = static_cast<const HTMLElement*>(node); + NamedNodeMap* attributes = elem->attributes(true); // readonly + if (attributes->isEmpty()) + return true; + + return isUnstyledStyleSpan(node); +} + +static bool isEmptyFontTag(const Node *node) +{ + if (!node || !node->hasTagName(fontTag)) + return false; + + const Element *elem = static_cast<const Element *>(node); + NamedNodeMap *map = elem->attributes(true); // true for read-only + if (!map) + return true; + return map->isEmpty() || (map->length() == 1 && elem->getAttribute(classAttr) == styleSpanClassString()); +} + +static PassRefPtr<Element> createFontElement(Document* document) +{ + RefPtr<Element> fontNode = createHTMLElement(document, fontTag); + fontNode->setAttribute(classAttr, styleSpanClassString()); + return fontNode.release(); +} + +PassRefPtr<HTMLElement> createStyleSpanElement(Document* document) +{ + RefPtr<HTMLElement> styleElement = createHTMLElement(document, spanTag); + styleElement->setAttribute(classAttr, styleSpanClassString()); + return styleElement.release(); +} + +static void diffTextDecorations(CSSMutableStyleDeclaration* style, int propertID, CSSValue* refTextDecoration) +{ + RefPtr<CSSValue> textDecoration = style->getPropertyCSSValue(propertID); + if (!textDecoration || !textDecoration->isValueList() || !refTextDecoration || !refTextDecoration->isValueList()) + return; + + RefPtr<CSSValueList> newTextDecoration = static_cast<CSSValueList*>(textDecoration.get())->copy(); + CSSValueList* valuesInRefTextDecoration = static_cast<CSSValueList*>(refTextDecoration); + + for (size_t i = 0; i < valuesInRefTextDecoration->length(); i++) + newTextDecoration->removeAll(valuesInRefTextDecoration->item(i)); + + setTextDecorationProperty(style, newTextDecoration.get(), propertID); +} + +static bool fontWeightIsBold(CSSStyleDeclaration* style) +{ + ASSERT(style); + RefPtr<CSSValue> fontWeight = style->getPropertyCSSValue(CSSPropertyFontWeight); + + if (!fontWeight) + return false; + if (!fontWeight->isPrimitiveValue()) + return false; + + // Because b tag can only bold text, there are only two states in plain html: bold and not bold. + // Collapse all other values to either one of these two states for editing purposes. + switch (static_cast<CSSPrimitiveValue*>(fontWeight.get())->getIdent()) { + case CSSValue100: + case CSSValue200: + case CSSValue300: + case CSSValue400: + case CSSValue500: + case CSSValueNormal: + return false; + case CSSValueBold: + case CSSValue600: + case CSSValue700: + case CSSValue800: + case CSSValue900: + return true; + } + + ASSERT_NOT_REACHED(); // For CSSValueBolder and CSSValueLighter + return false; // Make compiler happy +} + +static int getTextAlignment(CSSStyleDeclaration* style) +{ + int textAlign = getIdentifierValue(style, CSSPropertyTextAlign); + switch (textAlign) { + case CSSValueCenter: + case CSSValueWebkitCenter: + return CSSValueCenter; + case CSSValueJustify: + return CSSValueJustify; + case CSSValueLeft: + case CSSValueWebkitLeft: + return CSSValueLeft; + case CSSValueRight: + case CSSValueWebkitRight: + return CSSValueRight; + } + return CSSValueInvalid; +} + +RefPtr<CSSMutableStyleDeclaration> getPropertiesNotIn(CSSStyleDeclaration* styleWithRedundantProperties, CSSStyleDeclaration* baseStyle) +{ + ASSERT(styleWithRedundantProperties); + ASSERT(baseStyle); + RefPtr<CSSMutableStyleDeclaration> result = styleWithRedundantProperties->copy(); + baseStyle->diff(result.get()); + + RefPtr<CSSValue> baseTextDecorationsInEffect = baseStyle->getPropertyCSSValue(CSSPropertyWebkitTextDecorationsInEffect); + diffTextDecorations(result.get(), CSSPropertyTextDecoration, baseTextDecorationsInEffect.get()); + diffTextDecorations(result.get(), CSSPropertyWebkitTextDecorationsInEffect, baseTextDecorationsInEffect.get()); + + if (fontWeightIsBold(result.get()) == fontWeightIsBold(baseStyle)) + result->removeProperty(CSSPropertyFontWeight); + + if (getRGBAFontColor(result.get()) == getRGBAFontColor(baseStyle)) + result->removeProperty(CSSPropertyColor); + + if (getTextAlignment(result.get()) == getTextAlignment(baseStyle)) + result->removeProperty(CSSPropertyTextAlign); + + return result; +} + +ApplyStyleCommand::ApplyStyleCommand(Document* document, const EditingStyle* style, EditAction editingAction, EPropertyLevel propertyLevel) + : CompositeEditCommand(document) + , m_style(style->copy()) + , m_editingAction(editingAction) + , m_propertyLevel(propertyLevel) + , m_start(endingSelection().start().downstream()) + , m_end(endingSelection().end().upstream()) + , m_useEndingSelection(true) + , m_styledInlineElement(0) + , m_removeOnly(false) + , m_isInlineElementToRemoveFunction(0) +{ +} + +ApplyStyleCommand::ApplyStyleCommand(Document* document, const EditingStyle* style, const Position& start, const Position& end, EditAction editingAction, EPropertyLevel propertyLevel) + : CompositeEditCommand(document) + , m_style(style->copy()) + , m_editingAction(editingAction) + , m_propertyLevel(propertyLevel) + , m_start(start) + , m_end(end) + , m_useEndingSelection(false) + , m_styledInlineElement(0) + , m_removeOnly(false) + , m_isInlineElementToRemoveFunction(0) +{ +} + +ApplyStyleCommand::ApplyStyleCommand(PassRefPtr<Element> element, bool removeOnly, EditAction editingAction) + : CompositeEditCommand(element->document()) + , m_style(EditingStyle::create()) + , m_editingAction(editingAction) + , m_propertyLevel(PropertyDefault) + , m_start(endingSelection().start().downstream()) + , m_end(endingSelection().end().upstream()) + , m_useEndingSelection(true) + , m_styledInlineElement(element) + , m_removeOnly(removeOnly) + , m_isInlineElementToRemoveFunction(0) +{ +} + +ApplyStyleCommand::ApplyStyleCommand(Document* document, const EditingStyle* style, IsInlineElementToRemoveFunction isInlineElementToRemoveFunction, EditAction editingAction) + : CompositeEditCommand(document) + , m_style(style->copy()) + , m_editingAction(editingAction) + , m_propertyLevel(PropertyDefault) + , m_start(endingSelection().start().downstream()) + , m_end(endingSelection().end().upstream()) + , m_useEndingSelection(true) + , m_styledInlineElement(0) + , m_removeOnly(true) + , m_isInlineElementToRemoveFunction(isInlineElementToRemoveFunction) +{ +} + +void ApplyStyleCommand::updateStartEnd(const Position& newStart, const Position& newEnd) +{ + ASSERT(comparePositions(newEnd, newStart) >= 0); + + if (!m_useEndingSelection && (newStart != m_start || newEnd != m_end)) + m_useEndingSelection = true; + + setEndingSelection(VisibleSelection(newStart, newEnd, VP_DEFAULT_AFFINITY)); + m_start = newStart; + m_end = newEnd; +} + +Position ApplyStyleCommand::startPosition() +{ + if (m_useEndingSelection) + return endingSelection().start(); + + return m_start; +} + +Position ApplyStyleCommand::endPosition() +{ + if (m_useEndingSelection) + return endingSelection().end(); + + return m_end; +} + +void ApplyStyleCommand::doApply() +{ + switch (m_propertyLevel) { + case PropertyDefault: { + // Apply the block-centric properties of the style. + RefPtr<EditingStyle> blockStyle = m_style->extractAndRemoveBlockProperties(); + if (!blockStyle->isEmpty()) + applyBlockStyle(blockStyle->style()); + // Apply any remaining styles to the inline elements. + if (!m_style->isEmpty() || m_styledInlineElement || m_isInlineElementToRemoveFunction) { + RefPtr<CSSMutableStyleDeclaration> style = m_style->style() ? m_style->style() : CSSMutableStyleDeclaration::create(); + applyRelativeFontStyleChange(m_style.get()); + applyInlineStyle(style.get()); + } + break; + } + case ForceBlockProperties: + // Force all properties to be applied as block styles. + applyBlockStyle(m_style->style()); + break; + } +} + +EditAction ApplyStyleCommand::editingAction() const +{ + return m_editingAction; +} + +void ApplyStyleCommand::applyBlockStyle(CSSMutableStyleDeclaration *style) +{ + // update document layout once before removing styles + // so that we avoid the expense of updating before each and every call + // to check a computed style + updateLayout(); + + // get positions we want to use for applying style + Position start = startPosition(); + Position end = endPosition(); + if (comparePositions(end, start) < 0) { + Position swap = start; + start = end; + end = swap; + } + + VisiblePosition visibleStart(start); + VisiblePosition visibleEnd(end); + + if (visibleStart.isNull() || visibleStart.isOrphan() || visibleEnd.isNull() || visibleEnd.isOrphan()) + return; + + // Save and restore the selection endpoints using their indices in the document, since + // addBlockStyleIfNeeded may moveParagraphs, which can remove these endpoints. + // Calculate start and end indices from the start of the tree that they're in. + Node* scope = highestAncestor(visibleStart.deepEquivalent().node()); + Position rangeStart(scope, 0); + RefPtr<Range> startRange = Range::create(document(), rangeStart, rangeCompliantEquivalent(visibleStart.deepEquivalent())); + RefPtr<Range> endRange = Range::create(document(), rangeStart, rangeCompliantEquivalent(visibleEnd.deepEquivalent())); + int startIndex = TextIterator::rangeLength(startRange.get(), true); + int endIndex = TextIterator::rangeLength(endRange.get(), true); + + VisiblePosition paragraphStart(startOfParagraph(visibleStart)); + VisiblePosition nextParagraphStart(endOfParagraph(paragraphStart).next()); + VisiblePosition beyondEnd(endOfParagraph(visibleEnd).next()); + while (paragraphStart.isNotNull() && paragraphStart != beyondEnd) { + StyleChange styleChange(style, paragraphStart.deepEquivalent()); + if (styleChange.cssStyle().length() || m_removeOnly) { + RefPtr<Node> block = enclosingBlock(paragraphStart.deepEquivalent().node()); + if (!m_removeOnly) { + RefPtr<Node> newBlock = moveParagraphContentsToNewBlockIfNecessary(paragraphStart.deepEquivalent()); + if (newBlock) + block = newBlock; + } + ASSERT(block->isHTMLElement()); + if (block->isHTMLElement()) { + removeCSSStyle(style, static_cast<HTMLElement*>(block.get())); + if (!m_removeOnly) + addBlockStyle(styleChange, static_cast<HTMLElement*>(block.get())); + } + + if (nextParagraphStart.isOrphan()) + nextParagraphStart = endOfParagraph(paragraphStart).next(); + } + + paragraphStart = nextParagraphStart; + nextParagraphStart = endOfParagraph(paragraphStart).next(); + } + + startRange = TextIterator::rangeFromLocationAndLength(static_cast<Element*>(scope), startIndex, 0, true); + endRange = TextIterator::rangeFromLocationAndLength(static_cast<Element*>(scope), endIndex, 0, true); + if (startRange && endRange) + updateStartEnd(startRange->startPosition(), endRange->startPosition()); +} + +void ApplyStyleCommand::applyRelativeFontStyleChange(EditingStyle* style) +{ + static const float MinimumFontSize = 0.1f; + + if (!style || !style->hasFontSizeDelta()) + return; + + Position start = startPosition(); + Position end = endPosition(); + if (comparePositions(end, start) < 0) { + Position swap = start; + start = end; + end = swap; + } + + // Join up any adjacent text nodes. + if (start.node()->isTextNode()) { + joinChildTextNodes(start.node()->parentNode(), start, end); + start = startPosition(); + end = endPosition(); + } + if (end.node()->isTextNode() && start.node()->parentNode() != end.node()->parentNode()) { + joinChildTextNodes(end.node()->parentNode(), start, end); + start = startPosition(); + end = endPosition(); + } + + // Split the start text nodes if needed to apply style. + if (isValidCaretPositionInTextNode(start)) { + splitTextAtStart(start, end); + start = startPosition(); + end = endPosition(); + } + + if (isValidCaretPositionInTextNode(end)) { + splitTextAtEnd(start, end); + start = startPosition(); + end = endPosition(); + } + + // Calculate loop end point. + // If the end node is before the start node (can only happen if the end node is + // an ancestor of the start node), we gather nodes up to the next sibling of the end node + Node *beyondEnd; + if (start.node()->isDescendantOf(end.node())) + beyondEnd = end.node()->traverseNextSibling(); + else + beyondEnd = end.node()->traverseNextNode(); + + start = start.upstream(); // Move upstream to ensure we do not add redundant spans. + Node *startNode = start.node(); + if (startNode->isTextNode() && start.deprecatedEditingOffset() >= caretMaxOffset(startNode)) // Move out of text node if range does not include its characters. + startNode = startNode->traverseNextNode(); + + // Store away font size before making any changes to the document. + // This ensures that changes to one node won't effect another. + HashMap<Node*, float> startingFontSizes; + for (Node *node = startNode; node != beyondEnd; node = node->traverseNextNode()) + startingFontSizes.set(node, computedFontSize(node)); + + // These spans were added by us. If empty after font size changes, they can be removed. + Vector<RefPtr<HTMLElement> > unstyledSpans; + + Node* lastStyledNode = 0; + for (Node* node = startNode; node != beyondEnd; node = node->traverseNextNode()) { + RefPtr<HTMLElement> element; + if (node->isHTMLElement()) { + // Only work on fully selected nodes. + if (!nodeFullySelected(node, start, end)) + continue; + element = static_cast<HTMLElement*>(node); + } else if (node->isTextNode() && node->renderer() && node->parentNode() != lastStyledNode) { + // Last styled node was not parent node of this text node, but we wish to style this + // text node. To make this possible, add a style span to surround this text node. + RefPtr<HTMLElement> span = createStyleSpanElement(document()); + surroundNodeRangeWithElement(node, node, span.get()); + element = span.release(); + } else { + // Only handle HTML elements and text nodes. + continue; + } + lastStyledNode = node; + + CSSMutableStyleDeclaration* inlineStyleDecl = element->getInlineStyleDecl(); + float currentFontSize = computedFontSize(node); + float desiredFontSize = max(MinimumFontSize, startingFontSizes.get(node) + style->fontSizeDelta()); + RefPtr<CSSValue> value = inlineStyleDecl->getPropertyCSSValue(CSSPropertyFontSize); + if (value) { + inlineStyleDecl->removeProperty(CSSPropertyFontSize, true); + currentFontSize = computedFontSize(node); + } + if (currentFontSize != desiredFontSize) { + inlineStyleDecl->setProperty(CSSPropertyFontSize, String::number(desiredFontSize) + "px", false, false); + setNodeAttribute(element.get(), styleAttr, inlineStyleDecl->cssText()); + } + if (inlineStyleDecl->isEmpty()) { + removeNodeAttribute(element.get(), styleAttr); + // FIXME: should this be isSpanWithoutAttributesOrUnstyleStyleSpan? Need a test. + if (isUnstyledStyleSpan(element.get())) + unstyledSpans.append(element.release()); + } + } + + size_t size = unstyledSpans.size(); + for (size_t i = 0; i < size; ++i) + removeNodePreservingChildren(unstyledSpans[i].get()); +} + +static Node* dummySpanAncestorForNode(const Node* node) +{ + while (node && !isStyleSpan(node)) + node = node->parentNode(); + + return node ? node->parentNode() : 0; +} + +void ApplyStyleCommand::cleanupUnstyledAppleStyleSpans(Node* dummySpanAncestor) +{ + if (!dummySpanAncestor) + return; + + // Dummy spans are created when text node is split, so that style information + // can be propagated, which can result in more splitting. If a dummy span gets + // cloned/split, the new node is always a sibling of it. Therefore, we scan + // all the children of the dummy's parent + Node* next; + for (Node* node = dummySpanAncestor->firstChild(); node; node = next) { + next = node->nextSibling(); + if (isUnstyledStyleSpan(node)) + removeNodePreservingChildren(node); + node = next; + } +} + +HTMLElement* ApplyStyleCommand::splitAncestorsWithUnicodeBidi(Node* node, bool before, int allowedDirection) +{ + // We are allowed to leave the highest ancestor with unicode-bidi unsplit if it is unicode-bidi: embed and direction: allowedDirection. + // In that case, we return the unsplit ancestor. Otherwise, we return 0. + Node* block = enclosingBlock(node); + if (!block) + return 0; + + Node* highestAncestorWithUnicodeBidi = 0; + Node* nextHighestAncestorWithUnicodeBidi = 0; + int highestAncestorUnicodeBidi = 0; + for (Node* n = node->parentNode(); n != block; n = n->parentNode()) { + int unicodeBidi = getIdentifierValue(computedStyle(n).get(), CSSPropertyUnicodeBidi); + if (unicodeBidi && unicodeBidi != CSSValueNormal) { + highestAncestorUnicodeBidi = unicodeBidi; + nextHighestAncestorWithUnicodeBidi = highestAncestorWithUnicodeBidi; + highestAncestorWithUnicodeBidi = n; + } + } + + if (!highestAncestorWithUnicodeBidi) + return 0; + + HTMLElement* unsplitAncestor = 0; + + if (allowedDirection && highestAncestorUnicodeBidi != CSSValueBidiOverride + && getIdentifierValue(computedStyle(highestAncestorWithUnicodeBidi).get(), CSSPropertyDirection) == allowedDirection + && highestAncestorWithUnicodeBidi->isHTMLElement()) { + if (!nextHighestAncestorWithUnicodeBidi) + return static_cast<HTMLElement*>(highestAncestorWithUnicodeBidi); + + unsplitAncestor = static_cast<HTMLElement*>(highestAncestorWithUnicodeBidi); + highestAncestorWithUnicodeBidi = nextHighestAncestorWithUnicodeBidi; + } + + // Split every ancestor through highest ancestor with embedding. + Node* n = node; + while (true) { + Element* parent = static_cast<Element*>(n->parentNode()); + if (before ? n->previousSibling() : n->nextSibling()) + splitElement(parent, before ? n : n->nextSibling()); + if (parent == highestAncestorWithUnicodeBidi) + break; + n = n->parentNode(); + } + return unsplitAncestor; +} + +void ApplyStyleCommand::removeEmbeddingUpToEnclosingBlock(Node* node, Node* unsplitAncestor) +{ + Node* block = enclosingBlock(node); + if (!block) + return; + + Node* parent = 0; + for (Node* n = node->parentNode(); n != block && n != unsplitAncestor; n = parent) { + parent = n->parentNode(); + if (!n->isStyledElement()) + continue; + + StyledElement* element = static_cast<StyledElement*>(n); + int unicodeBidi = getIdentifierValue(computedStyle(element).get(), CSSPropertyUnicodeBidi); + if (!unicodeBidi || unicodeBidi == CSSValueNormal) + continue; + + // FIXME: This code should really consider the mapped attribute 'dir', the inline style declaration, + // and all matching style rules in order to determine how to best set the unicode-bidi property to 'normal'. + // For now, it assumes that if the 'dir' attribute is present, then removing it will suffice, and + // otherwise it sets the property in the inline style declaration. + if (element->hasAttribute(dirAttr)) { + // FIXME: If this is a BDO element, we should probably just remove it if it has no + // other attributes, like we (should) do with B and I elements. + removeNodeAttribute(element, dirAttr); + } else { + RefPtr<CSSMutableStyleDeclaration> inlineStyle = element->getInlineStyleDecl()->copy(); + inlineStyle->setProperty(CSSPropertyUnicodeBidi, CSSValueNormal); + inlineStyle->removeProperty(CSSPropertyDirection); + setNodeAttribute(element, styleAttr, inlineStyle->cssText()); + // FIXME: should this be isSpanWithoutAttributesOrUnstyleStyleSpan? Need a test. + if (isUnstyledStyleSpan(element)) + removeNodePreservingChildren(element); + } + } +} + +static Node* highestEmbeddingAncestor(Node* startNode, Node* enclosingNode) +{ + for (Node* n = startNode; n && n != enclosingNode; n = n->parentNode()) { + if (n->isHTMLElement() && getIdentifierValue(computedStyle(n).get(), CSSPropertyUnicodeBidi) == CSSValueEmbed) + return n; + } + + return 0; +} + +void ApplyStyleCommand::applyInlineStyle(CSSMutableStyleDeclaration *style) +{ + Node* startDummySpanAncestor = 0; + Node* endDummySpanAncestor = 0; + + // update document layout once before removing styles + // so that we avoid the expense of updating before each and every call + // to check a computed style + updateLayout(); + + // adjust to the positions we want to use for applying style + Position start = startPosition(); + Position end = endPosition(); + if (comparePositions(end, start) < 0) { + Position swap = start; + start = end; + end = swap; + } + + // split the start node and containing element if the selection starts inside of it + bool splitStart = isValidCaretPositionInTextNode(start); + if (splitStart) { + if (shouldSplitTextElement(start.node()->parentElement(), style)) + splitTextElementAtStart(start, end); + else + splitTextAtStart(start, end); + start = startPosition(); + end = endPosition(); + startDummySpanAncestor = dummySpanAncestorForNode(start.node()); + } + + // split the end node and containing element if the selection ends inside of it + bool splitEnd = isValidCaretPositionInTextNode(end); + if (splitEnd) { + if (shouldSplitTextElement(end.node()->parentElement(), style)) + splitTextElementAtEnd(start, end); + else + splitTextAtEnd(start, end); + start = startPosition(); + end = endPosition(); + endDummySpanAncestor = dummySpanAncestorForNode(end.node()); + } + + // Remove style from the selection. + // Use the upstream position of the start for removing style. + // This will ensure we remove all traces of the relevant styles from the selection + // and prevent us from adding redundant ones, as described in: + // <rdar://problem/3724344> Bolding and unbolding creates extraneous tags + Position removeStart = start.upstream(); + int unicodeBidi = getIdentifierValue(style, CSSPropertyUnicodeBidi); + int direction = 0; + RefPtr<CSSMutableStyleDeclaration> styleWithoutEmbedding; + if (unicodeBidi) { + // Leave alone an ancestor that provides the desired single level embedding, if there is one. + if (unicodeBidi == CSSValueEmbed) + direction = getIdentifierValue(style, CSSPropertyDirection); + HTMLElement* startUnsplitAncestor = splitAncestorsWithUnicodeBidi(start.node(), true, direction); + HTMLElement* endUnsplitAncestor = splitAncestorsWithUnicodeBidi(end.node(), false, direction); + removeEmbeddingUpToEnclosingBlock(start.node(), startUnsplitAncestor); + removeEmbeddingUpToEnclosingBlock(end.node(), endUnsplitAncestor); + + // Avoid removing the dir attribute and the unicode-bidi and direction properties from the unsplit ancestors. + Position embeddingRemoveStart = removeStart; + if (startUnsplitAncestor && nodeFullySelected(startUnsplitAncestor, removeStart, end)) + embeddingRemoveStart = positionInParentAfterNode(startUnsplitAncestor); + + Position embeddingRemoveEnd = end; + if (endUnsplitAncestor && nodeFullySelected(endUnsplitAncestor, removeStart, end)) + embeddingRemoveEnd = positionInParentBeforeNode(endUnsplitAncestor).downstream(); + + if (embeddingRemoveEnd != removeStart || embeddingRemoveEnd != end) { + RefPtr<CSSMutableStyleDeclaration> embeddingStyle = CSSMutableStyleDeclaration::create(); + embeddingStyle->setProperty(CSSPropertyUnicodeBidi, CSSValueEmbed); + embeddingStyle->setProperty(CSSPropertyDirection, direction); + if (comparePositions(embeddingRemoveStart, embeddingRemoveEnd) <= 0) + removeInlineStyle(embeddingStyle, embeddingRemoveStart, embeddingRemoveEnd); + styleWithoutEmbedding = style->copy(); + styleWithoutEmbedding->removeProperty(CSSPropertyUnicodeBidi); + styleWithoutEmbedding->removeProperty(CSSPropertyDirection); + } + } + + removeInlineStyle(styleWithoutEmbedding ? styleWithoutEmbedding.get() : style, removeStart, end); + start = startPosition(); + end = endPosition(); + if (start.isNull() || start.isOrphan() || end.isNull() || end.isOrphan()) + return; + + if (splitStart) { + if (mergeStartWithPreviousIfIdentical(start, end)) { + start = startPosition(); + end = endPosition(); + } + } + + if (splitEnd) { + mergeEndWithNextIfIdentical(start, end); + start = startPosition(); + end = endPosition(); + } + + // update document layout once before running the rest of the function + // so that we avoid the expense of updating before each and every call + // to check a computed style + updateLayout(); + + RefPtr<CSSMutableStyleDeclaration> styleToApply = style; + if (unicodeBidi) { + // Avoid applying the unicode-bidi and direction properties beneath ancestors that already have them. + Node* embeddingStartNode = highestEmbeddingAncestor(start.node(), enclosingBlock(start.node())); + Node* embeddingEndNode = highestEmbeddingAncestor(end.node(), enclosingBlock(end.node())); + + if (embeddingStartNode || embeddingEndNode) { + Position embeddingApplyStart = embeddingStartNode ? positionInParentAfterNode(embeddingStartNode) : start; + Position embeddingApplyEnd = embeddingEndNode ? positionInParentBeforeNode(embeddingEndNode) : end; + ASSERT(embeddingApplyStart.isNotNull() && embeddingApplyEnd.isNotNull()); + + RefPtr<CSSMutableStyleDeclaration> embeddingStyle = CSSMutableStyleDeclaration::create(); + embeddingStyle->setProperty(CSSPropertyUnicodeBidi, CSSValueEmbed); + embeddingStyle->setProperty(CSSPropertyDirection, direction); + fixRangeAndApplyInlineStyle(embeddingStyle.get(), embeddingApplyStart, embeddingApplyEnd); + + if (styleWithoutEmbedding) + styleToApply = styleWithoutEmbedding; + else { + styleToApply = style->copy(); + styleToApply->removeProperty(CSSPropertyUnicodeBidi); + styleToApply->removeProperty(CSSPropertyDirection); + } + } + } + + fixRangeAndApplyInlineStyle(styleToApply.get(), start, end); + + // Remove dummy style spans created by splitting text elements. + cleanupUnstyledAppleStyleSpans(startDummySpanAncestor); + if (endDummySpanAncestor != startDummySpanAncestor) + cleanupUnstyledAppleStyleSpans(endDummySpanAncestor); +} + +void ApplyStyleCommand::fixRangeAndApplyInlineStyle(CSSMutableStyleDeclaration* style, const Position& start, const Position& end) +{ + Node* startNode = start.node(); + + if (start.deprecatedEditingOffset() >= caretMaxOffset(start.node())) { + startNode = startNode->traverseNextNode(); + if (!startNode || comparePositions(end, Position(startNode, 0)) < 0) + return; + } + + Node* pastEndNode = end.node(); + if (end.deprecatedEditingOffset() >= caretMaxOffset(end.node())) + pastEndNode = end.node()->traverseNextSibling(); + + // FIXME: Callers should perform this operation on a Range that includes the br + // if they want style applied to the empty line. + if (start == end && start.node()->hasTagName(brTag)) + pastEndNode = start.node()->traverseNextNode(); + + // Start from the highest fully selected ancestor so that we can modify the fully selected node. + // e.g. When applying font-size: large on <font color="blue">hello</font>, we need to include the font element in our run + // to generate <font color="blue" size="4">hello</font> instead of <font color="blue"><font size="4">hello</font></font> + RefPtr<Range> range = Range::create(startNode->document(), start, end); + Element* editableRoot = startNode->rootEditableElement(); + if (startNode != editableRoot) { + while (editableRoot && startNode->parentNode() != editableRoot && isNodeVisiblyContainedWithin(startNode->parentNode(), range.get())) + startNode = startNode->parentNode(); + } + + applyInlineStyleToNodeRange(style, startNode, pastEndNode); +} + +static bool containsNonEditableRegion(Node* node) +{ + if (!node->isContentEditable()) + return true; + + Node* sibling = node->traverseNextSibling(); + for (Node* descendent = node->firstChild(); descendent && descendent != sibling; descendent = descendent->traverseNextNode()) { + if (!descendent->isContentEditable()) + return true; + } + + return false; +} + +void ApplyStyleCommand::applyInlineStyleToNodeRange(CSSMutableStyleDeclaration* style, Node* node, Node* pastEndNode) +{ + if (m_removeOnly) + return; + + for (RefPtr<Node> next; node && node != pastEndNode; node = next.get()) { + next = node->traverseNextNode(); + + if (!node->renderer() || !node->isContentEditable()) + continue; + + if (!node->isContentRichlyEditable() && node->isHTMLElement()) { + // This is a plaintext-only region. Only proceed if it's fully selected. + // pastEndNode is the node after the last fully selected node, so if it's inside node then + // node isn't fully selected. + if (pastEndNode && pastEndNode->isDescendantOf(node)) + break; + // Add to this element's inline style and skip over its contents. + HTMLElement* element = static_cast<HTMLElement*>(node); + RefPtr<CSSMutableStyleDeclaration> inlineStyle = element->getInlineStyleDecl()->copy(); + inlineStyle->merge(style); + setNodeAttribute(element, styleAttr, inlineStyle->cssText()); + next = node->traverseNextSibling(); + continue; + } + + if (isBlock(node)) + continue; + + if (node->childNodeCount()) { + if (node->contains(pastEndNode) || containsNonEditableRegion(node) || !node->parentNode()->isContentEditable()) + continue; + if (editingIgnoresContent(node)) { + next = node->traverseNextSibling(); + continue; + } + } + + RefPtr<Node> runStart = node; + RefPtr<Node> runEnd = node; + Node* sibling = node->nextSibling(); + while (sibling && sibling != pastEndNode && !sibling->contains(pastEndNode) + && (!isBlock(sibling) || sibling->hasTagName(brTag)) + && !containsNonEditableRegion(sibling)) { + runEnd = sibling; + sibling = runEnd->nextSibling(); + } + next = runEnd->traverseNextSibling(); + + if (!removeStyleFromRunBeforeApplyingStyle(style, runStart, runEnd)) + continue; + addInlineStyleIfNeeded(style, runStart.get(), runEnd.get(), AddStyledElement); + } +} + +bool ApplyStyleCommand::isStyledInlineElementToRemove(Element* element) const +{ + return (m_styledInlineElement && element->hasTagName(m_styledInlineElement->tagQName())) + || (m_isInlineElementToRemoveFunction && m_isInlineElementToRemoveFunction(element)); +} + +bool ApplyStyleCommand::removeStyleFromRunBeforeApplyingStyle(CSSMutableStyleDeclaration* style, RefPtr<Node>& runStart, RefPtr<Node>& runEnd) +{ + ASSERT(runStart && runEnd && runStart->parentNode() == runEnd->parentNode()); + RefPtr<Node> pastEndNode = runEnd->traverseNextSibling(); + bool needToApplyStyle = false; + for (Node* node = runStart.get(); node && node != pastEndNode.get(); node = node->traverseNextNode()) { + if (node->childNodeCount()) + continue; + // We don't consider m_isInlineElementToRemoveFunction here because we never apply style when m_isInlineElementToRemoveFunction is specified + if (getPropertiesNotIn(style, computedStyle(node).get())->length() + || (m_styledInlineElement && !enclosingNodeWithTag(positionBeforeNode(node), m_styledInlineElement->tagQName()))) { + needToApplyStyle = true; + break; + } + } + if (!needToApplyStyle) + return false; + + RefPtr<Node> next = runStart; + for (RefPtr<Node> node = next; node && node->inDocument() && node != pastEndNode; node = next) { + next = node->traverseNextNode(); + if (!node->isHTMLElement()) + continue; + + RefPtr<Node> previousSibling = node->previousSibling(); + RefPtr<Node> nextSibling = node->nextSibling(); + RefPtr<ContainerNode> parent = node->parentNode(); + removeInlineStyleFromElement(style, static_cast<HTMLElement*>(node.get()), RemoveAlways); + if (!node->inDocument()) { + // FIXME: We might need to update the start and the end of current selection here but need a test. + if (runStart == node) + runStart = previousSibling ? previousSibling->nextSibling() : parent->firstChild(); + if (runEnd == node) + runEnd = nextSibling ? nextSibling->previousSibling() : parent->lastChild(); + } + } + + return true; +} + +bool ApplyStyleCommand::removeInlineStyleFromElement(CSSMutableStyleDeclaration* style, PassRefPtr<HTMLElement> element, InlineStyleRemovalMode mode, CSSMutableStyleDeclaration* extractedStyle) +{ + ASSERT(style); + ASSERT(element); + + if (!element->parentNode() || !element->parentNode()->isContentEditable()) + return false; + + if (isStyledInlineElementToRemove(element.get())) { + if (mode == RemoveNone) + return true; + ASSERT(extractedStyle); + if (element->inlineStyleDecl()) + extractedStyle->merge(element->inlineStyleDecl()); + removeNodePreservingChildren(element); + return true; + } + + bool removed = false; + if (removeImplicitlyStyledElement(style, element.get(), mode, extractedStyle)) + removed = true; + + if (!element->inDocument()) + return removed; + + // If the node was converted to a span, the span may still contain relevant + // styles which must be removed (e.g. <b style='font-weight: bold'>) + if (removeCSSStyle(style, element.get(), mode, extractedStyle)) + removed = true; + + return removed; +} + +enum EPushDownType { ShouldBePushedDown, ShouldNotBePushedDown }; +struct HTMLEquivalent { + int propertyID; + bool isValueList; + int primitiveId; + const QualifiedName* element; + const QualifiedName* attribute; + PassRefPtr<CSSValue> (*attributeToCSSValue)(int propertyID, const String&); + EPushDownType pushDownType; +}; + +static PassRefPtr<CSSValue> stringToCSSValue(int propertyID, const String& value) +{ + RefPtr<CSSMutableStyleDeclaration> dummyStyle; + dummyStyle = CSSMutableStyleDeclaration::create(); + dummyStyle->setProperty(propertyID, value); + return dummyStyle->getPropertyCSSValue(propertyID); +} + +static PassRefPtr<CSSValue> fontSizeToCSSValue(int propertyID, const String& value) +{ + UNUSED_PARAM(propertyID); + ASSERT(propertyID == CSSPropertyFontSize); + int size; + if (!HTMLFontElement::cssValueFromFontSizeNumber(value, size)) + return 0; + return CSSPrimitiveValue::createIdentifier(size); +} + +static const HTMLEquivalent HTMLEquivalents[] = { + { CSSPropertyFontWeight, false, CSSValueBold, &bTag, 0, 0, ShouldBePushedDown }, + { CSSPropertyFontWeight, false, CSSValueBold, &strongTag, 0, 0, ShouldBePushedDown }, + { CSSPropertyVerticalAlign, false, CSSValueSub, &subTag, 0, 0, ShouldBePushedDown }, + { CSSPropertyVerticalAlign, false, CSSValueSuper, &supTag, 0, 0, ShouldBePushedDown }, + { CSSPropertyFontStyle, false, CSSValueItalic, &iTag, 0, 0, ShouldBePushedDown }, + { CSSPropertyFontStyle, false, CSSValueItalic, &emTag, 0, 0, ShouldBePushedDown }, + + // text-decorations should be CSSValueList + { CSSPropertyTextDecoration, true, CSSValueUnderline, &uTag, 0, 0, ShouldBePushedDown }, + { CSSPropertyTextDecoration, true, CSSValueLineThrough, &sTag, 0, 0, ShouldBePushedDown }, + { CSSPropertyTextDecoration, true, CSSValueLineThrough, &strikeTag, 0, 0, ShouldBePushedDown }, + { CSSPropertyWebkitTextDecorationsInEffect, true, CSSValueUnderline, &uTag, 0, 0, ShouldBePushedDown }, + { CSSPropertyWebkitTextDecorationsInEffect, true, CSSValueLineThrough, &sTag, 0, 0, ShouldBePushedDown }, + { CSSPropertyWebkitTextDecorationsInEffect, true, CSSValueLineThrough, &strikeTag, 0, 0, ShouldBePushedDown }, + + // FIXME: font attributes should only be removed if values were different + { CSSPropertyColor, false, CSSValueInvalid, &fontTag, &colorAttr, stringToCSSValue, ShouldBePushedDown }, + { CSSPropertyFontFamily, false, CSSValueInvalid, &fontTag, &faceAttr, stringToCSSValue, ShouldBePushedDown }, + { CSSPropertyFontSize, false, CSSValueInvalid, &fontTag, &sizeAttr, fontSizeToCSSValue, ShouldBePushedDown }, + + // unicode-bidi and direction are pushed down separately so don't push down with other styles. + { CSSPropertyDirection, false, CSSValueInvalid, 0, &dirAttr, stringToCSSValue, ShouldNotBePushedDown }, + { CSSPropertyUnicodeBidi, false, CSSValueInvalid, 0, &dirAttr, stringToCSSValue, ShouldNotBePushedDown }, +}; + +bool ApplyStyleCommand::removeImplicitlyStyledElement(CSSMutableStyleDeclaration* style, HTMLElement* element, InlineStyleRemovalMode mode, CSSMutableStyleDeclaration* extractedStyle) +{ + // Current implementation does not support stylePushedDown when mode == RemoveNone because of early exit. + ASSERT(!extractedStyle || mode != RemoveNone); + bool removed = false; + for (size_t i = 0; i < WTF_ARRAY_LENGTH(HTMLEquivalents); ++i) { + const HTMLEquivalent& equivalent = HTMLEquivalents[i]; + ASSERT(equivalent.element || equivalent.attribute); + if ((extractedStyle && equivalent.pushDownType == ShouldNotBePushedDown) + || (equivalent.element && !element->hasTagName(*equivalent.element)) + || (equivalent.attribute && !element->hasAttribute(*equivalent.attribute))) + continue; + + RefPtr<CSSValue> styleValue = style->getPropertyCSSValue(equivalent.propertyID); + if (!styleValue) + continue; + RefPtr<CSSValue> mapValue; + if (equivalent.attribute) + mapValue = equivalent.attributeToCSSValue(equivalent.propertyID, element->getAttribute(*equivalent.attribute)); + else + mapValue = CSSPrimitiveValue::createIdentifier(equivalent.primitiveId).get(); + + if (mode != RemoveAlways) { + if (equivalent.isValueList && styleValue->isValueList() && static_cast<CSSValueList*>(styleValue.get())->hasValue(mapValue.get())) + continue; // If CSS value assumes CSSValueList, then only skip if the value was present in style to apply. + else if (mapValue && styleValue->cssText() == mapValue->cssText()) + continue; // If CSS value is primitive, then skip if they are equal. + } + + if (extractedStyle && mapValue) + extractedStyle->setProperty(equivalent.propertyID, mapValue->cssText()); + + if (mode == RemoveNone) + return true; + + removed = true; + if (!equivalent.attribute) { + replaceWithSpanOrRemoveIfWithoutAttributes(element); + break; + } + removeNodeAttribute(element, *equivalent.attribute); + if (isEmptyFontTag(element) || isSpanWithoutAttributesOrUnstyleStyleSpan(element)) + removeNodePreservingChildren(element); + } + return removed; +} + +void ApplyStyleCommand::replaceWithSpanOrRemoveIfWithoutAttributes(HTMLElement*& elem) +{ + bool removeNode = false; + + // Similar to isSpanWithoutAttributesOrUnstyleStyleSpan, but does not look for Apple-style-span. + NamedNodeMap* attributes = elem->attributes(true); // readonly + if (!attributes || attributes->isEmpty()) + removeNode = true; + else if (attributes->length() == 1 && elem->hasAttribute(styleAttr)) { + // Remove the element even if it has just style='' (this might be redundantly checked later too) + CSSMutableStyleDeclaration* inlineStyleDecl = elem->inlineStyleDecl(); + if (!inlineStyleDecl || inlineStyleDecl->isEmpty()) + removeNode = true; + } + + if (removeNode) + removeNodePreservingChildren(elem); + else { + HTMLElement* newSpanElement = replaceElementWithSpanPreservingChildrenAndAttributes(elem); + ASSERT(newSpanElement && newSpanElement->inDocument()); + elem = newSpanElement; + } +} + +bool ApplyStyleCommand::removeCSSStyle(CSSMutableStyleDeclaration* style, HTMLElement* element, InlineStyleRemovalMode mode, CSSMutableStyleDeclaration* extractedStyle) +{ + ASSERT(style); + ASSERT(element); + + CSSMutableStyleDeclaration* decl = element->inlineStyleDecl(); + if (!decl) + return false; + + bool removed = false; + CSSMutableStyleDeclaration::const_iterator end = style->end(); + for (CSSMutableStyleDeclaration::const_iterator it = style->begin(); it != end; ++it) { + CSSPropertyID propertyID = static_cast<CSSPropertyID>(it->id()); + RefPtr<CSSValue> value = decl->getPropertyCSSValue(propertyID); + if (value && (propertyID != CSSPropertyWhiteSpace || !isTabSpanNode(element))) { + removed = true; + if (mode == RemoveNone) + return true; + + ExceptionCode ec = 0; + if (extractedStyle) + extractedStyle->setProperty(propertyID, value->cssText(), decl->getPropertyPriority(propertyID), ec); + removeCSSProperty(element, propertyID); + + if (propertyID == CSSPropertyUnicodeBidi && !decl->getPropertyValue(CSSPropertyDirection).isEmpty()) { + if (extractedStyle) + extractedStyle->setProperty(CSSPropertyDirection, decl->getPropertyValue(CSSPropertyDirection), decl->getPropertyPriority(CSSPropertyDirection), ec); + removeCSSProperty(element, CSSPropertyDirection); + } + } + } + + if (mode == RemoveNone) + return removed; + + // No need to serialize <foo style=""> if we just removed the last css property + if (decl->isEmpty()) + removeNodeAttribute(element, styleAttr); + + if (isSpanWithoutAttributesOrUnstyleStyleSpan(element)) + removeNodePreservingChildren(element); + + return removed; +} + +HTMLElement* ApplyStyleCommand::highestAncestorWithConflictingInlineStyle(CSSMutableStyleDeclaration* style, Node* node) +{ + if (!node) + return 0; + + HTMLElement* result = 0; + Node* unsplittableElement = unsplittableElementForPosition(Position(node, 0)); + + for (Node *n = node; n; n = n->parentNode()) { + if (n->isHTMLElement() && shouldRemoveInlineStyleFromElement(style, static_cast<HTMLElement*>(n))) + result = static_cast<HTMLElement*>(n); + // Should stop at the editable root (cannot cross editing boundary) and + // also stop at the unsplittable element to be consistent with other UAs + if (n == unsplittableElement) + break; + } + + return result; +} + +void ApplyStyleCommand::applyInlineStyleToPushDown(Node* node, CSSMutableStyleDeclaration* style) +{ + ASSERT(node); + + if (!style || !style->length() || !node->renderer()) + return; + + RefPtr<CSSMutableStyleDeclaration> newInlineStyle = style; + if (node->isHTMLElement()) { + HTMLElement* element = static_cast<HTMLElement*>(node); + CSSMutableStyleDeclaration* existingInlineStyle = element->inlineStyleDecl(); + + // Avoid overriding existing styles of node + if (existingInlineStyle) { + newInlineStyle = existingInlineStyle->copy(); + CSSMutableStyleDeclaration::const_iterator end = style->end(); + for (CSSMutableStyleDeclaration::const_iterator it = style->begin(); it != end; ++it) { + ExceptionCode ec; + if (!existingInlineStyle->getPropertyCSSValue(it->id())) + newInlineStyle->setProperty(it->id(), it->value()->cssText(), it->isImportant(), ec); + + // text-decorations adds up + if (it->id() == CSSPropertyTextDecoration && it->value()->isValueList()) { + RefPtr<CSSValue> textDecoration = newInlineStyle->getPropertyCSSValue(CSSPropertyTextDecoration); + if (textDecoration && textDecoration->isValueList()) { + CSSValueList* textDecorationOfInlineStyle = static_cast<CSSValueList*>(textDecoration.get()); + CSSValueList* textDecorationOfStyleApplied = static_cast<CSSValueList*>(it->value()); + + DEFINE_STATIC_LOCAL(RefPtr<CSSPrimitiveValue>, underline, (CSSPrimitiveValue::createIdentifier(CSSValueUnderline))); + DEFINE_STATIC_LOCAL(RefPtr<CSSPrimitiveValue>, lineThrough, (CSSPrimitiveValue::createIdentifier(CSSValueLineThrough))); + + if (textDecorationOfStyleApplied->hasValue(underline.get()) && !textDecorationOfInlineStyle->hasValue(underline.get())) + textDecorationOfInlineStyle->append(underline.get()); + + if (textDecorationOfStyleApplied->hasValue(lineThrough.get()) && !textDecorationOfInlineStyle->hasValue(lineThrough.get())) + textDecorationOfInlineStyle->append(lineThrough.get()); + } + } + } + } + } + + // Since addInlineStyleIfNeeded can't add styles to block-flow render objects, add style attribute instead. + // FIXME: applyInlineStyleToRange should be used here instead. + if ((node->renderer()->isBlockFlow() || node->childNodeCount()) && node->isHTMLElement()) { + setNodeAttribute(static_cast<HTMLElement*>(node), styleAttr, newInlineStyle->cssText()); + return; + } + + if (node->renderer()->isText() && static_cast<RenderText*>(node->renderer())->isAllCollapsibleWhitespace()) + return; + + // We can't wrap node with the styled element here because new styled element will never be removed if we did. + // If we modified the child pointer in pushDownInlineStyleAroundNode to point to new style element + // then we fall into an infinite loop where we keep removing and adding styled element wrapping node. + addInlineStyleIfNeeded(newInlineStyle.get(), node, node, DoNotAddStyledElement); +} + +void ApplyStyleCommand::pushDownInlineStyleAroundNode(CSSMutableStyleDeclaration* style, Node* targetNode) +{ + HTMLElement* highestAncestor = highestAncestorWithConflictingInlineStyle(style, targetNode); + if (!highestAncestor) + return; + + // The outer loop is traversing the tree vertically from highestAncestor to targetNode + Node* current = highestAncestor; + // Along the way, styled elements that contain targetNode are removed and accumulated into elementsToPushDown. + // Each child of the removed element, exclusing ancestors of targetNode, is then wrapped by clones of elements in elementsToPushDown. + Vector<RefPtr<Element> > elementsToPushDown; + while (current != targetNode) { + ASSERT(current); + ASSERT(current->isHTMLElement()); + ASSERT(current->contains(targetNode)); + Node* child = current->firstChild(); + Node* lastChild = current->lastChild(); + RefPtr<StyledElement> styledElement; + if (current->isStyledElement() && isStyledInlineElementToRemove(static_cast<Element*>(current))) { + styledElement = static_cast<StyledElement*>(current); + elementsToPushDown.append(styledElement); + } + RefPtr<CSSMutableStyleDeclaration> styleToPushDown = CSSMutableStyleDeclaration::create(); + removeInlineStyleFromElement(style, static_cast<HTMLElement*>(current), RemoveIfNeeded, styleToPushDown.get()); + + // The inner loop will go through children on each level + // FIXME: we should aggregate inline child elements together so that we don't wrap each child separately. + while (child) { + Node* nextChild = child->nextSibling(); + + if (!child->contains(targetNode) && elementsToPushDown.size()) { + for (size_t i = 0; i < elementsToPushDown.size(); i++) { + RefPtr<Element> wrapper = elementsToPushDown[i]->cloneElementWithoutChildren(); + ExceptionCode ec = 0; + wrapper->removeAttribute(styleAttr, ec); + ASSERT(!ec); + surroundNodeRangeWithElement(child, child, wrapper); + } + } + + // Apply text decoration to all nodes containing targetNode and their siblings but NOT to targetNode + // But if we've removed styledElement then go ahead and always apply the style. + if (child != targetNode || styledElement) + applyInlineStyleToPushDown(child, styleToPushDown.get()); + + // We found the next node for the outer loop (contains targetNode) + // When reached targetNode, stop the outer loop upon the completion of the current inner loop + if (child == targetNode || child->contains(targetNode)) + current = child; + + if (child == lastChild || child->contains(lastChild)) + break; + child = nextChild; + } + } +} + +void ApplyStyleCommand::removeInlineStyle(PassRefPtr<CSSMutableStyleDeclaration> style, const Position &start, const Position &end) +{ + ASSERT(start.isNotNull()); + ASSERT(end.isNotNull()); + ASSERT(start.node()->inDocument()); + ASSERT(end.node()->inDocument()); + ASSERT(comparePositions(start, end) <= 0); + + RefPtr<CSSValue> textDecorationSpecialProperty = style->getPropertyCSSValue(CSSPropertyWebkitTextDecorationsInEffect); + if (textDecorationSpecialProperty) { + style = style->copy(); + style->setProperty(CSSPropertyTextDecoration, textDecorationSpecialProperty->cssText(), style->getPropertyPriority(CSSPropertyWebkitTextDecorationsInEffect)); + } + + Position pushDownStart = start.downstream(); + // If the pushDownStart is at the end of a text node, then this node is not fully selected. + // Move it to the next deep quivalent position to avoid removing the style from this node. + // e.g. if pushDownStart was at Position("hello", 5) in <b>hello<div>world</div></b>, we want Position("world", 0) instead. + Node* pushDownStartContainer = pushDownStart.containerNode(); + if (pushDownStartContainer && pushDownStartContainer->isTextNode() + && pushDownStart.computeOffsetInContainerNode() == pushDownStartContainer->maxCharacterOffset()) + pushDownStart = nextVisuallyDistinctCandidate(pushDownStart); + Position pushDownEnd = end.upstream(); + pushDownInlineStyleAroundNode(style.get(), pushDownStart.node()); + pushDownInlineStyleAroundNode(style.get(), pushDownEnd.node()); + + // The s and e variables store the positions used to set the ending selection after style removal + // takes place. This will help callers to recognize when either the start node or the end node + // are removed from the document during the work of this function. + // If pushDownInlineStyleAroundNode has pruned start.node() or end.node(), + // use pushDownStart or pushDownEnd instead, which pushDownInlineStyleAroundNode won't prune. + Position s = start.isNull() || start.isOrphan() ? pushDownStart : start; + Position e = end.isNull() || end.isOrphan() ? pushDownEnd : end; + + Node* node = start.node(); + while (node) { + RefPtr<Node> next = node->traverseNextNode(); + if (node->isHTMLElement() && nodeFullySelected(node, start, end)) { + RefPtr<HTMLElement> elem = static_cast<HTMLElement*>(node); + RefPtr<Node> prev = elem->traversePreviousNodePostOrder(); + RefPtr<Node> next = elem->traverseNextNode(); + RefPtr<CSSMutableStyleDeclaration> styleToPushDown; + PassRefPtr<Node> childNode = 0; + if (isStyledInlineElementToRemove(elem.get())) { + styleToPushDown = CSSMutableStyleDeclaration::create(); + childNode = elem->firstChild(); + } + + removeInlineStyleFromElement(style.get(), elem.get(), RemoveIfNeeded, styleToPushDown.get()); + if (!elem->inDocument()) { + if (s.node() == elem) { + // Since elem must have been fully selected, and it is at the start + // of the selection, it is clear we can set the new s offset to 0. + ASSERT(s.deprecatedEditingOffset() <= caretMinOffset(s.node())); + s = Position(next, 0); + } + if (e.node() == elem) { + // Since elem must have been fully selected, and it is at the end + // of the selection, it is clear we can set the new e offset to + // the max range offset of prev. + ASSERT(e.deprecatedEditingOffset() >= lastOffsetForEditing(e.node())); + e = Position(prev, lastOffsetForEditing(prev.get())); + } + } + + if (styleToPushDown) { + for (; childNode; childNode = childNode->nextSibling()) + applyInlineStyleToPushDown(childNode.get(), styleToPushDown.get()); + } + } + if (node == end.node()) + break; + node = next.get(); + } + + updateStartEnd(s, e); +} + +bool ApplyStyleCommand::nodeFullySelected(Node *node, const Position &start, const Position &end) const +{ + ASSERT(node); + ASSERT(node->isElementNode()); + + Position pos = Position(node, node->childNodeCount()).upstream(); + return comparePositions(Position(node, 0), start) >= 0 && comparePositions(pos, end) <= 0; +} + +bool ApplyStyleCommand::nodeFullyUnselected(Node *node, const Position &start, const Position &end) const +{ + ASSERT(node); + ASSERT(node->isElementNode()); + + Position pos = Position(node, node->childNodeCount()).upstream(); + bool isFullyBeforeStart = comparePositions(pos, start) < 0; + bool isFullyAfterEnd = comparePositions(Position(node, 0), end) > 0; + + return isFullyBeforeStart || isFullyAfterEnd; +} + +void ApplyStyleCommand::splitTextAtStart(const Position& start, const Position& end) +{ + int endOffsetAdjustment = start.node() == end.node() ? start.deprecatedEditingOffset() : 0; + Text* text = static_cast<Text*>(start.node()); + splitTextNode(text, start.deprecatedEditingOffset()); + updateStartEnd(Position(start.node(), 0), Position(end.node(), end.deprecatedEditingOffset() - endOffsetAdjustment)); +} + +void ApplyStyleCommand::splitTextAtEnd(const Position& start, const Position& end) +{ + Text* text = static_cast<Text *>(end.node()); + splitTextNode(text, end.deprecatedEditingOffset()); + + Node* prevNode = text->previousSibling(); + ASSERT(prevNode); + Node* startNode = start.node() == end.node() ? prevNode : start.node(); + ASSERT(startNode); + updateStartEnd(Position(startNode, start.deprecatedEditingOffset()), Position(prevNode, caretMaxOffset(prevNode))); +} + +void ApplyStyleCommand::splitTextElementAtStart(const Position& start, const Position& end) +{ + int endOffsetAdjustment = start.node() == end.node() ? start.deprecatedEditingOffset() : 0; + Text* text = static_cast<Text*>(start.node()); + splitTextNodeContainingElement(text, start.deprecatedEditingOffset()); + updateStartEnd(Position(start.node()->parentNode(), start.node()->nodeIndex()), Position(end.node(), end.deprecatedEditingOffset() - endOffsetAdjustment)); +} + +void ApplyStyleCommand::splitTextElementAtEnd(const Position& start, const Position& end) +{ + Text* text = static_cast<Text*>(end.node()); + splitTextNodeContainingElement(text, end.deprecatedEditingOffset()); + + Node* prevNode = text->parentNode()->previousSibling()->lastChild(); + ASSERT(prevNode); + Node* startNode = start.node() == end.node() ? prevNode : start.node(); + ASSERT(startNode); + updateStartEnd(Position(startNode, start.deprecatedEditingOffset()), Position(prevNode->parentNode(), prevNode->nodeIndex() + 1)); +} + +bool ApplyStyleCommand::shouldSplitTextElement(Element* element, CSSMutableStyleDeclaration* style) +{ + if (!element || !element->isHTMLElement()) + return false; + + return shouldRemoveInlineStyleFromElement(style, static_cast<HTMLElement*>(element)); +} + +bool ApplyStyleCommand::isValidCaretPositionInTextNode(const Position& position) +{ + Node* node = position.node(); + if (!node->isTextNode()) + return false; + int offsetInText = position.deprecatedEditingOffset(); + return (offsetInText > caretMinOffset(node) && offsetInText < caretMaxOffset(node)); +} + +static bool areIdenticalElements(Node *first, Node *second) +{ + // check that tag name and all attribute names and values are identical + + if (!first->isElementNode()) + return false; + + if (!second->isElementNode()) + return false; + + Element *firstElement = static_cast<Element *>(first); + Element *secondElement = static_cast<Element *>(second); + + if (!firstElement->tagQName().matches(secondElement->tagQName())) + return false; + + NamedNodeMap *firstMap = firstElement->attributes(); + NamedNodeMap *secondMap = secondElement->attributes(); + + unsigned firstLength = firstMap->length(); + + if (firstLength != secondMap->length()) + return false; + + for (unsigned i = 0; i < firstLength; i++) { + Attribute *attribute = firstMap->attributeItem(i); + Attribute *secondAttribute = secondMap->getAttributeItem(attribute->name()); + + if (!secondAttribute || attribute->value() != secondAttribute->value()) + return false; + } + + return true; +} + +bool ApplyStyleCommand::mergeStartWithPreviousIfIdentical(const Position &start, const Position &end) +{ + Node *startNode = start.node(); + int startOffset = start.deprecatedEditingOffset(); + + if (isAtomicNode(start.node())) { + if (start.deprecatedEditingOffset() != 0) + return false; + + // note: prior siblings could be unrendered elements. it's silly to miss the + // merge opportunity just for that. + if (start.node()->previousSibling()) + return false; + + startNode = start.node()->parentNode(); + startOffset = 0; + } + + if (!startNode->isElementNode()) + return false; + + if (startOffset != 0) + return false; + + Node *previousSibling = startNode->previousSibling(); + + if (previousSibling && areIdenticalElements(startNode, previousSibling)) { + Element *previousElement = static_cast<Element *>(previousSibling); + Element *element = static_cast<Element *>(startNode); + Node *startChild = element->firstChild(); + ASSERT(startChild); + mergeIdenticalElements(previousElement, element); + + int startOffsetAdjustment = startChild->nodeIndex(); + int endOffsetAdjustment = startNode == end.node() ? startOffsetAdjustment : 0; + updateStartEnd(Position(startNode, startOffsetAdjustment), Position(end.node(), end.deprecatedEditingOffset() + endOffsetAdjustment)); + return true; + } + + return false; +} + +bool ApplyStyleCommand::mergeEndWithNextIfIdentical(const Position &start, const Position &end) +{ + Node *endNode = end.node(); + int endOffset = end.deprecatedEditingOffset(); + + if (isAtomicNode(endNode)) { + if (endOffset < caretMaxOffset(endNode)) + return false; + + unsigned parentLastOffset = end.node()->parentNode()->childNodes()->length() - 1; + if (end.node()->nextSibling()) + return false; + + endNode = end.node()->parentNode(); + endOffset = parentLastOffset; + } + + if (!endNode->isElementNode() || endNode->hasTagName(brTag)) + return false; + + Node *nextSibling = endNode->nextSibling(); + + if (nextSibling && areIdenticalElements(endNode, nextSibling)) { + Element *nextElement = static_cast<Element *>(nextSibling); + Element *element = static_cast<Element *>(endNode); + Node *nextChild = nextElement->firstChild(); + + mergeIdenticalElements(element, nextElement); + + Node *startNode = start.node() == endNode ? nextElement : start.node(); + ASSERT(startNode); + + int endOffset = nextChild ? nextChild->nodeIndex() : nextElement->childNodes()->length(); + updateStartEnd(Position(startNode, start.deprecatedEditingOffset()), Position(nextElement, endOffset)); + return true; + } + + return false; +} + +void ApplyStyleCommand::surroundNodeRangeWithElement(PassRefPtr<Node> passedStartNode, PassRefPtr<Node> endNode, PassRefPtr<Element> elementToInsert) +{ + ASSERT(passedStartNode); + ASSERT(endNode); + ASSERT(elementToInsert); + RefPtr<Node> startNode = passedStartNode; + RefPtr<Element> element = elementToInsert; + + insertNodeBefore(element, startNode); + + RefPtr<Node> node = startNode; + while (node) { + RefPtr<Node> next = node->nextSibling(); + removeNode(node); + appendNode(node, element); + if (node == endNode) + break; + node = next; + } + + RefPtr<Node> nextSibling = element->nextSibling(); + RefPtr<Node> previousSibling = element->previousSibling(); + if (nextSibling && nextSibling->isElementNode() && nextSibling->isContentEditable() + && areIdenticalElements(element.get(), static_cast<Element*>(nextSibling.get()))) + mergeIdenticalElements(element.get(), static_cast<Element*>(nextSibling.get())); + + if (previousSibling && previousSibling->isElementNode() && previousSibling->isContentEditable()) { + Node* mergedElement = previousSibling->nextSibling(); + if (mergedElement->isElementNode() && mergedElement->isContentEditable() + && areIdenticalElements(static_cast<Element*>(previousSibling.get()), static_cast<Element*>(mergedElement))) + mergeIdenticalElements(static_cast<Element*>(previousSibling.get()), static_cast<Element*>(mergedElement)); + } + + // FIXME: We should probably call updateStartEnd if the start or end was in the node + // range so that the endingSelection() is canonicalized. See the comments at the end of + // VisibleSelection::validate(). +} + +void ApplyStyleCommand::addBlockStyle(const StyleChange& styleChange, HTMLElement* block) +{ + // Do not check for legacy styles here. Those styles, like <B> and <I>, only apply for + // inline content. + if (!block) + return; + + String cssText = styleChange.cssStyle(); + CSSMutableStyleDeclaration* decl = block->inlineStyleDecl(); + if (decl) + cssText += decl->cssText(); + setNodeAttribute(block, styleAttr, cssText); +} + +void ApplyStyleCommand::addInlineStyleIfNeeded(CSSMutableStyleDeclaration *style, PassRefPtr<Node> passedStart, PassRefPtr<Node> passedEnd, EAddStyledElement addStyledElement) +{ + if (!passedStart || !passedEnd || !passedStart->inDocument() || !passedEnd->inDocument()) + return; + RefPtr<Node> startNode = passedStart; + RefPtr<Node> endNode = passedEnd; + + // It's okay to obtain the style at the startNode because we've removed all relevant styles from the current run. + RefPtr<HTMLElement> dummyElement; + Position positionForStyleComparison; + if (!startNode->isElementNode()) { + dummyElement = createStyleSpanElement(document()); + insertNodeAt(dummyElement, positionBeforeNode(startNode.get())); + positionForStyleComparison = positionBeforeNode(dummyElement.get()); + } else + positionForStyleComparison = firstPositionInNode(startNode.get()); + + StyleChange styleChange(style, positionForStyleComparison); + + if (dummyElement) + removeNode(dummyElement); + + // Find appropriate font and span elements top-down. + HTMLElement* fontContainer = 0; + HTMLElement* styleContainer = 0; + for (Node* container = startNode.get(); container && startNode == endNode; container = container->firstChild()) { + if (container->isHTMLElement() && container->hasTagName(fontTag)) + fontContainer = static_cast<HTMLElement*>(container); + bool styleContainerIsNotSpan = !styleContainer || !styleContainer->hasTagName(spanTag); + if (container->isHTMLElement() && (container->hasTagName(spanTag) || (styleContainerIsNotSpan && container->childNodeCount()))) + styleContainer = static_cast<HTMLElement*>(container); + if (!container->firstChild()) + break; + startNode = container->firstChild(); + endNode = container->lastChild(); + } + + // Font tags need to go outside of CSS so that CSS font sizes override leagcy font sizes. + if (styleChange.applyFontColor() || styleChange.applyFontFace() || styleChange.applyFontSize()) { + if (fontContainer) { + if (styleChange.applyFontColor()) + setNodeAttribute(fontContainer, colorAttr, styleChange.fontColor()); + if (styleChange.applyFontFace()) + setNodeAttribute(fontContainer, faceAttr, styleChange.fontFace()); + if (styleChange.applyFontSize()) + setNodeAttribute(fontContainer, sizeAttr, styleChange.fontSize()); + } else { + RefPtr<Element> fontElement = createFontElement(document()); + if (styleChange.applyFontColor()) + fontElement->setAttribute(colorAttr, styleChange.fontColor()); + if (styleChange.applyFontFace()) + fontElement->setAttribute(faceAttr, styleChange.fontFace()); + if (styleChange.applyFontSize()) + fontElement->setAttribute(sizeAttr, styleChange.fontSize()); + surroundNodeRangeWithElement(startNode, endNode, fontElement.get()); + } + } + + if (styleChange.cssStyle().length()) { + if (styleContainer) { + CSSMutableStyleDeclaration* existingStyle = static_cast<HTMLElement*>(styleContainer)->inlineStyleDecl(); + if (existingStyle) + setNodeAttribute(styleContainer, styleAttr, existingStyle->cssText() + styleChange.cssStyle()); + else + setNodeAttribute(styleContainer, styleAttr, styleChange.cssStyle()); + } else { + RefPtr<Element> styleElement = createStyleSpanElement(document()); + styleElement->setAttribute(styleAttr, styleChange.cssStyle()); + surroundNodeRangeWithElement(startNode, endNode, styleElement.release()); + } + } + + if (styleChange.applyBold()) + surroundNodeRangeWithElement(startNode, endNode, createHTMLElement(document(), bTag)); + + if (styleChange.applyItalic()) + surroundNodeRangeWithElement(startNode, endNode, createHTMLElement(document(), iTag)); + + if (styleChange.applyUnderline()) + surroundNodeRangeWithElement(startNode, endNode, createHTMLElement(document(), uTag)); + + if (styleChange.applyLineThrough()) + surroundNodeRangeWithElement(startNode, endNode, createHTMLElement(document(), sTag)); + + if (styleChange.applySubscript()) + surroundNodeRangeWithElement(startNode, endNode, createHTMLElement(document(), subTag)); + else if (styleChange.applySuperscript()) + surroundNodeRangeWithElement(startNode, endNode, createHTMLElement(document(), supTag)); + + if (m_styledInlineElement && addStyledElement == AddStyledElement) + surroundNodeRangeWithElement(startNode, endNode, m_styledInlineElement->cloneElementWithoutChildren()); +} + +float ApplyStyleCommand::computedFontSize(const Node *node) +{ + if (!node) + return 0; + + Position pos(const_cast<Node *>(node), 0); + RefPtr<CSSComputedStyleDeclaration> computedStyle = pos.computedStyle(); + if (!computedStyle) + return 0; + + RefPtr<CSSPrimitiveValue> value = static_pointer_cast<CSSPrimitiveValue>(computedStyle->getPropertyCSSValue(CSSPropertyFontSize)); + if (!value) + return 0; + + return value->getFloatValue(CSSPrimitiveValue::CSS_PX); +} + +void ApplyStyleCommand::joinChildTextNodes(Node *node, const Position &start, const Position &end) +{ + if (!node) + return; + + Position newStart = start; + Position newEnd = end; + + Node *child = node->firstChild(); + while (child) { + Node *next = child->nextSibling(); + if (child->isTextNode() && next && next->isTextNode()) { + Text *childText = static_cast<Text *>(child); + Text *nextText = static_cast<Text *>(next); + if (next == start.node()) + newStart = Position(childText, childText->length() + start.deprecatedEditingOffset()); + if (next == end.node()) + newEnd = Position(childText, childText->length() + end.deprecatedEditingOffset()); + String textToMove = nextText->data(); + insertTextIntoNode(childText, childText->length(), textToMove); + removeNode(next); + // don't move child node pointer. it may want to merge with more text nodes. + } + else { + child = child->nextSibling(); + } + } + + updateStartEnd(newStart, newEnd); +} + +} |